@saltcorn/server 0.7.3-beta.7 → 0.7.4-beta.1

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/routes/view.js CHANGED
@@ -20,6 +20,7 @@ const {
20
20
  } = require("../routes/utils.js");
21
21
  const { add_edit_bar } = require("../markup/admin.js");
22
22
  const { InvalidConfiguration } = require("@saltcorn/data/utils");
23
+ const { getState } = require("@saltcorn/data/db/state");
23
24
 
24
25
  /**
25
26
  * @type {object}
@@ -44,8 +45,11 @@ router.get(
44
45
  const query = { ...req.query };
45
46
  const view = await View.findOne({ name: viewname });
46
47
  const role = req.user && req.user.id ? req.user.role_id : 10;
48
+ const state = getState();
49
+ state.log(3, `Route /view/${viewname} user=${req.user?.id}`);
47
50
  if (!view) {
48
51
  req.flash("danger", req.__(`No such view: %s`, text(viewname)));
52
+ state.log(2, `View ${viewname} not found`);
49
53
  res.redirect("/");
50
54
  return;
51
55
  }
@@ -56,6 +60,7 @@ router.get(
56
60
  !(await view.authorise_get({ query, req, ...view }))
57
61
  ) {
58
62
  req.flash("danger", req.__("Not authorized"));
63
+ state.log(2, `View ${viewname} not authorized`);
59
64
  res.redirect("/");
60
65
  return;
61
66
  }
@@ -123,13 +128,21 @@ router.post(
123
128
  error_catcher(async (req, res) => {
124
129
  const { viewname, route } = req.params;
125
130
  const role = req.user && req.user.id ? req.user.role_id : 10;
131
+ const state = getState();
132
+ state.log(
133
+ 3,
134
+ `Route /view/${viewname} viewroute ${route} user=${req.user?.id}`
135
+ );
126
136
 
127
137
  const view = await View.findOne({ name: viewname });
128
138
  if (!view) {
129
139
  req.flash("danger", req.__(`No such view: %s`, text(viewname)));
140
+ state.log(2, `View ${viewname} not found`);
130
141
  res.redirect("/");
131
142
  } else if (role > view.min_role) {
132
143
  req.flash("danger", req.__("Not authorized"));
144
+ state.log(2, `View ${viewname} viewroute ${route} not authorized`);
145
+
133
146
  res.redirect("/");
134
147
  } else {
135
148
  await view.runRoute(route, req.body, res, { res, req });
@@ -150,10 +163,12 @@ router.post(
150
163
  const { viewname } = req.params;
151
164
  const role = req.user && req.user.id ? req.user.role_id : 10;
152
165
  const query = { ...req.query };
153
-
166
+ const state = getState();
167
+ state.log(3, `Route /view/${viewname} POST user=${req.user?.id}`);
154
168
  const view = await View.findOne({ name: viewname });
155
169
  if (!view) {
156
170
  req.flash("danger", req.__(`No such view: %s`, text(viewname)));
171
+ state.log(2, `View ${viewname} not found`);
157
172
  res.redirect("/");
158
173
  return;
159
174
  }
@@ -164,6 +179,8 @@ router.post(
164
179
  !(await view.authorise_post({ body: req.body, req, ...view }))
165
180
  ) {
166
181
  req.flash("danger", req.__("Not authorized"));
182
+ state.log(2, `View ${viewname} POST not authorized`);
183
+
167
184
  res.redirect("/");
168
185
  } else if (!view.runPost) {
169
186
  throw new InvalidConfiguration(
@@ -98,6 +98,13 @@ const view_dropdown = (view, req) =>
98
98
  '<i class="far fa-copy"></i>&nbsp;' + req.__("Duplicate"),
99
99
  req
100
100
  ),
101
+ a(
102
+ {
103
+ class: "dropdown-item",
104
+ href: `javascript:ajax_modal('/admin/snapshot-restore/view/${view.name}')`,
105
+ },
106
+ '<i class="fas fa-undo-alt"></i>&nbsp;' + req.__("Restore")
107
+ ),
101
108
  div({ class: "dropdown-divider" }),
102
109
  post_dropdown_item(
103
110
  `/viewedit/delete/${view.id}`,
@@ -138,57 +145,57 @@ router.get(
138
145
  const viewMarkup =
139
146
  views.length > 0
140
147
  ? mkTable(
141
- [
142
- {
143
- label: req.__("Name"),
144
- key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
145
- sortlink: `javascript:set_state_field('_sortby', 'name')`,
146
- },
147
- // description - currently I dont want to show description in view list
148
- // because description can be long
149
- /*
150
- {
151
- label: req.__("Description"),
152
- key: "description",
153
- // this is sorting by column
154
- sortlink: `javascript:set_state_field('_sortby', 'description')`,
155
- },
156
- */
157
- // template
158
- {
159
- label: req.__("Template"),
160
- key: "viewtemplate",
161
- sortlink: `javascript:set_state_field('_sortby', 'viewtemplate')`,
162
- },
163
- {
164
- label: req.__("Table"),
165
- key: (r) => link(`/table/${r.table}`, r.table),
166
- sortlink: `javascript:set_state_field('_sortby', 'table')`,
167
- },
168
- {
169
- label: req.__("Role to access"),
170
- key: (row) => editViewRoleForm(row, roles, req),
171
- },
172
- {
173
- label: "",
174
- key: (r) =>
175
- link(
176
- `/viewedit/config/${encodeURIComponent(r.name)}`,
177
- req.__("Configure")
178
- ),
179
- },
180
- {
181
- label: "",
182
- key: (r) => view_dropdown(r, req),
183
- },
184
- ],
185
- views,
186
- { hover: true }
187
- )
148
+ [
149
+ {
150
+ label: req.__("Name"),
151
+ key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
152
+ sortlink: `javascript:set_state_field('_sortby', 'name')`,
153
+ },
154
+ // description - currently I dont want to show description in view list
155
+ // because description can be long
156
+ /*
157
+ {
158
+ label: req.__("Description"),
159
+ key: "description",
160
+ // this is sorting by column
161
+ sortlink: `javascript:set_state_field('_sortby', 'description')`,
162
+ },
163
+ */
164
+ // template
165
+ {
166
+ label: req.__("Pattern"),
167
+ key: "viewtemplate",
168
+ sortlink: `javascript:set_state_field('_sortby', 'viewtemplate')`,
169
+ },
170
+ {
171
+ label: req.__("Table"),
172
+ key: (r) => link(`/table/${r.table}`, r.table),
173
+ sortlink: `javascript:set_state_field('_sortby', 'table')`,
174
+ },
175
+ {
176
+ label: req.__("Role to access"),
177
+ key: (row) => editViewRoleForm(row, roles, req),
178
+ },
179
+ {
180
+ label: "",
181
+ key: (r) =>
182
+ link(
183
+ `/viewedit/config/${encodeURIComponent(r.name)}`,
184
+ req.__("Configure")
185
+ ),
186
+ },
187
+ {
188
+ label: "",
189
+ key: (r) => view_dropdown(r, req),
190
+ },
191
+ ],
192
+ views,
193
+ { hover: true }
194
+ )
188
195
  : div(
189
- h4(req.__("No views defined")),
190
- p(req.__("Views define how table rows are displayed to the user"))
191
- );
196
+ h4(req.__("No views defined")),
197
+ p(req.__("Views define how table rows are displayed to the user"))
198
+ );
192
199
  res.sendWrap(req.__(`Views`), {
193
200
  above: [
194
201
  {
@@ -203,14 +210,14 @@ router.get(
203
210
  viewMarkup,
204
211
  tables.length > 0
205
212
  ? a(
206
- { href: `/viewedit/new`, class: "btn btn-primary" },
207
- req.__("Create view")
208
- )
213
+ { href: `/viewedit/new`, class: "btn btn-primary" },
214
+ req.__("Create view")
215
+ )
209
216
  : p(
210
- req.__(
211
- "You must create at least one table before you can create views."
212
- )
213
- ),
217
+ req.__(
218
+ "You must create at least one table before you can create views."
219
+ )
220
+ ),
214
221
  ],
215
222
  },
216
223
  ],
