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.
- checksums.yaml +4 -4
- data/client/hippo/components/asset.jsx +22 -2
- data/client/hippo/components/asset.scss +1 -1
- data/client/hippo/components/data-list.jsx +38 -27
- data/client/hippo/components/data-list/data-list.scss +2 -0
- data/client/hippo/components/data-table.jsx +4 -2
- data/client/hippo/components/data-table/formatters.js +7 -0
- data/client/hippo/components/data-table/table-styles.scss +10 -0
- data/client/hippo/components/date-time.jsx +93 -133
- data/client/hippo/components/date-time.scss +1 -36
- data/client/hippo/components/form/api.js +4 -3
- data/client/hippo/components/form/fields.jsx +7 -1
- data/client/hippo/components/form/fields/date-wrapper.jsx +4 -4
- data/client/hippo/components/icon.jsx +10 -1
- data/client/hippo/components/query-builder.jsx +6 -1
- data/client/hippo/components/record-finder/query-layer.jsx +1 -1
- data/client/hippo/components/save-button.jsx +55 -0
- data/client/hippo/components/text-editor.jsx +10 -9
- data/client/hippo/components/text-editor/text-editor.scss +0 -1
- data/client/hippo/components/time-zone-select.jsx +60 -0
- data/client/hippo/models/asset.js +10 -5
- data/client/hippo/models/base.js +1 -1
- data/client/hippo/models/pub_sub.js +22 -67
- data/client/hippo/models/pub_sub/channel.js +28 -8
- data/client/hippo/models/pub_sub/map.js +57 -0
- data/client/hippo/models/query/array-result.js +5 -4
- data/client/hippo/models/query/field.js +19 -3
- data/client/hippo/models/system-setting.js +16 -1
- data/client/hippo/models/tenant.js +8 -7
- data/client/hippo/screens/system-settings.jsx +10 -12
- data/client/hippo/screens/user-management/edit-form.jsx +10 -11
- data/client/hippo/workspace/index.jsx +6 -3
- data/client/hippo/workspace/menu-option.jsx +2 -5
- data/client/hippo/workspace/menu.jsx +13 -1
- data/client/hippo/workspace/styles.scss +11 -26
- data/command-reference-files/initial/Gemfile +1 -1
- data/command-reference-files/model/client/appy-app/models/test_test.js +1 -1
- data/command-reference-files/model/spec/server/models/test_test_spec.rb +10 -0
- data/lib/hippo/api/cable.rb +0 -2
- data/lib/hippo/command/generate_model.rb +2 -3
- data/lib/hippo/spec_helper.rb +4 -2
- data/lib/hippo/tenant.rb +7 -1
- data/lib/hippo/version.rb +1 -1
- data/package-lock.json +60 -46
- data/package.json +5 -2
- data/spec/client/components/__snapshots__/record-finder.spec.jsx.snap +1 -0
- data/spec/client/components/__snapshots__/time-zone-select.spec.jsx.snap +48 -0
- data/spec/client/components/form.spec.jsx +7 -0
- data/spec/client/components/time-zone-select.spec.jsx +11 -0
- data/spec/client/models/pub_sub.spec.js +1 -3
- data/spec/client/models/pub_sub/channel.spec.js +45 -0
- data/spec/client/models/system-setting.spec.js +14 -0
- data/spec/client/workspace/__snapshots__/menu.spec.jsx.snap +9 -9
- data/spec/server/api/tenant_change_spec.rb +1 -1
- data/templates/client/models/model.js +1 -1
- data/templates/spec/factories/model.rb +6 -0
- data/templates/spec/server/model_spec.rb +4 -4
- data/views/hippo_root_view.erb +4 -0
- metadata +11 -10
- data/client/hippo/components/date-time/calendar.jsx +0 -113
- data/client/hippo/components/date-time/date-time-drop.jsx +0 -75
- data/client/hippo/components/date-time/date-time.scss +0 -157
- data/client/hippo/components/date-time/time.jsx +0 -119
- data/client/hippo/workspace/foo.js +0 -0
- data/command-reference-files/model/spec/fixtures/appy-app/test_test.yml +0 -11
- data/command-reference-files/model/spec/server/test_test_spec.rb +0 -10
- 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
|
-
|
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
|
-
|
34
|
-
|
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
|
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
|
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
|
-
|
8
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
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
|
16
|
-
import
|
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
|
-
<
|
72
|
-
|
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 '
|
10
|
+
import Warning from '../../components/warning-notification';
|
11
|
+
import SaveButton from '../../components/save-button';
|
12
|
+
import Query from '../../models/query';
|
11
13
|
|
12
|
-
import
|
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.
|
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
|
-
<
|
76
|
-
label="Save"
|
75
|
+
<SaveButton
|
77
76
|
onClick={this.formState.isValid ? this.onSave : null}
|
78
|
-
|
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
|
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()}
|