hippo-fw 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/Gemfile +16 -11
  4. data/Rakefile +0 -7
  5. data/bin/hippo +5 -1
  6. data/client/hippo/__mocks__/config.js +2 -3
  7. data/client/hippo/boot.jsx +0 -2
  8. data/client/hippo/components/asset.jsx +1 -1
  9. data/client/hippo/components/form.jsx +8 -4
  10. data/client/hippo/components/form/fields.jsx +28 -14
  11. data/client/hippo/components/form/model.js +65 -20
  12. data/client/hippo/components/form/wrapper.jsx +11 -5
  13. data/client/hippo/components/icon.jsx +1 -1
  14. data/client/hippo/components/master-detail.jsx +66 -0
  15. data/client/hippo/components/master-detail.scss +50 -0
  16. data/client/hippo/components/record-finder.jsx +5 -5
  17. data/client/hippo/components/tool-tip.jsx +20 -0
  18. data/client/hippo/config.js +5 -3
  19. data/client/hippo/extensions/base.js +4 -0
  20. data/client/hippo/lib/smooth-scroll.js +17 -16
  21. data/client/hippo/models/asset.js +8 -10
  22. data/client/hippo/models/collection.js +1 -4
  23. data/client/hippo/models/query/array-result.js +11 -9
  24. data/client/hippo/models/sync.js +3 -3
  25. data/client/hippo/models/tenant.js +29 -0
  26. data/client/hippo/screens/system-settings.jsx +5 -4
  27. data/client/hippo/screens/system-settings/mailer-config.jsx +11 -17
  28. data/client/hippo/screens/system-settings/tenant.jsx +90 -0
  29. data/client/hippo/screens/user-management/edit-form.jsx +15 -25
  30. data/client/hippo/testing/index.js +1 -0
  31. data/client/hippo/workspace/styles.scss +0 -23
  32. data/command-reference-files/initial/.babelrc +10 -8
  33. data/command-reference-files/initial/Gemfile +1 -1
  34. data/{views/index.html → command-reference-files/initial/views/index.erb} +1 -0
  35. data/config/routes.rb +48 -17
  36. data/config/webpack.config.js +7 -12
  37. data/db/migrate/01_create_tenants.rb +13 -0
  38. data/db/migrate/{01_create_system_settings.rb → 02_create_system_settings.rb} +2 -1
  39. data/db/migrate/{02_create_assets.rb → 03_create_assets.rb} +2 -4
  40. data/db/migrate/{20140615031600_create_users.rb → 04_create_users.rb} +4 -2
  41. data/db/seed.rb +10 -1
  42. data/hippo-fw.gemspec +53 -51
  43. data/lib/hippo.rb +7 -1
  44. data/lib/hippo/access.rb +0 -1
  45. data/lib/hippo/access/roles/basic_user.rb +2 -0
  46. data/lib/hippo/api.rb +4 -3
  47. data/lib/hippo/{access → api}/authentication_provider.rb +3 -1
  48. data/lib/hippo/api/controller_base.rb +2 -2
  49. data/lib/hippo/api/handlers/asset.rb +28 -2
  50. data/lib/hippo/api/handlers/tenant.rb +26 -0
  51. data/lib/hippo/api/helper_methods.rb +5 -13
  52. data/lib/hippo/api/request_wrapper.rb +8 -1
  53. data/lib/hippo/api/root.rb +50 -51
  54. data/lib/hippo/api/route_set.rb +101 -0
  55. data/lib/hippo/api/routing.rb +9 -98
  56. data/lib/hippo/api/tenant_domain_router.rb +21 -0
  57. data/lib/hippo/asset.rb +1 -0
  58. data/lib/hippo/command.rb +1 -24
  59. data/lib/hippo/command/app.rb +4 -3
  60. data/lib/hippo/command/console.rb +7 -0
  61. data/lib/hippo/command/generate.rb +0 -5
  62. data/lib/hippo/command/guard.rb +1 -0
  63. data/lib/hippo/command/jest.rb +2 -2
  64. data/lib/hippo/command/server.rb +1 -3
  65. data/lib/hippo/command/webpack.rb +6 -26
  66. data/lib/hippo/configuration.rb +21 -13
  67. data/lib/hippo/db.rb +2 -0
  68. data/lib/hippo/db/migrations.rb +9 -2
  69. data/lib/hippo/extension.rb +49 -14
  70. data/lib/hippo/extension/definition.rb +0 -4
  71. data/lib/hippo/guard_tasks.rb +3 -11
  72. data/lib/hippo/mailer.rb +28 -16
  73. data/lib/hippo/model.rb +10 -0
  74. data/lib/hippo/numbers.rb +1 -1
  75. data/lib/hippo/rake_tasks.rb +7 -1
  76. data/lib/hippo/spec_helper.rb +33 -11
  77. data/lib/hippo/system_settings.rb +1 -0
  78. data/lib/hippo/templates/base.rb +1 -1
  79. data/lib/hippo/templates/mail.rb +26 -0
  80. data/lib/hippo/templates/tenant_change.rb +23 -0
  81. data/lib/hippo/tenant.rb +53 -0
  82. data/lib/hippo/user.rb +12 -6
  83. data/lib/hippo/version.rb +1 -1
  84. data/lib/hippo/webpack.rb +57 -0
  85. data/lib/hippo/{command → webpack}/client_config.rb +7 -21
  86. data/package.json +3 -3
  87. data/spec/client/components/__snapshots__/master-detail.spec.jsx.snap +22 -0
  88. data/spec/client/components/form.spec.jsx +14 -14
  89. data/spec/client/components/master-detail.spec.jsx +24 -0
  90. data/spec/client/components/record-finder.spec.jsx +5 -2
  91. data/spec/client/models/asset.spec.js +2 -13
  92. data/spec/client/models/base.spec.js +1 -11
  93. data/spec/client/models/query.spec.js +2 -4
  94. data/spec/client/models/sync.spec.js +7 -0
  95. data/spec/client/screens/__snapshots__/system-settings.spec.jsx.snap +79 -0
  96. data/spec/client/screens/system-settings-tenants.spec.jsx +18 -0
  97. data/spec/client/workspace/__snapshots__/menu.spec.jsx.snap +29 -313
  98. data/spec/client/workspace/menu.spec.jsx +1 -9
  99. data/spec/factories/tenant.rb +13 -0
  100. data/spec/fixtures/mail/test_email.liquid +1 -0
  101. data/spec/fixtures/{test_printer.tex → test_printer.tex.erb} +0 -0
  102. data/spec/server/api/controller_base_spec.rb +1 -1
  103. data/spec/server/api/tenant_change_spec.rb +24 -0
  104. data/spec/server/api/tenant_isolation_spec.rb +37 -0
  105. data/spec/server/asset_spec.rb +6 -6
  106. data/spec/server/command_spec.rb +0 -5
  107. data/spec/server/mailer_spec.rb +25 -23
  108. data/spec/server/numbers_spec.rb +12 -13
  109. data/spec/server/print/form_spec.rb +2 -1
  110. data/spec/server/strings_spec.rb +13 -13
  111. data/templates/.babelrc +10 -8
  112. data/templates/js/screen-definitions.js +8 -10
  113. data/templates/mail/tenant_change.liquid +13 -0
  114. data/{command-reference-files/initial/views/index.html → views/index.erb} +5 -2
  115. data/yarn.lock +22 -169
  116. metadata +56 -30
  117. data/client/hippo/components/form/field-prop-type.js +0 -16
  118. data/lib/hippo/api/default_routes.rb +0 -38
  119. data/lib/hippo/command/generate_component.rb +0 -28
  120. data/lib/hippo/command/generate_component.usage +0 -11
  121. data/lib/hippo/command/webpack_view.rb +0 -32
  122. data/lib/hippo/multi_server_boot.rb +0 -26
  123. data/lib/hippo/reloadable_view.rb +0 -13
  124. data/templates/client/components/.gitkeep +0 -0
  125. data/templates/client/components/BaseComponent.coffee +0 -9
  126. data/templates/client/components/Component.cjsx +0 -4
  127. data/templates/client/components/template.html +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 360b0939a3298ae53f570048af34125777a3f202
