Создаем почтовый клиент на Ext JS с нуля. Часть 2: Метки, дерево и динамические действия

В первой части мы заложили фундамент почтового клиента Ext Mail: создали Universal-приложение, описали модели и хранилища, построили Grid для списка писем и Reader для просмотра. Во второй части мы расширим функциональность:

  • добавим возможность помечать письма звёздочкой (Star/Unstar);

  • добавим дополнительные действия в тулбар (удалить, архивировать, пометить как непрочитанное);

  • выведем счётчик сообщений в тулбаре;

  • создадим дерево меток (Labels Tree) для фильтрации писем;

  • научимся подсчитывать количество непрочитанных сообщений для каждой метки и отображать его в дереве.

Все примеры кода взяты из оригинальной книги Stuart Ashworth (Sencha MVP) и адаптированы для русскоязычного читателя.

⭐ Помечаем письма звёздочкой (Starring and Unstarring)

Первое, что мы добавим — это возможность помечать письма звёздочкой прямо в Grid. Для этого используем колонку типа actioncolumn, которая позволяет добавлять в каждую строку интерактивные иконки.

Добавляем actioncolumn первой колонкой в MessageGrid:

javascript
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 подписываемся на эти события:

javascript
{
    xtype: 'messages-MessageGrid',
    bind: { store: '{messages}' },
    listeners: {
        itemclick: 'onMessageClick',
        starmessage: 'onStarMessage',
        unstarmessage: 'onUnStarMessage'
    }
}

А в MainControllerBase реализуем обработчики:

javascript
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:

javascript
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 не пуст).

javascript
{
    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:

javascript
listeners: {
    refresh: 'onRefreshMessages',
    back: 'onBackToMessagesGrid',
    delete: 'onDeleteMessage',
    markup: 'onMarkMessageUnread',
    archive: 'onArchiveMessage'
}

Реализация методов в MainControllerBase:

javascript
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:

javascript
{
    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:

javascript
requires: [
    'ExtMail.store.Messages',
    'ExtMail.store.Labels'
],
stores: {
    labels: {
        type: 'Labels'
    }
}

Создаём компонент LabelsTree в папке classic:

javascript
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:

javascript
{
    xtype: 'labels-LabelsTree',
    region: 'west',
    width: 300,
    bind: {
        store: '{labels}',
        selection: '{selectedLabel}'
    }
}

Здесь мы также привязываем выбранный элемент дерева к свойству selectedLabel вьюмодели.

🎯 Фильтрация писем по метке

Когда пользователь выбирает метку, мы должны отфильтровать хранилище messages. Для этого в MainModel добавляем обработчик изменения selectedLabel:

javascript
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 и пересчитываем счётчики:

javascript
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);
}

Затем в колонке дерева добавляем рендерер, который выводит название метки и количество непрочитанных:

javascript
renderer: function(value, meta, record) {
    var hasUnread = record.get('unreadCount') > 0;
    meta.tdStyle = 'font-weight: ' + (hasUnread ? 'bold' : 'normal');
    var unreadTpl = Ext.String.format('<span>&nbsp;({0})</span>', record.get('unreadCount'));
    return Ext.String.format('<span>{0}</span>{1}', value, hasUnread ? unreadTpl : '');
}

Теперь дерево меток всегда синхронизировано с хранилищем писем: как только письмо помечается прочитанным или удаляется, счётчики автоматически обновляются.

💎 Заключение

Во второй части мы значительно расширили функциональность почтового клиента:

  • Добавили action column для пометки звёздочкой.

  • Реализовали дополнительные действия: удаление, архивацию, пометку как непрочитанное.

  • Вывели счётчик сообщений в тулбар.

  • Создали дерево меток и настроили фильтрацию писем по выбранной метке.

  • Научились подсчитывать количество непрочитанных сообщений для каждой метки и отображать его в дереве.

В следующей части мы добавим возможность создания новых писем и работу с черновиками. Следите за обновлениями!

Оставьте комментарий