@saltcorn/server 0.9.5-beta.9 → 0.9.5

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/api.js CHANGED
@@ -373,7 +373,10 @@ router.all(
373
373
  res.redirect(resp.goto);
374
374
  else if (req.headers?.scgotourl)
375
375
  res.redirect(req.headers?.scgotourl);
376
- else res.json({ success: true, data: resp });
376
+ else {
377
+ if (trigger.configuration?._raw_output) res.json({ resp });
378
+ else res.json({ success: true, data: resp });
379
+ }
377
380
  } catch (e) {
378
381
  Crash.create(e, req);
379
382
  res.status(400).json({ success: false, error: e.message });
package/routes/fields.js CHANGED
@@ -251,6 +251,16 @@ const fieldFlow = (req) =>
251
251
  : ""
252
252
  }row).${model_output}`;
253
253
  }
254
+ if (context.expression_type === "Aggregation") {
255
+ expression = "__aggregation";
256
+ attributes.agg_relation = context.agg_relation;
257
+ attributes.agg_field = context.agg_field;
258
+ attributes.aggwhere = context.aggwhere;
259
+ attributes.aggregate = context.aggregate;
260
+ const [table, ref] = context.agg_relation.split(".");
261
+ attributes.table = table;
262
+ attributes.ref = ref;
263
+ }
254
264
  const { reftable_name, type } = calcFieldType(context.type);
255
265
  const fldRow = {
256
266
  table_id,
@@ -421,6 +431,50 @@ const fieldFlow = (req) =>
421
431
  );
422
432
  output_options[model.name] = outputs.map((o) => o.name);
423
433
  }
434
+ const aggStatOptions = {};
435
+
436
+ const { child_field_list, child_relations } =
437
+ await table.get_child_relations(true);
438
+ const agg_field_opts = child_relations.map(
439
+ ({ table, key_field, through }) => {
440
+ const aggKey =
441
+ (through ? `${through.name}->` : "") +
442
+ `${table.name}.${key_field.name}`;
443
+ aggStatOptions[aggKey] = [
444
+ "Count",
445
+ "CountUnique",
446
+ "Avg",
447
+ "Sum",
448
+ "Max",
449
+ "Min",
450
+ "Array_Agg",
451
+ ];
452
+ table.fields.forEach((f) => {
453
+ if (f.type && f.type.name === "Date") {
454
+ aggStatOptions[aggKey].push(`Latest ${f.name}`);
455
+ aggStatOptions[aggKey].push(`Earliest ${f.name}`);
456
+ }
457
+ });
458
+ return {
459
+ name: `agg_field`,
460
+ label: req.__("On Field"),
461
+ type: "String",
462
+ required: true,
463
+ attributes: {
464
+ options: table.fields
465
+ .filter((f) => !f.calculated || f.stored)
466
+ .map((f) => ({
467
+ label: f.name,
468
+ name: `${f.name}@${f.type_name}`,
469
+ })),
470
+ },
471
+ showIf: {
472
+ agg_relation: aggKey,
473
+ expression_type: "Aggregation",
474
+ },
475
+ };
476
+ }
477
+ );
424
478
  return new Form({
425
479
  fields: [
426
480
  {
@@ -429,9 +483,43 @@ const fieldFlow = (req) =>
429
483
  input_type: "select",
430
484
  options: [
431
485
  "JavaScript expression",
486
+ ...(child_relations.length && context.stored
487
+ ? ["Aggregation"]
488
+ : []),
432
489
  ...(models.length ? ["Model prediction"] : []),
433
490
  ],
434
491
  },
492
+ {
493
+ name: "agg_relation",
494
+ label: req.__("Relation"),
495
+ type: "String",
496
+ required: true,
497
+ attributes: {
498
+ options: child_field_list,
499
+ },
500
+ showIf: { expression_type: "Aggregation" },
501
+ },
502
+ ...agg_field_opts,
503
+ {
504
+ name: "aggregate",
505
+ label: req.__("Statistic"),
506
+ type: "String",
507
+ required: true,
508
+ attributes: {
509
+ calcOptions: ["agg_relation", aggStatOptions],
510
+ },
511
+
512
+ showIf: { expression_type: "Aggregation" },
513
+ },
514
+ {
515
+ name: "aggwhere",
516
+ label: req.__("Where"),
517
+ sublabel: req.__("Formula"),
518
+ class: "validate-expression",
519
+ type: "String",
520
+ required: false,
521
+ showIf: { expression_type: "Aggregation" },
522
+ },
435
523
  {
436
524
  name: "model",
437
525
  label: req.__("Model"),
@@ -487,7 +575,9 @@ const fieldFlow = (req) =>
487
575
  new Field({
488
576
  name: "test_btn",
489
577
  label: req.__("Test"),
490
- showIf: { expression_type: "JavaScript expression" },
578
+ showIf: {
579
+ expression_type: ["JavaScript expression"],
580
+ },
491
581
  // todo sublabel
492
582
  input_type: "custom_html",
493
583
  attributes: {
@@ -652,6 +742,9 @@ router.get(
652
742
  {
653
743
  ...field.toJson,
654
744
  ...field.attributes,
745
+ ...(field.expression === "__aggregation"
746
+ ? { expression_type: "Aggregation" }
747
+ : {}),
655
748
  },
656
749
  req
657
750
  );
@@ -955,8 +1048,11 @@ router.post(
955
1048
  }
956
1049
  return;
957
1050
  }
1051
+ }
1052
+ if (targetField.type === "File") {
1053
+ fv = getState().fileviews[fieldview];
958
1054
  } else {
959
- fv = targetField.type.fieldviews[fieldview];
1055
+ fv = targetField.type?.fieldviews?.[fieldview];
960
1056
  if (!fv)
961
1057
  fv =
962
1058
  targetField.type.fieldviews.show ||
@@ -994,13 +1090,15 @@ router.post(
994
1090
  if (oldRow) {
995
1091
  const value = oldRow[kpath[kpath.length - 1]];
996
1092
  //TODO run fieldview
997
- res.send(
998
- typeof value === "string"
999
- ? value
1000
- : value?.toString
1001
- ? value.toString()
1002
- : `${value}`
1003
- );
1093
+ if (value === null || typeof value === "undefined") res.send("");
1094
+ else
1095
+ res.send(
1096
+ typeof value === "string"
1097
+ ? value
1098
+ : value?.toString
1099
+ ? value.toString()
1100
+ : `${value}`
1101
+ );
1004
1102
  return;
1005
1103
  }
1006
1104
  }
package/routes/menu.js CHANGED
@@ -19,7 +19,7 @@ const { save_menu_items } = require("@saltcorn/data/models/config");
19
19
  const db = require("@saltcorn/data/db");
20
20
 
21
21
  const { renderForm } = require("@saltcorn/markup");
22
- const { script, domReady, div, ul } = require("@saltcorn/markup/tags");
22
+ const { script, domReady, div, ul, i } = require("@saltcorn/markup/tags");
23
23
  const { send_infoarch_page } = require("../markup/admin.js");
24
24
  const Table = require("@saltcorn/data/models/table");
25
25
  const Trigger = require("@saltcorn/data/models/trigger");
@@ -368,7 +368,7 @@ const menuEditorScript = (menu_items) => `
368
368
  iconPicker: iconPickerOptions,
