hippo-fw 0.9.3 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/client/hippo/__mocks__/config.js +4 -4
  4. data/client/hippo/boot.jsx +8 -8
  5. data/client/hippo/components/form.jsx +1 -2
  6. data/client/hippo/components/form/wrapper.jsx +20 -8
  7. data/client/hippo/config.android.js +8 -0
  8. data/client/hippo/config.ios.js +8 -0
  9. data/client/hippo/config.js +5 -71
  10. data/client/hippo/lib/pub_sub.js +34 -0
  11. data/client/hippo/models/base.js +6 -6
  12. data/client/hippo/models/config.js +60 -0
  13. data/client/hippo/models/pub_sub.js +157 -0
  14. data/client/hippo/models/pub_sub/channel.js +35 -0
  15. data/client/hippo/screens/definition.js +1 -1
  16. data/client/hippo/screens/index.js +2 -10
  17. data/client/hippo/screens/user-management/edit-form.jsx +10 -7
  18. data/client/hippo/user.js +1 -3
  19. data/client/hippo/workspace/index.jsx +16 -15
  20. data/client/hippo/workspace/menu.jsx +2 -8
  21. data/client/hippo/workspace/screen.jsx +6 -6
  22. data/config/database.yml +1 -0
  23. data/config/routes.rb +4 -4
  24. data/config/webpack.config.js +18 -16
  25. data/hippo-fw.gemspec +2 -2
  26. data/lib/hippo.rb +1 -3
  27. data/lib/hippo/api.rb +1 -0
  28. data/lib/hippo/api/cable.rb +19 -20
  29. data/lib/hippo/api/handlers/user_session.rb +1 -1
  30. data/lib/hippo/api/pub_sub.rb +7 -5
  31. data/lib/hippo/api/routing.rb +1 -1
  32. data/lib/hippo/api/updates.rb +1 -1
  33. data/lib/hippo/concerns/api_path.rb +2 -3
  34. data/lib/hippo/configuration.rb +2 -0
  35. data/lib/hippo/rails.rb +9 -0
  36. data/lib/hippo/user.rb +2 -1
  37. data/lib/hippo/version.rb +1 -1
  38. data/package-lock.json +6823 -0
  39. data/package.json +43 -34
  40. data/spec/client/models/pub_sub.spec.js +27 -0
  41. data/templates/js/config-data.js +1 -1
  42. data/templates/js/screen-definitions.js +2 -2
  43. data/yarn.lock +307 -15
  44. metadata +28 -9
  45. data/client/extension.js +0 -0
  46. data/client/hippo/models/PubSub.js +0 -208
  47. data/lib/generators/hippo/migrations/install_generator.rb +0 -42
  48. data/lib/hippo/rails_engine.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 82181b31b5579f88b13be87e7bbc04d6b51e4436
4
- data.tar.gz: 62de1b88b1ba249ed360afd5d482b788471a89a6
3
+ metadata.gz: 66252f602687cd15c345fe869696110e2b106c45
4
+ data.tar.gz: 6e63cb8121ff3b3a7b5d4186306755648397342f
5
5
  SHA512:
6
- metadata.gz: 687fc0c88df1d479e020340054d00233cbb6ce206d921ffa3c60de601ba86ba43be2c028f01f553d705a85192481e9fd8a8ee4b2250fdc74048af44f6c48197a
7
- data.tar.gz: 599be4e27ad43bbcaf10e910f4dbe06173f626b23038941e09a9ac85c9ec6f28b370d2c060c0281eb1524d30fc8649c7efc91e21d0d96c64c78a092a4b9a5d23
6
+ metadata.gz: 166aadffc4273278372aad7b702ddb0baac76f106afa7482be135aef026c5c0f3071ae6fa7dbffa5b3ff9667b81620b43356b64615a393d9d328156ae22e2a8a
7
+ data.tar.gz: f9b71478268619454b76bd254e45c5947274f70228b355fc39f8f66cf752fb86c19e3eb3501ea63ac7fa5bc74e0d4c803ae6e76cfdf236d8ac7ceab4c4ceb957
data/Rakefile CHANGED
@@ -19,7 +19,7 @@ YARD::Rake::YardocTask.new do |t|
19
19
  end
20
20
 
21
21
  task :npmrelease do
22
- sh "npm-release #{Hippo::VERSION}"
22
+ sh "npm-release"
23
23
  end
