mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-06 20:54:26 +08:00
docs(*): Break markdown docs into separate files, add model/attribute docs
This commit is contained in:
parent
0352090007
commit
0fdef6a1a6
28 changed files with 1065 additions and 899 deletions
|
@ -42,7 +42,7 @@
|
|||
"rimraf": "~2.2.2",
|
||||
"runas": "~1.0.1",
|
||||
"s3": "^4.3",
|
||||
"tello": "1.0.4",
|
||||
"tello": "1.0.5",
|
||||
"temp": "~0.8.1",
|
||||
"underscore-plus": "1.x",
|
||||
"unzip": "~0.1.9",
|
||||
|
|
|
@ -37,9 +37,14 @@ standardClasses = [
|
|||
'typeerror',
|
||||
'syntaxerror',
|
||||
'referenceerror',
|
||||
'rangeerror'
|
||||
'rangeerror',
|
||||
]
|
||||
|
||||
thirdPartyClasses = {
|
||||
'react.component': 'https://facebook.github.io/react/docs/component-api.html',
|
||||
'promise': 'https://github.com/petkaantonov/bluebird/blob/master/API.md'
|
||||
}
|
||||
|
||||
module.exports = (grunt) ->
|
||||
getClassesToInclude = ->
|
||||
modulesPath = path.resolve(__dirname, '..', '..', 'internal_packages')
|
||||
|
@ -113,7 +118,7 @@ module.exports = (grunt) ->
|
|||
grunt.registerTask 'render-docs', 'Builds html from the API docs', ->
|
||||
docsOutputDir = grunt.config.get('docsOutputDir')
|
||||
apiJsonPath = path.join(docsOutputDir, 'api.json')
|
||||
|
||||
|
||||
templatesPath = path.resolve(__dirname, '..', '..', 'docs-templates')
|
||||
grunt.file.recurse templatesPath, (abspath, root, subdir, filename) ->
|
||||
if filename[0] is '_' and path.extname(filename) is '.html'
|
||||
|
@ -127,12 +132,14 @@ module.exports = (grunt) ->
|
|||
console.log("Generating HTML for #{classnames.length} classes")
|
||||
|
||||
expandTypeReferences = (val) ->
|
||||
refRegex = /{([\w]*)}/g
|
||||
refRegex = /{([\w.]*)}/g
|
||||
while (match = refRegex.exec(val)) isnt null
|
||||
classname = match[1].toLowerCase()
|
||||
url = false
|
||||
if classname in standardClasses
|
||||
url = standardClassURLRoot+classname
|
||||
else if thirdPartyClasses[classname]
|
||||
url = thirdPartyClasses[classname]
|
||||
else if classname in classnames
|
||||
url = "./#{classname}.html"
|
||||
else
|
||||
|
|
38
docs/Architecture.md
Normal file
38
docs/Architecture.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
### Flux Architecture
|
||||
|
||||
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.
|
||||
|
||||
|
||||
###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.
|
||||
|
168
docs/Database.md
Normal file
168
docs/Database.md
Normal file
|
@ -0,0 +1,168 @@
|
|||
###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
|
||||
|
||||
|
||||
|
3
docs/Debugging.md
Normal file
3
docs/Debugging.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
###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 Nylas API, and tasks that are queued for processing with the `TaskQueue`.
|
|
@ -1,473 +0,0 @@
|
|||
##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
|
49
docs/Index.md
Normal file
49
docs/Index.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
##Nylas Package API
|
||||
|
||||
The Nylas Package API allows you to create powerful extensions to 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.
|
||||
|
||||
<table>
|
||||
<tr><td style="width:50%;">
|
||||
|
||||
###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.nylas.com/download?platform=darwin)
|
||||
- [Linux](https://edgehill.nylas.com/download?platform=linux)
|
||||
- [Windows](https://edgehill.nylas.com/download?platform=win32)
|
||||
|
||||
</td><td style="width:50%;">
|
||||
|
||||
###Package Architecture
|
||||
|
||||
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. Learn more about packages and create your first package.
|
||||
|
||||
- [Package Overview](./PackageOverview.md)
|
||||
|
||||
</td></tr>
|
||||
<tr><td style="width:50%; vertical-align:top;">
|
||||
|
||||
### Dive Deeper
|
||||
|
||||
- [Application Architecture](./Architecture.md)
|
||||
- [React & Component Injection](./React.md)
|
||||
- [Core Interface Concepts](./InterfaceConcepts.md)
|
||||
- [Accessing the Database](./Database.md)
|
||||
- [Draft Store Extensions](./DraftStoreExtensions.md)
|
||||
|
||||
</td><td style="width:50%; vertical-align:top;">
|
||||
|
||||
### Debugging Packages
|
||||
|
||||
Nylas Mail is built on top of Electron, which runs the latest version of Chromium. Learn how to access debug tools in Electron and use our Developer Tools Extensions:
|
||||
|
||||
- [Debugging in Nylas](./Debugging.md)
|
||||
|
||||
</td></tr>
|
||||
<tr colspan="2"><td>
|
||||
##### Questions?
|
||||
|
||||
Need help? Check out the [FAQ](./FAQ.md) or post a question in the [Nylas Mail Facebook Group](facebook.com/groups/nylas.mail)
|
||||
|
||||
</td></tr>
|
69
docs/InterfaceConcepts.md
Normal file
69
docs/InterfaceConcepts.md
Normal file
|
@ -0,0 +1,69 @@
|
|||
|
||||
|
||||
###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)
|
||||
|
||||
|
102
docs/PackageOverview.md
Normal file
102
docs/PackageOverview.md
Normal file
|
@ -0,0 +1,102 @@
|
|||
###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.
|
||||
|
74
docs/React.md
Normal file
74
docs/React.md
Normal file
|
@ -0,0 +1,74 @@
|
|||
### The Nylas Mail Interface
|
||||
|
||||
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).
|
||||
|
||||
#####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.
|
||||
|
||||
### React Component Injection
|
||||
|
||||
#####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.*
|
||||
|
|
@ -49,6 +49,12 @@ class Component
|
|||
# Avoid direct access to the registry
|
||||
registry = {}
|
||||
|
||||
|
||||
###
|
||||
Public: The ComponentRegistry maintains an index of React components registered
|
||||
by Nylas packages. Components can use {InjectedComponent} and {InjectedComponentSet}
|
||||
to dynamically render components registered with the ComponentRegistry.
|
||||
###
|
||||
class ComponentRegistry
|
||||
@include: CoffeeHelpers.includeModule
|
||||
|
||||
|
@ -59,6 +65,25 @@ class ComponentRegistry
|
|||
@_showComponentRegions = false
|
||||
@listenTo Actions.toggleComponentRegions, @_onToggleComponentRegions
|
||||
|
||||
|
||||
# Public: Register a new component with the Component Registry.
|
||||
# Typically, packages call this method from their main `activate` method
|
||||
# to extend the Nylas user interface, and call the corresponding `unregister`
|
||||
# method in `deactivate`.
|
||||
#
|
||||
# * `component` {Object} with the following keys:
|
||||
# * `name`: {String} name of your component. Must be globally unique.
|
||||
# * `view`: {React.Component} The React Component you are registering.
|
||||
# * `role`: (Optional) {String} If you want to display your component in a location
|
||||
# desigated by a role, pass the role identifier.
|
||||
# * `mode`: (Optional) {React.Component} If your component should only be displayed
|
||||
# in a particular Workspace Mode, pass the mode. ('list' or 'split')
|
||||
# * `location`: (Optional) {Object} If your component should be displayed in a
|
||||
# column or toolbar, pass the fully qualified location object, such as:
|
||||
# `WorkspaceStore.Location.ThreadList`
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
register: (component) ->
|
||||
# Receive a component or something which can build one
|
||||
throw new RegistryError 'Required: ComponentRegistry.Component or something which conforms to {name, view}' unless component instanceof Object
|
||||
|
@ -85,12 +110,24 @@ class ComponentRegistry
|
|||
throw new RegistryError 'No such component' unless component?
|
||||
component
|
||||
|
||||
# Public: Retrieve the registry entry for a given name.
|
||||
#
|
||||
# - `name`: The {String} name of the registered component to retrieve.
|
||||
#
|
||||
# Returns a {React.Component}
|
||||
#
|
||||
findByName: (name, alt) ->
|
||||
registry[name] ? alt
|
||||
|
||||
findViewByName: (name, alt) ->
|
||||
registry[name]?.view ? alt
|
||||
|
||||
# Public: Retrieve all of the registry entries for a given role.
|
||||
#
|
||||
# - `role`: The {String} role.
|
||||
#
|
||||
# Returns an {Array} of {React.Component} objects
|
||||
#
|
||||
findAllByRole: (role) ->
|
||||
_.filter (_.values registry), (component) ->
|
||||
component.role == role
|
||||
|
|
|
@ -1,293 +1,14 @@
|
|||
_ = require 'underscore-plus'
|
||||
{tableNameForJoin} = require './models/utils'
|
||||
AttributeBoolean = require './attributes/attribute-boolean'
|
||||
AttributeNumber = require './attributes/attribute-number'
|
||||
AttributeString = require './attributes/attribute-string'
|
||||
AttributeDateTime = require './attributes/attribute-datetime'
|
||||
AttributeCollection = require './attributes/attribute-collection'
|
||||
AttributeJoinedData = require './attributes/attribute-joined-data'
|
||||
Attribute = require './attributes/attribute'
|
||||
Matcher = require './attributes/matcher'
|
||||
SortOrder = require './attributes/sort-order'
|
||||
|
||||
NullPlaceholder = "!NULLVALUE!"
|
||||
|
||||
##
|
||||
# The Matcher class encapsulates a particular comparison clause on an attribute.
|
||||
# Matchers can evaluate whether or not an object matches them, and in the future
|
||||
# they will also compose WHERE clauses. Each matcher has a reference to a model
|
||||
# attribute, a comparator and a value.
|
||||
#
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class Matcher
|
||||
constructor: (@attr, @comparator, @val) ->
|
||||
@muid = Matcher.muid
|
||||
Matcher.muid = (Matcher.muid + 1) % 50
|
||||
@
|
||||
|
||||
evaluate: (model) ->
|
||||
value = model[@attr.modelKey]
|
||||
value = value() if value instanceof Function
|
||||
|
||||
switch @comparator
|
||||
when '=' then return value == @val
|
||||
when '<' then return value < @val
|
||||
when '>' then return value > @val
|
||||
when 'contains'
|
||||
# You can provide an ID or an object, and an array of IDs or an array of objects
|
||||
# Assumes that `value` is an array of items
|
||||
!!_.find value, (x) =>
|
||||
@val == x?.id || @val == x || @val?.id == x || @val?.id == x?.id
|
||||
when 'startsWith' then return value.startsWith(@val)
|
||||
when 'like' then value.search(new RegExp(".*#{@val}.*", "gi")) >= 0
|
||||
else
|
||||
throw new Error("Matcher.evaulate() not sure how to evaluate @{@attr.modelKey} with comparator #{@comparator}")
|
||||
|
||||
joinSQL: (klass) ->
|
||||
switch @comparator
|
||||
when 'contains'
|
||||
joinTable = tableNameForJoin(klass, @attr.itemClass)
|
||||
return "INNER JOIN `#{joinTable}` AS `M#{@muid}` ON `M#{@muid}`.`id` = `#{klass.name}`.`id`"
|
||||
else
|
||||
return false
|
||||
|
||||
whereSQL: (klass) ->
|
||||
|
||||
if @comparator is "like"
|
||||
val = "%#{@val}%"
|
||||
else
|
||||
val = @val
|
||||
|
||||
if _.isString(val)
|
||||
escaped = "'#{val.replace(/'/g, '\\\'')}'"
|
||||
else if val is true
|
||||
escaped = 1
|
||||
else if val is false
|
||||
escaped = 0
|
||||
else
|
||||
escaped = val
|
||||
|
||||
switch @comparator
|
||||
when 'startsWith'
|
||||
return " RAISE `TODO`; "
|
||||
when 'contains'
|
||||
return "`M#{@muid}`.`value` = #{escaped}"
|
||||
else
|
||||
return "`#{klass.name}`.`#{@attr.jsonKey}` #{@comparator} #{escaped}"
|
||||
|
||||
##
|
||||
# Represents a particular sort direction on a particular column. You should not
|
||||
# instantiate SortOrders manually. Instead, call `Attribute.ascending()` or
|
||||
# `Attribute.descending()` to obtain a sort order.
|
||||
#
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class SortOrder
|
||||
constructor: (@attr, @direction = 'DESC') ->
|
||||
orderBySQL: (klass) ->
|
||||
"`#{klass.name}`.`#{@attr.jsonKey}` #{@direction}"
|
||||
attribute: ->
|
||||
@attr
|
||||
|
||||
##
|
||||
# The Attribute class represents a single model attribute, like 'namespace_id'
|
||||
# Subclasses of Attribute like AttributeDateTime know how to covert between
|
||||
# the JSON representation of that type and the javascript representation.
|
||||
# The Attribute class also exposes convenience methods for generating Matchers.
|
||||
#
|
||||
# @abstract
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class Attribute
|
||||
constructor: ({modelKey, queryable, jsonKey}) ->
|
||||
@modelKey = modelKey
|
||||
@jsonKey = jsonKey || modelKey
|
||||
@queryable = queryable
|
||||
@
|
||||
|
||||
equal: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '=', val)
|
||||
not: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '!=', val)
|
||||
greaterThan: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '>', val)
|
||||
lessThan: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '<', val)
|
||||
contains: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, 'contains', val)
|
||||
startsWith: (val) ->
|
||||
throw new Error "startsWith cannot be applied to #{@.constructor.name}"
|
||||
descending: ->
|
||||
new SortOrder(@, 'DESC')
|
||||
ascending: ->
|
||||
new SortOrder(@, 'ASC')
|
||||
toJSON: (val) -> val
|
||||
fromJSON: (val) -> val ? null
|
||||
|
||||
##
|
||||
# The value of this attribute is always a number, or null.
|
||||
#
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class AttributeNumber extends Attribute
|
||||
toJSON: (val) -> val
|
||||
fromJSON: (val) -> unless isNaN(val) then Number(val) else null
|
||||
columnSQL: -> "#{@jsonKey} INTEGER"
|
||||
|
||||
##
|
||||
# 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`.
|
||||
#
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class AttributeJoinedData extends Attribute
|
||||
constructor: ({modelKey, jsonKey, modelTable}) ->
|
||||
super
|
||||
@modelTable = modelTable
|
||||
@
|
||||
|
||||
selectSQL: (klass) ->
|
||||
# NullPlaceholder is necessary because if the LEFT JOIN returns nothing, it leaves the field
|
||||
# blank, and it comes through in the result row as "" rather than NULL
|
||||
"IFNULL(`#{@modelTable}`.`value`, '#{NullPlaceholder}') AS `#{@modelKey}`"
|
||||
|
||||
includeSQL: (klass) ->
|
||||
"LEFT OUTER JOIN `#{@modelTable}` ON `#{@modelTable}`.`id` = `#{klass.name}`.`id`"
|
||||
|
||||
##
|
||||
# The value of this attribute is always a boolean. Null values are coerced to false.
|
||||
#
|
||||
# String attributes can be queries using `equal` and `not`. Matching on
|
||||
# `greaterThan` and `lessThan` is not supported.
|
||||
#
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class AttributeBoolean extends Attribute
|
||||
toJSON: (val) -> val
|
||||
fromJSON: (val) -> (val is 'true' or val is true) || false
|
||||
greaterThan: (val) -> throw new Error "greaterThan cannot be applied to AttributeBoolean"
|
||||
lessThan: (val) -> throw new Error "greaterThan cannot be applied to AttributeBoolean"
|
||||
columnSQL: -> "#{@jsonKey} INTEGER"
|
||||
|
||||
##
|
||||
# The value of this attribute is always a string or `null`.
|
||||
#
|
||||
# String attributes can be queries using `equal`, `not`, and `startsWith`. Matching on
|
||||
# `greaterThan` and `lessThan` is not supported.
|
||||
#
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class AttributeString extends Attribute
|
||||
toJSON: (val) -> val
|
||||
fromJSON: (val) -> val || ""
|
||||
greaterThan: (val) -> throw new Error "greaterThan cannot be applied to AttributeString"
|
||||
lessThan: (val) -> throw new Error "greaterThan cannot be applied to AttributeString"
|
||||
startsWith: (val) -> new Matcher(@, 'startsWith', val)
|
||||
columnSQL: -> "#{@jsonKey} TEXT"
|
||||
like: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, 'like', val)
|
||||
|
||||
##
|
||||
# The value of this attribute is always a Javascript `Date`, or `null`.
|
||||
#
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class AttributeDateTime extends Attribute
|
||||
toJSON: (val) ->
|
||||
return null unless val
|
||||
unless val instanceof Date
|
||||
throw new Error "Attempting to toJSON AttributeDateTime which is not a date: #{@modelKey} = #{val}"
|
||||
val.getTime() / 1000.0
|
||||
|
||||
fromJSON: (val) ->
|
||||
return null unless val
|
||||
new Date(val*1000)
|
||||
|
||||
columnSQL: -> "#{@jsonKey} INTEGER"
|
||||
|
||||
##
|
||||
# Collection attributes provide basic support for one-to-many relationships.
|
||||
# For example, Threads in N1 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
|
||||
#```
|
||||
#
|
||||
# The value of this attribute is always an array of ff other model objects. To use
|
||||
# a Collection attribute, the JSON for the parent object must contain the nested
|
||||
# objects, complete with their `object` field.
|
||||
#
|
||||
# @namespace Attribute Types
|
||||
#
|
||||
class AttributeCollection extends Attribute
|
||||
constructor: ({modelKey, jsonKey, itemClass}) ->
|
||||
super
|
||||
@itemClass = itemClass
|
||||
@
|
||||
|
||||
toJSON: (vals) ->
|
||||
return [] unless vals
|
||||
json = []
|
||||
for val in vals
|
||||
unless val instanceof @itemClass
|
||||
msg = "AttributeCollection.toJSON: Value `#{val}` in #{@modelKey} is not an #{@itemClass.name}"
|
||||
throw new Error msg
|
||||
if val.toJSON?
|
||||
json.push(val.toJSON())
|
||||
else
|
||||
json.push(val)
|
||||
json
|
||||
|
||||
fromJSON: (json) ->
|
||||
return [] unless json && json instanceof Array
|
||||
objs = []
|
||||
for objJSON in json
|
||||
obj = new @itemClass(objJSON)
|
||||
# Important: if no ids are in the JSON, don't make them up randomly.
|
||||
# This causes an object to be "different" each time it's de-serialized
|
||||
# even if it's actually the same, makes React components re-render!
|
||||
obj.id = undefined
|
||||
obj.fromJSON(objJSON) if obj.fromJSON?
|
||||
objs.push(obj)
|
||||
objs
|
||||
|
||||
Matcher.muid = 0
|
||||
|
||||
module.exports = {
|
||||
module.exports =
|
||||
Number: -> new AttributeNumber(arguments...)
|
||||
String: -> new AttributeString(arguments...)
|
||||
DateTime: -> new AttributeDateTime(arguments...)
|
||||
|
@ -303,7 +24,5 @@ module.exports = {
|
|||
AttributeBoolean: AttributeBoolean
|
||||
AttributeJoinedData: AttributeJoinedData
|
||||
|
||||
NullPlaceholder: NullPlaceholder
|
||||
SortOrder: SortOrder
|
||||
Matcher: Matcher
|
||||
}
|
||||
|
|
16
src/flux/attributes/attribute-boolean.coffee
Normal file
16
src/flux/attributes/attribute-boolean.coffee
Normal file
|
@ -0,0 +1,16 @@
|
|||
_ = require 'underscore-plus'
|
||||
{tableNameForJoin} = require '../models/utils'
|
||||
Attribute = require './attribute'
|
||||
|
||||
###
|
||||
Public: The value of this attribute is always a boolean. Null values are coerced to false.
|
||||
|
||||
String attributes can be queries using `equal` and `not`. Matching on
|
||||
`greaterThan` and `lessThan` is not supported.
|
||||
###
|
||||
class AttributeBoolean extends Attribute
|
||||
toJSON: (val) -> val
|
||||
fromJSON: (val) -> (val is 'true' or val is true) || false
|
||||
columnSQL: -> "#{@jsonKey} INTEGER"
|
||||
|
||||
module.exports = AttributeBoolean
|
72
src/flux/attributes/attribute-collection.coffee
Normal file
72
src/flux/attributes/attribute-collection.coffee
Normal file
|
@ -0,0 +1,72 @@
|
|||
_ = require 'underscore-plus'
|
||||
{tableNameForJoin} = require '../models/utils'
|
||||
Attribute = require './attribute'
|
||||
Matcher = require './matcher'
|
||||
|
||||
###
|
||||
Public: Collection attributes provide basic support for one-to-many relationships.
|
||||
For example, Threads in N1 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
|
||||
```
|
||||
|
||||
The value of this attribute is always an array of ff other model objects. To use
|
||||
a Collection attribute, the JSON for the parent object must contain the nested
|
||||
objects, complete with their `object` field.
|
||||
###
|
||||
class AttributeCollection extends Attribute
|
||||
constructor: ({modelKey, jsonKey, itemClass}) ->
|
||||
super
|
||||
@itemClass = itemClass
|
||||
@
|
||||
|
||||
toJSON: (vals) ->
|
||||
return [] unless vals
|
||||
json = []
|
||||
for val in vals
|
||||
unless val instanceof @itemClass
|
||||
msg = "AttributeCollection.toJSON: Value `#{val}` in #{@modelKey} is not an #{@itemClass.name}"
|
||||
throw new Error msg
|
||||
if val.toJSON?
|
||||
json.push(val.toJSON())
|
||||
else
|
||||
json.push(val)
|
||||
json
|
||||
|
||||
fromJSON: (json) ->
|
||||
return [] unless json && json instanceof Array
|
||||
objs = []
|
||||
for objJSON in json
|
||||
obj = new @itemClass(objJSON)
|
||||
# Important: if no ids are in the JSON, don't make them up randomly.
|
||||
# This causes an object to be "different" each time it's de-serialized
|
||||
# even if it's actually the same, makes React components re-render!
|
||||
obj.id = undefined
|
||||
obj.fromJSON(objJSON) if obj.fromJSON?
|
||||
objs.push(obj)
|
||||
objs
|
||||
|
||||
# Public: Returns a {Matcher} for objects containing the provided value.
|
||||
contains: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, 'contains', val)
|
||||
|
||||
module.exports = AttributeCollection
|
32
src/flux/attributes/attribute-datetime.coffee
Normal file
32
src/flux/attributes/attribute-datetime.coffee
Normal file
|
@ -0,0 +1,32 @@
|
|||
_ = require 'underscore-plus'
|
||||
Attribute = require './attribute'
|
||||
Matcher = require './matcher'
|
||||
|
||||
###
|
||||
Public: The value of this attribute is always a Javascript `Date`, or `null`.
|
||||
###
|
||||
class AttributeDateTime extends Attribute
|
||||
toJSON: (val) ->
|
||||
return null unless val
|
||||
unless val instanceof Date
|
||||
throw new Error "Attempting to toJSON AttributeDateTime which is not a date: #{@modelKey} = #{val}"
|
||||
val.getTime() / 1000.0
|
||||
|
||||
fromJSON: (val) ->
|
||||
return null unless val
|
||||
new Date(val*1000)
|
||||
|
||||
columnSQL: -> "#{@jsonKey} INTEGER"
|
||||
|
||||
# Public: Returns a {Matcher} for objects greater than the provided value.
|
||||
greaterThan: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '>', val)
|
||||
|
||||
# Public: Returns a {Matcher} for objects less than the provided value.
|
||||
lessThan: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '<', val)
|
||||
|
||||
|
||||
module.exports = AttributeDateTime
|
48
src/flux/attributes/attribute-joined-data.coffee
Normal file
48
src/flux/attributes/attribute-joined-data.coffee
Normal file
|
@ -0,0 +1,48 @@
|
|||
_ = require 'underscore-plus'
|
||||
{tableNameForJoin} = require '../models/utils'
|
||||
Attribute = require './attribute'
|
||||
|
||||
NullPlaceholder = "!NULLVALUE!"
|
||||
|
||||
###
|
||||
Public: 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`.
|
||||
###
|
||||
class AttributeJoinedData extends Attribute
|
||||
constructor: ({modelKey, jsonKey, modelTable}) ->
|
||||
super
|
||||
@modelTable = modelTable
|
||||
@
|
||||
|
||||
selectSQL: (klass) ->
|
||||
# NullPlaceholder is necessary because if the LEFT JOIN returns nothing, it leaves the field
|
||||
# blank, and it comes through in the result row as "" rather than NULL
|
||||
"IFNULL(`#{@modelTable}`.`value`, '#{NullPlaceholder}') AS `#{@modelKey}`"
|
||||
|
||||
includeSQL: (klass) ->
|
||||
"LEFT OUTER JOIN `#{@modelTable}` ON `#{@modelTable}`.`id` = `#{klass.name}`.`id`"
|
||||
|
||||
|
||||
module.exports = AttributeJoinedData
|
24
src/flux/attributes/attribute-number.coffee
Normal file
24
src/flux/attributes/attribute-number.coffee
Normal file
|
@ -0,0 +1,24 @@
|
|||
_ = require 'underscore-plus'
|
||||
Attribute = require './attribute'
|
||||
Matcher = require './matcher'
|
||||
|
||||
###
|
||||
Public: The value of this attribute is always a number, or null.
|
||||
###
|
||||
class AttributeNumber extends Attribute
|
||||
toJSON: (val) -> val
|
||||
fromJSON: (val) -> unless isNaN(val) then Number(val) else null
|
||||
columnSQL: -> "#{@jsonKey} INTEGER"
|
||||
|
||||
# Public: Returns a {Matcher} for objects greater than the provided value.
|
||||
greaterThan: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '>', val)
|
||||
|
||||
# Public: Returns a {Matcher} for objects less than the provided value.
|
||||
lessThan: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '<', val)
|
||||
|
||||
|
||||
module.exports = AttributeNumber
|
23
src/flux/attributes/attribute-string.coffee
Normal file
23
src/flux/attributes/attribute-string.coffee
Normal file
|
@ -0,0 +1,23 @@
|
|||
Attribute = require './attribute'
|
||||
Matcher = require './matcher'
|
||||
|
||||
###
|
||||
Public: The value of this attribute is always a string or `null`.
|
||||
|
||||
String attributes can be queries using `equal`, `not`, and `startsWith`. Matching on
|
||||
`greaterThan` and `lessThan` is not supported.
|
||||
###
|
||||
class AttributeString extends Attribute
|
||||
toJSON: (val) -> val
|
||||
fromJSON: (val) -> val || ""
|
||||
|
||||
# Public: Returns a {Matcher} for objects starting with the provided value.
|
||||
startsWith: (val) -> new Matcher(@, 'startsWith', val)
|
||||
|
||||
columnSQL: -> "#{@jsonKey} TEXT"
|
||||
|
||||
like: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, 'like', val)
|
||||
|
||||
module.exports = AttributeString
|
39
src/flux/attributes/attribute.coffee
Normal file
39
src/flux/attributes/attribute.coffee
Normal file
|
@ -0,0 +1,39 @@
|
|||
_ = require 'underscore-plus'
|
||||
Matcher = require './matcher'
|
||||
SortOrder = require './sort-order'
|
||||
|
||||
###
|
||||
Public: The Attribute class represents a single model attribute, like 'namespace_id'
|
||||
Subclasses of {Attribute} like {AttributeDateTime} know how to covert between
|
||||
the JSON representation of that type and the javascript representation.
|
||||
The Attribute class also exposes convenience methods for generating {Matcher} objects.
|
||||
###
|
||||
class Attribute
|
||||
constructor: ({modelKey, queryable, jsonKey}) ->
|
||||
@modelKey = modelKey
|
||||
@jsonKey = jsonKey || modelKey
|
||||
@queryable = queryable
|
||||
@
|
||||
|
||||
# Public: Returns a {Matcher} for objects `=` to the provided value.
|
||||
equal: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '=', val)
|
||||
|
||||
# Public: Returns a {Matcher} for objects `!=` to the provided value.
|
||||
not: (val) ->
|
||||
throw (new Error "this field cannot be queried against.") unless @queryable
|
||||
new Matcher(@, '!=', val)
|
||||
|
||||
# Public: Returns a descending {SortOrder} for this attribute.
|
||||
descending: ->
|
||||
new SortOrder(@, 'DESC')
|
||||
|
||||
# Public: Returns an ascending {SortOrder} for this attribute.
|
||||
ascending: ->
|
||||
new SortOrder(@, 'ASC')
|
||||
toJSON: (val) -> val
|
||||
fromJSON: (val) -> val ? null
|
||||
|
||||
|
||||
module.exports = Attribute
|
69
src/flux/attributes/matcher.coffee
Normal file
69
src/flux/attributes/matcher.coffee
Normal file
|
@ -0,0 +1,69 @@
|
|||
_ = require 'underscore-plus'
|
||||
{tableNameForJoin} = require '../models/utils'
|
||||
|
||||
###
|
||||
Public: The Matcher class encapsulates a particular comparison clause on an attribute.
|
||||
Matchers can evaluate whether or not an object matches them, and in the future
|
||||
they will also compose WHERE clauses. Each matcher has a reference to a model
|
||||
attribute, a comparator and a value.
|
||||
###
|
||||
class Matcher
|
||||
constructor: (@attr, @comparator, @val) ->
|
||||
@muid = Matcher.muid
|
||||
Matcher.muid = (Matcher.muid + 1) % 50
|
||||
@
|
||||
|
||||
evaluate: (model) ->
|
||||
value = model[@attr.modelKey]
|
||||
value = value() if value instanceof Function
|
||||
|
||||
switch @comparator
|
||||
when '=' then return value == @val
|
||||
when '<' then return value < @val
|
||||
when '>' then return value > @val
|
||||
when 'contains'
|
||||
# You can provide an ID or an object, and an array of IDs or an array of objects
|
||||
# Assumes that `value` is an array of items
|
||||
!!_.find value, (x) =>
|
||||
@val == x?.id || @val == x || @val?.id == x || @val?.id == x?.id
|
||||
when 'startsWith' then return value.startsWith(@val)
|
||||
when 'like' then value.search(new RegExp(".*#{@val}.*", "gi")) >= 0
|
||||
else
|
||||
throw new Error("Matcher.evaulate() not sure how to evaluate @{@attr.modelKey} with comparator #{@comparator}")
|
||||
|
||||
joinSQL: (klass) ->
|
||||
switch @comparator
|
||||
when 'contains'
|
||||
joinTable = tableNameForJoin(klass, @attr.itemClass)
|
||||
return "INNER JOIN `#{joinTable}` AS `M#{@muid}` ON `M#{@muid}`.`id` = `#{klass.name}`.`id`"
|
||||
else
|
||||
return false
|
||||
|
||||
whereSQL: (klass) ->
|
||||
|
||||
if @comparator is "like"
|
||||
val = "%#{@val}%"
|
||||
else
|
||||
val = @val
|
||||
|
||||
if _.isString(val)
|
||||
escaped = "'#{val.replace(/'/g, '\\\'')}'"
|
||||
else if val is true
|
||||
escaped = 1
|
||||
else if val is false
|
||||
escaped = 0
|
||||
else
|
||||
escaped = val
|
||||
|
||||
switch @comparator
|
||||
when 'startsWith'
|
||||
return " RAISE `TODO`; "
|
||||
when 'contains'
|
||||
return "`M#{@muid}`.`value` = #{escaped}"
|
||||
else
|
||||
return "`#{klass.name}`.`#{@attr.jsonKey}` #{@comparator} #{escaped}"
|
||||
|
||||
|
||||
Matcher.muid = 0
|
||||
|
||||
module.exports = Matcher
|
13
src/flux/attributes/sort-order.coffee
Normal file
13
src/flux/attributes/sort-order.coffee
Normal file
|
@ -0,0 +1,13 @@
|
|||
###
|
||||
Public: Represents a particular sort direction on a particular column. You should not
|
||||
instantiate SortOrders manually. Instead, call `Attribute.ascending()` or
|
||||
`Attribute.descending()` to obtain a sort order.
|
||||
###
|
||||
class SortOrder
|
||||
constructor: (@attr, @direction = 'DESC') ->
|
||||
orderBySQL: (klass) ->
|
||||
"`#{klass.name}`.`#{@attr.jsonKey}` #{@direction}"
|
||||
attribute: ->
|
||||
@attr
|
||||
|
||||
module.exports = SortOrder
|
|
@ -3,11 +3,9 @@ Actions = require '../actions'
|
|||
Attributes = require '../attributes'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
##
|
||||
# Files are small objects that wrap attachments and other files available via the API.
|
||||
#
|
||||
# @namespace Models
|
||||
#
|
||||
###
|
||||
Public: Files are small objects that wrap attachments and other files available via the API.
|
||||
###
|
||||
class File extends Model
|
||||
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
|
@ -34,4 +32,4 @@ class File extends Model
|
|||
jsonKey: 'content_id'
|
||||
|
||||
|
||||
module.exports = File
|
||||
module.exports = File
|
||||
|
|
|
@ -5,14 +5,12 @@ Model = require './model'
|
|||
Contact = require './contact'
|
||||
Attributes = require '../attributes'
|
||||
|
||||
##
|
||||
# Messages are a sub-object of threads. The content of a message is immutable (with the
|
||||
# exception being drafts). Nylas does not support operations such as move or delete on
|
||||
# individual messages; those operations should be performed on the message’s thread.
|
||||
# All messages are part of a thread, even if that thread has only one message.
|
||||
#
|
||||
# @namespace Models
|
||||
#
|
||||
###
|
||||
Public: Messages are a sub-object of threads. The content of a message is immutable (with the
|
||||
exception being drafts). Nylas does not support operations such as move or delete on
|
||||
individual messages; those operations should be performed on the message’s thread.
|
||||
All messages are part of a thread, even if that thread has only one message.
|
||||
###
|
||||
class Message extends Model
|
||||
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
|
|
|
@ -3,12 +3,10 @@ ModelQuery = require './query'
|
|||
{isTempId, generateTempId} = require './utils'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
##
|
||||
# A base class for API objects that provides abstract support for serialization
|
||||
# and deserialization, matching by attributes, and ID-based equality.
|
||||
#
|
||||
# @namespace Models
|
||||
#
|
||||
###
|
||||
Public: A base class for API objects that provides abstract support for
|
||||
serialization and deserialization, matching by attributes, and ID-based equality.
|
||||
###
|
||||
class Model
|
||||
|
||||
@attributes:
|
||||
|
@ -32,38 +30,36 @@ class Model
|
|||
@id ||= generateTempId()
|
||||
@
|
||||
|
||||
##
|
||||
# @return {Array<Attribute>} The set of attributes defined on the Model's constructor
|
||||
# Public: Returns an {Array} of {Attribute} objects defined on the Model's constructor
|
||||
#
|
||||
attributes: ->
|
||||
@constructor.attributes
|
||||
|
||||
##
|
||||
# @return {Boolean} True if the object has a server-provided ID, false otherwise.
|
||||
# Public Returns true if the object has a server-provided ID, false otherwise.
|
||||
#
|
||||
isSaved: ->
|
||||
!isTempId(@id)
|
||||
|
||||
##
|
||||
# Inflates the model object from JSON, using the defined attributes to guide type
|
||||
# coercision.
|
||||
# Public: Inflates the model object from JSON, using the defined attributes to
|
||||
# guide type coercision.
|
||||
#
|
||||
# @param {Object} json
|
||||
# @chainable
|
||||
# - `json` A plain Javascript {Object} with the JSON representation of the model.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
fromJSON: (json) ->
|
||||
for key, attr of @attributes()
|
||||
@[key] = attr.fromJSON(json[attr.jsonKey]) unless json[attr.jsonKey] is undefined
|
||||
@
|
||||
|
||||
##
|
||||
# Deflates the model to a plain JSON object. Only attributes defined on the model are
|
||||
# included in the JSON.
|
||||
# Public: Deflates the model to a plain JSON object. Only attributes defined
|
||||
# on the model are included in the JSON.
|
||||
#
|
||||
# @param {Object} options To include joined data attributes in the toJSON representation,
|
||||
# pass the `joined` option.
|
||||
# - `options` (Optional) An {Object} with additional options. To include joined
|
||||
# data attributes in the toJSON representation, pass the `joined:true`
|
||||
#
|
||||
# @return {Object} JSON object
|
||||
# Returns an {Object} with the JSON representation of the model.
|
||||
#
|
||||
toJSON: (options = {}) ->
|
||||
json = {}
|
||||
|
@ -81,9 +77,10 @@ class Model
|
|||
toString: ->
|
||||
JSON.stringify(@toJSON())
|
||||
|
||||
##
|
||||
# @param {Array<Matcher>} criteria Set of matchers to run on the model.
|
||||
# @return {Boolean} True, if the model matches the criteria.
|
||||
# Public: Evaluates the model against one or more {Matcher} objects.
|
||||
# - `criteria` An {Array} of {Matcher}s to run on the model.
|
||||
#
|
||||
# Returns true if the model matches the criteria.
|
||||
#
|
||||
matches: (criteria) ->
|
||||
return false unless criteria instanceof Array
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
{Matcher, NullPlaceholder, AttributeJoinedData} = require '../attributes'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
# Database: ModelQuery exposes an ActiveRecord-style syntax for building database queries.
|
||||
#
|
||||
###
|
||||
Public: ModelQuery exposes an ActiveRecord-style syntax for building database queries.
|
||||
###
|
||||
class ModelQuery
|
||||
|
||||
##
|
||||
# @param {Model.Constructor} klass The Model class to query
|
||||
# @param {DatabaseStore} An optional reference to a DatabaseStore the query will be executed on.
|
||||
# @constructor
|
||||
# Public
|
||||
# - `class` A {Model} class to query
|
||||
# - `database` (Optional) An optional reference to a {DatabaseStore} the
|
||||
# query will be executed on.
|
||||
#
|
||||
constructor: (@_klass, @_database) ->
|
||||
@_database || = require '../stores/database-store'
|
||||
|
@ -20,9 +21,11 @@ class ModelQuery
|
|||
@_count = false
|
||||
@
|
||||
|
||||
##
|
||||
# @param {Array<Matcher>} matchers One or more Matcher objects that add where clauses to the underlying query.
|
||||
# @chainable
|
||||
# Public: Add one or more where clauses to the query
|
||||
#
|
||||
# - `matchers` An {Array} of {Matcher} objects that add where clauses to the underlying query.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
where: (matchers) ->
|
||||
if matchers instanceof Matcher
|
||||
|
@ -39,11 +42,12 @@ class ModelQuery
|
|||
@_matchers.push(attr.equal(value))
|
||||
@
|
||||
|
||||
##
|
||||
# @param {AttributeJoinedData} attr A joined data attribute that you want to be populated in
|
||||
# the returned models. Note: This results in a LEFT OUTER JOIN. See {AttributeJoinedData}
|
||||
# for more information.
|
||||
# @chainable
|
||||
# Public: Include specific joined data attributes in result objects.
|
||||
# - `attr` A {AttributeJoinedData} that you want to be populated in
|
||||
# the returned models. Note: This results in a LEFT OUTER JOIN.
|
||||
# See {AttributeJoinedData} for more information.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
include: (attr) ->
|
||||
if attr instanceof AttributeJoinedData is false
|
||||
|
@ -52,8 +56,9 @@ class ModelQuery
|
|||
@
|
||||
|
||||
##
|
||||
# Include all of the available joined data attributes in returned models.
|
||||
# @chainable
|
||||
# Public: Include all of the available joined data attributes in returned models.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
includeAll: ->
|
||||
for key, attr of @_klass.attributes
|
||||
|
@ -61,27 +66,32 @@ class ModelQuery
|
|||
@
|
||||
|
||||
##
|
||||
# @param {Array<SortOrder>} orders One or more SortOrder objects that determine the sort order
|
||||
# of returned models.
|
||||
# @chainable
|
||||
# Public: Apply a sort order to the query.
|
||||
# - `orders` An {Array} of one or more {SortOrder} objects that determine the
|
||||
# sort order of returned models.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
order: (orders) ->
|
||||
orders = [orders] unless orders instanceof Array
|
||||
@_orders = @_orders.concat(orders)
|
||||
@
|
||||
|
||||
##
|
||||
# Set the `singular` flag - only one model will be returned from the query, and a `LIMIT 1` clause
|
||||
# will be used.
|
||||
# @chainable
|
||||
#
|
||||
# Public: Set the `singular` flag - only one model will be returned from the
|
||||
# query, and a `LIMIT 1` clause will be used.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
one: ->
|
||||
@_singular = true
|
||||
@
|
||||
|
||||
##
|
||||
# @param {Number} limit The number of models that should be returned.
|
||||
# @chainable
|
||||
# Public: Limit the number of query results.
|
||||
#
|
||||
# - `limit` The number of models that should be returned.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
limit: (limit) ->
|
||||
throw new Error("Cannot use limit > 2 with one()") if @_singular and limit > 1
|
||||
|
@ -89,40 +99,46 @@ class ModelQuery
|
|||
@_range.limit = limit
|
||||
@
|
||||
|
||||
##
|
||||
# @param {Number} offset The start offset of the query.
|
||||
# @chainable
|
||||
# Public:
|
||||
#
|
||||
# - `offset` The start offset of the query.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
offset: (offset) ->
|
||||
@_range ?= {}
|
||||
@_range.offset = offset
|
||||
@
|
||||
|
||||
##
|
||||
# Set the `count` flag - instead of returning inflated models, the query will return the result `COUNT`.
|
||||
# @chainable
|
||||
# Public: Set the `count` flag - instead of returning inflated models,
|
||||
# the query will return the result `COUNT`.
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
count: ->
|
||||
@_count = true
|
||||
@
|
||||
|
||||
##
|
||||
# Set the `evaluateImmediately` flag - instead of waiting for animations and other important user
|
||||
# Public: Set the `evaluateImmediately` flag - instead of waiting for animations and other important user
|
||||
# interactions to complete, the query result will be processed immediately. Use with care: forcing
|
||||
# immediate evaluation can cause glitches in animations.
|
||||
# @chainable
|
||||
#
|
||||
# This method is chainable.
|
||||
#
|
||||
evaluateImmediately: ->
|
||||
@_evaluateImmediately = true
|
||||
@
|
||||
|
||||
# Query Execution
|
||||
###
|
||||
Query Execution
|
||||
###
|
||||
|
||||
##
|
||||
# Starts query execution and returns a Promise.
|
||||
# Public: Starts query execution and returns a Promise.
|
||||
#
|
||||
# @return {Promise} A Promise that resolves with the Models returned by the query, or rejects with
|
||||
# an error from the Database layer.
|
||||
# Returns A {Promise} that resolves with the Models returned by the
|
||||
# query, or rejects with an error from the Database layer.
|
||||
#
|
||||
then: (next) ->
|
||||
@_database.run(@).then(next)
|
||||
|
@ -148,8 +164,7 @@ class ModelQuery
|
|||
|
||||
# Query SQL Building
|
||||
|
||||
##
|
||||
# @return {String} The SQL generated for the query.
|
||||
# Returns a {String} with the SQL generated for the query.
|
||||
#
|
||||
sql: ->
|
||||
if @_count
|
||||
|
|
|
@ -2,6 +2,23 @@ Model = require './model'
|
|||
Attributes = require '../attributes'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
###
|
||||
Public: The Tag model represents a Nylas Tag object. For more information
|
||||
about Tags on the Nylas Platform, read the
|
||||
[https://nylas.com/docs/api#tags](Tags API Documentation)
|
||||
|
||||
## Attributes
|
||||
|
||||
`name`: {AttributeString} The display-friendly name of the tag. Queryable.
|
||||
|
||||
`readonly`: {AttributeBoolean} True if the tag is read-only. See the Nylas
|
||||
API documentation for more information about what tags are read-only.
|
||||
|
||||
`unreadCount`: {AttributeNumber} The number of unread threads with the tag.
|
||||
|
||||
`threadCount`: {AttributeNumber} The number of threads with the tag.
|
||||
|
||||
###
|
||||
class Tag extends Model
|
||||
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
|
|
|
@ -9,72 +9,59 @@ Attributes = require '../attributes'
|
|||
Function::getter = (prop, get) ->
|
||||
Object.defineProperty @prototype, prop, {get, configurable: yes}
|
||||
|
||||
#
|
||||
# Public: Thread
|
||||
#
|
||||
###
|
||||
Public: The Thread model represents a Nylas Thread object. For more information
|
||||
about Threads on the Nylas Platform, read the
|
||||
[https://nylas.com/docs/api#threads](Threads API Documentation)
|
||||
|
||||
## Attributes
|
||||
|
||||
`snippet`: {AttributeString} A short, ~140 character string with the content
|
||||
of the last message in the thread. Queryable.
|
||||
|
||||
`subject`: {AttributeString} The subject of the thread. Queryable.
|
||||
|
||||
`unread`: {AttributeBoolean} True if the thread is unread. Queryable.
|
||||
|
||||
`version`: {AttributeNumber} The version number of the thread. Thread versions increment
|
||||
when tags are changed.
|
||||
|
||||
`tags`: {AttributeCollection} A set of {Tag} models representing
|
||||
the tags on this thread. Queryable using the `contains` matcher.
|
||||
|
||||
`participants`: {AttributeCollection} A set of {Contact} models
|
||||
representing the participants in the thread.
|
||||
Note: Contacts on Threads do not have IDs.
|
||||
|
||||
`lastMessageTimestamp`: {AttributeDateTime} The timestamp of the
|
||||
last message on the thread.
|
||||
|
||||
###
|
||||
class Thread extends Model
|
||||
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
##
|
||||
# A short, ~140 character string with the content of the last message in the thread.
|
||||
# @property snippet
|
||||
# @type AttributeString
|
||||
#
|
||||
'snippet': Attributes.String
|
||||
modelKey: 'snippet'
|
||||
|
||||
##
|
||||
# The subject of the thread
|
||||
# @property subject
|
||||
# @type AttributeString
|
||||
#
|
||||
'subject': Attributes.String
|
||||
modelKey: 'subject'
|
||||
|
||||
##
|
||||
# The unread state of the thread. Queryable.
|
||||
# @property unread
|
||||
# @type AttributeBoolean
|
||||
#
|
||||
'unread': Attributes.Boolean
|
||||
queryable: true
|
||||
modelKey: 'unread'
|
||||
|
||||
##
|
||||
# The version number of the thread. Thread versions increment
|
||||
# when tags are changed.
|
||||
# @property version
|
||||
# @type AttributeNumber
|
||||
#
|
||||
'version': Attributes.Number
|
||||
modelKey: 'version'
|
||||
|
||||
##
|
||||
# A set of Tag models representing the tags on this thread.
|
||||
# Queryable using the `contains` matcher.
|
||||
# @property tags
|
||||
# @type AttributeCollection
|
||||
#
|
||||
'tags': Attributes.Collection
|
||||
queryable: true
|
||||
modelKey: 'tags'
|
||||
itemClass: Tag
|
||||
|
||||
##
|
||||
# A set of Contact models representing the participants in the thread.
|
||||
# Note: Contacts on Threads do not have IDs.
|
||||
# @property participants
|
||||
# @type AttributeCollection
|
||||
#
|
||||
'participants': Attributes.Collection
|
||||
modelKey: 'participants'
|
||||
itemClass: Contact
|
||||
|
||||
##
|
||||
# The timestamp of the last message on the thread.
|
||||
# @property lastMessageTimestamp
|
||||
# @type AttributeDateTime
|
||||
#
|
||||
'lastMessageTimestamp': Attributes.DateTime
|
||||
queryable: true
|
||||
modelKey: 'lastMessageTimestamp'
|
||||
|
@ -85,19 +72,23 @@ class Thread extends Model
|
|||
|
||||
@getter 'unread', -> @isUnread()
|
||||
|
||||
##
|
||||
# The timestamp of the last message on the thread.
|
||||
# @return An array of Tag IDs
|
||||
# Public: Returns an {Array} of {Tag} IDs
|
||||
#
|
||||
tagIds: ->
|
||||
_.map @tags, (tag) -> tag.id
|
||||
|
||||
# Public: Returns true if the thread has a {Tag} with the given ID.
|
||||
#
|
||||
# * `id` A {String} {Tag} ID
|
||||
#
|
||||
hasTagId: (id) ->
|
||||
@tagIds().indexOf(id) != -1
|
||||
|
||||
# Public: Returns a {Boolean}, true if the thread is unread.
|
||||
isUnread: ->
|
||||
@hasTagId('unread')
|
||||
|
||||
# Public: Returns a {Boolean}, true if the thread is starred.
|
||||
isStarred: ->
|
||||
@hasTagId('starred')
|
||||
|
||||
|
@ -120,4 +111,4 @@ class Thread extends Model
|
|||
Actions.queueTask(task)
|
||||
|
||||
|
||||
module.exports = Thread
|
||||
module.exports = Thread
|
||||
|
|
|
@ -95,16 +95,17 @@ class DatabasePromiseTransaction
|
|||
, (err) =>
|
||||
@_resolve()
|
||||
|
||||
# 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.
|
||||
###
|
||||
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.
|
||||
#
|
||||
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
|
||||
@include: CoffeeHelpers.includeModule
|
||||
|
||||
|
@ -424,6 +425,14 @@ class DatabaseStore
|
|||
return reject("Find by local ID lookup failed") unless link
|
||||
query = @find(klass, link.objectId).includeAll().then(resolve)
|
||||
|
||||
# Public: Give a Model a localId.
|
||||
#
|
||||
# - `model` A {Model} object to assign a localId.
|
||||
# - `localId` (Optional) The {String} localId. If you don't pass a LocalId, one
|
||||
# will be automatically assigned.
|
||||
#
|
||||
# Returns a {Promise} that resolves with the localId assigned to the model.
|
||||
#
|
||||
bindToLocalId: (model, localId) ->
|
||||
return Promise.reject(new Error("You must provide a model to bindToLocalId")) unless model
|
||||
|
||||
|
@ -439,6 +448,12 @@ class DatabaseStore
|
|||
resolve(localId)
|
||||
.catch(reject)
|
||||
|
||||
# Public: Look up the localId assigned to the model. If no localId has been
|
||||
# assigned to the model yet, it assigns a new one and persists it to the database.
|
||||
#
|
||||
# - `model` A {Model} object to assign a localId.
|
||||
#
|
||||
# Returns a {Promise} that resolves with the {String} localId.
|
||||
localIdForModel: (model) ->
|
||||
return Promise.reject(new Error("You must provide a model to localIdForModel")) unless model
|
||||
|
||||
|
@ -458,6 +473,12 @@ class DatabaseStore
|
|||
|
||||
# Heavy Lifting
|
||||
|
||||
# Public: Executes a {ModelQuery} on the local database.
|
||||
#
|
||||
# - `modelQuery` A {ModelQuery} to execute.
|
||||
#
|
||||
# Returns a {Promise} that resolves with the result of the database query.
|
||||
#
|
||||
run: (modelQuery) ->
|
||||
@inTransaction {readonly: true}, (tx) ->
|
||||
tx.execute(modelQuery.sql(), [], null, null, modelQuery.executeOptions())
|
||||
|
|
Loading…
Add table
Reference in a new issue