@saltcorn/server 1.1.1-beta.0 → 1.1.1-beta.2

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/admin.js CHANGED
@@ -19,6 +19,7 @@ const Plugin = require("@saltcorn/data/models/plugin");
19
19
  const File = require("@saltcorn/data/models/file");
20
20
  const { spawn, exec } = require("child_process");
21
21
  const User = require("@saltcorn/data/models/user");
22
+ const Trigger = require("@saltcorn/data/models/trigger");
22
23
  const path = require("path");
23
24
  const { X509Certificate } = require("crypto");
24
25
  const { getAllTenants } = require("@saltcorn/admin-models/models/tenant");
@@ -146,6 +147,24 @@ const app_files_table = (files, buildDirName, req) =>
146
147
  ],
147
148
  files
148
149
  );
150
+ const intermediate_build_result = (outDirName, buildDir, req) => {
151
+ return div(
152
+ h3("Intermediate build result"),
153
+ div(
154
+ button(
155
+ {
156
+ id: "finishMobileAppBtnId",
157
+ type: "button",
158
+ onClick: `finish_mobile_app(this, '${outDirName}', '${buildDir}');`,
159
+ class: "btn btn-warning",
160
+ },
161
+ i({ class: "fas fa-hammer pe-2" }),
162
+
163
+ req.__("Finish the build")
164
+ )
165
+ )
166
+ );
167
+ };
149
168
 
