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
@@ -0,0 +1,50 @@
1
+ .master-detail-wrapper {
2
+
3
+ @keyframes slideOutLeft {
4
+ from { transform: translate3d(0, 0, 0); }
5
+ to { transform: translate3d(-50%, 0, 0); }
6
+ }
7
+
8
+ @keyframes slideInLeft {
9
+ from { transform: translate3d(-70%, 0, 0); }
10
+ to { transform: translate3d(0, 0, 0); }
11
+ }
12
+
13
+ display: flex;
14
+ flex: 1;
15
+ animation-duration: 0.5s;
16
+ animation-fill-mode: both;
17
+ .master,
18
+ .detail {
19
+ flex: 1;
20
+ display: flex;
21
+ animation-duration: 0.49s;
22
+ animation-fill-mode: both;
23
+ }
24
+ .detail {
25
+ display: none;
26
+ }
27
+ &.has-detail {
28
+ width: 200%;
29
+ animation-name: slideOutLeft;
30
+ .master {
31
+
32
+ }
33
+ .detail {
34
+ display: flex;
35
+ }
36
+ }
37
+ &.detail-visible {
38
+ width: 100%;
39
+ animation-name: initial;
40
+ .master {
41
+ display: none;
42
+ }
43
+ }
44
+ &.detail-removed {
45
+ animation-name: slideInLeft;
46
+ .master {
47
+ display: flex
48
+ }
49
+ }
50
+ }
@@ -4,7 +4,7 @@ import { inject, observer } from 'mobx-react';
4
4
  import { action, observable } from 'mobx';
5
5
  import { get } from 'lodash';
6
6
 
7
- import { Form, Field, FormFieldPropType, nonBlank } from 'hippo/components/form';
7
+ import { Field } from 'hippo/components/form';
8
8
 
9
9
  import Button from 'grommet/components/Button';
10
10
  import SearchIcon from 'grommet/components/icons/base/Search';
@@ -17,7 +17,8 @@ import Query from '../models/query';
17
17
  import './record-finder/record-finder.scss';
18
18
  import QueryLayer from './record-finder/query-layer';
19
19
 
