From fdff547e364f993fe18bfdd5eef06c8ade2dcb6e Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 10:23:00 +0100 Subject: [PATCH 001/165] feat(store): add message slice to search store - Introduced `MessageSliceState` to manage message-related state. - Added `MESSAGES_INITIAL_STATE` for initial message state. - Created `createMessageSlice` to initialize message slice state. - Renamed `setMessages` to `setMessagesInSearchSlice` for clarity. - Updated `MessageStoreState` to include `MessageSliceState`. --- src/store/conversations-slice.ts | 2 +- src/store/zustand/search/messages-slice.ts | 24 +++++++++++++++++++ src/store/zustand/search/store.test.ts | 18 +++++++------- src/store/zustand/search/store.ts | 4 +++- src/types/search/index.d.ts | 15 +++++++++++- src/views/search/test/search-view.test.tsx | 4 ++-- .../sidebar/sync-data-handler-hooks.test.tsx | 20 ++++++++-------- 7 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 src/store/zustand/search/messages-slice.ts diff --git a/src/store/conversations-slice.ts b/src/store/conversations-slice.ts index 390e37400..903689fc9 100644 --- a/src/store/conversations-slice.ts +++ b/src/store/conversations-slice.ts @@ -184,7 +184,7 @@ export const getConversationsSliceInitialState = (): ConversationsStateType => expandedStatus: {}, searchedInFolder: {}, searchRequestStatus: null - }) as ConversationsStateType; + }) as Convhttps://zextras.atlassian.net/browse/CO-1725ersationsStateType; export const conversationsSlice = createSlice({ name: 'conversations', diff --git a/src/store/zustand/search/messages-slice.ts b/src/store/zustand/search/messages-slice.ts new file mode 100644 index 000000000..dbf759054 --- /dev/null +++ b/src/store/zustand/search/messages-slice.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2024 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StateCreator } from 'zustand'; + +import { MessageSliceState, PopulatedItemsSliceState, SearchSliceState } from '../../../types'; + +export const MESSAGES_INITIAL_STATE: MessageSliceState['messages'] = { + messageIds: new Set(), + more: false, + offset: 0, + status: null +}; +export const createMessageSlice: StateCreator< + SearchSliceState & PopulatedItemsSliceState & MessageSliceState, + [], + [], + MessageSliceState +> = () => ({ + messages: MESSAGES_INITIAL_STATE +}); diff --git a/src/store/zustand/search/store.test.ts b/src/store/zustand/search/store.test.ts index 1393aa07a..e752faebe 100644 --- a/src/store/zustand/search/store.test.ts +++ b/src/store/zustand/search/store.test.ts @@ -12,7 +12,7 @@ import { updateMessages, setSearchResultsByConversation, updateConversationStatus, - setMessages, + setMessagesInSearchSlice, updateMessagesOnly, useConversationById, useConversationMessages, @@ -79,7 +79,7 @@ describe('message store', () => { const conversation2Messages = [generateMessage({ id: '4' }), generateMessage({ id: '5' })]; const conversation2 = generateConversation({ id: '2', messages: conversation2Messages }); setSearchResultsByConversation([conversation1, conversation2], false); - setMessages([...conversation1Messages, ...conversation2Messages]); + setMessagesInSearchSlice([...conversation1Messages, ...conversation2Messages]); updateMessages([generateMessage({ id: '100' })]); @@ -93,7 +93,7 @@ describe('message store', () => { it('should reset the searches and populated items', () => { setSearchResultsByConversation([generateConversation({ id: '1', messages: [] })], false); updateConversationStatus('1', API_REQUEST_STATUS.fulfilled); - setMessages([generateMessage({ id: '100' })]); + setMessagesInSearchSlice([generateMessage({ id: '100' })]); resetSearch(); @@ -141,7 +141,7 @@ describe('message store', () => { const conversation2Messages = [generateMessage({ id: '4' }), generateMessage({ id: '5' })]; const conversation2 = generateConversation({ id: '2', messages: conversation2Messages }); setSearchResultsByConversation([conversation1, conversation2], false); - setMessages([...conversation1Messages, ...conversation2Messages]); + setMessagesInSearchSlice([...conversation1Messages, ...conversation2Messages]); deleteConversations(['1']); @@ -176,7 +176,7 @@ describe('message store', () => { describe('messages', () => { it('should set and return a message', () => { const message = generateMessage({ id: '1' }); - setMessages([message]); + setMessagesInSearchSlice([message]); const { result } = renderHook(() => useMessageById('1')); @@ -192,7 +192,7 @@ describe('message store', () => { }); it('should not unset fields on message', () => { - setMessages([generateMessage({ id: '1', folderId: FOLDERS.INBOX })]); + setMessagesInSearchSlice([generateMessage({ id: '1', folderId: FOLDERS.INBOX })]); updateMessagesOnly([generateMessage({ id: '1', folderId: undefined })]); @@ -203,7 +203,7 @@ describe('message store', () => { it('should delete all messages', () => { act(() => { - setMessages([generateMessage({ id: '1' }), generateMessage({ id: '2' })]); + setMessagesInSearchSlice([generateMessage({ id: '1' }), generateMessage({ id: '2' })]); }); const { result: _message1 } = renderHook(() => useMessageById('1')); @@ -223,7 +223,7 @@ describe('message store', () => { it('should append messages to the store when appendMessages is called', () => { enableMapSet(); - setMessages([generateMessage({ id: '1' })]); + setMessagesInSearchSlice([generateMessage({ id: '1' })]); appendMessages([generateMessage({ id: '2' }), generateMessage({ id: '3' })], 0); @@ -239,7 +239,7 @@ describe('message store', () => { generateMessage({ id: '3' }) ]; setSearchResultsByMessage(messages, false); - setMessages(messages); + setMessagesInSearchSlice(messages); deleteMessages(['1', '2']); diff --git a/src/store/zustand/search/store.ts b/src/store/zustand/search/store.ts index 8aaa5fef4..25497d556 100644 --- a/src/store/zustand/search/store.ts +++ b/src/store/zustand/search/store.ts @@ -9,6 +9,7 @@ import produce, { enableMapSet } from 'immer'; import { merge } from 'lodash'; import { create } from 'zustand'; +import { createMessageSlice } from './messages-slice'; import { createPopulatedItemsSlice, POPULATED_ITEMS_INITIAL_STATE } from './populated-items-slice'; import { createSearchSlice, SEARCH_INITIAL_STATE } from './search-slice'; import { API_REQUEST_STATUS } from '../../../constants'; @@ -24,6 +25,7 @@ import { const useMessageStore = create()((...a) => ({ ...createSearchSlice(...a), + ...createMessageSlice(...a), ...createPopulatedItemsSlice(...a) })); @@ -179,7 +181,7 @@ export function updateMessagesOnly(messages: Array): void { ); } -export function setMessages(messages: Array): void { +export function setMessagesInSearchSlice(messages: Array): void { useMessageStore.setState((state: MessageStoreState) => ({ search: { ...state.search, diff --git a/src/types/search/index.d.ts b/src/types/search/index.d.ts index 2f289c0a0..2a041e9db 100644 --- a/src/types/search/index.d.ts +++ b/src/types/search/index.d.ts @@ -247,6 +247,19 @@ export type SearchSliceState = { }; }; +export type MessageSliceState = { + messages: { + messageIds: Set; + more: boolean; + offset: number; + sortBy?: SortBy; + query?: string; + status: SearchRequestStatus; + parent?: string; + tagName?: string; + error?: ErrorType; + }; +}; export type PopulatedItemsSliceState = { populatedItems: { messages: Record; @@ -256,4 +269,4 @@ export type PopulatedItemsSliceState = { }; }; -export type MessageStoreState = PopulatedItemsSliceState & SearchSliceState; +export type MessageStoreState = PopulatedItemsSliceState & SearchSliceState & MessageSliceState; diff --git a/src/views/search/test/search-view.test.tsx b/src/views/search/test/search-view.test.tsx index cba433e3e..e2ea546ed 100644 --- a/src/views/search/test/search-view.test.tsx +++ b/src/views/search/test/search-view.test.tsx @@ -31,7 +31,7 @@ import * as search from '../../../store/actions/search'; import { setSearchResultsByConversation, updateConversationStatus, - setMessages + setMessagesInSearchSlice } from '../../../store/zustand/search/store'; import { TESTID_SELECTORS } from '../../../tests/constants'; import { generateConversation } from '../../../tests/generators/generateConversation'; @@ -282,7 +282,7 @@ describe('SearchView', () => { [generateConversation({ id: '123', messages: [message] })], false ); - setMessages([message]); + setMessagesInSearchSlice([message]); updateConversationStatus('123', API_REQUEST_STATUS.fulfilled); const searchViewProps: SearchViewProps = { useQuery: () => [[], noop], diff --git a/src/views/sidebar/sync-data-handler-hooks.test.tsx b/src/views/sidebar/sync-data-handler-hooks.test.tsx index 1db08c83a..8bdb83b4c 100644 --- a/src/views/sidebar/sync-data-handler-hooks.test.tsx +++ b/src/views/sidebar/sync-data-handler-hooks.test.tsx @@ -25,7 +25,7 @@ import { folderWorker } from '../../carbonio-ui-commons/worker'; import * as reduxHooks from '../../hooks/redux'; import { setSearchResultsByConversation, - setMessages, + setMessagesInSearchSlice, useConversationById, useMessageById, setSearchResultsByMessage @@ -208,7 +208,7 @@ describe('sync data handler', () => { describe('messages', () => { it('should mark messages as read', async () => { - setMessages([generateMessage({ id: '1', isRead: false })]); + setMessagesInSearchSlice([generateMessage({ id: '1', isRead: false })]); mockSoapModifyMessageAction(mailboxNumber, '1', [READ]); renderHook(() => useSyncDataHandler(), { @@ -221,7 +221,7 @@ describe('sync data handler', () => { }); }); it('should mark messages as unread', async () => { - setMessages([generateMessage({ id: '1', isRead: true })]); + setMessagesInSearchSlice([generateMessage({ id: '1', isRead: true })]); mockSoapModifyMessageAction(mailboxNumber, '1', [UNREAD]); renderHook(() => useSyncDataHandler(), { @@ -235,7 +235,7 @@ describe('sync data handler', () => { }); it('should mark messages as flagged', async () => { - setMessages([generateMessage({ id: '1', isFlagged: false })]); + setMessagesInSearchSlice([generateMessage({ id: '1', isFlagged: false })]); mockSoapModifyMessageAction(mailboxNumber, '1', [FLAGGED]); renderHook(() => useSyncDataHandler(), { @@ -248,7 +248,7 @@ describe('sync data handler', () => { }); }); it('should mark messages as not flagged', async () => { - setMessages([generateMessage({ id: '1', isFlagged: true })]); + setMessagesInSearchSlice([generateMessage({ id: '1', isFlagged: true })]); mockSoapModifyMessageAction(mailboxNumber, '1', [NOTFLAGGED]); renderHook(() => useSyncDataHandler(), { @@ -262,7 +262,7 @@ describe('sync data handler', () => { }); it('should mark message as spam', async () => { - setMessages([generateMessage({ id: '1', folderId: FOLDERS.INBOX })]); + setMessagesInSearchSlice([generateMessage({ id: '1', folderId: FOLDERS.INBOX })]); mockSoapModifyMessageFolder(mailboxNumber, '1', FOLDERS.SPAM); renderHook(() => useSyncDataHandler(), { @@ -275,7 +275,7 @@ describe('sync data handler', () => { }); }); it('should mark message as not spam', async () => { - setMessages([generateMessage({ id: '1', folderId: FOLDERS.SPAM })]); + setMessagesInSearchSlice([generateMessage({ id: '1', folderId: FOLDERS.SPAM })]); mockSoapModifyMessageFolder(mailboxNumber, '1', FOLDERS.INBOX); renderHook(() => useSyncDataHandler(), { @@ -289,7 +289,7 @@ describe('sync data handler', () => { }); it('should move message to trash', async () => { - setMessages([generateMessage({ id: '1', folderId: FOLDERS.INBOX })]); + setMessagesInSearchSlice([generateMessage({ id: '1', folderId: FOLDERS.INBOX })]); mockSoapModifyMessageFolder(mailboxNumber, '1', FOLDERS.TRASH); renderHook(() => useSyncDataHandler(), { @@ -303,7 +303,7 @@ describe('sync data handler', () => { }); it('should restore message', async () => { - setMessages([generateMessage({ id: '1', folderId: FOLDERS.TRASH })]); + setMessagesInSearchSlice([generateMessage({ id: '1', folderId: FOLDERS.TRASH })]); mockSoapModifyMessageFolder(mailboxNumber, '1', FOLDERS.INBOX); renderHook(() => useSyncDataHandler(), { @@ -317,7 +317,7 @@ describe('sync data handler', () => { }); it('should move message to a folder', async () => { - setMessages([generateMessage({ id: '1', folderId: 'aaa' })]); + setMessagesInSearchSlice([generateMessage({ id: '1', folderId: 'aaa' })]); mockSoapModifyMessageFolder(mailboxNumber, '1', 'bbb'); renderHook(() => useSyncDataHandler(), { From d0f53fdc8295fa5d8d5d50cccd48e1620cf14784 Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 10:31:09 +0100 Subject: [PATCH 002/165] refactor: rename MessagesStore to EmailsStore --- src/store/zustand/search/store.ts | 79 +++++++++++++++++++------------ src/types/search/index.d.ts | 2 +- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/store/zustand/search/store.ts b/src/store/zustand/search/store.ts index 25497d556..7d28d5e26 100644 --- a/src/store/zustand/search/store.ts +++ b/src/store/zustand/search/store.ts @@ -16,26 +16,26 @@ import { API_REQUEST_STATUS } from '../../../constants'; import { IncompleteMessage, MailMessage, - MessageStoreState, + EmailsStoreState, NormalizedConversation, PopulatedItemsSliceState, SearchRequestStatus, SearchSliceState } from '../../../types'; -const useMessageStore = create()((...a) => ({ +const useEmailsStore = create()((...a) => ({ ...createSearchSlice(...a), ...createMessageSlice(...a), ...createPopulatedItemsSlice(...a) })); export function useConversationById(id: string): NormalizedConversation { - return useMessageStore((state) => state.populatedItems.conversations[id]); + return useEmailsStore((state) => state.populatedItems.conversations[id]); } export function resetSearch(): void { - useMessageStore.setState( - produce((state: MessageStoreState) => { + useEmailsStore.setState( + produce((state: EmailsStoreState) => { state.search = SEARCH_INITIAL_STATE; state.populatedItems = POPULATED_ITEMS_INITIAL_STATE; }) @@ -43,18 +43,18 @@ export function resetSearch(): void { } export function useMessageById(id: string): IncompleteMessage | MailMessage { - return useMessageStore((state) => state.populatedItems.messages[id]); + return useEmailsStore((state) => state.populatedItems.messages[id]); } export function useSearchResults(): SearchSliceState['search'] { - return useMessageStore(({ search }) => search); + return useEmailsStore(({ search }) => search); } export function useConversationMessages( conversationId: string ): Array { const messages: Array = []; - useMessageStore((state: MessageStoreState) => + useEmailsStore((state: EmailsStoreState) => state.populatedItems.conversations[conversationId].messages.forEach((message) => { if (state.populatedItems.messages[message.id]) messages.push(state.populatedItems.messages[message.id]); @@ -64,14 +64,14 @@ export function useConversationMessages( } export function useConversationStatus(id: string): SearchRequestStatus { - return useMessageStore((state) => state.populatedItems.conversationsStatus?.[id]); + return useEmailsStore((state) => state.populatedItems.conversationsStatus?.[id]); } export function setSearchResultsByConversation( conversations: Array, more: boolean ): void { - useMessageStore.setState( + useEmailsStore.setState( produce(({ search, populatedItems }) => { search.conversationIds = new Set(conversations.map((c) => c.id)); search.status = API_REQUEST_STATUS.fulfilled; @@ -92,7 +92,7 @@ export function setSearchResultsByMessage( messages: Array, more: boolean ): void { - useMessageStore.setState( + useEmailsStore.setState( produce(({ search, populatedItems }) => { search.messageIds = new Set(messages.map((message) => message.id)); search.status = API_REQUEST_STATUS.fulfilled; @@ -118,8 +118,8 @@ export function appendConversations( enableMapSet(); const newConversationsIds = new Set(conversations.map((c) => c.id)); - useMessageStore.setState( - produce((state: MessageStoreState) => { + useEmailsStore.setState( + produce((state: EmailsStoreState) => { newConversationsIds.forEach((id) => state.search.conversationIds.add(id)); state.search.offset = offset; state.search.more = more; @@ -132,7 +132,7 @@ export function appendConversations( } export function updateConversationsOnly(conversations: Array): void { - useMessageStore.setState( + useEmailsStore.setState( produce(({ populatedItems }: PopulatedItemsSliceState) => { conversations.forEach((conversation) => { populatedItems.conversations[conversation.id] = { @@ -146,8 +146,8 @@ export function updateConversationsOnly(conversations: Array): void { enableMapSet(); - useMessageStore.setState( - produce((state: MessageStoreState) => { + useEmailsStore.setState( + produce((state: EmailsStoreState) => { ids.forEach((id) => { state.search.conversationIds.delete(id); delete state.populatedItems.conversations[id]; @@ -158,8 +158,8 @@ export function deleteConversations(ids: Array): void { export function deleteMessages(ids: Array): void { enableMapSet(); - useMessageStore.setState( - produce((state: MessageStoreState) => { + useEmailsStore.setState( + produce((state: EmailsStoreState) => { ids.forEach((id) => { state.search.messageIds.delete(id); delete state.populatedItems.messages[id]; @@ -169,7 +169,7 @@ export function deleteMessages(ids: Array): void { } export function updateMessagesOnly(messages: Array): void { - useMessageStore.setState( + useEmailsStore.setState( produce(({ populatedItems }: PopulatedItemsSliceState) => { messages.forEach((message) => { populatedItems.messages[message.id] = { @@ -182,7 +182,7 @@ export function updateMessagesOnly(messages: Array): void { } export function setMessagesInSearchSlice(messages: Array): void { - useMessageStore.setState((state: MessageStoreState) => ({ + useEmailsStore.setState((state: EmailsStoreState) => ({ search: { ...state.search, messageIds: new Set(messages.map((c) => c.id)) @@ -201,14 +201,33 @@ export function setMessagesInSearchSlice(messages: Array): void { + useEmailsStore.setState((state: EmailsStoreState) => ({ + search: { + ...state.search, + messageIds: new Set(messages.map((c) => c.id)) + }, + populatedItems: { + ...state.populatedItems, + offset: 0, + messages: messages.reduce( + (acc, msg) => { + acc[msg.id] = msg; + return acc; + }, + {} as Record + ) + } + })); +} export function appendMessages( messages: Array, offset: number ): void { enableMapSet(); const newMessageIds = new Set(messages.map((message) => message.id)); - useMessageStore.setState( - produce((state: MessageStoreState) => { + useEmailsStore.setState( + produce((state: EmailsStoreState) => { newMessageIds.forEach((messageId) => state.search.messageIds.add(messageId)); state.search.offset = offset; state.populatedItems.messages = messages.reduce((acc, msg) => { @@ -220,7 +239,7 @@ export function appendMessages( } export function updateMessages(messages: MailMessage[]): void { - useMessageStore.setState( + useEmailsStore.setState( produce(({ populatedItems }: PopulatedItemsSliceState) => { messages.forEach((message) => { populatedItems.messages[message.id] = message; @@ -233,7 +252,7 @@ export function updateConversationStatus( conversationId: string, status: SearchRequestStatus ): void { - useMessageStore.setState( + useEmailsStore.setState( produce(({ populatedItems }: PopulatedItemsSliceState) => { populatedItems.conversationsStatus[conversationId] = status; }) @@ -241,18 +260,18 @@ export function updateConversationStatus( } export function updateMessageStatus(messageId: string, status: SearchRequestStatus): void { - useMessageStore.setState( + useEmailsStore.setState( produce(({ populatedItems }: PopulatedItemsSliceState) => { populatedItems.messagesStatus[messageId] = status; }) ); } export function useMessageStatus(id: string): SearchRequestStatus { - return useMessageStore((state) => state.populatedItems.messagesStatus?.[id]); + return useEmailsStore((state) => state.populatedItems.messagesStatus?.[id]); } export function updateSearchResultsLoadingStatus(status: SearchRequestStatus): void { - useMessageStore.setState( + useEmailsStore.setState( produce(({ search }: SearchSliceState) => { search.status = status; }) @@ -260,12 +279,12 @@ export function updateSearchResultsLoadingStatus(status: SearchRequestStatus): v } export function getSearchResultsLoadingStatus(): SearchRequestStatus { - return useMessageStore.getState().search.status; + return useEmailsStore.getState().search.status; } export function removeMessages(messageIds: Array): void { - useMessageStore.setState( - produce((state: MessageStoreState) => { + useEmailsStore.setState( + produce((state: EmailsStoreState) => { messageIds.forEach((messageId) => { delete state.populatedItems.messages[messageId]; }); diff --git a/src/types/search/index.d.ts b/src/types/search/index.d.ts index 2a041e9db..676c72428 100644 --- a/src/types/search/index.d.ts +++ b/src/types/search/index.d.ts @@ -269,4 +269,4 @@ export type PopulatedItemsSliceState = { }; }; -export type MessageStoreState = PopulatedItemsSliceState & SearchSliceState & MessageSliceState; +export type EmailsStoreState = PopulatedItemsSliceState & SearchSliceState & MessageSliceState; From 8d134c73e2622d35f3b1770b9b4986920209f82a Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 10:57:45 +0100 Subject: [PATCH 003/165] fix(store): remove duplicate function implementation Refactor setMessagesInMessagesSlice to avoid identical implementation with setMessagesInSearchSlice. This change ensures code consistency and reduces redundancy in the store module. --- src/store/zustand/search/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/zustand/search/store.ts b/src/store/zustand/search/store.ts index 7d28d5e26..d2084d587 100644 --- a/src/store/zustand/search/store.ts +++ b/src/store/zustand/search/store.ts @@ -201,7 +201,7 @@ export function setMessagesInSearchSlice(messages: Array): void { +export function setMessagesInMessagesSlice(messages: Array): void { useEmailsStore.setState((state: EmailsStoreState) => ({ search: { ...state.search, From 1211bb6ac55101ae275f2f1dcfd01f87883e40dd Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 10:59:31 +0100 Subject: [PATCH 004/165] refactor(store): rename search store folder to emails in zustand Renamed all instances of 'search' to 'emails' in the zustand store directory. This change improves clarity and aligns with the updated module naming conventions. --- src/store/zustand/{search => emails}/hooks/hooks.ts | 0 src/store/zustand/{search => emails}/messages-slice.ts | 0 src/store/zustand/{search => emails}/populated-items-slice.ts | 0 src/store/zustand/{search => emails}/search-slice.ts | 0 src/store/zustand/{search => emails}/store.test.ts | 0 src/store/zustand/{search => emails}/store.ts | 0 src/store/zustand/{search => emails}/test/hooks.test.tsx | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename src/store/zustand/{search => emails}/hooks/hooks.ts (100%) rename src/store/zustand/{search => emails}/messages-slice.ts (100%) rename src/store/zustand/{search => emails}/populated-items-slice.ts (100%) rename src/store/zustand/{search => emails}/search-slice.ts (100%) rename src/store/zustand/{search => emails}/store.test.ts (100%) rename src/store/zustand/{search => emails}/store.ts (100%) rename src/store/zustand/{search => emails}/test/hooks.test.tsx (100%) diff --git a/src/store/zustand/search/hooks/hooks.ts b/src/store/zustand/emails/hooks/hooks.ts similarity index 100% rename from src/store/zustand/search/hooks/hooks.ts rename to src/store/zustand/emails/hooks/hooks.ts diff --git a/src/store/zustand/search/messages-slice.ts b/src/store/zustand/emails/messages-slice.ts similarity index 100% rename from src/store/zustand/search/messages-slice.ts rename to src/store/zustand/emails/messages-slice.ts diff --git a/src/store/zustand/search/populated-items-slice.ts b/src/store/zustand/emails/populated-items-slice.ts similarity index 100% rename from src/store/zustand/search/populated-items-slice.ts rename to src/store/zustand/emails/populated-items-slice.ts diff --git a/src/store/zustand/search/search-slice.ts b/src/store/zustand/emails/search-slice.ts similarity index 100% rename from src/store/zustand/search/search-slice.ts rename to src/store/zustand/emails/search-slice.ts diff --git a/src/store/zustand/search/store.test.ts b/src/store/zustand/emails/store.test.ts similarity index 100% rename from src/store/zustand/search/store.test.ts rename to src/store/zustand/emails/store.test.ts diff --git a/src/store/zustand/search/store.ts b/src/store/zustand/emails/store.ts similarity index 100% rename from src/store/zustand/search/store.ts rename to src/store/zustand/emails/store.ts diff --git a/src/store/zustand/search/test/hooks.test.tsx b/src/store/zustand/emails/test/hooks.test.tsx similarity index 100% rename from src/store/zustand/search/test/hooks.test.tsx rename to src/store/zustand/emails/test/hooks.test.tsx From bb58ce20fe448e26ea0571ceb9b6c304eed34e89 Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 11:07:47 +0100 Subject: [PATCH 005/165] refactor: update zustand store imports Updated the imports in various files to reflect the new location of the zustand store from `search/store` to `emails/store`. This change ensures consistency and correctness in the import paths across the codebase. --- .../html-message-renderer.tsx | 2 +- .../tests/html-message-renderer.test.tsx | 2 +- src/store/conversations-slice.ts | 15 +++++++-------- .../detail-panel/conversation-message-preview.tsx | 2 +- .../search-conversation-extra-window-panel.tsx | 2 +- .../search-conversation-list-item.tsx | 4 ++-- .../list/message/search-message-list-item.tsx | 2 +- .../search-conversation-message-panel.tsx | 2 +- .../conversation/search-conversation-panel.tsx | 2 +- .../search/panel/message/search-message-panel.tsx | 2 +- .../message/tests/search-message-panel.test.tsx | 2 +- src/views/search/search-view-hooks.ts | 2 +- src/views/search/test/search-view-hooks.test.ts | 2 +- src/views/search/test/search-view.test.tsx | 2 +- .../sidebar/commons/sync-data-handler-hooks.ts | 2 +- .../sidebar/sync-data-handler-hooks.test.tsx | 2 +- 16 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/commons/mail-message-renderer/html-message-renderer.tsx b/src/commons/mail-message-renderer/html-message-renderer.tsx index 05d65e0f4..3ca52bfdb 100644 --- a/src/commons/mail-message-renderer/html-message-renderer.tsx +++ b/src/commons/mail-message-renderer/html-message-renderer.tsx @@ -18,7 +18,7 @@ import { getAttachmentParts } from '../../helpers/attachments'; import { getNoIdentityPlaceholder } from '../../helpers/identities'; import { useAppDispatch } from '../../hooks/redux'; import { getFullMsgAsyncThunk } from '../../store/actions'; -import { retrieveFullMessage } from '../../store/zustand/search/hooks/hooks'; +import { retrieveFullMessage } from '../../store/zustand/emails/hooks/hooks'; import { BodyPart, MailMessage } from '../../types'; import { useInSearchModule } from '../../ui-actions/utils'; import { getOriginalHtmlContent, getQuotedTextFromOriginalContent } from '../get-quoted-text-util'; diff --git a/src/commons/mail-message-renderer/tests/html-message-renderer.test.tsx b/src/commons/mail-message-renderer/tests/html-message-renderer.test.tsx index c4a1a902e..10e4f9188 100644 --- a/src/commons/mail-message-renderer/tests/html-message-renderer.test.tsx +++ b/src/commons/mail-message-renderer/tests/html-message-renderer.test.tsx @@ -9,7 +9,7 @@ import { act, screen, waitFor } from '@testing-library/react'; import { createSoapAPIInterceptor } from '../../../carbonio-ui-commons/test/mocks/network/msw/create-api-interceptor'; import { setupTest } from '../../../carbonio-ui-commons/test/test-setup'; -import { updateMessages } from '../../../store/zustand/search/store'; +import { updateMessages } from '../../../store/zustand/emails/store'; import { generateCompleteMessageFromAPI } from '../../../tests/generators/api'; import { generateMessage } from '../../../tests/generators/generateMessage'; import { generateStore } from '../../../tests/generators/store'; diff --git a/src/store/conversations-slice.ts b/src/store/conversations-slice.ts index 903689fc9..9005556a5 100644 --- a/src/store/conversations-slice.ts +++ b/src/store/conversations-slice.ts @@ -177,14 +177,13 @@ function getConvRejected(state: ConversationsStateType, { meta }: any): void { state.expandedStatus[meta.arg.conversationId] = meta.requestStatus; } -export const getConversationsSliceInitialState = (): ConversationsStateType => - ({ - currentFolder: FOLDERS.INBOX, - conversations: {}, - expandedStatus: {}, - searchedInFolder: {}, - searchRequestStatus: null - }) as Convhttps://zextras.atlassian.net/browse/CO-1725ersationsStateType; +export const getConversationsSliceInitialState = (): ConversationsStateType => ({ + currentFolder: FOLDERS.INBOX, + conversations: {}, + expandedStatus: {}, + searchedInFolder: {}, + searchRequestStatus: null +}); export const conversationsSlice = createSlice({ name: 'conversations', diff --git a/src/views/app/detail-panel/conversation-message-preview.tsx b/src/views/app/detail-panel/conversation-message-preview.tsx index d3ab61060..528547a1a 100644 --- a/src/views/app/detail-panel/conversation-message-preview.tsx +++ b/src/views/app/detail-panel/conversation-message-preview.tsx @@ -12,7 +12,7 @@ import MailPreview from './preview/mail-preview'; import { getParentFolderId } from '../../../helpers/folders'; import { useAppSelector } from '../../../hooks/redux'; import { selectMessage } from '../../../store/messages-slice'; -import { useMessageById } from '../../../store/zustand/search/store'; +import { useMessageById } from '../../../store/zustand/emails/store'; import { ConvMessage, MailsStateType } from '../../../types'; import { useInSearchModule } from '../../../ui-actions/utils'; diff --git a/src/views/search/extra-window/conversations/search-conversation-extra-window-panel.tsx b/src/views/search/extra-window/conversations/search-conversation-extra-window-panel.tsx index caad17e58..4dc317a07 100644 --- a/src/views/search/extra-window/conversations/search-conversation-extra-window-panel.tsx +++ b/src/views/search/extra-window/conversations/search-conversation-extra-window-panel.tsx @@ -10,7 +10,7 @@ import { useUserSettings } from '@zextras/carbonio-shell-ui'; import { map } from 'lodash'; import { API_REQUEST_STATUS } from '../../../../constants'; -import { useCompleteConversation } from '../../../../store/zustand/search/hooks/hooks'; +import { useCompleteConversation } from '../../../../store/zustand/emails/hooks/hooks'; import { ConversationMessagePreview } from '../../../app/detail-panel/conversation-message-preview'; import { useExtraWindow } from '../../../app/extra-windows/use-extra-window'; import { SearchExtraWindowPanelHeader } from '../search-extra-window-panel-header'; diff --git a/src/views/search/list/conversation/search-conversation-list-item.tsx b/src/views/search/list/conversation/search-conversation-list-item.tsx index 18cab8df5..d2fe897eb 100644 --- a/src/views/search/list/conversation/search-conversation-list-item.tsx +++ b/src/views/search/list/conversation/search-conversation-list-item.tsx @@ -28,12 +28,12 @@ import { participantToString } from '../../../../commons/utils'; import { API_REQUEST_STATUS } from '../../../../constants'; import { useConvPreviewOnSeparatedWindowFn } from '../../../../hooks/actions/use-conv-preview-on-separated-window'; import { useConvSetReadFn } from '../../../../hooks/actions/use-conv-set-read'; -import { retrieveConversation } from '../../../../store/zustand/search/hooks/hooks'; +import { retrieveConversation } from '../../../../store/zustand/emails/hooks/hooks'; import { useConversationById, useConversationMessages, useConversationStatus -} from '../../../../store/zustand/search/store'; +} from '../../../../store/zustand/emails/store'; import type { Conversation, Participant, TextReadValuesProps } from '../../../../types'; import { ConversationListItemActionWrapper } from '../../../app/folder-panel/conversations/conversation-list-item'; import { ItemAvatar } from '../../../app/folder-panel/parts/item-avatar'; diff --git a/src/views/search/list/message/search-message-list-item.tsx b/src/views/search/list/message/search-message-list-item.tsx index c24bb6fbc..9bce7ea7b 100644 --- a/src/views/search/list/message/search-message-list-item.tsx +++ b/src/views/search/list/message/search-message-list-item.tsx @@ -27,7 +27,7 @@ import { getTimeLabel, participantToString } from '../../../../commons/utils'; import { EditViewActions } from '../../../../constants'; import { useMsgPreviewOnSeparatedWindowFn } from '../../../../hooks/actions/use-msg-preview-on-separated-window'; import { useMsgSetReadFn } from '../../../../hooks/actions/use-msg-set-read'; -import { useMessageById } from '../../../../store/zustand/search/store'; +import { useMessageById } from '../../../../store/zustand/emails/store'; import { TextReadValuesType } from '../../../../types'; import { useTagExist } from '../../../../ui-actions/tag-actions'; import { createEditBoard } from '../../../app/detail-panel/edit/edit-view-board'; diff --git a/src/views/search/panel/conversation/search-conversation-message-panel.tsx b/src/views/search/panel/conversation/search-conversation-message-panel.tsx index 4a15d743e..d079cb691 100644 --- a/src/views/search/panel/conversation/search-conversation-message-panel.tsx +++ b/src/views/search/panel/conversation/search-conversation-message-panel.tsx @@ -7,7 +7,7 @@ import React, { useCallback } from 'react'; import { Padding } from '@zextras/carbonio-design-system'; -import { useMessageById } from '../../../../store/zustand/search/store'; +import { useMessageById } from '../../../../store/zustand/emails/store'; import MailPreview from '../../../app/detail-panel/preview/mail-preview'; export type SearchConversationMessagePreviewProps = { diff --git a/src/views/search/panel/conversation/search-conversation-panel.tsx b/src/views/search/panel/conversation/search-conversation-panel.tsx index 6771fa292..ae760709e 100644 --- a/src/views/search/panel/conversation/search-conversation-panel.tsx +++ b/src/views/search/panel/conversation/search-conversation-panel.tsx @@ -12,7 +12,7 @@ import { useParams } from 'react-router-dom'; import { SearchConversationMessagePanel } from './search-conversation-message-panel'; import { API_REQUEST_STATUS } from '../../../../constants'; -import { useCompleteConversation } from '../../../../store/zustand/search/hooks/hooks'; +import { useCompleteConversation } from '../../../../store/zustand/emails/hooks/hooks'; import { useExtraWindow } from '../../../app/extra-windows/use-extra-window'; import { SearchExtraWindowPanelHeader } from '../../extra-window/search-extra-window-panel-header'; diff --git a/src/views/search/panel/message/search-message-panel.tsx b/src/views/search/panel/message/search-message-panel.tsx index 014e21573..a8028fa99 100644 --- a/src/views/search/panel/message/search-message-panel.tsx +++ b/src/views/search/panel/message/search-message-panel.tsx @@ -9,7 +9,7 @@ import { Container, Padding } from '@zextras/carbonio-design-system'; import { replaceHistory } from '@zextras/carbonio-shell-ui'; import { API_REQUEST_STATUS } from '../../../../constants'; -import { useCompleteMessage } from '../../../../store/zustand/search/hooks/hooks'; +import { useCompleteMessage } from '../../../../store/zustand/emails/hooks/hooks'; import MailPreview from '../../../app/detail-panel/preview/mail-preview'; import { useExtraWindow } from '../../../app/extra-windows/use-extra-window'; import { SearchExtraWindowPanelHeader } from '../../extra-window/search-extra-window-panel-header'; diff --git a/src/views/search/panel/message/tests/search-message-panel.test.tsx b/src/views/search/panel/message/tests/search-message-panel.test.tsx index ad3292a8c..e22ecf098 100644 --- a/src/views/search/panel/message/tests/search-message-panel.test.tsx +++ b/src/views/search/panel/message/tests/search-message-panel.test.tsx @@ -12,7 +12,7 @@ import { API_REQUEST_STATUS } from '../../../../../constants'; import { setSearchResultsByMessage, updateMessageStatus -} from '../../../../../store/zustand/search/store'; +} from '../../../../../store/zustand/emails/store'; import { generateMessage } from '../../../../../tests/generators/generateMessage'; import { generateStore } from '../../../../../tests/generators/store'; import { SearchMessagePanel } from '../search-message-panel'; diff --git a/src/views/search/search-view-hooks.ts b/src/views/search/search-view-hooks.ts index 6ca94749f..4819dba0b 100644 --- a/src/views/search/search-view-hooks.ts +++ b/src/views/search/search-view-hooks.ts @@ -29,7 +29,7 @@ import { useSearchResults, resetSearch, setSearchResultsByMessage -} from '../../store/zustand/search/store'; +} from '../../store/zustand/emails/store'; import { IncompleteMessage, MailMessage, SearchResponse, SearchSliceState } from '../../types'; type UseRunSearchProps = { diff --git a/src/views/search/test/search-view-hooks.test.ts b/src/views/search/test/search-view-hooks.test.ts index 6b49b1fe7..abdb9942d 100644 --- a/src/views/search/test/search-view-hooks.test.ts +++ b/src/views/search/test/search-view-hooks.test.ts @@ -18,7 +18,7 @@ import { useConversationById, useMessageById, useSearchResults -} from '../../../store/zustand/search/store'; +} from '../../../store/zustand/emails/store'; import { generateConvMessageFromAPI } from '../../../tests/generators/api'; import { generateConversation } from '../../../tests/generators/generateConversation'; import { SearchRequest, SearchResponse, SoapConversation } from '../../../types'; diff --git a/src/views/search/test/search-view.test.tsx b/src/views/search/test/search-view.test.tsx index e2ea546ed..e828a56d5 100644 --- a/src/views/search/test/search-view.test.tsx +++ b/src/views/search/test/search-view.test.tsx @@ -32,7 +32,7 @@ import { setSearchResultsByConversation, updateConversationStatus, setMessagesInSearchSlice -} from '../../../store/zustand/search/store'; +} from '../../../store/zustand/emails/store'; import { TESTID_SELECTORS } from '../../../tests/constants'; import { generateConversation } from '../../../tests/generators/generateConversation'; import { generateMessage } from '../../../tests/generators/generateMessage'; diff --git a/src/views/sidebar/commons/sync-data-handler-hooks.ts b/src/views/sidebar/commons/sync-data-handler-hooks.ts index 2a9b52698..5ab81202e 100644 --- a/src/views/sidebar/commons/sync-data-handler-hooks.ts +++ b/src/views/sidebar/commons/sync-data-handler-hooks.ts @@ -40,7 +40,7 @@ import { deleteMessages, updateConversationsOnly, updateMessagesOnly -} from '../../../store/zustand/search/store'; +} from '../../../store/zustand/emails/store'; import type { Conversation, FolderState } from '../../../types'; function handleFoldersNotify( diff --git a/src/views/sidebar/sync-data-handler-hooks.test.tsx b/src/views/sidebar/sync-data-handler-hooks.test.tsx index 8bdb83b4c..98c4d7f64 100644 --- a/src/views/sidebar/sync-data-handler-hooks.test.tsx +++ b/src/views/sidebar/sync-data-handler-hooks.test.tsx @@ -29,7 +29,7 @@ import { useConversationById, useMessageById, setSearchResultsByMessage -} from '../../store/zustand/search/store'; +} from '../../store/zustand/emails/store'; import { generateConversation } from '../../tests/generators/generateConversation'; import { generateMessage } from '../../tests/generators/generateMessage'; import { generateStore } from '../../tests/generators/store'; From edde5064b121834be61e56880ccb2e6abd36e593 Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 11:50:17 +0100 Subject: [PATCH 006/165] refactor: restructure emails store moved slices into separate folders --- .../zustand/emails/{ => messages}/messages-slice.ts | 2 +- .../{ => populated-items}/populated-items-slice.ts | 4 ++-- src/store/zustand/emails/{ => search}/search-slice.ts | 4 ++-- src/store/zustand/emails/store.ts | 9 ++++++--- 4 files changed, 11 insertions(+), 8 deletions(-) rename src/store/zustand/emails/{ => messages}/messages-slice.ts (94%) rename src/store/zustand/emails/{ => populated-items}/populated-items-slice.ts (95%) rename src/store/zustand/emails/{ => search}/search-slice.ts (73%) diff --git a/src/store/zustand/emails/messages-slice.ts b/src/store/zustand/emails/messages/messages-slice.ts similarity index 94% rename from src/store/zustand/emails/messages-slice.ts rename to src/store/zustand/emails/messages/messages-slice.ts index dbf759054..a641ea748 100644 --- a/src/store/zustand/emails/messages-slice.ts +++ b/src/store/zustand/emails/messages/messages-slice.ts @@ -6,7 +6,7 @@ import { StateCreator } from 'zustand'; -import { MessageSliceState, PopulatedItemsSliceState, SearchSliceState } from '../../../types'; +import { MessageSliceState, PopulatedItemsSliceState, SearchSliceState } from '../../../../types'; export const MESSAGES_INITIAL_STATE: MessageSliceState['messages'] = { messageIds: new Set(), diff --git a/src/store/zustand/emails/populated-items-slice.ts b/src/store/zustand/emails/populated-items/populated-items-slice.ts similarity index 95% rename from src/store/zustand/emails/populated-items-slice.ts rename to src/store/zustand/emails/populated-items/populated-items-slice.ts index 7419b1ba2..0e01b21fa 100644 --- a/src/store/zustand/emails/populated-items-slice.ts +++ b/src/store/zustand/emails/populated-items/populated-items-slice.ts @@ -7,7 +7,7 @@ import { StateCreator } from 'zustand'; -import { PopulatedItemsSliceState, SearchSliceState } from '../../../types'; +import { PopulatedItemsSliceState, SearchSliceState } from '../../../../types'; export const POPULATED_ITEMS_INITIAL_STATE = { messages: {}, @@ -20,6 +20,6 @@ export const createPopulatedItemsSlice: StateCreator< [], [], PopulatedItemsSliceState -> = (set) => ({ +> = () => ({ populatedItems: POPULATED_ITEMS_INITIAL_STATE }); diff --git a/src/store/zustand/emails/search-slice.ts b/src/store/zustand/emails/search/search-slice.ts similarity index 73% rename from src/store/zustand/emails/search-slice.ts rename to src/store/zustand/emails/search/search-slice.ts index 8fb002a2b..85b9aa102 100644 --- a/src/store/zustand/emails/search-slice.ts +++ b/src/store/zustand/emails/search/search-slice.ts @@ -6,7 +6,7 @@ import { StateCreator } from 'zustand'; -import { PopulatedItemsSliceState, SearchSliceState } from '../../../types'; +import { MessageSliceState, PopulatedItemsSliceState, SearchSliceState } from '../../../../types'; export const SEARCH_INITIAL_STATE: SearchSliceState['search'] = { conversationIds: new Set(), @@ -16,7 +16,7 @@ export const SEARCH_INITIAL_STATE: SearchSliceState['search'] = { status: null }; export const createSearchSlice: StateCreator< - SearchSliceState & PopulatedItemsSliceState, + SearchSliceState & PopulatedItemsSliceState & MessageSliceState, [], [], SearchSliceState diff --git a/src/store/zustand/emails/store.ts b/src/store/zustand/emails/store.ts index d2084d587..b2a455711 100644 --- a/src/store/zustand/emails/store.ts +++ b/src/store/zustand/emails/store.ts @@ -9,9 +9,12 @@ import produce, { enableMapSet } from 'immer'; import { merge } from 'lodash'; import { create } from 'zustand'; -import { createMessageSlice } from './messages-slice'; -import { createPopulatedItemsSlice, POPULATED_ITEMS_INITIAL_STATE } from './populated-items-slice'; -import { createSearchSlice, SEARCH_INITIAL_STATE } from './search-slice'; +import { createMessageSlice } from './messages/messages-slice'; +import { + createPopulatedItemsSlice, + POPULATED_ITEMS_INITIAL_STATE +} from './populated-items/populated-items-slice'; +import { createSearchSlice, SEARCH_INITIAL_STATE } from './search/search-slice'; import { API_REQUEST_STATUS } from '../../../constants'; import { IncompleteMessage, From 63e8f58818adb1f0e9f5f907515b6f69c4a6e924 Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 13:06:50 +0100 Subject: [PATCH 007/165] refactor: move some store functions to a dedicated folder for readibility --- src/store/zustand/emails/search/hooks.ts | 97 +++++++++++++++++++ src/store/zustand/emails/store.test.ts | 8 +- src/store/zustand/emails/store.ts | 97 +++++-------------- src/views/search/search-view-hooks.ts | 8 +- src/views/search/test/search-view.test.tsx | 4 +- .../sidebar/sync-data-handler-hooks.test.tsx | 4 +- 6 files changed, 135 insertions(+), 83 deletions(-) create mode 100644 src/store/zustand/emails/search/hooks.ts diff --git a/src/store/zustand/emails/search/hooks.ts b/src/store/zustand/emails/search/hooks.ts new file mode 100644 index 000000000..b8fc2eab7 --- /dev/null +++ b/src/store/zustand/emails/search/hooks.ts @@ -0,0 +1,97 @@ +/* eslint-disable no-param-reassign */ +/* + * SPDX-FileCopyrightText: 2024 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import produce, { enableMapSet } from 'immer'; +import { UseBoundStore, StoreApi } from 'zustand'; + +import { SEARCH_INITIAL_STATE } from './search-slice'; +import { API_REQUEST_STATUS } from '../../../../constants'; +import { + EmailsStoreState, + IncompleteMessage, + MailMessage, + NormalizedConversation +} from '../../../../types'; +import { POPULATED_ITEMS_INITIAL_STATE } from '../populated-items/populated-items-slice'; + +export function resetSearchAndPopulatedItemsHook( + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce((state: EmailsStoreState) => { + state.search = SEARCH_INITIAL_STATE; + state.populatedItems = POPULATED_ITEMS_INITIAL_STATE; + }) + ); +} + +export function setSearchResultsByConversationHook( + conversations: Array, + more: boolean, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ search, populatedItems }) => { + search.conversationIds = new Set(conversations.map((c) => c.id)); + search.status = API_REQUEST_STATUS.fulfilled; + search.messageIds = new Set(); + search.offset = 0; + search.more = more; + populatedItems.conversations = conversations.reduce( + (acc, conv) => { + acc[conv.id] = conv; + return acc; + }, + {} as Record + ); + }) + ); +} + +export function setSearchResultsByMessageHook( + messages: Array, + more: boolean, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ search, populatedItems }) => { + search.messageIds = new Set(messages.map((message) => message.id)); + search.status = API_REQUEST_STATUS.fulfilled; + search.conversationIds = new Set(); + search.offset = 0; + search.more = more; + populatedItems.messages = messages.reduce( + (acc, message) => { + acc[message.id] = message; + return acc; + }, + {} as Record + ); + }) + ); +} +export function appendConversationsHook( + conversations: Array, + offset: number, + more: boolean, + useEmailsStore: UseBoundStore> +): void { + enableMapSet(); + const newConversationsIds = new Set(conversations.map((c) => c.id)); + + useEmailsStore.setState( + produce((state: EmailsStoreState) => { + newConversationsIds.forEach((id) => state.search.conversationIds.add(id)); + state.search.offset = offset; + state.search.more = more; + state.populatedItems.conversations = conversations.reduce((acc, conv) => { + acc[conv.id] = conv; + return acc; + }, state.populatedItems.conversations); + }) + ); +} diff --git a/src/store/zustand/emails/store.test.ts b/src/store/zustand/emails/store.test.ts index e752faebe..d53efdfb8 100644 --- a/src/store/zustand/emails/store.test.ts +++ b/src/store/zustand/emails/store.test.ts @@ -8,9 +8,8 @@ import { enableMapSet } from 'immer'; import { removeMessages, - resetSearch, + resetSearchAndPopulatedItems, updateMessages, - setSearchResultsByConversation, updateConversationStatus, setMessagesInSearchSlice, updateMessagesOnly, @@ -28,7 +27,8 @@ import { useMessageStatus, setSearchResultsByMessage, deleteMessages, - updateConversationsOnly + updateConversationsOnly, + setSearchResultsByConversation } from './store'; import { FOLDERS } from '../../../carbonio-ui-commons/constants/folders'; import { API_REQUEST_STATUS } from '../../../constants'; @@ -95,7 +95,7 @@ describe('message store', () => { updateConversationStatus('1', API_REQUEST_STATUS.fulfilled); setMessagesInSearchSlice([generateMessage({ id: '100' })]); - resetSearch(); + resetSearchAndPopulatedItems(); expect(renderHook(() => useConversationById('1')).result.current).toBeUndefined(); expect(renderHook(() => useConversationStatus('1')).result.current).toBeUndefined(); diff --git a/src/store/zustand/emails/store.ts b/src/store/zustand/emails/store.ts index b2a455711..42d2b3a5c 100644 --- a/src/store/zustand/emails/store.ts +++ b/src/store/zustand/emails/store.ts @@ -10,12 +10,14 @@ import { merge } from 'lodash'; import { create } from 'zustand'; import { createMessageSlice } from './messages/messages-slice'; +import { createPopulatedItemsSlice } from './populated-items/populated-items-slice'; import { - createPopulatedItemsSlice, - POPULATED_ITEMS_INITIAL_STATE -} from './populated-items/populated-items-slice'; -import { createSearchSlice, SEARCH_INITIAL_STATE } from './search/search-slice'; -import { API_REQUEST_STATUS } from '../../../constants'; + appendConversationsHook, + resetSearchAndPopulatedItemsHook, + setSearchResultsByConversationHook, + setSearchResultsByMessageHook +} from './search/hooks'; +import { createSearchSlice } from './search/search-slice'; import { IncompleteMessage, MailMessage, @@ -32,27 +34,23 @@ const useEmailsStore = create()((...a) => ({ ...createPopulatedItemsSlice(...a) })); -export function useConversationById(id: string): NormalizedConversation { - return useEmailsStore((state) => state.populatedItems.conversations[id]); -} - -export function resetSearch(): void { - useEmailsStore.setState( - produce((state: EmailsStoreState) => { - state.search = SEARCH_INITIAL_STATE; - state.populatedItems = POPULATED_ITEMS_INITIAL_STATE; - }) - ); -} - -export function useMessageById(id: string): IncompleteMessage | MailMessage { - return useEmailsStore((state) => state.populatedItems.messages[id]); +// Search related functions +export function resetSearchAndPopulatedItems(): void { + resetSearchAndPopulatedItemsHook(useEmailsStore); } export function useSearchResults(): SearchSliceState['search'] { return useEmailsStore(({ search }) => search); } +export function setSearchResultsByConversation( + conversations: Array, + more: boolean +): void { + setSearchResultsByConversationHook(conversations, more, useEmailsStore); +} + +// Populated Items related functions export function useConversationMessages( conversationId: string ): Array { @@ -65,52 +63,22 @@ export function useConversationMessages( ); return messages; } +export function useConversationById(id: string): NormalizedConversation { + return useEmailsStore((state) => state.populatedItems.conversations[id]); +} +export function useMessageById(id: string): IncompleteMessage | MailMessage { + return useEmailsStore((state) => state.populatedItems.messages[id]); +} export function useConversationStatus(id: string): SearchRequestStatus { return useEmailsStore((state) => state.populatedItems.conversationsStatus?.[id]); } -export function setSearchResultsByConversation( - conversations: Array, - more: boolean -): void { - useEmailsStore.setState( - produce(({ search, populatedItems }) => { - search.conversationIds = new Set(conversations.map((c) => c.id)); - search.status = API_REQUEST_STATUS.fulfilled; - search.messageIds = new Set(); - search.offset = 0; - search.more = more; - populatedItems.conversations = conversations.reduce( - (acc, conv) => { - acc[conv.id] = conv; - return acc; - }, - {} as Record - ); - }) - ); -} export function setSearchResultsByMessage( messages: Array, more: boolean ): void { - useEmailsStore.setState( - produce(({ search, populatedItems }) => { - search.messageIds = new Set(messages.map((message) => message.id)); - search.status = API_REQUEST_STATUS.fulfilled; - search.conversationIds = new Set(); - search.offset = 0; - search.more = more; - populatedItems.messages = messages.reduce( - (acc, message) => { - acc[message.id] = message; - return acc; - }, - {} as Record - ); - }) - ); + setSearchResultsByMessageHook(messages, more, useEmailsStore); } export function appendConversations( @@ -118,20 +86,7 @@ export function appendConversations( offset: number, more: boolean ): void { - enableMapSet(); - const newConversationsIds = new Set(conversations.map((c) => c.id)); - - useEmailsStore.setState( - produce((state: EmailsStoreState) => { - newConversationsIds.forEach((id) => state.search.conversationIds.add(id)); - state.search.offset = offset; - state.search.more = more; - state.populatedItems.conversations = conversations.reduce((acc, conv) => { - acc[conv.id] = conv; - return acc; - }, state.populatedItems.conversations); - }) - ); + appendConversationsHook(conversations, offset, more, useEmailsStore); } export function updateConversationsOnly(conversations: Array): void { diff --git a/src/views/search/search-view-hooks.ts b/src/views/search/search-view-hooks.ts index 4819dba0b..9434c68b4 100644 --- a/src/views/search/search-view-hooks.ts +++ b/src/views/search/search-view-hooks.ts @@ -24,11 +24,11 @@ import { normalizeMailMessageFromSoap } from '../../normalizations/normalize-mes import { appendConversations, appendMessages, - setSearchResultsByConversation, updateSearchResultsLoadingStatus, useSearchResults, - resetSearch, - setSearchResultsByMessage + resetSearchAndPopulatedItems, + setSearchResultsByMessage, + setSearchResultsByConversation } from '../../store/zustand/emails/store'; import { IncompleteMessage, MailMessage, SearchResponse, SearchSliceState } from '../../types'; @@ -118,7 +118,7 @@ export function handleSearchResults({ handleFulFilledMessagesResults({ searchResponse, tags }); } if (searchResponse && !searchResponse.c && !searchResponse.m) { - resetSearch(); + resetSearchAndPopulatedItems(); updateSearchResultsLoadingStatus(API_REQUEST_STATUS.fulfilled); } } diff --git a/src/views/search/test/search-view.test.tsx b/src/views/search/test/search-view.test.tsx index e828a56d5..c28cf3aa3 100644 --- a/src/views/search/test/search-view.test.tsx +++ b/src/views/search/test/search-view.test.tsx @@ -29,9 +29,9 @@ import { API_REQUEST_STATUS } from '../../../constants'; import * as useSelection from '../../../hooks/use-selection'; import * as search from '../../../store/actions/search'; import { - setSearchResultsByConversation, updateConversationStatus, - setMessagesInSearchSlice + setMessagesInSearchSlice, + setSearchResultsByConversation } from '../../../store/zustand/emails/store'; import { TESTID_SELECTORS } from '../../../tests/constants'; import { generateConversation } from '../../../tests/generators/generateConversation'; diff --git a/src/views/sidebar/sync-data-handler-hooks.test.tsx b/src/views/sidebar/sync-data-handler-hooks.test.tsx index 98c4d7f64..449916a39 100644 --- a/src/views/sidebar/sync-data-handler-hooks.test.tsx +++ b/src/views/sidebar/sync-data-handler-hooks.test.tsx @@ -24,11 +24,11 @@ import { tags } from '../../carbonio-ui-commons/test/mocks/tags/tags'; import { folderWorker } from '../../carbonio-ui-commons/worker'; import * as reduxHooks from '../../hooks/redux'; import { - setSearchResultsByConversation, setMessagesInSearchSlice, useConversationById, useMessageById, - setSearchResultsByMessage + setSearchResultsByMessage, + setSearchResultsByConversation } from '../../store/zustand/emails/store'; import { generateConversation } from '../../tests/generators/generateConversation'; import { generateMessage } from '../../tests/generators/generateMessage'; From a128ee13553e75357cb3e62c2b3cebb18ab9e23d Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 15:45:07 +0100 Subject: [PATCH 008/165] refactor: move store functions to a dedicated folder --- .../zustand/emails/populated-items/hooks.ts | 109 ++++++++ src/store/zustand/emails/search/hooks.ts | 89 ++++++- src/store/zustand/emails/store.test.ts | 12 +- src/store/zustand/emails/store.ts | 236 +++++------------- src/views/search/search-view-hooks.ts | 6 +- .../commons/sync-data-handler-hooks.ts | 8 +- 6 files changed, 275 insertions(+), 185 deletions(-) create mode 100644 src/store/zustand/emails/populated-items/hooks.ts diff --git a/src/store/zustand/emails/populated-items/hooks.ts b/src/store/zustand/emails/populated-items/hooks.ts new file mode 100644 index 000000000..724444c4f --- /dev/null +++ b/src/store/zustand/emails/populated-items/hooks.ts @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2024 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +/* eslint-disable no-param-reassign */ + +import produce from 'immer'; +import { merge } from 'lodash'; +import { UseBoundStore, StoreApi } from 'zustand'; + +import { + MailMessage, + IncompleteMessage, + EmailsStoreState, + NormalizedConversation, + PopulatedItemsSliceState, + SearchRequestStatus +} from '../../../../types'; + +export function useConversationMessagesHook( + conversationId: string, + useEmailsStore: UseBoundStore> +): Array { + const messages: Array = []; + useEmailsStore(({ populatedItems }: EmailsStoreState) => + populatedItems.conversations[conversationId].messages.forEach((message) => { + if (populatedItems.messages[message.id]) messages.push(populatedItems.messages[message.id]); + }) + ); + return messages; +} +export function updateConversationsOnlyHook( + conversations: Array, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ populatedItems }: PopulatedItemsSliceState) => { + conversations.forEach((conversation) => { + populatedItems.conversations[conversation.id] = { + ...merge(populatedItems.conversations[conversation.id], conversation), + tags: conversation.tags + }; + }); + }) + ); +} +export function updateMessagesOnlyHook( + messages: Array, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ populatedItems }: PopulatedItemsSliceState) => { + messages.forEach((message) => { + populatedItems.messages[message.id] = { + ...merge(populatedItems.messages[message.id], message), + tags: message.tags + }; + }); + }) + ); +} + +export function updateMessagesHook( + messages: MailMessage[], + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ populatedItems }: PopulatedItemsSliceState) => { + messages.forEach((message) => { + populatedItems.messages[message.id] = message; + }); + }) + ); +} +export function updateConversationStatusHook( + conversationId: string, + status: SearchRequestStatus, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ populatedItems }: PopulatedItemsSliceState) => { + populatedItems.conversationsStatus[conversationId] = status; + }) + ); +} +export function updateMessageStatusHook( + messageId: string, + status: SearchRequestStatus, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ populatedItems }: PopulatedItemsSliceState) => { + populatedItems.messagesStatus[messageId] = status; + }) + ); +} +export function removeMessagesHook( + messageIds: Array, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ populatedItems }: EmailsStoreState) => { + messageIds.forEach((messageId) => { + delete populatedItems.messages[messageId]; + }); + }) + ); +} diff --git a/src/store/zustand/emails/search/hooks.ts b/src/store/zustand/emails/search/hooks.ts index b8fc2eab7..51d8559c5 100644 --- a/src/store/zustand/emails/search/hooks.ts +++ b/src/store/zustand/emails/search/hooks.ts @@ -14,7 +14,9 @@ import { EmailsStoreState, IncompleteMessage, MailMessage, - NormalizedConversation + NormalizedConversation, + SearchRequestStatus, + SearchSliceState } from '../../../../types'; import { POPULATED_ITEMS_INITIAL_STATE } from '../populated-items/populated-items-slice'; @@ -74,7 +76,7 @@ export function setSearchResultsByMessageHook( }) ); } -export function appendConversationsHook( +export function appendConversationsToSearchHook( conversations: Array, offset: number, more: boolean, @@ -95,3 +97,86 @@ export function appendConversationsHook( }) ); } + +export function deleteConversationsFromSearchHook( + ids: Array, + useEmailsStore: UseBoundStore> +): void { + enableMapSet(); + useEmailsStore.setState( + produce((state: EmailsStoreState) => { + ids.forEach((id) => { + state.search.conversationIds.delete(id); + delete state.populatedItems.conversations[id]; + }); + }) + ); +} + +export function deleteMessagesFromSearchHook( + ids: Array, + useEmailsStore: UseBoundStore> +): void { + enableMapSet(); + useEmailsStore.setState( + produce((state: EmailsStoreState) => { + ids.forEach((id) => { + state.search.messageIds.delete(id); + delete state.populatedItems.messages[id]; + }); + }) + ); +} + +export function updateSearchResultsLoadingStatusHook( + status: SearchRequestStatus, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce(({ search }: SearchSliceState) => { + search.status = status; + }) + ); +} + +export function appendMessagesToSearchHook( + messages: Array, + offset: number, + useEmailsStore: UseBoundStore> +): void { + enableMapSet(); + const newMessageIds = new Set(messages.map((message) => message.id)); + useEmailsStore.setState( + produce((state: EmailsStoreState) => { + newMessageIds.forEach((messageId) => state.search.messageIds.add(messageId)); + state.search.offset = offset; + state.populatedItems.messages = messages.reduce((acc, msg) => { + acc[msg.id] = msg; + return acc; + }, state.populatedItems.messages); + }) + ); +} + +export function setMessagesInSearchSliceHook( + messages: Array, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState((state: EmailsStoreState) => ({ + search: { + ...state.search, + messageIds: new Set(messages.map((c) => c.id)) + }, + populatedItems: { + ...state.populatedItems, + offset: 0, + messages: messages.reduce( + (acc, msg) => { + acc[msg.id] = msg; + return acc; + }, + {} as Record + ) + } + })); +} diff --git a/src/store/zustand/emails/store.test.ts b/src/store/zustand/emails/store.test.ts index d53efdfb8..a894c3a00 100644 --- a/src/store/zustand/emails/store.test.ts +++ b/src/store/zustand/emails/store.test.ts @@ -18,15 +18,15 @@ import { useConversationStatus, useMessageById, appendConversations, - appendMessages, + appendMessagesToSearch, getSearchResultsLoadingStatus, updateSearchResultsLoadingStatus, - deleteConversations, + deleteConversationsFromSearch, useSearchResults, updateMessageStatus, useMessageStatus, setSearchResultsByMessage, - deleteMessages, + deleteMessagesFromSearch, updateConversationsOnly, setSearchResultsByConversation } from './store'; @@ -143,7 +143,7 @@ describe('message store', () => { setSearchResultsByConversation([conversation1, conversation2], false); setMessagesInSearchSlice([...conversation1Messages, ...conversation2Messages]); - deleteConversations(['1']); + deleteConversationsFromSearch(['1']); const { result } = renderHook(() => useSearchResults()); const { result: conversation1Store } = renderHook(() => useConversationById('1')); @@ -225,7 +225,7 @@ describe('message store', () => { enableMapSet(); setMessagesInSearchSlice([generateMessage({ id: '1' })]); - appendMessages([generateMessage({ id: '2' }), generateMessage({ id: '3' })], 0); + appendMessagesToSearch([generateMessage({ id: '2' }), generateMessage({ id: '3' })], 0); expect(renderHook(() => useMessageById('1')).result.current).toBeDefined(); expect(renderHook(() => useMessageById('2')).result.current).toBeDefined(); @@ -241,7 +241,7 @@ describe('message store', () => { setSearchResultsByMessage(messages, false); setMessagesInSearchSlice(messages); - deleteMessages(['1', '2']); + deleteMessagesFromSearch(['1', '2']); const { result } = renderHook(() => useSearchResults()); const { result: message1 } = renderHook(() => useMessageById('1')); diff --git a/src/store/zustand/emails/store.ts b/src/store/zustand/emails/store.ts index 42d2b3a5c..d46c663f5 100644 --- a/src/store/zustand/emails/store.ts +++ b/src/store/zustand/emails/store.ts @@ -5,17 +5,29 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import produce, { enableMapSet } from 'immer'; -import { merge } from 'lodash'; import { create } from 'zustand'; import { createMessageSlice } from './messages/messages-slice'; +import { + removeMessagesHook, + updateConversationsOnlyHook, + updateConversationStatusHook, + updateMessagesHook, + updateMessagesOnlyHook, + updateMessageStatusHook, + useConversationMessagesHook +} from './populated-items/hooks'; import { createPopulatedItemsSlice } from './populated-items/populated-items-slice'; import { - appendConversationsHook, + appendConversationsToSearchHook, + appendMessagesToSearchHook, + deleteConversationsFromSearchHook, + deleteMessagesFromSearchHook, resetSearchAndPopulatedItemsHook, + setMessagesInSearchSliceHook, setSearchResultsByConversationHook, - setSearchResultsByMessageHook + setSearchResultsByMessageHook, + updateSearchResultsLoadingStatusHook } from './search/hooks'; import { createSearchSlice } from './search/search-slice'; import { @@ -23,7 +35,6 @@ import { MailMessage, EmailsStoreState, NormalizedConversation, - PopulatedItemsSliceState, SearchRequestStatus, SearchSliceState } from '../../../types'; @@ -34,11 +45,18 @@ const useEmailsStore = create()((...a) => ({ ...createPopulatedItemsSlice(...a) })); -// Search related functions +// ################################ +// ##### Search related functions +// ################################ export function resetSearchAndPopulatedItems(): void { resetSearchAndPopulatedItemsHook(useEmailsStore); } - +export function setSearchResultsByMessage( + messages: Array, + more: boolean +): void { + setSearchResultsByMessageHook(messages, more, useEmailsStore); +} export function useSearchResults(): SearchSliceState['search'] { return useEmailsStore(({ search }) => search); } @@ -49,203 +67,81 @@ export function setSearchResultsByConversation( ): void { setSearchResultsByConversationHook(conversations, more, useEmailsStore); } - -// Populated Items related functions -export function useConversationMessages( - conversationId: string -): Array { - const messages: Array = []; - useEmailsStore((state: EmailsStoreState) => - state.populatedItems.conversations[conversationId].messages.forEach((message) => { - if (state.populatedItems.messages[message.id]) - messages.push(state.populatedItems.messages[message.id]); - }) - ); - return messages; +export function appendConversations( + conversations: Array, + offset: number, + more: boolean +): void { + appendConversationsToSearchHook(conversations, offset, more, useEmailsStore); } -export function useConversationById(id: string): NormalizedConversation { - return useEmailsStore((state) => state.populatedItems.conversations[id]); +export function deleteConversationsFromSearch(ids: Array): void { + deleteConversationsFromSearchHook(ids, useEmailsStore); } - -export function useMessageById(id: string): IncompleteMessage | MailMessage { - return useEmailsStore((state) => state.populatedItems.messages[id]); +export function deleteMessagesFromSearch(ids: Array): void { + deleteMessagesFromSearchHook(ids, useEmailsStore); } -export function useConversationStatus(id: string): SearchRequestStatus { - return useEmailsStore((state) => state.populatedItems.conversationsStatus?.[id]); +export function getSearchResultsLoadingStatus(): SearchRequestStatus { + return useEmailsStore.getState().search.status; } -export function setSearchResultsByMessage( +export function updateSearchResultsLoadingStatus(status: SearchRequestStatus): void { + updateSearchResultsLoadingStatusHook(status, useEmailsStore); +} +export function appendMessagesToSearch( messages: Array, - more: boolean + offset: number ): void { - setSearchResultsByMessageHook(messages, more, useEmailsStore); + appendMessagesToSearchHook(messages, offset, useEmailsStore); } -export function appendConversations( - conversations: Array, - offset: number, - more: boolean -): void { - appendConversationsHook(conversations, offset, more, useEmailsStore); +export function setMessagesInSearchSlice(messages: Array): void { + setMessagesInSearchSliceHook(messages, useEmailsStore); } -export function updateConversationsOnly(conversations: Array): void { - useEmailsStore.setState( - produce(({ populatedItems }: PopulatedItemsSliceState) => { - conversations.forEach((conversation) => { - populatedItems.conversations[conversation.id] = { - ...merge(populatedItems.conversations[conversation.id], conversation), - tags: conversation.tags - }; - }); - }) - ); +// ################################ +// #### Populated Items related functions +// ################################ +export function useConversationMessages( + conversationId: string +): Array { + return useConversationMessagesHook(conversationId, useEmailsStore); } - -export function deleteConversations(ids: Array): void { - enableMapSet(); - useEmailsStore.setState( - produce((state: EmailsStoreState) => { - ids.forEach((id) => { - state.search.conversationIds.delete(id); - delete state.populatedItems.conversations[id]; - }); - }) - ); +export function useConversationById(id: string): NormalizedConversation { + return useEmailsStore(({ populatedItems }) => populatedItems.conversations[id]); } -export function deleteMessages(ids: Array): void { - enableMapSet(); - useEmailsStore.setState( - produce((state: EmailsStoreState) => { - ids.forEach((id) => { - state.search.messageIds.delete(id); - delete state.populatedItems.messages[id]; - }); - }) - ); +export function useMessageById(id: string): IncompleteMessage | MailMessage { + return useEmailsStore(({ populatedItems }) => populatedItems.messages[id]); } - -export function updateMessagesOnly(messages: Array): void { - useEmailsStore.setState( - produce(({ populatedItems }: PopulatedItemsSliceState) => { - messages.forEach((message) => { - populatedItems.messages[message.id] = { - ...merge(populatedItems.messages[message.id], message), - tags: message.tags - }; - }); - }) - ); +export function useConversationStatus(id: string): SearchRequestStatus { + return useEmailsStore(({ populatedItems }) => populatedItems.conversationsStatus?.[id]); } -export function setMessagesInSearchSlice(messages: Array): void { - useEmailsStore.setState((state: EmailsStoreState) => ({ - search: { - ...state.search, - messageIds: new Set(messages.map((c) => c.id)) - }, - populatedItems: { - ...state.populatedItems, - offset: 0, - messages: messages.reduce( - (acc, msg) => { - acc[msg.id] = msg; - return acc; - }, - {} as Record - ) - } - })); +export function updateConversationsOnly(conversations: Array): void { + updateConversationsOnlyHook(conversations, useEmailsStore); } -export function setMessagesInMessagesSlice(messages: Array): void { - useEmailsStore.setState((state: EmailsStoreState) => ({ - search: { - ...state.search, - messageIds: new Set(messages.map((c) => c.id)) - }, - populatedItems: { - ...state.populatedItems, - offset: 0, - messages: messages.reduce( - (acc, msg) => { - acc[msg.id] = msg; - return acc; - }, - {} as Record - ) - } - })); -} -export function appendMessages( - messages: Array, - offset: number -): void { - enableMapSet(); - const newMessageIds = new Set(messages.map((message) => message.id)); - useEmailsStore.setState( - produce((state: EmailsStoreState) => { - newMessageIds.forEach((messageId) => state.search.messageIds.add(messageId)); - state.search.offset = offset; - state.populatedItems.messages = messages.reduce((acc, msg) => { - acc[msg.id] = msg; - return acc; - }, state.populatedItems.messages); - }) - ); +export function updateMessagesOnly(messages: Array): void { + updateMessagesOnlyHook(messages, useEmailsStore); } - export function updateMessages(messages: MailMessage[]): void { - useEmailsStore.setState( - produce(({ populatedItems }: PopulatedItemsSliceState) => { - messages.forEach((message) => { - populatedItems.messages[message.id] = message; - }); - }) - ); + updateMessagesHook(messages, useEmailsStore); } export function updateConversationStatus( conversationId: string, status: SearchRequestStatus ): void { - useEmailsStore.setState( - produce(({ populatedItems }: PopulatedItemsSliceState) => { - populatedItems.conversationsStatus[conversationId] = status; - }) - ); + updateConversationStatusHook(conversationId, status, useEmailsStore); } export function updateMessageStatus(messageId: string, status: SearchRequestStatus): void { - useEmailsStore.setState( - produce(({ populatedItems }: PopulatedItemsSliceState) => { - populatedItems.messagesStatus[messageId] = status; - }) - ); + updateMessageStatusHook(messageId, status, useEmailsStore); } export function useMessageStatus(id: string): SearchRequestStatus { return useEmailsStore((state) => state.populatedItems.messagesStatus?.[id]); } -export function updateSearchResultsLoadingStatus(status: SearchRequestStatus): void { - useEmailsStore.setState( - produce(({ search }: SearchSliceState) => { - search.status = status; - }) - ); -} - -export function getSearchResultsLoadingStatus(): SearchRequestStatus { - return useEmailsStore.getState().search.status; -} - export function removeMessages(messageIds: Array): void { - useEmailsStore.setState( - produce((state: EmailsStoreState) => { - messageIds.forEach((messageId) => { - delete state.populatedItems.messages[messageId]; - }); - }) - ); + removeMessagesHook(messageIds, useEmailsStore); } diff --git a/src/views/search/search-view-hooks.ts b/src/views/search/search-view-hooks.ts index 9434c68b4..951771ffb 100644 --- a/src/views/search/search-view-hooks.ts +++ b/src/views/search/search-view-hooks.ts @@ -23,7 +23,7 @@ import { mapToNormalizedConversation } from '../../normalizations/normalize-conv import { normalizeMailMessageFromSoap } from '../../normalizations/normalize-message'; import { appendConversations, - appendMessages, + appendMessagesToSearch, updateSearchResultsLoadingStatus, useSearchResults, resetSearchAndPopulatedItems, @@ -90,14 +90,14 @@ function handleLoadMoreResults({ ) ); appendConversations(conversations, offset, searchResponse.more); - appendMessages(messages, offset); + appendMessagesToSearch(messages, offset); } if (searchResponse.m) { const messages: (IncompleteMessage | MailMessage)[] = []; searchResponse.m?.forEach((soapMessage) => messages.push(normalizeMailMessageFromSoap(soapMessage, false)) ); - appendMessages(messages, offset); + appendMessagesToSearch(messages, offset); } } diff --git a/src/views/sidebar/commons/sync-data-handler-hooks.ts b/src/views/sidebar/commons/sync-data-handler-hooks.ts index 5ab81202e..781a5c4ef 100644 --- a/src/views/sidebar/commons/sync-data-handler-hooks.ts +++ b/src/views/sidebar/commons/sync-data-handler-hooks.ts @@ -36,8 +36,8 @@ import { selectMessages } from '../../../store/messages-slice'; import { - deleteConversations, - deleteMessages, + deleteConversationsFromSearch, + deleteMessagesFromSearch, updateConversationsOnly, updateMessagesOnly } from '../../../store/zustand/emails/store'; @@ -179,8 +179,8 @@ export const useSyncDataHandler = (): void => { } } if (notify.deleted) { - deleteConversations(notify.deleted); - deleteMessages(notify.deleted); + deleteConversationsFromSearch(notify.deleted); + deleteMessagesFromSearch(notify.deleted); dispatch(handleNotifyDeletedConversations(notify.deleted)); dispatch(handleDeletedMessages(notify.deleted)); dispatch(handleDeletedMessagesInConversation(notify.deleted)); From c9c7a13c5c7c970c41aea0eacc6a38b7d5034fc1 Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Mon, 9 Dec 2024 16:06:07 +0100 Subject: [PATCH 009/165] refactor(emails): rename hooks to utils and update imports - Renamed hooks to utils in populated-items directory - Updated function names to remove "Hook" suffix - Adjusted imports in store.ts to use populatedItemsSliceUtils - Ensured all references to hooks are updated accordingly --- .../populated-items/{hooks.ts => utils.ts} | 24 +++++++++++++------ src/store/zustand/emails/store.ts | 24 +++++++------------ 2 files changed, 25 insertions(+), 23 deletions(-) rename src/store/zustand/emails/populated-items/{hooks.ts => utils.ts} (87%) diff --git a/src/store/zustand/emails/populated-items/hooks.ts b/src/store/zustand/emails/populated-items/utils.ts similarity index 87% rename from src/store/zustand/emails/populated-items/hooks.ts rename to src/store/zustand/emails/populated-items/utils.ts index 724444c4f..692f0e986 100644 --- a/src/store/zustand/emails/populated-items/hooks.ts +++ b/src/store/zustand/emails/populated-items/utils.ts @@ -18,7 +18,7 @@ import { SearchRequestStatus } from '../../../../types'; -export function useConversationMessagesHook( +function useConversationMessages( conversationId: string, useEmailsStore: UseBoundStore> ): Array { @@ -30,7 +30,7 @@ export function useConversationMessagesHook( ); return messages; } -export function updateConversationsOnlyHook( +function updateConversationsOnly( conversations: Array, useEmailsStore: UseBoundStore> ): void { @@ -45,7 +45,7 @@ export function updateConversationsOnlyHook( }) ); } -export function updateMessagesOnlyHook( +function updateMessagesOnly( messages: Array, useEmailsStore: UseBoundStore> ): void { @@ -61,7 +61,7 @@ export function updateMessagesOnlyHook( ); } -export function updateMessagesHook( +function updateMessages( messages: MailMessage[], useEmailsStore: UseBoundStore> ): void { @@ -73,7 +73,7 @@ export function updateMessagesHook( }) ); } -export function updateConversationStatusHook( +function updateConversationStatus( conversationId: string, status: SearchRequestStatus, useEmailsStore: UseBoundStore> @@ -84,7 +84,7 @@ export function updateConversationStatusHook( }) ); } -export function updateMessageStatusHook( +function updateMessageStatus( messageId: string, status: SearchRequestStatus, useEmailsStore: UseBoundStore> @@ -95,7 +95,7 @@ export function updateMessageStatusHook( }) ); } -export function removeMessagesHook( +function removeMessages( messageIds: Array, useEmailsStore: UseBoundStore> ): void { @@ -107,3 +107,13 @@ export function removeMessagesHook( }) ); } + +export const populatedItemsSliceUtils = { + removeMessages, + updateMessageStatus, + updateConversationStatus, + updateMessages, + updateMessagesOnly, + updateConversationsOnly, + useConversationMessages +}; diff --git a/src/store/zustand/emails/store.ts b/src/store/zustand/emails/store.ts index d46c663f5..964368edd 100644 --- a/src/store/zustand/emails/store.ts +++ b/src/store/zustand/emails/store.ts @@ -8,16 +8,8 @@ import { create } from 'zustand'; import { createMessageSlice } from './messages/messages-slice'; -import { - removeMessagesHook, - updateConversationsOnlyHook, - updateConversationStatusHook, - updateMessagesHook, - updateMessagesOnlyHook, - updateMessageStatusHook, - useConversationMessagesHook -} from './populated-items/hooks'; import { createPopulatedItemsSlice } from './populated-items/populated-items-slice'; +import { populatedItemsSliceUtils } from './populated-items/utils'; import { appendConversationsToSearchHook, appendMessagesToSearchHook, @@ -104,7 +96,7 @@ export function setMessagesInSearchSlice(messages: Array { - return useConversationMessagesHook(conversationId, useEmailsStore); + return populatedItemsSliceUtils.useConversationMessages(conversationId, useEmailsStore); } export function useConversationById(id: string): NormalizedConversation { return useEmailsStore(({ populatedItems }) => populatedItems.conversations[id]); @@ -118,30 +110,30 @@ export function useConversationStatus(id: string): SearchRequestStatus { } export function updateConversationsOnly(conversations: Array): void { - updateConversationsOnlyHook(conversations, useEmailsStore); + populatedItemsSliceUtils.updateConversationsOnly(conversations, useEmailsStore); } export function updateMessagesOnly(messages: Array): void { - updateMessagesOnlyHook(messages, useEmailsStore); + populatedItemsSliceUtils.updateMessagesOnly(messages, useEmailsStore); } export function updateMessages(messages: MailMessage[]): void { - updateMessagesHook(messages, useEmailsStore); + populatedItemsSliceUtils.updateMessages(messages, useEmailsStore); } export function updateConversationStatus( conversationId: string, status: SearchRequestStatus ): void { - updateConversationStatusHook(conversationId, status, useEmailsStore); + populatedItemsSliceUtils.updateConversationStatus(conversationId, status, useEmailsStore); } export function updateMessageStatus(messageId: string, status: SearchRequestStatus): void { - updateMessageStatusHook(messageId, status, useEmailsStore); + populatedItemsSliceUtils.updateMessageStatus(messageId, status, useEmailsStore); } export function useMessageStatus(id: string): SearchRequestStatus { return useEmailsStore((state) => state.populatedItems.messagesStatus?.[id]); } export function removeMessages(messageIds: Array): void { - removeMessagesHook(messageIds, useEmailsStore); + populatedItemsSliceUtils.removeMessages(messageIds, useEmailsStore); } From 02fdb24aafc0967a93a4f89df18560c0e956fcbc Mon Sep 17 00:00:00 2001 From: Keshav Bhatt Date: Mon, 9 Dec 2024 21:18:18 +0530 Subject: [PATCH 010/165] refactor(search): rename hooks to utils and update imports --- .../emails/search/{hooks.ts => utils.ts} | 32 +++++++++++++------ src/store/zustand/emails/store.ts | 30 ++++++----------- 2 files changed, 32 insertions(+), 30 deletions(-) rename src/store/zustand/emails/search/{hooks.ts => utils.ts} (87%) diff --git a/src/store/zustand/emails/search/hooks.ts b/src/store/zustand/emails/search/utils.ts similarity index 87% rename from src/store/zustand/emails/search/hooks.ts rename to src/store/zustand/emails/search/utils.ts index 51d8559c5..cb0bf9f8e 100644 --- a/src/store/zustand/emails/search/hooks.ts +++ b/src/store/zustand/emails/search/utils.ts @@ -1,10 +1,10 @@ -/* eslint-disable no-param-reassign */ /* * SPDX-FileCopyrightText: 2024 Zextras * * SPDX-License-Identifier: AGPL-3.0-only */ +/* eslint-disable no-param-reassign */ import produce, { enableMapSet } from 'immer'; import { UseBoundStore, StoreApi } from 'zustand'; @@ -20,7 +20,7 @@ import { } from '../../../../types'; import { POPULATED_ITEMS_INITIAL_STATE } from '../populated-items/populated-items-slice'; -export function resetSearchAndPopulatedItemsHook( +function resetSearchAndPopulatedItems( useEmailsStore: UseBoundStore> ): void { useEmailsStore.setState( @@ -31,7 +31,7 @@ export function resetSearchAndPopulatedItemsHook( ); } -export function setSearchResultsByConversationHook( +function setSearchResultsByConversation( conversations: Array, more: boolean, useEmailsStore: UseBoundStore> @@ -54,7 +54,7 @@ export function setSearchResultsByConversationHook( ); } -export function setSearchResultsByMessageHook( +function setSearchResultsByMessage( messages: Array, more: boolean, useEmailsStore: UseBoundStore> @@ -76,7 +76,7 @@ export function setSearchResultsByMessageHook( }) ); } -export function appendConversationsToSearchHook( +function appendConversationsToSearch( conversations: Array, offset: number, more: boolean, @@ -98,7 +98,7 @@ export function appendConversationsToSearchHook( ); } -export function deleteConversationsFromSearchHook( +function deleteConversationsFromSearch( ids: Array, useEmailsStore: UseBoundStore> ): void { @@ -113,7 +113,7 @@ export function deleteConversationsFromSearchHook( ); } -export function deleteMessagesFromSearchHook( +function deleteMessagesFromSearch( ids: Array, useEmailsStore: UseBoundStore> ): void { @@ -128,7 +128,7 @@ export function deleteMessagesFromSearchHook( ); } -export function updateSearchResultsLoadingStatusHook( +function updateSearchResultsLoadingStatus( status: SearchRequestStatus, useEmailsStore: UseBoundStore> ): void { @@ -139,7 +139,7 @@ export function updateSearchResultsLoadingStatusHook( ); } -export function appendMessagesToSearchHook( +function appendMessagesToSearch( messages: Array, offset: number, useEmailsStore: UseBoundStore> @@ -158,7 +158,7 @@ export function appendMessagesToSearchHook( ); } -export function setMessagesInSearchSliceHook( +function setMessagesInSearchSlice( messages: Array, useEmailsStore: UseBoundStore> ): void { @@ -180,3 +180,15 @@ export function setMessagesInSearchSliceHook( } })); } + +export const searchSliceUtils = { + resetSearchAndPopulatedItems, + setSearchResultsByConversation, + setSearchResultsByMessage, + appendConversationsToSearch, + deleteConversationsFromSearch, + deleteMessagesFromSearch, + updateSearchResultsLoadingStatus, + appendMessagesToSearch, + setMessagesInSearchSlice +}; diff --git a/src/store/zustand/emails/store.ts b/src/store/zustand/emails/store.ts index 964368edd..29cebceab 100644 --- a/src/store/zustand/emails/store.ts +++ b/src/store/zustand/emails/store.ts @@ -10,18 +10,8 @@ import { create } from 'zustand'; import { createMessageSlice } from './messages/messages-slice'; import { createPopulatedItemsSlice } from './populated-items/populated-items-slice'; import { populatedItemsSliceUtils } from './populated-items/utils'; -import { - appendConversationsToSearchHook, - appendMessagesToSearchHook, - deleteConversationsFromSearchHook, - deleteMessagesFromSearchHook, - resetSearchAndPopulatedItemsHook, - setMessagesInSearchSliceHook, - setSearchResultsByConversationHook, - setSearchResultsByMessageHook, - updateSearchResultsLoadingStatusHook -} from './search/hooks'; import { createSearchSlice } from './search/search-slice'; +import { searchSliceUtils } from './search/utils'; import { IncompleteMessage, MailMessage, @@ -41,13 +31,13 @@ const useEmailsStore = create()((...a) => ({ // ##### Search related functions // ################################ export function resetSearchAndPopulatedItems(): void { - resetSearchAndPopulatedItemsHook(useEmailsStore); + searchSliceUtils.resetSearchAndPopulatedItems(useEmailsStore); } export function setSearchResultsByMessage( messages: Array, more: boolean ): void { - setSearchResultsByMessageHook(messages, more, useEmailsStore); + searchSliceUtils.setSearchResultsByMessage(messages, more, useEmailsStore); } export function useSearchResults(): SearchSliceState['search'] { return useEmailsStore(({ search }) => search); @@ -57,37 +47,37 @@ export function setSearchResultsByConversation( conversations: Array, more: boolean ): void { - setSearchResultsByConversationHook(conversations, more, useEmailsStore); + searchSliceUtils.setSearchResultsByConversation(conversations, more, useEmailsStore); } export function appendConversations( conversations: Array, offset: number, more: boolean ): void { - appendConversationsToSearchHook(conversations, offset, more, useEmailsStore); + searchSliceUtils.appendConversationsToSearch(conversations, offset, more, useEmailsStore); } export function deleteConversationsFromSearch(ids: Array): void { - deleteConversationsFromSearchHook(ids, useEmailsStore); + searchSliceUtils.deleteConversationsFromSearch(ids, useEmailsStore); } export function deleteMessagesFromSearch(ids: Array): void { - deleteMessagesFromSearchHook(ids, useEmailsStore); + searchSliceUtils.deleteMessagesFromSearch(ids, useEmailsStore); } export function getSearchResultsLoadingStatus(): SearchRequestStatus { return useEmailsStore.getState().search.status; } export function updateSearchResultsLoadingStatus(status: SearchRequestStatus): void { - updateSearchResultsLoadingStatusHook(status, useEmailsStore); + searchSliceUtils.updateSearchResultsLoadingStatus(status, useEmailsStore); } export function appendMessagesToSearch( messages: Array, offset: number ): void { - appendMessagesToSearchHook(messages, offset, useEmailsStore); + searchSliceUtils.appendMessagesToSearch(messages, offset, useEmailsStore); } export function setMessagesInSearchSlice(messages: Array): void { - setMessagesInSearchSliceHook(messages, useEmailsStore); + searchSliceUtils.setMessagesInSearchSlice(messages, useEmailsStore); } // ################################ From 104f39fc247951ffc5e2be5f9adeac837af08b9d Mon Sep 17 00:00:00 2001 From: Keshav Bhatt Date: Mon, 9 Dec 2024 22:09:43 +0530 Subject: [PATCH 011/165] refactor: (wip) resume from message-list-component.tsx --- src/hooks/use-message-list.ts | 65 +++++++++---------- src/store/zustand/emails/store.ts | 11 ++++ .../messages/message-list-component.tsx | 10 +-- .../messages/message-list-item-component.tsx | 7 +- .../folder-panel/messages/message-list.tsx | 36 +++++----- 5 files changed, 69 insertions(+), 60 deletions(-) diff --git a/src/hooks/use-message-list.ts b/src/hooks/use-message-list.ts index cc96eb1b5..0cf6f59a7 100644 --- a/src/hooks/use-message-list.ts +++ b/src/hooks/use-message-list.ts @@ -6,55 +6,52 @@ import { useEffect, useMemo } from 'react'; import { useUserSettings } from '@zextras/carbonio-shell-ui'; -import { filter, sortBy } from 'lodash'; import { useParams } from 'react-router-dom'; -import { useAppDispatch, useAppSelector } from './redux'; import { getFolder } from '../carbonio-ui-commons/store/zustand/folder/hooks'; import { LIST_LIMIT } from '../constants'; import { parseMessageSortingOptions } from '../helpers/sorting'; import { search } from '../store/actions'; -import { selectMessagesArray, selectMessagesSearchRequestStatus } from '../store/messages-slice'; -import type { MailMessage } from '../types'; +import { useMessages } from '../store/zustand/emails/store'; type RouteParams = { folderId: string; }; -export const useMessageList = (): Array => { +export const useMessageList = (): Set => { const { folderId } = useParams(); - const dispatch = useAppDispatch(); - const { prefs } = useUserSettings(); - const { sortOrder } = parseMessageSortingOptions(folderId, prefs.zimbraPrefSortOrder as string); + const { prefs: userSettings } = useUserSettings(); + const { sortOrder } = parseMessageSortingOptions( + folderId, + userSettings.zimbraPrefSortOrder as string + ); - const searchRequestStatus = useAppSelector(selectMessagesSearchRequestStatus); - const messages = useAppSelector(selectMessagesArray); + const messages = useMessages(); const folder = getFolder(folderId); - const filteredMessages = useMemo( - () => - folder - ? filter(messages, [ - 'parent', - 'rid' in folder && folder?.rid ? `${folder.zid}:${folder.rid}` : folder.id - ]) - : [], - [folder, messages] - ); - - const sortedMessages = useMemo(() => sortBy(filteredMessages, 'sortIndex'), [filteredMessages]); + const filteredMessages = useMemo(() => { + const messageSet = new Set(); + if (folder) { + const wantedFolderId = + 'rid' in folder && folder?.rid ? `${folder.zid}:${folder.rid}` : folder.id; + messages.messageIds.forEach((id) => { + if (id === wantedFolderId) { + messageSet.add(id); + } + }); + } + return messageSet; + }, [folder, messages]); useEffect(() => { - if (searchRequestStatus !== null) return; - dispatch( - search({ - folderId, - limit: LIST_LIMIT.INITIAL_LIMIT + 1, - sortBy: sortOrder, - types: 'message' - }) - ); - }, [dispatch, folderId, searchRequestStatus, sortOrder]); - - return sortedMessages; + if (messages.status !== null) return; + search({ + folderId, + limit: LIST_LIMIT.INITIAL_LIMIT + 1, + sortBy: sortOrder, + types: 'message' + }); + }, [folderId, messages.status, sortOrder]); + + return filteredMessages; }; diff --git a/src/store/zustand/emails/store.ts b/src/store/zustand/emails/store.ts index 29cebceab..efa188b74 100644 --- a/src/store/zustand/emails/store.ts +++ b/src/store/zustand/emails/store.ts @@ -127,3 +127,14 @@ export function useMessageStatus(id: string): SearchRequestStatus { export function removeMessages(messageIds: Array): void { populatedItemsSliceUtils.removeMessages(messageIds, useEmailsStore); } + +// ################################ +// #### Mail message related functions +// ################################ +export function useMessages(): EmailsStoreState['messages'] { + return useEmailsStore(({ messages }) => messages); +} + +export function useMessageIds(): EmailsStoreState['messages']['messageIds'] { + return useEmailsStore(({ messages }) => messages.messageIds); +} diff --git a/src/views/app/folder-panel/messages/message-list-component.tsx b/src/views/app/folder-panel/messages/message-list-component.tsx index 43ab2ee38..edb9764bc 100644 --- a/src/views/app/folder-panel/messages/message-list-component.tsx +++ b/src/views/app/folder-panel/messages/message-list-component.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { MessageListItem } from './message-list-item'; import { CustomList } from '../../../../carbonio-ui-commons/components/list/list'; import { useFolder, useRoot } from '../../../../carbonio-ui-commons/store/zustand/folder/hooks'; -import type { IncompleteMessage, MailMessage, MessageListItemProps } from '../../../../types'; +import type { IncompleteMessage, MessageListItemProps } from '../../../../types'; import ShimmerList from '../../../search/shimmer-list'; import { Breadcrumbs } from '../parts/breadcrumbs'; import { MultipleSelectionActionsPanel } from '../parts/multiple-selection-actions-panel'; @@ -78,7 +78,7 @@ export type MessageListComponentProps = { // the id of the current folder folderId: string; // the messages to display - messages: MailMessage[]; + messageIds: Set; // the ids of the messages being dragged draggedIds?: Record; // the function to call when the user starts dragging a message @@ -114,7 +114,7 @@ export const MessageListComponent: FC = memo( messagesLoadingCompleted, selectedIds, folderId, - messages, + messageIds, draggedIds, setDraggedIds, isSearchModule, @@ -156,7 +156,7 @@ export const MessageListComponent: FC = memo( <> {isSelectModeOn ? ( = memo( )} - + ) : ( diff --git a/src/views/app/folder-panel/messages/message-list-item-component.tsx b/src/views/app/folder-panel/messages/message-list-item-component.tsx index 2d37107eb..88656c730 100644 --- a/src/views/app/folder-panel/messages/message-list-item-component.tsx +++ b/src/views/app/folder-panel/messages/message-list-item-component.tsx @@ -8,11 +8,11 @@ import React, { FC, memo } from 'react'; import { noop } from 'lodash'; import { MessageListItem } from './message-list-item'; -import type { IncompleteMessage } from '../../../../types'; +import { useMessageById } from '../../../../store/zustand/emails/store'; import { DragItemWrapper } from '../parts/drag-item-wrapper'; export type ListItemComponentProps = { - message: IncompleteMessage; + messageId: string; selected: Record; isSelected: boolean; active: boolean; @@ -29,7 +29,7 @@ export type ListItemComponentProps = { export const MessageListItemComponent: FC = memo( function MessageListItemComponent({ - message, + messageId, selected, isSelected, active, @@ -42,6 +42,7 @@ export const MessageListItemComponent: FC = memo( setDraggedIds = noop, currentFolderId }) { + const message = useMessageById(messageId); return ( { const searchRequestStatus = useAppSelector(selectMessagesSearchRequestStatus); const searchedInFolderStatus = useAppSelector(selectFolderMsgSearchStatus(folderId)); - const messages = useMessageList(); + const messageIds = useMessageList(); const { prefs } = useUserSettings(); const { sortOrder } = parseMessageSortingOptions(folderId, prefs.zimbraPrefSortOrder as string); - + const items = [...messageIds].map((messageId) => ({ id: messageId })); const { selected, deselectAll, @@ -58,7 +58,7 @@ export const MessageList: FC = () => { } = useSelection({ setCount, count, - items: messages + items }); const hasMore = useMemo( @@ -68,7 +68,7 @@ export const MessageList: FC = () => { const loadMore = useCallback(() => { if (!hasMore) return; - const offset = messages.length; + const offset = messageIds.size; dispatch( search({ folderId, @@ -78,10 +78,10 @@ export const MessageList: FC = () => { types: 'message' }) ); - }, [dispatch, folderId, hasMore, messages.length, sortOrder]); + }, [dispatch, folderId, hasMore, messageIds.size, sortOrder]); const displayerTitle = useMemo(() => { - if (messages?.length === 0) { + if (messageIds?.size === 0) { if (getFolderIdParts(folderId).id === FOLDERS.SPAM) { return t('displayer.list_spam_title', 'There are no spam e-mails'); } @@ -97,24 +97,24 @@ export const MessageList: FC = () => { return t('displayer.list_folder_title', 'It looks like there are no e-mails yet'); } return null; - }, [messages, folderId]); + }, [messageIds, folderId]); const listItems = useMemo( () => - map(messages, (message) => { - const isSelected = selected[message.id]; - const active = itemId === message.id; + map(items, (item) => { + const isSelected = selected[item.id]; + const active = itemId === item.id; return ( {(visible: boolean): ReactElement => visible ? ( { isSelectModeOn={isSelectModeOn} dragImageRef={dragImageRef} draggedIds={draggedIds} - key={message.id} + key={item.id} deselectAll={deselectAll} visible={visible} setDraggedIds={setDraggedIds} @@ -135,12 +135,12 @@ export const MessageList: FC = () => { ); }), - [deselectAll, draggedIds, folderId, isSelectModeOn, itemId, messages, selected, toggle] + [deselectAll, draggedIds, folderId, isSelectModeOn, itemId, messageIds, selected, toggle] ); const totalMessages = useMemo( - () => (sortOrder === 'readAsc' ? messages.length : (folder?.n ?? messages.length ?? 0)), - [folder?.n, messages.length, sortOrder] + () => (sortOrder === 'readAsc' ? messageIds.size : (folder?.n ?? messageIds.size ?? 0)), + [folder?.n, messageIds.size, sortOrder] ); const selectedIds = useMemo(() => Object.keys(selected), [selected]); @@ -163,7 +163,7 @@ export const MessageList: FC = () => { messagesLoadingCompleted={messagesLoadingCompleted} selectedIds={selectedIds} folderId={folderId} - messages={messages} + messageIds={messageIds} draggedIds={draggedIds} setDraggedIds={setDraggedIds} isSelectModeOn={isSelectModeOn} From 1fb49466ea4f723f5b343cd5cba38ef834a90625 Mon Sep 17 00:00:00 2001 From: Giuliano Caregnato Date: Tue, 10 Dec 2024 11:49:43 +0100 Subject: [PATCH 012/165] WIP --- .../app/folder-panel/messages/message-list-component.tsx | 2 +- src/views/app/folder-panel/messages/message-list.tsx | 2 +- .../parts/multiple-selection-actions-panel.tsx | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/views/app/folder-panel/messages/message-list-component.tsx b/src/views/app/folder-panel/messages/message-list-component.tsx index edb9764bc..13b84b471 100644 --- a/src/views/app/folder-panel/messages/message-list-component.tsx +++ b/src/views/app/folder-panel/messages/message-list-component.tsx @@ -130,7 +130,7 @@ export const MessageListComponent: FC = memo( listRef }) { useEffect(() => { - setDraggedIds && setDraggedIds(selected); + setDraggedIds?.(selected); }, [selected, setDraggedIds]); const folder = useFolder(folderId); diff --git a/src/views/app/folder-panel/messages/message-list.tsx b/src/views/app/folder-panel/messages/message-list.tsx index c3ff0c757..308889b1f 100644 --- a/src/views/app/folder-panel/messages/message-list.tsx +++ b/src/views/app/folder-panel/messages/message-list.tsx @@ -135,7 +135,7 @@ export const MessageList: FC = () => { ); }), - [deselectAll, draggedIds, folderId, isSelectModeOn, itemId, messageIds, selected, toggle] + [deselectAll, draggedIds, folderId, isSelectModeOn, itemId, items, selected, toggle] ); const totalMessages = useMemo( diff --git a/src/views/app/folder-panel/parts/multiple-selection-actions-panel.tsx b/src/views/app/folder-panel/parts/multiple-selection-actions-panel.tsx index cb16fca4b..60bfe7a8b 100644 --- a/src/views/app/folder-panel/parts/multiple-selection-actions-panel.tsx +++ b/src/views/app/folder-panel/parts/multiple-selection-actions-panel.tsx @@ -9,13 +9,12 @@ import { Button, Container, IconButton, Row, Tooltip } from '@zextras/carbonio-d import { t, useUserSettings } from '@zextras/carbonio-shell-ui'; import { useUiUtilities } from '../../../../hooks/use-ui-utilities'; -import type { Conversation, MailMessage } from '../../../../types'; import { getFolderParentId } from '../../../../ui-actions/utils'; import { ConversationsMultipleSelectionActions } from '../conversations/conversations-multiple-selection-actions'; import { MessagesMultipleSelectionActions } from '../messages/messages-multiple-selection-actions'; type MultipleSelectionActionsPanelProps = { - items: Array & Pick> | Array; + items: Set; selectedIds: Array; deselectAll: () => void; selectAll: () => void; @@ -115,17 +114,17 @@ export const MultipleSelectionActionsPanel: FC {isConversation ? ( } + items={items} /> ) : ( } + items={items} /> )} From 9456c7145ff9ffc8481282d324973c275ba774e5 Mon Sep 17 00:00:00 2001 From: Keshav Bhatt Date: Tue, 10 Dec 2024 22:21:35 +0530 Subject: [PATCH 013/165] chore: wip --- src/hooks/use-message-list.ts | 138 +++- .../zustand/emails/messages/messages-slice.ts | 4 +- src/store/zustand/emails/messages/utils.ts | 71 ++ src/store/zustand/emails/store.ts | 16 + .../multi-sel-msg-primary-actions.test.tsx | 549 +++++++-------- .../multi-sel-msg-secondary-actions.test.tsx | 649 +++++++++--------- .../conversation-list-component.tsx | 23 +- .../messages/message-list-component.tsx | 26 +- .../tests/message-list-component.test.tsx | 173 ++--- .../multiple-selection-actions-panel.tsx | 266 +++---- .../search/list/parts/search-list-header.tsx | 23 +- 11 files changed, 1085 insertions(+), 853 deletions(-) create mode 100644 src/store/zustand/emails/messages/utils.ts diff --git a/src/hooks/use-message-list.ts b/src/hooks/use-message-list.ts index 0cf6f59a7..a039f854d 100644 --- a/src/hooks/use-message-list.ts +++ b/src/hooks/use-message-list.ts @@ -3,16 +3,26 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { useUserSettings } from '@zextras/carbonio-shell-ui'; +import { type ErrorSoapBodyResponse, useUserSettings } from '@zextras/carbonio-shell-ui'; +import { map } from 'lodash'; import { useParams } from 'react-router-dom'; +import { searchSoapApi } from '../api/search'; import { getFolder } from '../carbonio-ui-commons/store/zustand/folder/hooks'; -import { LIST_LIMIT } from '../constants'; +import { getTags } from '../carbonio-ui-commons/store/zustand/tags'; +import { Tags } from '../carbonio-ui-commons/types/tags'; +import { API_REQUEST_STATUS, LIST_LIMIT } from '../constants'; import { parseMessageSortingOptions } from '../helpers/sorting'; -import { search } from '../store/actions'; -import { useMessages } from '../store/zustand/emails/store'; +import { normalizeMailMessageFromSoap } from '../normalizations/normalize-message'; +import { + resetMessagesAndPopulatedItems, + setMessagesInEmailStore, + updateMessagesResultsLoadingStatus, + useMessages +} from '../store/zustand/emails/store'; +import { SearchResponse } from '../types'; type RouteParams = { folderId: string; @@ -43,15 +53,117 @@ export const useMessageList = (): Set => { return messageSet; }, [folder, messages]); + const queryPart = [`inId:"${folderId}"`]; + + let finalsortBy = sortOrder; + switch (sortOrder) { + case 'readAsc': + queryPart.push('is:unread'); + finalsortBy = 'dateAsc'; + break; + case 'readDesc': + queryPart.push('is:unread'); + finalsortBy = 'dateDesc'; + break; + case 'priorityAsc': + case 'priorityDesc': + queryPart.push('priority:high'); + break; + case 'flagAsc': + case 'flagDesc': + queryPart.push('is:flagged'); + break; + case 'attachAsc': + case 'attachDesc': + queryPart.push('has:attachment'); + break; + default: + break; + } + const settings = useUserSettings(); + + const prefLocale = useMemo( + () => settings.prefs.zimbraPrefLocale, + [settings.prefs.zimbraPrefLocale] + ); + + let finalQuery = ''; + + if (folderId) { + finalQuery = queryPart.join(' '); + } + + const previousQuery = useRef(finalQuery); + + function handleFulFilledMessagesResultsInEmailStore({ + searchResponse, + tags + }: { + searchResponse: SearchResponse; + tags: Tags; + }): void { + const normalizedMessages = map(searchResponse.m, (msg) => + normalizeMailMessageFromSoap(msg, false) + ); + + setMessagesInEmailStore(normalizedMessages, searchResponse.more); + } + + const handleMessageResults = useCallback( + ({ searchResponse }: { searchResponse: SearchResponse | ErrorSoapBodyResponse }): void => { + if ('Fault' in searchResponse) { + return; + } + const tags = getTags(); + + if (searchResponse.m) { + handleFulFilledMessagesResultsInEmailStore({ searchResponse, tags }); + } + if (searchResponse && !searchResponse.m) { + resetMessagesAndPopulatedItems(); + updateMessagesResultsLoadingStatus(API_REQUEST_STATUS.fulfilled); + } + }, + [] + ); + + const firstSearchCallback = useCallback( + async (abortSignal: AbortSignal | undefined) => { + updateMessagesResultsLoadingStatus(API_REQUEST_STATUS.pending); + const searchResponse = await searchSoapApi({ + query: finalQuery, + limit: LIST_LIMIT.INITIAL_LIMIT, + sortBy: finalsortBy, + types: 'message', + offset: 0, + recip: '0', + locale: prefLocale, + abortSignal + }); + if ( + 'Fault' in searchResponse && + searchResponse?.Fault?.Detail?.Error?.Code === 'mail.QUERY_PARSE_ERROR' + ) { + updateMessagesResultsLoadingStatus(API_REQUEST_STATUS.error); + } else { + handleMessageResults({ searchResponse }); + } + }, + [finalQuery, finalsortBy, handleMessageResults, prefLocale] + ); + useEffect(() => { - if (messages.status !== null) return; - search({ - folderId, - limit: LIST_LIMIT.INITIAL_LIMIT + 1, - sortBy: sortOrder, - types: 'message' - }); - }, [folderId, messages.status, sortOrder]); + const controller = new AbortController(); + const { signal } = controller; + if (previousQuery.current !== finalQuery && finalQuery.length > 0) { + firstSearchCallback(signal); + previousQuery.current = finalQuery; + } + return () => { + controller.abort(); + previousQuery.current = finalQuery; + }; + }, [finalQuery, firstSearchCallback]); return filteredMessages; }; diff --git a/src/store/zustand/emails/messages/messages-slice.ts b/src/store/zustand/emails/messages/messages-slice.ts index a641ea748..49eb10c22 100644 --- a/src/store/zustand/emails/messages/messages-slice.ts +++ b/src/store/zustand/emails/messages/messages-slice.ts @@ -6,7 +6,7 @@ import { StateCreator } from 'zustand'; -import { MessageSliceState, PopulatedItemsSliceState, SearchSliceState } from '../../../../types'; +import { MessageSliceState, PopulatedItemsSliceState } from '../../../../types'; export const MESSAGES_INITIAL_STATE: MessageSliceState['messages'] = { messageIds: new Set(), @@ -15,7 +15,7 @@ export const MESSAGES_INITIAL_STATE: MessageSliceState['messages'] = { status: null }; export const createMessageSlice: StateCreator< - SearchSliceState & PopulatedItemsSliceState & MessageSliceState, + PopulatedItemsSliceState & MessageSliceState, [], [], MessageSliceState diff --git a/src/store/zustand/emails/messages/utils.ts b/src/store/zustand/emails/messages/utils.ts new file mode 100644 index 000000000..e5b115f6f --- /dev/null +++ b/src/store/zustand/emails/messages/utils.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2024 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable no-param-reassign */ +import produce from 'immer'; +import { StoreApi, UseBoundStore } from 'zustand'; + +import { MESSAGES_INITIAL_STATE } from './messages-slice'; +import { API_REQUEST_STATUS } from '../../../../constants'; +import { + EmailsStoreState, + IncompleteMessage, + MailMessage, + MessageSliceState, + SearchRequestStatus +} from '../../../../types'; +import { POPULATED_ITEMS_INITIAL_STATE } from '../populated-items/populated-items-slice'; + +function setMessages( + messages: Array, + more: boolean, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce((draft) => { + draft.messages.messageIds = new Set(messages.map((message) => message.id)); + draft.messages.status = API_REQUEST_STATUS.fulfilled; + draft.messages.offset = 0; + draft.messages.more = more; + + draft.populatedItems.messages = messages.reduce( + (acc, message) => { + acc[message.id] = message; + return acc; + }, + {} as Record + ); + }) + ); +} + +function updateMessagesResultsLoadingStatus( + status: SearchRequestStatus, + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce((state: MessageSliceState) => { + state.messages.status = status; + }) + ); +} + +function resetMessagesAndPopulatedItems( + useEmailsStore: UseBoundStore> +): void { + useEmailsStore.setState( + produce((state: EmailsStoreState) => { + state.messages = MESSAGES_INITIAL_STATE; + state.populatedItems = POPULATED_ITEMS_INITIAL_STATE; + }) + ); +} + +export const messageSliceUtils = { + setMessages, + updateMessagesResultsLoadingStatus, + resetMessagesAndPopulatedItems +}; diff --git a/src/store/zustand/emails/store.ts b/src/store/zustand/emails/store.ts index efa188b74..06baad6a8 100644 --- a/src/store/zustand/emails/store.ts +++ b/src/store/zustand/emails/store.ts @@ -8,6 +8,7 @@ import { create } from 'zustand'; import { createMessageSlice } from './messages/messages-slice'; +import { messageSliceUtils } from './messages/utils'; import { createPopulatedItemsSlice } from './populated-items/populated-items-slice'; import { populatedItemsSliceUtils } from './populated-items/utils'; import { createSearchSlice } from './search/search-slice'; @@ -138,3 +139,18 @@ export function useMessages(): EmailsStoreState['messages'] { export function useMessageIds(): EmailsStoreState['messages']['messageIds'] { return useEmailsStore(({ messages }) => messages.messageIds); } + +export function setMessagesInEmailStore( + messages: Array, + more: boolean +): void { + messageSliceUtils.setMessages(messages, more, useEmailsStore); +} + +export function updateMessagesResultsLoadingStatus(status: SearchRequestStatus): void { + messageSliceUtils.updateMessagesResultsLoadingStatus(status, useEmailsStore); +} + +export function resetMessagesAndPopulatedItems(): void { + messageSliceUtils.resetMessagesAndPopulatedItems(useEmailsStore); +} diff --git a/src/ui-actions/tests/multi-sel-msg-primary-actions.test.tsx b/src/ui-actions/tests/multi-sel-msg-primary-actions.test.tsx index 11cb169a4..3d0c9cc6f 100644 --- a/src/ui-actions/tests/multi-sel-msg-primary-actions.test.tsx +++ b/src/ui-actions/tests/multi-sel-msg-primary-actions.test.tsx @@ -1,283 +1,290 @@ /* - * SPDX-FileCopyrightText: 2023 Zextras + * SPDX-FileCopyrightText: 2024 Zextras * * SPDX-License-Identifier: AGPL-3.0-only */ - -import React from 'react'; - -import { screen } from '@testing-library/react'; -import { forEach, noop, reduce } from 'lodash'; - -import { setupTest } from '../../carbonio-ui-commons/test/test-setup'; -import { FOLDERS_DESCRIPTORS, MessageActionsDescriptors } from '../../constants'; -import { ASSERTIONS, MSG_CONV_STATUS_DESCRIPTORS } from '../../tests/constants'; -import { generateMessage } from '../../tests/generators/generateMessage'; -import { generateStore } from '../../tests/generators/store'; -import type { MailMessage } from '../../types'; -import { MultipleSelectionActionsPanel } from '../../views/app/folder-panel/parts/multiple-selection-actions-panel'; - -const generalFolders = { - desc: 'general folders', - value: [ - FOLDERS_DESCRIPTORS.INBOX.id, - FOLDERS_DESCRIPTORS.SENT.id, - FOLDERS_DESCRIPTORS.USER_DEFINED.id - ] -}; -const foldersExcludedMarkReadUnread = { - desc: 'folders excluded mark read unread', - value: [FOLDERS_DESCRIPTORS.DRAFTS.id, FOLDERS_DESCRIPTORS.SPAM.id, FOLDERS_DESCRIPTORS.TRASH.id] -}; -const foldersExcludedTrash = [FOLDERS_DESCRIPTORS.TRASH]; -const foldersIncludedDeletePermanently = [FOLDERS_DESCRIPTORS.TRASH]; - -function getSelectedIds(messages: MailMessage[]): Array { - return messages.map((message) => message.id); -} - -function getFoldersExcluded( - _generalFolders: { desc: string; value: Array }, - excludedFolder: string -): { desc: string; value: Array } { - return { - ...generalFolders, - value: [...generalFolders.value, excludedFolder] - }; -} - -function getFoldersAllowed( - _generalFolders: { desc: string; value: Array }, - excludedFolder: string -): { desc: string; value: Array } { - const filteredFolderValues = reduce( - generalFolders.value, - (acc, folder) => { - if (!excludedFolder.includes(folder)) acc.push(folder); - return acc; - }, - [] as Array - ); - return { - ...generalFolders, - value: filteredFolderValues - }; -} - -const deselectAll = jest.fn(); -const selectAll = jest.fn(); -const store = generateStore(); - -const props = { - items: [], - selectAllModeOff: noop, - setIsSelectModeOn: noop, - folderId: '', - isAllSelected: false, - selectedIds: [], - deselectAll, - selectAll -}; -describe('Actions visibility', () => { - test.each` - case | read | excludedFolders | assertion | action - ${1} | ${MSG_CONV_STATUS_DESCRIPTORS.READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MARK_AS_UNREAD} - ${2} | ${MSG_CONV_STATUS_DESCRIPTORS.NOT_READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MARK_AS_READ} - ${3} | ${MSG_CONV_STATUS_DESCRIPTORS.READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.NOT_CONTAINS} | ${MessageActionsDescriptors.MARK_AS_READ} - ${4} | ${MSG_CONV_STATUS_DESCRIPTORS.NOT_READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.NOT_CONTAINS} | ${MessageActionsDescriptors.MARK_AS_UNREAD} - `( - `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, - async ({ excludedFolders, action, read, assertion }) => { - forEach(excludedFolders.value, (excludedFolder: string) => { - const folders = getFoldersExcluded(generalFolders, excludedFolder); - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder, isRead: read.value }) - ); - const selectedIds = getSelectedIds(messages); - - const testProps = { - ...props, - items: messages, - folderId: excludedFolder, - selectedIds - }; - - setupTest(, { store }); - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - - // test that the action is not visible if the message is read and the folder is excluded - forEach(excludedFolders.value, (excludedFolder) => { - const folders = getFoldersAllowed(generalFolders, excludedFolder); - - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder, isRead: read.value }) - ); - const selectedIds = getSelectedIds(messages); - const testProps = { - ...props, - items: messages, - folderId: folders.value[0], - selectedIds - }; - - setupTest(, { store }); - if (assertion === true) - expect( - screen.getByTestId(`primary-multi-action-button-${action.id}`) - ).toBeInTheDocument(); - if (assertion === false) - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - } - ); - - test.each` - case | excludedFolders | assertion | action - ${1} | ${foldersExcludedTrash} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MOVE_TO_TRASH} - `( - `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, - async ({ excludedFolders, action, assertion }) => { - forEach(excludedFolders.value, (excludedFolder: string) => { - const folders = getFoldersExcluded(generalFolders, excludedFolder); - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - - const testProps = { - ...props, - items: messages, - folderId: excludedFolder, - selectedIds - }; - - setupTest(, { store }); - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - - forEach(excludedFolders.value, (excludedFolder) => { - const folders = getFoldersAllowed(generalFolders, excludedFolder); - - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - const testProps = { - ...props, - items: messages, - folderId: folders.value[0], - selectedIds - }; - - setupTest(, { store }); - if (assertion === true) - expect( - screen.getByTestId(`primary-multi-action-button-${action.id}`) - ).toBeInTheDocument(); - if (assertion === false) - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - } - ); - - test.each` - case | excludedFolders | assertion | action - ${1} | ${foldersIncludedDeletePermanently} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.DELETE_PERMANENTLY} - `( - `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, - async ({ excludedFolders, action, assertion }) => { - forEach(excludedFolders.value, (excludedFolder: string) => { - const folders = getFoldersExcluded(generalFolders, excludedFolder); - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - - const testProps = { - ...props, - items: messages, - folderId: excludedFolder, - selectedIds - }; - - setupTest(, { store }); - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - - forEach(excludedFolders.value, (excludedFolder) => { - const folders = excludedFolder; - - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - const testProps = { - ...props, - items: messages, - folderId: folders.value[0], - selectedIds - }; - - setupTest(, { store }); - if (assertion.value === true) - expect( - screen.getByTestId(`primary-multi-action-button-${action.id}`) - ).toBeInTheDocument(); - if (assertion.value === false) - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - } - ); -}); - -test.todo( - 'primary actions contain the mark as read action if the selection doesn’t involve a trashed, junk or draft message and at least one message is unread' -); -test.todo( - 'primary actions contain the mark as unread action if the selection doesn’t involve a trashed, junk or draft message and at least one message is read' -); -test.todo( - 'primary actions don’t contain the mark as read action if the selection involve a trashed, junk or draft message' -); -test.todo( - 'primary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' -); -test.todo( - 'primary actions contain the delete permanently action if the selection involve trashed messages only' -); -test.todo( - 'primary actions contain the trash action if the selection doesn’t involve a trashed message' -); -test.todo( - 'secondary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' -); -test.todo( - 'secondary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' -); -// test.todo('secondary actions contain the flag action'); +// TODO: CO-1725 re-enable this +// +// /* +// * SPDX-FileCopyrightText: 2023 Zextras +// * +// * SPDX-License-Identifier: AGPL-3.0-only +// */ +// +// import React from 'react'; +// +// import { screen } from '@testing-library/react'; +// import { forEach, noop, reduce } from 'lodash'; +// +// import { setupTest } from '../../carbonio-ui-commons/test/test-setup'; +// import { FOLDERS_DESCRIPTORS, MessageActionsDescriptors } from '../../constants'; +// import { ASSERTIONS, MSG_CONV_STATUS_DESCRIPTORS } from '../../tests/constants'; +// import { generateMessage } from '../../tests/generators/generateMessage'; +// import { generateStore } from '../../tests/generators/store'; +// import type { MailMessage } from '../../types'; +// import { MultipleSelectionActionsPanel } from '../../views/app/folder-panel/parts/multiple-selection-actions-panel'; +// +// const generalFolders = { +// desc: 'general folders', +// value: [ +// FOLDERS_DESCRIPTORS.INBOX.id, +// FOLDERS_DESCRIPTORS.SENT.id, +// FOLDERS_DESCRIPTORS.USER_DEFINED.id +// ] +// }; +// const foldersExcludedMarkReadUnread = { +// desc: 'folders excluded mark read unread', +// value: [FOLDERS_DESCRIPTORS.DRAFTS.id, FOLDERS_DESCRIPTORS.SPAM.id, FOLDERS_DESCRIPTORS.TRASH.id] +// }; +// const foldersExcludedTrash = [FOLDERS_DESCRIPTORS.TRASH]; +// const foldersIncludedDeletePermanently = [FOLDERS_DESCRIPTORS.TRASH]; +// +// function getSelectedIds(messages: MailMessage[]): Array { +// return messages.map((message) => message.id); +// } +// +// function getFoldersExcluded( +// _generalFolders: { desc: string; value: Array }, +// excludedFolder: string +// ): { desc: string; value: Array } { +// return { +// ...generalFolders, +// value: [...generalFolders.value, excludedFolder] +// }; +// } +// +// function getFoldersAllowed( +// _generalFolders: { desc: string; value: Array }, +// excludedFolder: string +// ): { desc: string; value: Array } { +// const filteredFolderValues = reduce( +// generalFolders.value, +// (acc, folder) => { +// if (!excludedFolder.includes(folder)) acc.push(folder); +// return acc; +// }, +// [] as Array +// ); +// return { +// ...generalFolders, +// value: filteredFolderValues +// }; +// } +// +// const deselectAll = jest.fn(); +// const selectAll = jest.fn(); +// const store = generateStore(); +// +// const props = { +// items: [], +// selectAllModeOff: noop, +// setIsSelectModeOn: noop, +// folderId: '', +// isAllSelected: false, +// selectedIds: [], +// deselectAll, +// selectAll +// }; +// describe('Actions visibility', () => { +// test.each` +// case | read | excludedFolders | assertion | action +// ${1} | ${MSG_CONV_STATUS_DESCRIPTORS.READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MARK_AS_UNREAD} +// ${2} | ${MSG_CONV_STATUS_DESCRIPTORS.NOT_READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MARK_AS_READ} +// ${3} | ${MSG_CONV_STATUS_DESCRIPTORS.READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.NOT_CONTAINS} | ${MessageActionsDescriptors.MARK_AS_READ} +// ${4} | ${MSG_CONV_STATUS_DESCRIPTORS.NOT_READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.NOT_CONTAINS} | ${MessageActionsDescriptors.MARK_AS_UNREAD} +// `( +// `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, +// async ({ excludedFolders, action, read, assertion }) => { +// forEach(excludedFolders.value, (excludedFolder: string) => { +// const folders = getFoldersExcluded(generalFolders, excludedFolder); +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder, isRead: read.value }) +// ); +// const selectedIds = getSelectedIds(messages); +// +// const testProps = { +// ...props, +// items: messages, +// folderId: excludedFolder, +// selectedIds +// }; +// +// setupTest(, { store }); +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// +// // test that the action is not visible if the message is read and the folder is excluded +// forEach(excludedFolders.value, (excludedFolder) => { +// const folders = getFoldersAllowed(generalFolders, excludedFolder); +// +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder, isRead: read.value }) +// ); +// const selectedIds = getSelectedIds(messages); +// const testProps = { +// ...props, +// items: messages, +// folderId: folders.value[0], +// selectedIds +// }; +// +// setupTest(, { store }); +// if (assertion === true) +// expect( +// screen.getByTestId(`primary-multi-action-button-${action.id}`) +// ).toBeInTheDocument(); +// if (assertion === false) +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// } +// ); +// +// test.each` +// case | excludedFolders | assertion | action +// ${1} | ${foldersExcludedTrash} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MOVE_TO_TRASH} +// `( +// `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, +// async ({ excludedFolders, action, assertion }) => { +// forEach(excludedFolders.value, (excludedFolder: string) => { +// const folders = getFoldersExcluded(generalFolders, excludedFolder); +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// +// const testProps = { +// ...props, +// items: messages, +// folderId: excludedFolder, +// selectedIds +// }; +// +// setupTest(, { store }); +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// +// forEach(excludedFolders.value, (excludedFolder) => { +// const folders = getFoldersAllowed(generalFolders, excludedFolder); +// +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// const testProps = { +// ...props, +// items: messages, +// folderId: folders.value[0], +// selectedIds +// }; +// +// setupTest(, { store }); +// if (assertion === true) +// expect( +// screen.getByTestId(`primary-multi-action-button-${action.id}`) +// ).toBeInTheDocument(); +// if (assertion === false) +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// } +// ); +// +// test.each` +// case | excludedFolders | assertion | action +// ${1} | ${foldersIncludedDeletePermanently} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.DELETE_PERMANENTLY} +// `( +// `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, +// async ({ excludedFolders, action, assertion }) => { +// forEach(excludedFolders.value, (excludedFolder: string) => { +// const folders = getFoldersExcluded(generalFolders, excludedFolder); +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// +// const testProps = { +// ...props, +// items: messages, +// folderId: excludedFolder, +// selectedIds +// }; +// +// setupTest(, { store }); +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// +// forEach(excludedFolders.value, (excludedFolder) => { +// const folders = excludedFolder; +// +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// const testProps = { +// ...props, +// items: messages, +// folderId: folders.value[0], +// selectedIds +// }; +// +// setupTest(, { store }); +// if (assertion.value === true) +// expect( +// screen.getByTestId(`primary-multi-action-button-${action.id}`) +// ).toBeInTheDocument(); +// if (assertion.value === false) +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// } +// ); +// }); +// // test.todo( -// 'secondary actions don’t contain the move action if the selection involve a trashed or draft message' +// 'primary actions contain the mark as read action if the selection doesn’t involve a trashed, junk or draft message and at least one message is unread' // ); -// test.todo('secondary actions contain the tag submenu'); // test.todo( -// 'secondary actions don’t contain the mark as spam action if the selection involve a trashed, junk or draft message' +// 'primary actions contain the mark as unread action if the selection doesn’t involve a trashed, junk or draft message and at least one message is read' // ); // test.todo( -// 'secondary actions contain the restore action if the selection involve trashed messages only' +// 'primary actions don’t contain the mark as read action if the selection involve a trashed, junk or draft message' // ); // test.todo( -// 'secondary actions contain the delete permanently action if the selection involve trashed messages only' +// 'primary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' // ); +// test.todo( +// 'primary actions contain the delete permanently action if the selection involve trashed messages only' +// ); +// test.todo( +// 'primary actions contain the trash action if the selection doesn’t involve a trashed message' +// ); +// test.todo( +// 'secondary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' +// ); +// test.todo( +// 'secondary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' +// ); +// // test.todo('secondary actions contain the flag action'); +// // test.todo( +// // 'secondary actions don’t contain the move action if the selection involve a trashed or draft message' +// // ); +// // test.todo('secondary actions contain the tag submenu'); +// // test.todo( +// // 'secondary actions don’t contain the mark as spam action if the selection involve a trashed, junk or draft message' +// // ); +// // test.todo( +// // 'secondary actions contain the restore action if the selection involve trashed messages only' +// // ); +// // test.todo( +// // 'secondary actions contain the delete permanently action if the selection involve trashed messages only' +// // ); diff --git a/src/ui-actions/tests/multi-sel-msg-secondary-actions.test.tsx b/src/ui-actions/tests/multi-sel-msg-secondary-actions.test.tsx index 229138ecf..ae9f9c0ef 100644 --- a/src/ui-actions/tests/multi-sel-msg-secondary-actions.test.tsx +++ b/src/ui-actions/tests/multi-sel-msg-secondary-actions.test.tsx @@ -1,333 +1,340 @@ /* - * SPDX-FileCopyrightText: 2023 Zextras + * SPDX-FileCopyrightText: 2024 Zextras * * SPDX-License-Identifier: AGPL-3.0-only */ - -import React from 'react'; - -import { screen } from '@testing-library/react'; -import { forEach, noop, reduce } from 'lodash'; - -import { setupTest } from '../../carbonio-ui-commons/test/test-setup'; -import { FOLDERS_DESCRIPTORS, MessageActionsDescriptors } from '../../constants'; -import { ASSERTIONS, MSG_CONV_STATUS_DESCRIPTORS } from '../../tests/constants'; -import { generateMessage } from '../../tests/generators/generateMessage'; -import { generateStore } from '../../tests/generators/store'; -import type { MailMessage } from '../../types'; -import { MultipleSelectionActionsPanel } from '../../views/app/folder-panel/parts/multiple-selection-actions-panel'; - -const generalFolders = { - desc: 'general folders', - value: [ - FOLDERS_DESCRIPTORS.INBOX.id, - FOLDERS_DESCRIPTORS.SENT.id, - FOLDERS_DESCRIPTORS.USER_DEFINED.id - ] -}; -const foldersExcludedMarkReadUnread = { - desc: 'folders excluded mark read unread', - value: [FOLDERS_DESCRIPTORS.DRAFTS.id, FOLDERS_DESCRIPTORS.SPAM.id, FOLDERS_DESCRIPTORS.TRASH.id] -}; -const foldersExcludedTrash = [FOLDERS_DESCRIPTORS.TRASH]; -const foldersIncludedDeletePermanently = [FOLDERS_DESCRIPTORS.TRASH]; -const foldersExcludedMoveToFolder = [FOLDERS_DESCRIPTORS.DRAFTS, FOLDERS_DESCRIPTORS.TRASH]; - -function getSelectedIds(messages: MailMessage[]): Array { - return messages.map((message) => message.id); -} - -function getFoldersExcluded( - _generalFolders: { desc: string; value: Array }, - excludedFolder: string -): { desc: string; value: Array } { - return { - ...generalFolders, - value: [...generalFolders.value, excludedFolder] - }; -} - -function getFoldersAllowed( - _generalFolders: { desc: string; value: Array }, - excludedFolder: string -): { desc: string; value: Array } { - const filteredFolderValues = reduce( - generalFolders.value, - (acc, folder) => { - if (!excludedFolder.includes(folder)) acc.push(folder); - return acc; - }, - [] as Array - ); - return { - ...generalFolders, - value: filteredFolderValues - }; -} - -const deselectAll = jest.fn(); -const selectAll = jest.fn(); -const store = generateStore(); - -const props = { - items: [], - selectAllModeOff: noop, - setIsSelectModeOn: noop, - folderId: '', - isAllSelected: false, - selectedIds: [], - deselectAll, - selectAll -}; -describe('Actions visibility', () => { - test.each` - case | read | excludedFolders | assertion | action - ${1} | ${MSG_CONV_STATUS_DESCRIPTORS.READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MARK_AS_UNREAD} - ${2} | ${MSG_CONV_STATUS_DESCRIPTORS.NOT_READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MARK_AS_READ} - ${3} | ${MSG_CONV_STATUS_DESCRIPTORS.READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.NOT_CONTAINS} | ${MessageActionsDescriptors.MARK_AS_READ} - ${4} | ${MSG_CONV_STATUS_DESCRIPTORS.NOT_READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.NOT_CONTAINS} | ${MessageActionsDescriptors.MARK_AS_UNREAD} - `( - `(case #$case) secondary actions for a message in $folders.desc $assertion.desc the $action.desc action`, - async ({ excludedFolders, action, read, assertion }) => { - forEach(excludedFolders.value, (excludedFolder: string) => { - const folders = getFoldersExcluded(generalFolders, excludedFolder); - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder, isRead: read.value }) - ); - const selectedIds = getSelectedIds(messages); - - const testProps = { - ...props, - items: messages, - folderId: excludedFolder, - selectedIds - }; - - setupTest(, { store }); - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - - // test that the action is not visible if the message is read and the folder is excluded - forEach(excludedFolders.value, (excludedFolder) => { - const folders = getFoldersAllowed(generalFolders, excludedFolder); - - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder, isRead: read.value }) - ); - const selectedIds = getSelectedIds(messages); - const testProps = { - ...props, - items: messages, - folderId: folders.value[0], - selectedIds - }; - - setupTest(, { store }); - if (assertion === true) - expect( - screen.getByTestId(`primary-multi-action-button-${action.id}`) - ).toBeInTheDocument(); - if (assertion === false) - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - } - ); - - test.each` - case | excludedFolders | assertion | action - ${1} | ${foldersExcludedTrash} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MOVE_TO_TRASH} - `( - `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, - async ({ excludedFolders, action, assertion }) => { - forEach(excludedFolders.value, (excludedFolder: string) => { - const folders = getFoldersExcluded(generalFolders, excludedFolder); - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - - const testProps = { - ...props, - items: messages, - folderId: excludedFolder, - selectedIds - }; - - setupTest(, { store }); - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - - forEach(excludedFolders.value, (excludedFolder) => { - const folders = getFoldersAllowed(generalFolders, excludedFolder); - - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - const testProps = { - ...props, - items: messages, - folderId: folders.value[0], - selectedIds - }; - - setupTest(, { store }); - if (assertion === true) - expect( - screen.getByTestId(`primary-multi-action-button-${action.id}`) - ).toBeInTheDocument(); - if (assertion === false) - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - } - ); - - test.each` - case | excludedFolders | assertion | action - ${1} | ${foldersIncludedDeletePermanently} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.DELETE_PERMANENTLY} - `( - `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, - async ({ excludedFolders, action, assertion }) => { - forEach(excludedFolders.value, (excludedFolder: string) => { - const folders = getFoldersExcluded(generalFolders, excludedFolder); - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - - const testProps = { - ...props, - items: messages, - folderId: excludedFolder, - selectedIds - }; - - setupTest(, { store }); - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - - forEach(excludedFolders.value, (excludedFolder) => { - const folders = excludedFolder; - - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - const testProps = { - ...props, - items: messages, - folderId: folders.value[0], - selectedIds - }; - - setupTest(, { store }); - if (assertion === true) - expect( - screen.getByTestId(`primary-multi-action-button-${action.id}`) - ).toBeInTheDocument(); - if (assertion === false) - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - } - ); - test.each` - case | excludedFolders | action - ${1} | ${foldersExcludedMoveToFolder} | ${MessageActionsDescriptors.MOVE} - `( - `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, - async ({ excludedFolders, action }) => { - forEach(excludedFolders.value, (excludedFolder: string) => { - const folders = getFoldersExcluded(generalFolders, excludedFolder); - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - - const testProps = { - ...props, - items: messages, - folderId: excludedFolder, - selectedIds - }; - - setupTest(, { store }); - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - expect(screen.getByTestId(`primary-multi-action-button-${action.id}`)).toBeInTheDocument(); - }); - - forEach(excludedFolders.value, (excludedFolder) => { - const folders = getFoldersAllowed(generalFolders, excludedFolder); - - const messages = folders.value.map((folder: string) => - generateMessage({ folderId: folder }) - ); - const selectedIds = getSelectedIds(messages); - const testProps = { - ...props, - items: messages, - folderId: folders.value[0], - selectedIds - }; - - const { user } = setupTest(, { store }); - expect(screen.getByTestId(`primary-multi-action-button-${action.id}`)).toBeInTheDocument(); - expect( - screen.queryByTestId(`primary-multi-action-button-${action.id}`) - ).not.toBeInTheDocument(); - }); - } - ); -}); - -test.todo( - 'primary actions contain the mark as read action if the selection doesn’t involve a trashed, junk or draft message and at least one message is unread' -); -test.todo( - 'primary actions contain the mark as unread action if the selection doesn’t involve a trashed, junk or draft message and at least one message is read' -); -test.todo( - 'primary actions don’t contain the mark as read action if the selection involve a trashed, junk or draft message' -); -test.todo( - 'primary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' -); -test.todo( - 'primary actions contain the delete permanently action if the selection involve trashed messages only' -); -test.todo( - 'primary actions contain the trash action if the selection doesn’t involve a trashed message' -); -test.todo( - 'secondary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' -); -test.todo( - 'secondary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' -); -// test.todo('secondary actions contain the flag action'); +// /* +// * SPDX-FileCopyrightText: 2023 Zextras +// * +// * SPDX-License-Identifier: AGPL-3.0-only +// */ +// +// TODO: CO-1725 re-enable this + +// import React from 'react'; +// +// import { screen } from '@testing-library/react'; +// import { forEach, noop, reduce } from 'lodash'; +// +// import { setupTest } from '../../carbonio-ui-commons/test/test-setup'; +// import { FOLDERS_DESCRIPTORS, MessageActionsDescriptors } from '../../constants'; +// import { ASSERTIONS, MSG_CONV_STATUS_DESCRIPTORS } from '../../tests/constants'; +// import { generateMessage } from '../../tests/generators/generateMessage'; +// import { generateStore } from '../../tests/generators/store'; +// import type { MailMessage } from '../../types'; +// import { MultipleSelectionActionsPanel } from '../../views/app/folder-panel/parts/multiple-selection-actions-panel'; +// +// const generalFolders = { +// desc: 'general folders', +// value: [ +// FOLDERS_DESCRIPTORS.INBOX.id, +// FOLDERS_DESCRIPTORS.SENT.id, +// FOLDERS_DESCRIPTORS.USER_DEFINED.id +// ] +// }; +// const foldersExcludedMarkReadUnread = { +// desc: 'folders excluded mark read unread', +// value: [FOLDERS_DESCRIPTORS.DRAFTS.id, FOLDERS_DESCRIPTORS.SPAM.id, FOLDERS_DESCRIPTORS.TRASH.id] +// }; +// const foldersExcludedTrash = [FOLDERS_DESCRIPTORS.TRASH]; +// const foldersIncludedDeletePermanently = [FOLDERS_DESCRIPTORS.TRASH]; +// const foldersExcludedMoveToFolder = [FOLDERS_DESCRIPTORS.DRAFTS, FOLDERS_DESCRIPTORS.TRASH]; +// +// function getSelectedIds(messages: MailMessage[]): Array { +// return messages.map((message) => message.id); +// } +// +// function getFoldersExcluded( +// _generalFolders: { desc: string; value: Array }, +// excludedFolder: string +// ): { desc: string; value: Array } { +// return { +// ...generalFolders, +// value: [...generalFolders.value, excludedFolder] +// }; +// } +// +// function getFoldersAllowed( +// _generalFolders: { desc: string; value: Array }, +// excludedFolder: string +// ): { desc: string; value: Array } { +// const filteredFolderValues = reduce( +// generalFolders.value, +// (acc, folder) => { +// if (!excludedFolder.includes(folder)) acc.push(folder); +// return acc; +// }, +// [] as Array +// ); +// return { +// ...generalFolders, +// value: filteredFolderValues +// }; +// } +// +// const deselectAll = jest.fn(); +// const selectAll = jest.fn(); +// const store = generateStore(); +// +// const props = { +// items: [], +// selectAllModeOff: noop, +// setIsSelectModeOn: noop, +// folderId: '', +// isAllSelected: false, +// selectedIds: [], +// deselectAll, +// selectAll +// }; +// describe('Actions visibility', () => { +// test.each` +// case | read | excludedFolders | assertion | action +// ${1} | ${MSG_CONV_STATUS_DESCRIPTORS.READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MARK_AS_UNREAD} +// ${2} | ${MSG_CONV_STATUS_DESCRIPTORS.NOT_READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MARK_AS_READ} +// ${3} | ${MSG_CONV_STATUS_DESCRIPTORS.READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.NOT_CONTAINS} | ${MessageActionsDescriptors.MARK_AS_READ} +// ${4} | ${MSG_CONV_STATUS_DESCRIPTORS.NOT_READ} | ${foldersExcludedMarkReadUnread} | ${ASSERTIONS.NOT_CONTAINS} | ${MessageActionsDescriptors.MARK_AS_UNREAD} +// `( +// `(case #$case) secondary actions for a message in $folders.desc $assertion.desc the $action.desc action`, +// async ({ excludedFolders, action, read, assertion }) => { +// forEach(excludedFolders.value, (excludedFolder: string) => { +// const folders = getFoldersExcluded(generalFolders, excludedFolder); +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder, isRead: read.value }) +// ); +// const selectedIds = getSelectedIds(messages); +// +// const testProps = { +// ...props, +// items: messages, +// folderId: excludedFolder, +// selectedIds +// }; +// +// setupTest(, { store }); +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// +// // test that the action is not visible if the message is read and the folder is excluded +// forEach(excludedFolders.value, (excludedFolder) => { +// const folders = getFoldersAllowed(generalFolders, excludedFolder); +// +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder, isRead: read.value }) +// ); +// const selectedIds = getSelectedIds(messages); +// const testProps = { +// ...props, +// items: messages, +// folderId: folders.value[0], +// selectedIds +// }; +// +// setupTest(, { store }); +// if (assertion === true) +// expect( +// screen.getByTestId(`primary-multi-action-button-${action.id}`) +// ).toBeInTheDocument(); +// if (assertion === false) +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// } +// ); +// +// test.each` +// case | excludedFolders | assertion | action +// ${1} | ${foldersExcludedTrash} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.MOVE_TO_TRASH} +// `( +// `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, +// async ({ excludedFolders, action, assertion }) => { +// forEach(excludedFolders.value, (excludedFolder: string) => { +// const folders = getFoldersExcluded(generalFolders, excludedFolder); +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// +// const testProps = { +// ...props, +// items: messages, +// folderId: excludedFolder, +// selectedIds +// }; +// +// setupTest(, { store }); +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// +// forEach(excludedFolders.value, (excludedFolder) => { +// const folders = getFoldersAllowed(generalFolders, excludedFolder); +// +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// const testProps = { +// ...props, +// items: messages, +// folderId: folders.value[0], +// selectedIds +// }; +// +// setupTest(, { store }); +// if (assertion === true) +// expect( +// screen.getByTestId(`primary-multi-action-button-${action.id}`) +// ).toBeInTheDocument(); +// if (assertion === false) +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// } +// ); +// +// test.each` +// case | excludedFolders | assertion | action +// ${1} | ${foldersIncludedDeletePermanently} | ${ASSERTIONS.CONTAINS} | ${MessageActionsDescriptors.DELETE_PERMANENTLY} +// `( +// `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, +// async ({ excludedFolders, action, assertion }) => { +// forEach(excludedFolders.value, (excludedFolder: string) => { +// const folders = getFoldersExcluded(generalFolders, excludedFolder); +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// +// const testProps = { +// ...props, +// items: messages, +// folderId: excludedFolder, +// selectedIds +// }; +// +// setupTest(, { store }); +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// +// forEach(excludedFolders.value, (excludedFolder) => { +// const folders = excludedFolder; +// +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// const testProps = { +// ...props, +// items: messages, +// folderId: folders.value[0], +// selectedIds +// }; +// +// setupTest(, { store }); +// if (assertion === true) +// expect( +// screen.getByTestId(`primary-multi-action-button-${action.id}`) +// ).toBeInTheDocument(); +// if (assertion === false) +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// } +// ); +// test.each` +// case | excludedFolders | action +// ${1} | ${foldersExcludedMoveToFolder} | ${MessageActionsDescriptors.MOVE} +// `( +// `(case #$case) primary actions for a message in $folders.desc $assertion.desc the $action.desc action`, +// async ({ excludedFolders, action }) => { +// forEach(excludedFolders.value, (excludedFolder: string) => { +// const folders = getFoldersExcluded(generalFolders, excludedFolder); +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// +// const testProps = { +// ...props, +// items: messages, +// folderId: excludedFolder, +// selectedIds +// }; +// +// setupTest(, { store }); +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// expect(screen.getByTestId(`primary-multi-action-button-${action.id}`)).toBeInTheDocument(); +// }); +// +// forEach(excludedFolders.value, (excludedFolder) => { +// const folders = getFoldersAllowed(generalFolders, excludedFolder); +// +// const messages = folders.value.map((folder: string) => +// generateMessage({ folderId: folder }) +// ); +// const selectedIds = getSelectedIds(messages); +// const testProps = { +// ...props, +// items: messages, +// folderId: folders.value[0], +// selectedIds +// }; +// +// const { user } = setupTest(, { store }); +// expect(screen.getByTestId(`primary-multi-action-button-${action.id}`)).toBeInTheDocument(); +// expect( +// screen.queryByTestId(`primary-multi-action-button-${action.id}`) +// ).not.toBeInTheDocument(); +// }); +// } +// ); +// }); +// +// test.todo( +// 'primary actions contain the mark as read action if the selection doesn’t involve a trashed, junk or draft message and at least one message is unread' +// ); +// test.todo( +// 'primary actions contain the mark as unread action if the selection doesn’t involve a trashed, junk or draft message and at least one message is read' +// ); +// test.todo( +// 'primary actions don’t contain the mark as read action if the selection involve a trashed, junk or draft message' +// ); +// test.todo( +// 'primary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' +// ); // test.todo( -// 'secondary actions don’t contain the move action if the selection involve a trashed or draft message' +// 'primary actions contain the delete permanently action if the selection involve trashed messages only' // ); -// test.todo('secondary actions contain the tag submenu'); // test.todo( -// 'secondary actions don’t contain the mark as spam action if the selection involve a trashed, junk or draft message' +// 'primary actions contain the trash action if the selection doesn’t involve a trashed message' // ); // test.todo( -// 'secondary actions contain the restore action if the selection involve trashed messages only' +// 'secondary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' // ); // test.todo( -// 'secondary actions contain the delete permanently action if the selection involve trashed messages only' +// 'secondary actions don’t contain the mark as unread action if the selection involve a trashed, junk or draft message' // ); +// // test.todo('secondary actions contain the flag action'); +// // test.todo( +// // 'secondary actions don’t contain the move action if the selection involve a trashed or draft message' +// // ); +// // test.todo('secondary actions contain the tag submenu'); +// // test.todo( +// // 'secondary actions don’t contain the mark as spam action if the selection involve a trashed, junk or draft message' +// // ); +// // test.todo( +// // 'secondary actions contain the restore action if the selection involve trashed messages only' +// // ); +// // test.todo( +// // 'secondary actions contain the delete permanently action if the selection involve trashed messages only' +// // ); diff --git a/src/views/app/folder-panel/conversations/conversation-list-component.tsx b/src/views/app/folder-panel/conversations/conversation-list-component.tsx index 0428aecf7..928dea5cd 100644 --- a/src/views/app/folder-panel/conversations/conversation-list-component.tsx +++ b/src/views/app/folder-panel/conversations/conversation-list-component.tsx @@ -16,7 +16,6 @@ import { useFolder, useRoot } from '../../../../carbonio-ui-commons/store/zustan import type { Conversation } from '../../../../types'; import ShimmerList from '../../../search/shimmer-list'; import { Breadcrumbs } from '../parts/breadcrumbs'; -import { MultipleSelectionActionsPanel } from '../parts/multiple-selection-actions-panel'; import { getFolderPath } from '../parts/utils/utils'; const DragImageContainer = styled.div` @@ -155,17 +154,19 @@ export const ConversationListComponent: FC = mem return ( <> {isSelectModeOn ? ( - + <> ) : ( + // TODO: CO-1725 re-enable this + // showBreadcrumbs && ( = memo( return ( <> {isSelectModeOn ? ( - + <> ) : ( + // TODO: CO-1725 re-enable this + // showBreadcrumbs && ( = memo( )} - + {/* TODO CO-1725 re-enable it */} + {/* */} ) : ( diff --git a/src/views/app/folder-panel/messages/tests/message-list-component.test.tsx b/src/views/app/folder-panel/messages/tests/message-list-component.test.tsx index 0f96a6dc9..bd033be6c 100644 --- a/src/views/app/folder-panel/messages/tests/message-list-component.test.tsx +++ b/src/views/app/folder-panel/messages/tests/message-list-component.test.tsx @@ -1,88 +1,95 @@ /* - * SPDX-FileCopyrightText: 2023 Zextras + * SPDX-FileCopyrightText: 2024 Zextras * * SPDX-License-Identifier: AGPL-3.0-only */ -import React from 'react'; +// /* +// * SPDX-FileCopyrightText: 2023 Zextras +// * +// * SPDX-License-Identifier: AGPL-3.0-only +// */ +// TODO: CO-1725 re-enable this -import { screen } from '@testing-library/react'; -import { noop, times } from 'lodash'; - -import { FOLDERS } from '../../../../../carbonio-ui-commons/constants/folders'; -import { setupTest } from '../../../../../carbonio-ui-commons/test/test-setup'; -import { generateMessage } from '../../../../../tests/generators/generateMessage'; -import { generateStore } from '../../../../../tests/generators/store'; -import { MessageListComponent, MessageListComponentProps } from '../message-list-component'; -import { MessageListItemComponent } from '../message-list-item-component'; - -describe.each` - type | isSearchModule - ${'message list'} | ${false} - ${'search message list'} | ${true} -`('$type component', ({ isSearchModule }) => { - test('populate a message list and check that the messages are visible', async () => { - // Populate a message list - const MESSAGES_COUNT = 100; - const folderId = FOLDERS.INBOX; - const messages = times(MESSAGES_COUNT, (index) => - generateMessage({ id: `${index}`, folderId }) - ); - - const listItems = messages.map((message) => ( - - )); - - const props: MessageListComponentProps = { - deselectAll: noop, - displayerTitle: 'test', - folderId, - isAllSelected: false, - isSelectModeOn: false, - listItems, - messages, - messagesLoadingCompleted: true, - selectAll: noop, - selectAllModeOff: noop, - selected: {}, - selectedIds: [], - setIsSelectModeOn: noop, - isSearchModule, - totalMessages: messages.length, - setDraggedIds: noop - }; - - const store = generateStore({ - messages: { - searchedInFolder: {}, - messages: { - ...messages.map((msg) => ({ [msg.id]: msg })) - }, - searchRequestStatus: null - } - }); - - setupTest(, { store }); - - await screen.findByTestId(`message-list-${folderId}`); - const items = await screen.findAllByTestId(/MessageListItem-/); - - // Test that there is a list item for each message - expect(items.length).toBe(messages.length); - - // Test that every list item is visible - items.forEach((item) => { - expect(item).toBeVisible(); - }); - }); -}); +// import React from 'react'; +// +// import { screen } from '@testing-library/react'; +// import { noop, times } from 'lodash'; +// +// import { FOLDERS } from '../../../../../carbonio-ui-commons/constants/folders'; +// import { setupTest } from '../../../../../carbonio-ui-commons/test/test-setup'; +// import { generateMessage } from '../../../../../tests/generators/generateMessage'; +// import { generateStore } from '../../../../../tests/generators/store'; +// import { MessageListComponent, MessageListComponentProps } from '../message-list-component'; +// import { MessageListItemComponent } from '../message-list-item-component'; +// +// describe.each` +// type | isSearchModule +// ${'message list'} | ${false} +// ${'search message list'} | ${true} +// `('$type component', ({ isSearchModule }) => { +// test('populate a message list and check that the messages are visible', async () => { +// // Populate a message list +// const MESSAGES_COUNT = 100; +// const folderId = FOLDERS.INBOX; +// const messages = times(MESSAGES_COUNT, (index) => +// generateMessage({ id: `${index}`, folderId }) +// ); +// +// const listItems = messages.map((message) => ( +// +// )); +// +// const props: MessageListComponentProps = { +// deselectAll: noop, +// displayerTitle: 'test', +// folderId, +// isAllSelected: false, +// isSelectModeOn: false, +// listItems, +// messages, +// messagesLoadingCompleted: true, +// selectAll: noop, +// selectAllModeOff: noop, +// selected: {}, +// selectedIds: [], +// setIsSelectModeOn: noop, +// isSearchModule, +// totalMessages: messages.length, +// setDraggedIds: noop +// }; +// +// const store = generateStore({ +// messages: { +// searchedInFolder: {}, +// messages: { +// ...messages.map((msg) => ({ [msg.id]: msg })) +// }, +// searchRequestStatus: null +// } +// }); +// +// setupTest(, { store }); +// +// await screen.findByTestId(`message-list-${folderId}`); +// const items = await screen.findAllByTestId(/MessageListItem-/); +// +// // Test that there is a list item for each message +// expect(items.length).toBe(messages.length); +// +// // Test that every list item is visible +// items.forEach((item) => { +// expect(item).toBeVisible(); +// }); +// }); +// }); diff --git a/src/views/app/folder-panel/parts/multiple-selection-actions-panel.tsx b/src/views/app/folder-panel/parts/multiple-selection-actions-panel.tsx index 60bfe7a8b..c5b17d3c5 100644 --- a/src/views/app/folder-panel/parts/multiple-selection-actions-panel.tsx +++ b/src/views/app/folder-panel/parts/multiple-selection-actions-panel.tsx @@ -1,135 +1,143 @@ /* - * SPDX-FileCopyrightText: 2021 Zextras + * SPDX-FileCopyrightText: 2024 Zextras * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { FC, useCallback, useEffect, useState } from 'react'; +// /* +// * SPDX-FileCopyrightText: 2021 Zextras +// * +// * SPDX-License-Identifier: AGPL-3.0-only +// */ -import { Button, Container, IconButton, Row, Tooltip } from '@zextras/carbonio-design-system'; -import { t, useUserSettings } from '@zextras/carbonio-shell-ui'; +// TODO: CO-1725 re-enable this -import { useUiUtilities } from '../../../../hooks/use-ui-utilities'; -import { getFolderParentId } from '../../../../ui-actions/utils'; -import { ConversationsMultipleSelectionActions } from '../conversations/conversations-multiple-selection-actions'; -import { MessagesMultipleSelectionActions } from '../messages/messages-multiple-selection-actions'; - -type MultipleSelectionActionsPanelProps = { - items: Set; - selectedIds: Array; - deselectAll: () => void; - selectAll: () => void; - isAllSelected: boolean; - selectAllModeOff: () => void; - setIsSelectModeOn: (value: boolean) => void; - folderId: string; -}; - -export const MultipleSelectionActionsPanel: FC = ({ - items, - selectedIds, - deselectAll, - selectAll, - isAllSelected, - selectAllModeOff, - setIsSelectModeOn, - folderId -}) => { - const { createSnackbar } = useUiUtilities(); - const { zimbraPrefGroupMailBy } = useUserSettings().prefs; - const isConversation = zimbraPrefGroupMailBy === 'conversation'; - - const folderParentId = getFolderParentId({ folderId, isConversation, items }); - - const [currentFolderId] = useState(folderParentId); - - // This useEffect is required to reset the select mode when the user navigates to a different folder - useEffect(() => { - if (folderId && currentFolderId !== folderParentId) { - deselectAll(); - setIsSelectModeOn(false); - } - }, [currentFolderId, deselectAll, folderId, folderParentId, setIsSelectModeOn]); - - const ids = Object.values(selectedIds ?? []); - const messagesArrayIsNotEmpty = ids.length > 0; - - const arrowBackOnClick = useCallback(() => { - deselectAll(); - setIsSelectModeOn(false); - }, [deselectAll, setIsSelectModeOn]); - - const selectAllOnClick = useCallback(() => { - selectAll(); - createSnackbar({ - key: `selected-${ids}`, - replace: true, - severity: 'info', - label: t('label.all_items_selected', 'All visible items have been selected'), - autoHideTimeout: 5000, - hideButton: true - }); - }, [selectAll, createSnackbar, ids]); - - const iconButtonTooltip = t('label.exit_selection_mode', 'Exit selection mode'); - - return ( - - - - - - -