_ = require 'underscore' React = require 'react' {Utils, Label, Folder, Thread, Actions, TaskQueue, TaskFactory, AccountStore, CategoryStore, DatabaseStore, WorkspaceStore, SyncbackCategoryTask, TaskQueueStatusStore, FocusedPerspectiveStore} = require 'nylas-exports' {Menu, Popover, RetinaImg, KeyCommandsRegion, LabelColorizer} = require 'nylas-component-kit' {Categories} = require 'nylas-observables' # This changes the category on one or more threads. class CategoryPicker extends React.Component @displayName: "CategoryPicker" @containerRequired: false constructor: (@props) -> @_account = AccountStore.accountForItems(@_threads(@props)) @_categories = [] @_standardCategories = [] @_userCategories = [] @state = _.extend @_recalculateState(@props), searchValue: "" @contextTypes: sheetDepth: React.PropTypes.number componentDidMount: => @_registerObservables() # 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) -> @_account = AccountStore.accountForItems(@_threads(nextProps)) @_registerObservables() @setState @_recalculateState(nextProps) componentWillUnmount: => @_unregisterObservables() _registerObservables: => @_unregisterObservables() @disposables = [] @disposables.push( Categories.forAccount(@_account).subscribe(@_onCategoriesChanged) ) _unregisterObservables: => return unless @disposables disp.dispose() for disp in @disposables _keymapHandlers: -> "application:change-category": @_onOpenCategoryPopover render: => return if @state.disabled or not @_account? btnClasses = "btn btn-toolbar" btnClasses += " btn-disabled" if @state.disabled if @_account?.usesLabels() img = "toolbar-tag.png" tooltip = "Apply Labels" placeholder = "Label as" else if @_account?.usesFolders() img = "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) => threads = @_threads() return unless threads.length > 0 return unless @_account @refs.menu.setSelectedItem(null) if item.newCategoryItem CategoryClass = @_account.categoryClass() category = new CategoryClass displayName: @state.searchValue, accountId: @_account.id syncbackTask = new SyncbackCategoryTask({category}) TaskQueueStatusStore.waitForPerformRemote(syncbackTask).then => DatabaseStore.findBy(category.constructor, clientId: category.clientId).then (category) => applyTask = TaskFactory.taskForApplyingCategory threads: threads category: category Actions.queueTask(applyTask) Actions.queueTask(syncbackTask) else if item.usage is threads.length applyTask = TaskFactory.taskForRemovingCategory threads: threads category: item.category Actions.queueTask(applyTask) else applyTask = TaskFactory.taskForApplyingCategory threads: threads category: item.category Actions.queueTask(applyTask) @refs.popover.close() _onSearchValueChange: (event) => @setState @_recalculateState(@props, searchValue: event.target.value) _onPopoverOpened: => @setState @_recalculateState(@props, searchValue: "") @setState isPopoverOpen: true _onPopoverClosed: => @setState isPopoverOpen: false _onCategoriesChanged: (categories) => @_categories = categories @_standardCategories = categories.filter (cat) -> cat.isStandardCategory() @_userCategories = categories.filter (cat) -> cat.isUserCategory() @setState @_recalculateState() _recalculateState: (props = @props, {searchValue}={}) => return {disabled: true} unless @_account threads = @_threads(props) searchValue = searchValue ? @state?.searchValue ? "" numThreads = threads.length if numThreads is 0 return {categoryData: [], searchValue} if @_account.usesLabels() categories = @_categories else categories = @_standardCategories .concat([{divider: true, id: "category-divider"}]) .concat(@_userCategories) 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, disabled: false} _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 = FocusedPerspectiveStore.current()?.categoryId() if @_account?.usesLabels() hiddenCategories = ["all", "drafts", "sent", "archive", "starred", "important"] hiddenCategories.push("inbox") if allInInbox 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) -> return unless @_account? inbox = CategoryStore.getStandardCategory(@_account, "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