4
- data.tar.gz: 8bb44bc92271a71e436ae7fc55102c629c878301
3
+ metadata.gz: 646a4300a544e32e68520975fb4b9bad15e7ddd4
4
+ data.tar.gz: 98f55a67a95e64261cff0560af01143f2afaddeb
5
5
  SHA512:
6
- metadata.gz: 24c7a833728de14eeaec24dcae6fce1805c877f60d06ba579451a5d93730dbb61cc59b9559e5a7313fca099ec198ce5b27e8f05268299be026dc940e2ce29521
7
- data.tar.gz: 16d0cf71e576fd65d88a82defa3820afc943285e938f1626ebc6feed9b34c380e99ae1cee3852150fec24bc98dc2c6eb3bfe3b48d3032e9d3579f892e1c8a91d
6
+ metadata.gz: cfd18318f831660561a8e30f848c2b8f52540a2dc982b780ef7a4f331ac163b9c9073c2348dacd3fc22d62accd7367fd92b57563089592a8bd1562857df2eb25
7
+ data.tar.gz: 7862bfe0cc3c3e977c9958be2a8600522d9dbbf597d62968250d538ab0fc23393fab759c89a8789d9fc0c0722308d2ba4bc2daeb75bee5e8b5b2f41eada9d15a
@@ -1 +1 @@
1
- 2.4.0
1
+ 2.4.1
data/Gemfile CHANGED
@@ -1,17 +1,22 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem "yard-activerecord",
4
- git: 'https://github.com/nathanstitt/yard-activerecord',
5
- branch: 'develop'
3
+ gemspec
6
4
 
