mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-10 22:54:45 +08:00
add filters example
This commit is contained in:
parent
39266026d1
commit
0fa581e8bb
20 changed files with 1921 additions and 0 deletions
42
examples/N1-Filters/README.md
Normal file
42
examples/N1-Filters/README.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Filters package for Edgehill
|
||||
|
||||
<img src="https://raw.githubusercontent.com/nylas/edgehill-plugins/master/filters/filters-screencap.png?token=ABx0UZ4A4Qd2ikF3y6kwHOK3MX_ZEf1lks5WEr6WwA%3D%3D">
|
||||
|
||||
## Who?
|
||||
|
||||
The source is annotated for people who are familiar with React, but not familiar with APIs from either Atom or N1.
|
||||
|
||||
As such, we will not annotate any code that is specific for React, but we'll annotate code for everything else.
|
||||
|
||||
## Why?
|
||||
|
||||
There's no native way to automate mail filtering in Edgehill. This package provides a lightweight interface and implementation of mail filters and mail rules to handle repetitive mail tasks for you.
|
||||
|
||||
## How?
|
||||
|
||||
This package works in two steps: managing the filters and applying the filters.
|
||||
|
||||
Managing the filters boils down to simple CRUD operations.
|
||||
|
||||
Applying the filters boils down to checking each incoming message, checking to see if the message matches any of the requirements for the filters, and, if there's a match, applying the actions on the thread.
|
||||
|
||||
Currently, this package supports only simple filter operations. The only criteria it supports are:
|
||||
- exact match sender email
|
||||
- exact match recipient email
|
||||
- substring match with subject & body
|
||||
- substring absense with subject & body
|
||||
|
||||
The only actions this package supports currently are:
|
||||
- Marking as read
|
||||
- Applying labels or folders
|
||||
- Starring
|
||||
- Deleting
|
||||
- Archiving (skipping the inbox)
|
||||
|
||||
## Roadmap?
|
||||
|
||||
Right now, both managing the filters and applying the filters is done client-side.
|
||||
|
||||
The immediate objective is to implement an amazing user experience for managing mail filters.
|
||||
|
||||
The long-term objective is to remove the client-side implementation of applying filters and move this work to the backend.
|
518
examples/N1-Filters/docs/docco.css
Normal file
518
examples/N1-Filters/docs/docco.css
Normal file
|
@ -0,0 +1,518 @@
|
|||
/*--------------------- Typography ----------------------------*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'aller-light';
|
||||
src: url('public/fonts/aller-light.eot');
|
||||
src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'),
|
||||
url('public/fonts/aller-light.woff') format('woff'),
|
||||
url('public/fonts/aller-light.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'aller-bold';
|
||||
src: url('public/fonts/aller-bold.eot');
|
||||
src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'),
|
||||
url('public/fonts/aller-bold.woff') format('woff'),
|
||||
url('public/fonts/aller-bold.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'roboto-black';
|
||||
src: url('public/fonts/roboto-black.eot');
|
||||
src: url('public/fonts/roboto-black.eot?#iefix') format('embedded-opentype'),
|
||||
url('public/fonts/roboto-black.woff') format('woff'),
|
||||
url('public/fonts/roboto-black.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/*--------------------- Layout ----------------------------*/
|
||||
html { height: 100%; }
|
||||
body {
|
||||
font-family: "aller-light";
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: #30404f;
|
||||
margin: 0; padding: 0;
|
||||
height:100%;
|
||||
}
|
||||
#container { min-height: 100%; }
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: normal;
|
||||
font-family: "aller-bold";
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 15px 0 0px;
|
||||
}
|
||||
.annotation ul, .annotation ol {
|
||||
margin: 25px 0;
|
||||
}
|
||||
.annotation ul li, .annotation ol li {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #112233;
|
||||
line-height: 1em;
|
||||
font-weight: normal;
|
||||
font-family: "roboto-black";
|
||||
text-transform: uppercase;
|
||||
margin: 30px 0 15px 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 40px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.26em;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
background: 1px #ddd;
|
||||
height: 1px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
pre, tt, code {
|
||||
font-size: 12px; line-height: 16px;
|
||||
font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace;
|
||||
margin: 0; padding: 0;
|
||||
}
|
||||
.annotation pre {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
background: #fcfcfc;
|
||||
-moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
|
||||
-webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
|
||||
box-shadow: inset 0 0 10px rgba(0,0,0,0.1);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.annotation pre code {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
blockquote {
|
||||
border-left: 5px solid #ccc;
|
||||
margin: 0;
|
||||
padding: 1px 0 1px 1em;
|
||||
}
|
||||
.sections blockquote p {
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
font-size: 12px; line-height: 16px;
|
||||
color: #999;
|
||||
margin: 10px 0 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
ul.sections {
|
||||
list-style: none;
|
||||
padding:0 0 5px 0;;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
/*
|
||||
Force border-box so that % widths fit the parent
|
||||
container without overlap because of margin/padding.
|
||||
|
||||
More Info : http://www.quirksmode.org/css/box.html
|
||||
*/
|
||||
ul.sections > li > div {
|
||||
-moz-box-sizing: border-box; /* firefox */
|
||||
-ms-box-sizing: border-box; /* ie */
|
||||
-webkit-box-sizing: border-box; /* webkit */
|
||||
-khtml-box-sizing: border-box; /* konqueror */
|
||||
box-sizing: border-box; /* css3 */
|
||||
}
|
||||
|
||||
|
||||
/*---------------------- Jump Page -----------------------------*/
|
||||
#jump_to, #jump_page {
|
||||
margin: 0;
|
||||
background: white;
|
||||
-webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777;
|
||||
-webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px;
|
||||
font: 16px Arial;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#jump_to a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#jump_to a.large {
|
||||
display: none;
|
||||
}
|
||||
#jump_to a.small {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #676767;
|
||||
}
|
||||
|
||||
#jump_to, #jump_wrapper {
|
||||
position: fixed;
|
||||
right: 0; top: 0;
|
||||
padding: 10px 15px;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
#jump_wrapper {
|
||||
display: none;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
#jump_to:hover #jump_wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#jump_page_wrapper{
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#jump_page {
|
||||
padding: 5px 0 3px;
|
||||
margin: 0 0 25px 25px;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#jump_page .source {
|
||||
display: block;
|
||||
padding: 15px;
|
||||
text-decoration: none;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
#jump_page .source:hover {
|
||||
background: #f5f5ff;
|
||||
}
|
||||
|
||||
#jump_page .source:first-child {
|
||||
}
|
||||
|
||||
/*---------------------- Low resolutions (> 320px) ---------------------*/
|
||||
@media only screen and (min-width: 320px) {
|
||||
.pilwrap { display: none; }
|
||||
|
||||
ul.sections > li > div {
|
||||
display: block;
|
||||
padding:5px 10px 0 10px;
|
||||
}
|
||||
|
||||
ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
ul.sections > li > div.content {
|
||||
overflow-x:auto;
|
||||
-webkit-box-shadow: inset 0 0 5px #e5e5ee;
|
||||
box-shadow: inset 0 0 5px #e5e5ee;
|
||||
border: 1px solid #dedede;
|
||||
margin:5px 10px 5px 10px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
ul.sections > li > div.annotation pre {
|
||||
margin: 7px 0 7px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
ul.sections > li > div.annotation p tt, .annotation code {
|
||||
background: #f8f8ff;
|
||||
border: 1px solid #dedede;
|
||||
font-size: 12px;
|
||||
padding: 0 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------------- (> 481px) ---------------------*/
|
||||
@media only screen and (min-width: 481px) {
|
||||
#container {
|
||||
position: relative;
|
||||
}
|
||||
body {
|
||||
background-color: #F5F5FF;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
}
|
||||
pre, tt, code {
|
||||
line-height: 18px;
|
||||
}
|
||||
p, ul, ol {
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
|
||||
#jump_to {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
#jump_wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
#jump_to, #jump_page {
|
||||
font: 10px Arial;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
#jump_page .source {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
#jump_to a.large {
|
||||
display: inline-block;
|
||||
}
|
||||
#jump_to a.small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#background {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
width: 350px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e5e5ee;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
ul.sections > li {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
ul.sections > li > div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
ul.sections > li > div.annotation {
|
||||
max-width: 350px;
|
||||
min-width: 350px;
|
||||
min-height: 5px;
|
||||
padding: 13px;
|
||||
overflow-x: hidden;
|
||||
white-space: normal;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
ul.sections > li > div.annotation pre {
|
||||
margin: 15px 0 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
ul.sections > li > div.content {
|
||||
padding: 13px;
|
||||
vertical-align: top;
|
||||
border: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pilwrap {
|
||||
position: relative;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.pilcrow {
|
||||
font: 12px Arial;
|
||||
text-decoration: none;
|
||||
color: #454545;
|
||||
position: absolute;
|
||||
top: 3px; left: -20px;
|
||||
padding: 1px 2px;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
}
|
||||
.for-h1 .pilcrow {
|
||||
top: 47px;
|
||||
}
|
||||
.for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow {
|
||||
top: 35px;
|
||||
}
|
||||
|
||||
ul.sections > li > div.annotation:hover .pilcrow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------------- (> 1025px) ---------------------*/
|
||||
@media only screen and (min-width: 1025px) {
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
#background {
|
||||
width: 525px;
|
||||
}
|
||||
ul.sections > li > div.annotation {
|
||||
max-width: 525px;
|
||||
min-width: 525px;
|
||||
padding: 10px 25px 1px 50px;
|
||||
}
|
||||
ul.sections > li > div.content {
|
||||
padding: 9px 15px 16px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------------- Syntax Highlighting -----------------------------*/
|
||||
|
||||
td.linenos { background-color: #f0f0f0; padding-right: 10px; }
|
||||
span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }
|
||||
/*
|
||||
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
|
||||
*/
|
||||
|
||||
pre code {
|
||||
display: block; padding: 0.5em;
|
||||
color: #000;
|
||||
background: #f8f8ff
|
||||
}
|
||||
|
||||
pre .hljs-comment,
|
||||
pre .hljs-template_comment,
|
||||
pre .hljs-diff .hljs-header,
|
||||
pre .hljs-javadoc {
|
||||
color: #408080;
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
pre .hljs-keyword,
|
||||
pre .hljs-assignment,
|
||||
pre .hljs-literal,
|
||||
pre .hljs-css .hljs-rule .hljs-keyword,
|
||||
pre .hljs-winutils,
|
||||
pre .hljs-javascript .hljs-title,
|
||||
pre .hljs-lisp .hljs-title,
|
||||
pre .hljs-subst {
|
||||
color: #954121;
|
||||
/*font-weight: bold*/
|
||||
}
|
||||
|
||||
pre .hljs-number,
|
||||
pre .hljs-hexcolor {
|
||||
color: #40a070
|
||||
}
|
||||
|
||||
pre .hljs-string,
|
||||
pre .hljs-tag .hljs-value,
|
||||
pre .hljs-phpdoc,
|
||||
pre .hljs-tex .hljs-formula {
|
||||
color: #219161;
|
||||
}
|
||||
|
||||
pre .hljs-title,
|
||||
pre .hljs-id {
|
||||
color: #19469D;
|
||||
}
|
||||
pre .hljs-params {
|
||||
color: #00F;
|
||||
}
|
||||
|
||||
pre .hljs-javascript .hljs-title,
|
||||
pre .hljs-lisp .hljs-title,
|
||||
pre .hljs-subst {
|
||||
font-weight: normal
|
||||
}
|
||||
|
||||
pre .hljs-class .hljs-title,
|
||||
pre .hljs-haskell .hljs-label,
|
||||
pre .hljs-tex .hljs-command {
|
||||
color: #458;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
pre .hljs-tag,
|
||||
pre .hljs-tag .hljs-title,
|
||||
pre .hljs-rules .hljs-property,
|
||||
pre .hljs-django .hljs-tag .hljs-keyword {
|
||||
color: #000080;
|
||||
font-weight: normal
|
||||
}
|
||||
|
||||
pre .hljs-attribute,
|
||||
pre .hljs-variable,
|
||||
pre .hljs-instancevar,
|
||||
pre .hljs-lisp .hljs-body {
|
||||
color: #008080
|
||||
}
|
||||
|
||||
pre .hljs-regexp {
|
||||
color: #B68
|
||||
}
|
||||
|
||||
pre .hljs-class {
|
||||
color: #458;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
pre .hljs-symbol,
|
||||
pre .hljs-ruby .hljs-symbol .hljs-string,
|
||||
pre .hljs-ruby .hljs-symbol .hljs-keyword,
|
||||
pre .hljs-ruby .hljs-symbol .hljs-keymethods,
|
||||
pre .hljs-lisp .hljs-keyword,
|
||||
pre .hljs-tex .hljs-special,
|
||||
pre .hljs-input_number {
|
||||
color: #990073
|
||||
}
|
||||
|
||||
pre .hljs-builtin,
|
||||
pre .hljs-constructor,
|
||||
pre .hljs-built_in,
|
||||
pre .hljs-lisp .hljs-title {
|
||||
color: #0086b3
|
||||
}
|
||||
|
||||
pre .hljs-preprocessor,
|
||||
pre .hljs-pi,
|
||||
pre .hljs-doctype,
|
||||
pre .hljs-shebang,
|
||||
pre .hljs-cdata {
|
||||
color: #999;
|
||||
font-weight: bold
|
||||
}
|
||||
|
||||
pre .hljs-deletion {
|
||||
background: #fdd
|
||||
}
|
||||
|
||||
pre .hljs-addition {
|
||||
background: #dfd
|
||||
}
|
||||
|
||||
pre .hljs-diff .hljs-change {
|
||||
background: #0086b3
|
||||
}
|
||||
|
||||
pre .hljs-chunk {
|
||||
color: #aaa
|
||||
}
|
||||
|
||||
pre .hljs-tex .hljs-formula {
|
||||
opacity: 0.5;
|
||||
}
|
170
examples/N1-Filters/docs/main.html
Normal file
170
examples/N1-Filters/docs/main.html
Normal file
|
@ -0,0 +1,170 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Filters</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
|
||||
<link rel="stylesheet" media="all" href="docco.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="background"></div>
|
||||
|
||||
<ul class="sections">
|
||||
|
||||
|
||||
|
||||
<li id="section-1">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-1">¶</a>
|
||||
</div>
|
||||
<h1 id="filters">Filters</h1>
|
||||
<p>A way to apply filters, AKA mail rules, to incoming mail.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre>
|
||||
Filters = <span class="hljs-built_in">require</span> <span class="hljs-string">'./filters'</span></pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li id="section-2">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-2">¶</a>
|
||||
</div>
|
||||
<p>Requiring ‘nylas-exports’ is the way to access core N1 components.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre>{WorkspaceStore, ComponentRegistry} = <span class="hljs-built_in">require</span> <span class="hljs-string">'nylas-exports'</span></pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li id="section-3">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-3">¶</a>
|
||||
</div>
|
||||
<p>Your main.coffee (or main.cjsx) file needs to export an object for your
|
||||
package to run.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre><span class="hljs-built_in">module</span>.exports =</pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li id="section-4">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-4">¶</a>
|
||||
</div>
|
||||
<p>When your package is loading, the <code>activate</code> method runs. <code>activate</code> is the
|
||||
package’s time to insert React components into the applicatio and also
|
||||
listen to events.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre> <span class="hljs-attribute">activate</span>: <span class="hljs-function">-></span></pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li id="section-5">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-5">¶</a>
|
||||
</div>
|
||||
<p><code>WorkspaceStore.defineSheet</code> creates an N1 “sheet,” which is a large area
|
||||
for you to inject React components. Sheets span the whole window.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre> WorkspaceStore.defineSheet <span class="hljs-string">'Filters'</span>, {<span class="hljs-attribute">root</span>: <span class="hljs-literal">true</span>, <span class="hljs-attribute">name</span>: <span class="hljs-string">'Filters'</span>},
|
||||
<span class="hljs-attribute">list</span>: [<span class="hljs-string">'RootSidebar'</span>, <span class="hljs-string">'Filters'</span>]</pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li id="section-6">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-6">¶</a>
|
||||
</div>
|
||||
<p>Above, we named the sheet “Filters,” and we’re registering a React
|
||||
component to live inside the “Filters” sheet.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre> ComponentRegistry.register Filters,
|
||||
<span class="hljs-attribute">location</span>: WorkspaceStore.Location.Filters</pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li id="section-7">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-7">¶</a>
|
||||
</div>
|
||||
<p><code>WorkspaceStore.SidebarItem</code> is a React component which is meant to be
|
||||
inserted into the navigation bar on the left of the main worksheet.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre> <span class="hljs-property">@sidebarItem</span> = <span class="hljs-keyword">new</span> WorkspaceStore.SidebarItem
|
||||
<span class="hljs-attribute">sheet</span>: WorkspaceStore.Sheet.Filters
|
||||
<span class="hljs-attribute">id</span>: <span class="hljs-string">'Filters'</span>
|
||||
<span class="hljs-attribute">name</span>: <span class="hljs-string">'Filters'</span></pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li id="section-8">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-8">¶</a>
|
||||
</div>
|
||||
<p>And this is how we actually insert the SidebarItem into the sheet!</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre> WorkspaceStore.addSidebarItem(<span class="hljs-property">@sidebarItem</span>)</pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li id="section-9">
|
||||
<div class="annotation">
|
||||
|
||||
<div class="pilwrap ">
|
||||
<a class="pilcrow" href="#section-9">¶</a>
|
||||
</div>
|
||||
<p><code>deactivate</code> is called when packages are closing. It’s a good time to
|
||||
unregister React components.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="content"><div class='highlight'><pre> <span class="hljs-attribute">deactivate</span>: <span class="hljs-function">-></span>
|
||||
ComponentRegistry.unregister Filters</pre></div></div>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
examples/N1-Filters/docs/public/fonts/aller-bold.eot
Normal file
BIN
examples/N1-Filters/docs/public/fonts/aller-bold.eot
Normal file
Binary file not shown.
BIN
examples/N1-Filters/docs/public/fonts/aller-bold.ttf
Normal file
BIN
examples/N1-Filters/docs/public/fonts/aller-bold.ttf
Normal file
Binary file not shown.
BIN
examples/N1-Filters/docs/public/fonts/aller-bold.woff
Normal file
BIN
examples/N1-Filters/docs/public/fonts/aller-bold.woff
Normal file
Binary file not shown.
BIN
examples/N1-Filters/docs/public/fonts/aller-light.eot
Normal file
BIN
examples/N1-Filters/docs/public/fonts/aller-light.eot
Normal file
Binary file not shown.
BIN
examples/N1-Filters/docs/public/fonts/aller-light.ttf
Normal file
BIN
examples/N1-Filters/docs/public/fonts/aller-light.ttf
Normal file
Binary file not shown.
BIN
examples/N1-Filters/docs/public/fonts/aller-light.woff
Normal file
BIN
examples/N1-Filters/docs/public/fonts/aller-light.woff
Normal file
Binary file not shown.
BIN
examples/N1-Filters/docs/public/fonts/roboto-black.eot
Executable file
BIN
examples/N1-Filters/docs/public/fonts/roboto-black.eot
Executable file
Binary file not shown.
BIN
examples/N1-Filters/docs/public/fonts/roboto-black.ttf
Executable file
BIN
examples/N1-Filters/docs/public/fonts/roboto-black.ttf
Executable file
Binary file not shown.
BIN
examples/N1-Filters/docs/public/fonts/roboto-black.woff
Executable file
BIN
examples/N1-Filters/docs/public/fonts/roboto-black.woff
Executable file
Binary file not shown.
375
examples/N1-Filters/docs/public/stylesheets/normalize.css
vendored
Normal file
375
examples/N1-Filters/docs/public/stylesheets/normalize.css
vendored
Normal file
|
@ -0,0 +1,375 @@
|
|||
/*! normalize.css v2.0.1 | MIT License | git.io/normalize */
|
||||
|
||||
/* ==========================================================================
|
||||
HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Corrects `block` display not defined in IE 8/9.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Corrects `inline-block` display not defined in IE 8/9.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
video {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Prevents modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses styling for `hidden` attribute not present in IE 8/9.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Base
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* 1. Sets default font family to sans-serif.
|
||||
* 2. Prevents iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Links
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Addresses `outline` inconsistency between Chrome and other browsers.
|
||||
*/
|
||||
|
||||
a:focus {
|
||||
outline: thin dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
* Improves readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Typography
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Addresses `h1` font sizes within `section` and `article` in Firefox 4+,
|
||||
* Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses styling not present in IE 8/9, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses styling not present in Safari 5 and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Corrects font family set oddly in Safari 5 and Chrome.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/*
|
||||
* Improves readability of pre-formatted text in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sets consistent quote types.
|
||||
*/
|
||||
|
||||
q {
|
||||
quotes: "\201C" "\201D" "\2018" "\2019";
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
* Prevents `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Removes border when inside `a` element in IE 8/9.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Corrects overflow displayed oddly in IE 9.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Figures
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Addresses margin not present in IE 8/9 and Safari 5.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Forms
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Corrects color not being inherited in IE 8/9.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Corrects font family not being inherited in all browsers.
|
||||
* 2. Corrects font size not being inherited in all browsers.
|
||||
* 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
* Addresses Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Corrects inability to style clickable `input` types in iOS.
|
||||
* 3. Improves usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Addresses box sizing set to `content-box` in IE 8/9.
|
||||
* 2. Removes excess padding in IE 8/9.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||
* 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes inner padding and search cancel button in Safari 5 and Chrome
|
||||
* on OS X.
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Removes default vertical scrollbar in IE 8/9.
|
||||
* 2. Improves readability and alignment in all browsers.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto; /* 1 */
|
||||
vertical-align: top; /* 2 */
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Tables
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
BIN
examples/N1-Filters/filters-screencap.png
Normal file
BIN
examples/N1-Filters/filters-screencap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 316 KiB |
186
examples/N1-Filters/lib/filters-store.coffee
Normal file
186
examples/N1-Filters/lib/filters-store.coffee
Normal file
|
@ -0,0 +1,186 @@
|
|||
NylasStore = require 'nylas-store'
|
||||
_ = require 'underscore'
|
||||
_s = require 'underscore.string'
|
||||
{Actions, CategoryStore, AccountStore, ChangeLabelsTask,
|
||||
ChangeFolderTask, ArchiveThreadHelper, ChangeStarredTask,
|
||||
ChangeUnreadTask, Utils} = require 'nylas-exports'
|
||||
|
||||
# The FiltersStore performs all business logic for filters: the single source
|
||||
# of truth for any other code using filters, the gateway to persisting data
|
||||
# for filters, the subscriber to Actions which affect filters, and the
|
||||
# publisher for all React components which render filters.
|
||||
class FiltersStore extends NylasStore
|
||||
|
||||
# The store's instantiation is the best time during the store life cycle
|
||||
# to both set the store's initial state and also subscribe to Actions which
|
||||
# will be published elsewhere.
|
||||
constructor: ->
|
||||
|
||||
# ...here, we're setting initial state...
|
||||
@_filters = @_loadFilters()
|
||||
|
||||
# ...and here, we're subscribing to Actions which could be fired by React
|
||||
# components, other stores, or any other part of the application.
|
||||
@listenTo Actions.deleteFilter, @_onDeleteFilter
|
||||
@listenTo Actions.didPassivelyReceiveNewModels, @_onNewModels
|
||||
@listenTo Actions.saveFilter, @_onSaveFilter
|
||||
|
||||
# This method is the application's single source of truth for filters.
|
||||
# All FiltersStore consumers will invoke it to get the canonical filters at
|
||||
# the present moment.
|
||||
filters: =>
|
||||
@_filters
|
||||
|
||||
# The callback for Action.deleteFilter. This action's publishers will pass to
|
||||
# the callback a filter id for the filter to be deleted.
|
||||
_onDeleteFilter: (id) =>
|
||||
newFilters = @_filters.filter (f) ->
|
||||
f.id isnt id
|
||||
@_writeAndPublishChanges newFilters
|
||||
|
||||
# The callback for Action.saveFilter. This action's publishers will pass a
|
||||
# filter object. If the published object contains an id, then we assume we're
|
||||
# updating an existing filter. Otherwise, we assume we're creating a new one.
|
||||
_onSaveFilter: (filter) =>
|
||||
updatingExistingFilter = !!filter.id
|
||||
|
||||
if updatingExistingFilter
|
||||
updatedFilter = _.find @_filters, (f) -> f.id is filter.id
|
||||
index = _.indexOf @_filters, updatedFilter
|
||||
@_filters[index] = filter
|
||||
else
|
||||
filter.id = Utils.generateTempId()
|
||||
@_filters.push filter
|
||||
|
||||
@_writeAndPublishChanges @_filters
|
||||
|
||||
_writeAndPublishChanges: (filters) =>
|
||||
@_saveFilters filters
|
||||
@_filters = filters
|
||||
|
||||
# @trigger publishes to all React components subscribed to the FiltersStore.
|
||||
# This tells the React components that the store's underlying data has
|
||||
# changed. React components will update according to the new changes.
|
||||
@trigger()
|
||||
|
||||
# For filters, an `action` is performed when an incoming message matches a
|
||||
# filter's criteria. An action could be marking the message as read. These
|
||||
# actions are just N1 `Task` instances which will be queued to run by
|
||||
# `Actions.queueTask`.
|
||||
_makeActions: (filters, thread) ->
|
||||
_.chain filters
|
||||
.pluck 'actions'
|
||||
.map _.pairs
|
||||
.flatten true
|
||||
.map ([action, val]) ->
|
||||
if action is "applyLabel"
|
||||
label = _.find CategoryStore.getUserCategories(), (c) ->
|
||||
c.id is val
|
||||
new ChangeLabelsTask
|
||||
labelsToAdd: [label]
|
||||
threads: [thread]
|
||||
else if action is "applyFolder"
|
||||
folder = _.find CategoryStore.getUserCategories(), (c) ->
|
||||
c.id is val
|
||||
new ChangeFolderTask
|
||||
folder: folder
|
||||
threads: [thread]
|
||||
else if action is "markAsRead" and val is true
|
||||
new ChangeUnreadTask
|
||||
unread: false
|
||||
threads: [thread]
|
||||
else if action is "archive" and val is true
|
||||
ArchiveThreadHelper.getArchiveTask [thread]
|
||||
else if action is "star" and val is true
|
||||
new ChangeStarredTask
|
||||
starred: true
|
||||
threads: [thread]
|
||||
else if action is "delete" and val is true
|
||||
trash = CategoryStore.getStandardCategory "trash"
|
||||
|
||||
# Some email providers use labels, like Gmail, and others use folders,
|
||||
# like Microsoft Exchange. Labels and folders behave very differently,
|
||||
# so there are different Task classes to modify records for them.
|
||||
if AccountStore.current().usesFolders()
|
||||
new ChangeFolderTask
|
||||
folder: trash
|
||||
threads: [thread]
|
||||
else
|
||||
new ChangeLabelsTask
|
||||
labelsToAdd: [trash]
|
||||
threads: [thread]
|
||||
.value()
|
||||
|
||||
_getPassedFilters: ({message, thread}) =>
|
||||
@_filters.filter ({criteria}) ->
|
||||
_.every criteria, (val, criterion) ->
|
||||
if criterion is "from"
|
||||
_.find message.from, (contact) -> contact.email is val
|
||||
else if criterion is "to"
|
||||
_.find message.to, (contact) -> contact.email is val
|
||||
else if criterion is "subject"
|
||||
_s.contains thread.subject, val
|
||||
else if criterion is "has-words"
|
||||
_s.contains(thread.subject, val) or _s.contains(message.body, val)
|
||||
else if criterion is "doesnt-have"
|
||||
not _s.contains(thread.subject, val) and
|
||||
not _s.contains(message.body, val)
|
||||
|
||||
_getFilterActions: (incoming) =>
|
||||
|
||||
# The data structure representing all incoming models is a key-value hash
|
||||
# with the model type as the key and an array of models as the value. Here,
|
||||
# we're just accessing the models themselves from the `incoming` data
|
||||
# structure.
|
||||
message = incoming.message[0]
|
||||
thread = incoming.thread[0]
|
||||
|
||||
passedFilters = @_getPassedFilters {message, thread}
|
||||
@_makeActions passedFilters, thread
|
||||
|
||||
# The callback for Action.didPassivelyReceiveNewModels, a global action which
|
||||
# is published every time the application receives new data from the server.
|
||||
_onNewModels: (incoming) =>
|
||||
|
||||
# We ignore most incoming models, unless it's an incoming thread and
|
||||
# message. Those are the only models which are relevant to filters.
|
||||
if incoming.thread and incoming.message
|
||||
actions = @_getFilterActions incoming
|
||||
|
||||
# Actions.queueTask will take N1 tasks, which we generically call
|
||||
# `actions` in this method, and implement all remote-client syncing work.
|
||||
# Actions.queueTask is N1's way of creating, updating, and deleting
|
||||
# records in the backend while maintaining canonical data in the frontend.
|
||||
Actions.queueTask(action) for action in actions
|
||||
|
||||
# The filters are stored in the config.cson file.
|
||||
_loadFilters: =>
|
||||
atom.config.get('filters') ? []
|
||||
|
||||
# Rewrite the filters to the config.cson file.
|
||||
_saveFilters: (filters) =>
|
||||
filters = @_trimFilters filters
|
||||
|
||||
if @_validateFilters(filters)
|
||||
atom.config.set 'filters', filters
|
||||
else
|
||||
throw new Error("invalid filters")
|
||||
|
||||
# Prune the filters data for saving. We don't want to save malformed data!
|
||||
_trimFilters: (filters) =>
|
||||
for filter in filters
|
||||
for attr in ["criteria", "actions"]
|
||||
for key, val of filter[attr]
|
||||
if not val
|
||||
delete filter[attr][key]
|
||||
|
||||
return filters
|
||||
|
||||
# Simple validation to be run when _saveFilters is invoked.
|
||||
_validateFilters: (filters) =>
|
||||
Array.isArray(filters) and filters.every (f) ->
|
||||
f.id? and f.criteria? and f.actions?
|
||||
|
||||
|
||||
# A best practice is to export an instance of the FiltersStore, NOT the class!
|
||||
module.exports = new FiltersStore()
|
272
examples/N1-Filters/lib/filters.cjsx
Normal file
272
examples/N1-Filters/lib/filters.cjsx
Normal file
|
@ -0,0 +1,272 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
# Flux uses Stores to perform business logic and be the single source of truth
|
||||
# for data to render in the application.
|
||||
FiltersStore = require './filters-store'
|
||||
{CategoryStore, Actions, Utils} = require 'nylas-exports'
|
||||
# `RetinaImg` is a React component for optimistically rendering images for
|
||||
# Retina displays but falling back on normal images if needed.
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class Filters extends React.Component
|
||||
# Having a `@displayName` is a React best practice to make debugging easier.
|
||||
@displayName: 'Filters'
|
||||
|
||||
# Setting the component's initial state.
|
||||
constructor: ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
# For Flux, we want the Stores to publish its changes to all subscribed React
|
||||
# components. React components can subscribe to views in `componentDidMount`.
|
||||
# When they receive new changes from stores, then the component's callbacks
|
||||
# fire.
|
||||
componentDidMount: =>
|
||||
@_unsubscribers = []
|
||||
@_unsubscribers.push FiltersStore.listen @_onFiltersChange
|
||||
@_unsubscribers.push CategoryStore.listen @_onCategoriesChange
|
||||
|
||||
# Don't forget to unsubscribe from your stores on `componentWillUnmount`!
|
||||
# If you don't, the callbacks will still exist, but the components that the
|
||||
# callbacks are trying to update won't exist anymore. React will then throw
|
||||
# an exception.
|
||||
componentWillUnmount: =>
|
||||
unsubscribe() for unsubscribe in @_unsubscribers
|
||||
|
||||
render: =>
|
||||
if @state.focusedFilter
|
||||
|
||||
<div className="container-filters" style={padding: "0 15px"}>
|
||||
{@_renderFocusedFilter @state.focusedFilter}
|
||||
</div>
|
||||
|
||||
else
|
||||
|
||||
<div className="container-filters" style={padding: "0 15px"}>
|
||||
{@state.filters.map @_renderFilter}
|
||||
<div className="text-center" style={marginTop: 30}>
|
||||
<button className="btn btn-large"
|
||||
onClick={ => @_focus {} }>
|
||||
Create a new filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderFocusedFilter: ({criteria, actions}) =>
|
||||
criteria ?= {}
|
||||
actions ?= {}
|
||||
|
||||
# Gmail users have labels. Exchange users have Folders. They are implemented
|
||||
# differently and frequently require different functionality when you're
|
||||
# trying to manipulate either folders or labels.
|
||||
if CategoryStore.categoryLabel() is "Labels"
|
||||
applyCategory = <label>
|
||||
<input type="checkbox"
|
||||
checked={actions.applyLabel} />
|
||||
Apply the label:
|
||||
<select value={actions.applyLabel ? ""}
|
||||
onChange={(e) => @_changeAttr "actions", "applyLabel", e.target.value}>
|
||||
<option value="">Choose a label...</option>
|
||||
{@state.categories.map (c) =>
|
||||
<option value={c.id}>{c.displayName}</option>}
|
||||
</select>
|
||||
</label>
|
||||
else
|
||||
applyCategory = <label>
|
||||
<input type="checkbox"
|
||||
checked={actions.applyFolder} />
|
||||
Apply the folder:
|
||||
<select value={actions.applyFolder ? ""}
|
||||
onChange={(e) => @_changeAttr "actions", "applyFolder", e.target.value}>
|
||||
<option value="">Choose a folder...</option>
|
||||
{@state.categories.map (c) =>
|
||||
<option value={c.id}>{c.displayName}</option>}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<h4>Filter criteria:</h4>
|
||||
<label className="filter-input-label">From:</label>
|
||||
<div>
|
||||
<input type="text"
|
||||
className="filter-input"
|
||||
value={criteria.from}
|
||||
onChange={(e) => @_changeAttr "criteria", "from", e.target.value}
|
||||
placeholder="sender@email.com" />
|
||||
</div>
|
||||
<div style={clear: "both"}></div>
|
||||
<label className="filter-input-label">To:</label>
|
||||
<div>
|
||||
<input type="text"
|
||||
className="filter-input"
|
||||
value={criteria.to}
|
||||
onChange={(e) => @_changeAttr "criteria", "to", e.target.value}
|
||||
placeholder="recipient@email.com" />
|
||||
</div>
|
||||
<div style={clear: "both"}></div>
|
||||
<label className="filter-input-label">Subject:</label>
|
||||
<div>
|
||||
<input type="text"
|
||||
className="filter-input"
|
||||
value={criteria.subject}
|
||||
onChange={(e) => @_changeAttr "criteria", "subject", e.target.value}
|
||||
placeholder="subject contains this phrase" />
|
||||
</div>
|
||||
<div style={clear: "both"}></div>
|
||||
<label className="filter-input-label">Has words:</label>
|
||||
<div>
|
||||
<input type="text"
|
||||
className="filter-input"
|
||||
value={criteria["has-words"]}
|
||||
onChange={(e) => @_changeAttr "criteria", "has-words", e.target.value}
|
||||
placeholder="subject or body contains this phrase" />
|
||||
</div>
|
||||
<div style={clear: "both"}></div>
|
||||
<label className="filter-input-label">Doesn't have:</label>
|
||||
<div>
|
||||
<input type="text"
|
||||
className="filter-input"
|
||||
value={criteria["doesnt-have"]}
|
||||
onChange={(e) => @_changeAttr "criteria", "doesnt-have", e.target.value}
|
||||
placeholder="subject and body don't contain this phrase" />
|
||||
</div>
|
||||
<div style={clear: "both"}></div>
|
||||
|
||||
<h4>When a message arrives that matches this search:</h4>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={(e) => @_changeAttr "actions", "archive", e.target.checked}
|
||||
checked={actions.archive} />
|
||||
Skip the inbox (Archive it)
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={(e) => @_changeAttr "actions", "markAsRead", e.target.checked}
|
||||
checked={actions.markAsRead} />
|
||||
Mark as read
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={(e) => @_changeAttr "actions", "star", e.target.checked}
|
||||
checked={actions.star} />
|
||||
Star it
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
{applyCategory}
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
onChange={(e) => @_changeAttr "actions", "delete", e.target.checked}
|
||||
checked={actions.delete} />
|
||||
Delete it
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h1></h1>
|
||||
<div>
|
||||
<button className="btn pull-right"
|
||||
onClick={@_save}>
|
||||
Save filter
|
||||
</button>
|
||||
<button className="btn"
|
||||
onClick={@_unfocus}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderFilter: (filter) =>
|
||||
buttonStyles = paddingLeft: 15
|
||||
lineItemStyles =
|
||||
whiteSpace: "nowrap"
|
||||
overflow: "auto"
|
||||
|
||||
<div className="filter-item" key={filter.id}>
|
||||
<div>
|
||||
<div className="pull-right action-button"
|
||||
onClick={ => @_focus filter }>edit</div>
|
||||
<div className="line-item">
|
||||
<span>Matches: </span>
|
||||
<strong>{@_criteriaDisplay filter.criteria}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="pull-right action-button"
|
||||
onClick={ => @_delete filter.id }>delete</div>
|
||||
<div className="line-item">
|
||||
<span>Do this: </span>
|
||||
{@_actionsDisplay filter.actions}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_changeAttr: (attr1, attr2, val) =>
|
||||
f = @state.focusedFilter
|
||||
f[attr1] ?= {}
|
||||
f[attr1][attr2] = val
|
||||
@setState focusedFilter: f
|
||||
|
||||
_focus: (filter) =>
|
||||
@setState focusedFilter: Utils.deepClone(filter)
|
||||
|
||||
_unfocus: =>
|
||||
@setState focusedFilter: null
|
||||
|
||||
_delete: (id) =>
|
||||
# React components trigger changes by firing Actions, which is simply
|
||||
# invoking an Actions function with relevant data as arguments. Stores will
|
||||
# listen to Actions and update themselves accordingly.
|
||||
Actions.deleteFilter id;
|
||||
|
||||
_save: =>
|
||||
Actions.saveFilter @state.focusedFilter
|
||||
@_unfocus()
|
||||
|
||||
_criterionDisplay: (val, criterion) ->
|
||||
"#{criterion}(#{val})"
|
||||
|
||||
_criteriaDisplay: (criteria) =>
|
||||
_.map(criteria, @_criterionDisplay)
|
||||
.join(" ")
|
||||
|
||||
_actionDisplay: (val, action) =>
|
||||
if action is "applyLabel"
|
||||
category = _.find @state.categories, (c) ->
|
||||
c.id is val
|
||||
"Apply label \"#{category.displayName}\""
|
||||
else if action is "applyFolder"
|
||||
category = _.find @state.categories, (c) ->
|
||||
c.id is val
|
||||
"Apply folder \"#{category.displayName}\""
|
||||
else if action is "markAsRead" and val is true
|
||||
"Mark as read"
|
||||
else if action is "archive" and val is true
|
||||
"Skip the inbox (Archive it)"
|
||||
else if action is "star" and val is true
|
||||
"Star it"
|
||||
else if action is "delete" and val is true
|
||||
"Delete it"
|
||||
|
||||
_actionsDisplay: (actions) =>
|
||||
_.map(actions, @_actionDisplay)
|
||||
.join(", ")
|
||||
|
||||
_getStateFromStores: =>
|
||||
# A common N1 pattern is to dedicate a method solely to generating state,
|
||||
# like right here. It's usually called by the constructor and by listener
|
||||
# callbacks to Stores.
|
||||
filters: FiltersStore.filters()
|
||||
categories: CategoryStore.getUserCategories()
|
||||
|
||||
# Here's the callback that fires after Stores publish changes! `@setState`
|
||||
# will trigger a render with new data.
|
||||
_onFiltersChange: =>
|
||||
@setState @_getStateFromStores()
|
||||
|
||||
module.exports = Filters
|
272
examples/N1-Filters/lib/filters.coffee
Normal file
272
examples/N1-Filters/lib/filters.coffee
Normal file
|
@ -0,0 +1,272 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
# Flux uses Stores to perform business logic and be the single source of truth
|
||||
# for data to render in the application.
|
||||
FiltersStore = require './filters-store'
|
||||
{CategoryStore, Actions, Utils} = require 'nylas-exports'
|
||||
# `RetinaImg` is a React component for optimistically rendering images for
|
||||
# Retina displays but falling back on normal images if needed.
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class Filters extends React.Component
|
||||
# Having a `@displayName` is a React best practice to make debugging easier.
|
||||
@displayName: 'Filters'
|
||||
|
||||
# Setting the component's initial state.
|
||||
constructor: ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
# For Flux, we want the Stores to publish its changes to all subscribed React
|
||||
# components. React components can subscribe to views in `componentDidMount`.
|
||||
# When they receive new changes from stores, then the component's callbacks
|
||||
# fire.
|
||||
componentDidMount: =>
|
||||
@_unsubscribers = []
|
||||
@_unsubscribers.push FiltersStore.listen @_onFiltersChange
|
||||
@_unsubscribers.push CategoryStore.listen @_onCategoriesChange
|
||||
|
||||
# Don't forget to unsubscribe from your stores on `componentWillUnmount`!
|
||||
# If you don't, the callbacks will still exist, but the components that the
|
||||
# callbacks are trying to update won't exist anymore. React will then throw
|
||||
# an exception.
|
||||
componentWillUnmount: =>
|
||||
unsubscribe() for unsubscribe in @_unsubscribers
|
||||
|
||||
render: =>
|
||||
if @state.focusedFilter
|
||||
|
||||
React.createElement("div", {"className": "container-filters", "style": (padding: "0 15px")},
|
||||
(@_renderFocusedFilter @state.focusedFilter)
|
||||
)
|
||||
|
||||
else
|
||||
|
||||
React.createElement("div", {"className": "container-filters", "style": (padding: "0 15px")},
|
||||
(@state.filters.map @_renderFilter),
|
||||
React.createElement("div", {"className": "text-center", "style": (marginTop: 30)},
|
||||
React.createElement("button", {"className": "btn btn-large", \
|
||||
"onClick": ( => @_focus {} )}, """
|
||||
Create a new filter
|
||||
""")
|
||||
)
|
||||
)
|
||||
|
||||
_renderFocusedFilter: ({criteria, actions}) =>
|
||||
criteria ?= {}
|
||||
actions ?= {}
|
||||
|
||||
# Gmail users have labels. Exchange users have Folders. They are implemented
|
||||
# differently and frequently require different functionality when you're
|
||||
# trying to manipulate either folders or labels.
|
||||
if CategoryStore.categoryLabel() is "Labels"
|
||||
applyCategory = React.createElement("label", null,
|
||||
React.createElement("input", {"type": "checkbox", \
|
||||
"checked": (actions.applyLabel)}), """
|
||||
Apply the label:
|
||||
""", React.createElement("select", {"value": (actions.applyLabel ? ""), \
|
||||
"onChange": ((e) => @_changeAttr "actions", "applyLabel", e.target.value)},
|
||||
React.createElement("option", {"value": ""}, "Choose a label..."),
|
||||
(@state.categories.map (c) =>
|
||||
React.createElement("option", {"value": (c.id)}, (c.displayName)))
|
||||
)
|
||||
)
|
||||
else
|
||||
applyCategory = React.createElement("label", null,
|
||||
React.createElement("input", {"type": "checkbox", \
|
||||
"checked": (actions.applyFolder)}), """
|
||||
Apply the folder:
|
||||
""", React.createElement("select", {"value": (actions.applyFolder ? ""), \
|
||||
"onChange": ((e) => @_changeAttr "actions", "applyFolder", e.target.value)},
|
||||
React.createElement("option", {"value": ""}, "Choose a folder..."),
|
||||
(@state.categories.map (c) =>
|
||||
React.createElement("option", {"value": (c.id)}, (c.displayName)))
|
||||
)
|
||||
)
|
||||
|
||||
React.createElement("div", null,
|
||||
React.createElement("h4", null, "Filter criteria:"),
|
||||
React.createElement("label", {"className": "filter-input-label"}, "From:"),
|
||||
React.createElement("div", null,
|
||||
React.createElement("input", {"type": "text", \
|
||||
"className": "filter-input", \
|
||||
"value": (criteria.from), \
|
||||
"onChange": ((e) => @_changeAttr "criteria", "from", e.target.value), \
|
||||
"placeholder": "sender@email.com"})
|
||||
),
|
||||
React.createElement("div", {"style": (clear: "both")}),
|
||||
React.createElement("label", {"className": "filter-input-label"}, "To:"),
|
||||
React.createElement("div", null,
|
||||
React.createElement("input", {"type": "text", \
|
||||
"className": "filter-input", \
|
||||
"value": (criteria.to), \
|
||||
"onChange": ((e) => @_changeAttr "criteria", "to", e.target.value), \
|
||||
"placeholder": "recipient@email.com"})
|
||||
),
|
||||
React.createElement("div", {"style": (clear: "both")}),
|
||||
React.createElement("label", {"className": "filter-input-label"}, "Subject:"),
|
||||
React.createElement("div", null,
|
||||
React.createElement("input", {"type": "text", \
|
||||
"className": "filter-input", \
|
||||
"value": (criteria.subject), \
|
||||
"onChange": ((e) => @_changeAttr "criteria", "subject", e.target.value), \
|
||||
"placeholder": "subject contains this phrase"})
|
||||
),
|
||||
React.createElement("div", {"style": (clear: "both")}),
|
||||
React.createElement("label", {"className": "filter-input-label"}, "Has words:"),
|
||||
React.createElement("div", null,
|
||||
React.createElement("input", {"type": "text", \
|
||||
"className": "filter-input", \
|
||||
"value": (criteria["has-words"]), \
|
||||
"onChange": ((e) => @_changeAttr "criteria", "has-words", e.target.value), \
|
||||
"placeholder": "subject or body contains this phrase"})
|
||||
),
|
||||
React.createElement("div", {"style": (clear: "both")}),
|
||||
React.createElement("label", {"className": "filter-input-label"}, "Doesn\'t have:"),
|
||||
React.createElement("div", null,
|
||||
React.createElement("input", {"type": "text", \
|
||||
"className": "filter-input", \
|
||||
"value": (criteria["doesnt-have"]), \
|
||||
"onChange": ((e) => @_changeAttr "criteria", "doesnt-have", e.target.value), \
|
||||
"placeholder": "subject and body don't contain this phrase"})
|
||||
),
|
||||
React.createElement("div", {"style": (clear: "both")}),
|
||||
|
||||
React.createElement("h4", null, "When a message arrives that matches this search:"),
|
||||
React.createElement("div", null,
|
||||
React.createElement("label", null,
|
||||
React.createElement("input", {"type": "checkbox", \
|
||||
"onChange": ((e) => @_changeAttr "actions", "archive", e.target.checked), \
|
||||
"checked": (actions.archive)}), """
|
||||
Skip the inbox (Archive it)
|
||||
""")
|
||||
),
|
||||
React.createElement("div", null,
|
||||
React.createElement("label", null,
|
||||
React.createElement("input", {"type": "checkbox", \
|
||||
"onChange": ((e) => @_changeAttr "actions", "markAsRead", e.target.checked), \
|
||||
"checked": (actions.markAsRead)}), """
|
||||
Mark as read
|
||||
""")
|
||||
),
|
||||
React.createElement("div", null,
|
||||
React.createElement("label", null,
|
||||
React.createElement("input", {"type": "checkbox", \
|
||||
"onChange": ((e) => @_changeAttr "actions", "star", e.target.checked), \
|
||||
"checked": (actions.star)}), """
|
||||
Star it
|
||||
""")
|
||||
),
|
||||
React.createElement("div", null,
|
||||
(applyCategory)
|
||||
),
|
||||
React.createElement("div", null,
|
||||
React.createElement("label", null,
|
||||
React.createElement("input", {"type": "checkbox", \
|
||||
"onChange": ((e) => @_changeAttr "actions", "delete", e.target.checked), \
|
||||
"checked": (actions.delete)}), """
|
||||
Delete it
|
||||
""")
|
||||
),
|
||||
|
||||
React.createElement("h1", null),
|
||||
React.createElement("div", null,
|
||||
React.createElement("button", {"className": "btn pull-right", \
|
||||
"onClick": (@_save)}, """
|
||||
Save filter
|
||||
"""),
|
||||
React.createElement("button", {"className": "btn", \
|
||||
"onClick": (@_unfocus)}, """
|
||||
Cancel
|
||||
""")
|
||||
)
|
||||
)
|
||||
|
||||
_renderFilter: (filter) =>
|
||||
buttonStyles = paddingLeft: 15
|
||||
lineItemStyles =
|
||||
whiteSpace: "nowrap"
|
||||
overflow: "auto"
|
||||
|
||||
React.createElement("div", {"className": "filter-item", "key": (filter.id)},
|
||||
React.createElement("div", null,
|
||||
React.createElement("div", {"className": "pull-right action-button", \
|
||||
"onClick": ( => @_focus filter )}, "edit"),
|
||||
React.createElement("div", {"className": "line-item"},
|
||||
React.createElement("span", null, "Matches: "),
|
||||
React.createElement("strong", null, (@_criteriaDisplay filter.criteria))
|
||||
)
|
||||
),
|
||||
React.createElement("div", null,
|
||||
React.createElement("div", {"className": "pull-right action-button", \
|
||||
"onClick": ( => @_delete filter.id )}, "delete"),
|
||||
React.createElement("div", {"className": "line-item"},
|
||||
React.createElement("span", null, "Do this: "),
|
||||
(@_actionsDisplay filter.actions)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
_changeAttr: (attr1, attr2, val) =>
|
||||
f = @state.focusedFilter
|
||||
f[attr1] ?= {}
|
||||
f[attr1][attr2] = val
|
||||
@setState focusedFilter: f
|
||||
|
||||
_focus: (filter) =>
|
||||
@setState focusedFilter: Utils.deepClone(filter)
|
||||
|
||||
_unfocus: =>
|
||||
@setState focusedFilter: null
|
||||
|
||||
_delete: (id) =>
|
||||
# React components trigger changes by firing Actions, which is simply
|
||||
# invoking an Actions function with relevant data as arguments. Stores will
|
||||
# listen to Actions and update themselves accordingly.
|
||||
Actions.deleteFilter id;
|
||||
|
||||
_save: =>
|
||||
Actions.saveFilter @state.focusedFilter
|
||||
@_unfocus()
|
||||
|
||||
_criterionDisplay: (val, criterion) ->
|
||||
"#{criterion}(#{val})"
|
||||
|
||||
_criteriaDisplay: (criteria) =>
|
||||
_.map(criteria, @_criterionDisplay)
|
||||
.join(" ")
|
||||
|
||||
_actionDisplay: (val, action) =>
|
||||
if action is "applyLabel"
|
||||
category = _.find @state.categories, (c) ->
|
||||
c.id is val
|
||||
"Apply label \"#{category.displayName}\""
|
||||
else if action is "applyFolder"
|
||||
category = _.find @state.categories, (c) ->
|
||||
c.id is val
|
||||
"Apply folder \"#{category.displayName}\""
|
||||
else if action is "markAsRead" and val is true
|
||||
"Mark as read"
|
||||
else if action is "archive" and val is true
|
||||
"Skip the inbox (Archive it)"
|
||||
else if action is "star" and val is true
|
||||
"Star it"
|
||||
else if action is "delete" and val is true
|
||||
"Delete it"
|
||||
|
||||
_actionsDisplay: (actions) =>
|
||||
_.map(actions, @_actionDisplay)
|
||||
.join(", ")
|
||||
|
||||
_getStateFromStores: =>
|
||||
# A common N1 pattern is to dedicate a method solely to generating state,
|
||||
# like right here. It's usually called by the constructor and by listener
|
||||
# callbacks to Stores.
|
||||
filters: FiltersStore.filters()
|
||||
categories: CategoryStore.getUserCategories()
|
||||
|
||||
# Here's the callback that fires after Stores publish changes! `@setState`
|
||||
# will trigger a render with new data.
|
||||
_onFiltersChange: =>
|
||||
@setState @_getStateFromStores()
|
||||
|
||||
module.exports = Filters
|
39
examples/N1-Filters/lib/main.coffee
Normal file
39
examples/N1-Filters/lib/main.coffee
Normal file
|
@ -0,0 +1,39 @@
|
|||
# # Filters
|
||||
#
|
||||
# A way to apply filters, AKA mail rules, to incoming mail.
|
||||
|
||||
Filters = require './filters'
|
||||
# Requiring 'nylas-exports' is the way to access core N1 components.
|
||||
{WorkspaceStore, ComponentRegistry} = require 'nylas-exports'
|
||||
|
||||
# Your main.coffee (or main.cjsx) file needs to export an object for your
|
||||
# package to run.
|
||||
module.exports =
|
||||
# When your package is loading, the `activate` method runs. `activate` is the
|
||||
# package's time to insert React components into the applicatio and also
|
||||
# listen to events.
|
||||
activate: ->
|
||||
# `WorkspaceStore.defineSheet` creates an N1 "sheet," which is a large area
|
||||
# for you to inject React components. Sheets span the whole window.
|
||||
WorkspaceStore.defineSheet 'Filters', {root: true, name: 'Filters'},
|
||||
list: ['RootSidebar', 'Filters']
|
||||
|
||||
# Above, we named the sheet "Filters," and we're registering a React
|
||||
# component to live inside the "Filters" sheet.
|
||||
ComponentRegistry.register Filters,
|
||||
location: WorkspaceStore.Location.Filters
|
||||
|
||||
# `WorkspaceStore.SidebarItem` is a React component which is meant to be
|
||||
# inserted into the navigation bar on the left of the main worksheet.
|
||||
@sidebarItem = new WorkspaceStore.SidebarItem
|
||||
sheet: WorkspaceStore.Sheet.Filters
|
||||
id: 'Filters'
|
||||
name: 'Filters'
|
||||
|
||||
# And this is how we actually insert the SidebarItem into the sheet!
|
||||
WorkspaceStore.addSidebarItem(@sidebarItem)
|
||||
|
||||
# `deactivate` is called when packages are closing. It's a good time to
|
||||
# unregister React components.
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister Filters
|
13
examples/N1-Filters/package.json
Normal file
13
examples/N1-Filters/package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "preferences",
|
||||
"version": "0.0.0",
|
||||
"main": "./lib/main",
|
||||
"description": "Mail filters, AKA mail rules",
|
||||
"license": "Proprietary",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"atom": "#"
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
34
examples/N1-Filters/stylesheets/preferences.less
Normal file
34
examples/N1-Filters/stylesheets/preferences.less
Normal file
|
@ -0,0 +1,34 @@
|
|||
@import "ui-variables";
|
||||
|
||||
.container-filters {
|
||||
.action-button {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.line-item {
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid @border-color;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
border-radius: @border-radius-base;
|
||||
border: 1px solid @input-border;
|
||||
margin-bottom: 15px;
|
||||
float: left;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.filter-input-label {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 25%;
|
||||
text-align: right;
|
||||
padding-right: 15px;
|
||||
line-height: 27px;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue