14 |
15 | ### Checklist
16 |
17 |
22 |
23 | - [ ] New tests added or existing tests modified to cover all changes
24 | - [ ] Code conforms with the [style
25 | guide](http://loopback.io/doc/en/contrib/style-guide.html)
26 |
--------------------------------------------------------------------------------
/client/ngapp/config/routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "/": {
3 | "controller": "HomeCtrl",
4 | "templateUrl": "/views/welcome.html"
5 | },
6 | "/me": {
7 | "controller": "UserCtrl",
8 | "templateUrl": "/views/user.html"
9 | },
10 | "/my/todos/:status": {
11 | "controller": "TodoCtrl",
12 | "templateUrl": "/views/todos.html"
13 | },
14 | "/my/todos": {
15 | "controller": "TodoCtrl",
16 | "templateUrl": "/views/todos.html"
17 | },
18 | "/login": {
19 | "controller": "LoginCtrl",
20 | "templateUrl": "/views/login.html"
21 | },
22 | "/register": {
23 | "controller": "RegisterCtrl",
24 | "templateUrl": "/views/register.html"
25 | },
26 | "/debug": {
27 | "controller": "ChangeCtrl",
28 | "templateUrl": "/views/changes.html"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/client/ngapp/test/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "camelcase": true,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "newcap": true,
13 | "noarg": true,
14 | "quotmark": "single",
15 | "regexp": true,
16 | "undef": true,
17 | "unused": true,
18 | "strict": true,
19 | "trailing": true,
20 | "smarttabs": true,
21 | "globals": {
22 | "after": false,
23 | "afterEach": false,
24 | "angular": false,
25 | "before": false,
26 | "beforeEach": false,
27 | "browser": false,
28 | "describe": false,
29 | "expect": false,
30 | "inject": false,
31 | "it": false,
32 | "jasmine": false,
33 | "spyOn": false
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/server/config.local.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | var GLOBAL_CONFIG = require('../global-config');
9 |
10 | var env = (process.env.NODE_ENV || 'development');
11 | var isDevEnv = env === 'development' || env === 'test';
12 |
13 | module.exports = {
14 | hostname: GLOBAL_CONFIG.hostname,
15 | restApiRoot: GLOBAL_CONFIG.restApiRoot,
16 | livereload: process.env.LIVE_RELOAD,
17 | isDevEnv: isDevEnv,
18 | indexFile: require.resolve(isDevEnv ?
19 | '../client/ngapp/index.html' : '../client/dist/index.html'),
20 | port: GLOBAL_CONFIG.port,
21 | legacyExplorer: GLOBAL_CONFIG.legacyExplorer
22 | };
23 |
--------------------------------------------------------------------------------
/client/ngapp/test/spec/controllers/home.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | describe('Controller: HomeCtrl', function () {
9 |
10 | // load the controller's module
11 | beforeEach(module('loopbackExampleFullStackApp'));
12 |
13 | var HomeCtrl,
14 | scope;
15 |
16 | // Initialize the controller and a mock scope
17 | beforeEach(inject(function ($controller, $rootScope) {
18 | scope = $rootScope.$new();
19 | HomeCtrl = $controller('HomeCtrl', {
20 | $scope: scope
21 | });
22 | }));
23 |
24 | it('should attach a `foo` property to the scope', function () {
25 | expect(scope.foo).toBeDefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/client/ngapp/test/spec/controllers/user.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | describe('Controller: UserCtrl', function () {
9 |
10 | // load the controller's module
11 | beforeEach(module('loopbackExampleFullStackApp'));
12 |
13 | var UserCtrl,
14 | scope;
15 |
16 | // Initialize the controller and a mock scope
17 | beforeEach(inject(function ($controller, $rootScope) {
18 | scope = $rootScope.$new();
19 | UserCtrl = $controller('UserCtrl', {
20 | $scope: scope
21 | });
22 | }));
23 |
24 | it('should attach a `foo` property to the scope', function () {
25 | expect(scope.foo).toBeDefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/client/ngapp/test/spec/controllers/login.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | describe('Controller: LoginCtrl', function () {
9 |
10 | // load the controller's module
11 | beforeEach(module('loopbackExampleFullStackApp'));
12 |
13 | var LoginCtrl,
14 | scope;
15 |
16 | // Initialize the controller and a mock scope
17 | beforeEach(inject(function ($controller, $rootScope) {
18 | scope = $rootScope.$new();
19 | LoginCtrl = $controller('LoginCtrl', {
20 | $scope: scope
21 | });
22 | }));
23 |
24 | it('should attach a `foo` property to the scope', function () {
25 | expect(scope.foo).toBeDefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/client/ngapp/test/spec/controllers/todo.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | describe('Controller: TodoCtrl', function () {
9 |
10 | // load the controller's module
11 | beforeEach(module('loopbackExampleFullStackApp'));
12 |
13 | var TodoCtrl,
14 | scope;
15 |
16 | // Initialize the controller and a mock scope
17 | beforeEach(inject(function ($controller, $rootScope) {
18 | scope = $rootScope.$new();
19 | TodoCtrl = $controller('TodoCtrl', {
20 | $scope: scope
21 | });
22 | }));
23 |
24 | it('should attach a list of Todos to the scope', function () {
25 | expect(scope.todos.length).toBe(0);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/client/ngapp/test/spec/controllers/register.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | describe('Controller: RegisterCtrl', function () {
9 |
10 | // load the controller's module
11 | beforeEach(module('loopbackExampleFullStackApp'));
12 |
13 | var RegisterCtrl,
14 | scope;
15 |
16 | // Initialize the controller and a mock scope
17 | beforeEach(inject(function ($controller, $rootScope) {
18 | scope = $rootScope.$new();
19 | RegisterCtrl = $controller('RegisterCtrl', {
20 | $scope: scope
21 | });
22 | }));
23 |
24 | it('should attach a `foo` property to the scope', function () {
25 | expect(scope.foo).toBeDefined();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/client/ngapp/test/spec/controllers/change.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | describe('Controller: ChangeCtrl', function () {
9 |
10 | // load the controller's module
11 | beforeEach(module('loopbackExampleFullStackApp'));
12 |
13 | var ChangeCtrl,
14 | scope;
15 |
16 | // Initialize the controller and a mock scope
17 | beforeEach(inject(function ($controller, $rootScope) {
18 | scope = $rootScope.$new();
19 | ChangeCtrl = $controller('ChangeCtrl', {
20 | $scope: scope
21 | });
22 | }));
23 |
24 | it('should attach `clearLocalStorage()` to the scope', function () {
25 | expect(typeof scope.clearLocalStorage).toBe('function');
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/client/ngapp/scripts/services/lbclient.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | // load lbclient via browserify's require
9 | var client = (function() {
10 | /*global require:true*/
11 | return require('lbclient');
12 | })();
13 |
14 | /**
15 | * @ngdoc service
16 | * @name loopbackExampleFullStackApp.lbclient
17 | * @description
18 | * # lbclient
19 | * Value in the loopbackExampleFullStackApp.
20 | */
21 | angular.module('loopbackExampleFullStackApp')
22 | .value('Todo', client.models.LocalTodo)
23 | .value('RemoteTodo', client.models.RemoteTodo)
24 | .value('sync', client.sync)
25 | .value('network', client.network)
26 | .value('getReadableModelId', client.getReadableModelId);
27 |
--------------------------------------------------------------------------------
/client/ngapp/test/spec/services/lbclient.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | describe('Service: lbclient', function () {
9 |
10 | // load the service's module
11 | beforeEach(module('loopbackExampleFullStackApp'));
12 |
13 | it('should provide Todo model', function() {
14 | inject(function(Todo) {
15 | expect(Todo).toBeDefined();
16 | });
17 | });
18 |
19 | it('should provide `sync()` function', function() {
20 | inject(function(sync) {
21 | expect(typeof sync).toBe('function');
22 | });
23 | });
24 |
25 | it('should provide `network` object', function() {
26 | inject(function(network) {
27 | expect(network).toBeDefined();
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/client/ngapp/scripts/app.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | /**
9 | * @ngdoc overview
10 | * @name loopbackExampleFullStackApp
11 | * @description
12 | * # loopbackExampleFullStackApp
13 | *
14 | * Main module of the application.
15 | */
16 | angular
17 | .module('loopbackExampleFullStackApp', [
18 | 'ngRoute'
19 | ])
20 | .config(function ($routeProvider, $locationProvider) {
21 | Object.keys(window.CONFIG.routes)
22 | .forEach(function(route) {
23 | var routeDef = window.CONFIG.routes[route];
24 | $routeProvider.when(route, routeDef);
25 | });
26 |
27 | $routeProvider
28 | .otherwise({
29 | redirectTo: '/'
30 | });
31 |
32 | $locationProvider.html5Mode(true);
33 | });
34 |
--------------------------------------------------------------------------------
/global-config.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | /*
7 | * Global configuration shared by components.
8 | */
9 | 'use strict';
10 |
11 | var url = require('url');
12 |
13 | var conf = {
14 | hostname: 'localhost',
15 | port: 3000,
16 | restApiRoot: '/api', // The path where to mount the REST API app
17 | legacyExplorer: false
18 | };
19 |
20 | // The URL where the browser client can access the REST API is available.
21 | // Replace with a full url (including hostname) if your client is being
22 | // served from a different server than your REST API.
23 | conf.restApiUrl = url.format({
24 | protocol: 'http',
25 | slashes: true,
26 | hostname: conf.hostname,
27 | port: conf.port,
28 | pathname: conf.restApiRoot
29 | });
30 |
31 | module.exports = conf;
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
9 |
10 | # Description/Steps to reproduce
11 |
12 |
16 |
17 | # Link to reproduction sandbox
18 |
19 |
24 |
25 | # Expected result
26 |
27 |
30 |
31 | # Additional information
32 |
33 |
38 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 14
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | - critical
10 | - p1
11 | - major
12 | # Label to use when marking an issue as stale
13 | staleLabel: stale
14 | # Comment to post when marking an issue as stale. Set to `false` to disable
15 | markComment: >
16 | This issue has been automatically marked as stale because it has not had
17 | recent activity. It will be closed if no further activity occurs. Thank you
18 | for your contributions.
19 | # Comment to post when closing a stale issue. Set to `false` to disable
20 | closeComment: >
21 | This issue has been closed due to continued inactivity. Thank you for your understanding.
22 | If you believe this to be in error, please contact one of the code owners,
23 | listed in the `CODEOWNERS` file at the top-level of this repository.
24 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | var loopback = require('loopback');
9 | var boot = require('loopback-boot');
10 |
11 | var app = module.exports = loopback();
12 |
13 | // boot scripts mount components like REST API
14 | boot(app, __dirname);
15 |
16 | // optionally start the app
17 | app.start = function() {
18 | // start the web server
19 | return app.listen(function() {
20 | app.emit('started');
21 | var baseUrl = app.get('url').replace(/\/$/, '');
22 | console.log('Web server listening at: %s', baseUrl);
23 | if (app.get('loopback-component-explorer')) {
24 | var explorerPath = app.get('loopback-component-explorer').mountPath;
25 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
26 | }
27 | });
28 | };
29 |
30 | if (require.main === module) {
31 | app.start();
32 | }
33 |
--------------------------------------------------------------------------------
/client/ngapp/config/bundle.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | window.CONFIG = {
7 | "routes": {
8 | "/": {
9 | "controller": "HomeCtrl",
10 | "templateUrl": "/views/welcome.html"
11 | },
12 | "/me": {
13 | "controller": "UserCtrl",
14 | "templateUrl": "/views/user.html"
15 | },
16 | "/my/todos/:status": {
17 | "controller": "TodoCtrl",
18 | "templateUrl": "/views/todos.html"
19 | },
20 | "/my/todos": {
21 | "controller": "TodoCtrl",
22 | "templateUrl": "/views/todos.html"
23 | },
24 | "/login": {
25 | "controller": "LoginCtrl",
26 | "templateUrl": "/views/login.html"
27 | },
28 | "/register": {
29 | "controller": "RegisterCtrl",
30 | "templateUrl": "/views/register.html"
31 | },
32 | "/debug": {
33 | "controller": "ChangeCtrl",
34 | "templateUrl": "/views/changes.html"
35 | }
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/client/ngapp/scripts/controllers/change.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | /**
9 | * @ngdoc function
10 | * @name loopbackExampleFullStackApp.controller:ChangeCtrl
11 | * @description
12 | * # ChangeCtrl
13 | * Controller of the loopbackExampleFullStackApp
14 | */
15 | angular.module('loopbackExampleFullStackApp')
16 | .controller('ChangeCtrl', function ChangeCtrl($scope, $routeParams, $filter,
17 | Todo, RemoteTodo) {
18 |
19 | Todo.getChangeModel().find(function(err, changes) {
20 | $scope.changes = changes;
21 | $scope.$apply();
22 |
23 | RemoteTodo.diff(0, changes, function(err, diff) {
24 | $scope.diff = diff;
25 | $scope.$apply();
26 | });
27 | });
28 |
29 | $scope.clearLocalStorage = function() {
30 | localStorage.removeItem('todo-db');
31 | };
32 |
33 | Todo.find(function(err, todos) {
34 | $scope.todos = todos;
35 | $scope.$apply();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) IBM Corp. 2014,2017. All Rights Reserved.
2 | Node module: loopback-example-offline-sync
3 | This project is licensed under the MIT License, full text below.
4 |
5 | --------
6 |
7 | MIT license
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining a copy
10 | of this software and associated documentation files (the "Software"), to deal
11 | in the Software without restriction, including without limitation the rights
12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | copies of the Software, and to permit persons to whom the Software is
14 | furnished to do so, subject to the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be included in
17 | all copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/client/lbclient/build.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | var path = require('path');
9 | var pkg = require('./package.json');
10 | var fs = require('fs');
11 | var browserify = require('browserify');
12 | var boot = require('loopback-boot');
13 |
14 | module.exports = function buildBrowserBundle(env, callback) {
15 | var b = browserify({ basedir: __dirname });
16 | b.require('./' + pkg.main, { expose: 'lbclient' });
17 |
18 | try {
19 | boot.compileToBrowserify({
20 | appRootDir: __dirname,
21 | env: env
22 | }, b);
23 | } catch(err) {
24 | return callback(err);
25 | }
26 |
27 | var bundlePath = path.resolve(__dirname, 'browser.bundle.js');
28 | var out = fs.createWriteStream(bundlePath);
29 | var isDevEnv = ['debug', 'development', 'test'].indexOf(env) !== -1;
30 |
31 | b.bundle({
32 | // TODO(bajtos) debug should be always true, the sourcemaps should be
33 | // saved to a standalone file when !isDev(env)
34 | debug: isDevEnv
35 | })
36 | .on('error', callback)
37 | .pipe(out);
38 |
39 | out.on('error', callback);
40 | out.on('close', callback);
41 | };
42 |
--------------------------------------------------------------------------------
/common/models/todo.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | var async = require('async');
9 |
10 | module.exports = function(Todo) {
11 | Todo.definition.properties.created.default = Date.now;
12 |
13 | Todo.stats = function(filter, cb) {
14 | var stats = {};
15 | cb = arguments[arguments.length - 1];
16 | var Todo = this;
17 |
18 | async.parallel([
19 | countComplete,
20 | count
21 | ], function(err) {
22 | if (err) return cb(err);
23 | stats.remaining = stats.total - stats.completed;
24 | cb(null, stats);
25 | });
26 |
27 | function countComplete(cb) {
28 | Todo.count({completed: true}, function(err, count) {
29 | stats.completed = count;
30 | cb(err);
31 | });
32 | }
33 |
34 | function count(cb) {
35 | Todo.count(function(err, count) {
36 | stats.total = count;
37 | cb(err);
38 | });
39 | }
40 | };
41 |
42 | Todo.handleChangeError = function(err) {
43 | console.warn('Cannot update change records for Todo:', err);
44 | };
45 |
46 | Todo.remoteMethod('stats', {
47 | accepts: {arg: 'filter', type: 'object'},
48 | returns: {arg: 'stats', type: 'object'},
49 | http: { path: '/stats' }
50 | }, Todo.stats);
51 | };
52 |
--------------------------------------------------------------------------------
/client/lbclient/boot/replication.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | // TODO(bajtos) Move the bi-di replication to loopback core,
9 | // add model settings to enable the replication.
10 | // Example:
11 | // LocalTodo: { options: {
12 | // base: 'Todo',
13 | // replicate: {
14 | // target: 'Todo',
15 | // mode: 'push' | 'pull' | 'bidi'
16 | // }}}
17 | var proquint = require('proquint');
18 |
19 | module.exports = function(client) {
20 | var LocalTodo = client.models.LocalTodo;
21 | var RemoteTodo = client.models.RemoteTodo;
22 |
23 | client.network = {
24 | _isConnected: true,
25 | get isConnected() {
26 | console.log('isConnected?', this._isConnected);
27 | return this._isConnected;
28 | },
29 | set isConnected(value) {
30 | this._isConnected = value;
31 | }
32 | };
33 |
34 | // setup model replication
35 | var since = { push: -1, pull: -1 };
36 | function sync(cb) {
37 | LocalTodo.replicate(
38 | since.push,
39 | RemoteTodo,
40 | function pushed(err, conflicts, cps) {
41 | since.push = cps;
42 | RemoteTodo.replicate(
43 | since.pull,
44 | LocalTodo,
45 | function pulled(err, conflicts, cps) {
46 | since.pull = cps;
47 | if (cb) cb();
48 | });
49 | });
50 | }
51 |
52 | // sync local changes if connected
53 | LocalTodo.on('after save', function(ctx, next) {
54 | next();
55 | sync();
56 | });
57 | LocalTodo.on('after delete', function(ctx, next) {
58 | next();
59 | sync();
60 | });
61 |
62 | client.sync = sync;
63 |
64 | client.getReadableModelId = function(modelId) {
65 | return proquint.encode(new Buffer(modelId.substring(0, 8), 'binary'));
66 | };
67 | };
68 |
--------------------------------------------------------------------------------
/client/ngapp/views/changes.html:
--------------------------------------------------------------------------------
1 | Local Change List
2 |
3 | A list of all changes made to models in local storage.
4 |
5 |
6 | No local changes have been made.
7 |
8 |
9 |
10 | | Model ID |
11 | Type |
12 | Checkpoint |
13 | Revision |
14 | Prev Revision |
15 |
16 |
17 | | {{change.modelId}} |
18 | {{change.type()}} |
19 | {{change.checkpoint}} |
20 | {{change.rev}} |
21 | {{change.prev}} |
22 |
23 |
24 |
25 | Local to Server Deltas
26 |
27 | Below is list of changes required to replicate local data to the server.
28 |
29 | No changes required to replicate the local data to the server.
30 |
31 |
32 | | Model ID |
33 | Revision |
34 | Prev Revision |
35 |
36 |
37 | | {{delta.modelId}} |
38 | {{delta.rev}} |
39 | {{delta.prev}} |
40 |
41 |
42 |
43 | Local Storage Data
44 |
45 | Clear Local Storage
46 |
47 |
48 | There is no data in local storage.
49 |
50 |
51 |
52 | | Todo ID |
53 | Title |
54 | Completed |
55 |
56 |
57 | | {{todo.getId()}} |
58 | {{todo.title}} |
59 | {{todo.completed}} |
60 |
61 |
62 |
63 | Local to Server Conflicts
64 |
65 | Below is list of changes that cannot be replicated to the server.
66 |
67 | No conflicts...
68 |
69 |
70 | | Model ID |
71 | Revision |
72 | Prev Revision |
73 |
74 |
75 | | {{conflict.modelId}} |
76 | {{conflict.rev}} |
77 | {{conflict.prev}} |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/client/ngapp/test/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | // Karma configuration
7 | // http://karma-runner.github.io/0.12/config/configuration-file.html
8 | // Generated on 2014-06-23 using
9 | // generator-karma 0.8.2
10 | 'use strict';
11 |
12 | module.exports = function(config) {
13 | config.set({
14 | // enable / disable watching file and executing tests whenever any file changes
15 | autoWatch: true,
16 |
17 | // base path, that will be used to resolve files and exclude
18 | basePath: '../',
19 |
20 | // testing framework to use (jasmine/mocha/qunit/...)
21 | frameworks: ['jasmine'],
22 |
23 | // list of files / patterns to load in the browser
24 | files: [
25 | '../../bower_components/es5-shim/es5-shim.js',
26 | '../../bower_components/angular/angular.js',
27 | '../../bower_components/angular-mocks/angular-mocks.js',
28 | '../../bower_components/angular-route/angular-route.js',
29 | '../lbclient/browser.bundle.js',
30 | 'config/bundle.js',
31 | 'scripts/**/*.js',
32 | 'test/mock/**/*.js',
33 | 'test/spec/**/*.js'
34 | ],
35 |
36 | // list of files / patterns to exclude
37 | exclude: [],
38 |
39 | // web server port
40 | port: 8080,
41 |
42 | // Start these browsers, currently available:
43 | // - Chrome
44 | // - ChromeCanary
45 | // - Firefox
46 | // - Opera
47 | // - Safari (only Mac)
48 | // - PhantomJS
49 | // - IE (only Windows)
50 | browsers: [
51 | 'Chrome'
52 | ],
53 |
54 | // Which plugins to enable
55 | plugins: [
56 | 'karma-chrome-launcher',
57 | 'karma-phantomjs-launcher',
58 | 'karma-jasmine'
59 | ],
60 |
61 | // Continuous Integration mode
62 | // if true, it capture browsers, run tests and exit
63 | singleRun: false,
64 |
65 | colors: true,
66 |
67 | // level of logging
68 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
69 | logLevel: config.LOG_INFO,
70 |
71 | // Uncomment the following lines if you are using grunt's server to run the tests
72 | // proxies: {
73 | // '/': 'http://localhost:9000/'
74 | // },
75 | // URL root prevent conflicts with the site root
76 | // urlRoot: '_karma_'
77 | });
78 | };
79 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loopback-example-offline-sync",
3 | "version": "0.0.1",
4 | "description": "LoopBack browser and server example",
5 | "main": "server/server.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "scripts": {
10 | "install": "bower install",
11 | "test": "grunt test",
12 | "build": "grunt build"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git://github.com/strongloop/loopback-example-offline-sync.git"
17 | },
18 | "keywords": [
19 | "loopback",
20 | "example",
21 | "browser",
22 | "server"
23 | ],
24 | "author": "IBM Corp.",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/strongloop/loopback-example-offline-sync/issues"
28 | },
29 | "homepage": "https://github.com/strongloop/loopback-example-offline-sync",
30 | "devDependencies": {
31 | "bower": "^1.3.8",
32 | "browserify": "~4.2.3",
33 | "chai": "^3.4.1",
34 | "connect-livereload": "^0.4.0",
35 | "grunt": "^0.4.5",
36 | "grunt-autoprefixer": "^0.8.2",
37 | "grunt-cli": "^0.1.13",
38 | "grunt-concurrent": "^0.5.0",
39 | "grunt-contrib-clean": "^0.5.0",
40 | "grunt-contrib-concat": "^0.5.0",
41 | "grunt-contrib-connect": "^0.8.0",
42 | "grunt-contrib-copy": "^0.5.0",
43 | "grunt-contrib-cssmin": "^0.10.0",
44 | "grunt-contrib-htmlmin": "^0.3.0",
45 | "grunt-contrib-imagemin": "^0.8.1",
46 | "grunt-contrib-jshint": "^0.10.0",
47 | "grunt-contrib-uglify": "^0.5.1",
48 | "grunt-contrib-watch": "^0.6.1",
49 | "grunt-filerev": "^0.2.1",
50 | "grunt-google-cdn": "^0.4.0",
51 | "grunt-karma": "^0.8.3",
52 | "grunt-mocha-test": "^0.12.6",
53 | "grunt-newer": "^0.7.0",
54 | "grunt-ng-annotate": "^0.8.0",
55 | "grunt-svgmin": "^0.4.0",
56 | "grunt-usemin": "^2.3.0",
57 | "grunt-wiredep": "^1.8.0",
58 | "jshint": "^2.8.0",
59 | "jshint-stylish": "^0.4.0",
60 | "karma": "^0.12.17",
61 | "karma-chrome-launcher": "^0.1.4",
62 | "karma-jasmine": "^0.1.5",
63 | "karma-phantomjs-launcher": "^0.1.4",
64 | "load-grunt-tasks": "^0.6.0",
65 | "mocha": "^2.1.0",
66 | "supertest": "^1.1.0",
67 | "time-grunt": "^0.4.0"
68 | },
69 | "dependencies": {
70 | "async": "~0.9.0",
71 | "compression": "^1.0.9",
72 | "cors": "^2.7.1",
73 | "loopback": "^3.0.0",
74 | "loopback-boot": "^2.12.1",
75 | "loopback-component-explorer": "^2.1.0",
76 | "loopback-connector-mongodb": "^1.4.1",
77 | "loopback-datasource-juggler": "^2.0.0",
78 | "proquint": "0.0.1",
79 | "serve-static": "^1.10.0",
80 | "strong-error-handler": "^1.1.1"
81 | },
82 | "engines": {
83 | "node": ">=0.10.0"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/client/ngapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Todo App
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 | Welcome to the Todo App
32 |
33 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/server/test/todo.test.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | var lt = require('./helpers/loopback-testing-helpers');
9 | var assert = require('assert');
10 | var app = require('../server.js'); //path to app.js or server.js
11 |
12 | describe('Todo', function() {
13 | lt.beforeEach.withApp(app);
14 | lt.beforeEach.givenModel('Todo');
15 |
16 | // New todo
17 | describe('New todo empty data', function() {
18 |
19 | lt.describe.whenCalledRemotely('POST', '/api/Todos', {}, function() {
20 | lt.beforeEach.cleanDatasource();
21 |
22 | lt.it.shouldBeAllowed();
23 | it('should have statusCode 200', function() {
24 | assert.equal(this.res.statusCode, 200);
25 | });
26 |
27 | it('should respond with a new todo', function() {
28 | var guidRegex = /[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/;
29 | assert(this.res.body.id.match(guidRegex));
30 | });
31 | });
32 |
33 | });
34 |
35 | // New todo with id
36 | describe('New todo with id set', function() {
37 |
38 | lt.beforeEach.cleanDatasource();
39 | lt.describe.whenCalledRemotely('POST', '/api/Todos', {
40 | id: '123',
41 | title: 'Sample',
42 | completed: true,
43 | created: 1024
44 | }, function() {
45 |
46 | it('should respond with given todo', function() {
47 | assert.equal(this.res.body.id, 123);
48 | assert.equal(this.res.body.title, 'Sample');
49 | assert.equal(this.res.body.completed, true);
50 | assert.equal(this.res.body.created, 1024);
51 | });
52 |
53 | // Find todo in the list of todos
54 | lt.describe.whenCalledRemotely('GET', '/api/Todos', function() {
55 |
56 | it('should contain the todo', function() {
57 | var found = false;
58 | this.res.body.forEach(function(todo) {
59 | if (todo.id === '123') {
60 | found = true;
61 | }
62 | });
63 | assert(found);
64 | });
65 |
66 | });
67 |
68 | // Get todo stats
69 | lt.describe.whenCalledRemotely('POST', '/api/Todos/stats', function() {
70 | lt.it.shouldBeAllowed();
71 | it('should respond with 1 total, 0 remaining and 1 completed', function() {
72 | assert.equal(typeof this.res.body, 'object');
73 | assert.equal(this.res.body.stats.remaining, 0);
74 | assert.equal(this.res.body.stats.completed, 1);
75 | assert.equal(this.res.body.stats.total,1);
76 | });
77 | });
78 |
79 | // Set task as not completed
80 | lt.describe.whenCalledRemotely('PUT', '/api/Todos/123', {
81 | id: 123,
82 | completed: false
83 | }, function() {
84 | it('should respond with todo:123 as uncompleted', function() {
85 | assert.equal(typeof this.res.body, 'object');
86 | assert.equal(this.res.body.completed, false);
87 | });
88 | });
89 |
90 | // Get the specific todo
91 | lt.describe.whenCalledRemotely('GET', '/api/Todos/123', function() {
92 | it('should respond with todo:123', function() {
93 | assert.equal(typeof this.res.body, 'object');
94 | assert.equal(this.res.body.id, 123);
95 | });
96 | });
97 |
98 | // Delete the created todo
99 | lt.describe.whenCalledRemotely('DELETE', '/api/Todos/123', function() {
100 | it('should respond with status 200 - todo:123 deleted', function() {
101 | assert.equal(this.res.statusCode, 200);
102 | assert.equal(this.res.body.count, 1);
103 | });
104 |
105 | // Try to find it -- should return not found
106 | lt.describe.whenCalledRemotely('GET', '/api/Todos/123', function() {
107 | it('should respond with status 404 - todo:123 not found', function() {
108 | assert.equal(this.res.statusCode, 404);
109 | });
110 | });
111 | });
112 | });
113 |
114 | });
115 |
116 | });
117 |
--------------------------------------------------------------------------------
/client/ngapp/views/todos.html:
--------------------------------------------------------------------------------
1 |
2 | Local Conflicts
3 |
4 |
5 |
6 | | Local Data |
7 | Remote Data |
8 |
9 |
10 | |
11 |
12 | Deleted
13 |
14 |
15 |
16 | | id |
17 | change |
18 | title |
19 |
20 |
21 | | {{conflict.sourceChange.modelId}} |
22 | {{conflict.sourceChange.type()}} |
23 |
24 | {{conflict.source.title}}
25 | |
26 |
27 |
28 |
29 | |
30 |
31 |
32 | Deleted
33 |
34 |
35 |
36 | | id |
37 | change |
38 | title |
39 |
40 |
41 | | {{conflict.targetChange.modelId}} |
42 | {{conflict.targetChange.type()}} |
43 |
44 | {{conflict.target.title}}
45 | |
46 |
47 |
48 |
49 | |
50 |
51 |
52 |
53 |
Merge Manually
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
102 |
105 |
112 |
113 |
--------------------------------------------------------------------------------
/server/test/helpers/loopback-testing-helpers.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | var _describe = {};
7 | var _it = {};
8 | var _beforeEach = {};
9 | var helpers = {
10 | describe: _describe,
11 | it: _it,
12 | beforeEach: _beforeEach
13 | };
14 | module.exports = helpers;
15 |
16 | var assert = require('assert');
17 | var request = require('supertest');
18 | var expect = require('chai').expect;
19 |
20 | _beforeEach.withApp = function(app) {
21 | if (app.models.User) {
22 | // Speed up the password hashing algorithm
23 | app.models.User.settings.saltWorkFactor = 4;
24 | }
25 |
26 | beforeEach(function() {
27 | this.app = app;
28 | var _request = this.request = request(app);
29 | this.post = _request.post;
30 | this.get = _request.get;
31 | this.put = _request.put;
32 | this.del = _request.del;
33 | });
34 | };
35 |
36 | _beforeEach.cleanDatasource = function(dsName) {
37 | beforeEach(function(done) {
38 | if (!dsName) dsName = 'db';
39 |
40 | if (typeof this.app === 'function' &&
41 | typeof this.app.datasources === 'object' &&
42 | typeof this.app.datasources[dsName] === 'object') {
43 | this.app.datasources[dsName].automigrate();
44 | this.app.datasources[dsName].connector.ids = {};
45 | }
46 |
47 | done();
48 | });
49 | };
50 |
51 | _beforeEach.withArgs = function() {
52 | var args = Array.prototype.slice.call(arguments, 0);
53 | beforeEach(function() {
54 | this.args = args;
55 | });
56 | };
57 |
58 | _beforeEach.givenModel = function(modelName, attrs, optionalHandler) {
59 | var modelKey = modelName;
60 |
61 | if (typeof attrs === 'function') {
62 | optionalHandler = attrs;
63 | attrs = undefined;
64 | }
65 |
66 | if (typeof optionalHandler === 'string') {
67 | modelKey = optionalHandler;
68 | }
69 |
70 | attrs = attrs || {};
71 |
72 | beforeEach(function(done) {
73 | var test = this;
74 | var app = this.app;
75 | var model = app.models[modelName];
76 | assert(model, 'cannot get model of name ' + modelName + ' from app.models');
77 | assert(model.dataSource, 'cannot test model ' + modelName +
78 | ' without attached dataSource');
79 | assert(
80 | typeof model.create === 'function',
81 | modelName + ' does not have a create method'
82 | );
83 |
84 | model.create(attrs, function(err, result) {
85 | if (err) {
86 | console.error(err.message);
87 | if (err.details) console.error(err.details);
88 | done(err);
89 | } else {
90 | test[modelKey] = result;
91 | done();
92 | }
93 | });
94 | });
95 |
96 | if (typeof optionalHandler === 'function') {
97 | beforeEach(optionalHandler);
98 | }
99 |
100 | afterEach(function(done) {
101 | this[modelKey].destroy(done);
102 | });
103 | };
104 |
105 | _describe.whenCalledRemotely = function(verb, url, data, cb) {
106 | if (cb === undefined) {
107 | cb = data;
108 | data = null;
109 | }
110 |
111 | var urlStr = url;
112 | if (typeof url === 'function') {
113 | urlStr = '/';
114 | }
115 |
116 | describe(verb.toUpperCase() + ' ' + urlStr, function() {
117 | beforeEach(function(cb) {
118 | if (typeof url === 'function') {
119 | this.url = url.call(this);
120 | }
121 | this.remotely = true;
122 | this.verb = verb.toUpperCase();
123 | this.url = this.url || url;
124 | var methodForVerb = verb.toLowerCase();
125 | if (methodForVerb === 'delete') methodForVerb = 'del';
126 |
127 | if (this.request === undefined) {
128 | throw new Error('App is not specified. Please use lt.beforeEach.withApp to specify the app.');
129 | }
130 |
131 | this.http = this.request[methodForVerb](this.url);
132 | delete this.url;
133 | this.http.set('Accept', 'application/json');
134 | if (this.loggedInAccessToken) {
135 | this.http.set('authorization', this.loggedInAccessToken.id);
136 | }
137 | if (data) {
138 | var payload = data;
139 | if (typeof data === 'function')
140 | payload = data.call(this);
141 | this.http.send(payload);
142 | }
143 | this.req = this.http.req;
144 | var test = this;
145 | this.http.end(function(err) {
146 | test.req = test.http.req;
147 | test.res = test.http.res;
148 | delete test.url;
149 | cb();
150 | });
151 | });
152 |
153 | cb();
154 | });
155 | };
156 |
157 | _it.shouldBeAllowed = function() {
158 | it('should be allowed', function() {
159 | assert(this.req);
160 | assert(this.res);
161 | // expect success - status 2xx or 3xx
162 | expect(this.res.statusCode).to.be.within(100, 399);
163 | });
164 | };
165 |
--------------------------------------------------------------------------------
/client/ngapp/scripts/controllers/todo.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | 'use strict';
7 |
8 | /**
9 | * @ngdoc function
10 | * @name loopbackExampleFullStackApp.controller:TodoCtrl
11 | * @description
12 | * # TodoCtrl
13 | * Controller of the loopbackExampleFullStackApp
14 | */
15 | angular.module('loopbackExampleFullStackApp')
16 | .controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, Todo,
17 | $location, sync, network,
18 | getReadableModelId) {
19 | $scope.todos = [];
20 |
21 | $scope.newTodo = '';
22 | $scope.editedTodo = null;
23 |
24 | // sync the initial data
25 | sync(onChange);
26 |
27 | // the location service
28 | $scope.loc = $location;
29 |
30 | function onChange() {
31 | Todo.stats(function(err, stats) {
32 | if(err) return error(err);
33 | $scope.stats = stats;
34 | });
35 | Todo.find({
36 | where: $scope.statusFilter,
37 | sort: 'order DESC'
38 | }, function(err, todos) {
39 | $scope.todos = todos;
40 | $scope.$apply();
41 | });
42 | }
43 |
44 | function error(err) {
45 | //TODO error handling
46 | throw err;
47 | }
48 |
49 | function errorCallback(err) {
50 | if(err) error(err);
51 | }
52 |
53 | Todo.observe('after save', function(ctx, next) {
54 | next();
55 | onChange();
56 | });
57 | Todo.observe('after delete', function(ctx, next) {
58 | next();
59 | onChange();
60 | });
61 |
62 | // Monitor the current route for changes and adjust the filter accordingly.
63 | $scope.$on('$routeChangeSuccess', function () {
64 | var status = $scope.status = $routeParams.status || '';
65 | $scope.statusFilter = (status === 'active') ?
66 | { completed: false } : (status === 'completed') ?
67 | { completed: true } : {};
68 | });
69 |
70 | $scope.addTodo = function () {
71 | Todo.create({title: $scope.newTodo})
72 | .then(function() {
73 | $scope.newTodo = '';
74 | $scope.$apply();
75 | });
76 | };
77 |
78 | $scope.editTodo = function (todo) {
79 | $scope.editedTodo = todo;
80 | };
81 |
82 | $scope.todoCompleted = function(todo) {
83 | todo.completed = true;
84 | todo.save();
85 | };
86 |
87 | $scope.doneEditing = function (todo) {
88 | $scope.editedTodo = null;
89 | todo.title = todo.title.trim();
90 |
91 | if (!todo.title) {
92 | $scope.removeTodo(todo);
93 | } else {
94 | todo.save();
95 | }
96 | };
97 |
98 | $scope.removeTodo = function (todo) {
99 | todo.remove(errorCallback);
100 | };
101 |
102 | $scope.clearCompletedTodos = function () {
103 | Todo.destroyAll({completed: true}, onChange);
104 | };
105 |
106 | $scope.markAll = function (completed) {
107 | Todo.find(function(err, todos) {
108 | if(err) return errorCallback(err);
109 | todos.forEach(function(todo) {
110 | todo.completed = completed;
111 | todo.save(errorCallback);
112 | });
113 | });
114 | };
115 |
116 | $scope.sync = function() {
117 | sync();
118 | };
119 |
120 | $scope.connected = function() {
121 | return network.isConnected;
122 | };
123 |
124 | $scope.connect = function() {
125 | network.isConnected = true;
126 | sync();
127 | };
128 |
129 | $scope.disconnect = function() {
130 | network.isConnected = false;
131 | };
132 |
133 | Todo.on('conflicts', function(conflicts) {
134 | $scope.localConflicts = conflicts;
135 |
136 | conflicts.forEach(function(conflict) {
137 | conflict.type(function(err, type) {
138 | conflict.type = type;
139 | conflict.models(function(err, source, target) {
140 | conflict.source = source;
141 | conflict.target = target;
142 | conflict.manual = new conflict.SourceModel(source || target);
143 | $scope.$apply();
144 | });
145 | conflict.changes(function(err, source, target) {
146 | source.modelId = getReadableModelId(source.modelId);
147 | conflict.sourceChange = source;
148 | target.modelId = getReadableModelId(target.modelId);
149 | conflict.targetChange = target;
150 | $scope.$apply();
151 | });
152 | });
153 | });
154 | });
155 |
156 | $scope.resolveUsingSource = function(conflict) {
157 | conflict.resolveUsingSource(refreshConflicts);
158 | };
159 |
160 | $scope.resolveUsingTarget = function(conflict) {
161 | conflict.resolveUsingTarget(refreshConflicts);
162 | };
163 |
164 | $scope.resolveManually = function(conflict) {
165 | conflict.resolveManually(conflict.manual, refreshConflicts);
166 | };
167 |
168 | function refreshConflicts() {
169 | $scope.localConflicts = [];
170 | $scope.$apply();
171 | sync();
172 | }
173 | });
174 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # loopback-example-offline-sync
2 |
3 | **⚠️ This LoopBack 3 example project is no longer maintained. Please refer to [LoopBack 4 Examples](https://loopback.io/doc/en/lb4/Examples.html) instead. ⚠️**
4 |
5 | An example running LoopBack in the browser and server, demonstrating the
6 | following features:
7 |
8 | - offline data access and synchronization
9 | - routes shared between the AngularJS app and the HTTP server
10 |
11 | ## Install and Run
12 |
13 | 0. You must have `node` and `git` installed. It's recommended to have `mongod`
14 | installed too, so that the data is preserved across application restarts.
15 |
16 | 1. Clone the repo.
17 |
18 | 2. `cd loopback-example-offline-sync`
19 |
20 | 3. `npm install` - install the root package dependencies.
21 |
22 | 4. `npm install grunt-cli -g` - skip if you have Grunt CLI already installed.
23 |
24 | 5. `npm install bower -g` - skip if you already have Bower installed.
25 |
26 | 6. `bower install` - install front-end scripts
27 |
28 | 7. `mongod` - make sure mongodb is running if you want to run with
29 | `NODE_ENV=production`.
30 |
31 | 8. `grunt serve` - build and run the entire project in development mode.
32 |
33 | 9. open `http://localhost:3000` - point a browser at the running application.
34 |
35 | ## Project layout
36 |
37 | The project is composed from multiple components.
38 |
39 | - `common/models/` contains definition of models that are shared by both the server
40 | and the client.
41 |
42 | - `client/lbclient/` provides an isomorphic loopback client with offline synchronization.
43 | The client needs some client-only models for data synchronization. These
44 | models are defined in `client/lbclient/models/`.
45 |
46 | - `client/ngapp/` is a single-page AngularJS application scaffolded using `yo
47 | angular`, with a few modifications to make it work better in the full-stack
48 | project.
49 |
50 | - `server/` is the main HTTP server that brings together all other components.
51 | Also сontains the REST API server; it exposes the shared models via
52 | REST API.
53 |
54 | ## Build
55 |
56 | This project uses [Grunt](http://gruntjs.com) for the build, since that's what
57 | `yo angular` creates.
58 |
59 | There are three major changes from the generic Gruntfile required for this
60 | full-stack example:
61 |
62 | - `grunt serve` uses the `server/` component instead of `grunt connect`.
63 |
64 | - `lbclient` component provides a custom build script (`lbclient/build.js`)
65 | which runs `browserify` to produce a single js file to be used in the
66 | browser. The Gruntfile contains a custom task to run this build.
67 |
68 | - The definition of Angular routes is kept in a standalone JSON file
69 | that is used by the `server/` component too. To make this JSON file
70 | available in the browser, there is a custom task that builds
71 | `ngapp/config/bundle.js`.
72 |
73 | ### Targets
74 |
75 | - `grunt serve` starts the application in development mode, watching for file changes
76 | and automatically reloading the application.
77 | - `grunt test` runs automated tests (only the front-end has tests at the
78 | moment).
79 | - `grunt build` creates the bundle for deploying to production.
80 | - `grunt serve:dist` starts the application serving the production bundle of the
81 | front-end SPA.
82 | - `grunt jshint` checks consistency of the coding style.
83 |
84 | ## Adding more features
85 |
86 | ### Define a new shared model
87 |
88 | The instructions assume the name of the new model is 'MyModel'.
89 |
90 | 1. Create a file `models/my-model.json`, put the model definition there.
91 | Use `models/todo.json` as an example, see
92 | [loopback-boot docs](http://apidocs.strongloop.com/loopback-boot) for
93 | more details about the file format.
94 |
95 | 2. (Optional) Add `models/my-model.js` and implement your custom model
96 | methods. See `models/todo.js` for an example.
97 |
98 | 3. Add an entry to `rest/models.json` to configure the new model in the REST
99 | server:
100 |
101 | ```json
102 | {
103 | "MyModel": {
104 | "dataSource": "db"
105 | }
106 | }
107 | ```
108 |
109 | 4. Define a client-only model to represent the remote server model in the
110 | client - create `lbclient/models/my-model.json` with the following content:
111 |
112 | ```json
113 | {
114 | "name": "RemoteMyModel",
115 | "base": "MyModel"
116 | }
117 | ```
118 |
119 | 5. Add two entries to `lbclient/models.json` to configure the new models
120 | for the client:
121 |
122 | ```json
123 | {
124 | "MyModel": {
125 | "dataSource": "local"
126 | },
127 | "RemoteMyModel": {
128 | "dataSource": "remote"
129 | }
130 | }
131 | ```
132 |
133 | 6. Register the local model with Angular's injector in
134 | `ngapp/scripts/services/lbclient.js`:
135 |
136 | ```js
137 | .value('MyModel', app.models.LocalMyModel)
138 | ```
139 |
140 | ### Create a new Angular route
141 |
142 | Since the full-stack example project shares the routes between the client and
143 | the server, the new route cannot be added using the yeoman generator.
144 |
145 | 1. (Optional) Create a new angular controller using yeoman, for example,
146 |
147 | ```sh
148 | $ yo angular:controller MyModel
149 | ```
150 |
151 | 2. (Optional) Create a new angular view using yeoman, for example,
152 |
153 | ```sh
154 | $ yo angular:view models
155 | ```
156 |
157 | 3. Add a route entry to `ngapp/config/routes.json`, for example,
158 |
159 | ```json
160 | {
161 | "/models": {
162 | "controller": "MymodelCtrl",
163 | "templateUrl": "/views/models.html"
164 | }
165 | }
166 | ```
167 |
168 | ---
169 |
170 | [More LoopBack examples](https://loopback.io/doc/en/lb3/Tutorials-and-examples.html)
171 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ### Contributing ###
2 |
3 | Thank you for your interest in `loopback-example-full-stack`, an open source project
4 | administered by StrongLoop.
5 |
6 | Contributing to `loopback-example-full-stack` is easy. In a few simple steps:
7 |
8 | * Ensure that your effort is aligned with the project's roadmap by
9 | talking to the maintainers, especially if you are going to spend a
10 | lot of time on it.
11 |
12 | * Make something better or fix a bug.
13 |
14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and
15 | [Google Javascript Style Guide][].
16 |
17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-example-full-stack)
18 |
19 | * Submit a pull request through Github.
20 |
21 |
22 | ### Contributor License Agreement ###
23 |
24 | ```
25 | Individual Contributor License Agreement
26 |
27 | By signing this Individual Contributor License Agreement
28 | ("Agreement"), and making a Contribution (as defined below) to
29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and
30 | agree to the following terms and conditions for Your present and
31 | future Contributions submitted to StrongLoop. Except for the license
32 | granted in this Agreement to StrongLoop and recipients of software
33 | distributed by StrongLoop, You reserve all right, title, and interest
34 | in and to Your Contributions.
35 |
36 | 1. Definitions
37 |
38 | "You" or "Your" shall mean the copyright owner or the individual
39 | authorized by the copyright owner that is entering into this
40 | Agreement with StrongLoop.
41 |
42 | "Contribution" shall mean any original work of authorship,
43 | including any modifications or additions to an existing work, that
44 | is intentionally submitted by You to StrongLoop for inclusion in,
45 | or documentation of, any of the products owned or managed by
46 | StrongLoop ("Work"). For purposes of this definition, "submitted"
47 | means any form of electronic, verbal, or written communication
48 | sent to StrongLoop or its representatives, including but not
49 | limited to communication or electronic mailing lists, source code
50 | control systems, and issue tracking systems that are managed by,
51 | or on behalf of, StrongLoop for the purpose of discussing and
52 | improving the Work, but excluding communication that is
53 | conspicuously marked or otherwise designated in writing by You as
54 | "Not a Contribution."
55 |
56 | 2. You Grant a Copyright License to StrongLoop
57 |
58 | Subject to the terms and conditions of this Agreement, You hereby
59 | grant to StrongLoop and recipients of software distributed by
60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge,
61 | royalty-free, irrevocable copyright license to reproduce, prepare
62 | derivative works of, publicly display, publicly perform,
63 | sublicense, and distribute Your Contributions and such derivative
64 | works under any license and without any restrictions.
65 |
66 | 3. You Grant a Patent License to StrongLoop
67 |
68 | Subject to the terms and conditions of this Agreement, You hereby
69 | grant to StrongLoop and to recipients of software distributed by
70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge,
71 | royalty-free, irrevocable (except as stated in this Section)
72 | patent license to make, have made, use, offer to sell, sell,
73 | import, and otherwise transfer the Work under any license and
74 | without any restrictions. The patent license You grant to
75 | StrongLoop under this Section applies only to those patent claims
76 | licensable by You that are necessarily infringed by Your
77 | Contributions(s) alone or by combination of Your Contributions(s)
78 | with the Work to which such Contribution(s) was submitted. If any
79 | entity institutes a patent litigation against You or any other
80 | entity (including a cross-claim or counterclaim in a lawsuit)
81 | alleging that Your Contribution, or the Work to which You have
82 | contributed, constitutes direct or contributory patent
83 | infringement, any patent licenses granted to that entity under
84 | this Agreement for that Contribution or Work shall terminate as
85 | of the date such litigation is filed.
86 |
87 | 4. You Have the Right to Grant Licenses to StrongLoop
88 |
89 | You represent that You are legally entitled to grant the licenses
90 | in this Agreement.
91 |
92 | If Your employer(s) has rights to intellectual property that You
93 | create, You represent that You have received permission to make
94 | the Contributions on behalf of that employer, that Your employer
95 | has waived such rights for Your Contributions, or that Your
96 | employer has executed a separate Corporate Contributor License
97 | Agreement with StrongLoop.
98 |
99 | 5. The Contributions Are Your Original Work
100 |
101 | You represent that each of Your Contributions are Your original
102 | works of authorship (see Section 8 (Submissions on Behalf of
103 | Others) for submission on behalf of others). You represent that to
104 | Your knowledge, no other person claims, or has the right to claim,
105 | any right in any intellectual property right related to Your
106 | Contributions.
107 |
108 | You also represent that You are not legally obligated, whether by
109 | entering into an agreement or otherwise, in any way that conflicts
110 | with the terms of this Agreement.
111 |
112 | You represent that Your Contribution submissions include complete
113 | details of any third-party license or other restriction (including,
114 | but not limited to, related patents and trademarks) of which You
115 | are personally aware and which are associated with any part of
116 | Your Contributions.
117 |
118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions
119 |
120 | You are not expected to provide support for Your Contributions,
121 | except to the extent You desire to provide support. You may provide
122 | support for free, for a fee, or not at all.
123 |
124 | 6. No Warranties or Conditions
125 |
126 | StrongLoop acknowledges that unless required by applicable law or
127 | agreed to in writing, You provide Your Contributions on an "AS IS"
128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES
130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR
131 | FITNESS FOR A PARTICULAR PURPOSE.
132 |
133 | 7. Submission on Behalf of Others
134 |
135 | If You wish to submit work that is not Your original creation, You
136 | may submit it to StrongLoop separately from any Contribution,
137 | identifying the complete details of its source and of any license
138 | or other restriction (including, but not limited to, related
139 | patents, trademarks, and license agreements) of which You are
140 | personally aware, and conspicuously marking the work as
141 | "Submitted on Behalf of a Third-Party: [named here]".
142 |
143 | 8. Agree to Notify of Change of Circumstances
144 |
145 | You agree to notify StrongLoop of any facts or circumstances of
146 | which You become aware that would make these representations
147 | inaccurate in any respect. Email us at callback@strongloop.com.
148 | ```
149 |
150 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html
151 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml
152 |
--------------------------------------------------------------------------------
/client/ngapp/styles/main.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | color: inherit;
16 | -webkit-appearance: none;
17 | -ms-appearance: none;
18 | -o-appearance: none;
19 | appearance: none;
20 | }
21 |
22 | body {
23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
24 | line-height: 1.4em;
25 | background: #eaeaea;
26 | color: #4d4d4d;
27 | width: 550px;
28 | margin: 0 auto;
29 | -webkit-font-smoothing: antialiased;
30 | -moz-font-smoothing: antialiased;
31 | -ms-font-smoothing: antialiased;
32 | -o-font-smoothing: antialiased;
33 | font-smoothing: antialiased;
34 | }
35 |
36 | .header {
37 | border-bottom: 1px solid #e5e5e5;
38 | }
39 |
40 | .header .navigation {
41 | padding-left: 0;
42 | list-style-type: none;
43 | }
44 |
45 | .header .navigation li {
46 | display: inline;
47 | }
48 |
49 | button,
50 | input[type="checkbox"] {
51 | outline: none;
52 | }
53 |
54 | #todoapp {
55 | background: #fff;
56 | background: rgba(255, 255, 255, 0.9);
57 | margin: 130px 0 40px 0;
58 | border: 1px solid #ccc;
59 | position: relative;
60 | border-top-left-radius: 2px;
61 | border-top-right-radius: 2px;
62 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
63 | 0 25px 50px 0 rgba(0, 0, 0, 0.15);
64 | }
65 |
66 | #todoapp:before {
67 | content: '';
68 | border-left: 1px solid #f5d6d6;
69 | border-right: 1px solid #f5d6d6;
70 | width: 2px;
71 | position: absolute;
72 | top: 0;
73 | left: 40px;
74 | height: 100%;
75 | }
76 |
77 | #todoapp input::-webkit-input-placeholder {
78 | font-style: italic;
79 | }
80 |
81 | #todoapp input::-moz-placeholder {
82 | font-style: italic;
83 | color: #a9a9a9;
84 | }
85 |
86 | #todoapp h1 {
87 | position: absolute;
88 | top: -120px;
89 | width: 100%;
90 | font-size: 70px;
91 | font-weight: bold;
92 | text-align: center;
93 | color: #b3b3b3;
94 | color: rgba(255, 255, 255, 0.3);
95 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
96 | -webkit-text-rendering: optimizeLegibility;
97 | -moz-text-rendering: optimizeLegibility;
98 | -ms-text-rendering: optimizeLegibility;
99 | -o-text-rendering: optimizeLegibility;
100 | text-rendering: optimizeLegibility;
101 | }
102 |
103 | #header {
104 | padding-top: 15px;
105 | border-radius: inherit;
106 | }
107 |
108 | #header:before {
109 | content: '';
110 | position: absolute;
111 | top: 0;
112 | right: 0;
113 | left: 0;
114 | height: 15px;
115 | z-index: 2;
116 | border-bottom: 1px solid #6c615c;
117 | background: #8d7d77;
118 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
119 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
120 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
121 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
122 | border-top-left-radius: 1px;
123 | border-top-right-radius: 1px;
124 | }
125 |
126 | #new-todo,
127 | .edit {
128 | position: relative;
129 | margin: 0;
130 | width: 100%;
131 | font-size: 24px;
132 | font-family: inherit;
133 | line-height: 1.4em;
134 | border: 0;
135 | outline: none;
136 | color: inherit;
137 | padding: 6px;
138 | border: 1px solid #999;
139 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
140 | -moz-box-sizing: border-box;
141 | -ms-box-sizing: border-box;
142 | -o-box-sizing: border-box;
143 | box-sizing: border-box;
144 | -webkit-font-smoothing: antialiased;
145 | -moz-font-smoothing: antialiased;
146 | -ms-font-smoothing: antialiased;
147 | -o-font-smoothing: antialiased;
148 | font-smoothing: antialiased;
149 | }
150 |
151 | #new-todo {
152 | padding: 16px 16px 16px 60px;
153 | border: none;
154 | background: rgba(0, 0, 0, 0.02);
155 | z-index: 2;
156 | box-shadow: none;
157 | }
158 |
159 | #main {
160 | position: relative;
161 | z-index: 2;
162 | border-top: 1px dotted #adadad;
163 | }
164 |
165 | label[for='toggle-all'] {
166 | display: none;
167 | }
168 |
169 | #toggle-all {
170 | position: absolute;
171 | top: -42px;
172 | left: -4px;
173 | width: 40px;
174 | text-align: center;
175 | /* Mobile Safari */
176 | border: none;
177 | }
178 |
179 | #toggle-all:before {
180 | content: '»';
181 | font-size: 28px;
182 | color: #d9d9d9;
183 | padding: 0 25px 7px;
184 | }
185 |
186 | #toggle-all:checked:before {
187 | color: #737373;
188 | }
189 |
190 | #todo-list {
191 | margin: 0;
192 | padding: 0;
193 | list-style: none;
194 | }
195 |
196 | #todo-list li {
197 | position: relative;
198 | font-size: 24px;
199 | border-bottom: 1px dotted #ccc;
200 | }
201 |
202 | #todo-list li:last-child {
203 | border-bottom: none;
204 | }
205 |
206 | #todo-list li.editing {
207 | border-bottom: none;
208 | padding: 0;
209 | }
210 |
211 | #todo-list li.editing .edit {
212 | display: block;
213 | width: 506px;
214 | padding: 13px 17px 12px 17px;
215 | margin: 0 0 0 43px;
216 | }
217 |
218 | #todo-list li.editing .view {
219 | display: none;
220 | }
221 |
222 | #todo-list li .toggle {
223 | text-align: center;
224 | width: 40px;
225 | /* auto, since non-WebKit browsers doesn't support input styling */
226 | height: auto;
227 | position: absolute;
228 | top: 0;
229 | bottom: 0;
230 | margin: auto 0;
231 | /* Mobile Safari */
232 | border: none;
233 | -webkit-appearance: none;
234 | -ms-appearance: none;
235 | -o-appearance: none;
236 | appearance: none;
237 | }
238 |
239 | #todo-list li .toggle:after {
240 | content: '✔';
241 | /* 40 + a couple of pixels visual adjustment */
242 | line-height: 43px;
243 | font-size: 20px;
244 | color: #d9d9d9;
245 | text-shadow: 0 -1px 0 #bfbfbf;
246 | }
247 |
248 | #todo-list li .toggle:checked:after {
249 | color: #85ada7;
250 | text-shadow: 0 1px 0 #669991;
251 | bottom: 1px;
252 | position: relative;
253 | }
254 |
255 | #todo-list li label {
256 | white-space: pre;
257 | word-break: break-word;
258 | padding: 15px 60px 15px 15px;
259 | margin-left: 45px;
260 | display: block;
261 | line-height: 1.2;
262 | -webkit-transition: color 0.4s;
263 | transition: color 0.4s;
264 | }
265 |
266 | #todo-list li.completed label {
267 | color: #a9a9a9;
268 | text-decoration: line-through;
269 | }
270 |
271 | #todo-list li .destroy {
272 | display: none;
273 | position: absolute;
274 | top: 0;
275 | right: 10px;
276 | bottom: 0;
277 | width: 40px;
278 | height: 40px;
279 | margin: auto 0;
280 | font-size: 22px;
281 | color: #a88a8a;
282 | -webkit-transition: all 0.2s;
283 | transition: all 0.2s;
284 | }
285 |
286 | #todo-list li .destroy:hover {
287 | text-shadow: 0 0 1px #000,
288 | 0 0 10px rgba(199, 107, 107, 0.8);
289 | -webkit-transform: scale(1.3);
290 | -ms-transform: scale(1.3);
291 | transform: scale(1.3);
292 | }
293 |
294 | #todo-list li .destroy:after {
295 | content: '✖';
296 | }
297 |
298 | #todo-list li:hover .destroy {
299 | display: block;
300 | }
301 |
302 | #todo-list li .edit {
303 | display: none;
304 | }
305 |
306 | #todo-list li.editing:last-child {
307 | margin-bottom: -1px;
308 | }
309 |
310 | #footer {
311 | color: #777;
312 | padding: 0 15px;
313 | position: absolute;
314 | right: 0;
315 | bottom: -31px;
316 | left: 0;
317 | height: 20px;
318 | z-index: 1;
319 | text-align: center;
320 | }
321 |
322 | #footer:before {
323 | content: '';
324 | position: absolute;
325 | right: 0;
326 | bottom: 31px;
327 | left: 0;
328 | height: 50px;
329 | z-index: -1;
330 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
331 | 0 6px 0 -3px rgba(255, 255, 255, 0.8),
332 | 0 7px 1px -3px rgba(0, 0, 0, 0.3),
333 | 0 43px 0 -6px rgba(255, 255, 255, 0.8),
334 | 0 44px 2px -6px rgba(0, 0, 0, 0.2);
335 | }
336 |
337 | #todo-count {
338 | float: left;
339 | text-align: left;
340 | }
341 |
342 | #filters {
343 | margin: 0;
344 | padding: 0;
345 | list-style: none;
346 | position: absolute;
347 | right: 0;
348 | left: 0;
349 | }
350 |
351 | #filters li {
352 | display: inline;
353 | }
354 |
355 | #filters li a {
356 | color: #83756f;
357 | margin: 2px;
358 | text-decoration: none;
359 | }
360 |
361 | #filters li a.selected {
362 | font-weight: bold;
363 | }
364 |
365 | #clear-completed {
366 | float: right;
367 | position: relative;
368 | line-height: 20px;
369 | text-decoration: none;
370 | background: rgba(0, 0, 0, 0.1);
371 | font-size: 11px;
372 | padding: 0 10px;
373 | border-radius: 3px;
374 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
375 | }
376 |
377 | #clear-completed:hover {
378 | background: rgba(0, 0, 0, 0.15);
379 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
380 | }
381 |
382 | #info {
383 | margin: 65px auto 0;
384 | color: #a6a6a6;
385 | font-size: 12px;
386 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
387 | text-align: center;
388 | }
389 |
390 | #info a {
391 | color: inherit;
392 | }
393 |
394 | /*
395 | Hack to remove background from Mobile Safari.
396 | Can't use it globally since it destroys checkboxes in Firefox and Opera
397 | */
398 |
399 | @media screen and (-webkit-min-device-pixel-ratio:0) {
400 | #toggle-all,
401 | #todo-list li .toggle {
402 | background: none;
403 | }
404 |
405 | #todo-list li .toggle {
406 | height: 40px;
407 | }
408 |
409 | #toggle-all {
410 | top: -56px;
411 | left: -15px;
412 | width: 65px;
413 | height: 41px;
414 | -webkit-transform: rotate(90deg);
415 | -ms-transform: rotate(90deg);
416 | transform: rotate(90deg);
417 | -webkit-appearance: none;
418 | appearance: none;
419 | }
420 | }
421 |
422 | .hidden {
423 | display: none;
424 | }
425 |
426 | hr {
427 | margin: 20px 0;
428 | border: 0;
429 | border-top: 1px dashed #C5C5C5;
430 | border-bottom: 1px dashed #F7F7F7;
431 | }
432 |
433 | .learn a {
434 | font-weight: normal;
435 | text-decoration: none;
436 | color: #b83f45;
437 | }
438 |
439 | .learn a:hover {
440 | text-decoration: underline;
441 | color: #787e7e;
442 | }
443 |
444 | .learn h3,
445 | .learn h4,
446 | .learn h5 {
447 | margin: 10px 0;
448 | font-weight: 500;
449 | line-height: 1.2;
450 | color: #000;
451 | }
452 |
453 | .learn h3 {
454 | font-size: 24px;
455 | }
456 |
457 | .learn h4 {
458 | font-size: 18px;
459 | }
460 |
461 | .learn h5 {
462 | margin-bottom: 0;
463 | font-size: 14px;
464 | }
465 |
466 | .learn ul {
467 | padding: 0;
468 | margin: 0 0 30px 25px;
469 | }
470 |
471 | .learn li {
472 | line-height: 20px;
473 | }
474 |
475 | .learn p {
476 | font-size: 15px;
477 | font-weight: 300;
478 | line-height: 1.3;
479 | margin-top: 0;
480 | margin-bottom: 0;
481 | }
482 |
483 | .quote {
484 | border: none;
485 | margin: 20px 0 60px 0;
486 | }
487 |
488 | .quote p {
489 | font-style: italic;
490 | }
491 |
492 | .quote p:before {
493 | content: '“';
494 | font-size: 50px;
495 | opacity: .15;
496 | position: absolute;
497 | top: -20px;
498 | left: 3px;
499 | }
500 |
501 | .quote p:after {
502 | content: '”';
503 | font-size: 50px;
504 | opacity: .15;
505 | position: absolute;
506 | bottom: -42px;
507 | right: 3px;
508 | }
509 |
510 | .quote footer {
511 | position: absolute;
512 | bottom: -40px;
513 | right: 0;
514 | }
515 |
516 | .quote footer img {
517 | border-radius: 3px;
518 | }
519 |
520 | .quote footer a {
521 | margin-left: 5px;
522 | vertical-align: middle;
523 | }
524 |
525 | .speech-bubble {
526 | position: relative;
527 | padding: 10px;
528 | background: rgba(0, 0, 0, .04);
529 | border-radius: 5px;
530 | }
531 |
532 | .speech-bubble:after {
533 | content: '';
534 | position: absolute;
535 | top: 100%;
536 | right: 30px;
537 | border: 13px solid transparent;
538 | border-top-color: rgba(0, 0, 0, .04);
539 | }
540 |
541 | .learn-bar > .learn {
542 | position: absolute;
543 | width: 272px;
544 | top: 8px;
545 | left: -300px;
546 | padding: 10px;
547 | border-radius: 5px;
548 | background-color: rgba(255, 255, 255, .6);
549 | -webkit-transition-property: left;
550 | transition-property: left;
551 | -webkit-transition-duration: 500ms;
552 | transition-duration: 500ms;
553 | }
554 |
555 | @media (min-width: 899px) {
556 | .learn-bar {
557 | width: auto;
558 | margin: 0 0 0 300px;
559 | }
560 |
561 | .learn-bar > .learn {
562 | left: 8px;
563 | }
564 |
565 | .learn-bar #todoapp {
566 | width: 550px;
567 | margin: 130px auto 40px auto;
568 | }
569 | }
570 |
571 | /* changes */
572 |
573 | table, table td, table th {
574 | outline: solid 1px #ccc;
575 | padding: 5px;
576 | text-align: center;
577 | }
578 |
579 | /* debug footer */
580 |
581 | .debug button {
582 | background: #6c615c;
583 | padding: 5px;
584 | color: #fff;
585 | cursor: pointer;
586 | }
587 |
588 | .debug button:active {
589 | background: #000;
590 | }
591 |
592 |
593 | .debug {
594 | border: dashed 2px #6c615c;
595 | padding: 10px;
596 | }
597 |
598 | .conflicts button {
599 | background: #6c615c;
600 | padding: 5px;
601 | color: #fff;
602 | cursor: pointer;
603 | }
604 |
605 | .conflicts button:active {
606 | background: #000;
607 | }
608 |
609 | .deltas {background: red !important;}
610 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | // Copyright IBM Corp. 2014,2016. All Rights Reserved.
2 | // Node module: loopback-example-offline-sync
3 | // This file is licensed under the MIT License.
4 | // License text available at https://opensource.org/licenses/MIT
5 |
6 | // Generated on 2014-06-23 using generator-angular 0.9.1
7 | 'use strict';
8 |
9 | var buildClientBundle = require('./client/lbclient/build');
10 | var fs = require('fs');
11 | var path = require('path');
12 |
13 | // # Globbing
14 | // for performance reasons we're only matching one level down:
15 | // 'test/spec/{,*/}*.js'
16 | // use this if you want to recursively match all subfolders:
17 | // 'test/spec/**/*.js'
18 |
19 | module.exports = function (grunt) {
20 |
21 | // Load grunt tasks automatically
22 | require('load-grunt-tasks')(grunt);
23 |
24 | // Time how long tasks take. Can help when optimizing build times
25 | require('time-grunt')(grunt);
26 |
27 | // Configurable paths for the application
28 | var appConfig = {
29 | app: require('./bower.json').appPath || 'app',
30 | dist: 'client/dist'
31 | };
32 |
33 | // Define the configuration for all the tasks
34 | grunt.initConfig({
35 |
36 | // Project settings
37 | yeoman: appConfig,
38 |
39 | // Watches files for changes and runs tasks based on the changed files
40 | watch: {
41 | bower: {
42 | files: ['bower.json'],
43 | tasks: ['wiredep']
44 | },
45 | js: {
46 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
47 | tasks: ['newer:jshint:all'],
48 | options: {
49 | livereload: '<%= connect.options.livereload %>'
50 | }
51 | },
52 | jsTest: {
53 | files: ['<%= yeoman.app %>/test/spec/{,*/}*.js'],
54 | tasks: ['newer:jshint:test', 'karma']
55 | },
56 | styles: {
57 | files: ['<%= yeoman.app %>/styles/{,*/}*.css'],
58 | tasks: ['newer:copy:styles', 'autoprefixer']
59 | },
60 | gruntfile: {
61 | files: ['Gruntfile.js']
62 | },
63 | livereload: {
64 | options: {
65 | livereload: '<%= connect.options.livereload %>'
66 | },
67 | files: [
68 | '<%= yeoman.app %>/{,*/}*.html',
69 | '.tmp/styles/{,*/}*.css',
70 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
71 | ]
72 | },
73 | lbclient: {
74 | files: [
75 | 'lbclient/models/*',
76 | 'lbclient/app*',
77 | 'lbclient/datasources*',
78 | 'lbclient/models*',
79 | 'lbclient/build.js'
80 | ],
81 | tasks: ['build-lbclient'],
82 | options: {
83 | livereload: '<%= connect.options.livereload %>'
84 | },
85 | },
86 | config: {
87 | files: ['<%= yeoman.app %>/config/*.json'],
88 | tasks: ['build-config'],
89 | options: {
90 | livereload: '<%= connect.options.livereload %>'
91 | },
92 | },
93 | },
94 |
95 | // The actual grunt server settings
96 | connect: {
97 | options: {
98 | port: 3000,
99 | // Change this to '0.0.0.0' to access the server from outside.
100 | hostname: 'localhost',
101 | livereload: 35729
102 | },
103 | test: {
104 | options: {
105 | port: 9001,
106 | middleware: function (connect) {
107 | return [
108 | connect.static('.tmp'),
109 | connect.static('test'),
110 | connect().use(
111 | '/bower_components',
112 | connect.static('./bower_components')
113 | ),
114 | connect().use(
115 | '/lbclient',
116 | connect.static('./lbclient')
117 | ),
118 | connect.static(appConfig.app)
119 | ];
120 | }
121 | }
122 | }
123 | },
124 |
125 | // Make sure code styles are up to par and there are no obvious mistakes
126 | jshint: {
127 | options: {
128 | jshintrc: '.jshintrc',
129 | reporter: require('jshint-stylish')
130 | },
131 | all: {
132 | src: [
133 | 'Gruntfile.js',
134 | '<%= yeoman.app %>/scripts/{,*/}*.js'
135 | ]
136 | },
137 | test: {
138 | options: {
139 | jshintrc: '<%= yeoman.app %>/test/.jshintrc'
140 | },
141 | src: ['test/spec/{,*/}*.js']
142 | }
143 | },
144 |
145 | // Empties folders to start fresh
146 | clean: {
147 | dist: {
148 | files: [{
149 | dot: true,
150 | src: [
151 | '.tmp',
152 | '<%= yeoman.dist %>/{,*/}*',
153 | '!<%= yeoman.dist %>/.git*'
154 | ]
155 | }]
156 | },
157 | server: '.tmp',
158 | lbclient: 'lbclient/browser.bundle.js',
159 | config: '<%= yeoman.app %>/config/bundle.js'
160 | },
161 |
162 | // Add vendor prefixed styles
163 | autoprefixer: {
164 | options: {
165 | browsers: ['last 1 version']
166 | },
167 | dist: {
168 | files: [{
169 | expand: true,
170 | cwd: '.tmp/styles/',
171 | src: '{,*/}*.css',
172 | dest: '.tmp/styles/'
173 | }]
174 | }
175 | },
176 |
177 | // Automatically inject Bower components into the app
178 | wiredep: {
179 | options: {
180 | cwd: '<%= yeoman.app %>',
181 | bowerJson: require('./bower.json'),
182 | directory: './bower_components' //require('./.bowerrc').directory
183 | },
184 | app: {
185 | src: ['<%= yeoman.app %>/index.html'],
186 | ignorePath: /..\//
187 | }
188 | },
189 |
190 | // Renames files for browser caching purposes
191 | filerev: {
192 | dist: {
193 | src: [
194 | '<%= yeoman.dist %>/scripts/{,*/}*.js',
195 | '<%= yeoman.dist %>/styles/{,*/}*.css',
196 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
197 | '<%= yeoman.dist %>/styles/fonts/*'
198 | ]
199 | }
200 | },
201 |
202 | // Reads HTML for usemin blocks to enable smart builds that automatically
203 | // concat, minify and revision files. Creates configurations in memory so
204 | // additional tasks can operate on them
205 | useminPrepare: {
206 | html: '<%= yeoman.app %>/index.html',
207 | options: {
208 | dest: '<%= yeoman.dist %>',
209 | flow: {
210 | html: {
211 | steps: {
212 | js: ['concat', 'uglifyjs'],
213 | css: ['cssmin']
214 | },
215 | post: {}
216 | }
217 | }
218 | }
219 | },
220 |
221 | // Performs rewrites based on filerev and the useminPrepare configuration
222 | usemin: {
223 | html: ['<%= yeoman.dist %>/{,*/}*.html'],
224 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
225 | options: {
226 | assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images']
227 | }
228 | },
229 |
230 | // The following *-min tasks will produce minified files in the dist folder
231 | // By default, your `index.html`'s will take care of
232 | // minification. These next options are pre-configured if you do not wish
233 | // to use the Usemin blocks.
234 | // cssmin: {
235 | // dist: {
236 | // files: {
237 | // '<%= yeoman.dist %>/styles/main.css': [
238 | // '.tmp/styles/{,*/}*.css'
239 | // ]
240 | // }
241 | // }
242 | // },
243 | // uglify: {
244 | // dist: {
245 | // files: {
246 | // '<%= yeoman.dist %>/scripts/scripts.js': [
247 | // '<%= yeoman.dist %>/scripts/scripts.js'
248 | // ]
249 | // }
250 | // }
251 | // },
252 | // concat: {
253 | // dist: {}
254 | // },
255 |
256 | imagemin: {
257 | dist: {
258 | files: [{
259 | expand: true,
260 | cwd: '<%= yeoman.app %>/images',
261 | src: '{,*/}*.{png,jpg,jpeg,gif}',
262 | dest: '<%= yeoman.dist %>/images'
263 | }]
264 | }
265 | },
266 |
267 | svgmin: {
268 | dist: {
269 | files: [{
270 | expand: true,
271 | cwd: '<%= yeoman.app %>/images',
272 | src: '{,*/}*.svg',
273 | dest: '<%= yeoman.dist %>/images'
274 | }]
275 | }
276 | },
277 |
278 | htmlmin: {
279 | dist: {
280 | options: {
281 | collapseWhitespace: true,
282 | conservativeCollapse: true,
283 | collapseBooleanAttributes: true,
284 | removeCommentsFromCDATA: true,
285 | removeOptionalTags: true
286 | },
287 | files: [{
288 | expand: true,
289 | cwd: '<%= yeoman.dist %>',
290 | src: ['*.html', 'views/{,*/}*.html'],
291 | dest: '<%= yeoman.dist %>'
292 | }]
293 | }
294 | },
295 |
296 | // ngAnnotate tries to make the code safe for minification automatically by
297 | // using the Angular long form for dependency injection. It doesn't work on
298 | // things like resolve or inject so those have to be done manually.
299 | ngAnnotate: {
300 | dist: {
301 | files: [{
302 | expand: true,
303 | cwd: '.tmp/concat/scripts',
304 | src: '*.js',
305 | dest: '.tmp/concat/scripts'
306 | }]
307 | }
308 | },
309 |
310 | // Replace Google CDN references
311 | cdnify: {
312 | dist: {
313 | html: ['<%= yeoman.dist %>/*.html']
314 | }
315 | },
316 |
317 | // Copies remaining files to places other tasks can use
318 | copy: {
319 | dist: {
320 | files: [{
321 | expand: true,
322 | dot: true,
323 | cwd: '<%= yeoman.app %>',
324 | dest: '<%= yeoman.dist %>',
325 | src: [
326 | '*.{ico,png,txt}',
327 | '.htaccess',
328 | '*.html',
329 | 'views/{,*/}*.html',
330 | 'images/{,*/}*.{webp}',
331 | 'fonts/*'
332 | ]
333 | }, {
334 | expand: true,
335 | cwd: '.tmp/images',
336 | dest: '<%= yeoman.dist %>/images',
337 | src: ['generated/*']
338 | }]
339 | },
340 | styles: {
341 | expand: true,
342 | cwd: '<%= yeoman.app %>/styles',
343 | dest: '.tmp/styles/',
344 | src: '{,*/}*.css'
345 | }
346 | },
347 |
348 | // Run some tasks in parallel to speed up the build process
349 | concurrent: {
350 | server: [
351 | 'copy:styles'
352 | ],
353 | test: [
354 | 'copy:styles'
355 | ],
356 | dist: [
357 | 'copy:styles',
358 | 'imagemin',
359 | 'svgmin'
360 | ]
361 | },
362 |
363 | // Test settings
364 | karma: {
365 | unit: {
366 | configFile: '<%= yeoman.app %>/test/karma.conf.js',
367 | browsers: [ 'PhantomJS' ],
368 | singleRun: true
369 | }
370 | },
371 |
372 | // Server Tests
373 | mochaTest: {
374 | common: {
375 | options: {
376 | reporter: 'spec',
377 | quiet: false,
378 | clearRequireCache: false
379 | },
380 | src: ['common/models/test/**/*.js']
381 | },
382 | server: {
383 | options: {
384 | reporter: 'spec',
385 | quiet: false,
386 | clearRequireCache: false
387 | },
388 | src: ['server/test/**/*.js']
389 | }
390 | }
391 |
392 | });
393 |
394 | grunt.registerTask('build-lbclient', 'Build lbclient browser bundle', function() {
395 | var done = this.async();
396 | buildClientBundle(process.env.NODE_ENV || 'development', done);
397 | });
398 |
399 | grunt.registerTask('build-config', 'Build confg.js from JSON files', function() {
400 | var ngapp = path.resolve(__dirname, appConfig.app);
401 | var configDir = path.join(ngapp, 'config');
402 | var config = {};
403 |
404 | fs.readdirSync(configDir)
405 | .forEach(function(f) {
406 | if (f === 'bundle.js') return;
407 |
408 | var extname = path.extname(f);
409 | if (extname !== '.json') {
410 | grunt.warn('Ignoring ' + f + ' (' + extname + ')');
411 | return;
412 | }
413 |
414 | var fullPath = path.resolve(configDir, f);
415 | var key = path.basename(f, extname);
416 |
417 | config[key] = JSON.parse(fs.readFileSync(fullPath), 'utf-8');
418 | });
419 |
420 | var outputPath = path.resolve(ngapp, 'config', 'bundle.js');
421 | var content = 'window.CONFIG = ' +
422 | JSON.stringify(config, null, 2) +
423 | ';\n';
424 | fs.writeFileSync(outputPath, content, 'utf-8');
425 | });
426 |
427 | grunt.registerTask('run', 'Start the app server', function() {
428 | var done = this.async();
429 |
430 | var connectConfig = grunt.config.get().connect.options;
431 | process.env.LIVE_RELOAD = connectConfig.livereload;
432 | process.env.NODE_ENV = this.args[0];
433 |
434 | var keepAlive = this.flags.keepalive || connectConfig.keepalive;
435 |
436 | var server = require('./server');
437 | server.set('port', connectConfig.port);
438 | server.set('host', connectConfig.hostname);
439 | server.start()
440 | .on('listening', function() {
441 | if (!keepAlive) done();
442 | })
443 | .on('error', function(err) {
444 | if (err.code === 'EADDRINUSE') {
445 | grunt.fatal('Port ' + connectConfig.port +
446 | ' is already in use by another process.');
447 | } else {
448 | grunt.fatal(err);
449 | }
450 | });
451 | });
452 |
453 | grunt.registerTask('serve', 'Compile then start the app server', function (target) {
454 | if (target === 'dist') {
455 | return grunt.task.run(['build', 'run:dist:keepalive']);
456 | }
457 |
458 | grunt.task.run([
459 | 'clean:server',
460 | 'build-lbclient',
461 | 'build-config',
462 | 'wiredep',
463 | 'concurrent:server',
464 | 'autoprefixer',
465 | 'run:development',
466 | 'watch'
467 | ]);
468 | });
469 |
470 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
471 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
472 | grunt.task.run(['serve:' + target]);
473 | });
474 |
475 | grunt.registerTask('test:client', [
476 | 'clean:server',
477 | 'build-lbclient',
478 | 'build-config',
479 | 'concurrent:test',
480 | 'autoprefixer',
481 | 'connect:test',
482 | 'karma'
483 | ]);
484 |
485 | grunt.registerTask('test:common', [
486 | 'mochaTest:common'
487 | ]);
488 |
489 | grunt.registerTask('test:server', [
490 | 'mochaTest:server'
491 | ]);
492 |
493 | grunt.registerTask('test', [
494 | 'test:server',
495 | 'test:common',
496 | 'test:client'
497 | ]);
498 |
499 | grunt.registerTask('build', [
500 | 'clean:dist',
501 | 'build-lbclient',
502 | 'build-config',
503 | 'wiredep',
504 | 'useminPrepare',
505 | 'concurrent:dist',
506 | 'autoprefixer',
507 | 'concat',
508 | 'ngAnnotate',
509 | 'copy:dist',
510 | 'cdnify',
511 | 'cssmin',
512 | 'uglify',
513 | 'filerev',
514 | 'usemin',
515 | 'htmlmin'
516 | ]);
517 |
518 | grunt.registerTask('default', [
519 | 'newer:jshint',
520 | 'test',
521 | 'build'
522 | ]);
523 | };
524 |
--------------------------------------------------------------------------------