7
- gem "temping", '~> 3.9.0'
5
+ gem "activerecord-multi-tenant", git: 'https://github.com/citusdata/activerecord-multi-tenant.git', branch: 'release-0.5.1'
8
6
 
9
- gem 'puma'
10
- gem 'pry-byebug'
7
+ # gem "activerecord-multi-tenant", git: "https://github.com/nathanstitt/activerecord-multi-tenant", branch: 'query_rewriter'
11
8
 
12
- gem "knitter", git: "https://github.com/nathanstitt/knitter", branch: 'master'
13
- gem "webpack_driver", git: "https://github.com/nathanstitt/webpack_driver", branch: 'master'
14
- gem "guard-jest", git: "https://github.com/nathanstitt/guard-jest", branch: 'master'
15
- gem "bump"
9
+ # gem "webpack_driver", git: "https://github.com/nathanstitt/webpack_driver", branch: 'master'
16
10
 
17
- gemspec
11
+ group :development, :test do
12
+ gem "yard-activerecord",
13
+ git: 'https://github.com/nathanstitt/yard-activerecord',
14
+ branch: 'develop'
15
+
16
+ gem "temping", '~> 3.9.0'
17
+
18
+ gem 'puma'
19
+ gem 'pry-byebug'
20
+ # gem "guard-jest", git: "https://github.com/nathanstitt/guard-jest", branch: 'master'
21
+ gem "bump"
22
+ end
data/Rakefile CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'bundler/setup'
2
2
  require "bundler/gem_tasks"
3
- require 'rake/testtask'
4
3
  require 'yard'
5
4
  require 'yard-activerecord'
6
5
  require_relative 'yard_ext/all'
@@ -11,12 +10,6 @@ require "bump/tasks"
11
10
 
12
11
  Dir.glob('tasks/*.rake').each { |r| load r}
13
12
 
14
- Rake::TestTask.new do |t|
15
- t.libs << 'test'
16
- t.pattern = "test/*_test.rb"
17
- end
18
-
19
-
20
13
  YARD::Rake::YardocTask.new do |t|
21
14
  t.files = ['lib/skr/concerns/*.rb','lib/**/*.rb','db/schema.rb']