150
169
  admin_config_route({
151
170
  router,
@@ -1993,9 +2012,6 @@ const buildDialogScript = (capacitorBuilderAvailable, isSbadmin2) =>
1993
2012
  $("#entryPointTypeID").attr("value", type);
1994
2013
  }
1995
2014
 
1996
- function handleMessages() {
1997
- notifyAlert("Building the app, please wait.", true)
1998
- }
1999
2015
  const versionPattern = /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/;
2000
2016
  ${domReady(`
2001
2017
  const versionInput = document.getElementById('appVersionInputId');
@@ -3077,6 +3093,48 @@ router.get(
3077
3093
  )
3078
3094
  )
3079
3095
  )
3096
+ // Share Extension provisioning profile
3097
+ // disabled for now
3098
+ // div(
3099
+ // { class: "row pb-3" },
3100
+ // div(
3101
+ // { class: "col-sm-8" },
3102
+ // label(
3103
+ // {
3104
+ // for: "shareProvisioningProfileInputId",
3105
+ // class: "form-label fw-bold",
3106
+ // },
3107
+ // req.__("Share Extension Provisioning Profile"),
3108
+ // a(
3109
+ // {
3110
+ // href: "javascript:ajax_modal('/admin/help/Provisioning Profile?')",
3111
+ // },
3112
+ // i({ class: "fas fa-question-circle ps-1" })
3113
+ // )
3114
+ // ),
3115
+ // select(
3116
+ // {
3117
+ // class: "form-select",
3118
+ // name: "shareProvisioningProfile",
3119
+ // id: "shareProvisioningProfileInputId",
3120
+ // },
3121
+ // [
3122
+ // option({ value: "" }, ""),
3123
+ // ...provisioningFiles.map((file) =>
3124
+ // option(
3125
+ // {
3126
+ // value: file.location,
3127
+ // selected:
3128
+ // builderSettings.shareProvisioningProfile ===
3129
+ // file.location,
3130
+ // },
3131
+ // file.filename
3132
+ // )
3133
+ // ),
3134
+ // ].join("")
3135
+ // )
3136
+ // )
3137
+ // )
3080
3138
  )
3081
3139
  )
3082
3140
  ),
@@ -3100,15 +3158,13 @@ router.get(
3100
3158
  })
3101
3159
  );
3102
3160
 
3103
- const checkFiles = async (outDir, fileNames) => {
3161
+ const checkFiles = async (outDirName, fileNames) => {
3104
3162
  const rootFolder = await File.rootFolder();
3105
- const mobile_app_dir = path.join(rootFolder.location, "mobile_app", outDir);
3163
+ const outDir = path.join(rootFolder.location, "mobile_app", outDirName);
3106
3164
  const unsafeFiles = await Promise.all(
3107
3165
  fs
3108
- .readdirSync(mobile_app_dir)
3109
- .map(
3110
- async (outFile) => await File.from_file_on_disk(outFile, mobile_app_dir)
3111
- )
3166
+ .readdirSync(outDir)
3167
+ .map(async (outFile) => await File.from_file_on_disk(outFile, outDir))
3112
3168
  );
3113
3169
  const entries = unsafeFiles
3114
3170
  .filter(
@@ -3127,9 +3183,18 @@ router.get(
3127
3183
  "/build-mobile-app/finished",
3128
3184
  isAdmin,
3129
3185
  error_catcher(async (req, res) => {
3130
- const { build_dir } = req.query;
3186
+ const { out_dir_name, mode } = req.query;
3187
+ const stepDesc =
3188
+ mode === "prepare"
3189
+ ? "_prepare_step"
3190
+ : mode === "finish"
3191
+ ? "_finish_step"
3192
+ : "";
3131
3193
  res.json({
3132
- finished: await checkFiles(build_dir, ["logs.txt", "error_logs.txt"]),
3194
+ finished: await checkFiles(out_dir_name, [
3195
+ `logs${stepDesc}.txt`,
3196
+ `error_logs${stepDesc}.txt`,
3197
+ ]),
3133
3198
  });
3134
3199
  })
3135
3200
  );
@@ -3164,8 +3229,8 @@ router.get(
3164
3229
  "/build-mobile-app/result",
3165
3230
  isAdmin,
3166
3231
  error_catcher(async (req, res) => {
3167
- const { build_dir_name } = req.query;
3168
- if (!validateBuildDirName(build_dir_name)) {
3232
+ const { out_dir_name, build_dir, mode } = req.query;
3233
+ if (!validateBuildDirName(out_dir_name)) {
3169
3234
  return res.sendWrap(req.__(`Admin`), {
3170
3235
  above: [
3171
3236
  {
@@ -3177,11 +3242,7 @@ router.get(
3177
3242
  });
3178
3243
  }
3179
3244
  const rootFolder = await File.rootFolder();
3180
- const buildDir = path.join(
3181
- rootFolder.location,
3182
- "mobile_app",
3183
- build_dir_name
3184
- );
3245
+ const buildDir = path.join(rootFolder.location, "mobile_app", out_dir_name);
3185
3246
  if (!validateBuildDir(buildDir, rootFolder.location)) {
3186
3247
  return res.sendWrap(req.__(`Admin`), {
3187
3248
  above: [
@@ -3199,7 +3260,15 @@ router.get(
3199
3260
  .readdirSync(buildDir)
3200
3261
  .map(async (outFile) => await File.from_file_on_disk(outFile, buildDir))
3201
3262
  );
3202
- const resultMsg = files.find((file) => file.filename === "logs.txt")
3263
+ const stepDesc =
3264
+ mode === "prepare"
3265
+ ? "_prepare_step"
3266
+ : mode === "finish"
3267
+ ? "_finish_step"
3268
+ : "";
3269
+ const resultMsg = files.find(
3270
+ (file) => file.filename === `logs${stepDesc}.txt`
3271
+ )
3203
3272
  ? req.__("The build was successfully")
3204
3273
  : req.__("Unable to build the app");
3205
3274
  res.sendWrap(req.__(`Admin`), {
@@ -3209,11 +3278,98 @@ router.get(
3209
3278
  title: req.__("Build Result"),
3210
3279
  contents: div(resultMsg),
3211
3280
  },
3212
- files.length > 0 ? app_files_table(files, build_dir_name, req) : "",
3281
+ files.length > 0 ? app_files_table(files, out_dir_name, req) : "",
3282
+ mode === "prepare"
3283
+ ? intermediate_build_result(out_dir_name, build_dir, req)
3284
+ : "",
3213
3285
  ],
3214
3286
  });
3215
3287
  })
3216
3288
  );
3289
+
3290
+ router.post(
3291
+ "/build-mobile-app/finish",
3292
+ isAdmin,
3293
+ error_catcher(async (req, res) => {
3294
+ const { out_dir_name, build_dir } = req.body;
3295
+ const content = await fs.promises.readFile(
3296
+ path.join(build_dir, "spawnParams.json")
3297
+ );
3298
+ const spawnParams = JSON.parse(content);
3299
+ const rootFolder = await File.rootFolder();
3300
+ const outDirFullPath = path.join(
3301
+ rootFolder.location,
3302
+ "mobile_app",
3303
+ out_dir_name
3304
+ );
3305
+ res.json({
3306
+ success: true,
3307
+ });
3308
+ const child = spawn(
3309
+ getSafeSaltcornCmd(),
3310
+ [...spawnParams, "-m", "finish"],
3311
+ {
3312
+ stdio: ["ignore", "pipe", "pipe"],
3313
+ cwd: ".",
3314
+ }
3315
+ );
3316
+ const childOutputs = [];
3317
+ child.stdout.on("data", (data) => {
3318
+ const outMsg = data.toString();
3319
+ getState().log(5, outMsg);
3320
+ if (data) childOutputs.push(outMsg);
3321
+ });
3322
+ child.stderr.on("data", (data) => {
3323
+ const errMsg = data ? data.toString() : req.__("An error occurred");
3324
+ getState().log(5, errMsg);
3325
+ childOutputs.push(errMsg);
3326
+ });
3327
+ child.on("exit", async (exitCode, signal) => {
3328
+ const logFile =
3329
+ exitCode === 0 ? "logs_finish_step.txt" : "error_logs_finish_step.txt";
3330
+ try {
3331
+ const exitMsg = childOutputs.join("\n");
3332
+ await fs.promises.writeFile(
3333
+ path.join(outDirFullPath, logFile),
3334
+ exitMsg
3335
+ );
3336
+ await File.set_xattr_of_existing_file(
3337
+ logFile,
3338
+ outDirFullPath,
3339
+ req.user
3340
+ );
3341
+ } catch (error) {
3342
+ console.log(`unable to write '${logFile}' to '${outDirFullPath}'`);
3343
+ console.log(error);
3344
+ }
3345
+ });
3346
+ child.on("error", (msg) => {
3347
+ const message = msg.message ? msg.message : msg.code;
3348
+ const stack = msg.stack ? msg.stack : "";
3349
+ const logFile = "error_logs.txt";
3350
+ const errMsg = [message, stack].join("\n");
3351
+ getState().log(5, msg);
3352
+ fs.writeFile(
3353
+ path.join(outDirFullPath, "error_logs.txt"),
3354
+ errMsg,
3355
+ async (error) => {
3356
+ if (error) {
3357
+ console.log(`unable to write logFile to '${outDirFullPath}'`);
3358
+ console.log(error);
3359
+ } else {
3360
+ // no transaction, '/build-mobile-app/finished' filters for valid attributes
3361
+ await File.set_xattr_of_existing_file(
3362
+ logFile,
3363
+ outDirFullPath,
3364
+ req.user
3365
+ );
3366
+ }
3367
+ }
3368
+ );
3369
+ });
3370
+ })
3371
+ );
3372
+
3217
3373
  /**
3218
3374
  * Do Build Mobile App
3219
3375
  */
@@ -3222,6 +3378,8 @@ router.post(
3222
3378
  isAdmin,
3223
3379
  error_catcher(async (req, res) => {
3224
3380
  getState().log(2, `starting mobile build: ${JSON.stringify(req.body)}`);
3381
+ const msgs = [];
3382
+ let mode = "full";
3225
3383
  let {
3226
3384
  entryPoint,
3227
3385
  entryPointType,
@@ -3239,11 +3397,27 @@ router.post(
3239
3397
  synchedTables,
3240
3398
  includedPlugins,
3241
3399
  provisioningProfile,
3400
+ shareProvisioningProfile,
3242
3401
  buildType,
3243
3402
  keystoreFile,
3244
3403
  keystoreAlias,
3245
3404
  keystorePassword,
3246
3405
  } = req.body;
3406
+ // const receiveShareTriggers = Trigger.find({
3407
+ // when_trigger: "ReceiveMobileShareData",
3408
+ // });
3409
+ // disabeling share to support for now
3410
+ let allowShareTo = false; // receiveShareTriggers.length > 0;
3411
+ if (allowShareTo && iOSPlatform && !shareProvisioningProfile) {
3412
+ allowShareTo = false;
3413
+ msgs.push({
3414
+ type: "warning",
3415
+ text: req.__(
3416
+ "A ReceiveMobileShareData trigger exists, but no Share Extension Provisioning Profile is provided. " +
3417
+ "Building without share to support."
3418
+ ),
3419
+ });
3420
+ }
3247
3421
  if (!includedPlugins) includedPlugins = [];
3248
3422
  if (!synchedTables) synchedTables = [];
3249
3423
  if (!entryPoint) {
@@ -3279,14 +3453,26 @@ router.post(
3279
3453
  ),
3280
3454
  });
3281
3455
  }
3282
- if (iOSPlatform && !provisioningProfile) {
3283
- return res.json({
3284
- error: req.__(
3285
- "Please provide a Provisioning Profile for the iOS build."
3286
- ),
3456
+ if (iOSPlatform) {
3457
+ if (!provisioningProfile)
3458
+ return res.json({
3459
+ error: req.__(
3460
+ "Please provide a Provisioning Profile for the iOS build."
3461
+ ),
3462
+ });
3463
+ }
3464
+ if (buildType === "debug" && keystoreFile) {
3465
+ msgs.push({
3466
+ type: "warning",
3467
+ text: req.__("Keystore file is not applied for debug builds."),
3287
3468
  });
3288
3469
  }
3289
- if (keystoreFile && (!keystoreAlias || !keystorePassword)) {
3470
+
3471
+ if (
3472
+ buildType === "release" &&
3473
+ keystoreFile &&
3474
+ (!keystoreAlias || !keystorePassword)
3475
+ ) {
3290
3476
  return res.json({
3291
3477
  error: req.__(
3292
3478
  "Please provide the keystore alias and password for the android build."
@@ -3294,8 +3480,9 @@ router.post(
3294
3480
  });
3295
3481
  }
3296
3482
  const outDirName = `build_${new Date().valueOf()}`;
3483
+ const buildDir = `${os.userInfo().homedir}/mobile_app_build`;
3297
3484
  const rootFolder = await File.rootFolder();
3298
- const buildDir = path.join(rootFolder.location, "mobile_app", outDirName);
3485
+ const outDir = path.join(rootFolder.location, "mobile_app", outDirName);
3299
3486
  await File.new_folder(outDirName, "/mobile_app");
3300
3487
  const spawnParams = [
3301
3488
  "build-app",
@@ -3304,9 +3491,9 @@ router.post(
3304
3491
  "-t",
3305
3492
  entryPointType === "pagegroup" ? "page" : entryPointType,
3306
3493
  "-c",
3307
- buildDir,
3494
+ outDir,
3308
3495
  "-b",
3309
- `${os.userInfo().homedir}/mobile_app_build`,
3496
+ buildDir,
3310
3497
  "-u",
3311
3498
  req.user.email, // ensured by isAdmin
3312
3499
  ];
@@ -3319,6 +3506,13 @@ router.post(
3319
3506
  "--provisioningProfile",
3320
3507
  provisioningProfile
3321
3508
  );
3509
+ if (allowShareTo) {
3510
+ mode = "prepare";
3511
+ spawnParams.push(
3512
+ "--shareExtensionProvisioningProfile",
3513
+ shareProvisioningProfile
3514
+ );
3515
+ }
3322
3516
  }
3323
3517
  if (appName) spawnParams.push("--appName", appName);
3324
3518
  if (appId) spawnParams.push("--appId", appId);
@@ -3327,6 +3521,7 @@ router.post(
3327
3521
  if (serverURL) spawnParams.push("-s", serverURL);
3328
3522
  if (splashPage) spawnParams.push("--splashPage", splashPage);
3329
3523
  if (allowOfflineMode) spawnParams.push("--allowOfflineMode");
3524
+ if (allowShareTo) spawnParams.push("--allowShareTo");
3330
3525
  if (autoPublicLogin) spawnParams.push("--autoPublicLogin");
3331
3526
  if (synchedTables.length > 0)
3332
3527
  spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
@@ -3348,10 +3543,15 @@ router.post(
3348
3543
  spawnParams.push("--androidKeyStoreAlias", keystoreAlias);
3349
3544
  if (keystorePassword)
3350
3545
  spawnParams.push("--androidKeystorePassword", keystorePassword);
3351
- // end http call, return the out directory name
3546
+ // end http call, return the out directory name, the build directory path and the mode
3352
3547
  // the gui polls for results
3353
- res.json({ build_dir_name: outDirName });
3354
- const child = spawn(getSafeSaltcornCmd(), spawnParams, {
3548
+ res.json({
3549
+ out_dir_name: outDirName,
3550
+ build_dir: buildDir,
3551
+ mode: mode,
3552
+ msgs,
3553
+ });
3554
+ const child = spawn(getSafeSaltcornCmd(), [...spawnParams, "-m", mode], {
3355
3555
  stdio: ["ignore", "pipe", "pipe"],
3356
3556
  cwd: ".",
3357
3557
  });
@@ -3366,18 +3566,29 @@ router.post(
3366
3566
  getState().log(5, errMsg);
3367
3567
  childOutputs.push(errMsg);
3368
3568
  });
3369
- child.on("exit", (exitCode, signal) => {
3370
- const logFile = exitCode === 0 ? "logs.txt" : "error_logs.txt";
3371
- const exitMsg = childOutputs.join("\n");
3372
- fs.writeFile(path.join(buildDir, logFile), exitMsg, async (error) => {
3373
- if (error) {
3374
- console.log(`unable to write '${logFile}' to '${buildDir}'`);
3569
+ child.on("exit", async (exitCode, signal) => {
3570
+ if (mode === "prepare" && exitCode === 0) {
3571
+ try {
3572
+ fs.promises.writeFile(
3573
+ path.join(buildDir, "spawnParams.json"),
3574
+ JSON.stringify(spawnParams)
3575
+ );
3576
+ } catch (error) {
3577
+ console.log(`unable to write spawnParams to '${buildDir}'`);
3375
3578
  console.log(error);
3376
- } else {
3377
- // no transaction, '/build-mobile-app/finished' filters for valid attributes
3378
- await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3379
3579
  }
3380
- });
3580
+ }
3581
+ const stepDesc = mode === "prepare" ? "_prepare_step" : "";
3582
+ const logFile =
3583
+ exitCode === 0 ? `logs${stepDesc}.txt` : `error_logs${stepDesc}.txt`;
3584
+ try {
3585
+ const exitMsg = childOutputs.join("\n");
3586
+ await fs.promises.writeFile(path.join(outDir, logFile), exitMsg);
3587
+ await File.set_xattr_of_existing_file(logFile, outDir, req.user);
3588
+ } catch (error) {
3589
+ console.log(`unable to write '${logFile}' to '${outDir}'`);
3590
+ console.log(error);
3591
+ }
3381
3592
  });
3382
3593
  child.on("error", (msg) => {
3383
3594
  const message = msg.message ? msg.message : msg.code;
@@ -3386,15 +3597,15 @@ router.post(
3386
3597
  const errMsg = [message, stack].join("\n");
3387
3598
  getState().log(5, msg);
3388
3599
  fs.writeFile(
3389
- path.join(buildDir, "error_logs.txt"),
3600
+ path.join(outDir, "error_logs.txt"),
3390
3601
  errMsg,
3391
3602
  async (error) => {
3392
3603
  if (error) {
3393
- console.log(`unable to write logFile to '${buildDir}'`);
3604
+ console.log(`unable to write logFile to '${outDir}'`);
3394
3605
  console.log(error);
3395
3606
  } else {
3396
3607
  // no transaction, '/build-mobile-app/finished' filters for valid attributes
3397
- await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3608
+ await File.set_xattr_of_existing_file(logFile, outDir, req.user);
3398
3609
  }
3399
3610
  }
3400
3611
  );
@@ -3462,7 +3673,7 @@ router.post(
3462
3673
  .filter(
3463
3674
  (plugin) =>
3464
3675
  ["base", "sbadmin2"].indexOf(plugin.name) < 0 &&
3465
- newCfg.includedPlugins.indexOf(plugin.name) < 0
3676
+ (newCfg.includedPlugins || []).indexOf(plugin.name) < 0
3466
3677
  )
3467
3678
  .map((plugin) => plugin.name);
3468
3679
  newCfg.excludedPlugins = excludedPlugins;
package/routes/fields.js CHANGED
@@ -12,6 +12,7 @@ const { getState } = require("@saltcorn/data/db/state");
12
12
  const { renderForm } = require("@saltcorn/markup");
13
13
  const Field = require("@saltcorn/data/models/field");
14
14
  const Table = require("@saltcorn/data/models/table");
15
+ const Trigger = require("@saltcorn/data/models/trigger");
15
16
  const Form = require("@saltcorn/data/models/form");
16
17
  const Workflow = require("@saltcorn/data/models/workflow");
17
18
  const User = require("@saltcorn/data/models/user");
@@ -306,6 +307,10 @@ const fieldFlow = (req) =>
306
307
  }
307
308
 
308
309
  await field.update(fldRow);
310
+ Trigger.emitEvent("AppChange", `Field ${fldRow.name}`, req.user, {
311
+ entity_type: "Field",
312
+ entity_name: fldRow.name || fldRow.label,
313
+ });
309
314
  } catch (e) {
310
315
  return {
311
316
  redirect: `/table/${context.table_id}`,
@@ -315,6 +320,10 @@ const fieldFlow = (req) =>
315
320
  } else {
316
321
  try {
317
322
  await Field.create(fldRow);
323
+ Trigger.emitEvent("AppChange", `Field ${fldRow.name}`, req.user, {
324
+ entity_type: "Field",
325
+ entity_name: fldRow.name || fldRow.label,
326
+ });
318
327
  } catch (e) {
319
328
  return {
320
329
  redirect: `/table/${context.table_id}`,
package/routes/menu.js CHANGED
@@ -571,6 +571,7 @@ router.post(
571
571
  const new_menu = req.body;
572
572
  const menu_items = jQMEtoMenu(new_menu);
573
573
  await save_menu_items(menu_items);
574
+ Trigger.emitEvent("AppChange", `Menu`, req.user, {});
574
575
 
575
576
  res.json({ success: true });
576
577
  })
@@ -199,23 +199,31 @@ router.post(
199
199
  error_catcher(async (req, res) => {
200
200
  const role = req.user?.role_id || 100;
201
201
  if (role === 100) {
202
- req.flash("error", req.__("You must be logged in to share"));
203
- res.redirect("/auth/login");
202
+ const msg = req.__("You must be logged in to share");
203
+ if (!req.smr) {
204
+ req.flash("error", msg);
205
+ res.redirect("/auth/login");
206
+ } else res.json({ error: msg });
204
207
  } else if (!getState().getConfig("pwa_share_to_enabled", false)) {
205
- req.flash("error", req.__("Sharing not enabled"));
206
- res.redirect("/");
208
+ const msg = req.__("Sharing not enabled");
209
+ if (!req.smr) {
210
+ req.flash("error", msg);
211
+ res.redirect("/");
212
+ } else res.json({ error: msg });
207
213
  } else {
208
214
  Trigger.emitEvent("ReceiveMobileShareData", null, req.user, {
209
215
  row: req.body,
210
216
  });
211
- req.flash(
212
- "success",
213
- req.__(
214
- "Shared: %s",
215
- req.body.title || req.body.text || req.body.url || ""
216
- )
217
- );
218
- res.status(303).redirect("/");
217
+ if (!req.smr) {
218
+ req.flash(
219
+ "success",
220
+ req.__(
221
+ "Shared: %s",
222
+ req.body.title || req.body.text || req.body.url || ""
223
+ )
224
+ );
225
+ res.status(303).redirect("/");
226
+ } else res.json({ success: "ok" });
219
227
  }
220
228
  })
221
229
  );
package/routes/page.js CHANGED
@@ -68,13 +68,13 @@ const runPage = async (page, req, res, tic) => {
68
68
  no_menu: page.attributes?.no_menu,
69
69
  requestFluidLayout: page.attributes?.request_fluid_layout,
70
70
  } || `${page.name} page`,
71
- add_edit_bar({
71
+ req.smr ? contents : add_edit_bar({
72
72
  role,
73
73
  title: page.name,
74
74
  what: req.__("Page"),
75
75
  url: `/pageedit/edit/${encodeURIComponent(page.name)}`,
76
76
  contents,
77
- })
77
+ }),
78
78
  );
79
79
  } else {
80
80
  getState().log(2, `Page ${page.name} not authorized`);
@@ -146,7 +146,9 @@ const pagePropertiesForm = async (req, isNew) => {
146
146
  {
147
147
  name: "request_fluid_layout",
148
148
  label: req.__("Fluid layout"),
149
- sublabel: req.__("Request fluid layout from theme for a wider display for this page"),
149
+ sublabel: req.__(
150
+ "Request fluid layout from theme for a wider display for this page"
151
+ ),
150
152
  type: "Bool",
151
153
  },
152
154
  ],
@@ -503,12 +505,21 @@ router.post(
503
505
  pageRow.layout = {};
504
506
  }
505
507
  await Page.update(+id, pageRow);
508
+ Trigger.emitEvent("AppChange", `Page ${dbPage.name}`, req.user, {
509
+ entity_type: "Page",
510
+ entity_name: dbPage.name,
511
+ });
506
512
  if (req.xhr) res.json({ success: "ok" });
507
513
  else res.redirect(`/pageedit/`);
508
514
  } else {
509
515
  if (!pageRow.layout) pageRow.layout = {};
510
516
  if (!pageRow.fixed_states) pageRow.fixed_states = {};
511
517
  await Page.create(pageRow);
518
+ Trigger.emitEvent("AppChange", `Page ${pageRow.name}`, req.user, {
519
+ entity_type: "Page",
520
+ entity_name: pageRow.name,
521
+ });
522
+
512
523
  if (!html_file)
513
524
  res.redirect(
514
525
  addOnDoneRedirect(`/pageedit/edit/${pageRow.name}`, req)
@@ -679,6 +690,10 @@ router.post(
679
690
  await Page.update(page.id, {
680
691
  layout: decodeURIComponent(req.body.layout),
681
692
  });
693
+ Trigger.emitEvent("AppChange", `Page ${page.name}`, req.user, {
694
+ entity_type: "Page",
695
+ entity_name: page.name,
696
+ });
682
697
  req.flash("success", req.__(`Page %s saved`, pagename));
683
698
  res.redirect(redirectTarget);
684
699
  } else if (req.body.code) {
@@ -687,6 +702,10 @@ router.post(
687
702
  const file = await File.findOne(page.html_file);
688
703
  if (!file) throw new Error(req.__("File not found"));
689
704
  await fsp.writeFile(file.location, req.body.code);
705
+ Trigger.emitEvent("AppChange", `Page ${page.name}`, req.user, {
706
+ entity_type: "Page",
707
+ entity_name: page.name,
708
+ });
690
709
  if (!req.xhr) {
691
710
  req.flash("success", req.__(`Page %s saved`, pagename));
692
711
  res.redirect(redirectTarget);
@@ -723,6 +742,11 @@ router.post(
723
742
 
724
743
  if (id && req.body.layout) {
725
744
  await Page.update(+id, { layout: req.body.layout });
745
+ const page = await Page.findOne({ id });
746
+ Trigger.emitEvent("AppChange", `Page ${page.name}`, req.user, {
747
+ entity_type: "Page",
748
+ entity_name: page.name,
749
+ });
726
750
  res.json({
727
751
  success: "ok",
728
752
  });
@@ -744,6 +768,10 @@ router.post(
744
768
  error_catcher(async (req, res) => {
745
769
  const { id } = req.params;
746
770
  const page = await Page.findOne({ id });
771
+ Trigger.emitEvent("AppChange", `Page ${page.name}`, req.user, {
772
+ entity_type: "Page",
773
+ entity_name: page.name,
774
+ });
747
775
  await page.delete();
748
776
  req.flash("success", req.__(`Page deleted`));
749
777
  res.redirect(`/pageedit`);
@@ -796,6 +824,7 @@ router.post(
796
824
  min_role: page.min_role,
797
825
  pagename: page.name,
798
826
  });
827
+ Trigger.emitEvent("AppChange", `Menu`, req.user, {});
799
828
  req.flash(
800
829
  "success",
801
830
  req.__(
@@ -820,6 +849,10 @@ router.post(
820
849
  const { id } = req.params;
821
850
  const page = await Page.findOne({ id });
822
851
  const newpage = await page.clone();
852
+ Trigger.emitEvent("AppChange", `Page ${newpage.name}`, req.user, {
853
+ entity_type: "Page",
854
+ entity_name: newpage.name,
855
+ });
823
856
  req.flash(
824
857
  "success",
825
858
  req.__("Page %s duplicated as %s", page.name, newpage.name)