diff --git a/src/api/get-filters.ts b/src/api/get-filters.ts new file mode 100644 index 000000000..dae0e8c55 --- /dev/null +++ b/src/api/get-filters.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2025 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { soapFetch } from '@zextras/carbonio-shell-ui'; + +import { normalizeFilterRulesFromSoap } from '../normalizations/normalize-filter-rules'; +import type { FilterRules } from '../types'; + +export type FilterRulesAPIResponse = { + filterRules: FilterRules; +}; + +export const getIncomingFilters = async (): Promise => { + const { filterRules } = await soapFetch('GetFilterRules', { + _jsns: 'urn:zimbraMail' + }); + return normalizeFilterRulesFromSoap(filterRules); +}; + +export const getOutgoingFilters = async (): Promise => { + const { filterRules } = await soapFetch( + 'GetOutgoingFilterRules', + { + _jsns: 'urn:zimbraMail' + } + ); + return { filterRules }; +}; diff --git a/src/normalizations/normalize-filter-rules.ts b/src/normalizations/normalize-filter-rules.ts index e3cfcd30e..a906ae0d5 100644 --- a/src/normalizations/normalize-filter-rules.ts +++ b/src/normalizations/normalize-filter-rules.ts @@ -3,33 +3,27 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { map, omit, reduce } from 'lodash'; -import type { FilterRules, FilterTest } from '../types'; +import { map, omit } from 'lodash'; -export const normalizeFilterTests = (filterTests: FilterTest): FilterTest => - reduce( - Object.keys(filterTests), - (acc, testKey) => { - if (testKey !== 'condition') { - return { - ...acc, - [testKey]: map(filterTests[testKey], (filterTest) => omit(filterTest, 'index')) - }; - } - return { - ...acc, - condition: filterTests.condition - }; - }, - {} - ); +import { AllFiltersTest, FilterRules } from '../types'; + +const normalizeFilterTests = (filterTests: AllFiltersTest): AllFiltersTest => { + const result: AllFiltersTest = { condition: filterTests.condition }; + const keys = Object.keys(filterTests) as Array; + keys.forEach((testKey) => { + if (testKey !== 'condition') { + result[testKey] = map(filterTests[testKey], (filterTest) => omit(filterTest, 'index')); + } + }); + return result; +}; export const normalizeFilterRulesFromSoap = ( filterRules: FilterRules ): { filterRules: FilterRules } => { - const filterRule = map(filterRules?.[0]?.filterRule, (f) => ({ - ...f, - filterTests: [normalizeFilterTests(f.filterTests[0])] + const filterRule = map(filterRules?.[0]?.filterRule, (soapApiFilterRule) => ({ + ...soapApiFilterRule, + filterTests: [normalizeFilterTests(soapApiFilterRule.filterTests[0])] })); return { filterRules: [{ filterRule }] }; }; diff --git a/src/store/actions/get-incoming-filters.ts b/src/store/actions/get-incoming-filters.ts deleted file mode 100644 index 2dc06a736..000000000 --- a/src/store/actions/get-incoming-filters.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Zextras - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { soapFetch } from '@zextras/carbonio-shell-ui'; -import { normalizeFilterRulesFromSoap } from '../../normalizations/normalize-filter-rules'; -import type { FilterRules } from '../../types'; - -export const getIncomingFilters = async (): Promise => { - const { filterRules } = (await soapFetch('GetFilterRules', { - _jsns: 'urn:zimbraMail' - })) as { filterRules: FilterRules }; - const normalizedFilterRules = normalizeFilterRulesFromSoap(filterRules); - return normalizedFilterRules; -}; diff --git a/src/store/actions/get-outgoing-filters.ts b/src/store/actions/get-outgoing-filters.ts deleted file mode 100644 index f192a3fff..000000000 --- a/src/store/actions/get-outgoing-filters.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Zextras - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { soapFetch } from '@zextras/carbonio-shell-ui'; -import type { FilterRules } from '../../types'; - -export const getOutgoingFilters = createAsyncThunk( - 'filters/get_filters', - async (): Promise => { - const { filterRules } = (await soapFetch('GetOutgoingFilterRules', { - _jsns: 'urn:zimbraMail' - })) as { filterRules: FilterRules }; - return { filterRules }; - } -); diff --git a/src/types/filters/index.d.ts b/src/types/filters/index.d.ts index 3126aedaa..633b36a7e 100644 --- a/src/types/filters/index.d.ts +++ b/src/types/filters/index.d.ts @@ -1,22 +1,60 @@ /* - * SPDX-FileCopyrightText: 2021 Zextras + * SPDX-FileCopyrightText: 2025 Zextras * * SPDX-License-Identifier: AGPL-3.0-only */ + import { ChipProps } from '@zextras/carbonio-design-system'; -export type FilterTest = Record>; +import { ACTION_OPTION_KEYS } from '../../views/settings/filters/constants'; + +const TEST_CONDITIONS = [ + 'bodyTest', + 'addressTest', + 'headerTest', + 'sizeTest', + 'attachmentTest', + 'bulkTest', + 'listTest', + 'flaggedTest', + 'conversationTest', + 'dateTest', + 'mimeHeaderTest', + 'addressBookTest', + 'contactRankingTest', + 'meTest', + 'inviteTest', + 'linkedinTest', + 'facebookTest', + 'twitterTest' +] as const; +export type FilterTest = Record; +export type AllFiltersTest = { condition: string } & Partial< + Record<(typeof TEST_CONDITIONS)[number], Array> +>; + +type ApiFilterAction = { + actionRedirect?: Array; + actionTag?: Array; + actionFlag?: Array; + actionFileInto?: Array; + actionDiscard?: Array>; + actionStop?: Array>; + actionKeep?: Array>; +}; + +// TODO: add index to API actions export type Filter = { active: boolean; - filterActions: Array; - filterTests: Array; + filterActions: Array; + filterTests: Array; name: string; }; export type FilterRules = [ { - filterRule: Array; + filterRule?: Array; } ]; @@ -38,59 +76,73 @@ export type SearchEmailValue = { email: string; }; -export type FilterListType = { - active: boolean; - filterActions: Array; - filterTests: Array; +export type MarkAsOption = { + label: string; + value: { actionFlag: [{ flagName: string }] }; +}; +export type MailFilterTag = { + label: string; + color?: number; +}; + +type ActionFileInto = { + folderPath?: string; +}; +type ActionRedirect = { + a?: string; +}; +type ActionFlag = { + flagName?: string; +}; +type ActionTag = { + tagName?: string; +}; +// TODO: refactor the code and remove me after I'm not anymore needed +type CommonAction = { id?: string; - name: string; + actionStop?: [Record]; + // Only here for MarkAs + label?: string; + value?: string; +}; +type FilterKeep = CommonAction & { + actionKeep: [Record]; +}; +type FilterRedirect = CommonAction & { + actionRedirect: [ActionRedirect]; +}; +type FilterFlag = CommonAction & { + actionFlag: [ActionFlag]; }; -export type ListPropsType = { - isSelecting: boolean; - list: Array; - moveDown: (arg: number) => void; - moveUp: (arg: number) => void; - selected: Record; - toggle: (arg: string) => void; - unSelect: () => void; +type FilterFileInto = CommonAction & { + actionFileInto: [ActionFileInto]; +}; +type FilterDiscard = CommonAction & { + actionDiscard: [Record]; +}; +type FilterTag = CommonAction & { + actionTag: [ActionTag]; +}; +type FilterStop = CommonAction & { + actionStop: [Record]; }; -export type FilterActions = { - actionFileInto?: [ - { - folderPath: string; - index: string; - } - ]; - actionRedirect?: [ - { - a: string; - index: string; - } - ]; - actionFlag?: [ - { - flagName: string; - index: string; - } - ]; - actionStop?: [ - { - index?: string; - } - ]; - actionTag?: [ - { - index?: string; - tagName: string | undefined; - } - ]; - actionDiscard?: { - index: string; - }; - actionKeep?: [ - { - index?: string; - } - ]; +export type FilterAction = + | FilterKeep + | FilterRedirect + | FilterTag + | FilterFlag + | FilterFileInto + | FilterDiscard + | FilterStop; + +export type FilterActionsProps = { + isIncoming: boolean; + tempActions: Array; + setTempActions: (tempActions: Array) => void; + zimbraFeatureMailForwardingInFiltersEnabled: 'TRUE' | 'FALSE'; }; + +export type FilterActions = Array; + +export type ActionKey = (typeof ACTION_OPTION_KEYS)[number]; diff --git a/src/ui-actions/modals/apply-filter-modal.test.tsx b/src/ui-actions/modals/apply-filter-modal.test.tsx index 267cfb5c4..46259e1fc 100644 --- a/src/ui-actions/modals/apply-filter-modal.test.tsx +++ b/src/ui-actions/modals/apply-filter-modal.test.tsx @@ -15,22 +15,22 @@ describe('Apply Filter Modal', () => { test('should render the modal', async () => { const store = generateStore(); - setupTest(, { + setupTest(, { store }); - expect(await screen.findByText(/modals\.apply_filters\.title/i)).toBeInTheDocument(); + expect(await screen.findByText('Application filter My filter')).toBeInTheDocument(); }); test('should open folder selection modal when folder icon is clicked', async () => { const store = generateStore(); const { user } = setupTest( - , + , { store } ); - expect(await screen.findByText(/modals\.apply_filters\.title/i)).toBeInTheDocument(); + expect(await screen.findByText('Application filter My filter')).toBeInTheDocument(); const folderButton = screen.getByTestId('icon: FolderOutline'); act(() => { user.click(folderButton); diff --git a/src/ui-actions/modals/apply-filter-modal.tsx b/src/ui-actions/modals/apply-filter-modal.tsx index fa9f4a94b..6bbe2885a 100644 --- a/src/ui-actions/modals/apply-filter-modal.tsx +++ b/src/ui-actions/modals/apply-filter-modal.tsx @@ -15,8 +15,7 @@ import { useModal, useSnackbar } from '@zextras/carbonio-design-system'; -import { t } from '@zextras/carbonio-shell-ui'; -import { Trans } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { applyFilterRules } from '../../api/apply-filter-rules'; import { Folder } from '../../carbonio-ui-commons/types/folder'; @@ -33,6 +32,7 @@ export type ApplyFilterModalProps = { }; export const ApplyFilterModal: FC = ({ criteria, onClose }) => { + const [t] = useTranslation(); const { createModal, closeModal } = useModal(); const createSnackbar = useSnackbar(); const [folder, setFolder] = useState(); @@ -102,7 +102,7 @@ export const ApplyFilterModal: FC = ({ criteria, onClose hideButton: true }); onClose(); - }, [createSnackbar, criteria.filterName, folder, onClose]); + }, [createSnackbar, criteria.filterName, folder, onClose, t]); const onCancelAction = useCallback(() => onClose(), [onClose]); diff --git a/src/views/settings/filters/constants.ts b/src/views/settings/filters/constants.ts new file mode 100644 index 000000000..db5d60cdc --- /dev/null +++ b/src/views/settings/filters/constants.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const ACTION_OPTIONS = { + KEEP: 'actionKeep', + DISCARD: 'actionDiscard', + MOVE_TO_FOLDER: 'actionFileInto', + TAG: 'actionTag', + MARK_AS: 'actionFlag', + REDIRECT_TO: 'actionRedirect' +} as const; + +export type ACTION_OPTION_KEYS = (typeof ACTION_OPTIONS)[keyof typeof ACTION_OPTIONS]; diff --git a/src/views/settings/filters/filter-tabs.tsx b/src/views/settings/filters/filter-tabs.tsx new file mode 100644 index 000000000..a765adee3 --- /dev/null +++ b/src/views/settings/filters/filter-tabs.tsx @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2025 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import React, { FC, ReactElement, useCallback, useMemo, useState } from 'react'; + +import { Container, Divider, Padding, TabBar, TabBarProps } from '@zextras/carbonio-design-system'; +import { useTranslation } from 'react-i18next'; + +import { IncomingFiltersTab } from './incoming-filters-tab'; +import { OutgoingFiltersTab } from './outgoing-filters-tab'; + +export const FilterTabs: FC = (): ReactElement => { + const [t] = useTranslation(); + const [selectedFilterType, setSelectedFilterType] = useState('incoming-messages'); + const tabs = useMemo( + () => [ + { + id: 'incoming-messages', + label: t('filters.incoming_msg_filters', 'Incoming Message Filters') + }, + { + id: 'outgoing-messages', + label: t('filters.outgoing_message_filters', 'Outgoing Message Filters') + } + ], + [t] + ); + const onChange = useCallback((ev, selectedId) => { + setSelectedFilterType(selectedId); + }, []); + + return ( + + + + {selectedFilterType === 'incoming-messages' && } + {selectedFilterType === 'outgoing-messages' && } + + + + + + ); +}; diff --git a/src/views/settings/filters/incoming-filters-tab.tsx b/src/views/settings/filters/incoming-filters-tab.tsx new file mode 100644 index 000000000..e9dfa5c0a --- /dev/null +++ b/src/views/settings/filters/incoming-filters-tab.tsx @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2025 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useMemo } from 'react'; + +import { getFiltermanager } from './parts/filter-manager'; +import { MessageFilterTab } from './parts/message-filter-tab'; +import { getIncomingFilters } from '../../../api/get-filters'; +import { modifyFilterRules } from '../../../store/actions/modify-filter-rules'; + +export const IncomingFiltersTab = (): React.JSX.Element => { + const filtersManagerComponent = useMemo(() => getFiltermanager(true), []); + return ( + + ); +}; diff --git a/src/views/settings/filters/index.tsx b/src/views/settings/filters/index.tsx index 6615bc83b..a6d2612a4 100644 --- a/src/views/settings/filters/index.tsx +++ b/src/views/settings/filters/index.tsx @@ -3,10 +3,12 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { t } from '@zextras/carbonio-shell-ui'; import React, { FC, ReactElement, useMemo } from 'react'; + import { Container, Text, Divider } from '@zextras/carbonio-design-system'; -import FilterTabs from './parts/filter-tabs'; +import { t } from '@zextras/carbonio-shell-ui'; + +import { FilterTabs } from './filter-tabs'; import Heading from '../components/settings-heading'; import { filtersSubSection } from '../subsections'; diff --git a/src/views/settings/filters/outgoing-filters-tab.tsx b/src/views/settings/filters/outgoing-filters-tab.tsx new file mode 100644 index 000000000..6d4d4ea4d --- /dev/null +++ b/src/views/settings/filters/outgoing-filters-tab.tsx @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2025 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useMemo } from 'react'; + +import { getFiltermanager } from './parts/filter-manager'; +import { MessageFilterTab } from './parts/message-filter-tab'; +import { getOutgoingFilters } from '../../../api/get-filters'; +import { modifyOutgoingFilterRules } from '../../../store/actions/modify-filter-rules'; + +export const OutgoingFiltersTab = (): React.JSX.Element => { + const filtersManagerComponent = useMemo(() => getFiltermanager(false), []); + return ( + + ); +}; diff --git a/src/views/settings/filters/parts/actions.ts b/src/views/settings/filters/parts/actions.ts index 5fff07660..9f6c47679 100644 --- a/src/views/settings/filters/parts/actions.ts +++ b/src/views/settings/filters/parts/actions.ts @@ -5,15 +5,16 @@ */ import { useCallback } from 'react'; -import { TFunction } from 'i18next'; +import { useSnackbar } from '@zextras/carbonio-design-system'; import { concat, filter, findIndex } from 'lodash'; +import { useTranslation } from 'react-i18next'; import { useUiUtilities } from '../../../../hooks/use-ui-utilities'; -import { FilterListType } from '../../../../types'; +import { Filter } from '../../../../types'; export type ListType = { isSelecting: boolean; - list: Array; + list: Array; moveDown: (arg: number) => void; moveUp: (arg: number) => void; selected: Record; @@ -22,158 +23,84 @@ export type ListType = { }; export type CompProps = { - t: TFunction; availableList: ListType; activeList: ListType; - setFilters: (arg: Array) => void; - setFetchFilters: (arg: boolean) => void; - modifierFunc: (arg: FilterListType[]) => Promise; + modifierFunc: (arg: Filter[]) => Promise; }; export type DeleteFilterCompProps = CompProps & { onClose: () => void; - selectedFilter: FilterListType; - incomingFilters: FilterListType[]; -}; - -export type DeleteOutgoingFilterCompProps = CompProps & { - onClose: () => void; - selectedFilter: FilterListType; - outgoingFilters: FilterListType[]; + filterToDelete: Filter; + filters: Filter[]; }; export const useRemoveFilter = (): ((arg: CompProps) => void) => { const { createSnackbar } = useUiUtilities(); + const [t] = useTranslation(); return useCallback( - ({ - t, - activeList, - availableList, - setFilters, - setFetchFilters, - modifierFunc - }: CompProps): void => { + ({ activeList, availableList, modifierFunc }: CompProps): void => { const activeFiltersCopy = activeList?.list?.slice(); const availableFiltersCopy = availableList?.list?.slice(); - const activeFilter = filter(activeFiltersCopy, { id: Object.keys(activeList.selected)[0] }); + const activeFilter = filter(activeFiltersCopy, { name: Object.keys(activeList.selected)[0] }); const activeIndex = findIndex(activeFiltersCopy, activeFilter[0]); activeFiltersCopy.splice(activeIndex, 1); availableFiltersCopy.push({ ...activeFilter[0], active: false }); const newFilters = concat(activeFiltersCopy, availableFiltersCopy); - setFilters(newFilters); activeList.unSelect(); - modifierFunc(newFilters) - .then(() => { - setFetchFilters(true); - }) - .catch((error) => { - createSnackbar({ - key: `share`, - replace: true, - hideButton: true, - severity: 'error', - label: - error?.message || - t('label.error_try_again', 'Something went wrong, please try again'), - autoHideTimeout: 5000 - }); + modifierFunc(newFilters).catch((error) => { + createSnackbar({ + key: `share`, + replace: true, + hideButton: true, + severity: 'error', + label: + error?.message || t('label.error_try_again', 'Something went wrong, please try again'), + autoHideTimeout: 5000 }); + }); }, - [createSnackbar] + [createSnackbar, t] ); }; export const useAddFilter = (): ((arg: CompProps) => void) => { const { createSnackbar } = useUiUtilities(); + const [t] = useTranslation(); + return useCallback( - ({ - t, - activeList, - availableList, - setFilters, - setFetchFilters, - modifierFunc - }: CompProps): void => { + ({ activeList, availableList, modifierFunc }: CompProps): void => { const activeFiltersCopy = activeList?.list?.slice(); const availableFiltersCopy = availableList?.list?.slice(); const activeFilter = filter(availableFiltersCopy, { - id: Object.keys(availableList.selected)[0] + name: Object.keys(availableList.selected)[0] }); const activeIndex = findIndex(availableFiltersCopy, activeFilter[0]); availableFiltersCopy.splice(activeIndex, 1); activeFiltersCopy.push({ ...activeFilter[0], active: true }); const newFilters = concat(activeFiltersCopy, availableFiltersCopy); - setFilters(newFilters); availableList.unSelect(); - modifierFunc(newFilters) - .then(() => { - setFetchFilters(true); - }) - .catch((error: { message: string }) => { - createSnackbar({ - key: 'filter-delete-error', - severity: 'error', - label: - error.message || t('label.error_try_again', 'Something went wrong, please try again'), - hideButton: true - }); + modifierFunc(newFilters).catch((error: { message: string }) => { + createSnackbar({ + key: 'filter-delete-error', + severity: 'error', + label: + error.message || t('label.error_try_again', 'Something went wrong, please try again'), + hideButton: true }); + }); }, - [createSnackbar] - ); -}; - -export const useDeleteOutgoingFilter = (): ((args: DeleteOutgoingFilterCompProps) => void) => { - const { createSnackbar } = useUiUtilities(); - return useCallback( - ({ - t, - setFetchFilters, - modifierFunc, - onClose, - selectedFilter, - outgoingFilters - }: DeleteOutgoingFilterCompProps): void => { - const newFilters = filter(outgoingFilters, (f) => f.name !== selectedFilter.name); - modifierFunc(newFilters) - .then(() => { - createSnackbar({ - key: 'filter-delete-success', - severity: 'info', - label: t('settings.filter_deleted', 'Filter successfully deleted'), - hideButton: true - }); - setFetchFilters(true); - }) - .catch((error: { message: any }) => { - createSnackbar({ - key: 'filter-delete-error', - severity: 'error', - label: - error.message || t('label.error_try_again', 'Something went wrong, please try again'), - hideButton: true - }); - }); - onClose(); - }, - [createSnackbar] + [createSnackbar, t] ); }; export const useDeleteFilter = (): ((args: DeleteFilterCompProps) => void) => { - const { createSnackbar } = useUiUtilities(); + const createSnackbar = useSnackbar(); + const [t] = useTranslation(); return useCallback( - ({ - t, - setFetchFilters, - modifierFunc, - onClose, - selectedFilter, - incomingFilters - }: DeleteFilterCompProps): void => { - const newFilters = filter(incomingFilters, (f) => f.name !== selectedFilter.name); + ({ modifierFunc, onClose, filterToDelete, filters }: DeleteFilterCompProps): void => { + const newFilters = filter(filters, (f) => f.name !== filterToDelete.name); modifierFunc(newFilters) .then(() => { createSnackbar({ @@ -182,7 +109,6 @@ export const useDeleteFilter = (): ((args: DeleteFilterCompProps) => void) => { label: t('settings.filter_deleted', 'Filter successfully deleted'), hideButton: true }); - setFetchFilters(true); }) .catch((error: { message: string }) => { createSnackbar({ @@ -195,6 +121,6 @@ export const useDeleteFilter = (): ((args: DeleteFilterCompProps) => void) => { }); onClose(); }, - [createSnackbar] + [createSnackbar, t] ); }; diff --git a/src/views/settings/filters/parts/create-filter-modal.tsx b/src/views/settings/filters/parts/create-filter-modal.tsx index 821e9a622..356688305 100644 --- a/src/views/settings/filters/parts/create-filter-modal.tsx +++ b/src/views/settings/filters/parts/create-filter-modal.tsx @@ -6,42 +6,41 @@ import React, { FC, ReactElement, useCallback, useMemo, useState } from 'react'; import { Input, Container, Checkbox, Padding, Divider, Row } from '@zextras/carbonio-design-system'; -import { useUserSettings } from '@zextras/carbonio-shell-ui'; -import type { TFunction } from 'i18next'; +import { BooleanString, useUserSettings } from '@zextras/carbonio-shell-ui'; import { map, omit, reduce } from 'lodash'; +import { useTranslation } from 'react-i18next'; import { v4 as uuidv4 } from 'uuid'; import { CreateFilterContext } from './create-filter-context'; import ModalFooter from './create-filter-modal-footer'; import DefaultCondition from './create-filters-conditions/default'; -import FilterTestConditionRow from './filter-test-condition-row'; -import FilterActionConditions from './new-filter-action-conditions'; +import { FilterActionsPanel } from './filter-actions-panel'; +import { FilterConditionsPanel } from './filter-conditions-panel'; import { getButtonInfo } from './utils'; import ModalHeader from '../../../../carbonio-ui-commons/components/modals/modal-header'; -import { modifyFilterRules } from '../../../../store/actions/modify-filter-rules'; -import type { FilterActions } from '../../../../types'; +import type { Filter, FilterActions } from '../../../../types'; type ComponentProps = { - t: TFunction; onClose: () => void; - incomingFilters?: any; - setFetchIncomingFilters?: (arg: boolean) => void; - setIncomingFilters?: (arg: any) => void; + onConfirm: (newFilter: Filter) => void; + isIncoming: boolean; }; const CreateFilterModal: FC = ({ - t, onClose, - incomingFilters, - setFetchIncomingFilters, - setIncomingFilters + onConfirm, + isIncoming }): ReactElement => { + const [t] = useTranslation(); const [filterName, setFilterName] = useState(''); const [activeFilter, setActiveFilter] = useState(false); const [condition, setCondition] = useState('anyof'); const [dontProcessAddFilters, setDontProcessAddFilters] = useState(true); - const [tempActions, setTempActions] = useState([{ actionKeep: [{}], id: uuidv4() }]); - const { zimbraFeatureMailForwardingInFiltersEnabled } = useUserSettings().attrs; + const [tempActions, setTempActions] = useState([ + { actionKeep: [{}], id: uuidv4() } + ]); + const zimbraFeatureMailForwardingInFiltersEnabled = useUserSettings().attrs + .zimbraFeatureMailForwardingInFiltersEnabled as BooleanString; const finalActions = useMemo( () => reduce( @@ -79,9 +78,6 @@ const CreateFilterModal: FC = ({ (ev: React.ChangeEvent) => setFilterName(ev.target.value), [] ); - const modalTitle = useMemo(() => t('settings.create_new_filter', 'Create new Filter'), [t]); - const inputLabel = useMemo(() => `${t('settings.filter_name', 'Filter Name')}*`, [t]); - const activeFilterLabel = useMemo(() => t('settings.active_filter', 'Active filter'), [t]); const requiredFilterTest = useMemo(() => { const allTest = map(newFilters, (f) => f.filterTests[0]); @@ -108,12 +104,12 @@ const CreateFilterModal: FC = ({ ...omit(finalActions, 'id'), actionStop: [{}] } - ] as FilterActions[]) + ] as FilterActions) : ([ { ...omit(finalActions, 'id') } - ] as FilterActions[]), + ] as FilterActions), active: activeFilter, name: filterName, filterTests: [ @@ -131,16 +127,6 @@ const CreateFilterModal: FC = ({ [filterName, requiredFilters, t] ); - const incomingFiltersCopy = useMemo(() => incomingFilters?.slice() || [], [incomingFilters]); - const onConfirm = useCallback(() => { - const toSend = [...incomingFiltersCopy, requiredFilters]; - setIncomingFilters?.(toSend); - modifyFilterRules(toSend).then(() => { - setFetchIncomingFilters?.(true); - }); - onClose(); - }, [incomingFiltersCopy, onClose, requiredFilters, setFetchIncomingFilters, setIncomingFilters]); - const toggleCheckBox = useCallback(() => { setDontProcessAddFilters(!dontProcessAddFilters); }, [dontProcessAddFilters]); @@ -150,12 +136,19 @@ const CreateFilterModal: FC = ({ t, activeFilter, filterName, - isIncoming: true, + isIncoming, tempActions, setTempActions, zimbraFeatureMailForwardingInFiltersEnabled }), - [t, activeFilter, filterName, tempActions, zimbraFeatureMailForwardingInFiltersEnabled] + [ + t, + activeFilter, + filterName, + isIncoming, + tempActions, + zimbraFeatureMailForwardingInFiltersEnabled + ] ); const filterTestConditionRowProps = useMemo( () => ({ t, newFilters, setNewFilters, condition, setCondition, activeFilter, filterName }), @@ -170,19 +163,22 @@ const CreateFilterModal: FC = ({ maxHeight="100%" style={{ overflowY: 'scroll', overflowX: 'hidden' }} > - + = ({ maxWidth="100%" width="100%" > - + - + onConfirm(requiredFilters)} disabled={createFilterDisabled} onSecondaryAction={toggleCheckBox} checked={dontProcessAddFilters} diff --git a/src/views/settings/filters/parts/create-filters-conditions/address-in.tsx b/src/views/settings/filters/parts/create-filters-conditions/address-in.tsx index ada4ef11c..96438c805 100644 --- a/src/views/settings/filters/parts/create-filters-conditions/address-in.tsx +++ b/src/views/settings/filters/parts/create-filters-conditions/address-in.tsx @@ -12,11 +12,13 @@ import React, { useState, useEffect } from 'react'; + import { Container } from '@zextras/carbonio-design-system'; import type { TFunction } from 'i18next'; + +import { CreateFilterContext } from '../create-filter-context'; import CustomSelect from '../custom-select'; import { getFromOptions, getInOptions, getFolderOptions, findDefaultValue } from '../utils'; -import { CreateFilterContext } from '../create-filter-context'; type ComponentProps = { t: TFunction; diff --git a/src/views/settings/filters/parts/create-filters-conditions/default.tsx b/src/views/settings/filters/parts/create-filters-conditions/default.tsx index a0c330aea..c00adb0c9 100644 --- a/src/views/settings/filters/parts/create-filters-conditions/default.tsx +++ b/src/views/settings/filters/parts/create-filters-conditions/default.tsx @@ -12,11 +12,14 @@ import React, { useState, useEffect } from 'react'; + import { Input, Container } from '@zextras/carbonio-design-system'; import type { TFunction } from 'i18next'; -import CustomSelect from '../custom-select'; -import { getConditionStatements, findDefaultValue } from '../utils'; +import { find } from 'lodash'; + import { CreateFilterContext } from '../create-filter-context'; +import CustomSelect from '../custom-select'; +import { getConditionStatements } from '../utils'; type ComponentProps = { t: TFunction; @@ -74,7 +77,7 @@ const DefaultCondition: FC = ({ t, activeIndex, defaultValue }): const defaultSelection = useMemo( () => defaultValue - ? findDefaultValue(conditionStatementsOptions, previousOption) + ? find(conditionStatementsOptions, { value: previousOption }) : conditionStatementsOptions[2], [conditionStatementsOptions, defaultValue, previousOption] ); diff --git a/src/views/settings/filters/parts/create-filters-conditions/from-to.tsx b/src/views/settings/filters/parts/create-filters-conditions/from-to.tsx index 0fa8a4e31..a9a2b7ab5 100644 --- a/src/views/settings/filters/parts/create-filters-conditions/from-to.tsx +++ b/src/views/settings/filters/parts/create-filters-conditions/from-to.tsx @@ -12,11 +12,13 @@ import React, { useCallback, useEffect } from 'react'; + import { Input, Container } from '@zextras/carbonio-design-system'; import type { TFunction } from 'i18next'; + +import { CreateFilterContext } from '../create-filter-context'; import CustomSelect from '../custom-select'; import { getConditionStatements, findDefaultValue, getDomainOptions } from '../utils'; -import { CreateFilterContext } from '../create-filter-context'; type ComponentProps = { t: TFunction; diff --git a/src/views/settings/filters/parts/create-filters-conditions/size.tsx b/src/views/settings/filters/parts/create-filters-conditions/size.tsx index f7dcc24c6..317916596 100644 --- a/src/views/settings/filters/parts/create-filters-conditions/size.tsx +++ b/src/views/settings/filters/parts/create-filters-conditions/size.tsx @@ -12,12 +12,14 @@ import React, { useEffect, useCallback } from 'react'; + import { Input, Container } from '@zextras/carbonio-design-system'; import type { TFunction } from 'i18next'; import { includes } from 'lodash'; + +import { CreateFilterContext } from '../create-filter-context'; import CustomSelect from '../custom-select'; import { getSizeOptions, getSizeUnit, findDefaultValue } from '../utils'; -import { CreateFilterContext } from '../create-filter-context'; type ComponentProps = { t: TFunction; diff --git a/src/views/settings/filters/parts/create-outgoing-filter-modal.tsx b/src/views/settings/filters/parts/create-outgoing-filter-modal.tsx deleted file mode 100644 index 4d90e0cce..000000000 --- a/src/views/settings/filters/parts/create-outgoing-filter-modal.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Zextras - * - * SPDX-License-Identifier: AGPL-3.0-only - */ -import React, { FC, ReactElement, useCallback, useMemo, useState } from 'react'; - -import { Checkbox, Container, Divider, Input, Padding, Row } from '@zextras/carbonio-design-system'; -import { useUserSettings } from '@zextras/carbonio-shell-ui'; -import { TFunction } from 'i18next'; -import { map, omit, reduce } from 'lodash'; - -import { CreateFilterContext } from './create-filter-context'; -import ModalFooter from './create-filter-modal-footer'; -import DefaultCondition from './create-filters-conditions/default'; -import FilterTestConditionRow from './filter-test-condition-row'; -import FilterActionConditions from './new-filter-action-conditions'; -import { getButtonInfo } from './utils'; -import ModalHeader from '../../../../carbonio-ui-commons/components/modals/modal-header'; -import { useUiUtilities } from '../../../../hooks/use-ui-utilities'; -import { modifyOutgoingFilterRules } from '../../../../store/actions/modify-filter-rules'; -import type { FilterActions } from '../../../../types'; - -type ComponentProps = { - t: TFunction; - onClose: () => void; - outgoingFilters?: any; - setFetchOutgoingFilters: (arg: boolean) => void; - setOutgoingFilters: (arg: any) => void; -}; - -const CreateOutgoingFilterModal: FC = ({ - t, - onClose, - outgoingFilters, - setFetchOutgoingFilters, - setOutgoingFilters -}): ReactElement => { - const { createSnackbar } = useUiUtilities(); - const [filterName, setFilterName] = useState(''); - const [activeFilter, setActiveFilter] = useState(false); - const [condition, setCondition] = useState('anyof'); - const [dontProcessAddFilters, setDontProcessAddFilters] = useState(true); - const [tempActions, setTempActions] = useState([{ actionKeep: [{}] }]); - const { zimbraFeatureMailForwardingInFiltersEnabled } = useUserSettings().attrs; - - const finalActions = useMemo( - () => - reduce( - tempActions, - (a, i) => { - const firstKey = Object.keys(omit(i, 'id'))[0]; - if (Object.keys(a).includes(firstKey)) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return { ...a, [firstKey]: [...a[firstKey], ...i[firstKey]] }; - } - return { ...a, ...i }; - }, - {} - ), - [tempActions] - ); - const [newFilters, setNewFilters] = useState([ - { - filterActions: [{ actionKeep: [{}], actionStop: [{}] }], - active: activeFilter, - name: filterName, - key: 'subject', - label: 'Subject', - filterTests: [{}], - index: 0, - comp: - } - ]); - - const toggleActiveFilter = useCallback(() => setActiveFilter(!activeFilter), [activeFilter]); - const onFilterNameChange = useCallback( - (ev: React.ChangeEvent) => setFilterName(ev.target.value), - [] - ); - const modalTitle = useMemo(() => t('settings.create_new_filter', 'Create new Filter'), [t]); - const inputLabel = useMemo(() => `${t('settings.filter_name', 'Filter Name')}*`, [t]); - const activeFilterLabel = useMemo(() => t('settings.active_filter', 'Active filter'), [t]); - - const requiredFilterTest = useMemo(() => { - const allTest = map(newFilters, (f) => f.filterTests[0]); - - return reduce( - allTest, - (a, i) => { - const firstKey = Object.keys(omit(i, ['condition']))[0]; - if (Object.keys(a).includes(firstKey)) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return { ...a, [firstKey]: [...a[firstKey], ...i[firstKey]] }; - } - return { ...a, ...i }; - }, - {} - ); - }, [newFilters]); - - const requiredFilters = useMemo( - () => ({ - filterActions: dontProcessAddFilters - ? ([{ ...finalActions, actionStop: [{}] }] as FilterActions[]) - : ([{ ...finalActions }] as FilterActions[]), - active: activeFilter, - name: filterName, - filterTests: [ - { - ...requiredFilterTest, - condition - } - ] - }), - [activeFilter, filterName, condition, requiredFilterTest, finalActions, dontProcessAddFilters] - ); - - const [createFilterDisabled, buttonTooltip] = useMemo( - () => getButtonInfo(filterName, requiredFilters, t), - [filterName, requiredFilters, t] - ); - - const outgoingFiltersCopy = useMemo(() => outgoingFilters?.slice() || [], [outgoingFilters]); - const onConfirm = useCallback(() => { - const toSend = [...outgoingFiltersCopy, requiredFilters]; - setOutgoingFilters(toSend); - modifyOutgoingFilterRules(toSend) - .then(() => { - setFetchOutgoingFilters(true); - }) - .catch((error) => { - createSnackbar({ - key: `share`, - replace: true, - hideButton: true, - severity: 'error', - label: - error?.message || t('label.error_try_again', 'Something went wrong, please try again'), - autoHideTimeout: 5000 - }); - }); - onClose(); - }, [ - outgoingFiltersCopy, - requiredFilters, - setOutgoingFilters, - onClose, - setFetchOutgoingFilters, - createSnackbar, - t - ]); - - const toggleCheckBox = useCallback(() => { - setDontProcessAddFilters(!dontProcessAddFilters); - }, [dontProcessAddFilters]); - - const filterActionProps = useMemo( - () => ({ - t, - activeFilter, - filterName, - isIncoming: false, - tempActions, - setTempActions, - zimbraFeatureMailForwardingInFiltersEnabled - }), - [t, activeFilter, filterName, tempActions, zimbraFeatureMailForwardingInFiltersEnabled] - ); - const filterTestConditionRowProps = useMemo( - () => ({ t, newFilters, setNewFilters, condition, setCondition, activeFilter, filterName }), - [t, newFilters, setNewFilters, condition, setCondition, activeFilter, filterName] - ); - return ( - - - - - - - - - - - - - - - - - - ); -}; - -export default CreateOutgoingFilterModal; diff --git a/src/views/settings/filters/parts/custom-select.tsx b/src/views/settings/filters/parts/custom-select.tsx index da2c84258..07a6fc66a 100644 --- a/src/views/settings/filters/parts/custom-select.tsx +++ b/src/views/settings/filters/parts/custom-select.tsx @@ -82,13 +82,13 @@ const getItems = (items: Array<{ label: string; value: any }>): GetItemsReturnTy const CustomSelect: FC<{ onChange: (arg: any) => void; - defaultSelection: { label: string; value: any }; + defaultSelection?: { label: string; value: any }; label: string; items: Array<{ label: string; value: any }>; background?: string; disabled?: boolean; }> = ({ onChange, defaultSelection, label, items, disabled = false }) => { - const newItems = useMemo(() => getItems(items), [items]); + const internalItems = getItems(items); return ( - - )} - -