hippo-fw 0.9.6 → 0.9.7

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/client/hippo/components/asset.jsx +22 -2
  3. data/client/hippo/components/asset.scss +1 -1
  4. data/client/hippo/components/data-list.jsx +38 -27
  5. data/client/hippo/components/data-list/data-list.scss +2 -0
  6. data/client/hippo/components/data-table.jsx +4 -2
  7. data/client/hippo/components/data-table/formatters.js +7 -0
  8. data/client/hippo/components/data-table/table-styles.scss +10 -0
  9. data/client/hippo/components/date-time.jsx +93 -133
  10. data/client/hippo/components/date-time.scss +1 -36
  11. data/client/hippo/components/form/api.js +4 -3
  12. data/client/hippo/components/form/fields.jsx +7 -1
  13. data/client/hippo/components/form/fields/date-wrapper.jsx +4 -4
  14. data/client/hippo/components/icon.jsx +10 -1
  15. data/client/hippo/components/query-builder.jsx +6 -1
  16. data/client/hippo/components/record-finder/query-layer.jsx +1 -1
  17. data/client/hippo/components/save-button.jsx +55 -0
  18. data/client/hippo/components/text-editor.jsx +10 -9
  19. data/client/hippo/components/text-editor/text-editor.scss +0 -1
  20. data/client/hippo/components/time-zone-select.jsx +60 -0
  21. data/client/hippo/models/asset.js +10 -5
  22. data/client/hippo/models/base.js +1 -1
  23. data/client/hippo/models/pub_sub.js +22 -67
  24. data/client/hippo/models/pub_sub/channel.js +28 -8
  25. data/client/hippo/models/pub_sub/map.js +57 -0
  26. data/client/hippo/models/query/array-result.js +5 -4
  27. data/client/hippo/models/query/field.js +19 -3
  28. data/client/hippo/models/system-setting.js +16 -1
  29. data/client/hippo/models/tenant.js +8 -7
  30. data/client/hippo/screens/system-settings.jsx +10 -12
  31. data/client/hippo/screens/user-management/edit-form.jsx +10 -11
  32. data/client/hippo/workspace/index.jsx +6 -3
  33. data/client/hippo/workspace/menu-option.jsx +2 -5
  34. data/client/hippo/workspace/menu.jsx +13 -1
  35. data/client/hippo/workspace/styles.scss +11 -26
  36. data/command-reference-files/initial/Gemfile +1 -1
  37. data/command-reference-files/model/client/appy-app/models/test_test.js +1 -1
  38. data/command-reference-files/model/spec/server/models/test_test_spec.rb +10 -0
  39. data/lib/hippo/api/cable.rb +0 -2
  40. data/lib/hippo/command/generate_model.rb +2 -3
  41. data/lib/hippo/spec_helper.rb +4 -2
  42. data/lib/hippo/tenant.rb +7 -1
  43. data/lib/hippo/version.rb +1 -1
  44. data/package-lock.json +60 -46
  45. data/package.json +5 -2
  46. data/spec/client/components/__snapshots__/record-finder.spec.jsx.snap +1 -0
  47. data/spec/client/components/__snapshots__/time-zone-select.spec.jsx.snap +48 -0
  48. data/spec/client/components/form.spec.jsx +7 -0
  49. data/spec/client/components/time-zone-select.spec.jsx +11 -0
  50. data/spec/client/models/pub_sub.spec.js +1 -3
  51. data/spec/client/models/pub_sub/channel.spec.js +45 -0
  52. data/spec/client/models/system-setting.spec.js +14 -0
  53. data/spec/client/workspace/__snapshots__/menu.spec.jsx.snap +9 -9
  54. data/spec/server/api/tenant_change_spec.rb +1 -1
  55. data/templates/client/models/model.js +1 -1
  56. data/templates/spec/factories/model.rb +6 -0
  57. data/templates/spec/server/model_spec.rb +4 -4
  58. data/views/hippo_root_view.erb +4 -0
  59. metadata +11 -10
  60. data/client/hippo/components/date-time/calendar.jsx +0 -113
  61. data/client/hippo/components/date-time/date-time-drop.jsx +0 -75
  62. data/client/hippo/components/date-time/date-time.scss +0 -157
  63. data/client/hippo/components/date-time/time.jsx +0 -119
  64. data/client/hippo/workspace/foo.js +0 -0
  65. data/command-reference-files/model/spec/fixtures/appy-app/test_test.yml +0 -11
  66. data/command-reference-files/model/spec/server/test_test_spec.rb +0 -10
  67. data/spec/client/components/date-time.spec.jsx +0 -20
