├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src
├── app.jsx
├── components
│ ├── cells.css
│ ├── cells.jsx
│ ├── firebase-table.jsx
│ └── row.jsx
├── entry.js
├── index.html
└── utils.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Chris Villa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # firestation
2 | A simple, configurable, realtime admin interface for Firebase, built on React.
3 |
4 | See a live demo [here](https://s3-eu-west-1.amazonaws.com/firestation/demo/index.html) (and view the source [here](http://github.com/chrisvxd/firestation-demo)).
5 |
6 | 
7 |
8 |
9 | ## Global Dependencies
10 |
11 | - npm
12 | - webpack
13 |
14 |
15 | ## Installation
16 |
17 | Clone this repository.
18 |
19 | Setup packages:
20 |
21 | npm i
22 |
23 |
24 |
25 | ## Example
26 |
27 | You need to add a `firestation.config.js` file in the root of the project. This is where you configure your dashboard. Here's an example:
28 |
29 | import {ImageCell, TextCell, SelectCell} from 'components/cells.jsx';
30 |
31 | export default {
32 | auth: myAuthMethod,
33 | title: 'Pet Owners Database'
34 | refs: [
35 | {
36 | ref: myFirebaseRef.child('pets'),
37 | title: 'My Favorite Pets',
38 | orderBy: 'size.height',
39 | orderByDirection: 'desc',
40 | children: [
41 | {
42 | key: 'picture',
43 | title: 'Profile Pic',
44 | cell: ImageCell,
45 | cellProps: {
46 | width: '120',
47 | height: '120'
48 | },
49 | canFilter: false
50 | },
51 | {
52 | key: '_key',
53 | cell: TextCell
54 | },
55 | {
56 | key: 'name',
57 | cell: TextCell
58 | }, {
59 | key: 'size.height',
60 | title: 'Height',
61 | cell: TextCell
62 | },
63 | {
64 | key: 'size.weight',
65 | title: 'Weight',
66 | cell: TextCell
67 | },
68 | {
69 | key: 'species',
70 | cell: SelectCell,
71 | cellProps: {
72 | options: [
73 | {
74 | value: 'cat',
75 | title: 'Cat'
76 | },
77 | {
78 | value: 'dog',
79 | title: 'Dog'
80 | }
81 | ]
82 | }
83 | }
84 | ]
85 | },
86 | {
87 | ref: myFirebaseRef.child('owners').orderByChild('lazy').equalTo(true) // Full ref chaining support
88 | title: 'Lazy Owners',
89 | resolve: function (key, val, callback) { // Custom firebase resolve method
90 | val.calculatedLaziness = Math.random();
91 | callback(val);
92 | },
93 | children: [
94 | {
95 | key: 'surname',
96 | cell: TextCell,
97 | canWrite: true
98 | },
99 | {
100 | key: 'lazy',
101 | cell: TextCell
102 | },
103 | {
104 | key: 'calculatedLaziness',
105 | cell: TextCell
106 | }
107 | ]
108 | }
109 | ]
110 | }
111 |
112 | That's it. The `ref` array contains configuration for each firebase ref you want to render in the dashboard, and each of the objects in `children` represent a column that will be rendered for a key on _that ref_, and defines how to render them.
113 |
114 |
115 | ## Configuration API
116 | `firestation.config.js` contains the configuration for your firestation. It's important to do your exports using `ES6`, as shown in the [example above](#example). You'll need to export an object containing your configuration.
117 |
118 | Firestation works by rendering a table for each `ref` you're interested in, with each column representing a `key` for the objects in that `ref`.
119 |
120 |
121 | ### Top-level
122 | The top level defines how to connect to your firebase:
123 |
124 | - `auth` (function) - an authentication method to authenticate with your firebase server. It's up to you to implement that, but is should take a `callback`.
125 | - `title` (string) - the title of your database. This will render in the top left.
126 | - `refs` (array) - an array of `ref` configuration that will describe how to render your firebase. [See Refs](#refs) for more.
127 |
128 | Example:
129 |
130 | var myFirebaseRef = ...;
131 |
132 | // Custom authentication method. Up to you to implement.
133 | var myAuthMethod = function (callback) {
134 | ...
135 | callback();
136 | };
137 |
138 | export default {
139 | auth: myAuthMethod,
140 | refs: [...]
141 | }
142 |
143 |
144 | ### Refs
145 | Each item in the top-level `refs` renders to a table. It defines a [firebase ref](https://www.firebase.com/docs/web/guide/understanding-data.html#section-creating-references) for your database, and configuration for how it should be rendered:
146 |
147 | - `ref` (Firebase) - a full firebase reference for the child you want to create a table for. Supports full chaining, such as `orderByChild`.
148 | - `children` (array) - an array of configurations that define how each key should be rendered. See [Cell API](#cell-api) for cell configuration.
149 | - `resolve` (function, optional) - a custom method for running custom resolves before rendering the item ([see Resolves](#resolves))
150 | - `title` (string, optional) - human-readable title for this ref configuration
151 | - `orderBy` (string, optional) - the default `key` to order your
152 | - `orderByDirection` (string, optional) - the direction (`asc` or `desc`) for the ordering of your `orderBy` value
153 | - `rangeStart` (integer, optional) - the initial index of the first item in rendered range. Defaults to `1`
154 | - `rangeEnd` (integer, optional) - the initial index of the last of the rendered range. Defaults to `10`
155 |
156 | Example:
157 |
158 | {
159 | ref: myFirebaseRef.child('pets'),
160 | title: 'Pets',
161 | orderBy: 'age',
162 | orderByDirection: 'asc',
163 | resolve: function (value, callback) {
164 | value.myExtraField = 'Wohoo!';
165 | callback(value);
166 | },
167 | rangeStart: 1,
168 | rangeEnd: 25,
169 | children: [
170 | ...
171 | ]
172 | }
173 |
174 |
175 | ## Cell API
176 |
177 | Cells render your firebase values depending on their type and how you want to display them. For example, there's an `ImageCell` which loads an image from a URL, or just a lowly `TextCell`. Many of them have both read and write states, making them extremely powerful for managing your data. They are all built on [`React`](http://facebook.github.io/react/), which makes [defining custom cells](#custom-cells) extremely easily.
178 |
179 | Each configuration takes the following properties:
180 |
181 | - `key` (string) - The firebase key for this column
182 | - `cell` (object) - The cell to render the value with (see below)
183 | - `cellProps` (object) - Cell-specific attributes which are passed to the cell
184 | - `title` (string) - A human-readable name for this key
185 | - `canFilter` (boolean, optional) - Whether this column can be filtered using column searching
186 | - `canWrite` (boolean, optional) - Whether the cells can be used in write-mode (currently only partial support)
187 | - `width` (string or integer, optional) - Width of the cell column ([passed directly to `
`](http://www.w3schools.com/tags/att_col_width.asp))
188 |
189 | `key` can also take some special values, indiciated by a `_` prefix:
190 |
191 | - `_key` - The key (id) for this object
192 |
193 | Firestation provides various cells for common use cases:
194 |
195 | - [`TextCell`](#text-cell)
196 | - [`LongTextCell`](#long-text-cell)
197 | - [`NumberCell`](#number-cell)
198 | - [`BooleanCell`](#boolean-cell)
199 | - [`ImageCell`](#image-cell)
200 | - [`SelectCell`](#select-cell)
201 | - [`TimeSinceCell`](#time-since-cell)
202 | - [`DateCell`](#date-cell)
203 | - [`CurrencyCell`](#currency-cell)
204 | - [`ButtonCell`](#button-cell)
205 | - [`DropdownCell`](#dropdown-cell)
206 |
207 | We're adding to this (see [Future Cells](#future-cells)), but [you can write custom react cells](#custom-cells) if you need anything fancy.
208 |
209 |
210 | ### TextCell
211 | Renders the value as simple text. Does not require any `cellProps`.
212 |
213 | This cell _does_ support the `canWrite` method.
214 |
215 |
216 | ### LongTextCell
217 | Renders the value as text in a mulitline textarea. Does not require any `cellProps`.
218 |
219 | This cell _does_ support the `canWrite` method.
220 |
221 |
222 | ### Number
223 | Renders the value as number. Does not require any `cellProps`.
224 |
225 | This cell _does_ support the `canWrite` method.
226 |
227 |
228 | ### BooleanCell
229 | Renders a boolean value as a checkbox. Takes the following `cellProps`:
230 |
231 | - `label` (string, optional) - inline label for the checkbox
232 |
233 | Example:
234 |
235 | {
236 | key: 'likesChicken',
237 | cell: BooleanCell,
238 | cellProps: {
239 | label: 'Chicken Lover?'
240 | }
241 | }
242 |
243 | This cell _does_ support the `canWrite` method.
244 |
245 |
246 | ### ImageCell
247 | Renders the value as an image. Takes the following `cellProps`:
248 |
249 | - `width` (string) - width of the image
250 | - `height` (string) - height of the image
251 |
252 | Example:
253 |
254 | {
255 | key: 'species',
256 | cell: SelectCell,
257 | cellProps: {
258 | width: '50',
259 | height: '50'
260 | }
261 | }
262 |
263 | This cell __does not__ support the `canWrite` method.
264 |
265 |
266 | ### SelectCell
267 | Renders value as an option in a list of options, with human-readable counterparts. Takes the following `cellProps`:
268 |
269 | - `options` (array) - contains objects with the `key` and `title` for each possible value for this key
270 |
271 | Example:
272 |
273 | {
274 | key: 'species',
275 | cell: SelectCell,
276 | cellProps: {
277 | options: [
278 | {
279 | value: 'cat',
280 | title: 'Cat'
281 | },
282 | {
283 | value: 'dog',
284 | title: 'Dog'
285 | }
286 | ]
287 | }
288 | }
289 |
290 | This cell _does_ support the `canWrite` method.
291 |
292 |
293 | ### DateCell
294 | Renders a date in seconds or ISO format to a human-readable date. Write mode uses [a datepicker](https://github.com/YouCanBookMe/react-datetime) to make date selection easy. Formats for your locale and handles daylight changes.
295 |
296 | - `dateFormat` (string) - date format. Defaults to UK format, i.e. `DD/MM/YY`. Follows the [moment.js format](http://momentjs.com/docs/#/displaying/format/)
297 | - `timeFormat` (string) - time format. Also follows the [moment.js format](http://momentjs.com/docs/#/displaying/format/)
298 | - `saveFormat` (string) - the format to save to firebase. Either `numeric` (in ms) or `iso`. Defaults to `iso`.
299 |
300 | Example:
301 |
302 | {
303 | key: 'startAt',
304 | cell: Start,
305 | cellProps: {
306 | dateFormat: 'MM/DD/YY',
307 | saveFormat: 'numeric'
308 | },
309 | canWrite: true
310 | }
311 |
312 | This cell _does_ support the `canWrite` method.
313 |
314 |
315 | ### TimeSinceCell
316 | Renders a date in seconds or ISO format to a time since string, for example "2 days ago".
317 |
318 | This cell __does not__ support the `canWrite` method.
319 |
320 |
321 | ### CurrencyCell
322 | Renders a number as a currency. Takes the following `cellProps`:
323 |
324 | - `symbol` (string) - object containing the `key` and `title` of each option
325 |
326 | Support for adding trailing zeros will be added.
327 |
328 | Example:
329 |
330 | {
331 | key: 'price',
332 | cell: CurrencyCell,
333 | cellProps: {
334 | symbol: '£'
335 | }
336 | }
337 |
338 | This cell __does not__ support the `canWrite` method.
339 |
340 |
341 | ### ButtonCell
342 | Renders a button. Takes the following `cellProps`:
343 |
344 | - `title` (string) - title of the button
345 | - `type` (string) - type of the button. Options are `primary`, `success`, `warning` and `danger`. Defaults to `primary`
346 | - `disabled` (string) - whether the cell is disabled. Defaults to `false`
347 | - `action` (function) - method to call when the button is clicked. You can modify `title`, `type` and `disabled` in the callback.
348 |
349 | Simple example:
350 |
351 | {
352 | key: '',
353 | cell: ButtonCell,
354 | cellProps: {
355 | title: 'Click me!',
356 | action: function (rowKey, rowValue, callback) {
357 | console.log('Woohoo! Performed action on', rowKey);
358 |
359 | callback({
360 | disabled: true
361 | });
362 | }
363 | }
364 | }
365 |
366 |
367 | ### DropdownCell
368 | Renders a dropdown that can trigger actions. Takes the following `cellProps`:
369 |
370 | - `title` (string) - title of the button
371 | - `disabled` (string) - whether the cell is disabled. Defaults to `false`
372 | - `items` (function) - an array of items to render, made up of `actions`, `dividers` and `headers`. See example below for usage.
373 |
374 | Simple example:
375 |
376 | {
377 | key: '',
378 | cell: DropdownCell,
379 | cellProps: {
380 | title: 'Drop it like it's hot',
381 | items: [
382 | {
383 | label: 'Action',
384 | action: function (rowKey, rowValue) {
385 | console.log('Bam!', rowKey, rowValue);
386 | }
387 | },
388 | { label: 'Another action' },
389 | { label: 'Something else here' },
390 | { type: 'divider' },
391 | { type: 'header', label: 'Dropdown header' },
392 | { label: 'Separated link' }
393 | ]
394 | }
395 | }
396 |
397 |
398 | ### Future Cells
399 |
400 | Cells planned but not yet implemented:
401 |
402 | No cells are currently planned at this time.
403 |
404 |
405 | ### Custom Cells
406 | All cells are [built on react](http://facebook.github.io/react/). They are totally pluggable, so it is possible to write custom cells. It is **not** necessary to have an extensive knowledge of React to implement them. Here is a basic, read-only cell that renders an image:
407 |
408 | import React from 'react';
409 |
410 | var FancyImageCell = React.createClass({
411 | render: function () {
412 | return (
413 |
417 |
418 | );
419 | }
420 | });
421 |
422 | This is all basic React. The properties (`this.props`) that the cell will from firestation receive are:
423 |
424 | - `value` - the value for the key this cell will render
425 | - `clean` (bool) - whether the value of this cell is in sync with firebase. Useful for handling write state.
426 | - `rowKey` - the value for the entire row
427 | - `rowValue` - the key for entire row
428 | - `extras` (object) - the [`cellProps`](#cell-api) for that configuration. These could be anything you need.
429 | - `canWrite` (bool) - the [`canWrite`](#cell-api) for that configuration, determining if the cell should be writable
430 | - `childKey` (string) - the `key` that this cell is rendering (e.g. `age`)
431 | - `valueChanged` (function) - a method to inform firestation that the value has changed, enabling the Save Button which will write the changes to firebase.
432 |
433 | Here's a more advanced example, implementing a read-and-writable cell that renders a text input:
434 |
435 | import React from 'react';
436 |
437 | var FancyTextCell = React.createClass({
438 | getInitialState: function () {
439 | return {
440 | value: this.props.value
441 | }
442 | },
443 | handleChange: function (event) {
444 | this.setState({
445 | value: event.target.value
446 | });
447 | this.props.valueChanged(this.props.childKey, event.target.value);
448 | },
449 | render: function () {
450 | // We're in sync with firebase, so ensure values of props and state match.
451 | if (this.props.clean) {
452 | this.state.value = this.props.value;
453 | }
454 |
455 | if (this.props.canWrite) {
456 | // Read and write
457 | return (
458 |
459 | )
460 | } else {
461 | // Read only
462 | return (
463 |
464 | )
465 | }
466 | }
467 | });
468 |
469 | You can do whatever you want with the functionality of these cells, easily tailoring firestation to your specific needs.
470 |
471 |
472 | ## Resolves
473 | Resolve methods allow you to modify the `value` for each Firebase Snapshot _before_ it gets rendered in the table. This greatly extends the capability of firestation, enabling you to do things calculate values or even run extra firebase calls to flatten related objects!
474 |
475 | Calculation Example - adding a `yearBorn` value when we only know the age:
476 |
477 | function (key, value, callback) {
478 | var currentYear = new Date().getFullYear();
479 | value.yearBorn = currentYear - value.age;
480 | callback(value);
481 | }
482 |
483 | Flattening (denormalizing) related objects Example - `owner` onto `pet` using the `owner` key:
484 |
485 | function (key, petValue, callback) {
486 | myFirebaseRef.child('owners').child(petValue.ownerKey).once('value', function (ownerSnapshot) {
487 | petValue.owner = ownerSnapshot.value();
488 | callback(petValue);
489 | });
490 | }
491 |
492 | Neat, huh?
493 |
494 | A small caveat with this method for flattening resources is that firestation will not monitor for or render changes in the flattened object, only the main resource. You could use `.on('child_changed')` or similar method to address this, but this is not officially supported or tested and firestation may behave unexpectedly.
495 |
496 | __Please note, since these values are calculated and do not actually exist on the firebase resource, they are intrinsically read-only. It is impossible to write to them.__
497 |
498 |
499 | ## Favicons
500 | You can add favicons to your project. `index.html` will render favicons rendered by [RealFaviconGenerator](http://realfavicongenerator.net), placed in the `favicons` directory of your project (probably in `dist`).
501 |
502 |
503 | ## Running locally
504 |
505 | Just use
506 |
507 | npm start
508 |
509 | Now go to http://127.0.0.1:8080.
510 |
511 |
512 | ## Building for distribution
513 |
514 | Build that shiz:
515 |
516 | webpack
517 |
518 | Deploy `dist` wherever.
519 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "firestation",
3 | "version": "0.1.0",
4 | "description": "A simple, configurable admin interface for firebase",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "rsync -a src/index.html dist/ && webpack-dev-server --progress --colors --content-base dist",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/chrisvxd/firestation.git"
13 | },
14 | "author": "Chris Villa ",
15 | "license": "MIT",
16 | "bugs": {
17 | "url": "https://github.com/chrisvxd/firestation/issues"
18 | },
19 | "homepage": "https://github.com/chrisvxd/firestation#readme",
20 | "dependencies": {
21 | "elemental": "^0.5.11",
22 | "firebase": "^2.4.0",
23 | "lodash": "^4.3.0",
24 | "moment": "^2.11.2",
25 | "react": "^0.14.7",
26 | "react-addons-css-transition-group": "^0.14.7",
27 | "react-datetime": "^2.5.0",
28 | "react-dom": "^0.14.7",
29 | "string_score": "^0.1.22"
30 | },
31 | "devDependencies": {
32 | "babel-core": "^6.4.5",
33 | "babel-loader": "^6.2.2",
34 | "babel-preset-es2015": "^6.3.13",
35 | "babel-preset-react": "^6.3.13",
36 | "css-loader": "^0.23.1",
37 | "less": "^2.6.0",
38 | "less-loader": "^2.2.2",
39 | "style-loader": "^0.13.0",
40 | "webpack": "^1.12.13",
41 | "webpack-dev-server": "^1.14.1"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import configuration from '../firestation.config.js';
5 |
6 | import 'elemental/less/elemental.less';
7 |
8 | var elemental = require('elemental');
9 |
10 | var GridRow = elemental.Row;
11 | var GridCol = elemental.Col;
12 | var Card = elemental.Card;
13 | var Form = elemental.Form;
14 | var FormField = elemental.FormField;
15 | var FormInput = elemental.FormInput;
16 | var Spinner = elemental.Spinner;
17 | var Glyph = elemental.Glyph;
18 |
19 | import FirebaseTable from 'components/firebase-table.jsx';
20 |
21 | import {getNestedValue, setNestedValue} from './utils.js';
22 |
23 | var defaultResolve = function (key, val, callback) {
24 | callback(val);
25 | };
26 |
27 | export default React.createClass({
28 | componentWillMount: function () {
29 | this.items = {};
30 | this.layouts = {};
31 | this.refSelected(0);
32 | },
33 | getInitialState: function () {
34 | document.title = configuration.title + ' | ' + configuration.refs[0].title;
35 | return {
36 | currentRefIndex: 0,
37 | currentItems: [],
38 | currentHandlers: {},
39 | rangeStart: configuration.refs[0].rangeStart || 1,
40 | rangeEnd: configuration.refs[0].rangeEnd || 10,
41 | filteredSize: 0,
42 | loaded: false
43 | };
44 | },
45 | refSelected: function (refIndex) {
46 | document.title = configuration.title + ' | ' + configuration.refs[refIndex].title;
47 | this.setState({
48 | currentRefIndex: refIndex,
49 | currentItems: [],
50 | rangeStart: configuration.refs[refIndex].rangeStart || 1,
51 | rangeEnd: configuration.refs[refIndex].rangeEnd || 10
52 | });
53 |
54 | if (this.items[refIndex] === undefined) {
55 | this.setState({loaded: false});
56 | this.layouts[refIndex] = {
57 | filters: {},
58 | orderBy: configuration.refs[refIndex].orderBy,
59 | orderByDirection: configuration.refs[refIndex].orderByDirection,
60 | };
61 | this.makeQuery(refIndex);
62 | } else {
63 | this.layouts[refIndex].childAdded = false;
64 | this.layouts[refIndex].childChanged = false;
65 | this.layouts[refIndex].childRemoved = false;
66 | this.prepareAndSetState(refIndex);
67 | }
68 | },
69 | makeQuery: function (refIndex) {
70 | this.items[refIndex] = [];
71 |
72 | const config = configuration.refs[refIndex];
73 |
74 | const ref = (config.getRef || function () {return config.ref})();
75 | const batchRef = config.batchRef || ref;
76 |
77 | var _this = this;
78 |
79 | var monitorRef = function () {
80 | ref.on('child_added', function (snapshot) {
81 | if (this.state.currentRefIndex !== refIndex && this.layouts[refIndex].childAdded !== true) {
82 | this.layouts[refIndex].childAdded = true;
83 | this.setState({});
84 | }
85 | this.processSnapshot(refIndex, snapshot)}.bind(this)
86 | ),
87 | batchRef.on('child_changed', function (snapshot) {
88 | if (this.state.currentRefIndex !== refIndex && this.layouts[refIndex].childChanged !== true) {
89 | this.layouts[refIndex].childChanged = true;
90 | this.setState({});
91 | }
92 | this.processSnapshot(refIndex, snapshot)}.bind(this)
93 | ),
94 | batchRef.on('child_removed', function (snapshot) {
95 | if (this.state.currentRefIndex !== refIndex && this.layouts[refIndex].childRemoved !== true) {
96 | this.layouts[refIndex].childRemoved = true;
97 | this.setState({});
98 | }
99 | this.removeSnapshot(refIndex, snapshot)}.bind(this)
100 | )
101 | };
102 |
103 | // Run value once, then watch for children added
104 | batchRef.once('value', function (snapshot) {
105 | var i = 0;
106 |
107 | console.log('Queried firebase!');
108 |
109 | var resolve = configuration.refs[refIndex].resolve || defaultResolve;
110 |
111 | snapshot.forEach(function (snapshotChild) {
112 | resolve(snapshotChild.key(), snapshotChild.val(), function (val) {
113 | var key = snapshotChild.key();
114 |
115 | val._key = key;
116 |
117 | this.items[refIndex].push({
118 | val: val,
119 | key: key
120 | });
121 |
122 | i += 1;
123 |
124 | if (i === snapshot.numChildren()) {
125 | this.prepareAndSetState(refIndex);
126 | monitorRef.bind(this)();
127 | };
128 | }.bind(this));
129 | }.bind(this));
130 | }.bind(this));
131 |
132 | },
133 | processSnapshot: function (refIndex, snapshot) {
134 | var resolve = configuration.refs[refIndex].resolve || defaultResolve;
135 |
136 | resolve(snapshot.key(), snapshot.val(), function (val) {
137 | var key = snapshot.key();
138 |
139 | var existingIndex = _.findIndex(this.items[refIndex], {'key': key});
140 |
141 | val._key = key;
142 |
143 | var newItem = {
144 | val: val,
145 | key: key
146 | };
147 |
148 | if (existingIndex > -1) {
149 | if (JSON.stringify(this.items[refIndex][existingIndex]) !== JSON.stringify(newItem)) {
150 | this.items[refIndex][existingIndex] = newItem
151 | this.setStateIfRefActive(refIndex);
152 | }
153 | } else {
154 | this.items[refIndex].push(newItem);
155 | this.setStateIfRefActive(refIndex);
156 | };
157 | }.bind(this));
158 | },
159 | removeSnapshot: function (refIndex, snapshot) {
160 | var key = snapshot.key();
161 | var existingIndex = _.findIndex(this.items[refIndex], {'key': key});
162 |
163 | this.items[refIndex].splice(existingIndex, 1);
164 |
165 | this.setStateIfRefActive(refIndex);
166 | },
167 | setStateIfRefActive: function (refIndex) {
168 | if (refIndex === this.state.currentRefIndex) {
169 | this.prepareAndSetState(refIndex)
170 | }
171 | },
172 | prepareAndSetState: function (refIndex) {
173 | this.sortItems(refIndex); // in place
174 | this.setState({
175 | currentItems: this.filterItems(refIndex),
176 | filteredSize: this.items[refIndex].length,
177 | loaded: true
178 | });
179 | },
180 | // Filters this.items and returns filtered
181 | filterItems: function (refIndex) {
182 | var items = this.items[refIndex];
183 |
184 | for (var k in this.layouts[refIndex].filters) {
185 | items = _.filter(items, this.layouts[refIndex].filters[k]);
186 | }
187 |
188 | return items;
189 | },
190 | // Sorts in place
191 | sortItems: function (refIndex) {
192 | if (this.layouts[refIndex].orderBy) {
193 | this.items[refIndex] = _.orderBy(this.items[refIndex], function (item) {
194 | return getNestedValue(item.val, this.layouts[refIndex].orderBy);
195 | }.bind(this), this.layouts[refIndex].orderByDirection);
196 | };
197 | },
198 | setCurrentOrderBy: function (orderBy, direction) {
199 | this.layouts[this.state.currentRefIndex].orderBy = orderBy
200 | this.layouts[this.state.currentRefIndex].orderByDirection = direction
201 | this.prepareAndSetState(this.state.currentRefIndex);
202 | },
203 | setFiltersByKey: function (filtersByKey) {
204 | this.layouts[this.state.currentRefIndex].filtersByKey = filtersByKey
205 | },
206 | setOpenFilterFields: function (openFilterFields) {
207 | this.layouts[this.state.currentRefIndex].openFilterFields = openFilterFields
208 | },
209 | registerFilterAndRun: function (key, value) {
210 | if (value) {
211 | this.layouts[this.state.currentRefIndex].filters[key] = function(val){
212 | return function (o) {
213 | var queriedValue = getNestedValue(o.val, key);
214 |
215 | if (queriedValue.toString().toLowerCase().indexOf(val.toLowerCase()) > -1) {
216 | return true
217 | } else if (queriedValue.toString().score(val) > 0.4) {
218 | return true
219 | } else {
220 | return false
221 | }
222 | }
223 | }(value);
224 |
225 | } else {
226 | delete this.layouts[this.state.currentRefIndex].filters[key];
227 | };
228 |
229 | var items = this.filterItems(this.state.currentRefIndex);
230 |
231 | this.layouts[this.state.currentRefIndex].items = items;
232 |
233 | this.setState({
234 | currentItems: items,
235 | filteredSize: items.length
236 | });
237 | },
238 | updateBackgroundItems: function (items) {
239 |
240 | },
241 | handleRangeStartChange: function (event) {
242 | var rangeDiff = this.state.rangeEnd - this.state.rangeStart;
243 |
244 | var value = Number(event.target.value);
245 |
246 | if (value < 1) {
247 | value = 1;
248 | };
249 |
250 | this.setState({
251 | rangeStart: value,
252 | rangeEnd: value + Number(rangeDiff)
253 | });
254 | },
255 | handleRangeEndChange: function (event) {
256 | var value = Number(event.target.value);
257 | if (value < 1) {
258 | value = 1;
259 | }
260 |
261 | var rangeStart = this.state.rangeStart;
262 |
263 | if (this.state.rangeStart > this.state.rangeEnd) {
264 | rangeStart = this.state.rangeEnd
265 | }
266 |
267 | this.setState({
268 | rangeStart: rangeStart,
269 | rangeEnd: value
270 | });
271 | },
272 | render: function() {
273 | var refOptions = [];
274 |
275 | for (var i = 0; i < configuration.refs.length; i++) {
276 | if (this.state.currentRefIndex === i) {
277 | refOptions.push(
278 |