From 47ab0cf302b2651044cd2e4c98c95d3d6fc9e27b Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Fri, 2 Oct 2015 16:10:51 -0700 Subject: [PATCH] Docs styling improvements --- .../_docs.scssc | Bin 30940 -> 0 bytes .../_main.scssc | Bin 101704 -> 0 bytes .../_tomorrow.scssc | Bin 17476 -> 0 bytes _data/sidebar.json | 46 +- _includes/header.html | 2 +- _includes/sidebar_section.html | 22 +- _layouts/docs.html | 2 + _sass/_docs.scss | 85 ++- _sass/_main.scss | 48 +- _sass/_tomorrow.scss | 293 ++++++++--- docs/Actions.html | 10 +- docs/Config.html | 16 +- docs/FirstSteps.html | 432 --------------- docs/InterfaceConcepts.html | 2 +- docs/Menu.html | 6 +- docs/MultiselectActionBar.html | 4 +- docs/README.html | 9 + docs/React.html | 8 +- docs/TaskQueue.html | 6 +- docs/WritingSpecs.html | 5 +- docs/sidebar.json | 493 ------------------ 21 files changed, 365 insertions(+), 1124 deletions(-) delete mode 100644 .sass-cache/1364256e8af83dc96347e8f5ddec35dc4d4254bb/_docs.scssc delete mode 100644 .sass-cache/1364256e8af83dc96347e8f5ddec35dc4d4254bb/_main.scssc delete mode 100644 .sass-cache/1364256e8af83dc96347e8f5ddec35dc4d4254bb/_tomorrow.scssc delete mode 100644 docs/FirstSteps.html create mode 100644 docs/README.html delete mode 100644 docs/sidebar.json diff --git a/.sass-cache/1364256e8af83dc96347e8f5ddec35dc4d4254bb/_docs.scssc b/.sass-cache/1364256e8af83dc96347e8f5ddec35dc4d4254bb/_docs.scssc deleted file mode 100644 index 715514ffbd1b0d7e4fb253ee83225cbe296dd51e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30940 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 diff --git a/.sass-cache/1364256e8af83dc96347e8f5ddec35dc4d4254bb/_main.scssc b/.sass-cache/1364256e8af83dc96347e8f5ddec35dc4d4254bb/_main.scssc deleted file mode 100644 index 504f1f5f3c8ff96b4922677385ba8488edb54a1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 101704 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¤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 diff --git a/_data/sidebar.json b/_data/sidebar.json index fe161773b..24221ab52 100644 --- a/_data/sidebar.json +++ b/_data/sidebar.json @@ -24,20 +24,6 @@ { "name": "Guides", "items": [ - { - "html": "

The N1 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.

\n

\n

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.

\n

The Nylas 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.

\n

For each mode, Sheets register a set of column names.

\n

\n
@defineSheet 'Threads', {root: true},\n   split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']\n   list: ['RootSidebar', 'ThreadList']\n
\n

Column names are important. Once you've registered a sheet, your package (and other packages) register React components that appear in each column.

\n

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.

\n
ComponentRegistry.register AccountSidebar,\n  location: WorkspaceStore.Location.RootSidebar\n\n\nComponentRegistry.register NotificationsStickyBar,\n  location: WorkspaceStore.Sheet.Threads.Header\n
\n

Each column is laid out as a CSS Flexbox, making them extremely flexible. For more about layout using Flexbox, see Working with Flexbox.

\n

Toolbars

\n

Toolbars in N1 are also powered by the {ComponentRegistry}. 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.

\n

\n

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.

\n

\n

To add items to a toolbar, you inject them via the {ComponentRegistry}. There are several ways of describing the location of a toolbar component which are useful in different scenarios:

\n
    \n
  • <Location>.Toolbar: This component will always appear in the toolbar above the column named <Location>.

    \n

    (Example: Compose button which appears above the Left Sidebar column, regardless of what else is there.)

    \n
  • \n
  • <ComponentName>.Toolbar: This component will appear in the toolbar above <ComponentName>.

    \n

    (Example: Archive button that should always be coupled with the MessageList component, placed anywhere a MessageList component is placed.)

    \n
  • \n
  • Global.Toolbar.Left: This component will always be added to the leftmost column of the toolbar.

    \n

    (Example: Window Controls)

    \n
  • \n
\n", - "meta": { - "Title": "Interface Concepts", - "Section": "Guides", - "Order": 1, - "title": "Interface Concepts", - "section": "Guides", - "order": 1 - }, - "name": "Interface Concepts", - "filename": "InterfaceConcepts.md", - "link": "InterfaceConcepts.html" - }, { "html": "

Packages lie at the heart of N1. Each part of the core experience is a separate package that uses the Nylas 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.

\n

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.

\n

Package Structure

\n

Each package is defined by a package.json file that includes its name, version and dependencies. Packages may also declare dependencies which are loaded from npm - in this case, the request library. You'll need to npm install these dependencies locally when developing the package.

\n
{\n  \"name\": \"translate\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"An example package for N1\",\n  \"license\": \"Proprietary\",\n  \"engines\": {\n    \"atom\": \"*\"\n  },\n  \"dependencies\": {\n    \"request\": \"^2.53\"\n  }\n}\n

Our package also contains source files, a spec file with complete tests for the behavior the package adds, and a stylesheet for CSS:

\n
- package.json\n- lib/\n   - main.coffee\n   - translate-button.cjsx\n- spec/\n   - main-spec.coffee\n- stylesheets/\n   - translate.less\n

package.json lists lib/main as the root file of our package. Since N1 runs NodeJS, we can require other source files, Node packages, etc.

\n

N1 can read js, coffee, jsx, and cjsx files automatically.

\n

Inside main.coffee, there are two important functions being exported:

\n
require './translate-button'\n\nmodule.exports =\n\n  # Activate is called when the package is loaded. If your package previously\n  # saved state using `serialize` it is provided.\n  #\n  activate: (@state) ->\n    ComponentRegistry.register TranslateButton,\n      role: 'Composer:ActionButton'\n\n  # Serialize is called when your package is about to be unmounted.\n  # You can return a state object that will be passed back to your package\n  # when it is re-activated.\n  #\n  serialize: ->\n      {}\n\n  # This optional method is called when the window is shutting down,\n  # or when your package is being updated or disabled. If your package is\n  # watching any files, holding external resources, providing commands or\n  # subscribing to events, release them here.\n  #\n  deactivate: ->\n    ComponentRegistry.unregister(TranslateButton)\n
\n
\n

N1 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 plugin to Sublime Text, or the CJSX Language for syntax highlighting.

\n
\n

Package Stylesheets

\n

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.

\n

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 N1 seamlessly.

\n

If you do need special styling, try to keep only structural styles in the package stylesheets. 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!

\n

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.

\n

Package Assets

\n

Many packages need other static files, like images. You can add static files anywhere in your package directory, and reference them at runtime using the nylas:// url scheme:

\n
<img src=\"nylas://my-package-name/assets/goofy.png\">\n\na = new Audio()\na.src = \"nylas://my-package-name/sounds/bloop.mp3\"\na.play()\n

Installing a Package

\n

N1 ships with many packages already bundled with the application. When the application launches, it looks for additional packages in ~/.nylas/dev/packages. Each package you create belongs in its own directory inside this folder.

\n

In the future, it will be possible to install packages directly from within the client.

\n", "meta": { @@ -53,7 +39,21 @@ "link": "PackageOverview.html" }, { - "html": "

N1 uses React to create a fast, responsive UI. Packages that want to extend the N1 interface should use React. Using React's JSX syntax is optional, but both JSX and CJSX (CoffeeScript) are available.

\n

For a quick introduction to React, take a look at Facebook's Getting Started with React.

\n

React Components

\n

N1 provides a set of core React components you can use in your packages. Many of the standard components listen for key events, include considerations for different platforms, and have extensive CSS. Wrapping standard components makes it easy to build rich interfaces that are consistent with the rest of the N1 platform.

\n

To use a standard component, require it from nylas-component-kit and use it in your component's render method.

\n

Keep in mind that React's Component model is based on composition rather than inheritance. On other platforms, you might subclass {Popover} to create your own custom Popover. In React, you should wrap the standard Popover component in your own component, which provides the Popover with props and children to customize its behavior.

\n\n\n

Here's a quick look at standard components you can require from nylas-component-kit:

\n
    \n
  • {Menu}: Allows you to display a list of items consistent with the rest of the N1 user experience.

    \n
  • \n
  • {Spinner}: Displays an indeterminate progress indicator centered within it's container.

    \n
  • \n
  • {Popover}: Component for creating menus and popovers that appear in response to a click and stay open until the user clicks outside them.

    \n
  • \n
  • {Flexbox}: Component for creating a Flexbox layout.

    \n
  • \n
  • {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.

    \n
  • \n
  • {ListTabular}: Component for creating a list of items backed by a paginating ModelView.

    \n
  • \n
  • {MultiselectList}: Component for creating a list that supports multi-selection. (Internally wraps ListTabular)

    \n
  • \n
  • {MultiselectActionBar}: Component for creating a contextual toolbar that is activated when the user makes a selection on a ModelView.

    \n
  • \n
  • {ResizableRegion}: Component that renders it's children inside a resizable region with a draggable handle.

    \n
  • \n
  • {TokenizingTextField}: Wraps a standard <input> and takes function props for tokenizing input values and displaying autocompletion suggestions.

    \n
  • \n
  • {EventedIFrame}: Replacement for the standard <iframe> tag which handles events directed at the iFrame to ensure a consistent user experience.

    \n
  • \n
\n

React Component Injection

\n

The N1 interface is composed at runtime from components added by different packages. The app's left sidebar contains components from the composer package, the source list package, the activity package, and more. You can leverage the flexiblity of this system to extend almost any part of N1's interface.

\n

Registering Components

\n

After you've created React components in your package, you should register them with the {ComponentRegistry}. The Component Registry manages the dynamic injection of components that makes N1 so extensible. You can request that your components appear in a specific Location defined by the {WorkspaceStore}, or register your component for a Role that another package has declared.

\n
\n

The Component Registry allows you to insert your custom component without hacking up the DOM. Register for a Location or Role and your Component will be rendered into that part of the interface.

\n
\n

It's easy to see where registered components are displayed in N1. Enable the Developer bar at the bottom of the app by opening the Inspector panel, and then click "Component Regions":

\n

\n

Each region outlined in red is filled dynamically by looking up a React component or set of components from the Component Registry. You can see the role or location you'd need to register for, and the props that your component will receive in those locations.

\n

Here are a few examples of how to use it to extend N1. Typically, packages register components in their main activate method, and unregister them in deactivate:

\n
    \n
  1. Add a component to the Thread List column:

    \n
         ComponentRegistry.register ThreadList,\n       location: WorkspaceStore.Location.ThreadList\n
    \n
  2. \n
  3. Add a component to the action bar at the bottom of the Composer:

    \n
         ComponentRegistry.register TemplatePicker,\n       role: 'Composer:ActionButton'\n
    \n
  4. \n
  5. Replace the Participants component that ships with N1 to display thread participants on your own:

    \n
         ComponentRegistry.register ParticipantsWithStatusDots,\n         role: 'Participants'\n
    \n
  6. \n
\n

Tip: Remember to unregister components in the deactivate method of your package.

\n

Using Registered Components

\n

It's easy to build packages that use the Component Registry to display components vended by other parts of the application. You can query the Component Registry and display the components it returns. The Component Registry is a Reflux-compatible Store, so you can listen to it and update your state as the registry changes.

\n

There are also several convenience components that make it easy to dynamically inject components into your Virtual DOM. These are the preferred way of using injected components.

\n
    \n
  • {InjectedComponent}: Renders the first component for the matching criteria you provide, and passes it the props in externalProps. See the API reference for more information.
  • \n
\n
<InjectedComponent\n    matching={role:\"Attachment\"}\n    exposedProps={file: file, messageLocalId: @props.localId}/>\n
\n
    \n
  • {InjectedComponentSet}: Renders all of the components matching criteria you provide inside a {Flexbox}, and passes it the props in externalProps. See the API reference for more information.
  • \n
\n
<InjectedComponentSet\n    className=\"message-actions\"\n    matching={role:\"MessageAction\"}\n    exposedProps={thread:@props.thread, message: @props.message}>\n
\n

Unsafe Components

\n

N1 considers all injected components "unsafe". When you render them using {InjectedComponent} or {InjectedComponentSet}, they will be wrapped in a component that prevents exceptions in their React render and lifecycle methods from impacting your component. Instead of your component triggering a React Invariant exception in the application, an exception notice will be rendered in place of the unsafe component.

\n

\n

In the future, N1 may automatically disable packages when their React components throw exceptions.

\n", + "html": "

The N1 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.

\n

\n

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.

\n

The Nylas 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.

\n

For each mode, Sheets register a set of column names.

\n

\n
@defineSheet 'Threads', {root: true},\n   split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']\n   list: ['RootSidebar', 'ThreadList']\n
\n

Column names are important. Once you've registered a sheet, your package (and other packages) register React components that appear in each column.

\n

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.

\n
ComponentRegistry.register AccountSidebar,\n  location: WorkspaceStore.Location.RootSidebar\n\n\nComponentRegistry.register NotificationsStickyBar,\n  location: WorkspaceStore.Sheet.Threads.Header\n
\n

Each column is laid out as a CSS Flexbox, making them extremely flexible. For more about layout using Flexbox, see Working with Flexbox.

\n

###Toolbars

\n

Toolbars in N1 are also powered by the {ComponentRegistry}. 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.

\n

\n

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.

\n

\n

To add items to a toolbar, you inject them via the {ComponentRegistry}. There are several ways of describing the location of a toolbar component which are useful in different scenarios:

\n
    \n
  • <Location>.Toolbar: This component will always appear in the toolbar above the column named <Location>.

    \n

    (Example: Compose button which appears above the Left Sidebar column, regardless of what else is there.)

    \n
  • \n
  • <ComponentName>.Toolbar: This component will appear in the toolbar above <ComponentName>.

    \n

    (Example: Archive button that should always be coupled with the MessageList component, placed anywhere a MessageList component is placed.)

    \n
  • \n
  • Global.Toolbar.Left: This component will always be added to the leftmost column of the toolbar.

    \n

    (Example: Window Controls)

    \n
  • \n
\n", + "meta": { + "Title": "Interface Concepts", + "Section": "Guides", + "Order": 1, + "title": "Interface Concepts", + "section": "Guides", + "order": 1 + }, + "name": "Interface Concepts", + "filename": "InterfaceConcepts.md", + "link": "InterfaceConcepts.html" + }, + { + "html": "

N1 uses React to create a fast, responsive UI. Packages that want to extend the N1 interface should use React. Using React's JSX syntax is optional, but both JSX and CJSX (CoffeeScript) are available.

\n

For a quick introduction to React, take a look at Facebook's Getting Started with React.

\n

React Components

\n

N1 provides a set of core React components you can use in your packages. Many of the standard components listen for key events, include considerations for different platforms, and have extensive CSS. Wrapping standard components makes it easy to build rich interfaces that are consistent with the rest of the N1 platform.

\n

To use a standard component, require it from nylas-component-kit and use it in your component's render method.

\n

Keep in mind that React's Component model is based on composition rather than inheritance. On other platforms, you might subclass {Popover} to create your own custom Popover. In React, you should wrap the standard Popover component in your own component, which provides the Popover with props and children to customize its behavior.

\n\n\n

Here's a quick look at standard components you can require from nylas-component-kit:

\n
    \n
  • {Menu}: Allows you to display a list of items consistent with the rest of the N1 user experience.

    \n
  • \n
  • {Spinner}: Displays an indeterminate progress indicator centered within it's container.

    \n
  • \n
  • {Popover}: Component for creating menus and popovers that appear in response to a click and stay open until the user clicks outside them.

    \n
  • \n
  • {Flexbox}: Component for creating a Flexbox layout.

    \n
  • \n
  • {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.

    \n
  • \n
  • {ListTabular}: Component for creating a list of items backed by a paginating ModelView.

    \n
  • \n
  • {MultiselectList}: Component for creating a list that supports multi-selection. (Internally wraps ListTabular)

    \n
  • \n
  • {MultiselectActionBar}: Component for creating a contextual toolbar that is activated when the user makes a selection on a ModelView.

    \n
  • \n
  • {ResizableRegion}: Component that renders it's children inside a resizable region with a draggable handle.

    \n
  • \n
  • {TokenizingTextField}: Wraps a standard <input> and takes function props for tokenizing input values and displaying autocompletion suggestions.

    \n
  • \n
  • {EventedIFrame}: Replacement for the standard <iframe> tag which handles events directed at the iFrame to ensure a consistent user experience.

    \n
  • \n
\n

React Component Injection

\n

The N1 interface is composed at runtime from components added by different packages. The app's left sidebar contains components from the composer package, the source list package, the activity package, and more. You can leverage the flexiblity of this system to extend almost any part of N1's interface.

\n

Registering Components

\n

After you've created React components in your package, you should register them with the {ComponentRegistry}. The Component Registry manages the dynamic injection of components that makes N1 so extensible. You can request that your components appear in a specific Location defined by the {WorkspaceStore}, or register your component for a Role that another package has declared.

\n
\n

The Component Registry allows you to insert your custom component without hacking up the DOM. Register for a Location or Role and your Component will be rendered into that part of the interface.

\n
\n

It's easy to see where registered components are displayed in N1. Enable the Developer bar at the bottom of the app by opening the Inspector panel, and then click "Component Regions":

\n

\n

Each region outlined in red is filled dynamically by looking up a React component or set of components from the Component Registry. You can see the role or location you'd need to register for, and the props that your component will receive in those locations.

\n

Here are a few examples of how to use it to extend N1. Typically, packages register components in their main activate method, and unregister them in deactivate:

\n
    \n
  1. Add a component to the Thread List column:

    \n
         ComponentRegistry.register ThreadList,\n       location: WorkspaceStore.Location.ThreadList\n
    \n
  2. \n
  3. Add a component to the action bar at the bottom of the Composer:

    \n
         ComponentRegistry.register TemplatePicker,\n       role: 'Composer:ActionButton'\n
    \n
  4. \n
  5. Replace the Participants component that ships with N1 to display thread participants on your own:

    \n
         ComponentRegistry.register ParticipantsWithStatusDots,\n         role: 'Participants'\n
    \n
  6. \n
\n

Tip: Remember to unregister components in the deactivate method of your package.

\n

Using Registered Components

\n

It's easy to build packages that use the Component Registry to display components vended by other parts of the application. You can query the Component Registry and display the components it returns. The Component Registry is a Reflux-compatible Store, so you can listen to it and update your state as the registry changes.

\n

There are also several convenience components that make it easy to dynamically inject components into your Virtual DOM. These are the preferred way of using injected components.

\n
    \n
  • {InjectedComponent}: Renders the first component for the matching criteria you provide, and passes it the props in externalProps. See the API reference for more information.
  • \n
\n
<InjectedComponent\n    matching={role:\"Attachment\"}\n    exposedProps={file: file, messageLocalId: @props.localId}/>\n
\n
    \n
  • {InjectedComponentSet}: Renders all of the components matching criteria you provide inside a {Flexbox}, and passes it the props in externalProps. See the API reference for more information.
  • \n
\n
<InjectedComponentSet\n    className=\"message-actions\"\n    matching={role:\"MessageAction\"}\n    exposedProps={thread:@props.thread, message: @props.message}>\n
\n

Unsafe Components

\n

N1 considers all injected components "unsafe". When you render them using {InjectedComponent} or {InjectedComponentSet}, they will be wrapped in a component that prevents exceptions in their React render and lifecycle methods from impacting your component. Instead of your component triggering a React Invariant exception in the application, an exception notice will be rendered in place of the unsafe component.

\n

\n

In the future, N1 may automatically disable packages when their React components throw exceptions.

\n", "meta": { "Title": "Interface Components", "Section": "Guides", @@ -123,7 +123,7 @@ "link": "DraftStoreExtensions.html" }, { - "html": "

Writing specs

\n

Nylas uses Jasmine as its spec framework. As a package developer, you can write specs using Jasmine 1.3 and get some quick wins. Jasmine specs can be run in N1 directly from the Developer menu, and the test environment provides you with helpful stubs. You can also require your own test framework, or use Jasmine for integration tests and your own framework for your existing business logic.

\n

This documentation describes using Jasmine 1.3 to write specs for a Nylas package.

\n

Running Specs

\n

You can run your package specs from Developer > Run Package Specs.... Once you've opened the spec window, you can see output and re-run your specs by clicking Reload Specs.

\n

Writing Specs

\n

To create specs, place js, coffee, or cjsx files in the spec directory of your package. Spec files must end with the -spec suffix.

\n

Here's an annotated look at a typical Jasmine spec:

\n
# The `describe` method takes two arguments, a description and a function. If the description\n# explains a behavior it typically begins with `when`; if it is more like a unit test it begins\n# with the method name.\ndescribe \"when a test is written\", ->\n\n  # The `it` method also takes two arguments, a description and a function. Try and make the\n  # description flow with the `it` method. For example, a description of `this should work`\n  # doesn't read well as `it this should work`. But a description of `should work` sounds\n  # great as `it should work`.\n  it \"has some expectations that should pass\", ->\n\n    # The best way to learn about expectations is to read the Jasmine documentation:\n    # http://jasmine.github.io/1.3/introduction.html#section-Expectations\n    # Below is a simple example.\n\n    expect(\"apples\").toEqual(\"apples\")\n    expect(\"oranges\").not.toEqual(\"apples\")\n\ndescribe \"Editor::moveUp\", ->\n    ...\n
\n

Asynchronous Spcs

\n

Writing Asynchronous specs can be tricky at first, but a combination of spec helpers can make things easy. Here are a few quick examples:

\n
Promises
\n

You can use the global waitsForPromise function to make sure that the test does not complete until the returned promise has finished, and run your expectations in a chained promise.

\n
  describe \"when requesting a Draft Session\", ->\n    it \"a session with the correct ID is returned\", ->\n      waitsForPromise ->\n        DraftStore.sessionForLocalId('123').then (session) ->\n          expect(session.id).toBe('123')\n
\n

This method can be used in the describe, it, beforeEach and afterEach functions.

\n
describe \"when we open a file\", ->\n  beforeEach ->\n    waitsForPromise ->\n      atom.workspace.open 'c.coffee'\n\n  it \"should be opened in an editor\", ->\n    expect(atom.workspace.getActiveTextEditor().getPath()).toContain 'c.coffee'\n
\n

If you need to wait for multiple promises use a new waitsForPromise function for each promise. (Caution: Without beforeEach this example will fail!)

\n
describe \"waiting for the packages to load\", ->\n\n  beforeEach ->\n    waitsForPromise ->\n      atom.workspace.open('sample.js')\n    waitsForPromise ->\n      atom.packages.activatePackage('tabs')\n    waitsForPromise ->\n      atom.packages.activatePackage('tree-view')\n\n  it 'should have waited long enough', ->\n    expect(atom.packages.isPackageActive('tabs')).toBe true\n    expect(atom.packages.isPackageActive('tree-view')).toBe true\n
\n

Asynchronous functions with callbacks

\n

Specs for asynchronous functions can be done using the waitsFor and runs functions. A simple example.

\n
describe \"fs.readdir(path, cb)\", ->\n  it \"is async\", ->\n    spy = jasmine.createSpy('fs.readdirSpy')\n\n    fs.readdir('/tmp/example', spy)\n    waitsFor ->\n      spy.callCount > 0\n    runs ->\n      exp = [null, ['example.coffee']]\n      expect(spy.mostRecentCall.args).toEqual exp\n      expect(spy).toHaveBeenCalledWith(null, ['example.coffee'])\n
\n

For a more detailed documentation on asynchronous tests please visit the http://jasmine.github.io/1.3/introduction.html#section-Asynchronous_Support)[Jasmine documentation].

\n

Tips for Debugging Specs

\n

To run a limited subset of specs use the fdescribe or fit methods. You can use those to focus a single spec or several specs. In the example above, focusing an individual spec looks like this:

\n
describe \"when a test is written\", ->\n  fit \"has some expectations that should pass\", ->\n    expect(\"apples\").toEqual(\"apples\")\n    expect(\"oranges\").not.toEqual(\"apples\")\n
\n", + "html": "

Nylas uses Jasmine as its spec framework. As a package developer, you can write specs using Jasmine 1.3 and get some quick wins. Jasmine specs can be run in N1 directly from the Developer menu, and the test environment provides you with helpful stubs. You can also require your own test framework, or use Jasmine for integration tests and your own framework for your existing business logic.

\n

This documentation describes using Jasmine 1.3 to write specs for a Nylas package.

\n

Running Specs

\n

You can run your package specs from Developer > Run Package Specs.... Once you've opened the spec window, you can see output and re-run your specs by clicking Reload Specs.

\n

Writing Specs

\n

To create specs, place js, coffee, or cjsx files in the spec directory of your package. Spec files must end with the -spec suffix.

\n

Here's an annotated look at a typical Jasmine spec:

\n
# The `describe` method takes two arguments, a description and a function. If the description\n# explains a behavior it typically begins with `when`; if it is more like a unit test it begins\n# with the method name.\ndescribe \"when a test is written\", ->\n\n  # The `it` method also takes two arguments, a description and a function. Try and make the\n  # description flow with the `it` method. For example, a description of `this should work`\n  # doesn't read well as `it this should work`. But a description of `should work` sounds\n  # great as `it should work`.\n  it \"has some expectations that should pass\", ->\n\n    # The best way to learn about expectations is to read the Jasmine documentation:\n    # http://jasmine.github.io/1.3/introduction.html#section-Expectations\n    # Below is a simple example.\n\n    expect(\"apples\").toEqual(\"apples\")\n    expect(\"oranges\").not.toEqual(\"apples\")\n\ndescribe \"Editor::moveUp\", ->\n    ...\n
\n

Asynchronous Spcs

\n

Writing Asynchronous specs can be tricky at first, but a combination of spec helpers can make things easy. Here are a few quick examples:

\n
Promises
\n

You can use the global waitsForPromise function to make sure that the test does not complete until the returned promise has finished, and run your expectations in a chained promise.

\n
  describe \"when requesting a Draft Session\", ->\n    it \"a session with the correct ID is returned\", ->\n      waitsForPromise ->\n        DraftStore.sessionForLocalId('123').then (session) ->\n          expect(session.id).toBe('123')\n
\n

This method can be used in the describe, it, beforeEach and afterEach functions.

\n
describe \"when we open a file\", ->\n  beforeEach ->\n    waitsForPromise ->\n      atom.workspace.open 'c.coffee'\n\n  it \"should be opened in an editor\", ->\n    expect(atom.workspace.getActiveTextEditor().getPath()).toContain 'c.coffee'\n
\n

If you need to wait for multiple promises use a new waitsForPromise function for each promise. (Caution: Without beforeEach this example will fail!)

\n
describe \"waiting for the packages to load\", ->\n\n  beforeEach ->\n    waitsForPromise ->\n      atom.workspace.open('sample.js')\n    waitsForPromise ->\n      atom.packages.activatePackage('tabs')\n    waitsForPromise ->\n      atom.packages.activatePackage('tree-view')\n\n  it 'should have waited long enough', ->\n    expect(atom.packages.isPackageActive('tabs')).toBe true\n    expect(atom.packages.isPackageActive('tree-view')).toBe true\n
\n

Asynchronous functions with callbacks

\n

Specs for asynchronous functions can be done using the waitsFor and runs functions. A simple example.

\n
describe \"fs.readdir(path, cb)\", ->\n  it \"is async\", ->\n    spy = jasmine.createSpy('fs.readdirSpy')\n\n    fs.readdir('/tmp/example', spy)\n    waitsFor ->\n      spy.callCount > 0\n    runs ->\n      exp = [null, ['example.coffee']]\n      expect(spy.mostRecentCall.args).toEqual exp\n      expect(spy).toHaveBeenCalledWith(null, ['example.coffee'])\n
\n

For a more detailed documentation on asynchronous tests please visit the http://jasmine.github.io/1.3/introduction.html#section-Asynchronous_Support)[Jasmine documentation].

\n

Tips for Debugging Specs

\n

To run a limited subset of specs use the fdescribe or fit methods. You can use those to focus a single spec or several specs. In the example above, focusing an individual spec looks like this:

\n
describe \"when a test is written\", ->\n  fit \"has some expectations that should pass\", ->\n    expect(\"apples\").toEqual(\"apples\")\n    expect(\"oranges\").not.toEqual(\"apples\")\n
\n", "meta": { "Title": "Writing Specs", "TitleHidden": true, @@ -156,18 +156,8 @@ }, { "name": "Sample Code", - "items": [ - { - "name": "Composer Translation", - "link": "https://github.com/nylas/edgehill-plugins/tree/master/translate", - "external": true - }, - { - "name": "Github Sidebar", - "link": "https://github.com/nylas/edgehill-plugins/tree/master/sidebar-github-profile", - "external": true - } - ] + "link": "https://nylas.github.io/N1/examples", + "external": true }, { "name": "API Reference", diff --git a/_includes/header.html b/_includes/header.html index 4bce44056..1b819d5d1 100644 --- a/_includes/header.html +++ b/_includes/header.html @@ -21,7 +21,7 @@
  • Support
  • diff --git a/_includes/sidebar_section.html b/_includes/sidebar_section.html index f37694306..675334ffe 100644 --- a/_includes/sidebar_section.html +++ b/_includes/sidebar_section.html @@ -1,10 +1,12 @@ -
    {{include.section.name}}
    -
      - {% for item in include.section.items %} - {% if item.items %} - {% include sidebar_section.html section=item %} - {% else %} - - {% endif %} - {% endfor %} -
    +
    + {{include.section.name}} +
      + {% for item in include.section.items %} + {% if item.items %} + {% include sidebar_section.html section=item %} + {% else %} + + {% endif %} + {% endfor %} +
    +
    diff --git a/_layouts/docs.html b/_layouts/docs.html index ff5b02cef..f62eb5d79 100644 --- a/_layouts/docs.html +++ b/_layouts/docs.html @@ -4,6 +4,8 @@ {% include header.html %}
    +

    {{ page.title }}

    + diff --git a/_sass/_docs.scss b/_sass/_docs.scss index b7247815f..ace9cb2a3 100644 --- a/_sass/_docs.scss +++ b/_sass/_docs.scss @@ -1,5 +1,6 @@ .container-docs { - color: #737373; + color: #545454; + font-size:1em; h1, h2, @@ -17,6 +18,8 @@ } p { margin-bottom: 12px; + font-size:1em; + font-weight: 300; } h1, h2, @@ -26,6 +29,9 @@ h6 { color: #404040; line-height: 36px; + font-weight: 500; + opacity: 1; + margin-bottom:15px; } h1 { font-size: 40px; @@ -63,6 +69,13 @@ margin-top:0; } + .markdown-from-sourecode { + h4, h3, h2, h1 { + font-size: 18px; + margin-top:20px; + margin-bottom:5px; + } + } blockquote { padding: 13px 13px 21px 15px; margin-bottom: 18px; @@ -78,7 +91,7 @@ font-style: italic; } blockquote code { - background-color:white; + background-color:#f2f2f2; } img { max-width:100%; @@ -87,8 +100,8 @@ font-family: Monaco, Andale Mono, Courier New, monospace; } code { - color: rgba(0, 0, 0, 0.75); - background-color: #EAF7F6; + color: rgba(0,0,0,0.76); + background-color: #f2f2f2; padding: 1px 3px; font-size: 14px; -webkit-border-radius: 3px; @@ -105,7 +118,7 @@ } pre code { font-size: 13px; - background-color: white; + background-color: #f2f2f2; padding: 0; display: block; padding: 14px; @@ -118,6 +131,66 @@ * { -webkit-print-color-adjust: exact; } + a { + font-weight: 400; + } + + #sidebar { + float:left; + position:static; + width:250px; + + .section { + padding-bottom:15px; + + &.collapsed { + ul { + height: 0; + overflow: hidden; + opacity: 0; + } + } + + a.heading { + color:#404040; + line-height:28px; + font-size:16px; + font-weight:bold; + } + + ul { + margin:0; + padding-left:15px; + height: 100%; + transition: all 200ms ease-out; + + li { + list-style-type:none; + a { + color:#404040; + font-size:14px; + line-height:1.8em; + } + } + li.active a { + color:black; + font-size:14px; + font-weight: bold; + line-height:1.8em; + } + .heading { + line-height:22px; + font-size:14px; + padding-top:6px; + } + } + } + } + + #doc-title { + margin-top:30px; + margin-bottom:30px; + } .link { width:18px; @@ -125,7 +198,7 @@ display:inline-block; vertical-align:sub; opacity:0.4; - background:url(/images/link.png) top left; + background:url(../docs/images/link.png) top left; background-size:cover; } diff --git a/_sass/_main.scss b/_sass/_main.scss index b9df9bb98..404183d33 100644 --- a/_sass/_main.scss +++ b/_sass/_main.scss @@ -107,43 +107,6 @@ a img { margin-left:290px; } -#sidebar { - float:left; - position:static; - width:250px; -} - -#sidebar .heading { - color:#404040; - line-height:28px; - font-size:16px; - font-weight:bold; -} -#sidebar ul .heading { - line-height:22px; - font-size:14px; - padding-top:6px; -} - -#sidebar ul { - margin-top:0; - padding-left:15px; -} -#sidebar ul li { - list-style-type:none; -} -#sidebar ul li a { - color:#404040; - font-size:14px; - line-height:1.8em; -} -#sidebar ul li.active a { - color:black; - font-size:14px; - font-weight: bold; - line-height:1.8em; -} - .page-title { font-weight:200; font-size:40px; @@ -340,9 +303,10 @@ p { li { list-style-type: none; display: inline-block; - padding-left: 15px; - padding-right: 15px; color: rgba(0,0,0,0.7); + a { + padding: 15px; + } } li:hover { color: rgba(0,0,0,1); @@ -354,7 +318,8 @@ p { } } -.btn { +.btn, +.nav li .btn { -webkit-user-select:none; padding: 0.33em 1em; border-radius: 4px; @@ -379,7 +344,8 @@ p { font-size: 20px; } -.btn.btn-emphasis { +.btn.btn-emphasis, +.nav li .btn.btn-emphasis { position: relative; color: white; background: linear-gradient(to bottom, #6bb1f9 0%, #0a80ff 100%); diff --git a/_sass/_tomorrow.scss b/_sass/_tomorrow.scss index fb2e161a9..eb66fd1f1 100644 --- a/_sass/_tomorrow.scss +++ b/_sass/_tomorrow.scss @@ -1,93 +1,218 @@ -/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ - -/* Tomorrow Comment */ -.hljs-comment { - color: #8e908c; -} - -/* Tomorrow Red */ -.hljs-variable, -.hljs-attribute, -.hljs-tag, -.hljs-regexp, -.ruby .hljs-constant, -.xml .hljs-tag .hljs-title, -.xml .hljs-pi, -.xml .hljs-doctype, -.html .hljs-doctype, -.css .hljs-id, -.css .hljs-class, -.css .hljs-pseudo { - color: #c82829; -} - -/* Tomorrow Orange */ -.hljs-number, -.hljs-preprocessor, -.hljs-pragma, -.hljs-built_in, -.hljs-literal, -.hljs-params, -.hljs-constant { - color: #f5871f; -} - -/* Tomorrow Yellow */ -.ruby .hljs-class .hljs-title, -.css .hljs-rule .hljs-attribute { - color: #eab700; -} - -/* Tomorrow Green */ -.hljs-string, -.hljs-value, -.hljs-inheritance, -.hljs-header, -.hljs-name, -.ruby .hljs-symbol, -.xml .hljs-cdata { - color: #718c00; -} - -/* Tomorrow Aqua */ -.hljs-title, -.css .hljs-hexcolor { - color: #3e999f; -} - -/* Tomorrow Blue */ -.hljs-function, -.python .hljs-decorator, -.python .hljs-title, -.ruby .hljs-function .hljs-title, -.ruby .hljs-title .hljs-keyword, -.perl .hljs-sub, -.javascript .hljs-title, -.coffeescript .hljs-title { - color: #4271ae; -} - -/* Tomorrow Purple */ -.hljs-keyword, -.javascript .hljs-function { - color: #8959a8; -} +/** + * GitHub Gist Theme + * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro + */ .hljs { display: block; - overflow-x: auto; background: white; - color: #4d4d4c; padding: 0.5em; + color: #333333; + overflow-x: auto; -webkit-text-size-adjust: none; } -.coffeescript .javascript, -.javascript .xml, -.tex .hljs-formula, -.xml .javascript, -.xml .vbscript, -.xml .css, -.xml .hljs-cdata { - opacity: 0.5; +.hljs-comment, +.bash .hljs-shebang, +.java .hljs-javadoc, +.javascript .hljs-javadoc, +.rust .hljs-preprocessor { + color: #969896; +} + +.hljs-string, +.apache .hljs-sqbracket, +.coffeescript .hljs-subst, +.coffeescript .hljs-regexp, +.cpp .hljs-preprocessor, +.c .hljs-preprocessor, +.javascript .hljs-regexp, +.json .hljs-attribute, +.makefile .hljs-variable, +.markdown .hljs-value, +.markdown .hljs-link_label, +.markdown .hljs-strong, +.markdown .hljs-emphasis, +.markdown .hljs-blockquote, +.nginx .hljs-regexp, +.nginx .hljs-number, +.objectivec .hljs-preprocessor .hljs-title, +.perl .hljs-regexp, +.php .hljs-regexp, +.xml .hljs-value, +.less .hljs-built_in, +.scss .hljs-built_in { + color: #df5000; +} + +.hljs-keyword, +.css .hljs-at_rule, +.css .hljs-important, +.http .hljs-request, +.ini .hljs-setting, +.haskell .hljs-type, +.java .hljs-javadoctag, +.javascript .hljs-tag, +.javascript .hljs-javadoctag, +.nginx .hljs-title, +.objectivec .hljs-preprocessor, +.php .hljs-phpdoc, +.sql .hljs-built_in, +.less .hljs-tag, +.less .hljs-at_rule, +.scss .hljs-tag, +.scss .hljs-at_rule, +.scss .hljs-important, +.stylus .hljs-at_rule, +.go .hljs-typename, +.swift .hljs-preprocessor { + color: #a71d5d; +} + +.apache .hljs-common, +.apache .hljs-cbracket, +.apache .hljs-keyword, +.bash .hljs-literal, +.bash .hljs-built_in, +.coffeescript .hljs-literal, +.coffeescript .hljs-built_in, +.coffeescript .hljs-number, +.cpp .hljs-number, +.cpp .hljs-built_in, +.c .hljs-number, +.c .hljs-built_in, +.cs .hljs-number, +.cs .hljs-built_in, +.css .hljs-attribute, +.css .hljs-hexcolor, +.css .hljs-number, +.css .hljs-function, +.haskell .hljs-number, +.http .hljs-literal, +.http .hljs-attribute, +.java .hljs-number, +.javascript .hljs-built_in, +.javascript .hljs-literal, +.javascript .hljs-number, +.json .hljs-number, +.makefile .hljs-keyword, +.markdown .hljs-link_reference, +.nginx .hljs-built_in, +.objectivec .hljs-literal, +.objectivec .hljs-number, +.objectivec .hljs-built_in, +.php .hljs-literal, +.php .hljs-number, +.python .hljs-number, +.ruby .hljs-prompt, +.ruby .hljs-constant, +.ruby .hljs-number, +.ruby .hljs-subst .hljs-keyword, +.ruby .hljs-symbol, +.rust .hljs-number, +.sql .hljs-number, +.puppet .hljs-function, +.less .hljs-number, +.less .hljs-hexcolor, +.less .hljs-function, +.less .hljs-attribute, +.scss .hljs-preprocessor, +.scss .hljs-number, +.scss .hljs-hexcolor, +.scss .hljs-function, +.scss .hljs-attribute, +.stylus .hljs-number, +.stylus .hljs-hexcolor, +.stylus .hljs-attribute, +.stylus .hljs-params, +.go .hljs-built_in, +.go .hljs-constant, +.swift .hljs-built_in, +.swift .hljs-number { + color: #0086b3; +} + +.apache .hljs-tag, +.cs .hljs-xmlDocTag, +.css .hljs-tag, +.xml .hljs-title, +.stylus .hljs-tag { + color: #63a35c; +} + +.bash .hljs-variable, +.cs .hljs-preprocessor, +.cs .hljs-preprocessor .hljs-keyword, +.css .hljs-attr_selector, +.css .hljs-value, +.ini .hljs-value, +.ini .hljs-keyword, +.javascript .hljs-tag .hljs-title, +.makefile .hljs-constant, +.nginx .hljs-variable, +.xml .hljs-tag, +.scss .hljs-variable { + color: #333333; +} + +.bash .hljs-title, +.coffeescript .hljs-title, +.cpp .hljs-title, +.c .hljs-title, +.cs .hljs-title, +.css .hljs-id, +.css .hljs-class, +.css .hljs-pseudo, +.ini .hljs-title, +.haskell .hljs-title, +.haskell .hljs-pragma, +.java .hljs-title, +.javascript .hljs-title, +.makefile .hljs-title, +.objectivec .hljs-title, +.perl .hljs-sub, +.php .hljs-title, +.python .hljs-decorator, +.python .hljs-title, +.ruby .hljs-parent, +.ruby .hljs-title, +.rust .hljs-title, +.xml .hljs-attribute, +.puppet .hljs-title, +.less .hljs-id, +.less .hljs-pseudo, +.less .hljs-class, +.scss .hljs-id, +.scss .hljs-pseudo, +.scss .hljs-class, +.stylus .hljs-class, +.stylus .hljs-id, +.stylus .hljs-pseudo, +.stylus .hljs-title, +.swift .hljs-title, +.diff .hljs-chunk { + color: #795da3; +} + +.coffeescript .hljs-reserved, +.coffeescript .hljs-attribute { + color: #1d3e81; +} + +.diff .hljs-chunk { + font-weight: bold; +} + +.diff .hljs-addition { + color: #55a532; + background-color: #eaffea; +} + +.diff .hljs-deletion { + color: #bd2c00; + background-color: #ffecec; +} + +.markdown .hljs-link_url { + text-decoration: underline; } diff --git a/docs/Actions.html b/docs/Actions.html index f4896e264..99be30edc 100644 --- a/docs/Actions.html +++ b/docs/Actions.html @@ -99,12 +99,12 @@ that is not a Store, you can still use the listen method provided b

    Select the provided sheet in the current window. This action changes the top level sheet.

    Scope: Window

    -
    Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
    +    
    Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
         

    toggleWorkspaceLocationHidden

    Toggle whether a particular column is visible. Call this action with one of the Sheet location constants:

    -
    Actions.toggleWorkspaceLocationHidden(WorkspaceStore.Location.MessageListSidebar)
    +    
    Actions.toggleWorkspaceLocationHidden(WorkspaceStore.Location.MessageListSidebar)
         

    setCursorPosition

    Focus the keyboard on an item in a collection. This action moves the @@ -237,9 +237,9 @@ that is not a Store, you can still use the listen method provided b

    removeFile

    Remove a file from a draft.

    Scope: Window

    -
    Actions.removeFile
    -      file: fileObject
    -      messageClientId: draftClientId
    +    
    Actions.removeFile
    +      file: fileObject
    +      messageClientId: draftClientId
         

    popSheet

    Pop the current sheet off the Sheet stack maintained by the WorkspaceStore. diff --git a/docs/Config.html b/docs/Config.html index de9353385..fc90621aa 100644 --- a/docs/Config.html +++ b/docs/Config.html @@ -60,12 +60,12 @@ more info.

    default, the type it should be, etc. A simple example:

    # We want to provide an `enableThing`, and a `thingVolume`
     config:
    -  enableThing:
    +  enableThing:
         type: 'boolean'
    -    default: false
    +    default: false
       thingVolume:
         type: 'integer'
    -    default: 5
    +    default: 5
         minimum: 1
         maximum: 11
     
    @@ -172,11 +172,11 @@ valid CSS color format such as #abc, #abcdef, wh

    All types support an enum key. The enum key lets you specify all values that the config setting can possibly be. enum must be an array of values of your specified type. Schema:

    -
    config:
    -  someSetting:
    -    type: 'integer'
    -    default: 4
    -    enum: [2, 4, 6, 8]
    +
    config:
    +  someSetting:
    +    type: 'integer'
    +    default: 4
    +    enum: [2, 4, 6, 8]
     

    Usage:

    atom.config.set('my-package.someSetting', '2')
    diff --git a/docs/FirstSteps.html b/docs/FirstSteps.html
    deleted file mode 100644
    index e9686dd27..000000000
    --- a/docs/FirstSteps.html
    +++ /dev/null
    @@ -1,432 +0,0 @@
    ----
    -layout: docs
    -title: First Steps
    ----
    -
    -
    -
    -
    - - - -
    -
    -

    Start building on top of Nylas in minutes:

    -

    Packages lie at the heart of N1. The thread list, composer and other core parts of the app are packages bundled with the app, and extending N1 is as simple as creating a new package.

    -
    -
    - -
    -
    -

    1
    Install N1

    -

    Download and install Nylas for . Open it and sign in to your email account.

    -
    -
    - -
    -
    -

    2
    Start a Package

    -

    From the Developer Menu, choose Create a Package... and name your new package.

    -
    -
    - -
    -
    - -
    -
    -

    3
    See it in Action

    -

    The example package just created adds a section to the message sidebar and is already enabled, view a message to see it in action! If you make changes to the source, choose View > Refresh to see your changes in N1.

    -
    -
    - -
    - - - -
    - -
    -
    -

    A
    Source dive

    - -

    N1 is built on the modern web - packages are written in CoffeeScript or JavaScript. Packages are a lot like node modules, with their own source, assets, and tests. Check out the packages that power your N1, located in ~/.nylas/dev/packages!

    -
    -
    -

    B
    Run the specs

    - -

    In N1, select Developer > Run Package Specs... from the menu to run your package's new specs. Nylas and its packages use the Jasmine testing framework.

    -
    -
    - -

    Step 2: Building your first package

    - -

    If you followed the first part of our Getting Started Guide, you should have a brand new package just waiting to be explored.

    -

    This sample package simply adds the name of the currently focused contact to the sidebar:

    -

    -

    We're going to build on this to show the sender's Gravatar image in the sidebar, instead of just their name. You can check out the full code for the package in the sample packages repository.

    -

    Find the package source in ~/.nylas/dev/packages and open the contents in your favorite text editor.

    -
    -

    We use CJSX, a CoffeeScript syntax for JSX, to streamline our package code. - For syntax highlighting, we recommend Babel for Sublime, or the CJSX Language Atom package.

    -
    -

    Changing the data

    -

    Let's poke around and change what the sidebar displays.

    -

    You'll find the code responsible for the sidebar in lib/my-message-sidebar.cjsx. Take a look at the render method -- this generates the content which appears in the sidebar.

    -

    (How does it get in the sidebar? See Interface Concepts and look at main.cjsx for clues. We'll dive into this more later in the guide.)

    -

    We can change the sidebar to display the contact's email address as well. Check out the Contact attributes and change the _renderContent method to display more information:

    -
    _renderContent: =>
    -  <div className="header">
    -      <h1>Hi, {@state.contact.name} ({@state.contact.email})!</h1>
    -  </div>
    -
    -

    After making changes to the package, reload N1 by going to View > Reload.

    -

    Installing a dependency

    -

    Now we've figured out how to show the contact's email address, we can use that to generate the Gravatar for the contact. However, as per the Gravatar documentation, we need to be able to calculate the MD5 hash for an email address first.

    -

    Let's install the md5 package and save it as a dependency in our package.json:

    -
    $ npm install md5 --save
    -
    -

    Installing other dependencies works the same way.

    -

    Now, add the md5 requirement in my-message-sidebar.cjsx and update the _renderContent method to show the md5 hash:

    -
    md5 = require 'md5'
    -
    -class MyMessageSidebar extends React.Component
    -  @displayName: 'MyMessageSidebar'
    -
    -  ...
    -
    -  _renderContent: =>
    -    <div className="header">
    -      {md5(@state.contact.email)}
    -    </div>
    -
    -
    -

    JSX Tip: The {..} syntax is used for JavaScript expressions inside HTML elements. Learn more.

    -
    -

    You should see the MD5 hash appear in the sidebar (after you reload N1):

    -

    -

    Let's Render!

    -

    Turning the MD5 hash into a Gravatar image is simple. We need to add an <img> tag to the rendered HTML:

    -
    _renderContent =>
    -    <div className="header">
    -      <img src={'http://www.gravatar.com/avatar/' + md5(@state.contact.email)}/>
    -    </div>
    -
    -

    Now the Gravatar image associated with the currently focused contact appears in the sidebar. If there's no image available, the Gravatar default will show; you can add parameters to your image tag to change the default behavior.

    -

    -

    Styling

    -

    Adding styles to our Gravatar image is a matter of editing stylesheets/main.less and applying the class to our img tag. Let's make it round:

    -

    stylesheets/main.less

    -
    .gravatar {
    -    border-radius: 45px;
    -    border: 2px solid #ccc;
    -}
    -
    -

    lib/my-message-sidebar.cjsx

    -
    _renderContent =>
    -    gravatar = "http://www.gravatar.com/avatar/" + md5(@state.contact.email)
    -
    -    <div className="header">
    -      <img src={gravatar} className="gravatar"/>
    -    </div>
    -
    -
    -

    React Tip: Remember to use DOM property names, i.e. className instead of class.

    -
    -

    You'll see these styles reflected in your sidebar.

    -

    -

    If you're a fan of using the Chrome Developer Tools to tinker with styles, no fear; they work in N1, too. Open them by going to Developer > Toggle Developer Tools. You'll also find them helpful for debugging in the event that your package isn't behaving as expected.

    -
    - -

    Continue this guide: adding a data store to your package

    - -
    - - -

    Step 3: Adding a Data Store

    - -

    Building on the previous part of our Getting Started guide, we're going to introduce a data store to give our sidebar superpowers.

    -

    Stores and Data Flow

    -

    The Nylas data model revolves around a central DatabaseStore and lightweight Models that represent data with a particular schema. This works a lot like ActiveRecord, SQLAlchemy and other "smart model" ORMs. See the Database explanation for more details.

    -

    Using the Flux pattern for data flow means that we set up our UI components to 'listen' to specific data stores. When those stores change, we update the state inside our component, and re-render the view.

    -

    We've already used this (without realizing) in the Gravatar sidebar example:

    -
      componentDidMount: =>
    -    @unsubscribe = FocusedContactsStore.listen(@_onChange)
    -  ...
    -  _onChange: =>
    -    @setState(@_getStateFromStores())
    -
    -  _getStateFromStores: =>
    -    contact: FocusedContactsStore.focusedContact()
    -
    -

    In this case, the sidebar listens to the FocusedContactsStore, which updates when the person selected in the conversation changes. This triggers the _onChange method which updates the component state; this causes React to render the view with the new state.

    -

    To add more depth to our sidebar package, we need to:

    -
      -
    • Create our own data store which will listen to FocusedContactsStore
    • -
    • Extend our data store to do additional things with the contact data
    • -
    • Update our sidebar to listen to, and display data from, the new store.
    • -
    -

    In this guide, we'll fetch the GitHub profile for the currently focused contact and display a link to it, using the GitHub API.

    -

    Creating the Store

    -

    The boilerplate to create a new store which listens to FocusedContactsStore looks like this:

    -

    lib/github-user-store.coffee

    -
    Reflux = require 'reflux'
    -{FocusedContactsStore} = require 'nylas-exports'
    -
    -module.exports =
    -
    -GithubUserStore = Reflux.createStore
    -
    -  init: ->
    -      @listenTo FocusedContactsStore, @_onFocusedContactChanged
    -
    -  _onFocusedContactChanged: ->
    -      # TBD - This is fired when the focused contact changes
    -      @trigger(@)
    -
    -

    (Note: You'll need to set up the reflux dependency.)

    -

    You should be able to drop this store into the sidebar example's componentDidMount method -- all it does is listen for the FocusedContactsStore to change, and then trigger its own event.

    -

    Let's build this out to retrieve some new data based on the focused contact, and expose it via a UI component.

    -

    Getting Data In

    -

    We'll expand the _onFocusedContactChanged method to do something when the focused contact changes. In this case, we'll see if there's a GitHub profile for that user, and display some information if there is.

    -
    request = require 'request'
    -
    -GithubUserStore = Reflux.createStore
    -  init: ->
    -    @_profile = null
    -    @listenTo FocusedContactsStore, @_onFocusedContactChanged
    -
    -  getProfile: ->
    -    @_profile
    -
    -  _onFocusedContactChanged: ->
    -    # Get the newly focused contact
    -    contact = FocusedContactsStore.focusedContact()
    -    # Clear the profile we're currently showing
    -    @_profile = null    
    -    if contact
    -      @_fetchGithubProfile(contact.email)
    -    @trigger(@)
    -
    -  _fetchGithubProfile: (email) ->
    -    @_makeRequest "https://api.github.com/search/users?q=#{email}", (err, resp, data) =>
    -      console.warn(data.message) if data.message?
    -      # Make sure we actually got something back
    -      github = data?.items?[0] ? false
    -      if github
    -        @_profile = github
    -        console.log(github)
    -    @trigger(@)
    -
    -  _makeRequest: (url, callback) ->
    -    # GitHub needs a User-Agent header. Also, parse responses as JSON.
    -    request({url: url, headers: {'User-Agent': 'request'}, json: true}, callback)
    -
    -

    The console.log line should show the GitHub profile for a contact (if they have one!) inside the Developer Tools Console, which you can enable at Developer > Toggle Developer Tools.

    -

    You may run into rate-limiting issues with the GitHub API; to avoid these, you can add authentication with a pre-baked token by modifying the HTTP request your store makes. Caution! Use this for local development only. You could also try implementing a simple cache to avoid making the same request multiple times.

    -

    Display Time

    -

    To display this new data in the sidebar, we need to make sure our component is listening to the store, and load the appropriate state when it changes.

    -
    class GithubSidebar extends React.Component
    -  ...
    -  componentDidMount: =>
    -    @unsubscribe = GithubUserStore.listen(@_onChange)
    -
    -  _onChange: =>
    -    @setState(@_getStateFromStores())
    -
    -  _getStateFromStores: =>
    -    github: GithubUserStore.getProfile()
    -
    -

    Now we can access @state.github (which is the GitHub user profile object), and display the information it contains by updating the render and renderContent methods.

    -

    For example:

    -
      _renderContent: =>
    -    <img className="github" src={@state.github.avatar_url}/> <a href={@state.github.html_url}>GitHub</a>
    -
    -

    Extending The Store

    -

    To make this package more compelling, we can extend the store to make further API requests and fetch more data about the user. Passing this data back to the UI component follows exactly the same pattern as the barebones data shown above, so we'll leave it as an exercise for the reader. :)

    -
    -

    You can find a more extensive version of this example in our sample packages repository.

    -
    - -
    diff --git a/docs/InterfaceConcepts.html b/docs/InterfaceConcepts.html index 01a9ea4d4..a89f96e64 100644 --- a/docs/InterfaceConcepts.html +++ b/docs/InterfaceConcepts.html @@ -22,7 +22,7 @@ ComponentRegistry.register NotificationsStickyBa 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

    Toolbars in N1 are also powered by the ComponentRegistry. 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.

    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.

    diff --git a/docs/Menu.html b/docs/Menu.html index 42fb85c2e..1fcc1fa1a 100644 --- a/docs/Menu.html +++ b/docs/Menu.html @@ -22,9 +22,9 @@ components by providing the headerComponents and footerCompon These items are nested within .header-container. and .footer-container, and you can customize their appearance by providing CSS selectors scoped to your component's Menu instance:

    -
    .template-picker .menu .header-container {
    -  height: 100px;
    -}
    +
    .template-picker .menu .header-container {
    +  height: 100px;
    +}
     

    diff --git a/docs/MultiselectActionBar.html b/docs/MultiselectActionBar.html index 344167fdd..c2fd167c1 100644 --- a/docs/MultiselectActionBar.html +++ b/docs/MultiselectActionBar.html @@ -21,8 +21,8 @@ and other settings:

    The MultiselectActionBar uses the ComponentRegistry to find items to display for the given collection name. To add an item to the bar created in the example above, register it like this:

    -
    ComponentRegistry.register ThreadBulkRemoveButton,
    -  role: 'thread:BulkAction'
    +
    ComponentRegistry.register ThreadBulkRemoveButton,
    +  role: 'thread:BulkAction'
     

    diff --git a/docs/README.html b/docs/README.html new file mode 100644 index 000000000..00a6dc206 --- /dev/null +++ b/docs/README.html @@ -0,0 +1,9 @@ +--- +layout: docs +title: README.md +--- +

    N1 Documentation

    +

    For the full documentation (which includes these guides), go to: +https://nylas.github.io/N1/docs/

    +

    To see annotated package examples, go to: https://nylas.github.io/N1/examples/

    + diff --git a/docs/React.html b/docs/React.html index afb2cbe09..221d4a9a8 100644 --- a/docs/React.html +++ b/docs/React.html @@ -53,13 +53,13 @@ title: Interface Components
  • Add a component to the action bar at the bottom of the Composer:

    -
         ComponentRegistry.register TemplatePicker,
    -       role: 'Composer:ActionButton'
    +
         ComponentRegistry.register TemplatePicker,
    +       role: 'Composer:ActionButton'
     
  • Replace the Participants component that ships with N1 to display thread participants on your own:

    -
         ComponentRegistry.register ParticipantsWithStatusDots,
    -         role: 'Participants'
    +
         ComponentRegistry.register ParticipantsWithStatusDots,
    +         role: 'Participants'
     
  • diff --git a/docs/TaskQueue.html b/docs/TaskQueue.html index 25b032036..8389b21f4 100644 --- a/docs/TaskQueue.html +++ b/docs/TaskQueue.html @@ -24,12 +24,12 @@ main window via IPC.

    Actions.queueTask(new ChangeStarredTask(thread: @_thread, starred: true))

    Dequeueing a Task

    -
    Actions.dequeueMatchingTask({
    -  type: 'FileUploadTask',
    +
    Actions.dequeueMatchingTask({
    +  type: 'FileUploadTask',
       matching: {
         filePath: uploadData.filePath
       }
    -})
    +})
     

    Creating Tasks

    Support for creating custom Task subclasses in third-party packages is coming soon. diff --git a/docs/WritingSpecs.html b/docs/WritingSpecs.html index 2b882a14f..1e91ac2ca 100644 --- a/docs/WritingSpecs.html +++ b/docs/WritingSpecs.html @@ -2,12 +2,11 @@ layout: docs title: Writing Specs --- -

    Writing specs

    Nylas uses Jasmine as its spec framework. As a package developer, you can write specs using Jasmine 1.3 and get some quick wins. Jasmine specs can be run in N1 directly from the Developer menu, and the test environment provides you with helpful stubs. You can also require your own test framework, or use Jasmine for integration tests and your own framework for your existing business logic.

    This documentation describes using Jasmine 1.3 to write specs for a Nylas package.

    -

    Running Specs

    +

    Running Specs

    You can run your package specs from Developer > Run Package Specs.... Once you've opened the spec window, you can see output and re-run your specs by clicking Reload Specs.

    -

    Writing Specs

    +

    Writing Specs

    To create specs, place js, coffee, or cjsx files in the spec directory of your package. Spec files must end with the -spec suffix.

    Here's an annotated look at a typical Jasmine spec:

    # The `describe` method takes two arguments, a description and a function. If the description
    diff --git a/docs/sidebar.json b/docs/sidebar.json
    deleted file mode 100644
    index fe161773b..000000000
    --- a/docs/sidebar.json
    +++ /dev/null
    @@ -1,493 +0,0 @@
    -{
    -  "sections": [
    -    {
    -      "name": "Getting Started",
    -      "items": [
    -        {
    -          "html": "\n\n\n
    \n
    \n

    Start building on top of Nylas in minutes:

    \n

    1
    Install N1

    \n

    Download and install Nylas for . Open it and sign in to your email account.

    \n
    \n
    \n\n
    \n
    \n

    2
    Start a Package

    \n

    Packages lie at the heart of N1. The thread list, composer and other core parts of the app are packages bundled with the app, and you have access to the same set of APIs. From the Developer Menu, choose Create a Package... and name your new package.

    \n
    \n
    \n \n
    \n
    \n\n
    \n
    \n

    3
    See it in Action

    \n

    Your new package comes with some basic code that adds a section to the message sidebar, and it's already enabled! View a message to see it in action. If you make changes to the source, choose View > Refresh to see your changes in N1.

    \n
    \n
    \n\n
    \n\n\n\n
    \n\n
    \n
    \n

    A
    Explore the source

    \n \n

    Nylas is built on the modern web - packages are written in CoffeeScript or JavaScript. Packages are a lot like node modules, with their own source, assets, and tests. Check out yours in ~/.nylas/dev/packages.

    \n
    \n
    \n

    B
    Run the specs

    \n \n

    In N1, select Developer > Run Package Specs... from the menu to run your package's new specs. Nylas and its packages use the Jasmine testing framework.

    \n
    \n
    \n\n

    Step 2: Building your first package

    \n\n

    If you followed the first part of our Getting Started Guide, you should have a brand new package just waiting to be explored.

    \n

    This sample package simply adds the name of the currently focused contact to the sidebar:

    \n

    \n

    We're going to build on this to show the sender's Gravatar image in the sidebar, instead of just their name. You can check out the full code for the package in the sample packages repository.

    \n

    Find the package source in ~/.nylas/dev/packages and open the contents in your favorite text editor.

    \n
    \n

    We use CJSX, a CoffeeScript syntax for JSX, to streamline our package code.\n For syntax highlighting, we recommend Babel for Sublime, or the CJSX Language Atom package.

    \n
    \n

    Changing the data

    \n

    Let's poke around and change what the sidebar displays.

    \n

    You'll find the code responsible for the sidebar in lib/my-message-sidebar.cjsx. Take a look at the render method -- this generates the content which appears in the sidebar.

    \n

    (How does it get in the sidebar? See Interface Concepts and look at main.cjsx for clues. We'll dive into this more later in the guide.)

    \n

    We can change the sidebar to display the contact's email address as well. Check out the Contact attributes and change the _renderContent method to display more information:

    \n
    _renderContent: =>\n  <div className=\"header\">\n      <h1>Hi, {@state.contact.name} ({@state.contact.email})!</h1>\n  </div>\n
    \n

    After making changes to the package, reload N1 by going to View > Reload.

    \n

    Installing a dependency

    \n

    Now we've figured out how to show the contact's email address, we can use that to generate the Gravatar for the contact. However, as per the Gravatar documentation, we need to be able to calculate the MD5 hash for an email address first.

    \n

    Let's install the md5 package and save it as a dependency in our package.json:

    \n
    $ npm install md5 --save\n
    \n

    Installing other dependencies works the same way.

    \n

    Now, add the md5 requirement in my-message-sidebar.cjsx and update the _renderContent method to show the md5 hash:

    \n
    md5 = require 'md5'\n\nclass MyMessageSidebar extends React.Component\n  @displayName: 'MyMessageSidebar'\n\n  ...\n\n  _renderContent: =>\n    <div className=\"header\">\n      {md5(@state.contact.email)}\n    </div>\n
    \n
    \n

    JSX Tip: The {..} syntax is used for JavaScript expressions inside HTML elements. Learn more.

    \n
    \n

    You should see the MD5 hash appear in the sidebar (after you reload N1):

    \n

    \n

    Let's Render!

    \n

    Turning the MD5 hash into a Gravatar image is simple. We need to add an <img> tag to the rendered HTML:

    \n
    _renderContent =>\n    <div className=\"header\">\n      <img src={'http://www.gravatar.com/avatar/' + md5(@state.contact.email)}/>\n    </div>\n
    \n

    Now the Gravatar image associated with the currently focused contact appears in the sidebar. If there's no image available, the Gravatar default will show; you can add parameters to your image tag to change the default behavior.

    \n

    \n

    Styling

    \n

    Adding styles to our Gravatar image is a matter of editing stylesheets/main.less and applying the class to our img tag. Let's make it round:

    \n

    stylesheets/main.less

    \n
    .gravatar {\n    border-radius: 45px;\n    border: 2px solid #ccc;\n}\n
    \n

    lib/my-message-sidebar.cjsx

    \n
    _renderContent =>\n    gravatar = \"http://www.gravatar.com/avatar/\" + md5(@state.contact.email)\n\n    <div className=\"header\">\n      <img src={gravatar} className=\"gravatar\"/>\n    </div>\n
    \n
    \n

    React Tip: Remember to use DOM property names, i.e. className instead of class.

    \n
    \n

    You'll see these styles reflected in your sidebar.

    \n

    \n

    If you're a fan of using the Chrome Developer Tools to tinker with styles, no fear; they work in N1, too. Open them by going to Developer > Toggle Developer Tools. You'll also find them helpful for debugging in the event that your package isn't behaving as expected.

    \n
    \n\n

    Continue this guide: adding a data store to your package

    \n\n
    \n\n\n

    Step 3: Adding a Data Store

    \n\n

    Building on the previous part of our Getting Started guide, we're going to introduce a data store to give our sidebar superpowers.

    \n

    Stores and Data Flow

    \n

    The Nylas data model revolves around a central DatabaseStore and lightweight Models that represent data with a particular schema. This works a lot like ActiveRecord, SQLAlchemy and other "smart model" ORMs. See the Database explanation for more details.

    \n

    Using the Flux pattern for data flow means that we set up our UI components to 'listen' to specific data stores. When those stores change, we update the state inside our component, and re-render the view.

    \n

    We've already used this (without realizing) in the Gravatar sidebar example:

    \n
      componentDidMount: =>\n    @unsubscribe = FocusedContactsStore.listen(@_onChange)\n  ...\n  _onChange: =>\n    @setState(@_getStateFromStores())\n\n  _getStateFromStores: =>\n    contact: FocusedContactsStore.focusedContact()\n
    \n

    In this case, the sidebar listens to the FocusedContactsStore, which updates when the person selected in the conversation changes. This triggers the _onChange method which updates the component state; this causes React to render the view with the new state.

    \n

    To add more depth to our sidebar package, we need to:

    \n
      \n
    • Create our own data store which will listen to FocusedContactsStore
    • \n
    • Extend our data store to do additional things with the contact data
    • \n
    • Update our sidebar to listen to, and display data from, the new store.
    • \n
    \n

    In this guide, we'll fetch the GitHub profile for the currently focused contact and display a link to it, using the GitHub API.

    \n

    Creating the Store

    \n

    The boilerplate to create a new store which listens to FocusedContactsStore looks like this:

    \n

    lib/github-user-store.coffee

    \n
    Reflux = require 'reflux'\n{FocusedContactsStore} = require 'nylas-exports'\n\nmodule.exports =\n\nGithubUserStore = Reflux.createStore\n\n  init: ->\n      @listenTo FocusedContactsStore, @_onFocusedContactChanged\n\n  _onFocusedContactChanged: ->\n      # TBD - This is fired when the focused contact changes\n      @trigger(@)\n
    \n

    (Note: You'll need to set up the reflux dependency.)

    \n

    You should be able to drop this store into the sidebar example's componentDidMount method -- all it does is listen for the FocusedContactsStore to change, and then trigger its own event.

    \n

    Let's build this out to retrieve some new data based on the focused contact, and expose it via a UI component.

    \n

    Getting Data In

    \n

    We'll expand the _onFocusedContactChanged method to do something when the focused contact changes. In this case, we'll see if there's a GitHub profile for that user, and display some information if there is.

    \n
    request = require 'request'\n\nGithubUserStore = Reflux.createStore\n  init: ->\n    @_profile = null\n    @listenTo FocusedContactsStore, @_onFocusedContactChanged\n\n  getProfile: ->\n    @_profile\n\n  _onFocusedContactChanged: ->\n    # Get the newly focused contact\n    contact = FocusedContactsStore.focusedContact()\n    # Clear the profile we're currently showing\n    @_profile = null    \n    if contact\n      @_fetchGithubProfile(contact.email)\n    @trigger(@)\n\n  _fetchGithubProfile: (email) ->\n    @_makeRequest \"https://api.github.com/search/users?q=#{email}\", (err, resp, data) =>\n      console.warn(data.message) if data.message?\n      # Make sure we actually got something back\n      github = data?.items?[0] ? false\n      if github\n        @_profile = github\n        console.log(github)\n    @trigger(@)\n\n  _makeRequest: (url, callback) ->\n    # GitHub needs a User-Agent header. Also, parse responses as JSON.\n    request({url: url, headers: {'User-Agent': 'request'}, json: true}, callback)\n
    \n

    The console.log line should show the GitHub profile for a contact (if they have one!) inside the Developer Tools Console, which you can enable at Developer > Toggle Developer Tools.

    \n

    You may run into rate-limiting issues with the GitHub API; to avoid these, you can add authentication with a pre-baked token by modifying the HTTP request your store makes. Caution! Use this for local development only. You could also try implementing a simple cache to avoid making the same request multiple times.

    \n

    Display Time

    \n

    To display this new data in the sidebar, we need to make sure our component is listening to the store, and load the appropriate state when it changes.

    \n
    class GithubSidebar extends React.Component\n  ...\n  componentDidMount: =>\n    @unsubscribe = GithubUserStore.listen(@_onChange)\n\n  _onChange: =>\n    @setState(@_getStateFromStores())\n\n  _getStateFromStores: =>\n    github: GithubUserStore.getProfile()\n
    \n

    Now we can access @state.github (which is the GitHub user profile object), and display the information it contains by updating the render and renderContent methods.

    \n

    For example:

    \n
      _renderContent: =>\n    <img className=\"github\" src={@state.github.avatar_url}/> <a href={@state.github.html_url}>GitHub</a>\n
    \n

    Extending The Store

    \n

    To make this package more compelling, we can extend the store to make further API requests and fetch more data about the user. Passing this data back to the UI component follows exactly the same pattern as the barebones data shown above, so we'll leave it as an exercise for the reader. :)

    \n
    \n

    You can find a more extensive version of this example in our sample packages repository.

    \n
    \n", - "meta": { - "Title": "First Steps", - "TitleHidden": true, - "Section": "Getting Started", - "Order": 2, - "title": "First Steps", - "titlehidden": true, - "section": "Getting Started", - "order": 2 - }, - "name": "First Steps", - "filename": "index.md", - "link": "index.html" - } - ] - }, - { - "name": "Guides", - "items": [ - { - "html": "

    The N1 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.

    \n

    \n

    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.

    \n

    The Nylas 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.

    \n

    For each mode, Sheets register a set of column names.

    \n

    \n
    @defineSheet 'Threads', {root: true},\n   split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']\n   list: ['RootSidebar', 'ThreadList']\n
    \n

    Column names are important. Once you've registered a sheet, your package (and other packages) register React components that appear in each column.

    \n

    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.

    \n
    ComponentRegistry.register AccountSidebar,\n  location: WorkspaceStore.Location.RootSidebar\n\n\nComponentRegistry.register NotificationsStickyBar,\n  location: WorkspaceStore.Sheet.Threads.Header\n
    \n

    Each column is laid out as a CSS Flexbox, making them extremely flexible. For more about layout using Flexbox, see Working with Flexbox.

    \n

    Toolbars

    \n

    Toolbars in N1 are also powered by the {ComponentRegistry}. 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.

    \n

    \n

    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.

    \n

    \n

    To add items to a toolbar, you inject them via the {ComponentRegistry}. There are several ways of describing the location of a toolbar component which are useful in different scenarios:

    \n
      \n
    • <Location>.Toolbar: This component will always appear in the toolbar above the column named <Location>.

      \n

      (Example: Compose button which appears above the Left Sidebar column, regardless of what else is there.)

      \n
    • \n
    • <ComponentName>.Toolbar: This component will appear in the toolbar above <ComponentName>.

      \n

      (Example: Archive button that should always be coupled with the MessageList component, placed anywhere a MessageList component is placed.)

      \n
    • \n
    • Global.Toolbar.Left: This component will always be added to the leftmost column of the toolbar.

      \n

      (Example: Window Controls)

      \n
    • \n
    \n", - "meta": { - "Title": "Interface Concepts", - "Section": "Guides", - "Order": 1, - "title": "Interface Concepts", - "section": "Guides", - "order": 1 - }, - "name": "Interface Concepts", - "filename": "InterfaceConcepts.md", - "link": "InterfaceConcepts.html" - }, - { - "html": "

    Packages lie at the heart of N1. Each part of the core experience is a separate package that uses the Nylas 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.

    \n

    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.

    \n

    Package Structure

    \n

    Each package is defined by a package.json file that includes its name, version and dependencies. Packages may also declare dependencies which are loaded from npm - in this case, the request library. You'll need to npm install these dependencies locally when developing the package.

    \n
    {\n  \"name\": \"translate\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"An example package for N1\",\n  \"license\": \"Proprietary\",\n  \"engines\": {\n    \"atom\": \"*\"\n  },\n  \"dependencies\": {\n    \"request\": \"^2.53\"\n  }\n}\n

    Our package also contains source files, a spec file with complete tests for the behavior the package adds, and a stylesheet for CSS:

    \n
    - package.json\n- lib/\n   - main.coffee\n   - translate-button.cjsx\n- spec/\n   - main-spec.coffee\n- stylesheets/\n   - translate.less\n

    package.json lists lib/main as the root file of our package. Since N1 runs NodeJS, we can require other source files, Node packages, etc.

    \n

    N1 can read js, coffee, jsx, and cjsx files automatically.

    \n

    Inside main.coffee, there are two important functions being exported:

    \n
    require './translate-button'\n\nmodule.exports =\n\n  # Activate is called when the package is loaded. If your package previously\n  # saved state using `serialize` it is provided.\n  #\n  activate: (@state) ->\n    ComponentRegistry.register TranslateButton,\n      role: 'Composer:ActionButton'\n\n  # Serialize is called when your package is about to be unmounted.\n  # You can return a state object that will be passed back to your package\n  # when it is re-activated.\n  #\n  serialize: ->\n      {}\n\n  # This optional method is called when the window is shutting down,\n  # or when your package is being updated or disabled. If your package is\n  # watching any files, holding external resources, providing commands or\n  # subscribing to events, release them here.\n  #\n  deactivate: ->\n    ComponentRegistry.unregister(TranslateButton)\n
    \n
    \n

    N1 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 plugin to Sublime Text, or the CJSX Language for syntax highlighting.

    \n
    \n

    Package Stylesheets

    \n

    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.

    \n

    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 N1 seamlessly.

    \n

    If you do need special styling, try to keep only structural styles in the package stylesheets. 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!

    \n

    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.

    \n

    Package Assets

    \n

    Many packages need other static files, like images. You can add static files anywhere in your package directory, and reference them at runtime using the nylas:// url scheme:

    \n
    <img src=\"nylas://my-package-name/assets/goofy.png\">\n\na = new Audio()\na.src = \"nylas://my-package-name/sounds/bloop.mp3\"\na.play()\n

    Installing a Package

    \n

    N1 ships with many packages already bundled with the application. When the application launches, it looks for additional packages in ~/.nylas/dev/packages. Each package you create belongs in its own directory inside this folder.

    \n

    In the future, it will be possible to install packages directly from within the client.

    \n", - "meta": { - "Title": "Building a Package", - "Section": "Guides", - "Order": 1, - "title": "Building a Package", - "section": "Guides", - "order": 1 - }, - "name": "Building a Package", - "filename": "PackageOverview.md", - "link": "PackageOverview.html" - }, - { - "html": "

    N1 uses React to create a fast, responsive UI. Packages that want to extend the N1 interface should use React. Using React's JSX syntax is optional, but both JSX and CJSX (CoffeeScript) are available.

    \n

    For a quick introduction to React, take a look at Facebook's Getting Started with React.

    \n

    React Components

    \n

    N1 provides a set of core React components you can use in your packages. Many of the standard components listen for key events, include considerations for different platforms, and have extensive CSS. Wrapping standard components makes it easy to build rich interfaces that are consistent with the rest of the N1 platform.

    \n

    To use a standard component, require it from nylas-component-kit and use it in your component's render method.

    \n

    Keep in mind that React's Component model is based on composition rather than inheritance. On other platforms, you might subclass {Popover} to create your own custom Popover. In React, you should wrap the standard Popover component in your own component, which provides the Popover with props and children to customize its behavior.

    \n\n\n

    Here's a quick look at standard components you can require from nylas-component-kit:

    \n
      \n
    • {Menu}: Allows you to display a list of items consistent with the rest of the N1 user experience.

      \n
    • \n
    • {Spinner}: Displays an indeterminate progress indicator centered within it's container.

      \n
    • \n
    • {Popover}: Component for creating menus and popovers that appear in response to a click and stay open until the user clicks outside them.

      \n
    • \n
    • {Flexbox}: Component for creating a Flexbox layout.

      \n
    • \n
    • {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.

      \n
    • \n
    • {ListTabular}: Component for creating a list of items backed by a paginating ModelView.

      \n
    • \n
    • {MultiselectList}: Component for creating a list that supports multi-selection. (Internally wraps ListTabular)

      \n
    • \n
    • {MultiselectActionBar}: Component for creating a contextual toolbar that is activated when the user makes a selection on a ModelView.

      \n
    • \n
    • {ResizableRegion}: Component that renders it's children inside a resizable region with a draggable handle.

      \n
    • \n
    • {TokenizingTextField}: Wraps a standard <input> and takes function props for tokenizing input values and displaying autocompletion suggestions.

      \n
    • \n
    • {EventedIFrame}: Replacement for the standard <iframe> tag which handles events directed at the iFrame to ensure a consistent user experience.

      \n
    • \n
    \n

    React Component Injection

    \n

    The N1 interface is composed at runtime from components added by different packages. The app's left sidebar contains components from the composer package, the source list package, the activity package, and more. You can leverage the flexiblity of this system to extend almost any part of N1's interface.

    \n

    Registering Components

    \n

    After you've created React components in your package, you should register them with the {ComponentRegistry}. The Component Registry manages the dynamic injection of components that makes N1 so extensible. You can request that your components appear in a specific Location defined by the {WorkspaceStore}, or register your component for a Role that another package has declared.

    \n
    \n

    The Component Registry allows you to insert your custom component without hacking up the DOM. Register for a Location or Role and your Component will be rendered into that part of the interface.

    \n
    \n

    It's easy to see where registered components are displayed in N1. Enable the Developer bar at the bottom of the app by opening the Inspector panel, and then click "Component Regions":

    \n

    \n

    Each region outlined in red is filled dynamically by looking up a React component or set of components from the Component Registry. You can see the role or location you'd need to register for, and the props that your component will receive in those locations.

    \n

    Here are a few examples of how to use it to extend N1. Typically, packages register components in their main activate method, and unregister them in deactivate:

    \n
      \n
    1. Add a component to the Thread List column:

      \n
           ComponentRegistry.register ThreadList,\n       location: WorkspaceStore.Location.ThreadList\n
      \n
    2. \n
    3. Add a component to the action bar at the bottom of the Composer:

      \n
           ComponentRegistry.register TemplatePicker,\n       role: 'Composer:ActionButton'\n
      \n
    4. \n
    5. Replace the Participants component that ships with N1 to display thread participants on your own:

      \n
           ComponentRegistry.register ParticipantsWithStatusDots,\n         role: 'Participants'\n
      \n
    6. \n
    \n

    Tip: Remember to unregister components in the deactivate method of your package.

    \n

    Using Registered Components

    \n

    It's easy to build packages that use the Component Registry to display components vended by other parts of the application. You can query the Component Registry and display the components it returns. The Component Registry is a Reflux-compatible Store, so you can listen to it and update your state as the registry changes.

    \n

    There are also several convenience components that make it easy to dynamically inject components into your Virtual DOM. These are the preferred way of using injected components.

    \n
      \n
    • {InjectedComponent}: Renders the first component for the matching criteria you provide, and passes it the props in externalProps. See the API reference for more information.
    • \n
    \n
    <InjectedComponent\n    matching={role:\"Attachment\"}\n    exposedProps={file: file, messageLocalId: @props.localId}/>\n
    \n
      \n
    • {InjectedComponentSet}: Renders all of the components matching criteria you provide inside a {Flexbox}, and passes it the props in externalProps. See the API reference for more information.
    • \n
    \n
    <InjectedComponentSet\n    className=\"message-actions\"\n    matching={role:\"MessageAction\"}\n    exposedProps={thread:@props.thread, message: @props.message}>\n
    \n

    Unsafe Components

    \n

    N1 considers all injected components "unsafe". When you render them using {InjectedComponent} or {InjectedComponentSet}, they will be wrapped in a component that prevents exceptions in their React render and lifecycle methods from impacting your component. Instead of your component triggering a React Invariant exception in the application, an exception notice will be rendered in place of the unsafe component.

    \n

    \n

    In the future, N1 may automatically disable packages when their React components throw exceptions.

    \n", - "meta": { - "Title": "Interface Components", - "Section": "Guides", - "Order": 2, - "title": "Interface Components", - "section": "Guides", - "order": 2 - }, - "name": "Interface Components", - "filename": "React.md", - "link": "React.html" - }, - { - "html": "

    N1 uses Reflux, a slim implementation of Facebook's Flux Application Architecture 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:

    \n
      \n
    • Uni-directional data flow
    • \n
    • Loose coupling between components
    • \n
    \n

    For more information about the Flux pattern, check out this diagram. For a bit more insight into why we chose Reflux over other Flux implementations, there's a great blog post by the author of Reflux.

    \n

    There are several core stores in the application:

    \n
      \n
    • {AccountStore}: When the user signs in to N1, their auth token provides one or more accounts. The AccountStore manages the available Accounts, exposes the current Account, and allows you to observe changes to the current Account.

      \n
    • \n
    • {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.

      \n
    • \n
    • {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 N1.

      \n
    • \n
    • {DraftStore}: Manages Drafts, which are {Message} objects the user is authoring. Drafts present a unique case in N1 because they may be updated frequently by disconnected parts of the application. You should use the {DraftStore} to create, edit, and send drafts.

      \n
    • \n
    • {FocusedContentStore}: Manages focus within the main applciation window. The {FocusedContentStore} allows you to query and monitor changes to the selected thread, tag, file, etc.

      \n
    • \n
    \n

    Most packages declare additional stores that subscribe to these Stores, as well as user Actions, and vend data to the package's React components.

    \n

    Actions

    \n

    In Flux applications, 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. For example, when you click "Compose" in the top left corner of N1, the React component for the button fires {Actions::composeNewBlankDraft}. The {DraftStore} listens to this action and opens a new composer window.

    \n

    This approach means that your packages can fire existing {Actions}, like {Actions::composeNewBlankDraft}, or observe actions to add functionality. (For example, we have an Analytics package that also listens for {Actions::composeNewBlankDraft} and counts how many times it's been fired.) You can also define your own actions for use within your package.

    \n

    For a complete list of available actions, see {Actions}.

    \n", - "meta": { - "Title": "Application Architecture", - "Section": "Guides", - "Order": 3, - "title": "Application Architecture", - "section": "Guides", - "order": 3 - }, - "name": "Application Architecture", - "filename": "Architecture.md", - "link": "Architecture.html" - }, - { - "html": "

    Chromium DevTools

    \n

    N1 is built on top of Electron, which runs the latest version of Chromium (at the time of writing, Chromium 43). You can access the standard Chrome DevTools using the Command-Option-I (Ctrl-Shift-I on Windows/Linux) keyboard shortcut, including the Debugger, Profiler, and Console. You can find extensive information about the Chromium DevTools on developer.chrome.com.

    \n

    Here are a few hidden tricks for getting the most out of the Chromium DevTools:

    \n
      \n
    • You can use Command-P to "Open Quickly", jumping to a particular source file from any tab.

      \n
    • \n
    • You can set breakpoints by clicking the line number gutter while viewing a source file.

      \n
    • \n
    • While on a breakpoint, you can toggle the console panel by pressing Esc and type commands which are executed in the current scope.

      \n
    • \n
    • While viewing the DOM in the Elements panel, typing $0 on the console refers to the currently selected DOM node.

      \n
    • \n
    \n

    Nylas Developer Panel

    \n

    If you choose Developer > Relaunch with Debug Flags... from the menu, you can enable the Nylas Developer Panel at the bottom of the main window.

    \n

    The Developer Panel provides three views which you can click to activate:

    \n
      \n
    • Tasks: This view allows you to inspect the {TaskQueue} and see the what tasks are pending and complete. Click a task to see its JSON representation and inspect it's values, including the last error it encountered.

      \n
    • \n
    • Delta Stream: This view allows you to see the streaming updates from the Nylas API that the app has received. You can click individual updates to see the exact JSON that was consumed by the app, and search in the lower left for updates pertaining to an object ID or type.

      \n
    • \n
    • Requests: This view shows the requests the app has made to the Nylas API in curl-equivalent form. (The app does not actually make curl requests). You can click "Copy" to copy a curl command to the clipboard, or "Run" to execute it in a new Terminal window.

      \n
    • \n
    \n

    The Developer Panel also allows you to toggle "View Component Regions". Turning on component regions adds a red border to areas of the app that render dynamically injected components, and shows the props passed to React components in each one. See {react} for more information.

    \n

    The Development Workflow

    \n

    If you're debugging a package, you'll be modifying your code and re-running N1 over and over again. There are a few things you can do to make this development workflow less time consuming:

    \n
      \n
    • Inline Changes: Using the Chromium DevTools, you can change the contents of your coffeescript and javascript source files, type Command-S to save, and hot-swap the code. This makes it easy to test small adjustments to your code without re-launching N1.

      \n
    • \n
    • View > Refresh: From the View menu, choose "Refresh" to reload the N1 window just like a page in your browser. Refreshing is faster than restarting the app and allows you to iterate more quickly.

      \n
      \n

      Note: A bug in Electron causes the Chromium DevTools to become detatched if you refresh the app often. If you find that Chromium is not stopping at your breakpoints, quit N1 and re-launch it.

      \n
      \n
    • \n
    \n

    In the future, we'll support much richer hot-reloading of plugin components and code. Stay tuned!

    \n", - "meta": { - "Title": "Debugging N1", - "Section": "Guides", - "Order": 4, - "title": "Debugging N1", - "section": "Guides", - "order": 4 - }, - "name": "Debugging N1", - "filename": "Debugging.md", - "link": "Debugging.html" - }, - { - "html": "

    N1 is built on top of a custom database layer modeled after ActiveRecord. For many parts of the application, the database is the source of truth. Data is retrieved from the API, written to the database, and changes to the database trigger Stores and components to refresh their contents. The illustration below shows this flow of data:

    \n

    \n

    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.

    \n

    Declaring Models

    \n

    In N1, 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:

    \n
    class Example extends Model\n\n  @attributes:\n    'id': Attributes.String\n      queryable: true\n      modelKey: 'id'\n\n    'object': Attributes.String\n      modelKey: 'object'\n\n    'namespaceId': Attributes.String\n      queryable: true\n      modelKey: 'namespaceId'\n      jsonKey: 'namespace_id'\n\n    'body': Attributes.JoinedData\n      modelTable: 'MessageBody'\n      modelKey: 'body'\n\n    'files': Attributes.Collection\n      modelKey: 'files'\n      itemClass: File\n\n    'unread': Attributes.Boolean\n      queryable: true\n      modelKey: 'unread'\n
    \n

    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.

    \n

    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:

    \n
    Thread.attributes.namespaceId.equals(\"123\")\n// where namespace_id = '123'\n\nThread.attributes.lastMessageTimestamp.greaterThan(123)\n// where last_message_timestamp > 123\n\nThread.attributes.lastMessageTimestamp.descending()\n// order by last_message_timestamp DESC\n
    \n

    Retrieving Models

    \n

    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.

    \n
    DatabaseStore.find(Thread, '123').then (thread) ->\n    # thread is a thread object\n\nDatabaseStore.findBy(Thread, {subject: 'Hello World'}).then (thread) ->\n    # find a single thread by subject\n\nDatabaseStore.findAll(Thread).where([Thread.attributes.tags.contains('inbox')]).then (threads) ->\n    # find threads with the inbox tag\n\nDatabaseStore.count(Thread).where([Thread.attributes.lastMessageTimestamp.greaterThan(120315123)]).then (results) ->\n    # count threads where last message received since 120315123.\n
    \n

    Retrieving Pages of Models

    \n

    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.

    \n

    Saving and Updating Models

    \n

    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.

    \n

    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.

    \n

    Saving Drafts

    \n

    Drafts in N1 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.

    \n

    Removing Models

    \n

    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.

    \n

    Advanced Model Attributes

    \n
    Attribute.JoinedData
    \n

    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.

    \n

    When building a {ModelQuery} on a model with a {JoinedDataAttribute}, 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:

    \n
    DatabaseStore.find(Message, '123').then (message) ->\n    // message.body is undefined\n\nDatabaseStore.find(Message, '123').include(Message.attributes.body).then (message) ->\n    // message.body is defined\n
    \n

    When you call persistModel, JoinedData attributes are automatically written to the secondary table.

    \n

    JoinedData attributes cannot be queryable.

    \n
    Attribute.Collection
    \n

    Collection attributes provide basic support for one-to-many relationships. For example, {Thread}s in N1 have a collection of {Tag}s.

    \n

    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.

    \n

    Collection attributes have an additional clause builder, contains:

    \n
    DatabaseStore.findAll(Thread).where([Thread.attributes.tags.contains('inbox')])\n
    \n

    This is equivalent to writing the following SQL:

    \n
    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\n
    \n

    Listening for Changes

    \n

    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.

    \n

    Within Reflux Stores, you can listen to the {DatabaseStore} using the listenTo helper method:

    \n
    @listenTo(DatabaseStore, @_onDataChanged)\n
    \n

    Within generic code, you can listen to the {DatabaseStore} using this syntax:

    \n
    @unlisten = DatabaseStore.listen(@_onDataChanged, @)\n
    \n

    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:

    \n
    {\n    \"objectClass\": // string: the name of the class that was changed. ie: \"Thread\"\n    \"objects\": // array: the objects that were persisted or removed\n}\n

    But why can't I...?

    \n

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

    \n
      \n
    • Package code should not be tightly coupled to SQLite

      \n
    • \n
    • Queries should be composed in a way that makes invalid queries impossible

      \n
    • \n
    • All changes to the local database must be observable

      \n
    • \n
    \n", - "meta": { - "Title": "Accessing the Database", - "Section": "Guides", - "Order": 5, - "title": "Accessing the Database", - "section": "Guides", - "order": 5 - }, - "name": "Accessing the Database", - "filename": "Database.md", - "link": "Database.html" - }, - { - "html": "

    The composer lies at the heart of N1, and many improvements to the mail experience require deep integration with the composer. To enable these sort of plugins, the {DraftStore} exposes an extension API.

    \n

    This API allows your package to:

    \n
      \n
    • Display warning messages before a draft is sent. (ie: "Are you sure you want to send this without attaching a file?")

      \n
    • \n
    • Intercept keyboard and mouse events to the composer's text editor.

      \n
    • \n
    • Transform the draft and make additional changes before it is sent.

      \n
    • \n
    \n

    To create your own composer extensions, subclass {DraftStoreExtension} and override the methods your extension needs.

    \n

    In the sample packages repository, templates is an example of a package which uses a DraftStoreExtension to enhance the composer experience.

    \n

    Example

    \n

    This extension displays a warning before sending a draft that contains the names of competitors' products. If the user proceeds to send the draft containing the words, it appends a disclaimer.

    \n
    {DraftStoreExtension} = require 'nylas-exports'\n\nclass ProductsExtension extends DraftStoreExtension\n\n   @warningsForSending: (draft) ->\n      words = ['acme', 'anvil', 'tunnel', 'rocket', 'dynamite']\n      body = draft.body.toLowercase()\n      for word in words\n        if body.indexOf(word) > 0\n            return [\"with the word '#{word}'?\"]\n      return []\n\n   @finalizeSessionBeforeSending: (session) ->\n      draft = session.draft()\n      if @warningsForSending(draft)\n         bodyWithWarning = draft.body += \"<br>This email \\\n             contains competitor's product names \\\n            or trademarks used in context.\"\n         session.changes.add(body: bodyWithWarning)\n
    \n", - "meta": { - "Title": "Extending the Composer", - "Section": "Guides", - "Order": 6, - "title": "Extending the Composer", - "section": "Guides", - "order": 6 - }, - "name": "Extending the Composer", - "filename": "DraftStoreExtensions.md", - "link": "DraftStoreExtensions.html" - }, - { - "html": "

    Writing specs

    \n

    Nylas uses Jasmine as its spec framework. As a package developer, you can write specs using Jasmine 1.3 and get some quick wins. Jasmine specs can be run in N1 directly from the Developer menu, and the test environment provides you with helpful stubs. You can also require your own test framework, or use Jasmine for integration tests and your own framework for your existing business logic.

    \n

    This documentation describes using Jasmine 1.3 to write specs for a Nylas package.

    \n

    Running Specs

    \n

    You can run your package specs from Developer > Run Package Specs.... Once you've opened the spec window, you can see output and re-run your specs by clicking Reload Specs.

    \n

    Writing Specs

    \n

    To create specs, place js, coffee, or cjsx files in the spec directory of your package. Spec files must end with the -spec suffix.

    \n

    Here's an annotated look at a typical Jasmine spec:

    \n
    # The `describe` method takes two arguments, a description and a function. If the description\n# explains a behavior it typically begins with `when`; if it is more like a unit test it begins\n# with the method name.\ndescribe \"when a test is written\", ->\n\n  # The `it` method also takes two arguments, a description and a function. Try and make the\n  # description flow with the `it` method. For example, a description of `this should work`\n  # doesn't read well as `it this should work`. But a description of `should work` sounds\n  # great as `it should work`.\n  it \"has some expectations that should pass\", ->\n\n    # The best way to learn about expectations is to read the Jasmine documentation:\n    # http://jasmine.github.io/1.3/introduction.html#section-Expectations\n    # Below is a simple example.\n\n    expect(\"apples\").toEqual(\"apples\")\n    expect(\"oranges\").not.toEqual(\"apples\")\n\ndescribe \"Editor::moveUp\", ->\n    ...\n
    \n

    Asynchronous Spcs

    \n

    Writing Asynchronous specs can be tricky at first, but a combination of spec helpers can make things easy. Here are a few quick examples:

    \n
    Promises
    \n

    You can use the global waitsForPromise function to make sure that the test does not complete until the returned promise has finished, and run your expectations in a chained promise.

    \n
      describe \"when requesting a Draft Session\", ->\n    it \"a session with the correct ID is returned\", ->\n      waitsForPromise ->\n        DraftStore.sessionForLocalId('123').then (session) ->\n          expect(session.id).toBe('123')\n
    \n

    This method can be used in the describe, it, beforeEach and afterEach functions.

    \n
    describe \"when we open a file\", ->\n  beforeEach ->\n    waitsForPromise ->\n      atom.workspace.open 'c.coffee'\n\n  it \"should be opened in an editor\", ->\n    expect(atom.workspace.getActiveTextEditor().getPath()).toContain 'c.coffee'\n
    \n

    If you need to wait for multiple promises use a new waitsForPromise function for each promise. (Caution: Without beforeEach this example will fail!)

    \n
    describe \"waiting for the packages to load\", ->\n\n  beforeEach ->\n    waitsForPromise ->\n      atom.workspace.open('sample.js')\n    waitsForPromise ->\n      atom.packages.activatePackage('tabs')\n    waitsForPromise ->\n      atom.packages.activatePackage('tree-view')\n\n  it 'should have waited long enough', ->\n    expect(atom.packages.isPackageActive('tabs')).toBe true\n    expect(atom.packages.isPackageActive('tree-view')).toBe true\n
    \n

    Asynchronous functions with callbacks

    \n

    Specs for asynchronous functions can be done using the waitsFor and runs functions. A simple example.

    \n
    describe \"fs.readdir(path, cb)\", ->\n  it \"is async\", ->\n    spy = jasmine.createSpy('fs.readdirSpy')\n\n    fs.readdir('/tmp/example', spy)\n    waitsFor ->\n      spy.callCount > 0\n    runs ->\n      exp = [null, ['example.coffee']]\n      expect(spy.mostRecentCall.args).toEqual exp\n      expect(spy).toHaveBeenCalledWith(null, ['example.coffee'])\n
    \n

    For a more detailed documentation on asynchronous tests please visit the http://jasmine.github.io/1.3/introduction.html#section-Asynchronous_Support)[Jasmine documentation].

    \n

    Tips for Debugging Specs

    \n

    To run a limited subset of specs use the fdescribe or fit methods. You can use those to focus a single spec or several specs. In the example above, focusing an individual spec looks like this:

    \n
    describe \"when a test is written\", ->\n  fit \"has some expectations that should pass\", ->\n    expect(\"apples\").toEqual(\"apples\")\n    expect(\"oranges\").not.toEqual(\"apples\")\n
    \n", - "meta": { - "Title": "Writing Specs", - "TitleHidden": true, - "Section": "Guides", - "Order": 7, - "title": "Writing Specs", - "titlehidden": true, - "section": "Guides", - "order": 7 - }, - "name": "Writing Specs", - "filename": "WritingSpecs.md", - "link": "WritingSpecs.html" - }, - { - "html": "

    Do I have to use React?

    \n

    The short answer is yes, you need to use React. The {ComponentRegistry} expects React components, so you'll need to create them to extend the N1 interface.

    \n

    However, if you have significant code already written in another framework, like Angular or Backbone, it's possible to attach your application to a React component. See https://github.com/davidchang/ngReact/issues/80.

    \n

    Can I write a package that does X?

    \n

    If you don't find documentation for the part of N1 you want to extend, let us know! We're constantly working to enable new workflows by making more of the application extensible.

    \n

    Can I distribute my package?

    \n

    Yes! We'll be sharing more information about publishing packages in the coming months. However, you can already publish and share packages by following the steps below:

    \n
      \n
    1. Create a Github repository for your package, and publish a Tag to the repository matching the version number in your package.json file. (Ex: 0.1.0)

      \n
    2. \n
    3. Make the CURL request below to the package manager server to register your package:

      \n
       curl -H \"Content-Type:application/json\" -X POST -d '{\"repository\":\"https://github.com/<username>/<repo>\"}' https://edgehill-packages.nylas.com/api/packages\n
    4. \n
    5. Your package will now appear when users visit the N1 settings page and search for community packages.

      \n
    6. \n
    \n

    Note: In the near future, we'll be formalizing the process of distributing packages, and packages you publish now may need to be resubmitted.

    \n", - "meta": { - "Title": "FAQ", - "Section": "Guides", - "Order": 10, - "title": "FAQ", - "section": "Guides", - "order": 10 - }, - "name": "FAQ", - "filename": "FAQ.md", - "link": "FAQ.html" - } - ] - }, - { - "name": "Sample Code", - "items": [ - { - "name": "Composer Translation", - "link": "https://github.com/nylas/edgehill-plugins/tree/master/translate", - "external": true - }, - { - "name": "Github Sidebar", - "link": "https://github.com/nylas/edgehill-plugins/tree/master/sidebar-github-profile", - "external": true - } - ] - }, - { - "name": "API Reference", - "items": [ - { - "name": "General", - "items": [ - { - "name": "Actions", - "link": "Actions.html" - }, - { - "name": "Atom", - "link": "Atom.html" - }, - { - "name": "BufferedNodeProcess", - "link": "BufferedNodeProcess.html" - }, - { - "name": "BufferedProcess", - "link": "BufferedProcess.html" - }, - { - "name": "ChangeFolderTask", - "link": "ChangeFolderTask.html" - }, - { - "name": "ChangeLabelsTask", - "link": "ChangeLabelsTask.html" - }, - { - "name": "Config", - "link": "Config.html" - }, - { - "name": "DraggableImg", - "link": "DraggableImg.html" - }, - { - "name": "FocusTrackingRegion", - "link": "FocusTrackingRegion.html" - }, - { - "name": "Switch", - "link": "Switch.html" - }, - { - "name": "Task", - "link": "Task.html" - }, - { - "name": "TaskQueueStatusStore", - "link": "TaskQueueStatusStore.html" - } - ] - }, - { - "name": "Component Kit", - "items": [ - { - "name": "EventedIFrame", - "link": "EventedIFrame.html" - }, - { - "name": "Flexbox", - "link": "Flexbox.html" - }, - { - "name": "InjectedComponent", - "link": "InjectedComponent.html" - }, - { - "name": "InjectedComponentSet", - "link": "InjectedComponentSet.html" - }, - { - "name": "Menu", - "link": "Menu.html" - }, - { - "name": "MenuItem", - "link": "MenuItem.html" - }, - { - "name": "MenuNameEmailItem", - "link": "MenuNameEmailItem.html" - }, - { - "name": "MultiselectActionBar", - "link": "MultiselectActionBar.html" - }, - { - "name": "MultiselectList", - "link": "MultiselectList.html" - }, - { - "name": "Popover", - "link": "Popover.html" - }, - { - "name": "ResizableRegion", - "link": "ResizableRegion.html" - }, - { - "name": "RetinaImg", - "link": "RetinaImg.html" - }, - { - "name": "Spinner", - "link": "Spinner.html" - }, - { - "name": "TimeoutTransitionGroupChild", - "link": "TimeoutTransitionGroupChild.html" - }, - { - "name": "UnsafeComponent", - "link": "UnsafeComponent.html" - } - ] - }, - { - "name": "Models", - "items": [ - { - "name": "Account", - "link": "Account.html" - }, - { - "name": "Calendar", - "link": "Calendar.html" - }, - { - "name": "Contact", - "link": "Contact.html" - }, - { - "name": "File", - "link": "File.html" - }, - { - "name": "Folder", - "link": "Folder.html" - }, - { - "name": "Label", - "link": "Label.html" - }, - { - "name": "Message", - "link": "Message.html" - }, - { - "name": "Model", - "link": "Model.html" - }, - { - "name": "Thread", - "link": "Thread.html" - } - ] - }, - { - "name": "Stores", - "items": [ - { - "name": "AccountStore", - "link": "AccountStore.html" - }, - { - "name": "ComponentRegistry", - "link": "ComponentRegistry.html" - }, - { - "name": "ContactStore", - "link": "ContactStore.html" - }, - { - "name": "EventStore", - "link": "EventStore.html" - }, - { - "name": "FocusedContentStore", - "link": "FocusedContentStore.html" - }, - { - "name": "MessageStoreExtension", - "link": "MessageStoreExtension.html" - }, - { - "name": "TaskQueue", - "link": "TaskQueue.html" - }, - { - "name": "WorkspaceStore", - "link": "WorkspaceStore.html" - } - ] - }, - { - "name": "Database", - "items": [ - { - "name": "Attribute", - "link": "Attribute.html" - }, - { - "name": "AttributeBoolean", - "link": "AttributeBoolean.html" - }, - { - "name": "AttributeCollection", - "link": "AttributeCollection.html" - }, - { - "name": "AttributeDateTime", - "link": "AttributeDateTime.html" - }, - { - "name": "AttributeJoinedData", - "link": "AttributeJoinedData.html" - }, - { - "name": "AttributeNumber", - "link": "AttributeNumber.html" - }, - { - "name": "AttributeObject", - "link": "AttributeObject.html" - }, - { - "name": "AttributeServerId", - "link": "AttributeServerId.html" - }, - { - "name": "AttributeString", - "link": "AttributeString.html" - }, - { - "name": "DatabaseStore", - "link": "DatabaseStore.html" - }, - { - "name": "DatabaseView", - "link": "DatabaseView.html" - }, - { - "name": "Matcher", - "link": "Matcher.html" - }, - { - "name": "ModelQuery", - "link": "ModelQuery.html" - }, - { - "name": "SortOrder", - "link": "SortOrder.html" - } - ] - }, - { - "name": "Drafts", - "items": [ - { - "name": "DraftChangeSet", - "link": "DraftChangeSet.html" - }, - { - "name": "DraftStore", - "link": "DraftStore.html" - }, - { - "name": "DraftStoreExtension", - "link": "DraftStoreExtension.html" - }, - { - "name": "DraftStoreProxy", - "link": "DraftStoreProxy.html" - } - ] - }, - { - "name": "Atom", - "items": [ - { - "name": "Clipboard", - "link": "Clipboard.html" - }, - { - "name": "Color", - "link": "Color.html" - }, - { - "name": "CommandRegistry", - "link": "CommandRegistry.html" - }, - { - "name": "MenuManager", - "link": "MenuManager.html" - }, - { - "name": "PackageManager", - "link": "PackageManager.html" - }, - { - "name": "ScopeDescriptor", - "link": "ScopeDescriptor.html" - }, - { - "name": "StyleManager", - "link": "StyleManager.html" - }, - { - "name": "ThemeManager", - "link": "ThemeManager.html" - } - ] - } - ] - } - ] -} \ No newline at end of file