_ = require 'underscore' React = require 'react' {Utils, Label, Folder, Thread, Actions, TaskQueue, AccountStore, CategoryStore, DatabaseStore, WorkspaceStore, ChangeLabelsTask, ChangeFolderTask, SyncbackCategoryTask, TaskQueueStatusStore, FocusedMailViewStore} = require 'nylas-exports' {Menu, Popover, RetinaImg, LabelColorizer} = require 'nylas-component-kit' # This changes the category on one or more threads. class CategoryPicker extends React.Component @displayName: "CategoryPicker" @containerRequired: false constructor: (@props) -> @state = _.extend @_recalculateState(@props), searchValue: "" @contextTypes: sheetDepth: React.PropTypes.number componentDidMount: => @unsubscribers = [] @unsubscribers.push CategoryStore.listen @_onStoreChanged @unsubscribers.push AccountStore.listen @_onStoreChanged @_commandUnsubscriber = atom.commands.add 'body', "application:change-category": @_onOpenCategoryPopover # If the threads we're picking categories for change, (like when they # get their categories updated), we expect our parents to pass us new # props. We don't listen to the DatabaseStore ourselves. componentWillReceiveProps: (nextProps) -> @setState @_recalculateState(nextProps) componentWillUnmount: => return unless @unsubscribers unsubscribe() for unsubscribe in @unsubscribers @_commandUnsubscriber.dispose() render: => return unless @_account if @_account?.usesLabels() img = "ic-toolbar-tag.png" tooltip = "Apply Labels" placeholder = "Label as" else if @_account?.usesFolders() img = "ic-toolbar-movetofolder.png" tooltip = "Move to Folder" placeholder = "Move to folder" else img = "" tooltip = "" placeholder = "" if @state.isPopoverOpen then tooltip = "" button = headerComponents = [ ] item.id } itemContent={@_renderItemContent} onSelect={@_onSelectCategory} defaultSelectedIndex={if @state.searchValue is "" then -1 else 0} /> _onOpenCategoryPopover: => return unless @_threads().length > 0 return unless @context.sheetDepth is WorkspaceStore.sheetStack().length - 1 @refs.popover.open() return _renderItemContent: (item) => if item.divider return else if item.newCategoryItem return @_renderCreateNewItem(item) if @_account?.usesLabels() icon = @_renderCheckbox(item) else if @_account?.usesFolders() icon = @_renderFolderIcon(item) else return
{icon}
{@_renderBoldedSearchResults(item)}
_renderCreateNewItem: ({searchValue, name}) => if @_account?.usesLabels() picName = "tag" else if @_account?.usesFolders() picName = "folder"
“{searchValue}” (create new)
_renderCheckbox: (item) -> styles = {} styles.backgroundColor = item.backgroundColor if item.usage is 0 checkStatus = else if item.usage < item.numThreads checkStatus = @_onSelectCategory(item)}/> else checkStatus = @_onSelectCategory(item)}/>
@_onSelectCategory(item)}/> {checkStatus}
_renderFolderIcon: (item) -> _renderBoldedSearchResults: (item) -> name = item.display_name searchTerm = (@state.searchValue ? "").trim() return name if searchTerm.length is 0 re = Utils.wordSearchRegExp(searchTerm) parts = name.split(re).map (part) -> # The wordSearchRegExp looks for a leading non-word character to # deterine if it's a valid place to search. As such, we need to not # include that leading character as part of our match. if re.test(part) if /\W/.test(part[0]) return {part[0]}{part[1..-1]} else return {part} else return part return {parts} _onSelectCategory: (item) => return unless @_threads().length > 0 return unless @_account @refs.menu.setSelectedItem(null) if @_account.usesLabels() if item.newCategoryItem cat = new Label displayName: @state.searchValue, accountId: AccountStore.current().id task = new SyncbackCategoryTask category: cat organizationUnit: "label" TaskQueueStatusStore.waitForPerformRemote(task).then => DatabaseStore.findBy Label, clientId: cat.clientId .then (cat) => changeLabelsTask = new ChangeLabelsTask labelsToAdd: [cat] threads: @_threads() Actions.queueTask(changeLabelsTask) Actions.queueTask(task) else if item.usage > 0 task = new ChangeLabelsTask labelsToRemove: [item.category] threads: @_threads() Actions.queueTask(task) else task = new ChangeLabelsTask labelsToAdd: [item.category] threads: @_threads() Actions.queueTask(task) else if @_account.usesFolders() if item.newCategoryItem? cat = new Folder displayName: @state?.searchValue, accountId: AccountStore.current().id task = new SyncbackCategoryTask category: cat organizationUnit: "folder" TaskQueueStatusStore.waitForPerformRemote(task).then => DatabaseStore.findBy Folder, clientId: cat.clientId .then (cat) => return if not cat?.serverId changeFolderTask = new ChangeFolderTask folder: cat threads: @_threads() if @props.thread Actions.moveThread(@props.thread, changeFolderTask) else if @props.items Actions.moveThreads(@_threads(), changeFolderTask) Actions.queueTask(task) else task = new ChangeFolderTask folder: item.category threads: @_threads() if @props.thread Actions.moveThread(@props.thread, task) else if @props.items Actions.moveThreads(@_threads(), task) else throw new Error("Invalid organizationUnit") @refs.popover.close() _onStoreChanged: => @setState @_recalculateState(@props) _onSearchValueChange: (event) => @setState @_recalculateState(@props, searchValue: event.target.value) _onPopoverOpened: => @setState @_recalculateState(@props, searchValue: "") @setState isPopoverOpen: true _onPopoverClosed: => @setState isPopoverOpen: false _recalculateState: (props=@props, {searchValue}={}) => searchValue = searchValue ? @state?.searchValue ? "" numThreads = @_threads(props).length if numThreads is 0 return {categoryData: [], searchValue} @_account = AccountStore.current() return unless @_account categories = CategoryStore.getStandardCategories() .concat([{divider: true, id: "category-divider"}]) .concat(CategoryStore.getUserCategories()) usageCount = @_categoryUsageCount(props, categories) allInInbox = @_allInInbox(usageCount, numThreads) displayData = {usageCount, numThreads} categoryData = _.chain(categories) .filter(_.partial(@_isUserFacing, allInInbox)) .filter(_.partial(@_isInSearch, searchValue)) .map(_.partial(@_itemForCategory, displayData)) .value() if searchValue.length > 0 newItemData = searchValue: searchValue newCategoryItem: true id: "category-create-new" categoryData.push(newItemData) return {categoryData, searchValue} _categoryUsageCount: (props, categories) => categoryUsageCount = {} _.flatten(@_threads(props).map(@_threadCategories)).forEach (category) -> categoryUsageCount[category.id] ?= 0 categoryUsageCount[category.id] += 1 return categoryUsageCount _isInSearch: (searchValue, category) -> return Utils.wordSearchRegExp(searchValue).test(category.displayName) _isUserFacing: (allInInbox, category) => hiddenCategories = [] currentCategoryId = FocusedMailViewStore.mailView().categoryId() if @_account?.usesLabels() hiddenCategories = ["all", "spam", "trash", "drafts", "sent"] if allInInbox hiddenCategories.push("inbox") return false if category.divider else if @_account?.usesFolders() hiddenCategories = ["drafts", "sent"] return (category.name not in hiddenCategories) and (category.id isnt currentCategoryId) _allInInbox: (usageCount, numThreads) -> inbox = CategoryStore.getStandardCategory("inbox") return false unless inbox return usageCount[inbox.id] is numThreads _itemForCategory: ({usageCount, numThreads}, category) -> return category if category.divider item = category.toJSON() item.category = category item.backgroundColor = LabelColorizer.backgroundColorDark(category) item.usage = usageCount[category.id] ? 0 item.numThreads = numThreads item _threadCategories: (thread) => if @_account.usesLabels() return (thread.labels ? []) else if @_account.usesFolders() return (thread.folders ? []) else throw new Error("Invalid organizationUnit") _threads: (props = @props) => if props.items then return (props.items ? []) else if props.thread then return [props.thread] else return [] module.exports = CategoryPicker