diff --git a/package-lock.json b/package-lock.json index b81253d1e..19440b8e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1833,9 +1833,9 @@ } }, "node_modules/@babel/types": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", - "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", "dependencies": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -24872,9 +24872,9 @@ } }, "@babel/types": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", - "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", "requires": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", diff --git a/src/carbonio-ui-commons b/src/carbonio-ui-commons index 15609a5a6..e3d84cffa 160000 --- a/src/carbonio-ui-commons +++ b/src/carbonio-ui-commons @@ -1 +1 @@ -Subproject commit 15609a5a68b8b3ac9dbf0c026b9f4ec795db33ca +Subproject commit e3d84cffa772be6badfb164027ca0276221ce255 diff --git a/src/commons/actions-context.tsx b/src/commons/actions-context.tsx deleted file mode 100644 index 5a92ec6ff..000000000 --- a/src/commons/actions-context.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Zextras - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { DropdownItem } from '@zextras/carbonio-design-system'; -import { - useIntegratedComponent, - useTags, - useUserAccount, - useUserSettings -} from '@zextras/carbonio-shell-ui'; -import { noop } from 'lodash'; -import React, { createContext, FC, useCallback, useMemo } from 'react'; -import { useAppDispatch } from '../hooks/redux'; -import { Conversation, MailMessage } from '../types'; -import { getActions as conversationActions } from '../ui-actions/conversation-actions'; -import { getActions as messageActions } from '../ui-actions/message-actions'; - -type ACPProps = { - folderId: string; - isConversation?: boolean; -}; - -type ActionObj = DropdownItem & { - icon: NonNullable; - onClick: NonNullable; -}; - -type ActionList = Array; - -type GetMsgActionsFunction = (item: MailMessage, closeEditor: boolean) => [ActionList, ActionList]; -type GetConvActionsFunction = ( - item: Conversation, - closeEditor: boolean -) => [ActionList, ActionList]; -export const ActionsContext = createContext<{ - getConversationActions: GetConvActionsFunction; - getMessageActions: GetMsgActionsFunction; -}>({ - getConversationActions: (i: Conversation) => [[], []], - getMessageActions: (i: MailMessage) => [[], []] -}); - -export const ActionsContextProvider: FC = ({ children, folderId }) => { - const dispatch = useAppDispatch(); - const settings = useUserSettings(); - const account = useUserAccount(); - const timezone = useMemo(() => settings?.prefs.zimbraPrefTimeZoneId, [settings]); - const tags = useTags(); - - const [ContactInput] = useIntegratedComponent('contact-input'); - - const [conversationActionsCallback, messageActionsCallback] = useMemo( - () => [ - conversationActions({ - folderId, - dispatch, - tags, - account, - deselectAll: noop - }), - messageActions({ - folderId, - - dispatch, - account, - tags, - deselectAll: noop - }) - ], - [folderId, dispatch, tags, account] - ); - const getMessageActions = useCallback( - (item: MailMessage, closeEditor: boolean): [ActionList, ActionList] => - messageActionsCallback(item, closeEditor), - [messageActionsCallback] - ); - const getConversationActions = useCallback( - (item: Conversation): [ActionList, ActionList] => conversationActionsCallback(item), - [conversationActionsCallback] - ); - return ( - - {children} - - ); -}; diff --git a/src/commons/mail-message-renderer.tsx b/src/commons/mail-message-renderer.tsx index 6ca30512e..cc095bb78 100644 --- a/src/commons/mail-message-renderer.tsx +++ b/src/commons/mail-message-renderer.tsx @@ -27,7 +27,7 @@ import React, { import { Trans } from 'react-i18next'; import styled from 'styled-components'; import { ParticipantRole } from '../carbonio-ui-commons/constants/participants'; -import { EditorAttachmentFiles, MailMessage, MailMessagePart, Participant } from '../types'; +import type { EditorAttachmentFiles, MailMessage, MailMessagePart, Participant } from '../types'; import { getOriginalContent, getQuotedTextOnly } from './get-quoted-text-util'; import { isAvailableInTrusteeList } from './utils'; @@ -461,8 +461,6 @@ const MailMessageRenderer: FC<{ mailMsg: MailMessage; onLoadChange: () => void } mailMsg, onLoadChange }) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore const parts = findAttachments(mailMsg.parts ?? [], []); useEffect(() => { diff --git a/src/commons/preview-eml/get-actions-row.ts b/src/commons/preview-eml/get-actions-row.ts index 3ebd425ea..727a47373 100644 --- a/src/commons/preview-eml/get-actions-row.ts +++ b/src/commons/preview-eml/get-actions-row.ts @@ -6,7 +6,7 @@ import { t } from '@zextras/carbonio-shell-ui'; import { map } from 'lodash'; -import { MailMessage } from '../../types'; +import type { MailMessage } from '../../types'; import { getAttachmentsDownloadLink } from '../../views/app/detail-panel/preview/utils'; export const getActionsRow = ({ msg }: { msg: MailMessage }): string => ` diff --git a/src/commons/preview-eml/get-attachments.ts b/src/commons/preview-eml/get-attachments.ts index 82836a4f8..c383ede43 100644 --- a/src/commons/preview-eml/get-attachments.ts +++ b/src/commons/preview-eml/get-attachments.ts @@ -6,7 +6,7 @@ import { find, map } from 'lodash'; import { DefaultTheme } from 'styled-components'; -import { MailMessage } from '../../types'; +import type { MailMessage } from '../../types'; import { humanFileSize } from '../../views/app/detail-panel/preview/file-preview'; import { getAttachmentIconColors } from '../../views/app/detail-panel/preview/utils'; import { getFileExtension } from '../utilities'; diff --git a/src/commons/preview-eml/get-eml-content.ts b/src/commons/preview-eml/get-eml-content.ts index 4be1dfa98..ca43e1ff1 100644 --- a/src/commons/preview-eml/get-eml-content.ts +++ b/src/commons/preview-eml/get-eml-content.ts @@ -6,7 +6,7 @@ import { filter, forEach, isEmpty, map, reduce } from 'lodash'; import { DefaultTheme } from 'styled-components'; -import { MailMessage } from '../../types'; +import type { MailMessage } from '../../types'; import { findAttachments, plainTextToHTML, diff --git a/src/commons/preview-eml/get-eml-header.ts b/src/commons/preview-eml/get-eml-header.ts index c3dc422cb..a2154b896 100644 --- a/src/commons/preview-eml/get-eml-header.ts +++ b/src/commons/preview-eml/get-eml-header.ts @@ -7,7 +7,7 @@ import { filter } from 'lodash'; import moment from 'moment'; import { DefaultTheme } from 'styled-components'; -import { MailMessage } from '../../types'; +import type { MailMessage } from '../../types'; import { getAvatarLabel } from '../useGetAvatarLabel'; import { banner } from './banner'; import { getActionsRow } from './get-actions-row'; diff --git a/src/commons/preview-eml/get-participant-header.ts b/src/commons/preview-eml/get-participant-header.ts index eb3cfe192..c848ca4d1 100644 --- a/src/commons/preview-eml/get-participant-header.ts +++ b/src/commons/preview-eml/get-participant-header.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { map } from 'lodash'; -import { Participant } from '../../types'; +import type { Participant } from '../../types'; export const getParticipantHeader = (participants: Participant[], type: string): string => { const participantsList = map( diff --git a/src/commons/utilities.tsx b/src/commons/utilities.tsx index 67a473b59..f42056097 100644 --- a/src/commons/utilities.tsx +++ b/src/commons/utilities.tsx @@ -5,7 +5,8 @@ */ import { isNil } from 'lodash'; import { DefaultTheme } from 'styled-components'; -import { AttachmentPart, EditorAttachmentFiles } from '../types'; +import type { EditorAttachmentFiles } from '../types/editor'; +import type { AttachmentPart } from '../types/messages'; const FileExtensionRegex = /^.+\.([^.]+)$/; diff --git a/src/commons/utils.tsx b/src/commons/utils.tsx index 785648ac6..f96757ff5 100644 --- a/src/commons/utils.tsx +++ b/src/commons/utils.tsx @@ -6,7 +6,7 @@ import moment from 'moment'; import { find, isArray } from 'lodash'; import { Account, t } from '@zextras/carbonio-shell-ui'; -import { Participant } from '../types'; +import type { Participant } from '../types'; export const getTimeLabel = (date: number): string => { const momentDate = moment(date); diff --git a/src/constants/index.ts b/src/constants/index.ts index a89350dd9..3490ebdfe 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -4,28 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { FOLDERS } from '@zextras/carbonio-shell-ui'; import { TFunction } from 'i18next'; -// TODO: update the shell constants with this and update the usages in other modules -export const CORRESPONDING_COLORS = [ - { zValue: 0, uiRgb: '#000000', zLabel: 'black' }, - { zValue: 1, uiRgb: '#2b73d2', zLabel: 'blue' }, - { zValue: 2, uiRgb: '#2196d3', zLabel: 'cyan' }, - { zValue: 3, uiRgb: '#639030', zLabel: 'green' }, - { zValue: 4, uiRgb: '#1a75a7', zLabel: 'purple' }, - { zValue: 5, uiRgb: '#d74942', zLabel: 'red' }, - { zValue: 6, uiRgb: '#ffc107', zLabel: 'yellow' }, - { zValue: 7, uiRgb: '#edaeab', zLabel: 'pink' }, - { zValue: 8, uiRgb: '#828282', zLabel: 'gray' }, - { zValue: 9, uiRgb: '#ba8b00', zLabel: 'orange' } -]; - -/* -reference: https://zextras.atlassian.net/wiki/spaces/IRIS/pages/223215854/UI+Guidelines+and+theming -*/ - export const MAILS_ROUTE = 'mails'; +export const MAIL_APP_ID = 'carbonio-mails-ui'; + type AttachmentTypeItemsConstantProps = { label: string; icon: string; @@ -192,4 +177,177 @@ export const emailStatusItemsConstant = (t: TFunction): Array { + if ( + folder?.isLink && + folder?.owner && + folder.parent?.parent === undefined && + folder.oname === ROOT_NAME + ) { + return folder?.owner; + } + if (folder?.parent) { + return getRootAccountName(folder?.parent); + } + return null; +}; + +/** + * Removes the uuid and colon from a folder id (e.g. 123456:2 -> 2) + * @param folderId a folder id + * @returns the folder id without the uuid and colon + */ +export const getSystemFolderParentId = (folderId: string): string => + (folderId.includes(':') ? folderId?.split(':')[1] : folderId) ?? '0'; + +/** + * Returns the parent folder id for a given folder + * @param folder a Folder or LinkFolder + * @returns the path to pass down as props to the Breadcrumb component + */ +export const getFolderPathForBreadcrumb = ( + folderPath: string +): { folderPathFirstPart: string; folderPathLastPart: string } => { + if (folderPath === '') return { folderPathFirstPart: '', folderPathLastPart: '' }; + const folderPathArray = folderPath.split('/'); + const folderPathLastPart = `/ ${folderPathArray[folderPathArray.length - 1]}`; + folderPathArray.pop(); + const folderPathFirstPart = folderPathArray.join('/'); + return { folderPathFirstPart, folderPathLastPart }; +}; diff --git a/src/helpers/identities.ts b/src/helpers/identities.ts index 31e3e9f2d..db6edae8d 100644 --- a/src/helpers/identities.ts +++ b/src/helpers/identities.ts @@ -3,10 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { Account, AccountSettings, Folder, Roots } from '@zextras/carbonio-shell-ui'; -import { find, isArray } from 'lodash'; -import { MailMessage } from '../types'; +import { Account, AccountSettings, Folder } from '@zextras/carbonio-shell-ui'; +import { isArray } from 'lodash'; import { ParticipantRole } from '../carbonio-ui-commons/constants/participants'; +import type { MailMessage } from '../types'; import { getMessageOwnerAccountName } from './folders'; /** @@ -423,14 +423,14 @@ const getMessageSenderAccount = ( export { MatchingReplyIdentity, RecipientWeight, - getRecipientReplyIdentity, - getIdentities, - getAvailableAddresses, - getAddressOwnerAccount, - getRecipients, - computeIdentityWeight, checkMatchingAddress, + computeIdentityWeight, filterMatchingRecipients, + getAddressOwnerAccount, + getAvailableAddresses, + getIdentities, + getMessageSenderAccount, getMessageSenderAddress, - getMessageSenderAccount + getRecipientReplyIdentity, + getRecipients }; diff --git a/src/helpers/messages.ts b/src/helpers/messages.ts new file mode 100644 index 000000000..69ab30d75 --- /dev/null +++ b/src/helpers/messages.ts @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2023 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { ParticipantRoleType } from '../carbonio-ui-commons/constants/participants'; +import type { MailMessage, Participant } from '../types'; + +/** + * Collect all the participants of the given type (or any type if the type params is not set) + * from the given message + * @param message + * @param type + */ +export const collectParticipantsFromMessage = ( + message: MailMessage, + type?: ParticipantRoleType +): Array => { + if (!message || !message.participants) { + return []; + } + + // Filter and return the participants that match the type, or any participant if type is not specified + return message.participants.filter((participant) => !type || participant.type === type); +}; + +/** + * + * Collect all the participants of the given type (or any type if the type params is not set) + * from the given messages + * @param messages + * @param type + */ +export const collectParticipantsFromMessages = ( + messages: Array, + type?: ParticipantRoleType +): Array => { + if (!messages || !messages.length) { + return []; + } + + // Collect and return the filtered participant from each message + return messages.reduce>((result, message): Array => { + result.push(...collectParticipantsFromMessage(message, type)); + return result; + }, []); +}; diff --git a/src/helpers/signatures.ts b/src/helpers/signatures.ts index e25478d85..e67b65b8d 100644 --- a/src/helpers/signatures.ts +++ b/src/helpers/signatures.ts @@ -7,7 +7,7 @@ import { Account } from '@zextras/carbonio-shell-ui'; import { find, map } from 'lodash'; import { convertHtmlToPlainText } from '../carbonio-ui-commons/utils/text/html'; import { LineType } from '../commons/utils'; -import { SignatureDescriptor } from '../types/signatures'; +import type { SignatureDescriptor } from '../types/signatures'; const NO_SIGNATURE_ID = '11111111-1111-1111-1111-111111111111'; const NO_SIGNATURE_LABEL = 'No signature'; @@ -23,8 +23,6 @@ const getSignatures = (account: Account): Array => { value: { description: '', id: NO_SIGNATURE_ID } } ]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore map(account?.signatures?.signature, (item) => signatureArray.push({ // FIXME the Account type defined in Shell needs to be refactored (signatures and identities type) diff --git a/src/hooks/keyboard-shortcuts.tsx b/src/hooks/keyboard-shortcuts.tsx index f4eac6dbe..2a57f91f0 100644 --- a/src/hooks/keyboard-shortcuts.tsx +++ b/src/hooks/keyboard-shortcuts.tsx @@ -5,10 +5,10 @@ */ import { noop } from 'lodash'; import { - setConversationsSpam, moveConversationToTrash, setConversationsFlag, - setConversationsRead + setConversationsRead, + setConversationsSpam } from '../ui-actions/conversation-actions'; type handleKeyboardShortcutsProps = { @@ -17,7 +17,6 @@ type handleKeyboardShortcutsProps = { folderId: any; dispatch: any; deselectAll: any; - createSnackbar: any; conversations: Array; }; diff --git a/src/hooks/redux.ts b/src/hooks/redux.ts index b62f3a522..a2f346cdc 100644 --- a/src/hooks/redux.ts +++ b/src/hooks/redux.ts @@ -6,7 +6,7 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import type { AppDispatch, RootState } from '../store/redux'; -// Use throughout your app instead of plain `useDispatch` and `useSelector` -type DispatchFunc = () => AppDispatch; +export type DispatchFunc = () => AppDispatch; + export const useAppDispatch: DispatchFunc = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/hooks/use-click-outside-picker.tsx b/src/hooks/use-click-outside-picker.tsx index 63e9c578f..420e0cae7 100644 --- a/src/hooks/use-click-outside-picker.tsx +++ b/src/hooks/use-click-outside-picker.tsx @@ -17,8 +17,6 @@ const useClickOutside = (ref: RefObject, handler: (arg: any) = // Do nothing if `mousedown` or `touchstart` started inside ref element if (startedInside || !startedWhenMounted) return; // Do nothing if clicking ref's element or descendent elements - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore if (!ref.current || ref.current.contains(event.target)) return; handler(event); diff --git a/src/hooks/use-conversation-list.ts b/src/hooks/use-conversation-list.ts index dce732396..171a7890c 100644 --- a/src/hooks/use-conversation-list.ts +++ b/src/hooks/use-conversation-list.ts @@ -9,7 +9,7 @@ import { useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { search } from '../store/actions'; import { selectConversationsArray, selectFolderSearchStatus } from '../store/conversations-slice'; -import { Conversation, StateType } from '../types'; +import type { Conversation, StateType } from '../types'; import { useAppDispatch, useAppSelector } from './redux'; type RouteParams = { @@ -19,7 +19,7 @@ type RouteParams = { export const useConversationListItems = (): Array => { const { folderId } = useParams(); const dispatch = useAppDispatch(); - const folderStatus = useAppSelector((state) => + const folderStatus = useAppSelector((state: StateType) => selectFolderSearchStatus(state, folderId) ); const conversations = useAppSelector(selectConversationsArray); @@ -35,11 +35,6 @@ export const useConversationListItems = (): Array => { // [folderId, zimbraPrefSortOrder] // ); - // const sortedConversations = useMemo( - // () => orderBy(conversations, 'date', sorting === 'dateDesc' ? 'desc' : 'asc'), - // [conversations, sorting] - // ); - const filteredConversations = useMemo( () => folder diff --git a/src/hooks/use-get-tags-accordions.tsx b/src/hooks/use-get-tags-accordions.tsx index 98303e65f..ce4842b07 100644 --- a/src/hooks/use-get-tags-accordions.tsx +++ b/src/hooks/use-get-tags-accordions.tsx @@ -6,20 +6,20 @@ import React, { FC, SyntheticEvent, useCallback, useContext, useMemo } from 'react'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -import { t, useTags, ZIMBRA_STANDARD_COLORS, runSearch } from '@zextras/carbonio-shell-ui'; import { AccordionItem, Dropdown, - Row, Icon, + ModalManagerContext, Padding, - Tooltip, - ModalManagerContext + Row, + Tooltip } from '@zextras/carbonio-design-system'; +import { ZIMBRA_STANDARD_COLORS, runSearch, t, useTags } from '@zextras/carbonio-shell-ui'; import { reduce } from 'lodash'; -import { TagsAccordionItems } from '../carbonio-ui-commons/types/tags'; +import type { TagsAccordionItems } from '../carbonio-ui-commons/types/tags'; +import type { ItemType } from '../types'; import { createTag, useGetTagsActions } from '../ui-actions/tag-actions'; -import { ItemType } from '../types'; type ItemProps = { item: ItemType; diff --git a/src/hooks/use-message-actions.tsx b/src/hooks/use-message-actions.tsx index fcca6741b..4875f8516 100644 --- a/src/hooks/use-message-actions.tsx +++ b/src/hooks/use-message-actions.tsx @@ -7,7 +7,7 @@ import { FOLDERS, useAppContext, useTags, useUserAccount } from '@zextras/carbon import { includes } from 'lodash'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; -import { AppContext, MailMessage } from '../types'; +import type { AppContext, MailMessage } from '../types'; import { deleteMessagePermanently, deleteMsg, @@ -28,13 +28,13 @@ import { } from '../ui-actions/message-actions'; import { applyTag } from '../ui-actions/tag-actions'; import { useAppDispatch } from './redux'; -import { useSelection } from './useSelection'; +import { useSelection } from './use-selection'; export const useMessageActions = (message: MailMessage, isAlone = false): Array => { const { folderId }: { folderId: string } = useParams(); const dispatch = useAppDispatch(); const { setCount } = useAppContext(); - const { deselectAll } = useSelection(folderId, setCount); + const { deselectAll } = useSelection({ currentFolderId: folderId, setCount, count: 0 }); const account = useUserAccount(); diff --git a/src/hooks/use-message-list.ts b/src/hooks/use-message-list.ts index b440a775e..0f0ccb565 100644 --- a/src/hooks/use-message-list.ts +++ b/src/hooks/use-message-list.ts @@ -9,7 +9,7 @@ import { useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { search } from '../store/actions'; import { selectFolderMsgSearchStatus, selectMessagesArray } from '../store/messages-slice'; -import { MailMessage } from '../types'; +import type { MailMessage } from '../types'; import { useAppDispatch, useAppSelector } from './redux'; type RouteParams = { @@ -42,11 +42,6 @@ export const useMessageList = (): Array => { // [folderId, zimbraPrefSortOrder] // ); - // const sortedMessages = useMemo( - // () => orderBy(messages, 'date', sorting === 'dateDesc' ? 'desc' : 'asc'), - // [messages, sorting] - // ); - const sortedMessages = useMemo( () => orderBy(filteredMessages, 'date', 'desc'), [filteredMessages] diff --git a/src/hooks/useQueryParam.ts b/src/hooks/use-query-param.ts similarity index 100% rename from src/hooks/useQueryParam.ts rename to src/hooks/use-query-param.ts diff --git a/src/hooks/use-selection.tsx b/src/hooks/use-selection.tsx new file mode 100644 index 000000000..f60a1f52a --- /dev/null +++ b/src/hooks/use-selection.tsx @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2021 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { map, omit } from 'lodash'; +import { useCallback, useMemo, useState } from 'react'; +import type { Conversation, IncompleteMessage } from '../types'; + +export type useSelectionProps = { + currentFolderId: string; + count: number; + items?: Array; + setCount: (value: number | ((prevState: number) => number)) => void; +}; + +export type useSelectionReturnType = { + selected: Record; + isSelectModeOn: boolean; + toggle: (id: string) => void; + deselectAll: () => void; + selectAll: () => void; + isAllSelected: boolean; + selectAllModeOff: () => void; + setIsSelectModeOn: (isSelectModeOn: boolean | ((prevState: boolean) => boolean)) => void; +}; + +export const useSelection = ({ + setCount, + count, + items = [] +}: useSelectionProps): useSelectionReturnType => { + const [selected, setSelected] = useState>({}); + const [isSelectModeOn, setIsSelectModeOn] = useState(false); + const isAllSelected = useMemo(() => count === items.length, [count, items.length]); + + const selectItem = useCallback( + (id) => { + if (selected[id]) { + setSelected((s) => omit(s, [id])); + setCount((prev: number) => prev - 1); + if (count - 1 === 0) { + setIsSelectModeOn(false); + } else if (count === 0) { + setIsSelectModeOn(true); + } + } else { + setSelected((s) => ({ ...s, [id]: true })); + setCount((prev: number) => prev + 1); + setIsSelectModeOn(true); + } + }, + [count, selected, setCount] + ); + + const deselectAll = useCallback(() => { + setSelected({}); + setCount(0); + setIsSelectModeOn(false); + }, [setCount]); + + const selectAll = useCallback(() => { + map(items, (conv) => { + if (!selected[conv.id]) { + selectItem(conv.id); + } + }); + }, [items, selectItem, selected]); + + const selectAllModeOff = useCallback(() => { + setSelected({}); + setCount(0); + setTimeout(() => { + setIsSelectModeOn(true); + }); + }, [setCount]); + + return { + selected, + toggle: selectItem, + deselectAll, + isSelectModeOn, + setIsSelectModeOn, + selectAll, + isAllSelected, + selectAllModeOff + }; +}; diff --git a/src/hooks/useSelection.jsx b/src/hooks/useSelection.jsx deleted file mode 100644 index 4beb07049..000000000 --- a/src/hooks/useSelection.jsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Zextras - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { omit, isEmpty, map } from 'lodash'; - -export const useSelection = (currentFolderId, setCount, count, items = []) => { - const [selected, setSelected] = useState({}); - const [folderId, setFolderId] = useState(currentFolderId); - const [isSelectModeOn, setIsSelectModeOn] = useState(false); - useEffect(() => { - setIsSelectModeOn(!isEmpty(selected)); - if (currentFolderId !== folderId) { - setSelected({}); - setFolderId(currentFolderId); - setIsSelectModeOn(false); - setCount(0); - } - }, [currentFolderId, folderId, selected, setCount]); - - const isAllSelected = useMemo(() => count === items.length, [count, items.length]); - const selectItem = useCallback( - (id) => { - if (selected[id]) { - setSelected((s) => omit(s, [id])); - setCount((c) => c - 1); - } else { - setSelected((s) => ({ ...s, [id]: true })); - setCount((c) => c + 1); - } - }, - [selected, setCount] - ); - - const deselectAll = useCallback(() => { - setIsSelectModeOn(false); - setSelected({}); - setCount(0); - }, [setCount]); - - const selectAll = useCallback(() => { - map(items, (conv) => { - if (!selected[conv.id]) { - selectItem(conv.id); - } - }); - }, [items, selectItem, selected]); - - const selectAllModeOff = useCallback(() => { - setSelected({}); - setCount(0); - setTimeout(() => { - setIsSelectModeOn(true); - }); - }, [setCount]); - - return { - selected, - toggle: selectItem, - deselectAll, - isSelectModeOn, - setIsSelectModeOn, - selectAll, - isAllSelected, - selectAllModeOff - }; -}; diff --git a/src/integrations/shared-functions.ts b/src/integrations/shared-functions.ts index 741cd373e..189bd3440 100644 --- a/src/integrations/shared-functions.ts +++ b/src/integrations/shared-functions.ts @@ -5,7 +5,7 @@ */ import { addBoard } from '@zextras/carbonio-shell-ui'; import { isNil, omit, omitBy } from 'lodash'; -import { Participant } from '../types'; +import type { Participant } from '../types'; import { MAILS_ROUTE } from '../constants'; import { ActionsType } from '../commons/utils'; diff --git a/src/integrations/shared-invite-reply/index.tsx b/src/integrations/shared-invite-reply/index.tsx index e8853c863..c1a219af2 100644 --- a/src/integrations/shared-invite-reply/index.tsx +++ b/src/integrations/shared-invite-reply/index.tsx @@ -3,7 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { FC, ReactElement, useContext, useEffect, useMemo, useState } from 'react'; import { Button, Collapse, @@ -12,17 +11,16 @@ import { Icon, Padding, Row, - SnackbarManagerContext, Text } from '@zextras/carbonio-design-system'; import { FOLDERS, t } from '@zextras/carbonio-shell-ui'; -import { useDispatch } from 'react-redux'; +import React, { FC, ReactElement, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { useAppDispatch } from '../../hooks/redux'; +import type { MailMessage } from '../../types'; import LabelRow from './parts/label-row'; import ResponseActions from './parts/response-actions'; import { ShareCalendarRoleOptions, findLabel } from './parts/utils'; -import { MailMessage } from '../../types'; -import { useAppDispatch } from '../../hooks/redux'; const InviteContainer = styled(Container)` border: 0.0625rem solid ${({ theme }: any): string => theme.palette.gray2.regular}; @@ -30,13 +28,13 @@ const InviteContainer = styled(Container)` margin: ${({ theme }: any): string => theme.sizes.padding.extrasmall}; `; -type SharedCalendarResponse = { +type SharedCalendarResponseReturnType = { sharedContent: string; mailMsg: MailMessage; onLoadChange?: () => void; }; -const SharedCalendarResponse: FC = ({ +const SharedCalendarResponse: FC = ({ sharedContent, mailMsg, onLoadChange @@ -46,7 +44,6 @@ const SharedCalendarResponse: FC = ({ onLoadChange && onLoadChange(); } }, [mailMsg.read, onLoadChange]); - const createSnackbar = useContext(SnackbarManagerContext); const dispatch = useAppDispatch(); const rights = useMemo( @@ -208,7 +205,6 @@ const SharedCalendarResponse: FC = ({ <> ( +const LabelFactory = ({ selected, label, open, focus }: CustomLabelFactoryProps): JSX.Element => ( ( ); -export default function ColorSelect({ onChange, defaultColor = 0, label }) { +function getColorLabel(color: string): string { + /* i18next-extract-disable-next-line */ + return t(`color.${color}`, '{{color}}', { + context: ZIMBRA_STANDARD_COLORS, + replace: { + color + } + }); +} + +export default function ColorSelect({ + onChange, + defaultColor = 0, + label +}: { + onChange: SingleSelectionOnChange; + defaultColor: number; + label: string; +}): JSX.Element { const colors = useMemo( () => ZIMBRA_STANDARD_COLORS.map((el, index) => ({ - label: t(el.zLabel), + label: getColorLabel(el.zLabel), value: index.toString(), customComponent: ( - {t(el.zLabel)} + {getColorLabel(el.zLabel)} diff --git a/src/integrations/shared-invite-reply/parts/response-actions.tsx b/src/integrations/shared-invite-reply/parts/response-actions.tsx index b2a748c72..dc72ce297 100644 --- a/src/integrations/shared-invite-reply/parts/response-actions.tsx +++ b/src/integrations/shared-invite-reply/parts/response-actions.tsx @@ -4,29 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only */ /* eslint-disable import/extensions */ -import React, { FC, ReactElement, useCallback, useMemo, useState } from 'react'; import { - Padding, Button, - Divider, - Row, Checkbox, + Divider, Input, + Padding, + Row, Text } from '@zextras/carbonio-design-system'; import { useFoldersByView, useUserAccounts } from '@zextras/carbonio-shell-ui'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore import { find } from 'lodash'; +import React, { ChangeEvent, FC, ReactElement, useCallback, useMemo, useState } from 'react'; import { FOLDER_VIEW } from '../../../carbonio-ui-commons/constants'; +import type { ResponseActionsProps } from '../../../types'; import ColorSelect from './color-select'; -import { ResponseActionsProps } from '../../../types'; import { accept, decline } from './share-calendar-actions'; const ResponseActions: FC = ({ dispatch, t, - createSnackbar, zid, view, rid, @@ -41,7 +38,7 @@ const ResponseActions: FC = ({ const [customMessage, setCustomMessage] = useState(''); const [notifyOrganizer, setNotifyOrganizer] = useState(false); const [calendarName, setCalendarName] = useState(sharedCalendarName); - const [selectedColor, setSelectedColor] = useState(0); + const [selectedColor, setSelectedColor] = useState('0'); const accounts = useUserAccounts(); const calFolders = useFoldersByView(FOLDER_VIEW.appointment); const showError = useMemo( @@ -63,7 +60,7 @@ const ResponseActions: FC = ({ view, rid, calendarName, - color: selectedColor, + color: parseInt(selectedColor ?? '0', 10), accounts, t, dispatch, @@ -75,8 +72,7 @@ const ResponseActions: FC = ({ customMessage, role, allowedActions, - notifyOrganizer, - createSnackbar + notifyOrganizer }), [ zid, @@ -95,8 +91,7 @@ const ResponseActions: FC = ({ customMessage, role, allowedActions, - notifyOrganizer, - createSnackbar + notifyOrganizer ] ); @@ -104,7 +99,6 @@ const ResponseActions: FC = ({ decline({ dispatch, t, - createSnackbar, msgId, sharedCalendarName, owner, @@ -118,7 +112,7 @@ const ResponseActions: FC = ({ }, [ dispatch, t, - createSnackbar, + msgId, sharedCalendarName, owner, @@ -159,8 +153,8 @@ const ResponseActions: FC = ({ label={t('label.type_name_here', 'Item name')} backgroundColor="gray5" value={calendarName} - hasError={disabled && false} - onChange={(e: any): void => setCalendarName(e.target.value)} + hasError={!disabled} + onChange={(e: ChangeEvent): void => setCalendarName(e.target.value)} /> = ({ padding={{ horizontal: 'small', vertical: 'small' }} > setSelectedColor(a)} - t={t} + onChange={(a: string | null): void => setSelectedColor(a)} + defaultColor={0} label={t('label.calendar_color', `Item color`)} /> diff --git a/src/integrations/shared-invite-reply/parts/share-calendar-actions.ts b/src/integrations/shared-invite-reply/parts/share-calendar-actions.ts index 75992ddea..3176ba47b 100644 --- a/src/integrations/shared-invite-reply/parts/share-calendar-actions.ts +++ b/src/integrations/shared-invite-reply/parts/share-calendar-actions.ts @@ -3,19 +3,16 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ +import { getBridgedFunctions } from '@zextras/carbonio-shell-ui'; import { map } from 'lodash'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import { Dispatch } from 'redux'; import { ParticipantRole } from '../../../carbonio-ui-commons/constants/participants'; import { msgAction } from '../../../store/actions'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore import { acceptSharedCalendarReply } from '../../../store/actions/acceptSharedCalendarReply'; import { mountSharedCalendar } from '../../../store/actions/mount-share-calendar'; -import { MailsEditor, Participant } from '../../../types'; +import { AppDispatch } from '../../../store/redux'; +import type { MailsEditor, Participant } from '../../../types'; -type accept = { +type Accept = { zid: string; view: string; rid: string; @@ -23,36 +20,35 @@ type accept = { color: number; accounts: any; t: (...args: any[]) => string; - dispatch: Dispatch; + dispatch: AppDispatch; msgId: Array | any; sharedCalendarName: string; owner: string; participants: Participant[]; grantee: string; customMessage: string; - createSnackbar: any; role: string; allowedActions: string; notifyOrganizer: boolean; }; -type moveInviteToTrashType = { +type MoveInviteToTrashType = { t: (...args: any[]) => string; - dispatch: Dispatch; + dispatch: AppDispatch; msgId: string; }; -type mountSharedCalendarType = { +type MountSharedCalendarType = { zid: string; view: string; rid: string; calendarName: string; color: number; accounts: any; - dispatch: Dispatch; + dispatch: AppDispatch; }; -type acceptSharedCalendarType = { - dispatch: Dispatch; +type AcceptSharedCalendarType = { + dispatch: AppDispatch; sharedCalendarName: string; owner: string; participants: Participant[]; @@ -63,10 +59,9 @@ type acceptSharedCalendarType = { isAccepted: boolean; }; -type declineType = { - dispatch: Dispatch; +type DeclineType = { + dispatch: AppDispatch; t: (...args: any[]) => string; - createSnackbar: any; msgId: string; sharedCalendarName: string; owner: string; @@ -85,10 +80,8 @@ const mountSharedCalendarFunc = ({ color, accounts, dispatch -}: mountSharedCalendarType): any => +}: MountSharedCalendarType): any => dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore mountSharedCalendar({ zid, view, @@ -109,10 +102,9 @@ const sharedCalendarReplyFunc = ({ role, allowedActions, isAccepted -}: acceptSharedCalendarType): any => - dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore +}: AcceptSharedCalendarType): any => { + const displayMessage = customMessage?.length > 0 ? customMessage : ''; + return dispatch( acceptSharedCalendarReply({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -131,30 +123,23 @@ const sharedCalendarReplyFunc = ({ }), text: [ isAccepted - ? `Accepted: ${grantee} has accepted the sharing of "${sharedCalendarName}"\n\n----------------------------------------------\n\nShared item: ${sharedCalendarName}\nOwner: ${owner}\nGrantee: ${grantee}\nRole: ${role}\nAllowed actions: ${allowedActions}\n*~*~*~*~*~*~*~*~*~*\n${ - customMessage?.length > 0 ? customMessage : '' - }` - : `Declined: ${grantee} has declined the sharing of "${sharedCalendarName}"\n\n----------------------------------------------\n\nShared item: ${sharedCalendarName}\nOwner: ${owner}\nGrantee: ${grantee}\nRole: ${role}\nAllowed actions: ${allowedActions}\n*~*~*~*~*~*~*~*~*~*\n${ - customMessage?.length > 0 ? customMessage : '' - }` + ? `Accepted: ${grantee} has accepted the sharing of "${sharedCalendarName}"\n\n----------------------------------------------\n\nShared item: ${sharedCalendarName}\nOwner: ${owner}\nGrantee: ${grantee}\nRole: ${role}\nAllowed actions: ${allowedActions}\n*~*~*~*~*~*~*~*~*~*\n${displayMessage}` + : `Declined: ${grantee} has declined the sharing of "${sharedCalendarName}"\n\n----------------------------------------------\n\nShared item: ${sharedCalendarName}\nOwner: ${owner}\nGrantee: ${grantee}\nRole: ${role}\nAllowed actions: ${allowedActions}\n*~*~*~*~*~*~*~*~*~*\n${displayMessage}` ] } as MailsEditor }) ); +}; -const moveInviteToTrashFunc = ({ msgId, dispatch, t }: moveInviteToTrashType): any => +const moveInviteToTrashFunc = ({ msgId, dispatch, t }: MoveInviteToTrashType): any => dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore msgAction({ operation: `trash`, ids: [msgId] }) ).then((res2: any): void => { if (!res2.type.includes('fulfilled')) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `share`, replace: true, hideButton: true, @@ -182,9 +167,8 @@ export const accept = ({ customMessage, role, allowedActions, - notifyOrganizer, - createSnackbar -}: accept): void => + notifyOrganizer +}: Accept): void => mountSharedCalendarFunc({ zid, view, @@ -194,8 +178,6 @@ export const accept = ({ accounts, dispatch }).then((res: any): void => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore if (res.type.includes('fulfilled')) { notifyOrganizer && sharedCalendarReplyFunc({ @@ -210,9 +192,7 @@ export const accept = ({ isAccepted: true }); moveInviteToTrashFunc({ msgId, dispatch, t }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `share_accepted`, replace: true, type: 'info', @@ -221,9 +201,7 @@ export const accept = ({ hideButton: true }); } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `share`, replace: true, type: 'error', @@ -237,7 +215,6 @@ export const accept = ({ export const decline = ({ dispatch, t, - createSnackbar, msgId, sharedCalendarName, owner, @@ -247,10 +224,8 @@ export const decline = ({ role, allowedActions, notifyOrganizer -}: declineType): any => +}: DeclineType): any => dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore msgAction({ operation: `trash`, ids: [msgId] @@ -269,8 +244,7 @@ export const decline = ({ allowedActions, isAccepted: false }); - - createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `share_declined`, replace: true, type: 'info', @@ -279,9 +253,7 @@ export const decline = ({ hideButton: true }); } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `share`, replace: true, type: 'error', diff --git a/src/normalizations/normalize-conversation.ts b/src/normalizations/normalize-conversation.ts index 78c195244..342ae3403 100644 --- a/src/normalizations/normalize-conversation.ts +++ b/src/normalizations/normalize-conversation.ts @@ -6,7 +6,7 @@ import { Tags } from '@zextras/carbonio-shell-ui'; import { filter, find, isNil, map } from 'lodash'; import { omitBy } from '../commons/utils'; -import { Conversation, SoapIncompleteMessage, SoapConversation } from '../types'; +import type { Conversation, SoapIncompleteMessage, SoapConversation } from '../types'; import { normalizeParticipantsFromSoap } from './normalize-message'; const getTagIdsFromName = (names: string | undefined, tags?: Tags): Array => @@ -52,8 +52,6 @@ export const normalizeConversation = ({ tags: getTagIds(c.t, c.tn, tags), id: c.id, date: c.d, - msgCount: c.n, - unreadMsgCount: c.u, messages, participants: c.e ? map(c.e, normalizeParticipantsFromSoap) : undefined, subject: c.su, diff --git a/src/normalizations/normalize-filter-rules.ts b/src/normalizations/normalize-filter-rules.ts index d5afcbb16..e3cfcd30e 100644 --- a/src/normalizations/normalize-filter-rules.ts +++ b/src/normalizations/normalize-filter-rules.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { map, omit, reduce } from 'lodash'; -import { FilterRules, FilterTest } from '../types'; +import type { FilterRules, FilterTest } from '../types'; export const normalizeFilterTests = (filterTests: FilterTest): FilterTest => reduce( diff --git a/src/normalizations/normalize-message.ts b/src/normalizations/normalize-message.ts index 3080186ce..41c1bc5ab 100644 --- a/src/normalizations/normalize-message.ts +++ b/src/normalizations/normalize-message.ts @@ -9,7 +9,7 @@ import { ParticipantRole, ParticipantRoleType } from '../carbonio-ui-commons/constants/participants'; -import { +import type { AttachmentPart, IncompleteMessage, MailMessagePart, diff --git a/src/store/actions/acceptSharedCalendarReply.ts b/src/store/actions/acceptSharedCalendarReply.ts index c1b6cf15e..7aa44f9a9 100644 --- a/src/store/actions/acceptSharedCalendarReply.ts +++ b/src/store/actions/acceptSharedCalendarReply.ts @@ -5,7 +5,7 @@ */ import { createAsyncThunk } from '@reduxjs/toolkit'; import { soapFetch } from '@zextras/carbonio-shell-ui'; -import { ReplyShareParameters, SaveDraftRequest, SaveDraftResponse } from '../../types'; +import type { ReplyShareParameters, SaveDraftRequest, SaveDraftResponse } from '../../types'; import { generateRequest } from '../editor-slice-utils'; // TODO probably the owner account should be set also here diff --git a/src/store/actions/conv-action.ts b/src/store/actions/conv-action.ts index 648a6c855..8ebb9fecf 100644 --- a/src/store/actions/conv-action.ts +++ b/src/store/actions/conv-action.ts @@ -7,7 +7,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { soapFetch } from '@zextras/carbonio-shell-ui'; import { isNil } from 'lodash'; import { omitBy } from '../../commons/utils'; -import { +import type { ConvActionParameters, ConvActionRequest, ConvActionResponse, diff --git a/src/store/actions/folder-action.ts b/src/store/actions/folder-action.ts index edf7506c9..3098f7e89 100644 --- a/src/store/actions/folder-action.ts +++ b/src/store/actions/folder-action.ts @@ -6,7 +6,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { Folder, soapFetch } from '@zextras/carbonio-shell-ui'; import { isEmpty, isNil, omitBy } from 'lodash'; -import { DataProps, FolderType } from '../../types'; +import type { DataProps, FolderType } from '../../types'; type FolderActionProps = { folder: FolderType | DataProps | Omit; diff --git a/src/store/actions/get-conv.ts b/src/store/actions/get-conv.ts index b432b9da5..0e1f86dac 100644 --- a/src/store/actions/get-conv.ts +++ b/src/store/actions/get-conv.ts @@ -8,7 +8,7 @@ import { getTags, soapFetch } from '@zextras/carbonio-shell-ui'; import { map } from 'lodash'; import { normalizeConversation } from '../../normalizations/normalize-conversation'; import { normalizeMailMessageFromSoap } from '../../normalizations/normalize-message'; -import { +import type { Conversation, GetConvParameters, GetConvRequest, diff --git a/src/store/actions/get-incoming-filters.ts b/src/store/actions/get-incoming-filters.ts index 0f5344157..2dc06a736 100644 --- a/src/store/actions/get-incoming-filters.ts +++ b/src/store/actions/get-incoming-filters.ts @@ -5,7 +5,7 @@ */ import { soapFetch } from '@zextras/carbonio-shell-ui'; import { normalizeFilterRulesFromSoap } from '../../normalizations/normalize-filter-rules'; -import { FilterRules } from '../../types'; +import type { FilterRules } from '../../types'; export const getIncomingFilters = async (): Promise => { const { filterRules } = (await soapFetch('GetFilterRules', { diff --git a/src/store/actions/get-msg-for-print.ts b/src/store/actions/get-msg-for-print.ts index b0404ffa0..36262cbd7 100644 --- a/src/store/actions/get-msg-for-print.ts +++ b/src/store/actions/get-msg-for-print.ts @@ -7,7 +7,7 @@ import { soapFetch } from '@zextras/carbonio-shell-ui'; import { isNull, map, omitBy } from 'lodash'; import { normalizeMailMessageFromSoap } from '../../normalizations/normalize-message'; -import { +import type { GetMsgForPrintParameter, GetMsgResponse as GetMsgResponseType, MailMessage diff --git a/src/store/actions/get-msg.ts b/src/store/actions/get-msg.ts index 036b35f81..89b9f5286 100644 --- a/src/store/actions/get-msg.ts +++ b/src/store/actions/get-msg.ts @@ -6,7 +6,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { soapFetch } from '@zextras/carbonio-shell-ui'; import { normalizeMailMessageFromSoap } from '../../normalizations/normalize-message'; -import { GetMsgParameters, MailMessage, GetMsgRequest, GetMsgResponse } from '../../types'; +import type { GetMsgParameters, MailMessage, GetMsgRequest, GetMsgResponse } from '../../types'; export const getMsg = createAsyncThunk( 'messages/getMsg', diff --git a/src/store/actions/get-outgoing-filters.ts b/src/store/actions/get-outgoing-filters.ts index dc3b6e92f..f192a3fff 100644 --- a/src/store/actions/get-outgoing-filters.ts +++ b/src/store/actions/get-outgoing-filters.ts @@ -5,7 +5,7 @@ */ import { createAsyncThunk } from '@reduxjs/toolkit'; import { soapFetch } from '@zextras/carbonio-shell-ui'; -import { FilterRules } from '../../types'; +import type { FilterRules } from '../../types'; export const getOutgoingFilters = createAsyncThunk( 'filters/get_filters', diff --git a/src/store/actions/msg-action.ts b/src/store/actions/msg-action.ts index 2e55f9abb..8cb78c306 100644 --- a/src/store/actions/msg-action.ts +++ b/src/store/actions/msg-action.ts @@ -6,7 +6,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { soapFetch } from '@zextras/carbonio-shell-ui'; import { isNil, omitBy } from 'lodash'; -import { +import type { MsgActionRequest, MsgActionResponse, MsgActionResult, diff --git a/src/store/actions/redirect-action.ts b/src/store/actions/redirect-action.ts index b101c6577..727eec0af 100644 --- a/src/store/actions/redirect-action.ts +++ b/src/store/actions/redirect-action.ts @@ -5,7 +5,7 @@ */ import { soapFetch } from '@zextras/carbonio-shell-ui'; -import { RedirectMessageActionRequest, MessageSpecification } from '../../types'; +import type { RedirectMessageActionRequest, MessageSpecification } from '../../types'; export const redirectMessageAction = async ({ id, e }: MessageSpecification): Promise => { const res = await soapFetch('BounceMsg', { diff --git a/src/store/actions/save-draft.ts b/src/store/actions/save-draft.ts index 3d0fc3c47..3c11665e1 100644 --- a/src/store/actions/save-draft.ts +++ b/src/store/actions/save-draft.ts @@ -5,7 +5,7 @@ */ import { createAsyncThunk } from '@reduxjs/toolkit'; import { soapFetch } from '@zextras/carbonio-shell-ui'; -import { +import type { MailsEditor, PrefsType, SaveDraftNewParameters, diff --git a/src/store/actions/search-conv.ts b/src/store/actions/search-conv.ts index 73b238c6e..681327e6b 100644 --- a/src/store/actions/search-conv.ts +++ b/src/store/actions/search-conv.ts @@ -7,7 +7,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { soapFetch } from '@zextras/carbonio-shell-ui'; import { map } from 'lodash'; import { normalizeMailMessageFromSoap } from '../../normalizations/normalize-message'; -import { +import type { MailMessage, SearchConvParameters, SearchConvRequest, diff --git a/src/store/actions/search.ts b/src/store/actions/search.ts index 7873ae43b..eaf773f07 100644 --- a/src/store/actions/search.ts +++ b/src/store/actions/search.ts @@ -10,12 +10,12 @@ import { ErrorSoapBodyResponse, getTags, soapFetch } from '@zextras/carbonio-she import { keyBy, map, reduce } from 'lodash'; import { normalizeConversation } from '../../normalizations/normalize-conversation'; import { normalizeMailMessageFromSoap } from '../../normalizations/normalize-message'; -import { +import type { Conversation, - SearchRequest, - SearchResponse, + FetchConversationsParameters, FetchConversationsReturn, - FetchConversationsParameters + SearchRequest, + SearchResponse } from '../../types'; export const search = createAsyncThunk< @@ -23,6 +23,8 @@ export const search = createAsyncThunk< FetchConversationsParameters >( 'fetchConversations', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore async ( { folderId, diff --git a/src/store/actions/send-msg.ts b/src/store/actions/send-msg.ts index ba117473e..a20a0733a 100644 --- a/src/store/actions/send-msg.ts +++ b/src/store/actions/send-msg.ts @@ -5,8 +5,13 @@ */ import { createAsyncThunk } from '@reduxjs/toolkit'; import { getUserAccount, getUserSettings, soapFetch } from '@zextras/carbonio-shell-ui'; -import { getAddressOwnerAccount, getMessageSenderAccount } from '../../helpers/identities'; -import { StateType, SaveDraftRequest, SaveDraftResponse, SendMsgParameters } from '../../types'; +import { getAddressOwnerAccount } from '../../helpers/identities'; +import type { + SaveDraftRequest, + SaveDraftResponse, + SendMsgParameters, + StateType +} from '../../types'; import { closeEditor } from '../editor-slice'; import { generateMailRequest, generateRequest } from '../editor-slice-utils'; import { getConv } from './get-conv'; diff --git a/src/store/conversations-slice.ts b/src/store/conversations-slice.ts index 494a46f9b..32aa46472 100644 --- a/src/store/conversations-slice.ts +++ b/src/store/conversations-slice.ts @@ -10,7 +10,7 @@ import { createSlice } from '@reduxjs/toolkit'; import produce from 'immer'; import { forEach, merge, reduce } from 'lodash'; -import { +import type { FolderType, ConversationsFolderStatus, ConversationsStateType, diff --git a/src/store/editor-slice-utils.ts b/src/store/editor-slice-utils.ts index 09b4dc5a4..f0100b7d0 100644 --- a/src/store/editor-slice-utils.ts +++ b/src/store/editor-slice-utils.ts @@ -14,7 +14,7 @@ import { convertHtmlToPlainText } from '../carbonio-ui-commons/utils/text/html'; import { LineType } from '../commons/utils'; import { htmlEncode } from '../commons/get-quoted-text-util'; import { composeMailBodyWithSignature, getSignatureValue } from '../helpers/signatures'; -import { +import type { EditorAttachmentFiles, InlineAttachedType, MailAttachmentParts, diff --git a/src/store/editor-slice.ts b/src/store/editor-slice.ts index 9d725369d..8f60eefa3 100644 --- a/src/store/editor-slice.ts +++ b/src/store/editor-slice.ts @@ -10,7 +10,7 @@ import { drop } from 'lodash'; import { ActionsType } from '../commons/utils'; import { composeMailBodyWithSignature, getSignatureValue } from '../helpers/signatures'; import { normalizeMailMessageFromSoap } from '../normalizations/normalize-message'; -import { +import type { EditorsStateType, MailsEditorMap, StateType, diff --git a/src/store/folders-slice.ts b/src/store/folders-slice.ts index 924f5da19..944e1d1bb 100644 --- a/src/store/folders-slice.ts +++ b/src/store/folders-slice.ts @@ -8,7 +8,13 @@ import { createSlice } from '@reduxjs/toolkit'; import produce from 'immer'; import { forEach, reduce, cloneDeep, map, filter } from 'lodash'; -import { ISoapFolderObj, FolderType, FoldersStateType, MailsFolderMap, StateType } from '../types'; +import type { + ISoapFolderObj, + FolderType, + FoldersStateType, + MailsFolderMap, + StateType +} from '../types'; import { createFolder } from './actions/create-folder'; import { folderAction } from './actions/folder-action'; import { searchFolder } from './actions/search-folders'; diff --git a/src/store/messages-slice.ts b/src/store/messages-slice.ts index 882299c3e..c338c3e73 100644 --- a/src/store/messages-slice.ts +++ b/src/store/messages-slice.ts @@ -13,7 +13,7 @@ import produce from 'immer'; import { cloneDeep, forEach, merge, mergeWith, reduce } from 'lodash'; import { CONVACTIONS } from '../commons/utilities'; import { normalizeMailMessageFromSoap } from '../normalizations/normalize-message'; -import { +import type { MsgMap, MsgStateType, StateType, diff --git a/src/store/search-slice-reducers.ts b/src/store/search-slice-reducers.ts index 8975fcf49..b6d46cbe9 100644 --- a/src/store/search-slice-reducers.ts +++ b/src/store/search-slice-reducers.ts @@ -5,7 +5,7 @@ */ import { filter, find, forEach, map, merge, omit, reduce, some } from 'lodash'; -import { ConvMessage, Payload, SearchesStateType } from '../types'; +import type { ConvMessage, Payload, SearchesStateType } from '../types'; export const handleCreatedConversationsReducer = ( state: SearchesStateType, diff --git a/src/store/searches-slice.ts b/src/store/searches-slice.ts index ff05f5285..894e90749 100644 --- a/src/store/searches-slice.ts +++ b/src/store/searches-slice.ts @@ -5,11 +5,12 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { ErrorSoapResponse, FOLDERS } from '@zextras/carbonio-shell-ui'; import { createSlice } from '@reduxjs/toolkit'; +import { FOLDERS } from '@zextras/carbonio-shell-ui'; import produce from 'immer'; -import { includes, map } from 'lodash'; -import { +import { forEach } from 'lodash'; +import { CONVACTIONS } from '../commons/utilities'; +import type { ConvActionParameters, FetchConversationsReturn, SearchesStateType, @@ -19,21 +20,24 @@ import { msgAction, search } from './actions'; import { handleAddMessagesInConversationReducer, handleCreatedConversationsReducer, + handleCreatedMessagesInConversationsReducer, handleDeletedConversationsReducer, handleDeletedMessagesInConversationReducer, handleDeletedMessagesReducer, handleModifiedConversationsReducer, - handleModifiedMessagesInConversationReducer, - handleCreatedMessagesInConversationsReducer + handleModifiedMessagesInConversationReducer } from './search-slice-reducers'; +import { extractIds, isItemInSearches } from './utils'; export const getSearchSliceInitialiState = (): SearchesStateType => ({ searchResults: undefined, + searchResultsIds: [], conversations: [], messages: [], more: false, offset: 0, + limit: 500, sortBy: 'dateDesc', query: '', status: 'empty', @@ -63,6 +67,7 @@ const fetchSearchesFulfilled = ( state.offset = (meta?.arg.offset ?? 0) + 100; state.sortBy = meta?.arg.sortBy ?? 'dateDesc'; state.error = undefined; + state.searchResultsIds = extractIds(payload) ?? []; } }; @@ -81,28 +86,50 @@ const msgActionFulfilled = ( state: SearchesStateType, { meta }: { meta: { arg: ConvActionParameters; requestId: string; requestStatus: string } } ): void => { - if (meta.arg.ids) { - state.error = undefined; - state.conversations = state.conversations - ? map(state.conversations, (conv) => ({ - ...conv, - messages: map(conv.messages, (msg) => { - if (includes(meta.arg.ids, msg.id)) { - return { ...msg, parent: FOLDERS.TRASH }; - } - return msg; - }) - })) - : []; - state.messages = state.messages - ? map(state.messages, (msg) => { - if (includes(meta.arg.ids, msg.id)) { - return { ...msg, parent: FOLDERS.TRASH }; - } - return msg; - }) - : []; + const itemIsInSearches = isItemInSearches({ + ids: meta.arg.ids, + searchResultsIds: state.searchResultsIds + }); + + if (!itemIsInSearches) { + return; } + const { ids, operation } = meta.arg; + state.error = undefined; + forEach(ids, (id) => { + const message = state?.messages?.[id]; + const { conversations } = state; + + if (operation.includes(CONVACTIONS.FLAG)) { + message.flagged = !operation.startsWith('!'); + if (conversations) conversations[id].flagged = !operation.startsWith('!'); + } + + if (operation.includes(CONVACTIONS.MARK_READ)) { + message.read = !operation.startsWith('!'); + if (conversations) conversations[id].read = !operation.startsWith('!'); + } + + if (operation === CONVACTIONS.TRASH) { + message.parent = FOLDERS.TRASH; + } + + if (operation === CONVACTIONS.DELETE) { + delete message[id]; + } + + if (operation === CONVACTIONS.MOVE) { + message.parent = meta.arg.parent; + } + + if (operation === CONVACTIONS.MARK_SPAM) { + message.parent = FOLDERS.SPAM; + } + + if (operation === CONVACTIONS.MARK_NOT_SPAM) { + message.parent = FOLDERS.INBOX; + } + }); }; export const searchesSlice = createSlice({ diff --git a/src/store/sync/conversation.ts b/src/store/sync/conversation.ts index 907517cf9..5f2e5c95a 100644 --- a/src/store/sync/conversation.ts +++ b/src/store/sync/conversation.ts @@ -5,7 +5,7 @@ */ import { FOLDERS } from '@zextras/carbonio-shell-ui'; import { filter, find, forEach, map, merge, omit, reduce, some, last, sortBy } from 'lodash'; -import { ConvMessage, ConversationsStateType, Payload } from '../../types'; +import type { ConvMessage, ConversationsStateType, Payload } from '../../types'; export const handleCreatedConversationsReducer = ( state: ConversationsStateType, diff --git a/src/store/sync/folder.ts b/src/store/sync/folder.ts index 8fbb9156b..f577f44a4 100644 --- a/src/store/sync/folder.ts +++ b/src/store/sync/folder.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { map } from 'lodash'; -import { FolderType, FoldersStateType } from '../../types'; +import type { FolderType, FoldersStateType } from '../../types'; import { extractFolders, normalizeFolder } from '../../views/sidebar/utils'; import { addFoldersToStore, removeFoldersFromStore, updateFolders } from '../utils'; diff --git a/src/store/sync/message.ts b/src/store/sync/message.ts index 97d1740dd..acf025a7b 100644 --- a/src/store/sync/message.ts +++ b/src/store/sync/message.ts @@ -10,7 +10,7 @@ import { getUserSettings, replaceHistory } from '@zextras/carbonio-shell-ui'; -import { NotificationConfig } from '@zextras/carbonio-shell-ui/types/notification'; +import type { NotificationConfig } from '@zextras/carbonio-shell-ui/types/notification'; import { forEach, pick, @@ -26,7 +26,7 @@ import { } from 'lodash'; import { MAILS_ROUTE } from '../../constants'; import { normalizeMailMessageFromSoap } from '../../normalizations/normalize-message'; -import { SoapIncompleteMessage, MsgStateType, IncompleteMessage, Payload } from '../../types'; +import type { SoapIncompleteMessage, MsgStateType, IncompleteMessage, Payload } from '../../types'; const triggerNotification = (m: Array): void => { const { props, prefs } = getUserSettings(); diff --git a/src/store/tests/utils.test.js b/src/store/tests/utils.test.js new file mode 100644 index 000000000..3ac6cf130 --- /dev/null +++ b/src/store/tests/utils.test.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { extractIdsFromMessagesAndConversations } from '../utils'; + +// write a unit test for function extractIdsFromMessagesAndConversations +// and make it pass +describe('extractIdsFromMessagesAndConversations', () => { + test('should return an empty array when no messages or conversations are passed', () => { + const result = extractIdsFromMessagesAndConversations(); + expect(result).toEqual([]); + }); +}); diff --git a/src/store/utils.ts b/src/store/utils.ts index 1480ede2d..54c096ac4 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -3,8 +3,15 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { max, map, filter, find, reduce, some, merge, isNil, omitBy } from 'lodash'; -import { FolderType, MailsFolderMap, FoldersStateType } from '../types'; +import { filter, find, includes, isNil, map, max, merge, omitBy, reduce, some } from 'lodash'; +import type { + Conversation, + FetchConversationsReturn, + FoldersStateType, + FolderType, + MailMessage, + MailsFolderMap +} from '../types'; export function findDepth(subFolder: FolderType, depth = 1): number { if (subFolder && subFolder.items && subFolder.items.length) { @@ -19,7 +26,7 @@ export function calcFolderItems( id: string ): FolderType[] { return map( - filter(folders, (item) => item.parent === id), + filter(folders, (item: FolderType) => item.parent === id), (item) => ({ ...item, items: calcFolderItems(folders, subFolders, item.id), @@ -128,3 +135,50 @@ export function removeFoldersFromStore(state: FoldersStateType, idsToDelete: any {} as MailsFolderMap ); } + +/** + * Extracts all ids from conversations and messages + * @param items conversations or messages + * @returns array of ids + */ +export function extractIdsFromMessagesAndConversations( + items: Record | Record | undefined +): Array { + return Object.keys(items ?? []).reduce((acc: Array, itemId) => { + const item = items?.[itemId]; + item && acc.push(itemId); + if (item && 'messages' in item) { + acc.push(...item.messages.map((msg) => msg.id)); + } + return acc; + }, []); +} + +/** + * Extracts all ids from conversations and messages from fetchConversations payload + * @param payload payload from fetchConversations + * @returns array of ids + */ +export function extractIds(payload: FetchConversationsReturn | undefined): Array { + const ids = extractIdsFromMessagesAndConversations(payload?.conversations); + ids.push(...extractIdsFromMessagesAndConversations(payload?.messages)); + return ids; +} + +/** + * Checks if all items are in search results ids + * @param ids ids to check + * @param searchResultsIds ids alread present in search results + * @returns boolean + */ +export const isItemInSearches = ({ + ids, + searchResultsIds +}: { + ids: Array; + searchResultsIds: Array; +}): boolean => + !includes( + map(ids, (id) => searchResultsIds.includes(id)), + false + ); diff --git a/src/tests/constants.ts b/src/tests/constants.ts new file mode 100644 index 000000000..b4e74b75a --- /dev/null +++ b/src/tests/constants.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2023 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { FOLDERS } from '@zextras/carbonio-shell-ui'; + +export const MSG_CONV_STATUS_DESCRIPTORS = { + FLAGGED: { + value: true, + desc: 'flagged' + }, + NOT_FLAGGED: { + value: false, + desc: 'not flagged' + }, + READ: { + value: true, + desc: 'read' + }, + NOT_READ: { + value: false, + desc: 'not read' + } +}; + +export const FOLDERS_DESCRIPTORS = { + INBOX: { + id: FOLDERS.INBOX, + desc: 'inbox' + }, + SENT: { + id: FOLDERS.SENT, + desc: 'sent' + }, + DRAFTS: { + id: FOLDERS.DRAFTS, + desc: 'drafts' + }, + SPAM: { + id: FOLDERS.SPAM, + desc: 'junk' + }, + TRASH: { + id: FOLDERS.TRASH, + desc: 'trash' + }, + USER_DEFINED: { + id: '1234567', + desc: 'user defined' + } +}; + +export const CONTAIN_ASSERTION = { + CONTAIN: { + value: true, + desc: 'contain' + }, + NOT_CONTAIN: { + value: false, + desc: 'not contain' + } +}; + +export const VISIBILITY_ASSERTION = { + IS_VISIBLE: { + value: true, + desc: 'is visible' + }, + IS_NOT_VISIBLE: { + value: false, + desc: 'is not visible' + } +}; diff --git a/src/tests/generators/editors.ts b/src/tests/generators/editors.ts index 0727b5301..0f0dce441 100644 --- a/src/tests/generators/editors.ts +++ b/src/tests/generators/editors.ts @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { MailsEditor } from '../../types'; +import type { MailsEditor } from '../../types'; export const generateEditorCase = async (id: number): Promise => { const { editorCase } = await import(`./editorCases/editorCase-${id}`); diff --git a/src/tests/generators/generateConversation.ts b/src/tests/generators/generateConversation.ts new file mode 100644 index 000000000..8f46a0523 --- /dev/null +++ b/src/tests/generators/generateConversation.ts @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2023 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { faker } from '@faker-js/faker'; +import { FOLDERS } from '@zextras/carbonio-shell-ui'; +import { times } from 'lodash'; +import { + ParticipantRole, + ParticipantRoleType +} from '../../carbonio-ui-commons/constants/participants'; +import { collectParticipantsFromMessages } from '../../helpers/messages'; +import type { Conversation, MailMessage, Participant } from '../../types'; +import { generateMessage } from './generateMessage'; + +/** + * + */ +type ConversationGenerationParams = { + id?: string; + folderId?: string; + from?: Array; + to?: Array; + cc?: Array; + receiveDate?: number; + subject?: string; + isRead?: boolean; + isFlagged?: boolean; + isSingleMessageConversation?: boolean; + messages?: Array; + messageGenerationCount?: number; +}; + +/** + * + */ +const generateRandomParticipants = (count: number, type: ParticipantRoleType): Array => + times(count, () => ({ + type, + address: faker.internet.email() + })); + +/** + * + * @param id + * @param folderId + * @param receiveDate + * @param to + * @param cc + * @param from + * @param subject + * @param isRead + * @param isFlagged + * @param isSingleMessageConversation + * @param messages + * @param messageGenerationCount + */ +const generateConversation = ({ + id = faker.datatype.number().toString(), + folderId = FOLDERS.INBOX, + receiveDate = faker.date.recent(1).valueOf(), + to, + cc, + from, + subject = faker.lorem.word(6), + isRead = false, + isFlagged = false, + messages, + messageGenerationCount = 1 +}: ConversationGenerationParams): Conversation => { + const finalFrom = + from ?? + (messages && messages.length + ? collectParticipantsFromMessages(messages, ParticipantRole.FROM) + : generateRandomParticipants(messageGenerationCount, ParticipantRole.FROM)); + const finalTo = + to ?? + (messages && messages.length + ? collectParticipantsFromMessages(messages, ParticipantRole.TO) + : generateRandomParticipants(messageGenerationCount, ParticipantRole.TO)); + const finalCc = + cc ?? + (messages && messages.length + ? collectParticipantsFromMessages(messages, ParticipantRole.CARBON_COPY) + : generateRandomParticipants(messageGenerationCount, ParticipantRole.CARBON_COPY)); + const finalMessages = + messages ?? times(messageGenerationCount, () => generateMessage({ folderId })); + + const result = { + date: receiveDate, + flagged: isFlagged, + fragment: '', + hasAttachment: false, + id, + parent: folderId, + participants: [...finalFrom, ...finalTo, ...finalCc], + read: isRead, + subject, + tags: [], + urgent: false, + messages: finalMessages + }; + + return result; +}; + +export { ConversationGenerationParams, generateConversation }; diff --git a/src/tests/generators/generateMessage.ts b/src/tests/generators/generateMessage.ts index c61aa0d52..d3800e2ea 100644 --- a/src/tests/generators/generateMessage.ts +++ b/src/tests/generators/generateMessage.ts @@ -7,13 +7,8 @@ import { faker } from '@faker-js/faker'; import { FOLDERS } from '@zextras/carbonio-shell-ui'; import { ParticipantRole } from '../../carbonio-ui-commons/constants/participants'; -import { MailMessage, Participant } from '../../types'; - -/** - * - * @param date - */ -const toUnixTimestamp = (date: Date): number => Math.floor(date.getTime() / 1000); +import { convertHtmlToPlainText } from '../../carbonio-ui-commons/utils/text/html'; +import type { MailMessage, Participant } from '../../types'; /** * @@ -29,6 +24,7 @@ type MessageGenerationParams = { subject?: string; body?: string; isRead?: boolean; + isFlagged?: boolean; isComplete?: boolean; isDeleted?: boolean; isDraft?: boolean; @@ -43,24 +39,38 @@ type MessageGenerationParams = { /** * * @param id - * @param folder + * @param folderId * @param sendDate * @param receiveDate - * @param participants + * @param to + * @param cc + * @param from * @param subject * @param body + * @param isRead + * @param isFlagged + * @param isComplete + * @param isDeleted + * @param isDraft + * @param isForwarded + * @param isInvite + * @param isReadReceiptRequested + * @param isReplied + * @param isScheduled + * @param isSentByMe */ const generateMessage = ({ id = faker.datatype.number().toString(), folderId = FOLDERS.INBOX, - sendDate = toUnixTimestamp(faker.date.recent(2)), - receiveDate = toUnixTimestamp(faker.date.recent(1)), + sendDate = faker.date.recent(2).valueOf(), + receiveDate = faker.date.recent(1).valueOf(), to = [{ type: ParticipantRole.TO, address: faker.internet.email() }], cc = [], from = { type: ParticipantRole.FROM, address: faker.internet.email() }, subject = faker.lorem.word(6), body = faker.lorem.paragraph(4), isRead = false, + isFlagged = false, isComplete = false, isDeleted = false, isDraft = false, @@ -77,8 +87,8 @@ const generateMessage = ({ conversation: '', date: receiveDate, did: '', - flagged: false, - fragment: '', + flagged: isFlagged, + fragment: convertHtmlToPlainText(body).substring(0, 40), hasAttachment: false, id, invite: undefined, diff --git a/src/tests/generators/getMsgResponse.ts b/src/tests/generators/getMsgResponse.ts index f7be4dcd8..d7757807e 100644 --- a/src/tests/generators/getMsgResponse.ts +++ b/src/tests/generators/getMsgResponse.ts @@ -7,7 +7,7 @@ import { faker } from '@faker-js/faker'; import { FOLDERS, SoapResponse } from '@zextras/carbonio-shell-ui'; import { ParticipantRole } from '../../carbonio-ui-commons/constants/participants'; -import { GetMsgResponse, Participant, SoapMailParticipant } from '../../types'; +import type { GetMsgResponse, Participant, SoapMailParticipant } from '../../types'; /** * diff --git a/src/tests/generators/store.ts b/src/tests/generators/store.ts index 895a0bc4f..588277139 100644 --- a/src/tests/generators/store.ts +++ b/src/tests/generators/store.ts @@ -13,7 +13,7 @@ import { getFoldersSliceInitialState } from '../../store/folders-slice'; import { getMessagesSliceInitialState } from '../../store/messages-slice'; import { storeReducers } from '../../store/reducers'; import { getSearchSliceInitialiState } from '../../store/searches-slice'; -import { StateType } from '../../types'; +import type { StateType } from '../../types'; /** * diff --git a/src/types/actions/index.d.ts b/src/types/actions/index.d.ts index 157dffcd3..62c8c9129 100644 --- a/src/types/actions/index.d.ts +++ b/src/types/actions/index.d.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { TagActionItemType } from '../tags'; + export type ActionProps = { folder: Folder; grant: Grant; @@ -11,3 +13,28 @@ export type ActionProps = { onMouseLeave: () => void; onMouseEnter: () => void; }; + +export type MessageActionReturnType = { + id: string; + icon: string; + label: string; + onClick: (ev: KeyboardEvent | SyntheticEvent) => void; + items?: ItemType[]; + customComponent?: React.ReactElement; +}; + +export type ConvActionReturnType = { + id: string; + icon: string; + label: string; + disabled?: boolean; + onClick: (ev: KeyboardEvent | SyntheticEvent) => void; + customComponent?: JSX.Element; + items?: ItemType[]; +}; + +export type ActionReturnType = + | false + | MessageActionReturnType + | ConvActionReturnType + | TagActionItemType; diff --git a/src/types/conversations/index.d.ts b/src/types/conversations/index.d.ts index d4082b223..ce62bc95e 100644 --- a/src/types/conversations/index.d.ts +++ b/src/types/conversations/index.d.ts @@ -16,8 +16,6 @@ export type ConvMessage = { export type Conversation = { readonly id: string; date: number; - msgCount: number; - unreadMsgCount: number; messages: Array; participants: Participant[]; subject: string; @@ -59,6 +57,7 @@ export type FetchConversationsReturn = { messages?: Record; hasMore: boolean; types: string; + Detail: { Error: { Code: string; Message: string } }; }; export type DeleteAttachmentsReturn = { diff --git a/src/types/folder/index.d.ts b/src/types/folder/index.d.ts index d489295f9..1f903a292 100644 --- a/src/types/folder/index.d.ts +++ b/src/types/folder/index.d.ts @@ -42,24 +42,23 @@ export type GrantType = { gt: string; perm: string; zid: string; d?: string }; export type SenderNameProps = { item: Conversation | IncompleteMessage; - isFromSearch?: boolean; + isSearchModule?: boolean; textValues?: TextReadValuesProps; + folderId?: string; }; export type MessageListItemProps = { - item: IncompleteMessage & { isFromSearch?: boolean }; - folderId: string; + item: IncompleteMessage & { isSearchModule?: boolean }; selected: boolean; selecting: boolean; - toggle?: () => void; - draggedIds: Record; - setDraggedIds: (ids: Record) => void; - setIsDragging: (isDragging: boolean) => void; - selectedItems: Record; - dragImageRef?: React.RefObject; + toggle: (id: string) => void; visible: boolean; isConvChildren: boolean; active?: boolean; + isSearchModule?: boolean; + isConversation?: boolean; + deselectAll: () => void; + currentFolderId?: string; }; export type TextReadValuesType = { @@ -78,15 +77,14 @@ export type MsgListDraggableItemType = { }; export type ListItemActionWrapperProps = { children?: ReactNode; - current?: boolean; onClick?: ContainerProps['onClick']; onDoubleClick?: ContainerProps['onDoubleClick']; messagesToRender?: Array; hoverTooltipLabel?: string; -} & ( - | { isConversation: true; item: Conversation } - | { isConversation?: false; item: IncompleteMessage } -); + active?: boolean; + item: Conversation | MailMessage; + deselectAll: () => void; +}; export type ItemAvatarType = { item: any; @@ -94,10 +92,9 @@ export type ItemAvatarType = { selecting: boolean; toggle: (arg: string) => void; folderId: string; - isSearch?: boolean; }; -export type CustomListItem = Partial & { id: string; isFromSearch?: boolean }; +export type CustomListItem = Partial & { id: string; isSearchModule?: boolean }; export type ConversationMessagesListProps = { active: string; @@ -105,5 +102,24 @@ export type ConversationMessagesListProps = { messages: Array; folderId: string; length: number; - isFromSearch?: boolean; + isSearchModule?: boolean; + dragImageRef?: React.RefObject; +}; + +export type ConversationListItemProps = { + item: Conversation; + selected: boolean; + selecting: boolean; + toggle: (id: string) => void; + visible?: boolean; + isConvChildren: boolean; + active?: boolean; + isSearchModule?: boolean; + activeItemId: string; + dragImageRef?: React.RefObject; + setDraggedIds?: (ids: Record) => void; + draggedIds?: Record | undefined; + selectedItems?: Record; + deselectAll: () => void; + folderId?: string; }; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index f706d8768..1182b3e87 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -4,8 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +export * from '../carbonio-ui-commons/types'; export * from './actions'; export * from './conversations'; +export * from './details-pannel'; export * from './editor'; export * from './extra-windows'; export * from './filters'; @@ -13,11 +15,9 @@ export * from './folder'; export * from './messages'; export * from './participant'; export * from './search'; +export * from './settings'; export * from './share'; export * from './soap'; export * from './state'; export * from './tags'; export * from './utils'; -export * from './settings'; -export * from './details-pannel'; -export * from '../carbonio-ui-commons/types'; diff --git a/src/types/search/index.d.ts b/src/types/search/index.d.ts index 9c04ec1f4..805491a4e 100644 --- a/src/types/search/index.d.ts +++ b/src/types/search/index.d.ts @@ -56,12 +56,6 @@ export type SearchPanelProps = { query: Array; }; -export type SearchMessageListItemProps = { - item: IncompleteMessage; - isConvChildren?: boolean; - active?: boolean; -}; - export type SearchConversationListItemProps = { itemId?: string; item: Conversation; @@ -72,7 +66,6 @@ export type SearchConversationListItemProps = { }; export type AdvancedFilterModalProps = { - id: string; open: boolean; onClose: () => void; query: Array<{ diff --git a/src/types/share/index.d.ts b/src/types/share/index.d.ts index 8d8c97b48..d37a8779d 100644 --- a/src/types/share/index.d.ts +++ b/src/types/share/index.d.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Dispatch } from '@reduxjs/toolkit'; import { Grant } from '@zextras/carbonio-shell-ui'; +import type { AppDispatch } from '../../store/redux'; import { Participant } from '../participant'; export type ShareCalendarModalProps = { openModal: () => void; setModal: (a: any) => void; - dispatch: Dispatch; + dispatch: AppDispatch; t: (...args: any[]) => string; toggleSnackbar: () => void; folder: string; @@ -20,9 +20,8 @@ export type ShareCalendarModalProps = { }; export type ResponseActionsProps = { - dispatch: Dispatch; + dispatch: AppDispatch; t: (...args: any[]) => string; - createSnackbar: any; zid: string; view: string; rid: string; diff --git a/src/types/sidebar/index.d.ts b/src/types/sidebar/index.d.ts index 4484aa971..72c06f946 100644 --- a/src/types/sidebar/index.d.ts +++ b/src/types/sidebar/index.d.ts @@ -89,18 +89,6 @@ export type FolderActionsProps = { disabled?: boolean; }; -export type DragEnterAction = - | undefined - | { - success: false; - }; - -export type OnDropActionProps = { - event: React.DragEvent; - type: string; - data: DataProps; -}; - export type ShareRevokeModalType = { folder: Folder; onClose?: () => void; diff --git a/src/types/state/index.d.ts b/src/types/state/index.d.ts index 8624c814e..32bc1bb01 100644 --- a/src/types/state/index.d.ts +++ b/src/types/state/index.d.ts @@ -45,6 +45,7 @@ export type ConversationsStateType = { export type SearchesStateType = { searchResults: any; + searchResultsIds: Array; conversations?: Record; messages?: Record>; more: boolean; diff --git a/src/types/tags/index.d.ts b/src/types/tags/index.d.ts index 0a5be0ef6..cfc161145 100644 --- a/src/types/tags/index.d.ts +++ b/src/types/tags/index.d.ts @@ -6,9 +6,16 @@ import { ItemType as AccordionItemType } from '@zextras/carbonio-design-system'; import { Tag } from '@zextras/carbonio-shell-ui'; -import React, { ComponentType, SyntheticEvent } from 'react'; +import React, { ComponentType } from 'react'; -export type ReturnType = { +export type TagActionItemType = { + id: string; + items: ItemType[]; + customComponent: ReactElement; + onClick?: (ev: KeyboardEvent | SyntheticEvent) => void; +}; + +export type TagActionsReturnType = { id: string; icon: string; label: string; @@ -25,8 +32,7 @@ export type TagsFromStoreType = Record; export type ArgumentType = { createModal?: (...args: any) => () => void; - createSnackbar?: (...args: any) => void; - items?: ReturnType; + items?: TagActionsReturnType; tag?: ItemType; }; diff --git a/src/types/utils/index.d.ts b/src/types/utils/index.d.ts index 6d174524f..96f367ef7 100644 --- a/src/types/utils/index.d.ts +++ b/src/types/utils/index.d.ts @@ -6,17 +6,6 @@ import { TextProps } from '@zextras/carbonio-design-system'; import { Folder } from '@zextras/carbonio-shell-ui'; -export type CreateSnackbar = (arg: { - key: string; - replace?: boolean; - type: string; - hideButton?: boolean; - label: string; - autoHideTimeout: number; - actionLabel?: string; - onActionClick?: () => void; -}) => void; - export type ModalProps = { folder: Folder; onClose: () => void; @@ -30,8 +19,6 @@ export type Crumb = { export type DataProps = { id: string; date: number; - msgCount: number; - unreadMsgCount: number; messages: [ { id: string; @@ -71,7 +58,7 @@ export type TextReadValuesProps = { export type AppContext = { isMessageView: boolean; count: number; - setCount: (arg: number) => void; + setCount: (arg: number | ((prevState: number) => number)) => void; }; export type BoardContext = { @@ -131,3 +118,12 @@ export type GetAttachmentsDownloadLinkProps = { messageSubject: string; attachments: Array; }; + +export type DragItemWrapperProps = { + item: IncompleteMessage; + selectedIds: Array; + selectedItems: Record; + setDraggedIds: (ids: Record) => void; + dragImageRef: React.RefObject | undefined; + dragAndDropIsDisabled: boolean; +}; diff --git a/src/ui-actions/conversation-actions.tsx b/src/ui-actions/conversation-actions.tsx index 256e2dabc..6961290cb 100644 --- a/src/ui-actions/conversation-actions.tsx +++ b/src/ui-actions/conversation-actions.tsx @@ -3,25 +3,17 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import React from 'react'; -import { - FOLDERS, - replaceHistory, - t, - getBridgedFunctions, - Tags, - Account -} from '@zextras/carbonio-shell-ui'; +import { Account, getBridgedFunctions, replaceHistory, t } from '@zextras/carbonio-shell-ui'; import { forEach, isArray, map } from 'lodash'; -import { Dispatch } from '@reduxjs/toolkit'; +import React from 'react'; +import { errorPage } from '../commons/preview-eml/error-page'; +import { getContentForPrint } from '../commons/print-conversation'; +import { ConversationActionsDescriptors } from '../constants'; import { convAction, getMsgsForPrint } from '../store/actions'; +import { AppDispatch, StoreProvider } from '../store/redux'; +import type { ConvActionReturnType, Conversation, MailMessage } from '../types'; import DeleteConvConfirm from './delete-conv-modal'; import MoveConvMessage from './move-conv-msg'; -import { getContentForPrint } from '../commons/print-conversation'; -import { applyTag } from './tag-actions'; -import { StoreProvider } from '../store/redux'; -import { Conversation, MailMessage } from '../types'; -import { errorPage } from '../commons/preview-eml/error-page'; type ConvActionIdsType = Array; type ConvActionValueType = string | boolean; @@ -31,7 +23,7 @@ type ConvActionPropType = { ids: ConvActionIdsType; id: string | ConvActionIdsType; value: ConvActionValueType; - dispatch: Dispatch; + dispatch: AppDispatch; folderId: string; shouldReplaceHistory: boolean; deselectAll: DeselectAllType; @@ -42,28 +34,20 @@ type ConvActionPropType = { disabled: boolean; }; -type ConvActionReturnType = { - id: string; - icon: string; - label: string; - disabled?: boolean; - onClick: (ev: MouseEvent) => void; - customComponent?: JSX.Element; -}; - export function setConversationsFlag({ ids, value, dispatch }: Pick): ConvActionReturnType { + const actDescriptor = value + ? ConversationActionsDescriptors.UNFLAG.id + : ConversationActionsDescriptors.FLAG.id; return { - id: 'flag-conversation', + id: actDescriptor, icon: value ? 'Flag' : 'FlagOutline', - label: value ? t('action.unflag', 'Remove flag') : t('action.flag', 'Add flag'), + label: value ? t('action.unflag', 'Add flag') : t('action.flag', 'Remove flag'), onClick: (): void => { dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore convAction({ operation: `${value ? '!' : ''}flag`, ids @@ -85,8 +69,6 @@ export function setMultipleConversationsFlag({ disabled, onClick: (): void => { dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore convAction({ operation: `flag`, ids @@ -108,8 +90,6 @@ export function unSetMultipleConversationsFlag({ disabled, onClick: (): void => { dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore convAction({ operation: `!flag`, ids @@ -130,23 +110,21 @@ export function setConversationsRead({ ConvActionPropType, 'ids' | 'dispatch' | 'value' | 'folderId' | 'shouldReplaceHistory' | 'deselectAll' >): ConvActionReturnType { - // eslint-disable-next-line react-hooks/rules-of-hooks + const actDescriptor = value + ? ConversationActionsDescriptors.MARK_AS_UNREAD.id + : ConversationActionsDescriptors.MARK_AS_READ.id; return { - id: `read-conversations-${value}`, + id: actDescriptor, icon: value ? 'EmailOutline' : 'EmailReadOutline', label: value ? t('action.mark_as_unread', 'Mark as unread') : t('action.mark_as_read', 'Mark as read'), onClick: (): void => { dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore convAction({ operation: `${value ? '!' : ''}read`, ids }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ).then((res) => { deselectAll && deselectAll(); if (res.type.includes('fulfilled') && shouldReplaceHistory) { @@ -173,9 +151,7 @@ export function printConversation({ }); }); } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - messageIds = map(conversation?.messages, (m) => m.id); + messageIds = map((conversation as Conversation)?.messages, (m) => m.id); } return { @@ -239,15 +215,10 @@ export function setConversationsSpam({ setTimeout((): void => { if (notCanceled) { dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore convAction({ operation: `${value ? '!' : ''}spam`, - // operation: `spam`, ids }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ).then((res) => { if (res.type.includes('fulfilled')) { deselectAll(); @@ -276,23 +247,20 @@ export function moveConversationToTrash({ ConvActionPropType, 'ids' | 'dispatch' | 'folderId' | 'deselectAll' >): ConvActionReturnType { + const actDescriptor = ConversationActionsDescriptors.MOVE_TO_TRASH.id; + return { - id: 'trash-conversations', + id: actDescriptor, icon: 'Trash2Outline', label: t('label.delete', 'Delete'), - // first click, delete email onClick: (): void => { const restoreConversation = (): void => { dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore convAction({ operation: `move`, ids, parent: folderId }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ).then((res) => { if (res.type.includes('fulfilled')) { deselectAll(); @@ -318,14 +286,10 @@ export function moveConversationToTrash({ }); }; dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore convAction({ operation: `trash`, ids }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ).then((res) => { if (res.type.includes('fulfilled')) { deselectAll(); @@ -337,9 +301,7 @@ export function moveConversationToTrash({ actionLabel: t('label.undo', 'Undo'), label: t('snackbar.email_moved_to_trash', 'E-mail moved to Trash'), autoHideTimeout: 5000, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - onActionClick: (): void => restoreConversation(ids, dispatch, deselectAll) + onActionClick: (): void => restoreConversation() }); } else { getBridgedFunctions()?.createSnackbar({ @@ -425,207 +387,3 @@ export function deleteConversationPermanently({ } }; } - -type GetConvActionsType = { - folderId: string; - dispatch: Dispatch; - deselectAll: () => void; - account: Account; - tags: Tags; -}; -export const getActions = ({ - folderId, - dispatch, - deselectAll, - account, - tags -}: GetConvActionsType): any => { - switch (folderId) { - case FOLDERS.TRASH: - return (conversation: Conversation): Array => [ - [setConversationsFlag({ ids: [conversation.id], value: conversation.flagged, dispatch })], - [ - setConversationsRead({ - ids: [conversation.id], - value: conversation.read, - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - setConversationsFlag({ - ids: [conversation.id], - value: conversation.flagged, - - dispatch - }), - applyTag({ tags, conversation }), - setConversationsSpam({ - ids: [conversation.id], - value: false, - dispatch, - deselectAll - }), - printConversation({ - conversation: [conversation], - account - }), - moveConversationToFolder({ - ids: [conversation.id], - folderId, - dispatch, - isRestore: true, - deselectAll - }), - deleteConversationPermanently({ - ids: [conversation.id], - deselectAll - }) - ] - ]; - case FOLDERS.SPAM: - return (conversation: Conversation): Array => [ - [ - moveConversationToTrash({ - ids: [conversation.id], - dispatch, - deselectAll, - folderId - }), - setConversationsFlag({ ids: [conversation.id], value: conversation.flagged, dispatch }) - ], - [ - setConversationsRead({ - ids: [conversation.id], - value: conversation.read, - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - setConversationsFlag({ - ids: [conversation.id], - value: conversation.flagged, - dispatch - }), - applyTag({ tags, conversation }), - setConversationsSpam({ - ids: [conversation.id], - value: true, - - dispatch, - - deselectAll - }), - - printConversation({ - conversation: [conversation], - account - }), - moveConversationToTrash({ - ids: [conversation.id], - - dispatch, - - deselectAll, - folderId - }) - ] - ]; - - case FOLDERS.DRAFTS: - return (conversation: Conversation): Array => [ - [ - moveConversationToTrash({ - ids: [conversation.id], - - dispatch, - - deselectAll, - folderId - }), - setConversationsFlag({ ids: [conversation.id], value: conversation.flagged, dispatch }) - ], - [ - setConversationsFlag({ - ids: [conversation.id], - value: conversation.flagged, - - dispatch - }), - applyTag({ tags, conversation }), - moveConversationToTrash({ - ids: [conversation.id], - dispatch, - deselectAll, - folderId - }), - printConversation({ - conversation: [conversation], - account - }) - ] - ]; - case FOLDERS.INBOX: - case FOLDERS.SENT: - default: - return (conversation: Conversation): Array => [ - [ - setConversationsRead({ - ids: [conversation.id], - value: conversation.read, - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - moveConversationToTrash({ - ids: [conversation.id], - dispatch, - deselectAll, - folderId - }), - setConversationsFlag({ ids: [conversation.id], value: conversation.flagged, dispatch }) - ], - [ - applyTag({ tags, conversation }), - setConversationsRead({ - ids: [conversation.id], - value: conversation.read, - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - setConversationsFlag({ - ids: [conversation.id], - value: conversation.flagged, - dispatch - }), - setConversationsSpam({ - ids: [conversation.id], - value: false, - dispatch, - deselectAll - }), - printConversation({ - conversation: [conversation], - account - }), - moveConversationToFolder({ - ids: [conversation.id], - folderId, - dispatch, - isRestore: false, - deselectAll - }), - moveConversationToTrash({ - ids: [conversation.id], - dispatch, - deselectAll, - folderId - }) - ] - ]; - } -}; diff --git a/src/ui-actions/delete-conv-modal.tsx b/src/ui-actions/delete-conv-modal.tsx index b04f01be0..dcd75d5f5 100644 --- a/src/ui-actions/delete-conv-modal.tsx +++ b/src/ui-actions/delete-conv-modal.tsx @@ -3,9 +3,9 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { Container, SnackbarManagerContext, Text } from '@zextras/carbonio-design-system'; -import { t } from '@zextras/carbonio-shell-ui'; -import React, { FC, useCallback, useContext } from 'react'; +import { Container, Text } from '@zextras/carbonio-design-system'; +import { getBridgedFunctions, t } from '@zextras/carbonio-shell-ui'; +import React, { FC, useCallback } from 'react'; import ModalFooter from '../carbonio-ui-commons/components/modals/modal-footer'; import ModalHeader from '../carbonio-ui-commons/components/modals/modal-header'; import { useAppDispatch } from '../hooks/redux'; @@ -24,49 +24,41 @@ const DeleteConvConfirm: FC = ({ onClose }) => { const dispatch = useAppDispatch(); - const createSnackbar = useContext(SnackbarManagerContext); const onConfirmConvDelete = useCallback(() => { dispatch( isMessageView ? msgAction({ - operation: `delete`, + operation: 'delete', ids: selectedIDs }) : convAction({ - operation: `delete`, + operation: 'delete', ids: selectedIDs }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore ).then((res) => { if (res.type.includes('fulfilled')) { deselectAll && deselectAll(); - createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `trash-${selectedIDs}`, replace: true, type: 'info', - label: isMessageView - ? t('label.email_perm_deleted', 'E-mail permanently deleted') - : t('label.email_perm_deleted', 'E-mail permanently deleted'), + label: t('label.email_perm_deleted', 'E-mail permanently deleted'), autoHideTimeout: 3000, hideButton: true }); } else { - createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `edit`, replace: true, type: 'error', - label: isMessageView - ? t('label.error_try_again', 'Something went wrong, please try again') - : t('label.error_try_again', 'Something went wrong, please try again'), + label: t('label.error_try_again', 'Something went wrong, please try again'), autoHideTimeout: 3000 }); } - // setOpenConfirm(false); onClose(); }); - }, [dispatch, isMessageView, selectedIDs, onClose, createSnackbar, deselectAll]); + }, [dispatch, isMessageView, selectedIDs, onClose, deselectAll]); return ( <> diff --git a/src/ui-actions/get-msg-conv-actions-functions.ts b/src/ui-actions/get-msg-conv-actions-functions.ts new file mode 100644 index 000000000..aa4a1ee51 --- /dev/null +++ b/src/ui-actions/get-msg-conv-actions-functions.ts @@ -0,0 +1,383 @@ +/* + * SPDX-FileCopyrightText: 2021 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Account, FOLDERS, Tags } from '@zextras/carbonio-shell-ui'; +import { AppDispatch } from '../store/redux'; +import type { ActionReturnType, Conversation, MailMessage } from '../types'; +import { + deleteConversationPermanently, + moveConversationToFolder, + moveConversationToTrash, + printConversation, + setConversationsFlag, + setConversationsRead, + setConversationsSpam +} from './conversation-actions'; +import { + deleteMessagePermanently, + editAsNewMsg, + editDraft, + forwardMsg, + moveMessageToFolder, + moveMsgToTrash, + printMsg, + redirectMsg, + replyAllMsg, + replyMsg, + sendDraft, + setMsgAsSpam, + setMsgFlag, + setMsgRead, + showOriginalMsg +} from './message-actions'; +import { applyTag } from './tag-actions'; +import { getSystemFolderParentId } from '../helpers/folders'; + +/** + * get the action to be executed when the user clicks on the "Mark as read/unread" button + * @param isConversation true if the item is a conversation + * @param id the id of the item + * @param item the item itself + * @param dispatch the dispatch function + * @param folderId the id of the folder where the item is located + * @param deselectAll the function to deselect all the items + * @param foldersExcludedMarkReadUnread the list of folders where the "Mark as read/unread" button is disabled + * @returns {function(): ActionReturnType} the action to be executed when the user clicks on the "Mark as read/unread" button + */ +export function getReadUnreadAction({ + isConversation, + id, + item, + dispatch, + folderId, + deselectAll, + foldersExcludedMarkReadUnread +}: { + isConversation: boolean; + id: string; + item: MailMessage | Conversation; + dispatch: AppDispatch; + folderId: string; + deselectAll: () => void; + foldersExcludedMarkReadUnread: string[]; +}): ActionReturnType { + const action = isConversation + ? setConversationsRead({ + ids: [id], + value: item.read, + dispatch, + folderId, + deselectAll, + shouldReplaceHistory: false + }) + : setMsgRead({ ids: [id], value: item.read, dispatch, folderId }); + return ( + !foldersExcludedMarkReadUnread.includes(getSystemFolderParentId(folderId) ?? '0') && action + ); +} + +export function getReplyAction( + isConversation: boolean, + isSingleMessageConversation: boolean, + firstConversationMessageId: string, + folderId: string, + id: string, + folderExcludedReply: string[] +): ActionReturnType { + const action = isConversation + ? isSingleMessageConversation && replyMsg({ id: firstConversationMessageId, folderId }) + : replyMsg({ id, folderId }); + return !folderExcludedReply.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getReplyAllAction({ + isConversation, + isSingleMessageConversation, + firstConversationMessageId, + folderId, + id, + folderExcludedReplyAll +}: { + isConversation: boolean; + isSingleMessageConversation: boolean; + firstConversationMessageId: string; + folderId: string; + id: string; + folderExcludedReplyAll: string[]; +}): ActionReturnType { + const action = isConversation + ? isSingleMessageConversation && replyAllMsg({ id: firstConversationMessageId, folderId }) + : replyAllMsg({ id, folderId }); + return !folderExcludedReplyAll.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getForwardAction({ + isConversation, + isSingleMessageConversation, + firstConversationMessageId, + folderId, + id, + folderExcludedForward +}: { + isConversation: boolean; + isSingleMessageConversation: boolean; + firstConversationMessageId: string; + folderId: string; + id: string; + folderExcludedForward: string[]; +}): ActionReturnType { + const action = isConversation + ? isSingleMessageConversation && forwardMsg({ id: firstConversationMessageId, folderId }) + : forwardMsg({ id, folderId }); + return !folderExcludedForward.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getMoveToTrashAction({ + isConversation, + id, + dispatch, + folderId, + deselectAll, + foldersExcludedTrash +}: { + isConversation: boolean; + id: string; + dispatch: AppDispatch; + folderId: string; + deselectAll: () => void; + foldersExcludedTrash: string[]; +}): ActionReturnType { + const action = isConversation + ? moveConversationToTrash({ ids: [id], dispatch, folderId, deselectAll }) + : moveMsgToTrash({ ids: [id], dispatch, deselectAll }); + return !foldersExcludedTrash.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getDeletePermanentlyAction({ + isConversation, + id, + deselectAll, + dispatch, + foldersIncludedDeletePermanently, + folderId +}: { + isConversation: boolean; + id: string; + deselectAll: () => void; + dispatch: AppDispatch; + foldersIncludedDeletePermanently: string[]; + folderId: string; +}): ActionReturnType { + const action = isConversation + ? deleteConversationPermanently({ ids: [id], deselectAll }) + : deleteMessagePermanently({ ids: [id], dispatch, deselectAll }); + return ( + foldersIncludedDeletePermanently.includes(getSystemFolderParentId(folderId) ?? '0') && action + ); +} + +export function getAddRemoveFlagAction({ + isConversation, + id, + item, + dispatch +}: { + isConversation: boolean; + id: string; + item: MailMessage | Conversation; + dispatch: AppDispatch; +}): ActionReturnType { + const action = isConversation + ? setConversationsFlag({ ids: [id], value: item.flagged, dispatch }) + : setMsgFlag({ ids: [id], value: item.flagged, dispatch }); + return action; +} + +export function getSendDraftAction({ + isConversation, + id, + item, + dispatch, + folderIncludedSendDraft, + folderId +}: { + isConversation: boolean; + id: string; + item: MailMessage | Conversation; + dispatch: AppDispatch; + folderIncludedSendDraft: string[]; + folderId: string; +}): ActionReturnType { + const action = isConversation ? false : sendDraft({ id, message: item as MailMessage, dispatch }); + return folderIncludedSendDraft.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getMarkRemoveSpam({ + isConversation, + id, + folderId, + dispatch, + deselectAll, + foldersExcludedMarkUnmarkSpam +}: { + isConversation: boolean; + id: string; + folderId: string; + dispatch: AppDispatch; + deselectAll: () => void; + foldersExcludedMarkUnmarkSpam: string[]; +}): ActionReturnType { + const action = isConversation + ? setConversationsSpam({ + ids: [id], + value: folderId === FOLDERS.SPAM, + dispatch, + deselectAll + }) + : setMsgAsSpam({ + ids: [id], + value: folderId === FOLDERS.SPAM, + dispatch, + folderId + }); + return ( + !foldersExcludedMarkUnmarkSpam.includes(getSystemFolderParentId(folderId) ?? '0') && action + ); +} + +export function getApplyTagAction({ + tags, + item, + isConversation, + foldersExcludedTags, + folderId +}: { + tags: Tags; + item: MailMessage | Conversation; + isConversation: boolean; + foldersExcludedTags: string[]; + folderId: string; +}): ActionReturnType { + const action = applyTag({ tags, conversation: item, isMessage: !isConversation }); + return ( + !foldersExcludedTags.includes(getSystemFolderParentId(folderId) ?? '0') && + (action as ActionReturnType) + ); +} + +export function getMoveToFolderAction({ + isConversation, + id, + dispatch, + folderId, + deselectAll +}: { + isConversation: boolean; + id: string; + dispatch: AppDispatch; + folderId: string; + deselectAll: () => void; +}): ActionReturnType { + const action = isConversation + ? moveConversationToFolder({ + ids: [id], + dispatch, + folderId, + isRestore: folderId === FOLDERS.TRASH, + deselectAll + }) + : moveMessageToFolder({ + id: [id], + folderId, + dispatch, + isRestore: folderId === FOLDERS.TRASH, + deselectAll + }); + return action; +} + +export function getPrintAction({ + isConversation, + item, + account, + folderExcludedPrintMessage, + folderId +}: { + isConversation: boolean; + item: MailMessage | Conversation; + account: Account; + folderExcludedPrintMessage: string[]; + folderId: string; +}): ActionReturnType { + const action = isConversation + ? printConversation({ + conversation: [item as Conversation], + account + }) + : printMsg({ message: item as MailMessage, account }); + return !folderExcludedPrintMessage.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getRedirectAction({ + isConversation, + id, + folderExcludedRedirect, + folderId +}: { + isConversation: boolean; + id: string; + folderExcludedRedirect: string[]; + folderId: string; +}): ActionReturnType { + const action = isConversation ? false : redirectMsg({ id }); + return !folderExcludedRedirect.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getEditDraftAction({ + isConversation, + id, + folderId, + folderIncludeEditDraft +}: { + isConversation: boolean; + id: string; + folderId: string; + folderIncludeEditDraft: string[]; +}): ActionReturnType { + const action = isConversation ? false : editDraft({ id, folderId }); + return folderIncludeEditDraft.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getEditAsNewAction({ + isConversation, + id, + folderId, + folderExcludedEditAsNew +}: { + isConversation: boolean; + id: string; + folderId: string; + folderExcludedEditAsNew: string[]; +}): ActionReturnType { + const action = isConversation ? false : editAsNewMsg({ id, folderId }); + return !folderExcludedEditAsNew.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} + +export function getShowOriginalAction({ + isConversation, + id, + folderExcludedShowOriginal, + folderId +}: { + isConversation: boolean; + id: string; + folderExcludedShowOriginal: string[]; + folderId: string; +}): ActionReturnType { + const action = isConversation ? false : showOriginalMsg({ id }); + + return !folderExcludedShowOriginal.includes(getSystemFolderParentId(folderId) ?? '0') && action; +} diff --git a/src/ui-actions/get-msg-conv-actions.ts b/src/ui-actions/get-msg-conv-actions.ts new file mode 100644 index 000000000..3a3f405f9 --- /dev/null +++ b/src/ui-actions/get-msg-conv-actions.ts @@ -0,0 +1,251 @@ +/* + * SPDX-FileCopyrightText: 2021 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Account, FOLDERS, Tags } from '@zextras/carbonio-shell-ui'; +import { AppDispatch } from '../store/redux'; +import type { ActionReturnType, Conversation, MailMessage } from '../types'; +import { + getAddRemoveFlagAction, + getApplyTagAction, + getDeletePermanentlyAction, + getEditAsNewAction, + getEditDraftAction, + getForwardAction, + getMarkRemoveSpam, + getMoveToFolderAction, + getMoveToTrashAction, + getPrintAction, + getReadUnreadAction, + getRedirectAction, + getReplyAction, + getReplyAllAction, + getSendDraftAction, + getShowOriginalAction +} from './get-msg-conv-actions-functions'; + +type GetMessageActionsProps = { + item: MailMessage | Conversation; + dispatch: AppDispatch; + deselectAll: () => void; + account: Account; + tags: Tags; +}; + +export type MsgConvActionsReturnType = [ + Array>, + Array> +]; + +export function getMsgConvActions({ + item, + dispatch, + deselectAll, + account, + tags +}: GetMessageActionsProps): MsgConvActionsReturnType { + const isConversation = 'messages' in (item || {}); + const folderId = isConversation ? (item as Conversation)?.messages?.[0].parent : item.parent; + const firstConversationMessageId = isConversation + ? (item as Conversation)?.messages?.[0]?.id + : item.id; + const isSingleMessageConversation = + isConversation && (item as Conversation).messages.length === 1; + const { id } = item; + + /** + * Folders where the actions are enabled or disabled + */ + const foldersExcludedMarkReadUnread = [FOLDERS.DRAFTS]; + const foldersExcludedTrash = [FOLDERS.TRASH]; + const foldersIncludedDeletePermanently = [FOLDERS.TRASH, FOLDERS.SPAM]; + const foldersExcludedTags = [FOLDERS.SPAM]; + const foldersExcludedMarkUnmarkSpam = [FOLDERS.DRAFTS]; + const folderExcludedPrintMessage = [FOLDERS.DRAFTS, FOLDERS.TRASH]; + const folderExcludedShowOriginal = [FOLDERS.DRAFTS, FOLDERS.TRASH]; + const folderIncludeEditDraft = [FOLDERS.DRAFTS]; + const folderExcludedReply = [FOLDERS.DRAFTS, FOLDERS.SPAM]; + const folderExcludedReplyAll = [FOLDERS.DRAFTS, FOLDERS.SPAM]; + const folderExcludedForward = [FOLDERS.DRAFTS, FOLDERS.SPAM]; + const folderExcludedEditAsNew = [FOLDERS.DRAFTS, FOLDERS.TRASH]; + const folderIncludedSendDraft = [FOLDERS.DRAFTS]; + const folderExcludedRedirect = [FOLDERS.DRAFTS, FOLDERS.TRASH]; + + const addRemoveFlagAction = getAddRemoveFlagAction({ isConversation, id, item, dispatch }); + + const msgReadUnreadAction = getReadUnreadAction({ + isConversation, + id, + item, + dispatch, + folderId, + deselectAll, + foldersExcludedMarkReadUnread + }); + + const moveToTrashAction = getMoveToTrashAction({ + isConversation, + id, + dispatch, + folderId, + deselectAll, + foldersExcludedTrash + }); + + const deletePermanentlyAction = getDeletePermanentlyAction({ + isConversation, + id, + deselectAll, + dispatch, + foldersIncludedDeletePermanently, + folderId + }); + + const moveToFolderAction = getMoveToFolderAction({ + isConversation, + id, + dispatch, + folderId, + deselectAll + }); + + const printAction = getPrintAction({ + isConversation, + item, + account, + folderExcludedPrintMessage, + folderId + }); + + const applyTagAction = getApplyTagAction({ + tags, + item, + isConversation, + foldersExcludedTags, + folderId + }); + + const markRemoveSpam = getMarkRemoveSpam({ + isConversation, + id, + folderId, + dispatch, + deselectAll, + foldersExcludedMarkUnmarkSpam + }); + + const showOriginalAction = getShowOriginalAction({ + isConversation, + id, + folderExcludedShowOriginal, + folderId + }); + + const editDraftAction = getEditDraftAction({ + isConversation, + id, + folderId, + folderIncludeEditDraft + }); + + const replyAction = getReplyAction( + isConversation, + isSingleMessageConversation, + firstConversationMessageId, + folderId, + id, + folderExcludedReply + ); + + const replyAllAction = getReplyAllAction({ + isConversation, + isSingleMessageConversation, + firstConversationMessageId, + folderId, + id, + folderExcludedReplyAll + }); + + const forwardAction = getForwardAction({ + isConversation, + isSingleMessageConversation, + firstConversationMessageId, + folderId, + id, + folderExcludedForward + }); + + const editAsNewAction = getEditAsNewAction({ + isConversation, + id, + folderId, + folderExcludedEditAsNew + }); + + const sendDraftAction = getSendDraftAction({ + isConversation, + id, + item, + dispatch, + folderIncludedSendDraft, + folderId + }); + + const redirectAction = getRedirectAction({ + isConversation, + id, + folderExcludedRedirect, + folderId + }); + + /** + * Primary actions are the ones that are shown when the user hovers over a message + * @returns an array of arrays of actions + */ + const primaryActions: Array> = [ + replyAction, + replyAllAction, + forwardAction, + moveToTrashAction, + deletePermanentlyAction, + msgReadUnreadAction, + addRemoveFlagAction + ].reduce((acc: Array>, action) => { + if (action) { + acc.push(action); + } + return acc; + }, []); + + /** + * Secondary actions are the ones that are shown when the user right-clicks on the message + * @returns an array of arrays of actions + */ + const secondaryActions: Array> = [ + replyAction, + replyAllAction, + forwardAction, + sendDraftAction, + moveToTrashAction, + deletePermanentlyAction, + msgReadUnreadAction, + addRemoveFlagAction, + markRemoveSpam, + applyTagAction, + moveToFolderAction, + printAction, + redirectAction, + editDraftAction, + editAsNewAction, + showOriginalAction + ].reduce((acc: Array>, action) => { + if (action) { + acc.push(action); + } + return acc; + }, []); + + return [primaryActions, secondaryActions]; +} diff --git a/src/ui-actions/mail-hover-bar.tsx b/src/ui-actions/mail-hover-bar.tsx index 24757a6a6..2b8ac6dfd 100644 --- a/src/ui-actions/mail-hover-bar.tsx +++ b/src/ui-actions/mail-hover-bar.tsx @@ -94,14 +94,14 @@ const MailHoverBar: FC = ({ return ( - {map(actions, (action: { icon: string; label: string; onClick: () => void }) => ( + {map(actions, (action) => ( { ev.preventDefault(); - action.onClick(); + action.onClick(ev); }} /> diff --git a/src/ui-actions/message-actions.tsx b/src/ui-actions/message-actions.tsx index f208028e0..8978ae30e 100644 --- a/src/ui-actions/message-actions.tsx +++ b/src/ui-actions/message-actions.tsx @@ -3,7 +3,7 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import React from 'react'; +import { AsyncThunkAction, Dispatch } from '@reduxjs/toolkit'; import { Text } from '@zextras/carbonio-design-system'; import { Account, @@ -11,33 +11,37 @@ import { FOLDERS, getBridgedFunctions, replaceHistory, - t, - Tags + t } from '@zextras/carbonio-shell-ui'; import { map, noop } from 'lodash'; -import { AsyncThunkAction, Dispatch } from '@reduxjs/toolkit'; -import { MAILS_ROUTE } from '../constants'; -import { getMsgsForPrint, msgAction } from '../store/actions'; +import React from 'react'; +import { errorPage } from '../commons/preview-eml/error-page'; +import { getContentForPrint } from '../commons/print-conversation'; import { ActionsType } from '../commons/utils'; +import { MAILS_ROUTE, MessageActionsDescriptors } from '../constants'; +import { getMsgsForPrint, msgAction } from '../store/actions'; import { sendMsg } from '../store/actions/send-msg'; -import MoveConvMessage from './move-conv-msg'; +import { AppDispatch, StoreProvider } from '../store/redux'; +import type { + BoardContext, + MailMessage, + MessageActionReturnType, + MsgActionParameters, + MsgActionResult +} from '../types'; import DeleteConvConfirm from './delete-conv-modal'; +import MoveConvMessage from './move-conv-msg'; import RedirectAction from './redirect-message-action'; -import { getContentForPrint } from '../commons/print-conversation'; -import { applyTag } from './tag-actions'; -import { BoardContext, MailMessage, MsgActionParameters, MsgActionResult } from '../types'; -import { StoreProvider } from '../store/redux'; -import { errorPage } from '../commons/preview-eml/error-page'; type MessageActionIdsType = Array; type MessageActionValueType = string | boolean; type DeselectAllType = () => void; -// type CloseEditorType = () => void; + type MessageActionPropType = { ids: MessageActionIdsType; id?: string | MessageActionIdsType; value?: MessageActionValueType; - dispatch: Dispatch; + dispatch: AppDispatch; folderId?: string; shouldReplaceHistory?: boolean; deselectAll?: DeselectAllType; @@ -47,12 +51,6 @@ type MessageActionPropType = { message?: MailMessage; }; -export type MessageActionReturnType = { - id: string; - icon: string; - label: string; - onClick: (ev?: MouseEvent) => void; -}; export const setMsgRead = ({ ids, value, @@ -60,31 +58,33 @@ export const setMsgRead = ({ folderId, shouldReplaceHistory = false, deselectAll -}: MessageActionPropType): MessageActionReturnType => ({ - id: 'message-mark_as_read', - icon: value ? 'EmailOutline' : 'EmailReadOutline', - label: value - ? t('action.mark_as_unread', 'Mark as unread') - : t('action.mark_as_read', 'Mark as read'), - onClick: (ev): void => { - if (ev) ev.preventDefault(); - dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - msgAction({ - operation: `${value ? '!' : ''}read`, - ids - }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - ).then((res) => { - deselectAll && deselectAll(); - if (res.type.includes('fulfilled') && shouldReplaceHistory) { - replaceHistory(`/folder/${folderId}`); - } - }); - } -}); +}: MessageActionPropType): MessageActionReturnType => { + const actDescriptor = value + ? MessageActionsDescriptors.MARK_AS_UNREAD + : MessageActionsDescriptors.MARK_AS_READ; + + return { + id: actDescriptor.id, + icon: value ? 'EmailOutline' : 'EmailReadOutline', + label: value + ? t('action.mark_as_unread', 'Mark as unread') + : t('action.mark_as_read', 'Mark as read'), + onClick: (ev): void => { + if (ev) ev.preventDefault(); + dispatch( + msgAction({ + operation: `${value ? '!' : ''}read`, + ids + }) + ).then((res) => { + deselectAll && deselectAll(); + if (res.type.includes('fulfilled') && shouldReplaceHistory) { + replaceHistory(`/folder/${folderId}`); + } + }); + } + }; +}; export function setMsgFlag({ ids, @@ -94,15 +94,15 @@ export function setMsgFlag({ MessageActionPropType, 'folderId' | 'shouldReplaceHistory' | 'deselectAll' >): MessageActionReturnType { + const actDescriptor = value ? MessageActionsDescriptors.UNFLAG : MessageActionsDescriptors.FLAG; + return { - id: 'message-flag', + id: actDescriptor.id, icon: value ? 'Flag' : 'FlagOutline', label: value ? t('action.unflag', 'Remove flag') : t('action.flag', 'Add flag'), onClick: (ev): void => { if (ev) ev.preventDefault(); dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore msgAction({ operation: `${value ? '!' : ''}flag`, ids @@ -112,6 +112,44 @@ export function setMsgFlag({ }; } +type SetAsSpamProps = { + notCanceled: boolean; + value: MessageActionValueType | undefined; + dispatch: AppDispatch; + ids: Array; + shouldReplaceHistory?: boolean; + folderId?: string; +}; +function setAsSpam({ + notCanceled, + value, + dispatch, + ids, + shouldReplaceHistory, + folderId +}: SetAsSpamProps): void { + if (!notCanceled) return; + dispatch( + msgAction({ + operation: `${value ? '!' : ''}spam`, + ids + }) + ).then((res) => { + if (res.type.includes('fulfilled') && shouldReplaceHistory) { + replaceHistory(`/folder/${folderId}`); + } + if (!res.type.includes('fulfilled')) { + getBridgedFunctions()?.createSnackbar({ + key: `trash-${ids}`, + replace: true, + type: 'error', + label: t('label.error_try_again', 'Something went wrong, please try again'), + autoHideTimeout: 3000 + }); + } + }); +} + export function setMsgAsSpam({ ids, value, @@ -119,8 +157,12 @@ export function setMsgAsSpam({ shouldReplaceHistory = true, folderId }: MessageActionPropType): MessageActionReturnType { + const actDescriptor = value + ? MessageActionsDescriptors.MARK_AS_NOT_SPAM + : MessageActionsDescriptors.MARK_AS_SPAM; + return { - id: 'message-mark_as_spam', + id: actDescriptor.id, icon: value ? 'AlertCircleOutline' : 'AlertCircle', label: value ? t('action.mark_as_non_spam', 'Not spam') @@ -140,38 +182,15 @@ export function setMsgAsSpam({ autoHideTimeout: 3000, hideButton, actionLabel: t('label.undo', 'Undo'), - onActionClick: () => { + onActiononClick: () => { notCanceled = false; } }); }; infoSnackbar(); setTimeout(() => { - if (notCanceled) { - dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - msgAction({ - operation: `${value ? '!' : ''}spam`, - // operation: `spam`, - ids - }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - ).then((res) => { - if (res.type.includes('fulfilled') && shouldReplaceHistory) { - replaceHistory(`/folder/${folderId}`); - } - if (!res.type.includes('fulfilled')) { - getBridgedFunctions()?.createSnackbar({ - key: `trash-${ids}`, - replace: true, - type: 'error', - label: t('label.error_try_again', 'Something went wrong, please try again'), - autoHideTimeout: 3000 - }); - } - }); - } + /** If the user has not clicked on the undo button, we can proceed with the action */ + setAsSpam({ notCanceled, value, dispatch, ids, shouldReplaceHistory, folderId }); }, 3000); } }; @@ -188,8 +207,11 @@ export function printMsg({ conversation: msg.conversation, subject: msg.subject })); + + const actDescriptor = MessageActionsDescriptors.PRINT; + return { - id: 'message-print', + id: actDescriptor.id, icon: 'PrinterOutline', label: t('action.print', 'Print'), onClick: (): void => { @@ -215,8 +237,10 @@ export function printMsg({ } export function showOriginalMsg({ id }: { id: string }): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.SHOW_SOURCE; + return { - id: 'message-show_original', + id: actDescriptor.id, icon: 'CodeOutline', label: t('action.show_original', 'Show original'), onClick: (ev): void => { @@ -226,7 +250,7 @@ export function showOriginalMsg({ id }: { id: string }): MessageActionReturnType }; } -export const dispatchMsgMove = ( +const dispatchMsgMove = ( dispatch: Dispatch, ids: MessageActionIdsType, folderId: string @@ -240,41 +264,42 @@ export const dispatchMsgMove = ( ); const restoreMessage = ( - dispatch: Dispatch, + dispatch: AppDispatch, ids: MessageActionIdsType, folderId: string, closeEditor: boolean | undefined, conversationId: string | undefined ): void => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - dispatchMsgMove(dispatch, ids, folderId).then((res) => { - if (res.type.includes('fulfilled')) { - closeEditor && - replaceHistory( - conversationId - ? `/folder/${folderId}/conversation/${conversationId}` - : `/folder/${folderId}/conversation/-${ids[0]}` - ); - getBridgedFunctions()?.createSnackbar({ - key: `move-${ids}`, - replace: true, - type: 'success', - label: t('messages.snackbar.email_restored', 'E-mail restored in destination folder'), - autoHideTimeout: 3000, - hideButton: true - }); - } else { - getBridgedFunctions()?.createSnackbar({ - key: `move-${ids}`, - replace: true, - type: 'error', - label: t('label.error_try_again', 'Something went wrong, please try again'), - autoHideTimeout: 3000, - hideButton: true - }); - } - }); + dispatchMsgMove(dispatch, ids, folderId) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + .then((res) => { + if (res.type.includes('fulfilled')) { + closeEditor && + replaceHistory( + conversationId + ? `/folder/${folderId}/conversation/${conversationId}` + : `/folder/${folderId}/conversation/-${ids[0]}` + ); + getBridgedFunctions()?.createSnackbar({ + key: `move-${ids}`, + replace: true, + type: 'success', + label: t('messages.snackbar.email_restored', 'E-mail restored in destination folder'), + autoHideTimeout: 3000, + hideButton: true + }); + } else { + getBridgedFunctions()?.createSnackbar({ + key: `move-${ids}`, + replace: true, + type: 'error', + label: t('label.error_try_again', 'Something went wrong, please try again'), + autoHideTimeout: 3000, + hideButton: true + }); + } + }); }; export function moveMsgToTrash({ @@ -285,21 +310,20 @@ export function moveMsgToTrash({ conversationId, closeEditor }: MessageActionPropType): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.MOVE_TO_TRASH; + return { - id: 'message-trash', + id: actDescriptor.id, icon: 'Trash2Outline', label: t('label.delete', 'Delete'), onClick: (ev): void => { if (ev) ev.preventDefault(); dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore msgAction({ - operation: `trash`, + operation: 'trash', ids - }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + }) ).then((res) => { if (res.type.includes('fulfilled')) { deselectAll && deselectAll(); @@ -312,7 +336,7 @@ export function moveMsgToTrash({ autoHideTimeout: 5000, hideButton: false, actionLabel: t('label.undo', 'Undo'), - onActionClick: () => + onActiononClick: () => restoreMessage(dispatch, ids, folderId, closeEditor, conversationId) }); } else { @@ -334,8 +358,10 @@ export function deleteMsg({ ids, dispatch }: Pick): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.DELETE; + return { - id: 'message-delete', + id: actDescriptor.id, icon: 'Trash2Outline', label: t('label.delete', 'Delete'), onClick: (ev): void => { @@ -345,13 +371,10 @@ export function deleteMsg({ confirmLabel: t('action.ok', 'Ok'), onConfirm: () => { dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore msgAction({ operation: 'delete', ids - }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + }) ).then((res) => { // TODO: Fix it in DS // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -414,8 +437,9 @@ export function replyMsg({ id, folderId }: Pick): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.REPLY; return { - id: 'message-reply', + id: actDescriptor.id, icon: 'UndoOutline', label: t('action.reply', 'Reply'), onClick: (ev): void => { @@ -433,8 +457,9 @@ export function replyAllMsg({ id, folderId }: Pick): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.REPLY_ALL; return { - id: 'message-reply_all', + id: actDescriptor.id, icon: 'ReplyAll', label: t('action.reply_all', 'Reply all'), onClick: (ev): void => { @@ -452,8 +477,9 @@ export function forwardMsg({ id, folderId }: Pick): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.FORWARD; return { - id: 'message-forward', + id: actDescriptor.id, icon: 'Forward', label: t('action.forward', 'Forward'), onClick: (ev): void => { @@ -471,8 +497,9 @@ export function editAsNewMsg({ id, folderId }: Pick): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.EDIT_AS_NEW; return { - id: 'message-edit_as_new', + id: actDescriptor.id, icon: 'Edit2Outline', label: t('action.edit_as_new', 'Edit as new'), onClick: (ev): void => { @@ -491,8 +518,9 @@ export function editDraft({ folderId, message }: Pick): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.EDIT_DRAFT; return { - id: 'message-edit_as_draft', + id: actDescriptor.id, icon: 'Edit2Outline', label: t('label.edit', 'Edit'), onClick: (ev): void => { @@ -546,17 +574,16 @@ export function sendDraft({ }: { id: string; message: MailMessage; - dispatch: Dispatch; + dispatch: AppDispatch; }): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.SEND; return { - id: 'message-send', + id: actDescriptor.id, icon: 'PaperPlaneOutline', label: t('label.send', 'Send'), onClick: (ev): void => { if (ev) ev.preventDefault(); dispatch( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore sendMsg({ editorId: id, msg: message @@ -567,8 +594,9 @@ export function sendDraft({ } export function redirectMsg({ id }: { id: string }): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.REDIRECT; return { - id: 'message-redirect', + id: actDescriptor.id, icon: 'CornerUpRight', label: t('action.redirect', 'Redirect'), onClick: (ev): void => { @@ -601,8 +629,11 @@ export function moveMessageToFolder({ MessageActionPropType, 'id' | 'dispatch' | 'isRestore' | 'deselectAll' | 'folderId' >): MessageActionReturnType { + const actDescriptor = isRestore + ? MessageActionsDescriptors.RESTORE + : MessageActionsDescriptors.MOVE; return { - id: 'message-restore', + id: actDescriptor.id, icon: isRestore ? 'RestoreOutline' : 'MoveOutline', label: isRestore ? t('label.restore', 'Restore') : t('label.move', 'Move'), onClick: (): void => { @@ -636,8 +667,9 @@ export function deleteMessagePermanently({ ids, deselectAll }: MessageActionPropType): MessageActionReturnType { + const actDescriptor = MessageActionsDescriptors.DELETE_PERMANENTLY; return { - id: 'message-delete-permanently', + id: actDescriptor.id, icon: 'DeletePermanentlyOutline', label: t('label.delete_permanently', 'Delete Permanently'), onClick: (): void => { @@ -662,231 +694,3 @@ export function deleteMessagePermanently({ } }; } - -type GetMessageActionsType = { - folderId: string; - dispatch: Dispatch; - deselectAll: () => void; - account: Account; - tags: Tags; -}; -export const getActions = ({ - folderId, - dispatch, - deselectAll, - account, - tags -}: GetMessageActionsType): any => { - switch (folderId) { - case FOLDERS.TRASH: - return (message: MailMessage): any => [ - [ - setMsgRead({ - ids: [message.id], - value: message.read, - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - setMsgFlag({ ids: [message.id], value: message.flagged, dispatch }), - replyMsg({ id: message.id, folderId }), - forwardMsg({ id: message.id, folderId }), - deleteMessagePermanently({ ids: [message.id], dispatch, deselectAll }), - moveMessageToFolder({ - id: [message.id], - folderId, - dispatch, - isRestore: true, - deselectAll - }) - ], - [ - setMsgRead({ - ids: [message.id], - value: message.read, - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - setMsgFlag({ ids: [message.id], value: message.flagged, dispatch }), - applyTag({ tags, conversation: message, isMessage: true }), - setMsgAsSpam({ ids: [message.id], value: false, dispatch, folderId }), - printMsg({ message, account }), - deleteMessagePermanently({ ids: [message.id], dispatch, deselectAll }), - moveMessageToFolder({ - id: [message.id], - folderId, - dispatch, - isRestore: true, - deselectAll - }), - replyMsg({ id: message.id, folderId }), - replyAllMsg({ id: message.id, folderId }), - forwardMsg({ id: message.id, folderId }), - editAsNewMsg({ id: message.id, folderId }), - sendDraft({ id: message.id, message, dispatch }), - redirectMsg({ id: message.id }) - ] - ]; - case FOLDERS.SPAM: - return (message: MailMessage, closeEditor: boolean): any => [ - [ - setMsgRead({ - ids: [message.id], - value: message.read, - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - setMsgFlag({ ids: [message.id], value: message.flagged, dispatch }), - setMsgAsSpam({ - ids: [message.id], - value: true, - dispatch, - folderId, - shouldReplaceHistory: true - }), - deleteMsg({ ids: [message.id], dispatch }) - ], - [ - setMsgRead({ - ids: [message.id], - value: message.read, - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - setMsgFlag({ ids: [message.id], value: message.flagged, dispatch }), - applyTag({ tags, conversation: message, isMessage: true }), - setMsgAsSpam({ - ids: [message.id], - value: true, - dispatch, - folderId, - shouldReplaceHistory: true - }), - printMsg({ message, account }), - showOriginalMsg({ id: message.id }), - moveMsgToTrash({ - ids: [message.id], - dispatch, - deselectAll, - folderId, - conversationId: message.conversation, - closeEditor - }), - replyMsg({ id: message.id, folderId }), - replyAllMsg({ id: message.id, folderId }), - forwardMsg({ id: message.id, folderId }), - editAsNewMsg({ id: message.id, folderId }), - sendDraft({ id: message.id, message, dispatch }), - redirectMsg({ id: message.id }) - ] - ]; - case FOLDERS.DRAFTS: - return (message: MailMessage, closeEditor: boolean) => [ - [ - editDraft({ id: message.id, folderId }), - sendDraft({ id: message.id, message, dispatch }), - moveMsgToTrash({ - ids: [message.id], - dispatch, - deselectAll, - folderId, - conversationId: message.conversation, - closeEditor - }), - setMsgFlag({ ids: [message.id], value: message.flagged, dispatch }) - ], - [ - setMsgFlag({ ids: [message.id], value: message.flagged, dispatch }), - applyTag({ tags, conversation: message, isMessage: true }), - moveMsgToTrash({ - ids: [message.id], - - dispatch, - - deselectAll, - folderId, - conversationId: message.conversation, - closeEditor - }), - editDraft({ id: message.id, folderId }), - sendDraft({ id: message.id, message, dispatch }), - printMsg({ message, account }) - ] - ]; - case FOLDERS.SENT: - case FOLDERS.INBOX: - default: - return (message: MailMessage, closeEditor: boolean) => [ - [ - replyMsg({ id: message.id, folderId }), - replyAllMsg({ id: message.id, folderId }), - forwardMsg({ id: message.id, folderId }), - setMsgRead({ - ids: [message.id], - value: message.read, - - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - moveMsgToTrash({ - ids: [message.id], - - dispatch, - - deselectAll, - folderId, - conversationId: message.conversation, - closeEditor - }), - - setMsgFlag({ ids: [message.id], value: message.flagged, dispatch }) - ], - [ - setMsgRead({ - ids: [message.id], - value: message.read, - - dispatch, - folderId, - shouldReplaceHistory: true, - deselectAll - }), - setMsgFlag({ ids: [message.id], value: message.flagged, dispatch }), - - applyTag({ tags, conversation: message, isMessage: true }), - setMsgAsSpam({ - ids: [message.id], - value: false, - dispatch, - folderId, - shouldReplaceHistory: true - }), - printMsg({ message, account }), - showOriginalMsg({ id: message.id }), - moveMsgToTrash({ - ids: [message.id], - dispatch, - deselectAll, - folderId, - conversationId: message.conversation, - closeEditor - }), - replyMsg({ id: message.id, folderId }), - replyAllMsg({ id: message.id, folderId }), - forwardMsg({ id: message.id, folderId }), - editAsNewMsg({ id: message.id, folderId }), - sendDraft({ id: message.id, message, dispatch }), - redirectMsg({ id: message.id }) - ] - ]; - } -}; diff --git a/src/ui-actions/move-conv-msg.jsx b/src/ui-actions/move-conv-msg.jsx index 806c3f487..b023399bc 100644 --- a/src/ui-actions/move-conv-msg.jsx +++ b/src/ui-actions/move-conv-msg.jsx @@ -3,16 +3,16 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { useCallback, useMemo, useState } from 'react'; -import { Container, Input, Padding, Text } from '@zextras/carbonio-design-system'; -import { some } from 'lodash'; import { nanoid } from '@reduxjs/toolkit'; +import { Container, Input, Padding, Text } from '@zextras/carbonio-design-system'; import { FOLDERS, getBridgedFunctions, replaceHistory, t } from '@zextras/carbonio-shell-ui'; +import { some } from 'lodash'; +import React, { useCallback, useMemo, useState } from 'react'; +import ModalFooter from '../carbonio-ui-commons/components/modals/modal-footer'; +import ModalHeader from '../carbonio-ui-commons/components/modals/modal-header'; import { convAction, msgAction } from '../store/actions'; import { createFolder } from '../store/actions/create-folder'; -import ModalHeader from '../carbonio-ui-commons/components/modals/modal-header'; import { FolderSelector } from '../views/sidebar/commons/folder-selector'; -import ModalFooter from '../carbonio-ui-commons/components/modals/modal-footer'; const MoveConvMessage = ({ selectedIDs, @@ -45,7 +45,7 @@ const MoveConvMessage = ({ ).then((res) => { if (res.type.includes('fulfilled')) { deselectAll && deselectAll(); - getBridgedFunctions().createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `edit`, replace: true, type: 'info', @@ -59,7 +59,7 @@ const MoveConvMessage = ({ } }); } else { - getBridgedFunctions().createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `edit`, replace: true, type: 'error', @@ -86,7 +86,7 @@ const MoveConvMessage = ({ ).then((res) => { if (res.type.includes('fulfilled')) { deselectAll && deselectAll(); - getBridgedFunctions().createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `edit`, replace: true, type: 'info', @@ -97,7 +97,7 @@ const MoveConvMessage = ({ hideButton: true // todo: add Go to folder action }); } else { - getBridgedFunctions().createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `edit`, replace: true, type: 'error', @@ -146,7 +146,7 @@ const MoveConvMessage = ({ ? onConfirmMessageMove(res.payload[0].id) : onConfirmConvMove(res.payload[0].id); } else { - getBridgedFunctions().createSnackbar({ + getBridgedFunctions()?.createSnackbar({ key: `edit`, replace: true, type: 'error', diff --git a/src/ui-actions/multiple-selection-actions-panel.tsx b/src/ui-actions/multiple-selection-actions-panel.tsx new file mode 100644 index 000000000..66e328b2c --- /dev/null +++ b/src/ui-actions/multiple-selection-actions-panel.tsx @@ -0,0 +1,399 @@ +/* + * SPDX-FileCopyrightText: 2021 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { + Button, + Container, + Dropdown, + IconButton, + Row, + Tooltip +} from '@zextras/carbonio-design-system'; +import { FOLDERS, getBridgedFunctions, t, useTags } from '@zextras/carbonio-shell-ui'; +import { every, filter, findIndex } from 'lodash'; +import React, { FC, ReactElement, SyntheticEvent, useCallback, useEffect, useState } from 'react'; + +import { useAppDispatch } from '../hooks/redux'; +import type { + ActionReturnType, + ConvActionReturnType, + Conversation, + MailMessage, + MessageActionReturnType, + TagActionItemType +} from '../types'; +import { + deleteConversationPermanently, + moveConversationToFolder, + moveConversationToTrash, + setConversationsFlag, + setConversationsRead, + setConversationsSpam +} from './conversation-actions'; +import { + deleteMessagePermanently, + moveMessageToFolder, + moveMsgToTrash, + setMsgAsSpam, + setMsgFlag, + setMsgRead +} from './message-actions'; +import { applyMultiTag } from './tag-actions'; +import { getFolderParentId } from './utils'; +import { getSystemFolderParentId } from '../helpers/folders'; + +type MultipleSelectionActionsPanelProps = { + items: Array> | Array; + selectedIds: Array; + deselectAll: () => void; + selectAll: () => void; + isAllSelected: boolean; + selectAllModeOff: () => void; + setIsSelectModeOn: (value: boolean) => void; + folderId: string; +}; +type MsgOrConv = Partial | Conversation; + +export const MultipleSelectionActionsPanel: FC = ({ + items, + selectedIds, + deselectAll, + selectAll, + isAllSelected, + selectAllModeOff, + setIsSelectModeOn, + folderId +}) => { + const isConversation = 'messages' in (items?.[0] || {}); + + const folderParentId = getFolderParentId({ folderId, isConversation, items }); + + const [currentFolderId] = useState(folderParentId); + + // This useEffect is used to reset the select mode when the user navigates to a different folder + useEffect(() => { + if (folderId && currentFolderId !== folderParentId) { + setIsSelectModeOn(false); + } + }, [currentFolderId, folderId, folderParentId, setIsSelectModeOn]); + + const dispatch = useAppDispatch(); + const ids = Object.values(selectedIds ?? []); + const selectedConversation = filter(items, (item: MsgOrConv) => ids.includes(item.id ?? '0')); + const tags = useTags(); + const foldersExcludedMarkReadUnread = [FOLDERS.DRAFTS, FOLDERS.SPAM, FOLDERS.TRASH]; + const foldersExcludedTrash = [FOLDERS.TRASH]; + const foldersIncludedDeletePermanently = [FOLDERS.TRASH]; + const foldersExcludedMoveToFolder = [FOLDERS.DRAFTS, FOLDERS.TRASH]; + const foldersExcludedTags = [FOLDERS.SPAM]; + const foldersExcludedMarkSpam = [FOLDERS.DRAFTS, FOLDERS.TRASH, FOLDERS.SPAM]; + const foldersIncludedMarkNotSpam = [FOLDERS.SPAM]; + + const addFlagAction = (): ActionReturnType => { + const selectedItems = filter(items, (item: MsgOrConv) => ids.includes(item.id ?? '0')); + const action = isConversation + ? setConversationsFlag({ ids, value: false, dispatch }) + : setMsgFlag({ ids, value: false, dispatch }); + return every(selectedItems, ['flagged', false]) && action; + }; + + const removeFlagAction = (): ActionReturnType => { + const selectedItems = filter(items, (item: MsgOrConv) => ids.includes(item.id ?? '0')); + const action = isConversation + ? setConversationsFlag({ ids, value: true, dispatch }) + : setMsgFlag({ ids, value: true, dispatch }); + return every(selectedItems, ['flagged', true]) && action; + }; + + const setMsgReadAction = (): ActionReturnType => { + const selectedItems = filter( + items, + (item: MsgOrConv) => + ids.includes(item.id ?? '0') && + !foldersExcludedMarkReadUnread.includes(getSystemFolderParentId(folderParentId) ?? '0') + ); + const action = isConversation + ? setConversationsRead({ + ids, + value: false, + dispatch, + folderId, + deselectAll, + shouldReplaceHistory: false + }) + : setMsgRead({ ids, value: false, dispatch, folderId: folderParentId }); + return findIndex(selectedItems, ['read', false]) !== -1 && action; + }; + + const setMsgUnreadAction = (): ActionReturnType => { + const selectedItems = filter( + items, + (item: MsgOrConv) => + ids.includes(item.id ?? '0') && + !foldersExcludedMarkReadUnread.includes(getSystemFolderParentId(folderParentId) ?? '0') + ); + const action = isConversation + ? setConversationsRead({ + ids, + value: true, + dispatch, + folderId, + deselectAll, + shouldReplaceHistory: false + }) + : setMsgRead({ ids, value: true, dispatch, folderId: folderParentId }); + return selectedItems.length > 0 && every(selectedItems, ['read', true]) && action; + }; + + const getMoveToTrashAction = (): false | ConvActionReturnType | MessageActionReturnType => { + const selectedItems = filter( + items, + (item: MsgOrConv) => + ids.includes(item.id ?? '0') && + !foldersExcludedTrash.includes(getSystemFolderParentId(folderParentId) ?? '0') + ); + const action = isConversation + ? moveConversationToTrash({ ids, dispatch, folderId, deselectAll }) + : moveMsgToTrash({ ids, dispatch, deselectAll }); + return selectedItems.length > 0 && selectedItems.length === ids.length && action; + }; + + const deletePermanentlyAction = (): ActionReturnType => { + const selectedItems = filter( + items, + (item: MsgOrConv) => + ids.includes(item.id ?? '0') && + foldersIncludedDeletePermanently.includes(getSystemFolderParentId(folderParentId) ?? '0') + ); + const action = isConversation + ? deleteConversationPermanently({ ids, deselectAll }) + : deleteMessagePermanently({ ids, dispatch, deselectAll }); + return selectedItems.length > 0 && selectedItems.length === ids.length && action; + }; + + const moveToFolderAction = (): ActionReturnType => { + const selectedItems = filter( + items, + (item: MsgOrConv) => + ids.includes(item.id ?? '0') && + !foldersExcludedMoveToFolder.includes(getSystemFolderParentId(folderParentId) ?? '0') + ); + const action = isConversation + ? moveConversationToFolder({ + ids, + dispatch, + folderId, + isRestore: false, + deselectAll + }) + : moveMessageToFolder({ + id: ids, + folderId: folderParentId, + dispatch, + isRestore: false, + deselectAll + }); + return selectedItems.length > 0 && selectedItems.length === ids.length && action; + }; + + const applyTagAction = (): false | TagActionItemType => { + const selectedItems = filter( + items, + (item: MsgOrConv) => + ids.includes(item.id ?? '0') && + !foldersExcludedTags.includes(getSystemFolderParentId(folderParentId) ?? '0') + ); + const action = applyMultiTag({ + ids, + tags, + conversations: selectedConversation, + folderId: folderParentId, + deselectAll, + isMessage: !isConversation + }); + return selectedItems.length > 0 && selectedItems.length === ids.length && action; + }; + + const markMsgAsSpam = (): ActionReturnType => { + const selectedItems = filter( + items, + (item: MsgOrConv) => + ids.includes(item.id ?? '0') && + !foldersExcludedMarkSpam.includes(getSystemFolderParentId(folderParentId) ?? '0') + ); + const action = isConversation + ? setConversationsSpam({ + ids, + value: false, + dispatch, + deselectAll + }) + : setMsgAsSpam({ ids, value: false, dispatch, folderId: folderParentId }); + + return selectedItems.length > 0 && selectedItems.length === ids.length && action; + }; + + const markMsgAsNotSpam = (): ActionReturnType => { + const selectedItems = filter( + items, + (item: MsgOrConv) => + ids.includes(item.id ?? '0') && + foldersIncludedMarkNotSpam.includes(getSystemFolderParentId(folderParentId) ?? '0') + ); + const action = isConversation + ? setConversationsSpam({ + ids, + value: true, + dispatch, + deselectAll + }) + : setMsgAsSpam({ ids, value: true, dispatch, folderId: folderParentId }); + + return selectedItems.length > 0 && selectedItems.length === ids.length && action; + }; + + const messagesArrayIsNotEmpty = ids.length > 0; + + const primaryActions = (): ActionReturnType[] => { + if (messagesArrayIsNotEmpty) + return [ + setMsgReadAction(), + setMsgUnreadAction(), + getMoveToTrashAction(), + deletePermanentlyAction() + ]; + return []; + }; + + const secondaryActions = (): ActionReturnType[] => { + if (messagesArrayIsNotEmpty) + return [ + setMsgReadAction(), + setMsgUnreadAction(), + addFlagAction(), + removeFlagAction(), + moveToFolderAction(), + applyTagAction(), + markMsgAsSpam(), + markMsgAsNotSpam() + ]; + return []; + }; + + const primaryActionsArray = primaryActions()?.reduce((acc, action) => { + if (action) + acc.push( +
+ + ): void => { + if (ev) ev.preventDefault(); + action.onClick && action.onClick(ev); + }} + size="large" + /> + +
+ ); + return acc; + }, [] as Array); + + const secondaryActionsArray: Array & { label: string }> = + secondaryActions()?.reduce((acc, action) => { + if (action) + acc.push({ + id: 'label' in action ? action.label : action.id, + icon: 'icon' in action ? action.icon : '', + label: 'label' in action ? action.label : '', + onClick: (ev: KeyboardEvent | SyntheticEvent): void => { + if (ev) ev.preventDefault(); + action.onClick && action.onClick(ev); + }, + customComponent: action.customComponent, + items: action.items + }); + return acc; + }, [] as Array & { label: string }>); + + const arrowBackOnClick = useCallback(() => { + deselectAll(); + setIsSelectModeOn(false); + }, [deselectAll, setIsSelectModeOn]); + + const selectAllOnClick = useCallback(() => { + selectAll(); + getBridgedFunctions()?.createSnackbar({ + key: `selected-${ids}`, + replace: true, + type: 'info', + label: t('label.all_items_selected', 'All visible items have been selected'), + autoHideTimeout: 5000, + hideButton: true + }); + }, [selectAll, ids]); + + const actionsIsNotEmpty = primaryActionsArray.length > 0 || secondaryActionsArray.length > 0; + + return ( + + + + +