docs(*): Additional documentation and high level reference guides

This commit is contained in:
Ben Gotow 2015-04-27 13:29:08 -07:00
parent 9248747b26
commit 0352090007
23 changed files with 1197 additions and 137 deletions

View file

@ -30,9 +30,10 @@
"grunt-shell": "~0.3.1",
"handlebars": "^3.0.2",
"harmony-collections": "~0.3.8",
"highlight.js": "^8.5.0",
"json-front-matter": "^1.0.0",
"legal-eagle": "~0.9.0",
"markdown": "^0.5.0",
"marked": "^0.3.3",
"minidump": "~0.8",
"moment": "^2.8",
"npm": "~1.4.5",

View file

@ -1,6 +1,6 @@
path = require 'path'
Handlebars = require 'handlebars'
markdown = require('markdown').markdown
marked = require 'marked'
cjsxtransform = require 'coffee-react-transform'
rimraf = require 'rimraf'
@ -14,6 +14,10 @@ moduleBlacklist = [
'space-pen'
]
marked.setOptions
highlight: (code) ->
require('highlight.js').highlightAuto(code).value
standardClassURLRoot = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/'
standardClasses = [
@ -154,7 +158,7 @@ module.exports = (grunt) ->
val
for classname, contents of api.classes
processFields(contents, ['description'], [markdown.toHTML, expandTypeReferences, expandFuncReferences])
processFields(contents, ['description'], [marked, expandTypeReferences, expandFuncReferences])
processFields(contents, ['type'], [expandTypeReferences])
result = template(contents)

View file

@ -0,0 +1,19 @@
<h4 id={{name}}>{{name}} <a href="#{{name}}">Link</a></h4>
<p>{{{description}}}</p>
{{#if arguments.length}}
Parameters
<table>
{{#each arguments}}
<tr><td>{{name}}</td><td>{{type}}</td><td>{{{description}}}</td><td>{{isOptional}}</td></tr>
{{/each}}
</table>
{{/if}}
{{#if returnValues.length}}
Returns
<table>
{{#each returnValues}}
<tr><td>{{type}}</td><td>{{{description}}}</td></tr>
{{/each}}
</table>
{{/if}}

View file

@ -0,0 +1,9 @@
<h4 id={{name}}>{{name}} <a href="#{{name}}">Link</a></h4>
<p>{{{description}}}</p>
{{#if arguments.length}}
<table>
{{#each arguments}}
<tr><td>{{name}}</td><td>{{type}}</td><td>{{{description}}}</p></td><td>{{isOptional}}</td></tr>
{{/each}}
</table>
{{/if}}

204
docs-templates/class.html Normal file
View file

@ -0,0 +1,204 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<style>
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote {
margin: 0;
padding: 0;
}
body {
font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif;
font-size: 13px;
line-height: 18px;
color: #737373;
background-color: white;
margin: 10px 13px 10px 13px;
}
table {
margin: 10px 0 15px 0;
border-collapse: collapse;
}
td,th {
vertical-align: top;
border: 1px solid #ddd;
padding: 3px 10px;
}
th {
padding: 5px 10px;
}
a {
color: #0069d6;
}
a:hover {
color: #0050a3;
text-decoration: none;
}
a img {
border: none;
}
p,td {
margin-bottom: 9px;
font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif;
font-size: 13px;
line-height: 18px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: #404040;
line-height: 36px;
}
h1 {
margin-bottom: 18px;
font-size: 30px;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 13px;
}
hr {
margin: 0 0 19px;
border: 0;
border-bottom: 1px solid #ccc;
}
blockquote {
padding: 13px 13px 21px 15px;
margin-bottom: 18px;
font-family:georgia,serif;
font-style: italic;
}
blockquote:before {
content:"\201C";
font-size:40px;
margin-left:-10px;
font-family:georgia,serif;
color:#eee;
}
blockquote p {
font-size: 14px;
font-weight: 300;
line-height: 18px;
margin-bottom: 0;
font-style: italic;
}
code, pre {
font-family: Monaco, Andale Mono, Courier New, monospace;
}
code {
background-color: #fee9cc;
color: rgba(0, 0, 0, 0.75);
padding: 1px 3px;
font-size: 12px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
pre {
display: block;
padding: 14px;
margin: 0 0 18px;
line-height: 16px;
font-size: 11px;
border: 1px solid #d9d9d9;
white-space: pre-wrap;
word-wrap: break-word;
}
pre code {
background-color: #fff;
color:#737373;
font-size: 11px;
padding: 0;
}
sup {
font-size: 0.83em;
vertical-align: super;
line-height: 0;
}
* {
-webkit-print-color-adjust: exact;
}
@media screen and (min-width: 914px) {
body {
width: 854px;
margin:10px auto;
}
}
@media print {
body,code,pre code,h1,h2,h3,h4,h5,h6 {
color: black;
}
table, pre {
page-break-inside: avoid;
}
}
</style>
<title>asdasda</title>
</head>
<body>
<h2>{{name}}{{#if superClass}} extends {{superClass}}{{/if}}</h2>
<span>{{filename}}</span>
<p>{{{description}}}</p>
<ul>
{{#each sections}}
<li><a href="#{{name}}">{{name}}</a></li>
{{/each}}
</ul>
{{#if classProperties.length}}
<h3>Class Properties</h3>
{{#each classProperties}}
{{>_property}}
{{/each}}
{{/if}}
{{#if classMethods.length}}
<h3>Class Methods</h3>
{{#each classMethods}}
{{>_function}}
{{/each}}
{{/if}}
{{#if instanceMethods.length}}
<h3>Instance Methods</h3>
{{#each instanceMethods}}
{{>_function}}
{{/each}}
{{/if}}
</body>
</html>

View file

@ -0,0 +1,21 @@
```
DraftStore.registerExtension(Extension)
```
```
module.exports =
warningsForSending: (draft) ->
warnings = []
if draft.body.search(/<code[^>]*empty[^>]*>/i) > 0
warnings.push("with an empty template area")
warnings
finalizeSessionBeforeSending: (session) ->
body = session.draft().body
clean = body.replace(/<\/?code[^>]*>/g, '')
if body != clean
session.changes.add(body: clean)
```

4
docs/FAQ.md Normal file
View file

@ -0,0 +1,4 @@
#####Do I have to use React?
You need to use React if you want to register UI components with the Component Registry. However, you can create a React component and attach DOM to it's DOM node manually. Packages can also add a DOM node directly to `document.body`, though this is discouraged.

473
docs/GettingStarted.md Normal file
View file

@ -0,0 +1,473 @@
##Quick Start
The Nilas Package API allows you to create powerful extensions to the Nilas Mail client, Nylas Mail. The client is built on top of Atom Shell and runs on Mac OS X, Windows, and Linux. It exposes rich APIs for working with the mail, contacts, and calendar and a robust local cache layer. Your packages can leverage NodeJS and other web technologies to create innovative new experiences.
###Installing Nylas Mail
Nylas Mail is available for Mac, Windows, and Linux. Download the latest build for your platform below:
- [Mac OS X](https://edgehill.nilas.com/download?platform=darwin)
- [Linux](https://edgehill.nilas.com/download?platform=linux)
- [Windows](https://edgehill.nilas.com/download?platform=win32)
###Building a Package
Packages lie at the heart of Nylas Mail. Each part of the core experience is a separate package that uses the Nilas Package API to add functionality to the client. Want to make a read-only mail client? Remove the core `Composer` package and you'll see reply buttons and composer functionality disappear.
Let's explore the files in a simple package that adds a Translate option to the Composer. When you tap the Translate button, we'll display a popup menu with a list of languages. When you pick a language, we'll make a web request and convert your reply into the desired language.
#####Package Structure
Each package is defined by a `package.json` file that includes it's name, version and dependencies. Our `translate` package uses React and the Node [request](https://github.com/request/request) library.
```
{
"name": "translate",
"version": "0.1.0",
"main": "./lib/main",
"description": "An example package for Nylas Mail",
"license": "Proprietary",
"engines": {
"atom": "*"
},
"dependencies": {
"react": "^0.12.2",
"request": "^2.53"
}
}
```
Our package also contains source files, a spec file with complete tests for the behavior the package adds, and a stylesheet for CSS.
```
- package.json
- lib/
- main.cjsx
- spec/
- main-spec.coffee
- stylesheets/
- translate.less
```
`package.json` lists `lib/main` as the root file of our package. As our package expands, we can add other source files. Since Nylas Mail runs NodeJS, you can `require` other source files, Node packages, etc. Inside `main.cjsx`, there are two important functions being exported:
```
module.exports =
##
# Activate is called when the package is loaded. If your package previously
# saved state using `serialize` it is provided.
#
activate: (@state) ->
ComponentRegistry.register
view: TranslateButton
name: 'TranslateButton'
role: 'Composer:ActionButton'
##
# Serialize is called when your package is about to be unmounted.
# You can return a state object that will be passed back to your package
# when it is re-activated.
#
serialize: ->
{}
##
# This optional method is called when the window is shutting down,
# or when your package is being updated or disabled. If your package is
# watching any files, holding external resources, providing commands or
# subscribing to events, release them here.
deactivate: ->
ComponentRegistry.unregister('TranslateButton')
```
> Nylas Mail uses CJSX, a Coffeescript version of JSX, which makes it easy to express Virtual DOM in React `render` methods! You may want to add the [Babel](https://github.com/babel/babel-sublime) plugin to Sublime Text, or the [CJSX Language](https://atom.io/packages/language-cjsx) for syntax highlighting.
#####Package Style Sheets
Style sheets for your package should be placed in the _styles_ directory.
Any style sheets in this directory will be loaded and attached to the DOM when
your package is activated. Style sheets can be written as CSS or [Less], but
Less is recommended.
Ideally, you won't need much in the way of styling. We've provided a standard
set of components which define both the colors and UI elements for any package
that fits into Nylas Mail seamlessly.
If you _do_ need special styling, try to keep only structural styles in the
package style sheets. If you _must_ specify colors and sizing, these should be
taken from the active theme's [ui-variables.less][ui-variables]. For more
information, see the [theme variables docs][theme-variables]. If you follow this
guideline, your package will look good out of the box with any theme!
An optional `stylesheets` array in your _package.json_ can list the style sheets
by name to specify a loading order; otherwise, all style sheets are loaded.
###Installing a Package
Nylas Mail ships with many packages already bundled with the application. When the application launches, it looks for additional packages in `~/.inbox/packages`. Each package you create belongs in it's own directory inside this folder.
In the future, it will be possible to install packages directly from within the client.
-----
##Core Concepts
Nylas Mail uses [React](https://facebook.github.io/react/) to create a fast, responsive UI. Packages that want to extend the Nylas Mail interface should use React. Using React's `JSX` is optional, but both `JSX` and `CJSX` (Coffeescript) are available.
For a quick introduction to React, take a look at Facebook's [Getting Started with React](https://facebook.github.io/react/docs/getting-started.html).
Nylas Mail also uses [Reflux](https://github.com/spoike/refluxjs), a slim implementation of Facebook's [Flux Application Architecture](https://facebook.github.io/flux/) to coordinate the movement of data through the application. Flux is extremely well suited for applications that support third-party extension, because it emphasizes loose coupling and well defined interfaces between components. It enforces:
- **Uni-directional data flow**
- **Loose coupling between components**
For more information about the Flux pattern, check out [this diagram](https://facebook.github.io/flux/docs/overview.html#structure-and-data-flow).
There are several core stores in the application:
- **NamespaceStore**: When the user signs in to Nylas Mail, their auth token provides one or more namespaces. The NamespaceStore manages the available Namespaces, exposes the current Namespace, and allows you to observe changes to the current namespace.
- **TaskQueue**: Manages `Tasks`, operations queued for processing on the backend. `Task` objects represent individual API actions and are persisted to disk, ensuring that they are performed eventually. Each `Task` may depend on other tasks, and `Tasks` are executed in order.
- **DatabaseStore**: The DatabaseStore marshalls data in and out of the local cache, and exposes an ActiveRecord-style query interface. You can observe the DatabaseStore to monitor the state of data in Nylas Mail.
- **DraftStore**: Manages `Drafts`. Drafts present a unique case in Nylas Mail because they may be updated frequently by disconnected parts of the application. You should use the DraftStore to create, edit, and send drafts.
- **FocusedContentStore**: Manages focus within the main applciation window. The FocusedContentStore allows you to query and monitor changes to the selected thread, tag, file, etc.
Most packages declare additional stores that subscribe to these Stores, as well as user Actions, and vend data to the package's React components.
### React
#####Standard React Components
The Nylas Mail client provides a set of core React components you can use in your packages. To use a pre-built component, require it from `ui-components` and wrap it in your own React component. React uses composition rather than inheritance, so your `<ThreadList>` component may render a `<ModelList>` component and pass it function arguments and other `props` to adjust it's behavior.
Many of the standard components listen for key events, include considerations for different platforms, and have extensive CSS. Wrapping standard components makes your package match the rest of Nylas Mail and is encouraged!
Here's a quick look at pre-built components you can require from `ui-components`:
- **Menu**: Allows you to display a list of items consistent with the rest of the Nylas Mail user experience.
- **Spinner**: Displays an indeterminate progress indicator centered within it's container.
- **Popover**: Component for creating menus and popovers that appear in response to a click and stay open until the user clicks outside them.
- **Flexbox**: Component for creating a Flexbox layout.
- **RetinaImg**: Replacement for standard `<img>` tags which automatically resolves the best version of the image for the user's display and can apply many image transforms.
- **ListTabular**: Component for creating a list of items backed by a paginating ModelView.
- **MultiselectList**: Component for creating a list that supports multi-selection. (Internally wraps ListTabular)
- **MultiselectActionBar**: Component for creating a contextual toolbar that is activated when the user makes a selection on a ModelView.
- **ResizableRegion**: Component that renders it's children inside a resizable region with a draggable handle.
- **TokenizingTextField**: Wraps a standard `<input>` and takes function props for tokenizing input values and displaying autocompletion suggestions.
- **EventedIFrame**: Replacement for the standard `<iframe>` tag which handles events directed at the iFrame to ensure a consistent user experience.
#####Registering Components
Once you've created components, the next step is to register them with the Component Registry. The Component Registry enables the React component injection that makes Nylas Mail so extensible. You can request that your components appear in a specific `Location`, override a built-in component by re-registering under it's `name`, or register your component for a `Role` that another package has declared.
The Component Registry API will be refined in the months to come. Here are a few examples of how to use it to extend Nylas Mail:
1. Add a component to the bottom of the Thread List column:
```
ComponentRegistry.register
view: ThreadList
name: 'ThreadList'
location: WorkspaceStore.Location.ThreadList
```
2. Add a component to the footer of the Composer:
```
ComponentRegistry.register
name: 'TemplatePicker'
role: 'Composer:ActionButton'
view: TemplatePicker
```
3. Replace the `Participants` component that ships with Nylas Mail to display thread participants on your own:
```
ComponentRegistry.register
name: 'Participants'
view: InboxParticipants
```
*Tip: Remember to unregister components in the `deactivate` method of your package.*
###Actions
Nylas Mail is built on top of Reflux, an implementation of the Flux architecture. React views fire `Actions`, which anyone in the application can subscribe to. Typically, `Stores` listen to actions to perform business logic and trigger updates to their corresponding views.
Your packages can fire `Actions` to trigger behaviors in the app. You can also define your own actions for use within your package.
For a complete list of available actions, see `Actions.coffee`. Actions in Nylas Mail are broken into three categories:
- Global Actions: These actions can be fired in any window and are automatically distributed to all windows via IPC.
- Main Window Actions: These actions can be fired in any window and are automatically sent to the main window via IPC. They are not sent to other windows of the app.
- Window Actions: These actions only broadcast within the window they're fired in.
###Database
Nylas Mail is built on top of a custom database layer modeled after ActiveRecord. For many parts of the application, the database is the source of truth. Data is retrieved from the API, written to the database, and changes to the database trigger Stores and components to refresh their contents. The illustration below shows this flow of data:
<img src="./images/database-flow.png" style="max-width:750px;">
The Database connection is managed by the `DatabaseStore`, a singleton object that exists in every window. All Database requests are asynchronous. Queries are forwarded to the application's `Browser` process via IPC and run in SQLite.
#####Declaring Models
In Nylas Mail, Models are thin wrappers around data with a particular schema. Each Model class declares a set of attributes that define the object's data. For example:
```
class Example extends Model
@attributes:
'id': Attributes.String
queryable: true
modelKey: 'id'
'object': Attributes.String
modelKey: 'object'
'namespaceId': Attributes.String
queryable: true
modelKey: 'namespaceId'
jsonKey: 'namespace_id'
'body': Attributes.JoinedData
modelTable: 'MessageBody'
modelKey: 'body'
'files': Attributes.Collection
modelKey: 'files'
itemClass: File
'unread': Attributes.Boolean
queryable: true
modelKey: 'unread'
```
When models are inflated from JSON using `fromJSON` or converted to JSON using `toJSON`, only the attributes declared on the model are copied. The `modelKey` and `jsonKey` options allow you to specify where a particular key should be found. Attributes are also coerced to the proper types: String attributes will always be strings, Boolean attributes will always be `true` or `false`, etc. `null` is a valid value for all types.
The DatabaseStore automatically maintains cache tables for storing Model objects. By default, models are stored in the cache as JSON blobs and basic attributes are not queryable. When the `queryable` option is specified on an attribute, it is given a separate column and index in the SQLite table for the model, and you can construct queries using the attribute:
```
Thread.attributes.namespaceId.equals("123")
// where namespace_id = '123'
Thread.attributes.lastMessageTimestamp.greaterThan(123)
// where last_message_timestamp > 123
Thread.attributes.lastMessageTimestamp.descending()
// order by last_message_timestamp DESC
```
#####Retrieving Models
You can make queries for models stored in SQLite using a Promise-based ActiveRecord-style syntax. There is no way to make raw SQL queries against the local data store.
```
DatabaseStore.find(Thread, '123').then (thread) ->
# thread is a thread object
DatabaseStore.findBy(Thread, {subject: 'Hello World'}).then (thread) ->
# find a single thread by subject
DatabaseStore.findAll(Thread).where([Thread.attributes.tags.contains('inbox')]).then (threads) ->
# find threads with the inbox tag
DatabaseStore.count(Thread).where([Thread.attributes.lastMessageTimestamp.greaterThan(120315123)]).then (results) ->
# count threads where last message received since 120315123.
```
#####Retrieving Pages of Models
If you need to paginate through a view of data, you should use a `DatabaseView`. Database views can be configured with a sort order and a set of where clauses. After the view is configured, it maintains a cache of models in memory in a highly efficient manner and makes it easy to implement pagination. `DatabaseView` also performs deep inspection of it's cache when models are changed and can avoid costly SQL queries.
#####Saving and Updating Models
The DatabaseStore exposes two methods for creating and updating models: `persistModel` and `persistModels`. When you call `persistModel`, queries are automatically executed to update the object in the cache and the DatabaseStore triggers, broadcasting an update to the rest of the application so that views dependent on these kind of models can refresh.
When possible, you should accumulate the objects you want to save and call `persistModels`. The DatabaseStore will generate batch insert statements, and a single notification will be broadcast throughout the application. Since saving objects can result in objects being re-fetched by many stores and components, you should be mindful of database insertions.
#####Saving Drafts
Drafts in Nylas Mail presented us with a unique challenge. The same draft may be edited rapidly by unrelated parts of the application, causing race scenarios. (For example, when the user is typing and attachments finish uploading at the same time.) This problem could be solved by object locking, but we chose to marshall draft changes through a central DraftStore that debounces database queries and adds other helpful features. See the `DraftStore` documentation for more information.
#####Removing Models
The DatabaseStore exposes a single method, `unpersistModel`, that allows you to purge an object from the cache. You cannot remove a model by ID alone - you must load it first.
####Advanced Model Attributes
#####Attribute.JoinedData
Joined Data attributes allow you to store certain attributes of an object in a separate table in the database. We use this attribute type for Message bodies. Storing message bodies, which can be very large, in a separate table allows us to make queries on message metadata extremely fast, and inflate Message objects without their bodies to build the thread list.
When building a query on a model with a JoinedData attribute, you need to call `include` to explicitly load the joined data attribute. The query builder will automatically perform a `LEFT OUTER JOIN` with the secondary table to retrieve the attribute:
```
DatabaseStore.find(Message, '123').then (message) ->
// message.body is undefined
DatabaseStore.find(Message, '123').include(Message.attributes.body).then (message) ->
// message.body is defined
```
When you call `persistModel`, JoinedData attributes are automatically written to the secondary table.
JoinedData attributes cannot be `queryable`.
#####Attribute.Collection
Collection attributes provide basic support for one-to-many relationships. For example, Threads in Nylas Mail have a collection of Tags.
When Collection attributes are marked as `queryable`, the DatabaseStore automatically creates a join table and maintains it as you create, save, and delete models. When you call `persistModel`, entries are added to the join table associating the ID of the model with the IDs of models in the collection.
Collection attributes have an additional clause builder, `contains`:
```
DatabaseStore.findAll(Thread).where([Thread.attributes.tags.contains('inbox')])
```
This is equivalent to writing the following SQL:
```
SELECT `Thread`.`data` FROM `Thread` INNER JOIN `Thread-Tag` AS `M1` ON `M1`.`id` = `Thread`.`id` WHERE `M1`.`value` = 'inbox' ORDER BY `Thread`.`last_message_timestamp` DESC
```
#### Listening for Changes
For many parts of the application, the Database is the source of truth. Funneling changes through the database ensures that they are available to the entire application. Basing your packages on the Database, and listening to it for changes, ensures that your views never fall out of sync.
Within Reflux Stores, you can listen to the DatabaseStore using the `listenTo` helper method:
```
@listenTo(DatabaseStore, @_onDataChanged)
```
Within generic code, you can listen to the DatabaseStore using this syntax:
```
@unlisten = DatabaseStore.listen(@_onDataChanged, @)
```
When a model is persisted or unpersisted from the database, your listener method will fire. It's very important to inspect the change payload before making queries to refresh your data. The change payload is a simple object with the following keys:
```
{
"objectClass": // string: the name of the class that was changed. ie: "Thread"
"objects": // array: the objects that were persisted or removed
}
```
##### But why can't I...?
Nylas Mail exposes a minimal Database API that exposes high-level methods for saving and retrieving objects. The API was designed with several goals in mind, which will help us create a healthy ecosystem of third-party packages:
- Package code should not be tightly coupled to SQLite
- Queries should be composed in a way that makes invalid queries impossible
- All changes to the local database must be observable
###Sheets and Columns
The Nylas Mail user interface is conceptually organized into Sheets. Each Sheet represents a window of content. For example, the `Threads` sheet lies at the heart of the application. When the user chooses the "Files" tab, a separate `Files` sheet is displayed in place of `Threads`. When the user clicks a thread in single-pane mode, a `Thread` sheet is pushed on to the workspace and appears after a brief transition.
<img src="./images/sheets.png" style="max-width:400px;">
The `WorkspaceStore` maintains the state of the application's workspace and the stack of sheets currently being displayed. Your packages can declare "root" sheets which are listed in the app's main sidebar, or push custom sheets on top of sheets to display data.
The Nilas Workspace supports two display modes: `split` and `list`. Each Sheet describes it's appearance in each of the view modes it supports. For example, the `Threads` sheet describes a three column `split` view and a single column `list` view. Other sheets, like `Files` register for only one mode, and the user's mode preference is ignored.
For each mode, Sheets register a set of column names.
<img src="./images/columns.png" style="max-width:800px;">
```
@defineSheet 'Threads', {root: true},
split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']
list: ['RootSidebar', 'ThreadList']
```
Column names are important. Once you've registered a sheet, your package (and other packages) register React components that appear in each column.
Sheets also have a `Header` and `Footer` region that spans all of their content columns. You can register components to appear in these regions to display notifications, add bars beneath the toolbar, etc.
```
ComponentRegistry.register
view: AccountSidebar
name: 'AccountSidebar'
location: WorkspaceStore.Location.RootSidebar
ComponentRegistry.register
view: NotificationsStickyBar
name: 'NotificationsStickyBar'
location: WorkspaceStore.Sheet.Threads.Header
```
Each column is laid out as a CSS Flexbox, making them extremely flexible. For more about layout using Flexbox, see Working with Flexbox.
###Toolbars
Toolbars in Nylas Mail are also powered by the Component Registry. Though toolbars appear to be a single unit at the top of a sheet, they are divided into columns with the same widths as the columns in the sheet beneath them.
<img src="./images/toolbar.png" style="max-width:800px;">
Each Toolbar column is laid out using Flexbox. You can control where toolbar elements appear within the column using the CSS `order` attribute. To make it easy to position toolbar items on the left, right, or center of a column, we've added two "spacer" elements with `order:50` and `order:-50` that evenly use up available space. Other CSS attributes allow you to control whether your items shrink or expand as the column's size changes.
<img src="./images/toolbar-column.png" style="max-width:800px;">
To add items to a toolbar, you inject them via the Component Registry. There are several ways of describing the location of a toolbar component which are useful in different scenarios:
- `<Location>.Toolbar`: This component will always appear in the toolbar above the column named `<Location>`.
(Example: Compose button which appears above the Left Sidebar column, regardless of what else is there.)
- `<ComponentName>.Toolbar`: This component will appear in the toolbar above `<ComponentName>`.
(Example: Archive button that should always be coupled with the MessageList component, placed anywhere a MessageList component is placed.)
- `Global.Toolbar.Left`: This component will always be added to the leftmost column of the toolbar.
(Example: Window Controls)
###Debugging
Nylas Mail is built on top of Atom Shell, which runs the latest version of Chromium (at the time of writing, Chromium 41). You can access the standard Chrome Developer Tools using the Command-Option-I keyboard shortcut. When you open the developer tools, you'll also notice a bar appear at the bottom of the window. This bar allows you to inspect API requests sent from the app, streaming updates received from the Nilas API, and tasks that are queued for processing with the `TaskQueue`.
###Software Architecture
Promises:
Loose Coupling

BIN
docs/images/columns.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
docs/images/sheets.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
docs/images/toolbar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -1,7 +1,9 @@
Reflux = require 'reflux'
_ = require 'underscore-plus'
Actions = require './flux/actions'
{Listener, Publisher} = require './flux/modules/reflux-coffee'
CoffeeHelpers = require './flux/coffee-helpers'
# Error types
class RegistryError extends Error
@ -47,8 +49,13 @@ class Component
# Avoid direct access to the registry
registry = {}
ComponentRegistry = Reflux.createStore
init: ->
class ComponentRegistry
@include: CoffeeHelpers.includeModule
@include Publisher
@include Listener
constructor: ->
@_showComponentRegions = false
@listenTo Actions.toggleComponentRegions, @_onToggleComponentRegions
@ -111,4 +118,5 @@ ComponentRegistry = Reflux.createStore
RegistryError: RegistryError
Mixin: Mixin
module.exports = ComponentRegistry
module.exports = new ComponentRegistry()

View file

@ -9,7 +9,7 @@ InjectedComponentSet = require './injected-component-set'
RetinaImg = require './retina-img'
###
# Public: MultiselectActionBar is a simple component that can be placed in a {Sheet} Toolbar.
Public: MultiselectActionBar is a simple component that can be placed in a {Sheet} Toolbar.
When the provided `dataStore` has a selection, it appears over the other items in the toolbar.
Generally, you wrap {MultiselectActionBar} in your own simple component to provide a dataStore
@ -30,10 +30,10 @@ The MultiselectActionBar uses the `ComponentRegistry` to find items to display f
collection name. To add an item to the bar created in the example above, register it like this:
```
ComponentRegistry.register
view: ThreadBulkArchiveButton
name: 'ThreadBulkArchiveButton'
role: 'thread:BulkAction'
ComponentRegistry.register
view: ThreadBulkArchiveButton
name: 'ThreadBulkArchiveButton'
role: 'thread:BulkAction'
```
###
class MultiselectActionBar extends React.Component

View file

@ -0,0 +1,10 @@
convenience methods to insert plaintext into draft
documentation of roles
documentation of DraftStore extensions
RetinaImg with bad image name throws exception
when third-party react components throw exceptions, it breaks everything
when typing team @ nilas, undefined react component in dropdown list?

View file

@ -0,0 +1,69 @@
_ = require 'underscore-plus'
module.exports =
# This copied out CoffeeScript
includeModule: (mixin) ->
if not mixin
return throw 'Supplied mixin was not found'
if not _
return throw 'Underscore was not found'
mixin = mixin.prototype if _.isFunction(mixin)
# Make a copy of the superclass with the same constructor and use it
# instead of adding functions directly to the superclass.
if @.__super__
tmpSuper = _.extend({}, @.__super__)
tmpSuper.constructor = @.__super__.constructor
@.__super__ = tmpSuper || {}
# Copy function over to prototype and the new intermediate superclass.
for methodName, funct of mixin when methodName not in ['included']
@.__super__[methodName] = funct
if not @prototype.hasOwnProperty(methodName)
@prototype[methodName] = funct
mixin.included?.apply(this)
this
# Allows the root objects to extend other objects as class methods via the
# object.
extendModule: (module) ->
if not module?
console.warn "The module you are trying to extend does not exist. Ensure you have put it on this page's manifest."
if _.isFunction(module) then module = module()
@[key] = value for key, value of module
return @
# Allows the root objects to include other objects as instance methods via
# the prototype
simpleInclude: (module) ->
if not module?
console.warn "The module you are trying to include does not exist. Ensure you have put it on this page's manifest."
if _.isFunction(module) then module = module()
@::[key] = value for key, value of module
return @
# This should be called as the first item from the constructor of an
# object.
#
# You can optionally pass a refernce to a super's prototype.
boundInclude: (module, _super) ->
if not module?
console.warn "The module you are trying to include does not exist. Ensure you have put it on this page's manifest."
return
if not _.isFunction(module)
console.warn "To do a scoped include the Module must be a function instead of a plain old javascript object thereby allowing `this` to be bound properly."
return
for key, value of module.call(@, _super)
@[key] = value unless @[key]?
return @

View file

@ -0,0 +1,159 @@
_ = require('underscore-plus')
EventEmitter = require('events').EventEmitter
###*
# Extract child listenables from a parent from their
# children property and return them in a keyed Object
#
# @param {Object} listenable The parent listenable
###
mapChildListenables = (listenable) ->
i = 0
children = {}
childName = undefined
while i < (listenable.children or []).length
childName = listenable.children[i]
if listenable[childName]
children[childName] = listenable[childName]
++i
children
###*
# Make a flat dictionary of all listenables including their
# possible children (recursively), concatenating names in camelCase.
#
# @param {Object} listenables The top-level listenables
###
flattenListenables = (listenables) ->
flattened = {}
for key of listenables
listenable = listenables[key]
childMap = mapChildListenables(listenable)
# recursively flatten children
children = flattenListenables(childMap)
# add the primary listenable and chilren
flattened[key] = listenable
for childKey of children
childListenable = children[childKey]
flattened[key + _.capitalize(childKey)] = childListenable
flattened
module.exports =
Listener:
hasListener: (listenable) ->
i = 0
j = undefined
listener = undefined
listenables = undefined
while i < (@subscriptions or []).length
listenables = [].concat(@subscriptions[i].listenable)
j = 0
while j < listenables.length
listener = listenables[j]
if listener == listenable or listener.hasListener and listener.hasListener(listenable)
return true
j++
++i
false
listenToMany: (listenables) ->
allListenables = flattenListenables(listenables)
for key of allListenables
cbname = _.callbackName(key)
localname = if @[cbname] then cbname else if @[key] then key else undefined
if localname
@listenTo allListenables[key], localname, @[cbname + 'Default'] or @[localname + 'Default'] or localname
return
validateListening: (listenable) ->
if listenable == this
return 'Listener is not able to listen to itself'
if !_.isFunction(listenable.listen)
return listenable + ' is missing a listen method'
if listenable.hasListener and listenable.hasListener(this)
return 'Listener cannot listen to this listenable because of circular loop'
return
listenTo: (listenable, callback, defaultCallback) ->
desub = undefined
unsubscriber = undefined
subscriptionobj = undefined
subs = @subscriptions = @subscriptions or []
err = @validateListening(listenable)
throw err if err
@fetchInitialState listenable, defaultCallback
desub = listenable.listen(@[callback] or callback, this)
unsubscriber = ->
index = subs.indexOf(subscriptionobj)
_.throwIf index == -1, 'Tried to remove listen already gone from subscriptions list!'
subs.splice index, 1
desub()
return
subscriptionobj =
stop: unsubscriber
listenable: listenable
subs.push subscriptionobj
subscriptionobj
stopListeningTo: (listenable) ->
sub = undefined
i = 0
subs = @subscriptions or []
while i < subs.length
sub = subs[i]
if sub.listenable == listenable
sub.stop()
_.throwIf subs.indexOf(sub) != -1, 'Failed to remove listen from subscriptions list!'
return true
i++
false
stopListeningToAll: ->
remaining = undefined
subs = @subscriptions or []
while remaining = subs.length
subs[0].stop()
_.throwIf subs.length != remaining - 1, 'Failed to remove listen from subscriptions list!'
return
fetchInitialState: (listenable, defaultCallback) ->
defaultCallback = defaultCallback and @[defaultCallback] or defaultCallback
me = this
if _.isFunction(defaultCallback) and _.isFunction(listenable.getInitialState)
data = listenable.getInitialState()
if data and _.isFunction(data.then)
data.then ->
defaultCallback.apply me, arguments
return
else
defaultCallback.call this, data
return
Publisher:
setupEmitter: ->
return if @_emitter
@_emitter ?= new EventEmitter()
@_emitter.setMaxListeners(25)
listen: (callback, bindContext) ->
@setupEmitter()
bindContext ?= @
aborted = false
eventHandler = ->
return if aborted
callback.apply(bindContext, arguments)
@_emitter.addListener('trigger', eventHandler)
return =>
aborted = true
@_emitter.removeListener('trigger', eventHandler)
trigger: (arg) ->
@setupEmitter()
@_emitter.emit('trigger', arg)

View file

@ -1,4 +1,3 @@
Reflux = require 'reflux'
async = require 'async'
remote = require 'remote'
_ = require 'underscore-plus'
@ -8,22 +7,26 @@ LocalLink = require '../models/local-link'
ModelQuery = require '../models/query'
PriorityUICoordinator = require '../../priority-ui-coordinator'
{AttributeCollection, AttributeJoinedData} = require '../attributes'
{modelFromJSON, modelClassMap, tableNameForJoin, generateTempId, isTempId} = require '../models/utils'
{modelFromJSON,
modelClassMap,
tableNameForJoin,
generateTempId,
isTempId} = require '../models/utils'
fs = require 'fs-plus'
path = require 'path'
ipc = require 'ipc'
{Listener, Publisher} = require '../modules/reflux-coffee'
CoffeeHelpers = require '../coffee-helpers'
silent = atom.getLoadSettings().isSpec
verboseFilter = (query) ->
false
##
# The DatabaseProxy dispatches queries to the Browser process via IPC and listens
# for results. It maintains a hash of `queryRecords` representing queries that are
# currently running and fires the correct callbacks when data is received.
#
# @namespace Application
#
class DatabaseProxy
constructor: (@databasePath) ->
@windowId = remote.getCurrentWindow().id
@ -56,13 +59,10 @@ class DatabaseProxy
console.log(query,values) if verboseFilter(query)
ipc.send('database-query', {@databasePath, queryKey, query, values})
##
# DatabasePromiseTransaction converts the callback syntax of the Database
# into a promise syntax with nice features like serial execution of many
# queries in the same promise.
#
# @namespace Application
#
class DatabasePromiseTransaction
constructor: (@_db, @_resolve, @_reject) ->
@_running = 0
@ -95,22 +95,23 @@ class DatabasePromiseTransaction
, (err) =>
@_resolve()
###
# N1 is built on top of a custom database layer modeled after ActiveRecord.
# For many parts of the application, the database is the source of truth.
# Data is retrieved from the API, written to the database, and changes to the
# database trigger Stores and components to refresh their contents.
# Public: Nylas Mail is built on top of a custom database layer modeled after
# ActiveRecord. For many parts of the application, the database is the source
# of truth. Data is retrieved from the API, written to the database, and changes
# to the database trigger Stores and components to refresh their contents.
# The DatabaseStore is available in every application window and allows you to
# make queries against the local cache. Every change to the local cache is
# broadcast as a change event, and listening to the DatabaseStore keeps the
# rest of the application in sync.
#
# @class DatabaseStore
# @namespace Application
###
DatabaseStore = Reflux.createStore
init: ->
class DatabaseStore
@include: CoffeeHelpers.includeModule
@include Publisher
@include Listener
constructor: ->
@_root = atom.isMainWindow()
@_localIdLookupCache = {}
@_db = null
@ -284,9 +285,9 @@ DatabaseStore = Reflux.createStore
Namespace = require '../models/namespace'
@trigger({objectClass: Namespace.name})
##
# Asynchronously writes `model` to the cache and triggers a change event.
# @param {Model} model
# Public: Asynchronously writes `model` to the cache and triggers a change event.
#
# - `model` A {Model} to write to the database.
#
persistModel: (model) ->
@inTransaction {}, (tx) =>
@ -295,10 +296,10 @@ DatabaseStore = Reflux.createStore
tx.execute('COMMIT')
@trigger({objectClass: model.constructor.name, objects: [model]})
##
# Asynchronously writes `models` to the cache and triggers a single change event.
# Note: Models must be of the same class to be persisted in a batch operation.
# @param {Array<Model>} model
# Public: Asynchronously writes `models` to the cache and triggers a single change
# event. Note: Models must be of the same class to be persisted in a batch operation.
#
# - `models` An {Array} of {Model} objects to write to the database.
#
persistModels: (models) ->
klass = models[0].constructor
@ -316,9 +317,9 @@ DatabaseStore = Reflux.createStore
tx.execute('COMMIT')
@trigger({objectClass: models[0].constructor.name, objects: models})
##
# Asynchronously removes `model` from the cache and triggers a change event.
# @param {Model} model
# Public: Asynchronously removes `model` from the cache and triggers a change event.
#
# - `model` A {Model} to write to the database.
#
unpersistModel: (model) ->
@inTransaction {}, (tx) =>
@ -337,52 +338,79 @@ DatabaseStore = Reflux.createStore
@trigger({objectClass: newModel.constructor.name, objects: [oldModel, newModel]})
Actions.didSwapModel({oldModel, newModel, localId})
# ActiveRecord-style Querying
###
ActiveRecord-style Querying
###
##
# Creates a new Model Query for retrieving a single model specified by the class and id.
# @param {Model.constructor} klass The class of the Model you are requesting
# @param {String} id The id of the Model you are requesting
# @return {ModelQuery}
# Public: Creates a new Model Query for retrieving a single model specified by
# the class and id.
#
# - `class` The class of the {Model} you're trying to retrieve.
# - `id` The {String} id of the {Model} you're trying to retrieve
#
# Example:
# ```
# DatabaseStore.find(Thread, 'id-123').then (thread) ->
# # thread is a Thread object, or null if no match was found.
# ```
#
# Returns a {ModelQuery}
#
find: (klass, id) ->
throw new Error("You must provide a class to findByLocalId") unless klass
throw new Error("find takes a string id. You may have intended to use findBy.") unless _.isString(id)
new ModelQuery(klass, @).where({id:id}).one()
##
# Creates a new Model Query for retrieving a single model matching the predicates provided.
# @param {Model.constructor} klass The class of the Model you are requesting
# @param {Array<Matcher>} predicates A set of predicates (where clauses) the
# returned model must match.
# @return {ModelQuery}
# Public: Creates a new Model Query for retrieving a single model matching the
# predicates provided.
#
# - `class` The class of the {Model} you're trying to retrieve.
# - `predicates` An {Array} of {matcher} objects. The set of predicates the
# returned model must match.
#
# Returns a {ModelQuery}
#
findBy: (klass, predicates = []) ->
throw new Error("You must provide a class to findBy") unless klass
new ModelQuery(klass, @).where(predicates).one()
##
# Creates a new Model Query for retrieving models matching the predicates provided.
# @param {Model.constructor} klass The class of the Model you are requesting
# @param {Array<Matcher>} predicates A set of predicates (where clauses) that
# returned models must match.
# @return {ModelQuery}
# Public: Creates a new Model Query for retrieving all models matching the
# predicates provided.
#
# - `class` The class of the {Model} you're trying to retrieve.
# - `predicates` An {Array} of {matcher} objects. The set of predicates the
# returned model must match.
#
# Returns a {ModelQuery}
#
findAll: (klass, predicates = []) ->
throw new Error("You must provide a class to findAll") unless klass
new ModelQuery(klass, @).where(predicates)
##
# Creates a new Model Query for counting models matching the predicates provided.
# @param {Model.constructor} klass The class of the Model you are requesting
# @param {Array<Matcher>} predicates A set of predicates (where clauses)
# @return {ModelQuery}
# Public: Creates a new Model Query that returns the {Number} of models matching
# the predicates provided.
#
# - `class` The class of the {Model} you're trying to retrieve.
# - `predicates` An {Array} of {matcher} objects. The set of predicates the
# returned model must match.
#
# Returns a {ModelQuery}
#
count: (klass, predicates = []) ->
throw new Error("You must provide a class to count") unless klass
new ModelQuery(klass, @).where(predicates).count()
# Support for Local IDs
###
Support for Local IDs
###
# Public: Retrieve a Model given a localId.
#
# - `class` The class of the {Model} you're trying to retrieve.
# - `localId` The {String} localId of the object.
#
# Returns a {Promise} that resolves with the Model associated with the localId,
# or rejects if no matching object is found.
# Note: When fetching an object by local Id, joined attributes
# (like body, stored in a separate table) are always included.
@ -471,4 +499,4 @@ DatabaseStore = Reflux.createStore
queries
module.exports = DatabaseStore
module.exports = new DatabaseStore()

View file

@ -3,21 +3,22 @@ Actions = require '../actions'
EventEmitter = require('events').EventEmitter
_ = require 'underscore-plus'
# As the user interacts with the draft, changes are accumulated in the
# DraftChangeSet associated with the store proxy. The DraftChangeSet does two things:
#
# 1. It debounces changes and calls Actions.saveDraft() at a reasonable interval.
#
# 2. It exposes `applyToModel`, which allows you to optimistically apply changes
# to a draft object. When the proxy vends the draft, it passes it through this
# function to apply uncommitted changes. This means the Draft provided by the
# DraftStoreProxy will always relfect recent changes, even though they're
# written to the database intermittently.
#
###
Public: As the user interacts with the draft, changes are accumulated in the
DraftChangeSet associated with the store proxy. The DraftChangeSet does two things:
1. It debounces changes and calls Actions.saveDraft() at a reasonable interval.
2. It exposes `applyToModel`, which allows you to optimistically apply changes
to a draft object. When the proxy vends the draft, it passes it through this
function to apply uncommitted changes. This means the Draft provided by the
DraftStoreProxy will always relfect recent changes, even though they're
written to the database intermittently.
###
class DraftChangeSet
constructor: (@localId, @_onChange) ->
@reset()
reset: ->
@_pending = {}
clearTimeout(@_timer) if @_timer
@ -47,17 +48,17 @@ class DraftChangeSet
model.fromJSON(@_pending) if model
model
# DraftStoreProxy is a small class that makes it easy to implement components
# that display Draft objects or allow for interactive editing of Drafts.
#
# 1. It synchronously provides an instance of a draft via `draft()`, and
# triggers whenever that draft instance has changed.
#
# 2. It provides an interface for modifying the draft that transparently
# batches changes, and ensures that the draft provided via `draft()`
# always has pending changes applied.
#
module.exports =
###
Public: DraftStoreProxy is a small class that makes it easy to implement components
that display Draft objects or allow for interactive editing of Drafts.
1. It synchronously provides an instance of a draft via `draft()`, and
triggers whenever that draft instance has changed.
2. It provides an interface for modifying the draft that transparently
batches changes, and ensures that the draft provided via `draft()`
always has pending changes applied.
###
class DraftStoreProxy
constructor: (@draftLocalId) ->
@ -80,7 +81,7 @@ class DraftStoreProxy
draft: ->
@changes.applyToModel(@_draft)
@_draft
prepare: ->
@_draftPromise ?= new Promise (resolve, reject) =>
DatabaseStore = require './database-store'
@ -104,7 +105,7 @@ class DraftStoreProxy
# Unlink ourselves from the stores/actions we were listening to
# so that we can be garbage collected
unlisten() for unlisten in @unlisteners
_setDraft: (draft) ->
if !draft.body?
throw new Error("DraftStoreProxy._setDraft - new draft has no body!")
@ -127,3 +128,6 @@ class DraftStoreProxy
# localId.
if change.oldModel.id is @_draft.id
@_setDraft(change.newModel)
module.exports = DraftStoreProxy

View file

@ -2,7 +2,6 @@ _ = require 'underscore-plus'
moment = require 'moment'
ipc = require 'ipc'
Reflux = require 'reflux'
DraftStoreProxy = require './draft-store-proxy'
DatabaseStore = require './database-store'
NamespaceStore = require './namespace-store'
@ -17,18 +16,25 @@ Actions = require '../actions'
{subjectWithPrefix} = require '../models/utils'
# A DraftStore responds to Actions that interact with Drafts and exposes
# public getter methods to return Draft objects.
#
# It also handles the dispatching of Tasks to persist changes to the Inbox
# API.
#
# Remember that a "Draft" is actually just a "Message" with draft: true.
#
{Listener, Publisher} = require '../modules/reflux-coffee'
CoffeeHelpers = require '../coffee-helpers'
module.exports =
DraftStore = Reflux.createStore
init: ->
###
Public: DraftStore responds to Actions that interact with Drafts and exposes
public getter methods to return Draft objects and sessions.
It also creates and queues {Task} objects to persist changes to the Nylas
API.
Remember that a "Draft" is actually just a "Message" with `draft: true`.
###
class DraftStore
@include: CoffeeHelpers.includeModule
@include Publisher
@include Listener
constructor: ->
@listenTo DatabaseStore, @_onDataChanged
@listenTo Actions.composeReply, @_onComposeReply
@ -63,8 +69,25 @@ DraftStore = Reflux.createStore
######### PUBLIC #######################################################
# Returns a promise
# Public: Fetch a {DraftStoreProxy} for displaying and/or editing the
# draft with `localId`. After calling `sessionForLocalId`, you generally
# want to call {DraftStoreProxy::prepare} and wait for the returned
# {Promise} to resolve:
#
# Example:
#
# ```
# session = DraftStore.sessionForLocalId(localId)
# session.prepare().then ->
# # session.draft() is now ready
# ```
#
# - `localId` The {String} local ID of the draft.
#
# Returns a {Object} with:
# - `key1` A {String} that contains bla
# - `key2` A {String} that contains bla
#
sessionForLocalId: (localId) ->
if not localId
console.log((new Error).stack)
@ -72,21 +95,35 @@ DraftStore = Reflux.createStore
@_draftSessions[localId] ?= new DraftStoreProxy(localId)
@_draftSessions[localId]
# Public: Look up the sending state of the given draft Id.
sendingState: (draftLocalId) -> @_sendingState[draftLocalId] ? false
# Composer Extensions
###
Composer Extensions
###
# Public: Returns the extensions registered with the DraftStore.
extensions: (ext) ->
@_extensions
# Public: Registers a new extension with the DraftStore. DraftStore extensions
# make it possible to extend the editor experience, modify draft contents,
# display warnings before draft are sent, and more.
#
# - `ext` A {DraftStoreExtension} instance.
#
registerExtension: (ext) ->
@_extensions.push(ext)
# Public: Unregisters the extension provided from the DraftStore.
#
# - `ext` A {DraftStoreExtension} instance.
#
unregisterExtension: (ext) ->
@_extensions = _.without(@_extensions, ext)
########### PRIVATE ####################################################
cleanupSessionForLocalId: (localId) ->
return unless @_draftSessions[localId]
@ -322,3 +359,6 @@ DraftStore = Reflux.createStore
files = proxy.draft().files ? []
files = _.reject files, (f) -> f.id is file.id
proxy.changes.add({files}, true)
module.exports = new DraftStore()

View file

@ -1,14 +1,22 @@
Reflux = require 'reflux'
Actions = require '../actions'
Namespace = require '../models/namespace'
DatabaseStore = require './database-store'
_ = require 'underscore-plus'
# The ThreadStore listens to changes to the avaialble namespaces in the database
# and manages the currently selected namespace.
{Listener, Publisher} = require '../modules/reflux-coffee'
CoffeeHelpers = require '../coffee-helpers'
NamespaceStore = Reflux.createStore
init: ->
###
Public: The NamespaceStore listens to changes to the available namespaces in
the database and exposes the currently active Namespace via {::current}
###
class NamespaceStore
@include: CoffeeHelpers.includeModule
@include Publisher
@include Listener
constructor: ->
@_items = []
@_current = null
@ -39,10 +47,12 @@ NamespaceStore = Reflux.createStore
# Exposed Data
# Public: Returns an {Array} of {Namespace} objects
items: ->
@_namespaces
# Public: Returns the currently active {Namespace}.
current: ->
@_current
module.exports = NamespaceStore
module.exports = new NamespaceStore()

View file

@ -6,13 +6,9 @@ Location = {}
Sheet = {}
###
# The WorkspaceStore manages Sheets and layout modes in the application.
# Observing the WorkspaceStore makes it easy to monitor the sheet stack.
#
# @class WorkspaceStore
# @namespace Application
Public: The WorkspaceStore manages Sheets and layout modes in the application.
Observing the WorkspaceStore makes it easy to monitor the sheet stack.
###
WorkspaceStore = Reflux.createStore
init: ->
@defineSheet 'Global'
@ -46,7 +42,9 @@ WorkspaceStore = Reflux.createStore
@_onSelectRootSheet(Sheet.Threads)
# Inbound Events
###
Inbound Events
###
_onSelectRootSheet: (sheet) ->
if not sheet
@ -77,10 +75,11 @@ WorkspaceStore = Reflux.createStore
if not item and @topSheet() is Sheet.File
@popSheet()
# Accessing Data
###
Accessing Data
###
##
# @return {String} The current layout mode. Either `split` or `list`
# Returns a {String}: The current layout mode. Either `split` or `list`
#
layoutMode: ->
if @_preferredLayoutMode in @rootSheet().supportedModes
@ -88,33 +87,31 @@ WorkspaceStore = Reflux.createStore
else
@rootSheet().supportedModes[0]
##
# @return {Sheet} The top sheet in the current stack. Use this method to determine
# Returns The top {Sheet} in the current stack. Use this method to determine
# the sheet the user is looking at.
#
topSheet: ->
@_sheetStack[@_sheetStack.length - 1]
##
# @return {Sheet} The sheet at the root of the current stack.
# Returns The {Sheet} at the root of the current stack.
#
rootSheet: ->
@_sheetStack[0]
##
# @return {Array<Sheet>} The stack of sheets
# Returns an {Array<Sheet>} The stack of sheets
#
sheetStack: ->
@_sheetStack
# Managing Sheets
##
# @param {String} id The ID of the Sheet being defined.
# @param {Object} options If the sheet should be listed in the left sidebar,
# pass `root: true, name: 'Label'`.
# @param {Object} columns An object with keys for each layout mode the Sheet
# supports. For each key, provide an array of column names.
###
Managing Sheets
###
# * `id` {String} The ID of the Sheet being defined.
# * `options` {Object} If the sheet should be listed in the left sidebar,
# pass `{root: true, name: 'Label'}`.
# *`columns` An {Object} with keys for each layout mode the Sheet
# supports. For each key, provide an array of column names.
#
defineSheet: (id, options = {}, columns = {}) ->
# Make sure all the locations have definitions so that packages
@ -142,7 +139,7 @@ WorkspaceStore = Reflux.createStore
# A back button will appear in the top left of the pushed sheet.
# This method triggers, allowing observers to update.
#
# @param {Sheet} sheet The sheet type to push onto the stack.
# * `sheet` The {Sheet} type to push onto the stack.
#
pushSheet: (sheet) ->
@_sheetStack.push(sheet)