* — backend
// Получение списка тредов Thread.find({folder: 8}).then(threads => { // ... });
RPC.intercept({ status: "invalid", process(req) { if (req.get("body.folder.error") === "not_open") { // Возвращаем Promise return openSecureFolderLayer({ folder: req.get("body.folder.value") }); } } });
* name — поддерживает dot-нотацию
const listFoo = new BackboneThreadCollection({folder: 123}); listFoo.fetch(); const listBar = new BackboneThreadCollection({folder: 123}); listBar.fetch(); #!+ console.log(listFoo.get(456) === listBar.get(456)); // false :[ listFoo.on("chnage", () => { console.log("Cnanged"); }); listBar.get(456).set("flag", true); #!- // ничего не произойдет, потому что инстансы разные
// Где-то получаем тред Thread.findOne(123).then(thread => { thread.get("messages").on("change:flags", () => { // Реагируем на изменение флагов }); }); #!+ // Где-то письмо (которое входит в тред) Message.findOne(345).then(message => { #!+ // Инвертируем флаг (мы так не делаем :]) #!- message.set("flags.unread", !message.is("flags.unread")); #!- });
const Thread = Message.extend({ className: "mail.Thread", // для логирования findUrl: "threads", // Получение списка тредов findOneUrl: "threads/thread", // Запрос за тредом defaults: { // Свойства по умолчанию ... messages: new Message.List.Sorted, } }); // Отсортированный список моделей Message.List.Sorted = Message.List.extend({ comparator: {date: true} // desc });
const Thread = Message.extend({findOneUrl: "threads/thread"findOneUrl: (query) => query.withoutQuote ? "threads/thread/short" : "threads/thread" }, isDataFully(query) { #!+ return this.is("messages.length") && ( query.withoutQuote || hasQuotes(this) #!- ); } });
Client.find().then(list => ...);
request.on("error", (evt, req) => { saveToLog(req); });
const MyAction = Action.extend(/** @lends MyAction# */{ #!+ // Подготовка данных prepare(params, options)/* Promise|Object */ { return {computed: params.foo + params.bar}; #!- }, #!+ // Выполняемая операция #!- operation({computed}, params, options) /* Promise */ { /* … */ }, #!+ // Обратная операция #!- undoOperation(data, params, options) /* Promise */ { /* … */ }, #!+ // Откатываем изменения в случае ошибки при выполнении #!- rollbackOperation() { } });
MoveTo.execute({ folderTo: 123, // id папки куда переносим models: [...] // Массив Писем или Тредов (модели) }).then(action => { // Ссылка на модель Папки куда переместили const folderTo = action.folderTo; // Список реально перемещенных моделей const affectedModels = action.models; // Показывает нотификацию для отмены действия showUndoNotify().onUndo(() => action.undo()); });
require(["app/app", "logger"], function (app, logger) { // Скрыть лоудер загрузки приложения app.ready(hideLoading); // Перехват ссылок и запуск приложения app.listenFrom(document, { autoStart: true, logger: logger }); });
const app = Pilot.create({ #! model: { /* модели доступные всем маршрутам */ }, #!+ "#letters": { url: "/:folder", // "/inbox/", "/trash/" или "/123/" #!+ model: { // модели конкретного маршрута threads: { fetch: ({params: {folder}}) => Thread.find({folder}), }, #!- } #!- }, });
// Перейти на нужный маршрут app.go("#letters"); // "/inbox/" app.go("#letters", {folder: 123}); // "/123/" // Сформировать url app.getUrl("#letters", {folder: 0}); // "/inbox/" app.getUrl("#letters", {folder: 123}); // "/123/" // Или для текущего маршрута app.route.getUrl({folder: 0}); // "/inbox/"
"#letters": { url: { pattern: "/:folder", // "/inbox/", "/trash/" или "/123/" #!+ params: { folder: { #! default: Folder.INBOX, #! decode: (val) => (val in TYPE2ID) ? TYPE2ID[val] : toInt(val), #! encode: (id) => (id in ID2TYPE) ? ID2TYPE[id] : id, } #!- }, #! toUrl: (params, builder) => builder({params, ...extra}), }, model: { /*...*/ }, // ... }
console.dir(app.model); { request: Pilot.Request, // текущий «location» authUser: User, // авторизованный юзер folder: Folder, // активная папка folders: Folder.List, // список папок letters: Thread.List, // список тредов или писем letter: Thread, // активный тред (на чтении) status: Object, // статус ящика ... }
// Источники данных import request from "./source/request"; import authUser from "./source/authUser"; import status from "./source/status"; import folders from "./source/folders"; // ...etc const loader = new Pilot.Loader({ request, authUser, status, folders, // ...etc }, { /* разные опции */ });
{ default: null, // значение по умолчанию match: null, // или массив id-маршрутов fetch: (/** @Pilot.Request */req) => ..., reaction: (req, newStatus, event) => { // Реакция на события // (загрузка данных или «Действия» над моделями) } }
// source/status.js — состояние ящика (папки, письма и т.п.) export default { fetch: (req) => loadStatusByRequest(req), }; #!+ // source/folders.js — список папок export default { fetch: (req, **waitFor**) => waitFor("status").then(status => status.folders), #!- };
// source/folder.js — активная папка export default { fetch(req, waitFor) { if (req.is("#read-letter")) { // Если это «Чтение» #!+ return Promise .all([waitFor("folders"), waitFor("letter")]) #!- .then(([folders, letter]) => folders.get(letter.get("folder"))); } else { #! return waitFor("folders").then(folders => folders.get(req.params.folder)); } } };
// source/letter.js — активный тред или письмо export default { match: ["#read-letter", "#search-letter"], fetch: ({params:{letter}}) => Thread.findOne(letter), };
const app = Pilot.create(sitemap); #!+ // Создаём AppView app.view = new UIApplication(dataSource.getData(), { router: app, dataSource, #!- }); #!+ // Подписываем View на изменение данных dataSource.onChange(data => { app.view.set(data); #!- });
<b:letter-status name="unread" state="{attrs.unread}" remit:click="invert:unread" /> #!+ <b:dataset-letters> // <--- слушает "invert:unread" <b:dataset> <b:dataset-letters-item> #!- <b:letter-status/> // <--- излучает "invert:unread"
/Button/
export default { keywords: "btn кнопка", cases: { "base": { attrs: [{text: "Написать"}] }, "primary": { attrs: [ {type: "submit", text: "Написать"}, {ico: "compose", type: "submit", text: "Написать"}, {ico: "compose", short: true, type: "submit", text: "Написать"}, {pressed: true, type: "submit", text: "Написать"} ] }, } };
<b:portal-menu #! use:mediator="letters-actions letters-selection" />
import DeleteAction from "jssdk/mail/actions/Delete"; import UIPortalMenu from "ui-block/PortalMenu/PortalMenu"; import lettersSelection from "mediator/letters-selection"; #!+ export default feast.Mediator.create("letters-actions", { components: { #!+ "portal": { // имя для обращения внутри медиатора #! "class": UIPortalMenu, // <b:portal-menu/> #!+ // события, на которые подписывается медиатор #!- "events": ["delete", "move" ...], #!- }, #!+ "router": { "class": Pilot, #!- }, // ... }, handleEvent(evt) { /* ... */ }, // ... #!- });
export default feast.Mediator.create("layout-manager", { components: { /* ... */ }, handleEvent(evt) { // Тип события const type = evt.type; // Получаем список моделей от медиатора отвечающего за выделение писем const selectedModels = lettersSelection.getModels(); #!+ if (type === "delete") { #!+ DeleteAction.execute({models: selectedModels}) .then(this._done) #!- .catch(this._fail); } else if (type === "move") { // ... } #!- #!+ swipeManager.reset(); // Закрываем свайпы lettersSelection.reset(); // Сбрасываем выделение #!- // По завершению `handleEvent`, всем связанные блоки с медиатором будут обновлены } });