@vendure/admin-ui 1.4.3 → 1.4.7

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.
Files changed (51) hide show
  1. package/bundles/vendure-admin-ui-catalog.umd.js +68 -28
  2. package/bundles/vendure-admin-ui-catalog.umd.js.map +1 -1
  3. package/bundles/vendure-admin-ui-core.umd.js +101 -40
  4. package/bundles/vendure-admin-ui-core.umd.js.map +1 -1
  5. package/bundles/vendure-admin-ui-dashboard.umd.js.map +1 -1
  6. package/bundles/vendure-admin-ui-settings.umd.js +1 -1
  7. package/bundles/vendure-admin-ui-settings.umd.js.map +1 -1
  8. package/catalog/components/facet-detail/facet-detail.component.d.ts +7 -2
  9. package/catalog/vendure-admin-ui-catalog.metadata.json +1 -1
  10. package/core/common/generated-types.d.ts +7 -7
  11. package/core/common/version.d.ts +1 -1
  12. package/core/data/utils/remove-readonly-custom-fields.d.ts +12 -3
  13. package/core/providers/i18n/i18n.service.d.ts +3 -1
  14. package/core/shared/directives/if-default-channel-active.directive.d.ts +1 -1
  15. package/core/shared/directives/if-multichannel.directive.d.ts +1 -1
  16. package/core/shared/pipes/locale-base.pipe.d.ts +5 -0
  17. package/core/vendure-admin-ui-core.metadata.json +1 -1
  18. package/esm2015/catalog/components/facet-detail/facet-detail.component.js +44 -16
  19. package/esm2015/catalog/components/product-list/product-list.component.js +2 -2
  20. package/esm2015/core/common/base-detail.component.js +4 -4
  21. package/esm2015/core/common/generated-types.js +1 -1
  22. package/esm2015/core/common/introspection-result.js +1 -1
  23. package/esm2015/core/common/utilities/create-updated-translatable.js +1 -1
  24. package/esm2015/core/common/version.js +2 -2
  25. package/esm2015/core/components/main-nav/main-nav.component.js +3 -3
  26. package/esm2015/core/data/utils/remove-readonly-custom-fields.js +15 -3
  27. package/esm2015/core/providers/i18n/i18n.service.js +14 -5
  28. package/esm2015/core/shared/components/rich-text-editor/prosemirror/prosemirror.service.js +2 -2
  29. package/esm2015/core/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.js +3 -2
  30. package/esm2015/core/shared/directives/if-default-channel-active.directive.js +2 -2
  31. package/esm2015/core/shared/directives/if-multichannel.directive.js +2 -2
  32. package/esm2015/core/shared/dynamic-form-inputs/select-form-input/select-form-input.component.js +2 -2
  33. package/esm2015/core/shared/pipes/asset-preview.pipe.js +2 -2
  34. package/esm2015/core/shared/pipes/locale-base.pipe.js +23 -1
  35. package/esm2015/core/shared/pipes/locale-currency-name.pipe.js +4 -4
  36. package/esm2015/core/shared/pipes/locale-currency.pipe.js +2 -2
  37. package/esm2015/core/shared/pipes/locale-date.pipe.js +2 -2
  38. package/esm2015/core/shared/pipes/locale-language-name.pipe.js +2 -3
  39. package/esm2015/core/shared/pipes/locale-region-name.pipe.js +2 -3
  40. package/esm2015/dashboard/widgets/latest-orders-widget/latest-orders-widget.component.js +1 -1
  41. package/esm2015/settings/components/channel-list/channel-list.component.js +2 -2
  42. package/fesm2015/vendure-admin-ui-catalog.js +44 -16
  43. package/fesm2015/vendure-admin-ui-catalog.js.map +1 -1
  44. package/fesm2015/vendure-admin-ui-core.js +66 -26
  45. package/fesm2015/vendure-admin-ui-core.js.map +1 -1
  46. package/fesm2015/vendure-admin-ui-dashboard.js.map +1 -1
  47. package/fesm2015/vendure-admin-ui-settings.js +1 -1
  48. package/fesm2015/vendure-admin-ui-settings.js.map +1 -1
  49. package/package.json +11 -11
  50. package/settings/vendure-admin-ui-settings.metadata.json +1 -1
  51. package/static/i18n-messages/pt_PT.json +22 -22
