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,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
  }