@saltcorn/server 0.7.3-beta.3 → 0.7.3
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/app.js +5 -1
- package/auth/admin.js +7 -4
- package/auth/routes.js +37 -10
- package/locales/en.json +6 -1
- package/locales/zh.json +188 -188
- package/markup/admin.js +5 -3
- package/package.json +8 -8
- package/public/gridedit.js +6 -0
- package/public/jquery-menu-editor.min.js +15 -2
- package/public/saltcorn-common.js +37 -4
- package/public/saltcorn.css +17 -0
- package/public/saltcorn.js +24 -3
- package/routes/admin.js +142 -22
- package/routes/eventlog.js +24 -22
- package/routes/fields.js +3 -0
- package/routes/files.js +10 -8
- package/routes/homepage.js +7 -7
- package/routes/infoarch.js +6 -3
- package/routes/pageedit.js +1 -0
- package/routes/plugins.js +78 -11
- package/routes/search.js +4 -2
- package/routes/tables.js +11 -9
- package/routes/tenant.js +4 -2
- package/routes/viewedit.js +13 -2
- package/tests/tenant.test.js +6 -0
package/routes/files.js
CHANGED
|
@@ -184,11 +184,11 @@ router.get(
|
|
|
184
184
|
* @function
|
|
185
185
|
*/
|
|
186
186
|
router.get(
|
|
187
|
-
"/resize/:id/:width_str",
|
|
187
|
+
"/resize/:id/:width_str/:height_str?",
|
|
188
188
|
error_catcher(async (req, res) => {
|
|
189
189
|
const role = req.user && req.user.id ? req.user.role_id : 10;
|
|
190
190
|
const user_id = req.user && req.user.id;
|
|
191
|
-
const { id, width_str } = req.params;
|
|
191
|
+
const { id, width_str, height_str } = req.params;
|
|
192
192
|
let file;
|
|
193
193
|
if (typeof strictParseInt(id) !== "undefined")
|
|
194
194
|
file = await File.findOne({ id });
|
|
@@ -208,15 +208,17 @@ router.get(
|
|
|
208
208
|
if (file.s3_store) s3storage.serveObject(file, res, false);
|
|
209
209
|
else {
|
|
210
210
|
const width = strictParseInt(width_str);
|
|
211
|
+
const height = height_str ? strictParseInt(height_str) : null;
|
|
211
212
|
if (!width) {
|
|
212
213
|
res.sendFile(file.location);
|
|
213
214
|
return;
|
|
214
215
|
}
|
|
215
|
-
const fnm = `${file.location}_w${width}`;
|
|
216
|
+
const fnm = `${file.location}_w${width}${height ? `_h${height}` : ""}`;
|
|
216
217
|
if (!fs.existsSync(fnm)) {
|
|
217
218
|
await resizer({
|
|
218
219
|
fromFileName: file.location,
|
|
219
220
|
width,
|
|
221
|
+
height,
|
|
220
222
|
toFileName: fnm,
|
|
221
223
|
});
|
|
222
224
|
}
|
|
@@ -376,9 +378,6 @@ const storage_form = async (req) => {
|
|
|
376
378
|
],
|
|
377
379
|
action: "/files/storage",
|
|
378
380
|
});
|
|
379
|
-
form.submitButtonClass = "btn-outline-primary";
|
|
380
|
-
form.submitLabel = req.__("Save");
|
|
381
|
-
form.onChange = "remove_outline(this)";
|
|
382
381
|
return form;
|
|
383
382
|
};
|
|
384
383
|
|
|
@@ -429,8 +428,11 @@ router.post(
|
|
|
429
428
|
});
|
|
430
429
|
} else {
|
|
431
430
|
await save_config_from_form(form);
|
|
432
|
-
|
|
433
|
-
|
|
431
|
+
|
|
432
|
+
if (!req.xhr) {
|
|
433
|
+
req.flash("success", req.__("Storage settings updated"));
|
|
434
|
+
res.redirect("/files/storage");
|
|
435
|
+
} else res.json({ success: "ok" });
|
|
434
436
|
}
|
|
435
437
|
})
|
|
436
438
|
);
|
package/routes/homepage.js
CHANGED
|
@@ -49,7 +49,7 @@ const tableTable = (tables, req) =>
|
|
|
49
49
|
*/
|
|
50
50
|
const tableCard = (tables, req) => ({
|
|
51
51
|
type: "card",
|
|
52
|
-
class: "welcome-page-entity-list",
|
|
52
|
+
class: "welcome-page-entity-list mt-1",
|
|
53
53
|
title: link("/table", req.__("Tables")),
|
|
54
54
|
contents:
|
|
55
55
|
(tables.length <= 1
|
|
@@ -102,7 +102,7 @@ const viewTable = (views, req) =>
|
|
|
102
102
|
const viewCard = (views, req) => ({
|
|
103
103
|
type: "card",
|
|
104
104
|
title: link("/viewedit", req.__("Views")),
|
|
105
|
-
class: "welcome-page-entity-list",
|
|
105
|
+
class: "welcome-page-entity-list mt-1",
|
|
106
106
|
bodyClass: "py-0 pe-0",
|
|
107
107
|
contents:
|
|
108
108
|
(views.length <= 1
|
|
@@ -156,7 +156,7 @@ const pageTable = (pages, req) =>
|
|
|
156
156
|
const pageCard = (pages, req) => ({
|
|
157
157
|
type: "card",
|
|
158
158
|
title: link("/pageedit", req.__("Pages")),
|
|
159
|
-
class: "welcome-page-entity-list",
|
|
159
|
+
class: "welcome-page-entity-list mt-1",
|
|
160
160
|
contents:
|
|
161
161
|
(pages.length <= 1
|
|
162
162
|
? p(
|
|
@@ -369,9 +369,9 @@ const welcome_page = async (req) => {
|
|
|
369
369
|
above: [
|
|
370
370
|
{
|
|
371
371
|
besides: [
|
|
372
|
-
pageCard(pages, req),
|
|
373
|
-
viewCard(views, req),
|
|
374
372
|
tableCard(tables, req),
|
|
373
|
+
viewCard(views, req),
|
|
374
|
+
pageCard(pages, req),
|
|
375
375
|
],
|
|
376
376
|
},
|
|
377
377
|
{
|
|
@@ -380,7 +380,7 @@ const welcome_page = async (req) => {
|
|
|
380
380
|
type: "card",
|
|
381
381
|
//title: req.__("Install pack"),
|
|
382
382
|
bodyClass: "py-0 pe-0",
|
|
383
|
-
class: "welcome-page-entity-list",
|
|
383
|
+
class: "welcome-page-entity-list mt-2",
|
|
384
384
|
|
|
385
385
|
tabContents:
|
|
386
386
|
triggers.length > 0
|
|
@@ -399,7 +399,7 @@ const welcome_page = async (req) => {
|
|
|
399
399
|
type: "card",
|
|
400
400
|
//title: req.__("Learn"),
|
|
401
401
|
bodyClass: "py-0 pe-0",
|
|
402
|
-
class: "welcome-page-entity-list",
|
|
402
|
+
class: "welcome-page-entity-list mt-2",
|
|
403
403
|
tabContents:
|
|
404
404
|
users.length > 4
|
|
405
405
|
? {
|
package/routes/infoarch.js
CHANGED
|
@@ -48,8 +48,8 @@ router.get(
|
|
|
48
48
|
const languageForm = (req) =>
|
|
49
49
|
new Form({
|
|
50
50
|
action: "/site-structure/localizer/save-lang",
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
onChange: "saveAndContinue(this)",
|
|
52
|
+
noSubmitButton: true,
|
|
53
53
|
fields: [
|
|
54
54
|
{
|
|
55
55
|
name: "name",
|
|
@@ -270,7 +270,10 @@ router.post(
|
|
|
270
270
|
...cfgLangs,
|
|
271
271
|
[lang.locale]: lang,
|
|
272
272
|
});
|
|
273
|
-
|
|
273
|
+
|
|
274
|
+
if (!req.xhr)
|
|
275
|
+
res.redirect(`/site-structure/localizer/edit/${lang.locale}`);
|
|
276
|
+
else res.json({ success: "ok" });
|
|
274
277
|
}
|
|
275
278
|
})
|
|
276
279
|
);
|
package/routes/pageedit.js
CHANGED
package/routes/plugins.js
CHANGED
|
@@ -506,6 +506,7 @@ const plugin_store_html = (items, req) => {
|
|
|
506
506
|
},
|
|
507
507
|
{
|
|
508
508
|
type: "card",
|
|
509
|
+
class: "mt-0",
|
|
509
510
|
contents: div(
|
|
510
511
|
{ class: "d-flex justify-content-between" },
|
|
511
512
|
storeNavPills(req),
|
|
@@ -560,12 +561,30 @@ router.get(
|
|
|
560
561
|
}
|
|
561
562
|
const flow = module.configuration_workflow();
|
|
562
563
|
flow.action = `/plugins/configure/${encodeURIComponent(plugin.name)}`;
|
|
564
|
+
flow.autoSave = true;
|
|
565
|
+
flow.saveURL = `/plugins/saveconfig/${encodeURIComponent(plugin.name)}`;
|
|
563
566
|
const wfres = await flow.run(plugin.configuration || {});
|
|
567
|
+
if (module.layout) {
|
|
568
|
+
wfres.renderForm.additionalButtons = [
|
|
569
|
+
...(wfres.renderForm.additionalButtons || []),
|
|
570
|
+
{
|
|
571
|
+
label: "Reload page to see changes",
|
|
572
|
+
id: "btnReloadNow",
|
|
573
|
+
class: "btn btn-outline-secondary",
|
|
574
|
+
onclick: "location.reload()",
|
|
575
|
+
},
|
|
576
|
+
];
|
|
577
|
+
wfres.renderForm.onChange = `${
|
|
578
|
+
wfres.renderForm.onChange || ""
|
|
579
|
+
};$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
|
|
580
|
+
}
|
|
564
581
|
|
|
565
|
-
res.sendWrap(
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
582
|
+
res.sendWrap(req.__(`Configure %s Plugin`, plugin.name), {
|
|
583
|
+
type: "card",
|
|
584
|
+
class: "mt-0",
|
|
585
|
+
title: req.__(`Configure %s Plugin`, plugin.name),
|
|
586
|
+
contents: renderForm(wfres.renderForm, req.csrfToken()),
|
|
587
|
+
});
|
|
569
588
|
})
|
|
570
589
|
);
|
|
571
590
|
|
|
@@ -587,13 +606,31 @@ router.post(
|
|
|
587
606
|
}
|
|
588
607
|
const flow = module.configuration_workflow();
|
|
589
608
|
flow.action = `/plugins/configure/${encodeURIComponent(plugin.name)}`;
|
|
609
|
+
flow.autoSave = true;
|
|
610
|
+
flow.saveURL = `/plugins/saveconfig/${encodeURIComponent(plugin.name)}`;
|
|
590
611
|
const wfres = await flow.run(req.body);
|
|
591
|
-
if (wfres.renderForm)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
612
|
+
if (wfres.renderForm) {
|
|
613
|
+
if (module.layout) {
|
|
614
|
+
wfres.renderForm.additionalButtons = [
|
|
615
|
+
...(wfres.renderForm.additionalButtons || []),
|
|
616
|
+
{
|
|
617
|
+
label: "Reload page to see changes",
|
|
618
|
+
id: "btnReloadNow",
|
|
619
|
+
class: "btn btn-outline-secondary",
|
|
620
|
+
onclick: "location.reload()",
|
|
621
|
+
},
|
|
622
|
+
];
|
|
623
|
+
wfres.renderForm.onChange = `${
|
|
624
|
+
wfres.renderForm.onChange || ""
|
|
625
|
+
};$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
|
|
626
|
+
}
|
|
627
|
+
res.sendWrap(req.__(`Configure %s Plugin`, plugin.name), {
|
|
628
|
+
type: "card",
|
|
629
|
+
class: "mt-0",
|
|
630
|
+
title: req.__(`Configure %s Plugin`, plugin.name),
|
|
631
|
+
contents: renderForm(wfres.renderForm, req.csrfToken()),
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
597
634
|
plugin.configuration = wfres;
|
|
598
635
|
await plugin.upsert();
|
|
599
636
|
await load_plugins.loadPlugin(plugin);
|
|
@@ -605,12 +642,42 @@ router.post(
|
|
|
605
642
|
refresh_plugin_cfg: plugin.name,
|
|
606
643
|
tenant: db.getTenantSchema(),
|
|
607
644
|
});
|
|
608
|
-
await sleep(500); // Allow other workers to reload this plugin
|
|
645
|
+
if (module.layout) await sleep(500); // Allow other workers to reload this plugin
|
|
609
646
|
res.redirect("/plugins");
|
|
610
647
|
}
|
|
611
648
|
})
|
|
612
649
|
);
|
|
613
650
|
|
|
651
|
+
router.post(
|
|
652
|
+
"/saveconfig/:name",
|
|
653
|
+
isAdmin,
|
|
654
|
+
error_catcher(async (req, res) => {
|
|
655
|
+
const { name } = req.params;
|
|
656
|
+
const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
|
|
657
|
+
let module = getState().plugins[plugin.name];
|
|
658
|
+
if (!module) {
|
|
659
|
+
module = getState().plugins[getState().plugin_module_names[plugin.name]];
|
|
660
|
+
}
|
|
661
|
+
const flow = module.configuration_workflow();
|
|
662
|
+
const step = await flow.singleStepForm(req.body, req);
|
|
663
|
+
if (step?.renderForm) {
|
|
664
|
+
if (!step.renderForm.hasErrors) {
|
|
665
|
+
plugin.configuration = {
|
|
666
|
+
...plugin.configuration,
|
|
667
|
+
...step.renderForm.values,
|
|
668
|
+
};
|
|
669
|
+
await plugin.upsert();
|
|
670
|
+
await load_plugins.loadPlugin(plugin);
|
|
671
|
+
process.send &&
|
|
672
|
+
process.send({
|
|
673
|
+
refresh_plugin_cfg: plugin.name,
|
|
674
|
+
tenant: db.getTenantSchema(),
|
|
675
|
+
});
|
|
676
|
+
res.json({ success: "ok" });
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
);
|
|
614
681
|
/**
|
|
615
682
|
* @name get/new
|
|
616
683
|
* @function
|
package/routes/search.js
CHANGED
|
@@ -55,7 +55,8 @@ const searchConfigForm = (tables, views, req) => {
|
|
|
55
55
|
);
|
|
56
56
|
return new Form({
|
|
57
57
|
action: "/search/config",
|
|
58
|
-
|
|
58
|
+
noSubmitButton: true,
|
|
59
|
+
onChange: `saveAndContinue(this)`,
|
|
59
60
|
blurb:
|
|
60
61
|
blurb1 +
|
|
61
62
|
(tbls_noviews.length > 0
|
|
@@ -111,7 +112,8 @@ router.post(
|
|
|
111
112
|
|
|
112
113
|
if (result.success) {
|
|
113
114
|
await getState().setConfig("globalSearch", result.success);
|
|
114
|
-
res.redirect("/search/config");
|
|
115
|
+
if (!req.xhr) res.redirect("/search/config");
|
|
116
|
+
else res.json({ success: "ok" });
|
|
115
117
|
} else {
|
|
116
118
|
send_infoarch_page({
|
|
117
119
|
res,
|
package/routes/tables.js
CHANGED
|
@@ -83,8 +83,8 @@ const tableForm = async (table, req) => {
|
|
|
83
83
|
.map((f) => ({ value: f.id, label: f.name }));
|
|
84
84
|
const form = new Form({
|
|
85
85
|
action: "/table",
|
|
86
|
-
|
|
87
|
-
onChange: "
|
|
86
|
+
noSubmitButton: true,
|
|
87
|
+
onChange: "saveAndContinue(this)",
|
|
88
88
|
fields: [
|
|
89
89
|
...(!table.external
|
|
90
90
|
? [
|
|
@@ -448,6 +448,7 @@ router.get(
|
|
|
448
448
|
},
|
|
449
449
|
{
|
|
450
450
|
type: "card",
|
|
451
|
+
class: "mt-0",
|
|
451
452
|
title: cardHeaderTabs([
|
|
452
453
|
{ label: req.__("Your tables"), href: "/table" },
|
|
453
454
|
{
|
|
@@ -636,7 +637,9 @@ router.get(
|
|
|
636
637
|
}
|
|
637
638
|
var viewCard;
|
|
638
639
|
if (fields.length > 0) {
|
|
639
|
-
const views = await View.find(
|
|
640
|
+
const views = await View.find(
|
|
641
|
+
table.external ? { exttable_name: table.name } : { table_id: table.id }
|
|
642
|
+
);
|
|
640
643
|
var viewCardContents;
|
|
641
644
|
if (views.length > 0) {
|
|
642
645
|
viewCardContents = mkTable(
|
|
@@ -801,15 +804,12 @@ router.get(
|
|
|
801
804
|
type: "breadcrumbs",
|
|
802
805
|
crumbs: [
|
|
803
806
|
{ text: req.__("Tables"), href: "/table" },
|
|
804
|
-
{ text: table.name },
|
|
807
|
+
{ text: span({ class: "fw-bold text-body" }, table.name) },
|
|
805
808
|
],
|
|
806
809
|
},
|
|
807
|
-
{
|
|
808
|
-
type: "pageHeader",
|
|
809
|
-
title: req.__(`%s table`, table.name),
|
|
810
|
-
},
|
|
811
810
|
{
|
|
812
811
|
type: "card",
|
|
812
|
+
class: "mt-0",
|
|
813
813
|
title: req.__("Fields"),
|
|
814
814
|
contents: fieldCard,
|
|
815
815
|
},
|
|
@@ -910,7 +910,8 @@ router.post(
|
|
|
910
910
|
);
|
|
911
911
|
else if (!hasError) req.flash("success", req.__("Table saved"));
|
|
912
912
|
|
|
913
|
-
res.redirect(`/table/${id}`);
|
|
913
|
+
if (!req.xhr) res.redirect(`/table/${id}`);
|
|
914
|
+
else res.json({ success: "ok" });
|
|
914
915
|
}
|
|
915
916
|
})
|
|
916
917
|
);
|
|
@@ -1076,6 +1077,7 @@ router.get(
|
|
|
1076
1077
|
},
|
|
1077
1078
|
{
|
|
1078
1079
|
type: "card",
|
|
1080
|
+
class: "mt-0",
|
|
1079
1081
|
title: cardHeaderTabs([
|
|
1080
1082
|
{ label: req.__("Your tables"), href: "/table", active: true },
|
|
1081
1083
|
{
|
package/routes/tenant.js
CHANGED
|
@@ -452,8 +452,10 @@ router.post(
|
|
|
452
452
|
} else {
|
|
453
453
|
await save_config_from_form(form);
|
|
454
454
|
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
if (!req.xhr) {
|
|
456
|
+
req.flash("success", req.__("Tenant settings updated"));
|
|
457
|
+
res.redirect("/tenant/settings");
|
|
458
|
+
} else res.json({ success: "ok" });
|
|
457
459
|
}
|
|
458
460
|
})
|
|
459
461
|
);
|
package/routes/viewedit.js
CHANGED
|
@@ -197,6 +197,7 @@ router.get(
|
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
type: "card",
|
|
200
|
+
class: "mt-0",
|
|
200
201
|
title: req.__("Your views"),
|
|
201
202
|
contents: [
|
|
202
203
|
viewMarkup,
|
|
@@ -378,7 +379,13 @@ router.get(
|
|
|
378
379
|
},
|
|
379
380
|
{
|
|
380
381
|
type: "card",
|
|
381
|
-
|
|
382
|
+
class: "mt-0",
|
|
383
|
+
title: req.__(
|
|
384
|
+
`%s view - %s on %s`,
|
|
385
|
+
viewname,
|
|
386
|
+
viewrow.viewtemplate,
|
|
387
|
+
viewrow.table_name
|
|
388
|
+
),
|
|
382
389
|
contents: renderForm(form, req.csrfToken()),
|
|
383
390
|
},
|
|
384
391
|
],
|
|
@@ -415,6 +422,7 @@ router.get(
|
|
|
415
422
|
},
|
|
416
423
|
{
|
|
417
424
|
type: "card",
|
|
425
|
+
class: "mt-0",
|
|
418
426
|
title: req.__(`Create view`),
|
|
419
427
|
contents: renderForm(form, req.csrfToken()),
|
|
420
428
|
},
|
|
@@ -452,6 +460,7 @@ router.post(
|
|
|
452
460
|
},
|
|
453
461
|
{
|
|
454
462
|
type: "card",
|
|
463
|
+
class: "mt-0",
|
|
455
464
|
title: req.__(`Edit view`),
|
|
456
465
|
contents: renderForm(form, req.csrfToken()),
|
|
457
466
|
},
|
|
@@ -527,6 +536,7 @@ const respondWorkflow = (view, wf, wfres, req, res) => {
|
|
|
527
536
|
},
|
|
528
537
|
{
|
|
529
538
|
type: noCard ? "container" : "card",
|
|
539
|
+
class: !noCard && "mt-0",
|
|
530
540
|
title: wfres.title,
|
|
531
541
|
contents,
|
|
532
542
|
},
|
|
@@ -574,7 +584,7 @@ router.get(
|
|
|
574
584
|
isAdmin,
|
|
575
585
|
error_catcher(async (req, res) => {
|
|
576
586
|
const { name } = req.params;
|
|
577
|
-
|
|
587
|
+
const { step } = req.query;
|
|
578
588
|
const view = await View.findOne({ name });
|
|
579
589
|
if (!view) {
|
|
580
590
|
req.flash("error", `View not found: ${text(name)}`);
|
|
@@ -591,6 +601,7 @@ router.get(
|
|
|
591
601
|
table_id: view.table_id,
|
|
592
602
|
exttable_name: view.exttable_name,
|
|
593
603
|
viewname: name,
|
|
604
|
+
...(step ? { stepName: step } : {}),
|
|
594
605
|
},
|
|
595
606
|
req
|
|
596
607
|
);
|
package/tests/tenant.test.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
toInclude,
|
|
11
11
|
toNotInclude,
|
|
12
12
|
} = require("../auth/testhelp");
|
|
13
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
13
14
|
|
|
14
15
|
afterAll(db.close);
|
|
15
16
|
|
|
@@ -24,6 +25,8 @@ describe("tenant routes", () => {
|
|
|
24
25
|
if (!db.isSQLite) {
|
|
25
26
|
it("shows create form", async () => {
|
|
26
27
|
db.enable_multi_tenant();
|
|
28
|
+
await getState().setConfig("role_to_create_tenant", "10");
|
|
29
|
+
|
|
27
30
|
const app = await getApp({ disableCsrf: true });
|
|
28
31
|
await request(app).get("/tenant/create").expect(toInclude("subdomain"));
|
|
29
32
|
});
|
|
@@ -37,6 +40,8 @@ describe("tenant routes", () => {
|
|
|
37
40
|
});
|
|
38
41
|
it("creates tenant with capital letter", async () => {
|
|
39
42
|
db.enable_multi_tenant();
|
|
43
|
+
await getState().setConfig("role_to_create_tenant", "10");
|
|
44
|
+
|
|
40
45
|
const app = await getApp({ disableCsrf: true });
|
|
41
46
|
await request(app)
|
|
42
47
|
.post("/tenant/create")
|
|
@@ -46,6 +51,7 @@ describe("tenant routes", () => {
|
|
|
46
51
|
});
|
|
47
52
|
it("rejects existing tenant", async () => {
|
|
48
53
|
db.enable_multi_tenant();
|
|
54
|
+
await getState().setConfig("role_to_create_tenant", "10");
|
|
49
55
|
const app = await getApp({ disableCsrf: true });
|
|
50
56
|
await request(app)
|
|
51
57
|
.post("/tenant/create")
|