@@ -263,10 +270,10 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
263
270
  ),
264
271
  }),
265
272
  new Field({
266
- label: req.__("Template"),
273
+ label: req.__("View pattern"),
267
274
  name: "viewtemplate",
268
275
  input_type: "select",
269
- sublabel: req.__("Views are based on a view template"),
276
+ sublabel: req.__("The view pattern sets the foundation of how the view relates to the table and the behaviour of the view"),
270
277
  options: Object.keys(getState().viewtemplates),
271
278
  attributes: {
272
279
  explainers: mapObjectValues(
@@ -320,15 +327,15 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
320
327
  }),
321
328
  ...(isEdit
322
329
  ? [
323
- new Field({
324
- name: "viewtemplate",
325
- input_type: "hidden",
326
- }),
327
- new Field({
328
- name: "table_name",
329
- input_type: "hidden",
330
- }),
331
- ]
330
+ new Field({
331
+ name: "viewtemplate",
332
+ input_type: "hidden",
333
+ }),
334
+ new Field({
335
+ name: "table_name",
336
+ input_type: "hidden",
337
+ }),
338
+ ]
332
339
  : []),
333
340
  ],
334
341
  values,
@@ -355,7 +362,7 @@ router.get(
355
362
  }
356
363
  const tables = await Table.find_with_external();
