hippo-fw 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }