@eeacms/volto-eea-website-policy 1.2.1 → 1.3.0

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [1.3.0](https://github.com/eea/volto-eea-website-policy/compare/1.2.2...1.3.0) - 7 February 2023
8
+
9
+ #### :nail_care: Enhancements
10
+
11
+ - refactor(Toolbar): Upgrade Toolbar.jsx shadow customization to Volto 16.10.0 [Alin Voinea - [`d759206`](https://github.com/eea/volto-eea-website-policy/commit/d759206b48f31e1835da1b5cc99f4b640812b6a0)]
12
+ - refactor(Controlpanel): Cleanup obsolete shadow ContentTypeLayout [Alin Voinea - [`7469ea9`](https://github.com/eea/volto-eea-website-policy/commit/7469ea9670d1dcf9dc98d6e3f4537244069222fa)]
13
+ - refactor(config): Move some common settings to volto-eea-website-theme - refs #160747 [Alin Voinea - [`e7b8508`](https://github.com/eea/volto-eea-website-policy/commit/e7b85082449406962d2e90e0362ac596f127a0e5)]
14
+
15
+ #### :hammer_and_wrench: Others
16
+
17
+ - Release 1.3.0 [Alin Voinea - [`8a8e906`](https://github.com/eea/volto-eea-website-policy/commit/8a8e9061aedbbec89d7ab7e7493cfcabefdd3bc5)]
18
+ ### [1.2.2](https://github.com/eea/volto-eea-website-policy/compare/1.2.1...1.2.2) - 25 January 2023
19
+
20
+ #### :hammer_and_wrench: Others
21
+
22
+ - Safely set extractScripts.errorPages [Alin Voinea - [`439fb46`](https://github.com/eea/volto-eea-website-policy/commit/439fb46167aa24fe1ae87c1721894aceb89a1a55)]
7
23
  ### [1.2.1](https://github.com/eea/volto-eea-website-policy/compare/1.2.0...1.2.1) - 29 December 2022
8
24
 
9
25
  ### [1.2.0](https://github.com/eea/volto-eea-website-policy/compare/1.1.0...1.2.0) - 28 December 2022
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-eea-website-policy",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "@eeacms/volto-eea-website-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -29,7 +29,12 @@ import {
29
29
  unlockContent,
30
30
  } from '@plone/volto/actions';
31
31
  import { Icon } from '@plone/volto/components';
32
- import { BodyClass, getBaseUrl, getCookieOptions } from '@plone/volto/helpers';
32
+ import {
33
+ BodyClass,
34
+ getBaseUrl,
35
+ getCookieOptions,
36
+ hasApiExpander,
37
+ } from '@plone/volto/helpers';
33
38
  import { Pluggable } from '@plone/volto/components/manage/Pluggable';
34
39
 
35
40
  import penSVG from '@plone/volto/icons/pen.svg';
@@ -104,7 +109,7 @@ const messages = defineMessages({
104
109
  },
105
110
  });
106
111
 
107
- const toolbarComponents = {
112
+ let toolbarComponents = {
108
113
  personalTools: { component: PersonalTools, wrapper: null },
109
114
  more: { component: More, wrapper: null },
110
115
  types: { component: Types, wrapper: null, contentAsProps: true },
@@ -197,8 +202,20 @@ class Toolbar extends Component {
197
202
  * @returns {undefined}
198
203
  */
199
204
  componentDidMount() {
200
- this.props.listActions(getBaseUrl(this.props.pathname));
201
- this.props.getTypes(getBaseUrl(this.props.pathname));
205
+ // Do not trigger the actions action if the expander is present
206
+ if (!hasApiExpander('actions', getBaseUrl(this.props.pathname))) {
207
+ this.props.listActions(getBaseUrl(this.props.pathname));
208
+ }
209
+ // Do not trigger the types action if the expander is present
210
+ if (!hasApiExpander('types', getBaseUrl(this.props.pathname))) {
211
+ this.props.getTypes(getBaseUrl(this.props.pathname));
212
+ }
213
+ toolbarComponents = {
214
+ ...(config.settings
215
+ ? config.settings.additionalToolbarComponents || {}
216
+ : {}),
217
+ ...toolbarComponents,
218
+ };
202
219
  this.props.setExpandedToolbar(this.state.expanded);
203
220
  document.addEventListener('mousedown', this.handleClickOutside, false);
204
221
  }
@@ -211,8 +228,14 @@ class Toolbar extends Component {
211
228
  */
212
229
  UNSAFE_componentWillReceiveProps(nextProps) {
213
230
  if (nextProps.pathname !== this.props.pathname) {
214
- this.props.listActions(getBaseUrl(nextProps.pathname));
215
- this.props.getTypes(getBaseUrl(nextProps.pathname));
231
+ // Do not trigger the actions action if the expander is present
232
+ if (!hasApiExpander('actions', getBaseUrl(nextProps.pathname))) {
233
+ this.props.listActions(getBaseUrl(nextProps.pathname));
234
+ }
235
+ // Do not trigger the types action if the expander is present
236
+ if (!hasApiExpander('types', getBaseUrl(nextProps.pathname))) {
237
+ this.props.getTypes(getBaseUrl(nextProps.pathname));
238
+ }
216
239
  }
217
240
 
218
241
  // Unlock
@@ -534,9 +557,13 @@ class Toolbar extends Component {
534
557
  </button>
535
558
  </>
536
559
  )}
560
+ <Pluggable name="main.toolbar.top" />
537
561
  </div>
538
562
  <div className="toolbar-bottom">
539
- <Pluggable name="main.toolbar.bottom" />
563
+ <Pluggable
564
+ name="main.toolbar.bottom"
565
+ params={{ onClickHandler: this.toggleMenu }}
566
+ />
540
567
  {!this.props.hideDefaultViewButtons && (
541
568
  <button
542
569
  className="user"
package/src/index.js CHANGED
@@ -1,37 +1,9 @@
1
1
  import { runtimeConfig } from '@plone/volto/runtime_config';
2
- import installCallout from '@plone/volto-slate/editor/plugins/Callout';
3
2
  import installContextNavigationBlock from '@eeacms/volto-eea-website-policy/components/Blocks/ContextNavigation';
4
3
  import installLayoutSettingsBlock from '@eeacms/volto-eea-website-policy/components/Blocks/LayoutSettings';
5
4
  import { addStylingFieldsetSchemaEnhancer } from '@eeacms/volto-eea-website-policy/components/Blocks/schema';
6
5
 
7
6
  const applyConfig = (config) => {
8
- // if (process.env.NODE_ENV === 'production') {
9
- // // Restrict block-style to Layout only
10
- // config.settings.layoutOnlyBlockStyles = true;
11
- // // Restrict slate metadata mentions to Layout only
12
- // config.settings.layoutOnlySlateMetadataMentions = true;
13
- // }
14
- // Callout slate button
15
- config = installCallout(config);
16
-
17
- // Remove blockquote slate button
18
- config.settings.slate.toolbarButtons = config.settings.slate.toolbarButtons.filter(
19
- (item) => item !== 'blockquote',
20
- );
21
-
22
- // Disable tags on View
23
- config.settings.showTags = false;
24
-
25
- // Enable Title block
26
- config.blocks.blocksConfig.title.restricted = false;
27
-
28
- // Enable description block (also for cypress)
29
- config.blocks.blocksConfig.description.restricted = false;
30
- config.blocks.requiredBlocks = [];
31
-
32
- // Date format for EU
33
- config.settings.dateLocale = 'en-gb';
34
-
35
7
  // #137187 Keycloak integration
36
8
  if (runtimeConfig['RAZZLE_KEYCLOAK'] === 'Yes') {
37
9
  config.settings.externalRoutes = [
@@ -131,6 +103,14 @@ const applyConfig = (config) => {
131
103
  config.blocks.blocksConfig.imagecards.restricted = true;
132
104
  }
133
105
 
106
+ config.settings.apiExpanders = [
107
+ ...config.settings.apiExpanders,
108
+ {
109
+ match: '',
110
+ GET_CONTENT: ['subsite'],
111
+ },
112
+ ];
113
+
134
114
  // Done
135
115
  return config;
136
116
  };
@@ -1,520 +0,0 @@
1
- /**
2
- * Content Type component.
3
- * @module components/manage/Controlpanels/ContentTypeLayout
4
- */
5
-
6
- import React, { Component } from 'react';
7
- import PropTypes from 'prop-types';
8
- import { connect } from 'react-redux';
9
- import { compose } from 'redux';
10
- import { Link } from 'react-router-dom';
11
- import {
12
- getParentUrl,
13
- hasBlocksData,
14
- getBlocksFieldname,
15
- getBlocksLayoutFieldname,
16
- } from '@plone/volto/helpers';
17
- import { Portal } from 'react-portal';
18
- import { Button, Segment } from 'semantic-ui-react';
19
- import { toast } from 'react-toastify';
20
- import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
21
- import { nth, join } from 'lodash';
22
- import {
23
- Error,
24
- Form,
25
- Icon,
26
- Toolbar,
27
- Sidebar,
28
- Toast,
29
- } from '@plone/volto/components';
30
- import {
31
- getSchema,
32
- updateSchema,
33
- getControlpanel,
34
- updateControlpanel,
35
- } from '@plone/volto/actions';
36
-
37
- import saveSVG from '@plone/volto/icons/save.svg';
38
- import clearSVG from '@plone/volto/icons/clear.svg';
39
- import backSVG from '@plone/volto/icons/back.svg';
40
-
41
- const messages = defineMessages({
42
- changesSaved: {
43
- id: 'Changes saved.',
44
- defaultMessage: 'Changes saved.',
45
- },
46
- back: {
47
- id: 'Back',
48
- defaultMessage: 'Back',
49
- },
50
- save: {
51
- id: 'Save',
52
- defaultMessage: 'Save',
53
- },
54
- cancel: {
55
- id: 'Cancel',
56
- defaultMessage: 'Cancel',
57
- },
58
- info: {
59
- id: 'Info',
60
- defaultMessage: 'Info',
61
- },
62
- enable: {
63
- id: 'Enable editable Blocks',
64
- defaultMessage: 'Enable editable Blocks',
65
- },
66
- });
67
-
68
- /**
69
- * ContentTypeLayout class.
70
- * @class ContentTypeLayout
71
- * @extends Component
72
- */
73
- class ContentTypeLayout extends Component {
74
- /**
75
- * Property types.
76
- * @property {Object} propTypes Property types.
77
- * @static
78
- */
79
- static propTypes = {
80
- updateControlpanel: PropTypes.func.isRequired,
81
- getControlpanel: PropTypes.func.isRequired,
82
- getSchema: PropTypes.func.isRequired,
83
- updateSchema: PropTypes.func.isRequired,
84
- id: PropTypes.string.isRequired,
85
- parent: PropTypes.string.isRequired,
86
- pathname: PropTypes.string.isRequired,
87
- schemaRequest: PropTypes.objectOf(PropTypes.any).isRequired,
88
- cpanelRequest: PropTypes.objectOf(PropTypes.any).isRequired,
89
- schema: PropTypes.objectOf(PropTypes.any),
90
- controlpanel: PropTypes.shape({
91
- '@id': PropTypes.string,
92
- data: PropTypes.object,
93
- schema: PropTypes.object,
94
- title: PropTypes.string,
95
- }),
96
- };
97
-
98
- /**
99
- * Default properties.
100
- * @property {Object} defaultProps Default properties.
101
- * @static
102
- */
103
- static defaultProps = {
104
- schema: {},
105
- controlpanel: null,
106
- };
107
-
108
- /**
109
- * Constructor
110
- * @method constructor
111
- * @param {Object} props Component properties
112
- * @constructs ContentTypeLayout
113
- */
114
- constructor(props) {
115
- super(props);
116
-
117
- this.state = {
118
- visual: false,
119
- content: null,
120
- readOnlyBehavior: null,
121
- error: null,
122
- isClient: false,
123
- };
124
-
125
- this.onCancel = this.onCancel.bind(this);
126
- this.onSubmit = this.onSubmit.bind(this);
127
- this.onEnableBlocks = this.onEnableBlocks.bind(this);
128
- this.onEnableBlocksBehavior = this.onEnableBlocksBehavior.bind(this);
129
- this.onDisableBlocksBehavior = this.onDisableBlocksBehavior.bind(this);
130
- this.form = React.createRef();
131
- }
132
-
133
- /**
134
- * Component did mount
135
- * @method componentDidMount
136
- * @returns {undefined}
137
- */
138
- componentDidMount() {
139
- this.props.getControlpanel(join([this.props.parent, this.props.id], '/'));
140
- this.props.getSchema(this.props.id);
141
- this.setState({ isClient: true });
142
- }
143
-
144
- /**
145
- * Component will receive props
146
- * @method componentWillReceiveProps
147
- * @param {Object} nextProps Next properties
148
- * @returns {undefined}
149
- */
150
- UNSAFE_componentWillReceiveProps(nextProps) {
151
- // Control Panel GET
152
- if (
153
- this.props.cpanelRequest.get.loading &&
154
- nextProps.cpanelRequest.get.error
155
- ) {
156
- this.setState({
157
- error: nextProps.cpanelRequest.get.error,
158
- });
159
- }
160
-
161
- // Schema GET
162
- if (this.props.schemaRequest.loading && nextProps.schemaRequest.loaded) {
163
- const properties = nextProps.schema?.properties || {};
164
- const content = {};
165
- for (const key in properties) {
166
- const value = properties[key].default;
167
- if (value) {
168
- content[key] = value;
169
- }
170
- }
171
-
172
- if (hasBlocksData(properties)) {
173
- this.setState({
174
- visual: true,
175
- });
176
-
177
- const blocksFieldName = getBlocksFieldname(properties);
178
- const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
179
- content[blocksFieldName] = properties[blocksFieldName]?.default || {};
180
- content[blocksLayoutFieldname] = properties[blocksLayoutFieldname]
181
- ?.default || { items: [] };
182
-
183
- const blocksBehavior = properties[blocksFieldName]?.behavior || '';
184
- this.setState({
185
- readOnlyBehavior: !blocksBehavior.includes('generated')
186
- ? blocksBehavior
187
- : '',
188
- });
189
- } else {
190
- this.setState({
191
- visual: false,
192
- readOnlyBehavior: '',
193
- });
194
- }
195
-
196
- this.setState({
197
- content: content,
198
- });
199
- }
200
-
201
- // Schema updated
202
- if (
203
- this.props.schemaRequest.update.loading &&
204
- nextProps.schemaRequest.update.loaded
205
- ) {
206
- this.props.getSchema(this.props.id);
207
- toast.info(
208
- <Toast
209
- info
210
- title={this.props.intl.formatMessage(messages.info)}
211
- content={this.props.intl.formatMessage(messages.changesSaved)}
212
- />,
213
- );
214
- }
215
-
216
- // Blocks behavior disabled
217
- if (
218
- this.props.cpanelRequest.update.loading &&
219
- nextProps.cpanelRequest.update.loaded
220
- ) {
221
- this.onEnableBlocks();
222
- }
223
- }
224
-
225
- /**
226
- * Submit handler
227
- * @method onSubmit
228
- * @param {object} data Form data.
229
- * @returns {undefined}
230
- */
231
- onSubmit(data) {
232
- const schema = { properties: {} };
233
- Object.keys(data)
234
- .filter((k) => data[k])
235
- .forEach((k) => (schema.properties[k] = { default: data[k] }));
236
- this.props.updateSchema(this.props.id, schema);
237
- }
238
-
239
- /**
240
- * Cancel handler
241
- * @method onCancel
242
- * @returns {undefined}
243
- */
244
- onCancel() {
245
- const url = getParentUrl(this.props.pathname);
246
- this.props.history.push(getParentUrl(url));
247
- }
248
-
249
- /**
250
- * Enable blocks handler
251
- * @method onEnableBlocks
252
- * @returns {undefined}
253
- */
254
- onEnableBlocks() {
255
- const { properties = {} } = this.props.schema;
256
- const blocksFieldName = getBlocksFieldname(properties);
257
- const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
258
- const schema = {
259
- fieldsets: [
260
- {
261
- id: 'layout',
262
- title: 'Layout',
263
- fields: ['blocks', 'blocks_layout'],
264
- },
265
- ],
266
- properties: {
267
- blocks: {
268
- title: 'Blocks',
269
- type: 'dict',
270
- widget: 'json',
271
- factory: 'JSONField',
272
- default: properties[blocksFieldName]?.default || {},
273
- },
274
- blocks_layout: {
275
- title: 'Blocks Layout',
276
- type: 'dict',
277
- widget: 'json',
278
- factory: 'JSONField',
279
- default: properties[blocksLayoutFieldname]?.default || { items: [] },
280
- },
281
- },
282
- };
283
- this.props.updateSchema(this.props.id, schema);
284
- }
285
-
286
- /**
287
- * Disable Blocks behavior handler
288
- * @method onDisableBlocksBehavior
289
- * @returns {undefined}
290
- */
291
- onDisableBlocksBehavior() {
292
- this.props.updateControlpanel(this.props.controlpanel['@id'], {
293
- [this.state.readOnlyBehavior]: false,
294
- 'volto.blocks.editable.layout': true,
295
- });
296
- }
297
-
298
- /**
299
- * Enable Blocks behavior handler
300
- * @method onEnableBlocksBehavior
301
- * @returns {undefined}
302
- */
303
- onEnableBlocksBehavior() {
304
- this.props.updateControlpanel(this.props.controlpanel['@id'], {
305
- 'volto.blocks.editable.layout': true,
306
- });
307
- }
308
-
309
- /**
310
- * Render method.
311
- * @method render
312
- * @returns {string} Markup for the component.
313
- */
314
- render() {
315
- // Error
316
- if (this.state.error) {
317
- return <Error error={this.state.error} />;
318
- }
319
-
320
- if (!this.state.visual) {
321
- // Still loading
322
- if (!this.state.content) {
323
- return <div />;
324
- }
325
-
326
- // Blocks are not enabled
327
- return (
328
- <>
329
- <Segment
330
- placeholder
331
- id="page-controlpanel-layout"
332
- className="ui container center aligned"
333
- >
334
- <div>
335
- <FormattedMessage
336
- id="Can not edit Layout for <strong>{type}</strong> content-type as it doesn't have support for <strong>Volto Blocks</strong> enabled"
337
- defaultMessage="Can not edit Layout for <strong>{type}</strong> content-type as it doesn't have support for <strong>Volto Blocks</strong> enabled"
338
- values={{
339
- strong: (...chunks) => <strong>{chunks}</strong>,
340
- type: this.props?.controlpanel?.title || this.props.id,
341
- }}
342
- />
343
- </div>
344
- <div className="ui divider"></div>
345
- <Button
346
- primary
347
- onClick={this.onEnableBlocksBehavior}
348
- content={this.props.intl.formatMessage(messages.enable)}
349
- />
350
- </Segment>
351
- <Portal
352
- node={this.state.isClient && document.getElementById('toolbar')}
353
- >
354
- <Toolbar
355
- pathname={this.props.pathname}
356
- hideDefaultViewButtons
357
- inner={
358
- <>
359
- <Link className="item" to="#" onClick={() => this.onCancel()}>
360
- <Icon
361
- name={backSVG}
362
- size="30px"
363
- className="contents circled"
364
- title={this.props.intl.formatMessage(messages.back)}
365
- />
366
- </Link>
367
- </>
368
- }
369
- />
370
- </Portal>
371
- </>
372
- );
373
- }
374
-
375
- if (this.state.readOnlyBehavior) {
376
- return (
377
- <>
378
- <Segment
379
- placeholder
380
- id="page-controlpanel-layout"
381
- className="ui container center aligned"
382
- >
383
- <div>
384
- <FormattedMessage
385
- id="Can not edit Layout for <strong>{type}</strong> content-type as the <strong>Blocks behavior</strong> is enabled and <strong>read-only</strong>"
386
- defaultMessage="Can not edit Layout for <strong>{type}</strong> content-type as the <strong>Blocks behavior</strong> is enabled and <strong>read-only</strong>"
387
- values={{
388
- strong: (...chunks) => <strong>{chunks}</strong>,
389
- type: this.props?.controlpanel?.title || this.props.id,
390
- }}
391
- />
392
- </div>
393
- <div className="ui divider"></div>
394
- <Button
395
- primary
396
- onClick={this.onDisableBlocksBehavior}
397
- content={this.props.intl.formatMessage(messages.enable)}
398
- />
399
- </Segment>
400
- <Portal
401
- node={this.state.isClient && document.getElementById('toolbar')}
402
- >
403
- <Toolbar
404
- pathname={this.props.pathname}
405
- hideDefaultViewButtons
406
- inner={
407
- <>
408
- <Link className="item" to="#" onClick={() => this.onCancel()}>
409
- <Icon
410
- name={backSVG}
411
- size="30px"
412
- className="contents circled"
413
- title={this.props.intl.formatMessage(messages.back)}
414
- />
415
- </Link>
416
- </>
417
- }
418
- />
419
- </Portal>
420
- </>
421
- );
422
- }
423
-
424
- // Render layout editor
425
- const blocksFieldName = getBlocksFieldname(
426
- this.props.schema?.properties || {},
427
- );
428
- const blocksLayoutFieldname = getBlocksLayoutFieldname(
429
- this.props.schema?.properties || {},
430
- );
431
- return (
432
- <div id="page-controlpanel-layout">
433
- <Form
434
- isAdminForm
435
- ref={this.form}
436
- schema={{
437
- fieldsets: [
438
- {
439
- id: 'layout',
440
- title: 'Layout',
441
- fields: [blocksFieldName, blocksLayoutFieldname],
442
- },
443
- ],
444
- properties: {
445
- ...this.props.schema.properties[blocksFieldName],
446
- ...this.props.schema.properties[blocksLayoutFieldname],
447
- },
448
- required: [],
449
- }}
450
- formData={this.state.content}
451
- onSubmit={this.onSubmit}
452
- onCancel={this.onCancel}
453
- pathname={this.props.pathname}
454
- visual={this.state.visual}
455
- hideActions
456
- />
457
- <Portal
458
- node={this.state.isClient && document.getElementById('sidebar')}
459
- >
460
- <Sidebar settingsTab={true} documentTab={false} />
461
- </Portal>
462
- <Portal
463
- node={this.state.isClient && document.getElementById('toolbar')}
464
- >
465
- <Toolbar
466
- pathname={this.props.pathname}
467
- hideDefaultViewButtons
468
- inner={
469
- <>
470
- <Button
471
- id="toolbar-save"
472
- className="save"
473
- aria-label={this.props.intl.formatMessage(messages.save)}
474
- onClick={() => this.form.current.onSubmit()}
475
- disabled={this.props.schemaRequest.update.loading}
476
- loading={this.props.schemaRequest.update.loading}
477
- >
478
- <Icon
479
- name={saveSVG}
480
- className="circled"
481
- size="30px"
482
- title={this.props.intl.formatMessage(messages.save)}
483
- />
484
- </Button>
485
- <Button
486
- className="cancel"
487
- aria-label={this.props.intl.formatMessage(messages.cancel)}
488
- onClick={() => this.onCancel()}
489
- >
490
- <Icon
491
- name={clearSVG}
492
- className="circled"
493
- size="30px"
494
- title={this.props.intl.formatMessage(messages.cancel)}
495
- />
496
- </Button>
497
- </>
498
- }
499
- />
500
- </Portal>
501
- </div>
502
- );
503
- }
504
- }
505
-
506
- export default compose(
507
- injectIntl,
508
- connect(
509
- (state, props) => ({
510
- schema: state.schema.schema,
511
- schemaRequest: state.schema,
512
- cpanelRequest: state.controlpanels,
513
- controlpanel: state.controlpanels.controlpanel,
514
- pathname: props.location.pathname,
515
- id: nth(props.location.pathname.split('/'), -2),
516
- parent: nth(props.location.pathname.split('/'), -3),
517
- }),
518
- { getSchema, updateSchema, getControlpanel, updateControlpanel },
519
- ),
520
- )(ContentTypeLayout);