chrome-devtools-frontend 1.0.945579 → 1.0.945677

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.
@@ -332,7 +332,7 @@ export class AnimationTimeline extends UI.Widget.VBox implements SDK.TargetManag
332
332
  show: (popover: UI.GlassPane.GlassPane): Promise<boolean> => {
333
333
  let animGroup;
334
334
  for (const [group, previewUI] of this.#previewMap) {
335
- if (previewUI.element === element.parentElement) {
335
+ if (previewUI.element === element || previewUI.element === element.parentElement) {
336
336
  animGroup = group;
337
337
  }
338
338
  }
@@ -270,7 +270,13 @@ export class NetworkItemView extends UI.TabbedPane.TabbedPane {
270
270
 
271
271
  private selectTabInternal(tabId: string): void {
272
272
  if (!this.selectTab(tabId)) {
273
- this.selectTab('headers');
273
+ // maybeAppendPayloadPanel might cause payload tab to appear asynchronously, so
274
+ // it makes sense to retry on the next tick
275
+ setTimeout(() => {
276
+ if (!this.selectTab(tabId)) {
277
+ this.selectTab('headers');
278
+ }
279
+ }, 0);
274
280
  }
275
281
  }
276
282
 
@@ -132,11 +132,8 @@
132
132
  }
133
133
  }
134
134
 
135
- .heap-snapshot-view tr:not(.selected) td.object-column span.heap-object-tag {
136
- color: var(--color-text-secondary);
137
- }
138
-
139
- .heap-snapshot-view td.object-column span.grayed {
135
+ .heap-snapshot-view tr:not(.selected) td.object-column span.heap-object-tag,
136
+ .heap-snapshot-view tr:not(.selected) td.object-column span.grayed {
140
137
  color: var(--color-text-secondary);
141
138
  }
142
139
 
@@ -227,15 +227,15 @@ const PROTOCOL_AUTHENTICATOR_VALUES: Protocol.EnumerableEnum<typeof Protocol.Web
227
227
  U2f: Protocol.WebAuthn.AuthenticatorProtocol.U2f,
228
228
  };
229
229
 
230
- export class WebauthnPaneImpl extends UI.Widget.VBox {
231
- private enabled: boolean;
232
- private activeAuthId: Protocol.WebAuthn.AuthenticatorId|null;
233
- private hasBeenEnabled: boolean;
234
- private readonly dataGrids: Map<Protocol.WebAuthn.AuthenticatorId, DataGrid.DataGrid.DataGridImpl<DataGridNode>>;
235
- // @ts-ignore
236
- private enableCheckbox: UI.Toolbar.ToolbarCheckbox;
230
+ export class WebauthnPaneImpl extends UI.Widget.VBox implements
231
+ SDK.TargetManager.SDKModelObserver<SDK.WebAuthnModel.WebAuthnModel> {
232
+ private activeAuthId: Protocol.WebAuthn.AuthenticatorId|null = null;
233
+ private hasBeenEnabled = false;
234
+ private readonly dataGrids =
235
+ new Map<Protocol.WebAuthn.AuthenticatorId, DataGrid.DataGrid.DataGridImpl<DataGridNode>>();
236
+ private enableCheckbox!: UI.Toolbar.ToolbarCheckbox;
237
237
  private readonly availableAuthenticatorSetting: Common.Settings.Setting<AvailableAuthenticatorOptions[]>;
238
- private model: SDK.WebAuthnModel.WebAuthnModel|null|undefined;
238
+ private model?: SDK.WebAuthnModel.WebAuthnModel;
239
239
  private authenticatorsView: HTMLElement;
240
240
  private topToolbarContainer: HTMLElement|undefined;
241
241
  private topToolbar: UI.Toolbar.Toolbar|undefined;
@@ -253,21 +253,13 @@ export class WebauthnPaneImpl extends UI.Widget.VBox {
253
253
 
254
254
  constructor() {
255
255
  super(true);
256
+ SDK.TargetManager.TargetManager.instance().observeModels(SDK.WebAuthnModel.WebAuthnModel, this);
256
257
 
257
258
  this.contentElement.classList.add('webauthn-pane');
258
- this.enabled = false;
259
- this.activeAuthId = null;
260
- this.hasBeenEnabled = false;
261
- this.dataGrids = new Map();
262
259
 
263
260
  this.availableAuthenticatorSetting =
264
- (Common.Settings.Settings.instance().createSetting('webauthnAuthenticators', []) as
265
- Common.Settings.Setting<AvailableAuthenticatorOptions[]>);
266
-
267
- const mainTarget = SDK.TargetManager.TargetManager.instance().mainTarget();
268
- if (mainTarget) {
269
- this.model = mainTarget.model(SDK.WebAuthnModel.WebAuthnModel);
270
- }
261
+ Common.Settings.Settings.instance().createSetting<AvailableAuthenticatorOptions[]>(
262
+ 'webauthnAuthenticators', []);
271
263
 
272
264
  this.createToolbar();
273
265
  this.authenticatorsView = this.contentElement.createChild('div', 'authenticators-view');
@@ -284,6 +276,18 @@ export class WebauthnPaneImpl extends UI.Widget.VBox {
284
276
  return webauthnPaneImplInstance;
285
277
  }
286
278
 
279
+ modelAdded(model: SDK.WebAuthnModel.WebAuthnModel): void {
280
+ if (model.target() === SDK.TargetManager.TargetManager.instance().mainTarget()) {
281
+ this.model = model;
282
+ }
283
+ }
284
+
285
+ modelRemoved(model: SDK.WebAuthnModel.WebAuthnModel): void {
286
+ if (model.target() === SDK.TargetManager.TargetManager.instance().mainTarget()) {
287
+ this.model = undefined;
288
+ }
289
+ }
290
+
287
291
  private async loadInitialAuthenticators(): Promise<void> {
288
292
  let activeAuthenticatorId: Protocol.WebAuthn.AuthenticatorId|null = null;
289
293
  const availableAuthenticators = this.availableAuthenticatorSetting.get();
@@ -422,7 +426,6 @@ export class WebauthnPaneImpl extends UI.Widget.VBox {
422
426
  Host.userMetrics.actionTaken(Host.UserMetrics.Action.VirtualAuthenticatorEnvironmentEnabled);
423
427
  this.hasBeenEnabled = true;
424
428
  }
425
- this.enabled = enable;
426
429
  if (this.model) {
427
430
  await this.model.setVirtualAuthEnvEnabled(enable);
428
431
  }
@@ -609,7 +612,7 @@ export class WebauthnPaneImpl extends UI.Widget.VBox {
609
612
  UI.UIUtils.createRadioLabel(`active-authenticator-${authenticatorId}`, i18nString(UIStrings.active));
610
613
  activeLabel.radioElement.addEventListener('click', this.setActiveAuthenticator.bind(this, authenticatorId));
611
614
  activeButtonContainer.appendChild(activeLabel);
612
- /** @type {!HTMLInputElement} */ (activeLabel.radioElement as HTMLInputElement).checked = true;
615
+ (activeLabel.radioElement as HTMLInputElement).checked = true;
613
616
  this.activeAuthId = authenticatorId; // Newly added authenticator is automatically set as active.
614
617
 
615
618
  const removeButton = headerElement.createChild('button', 'text-button');
@@ -783,19 +786,16 @@ export class WebauthnPaneImpl extends UI.Widget.VBox {
783
786
  throw new Error('Unable to create options from current inputs');
784
787
  }
785
788
 
786
- /**
787
- * @type {!Protocol.WebAuthn.VirtualAuthenticatorOptions}
788
- */
789
- const options = ({
790
- protocol: this.protocolSelect.options[this.protocolSelect.selectedIndex].value,
791
- transport: this.transportSelect.options[this.transportSelect.selectedIndex].value,
789
+ return {
790
+ protocol: this.protocolSelect.options[this.protocolSelect.selectedIndex].value as
791
+ Protocol.WebAuthn.AuthenticatorProtocol,
792
+ transport: this.transportSelect.options[this.transportSelect.selectedIndex].value as
793
+ Protocol.WebAuthn.AuthenticatorTransport,
792
794
  hasResidentKey: this.residentKeyCheckbox.checked,
793
795
  hasUserVerification: this.userVerificationCheckbox.checked,
794
796
  automaticPresenceSimulation: true,
795
797
  isUserVerified: true,
796
- } as Protocol.WebAuthn.VirtualAuthenticatorOptions);
797
-
798
- return options;
798
+ };
799
799
  }
800
800
 
801
801
  /**
@@ -824,8 +824,7 @@ export class WebauthnPaneImpl extends UI.Widget.VBox {
824
824
  if (!button) {
825
825
  return;
826
826
  }
827
- button.checked =
828
- /** @type {!HTMLElement} */ (authenticator as HTMLElement).dataset.authenticatorId === this.activeAuthId;
827
+ button.checked = (authenticator as HTMLElement).dataset.authenticatorId === this.activeAuthId;
829
828
  });
830
829
  }
831
830
 
@@ -33,6 +33,7 @@ interface ButtonState {
33
33
  size?: Size;
34
34
  disabled: boolean;
35
35
  active: boolean;
36
+ spinner?: boolean;
36
37
  type: ButtonType;
37
38
  value?: string;
38
39
  }
@@ -43,6 +44,7 @@ export type ButtonData = {
43
44
  size?: Size,
44
45
  disabled?: boolean,
45
46
  active?: boolean,
47
+ spinner?: boolean,
46
48
  type?: ButtonType,
47
49
  value?: string,
48
50
  }|{
@@ -51,6 +53,7 @@ export type ButtonData = {
51
53
  size?: Size,
52
54
  disabled?: boolean,
53
55
  active?: boolean,
56
+ spinner?: boolean,
54
57
  type?: ButtonType,
55
58
  value?: string,
56
59
  };
@@ -74,6 +77,7 @@ export class Button extends HTMLElement {
74
77
  size: Size.MEDIUM,
75
78
  disabled: false,
76
79
  active: false,
80
+ spinner: false,
77
81
  type: 'button',
78
82
  };
79
83
  #isEmpty = true;
@@ -94,6 +98,7 @@ export class Button extends HTMLElement {
94
98
  this.#props.iconUrl = data.iconUrl;
95
99
  this.#props.size = data.size || Size.MEDIUM;
96
100
  this.#props.active = Boolean(data.active);
101
+ this.#props.spinner = Boolean(data.spinner);
97
102
  this.#props.type = data.type || 'button';
98
103
  this.setDisabledProperty(data.disabled || false);
99
104
  ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
@@ -129,6 +134,11 @@ export class Button extends HTMLElement {
129
134
  ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
130
135
  }
131
136
 
137
+ set spinner(spinner: boolean) {
138
+ this.#props.spinner = spinner;
139
+ ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
140
+ }
141
+
132
142
  private setDisabledProperty(disabled: boolean): void {
133
143
  this.#props.disabled = disabled;
134
144
  this.toggleAttribute('disabled', disabled);
@@ -189,6 +199,12 @@ export class Button extends HTMLElement {
189
199
  small: Boolean(this.#props.size === Size.SMALL),
190
200
  active: this.#props.active,
191
201
  };
202
+ const spinnerClasses = {
203
+ primary: this.#props.variant === Variant.PRIMARY,
204
+ secondary: this.#props.variant === Variant.SECONDARY,
205
+ disabled: Boolean(this.#props.disabled),
206
+ 'spinner-component': true,
207
+ };
192
208
  // clang-format off
193
209
  LitHtml.render(
194
210
  LitHtml.html`
@@ -200,6 +216,7 @@ export class Button extends HTMLElement {
200
216
  } as IconButton.Icon.IconData}
201
217
  >
202
218
  </${IconButton.Icon.Icon.litTagName}>` : ''}
219
+ ${this.#props.spinner ? LitHtml.html`<span class=${LitHtml.Directives.classMap(spinnerClasses)}></span>` : ''}
203
220
  <slot @slotchange=${this.onSlotChange}></slot>
204
221
  </button>
205
222
  `, this.#shadow, {host: this});
@@ -201,3 +201,34 @@ button.primary:disabled devtools-icon {
201
201
  button.secondary:disabled devtools-icon {
202
202
  --icon-color: var(--color-text-disabled);
203
203
  }
204
+
205
+ .spinner-component.secondary {
206
+ border: 2px solid var(--color-primary);
207
+ border-right-color: transparent;
208
+ }
209
+
210
+ .spinner-component.disabled {
211
+ border: 2px solid var(--color-text-disabled);
212
+ border-right-color: transparent;
213
+ }
214
+
215
+ .spinner-component {
216
+ display: block;
217
+ width: 12px;
218
+ height: 12px;
219
+ border-radius: 6px;
220
+ border: 2px solid var(--color-background);
221
+ animation: spinner-animation 1s linear infinite;
222
+ border-right-color: transparent;
223
+ margin-right: 6px;
224
+ }
225
+
226
+ @keyframes spinner-animation {
227
+ from {
228
+ transform: rotate(0);
229
+ }
230
+
231
+ to {
232
+ transform: rotate(360deg);
233
+ }
234
+ }
@@ -832,6 +832,15 @@ export class DataGrid extends HTMLElement {
832
832
  }
833
833
  this.scrollToBottomIfRequired();
834
834
  this.engageResizeObserver();
835
+ if (this.#hasRenderedAtLeastOnce) {
836
+ // We may have had a cell's width change on a re-render, or it may have
837
+ // been hidden entirely, so we need to ensure that the resize handlers are
838
+ // re-positioned correctly if so.
839
+
840
+ // We don't have to do this on first render as it will fire when the resize observer is engaged.
841
+ this.alignScrollHandlers();
842
+ }
843
+
835
844
  this.#isRendering = false;
836
845
  this.#hasRenderedAtLeastOnce = true;
837
846
 
@@ -42,6 +42,16 @@ forcedActive.innerText = 'Forced active';
42
42
  forcedActive.onclick = () => alert('clicked');
43
43
  appendButton(forcedActive);
44
44
 
45
+ // Primary (forced spinner)
46
+ const forcedSpinner = new Buttons.Button.Button();
47
+ forcedSpinner.data = {
48
+ variant: Buttons.Button.Variant.PRIMARY,
49
+ spinner: true,
50
+ };
51
+ forcedSpinner.innerText = 'Forced spinner';
52
+ forcedSpinner.onclick = () => alert('clicked');
53
+ appendButton(forcedSpinner);
54
+
45
55
  // Secondary
46
56
  const secondaryButton = new Buttons.Button.Button();
47
57
  secondaryButton.innerText = 'Click me';
@@ -51,6 +61,16 @@ secondaryButton.data = {
51
61
  };
52
62
  appendButton(secondaryButton);
53
63
 
64
+ // Secondary spinner
65
+ const secondarySpinnerButton = new Buttons.Button.Button();
66
+ secondarySpinnerButton.innerText = 'Click me';
67
+ secondarySpinnerButton.onclick = () => alert('clicked');
68
+ secondarySpinnerButton.data = {
69
+ variant: Buttons.Button.Variant.SECONDARY,
70
+ spinner: true,
71
+ };
72
+ appendButton(secondarySpinnerButton);
73
+
54
74
  // Primary
55
75
  const disabledPrimaryButtons = new Buttons.Button.Button();
56
76
  disabledPrimaryButtons.data = {
@@ -61,6 +81,17 @@ disabledPrimaryButtons.innerText = 'Cannot click me';
61
81
  disabledPrimaryButtons.onclick = () => alert('clicked');
62
82
  appendButton(disabledPrimaryButtons);
63
83
 
84
+ // Primary spinner
85
+ const disabledSpinnerPrimaryButtons = new Buttons.Button.Button();
86
+ disabledSpinnerPrimaryButtons.data = {
87
+ variant: Buttons.Button.Variant.PRIMARY,
88
+ disabled: true,
89
+ spinner: true,
90
+ };
91
+ disabledSpinnerPrimaryButtons.innerText = 'Cannot click me';
92
+ disabledSpinnerPrimaryButtons.onclick = () => alert('clicked');
93
+ appendButton(disabledSpinnerPrimaryButtons);
94
+
64
95
  // Secondary
65
96
  const disabledSecondaryButton = new Buttons.Button.Button();
66
97
  disabledSecondaryButton.innerText = 'Cannot click me';
@@ -71,6 +102,17 @@ disabledSecondaryButton.data = {
71
102
  };
72
103
  appendButton(disabledSecondaryButton);
73
104
 
105
+ // Secondary spinner
106
+ const disabledSpinnerSecondaryButton = new Buttons.Button.Button();
107
+ disabledSpinnerSecondaryButton.innerText = 'Cannot click me';
108
+ disabledSpinnerSecondaryButton.onclick = () => alert('clicked');
109
+ disabledSpinnerSecondaryButton.data = {
110
+ variant: Buttons.Button.Variant.SECONDARY,
111
+ disabled: true,
112
+ spinner: true,
113
+ };
114
+ appendButton(disabledSpinnerSecondaryButton);
115
+
74
116
  // Primary Icon
75
117
  const primaryIconButton = new Buttons.Button.Button();
76
118
  primaryIconButton.innerText = 'Click me';
@@ -28,7 +28,7 @@ export class TextEditor extends HTMLElement {
28
28
  #pendingState: CodeMirror.EditorState|undefined;
29
29
  #lastScrollPos = {left: 0, top: 0};
30
30
  #resizeTimeout = -1;
31
- #devtoolsResizeObserver = new ResizeObserver(() => {
31
+ #resizeListener = (): void => {
32
32
  if (this.#resizeTimeout < 0) {
33
33
  this.#resizeTimeout = window.setTimeout(() => {
34
34
  this.#resizeTimeout = -1;
@@ -37,7 +37,8 @@ export class TextEditor extends HTMLElement {
37
37
  }
38
38
  }, 50);
39
39
  }
40
- });
40
+ };
41
+ #devtoolsResizeObserver = new ResizeObserver(this.#resizeListener);
41
42
 
42
43
  constructor(pendingState?: CodeMirror.EditorState) {
43
44
  super();
@@ -104,6 +105,7 @@ export class TextEditor extends HTMLElement {
104
105
  if (this.#activeEditor) {
105
106
  this.#pendingState = this.#activeEditor.state;
106
107
  this.#devtoolsResizeObserver.disconnect();
108
+ window.removeEventListener('resize', this.#resizeListener);
107
109
  this.#activeEditor.destroy();
108
110
  this.#activeEditor = undefined;
109
111
  this.ensureSettingListeners();
@@ -148,6 +150,7 @@ export class TextEditor extends HTMLElement {
148
150
  if (devtoolsElement) {
149
151
  this.#devtoolsResizeObserver.observe(devtoolsElement);
150
152
  }
153
+ window.addEventListener('resize', this.#resizeListener);
151
154
  }
152
155
 
153
156
  revealPosition(selection: CodeMirror.EditorSelection, highlight: boolean = true): void {
@@ -276,10 +276,10 @@ export class Editor<T> {
276
276
  this.element = document.createElement('div');
277
277
  this.element.classList.add('editor-container');
278
278
  this.element.addEventListener('keydown', onKeyDown.bind(null, isEscKey, this.cancelClicked.bind(this)), false);
279
- this.element.addEventListener(
280
- 'keydown', onKeyDown.bind(null, event => event.key === 'Enter', this.commitClicked.bind(this)), false);
281
279
 
282
280
  this.contentElementInternal = this.element.createChild('div', 'editor-content');
281
+ this.contentElementInternal.addEventListener(
282
+ 'keydown', onKeyDown.bind(null, event => event.key === 'Enter', this.commitClicked.bind(this)), false);
283
283
 
284
284
  const buttonsRow = this.element.createChild('div', 'editor-buttons');
285
285
  this.commitButton = createTextButton('', this.commitClicked.bind(this), '', true /* primary */);
package/package.json CHANGED
@@ -55,5 +55,5 @@
55
55
  "unittest": "scripts/test/run_unittests.py --no-text-coverage",
56
56
  "watch": "third_party/node/node.py --output scripts/watch_build.js"
57
57
  },
58
- "version": "1.0.945579"
58
+ "version": "1.0.945677"
59
59
  }
@@ -5,8 +5,9 @@
5
5
  'use strict';
6
6
 
7
7
  const path = require('path');
8
+ const {devtoolsRootPath} = require('../../devtools_paths.js');
8
9
 
9
- const FRONT_END_DIRECTORY = path.join(__dirname, '..', '..', '..', 'front_end');
10
+ const DEFAULT_FRONT_END_DIRECTORY = path.join(devtoolsRootPath(), 'front_end');
10
11
 
11
12
  function isModuleScope(context) {
12
13
  return context.getScope().type === 'module';
@@ -36,7 +37,15 @@ module.exports = {
36
37
  category: 'Possible Errors',
37
38
  },
38
39
  fixable: 'code',
39
- schema: [] // no options
40
+ schema: [{
41
+ 'type': 'object',
42
+ 'properties': {
43
+ 'rootFrontendDirectory': {
44
+ 'type': 'string',
45
+ },
46
+ },
47
+ additionalProperties: false,
48
+ }]
40
49
  },
41
50
  create: function(context) {
42
51
  return {
@@ -51,8 +60,12 @@ module.exports = {
51
60
  return;
52
61
  }
53
62
 
63
+ let frontEndDirectory = DEFAULT_FRONT_END_DIRECTORY;
64
+ if (context.options && context.options[0]?.rootFrontendDirectory) {
65
+ frontEndDirectory = context.options[0].rootFrontendDirectory;
66
+ }
54
67
  const currentSourceFile = path.resolve(context.getFilename());
55
- const currentFileRelativeToFrontEnd = path.relative(FRONT_END_DIRECTORY, currentSourceFile);
68
+ const currentFileRelativeToFrontEnd = path.relative(frontEndDirectory, currentSourceFile);
56
69
 
57
70
  const currentModuleDirectory = path.dirname(currentSourceFile);
58
71
  const allowedPathArguments = [
@@ -62,7 +75,7 @@ module.exports = {
62
75
  ];
63
76
 
64
77
  const previousFileLocationArgument = callExpression.arguments[0];
65
- const actualPath = path.join(FRONT_END_DIRECTORY, previousFileLocationArgument.value);
78
+ const actualPath = path.join(frontEndDirectory, previousFileLocationArgument.value);
66
79
  if (!allowedPathArguments.includes(actualPath)) {
67
80
  const newFileName = currentFileRelativeToFrontEnd.replace(/\\/g, '/');
68
81
  context.report({
@@ -4,6 +4,8 @@
4
4
 
5
5
  'use strict';
6
6
 
7
+ const path = require('path');
8
+
7
9
  const rule = require('../lib/l10n_filename_matches.js');
8
10
  const ruleTester = new (require('eslint').RuleTester)({
9
11
  parserOptions: {ecmaVersion: 9, sourceType: 'module'},
@@ -24,6 +26,16 @@ ruleTester.run('l10n_filename_matches', rule, {
24
26
  code: 'const str_ = i18n.i18n.registerUIStrings(\'components/ModuleUIStrings.ts\', UIStrings);',
25
27
  filename: 'front_end/components/test.ts',
26
28
  },
29
+ {
30
+ code: 'const str_ = i18n.i18n.registerUIStrings(\'ModuleUIStrings.ts\', UIStrings);',
31
+ filename: 'front_end/components/test.ts',
32
+ options: [{rootFrontendDirectory: path.join(__dirname, '..', '..', '..', 'front_end', 'components')}]
33
+ },
34
+ {
35
+ code: 'const str_ = i18n.i18n.registerUIStrings(\'test.ts\', UIStrings);',
36
+ filename: 'front_end/components/test.ts',
37
+ options: [{rootFrontendDirectory: path.join(__dirname, '..', '..', '..', 'front_end', 'components')}]
38
+ },
27
39
  ],
28
40
  invalid: [
29
41
  {
@@ -35,5 +47,14 @@ ruleTester.run('l10n_filename_matches', rule, {
35
47
  }],
36
48
  output: 'const str_ = i18n.i18n.registerUIStrings(\'components/test.ts\', UIStrings);',
37
49
  },
50
+ {
51
+ code: 'const str_ = i18n.i18n.registerUIStrings(\'components/test.ts\', UIStrings);',
52
+ filename: 'front_end/components/test.ts',
53
+ errors: [
54
+ {message: 'First argument to \'registerUIStrings\' call must be \'test.ts\' or the ModuleUIStrings.(js|ts)'}
55
+ ],
56
+ output: 'const str_ = i18n.i18n.registerUIStrings(\'test.ts\', UIStrings);',
57
+ options: [{rootFrontendDirectory: path.join(__dirname, '..', '..', '..', 'front_end', 'components')}]
58
+ },
38
59
  ]
39
60
  });