369
369
  getLabelText: (item) => item?.text || item?.type,
370
370
  labelEdit: 'Edit&nbsp;<i class="fas fa-edit clickable"></i>',
371
- maxLevel: 1 // (Optional) Default is -1 (no level limit)
371
+ maxLevel: 2 // (Optional) Default is -1 (no level limit)
372
372
  // Valid levels are from [0, 1, 2, 3,...N]
373
373
  });
374
374
  editor.setForm($('#menuForm'));
@@ -446,7 +446,16 @@ router.get(
446
446
  above: [
447
447
  {
448
448
  besides: [
449
- div(ul({ id: "myEditor", class: "sortableLists list-group" })),
449
+ div(
450
+ ul({ id: "myEditor", class: "sortableLists list-group" }),
451
+ div(
452
+ i(
453
+ req.__(
454
+ "Some themes support only one level of menu nesting."
455
+ )
456
+ )
457
+ )
458
+ ),
450
459
  div(
451
460
  renderForm(form, req.csrfToken()),
452
461
  script(domReady(menuEditorScript(menu_items)))
package/routes/page.js CHANGED
@@ -135,7 +135,13 @@ router.get(
135
135
  "/:pagename",
136
136
  error_catcher(async (req, res) => {
137
137
  const { pagename } = req.params;
138
- getState().log(3, `Route /page/${pagename} user=${req.user?.id}`);
138
+ const state = getState();
139
+ state.log(
140
+ 3,
141
+ `Route /page/${pagename} user=${req.user?.id}${
142
+ state.getConfig("log_ip_address", false) ? ` IP=${req.ip}` : ""
143
+ }`
144
+ );
139
145
  const tic = new Date();
140
146
  const { page, pageGroup } = findPageOrGroup(pagename);
141
147
  if (page) await runPage(page, req, res, tic);
@@ -144,7 +150,7 @@ router.get(
144
150
  if ((page || pageGroup) && !req.user) {
145
151
  res.redirect(`/auth/login?dest=${encodeURIComponent(req.originalUrl)}`);
146
152
  } else {
147
- getState().log(2, `Page ${pagename} not found or not authorized`);
153
+ state.log(2, `Page ${pagename} not found or not authorized`);
148
154
  res
149
155
  .status(404)
150
156
  .sendWrap(
@@ -196,7 +196,7 @@ const pageBuilderData = async (req, context) => {
196
196
  f.required = false;
197
197
  if (f.type && f.type.name === "Bool") f.fieldview = "tristate";
198
198
 
199
- await f.fill_fkey_options(true);
199
+ //await f.fill_fkey_options(true);
200
200
  fixed_state_fields[view.name].push(f);
201
201
  if (table.name === "users" && f.primary_key)
202
202
  fixed_state_fields[view.name].push(
package/routes/plugins.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  const Router = require("express-promise-router");
9
- const { isAdmin, error_catcher } = require("./utils.js");
9
+ const { isAdmin, loggedIn, error_catcher } = require("./utils.js");
10
10
  const { renderForm, link, post_btn } = require("@saltcorn/markup");
11
11
  const {
12
12
  getState,
@@ -16,6 +16,7 @@ const {
16
16
  const Form = require("@saltcorn/data/models/form");
17
17
  const Field = require("@saltcorn/data/models/field");
18
18
  const Plugin = require("@saltcorn/data/models/plugin");
19
+ const User = require("@saltcorn/data/models/user");
19
20
  const { fetch_available_packs } = require("@saltcorn/admin-models/models/pack");
20
21
  const {
21
22
  upgrade_all_tenants_plugins,
@@ -362,7 +363,9 @@ const storeNavPills = (req) => {
362
363
  { class: "nav-item" },
363
364
  a(
364
365
  {
365
- href: `/plugins?set=${txt.toLowerCase()}`,
366
+ href: `/plugins?set=${txt.toLowerCase()}${
367
+ req.query.q ? `&q=${req.query.q}` : ""
368
+ }`,
366
369
  class: [
367
370
  "nav-link",
368
371
  (req.query.set === txt.toLowerCase() ||
@@ -546,6 +549,16 @@ const plugin_store_html = (items, req) => {
546
549
  };
547
550
  };
548
551
 
552
+ const flash_relogin = (req, exposedConfigs) => {
553
+ req.flash(
554
+ "warning",
555
+ req.__(
556
+ "To see changes for '%s' in show-if-formulas, users need to relogin",
557
+ exposedConfigs.join(", ")
558
+ )
559
+ );
560
+ };
561
+
549
562
  /**
550
563
  * @name get
551
564
  * @function
@@ -697,7 +710,12 @@ router.get(
697
710
  label: "Reload page to see changes",
698
711
  id: "btnReloadNow",
699
712
  class: "btn btn-outline-secondary",
700
- onclick: "location.reload()",
713
+ onclick: `if (window.savingViewConfig)
714
+ notifyAlert({
715
+ type: 'danger',
716
+ text: 'Still saving, please wait',
717
+ });
718
+ else location.reload();`,
701
719
  },
702
720
  ];
703
721
  wfres.renderForm.onChange = `${
@@ -705,25 +723,31 @@ router.get(
705
723
  };$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
706
724
  }
707
725
 
708
- res.sendWrap(req.__(`Configure %s Plugin`, plugin.name), {
709
- above: [
710
- {
711
- type: "breadcrumbs",
712
- crumbs: [
713
- { text: req.__("Settings"), href: "/settings" },
714
- { text: req.__("Module store"), href: "/plugins" },
715
- { text: plugin.name },
716
- ],
717
- },
718
- {
719
- type: "card",
720
- class: "mt-0",
721
- title: req.__(`Configure %s Plugin`, plugin.name),
722
- titleAjaxIndicator: true,
723
- contents: renderForm(wfres.renderForm, req.csrfToken()),
724
- },
725
- ],
726
- });
726
+ res.sendWrap(
727
+ {
728
+ title: req.__(`Configure %s Plugin`, plugin.name),
729
+ headers: wfres.renderForm?.additionalHeaders || [],
730
+ },
731
+ {
732
+ above: [
733
+ {
734
+ type: "breadcrumbs",
735
+ crumbs: [
736
+ { text: req.__("Settings"), href: "/settings" },
737
+ { text: req.__("Module store"), href: "/plugins" },
738
+ { text: plugin.name },
739
+ ],
740
+ },
741
+ {
742
+ type: "card",
743
+ class: "mt-0",
744
+ title: req.__(`Configure %s Plugin`, plugin.name),
745
+ titleAjaxIndicator: true,
746
+ contents: renderForm(wfres.renderForm, req.csrfToken()),
747
+ },
748
+ ],
749
+ }
750
+ );
727
751
  })
728
752
  );
729
753
 
@@ -770,17 +794,21 @@ router.post(
770
794
  contents: renderForm(wfres.renderForm, req.csrfToken()),
771
795
  });
772
796
  } else {
773
- plugin.configuration = wfres;
797
+ const newCfg = wfres.cleanup ? wfres.context : wfres;
798
+ plugin.configuration = newCfg;
774
799
  await plugin.upsert();
775
800
  await load_plugins.loadPlugin(plugin);
776
801
  const instore = await Plugin.store_plugins_available();
777
802
  const store_plugin = instore.find((p) => p.name === plugin.name);
778
803
  if (store_plugin && store_plugin.has_auth) flash_restart(req);
804
+ if (module.exposed_configs?.length > 0)
805
+ flash_relogin(req, module.exposed_configs);
779
806
  getState().processSend({
780
807
  refresh_plugin_cfg: plugin.name,
781
808
  tenant: db.getTenantSchema(),
782
809
  });
783
810
  if (module.layout) await sleep(500); // Allow other workers to reload this plugin
811
+ if (wfres.cleanup) await wfres.cleanup();
784
812
  res.redirect("/plugins");
785
813
  }
786
814
  })
@@ -799,22 +827,223 @@ router.post(
799
827
  const flow = module.configuration_workflow();
800
828
  const step = await flow.singleStepForm(req.body, req);
801
829
  if (step?.renderForm) {
802
- if (!step.renderForm.hasErrors) {
830
+ if (step.renderForm.hasErrors || step.savingErrors)
831
+ res.status(400).send(step.savingErrors || "Error");
832
+ else {
803
833
  plugin.configuration = {
804
834
  ...plugin.configuration,
805
835
  ...step.renderForm.values,
806
836
  };
807
837
  await plugin.upsert();
808
838
  await load_plugins.loadPlugin(plugin);
809
- getState().processSend({
810
- refresh_plugin_cfg: plugin.name,
811
- tenant: db.getTenantSchema(),
812
- });
813
- res.json({ success: "ok" });
814
839
  }
840
+ getState().processSend({
841
+ refresh_plugin_cfg: plugin.name,
842
+ tenant: db.getTenantSchema(),
843
+ });
844
+ res.json({ success: "ok" });
845
+ }
846
+ })
847
+ );
848
+
849
+ router.get(
850
+ "/user_configure/:name",
851
+ loggedIn,
852
+ error_catcher(async (req, res) => {
853
+ const user = await User.findOne({ id: req.user?.id });
854
+ if (!user) {
855
+ req.flash("error", req.__("Not authorized"));
856
+ return res.redirect("/");
857
+ }
858
+ const { name } = req.params;
859
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
860
+ if (!plugin) {
861
+ req.flash("warning", req.__("Module not found"));
862
+ return res.redirect("/auth/settings");
863
+ }
864
+ let module = getState().plugins[plugin.name];
865
+ if (!module) {
866
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
867
+ }
868
+ const form = await module.user_config_form({
869
+ ...(plugin.configuration || {}),
870
+ ...(user._attributes?.layout?.config || {}),
871
+ });
872
+ form.action = `/plugins/user_configure/${encodeURIComponent(plugin.name)}`;
873
+ form.onChange = `applyViewConfig(this, '/plugins/user_saveconfig/${encodeURIComponent(
874
+ name
875
+ )}', null, event);$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
876
+
877
+ form.additionalButtons = [
878
+ {
879
+ label: "Reload page to see changes",
880
+ id: "btnReloadNow",
881
+ class: "btn btn-outline-secondary",
882
+ onclick: "location.reload();",
883
+ },
884
+ ];
885
+ form.submitLabel = req.__("Finish") + " &raquo;";
886
+ res.sendWrap(
887
+ {
888
+ title: req.__(`Configure %s Plugin for %s`, plugin.name, user.email),
889
+ headers: form.additionalHeaders || [],
890
+ },
891
+ {
892
+ above: [
893
+ {
894
+ type: "breadcrumbs",
895
+ crumbs: [
896
+ { text: req.__("Settings"), href: "/settings" },
897
+ { text: req.__("Module store"), href: "/plugins" },
898
+ { text: plugin.name },
899
+ ],
900
+ },
901
+ {
902
+ type: "card",
903
+ class: "mt-0",
904
+ title: req.__(`Configure %s Plugin`, plugin.name),
905
+ titleAjaxIndicator: true,
906
+ contents: renderForm(form, req.csrfToken()),
907
+ },
908
+ ],
909
+ }
910
+ );
911
+ })
912
+ );
913
+
914
+ router.post(
915
+ "/user_configure/:name",
916
+ loggedIn,
917
+ error_catcher(async (req, res) => {
918
+ const user = await User.findOne({ id: req.user?.id });
919
+ if (!user) {
920
+ req.flash("error", req.__("Not authorized"));
921
+ return res.redirect("/");
815
922
  }
923
+ const { name } = req.params;
924
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
925
+ let module = getState().plugins[plugin.name];
926
+ if (!module) {
927
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
928
+ }
929
+ const form = await module.user_config_form({
930
+ ...(plugin.configuration || {}),
931
+ ...(user._attributes?.layout?.config || {}),
932
+ });
933
+ const valResult = form.validate(req.body);
934
+ if (form.hasErrors) {
935
+ req.flash("warning", req.__("An error occurred"));
936
+ return res.sendWrap(
937
+ req.__(`Configure %s Plugin for %s`, plugin.name, user.email),
938
+ renderForm(form, req.csrfToken())
939
+ );
940
+ }
941
+ const values = valResult.success;
942
+ values.is_user_config = true;
943
+ const userAttrs = user._attributes ? { ...user._attributes } : {};
944
+ userAttrs.layout = {
945
+ plugin: plugin.name,
946
+ config: values,
947
+ };
948
+ await user.update({ _attributes: userAttrs });
949
+ getState().userLayouts[req.user.email] = module.layout({
950
+ ...(plugin.configuration ? plugin.configuration : {}),
951
+ ...values,
952
+ });
953
+ const sessionUser = req.session?.passport?.user;
954
+ if (sessionUser) {
955
+ const pluginName = module.plugin_name;
956
+ if (sessionUser.attributes) {
957
+ const oldAttrs = sessionUser.attributes[pluginName] || {};
958
+ sessionUser.attributes[pluginName] = { ...oldAttrs, ...values };
959
+ } else sessionUser.attributes = { [pluginName]: values };
960
+ }
961
+ getState().processSend({
962
+ refresh_plugin_cfg: plugin.name,
963
+ tenant: db.getTenantSchema(),
964
+ });
965
+ if (module.layout) await sleep(500); // Allow other workers to reload this plugin
966
+ res.redirect("/auth/settings");
816
967
  })
817
968
  );
969
+
970
+ router.post(
971
+ "/user_saveconfig/:name",
972
+ loggedIn,
973
+ error_catcher(async (req, res) => {
974
+ const user = await User.findOne({ id: req.user?.id });
975
+ if (!user) return res.status(401).json({ error: req.__("Not authorized") });
976
+ const { name } = req.params;
977
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
978
+ let module = getState().plugins[plugin.name];
979
+ if (!module) {
980
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
981
+ }
982
+ const form = await module.user_config_form({
983
+ ...(plugin.configuration || {}),
984
+ ...(user._attributes?.layout?.config || {}),
985
+ });
986
+ const valResult = form.validate(req.body);
987
+ if (form.hasErrors) {
988
+ return res.status(400).json({ error: req.__("An error occured") });
989
+ }
990
+ const values = valResult.success;
991
+ values.is_user_config = true;
992
+ const userAttrs = user._attributes ? { ...user._attributes } : {};
993
+ userAttrs.layout = {
994
+ plugin: plugin.name,
995
+ config: values,
996
+ };
997
+ await user.update({ _attributes: userAttrs });
998
+ getState().userLayouts[req.user.email] = module.layout(
999
+ userAttrs.layout.config
1000
+ );
1001
+ const sessionUser = req.session?.passport?.user;
1002
+ if (sessionUser) {
1003
+ const pluginName = module.plugin_name;
1004
+ if (sessionUser.attributes) {
1005
+ const oldAttrs = sessionUser.attributes[pluginName] || {};
1006
+ sessionUser.attributes[pluginName] = { ...oldAttrs, ...values };
1007
+ } else sessionUser.attributes = { [pluginName]: values };
1008
+ }
1009
+ getState().processSend({
1010
+ refresh_plugin_cfg: plugin.name,
1011
+ tenant: db.getTenantSchema(),
1012
+ });
1013
+ res.json({ success: "ok" });
1014
+ })
1015
+ );
1016
+
1017
+ router.post(
1018
+ "/remove_user_layout",
1019
+ loggedIn,
1020
+ error_catcher(async (req, res) => {
1021
+ const user = await User.findOne({ id: req.user.id });
1022
+ if (!user) {
1023
+ return res.status(401).json({ error: req.__("Not authorized") });
1024
+ } else if (user._attributes?.layout) {
1025
+ const userAttrs = { ...user._attributes };
1026
+ const plugin = userAttrs.layout.plugin;
1027
+ delete userAttrs.layout;
1028
+ await user.update({ _attributes: userAttrs });
1029
+ getState().userLayouts[req.user.email] = null;
1030
+ let module = getState().plugins[plugin];
1031
+ if (!module) {
1032
+ module = getState().plugins[getState().plugin_module_names[plugin]];
1033
+ }
1034
+ const pluginName = module.plugin_name;
1035
+ const sessionUser = req.session?.passport?.user;
1036
+ if (sessionUser?.attributes[pluginName])
1037
+ sessionUser.attributes[pluginName] = {};
1038
+ getState().processSend({
1039
+ refresh_plugin_cfg: plugin,
1040
+ tenant: db.getTenantSchema(),
1041
+ });
1042
+ }
1043
+ res.json({ success: "ok", reload_page: true });
1044
+ })
1045
+ );
1046
+
818
1047
  /**
819
1048
  * @name get/new
820
1049
  * @function
@@ -866,8 +1095,15 @@ router.get(
866
1095
  const fullpath = path.join(location, "public", safeFile);
867
1096
  if (fs.existsSync(fullpath))
868
1097
  res.sendFile(fullpath, { maxAge: hasVersion ? "100d" : "1d" });
869
- else res.status(404).send(req.__("Not found"));
1098
+ else {
1099
+ getState().log(6, `Plugin serve public: file not found ${fullpath}`);
1100
+ res.status(404).send(req.__("Not found"));
1101
+ }
870
1102
  } else {
1103
+ getState().log(
1104
+ 6,
1105
+ `Plugin serve public: No location for plugin: ${plugin}`
1106
+ );
871
1107
  res.status(404).send(req.__("Not found"));
872
1108
  }
873
1109
  })