22
15
  t.options = ["--title=Stockor Core Documentation",
data/bin/hippo CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative '../lib/hippo/cli'
3
+ require 'rubygems'
4
+ Bundler.require :default, ENV['HIPPO_ENV'] || "development"
5
+
6
+ require 'hippo'
7
+ require 'hippo/cli'
4
8
 
5
9
  Hippo::CLI.start(ARGV)
@@ -1,9 +1,8 @@
1
+ /* global jest */
1
2
  window.localStorage = {
2
-
3
3
  getItem() {
4
4
  return '{}';
5
5
  },
6
-
7
6
  };
8
7
 
9
8
  const config = jest.genMockFromModule('hippo/config');
@@ -11,7 +10,7 @@ const config = jest.genMockFromModule('hippo/config');
11
10
  config.bootstrapUserData = jest.fn();
12
11
  config.reset = jest.fn();
13
12
  Object.defineProperty(config, 'api_path', {
14
- value: '/api'
13
+ value: '/api',
15
14
  });
16
15
 
17
16
  export default config;
@@ -5,8 +5,6 @@ import { delay } from 'lodash';
5
5
  import { AppContainer } from 'react-hot-loader';
6
6
  import { withAsyncComponents } from 'react-async-component';
7
7
 
8
- import Config from './config';
9
-
10
8
  const Workspace = require('hippo/workspace').default;
11
9
 
12
10
  let Root;
@@ -45,7 +45,7 @@ export default class Asset extends React.PureComponent {
45
45
  }
46
46
 
47
47
  preview() {
48
- if (this.asset) {
48
+ if (this.asset && this.asset.exists) {
49
49
  return this.asset.isImage ?
50
50
  <img src={this.asset.previewUrl} alt="" /> :
51
51
  <DocumentIcon size="xlarge" type="status" />;
@@ -9,8 +9,7 @@ import { isBlank } from '../lib/util';
9
9
 
10
10
  export Form from './form/wrapper';
11
11
  export Field from './form/fields';
12
- export FieldDefinitions from './form/model';
13
- export FormFieldPropType from './form/field-prop-type';
12
+ export { FormField, FormState } from './form/model';
14
13
 
15
14
  function buildTest(options, defaultOptions) {
16
15
  return merge({}, defaultOptions, options);
@@ -44,8 +43,13 @@ export function validEmail(options) {
44
43
  return buildTest(options, { message: 'must be a valid email', test: email => isEmail(email || '') });
45
44
  }
46
45
 
47
- export function validURL(options) {
48
- return buildTest(options, { message: 'must be a valid address', test: url => isURL(url || '') });
46
+ export function validURL(options = {}) {
47
+ return buildTest(options, {
48
+ message: 'must be a valid address',
49
+ test(url) {
50
+ return (options.allowBlank && !url) || isURL(url || '');
51
+ },
52
+ });
49
53
  }
50
54
 
51
55
  export function booleanValue(options = {}) {
@@ -5,12 +5,13 @@ import { inject, observer } from 'mobx-react';
5
5
  import classnames from 'classnames';
6
6
  import { Col, getColumnProps } from 'react-flexbox-grid';
7
7
 
8
+ import invariant from 'invariant';
8
9
  import Field from 'grommet/components/FormField';
9
10
  import NumberInput from 'grommet/components/NumberInput';
10
11
 
11
12
  import { titleize } from '../../lib/util';
12
- import FormFieldPropType from './field-prop-type';
13
13
 
14
+ import { FormField as FormFieldModel } from './model';
14
15
  import DateWrapper from './fields/date-wrapper';
15
16
  import SelectWrapper from './fields/select-wrapper';
16
17
  import TextWrapper from './fields/text-wrapper';
@@ -27,8 +28,17 @@ const TypesMapping = {
27
28
 
28
29
  };
29
30
 
30
- @inject('formFields') @observer
31
+
32
+ @inject('formState')
33
+ @observer
31
34
  export default class FormField extends React.PureComponent {
35
+ static propTypes = Object.assign({
36
+ label: PropTypes.string,
37
+ name: PropTypes.string.isRequired,
38
+ className: PropTypes.string,
39
+ type: PropTypes.string,
40
+ }, Col.PropTypes)
41
+
32
42
  static defaultProps = {
33
43
  label: '',
34
44
  className: '',
@@ -39,33 +49,37 @@ export default class FormField extends React.PureComponent {
39
49
  if (this.inputRef) { this.inputRef.focus(); }
40
50
  }
41
51
 
42
- static propTypes = Object.assign({
43
- label: PropTypes.string,
44
- name: PropTypes.string.isRequired,
45
- formFields: FormFieldPropType,
46
- className: PropTypes.string,
47
- type: PropTypes.string,
48
- }, Col.PropTypes)
52
+ componentWillMount() {
53
+ this.field = this.props.formState.setField(this.props.name, this.props);
54
+ }
55
+
56
+ componentWillReceiveProps(nextProps) {
57
+ invariant(nextProps.name === this.props.name,
58
+ `cannot update 'name' prop from ${this.props.name} to ${nextProps.name}`);
59
+ this.field.update(this.props);
60
+ }
49
61
 
50
62
  render() {
51
63
  const {
52
- name, className, autoFocus, type, children, label, formFields, ...otherProps
64
+ name, className, autoFocus, type, children, label,
65
+ validate: _, formState: __, help: ___, ...otherProps
53
66
  } = getColumnProps(this.props);
54
67
  const InputTag = TypesMapping[type] || TypesMapping.text;
55
- const field = formFields.get(name);
68
+
56
69
  return (
57
70
  <div className={classnames('form-field', className)}>
58
71
  <Field
59
72
  label={label || titleize(name)}
60
- error={field.invalidMessage}
73
+ error={this.field.invalidMessage}
74
+ help={this.field.help}
61
75
  >
62
76
  <InputTag
63
77
  name={name}
64
78
  autoFocus={autoFocus}
65
79
  ref={f => (this.inputRef = f)}
66
- value={field.value || ''}
80
+ value={this.field.value || ''}
67
81
  type={InputTag === TypesMapping.text ? this.props.type : undefined}
68
- {...field.events}
82
+ {...this.field.events}
69
83
  {...otherProps}
70
84
  />
71
85
  {children}
@@ -1,30 +1,41 @@
1
- /* eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsFor": ["field", "model"] }] */
2
- import { observable, computed, when, action, } from 'mobx';
1
+ /* eslint no-param-reassign: ["error", {
2
+ "props": true, "ignorePropertyModificationsFor": ["field", "model"]
3
+ }] */
4
+ import { observable, computed, when, action } from 'mobx';
3
5
  import {
4
- extend, isObject, isFunction, mapValues, every, find, get,
6
+ pick, isFunction, mapValues, every, get, filter, isNil, each,
5
7
  } from 'lodash';
6
8
 
7
- class Field {
9
+ export class FormField {
8
10
  name: '';
9
11
  @observable isTouched = false
12
+ @observable isChanged = false;
10
13
  @observable value = '';
11
14
  @observable message = '';
15
+ @observable help;
16
+ @observable validate;
17
+ @observable default;
12
18
 
13
19
  constructor(name, attrs) {
14
20
  this.name = name;
15
- let extendWith = isFunction(attrs) ? attrs() : attrs;
16
- if (!isObject(extendWith)) {
17
- extendWith = { test: extendWith };
18
- }
19
- extend(this, extendWith);
21
+ this.update(attrs);
20
22
  if (this.default) {
21
23
  this.value = this.default;
22
24
  }
23
25
  }
24
26
 
27
+ update(attrs) {
28
+ each(pick(attrs, [
29
+ 'name', 'default', 'help', 'validate',
30
+ ]), (v, k) => {
31
+ this[k] = isFunction(v) ? v.call(this) : v;
32
+ });
33
+ }
34
+
25
35
  @action.bound
26
- onChange(ev) {
27
- this.value = ev.target.value;
36
+ onChange({ target: { value: updatedValue } }) {
37
+ this.isChanged = (this.value !== updatedValue);
38
+ this.value = updatedValue;
28
39
  }
29
40
 
30
41
  @action.bound
@@ -33,11 +44,12 @@ class Field {
33
44
  }
34
45
 
35
46
  @computed get isValid() {
36
- return !!(!this.test || this.test(this.value));
47
+ if (!this.validate) { return true; }
48
+ return !!this.validate.test(this.value);
37
49
  }
38
50
 
39
51
  @computed get invalidMessage() {
40
- return (!this.isValid && this.isTouched) ? this.message : null;
52
+ return (!this.isValid && this.isTouched) ? this.validate.message : null;
41
53
  }
42
54
 
43
55
  get events() {
@@ -47,33 +59,66 @@ class Field {
47
59
 
48
60
  };
49
61
  }
62
+
63
+ reset() {
64
+ this.value = '';
65
+ this.isTouched = false;
66
+ this.isChanged = false;
67
+ }
50
68
  }
51
69
 
52
70
 
53
- export default class FormFieldDefinitions {
71
+ export class FormState {
54
72
 
55
73
  fields = observable.map();
56
74
 
57
- constructor(fields) {
75
+ @action
76
+ setFields(fields) {
58
77
  this.fields.replace(
59
- mapValues(fields, (field, name) => new Field(name, field)),
78
+ mapValues(fields, (field, name) => new FormField(name, field)),
60
79
  );
61
80
  }
62
81
 
82
+ @action
83
+ setField(name, attrs) {
84
+ let field = this.fields.get(name);
85
+ if (field) {
86
+ field.update(attrs);
87
+ } else {
88
+ field = new FormField(name, attrs);
89
+ this.fields.set(name, field);
90
+ }
91
+ return field;
92
+ }
93
+
94
+ @computed get invalidFields() {
95
+ return filter(this.fields.values(), { isValid: false });
96
+ }
97
+
63
98
  @computed get isValid() {
64
- return every(this.fields.values(), 'isValid');
99
+ return 0 === this.invalidFields.length;
65
100
  }
66
101
 
67
102
  @computed get isTouched() {
68
103
  return !every(this.fields.values(), { isTouched: false });
69
104
  }
70
105
 
71
- get(name) {
72
- return this.fields.get(name);
106
+ get(path, defaultValue) {
107
+ const [name, ...rest] = path.split('.');
108
+ const field = this.fields.get(name);
109
+ if (!field) { return defaultValue; }
110
+ if (rest.length) {
111
+ return get(field, rest.join('.'), defaultValue);
112
+ }
113
+ return field;
114
+ }
115
+
116
+ reset() {
117
+ this.fields.forEach(field => field.reset());
73
118
  }
74
119
 
75
120
  set(values) {
76
- this.fields.forEach((field, name) => (field.value = values[name]));
121
+ this.fields.forEach((field, name) => (field.value = isNil(values[name]) ? '' : values[name]));
77
122
  }
78
123
 
79
124
  setFromModel(model) {
@@ -2,30 +2,36 @@ import React from 'react';
2
2
 
3
3
  import { Provider, observer } from 'mobx-react';
4
4
 
5
- import FormFields from './model';
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
11
  children: React.PropTypes.node.isRequired,
12
- fields: React.PropTypes.instanceOf(FormFields),
12
+ state: React.PropTypes.instanceOf(FormState),
13
13
  tag: React.PropTypes.string,
14
14
  className: React.PropTypes.string,
15
15
  }
16
16
 
17
+ static get defaultProps() {
18
+ return {
19
+ state: new FormState(),
20
+ };
21
+ }
22
+
17
23
  renderTagless() {
18
24
  return (
19
- <Provider formFields={this.props.fields}>
25
+ <Provider formState={this.props.state}>
20
26
  {this.props.children}
21
27
  </Provider>
22
28
  );
23
29
  }
24
30
 
25
31
  renderTagged() {
26
- const { tag: Tag, fields, children, ...otherProps } = this.props;
32
+ const { tag: Tag, state, children, ...otherProps } = this.props;
27
33
  return (
28
- <Provider formFields={fields}>
34
+ <Provider formState={state}>
29
35
  <Tag {...otherProps}>
30
36
  {children}
31
37
  </Tag>
@@ -7,7 +7,7 @@ import classnames from 'classnames';
7
7
 
8
8
  const DEFAULT_TOOLTIP_PROPS = { placement: 'top', trigger: 'click' };
9
9
 
10
- export default class Icon extends React.Component {
10
+ export default class Icon extends React.PureComponent {
11
11
 
12
12
  static propTypes = {
13
13
  type: PropTypes.string.isRequired,
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import cn from 'classnames';
4
+ import { observable, action, computed } from 'mobx';
5
+ import { observer } from 'mobx-react';
6
+ import { delay } from 'lodash';
7
+
8
+ import './master-detail.scss';
9
+
10
+ const DELAY_TIME = 500;
11
+
12
+ @observer
13
+ export default class MasterDetail extends React.PureComponent {
14
+
15
+
16
+ static propTypes = {
17
+ master: PropTypes.element.isRequired,
18
+ detail: PropTypes.element,
19
+ }
20
+
21
+ @observable detailVisible;
22
+
23
+ componentWillUnmount() {
24
+ if (this.pendingDefer) { this.clearTimeout(this.pendingDefer); }
25
+ }
26
+
27
+ @action.bound
28
+ setVisible(val) {
29
+ this.pendingDefer = delay(() => {
30
+ this.pendingDefer = null;
31
+ this.detailVisible = val;
32
+ }, DELAY_TIME);
33
+ }
34
+
35
+ setVisible
36
+ componentWillReceiveProps(nextProps) {
37
+ if (this.props.detail && !nextProps.detail) {
38
+ this.setVisible(false);
39
+ } else if (!this.props.detail && nextProps.detail) {
40
+ this.setVisible(true);
41
+ }
42
+ }
43
+
44
+ @computed get className() {
45
+ return cn('master-detail-wrapper', {
46
+ 'detail-visible': this.detailVisible,
47
+ 'has-detail': this.props.detail,
48
+ 'detail-removed': this.detailVisible && !this.props.detail,
49
+ });
50
+ }
51
+
52
+ render() {
53
+ return (
54
+ <div
55
+ className={this.className}
56
+ >
57
+ <div className="master">
58
+ {this.props.master}
59
+ </div>
60
+ <div className="detail">
61
+ {this.props.detail}
62
+ </div>
63
+ </div>
64
+ );
65
+ }
66
+ }