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,38 +1,3 @@
1
1
  #hippo-root {
2
- @import '~input-moment/dist/input-moment.css';
3
- .date-time.component {
4
-
5
- .tab {
6
- display: none;
7
- &.active { display: block; }
8
- }
9
-
10
- .tabs {
11
- > * { padding: 0.23rem; }
12
- margin-bottom: 10px;
13
- .grommetux-control-icon { margin-right: 0.5rem; }
14
- }
15
-
16
- .toolbar {
17
- display: flex;
18
- justify-content: space-between;
19
- button {
20
- min-width: 30px;
21
- padding: 0;
22
- &.next-month {
23
- &:before {
24
- content: ">";
25
- display: block;
26
- }
27
- }
28
- &.prev-month {
29
- &:before {
30
- content: "<";
31
- display: block;
32
- }
33
- }
34
- }
35
- }
36
- }
37
-
2
+ @import '~flatpickr/dist/themes/material_orange.css';
38
3
  }
@@ -3,7 +3,7 @@
3
3
  }] */
4
4
  import { observable, computed, when, action } from 'mobx';
5
5
  import {
6
- pick, isFunction, mapValues, every, get, set, filter, isNil, each,
6
+ pick, isFunction, mapValues, every, get, set, filter, isNil, each, extend,
7
7
  } from 'lodash';
8
8
 
