В первой части мы заложили фундамент почтового клиента Ext Mail: создали Universal-приложение, описали модели и хранилища, построили Grid для списка писем и Reader для просмотра. Во второй части мы расширим функциональность:
-
добавим возможность помечать письма звёздочкой (Star/Unstar);
-
добавим дополнительные действия в тулбар (удалить, архивировать, пометить как непрочитанное);
-
выведем счётчик сообщений в тулбаре;
-
создадим дерево меток (Labels Tree) для фильтрации писем;
-
научимся подсчитывать количество непрочитанных сообщений для каждой метки и отображать его в дереве.
Все примеры кода взяты из оригинальной книги Stuart Ashworth (Sencha MVP) и адаптированы для русскоязычного читателя.
⭐ Помечаем письма звёздочкой (Starring and Unstarring)
Первое, что мы добавим — это возможность помечать письма звёздочкой прямо в Grid. Для этого используем колонку типа actioncolumn, которая позволяет добавлять в каждую строку интерактивные иконки.
Добавляем actioncolumn первой колонкой в MessageGrid:
columns: [ { xtype: 'actioncolumn', width: 25, items: [ { glyph: '★', tooltip: 'Star', getClass: function(v, meta, rec) { if (rec.get('starred')) { return 'x-hidden-display'; } }, handler: function(view, rowIndex, colIndex, item, e, rec) { view.grid.fireEvent('starmessage', rec); } }, { glyph: '★', tooltip: 'Un-star', getClass: function(v, meta, rec) { if (!rec.get('starred')) { return 'x-hidden-display'; } }, handler: function(view, rowIndex, colIndex, item, e, rec) { view.grid.fireEvent('unstarmessage', rec); } } ] }, // ... остальные колонки (fullName, subject, date) ]
Мы определяем две отдельные иконки: одна для установки звёздочки, другая — для снятия. С помощью getClass мы скрываем ненужную иконку в зависимости от поля starred в записи. При клике каждая иконка генерирует пользовательское событие (starmessage или unstarmessage) на родительском Grid.
Теперь в главном компоненте Main подписываемся на эти события:
{ xtype: 'messages-MessageGrid', bind: { store: '{messages}' }, listeners: { itemclick: 'onMessageClick', starmessage: 'onStarMessage', unstarmessage: 'onUnStarMessage' } }
А в MainControllerBase реализуем обработчики:
onStarMessage: function(messageRecord) { messageRecord.addLabel(ExtMail.enums.Labels.STARRED); messageRecord.set('starred', true); messageRecord.commit(); }, onUnStarMessage: function(messageRecord) { messageRecord.removeLabel(ExtMail.enums.Labels.STARRED); messageRecord.set('starred', false); messageRecord.commit(); }
Здесь мы используем вспомогательные методы addLabel и removeLabel, созданные в модели Message в первой части. Они принимают ID метки. Чтобы не оперировать «магическими числами», мы создали перечисление ExtMail.enums.Labels:
Ext.define('ExtMail.enums.Labels', { singleton: true, INBOX: 1, STARRED: 2, SENT: 3, DRAFTS: 4, ALL_MAIL: 5 });
Это делает код более читаемым и устойчивым к изменениям ID в будущем.
🧰 Добавляем дополнительные действия в тулбар
Теперь расширим панель инструментов MessagesToolbar. Добавим кнопки Archive, Delete и Mark as Unread, которые должны быть видны только при просмотре письма (когда selectedMessage не пуст).
{ tooltip: 'Archive', iconCls: 'x-fa fa-archive', handler: 'onArchiveClick', hidden: true, bind: { hidden: '{!selectedMessage}' } }, { tooltip: 'Delete', iconCls: 'x-fa fa-trash', handler: 'onDeleteClick', hidden: true, bind: { hidden: '{!selectedMessage}' } }, { tooltip: 'Mark as Unread', iconCls: 'x-fa fa-envelope', handler: 'onMarkAsUnreadClick', hidden: true, bind: { hidden: '{!selectedMessage}' } }
Каждая кнопка генерирует своё событие, которое мы обрабатываем в Main:
listeners: { refresh: 'onRefreshMessages', back: 'onBackToMessagesGrid', delete: 'onDeleteMessage', markup: 'onMarkMessageUnread', archive: 'onArchiveMessage' }
Реализация методов в MainControllerBase:
onDeleteMessage: function() { var vm = this.getViewModel(); vm.getStore('messages').remove(vm.get('selectedMessage')); this.onBackToMessagesGrid(); }, onMarkMessageUnread: function() { this.getViewModel().get('selectedMessage').set('unread', true); this.onBackToMessagesGrid(); }, onArchiveMessage: function() { this.getViewModel().get('selectedMessage').removeLabel(ExtMail.enums.Labels.INBOX); this.onBackToMessagesGrid(); }
🔢 Счётчик сообщений в тулбаре
Добавим в тулбар компонент, который показывает текущее количество писем в Grid. Он должен быть виден только на экране списка. Используем привязку к свойству count хранилища messages:
{ xtype: 'component', tpl: '{count} messages', data: {}, bind: { hidden: '{selectedMessage}', data: { count: '{messages.count}' } } }
Специальный синтаксис {messages.count} — это сокращённая привязка к количеству записей в store. При добавлении или удалении письма шаблон автоматически перерисуется. Всего у store есть пять таких сокращений: count, first, last, loaded, totalCount.
🌳 Создаём дерево меток (Labels Tree)
Теперь добавим в приложение дерево меток, чтобы пользователь мог фильтровать письма по папкам (Inbox, Starred, Sent, Drafts, All Mail).
Сначала подключим хранилище Labels в MainModel:
requires: [ 'ExtMail.store.Messages', 'ExtMail.store.Labels' ], stores: { labels: { type: 'Labels' } }
Создаём компонент LabelsTree в папке classic:
Ext.define('ExtMail.view.labels.LabelsTree', { extend: 'Ext.tree.Panel', alias: 'widget.labels-LabelsTree', initComponent: function() { Ext.apply(this, { rootVisible: false, columns: [{ xtype: 'treecolumn', header: false, text: 'Name', dataIndex: 'name', flex: 1 }] }); this.callParent(arguments); } });
Добавляем дерево в Main как западную область (region: 'west') и привязываем к хранилищу labels:
{ xtype: 'labels-LabelsTree', region: 'west', width: 300, bind: { store: '{labels}', selection: '{selectedLabel}' } }
Здесь мы также привязываем выбранный элемент дерева к свойству selectedLabel вьюмодели.
🎯 Фильтрация писем по метке
Когда пользователь выбирает метку, мы должны отфильтровать хранилище messages. Для этого в MainModel добавляем обработчик изменения selectedLabel:
constructor: function() { this.callParent(arguments); this.bind('{selectedLabel}', this.onSelectedLabelChange, this); this.bind('{labels.first}', this.onFirstLabelRecordChange, this); }, onSelectedLabelChange: function(labelRecord) { this.getStore('messages').clearFilter(); this.getStore('messages').filterBy(function(messageRecord) { return labelRecord ? messageRecord.hasLabel(labelRecord.getId()) : true; }); }, onFirstLabelRecordChange: function(firstLabelRecord) { this.set('selectedLabel', firstLabelRecord); }
При смене метки мы сбрасываем старые фильтры и применяем новый, используя метод hasLabel модели Message. Также при загрузке приложения автоматически выбирается первая метка (Inbox), чтобы Grid не был пустым.
📊 Подсчёт непрочитанных сообщений для каждой метки
Последний штрих — показать количество непрочитанных писем рядом с каждой меткой. Для этого подписываемся на событие datachanged хранилища messages и пересчитываем счётчики:
constructor: function() { // ... this.getStore('messages').on('datachanged', this.calculateUnreadCounts, this); }, calculateUnreadCounts: function() { this.getStore('labels').each(function(labelRecord) { var count = this.getStore('messages').queryBy(function(messageRecord) { return ( (messageRecord.hasLabel(labelRecord.getId()) && messageRecord.get('unread')) || (messageRecord.hasLabel(ExtMail.enums.Labels.DRAFTS) && messageRecord.get('draft')) ); }).getCount(); labelRecord.set('unreadCount', count); }, this); }
Затем в колонке дерева добавляем рендерер, который выводит название метки и количество непрочитанных:
renderer: function(value, meta, record) { var hasUnread = record.get('unreadCount') > 0; meta.tdStyle = 'font-weight: ' + (hasUnread ? 'bold' : 'normal'); var unreadTpl = Ext.String.format('<span> ({0})</span>', record.get('unreadCount')); return Ext.String.format('<span>{0}</span>{1}', value, hasUnread ? unreadTpl : ''); }
Теперь дерево меток всегда синхронизировано с хранилищем писем: как только письмо помечается прочитанным или удаляется, счётчики автоматически обновляются.
💎 Заключение
Во второй части мы значительно расширили функциональность почтового клиента:
-
Добавили action column для пометки звёздочкой.
-
Реализовали дополнительные действия: удаление, архивацию, пометку как непрочитанное.
-
Вывели счётчик сообщений в тулбар.
-
Создали дерево меток и настроили фильтрацию писем по выбранной метке.
-
Научились подсчитывать количество непрочитанных сообщений для каждой метки и отображать его в дереве.
В следующей части мы добавим возможность создания новых писем и работу с черновиками. Следите за обновлениями!