@@ -1,12 +1,12 @@
1
1
  import { omit } from 'lodash';
2
- import { action } from 'mobx';
2
+ import { action, observable } from 'mobx';
3
3
  import { logger } from '../../lib/util';
4
- import { BaseModel } from '../base';
5
4
 
6
5
  const CHANNEL_SPLITTER = new RegExp('^(.*):(.*)/([^/]+)$');
7
6
 
8
7
  export default class PubSubCableChannel {
9
8
  constructor(pub_sub) {
9
+ this.callbacks = observable.map();
10
10
  this.channel = pub_sub.cable.subscriptions.create(
11
11
  'Hippo::API::PubSub', this,
12
12
  );
@@ -14,14 +14,31 @@ export default class PubSubCableChannel {
14
14
  this.pub_sub = pub_sub;
15
15
  }
16
16
 
17
- subscribe(channel) {
17
+ subscribe(channel, cb) {
18
18
  logger.info(`[pubsub] subscribe to: ${channel}`);
19
+ if (cb) {
20
+ const callbacks = this.callbacks.get(channel) || observable([]);
21
+ callbacks.push(cb);
22
+ this.callbacks.set(channel, callbacks);
23
+ }
19
24
  this.channel.perform('on', { channel });
20
25
  }
21
26
 
22
- unsubscribe(channel) {
27
+ unsubscribe(channel, cb) {
23
28
  logger.info(`[pubsub] unsubscribe from: ${channel}`);
24
- this.channel.perform('off', { channel });
29
+ if (cb) {
30
+ const callbacks = this.callbacks.get(channel);
31
+ if (callbacks) {
32
+ callbacks.remove(cb);
33
+ if (callbacks.length) {
34
+ this.callbacks.set(channel, callbacks);
35
+ } else {
36
+ this.callbacks.delete(channel);
37
+ }
38
+ }
39
+ } else {
40
+ this.channel.perform('off', { channel });
41
+ }
25
42
  }
26
43
 
27
44
  @action.bound
@@ -29,8 +46,11 @@ export default class PubSubCableChannel {
29
46
  const [_, __, modelId, id] = Array.from(
30
47
  data.channel.match(CHANNEL_SPLITTER),
31
48
  );
32
-
33
- const model = BaseModel.findDerived(modelId);
34
- this.pub_sub.onModelChange(model, id, omit(data, 'channel'));
49
+ const callbacks = this.callbacks.get(`${modelId}/${id}`);
50
+ if (callbacks) {
51
+ callbacks.forEach(c => c(data));
52
+ } else {
53
+ this.pub_sub.onModelChange(modelId, id, omit(data, 'channel'));
54
+ }
35
55
  }
36
56
  }
@@ -0,0 +1,57 @@
1
+ import invariant from 'invariant';
2
+ import { isEmpty, mapValues } from 'lodash';
3
+
4
+ export default class PubSubMap {
5
+ constructor(modelKlass, channel) {
6
+ this.channel = channel;
7
+ this.channel_prefix = modelKlass.identifiedBy;
8
+ this.map = Object.create(null);
9
+ }
10
+
11
+ channelForId(id) {
12
+ return `${this.channel_prefix}/${id}`;
13
+ }
14
+
15
+ forModel(model) {
16
+ const id = model[model.constructor.identifierFieldName];
17
+ let models = this.map[id];
18
+ if (!models) {
19
+ models = [];
20
+ this.map[id] = models;
21
+ }
22
+ return { id, models };
23
+ }
24
+
25
+ onChange(id, data) {
26
+ const models = this.map[id];
27
+ if (!isEmpty(models)) {
28
+ const update = mapValues(data.update, '[1]');
29
+ for (let i = 0; i < models.length; i += 1) {
30
+ models[i].set(update);
31
+ }
32
+ }
33
+ }
34
+
35
+ observe(model) {
36
+ const { id, models } = this.forModel(model);
37
+ if (!models.includes(model)) {
38
+ models.push(model);
39
+ if (1 === models.length) {
40
+ const channel = this.channelForId(id);
41
+ this.channel.subscribe(channel);
42
+ }
43
+ }
44
+ }
45
+
46
+ remove(model) {
47
+ const { id, models } = this.forModel(model);
48
+ const indx = models.indexOf(model);
49
+ invariant(-1 !== indx, 'Asked to remove model from pubsub but it was never observed');
50
+ if (1 === models.length) {
51
+ delete this.map[id];
52
+ this.channel.unsubscribe(this.channelForId(id));
53
+ } else {
54
+ models.splice(indx, 1);
55
+ }
56
+ }
57
+ }
@@ -1,5 +1,5 @@
1
1
  import {
2
- isEmpty, isNil, extend, map, bindAll, inRange, find, range,
2
+ isEmpty, isNil, extend, map, bindAll, inRange, find, range, isUndefined,
3
3
  } from 'lodash';
4
4
  import { reaction, observe, toJS } from 'mobx';
5
5
 
@@ -32,13 +32,14 @@ export default class ArrayResult extends Result {
32
32
  );
33
33
  }
34
34
 
35
- @computed get updateKey() {
35
+ @computed get fingerprint() {
36
36
  return [
37
+ this.query.fingerprint,
37
38
  this.rows.length,
38
39
  (this.sortField ? this.sortField.id : 'none'),
39
40
  this.sortAscending,
40
41
  this.rowUpdateCount,
41
- ].join('-');
42
+ ].join(';');
42
43
  }
43
44
 
44
45
  insertRow() {
@@ -73,7 +74,7 @@ export default class ArrayResult extends Result {
73
74
  newValue.whenComplete(() => {
74
75
  const row = this.rows[index];
75
76
  this.query.info.loadableFields.forEach((f) => {
76
- if (model[f.id]) { row[f.dataIndex] = model[f.id]; }
77
+ if (!isUndefined(model[f.id])) { row[f.dataIndex] = model[f.id]; }
77
78
  });
78
79
  this.rowUpdateCount += 1;
79
80
  }, 99);
@@ -1,4 +1,5 @@
1
1
  import { get, includes } from 'lodash';
2
+ import classname from 'classnames';
2
3
  import { titleize } from '../../lib/util';
3
4
  import {
4
5
  BaseModel, identifiedBy, session, belongsTo, identifier, computed, observable,
@@ -17,15 +18,14 @@ export default class Field extends BaseModel {
17
18
  @session loadable = true;
18
19
  @session queryable = true;
19
20
  @session sortable = true;
20
- @session textAlign = 'left';
21
+ @session _className = '';
22
+ @session align = 'left';
21
23
  @session dataType = 'string'
22
24
  @session cellRenderer;
23
25
  @session defaultValue;
24
26
  @session fetchIndex;
25
27
  @session sortBy;
26
28
 
27
- @observable format;
28
- @observable component;
29
29
  @observable onColumnClick;
30
30
 
31
31
  @belongsTo query;
@@ -46,6 +46,22 @@ export default class Field extends BaseModel {
46
46
  return includes(['number'], this.queryType);
47
47
  }
48
48
 
49
+ set className(c) {
50
+ this._className = c;
51
+ }
52
+
53
+ @computed get headerClassName() {
54
+ return classname('header', this.className);
55
+ }
56
+
57
+ @computed get className() {
58
+ return classname(this._className, {
59
+ r: 'right' === this.align,
60
+ l: 'left' === this.align,
61
+ c: 'center' === this.align,
62
+ });
63
+ }
64
+
49
65
  @computed get dataIndex() {
50
66
  if (!this.loadable) { return null; }
51
67
 
@@ -1,9 +1,10 @@
1
1
  import { merge } from 'lodash';
2
+ import { when } from 'mobx';
2
3
  import {
3
4
  BaseModel, identifiedBy, identifier, belongsTo, field, computed,
4
5
  } from './base';
5
6
  import Sync from './sync';
6
-
7
+ import Config from '../config';
7
8
  import Asset from './asset';
8
9
 
9
10
  @identifiedBy('hippo/system-settings')
@@ -21,4 +22,18 @@ export default class SystemSettings extends BaseModel {
21
22
  @computed get syncUrl() {
22
23
  return this.constructor.syncUrl;
23
24
  }
25
+
26
+ get syncData() {
27
+ return this.serialize();
28
+ }
29
+
30
+ set syncData(data) {
31
+ super.syncData = data;
32
+ if (this.logo && this.logo.isDirty) {
33
+ when(
34
+ () => !this.logo.isDirty,
35
+ () => { Config.logo = this.logo.file_data; },
36
+ );
37
+ }
38
+ }
24
39
  }
@@ -4,18 +4,19 @@ import {
4
4
  } from './base';
5
5
  import Config from '../config';
6
6
 
7
- const CACHE = observable({
8
- Tenant: undefined,
9
- });
7
+
8
+ const CACHED = observable.box();
10
9
 
11
10
  @identifiedBy('hippo/tenant')
12
11
  export default class Tenant extends BaseModel {
13
12
  @computed static get current() {
14
- if (!CACHE.Tenant) {
15
- CACHE.Tenant = new Tenant();
16
- CACHE.Tenant.fetch({ query: 'current' });
13
+ let tenant = CACHED.get();
14
+ if (!tenant) {
15
+ tenant = new Tenant();
16
+ CACHED.set(tenant);
17
+ tenant.fetch({ query: 'current' });
17
18
  }
18
- return CACHE.Tenant;
19
+ return tenant;
19
20
  }
20
21
 
21
22
  @identifier id;
@@ -3,17 +3,16 @@ import PropTypes from 'prop-types';
3
3
  import { observable, action } from 'mobx';
4
4
  import { observer } from 'mobx-react';
5
5
  import { map, compact, invoke } from 'lodash';
6
+ import { Row, Col } from 'react-flexbox-grid';
6
7
  import Heading from 'grommet/components/Heading';
7
8
  import Header from 'grommet/components/Header';
8
- import Button from 'grommet/components/Button';
9
- import SaveIcon from 'grommet/components/icons/base/Save';
10
-
11
- import Screen from 'hippo/components/screen';
12
- import Asset from 'hippo/components/asset';
13
- import Settings from 'hippo/models/system-setting';
14
9
 
15
- import { Row, Col } from 'react-flexbox-grid';
16
- import ScreenInstance from 'hippo/screens/instance';
10
+ import Screen from '../components/screen';
11
+ import Asset from '../components/asset';
12
+ import Settings from '../models/system-setting';
13
+ import Warning from '../components/warning-notification';
14
+ import SaveButton from '../components/save-button';
15
+ import ScreenInstance from '../screens/instance';
17
16
  import Extensions from '../extensions';
18
17
  import MailerConfig from './system-settings/mailer-config';
19
18
  import TenantSettings from './system-settings/tenant';
@@ -68,14 +67,13 @@ export default class SystemSettings extends React.PureComponent {
68
67
  return (
69
68
  <Screen {...this.props}>
70
69
  <Header fixed>
71
- <Button
72
- primary
73
- icon={<SaveIcon />}
74
- label='Save'
70
+ <SaveButton
71
+ model={this.settings}
75
72
  onClick={this.onSave}
76
73
  />
77
74
  </Header>
78
75
  <Heading>{this.props.screen.definition.title}</Heading>
76
+ <Warning message={this.settings.errorMessage} />
79
77
  <TenantSettings ref={this.setTenantRef} />
80
78
  <Heading tag="h3">Images</Heading>
81
79
  <Row>
@@ -3,15 +3,15 @@ import PropTypes from 'prop-types';
3
3
  import { Row, Col } from 'react-flexbox-grid';
4
4
 
5
5
  import { observer } from 'mobx-react';
6
- import { action } from 'mobx';
6
+ import { observable, action } from 'mobx';
7
7
 
8
8
  import Box from 'grommet/components/Box';
9
9
  import Button from 'grommet/components/Button';
10
- import Warning from 'hippo/components/warning-notification';
10
+ import Warning from '../../components/warning-notification';
11
+ import SaveButton from '../../components/save-button';
12
+ import Query from '../../models/query';
11
13
 
12
- import Query from 'hippo/models/query';
13
-
14
- import { Form, Field, FormState, nonBlank, validEmail, booleanValue } from 'hippo/components/form';
14
+ import { Form, Field, FormState, nonBlank, validEmail, booleanValue } from '../../components/form';
15
15
 
16
16
  @observer
17
17
  export default class EditForm extends React.PureComponent {
@@ -22,7 +22,7 @@ export default class EditForm extends React.PureComponent {
22
22
  }
23
23
 
24
24
  static desiredHeight = 300
25
-
25
+ @observable errorMessage;
26
26
  formState = new FormState();
27
27
 
28
28
  constructor(props) {
@@ -43,7 +43,7 @@ export default class EditForm extends React.PureComponent {
43
43
  @action.bound
44
44
  onSaved(user) {
45
45
  if (user.errors) {
46
- this.errorMessage = user.lastServerMessage;
46
+ this.errorMessage = user.errorMessage;
47
47
  } else {
48
48
  this.props.onComplete();
49
49
  }
@@ -70,12 +70,11 @@ export default class EditForm extends React.PureComponent {
70
70
  <Field md={4} xs={6} type="password" name="password" />
71
71
  <Field md={4} xs={6} type="checkbox" name="is_admin" validate={booleanValue} />
72
72
  <Col md={4} xs={6}>
73
- <Box direction="row">
73
+ <Box direction="row" justify="between">
74
74
  <Button label="Cancel" onClick={this.onCancel} accent />
75
- <Button
76
- label="Save"
75
+ <SaveButton
77
76
  onClick={this.formState.isValid ? this.onSave : null}
78
- primary
77
+ model={this.user}
79
78
  />
80
79
  </Box>
81
80
  </Col>
@@ -70,19 +70,22 @@ class Workspace extends React.Component {
70
70
  <LoginDialog />
71
71
  <Sidebar
72
72
  styles={{ sidebar: { zIndex: 5 } }}
73
- sidebar={<Menu onDockToggle={this.toggleSidebarDocked} />}
73
+ sidebar={<Menu
74
+ isOpen={this.sidebarOpen}
75
+ isDocked={this.sidebarDocked}
76
+ onCloseMenu={this.toggleSidebarDocked}
77
+ onDockToggle={this.toggleSidebarDocked}
78
+ />}
74
79
  open={this.sidebarOpen}
75
80
  docked={this.sidebarDocked}
76
81
  onSetOpen={this.onSetSidebarOpen}
77
82
  >
78
-
79
83
  <Button
80
84
  primary
81
85
  icon={<CirclePlayIcon />}
82
86
  onClick={this.toggleSidebarDocked}
83
87
  className={cn('sidebar-toggle', { 'is-open': this.sidebarOpen })}
84
88
  />
85
-
86
89
  <Switch>
87
90
  <Route name='screen' path="/:screenId/:identifier?" component={Screen} />
88
91
  <Route component={NoMatch} />
@@ -1,12 +1,9 @@
1
1
  import React from 'react';
2
-
3
2
  import PropTypes from 'prop-types';
4
-
5
3
  import { action } from 'mobx';
6
4
  import { observer } from 'mobx-react';
7
- import Icon from 'hippo/components/icon';
8
-
9
5
  import Anchor from 'grommet/components/Anchor';
6
+ import Icon from '../components/icon';
10
7
 
11
8
  @observer
12
9
  export default class MenuOption extends React.Component {
@@ -31,8 +28,8 @@ export default class MenuOption extends React.Component {
31
28
  const { screen } = this.props;
32
29
  return (
33
30
  <Anchor path={`/${screen.id}/`} onClick={this.activateScreen}>
34
- {screen.title}
35
31
  <Icon name={screen.icon} />
32
+ {screen.title}
36
33
  </Anchor>
37
34
  );
38
35
  }
@@ -5,9 +5,12 @@ import { isEmpty, get } from 'lodash';
5
5
  import PropTypes from 'prop-types';
6
6
  import Box from 'grommet/components/Box';
7
7
  import Sidebar from 'grommet/components/Sidebar';
8
+ import Button from 'grommet/components/Button';
9
+ import CloseIcon from 'grommet/components/icons/base/Close';
8
10
  import Header from 'grommet/components/Header';
9
11
  import Anchor from 'grommet/components/Anchor';
10
12
  import Menu from 'grommet/components/Menu';
13
+ import Icon from '../components/icon';
11
14
  import Group from './menu-group';
12
15
  import Screens from '../screens';
13
16
  import MenuOption from './menu-option';
@@ -34,6 +37,7 @@ class Logout extends React.PureComponent {
34
37
  return (
35
38
  <Menu direction="column" align="start" justify="between" primary>
36
39
  <Anchor label="Log Out" onClick={this.onLogoutClick}>
40
+ <Icon name="sign-out" />
37
41
  Log Out
38
42
  </Anchor>
39
43
  </Menu>
@@ -41,7 +45,6 @@ class Logout extends React.PureComponent {
41
45
  }
42
46
  }
43
47
 
44
-
45
48
  const Logo = observer(() => {
46
49
  if (!get(Config, 'logo.thumbnail')) {
47
50
  if (!isEmpty(Config.product_name)) {
@@ -65,14 +68,23 @@ export default class WorkspaceMenu extends React.PureComponent {
65
68
  );
66
69
  }
67
70
 
71
+ renderClose() {
72
+ if (this.props.isOpen && !this.props.isDocked) {
73
+ return <Button icon={<CloseIcon />} onClick={this.props.onCloseMenu} plain />;
74
+ }
75
+ return null;
76
+ }
77
+
68
78
  render() {
69
79
  return (
70
80
  <Sidebar
71
81
  full size="small" separator="right"
72
82
  colorIndex="brand"
83
+ className="screen-selection-menu"
73
84
  >
74
85
  <Header justify="between" size="large" pad={{ horizontal: 'medium' }}>
75
86
  <Logo />
87
+ {this.renderClose()}
76
88
  </Header>
77
89
  {Screens.activeGroups.map(g => <Group key={g.id} group={g} />)}
78
90
  {this.renderUnGrouped()}