@@ -1572,8 +1572,20 @@ function extractInputType(type) {
1572
1572
  * To be used before submitting the entity for a create or update request.
1573
1573
  */
1574
1574
  function removeReadonlyCustomFields(variables, customFieldConfig) {
1575
- if (variables.input) {
1576
- removeReadonly(variables.input, customFieldConfig);
1575
+ if (!Array.isArray(variables)) {
1576
+ if (Array.isArray(variables.input)) {
1577
+ for (const input of variables.input) {
1578
+ removeReadonly(input, customFieldConfig);
1579
+ }
1580
+ }
1581
+ else {
1582
+ removeReadonly(variables.input, customFieldConfig);
1583
+ }
1584
+ }
1585
+ else {
1586
+ for (const input of variables) {
1587
+ removeReadonly(input, customFieldConfig);
1588
+ }
1577
1589
  }
1578
1590
  return removeReadonly(variables, customFieldConfig);
1579
1591
  }
@@ -5769,9 +5781,11 @@ AuthService.ctorParameters = () => [
5769
5781
  { type: ServerConfigService }
5770
5782
  ];
5771
5783
 
5784
+ /** @dynamic */
5772
5785
  class I18nService {
5773
- constructor(ngxTranslate) {
5786
+ constructor(ngxTranslate, document) {
5774
5787
  this.ngxTranslate = ngxTranslate;
5788
+ this.document = document;
5775
5789
  this._availableLanguages = [];
5776
5790
  }
5777
5791
  get availableLanguages() {
@@ -5787,7 +5801,11 @@ class I18nService {
5787
5801
  * Set the UI language
5788
5802
  */
5789
5803
  setLanguage(language) {
5804
+ var _a;
5790
5805
  this.ngxTranslate.use(language);
5806
+ if ((_a = this.document) === null || _a === void 0 ? void 0 : _a.documentElement) {
5807
+ this.document.documentElement.lang = language;
5808
+ }
5791
5809
  }
5792
5810
  /**
5793
5811
  * Set the available UI languages
@@ -5802,14 +5820,15 @@ class I18nService {
5802
5820
  return this.ngxTranslate.instant(key, params);
5803
5821
  }
5804
5822
  }
5805
- I18nService.ɵprov = i0.ɵɵdefineInjectable({ factory: function I18nService_Factory() { return new I18nService(i0.ɵɵinject(i1$1.TranslateService)); }, token: I18nService, providedIn: "root" });
5823
+ I18nService.ɵprov = i0.ɵɵdefineInjectable({ factory: function I18nService_Factory() { return new I18nService(i0.ɵɵinject(i1$1.TranslateService), i0.ɵɵinject(i1.DOCUMENT)); }, token: I18nService, providedIn: "root" });
5806
5824
  I18nService.decorators = [
5807
5825
  { type: Injectable, args: [{
5808
5826
  providedIn: 'root',
5809
5827
  },] }
5810
5828
  ];
5811
5829
  I18nService.ctorParameters = () => [
5812
- { type: TranslateService }
5830
+ { type: TranslateService },
5831
+ { type: Document, decorators: [{ type: Inject, args: [DOCUMENT,] }] }
5813
5832
  ];
5814
5833
 
5815
5834
  /**
@@ -7188,8 +7207,8 @@ class MainNavComponent {
7188
7207
  MainNavComponent.decorators = [
7189
7208
  { type: Component, args: [{
7190
7209
  selector: 'vdr-main-nav',
7191
- template: "<nav class=\"sidenav\" [clr-nav-level]=\"2\">\r\n <section class=\"sidenav-content\">\r\n <ng-container *ngFor=\"let section of navBuilderService.navMenuConfig$ | async\">\r\n <section\r\n class=\"nav-group\"\r\n [attr.data-section-id]=\"section.id\"\r\n [class.collapsible]=\"section.collapsible\"\r\n *ngIf=\"shouldDisplayLink(section)\"\r\n >\r\n <vdr-ui-extension-point [locationId]=\"section.id\" api=\"navMenu\" [topPx]=\"-6\" [leftPx]=\"8\">\r\n <ng-container *ngIf=\"navBuilderService.sectionBadges[section.id] | async as sectionBadge\">\r\n <vdr-status-badge\r\n *ngIf=\"sectionBadge !== 'none'\"\r\n [type]=\"sectionBadge\"\r\n ></vdr-status-badge>\r\n </ng-container>\r\n <input [id]=\"section.id\" type=\"checkbox\" [checked]=\"section.collapsedByDefault\" />\r\n <label [for]=\"section.id\">{{ section.label | translate }}</label>\r\n <ul class=\"nav-list\">\r\n <ng-container *ngFor=\"let item of section.items\">\r\n <li *ngIf=\"shouldDisplayLink(item)\">\r\n <a\r\n class=\"nav-link\"\r\n [attr.data-item-id]=\"section.id\"\r\n [routerLink]=\"getRouterLink(item)\"\r\n routerLinkActive=\"active\"\r\n >\r\n <ng-container *ngIf=\"item.statusBadge | async as itemBadge\">\r\n <vdr-status-badge\r\n *ngIf=\"itemBadge.type !== 'none'\"\r\n [type]=\"itemBadge.type\"\r\n ></vdr-status-badge>\r\n </ng-container>\r\n <clr-icon [attr.shape]=\"item.icon || 'block'\" size=\"20\"></clr-icon>\r\n {{ item.label | translate }}\r\n </a>\r\n </li>\r\n </ng-container>\r\n </ul>\r\n </vdr-ui-extension-point>\r\n </section>\r\n </ng-container>\r\n </section>\r\n</nav>\r\n",
7192
- styles: [":host{order:-1;background-color:var(--clr-nav-background-color)}nav.sidenav{height:100%;width:10.8rem;border-right-color:var(--clr-sidenav-border-color)}.nav-list clr-icon{margin-right:12px}.nav-group,.nav-link{position:relative}.nav-group vdr-status-badge{left:10px;top:6px}.nav-link vdr-status-badge{left:25px;top:3px}\n"]
7210
+ template: "<nav class=\"sidenav\" [clr-nav-level]=\"2\">\r\n <section class=\"sidenav-content\">\r\n <ng-container *ngFor=\"let section of navBuilderService.navMenuConfig$ | async\">\r\n <section\r\n class=\"nav-group\"\r\n [attr.data-section-id]=\"section.id\"\r\n [class.collapsible]=\"section.collapsible\"\r\n *ngIf=\"shouldDisplayLink(section)\"\r\n >\r\n <vdr-ui-extension-point [locationId]=\"section.id\" api=\"navMenu\" [topPx]=\"-6\" [leftPx]=\"8\">\r\n <ng-container *ngIf=\"navBuilderService.sectionBadges[section.id] | async as sectionBadge\">\r\n <vdr-status-badge\r\n *ngIf=\"sectionBadge !== 'none'\"\r\n [type]=\"sectionBadge\"\r\n ></vdr-status-badge>\r\n </ng-container>\r\n <input [id]=\"section.id\" type=\"checkbox\" [checked]=\"section.collapsedByDefault\" />\r\n <label class=\"nav-group-header\" [for]=\"section.id\">{{ section.label | translate }}</label>\r\n <ul class=\"nav-list\">\r\n <ng-container *ngFor=\"let item of section.items\">\r\n <li *ngIf=\"shouldDisplayLink(item)\">\r\n <a\r\n class=\"nav-link\"\r\n [attr.data-item-id]=\"section.id\"\r\n [routerLink]=\"getRouterLink(item)\"\r\n routerLinkActive=\"active\"\r\n >\r\n <ng-container *ngIf=\"item.statusBadge | async as itemBadge\">\r\n <vdr-status-badge\r\n *ngIf=\"itemBadge.type !== 'none'\"\r\n [type]=\"itemBadge.type\"\r\n ></vdr-status-badge>\r\n </ng-container>\r\n <clr-icon [attr.shape]=\"item.icon || 'block'\" size=\"20\"></clr-icon>\r\n {{ item.label | translate }}\r\n </a>\r\n </li>\r\n </ng-container>\r\n </ul>\r\n </vdr-ui-extension-point>\r\n </section>\r\n </ng-container>\r\n </section>\r\n</nav>\r\n",
7211
+ styles: [":host{order:-1;background-color:var(--clr-nav-background-color)}nav.sidenav{height:100%;width:10.8rem;border-right-color:var(--clr-sidenav-border-color)}.sidenav .nav-group .nav-list{margin:0}.sidenav .nav-group .nav-group-header{margin:0;line-height:1.2}.sidenav .nav-group .nav-link{display:inline-flex;line-height:1rem;padding-right:.6rem}.nav-list clr-icon{flex-shrink:0;margin-right:12px}.nav-group{-webkit-hyphens:auto;hyphens:auto}.nav-group,.nav-link{position:relative}.nav-group vdr-status-badge{left:10px;top:6px}.nav-link vdr-status-badge{left:25px;top:3px}\n"]
7193
7212
  },] }
7194
7213
  ];
7195
7214
  MainNavComponent.ctorParameters = () => [
@@ -8788,7 +8807,7 @@ SelectFormInputComponent.id = 'select-form-input';
8788
8807
  SelectFormInputComponent.decorators = [
8789
8808
  { type: Component, args: [{
8790
8809
  selector: 'vdr-select-form-input',
8791
- template: "<select clrSelect [formControl]=\"formControl\" [vdrDisabled]=\"readonly\">\r\n <option *ngIf=\"config.nullable\" [ngValue]=\"null\"></option>\r\n <option *ngFor=\"let option of options\" [value]=\"option.value\">\r\n {{ (option | customFieldLabel) || option.label || option.value }}\r\n </option>\r\n</select>\r\n",
8810
+ template: "<select clrSelect [formControl]=\"formControl\" [vdrDisabled]=\"readonly\">\r\n <option *ngIf=\"config.nullable\" [ngValue]=\"null\"></option>\r\n <option *ngFor=\"let option of options\" [ngValue]=\"option.value\">\r\n {{ (option | customFieldLabel) || option.label || option.value }}\r\n </option>\r\n</select>\r\n",
8792
8811
  changeDetection: ChangeDetectionStrategy.OnPush,
8793
8812
  styles: ["select{width:100%}\n"]
8794
8813
  },] }
@@ -12856,7 +12875,7 @@ class ProsemirrorService {
12856
12875
  }
12857
12876
  getStateFromText(text) {
12858
12877
  const div = document.createElement('div');
12859
- div.innerHTML = text;
12878
+ div.innerHTML = text !== null && text !== void 0 ? text : '';
12860
12879
  return EditorState.create({
12861
12880
  doc: DOMParser.fromSchema(this.mySchema).parse(div),
12862
12881
  plugins: this.configurePlugins({ schema: this.mySchema, floatingMenu: false }),
@@ -13038,7 +13057,8 @@ class TabbedCustomFieldsComponent {
13038
13057
  this.tabbedCustomFields = this.groupByTabs(this.customFields);
13039
13058
  }
13040
13059
  customFieldIsSet(name) {
13041
- return !!this.customFieldsFormGroup.get(name);
13060
+ var _a;
13061
+ return !!((_a = this.customFieldsFormGroup) === null || _a === void 0 ? void 0 : _a.get(name));
13042
13062
  }
13043
13063
  groupByTabs(customFieldConfigs) {
13044
13064
  var _a, _b, _c;
@@ -13352,7 +13372,7 @@ class IfDefaultChannelActiveDirective extends IfDirectiveBase {
13352
13372
  this.changeDetectorRef = changeDetectorRef;
13353
13373
  }
13354
13374
  /**
13355
- * A template to show if the current user does not have the speicified permission.
13375
+ * A template to show if the current user does not have the specified permission.
13356
13376
  */
13357
13377
  set vdrIfMultichannelElse(templateRef) {
13358
13378
  this.setElseTemplate(templateRef);
@@ -13401,7 +13421,7 @@ class IfMultichannelDirective extends IfDirectiveBase {
13401
13421
  this.dataService = dataService;
13402
13422
  }
13403
13423
  /**
13404
- * A template to show if the current user does not have the speicified permission.
13424
+ * A template to show if the current user does not have the specified permission.
13405
13425
  */
13406
13426
  set vdrIfMultichannelElse(templateRef) {
13407
13427
  this.setElseTemplate(templateRef);
@@ -14088,7 +14108,7 @@ class AssetPreviewPipe {
14088
14108
  if (!asset) {
14089
14109
  return '';
14090
14110
  }
14091
- if (!asset.preview || typeof asset.preview !== 'string') {
14111
+ if (asset.preview == null || typeof asset.preview !== 'string') {
14092
14112
  throw new Error(`Expected an Asset, got ${JSON.stringify(asset)}`);
14093
14113
  }
14094
14114
  const fp = asset.focalPoint ? `&fpx=${asset.focalPoint.x}&fpy=${asset.focalPoint.y}` : '';
@@ -14340,6 +14360,28 @@ class LocaleBasePipe {
14340
14360
  this.subscription.unsubscribe();
14341
14361
  }
14342
14362
  }
14363
+ /**
14364
+ * Returns the active locale after attempting to ensure that the locale string
14365
+ * is valid for the Intl API.
14366
+ */
14367
+ getActiveLocale(localeOverride) {
14368
+ var _a;
14369
+ const locale = typeof localeOverride === 'string' ? localeOverride : (_a = this.locale) !== null && _a !== void 0 ? _a : 'en';
14370
+ const hyphenated = locale === null || locale === void 0 ? void 0 : locale.replace(/_/g, '-');
14371
+ // Check for a double-region string, containing 2 region codes like
14372
+ // pt-BR-BR, which is invalid. In this case, the second region is used
14373
+ // and the first region discarded. This would only ever be an issue for
14374
+ // those languages where the translation file itself encodes the region,
14375
+ // as in pt_BR & pt_PT.
14376
+ const matches = hyphenated === null || hyphenated === void 0 ? void 0 : hyphenated.match(/^([a-zA-Z_-]+)(-[A-Z][A-Z])(-[A-Z][A-z])$/);
14377
+ if (matches === null || matches === void 0 ? void 0 : matches.length) {
14378
+ const overriddenLocale = matches[1] + matches[3];
14379
+ return overriddenLocale;
14380
+ }
14381
+ else {
14382
+ return hyphenated;
14383
+ }
14384
+ }
14343
14385
  }
14344
14386
  LocaleBasePipe.decorators = [
14345
14387
  { type: Injectable }
@@ -14365,7 +14407,7 @@ class LocaleCurrencyNamePipe extends LocaleBasePipe {
14365
14407
  super(dataService, changeDetectorRef);
14366
14408
  }
14367
14409
  transform(value, display = 'full', locale) {
14368
- var _a, _b;
14410
+ var _a;
14369
14411
  if (value == null || value === '') {
14370
14412
  return '';
14371
14413
  }
@@ -14374,7 +14416,7 @@ class LocaleCurrencyNamePipe extends LocaleBasePipe {
14374
14416
  }
14375
14417
  let name = '';
14376
14418
  let symbol = '';
14377
- const activeLocale = typeof locale === 'string' ? locale : (_a = this.locale) !== null && _a !== void 0 ? _a : 'en';
14419
+ const activeLocale = this.getActiveLocale(locale);
14378
14420
  // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
14379
14421
  const DisplayNames = Intl.DisplayNames;
14380
14422
  if (display === 'full' || display === 'name') {
@@ -14388,7 +14430,7 @@ class LocaleCurrencyNamePipe extends LocaleBasePipe {
14388
14430
  currency: value,
14389
14431
  currencyDisplay: 'symbol',
14390
14432
  }).formatToParts();
14391
- symbol = ((_b = parts.find(p => p.type === 'currency')) === null || _b === void 0 ? void 0 : _b.value) || value;
14433
+ symbol = ((_a = parts.find(p => p.type === 'currency')) === null || _a === void 0 ? void 0 : _a.value) || value;
14392
14434
  }
14393
14435
  return display === 'full' ? `${name} (${symbol})` : display === 'name' ? name : symbol;
14394
14436
  }
@@ -14423,7 +14465,7 @@ class LocaleCurrencyPipe extends LocaleBasePipe {
14423
14465
  transform(value, ...args) {
14424
14466
  const [currencyCode, locale] = args;
14425
14467
  if (typeof value === 'number' && typeof currencyCode === 'string') {
14426
- const activeLocale = typeof locale === 'string' ? locale : this.locale;
14468
+ const activeLocale = this.getActiveLocale(locale);
14427
14469
  const majorUnits = value / 100;
14428
14470
  return new Intl.NumberFormat(activeLocale, { style: 'currency', currency: currencyCode }).format(majorUnits);
14429
14471
  }
@@ -14460,7 +14502,7 @@ class LocaleDatePipe extends LocaleBasePipe {
14460
14502
  transform(value, ...args) {
14461
14503
  const [format, locale] = args;
14462
14504
  if (this.locale || typeof locale === 'string') {
14463
- const activeLocale = typeof locale === 'string' ? locale : this.locale;
14505
+ const activeLocale = this.getActiveLocale(locale);
14464
14506
  const date = value instanceof Date ? value : typeof value === 'string' ? new Date(value) : undefined;
14465
14507
  if (date) {
14466
14508
  const options = this.getOptionsForFormat(typeof format === 'string' ? format : 'medium');
@@ -14534,14 +14576,13 @@ class LocaleLanguageNamePipe extends LocaleBasePipe {
14534
14576
  super(dataService, changeDetectorRef);
14535
14577
  }
14536
14578
  transform(value, locale) {
14537
- var _a;
14538
14579
  if (value == null || value === '') {
14539
14580
  return '';
14540
14581
  }
14541
14582
  if (typeof value !== 'string') {
14542
14583
  return `Invalid language code "${value}"`;
14543
14584
  }
14544
- const activeLocale = typeof locale === 'string' ? locale : (_a = this.locale) !== null && _a !== void 0 ? _a : 'en';
14585
+ const activeLocale = this.getActiveLocale(locale);
14545
14586
  // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
14546
14587
  const DisplayNames = Intl.DisplayNames;
14547
14588
  try {
@@ -14579,14 +14620,13 @@ class LocaleRegionNamePipe extends LocaleBasePipe {
14579
14620
  super(dataService, changeDetectorRef);
14580
14621
  }
14581
14622
  transform(value, locale) {
14582
- var _a;
14583
14623
  if (value == null || value === '') {
14584
14624
  return '';
14585
14625
  }
14586
14626
  if (typeof value !== 'string') {
14587
14627
  return `Invalid region code "${value}"`;
14588
14628
  }
14589
- const activeLocale = typeof locale === 'string' ? locale : (_a = this.locale) !== null && _a !== void 0 ? _a : 'en';
14629
+ const activeLocale = this.getActiveLocale(locale);
14590
14630
  // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
14591
14631
  const DisplayNames = Intl.DisplayNames;
14592
14632
  try {
@@ -15154,12 +15194,12 @@ class BaseDetailComponent {
15154
15194
  return this.detailForm && this.detailForm.pristine;
15155
15195
  }
15156
15196
  setCustomFieldFormValues(customFields, formGroup, entity, currentTranslation) {
15157
- var _a, _b;
15197
+ var _a, _b, _c;
15158
15198
  for (const fieldDef of customFields) {
15159
15199
  const key = fieldDef.name;
15160
15200
  const value = fieldDef.type === 'localeString'
15161
- ? (_a = currentTranslation.customFields) === null || _a === void 0 ? void 0 : _a[key]
15162
- : (_b = entity.customFields) === null || _b === void 0 ? void 0 : _b[key];
15201
+ ? (_b = (_a = currentTranslation) === null || _a === void 0 ? void 0 : _a.customFields) === null || _b === void 0 ? void 0 : _b[key]
15202
+ : (_c = entity.customFields) === null || _c === void 0 ? void 0 : _c[key];
15163
15203
  const control = formGroup === null || formGroup === void 0 ? void 0 : formGroup.get(key);
15164
15204
  if (control) {
15165
15205
  control.patchValue(value);
@@ -15493,7 +15533,7 @@ function patchObject(obj, patch) {
15493
15533
  }
15494
15534
 
15495
15535
  // Auto-generated by the set-version.js script.
15496
- const ADMIN_UI_VERSION = '1.4.3';
15536
+ const ADMIN_UI_VERSION = '1.4.7';
15497
15537
 
15498
15538
  /**
15499
15539
  * Responsible for registering dashboard widget components and querying for layouts.