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.
- checksums.yaml +4 -4
- data/Rakefile +1 -1
- data/client/hippo/__mocks__/config.js +4 -4
- data/client/hippo/boot.jsx +8 -8
- data/client/hippo/components/form.jsx +1 -2
- data/client/hippo/components/form/wrapper.jsx +20 -8
- data/client/hippo/config.android.js +8 -0
- data/client/hippo/config.ios.js +8 -0
- data/client/hippo/config.js +5 -71
- data/client/hippo/lib/pub_sub.js +34 -0
- data/client/hippo/models/base.js +6 -6
- data/client/hippo/models/config.js +60 -0
- data/client/hippo/models/pub_sub.js +157 -0
- data/client/hippo/models/pub_sub/channel.js +35 -0
- data/client/hippo/screens/definition.js +1 -1
- data/client/hippo/screens/index.js +2 -10
- data/client/hippo/screens/user-management/edit-form.jsx +10 -7
- data/client/hippo/user.js +1 -3
- data/client/hippo/workspace/index.jsx +16 -15
- data/client/hippo/workspace/menu.jsx +2 -8
- data/client/hippo/workspace/screen.jsx +6 -6
- data/config/database.yml +1 -0
- data/config/routes.rb +4 -4
- data/config/webpack.config.js +18 -16
- data/hippo-fw.gemspec +2 -2
- data/lib/hippo.rb +1 -3
- data/lib/hippo/api.rb +1 -0
- data/lib/hippo/api/cable.rb +19 -20
- data/lib/hippo/api/handlers/user_session.rb +1 -1
- data/lib/hippo/api/pub_sub.rb +7 -5
- data/lib/hippo/api/routing.rb +1 -1
- data/lib/hippo/api/updates.rb +1 -1
- data/lib/hippo/concerns/api_path.rb +2 -3
- data/lib/hippo/configuration.rb +2 -0
- data/lib/hippo/rails.rb +9 -0
- data/lib/hippo/user.rb +2 -1
- data/lib/hippo/version.rb +1 -1
- data/package-lock.json +6823 -0
- data/package.json +43 -34
- data/spec/client/models/pub_sub.spec.js +27 -0
- data/templates/js/config-data.js +1 -1
- data/templates/js/screen-definitions.js +2 -2
- data/yarn.lock +307 -15
- metadata +28 -9
- data/client/extension.js +0 -0
- data/client/hippo/models/PubSub.js +0 -208
- data/lib/generators/hippo/migrations/install_generator.rb +0 -42
- data/lib/hippo/rails_engine.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66252f602687cd15c345fe869696110e2b106c45
|
4
|
+
data.tar.gz: 6e63cb8121ff3b3a7b5d4186306755648397342f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 166aadffc4273278372aad7b702ddb0baac76f106afa7482be135aef026c5c0f3071ae6fa7dbffa5b3ff9667b81620b43356b64615a393d9d328156ae22e2a8a
|
7
|
+
data.tar.gz: f9b71478268619454b76bd254e45c5947274f70228b355fc39f8f66cf752fb86c19e3eb3501ea63ac7fa5bc74e0d4c803ae6e76cfdf236d8ac7ceab4c4ceb957
|
data/Rakefile
CHANGED
@@ -5,10 +5,10 @@ window.localStorage = {
|
|
5
5
|
},
|
6
6
|
};
|
7
7
|
|
8
|
-
const config = jest.genMockFromModule('
|
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
|
});
|
data/client/hippo/boot.jsx
CHANGED
@@ -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 {
|
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
|
-
|
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(() =>
|
32
|
+
delay(() => {
|
33
|
+
onBoot();
|
34
|
+
loading.parentNode.removeChild(loading);
|
35
|
+
|
36
|
+
}, 400);
|
37
|
+
|
38
38
|
}
|
39
39
|
});
|
@@ -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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
|
data/client/hippo/config.js
CHANGED
@@ -1,74 +1,8 @@
|
|
1
|
-
import
|
2
|
-
import { keysIn, pick, assign, get } from 'lodash';
|
3
|
-
import Extensions from './extensions';
|
1
|
+
import Config from './models/config';
|
4
2
|
|
5
|
-
const
|
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
|
+
}
|
data/client/hippo/models/base.js
CHANGED
@@ -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}/${
|
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)) {
|
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
|
+
}
|