24
24
 
25
25
  task :release => :npmrelease
@@ -5,10 +5,10 @@ window.localStorage = {
5
5
  },
6
6
  };
7
7
 
8
- const config = jest.genMockFromModule('hippo/config');
9
-
10
- config.bootstrapUserData = jest.fn();
11
- config.reset = jest.fn();
8
+ const config = jest.genMockFromModule('../models/config');
9
+ //
10
+ // config.bootstrapUserData = jest.fn();
11
+ // config.reset = jest.fn();
12
12
  Object.defineProperty(config, 'api_path', {
13
13
  value: '/api',
14
14
  });
@@ -3,19 +3,14 @@ import ReactDOM from 'react-dom';
3
3
  import whenDomReady from 'when-dom-ready';
4
4
  import { delay } from 'lodash';
5
5
  import { AppContainer } from 'react-hot-loader';
6
- import { withAsyncComponents } from 'react-async-component';
6
+ import { onBoot } from './models/pub_sub';
7
7
 
8
8
  const Workspace = require('hippo/workspace').default;
9
9
 
10
10
  let Root;
11
- let App;
12
11
 
13
12
  function renderer(Body) {
14
- withAsyncComponents(<AppContainer><Body /></AppContainer>)
15
- .then((result) => {
16
- App = result.appWithAsyncComponents;
17
- ReactDOM.render(App, Root);
18
- });
13
+ ReactDOM.render(<AppContainer><Body /></AppContainer>, Root);
19
14
  }
20
15
 
21
16
  if (module.hot) {
@@ -34,6 +29,11 @@ whenDomReady().then(() => {
34
29
  const loading = document.querySelector('.loading');
35
30
  if (loading) {
36
31
  loading.classList.add('complete');
37
- delay(() => loading.parentNode.removeChild(loading), 400);
32
+ delay(() => {
33
+ onBoot();
34
+ loading.parentNode.removeChild(loading);
35
+
36
+ }, 400);
37
+
38
38
  }
39
39
  });
@@ -1,6 +1,5 @@
1
1
  import {
2
- negate, isNil, isString, isObject, mapValues, extend, forIn, isFunction, pick, isBoolean, get, keys,
3
- merge,
2
+ negate, isString, isObject, isBoolean, merge,
4
3
  } from 'lodash';
5
4
  import moment from 'moment';
6
5
  import isEmail from 'validator/lib/isEmail';
@@ -1,17 +1,18 @@
1
1
  import React from 'react';
2
-
3
- import { Provider, observer } from 'mobx-react';
4
-
2
+ import PropTypes from 'prop-types';
3
+ import { PropTypes as MobxPropTypes, Provider, observer } from 'mobx-react';
4
+ import { observePubSub } from '../../models/pub_sub';
5
5
  import { FormState } from './model';
6
6
 
7
7
  @observer
8
8
  export default class FormWrapper extends React.PureComponent {
9
9
 
10
10
  static propTypes = {
11
- children: React.PropTypes.node.isRequired,
12
- state: React.PropTypes.instanceOf(FormState),
13
- tag: React.PropTypes.string,
14
- className: React.PropTypes.string,
11
+ tag: PropTypes.string,
12
+ className: PropTypes.string,
13
+ children: PropTypes.node.isRequired,
14
+ state: PropTypes.instanceOf(FormState),
15
+ model: MobxPropTypes.observableObject,
15
16
  }
16
17
 
17
18
  static get defaultProps() {
@@ -20,6 +21,12 @@ export default class FormWrapper extends React.PureComponent {
20
21
  };
21
22
  }
22
23
 
24
+ componentDidMount() {
25
+ if (this.props.model) {
26
+ this.props.state.setFromModel(this.props.model);
27
+ }
28
+ }
29
+
23
30
  renderTagless() {
24
31
  return (
25
32
  <Provider formState={this.props.state}>
@@ -28,8 +35,12 @@ export default class FormWrapper extends React.PureComponent {
28
35
  );
29
36
  }
30
37
 
38
+ persistTo(model) {
39
+ this.props.state.persistTo(model);
40
+ }
41
+
31
42
  renderTagged() {
32
- const { tag: Tag, state, children, ...otherProps } = this.props;
43
+ const { tag: Tag, state, children, model: _, ...otherProps } = this.props;
33
44
  return (
34
45
  <Provider formState={state}>
35
46
  <Tag {...otherProps}>
@@ -40,6 +51,7 @@ export default class FormWrapper extends React.PureComponent {
40
51
  }
41
52
 
42
53
  render() {
54
+ if (this.props.model) { observePubSub(this.props.model); }
43
55
  return this.props.tag ? this.renderTagged() : this.renderTagless();
44
56
  }
45
57
 
@@ -0,0 +1,8 @@
1
+ import { AsyncStorage } from 'react-native';
2
+ import Config from './models/config';
3
+
4
+ const ConfigInstance = Config.create({
5
+ storage: AsyncStorage,
6
+ });
7
+
8
+ export default ConfigInstance;
@@ -0,0 +1,8 @@
1
+ import { AsyncStorage } from 'react-native';
2
+ import Config from './models/config';
3
+
4
+ const ConfigInstance = Config.create({
5
+ storage: AsyncStorage,
6
+ });
7
+
8
+ export default ConfigInstance;
@@ -1,74 +1,8 @@
1
- import { observable, observe } from 'mobx';
2
- import { keysIn, pick, assign, get } from 'lodash';
3
- import Extensions from './extensions';
1
+ import Config from './models/config';
4
2
 
5
- const STORAGE_KEY = 'hippo-user-data';
3
+ const ConfigInstance = Config.create({
4
+ storage: localStorage,
5
+ jsonify: true,
6
+ });
6
7
 
7
- class Config {
8
-
9
- @observable api_host = get(window, 'location.origin', '');
10
- @observable api_path = '/api';
11
- @observable access_token;
12
- @observable root_view;
13
- @observable assets_path_prefix = '/assets';
14
- @observable user;
15
- @observable website_domain;
16
- @observable product_name;
17
- @observable screens;
18
-
19
- constructor() {
20
- this.bootstrapUserData();
21
- observe(this, 'user', ({ newValue }) => {
22
- if (newValue) { this.setUserData(); }
23
- });
24
- observe(this, 'screens', ({ newValue }) => {
25
- if (newValue) { this.setScreenData(); }
26
- });
27
- observe(this, 'access_token', ({ newValue: token }) => {
28
- this.data.token = token;
29
- this.persistToStorage();
30
- });
31
- }
32
-
33
- bootstrap(attrs) {
34
- assign(this, pick(attrs, keysIn(this)));
35
- Extensions.setBootstrapData(attrs);
36
- }
37
-
38
- persistToStorage() {
39
- window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.data));
40
- }
41
-
42
- setScreenData() {
43
- if (this.screens && this.data) {
44
- this.screens.configure(this.data.screens);
45
- }
46
- }
47
-
48
- setUserData() {
49
- if (this.user && this.data) {
50
- this.user.set(this.data.user);
51
- this.user.access = this.data.access;
52
- }
53
- }
54
-
55
- bootstrapUserData() {
56
- const savedData = window.localStorage.getItem(STORAGE_KEY);
57
- this.data = JSON.parse(savedData);
58
- if (!this.data) { return; }
59
- this.access_token = this.data.token;
60
- this.setUserData();
61
- this.setScreenData();
62
- }
63
-
64
- reset() {
65
- this.data = {};
66
- this.persistToStorage();
67
- this.access_token = null;
68
- if (this.user) { this.user.reset(); }
69
- if (this.screens) { this.screens.reset(); }
70
- }
71
- }
72
-
73
- const ConfigInstance = new Config();
74
8
  export default ConfigInstance;
@@ -0,0 +1,34 @@
1
+ import Cable from 'es6-actioncable';
2
+
3
+ import Config from '../config';
4
+
5
+ const INSTANCE = null;
6
+
7
+ export default class Websocket {
8
+ static initialize() {
9
+ const INSTANCE = new Websocket()
10
+ INSTANCE.connect();
11
+ }
12
+
13
+ connect() {
14
+ console.log('connecting websocket');
15
+
16
+ this.consumer = Cable.createConsumer(
17
+ `${Config.api_host}${Config.api_path}/cable?token=${Config.access_token}`
18
+ );
19
+ }
20
+
21
+ getConsumer() {
22
+ if(!this.consumer) {
23
+ this.connect();
24
+ }
25
+ return this.consumer;
26
+ }
27
+
28
+ closeConnection() {
29
+ if(this.consumer) {
30
+ Cable.endConsumer(this.consumer);
31
+ }
32
+ delete this.consumer;
33
+ }
34
+ }
@@ -9,8 +9,6 @@ import {
9
9
 
10
10
  import { action, observable, computed } from 'mobx';
11
11
 
12
- import pluralize from 'pluralize';
13
-
14
12
  import Sync from './sync';
15
13
  import Config from '../config';
16
14
  import { toSentence, humanize } from '../lib/util';
@@ -41,7 +39,7 @@ export class BaseModel {
41
39
  }
42
40
 
43
41
  static get assignableKeys() {
44
- return this.$schema.keys();
42
+ return Array.from(this.$schema.keys());
45
43
  }
46
44
 
47
45
  static get propertyOptions() {
@@ -52,11 +50,11 @@ export class BaseModel {
52
50
 
53
51
  static get syncUrl() {
54
52
  invariant(this.identifiedBy, 'must have an identifiedBy property in order to calulate syncUrl');
55
- return `${Config.api_path}/${pluralize(this.identifiedBy)}`;
53
+ return `${Config.api_path}/${this.identifiedBy}`;
56
54
  }
57
55
 
58
56
  static get identifierFieldName() {
59
- const field = find(this.$schema.values(), { type: 'identifier' });
57
+ const field = find(Array.from(this.$schema.values()), { type: 'identifier' });
60
58
  invariant(field, 'identifierFieldName called on a model that has not designated one with `@identifier`');
61
59
  return field.name;
62
60
  }
@@ -66,7 +64,9 @@ export class BaseModel {
66
64
  }
67
65
 
68
66
  constructor(attrs) {
69
- if (!isEmpty(attrs)) { this.set(attrs); }
67
+ if (!isEmpty(attrs)) {
68
+ this.set(attrs);
69
+ }
70
70
  }
71
71
 
72
72
  get isModel() { return true; }
@@ -0,0 +1,60 @@
1
+ import { observable, observe } from 'mobx';
2
+ import { keysIn, pick, assign, get } from 'lodash';
3
+ import { persist, create as createHydrator } from 'mobx-persist';
4
+
5
+ import Extensions from '../extensions';
6
+
7
+ const STORAGE_KEY = 'hippo-user-data';
8
+
9
+ export default class Config {
10
+ @persist @observable api_host = get(window, 'location.origin', '');
11
+ @persist @observable api_path = '/api';
12
+ @persist @observable access_token;
13
+ @persist @observable root_view;
14
+ @persist @observable assets_path_prefix = '/assets';
15
+ @persist @observable website_domain;
16
+ @persist @observable product_name;
17
+ @persist('list') @observable screen_ids = [];
18
+ @persist @observable user_info;
19
+
20
+ @observable user;
21
+ @observable isIntialized = false;
22
+
23
+ static create(hydrationConfig) {
24
+ const hydrate = createHydrator(hydrationConfig);
25
+ const ConfigInstance = new Config();
26
+ hydrate('config', ConfigInstance).then(() => (ConfigInstance.isIntialized = true));
27
+ return ConfigInstance;
28
+ }
29
+
30
+ constructor() {
31
+ observe(this, 'user', ({ newValue }) => {
32
+ if (newValue) { this.setUserData(); }
33
+ });
34
+ }
35
+
36
+ update(attrs) {
37
+ assign(this, pick(attrs, keysIn(this)));
38
+ Extensions.setBootstrapData(attrs);
39
+ }
40
+
41
+ setScreenData() {
42
+ if (this.screens && this.data) {
43
+ this.screens.configure(this.data.screens);
44
+ }
45
+ }
46
+
47
+ setUserData() {
48
+ if (this.user && this.data) {
49
+ this.user.set(this.data.user);
50
+ this.user.access = this.data.access;
51
+ }
52
+ }
53
+
54
+ reset() {
55
+ this.data = {};
56
+ this.access_token = null;
57
+ if (this.user) { this.user.reset(); }
58
+ }
59
+
60
+ }
@@ -0,0 +1,157 @@
1
+ import { Atom, when, reaction } from 'mobx';
2
+ import ActionCable from 'actioncable';
3
+ import invariant from 'invariant';
4
+ import { omit, invoke, mapValues } from 'lodash';
5
+
6
+ import { logger } from '../lib/util';
7
+ import User from '../user';
8
+ import Config from '../config';
9
+
10
+ import PubSubCableChannel from './pub_sub/channel';
11
+
12
+ let PubSub;
13
+
14
+ class PubSubMap {
15
+
16
+ constructor(modelKlass) {
17
+ this.channel_prefix = modelKlass.identifiedBy;
18
+ this.map = Object.create(null);
19
+ }
20
+
21
+ channelForId(id) {
22
+ return `${this.channel_prefix}/${id}`;
23
+ }
24
+
25
+ forModel(model) {
26
+ const id = model[model.constructor.identifierFieldName];
27
+ let models = this.map[id];
28
+ if (!models) {
29
+ models = [];
30
+ this.map[id] = models;
31
+ }
32
+ return { id, models };
33
+ }
34
+
35
+ onChange(id, data) {
36
+ const models = this.map[id];
37
+ if (models) {
38
+ const update = mapValues(data.update, '[1]');
39
+ invoke(models, 'set', update);
40
+ }
41
+ }
42
+
43
+ observe(model) {
44
+ const { id, models } = this.forModel(model);
45
+ if (!models.includes(model)) {
46
+ models.push(model);
47
+ if (1 === models.length) {
48
+ PubSub.channel.subscribe(this.channelForId(id));
49
+ }
50
+ }
51
+ }
52
+
53
+ remove(model) {
54
+ const { id, models } = this.forModel(model);
55
+ const indx = models.indexOf(model);
56
+ invariant(-1 !== indx, "Asked to remove model from pubsub but it was never observed");
57
+ if (1 === models.length) {
58
+ delete this.map[id];
59
+ PubSub.channel.unsubscribe(this.channelForId(id));
60
+ } else {
61
+ models.splice(indx, 1);
62
+ }
63
+ }
64
+ }
65
+
66
+ PubSub = {
67
+
68
+ types: new WeakMap(),
69
+
70
+ add(model) {
71
+ return this.mapForModel(model).observe(model);
72
+ },
73
+
74
+ remove(model) {
75
+ this.mapForModel(model).remove(model);
76
+ },
77
+
78
+ onModelChange(model, id, data) {
79
+ const map = this.types.get(model);
80
+ if (map) {
81
+ map.onChange(id, data);
82
+ }
83
+ },
84
+
85
+ mapForModel(model) {
86
+ const klass = model.constructor;
87
+ let map = this.types.get(klass);
88
+ if (!map) {
89
+ map = new PubSubMap(klass);
90
+ this.types.set(klass, map);
91
+ }
92
+ return map;
93
+ },
94
+
95
+
96
+ onLoginChange() {
97
+ if (User.isLoggedIn) {
98
+ const url = `${Config.api_host}${Config.api_path}/cable?token=${Config.access_token}`;
99
+ PubSub.cable = ActionCable.createConsumer(url);
100
+ PubSub.channel = new PubSubCableChannel(PubSub);
101
+ } else if (PubSub.cable) {
102
+ PubSub.cable.disconnect();
103
+ delete PubSub.cable;
104
+ delete PubSub.channel;
105
+ }
106
+ },
107
+
108
+ };
109
+
110
+ export function onBoot() {
111
+ reaction(
112
+ () => User.isLoggedIn,
113
+ PubSub.onLoginChange,
114
+ { fireImmediately: true },
115
+ );
116
+ }
117
+
118
+ export function stop() {
119
+ PubSub.kill();
120
+ }
121
+
122
+ export class PubSubAtom {
123
+
124
+ constructor(model) {
125
+ this.model = model;
126
+ if (model.identifierFieldValue) {
127
+ this.buildAtom();
128
+ } else {
129
+ when(
130
+ () => model.identifierFieldValue,
131
+ () => this.buildAtom(),
132
+ );
133
+ }
134
+ }
135
+
136
+ buildAtom() {
137
+ this.atom = new Atom(
138
+ 'ModelPubSub',
139
+ () => PubSub.add(this.model),
140
+ () => PubSub.remove(this.model),
141
+ );
142
+ }
143
+
144
+ reportObserved() {
145
+ if (this.atom) { this.atom.reportObserved(); }
146
+ }
147
+ }
148
+
149
+ export function observePubSub(...models) {
150
+ for (let i = 0; i < models.length; i += 1) {
151
+ const model = models[i];
152
+ if (!model.$pubSub) {
153
+ model.$pubSub = new PubSubAtom(model);
154
+ }
155
+ model.$pubSub.reportObserved();
156
+ }
157
+ }