20
- @inject('formFields') @observer
20
+ @inject('formState')
21
+ @observer
21
22
  export default class RecordFinder extends React.PureComponent {
22
23
  static propTypes = {
23
24
  query: PropTypes.instanceOf(Query).isRequired,
@@ -25,7 +26,6 @@ export default class RecordFinder extends React.PureComponent {
25
26
  label: PropTypes.string,
26
27
  recordsTitle: PropTypes.string.isRequired,
27
28
  onRecordFound: PropTypes.func.isRequired,
28
- formFields: FormFieldPropType,
29
29
  }
30
30
 
31
31
  @observable showingSearch = false;
@@ -43,7 +43,7 @@ export default class RecordFinder extends React.PureComponent {
43
43
  @action.bound
44
44
  onRecordSelect(model) {
45
45
  this.showingSearch = false;
46
- this.props.formFields.get(this.props.name).value = model[this.props.name]
46
+ this.props.formState.get(this.props.name).value = model[this.props.name]
47
47
  this.props.onRecordFound(model);
48
48
  }
49
49
 
@@ -54,7 +54,7 @@ export default class RecordFinder extends React.PureComponent {
54
54
 
55
55
 
56
56
  get field() {
57
- return this.props.fields[this.props.name];
57
+ return this.props.formState.fields.get(this.props.name);
58
58
  }
59
59
 
60
60
  loadCurrentSelection() {
@@ -0,0 +1,20 @@
1
+ import React, { PureComponent } from 'react';
2
+ import { Tooltip as Tippy } from 'react-tippy';
3
+
4
+ import 'react-tippy/dist/tippy.css';
5
+
6
+ // Use a wrapper component even though it doesn't really add any functionality
7
+ // In the future we'll add a Manager wrapper so that multiple tooltips cannot be shown at once
8
+ export default class ToolTip extends PureComponent {
9
+
10
+ render() {
11
+ const { children, ...tipProps } = this.props;
12
+ return (
13
+ <Tippy
14
+ {...tipProps}
15
+ >
16
+ {children}
17
+ </Tippy>
18
+ );
19
+ }
20
+ }
@@ -1,17 +1,19 @@
1
- import { observable, autorun, observe } from 'mobx';
2
- import { keysIn, pick, assign, isString } from 'lodash';
1
+ import { observable, observe } from 'mobx';
2
+ import { keysIn, pick, assign, get } from 'lodash';
3
3
  import Extensions from './extensions';
4
4
 
5
5
  const STORAGE_KEY = 'hippo-user-data';
6
6
 
7
7
  class Config {
8
8
 
9
- @observable api_host = window.location.origin;
9
+ @observable api_host = get(window, 'location.origin', '');
10
10
  @observable api_path = '/api';
11
11
  @observable access_token;
12
12
  @observable root_view;
13
13
  @observable assets_path_prefix = '/assets';
14
14
  @observable user;
15
+ @observable website_domain;
16
+ @observable product_name;
15
17
  @observable screens;
16
18
 
17
19
  constructor() {
@@ -20,4 +20,8 @@ export class BaseExtension {
20
20
  setBootstrapData(data) {
21
21
  this.data = data;
22
22
  }
23
+
24
+ @computed get domain() {
25
+ return window.location.hostname.split('.').slice(-2).join('.');
26
+ }
23
27
  }
@@ -3,24 +3,24 @@ if (!window.Argosity) { window.Argosity = {}; }
3
3
  const DEFAULT_DURATION = 750; // milliseconds
4
4
 
5
5
  const EASE_IN_OUT = function(t) {
6
- if (t < .5) { return 4 * t * t * t; } else { return ((t - 1) * ((2 * t) - 2) * ((2 * t) - 2)) + 1; }
6
+ if (0.5 >= t) {
7
+ return 4 * t * t * t;
8
+ }
9
+ return ((t - 1) * ((2 * t) - 2) * ((2 * t) - 2)) + 1;
7
10
  };
8
11
 
9
12
  const POSITION = function(start, end, elapsed, duration) {
10
13
  if (elapsed > duration) {
11
14
  return end;
12
- } else {
13
- return start + ((end - start) * EASE_IN_OUT(elapsed / duration));
14
15
  }
16
+ return start + ((end - start) * EASE_IN_OUT(elapsed / duration));
15
17
  };
16
18
 
17
-
18
19
  export default class SmoothScroll {
19
20
 
20
- constructor(link, destination, options) {
21
+ constructor(link, destination, options = {}) {
21
22
  this.link = link;
22
23
  this.destination = destination;
23
- if (options == null) { options = {}; }
24
24
  this.options = options;
25
25
  if (!(this.destination instanceof Element)) {
26
26
  this.destination = document.querySelector(this.destination);
@@ -31,7 +31,7 @@ export default class SmoothScroll {
31
31
  if (this.link && this.destination) {
32
32
  this.link.addEventListener('click', () => this.scrollToElement());
33
33
  } else {
34
- console.warn("failed to setup link", this.link, this.destination);
34
+ console.warn("failed to setup link", this.link, this.destination); // eslint-disable-line
35
35
  }
36
36
  }
37
37
 
@@ -39,14 +39,13 @@ export default class SmoothScroll {
39
39
  return this.constructor.scroll(this.destination, this.options.duration || DEFAULT_DURATION);
40
40
  }
41
41
 
42
- static scroll(destination, duration) {
43
- if (duration == null) { duration = DEFAULT_DURATION; }
42
+ static scroll(destination, duration = DEFAULT_DURATION) {
44
43
  if (!(destination instanceof Element)) {
45
- destination = document.querySelector(destination);
44
+ destination = document.querySelector(destination); // eslint-disable-line
46
45
  }
47
46
 
48
47
  if (!destination) {
49
- console.warn("failed to scroll to", destination);
48
+ console.warn("failed to scroll to", destination); // eslint-disable-line
50
49
  return false;
51
50
  }
52
51
 
@@ -58,11 +57,13 @@ export default class SmoothScroll {
58
57
 
59
58
  const startTime = Date.now();
60
59
 
61
- var step = function() {
60
+ function step() {
62
61
  const elapsed = Date.now() - startTime;
63
- window.scroll(0, POSITION(startPos, endPos, elapsed, duration) );
64
- if (elapsed < duration) { return window.requestAnimationFrame(step); }
65
- };
62
+ window.scroll(0, POSITION(startPos, endPos, elapsed, duration));
63
+ if (elapsed < duration) {
64
+ window.requestAnimationFrame(step);
65
+ }
66
+ }
66
67
  return step();
67
68
  }
68
- };
69
+ }
@@ -1,5 +1,4 @@
1
- import { includes, get, isEmpty } from 'lodash';
2
- import qs from 'qs';
1
+ import { includes, get } from 'lodash';
3
2
  import { observe } from 'mobx';
4
3
  import {
5
4
  BaseModel, identifiedBy, field, session, identifier, computed,
@@ -31,7 +30,6 @@ export default class Asset extends BaseModel {
31
30
 
32
31
  constructor(props) {
33
32
  super(props);
34
-
35
33
  observe(this, 'owner', ({ newValue: owner }) => {
36
34
  if (this.ownerSaveDisposer) { this.ownerSaveDisposer(); }
37
35
  if (!owner || !owner.isModel) { return; }
@@ -55,6 +53,10 @@ export default class Asset extends BaseModel {
55
53
  return !!this.file;
56
54
  }
57
55
 
56
+ @computed get exists() {
57
+ return !!(this.file || this.id);
58
+ }
59
+
58
60
  @computed get isImage() {
59
61
  return includes(IMAGES, this.mimeType);
60
62
  }
@@ -85,15 +87,11 @@ export default class Asset extends BaseModel {
85
87
  form.append('owner_id', this.owner.identifierFieldValue);
86
88
  form.append('owner_association', this.owner_association_name);
87
89
 
88
- let url = `${Config.api_path}${Config.assets_path_prefix}`;
89
- const query = {};
90
+ const options = { method: 'POST', body: form, headers: {} };
90
91
  if (Config.access_token) {
91
- query.jwt = Config.access_token;
92
- }
93
- if (!isEmpty(query)) {
94
- url += `?${qs.stringify(query, { arrayFormat: 'brackets' })}`;
92
+ options.headers.Authorization = Config.access_token;
95
93
  }
96
- return fetch(url, { method: 'POST', body: form })
94
+ return fetch(`${Config.api_path}${Config.assets_path_prefix}`, options)
97
95
  .then(resp => resp.json())
98
96
  .then((json) => {
99
97
  this.file = undefined;
@@ -1,6 +1,6 @@
1
1
  import { observable } from 'mobx';
2
2
  import invariant from 'invariant';
3
- import { extend, each, isArray, isObject, isEmpty } from 'lodash';
3
+ import { extend, isArray, isObject } from 'lodash';
4
4
  import { createCollection } from 'mobx-decorated-models';
5
5
 
6
6
  import Sync from './sync';
@@ -50,9 +50,6 @@ export default class ModelCollection {
50
50
  return this;
51
51
  }
52
52
  create(models = [], options = {}) {
53
- if (isObject(models) && isEmpty(options)) {
54
- return extendAry(this.$model, [], models, this);
55
- }
56
53
  return extendAry(this.$model, models, options, this);
57
54
  }
58
55
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
- isEmpty, isNil, extend, map, bindAll, omit, range, has,
2
+ isEmpty, isNil, extend, map, bindAll, omit, inRange, find, range,
3
3
  } from 'lodash';
4
- import { autorun, reaction, observe } from 'mobx';
4
+ import { reaction, observe } from 'mobx';
5
5
 
6
6
  import Sync from '../sync';
7
7
  import Result from './result';
@@ -15,10 +15,10 @@ export default class ArrayResult extends Result {
15
15
  @belongsTo query;
16
16
  @observable totalCount = 0;
17
17
  @observable rows;
18
- @observable isLoading;
19
18
  @observable rowUpdateCount = 0;
20
19
  @observable sortAscending;
21
20
  @observable sortField;
21
+ @observable.shallow loadingRows = [];
22
22
 
23
23
  constructor(attrs) {
24
24
  super(attrs);
@@ -139,14 +139,12 @@ export default class ArrayResult extends Result {
139
139
  }
140
140
 
141
141
  isRowLoading(index) {
142
- return !!(this.rows.length > index && this.rows[index].isLoading);
142
+ return !!find(this.loadingRows, ([start, end]) => inRange(index, start, end));
143
143
  }
144
144
 
145
145
  fetch({ start = this.rows.length, limit = this.query.pageSize } = {}) {
146
- if (start + limit >= this.rows.length) {
147
- range(this.rows.length, start + limit).forEach(() => this.rows.push([]));
148
- }
149
- range(start, start + limit).forEach(i => (this.rows[i].isLoading = true));
146
+ const inProgress = [start, start + limit];
147
+ this.loadingRows.push(inProgress);
150
148
 
151
149
  const query = {};
152
150
  this.query.clauses.forEach((clause) => {
@@ -155,7 +153,7 @@ export default class ArrayResult extends Result {
155
153
  }
156
154
  });
157
155
 
158
- const options = extend({}, this.query.syncOptions, {
156
+ const options = extend({}, omit(this.query.syncOptions, 'include'), {
159
157
  start,
160
158
  limit,
161
159
  total_count: 't',
@@ -179,8 +177,12 @@ export default class ArrayResult extends Result {
179
177
 
180
178
  return Sync.perform(this.query.info.syncUrl, options).then((resp) => {
181
179
  const rows = resp.data || [];
180
+ if (start > this.rows.length) {
181
+ range(this.rows.length, start).forEach(() => this.rows.push([]));
182
+ }
182
183
  this.rows.splice(start, Math.max(limit, rows.length), ...rows);
183
184
  this.totalCount = resp.total;
185
+ this.loadingRows.remove(inProgress);
184
186
  delete this.syncInProgress;
185
187
  return this;
186
188
  });
@@ -64,15 +64,15 @@ function perform(urlPrefix, defaultOptions = {}) {
64
64
  });
65
65
 
66
66
  let url = `${Config.api_host}${urlPrefix}.json`;
67
- if (Config.access_token) {
68
- query.jwt = Config.access_token;
69
- }
70
67
  if (!isEmpty(query)) {
71
68
  url += `?${qs.stringify(query, { arrayFormat: 'brackets' })}`;
72
69
  }
73
70
  if (!options.headers) { options.headers = {}; }
74
71
 
75
72
  options.headers['Content-Type'] = 'application/json';
73
+ if (Config.access_token) {
74
+ options.headers.Authorization = Config.access_token;
75
+ }
76
76
  return fetch(url, options)
77
77
  .then(resp => resp.json())
78
78
  .then((json) => {
@@ -0,0 +1,29 @@
1
+ import { observable } from 'mobx';
2
+ import {
3
+ BaseModel, identifiedBy, field, identifier, computed,
4
+ } from './base';
5
+ import Config from '../config';
6
+
7
+ const CACHE = observable({
8
+ Tenant: undefined,
9
+ });
10
+
11
+ @identifiedBy('hippo/tenant')
12
+ export default class Tenant extends BaseModel {
13
+
14
+ @computed static get current() {
15
+ if (!CACHE.Tenant) {
16
+ CACHE.Tenant = new Tenant();
17
+ CACHE.Tenant.fetch({ query: 'current' });
18
+ }
19
+ return CACHE.Tenant;
20
+ }
21
+
22
+ @identifier id;
23
+ @field slug = Tenant.slug;
24
+ @field name;
25
+
26
+ @computed get domain() {
27
+ return `${this.slug}.${Config.website_domain}`;
28
+ }
29
+ }
@@ -16,7 +16,7 @@ import {Row, Col} from 'react-flexbox-grid';
16
16
  import ScreenInstance from 'hippo/screens/instance';
17
17
  import Extensions from '../extensions';
18
18
  import MailerConfig from './system-settings/mailer-config';
19
-
19
+ import TenantSettings from './system-settings/tenant';
20
20
  import './system-settings/system-settings.scss';
21
21
 
22
22
  @observer
@@ -26,7 +26,7 @@ export default class SystemSettings extends React.PureComponent {
26
26
  extensionPanelRefs = new Map();
27
27
 
28
28
  static propTypes = {
29
- screen: PropTypes.instanceOf(ScreenInstance).isRequired,
29
+ screen: PropTypes.instanceOf(ScreenInstance).isRequired,
30
30
  }
31
31
 
32
32
  renderExtPanel(ext) {
@@ -56,6 +56,7 @@ export default class SystemSettings extends React.PureComponent {
56
56
  @action.bound
57
57
  onSave() {
58
58
  this.extensionPanelRefs.forEach(panel => invoke(panel, 'onSave'));
59
+ this.tenantSettings.onSave()
59
60
  this.settings.save();
60
61
  }
61
62
 
@@ -71,8 +72,7 @@ export default class SystemSettings extends React.PureComponent {
71
72
  />
72
73
  </Header>
73
74
  <Heading>{this.props.screen.definition.title}</Heading>
74
- {this.extensionPanels}
75
-
75
+ <TenantSettings ref={ts => this.tenantSettings = ts} />
76
76
  <Heading tag="h3">Images</Heading>
77
77
  <Row>
78
78
  <Col sm={4} xs={12}>
@@ -86,6 +86,7 @@ export default class SystemSettings extends React.PureComponent {
86
86
  settings={this.settings.settings}
87
87
  registerForSave={panel => this.extensionPanelRefs.set('mail', panel)}
88
88
  />
89
+ {this.extensionPanels}
89
90
  </Screen>
90
91
  );
91
92
  }
@@ -4,32 +4,26 @@ import { observer } from 'mobx-react';
4
4
  import { Row } from 'react-flexbox-grid';
5
5
  import Heading from 'grommet/components/Heading';
6
6
 
7
- import { Form, Field, FieldDefinitions, nonBlank, validEmail } from 'hippo/components/form';
7
+ import { Form, Field, FormState, nonBlank, validEmail } from 'hippo/components/form';
8
8
 
9
9
  @observer
10
10
  export default class MailerConfig extends React.PureComponent {
11
11
 
12
- fields = new FieldDefinitions({
13
- user_name: nonBlank,
14
- password: nonBlank,
15
- address: nonBlank,
16
- from_email: validEmail,
17
- from_name: nonBlank,
18
- })
12
+ formState = new FormState()
19
13
 
20
14
  @action.bound
21
15
  onSave() {
22
16
  if (!this.props.settings.smtp) { this.props.settings.smtp = {}; }
23
- this.fields.persistTo(this.props.settings.smtp);
17
+ this.formState.persistTo(this.props.settings.smtp);
24
18
  }
25
19
 
26
20
  componentWillMount() {
27
21
  this.props.registerForSave(this);
28
- this.fields.set(this.props.settings.smtp || {});
22
+ this.formState.set(this.props.settings.smtp || {});
29
23
  }
30
24
 
31
25
  componentWillReceiveProps(nextProps) {
32
- this.fields.set(nextProps.settings.smtp || {});
26
+ this.formState.set(nextProps.settings.smtp || {});
33
27
  }
34
28
 
35
29
 
@@ -37,13 +31,13 @@ export default class MailerConfig extends React.PureComponent {
37
31
  return (
38
32
  <div className="section">
39
33
  <Heading tag="h3">Email settings</Heading>
40
- <Form fields={this.fields}>
34
+ <Form state={this.formState}>
41
35
  <Row className="section">
42
- <Field md={4} xs={6} name="user_name" />
43
- <Field md={4} xs={6} name="password" type="password" />
44
- <Field md={4} xs={6} name="address" label="Server Address" />
45
- <Field md={4} xs={6} name="from_email" label="From Email" />
46
- <Field md={4} xs={6} name="from_name" label="From Name" />
36
+ <Field md={4} xs={6} name="user_name" validate={nonBlank} />
37
+ <Field md={4} xs={6} name="password" type="password" validate={nonBlank} />
38
+ <Field md={4} xs={6} name="address" label="Server Address" validate={nonBlank} />
39
+ <Field md={4} xs={6} name="from_email" label="From Email" validate={validEmail} />
40
+ <Field md={4} xs={6} name="from_name" label="From Name" validate={nonBlank} />
47
41
  </Row>
48
42
  </Form>
49
43
  </div>