From 0dff999e8cf3fb69278544638d2b33e9d96f7fc8 Mon Sep 17 00:00:00 2001 From: Duc-Bao Trinh Date: Mon, 30 Sep 2024 16:19:22 +0200 Subject: [PATCH] Add confirmation when deleting or trashing many items [IDTEAM-2836] --- .../components/Item/Actions/ConfirmAliasAction.tsx | 81 ------- .../Item/Actions/ConfirmAliasActions.tsx | 184 +++++++++++++++ .../components/Item/Actions/ConfirmBulkActions.tsx | 147 ++++++++++++ .../Item/Actions/ConfirmDeleteAliases.tsx | 34 --- ...{ConfirmMoveItem.tsx => ConfirmItemActions.tsx} | 27 ++- .../Item/Actions/ConfirmMoveManyItems.tsx | 52 ----- packages/pass/components/Item/Alias/Alias.view.tsx | 249 +++++++-------------- .../pass/components/Item/ItemActionsProvider.tsx | 184 +++++++++++---- .../components/Vault/Actions/ConfirmTrashEmpty.tsx | 7 +- packages/pass/hooks/useConfirm.ts | 2 + packages/pass/lib/items/item.utils.ts | 4 + 11 files changed, 573 insertions(+), 398 deletions(-) delete mode 100644 packages/pass/components/Item/Actions/ConfirmAliasAction.tsx create mode 100644 packages/pass/components/Item/Actions/ConfirmAliasActions.tsx create mode 100644 packages/pass/components/Item/Actions/ConfirmBulkActions.tsx delete mode 100644 packages/pass/components/Item/Actions/ConfirmDeleteAliases.tsx rename packages/pass/components/Item/Actions/{ConfirmMoveItem.tsx => ConfirmItemActions.tsx} (60%) delete mode 100644 packages/pass/components/Item/Actions/ConfirmMoveManyItems.tsx rewrite packages/pass/components/Item/Alias/Alias.view.tsx (62%) diff --git a/packages/pass/components/Item/Actions/ConfirmAliasAction.tsx b/packages/pass/components/Item/Actions/ConfirmAliasAction.tsx deleted file mode 100644 index 3cecb144cc..0000000000 --- a/packages/pass/components/Item/Actions/ConfirmAliasAction.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import type { FC } from 'react'; -import { type ReactNode, useState } from 'react'; - -import { c } from 'ttag'; - -import { Button } from '@proton/atoms'; -import { Alert, Checkbox, ModalTwoContent, ModalTwoFooter, ModalTwoHeader } from '@proton/components'; -import { PassModal } from '@proton/pass/components/Layout/Modal/PassModal'; -import { pipe } from '@proton/pass/utils/fp/pipe'; - -type Props = { - actionText: string; - disableText?: string; - message: ReactNode; - remember: boolean; - title: string; - warning?: ReactNode; - onAction: (noRemind: boolean) => void; - onClose: () => void; - onDisable?: (noRemind: boolean) => void; -}; - -export const ConfirmAliasAction: FC = ({ - actionText, - disableText, - message, - remember, - title, - warning, - onAction, - onClose, - onDisable, -}) => { - const [noRemind, setNoRemind] = useState(false); - - return ( - - - - {warning && ( - - {warning} - - )} - - {message &&
{message}
} -
- - {remember && ( - setNoRemind(target.checked)} - > - {c('Label').t`Don't remind me again`} - - )} - - - - {onDisable && ( - - )} - - - -
- ); -}; diff --git a/packages/pass/components/Item/Actions/ConfirmAliasActions.tsx b/packages/pass/components/Item/Actions/ConfirmAliasActions.tsx new file mode 100644 index 0000000000..5cf62f0c53 --- /dev/null +++ b/packages/pass/components/Item/Actions/ConfirmAliasActions.tsx @@ -0,0 +1,184 @@ +import type { FC } from 'react'; +import { type ReactNode, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { c } from 'ttag'; + +import { Button } from '@proton/atoms'; +import { Alert, Checkbox, ModalTwoContent, ModalTwoFooter, ModalTwoHeader } from '@proton/components'; +import { type ConfirmationPromptHandles } from '@proton/pass/components/Confirmation/ConfirmationPrompt'; +import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider'; +import { PassModal } from '@proton/pass/components/Layout/Modal/PassModal'; +import { isAliasDisabled, isAliasItem } from '@proton/pass/lib/items/item.predicates'; +import { aliasSyncStatusToggle } from '@proton/pass/store/actions'; +import { selectLoginItemByEmail } from '@proton/pass/store/selectors'; +import { type ItemRevision, OnboardingMessage } from '@proton/pass/types'; +import { pipe } from '@proton/pass/utils/fp/pipe'; + +type Props = { + actionText: string; + disableText?: string; + message: ReactNode; + remember: boolean; + title: string; + warning?: ReactNode; + onAction: () => void; + onClose: () => void; + onDisable?: () => void; +}; + +const ConfirmAliasAction: FC = ({ + actionText, + disableText, + message, + remember, + title, + warning, + onAction, + onClose, + onDisable, +}) => { + const { onboardingAcknowledge } = usePassCore(); + const [noRemind, setNoRemind] = useState(false); + + const withAcknowledge = + (fn?: () => void) => + (noRemind: boolean): void => { + fn?.(); + /** FIXME: ideally this should be moved away from spotlight + * message state and be stored in the settings */ + if (noRemind) void onboardingAcknowledge?.(OnboardingMessage.ALIAS_TRASH_CONFIRM); + }; + + const doAction = withAcknowledge(onAction); + const doDisable = withAcknowledge(onDisable); + + return ( + + + + {warning && ( + + {warning} + + )} + + {message &&
{message}
} +
+ + {remember && ( + setNoRemind(target.checked)} + > + {c('Label').t`Don't remind me again`} + + )} + + + + {onDisable && ( + + )} + + + +
+ ); +}; + +const useAliasActions = (item: ItemRevision) => { + const dispatch = useDispatch(); + + const { shareId, itemId } = item; + const aliasEnabled = !isAliasDisabled(item); + const aliasEmail = item.aliasEmail!; + const relatedLogin = useSelector(selectLoginItemByEmail(aliasEmail)); + + return useMemo( + () => ({ + aliasEmail, + aliasEnabled, + relatedLogin, + disableAlias: aliasEnabled + ? () => dispatch(aliasSyncStatusToggle.intent({ shareId, itemId, enabled: false })) + : undefined, + }), + [aliasEnabled, aliasEmail, relatedLogin] + ); +}; + +export const ConfirmTrashAlias: FC = ({ + item, + onCancel, + onConfirm, +}) => { + const { aliasEnabled, aliasEmail, relatedLogin, disableAlias } = useAliasActions(item); + const relatedLoginName = relatedLogin?.data.metadata.name ?? ''; + + return isAliasItem(item.data) ? ( + + ) : null; +}; + +export const ConfirmDeleteAlias: FC = ({ + item, + onCancel, + onConfirm, +}) => { + const { aliasEnabled, aliasEmail, relatedLogin, disableAlias } = useAliasActions(item); + const relatedLoginName = relatedLogin?.data.metadata.name ?? ''; + + return isAliasItem(item.data) ? ( + + ) : null; +}; diff --git a/packages/pass/components/Item/Actions/ConfirmBulkActions.tsx b/packages/pass/components/Item/Actions/ConfirmBulkActions.tsx new file mode 100644 index 0000000000..f62d19ef3f --- /dev/null +++ b/packages/pass/components/Item/Actions/ConfirmBulkActions.tsx @@ -0,0 +1,147 @@ +import { type FC } from 'react'; +import { useSelector } from 'react-redux'; + +import { c, msgid } from 'ttag'; + +import { Alert } from '@proton/components'; +import { useBulkSelect } from '@proton/pass/components/Bulk/BulkSelectProvider'; +import { + ConfirmationPrompt, + type ConfirmationPromptHandles, +} from '@proton/pass/components/Confirmation/ConfirmationPrompt'; +import { WithVault } from '@proton/pass/components/Vault/WithVault'; +import { getCountOfBulkSelectionDTO } from '@proton/pass/lib/items/item.utils'; +import { selectSecureLinksByItems } from '@proton/pass/store/selectors'; +import type { BulkSelectionDTO } from '@proton/pass/types'; + +export const ConfirmTrashManyItems: FC = ({ + selected, + onCancel, + onConfirm, +}) => { + const trashedItemsCount = getCountOfBulkSelectionDTO(selected); + const { aliasCount } = useBulkSelect(); + + return ( + + {aliasCount > 0 && ( + + {c('Warning').t`Aliases in trash will continue forwarding emails.`} + + )} + + {c('Warning').ngettext( + msgid`Are you sure you want to move ${trashedItemsCount} item to trash?`, + `Are you sure you want to move ${trashedItemsCount} items to trash?`, + trashedItemsCount + )} + + + } + /> + ); +}; + +export const ConfirmMoveManyItems: FC< + ConfirmationPromptHandles & { + selected: BulkSelectionDTO; + shareId: string; + } +> = ({ selected, shareId, onCancel, onConfirm }) => { + const hasLinks = Boolean(useSelector(selectSecureLinksByItems(selected)).length); + const count = getCountOfBulkSelectionDTO(selected); + + return ( + + {({ content: { name: vaultName } }) => ( + + )} + + ); +}; + +export const ConfirmDeleteManyItems: FC = ({ + selected, + onConfirm, + onCancel, +}) => { + const deletedItemsCount = getCountOfBulkSelectionDTO(selected); + const { aliasCount } = useBulkSelect(); + + return ( + + {aliasCount > 0 && ( + + {c('Title').ngettext( + msgid`You’re about to permanently delete ${aliasCount} alias.`, + `You’re about to permanently delete ${aliasCount} aliases.`, + aliasCount + )}{' '} + {c('Title').ngettext( + msgid`Please note that once deleted, the alias can't be restored.`, + `Please note that once once deleted, the aliases can't be restored.`, + aliasCount + )} + + )} + + {c('Warning').ngettext( + msgid`Are you sure you want to permanently delete ${deletedItemsCount} item?`, + `Are you sure you want to permanently delete ${deletedItemsCount} items?`, + deletedItemsCount + )} + + + } + confirmText={c('Action').ngettext( + msgid`Understood, I will never need it`, + `Understood, I will never need them`, + deletedItemsCount + )} + /> + ); +}; diff --git a/packages/pass/components/Item/Actions/ConfirmDeleteAliases.tsx b/packages/pass/components/Item/Actions/ConfirmDeleteAliases.tsx deleted file mode 100644 index fc4e1e73b8..0000000000 --- a/packages/pass/components/Item/Actions/ConfirmDeleteAliases.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { type FC } from 'react'; - -import { c, msgid } from 'ttag'; - -import { useBulkSelect } from '@proton/pass/components/Bulk/BulkSelectProvider'; -import { type ConfirmationPromptHandles } from '@proton/pass/components/Confirmation/ConfirmationPrompt'; -import { ConfirmAliasAction } from '@proton/pass/components/Item/Actions/ConfirmAliasAction'; - -export const ConfirmDeleteAliases: FC = (props) => { - const { aliasCount } = useBulkSelect(); - - return ( - - ); -}; diff --git a/packages/pass/components/Item/Actions/ConfirmMoveItem.tsx b/packages/pass/components/Item/Actions/ConfirmItemActions.tsx similarity index 60% rename from packages/pass/components/Item/Actions/ConfirmMoveItem.tsx rename to packages/pass/components/Item/Actions/ConfirmItemActions.tsx index 866406b6c0..7a1579788c 100644 --- a/packages/pass/components/Item/Actions/ConfirmMoveItem.tsx +++ b/packages/pass/components/Item/Actions/ConfirmItemActions.tsx @@ -7,23 +7,38 @@ import { ConfirmationPrompt, type ConfirmationPromptHandles, } from '@proton/pass/components/Confirmation/ConfirmationPrompt'; +import { ConfirmDeleteAlias } from '@proton/pass/components/Item/Actions/ConfirmAliasActions'; import { WithVault } from '@proton/pass/components/Vault/WithVault'; +import { isAliasItem } from '@proton/pass/lib/items/item.predicates'; import { selectItemSecureLinks } from '@proton/pass/store/selectors'; import type { ItemRevision } from '@proton/pass/types'; -type Props = ConfirmationPromptHandles & { - item: ItemRevision; - shareId: string; -}; +export const ConfirmDeleteItem: FC = (props) => + isAliasItem(props.item.data) ? ( + + ) : ( + + ); -export const ConfirmMoveItem: FC = ({ item, open, shareId, onCancel, onConfirm }) => { +export const ConfirmMoveItem: FC< + ConfirmationPromptHandles & { + item: ItemRevision; + shareId: string; + } +> = ({ item, shareId, onCancel, onConfirm }) => { const hasLinks = Boolean(useSelector(selectItemSecureLinks(item.shareId, item.itemId)).length); return ( {({ content: { name: vaultName } }) => ( = ({ open, selected, shareId, onCancel, onConfirm }) => { - const hasLinks = Boolean(useSelector(selectSecureLinksByItems(selected)).length); - const count = Object.values(selected).reduce((acc, items) => acc + Object.keys(items).length, 0); - - return ( - - {({ content: { name: vaultName } }) => ( - - )} - - ); -}; diff --git a/packages/pass/components/Item/Alias/Alias.view.tsx b/packages/pass/components/Item/Alias/Alias.view.tsx dissimilarity index 62% index 18a8609eb5..6a7d951f41 100644 --- a/packages/pass/components/Item/Alias/Alias.view.tsx +++ b/packages/pass/components/Item/Alias/Alias.view.tsx @@ -1,173 +1,76 @@ -import { type FC, type MouseEvent, useCallback, useState } from 'react'; -import { useSelector } from 'react-redux'; - -import { c } from 'ttag'; - -import { InlineLinkButton } from '@proton/atoms'; -import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider'; -import { ConfirmAliasAction } from '@proton/pass/components/Item/Actions/ConfirmAliasAction'; -import { AliasContent } from '@proton/pass/components/Item/Alias/Alias.content'; -import { ItemHistoryStats } from '@proton/pass/components/Item/History/ItemHistoryStats'; -import { DropdownMenuButton } from '@proton/pass/components/Layout/Dropdown/DropdownMenuButton'; -import { MoreInfoDropdown } from '@proton/pass/components/Layout/Dropdown/MoreInfoDropdown'; -import { ItemViewPanel } from '@proton/pass/components/Layout/Panel/ItemViewPanel'; -import { useNavigation } from '@proton/pass/components/Navigation/NavigationProvider'; -import { getNewItemRoute } from '@proton/pass/components/Navigation/routing'; -import type { ItemViewProps } from '@proton/pass/components/Views/types'; -import { useRequest } from '@proton/pass/hooks/useActionRequest'; -import { useFeatureFlag } from '@proton/pass/hooks/useFeatureFlag'; -import { getOccurrenceString } from '@proton/pass/lib/i18n/helpers'; -import { isAliasDisabled, isTrashed } from '@proton/pass/lib/items/item.predicates'; -import { aliasSyncStatusToggle } from '@proton/pass/store/actions'; -import { selectLoginItemByEmail } from '@proton/pass/store/selectors'; -import { OnboardingMessage } from '@proton/pass/types'; -import { PassFeature } from '@proton/pass/types/api/features'; -import { epochToDateTime } from '@proton/pass/utils/time/format'; - -export const AliasView: FC> = (itemViewProps) => { - const { navigate } = useNavigation(); - const { onboardingCheck, onboardingAcknowledge } = usePassCore(); - const canToggleStatus = useFeatureFlag(PassFeature.PassSimpleLoginAliasesSync); - - const { revision, vault, handleHistoryClick } = itemViewProps; - const { createTime, modifyTime, revision: revisionNumber, optimistic, itemId } = revision; - const { shareId } = vault; - const aliasEmail = revision.aliasEmail!; - const trashed = isTrashed(revision); - const aliasEnabled = !isAliasDisabled(revision); - const modifiedCount = revisionNumber - 1; - - const relatedLogin = useSelector(selectLoginItemByEmail(aliasEmail)); - const relatedLoginName = relatedLogin?.data.metadata.name ?? ''; - const relatedWarning = - relatedLogin && - c('Warning').t`This alias "${aliasEmail}" is currently used in the login "${relatedLoginName}".`; - - const [aliasTrashConfirmed, setAliasTrashConfirmed] = useState(false); - const [confirmTrash, setConfirmTrash] = useState(false); - const [confirmDelete, setConfirmDelete] = useState(false); - - const toggleStatus = useRequest(aliasSyncStatusToggle, {}); - - const createLoginFromAlias = (evt: MouseEvent) => { - evt.stopPropagation(); - evt.preventDefault(); - - navigate(getNewItemRoute('login'), { - searchParams: { email: aliasEmail }, - filters: { selectedShareId: shareId }, - }); - }; - - const handleMoveToTrashClick = useCallback(async () => { - const shouldPrompt = Boolean( - await Promise.resolve(onboardingCheck?.(OnboardingMessage.ALIAS_TRASH_CONFIRM)).catch(() => false) - ); - - setAliasTrashConfirmed(!shouldPrompt); - - /* Show trash confirmation modal if: - - alias is enabled and user didn't previously click "Don't remind me again" - - or the alias is currently used in a login item */ - if (relatedLogin || (canToggleStatus && aliasEnabled && shouldPrompt)) setConfirmTrash(true); - else itemViewProps.handleMoveToTrashClick(); - }, [aliasEnabled, relatedLogin, canToggleStatus, itemViewProps.handleMoveToTrashClick]); - - const handleConfirmDisableClick = aliasEnabled - ? (noRemind: boolean) => { - toggleStatus.dispatch({ shareId, itemId, enabled: false }); - if (noRemind) void onboardingAcknowledge?.(OnboardingMessage.ALIAS_TRASH_CONFIRM); - } - : undefined; - - const handleConfirmTrashClick = (noRemind: boolean) => { - setConfirmTrash(false); - itemViewProps.handleMoveToTrashClick(); - if (noRemind) void onboardingAcknowledge?.(OnboardingMessage.ALIAS_TRASH_CONFIRM); - }; - - const handleConfirmDeleteClick = () => { - setConfirmDelete(false); - itemViewProps.handleDeleteClick(); - }; - - return ( - , - ], - } - : {})} - handleMoveToTrashClick={handleMoveToTrashClick} - handleDeleteClick={() => setConfirmDelete(true)} - > - - {c('Action').t`Create login`} - - ) : null - } - /> - - - - - - {confirmTrash && ( - setConfirmTrash(false)} - onDisable={handleConfirmDisableClick} - onAction={handleConfirmTrashClick} - /> - )} - - {confirmDelete && ( - setConfirmDelete(false)} - onDisable={handleConfirmDisableClick} - onAction={handleConfirmDeleteClick} - /> - )} - - ); -}; +import { type FC, type MouseEvent } from 'react'; + +import { c } from 'ttag'; + +import { InlineLinkButton } from '@proton/atoms'; +import { AliasContent } from '@proton/pass/components/Item/Alias/Alias.content'; +import { ItemHistoryStats } from '@proton/pass/components/Item/History/ItemHistoryStats'; +import { DropdownMenuButton } from '@proton/pass/components/Layout/Dropdown/DropdownMenuButton'; +import { MoreInfoDropdown } from '@proton/pass/components/Layout/Dropdown/MoreInfoDropdown'; +import { ItemViewPanel } from '@proton/pass/components/Layout/Panel/ItemViewPanel'; +import { useNavigation } from '@proton/pass/components/Navigation/NavigationProvider'; +import { getNewItemRoute } from '@proton/pass/components/Navigation/routing'; +import type { ItemViewProps } from '@proton/pass/components/Views/types'; +import { getOccurrenceString } from '@proton/pass/lib/i18n/helpers'; +import { isTrashed } from '@proton/pass/lib/items/item.predicates'; +import { epochToDateTime } from '@proton/pass/utils/time/format'; + +export const AliasView: FC> = (itemViewProps) => { + const { navigate } = useNavigation(); + const { revision, vault, handleHistoryClick } = itemViewProps; + const { createTime, modifyTime, revision: revisionNumber, optimistic } = revision; + const { shareId } = vault; + const aliasEmail = revision.aliasEmail!; + const trashed = isTrashed(revision); + const modifiedCount = revisionNumber - 1; + + const createLoginFromAlias = (evt: MouseEvent) => { + evt.stopPropagation(); + evt.preventDefault(); + + navigate(getNewItemRoute('login'), { + searchParams: { email: aliasEmail }, + filters: { selectedShareId: shareId }, + }); + }; + + return ( + , + ], + } + : {})} + > + + {c('Action').t`Create login`} + + ) : null + } + /> + + + + + + ); +}; diff --git a/packages/pass/components/Item/ItemActionsProvider.tsx b/packages/pass/components/Item/ItemActionsProvider.tsx index a58d8887f7..e4546b09ae 100644 --- a/packages/pass/components/Item/ItemActionsProvider.tsx +++ b/packages/pass/components/Item/ItemActionsProvider.tsx @@ -1,12 +1,20 @@ -import { type FC, type PropsWithChildren, createContext, useContext, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; +import { type FC, type PropsWithChildren, createContext, useCallback, useContext, useMemo } from 'react'; +import { useDispatch, useStore } from 'react-redux'; import { c } from 'ttag'; import { useBulkSelect } from '@proton/pass/components/Bulk/BulkSelectProvider'; -import { ConfirmDeleteAliases } from '@proton/pass/components/Item/Actions/ConfirmDeleteAliases'; +import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider'; +import { ConfirmTrashAlias } from '@proton/pass/components/Item/Actions/ConfirmAliasActions'; +import { + ConfirmDeleteManyItems, + ConfirmMoveManyItems, + ConfirmTrashManyItems, +} from '@proton/pass/components/Item/Actions/ConfirmBulkActions'; +import { ConfirmDeleteItem, ConfirmMoveItem } from '@proton/pass/components/Item/Actions/ConfirmItemActions'; import { VaultSelect, VaultSelectMode, useVaultSelectModalHandles } from '@proton/pass/components/Vault/VaultSelect'; import { useConfirm } from '@proton/pass/hooks/useConfirm'; +import { isAliasDisabled, isAliasItem } from '@proton/pass/lib/items/item.predicates'; import { itemBulkDeleteIntent, itemBulkMoveIntent, @@ -17,12 +25,11 @@ import { itemRestoreIntent, itemTrashIntent, } from '@proton/pass/store/actions'; -import type { BulkSelectionDTO, ItemRevision, MaybeNull } from '@proton/pass/types'; +import { selectLoginItemByEmail } from '@proton/pass/store/selectors'; +import type { State } from '@proton/pass/store/types'; +import { type BulkSelectionDTO, type ItemRevision, type MaybeNull, OnboardingMessage } from '@proton/pass/types'; import { uniqueId } from '@proton/pass/utils/string/unique-id'; -import { ConfirmMoveItem } from './Actions/ConfirmMoveItem'; -import { ConfirmMoveManyItems } from './Actions/ConfirmMoveManyItems'; - /** Ongoing: move every item action definition to this * context object. This context should be loosely connected */ type ItemActionsContextType = { @@ -40,49 +47,92 @@ type ItemActionsContextType = { const ItemActionsContext = createContext>(null); export const ItemActionsProvider: FC = ({ children }) => { - const dispatch = useDispatch(); + const core = usePassCore(); const bulk = useBulkSelect(); + const dispatch = useDispatch(); + const store = useStore(); const { closeVaultSelect, openVaultSelect, modalState } = useVaultSelectModalHandles(); - const moveItem = useConfirm((options: { item: ItemRevision; shareId: string }) => { - const optimisticId = uniqueId(); - dispatch(itemMoveIntent({ ...options, optimisticId })); - }); - - const moveManyItems = useConfirm((options: { selected: BulkSelectionDTO; shareId: string }) => { - dispatch(itemBulkMoveIntent(options)); - bulk.disable(); - }); + const moveItem = useConfirm( + useCallback( + (options: { item: ItemRevision; shareId: string }) => + dispatch( + itemMoveIntent({ + ...options, + optimisticId: uniqueId(), + }) + ), + [] + ) + ); - const trashItem = (item: ItemRevision) => { - dispatch(itemTrashIntent({ itemId: item.itemId, shareId: item.shareId, item })); - }; + const moveManyItems = useConfirm( + useCallback( + (options: { selected: BulkSelectionDTO; shareId: string }) => { + dispatch(itemBulkMoveIntent(options)); + bulk.disable(); + }, + [bulk] + ) + ); - const trashManyItems = (selected: BulkSelectionDTO) => { - dispatch(itemBulkTrashIntent({ selected })); - bulk.disable(); - }; + const trashItem = useConfirm( + useCallback( + (item: ItemRevision) => + dispatch( + itemTrashIntent({ + itemId: item.itemId, + shareId: item.shareId, + item, + }) + ), + [] + ) + ); - const deleteItem = (item: ItemRevision) => { - dispatch(itemDeleteIntent({ itemId: item.itemId, shareId: item.shareId, item })); - }; + const trashManyItems = useConfirm( + useCallback((selected: BulkSelectionDTO) => { + dispatch(itemBulkTrashIntent({ selected })); + bulk.disable(); + }, []) + ); - const deleteManyItems = (options: { selected: BulkSelectionDTO }) => { - dispatch(itemBulkDeleteIntent(options)); - bulk.disable(); - }; + const deleteItem = useConfirm( + useCallback((item: ItemRevision) => { + dispatch( + itemDeleteIntent({ + itemId: item.itemId, + shareId: item.shareId, + item, + }) + ); + }, []) + ); - const confirmDeleteAliases = useConfirm(deleteManyItems); + const deleteManyItems = useConfirm( + useCallback((selected: BulkSelectionDTO) => { + dispatch(itemBulkDeleteIntent({ selected })); + bulk.disable(); + }, []) + ); - const restoreItem = (item: ItemRevision) => { - dispatch(itemRestoreIntent({ itemId: item.itemId, shareId: item.shareId, item })); - }; + const restoreItem = useCallback( + (item: ItemRevision) => + dispatch( + itemRestoreIntent({ + itemId: item.itemId, + shareId: item.shareId, + item, + }) + ), + [] + ); - const restoreManyItems = (selected: BulkSelectionDTO) => { + const restoreManyItems = useCallback((selected: BulkSelectionDTO) => { dispatch(itemBulkRestoreIntent({ selected })); bulk.disable(); - }; + }, []); const context = useMemo(() => { return { @@ -104,14 +154,24 @@ export const ItemActionsProvider: FC = ({ children }) => { closeVaultSelect(); }, }), - moveFromDragAndDrop: (selected, shareId) => { - moveManyItems.prompt({ selected, shareId }); + moveFromDragAndDrop: (selected, shareId) => moveManyItems.prompt({ selected, shareId }), + trash: (item) => { + if (isAliasItem(item.data)) { + const aliasEmail = item.aliasEmail!; + const relatedLogin = selectLoginItemByEmail(aliasEmail)(store.getState()); + if (isAliasDisabled(item) && !relatedLogin) trashItem.call(item); + else { + Promise.resolve(core.onboardingCheck?.(OnboardingMessage.ALIAS_TRASH_CONFIRM) ?? false) + .then((prompt) => (prompt ? trashItem.prompt(item) : trashItem.call(item))) + .catch(() => trashItem.call(item)); + } + } + + trashItem.call(item); }, - trash: trashItem, - trashMany: trashManyItems, - delete: deleteItem, - deleteMany: (selected) => - bulk.aliasCount ? confirmDeleteAliases.prompt({ selected }) : deleteManyItems({ selected }), + trashMany: trashManyItems.prompt, + delete: deleteItem.prompt, + deleteMany: deleteManyItems.prompt, restore: restoreItem, restoreMany: restoreManyItems, }; @@ -147,11 +207,39 @@ export const ItemActionsProvider: FC = ({ children }) => { /> )} - {confirmDeleteAliases.pending && ( - + )} + + {trashManyItems.pending && ( + + )} + + {deleteItem.pending && ( + + )} + + {deleteManyItems.pending && ( + )} diff --git a/packages/pass/components/Vault/Actions/ConfirmTrashEmpty.tsx b/packages/pass/components/Vault/Actions/ConfirmTrashEmpty.tsx index 6d54da55c6..d5d3245bf1 100644 --- a/packages/pass/components/Vault/Actions/ConfirmTrashEmpty.tsx +++ b/packages/pass/components/Vault/Actions/ConfirmTrashEmpty.tsx @@ -26,11 +26,10 @@ export const ConfirmTrashEmpty: FC = ({ open, onCance {aliasCount > 0 && ( {c('Title').ngettext( - msgid`You’re about to permanently delete ${aliasCount} alias`, - `You’re about to permanently delete ${aliasCount} aliases`, + msgid`You’re about to permanently delete ${aliasCount} alias.`, + `You’re about to permanently delete ${aliasCount} aliases.`, aliasCount - )} - {'. '} + )}{' '} {c('Title').ngettext( msgid`Please note that once deleted, the alias can't be restored.`, `Please note that once once deleted, the aliases can't be restored.`, diff --git a/packages/pass/hooks/useConfirm.ts b/packages/pass/hooks/useConfirm.ts index 387788cab9..a37bdc778c 100644 --- a/packages/pass/hooks/useConfirm.ts +++ b/packages/pass/hooks/useConfirm.ts @@ -5,6 +5,7 @@ import { pipe, tap } from '@proton/pass/utils/fp/pipe'; import noop from '@proton/utils/noop'; type UseConfirmResult = { + call: (param: P) => R; cancel: () => void; confirm: () => Maybe; prompt: Dispatch>>; @@ -37,6 +38,7 @@ export const useConfirm =

(action: (param: P) => R return { param, pending: param !== null, + call: action, cancel, confirm, prompt: setParam, diff --git a/packages/pass/lib/items/item.utils.ts b/packages/pass/lib/items/item.utils.ts index 8fcb2e4f9c..8a9dac821d 100644 --- a/packages/pass/lib/items/item.utils.ts +++ b/packages/pass/lib/items/item.utils.ts @@ -226,3 +226,7 @@ export const itemsIntoBulkSelectionDTO = (items: UniqueItem[]): BulkSelectionDTO return dto; }, {}); }; + +export const getCountOfBulkSelectionDTO = (selected: BulkSelectionDTO) => { + return Object.values(selected).reduce((acc, items) => acc + Object.keys(items).length, 0); +}; -- 2.11.4.GIT