hippo-fw 0.9.6 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
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()}