9
9
  export class FormField {
@@ -26,7 +26,7 @@ export class FormField {
26
26
 
27
27
  update(attrs) {
28
28
  each(pick(attrs, [
29
- 'name', 'default', 'help', 'validate',
29
+ 'name', 'default', 'help', 'validate', 'value',
30
30
  ]), (v, k) => {
31
31
  this[k] = isFunction(v) ? v.call(this) : v;
32
32
  });
@@ -91,7 +91,7 @@ export class FormState {
91
91
  if (field) {
92
92
  field.update(attrs);
93
93
  } else {
94
- field = new FormField(name, attrs);
94
+ field = new FormField(name, extend({}, attrs, { value: get(this.values, name) }));
95
95
  this.fields.set(name, field);
96
96
  }
97
97
  return field;
@@ -131,6 +131,7 @@ export class FormState {
131
131
 
132
132
  @action
133
133
  set(values) {
134
+ this.values = values;
134
135
  this.fields.forEach((field, name) => {
135
136
  const value = get(values, name);
136
137
  field.value = isNil(value) ? '' : value;
@@ -13,6 +13,8 @@ import SelectWrapper from './fields/select-wrapper';
13
13
  import TextWrapper from './fields/text-wrapper';
14
14
  import CheckBoxWrapper from './fields/checkbox-wrapper';
15
15
  import Label from './fields/label';
16
+ import TimeZoneSelect from '../../components/time-zone-select';
17
+
16
18
  import './fields/form-field.scss';
17
19
 
18
20
  const TypesMapping = {
@@ -21,6 +23,7 @@ const TypesMapping = {
21
23
  label: Label,
22
24
  select: SelectWrapper,
23
25
  number: NumberInput,
26
+ timezone: TimeZoneSelect,
24
27
  checkbox: CheckBoxWrapper,
25
28
  };
26
29
 
@@ -32,12 +35,14 @@ export default class FormField extends React.PureComponent {
32
35
  name: PropTypes.string.isRequired,
33
36
  className: PropTypes.string,
34
37
  type: PropTypes.string,
38
+ tabIndex: PropTypes.number,
35
39
  }, Col.PropTypes)
36
40
 
37
41
  static defaultProps = {
38
42
  label: '',
39
43
  className: '',
40
44
  type: 'text',
45
+ tabIndex: 0,
41
46
  }
42
47
 
43
48
  focus() {
@@ -61,7 +66,7 @@ export default class FormField extends React.PureComponent {
61
66
 
62
67
  render() {
63
68
  const {
64
- name, className, autoFocus, type, children, label,
69
+ name, className, autoFocus, type, children, label, tabIndex,
65
70
  validate: _, formState: __, help: ___, ...otherProps
66
71
  } = getColumnProps(this.props);
67
72
 
@@ -76,6 +81,7 @@ export default class FormField extends React.PureComponent {
76
81
  >
77
82
  <InputTag
78
83
  name={name}
84
+ tabIndex={tabIndex}
79
85
  autoFocus={autoFocus}
80
86
  ref={this.setRef}
81
87
  value={this.field.value || ''}
@@ -8,7 +8,7 @@ import DateTime from '../../date-time';
8
8
  @observer
9
9
  export default class DateWrapper extends React.PureComponent {
10
10
  static defaultProps = {
11
- format: 'M/D/YYYY h:mm a',
11
+ format: 'M/d/Y h:iK',
12
12
  }
13
13
  static childContextTypes = { onDropChange: PropTypes.func }
14
14
  @observable isSelecting;
@@ -26,9 +26,9 @@ export default class DateWrapper extends React.PureComponent {
26
26
  this.isSelecting = active;
27
27
  }
28
28
 
29
- @action.bound onDateChange(date) {
30
- this.dateValue = date;
31
- this.props.onChange({ target: { value: this.dateValue } });
29
+ @action.bound onDateChange({ target: { value } }) {
30
+ this.dateValue = value;
31
+ this.props.onChange({ target: { value } });
32
32
  }
33
33
 
34
34
  @action.bound onBlur(ev) {
@@ -1,3 +1,12 @@
1
- import Icon from 'react-fontawesome';
1
+ import React from 'react'; // eslint-disable-line no-unused-vars
2
+ import FAIcon from 'react-fontawesome';
3
+ import cn from 'classnames';
4
+
5
+ const Icon = (props) => {
6
+ const { className, ...otherProps } = props;
7
+ return (
8
+ <FAIcon className={cn('icon', className)} {...otherProps} />
9
+ );
10
+ };
2
11
 
3
12
  export default Icon;
@@ -74,12 +74,17 @@ class Clause extends React.PureComponent {
74
74
  this.props.clause.value = ev.target.value;
75
75
  }
76
76
 
77
+ @action.bound
78
+ setMenuRef(ref) {
79
+ this.menuRef = ref;
80
+ }
81
+
77
82
  render() {
78
83
  const { clause } = this.props;
79
84
  return (
80
85
  <Box direction='row' pad={{ between: 'small' }}>
81
86
  <Menu
82
- ref={ref => (this.menuRef = ref)}
87
+ ref={this.setMenuRef}
83
88
  size='large'
84
89
  pad='small'
85
90
  closeOnClick={false} icon={<ClauseFilter clause={clause} />}
@@ -53,7 +53,7 @@ export default class QueryLayer extends React.PureComponent {
53
53
  flex='grow'
54
54
  >
55
55
  <Box flex="grow">
56
- <Heading tag="h3" margin="none">Find {title}</Heading>
56
+ <Heading tag="h3" margin="none">{title}</Heading>
57
57
  </Box>
58
58
  <Button plain icon={<CloseIcon />} onClick={onClose} />
59
59
  </Box>
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { observer } from 'mobx-react';
4
+ import { computed } from 'mobx';
5
+ import { get } from 'lodash';
6
+ import cn from 'classnames';
7
+
8
+ import Button from 'grommet/components/Button';
9
+ import Spinning from 'grommet/components/icons/Spinning';
10
+ import SaveIcon from 'grommet/components/icons/base/Save';
11
+ import { BaseModel } from '../models/base';
12
+
13
+
14
+ @observer
15
+ export default class SaveButton extends React.PureComponent {
16
+ static propTypes = {
17
+ busy: PropTypes.bool,
18
+ model: PropTypes.instanceOf(BaseModel),
19
+ busyLabel: PropTypes.string,
20
+ label: PropTypes.string,
21
+ }
22
+
23
+ static defaultProps = {
24
+ label: 'Save',
25
+ busyLabel: 'Saving…',
26
+ }
27
+
28
+ @computed get isBusy() {
29
+ return this.props.busy || get(this.props, 'model.syncInProgress.isUpdate');
30
+ }
31
+
32
+ @computed get label() {
33
+ return this.isBusy ? this.props.busyLabel : this.props.label;
34
+ }
35
+
36
+ @computed get icon() {
37
+ return this.isBusy ? <Spinning /> : <SaveIcon />;
38
+ }
39
+
40
+ render() {
41
+ // eslint-disable-next-line no-unused-vars
42
+ const { label, icon, props: { busyLabel, busy, model, label: _, ...props } } = this;
43
+
44
+ return (
45
+ <Button
46
+ {...props}
47
+ primary
48
+ className={cn('save-button', this.props.className)}
49
+ icon={icon}
50
+ disabled={busy}
51
+ label={label}
52
+ />
53
+ );
54
+ }
55
+ }
@@ -16,6 +16,11 @@ import { plugins, defaultPlugin } from './text-editor/plugins';
16
16
  import DisplayModeToggle from './text-editor/display-modes';
17
17
  import './text-editor/text-editor.scss';
18
18
 
19
+ const editorInstance = new Editor({
20
+ plugins,
21
+ editables: [createEmptyState()],
22
+ defaultPlugin,
23
+ });
19
24
 
20
25
  @observer
21
26
  export default class TextEditor extends React.PureComponent {
@@ -32,11 +37,7 @@ export default class TextEditor extends React.PureComponent {
32
37
  componentWillMount() {
33
38
  const content = toJS(this.props.defaultContent);
34
39
  this.content = isEmpty(content) ? createEmptyState() : content;
35
- this.editor = new Editor({
36
- plugins,
37
- editables: [this.content],
38
- defaultPlugin,
39
- });
40
+ editorInstance.trigger.editable.add(this.content);
40
41
  }
41
42
 
42
43
  @action.bound
@@ -56,21 +57,21 @@ export default class TextEditor extends React.PureComponent {
56
57
  >
57
58
  <div className="text-editor">
58
59
  <DisplayModeToggle
59
- editor={this.editor}
60
+ editor={editorInstance}
60
61
  onSave={this.onSave}
61
62
  >
62
63
  {this.props.children}
63
64
  </DisplayModeToggle>
64
65
  <div className="text-editor-content">
65
66
  <Editable
66
- editor={this.editor}
67
+ editor={editorInstance}
67
68
  id={this.content.id}
68
69
  onAddImage={this.props.onAddImage}
69
70
  onChange={this.onEditStateChange}
70
71
  />
71
72
  </div>
72
- <Trash editor={this.editor}/>
73
- <Toolbar editor={this.editor} />
73
+ <Trash editor={editorInstance}/>
74
+ <Toolbar editor={editorInstance} />
74
75
  </div>
75
76
 
76
77
  </Provider>
@@ -2,7 +2,6 @@
2
2
 
3
3
  .grommet .text-editor {
4
4
 
5
- border: 1px solid #ddd;
6
5
  flex: 1;
7
6
  display: flex;
8
7
  flex-direction: column;
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import Select from 'grommet/components/Select';
4
+ import { observer } from 'mobx-react';
5
+ import { action, computed, observable } from 'mobx';
6
+ import { filter } from 'lodash';
7
+ import moment from 'moment-timezone';
8
+
9
+
10
+ @observer
11
+ export default class TimeZoneSelect extends React.PureComponent {
12
+ static propTypes = {
13
+ value: PropTypes.string,
14
+ onChange: PropTypes.func.isRequired,
15
+ }
16
+
17
+ static defaultProps = {
18
+ value: '',
19
+ }
20
+
21
+ @observable currentSearch;
22
+
23
+ choices = moment.tz.names();
24
+
25
+ get value() {
26
+ return this.props.value || moment.tz.guess();
27
+ }
28
+
29
+ @action.bound filterFn(tz) {
30
+ return this.currentSearch.test(tz);
31
+ }
32
+
33
+ @computed get names() {
34
+ if (this.currentSearch) {
35
+ return filter(moment.tz.names(), this.filterFn);
36
+ }
37
+ return this.choices;
38
+ }
39
+
40
+ @action.bound onSearch({ target: { value } }) {
41
+ this.currentSearch = new RegExp(`^${value}|/${value}`, 'i');
42
+ }
43
+
44
+ @action.bound
45
+ onChange({ value }) {
46
+ this.props.onChange({ value, target: { value } });
47
+ }
48
+
49
+ render() {
50
+ return (
51
+ <Select
52
+ placeHolder='None'
53
+ options={this.names}
54
+ onSearch={this.onSearch}
55
+ value={this.value}
56
+ onChange={this.onChange}
57
+ />
58
+ );
59
+ }
60
+ }
@@ -73,13 +73,12 @@ export default class Asset extends BaseModel {
73
73
  });
74
74
  }
75
75
 
76
-
77
76
  @computed get isDirty() {
78
- return Boolean(this.isNew || !this.file);
77
+ return Boolean(this.file);
79
78
  }
80
79
 
81
80
  @computed get exists() {
82
- return !!(this.file || this.id);
81
+ return !!(this.file || this.file_data);
83
82
  }
84
83
 
85
84
  @computed get isImage() {
@@ -117,9 +116,15 @@ export default class Asset extends BaseModel {
117
116
  }
118
117
  return fetch(`${Config.api_path}${Config.assets_path_prefix}`, options)
119
118
  .then(resp => resp.json())
119
+ .then(json => this.set(json.data))
120
120
  .then((json) => {
121
- this.file = undefined;
121
+ if (this.file) {
122
+ if (this.file.preview && window.URL.revokeObjectURL) {
123
+ window.URL.revokeObjectURL(this.file.preview);
124
+ }
125
+ this.file = undefined;
126
+ }
122
127
  return json;
123
- }).then(json => this.set(json.data));
128
+ });
124
129
  }
125
130
  }
@@ -101,7 +101,7 @@ export class BaseModel {
101
101
  }
102
102
 
103
103
  @action reset() {
104
- this.constructor.assignableKeys.forEach(k => (this[k] = null));
104
+ this.constructor.assignableKeys.forEach((k) => { this[k] = null; });
105
105
  }
106
106
 
107
107
  set syncData(data) {
@@ -1,69 +1,12 @@
1
1
  import { Atom, when, reaction } from 'mobx';
2
2
  import ActionCable from 'actioncable';
3
- import invariant from 'invariant';
4
- import { isEmpty, mapValues } from 'lodash';
3
+ import { BaseModel } from './base';
5
4
  import User from '../user';
6
5
  import Config from '../config';
7
-
8
6
  import PubSubCableChannel from './pub_sub/channel';
7
+ import PubSubMap from './pub_sub/map';
9
8
 
10
- let PubSub;
11
-
12
- class PubSubMap {
13
- constructor(modelKlass) {
14
- this.channel_prefix = modelKlass.identifiedBy;
15
- this.map = Object.create(null);
16
- }
17
-
18
- channelForId(id) {
19
- return `${this.channel_prefix}/${id}`;
20
- }
21
-
22
- forModel(model) {
23
- const id = model[model.constructor.identifierFieldName];
24
- let models = this.map[id];
25
- if (!models) {
26
- models = [];
27
- this.map[id] = models;
28
- }
29
- return { id, models };
30
- }
31
-
32
- onChange(id, data) {
33
- const models = this.map[id];
34
- if (!isEmpty(models)) {
35
- const update = mapValues(data.update, '[1]');
36
- for (let i = 0; i < models.length; i += 1) {
37
- models[i].set(update);
38
- }
39
- }
40
- }
41
-
42
- observe(model) {
43
- const { id, models } = this.forModel(model);
44
- if (!models.includes(model)) {
45
- models.push(model);
46
- if (1 === models.length) {
47
- const channel = this.channelForId(id);
48
- PubSub.channel.subscribe(channel);
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 = {
9
+ const PubSub = {
67
10
 
68
11
  types: new WeakMap(),
69
12
 
@@ -75,7 +18,9 @@ PubSub = {
75
18
  this.mapForModel(model).remove(model);
76
19
  },
77
20
 
78
- onModelChange(model, id, data) {
21
+ onModelChange(modelId, id, data) {
22
+ const model = BaseModel.findDerived(modelId);
23
+ if (!model) { return; }
79
24
  const map = this.types.get(model);
80
25
  if (map) {
81
26
  map.onChange(id, data);
@@ -86,7 +31,7 @@ PubSub = {
86
31
  const klass = model.constructor;
87
32
  let map = this.types.get(klass);
88
33
  if (!map) {
89
- map = new PubSubMap(klass);
34
+ map = new PubSubMap(klass, this.channel);
90
35
  this.types.set(klass, map);
91
36
  }
92
37
  return map;
@@ -95,6 +40,7 @@ PubSub = {
95
40
 
96
41
  onLoginChange() {
97
42
  if (User.isLoggedIn) {
43
+ if (PubSub.cable) { return; }
98
44
  const host = Config.api_host.replace(/^http/, 'ws');
99
45
  const url = `${host}${Config.api_path}/cable?token=${Config.access_token}`;
100
46
  ActionCable.startDebugging();
@@ -109,14 +55,23 @@ PubSub = {
109
55
 
110
56
  };
111
57
 
58
+ export default PubSub;
59
+
112
60
  export function onBoot() {
113
- reaction(
114
- () => User.isLoggedIn,
115
- PubSub.onLoginChange,
116
- { fireImmediately: true },
117
- );
61
+ PubSub.onLoginChange();
118
62
  }
119
63
 
64
+ when(
65
+ () => Config.isIntialized,
66
+ () => {
67
+ reaction(
68
+ () => User.isLoggedIn,
69
+ PubSub.onLoginChange,
70
+ { fireImmediately: true },
71
+ );
72
+ },
73
+ );
74
+
120
75
  export function stop() {
121
76
  PubSub.kill();
122
77
  }