From 6c84186e8b56f54edf22b6b1f1d64e3adc6c1b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radom=C3=ADr=20Pol=C3=A1ch?= Date: Thu, 26 Nov 2020 00:45:15 +0100 Subject: [PATCH 1/2] feature: first support for email search operators: from:, to:, has:attachment --- .../header/AdvancedSearchDropdown.tsx | 12 ++-- src/app/components/layout/PrivateLayout.tsx | 56 +++++++++++++++++-- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/app/components/header/AdvancedSearchDropdown.tsx b/src/app/components/header/AdvancedSearchDropdown.tsx index 5b2fabaa..846da087 100644 --- a/src/app/components/header/AdvancedSearchDropdown.tsx +++ b/src/app/components/header/AdvancedSearchDropdown.tsx @@ -36,7 +36,7 @@ import { extractSearchParameters, keywordToString } from '../../helpers/mailboxU import './AdvancedSearchDropdown.scss'; -interface SearchModel { +export interface SearchModel { keyword: string; labelID: string; from: Recipient[]; @@ -54,13 +54,13 @@ interface LabelInfo { group: string; } -const UNDEFINED = undefined; +export const UNDEFINED = undefined; const AUTO_WILDCARD = undefined; const ALL_ADDRESSES = 'all'; const NO_ATTACHMENTS = 0; -const WITH_ATTACHMENTS = 1; +export const WITH_ATTACHMENTS = 1; const { INBOX, TRASH, SPAM, ARCHIVE, ALL_MAIL, ALL_SENT, SENT, ALL_DRAFTS, DRAFTS } = MAILBOX_LABEL_IDS; -const DEFAULT_MODEL: SearchModel = { +export const DEFAULT_MODEL: SearchModel = { keyword: '', labelID: ALL_MAIL, from: [], @@ -70,12 +70,12 @@ const DEFAULT_MODEL: SearchModel = { wildcard: AUTO_WILDCARD, }; -const getRecipients = (value = '') => +export const getRecipients = (value = '') => value .split(',') .filter(validateEmailAddress) .map((Address) => ({ Address, Name: '' })); -const formatRecipients = (recipients: Recipient[] = []) => recipients.map(({ Address }) => Address).join(','); +export const formatRecipients = (recipients: Recipient[] = []) => recipients.map(({ Address }) => Address).join(','); const folderReducer = (acc: LabelInfo[], folder: FolderWithSubFolders, level = 0) => { acc.push({ diff --git a/src/app/components/layout/PrivateLayout.tsx b/src/app/components/layout/PrivateLayout.tsx index cb33ec10..b809e82d 100644 --- a/src/app/components/layout/PrivateLayout.tsx +++ b/src/app/components/layout/PrivateLayout.tsx @@ -1,14 +1,21 @@ import React, { useState, useEffect, ReactNode, useCallback } from 'react'; import { PrivateAppContainer } from 'react-components'; import { MAILBOX_LABEL_IDS } from 'proton-shared/lib/constants'; +import { changeSearchParams } from 'proton-shared/lib/helpers/url'; import { Location, History } from 'history'; -import MailHeader from '../header/MailHeader'; import MailSidebar from '../sidebar/MailSidebar'; +import MailHeader from '../header/MailHeader'; import { getHumanLabelID } from '../../helpers/labels'; -import { setKeywordInUrl } from '../../helpers/mailboxUrl'; import { Breakpoints } from '../../models/utils'; import { OnCompose } from '../../hooks/useCompose'; +import { + getRecipients, + formatRecipients, + DEFAULT_MODEL, + UNDEFINED, + WITH_ATTACHMENTS, +} from '../header/AdvancedSearchDropdown'; interface Props { children: ReactNode; @@ -33,8 +40,49 @@ const PrivateLayout = ({ }: Props) => { const [expanded, setExpand] = useState(false); - const handleSearch = useCallback((keyword = '', labelID = MAILBOX_LABEL_IDS.ALL_MAIL as string) => { - history.push(setKeywordInUrl({ ...location, pathname: `/${getHumanLabelID(labelID)}` }, keyword)); + const handleSearch = useCallback((search = '', labelID = MAILBOX_LABEL_IDS.ALL_MAIL as string) => { + const model = { + ...DEFAULT_MODEL, + keyword: search || '', + labelID, + }; + + const keywords = []; + search.split(' ').forEach((part) => { + const parsed = part.match(/^([^:]*):(.*)$/); + if (parsed && parsed[1]) { + switch (parsed[1]) { + case 'from': + case 'to': + model[parsed[1]] = getRecipients(parsed[2]); + return; + case 'has': + if (parsed[2]) { + switch (parsed[2]) { + case 'attachment': + model.attachments = WITH_ATTACHMENTS; + return; + default: + } + } + break; + default: + } + } + keywords.push(part); + }); + model.keyword = keywords.join(' '); + + const { keyword, from, to, attachments } = model; + + history.push( + changeSearchParams(`/${getHumanLabelID(model.labelID)}`, location.search, { + keyword: keyword || UNDEFINED, + from: from.length ? formatRecipients(from) : UNDEFINED, + to: to.length ? formatRecipients(to) : UNDEFINED, + attachments: typeof attachments === 'number' ? String(attachments) : UNDEFINED, + }) + ); }, []); const handleToggleExpand = useCallback(() => setExpand((expanded) => !expanded), []); From d7d2dd4ca1d803ab04d023df59320f696e842292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radom=C3=ADr=20Pol=C3=A1ch?= Date: Fri, 27 Nov 2020 03:28:08 +0100 Subject: [PATCH 2/2] feature: cleanup & reconstruct search operators --- .../header/AdvancedSearchDropdown.tsx | 59 ++++++++++++++++++- src/app/components/header/MailHeader.tsx | 6 +- src/app/components/layout/PrivateLayout.tsx | 41 +------------ 3 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/app/components/header/AdvancedSearchDropdown.tsx b/src/app/components/header/AdvancedSearchDropdown.tsx index 846da087..54b208c1 100644 --- a/src/app/components/header/AdvancedSearchDropdown.tsx +++ b/src/app/components/header/AdvancedSearchDropdown.tsx @@ -58,9 +58,9 @@ export const UNDEFINED = undefined; const AUTO_WILDCARD = undefined; const ALL_ADDRESSES = 'all'; const NO_ATTACHMENTS = 0; -export const WITH_ATTACHMENTS = 1; +const WITH_ATTACHMENTS = 1; const { INBOX, TRASH, SPAM, ARCHIVE, ALL_MAIL, ALL_SENT, SENT, ALL_DRAFTS, DRAFTS } = MAILBOX_LABEL_IDS; -export const DEFAULT_MODEL: SearchModel = { +const DEFAULT_MODEL: SearchModel = { keyword: '', labelID: ALL_MAIL, from: [], @@ -70,7 +70,7 @@ export const DEFAULT_MODEL: SearchModel = { wildcard: AUTO_WILDCARD, }; -export const getRecipients = (value = '') => +const getRecipients = (value = '') => value .split(',') .filter(validateEmailAddress) @@ -347,4 +347,57 @@ const AdvancedSearchDropdown = ({ labelID, keyword: fullInput = '', location, hi ); }; +export const parseSearch = (search) => { + const model = { + ...DEFAULT_MODEL, + keyword: search || '', + }; + + const keywords = []; + search.split(' ').forEach((part) => { + const parsed = part.match(/^([^:]*):(.*)$/); + if (parsed && parsed[1]) { + switch (parsed[1]) { + case 'from': + case 'to': + model[parsed[1]] = getRecipients(parsed[2]); + return; + case 'has': + if (parsed[2]) { + switch (parsed[2]) { + case 'attachment': + model.attachments = WITH_ATTACHMENTS; + return; + default: + } + } + break; + default: + } + } + keywords.push(part); + }); + model.keyword = keywords.join(' '); + return model; +}; + +export const generateSearch = (location) => { + const { keyword = '', from = undefined, to = undefined, attachments = undefined } = extractSearchParameters( + location + ); + + const search = [keyword]; + if (from) { + search.push(`from:${from}`); + } + if (to) { + search.push(`to:${to}`); + } + if (attachments) { + search.push('has:attachment'); + } + + return search.join(' '); +}; + export default AdvancedSearchDropdown; diff --git a/src/app/components/header/MailHeader.tsx b/src/app/components/header/MailHeader.tsx index 902cacd1..b62a31ca 100644 --- a/src/app/components/header/MailHeader.tsx +++ b/src/app/components/header/MailHeader.tsx @@ -12,8 +12,8 @@ import { } from 'react-components'; import { MAILBOX_LABEL_IDS, APPS } from 'proton-shared/lib/constants'; -import AdvancedSearchDropdown from './AdvancedSearchDropdown'; -import { extractSearchParameters, setParamsInUrl } from '../../helpers/mailboxUrl'; +import AdvancedSearchDropdown, { generateSearch } from './AdvancedSearchDropdown'; +import { setParamsInUrl } from '../../helpers/mailboxUrl'; import { Breakpoints } from '../../models/utils'; import { getLabelName } from '../../helpers/labels'; import { OnCompose } from '../../hooks/useCompose'; @@ -42,7 +42,7 @@ const MailHeader = ({ onSearch, onCompose, }: Props) => { - const { keyword = '' } = extractSearchParameters(location); + const keyword = generateSearch(location); const [value, updateValue] = useState(keyword); const [oldLabelID, setOldLabelID] = useState(MAILBOX_LABEL_IDS.INBOX); const [labels = []] = useLabels(); diff --git a/src/app/components/layout/PrivateLayout.tsx b/src/app/components/layout/PrivateLayout.tsx index b809e82d..a8dad640 100644 --- a/src/app/components/layout/PrivateLayout.tsx +++ b/src/app/components/layout/PrivateLayout.tsx @@ -9,13 +9,7 @@ import MailHeader from '../header/MailHeader'; import { getHumanLabelID } from '../../helpers/labels'; import { Breakpoints } from '../../models/utils'; import { OnCompose } from '../../hooks/useCompose'; -import { - getRecipients, - formatRecipients, - DEFAULT_MODEL, - UNDEFINED, - WITH_ATTACHMENTS, -} from '../header/AdvancedSearchDropdown'; +import { parseSearch, formatRecipients, UNDEFINED } from '../header/AdvancedSearchDropdown'; interface Props { children: ReactNode; @@ -41,37 +35,8 @@ const PrivateLayout = ({ const [expanded, setExpand] = useState(false); const handleSearch = useCallback((search = '', labelID = MAILBOX_LABEL_IDS.ALL_MAIL as string) => { - const model = { - ...DEFAULT_MODEL, - keyword: search || '', - labelID, - }; - - const keywords = []; - search.split(' ').forEach((part) => { - const parsed = part.match(/^([^:]*):(.*)$/); - if (parsed && parsed[1]) { - switch (parsed[1]) { - case 'from': - case 'to': - model[parsed[1]] = getRecipients(parsed[2]); - return; - case 'has': - if (parsed[2]) { - switch (parsed[2]) { - case 'attachment': - model.attachments = WITH_ATTACHMENTS; - return; - default: - } - } - break; - default: - } - } - keywords.push(part); - }); - model.keyword = keywords.join(' '); + const model = parseSearch(search); + model.labelID = labelID; const { keyword, from, to, attachments } = model;