357
364
  const currentTable = tables.find(
358
- (t) => t.id === viewrow.table_id || t.name === viewrow.exttable_name
365
+ (t) => (t.id && t.id === viewrow.table_id) || t.name === viewrow.exttable_name
359
366
  );
360
367
  viewrow.table_name = currentTable && currentTable.name;
361
368
  if (viewrow.slug && currentTable) {
@@ -584,7 +591,7 @@ router.get(
584
591
  isAdmin,
585
592
  error_catcher(async (req, res) => {
586
593
  const { name } = req.params;
587
-
594
+ const { step } = req.query;
588
595
  const view = await View.findOne({ name });
589
596
  if (!view) {
590
597
  req.flash("error", `View not found: ${text(name)}`);
@@ -601,6 +608,7 @@ router.get(
601
608
  table_id: view.table_id,
602
609
  exttable_name: view.exttable_name,
603
610
  viewname: name,
611
+ ...(step ? { stepName: step } : {}),
604
612
  },
605
613
  req
606
614
  );
package/serve.js CHANGED
@@ -30,7 +30,7 @@ const { getConfig } = require("@saltcorn/data/models/config");
30
30
  const { migrate } = require("@saltcorn/data/migrate");
31
31
  const socketio = require("socket.io");
32
32
  const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
33
- const { setTenant, getSessionStore } = require("./routes/utils");
33
+ const { setTenant, getSessionStore, get_tenant_from_req } = require("./routes/utils");
34
34
  const passport = require("passport");
35
35
  const { authenticate } = require("passport");
36
36
  const View = require("@saltcorn/data/models/view");
@@ -44,6 +44,11 @@ const {
44
44
  getAllTenants,
45
45
  } = require("@saltcorn/admin-models/models/tenant");
46
46
  const { auto_backup_now } = require("@saltcorn/admin-models/models/backup");
47
+ const Snapshot = require("@saltcorn/admin-models/models/snapshot");
48
+
49
+ const take_snapshot = async () => {
50
+ return await Snapshot.take_if_changed();
51
+ };
47
52
 
48
53
  // helpful https://gist.github.com/jpoehls/2232358
49
54
  /**
@@ -132,32 +137,33 @@ const workerDispatchMsg = ({ tenant, ...msg }) => {
132
137
  */
133
138
  const onMessageFromWorker =
134
139
  (masterState, { port, watchReaper, disableScheduler, pid }) =>
135
- (msg) => {
136
- //console.log("worker msg", typeof msg, msg);
137
- if (msg === "Start" && !masterState.started) {
138
- masterState.started = true;
139
- runScheduler({
140
- port,
141
- watchReaper,
142
- disableScheduler,
143
- eachTenant,
144
- auto_backup_now,
145
- });
146
- require("./systemd")({ port });
147
- return true;
148
- } else if (msg === "RestartServer") {
149
- process.exit(0);
150
- return true;
151
- } else if (msg.tenant || msg.createTenant) {
152
- ///ie from saltcorn
153
- //broadcast
154
- Object.entries(cluster.workers).forEach(([wpid, w]) => {
155
- if (wpid !== pid) w.send(msg);
156
- });
157
- workerDispatchMsg(msg); //also master
158
- return true;
159
- }
160
- };
140
+ (msg) => {
141
+ //console.log("worker msg", typeof msg, msg);
142
+ if (msg === "Start" && !masterState.started) {
143
+ masterState.started = true;
144
+ runScheduler({
145
+ port,
146
+ watchReaper,
147
+ disableScheduler,
148
+ eachTenant,
149
+ auto_backup_now,
150
+ take_snapshot,
151
+ });
152
+ require("./systemd")({ port });
153
+ return true;
154
+ } else if (msg === "RestartServer") {
155
+ process.exit(0);
156
+ return true;
157
+ } else if (msg.tenant || msg.createTenant) {
158
+ ///ie from saltcorn
159
+ //broadcast
160
+ Object.entries(cluster.workers).forEach(([wpid, w]) => {
161
+ if (wpid !== pid) w.send(msg);
162
+ });
163
+ workerDispatchMsg(msg); //also master
164
+ return true;
165
+ }
166
+ };
161
167
 
162
168
  module.exports =
163
169
  /**
@@ -274,6 +280,7 @@ module.exports =
274
280
  disableScheduler,
275
281
  eachTenant,
276
282
  auto_backup_now,
283
+ take_snapshot,
277
284
  });
278
285
  }
279
286
  Trigger.emitEvent("Startup");
@@ -345,24 +352,33 @@ const setupSocket = (...servers) => {
345
352
  io.attach(server);
346
353
  }
347
354
 
348
- io.use(wrap(setTenant));
355
+ //io.use(wrap(setTenant));
349
356
  io.use(wrap(getSessionStore()));
350
357
  io.use(wrap(passport.initialize()));
351
358
  io.use(wrap(passport.authenticate(["jwt", "session"])));
352
359
  if (process.send && !cluster.isMaster) io.adapter(createAdapter());
353
- getState().setRoomEmitter((viewname, room_id, msg) => {
354
- io.to(`${viewname}_${room_id}`).emit("message", msg);
360
+ getState().setRoomEmitter((tenant, viewname, room_id, msg) => {
361
+ io.to(`${tenant}_${viewname}_${room_id}`).emit("message", msg);
355
362
  });
356
363
  io.on("connection", (socket) => {
357
364
  socket.on("join_room", ([viewname, room_id]) => {
358
- const view = View.findOne({ name: viewname });
359
- if (view.viewtemplateObj.authorize_join) {
360
- view.viewtemplateObj
361
- .authorize_join(view.configuration, room_id, socket.request.user)
362
- .then((authorized) => {
363
- if (authorized) socket.join(`${viewname}_${room_id}`);
364
- });
365
- } else socket.join(`${viewname}_${room_id}`);
365
+ const ten = get_tenant_from_req(socket.request) || "public";
366
+ const f = () => {
367
+ try {
368
+ const view = View.findOne({ name: viewname });
369
+ if (view.viewtemplateObj.authorize_join) {
370
+ view.viewtemplateObj
371
+ .authorize_join(view.configuration, room_id, socket.request.user)
372
+ .then((authorized) => {
373
+ if (authorized) socket.join(`${ten}_${viewname}_${room_id}`);
374
+ });
375
+ } else socket.join(`${ten}_${viewname}_${room_id}`);
376
+ } catch (err) {
377
+ getState().log(1, `Socket join_room error: ${err.stack}`);
378
+ }
379
+ }
380
+ if (ten && ten !== "public") db.runWithTenant(ten, f);
381
+ else f();
366
382
  });
367
383
  });
368
384
  };
@@ -57,7 +57,7 @@ describe("admin page", () => {
57
57
  await request(app)
58
58
  .get("/settings")
59
59
  .set("Cookie", loginCookie)
60
- .expect(toInclude("Plugin and pack installation and control"));
60
+ .expect(toInclude("Module installation and control"));
61
61
  });
62
62
  it("show admin page", async () => {
63
63
  const app = await getApp({ disableCsrf: true });
package/tests/api.test.js CHANGED
@@ -84,6 +84,23 @@ describe("API read", () => {
84
84
  )
85
85
  );
86
86
  });
87
+ it("should handle fkey args ", async () => {
88
+ const loginCookie = await getAdminLoginCookie();
89
+ const app = await getApp({ disableCsrf: true });
90
+ await request(app)
91
+ .get("/api/patients/?favbook=1")
92
+ .set("Cookie", loginCookie)
93
+ .expect(succeedJsonWith((rows) => rows.length == 1));
94
+ });
95
+ it("should handle fkey args with no value", async () => {
96
+ const loginCookie = await getAdminLoginCookie();
97
+ const app = await getApp({ disableCsrf: true });
98
+ await request(app)
99
+ .get("/api/patients/?favbook=")
100
+ .set("Cookie", loginCookie)
101
+ .expect(succeedJsonWith((rows) => rows.length == 0));
102
+ });
103
+
87
104
  it("should get books for public with search and one field", async () => {
88
105
  const app = await getApp({ disableCsrf: true });
89
106
  await request(app)
@@ -34,8 +34,18 @@ test("updateQueryStringParameter", () => {
34
34
  expect(removeQueryStringParameter("/foo?name=Bar&age=45", "age")).toBe(
35
35
  "/foo?name=Bar"
36
36
  );
37
+ expect(
38
+ updateQueryStringParameter("/foo", "publisher.publisher->name", "AK")
39
+ ).toBe("/foo?publisher.publisher->name=AK");
40
+ expect(
41
+ updateQueryStringParameter(
42
+ "/foo?publisher.publisher->name=AB",
43
+ "publisher.publisher->name",
44
+ "AK"
45
+ )
46
+ ).toBe("/foo?publisher.publisher->name=AK");
37
47
  });
38
-
48
+ //publisher.publisher->name
39
49
  test("updateQueryStringParameter hash", () => {
40
50
  expect(updateQueryStringParameter("/foo#baz", "age", 43)).toBe(
41
51
  "/foo?age=43#baz"
@@ -30,7 +30,7 @@ describe("Plugin Endpoints", () => {
30
30
  await request(app)
31
31
  .get("/plugins")
32
32
  .set("Cookie", loginCookie)
33
- .expect(toInclude("Plugin and pack store"));
33
+ .expect(toInclude("Module store"));
34
34
  });
35
35
 
36
36
  it("should show new", async () => {
@@ -54,7 +54,7 @@ describe("viewedit new List", () => {
54
54
  await request(app)
55
55
  .get("/viewedit/new")
56
56
  .set("Cookie", loginCookie)
57
- .expect(toInclude("Template"));
57
+ .expect(toInclude("View pattern"));
58
58
  });
59
59
  it("submit new view", async () => {
60
60
  const loginCookie = await getAdminLoginCookie();
package/wrapper.js CHANGED
@@ -13,7 +13,7 @@ const renderLayout = require("@saltcorn/markup/layout");
13
13
  * @returns {T[]}
14
14
  */
15
15
  const getFlashes = (req) =>
16
- ["error", "success", "danger", "warning","information"]
16
+ ["error", "success", "danger", "warning", "information"]
17
17
  .map((type) => {
18
18
  return { type, msg: req.flash(type) };
19
19
  })
@@ -44,11 +44,13 @@ const get_extra_menu = (role, state, req) => {
44
44
  link:
45
45
  item.type === "Link"
46
46
  ? item.url
47
- : item.type === "View"
48
- ? `/view/${encodeURIComponent(item.viewname)}`
49
- : item.type === "Page"
50
- ? `/page/${encodeURIComponent(item.pagename)}`
51
- : undefined,
47
+ : item.type === "Action"
48
+ ? `javascript:ajax_post_json('/menu/runaction/${item.action_name}')`
49
+ : item.type === "View"
50
+ ? `/view/${encodeURIComponent(item.viewname)}`
51
+ : item.type === "Page"
52
+ ? `/page/${encodeURIComponent(item.pagename)}`
53
+ : undefined,
52
54
  ...(item.subitems ? { subitems: transform(item.subitems) } : {}),
53
55
  }));
54
56
  return transform(cfg);
@@ -68,41 +70,41 @@ const get_menu = (req) => {
68
70
  const extra_menu = get_extra_menu(role, state, req);
69
71
  const authItems = isAuth
70
72
  ? [
71
- {
72
- label: req.__("User"),
73
- icon: "far fa-user",
74
- isUser: true,
75
- subitems: [
76
- { label: small((req.user.email || "").split("@")[0]) },
77
- {
78
- label: req.__("User Settings"),
79
- icon: "fas fa-user-cog",
73
+ {
74
+ label: req.__("User"),
75
+ icon: "far fa-user",
76
+ isUser: true,
77
+ subitems: [
78
+ { label: small((req.user.email || "").split("@")[0]) },
79
+ {
80
+ label: req.__("User Settings"),
81
+ icon: "fas fa-user-cog",
80
82
 
81
- link: "/auth/settings",
82
- },
83
- {
84
- link: "/auth/logout",
85
- icon: "fas fa-sign-out-alt",
86
- label: req.__("Logout"),
87
- },
88
- ],
89
- },
90
- ]
83
+ link: "/auth/settings",
84
+ },
85
+ {
86
+ link: "/auth/logout",
87
+ icon: "fas fa-sign-out-alt",
88
+ label: req.__("Logout"),
89
+ },
90
+ ],
91
+ },
92
+ ]
91
93
  : [
92
- ...(allow_signup
93
- ? [{ link: "/auth/signup", label: req.__("Sign up") }]
94
- : []),
95
- ...(login_menu
96
- ? [{ link: "/auth/login", label: req.__("Login") }]
97
- : []),
98
- ];
94
+ ...(allow_signup
95
+ ? [{ link: "/auth/signup", label: req.__("Sign up") }]
96
+ : []),
97
+ ...(login_menu
98
+ ? [{ link: "/auth/login", label: req.__("Login") }]
99
+ : []),
100
+ ];
99
101
  // const schema = db.getTenantSchema();
100
102
  // Admin role id (todo move to common constants)
101
103
  const isAdmin = role === 1;
102
- /*
103
- * Admin Menu items
104
- *
105
- */
104
+ /*
105
+ * Admin Menu items
106
+ *
107
+ */
106
108
  const adminItems = [
107
109
  { link: "/table", icon: "fas fa-table", label: req.__("Tables") },
108
110
  { link: "/viewedit", icon: "far fa-eye", label: req.__("Views") },
@@ -116,7 +118,7 @@ const get_menu = (req) => {
116
118
  icon: "fas fa-tools",
117
119
  label: req.__("About application"),
118
120
  },
119
- { link: "/plugins", icon: "fas fa-plug", label: req.__("Plugins") },
121
+ { link: "/plugins", icon: "fas fa-cubes", label: req.__("Modules") },
120
122
  {
121
123
  link: "/useradmin",
122
124
  icon: "fas fa-users-cog",
@@ -177,17 +179,17 @@ const get_headers = (req, version_tag, description, extras = []) => {
177
179
 
178
180
  const iconHeader = favicon
179
181
  ? [
180
- {
181
- headerTag: `<link rel="icon" type="image/png" href="/files/serve/${favicon}">`,
182
- },
183
- ]
182
+ {
183
+ headerTag: `<link rel="icon" type="image/png" href="/files/serve/${favicon}">`,
184
+ },
185
+ ]
184
186
  : [];
185
187
  const meta_description = description
186
188
  ? [
187
- {
188
- headerTag: `<meta name="description" content="${description}">`,
189
- },
190
- ]
189
+ {
190
+ headerTag: `<meta name="description" content="${description}">`,
191
+ },
192
+ ]
191
193
  : [];
192
194
  const stdHeaders = [
193
195
  {
@@ -228,12 +230,12 @@ const get_brand = (state) => {
228
230
  };
229
231
  };
230
232
  module.exports = (version_tag) =>
231
- /**
232
- *
233
- * @param req
234
- * @param res
235
- * @param next
236
- */
233
+ /**
234
+ *
235
+ * @param req
236
+ * @param res
237
+ * @param next
238
+ */
237
239
  function (req, res, next) {
238
240
  const role = (req.user || {}).role_id || 10;
239
241
 
@@ -349,7 +351,7 @@ const defaultRenderToHtml = (s, role) =>
349
351
  typeof s === "string"
350
352
  ? s
351
353
  : renderLayout({
352
- blockDispatch: {},
353
- role,
354
- layout: s,
355
- });
354
+ blockDispatch: {},
355
+ role,
356
+ layout: s,
357
+ });