@saltcorn/server 1.1.1-beta.2 → 1.1.1-beta.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/CHANGELOG.md CHANGED
@@ -1,8 +1,86 @@
1
- # Change Log
1
+ # Notable changes
2
2
 
3
- All notable changes to this project will be documented in this file.
4
- See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
3
+ ## 1.1.1 - In beta
5
4
 
6
- ## [0.4.5](https://github.com/saltcorn/saltcorn/compare/v0.4.5-beta.1...v0.4.5) (2021-05-07)
5
+ * Stored calculated fields that contain joinfields in the expression are now automatically
6
+ updated when the values they reference are changed, i.e. changes occur in the tables they
7
+ reference. This is limited to single (expression contains x.y) and double joinfields
8
+ (expression contains x.y.z). In most cases, you can now remove all recalculate_stored_fields
9
+ actions.
7
10
 
8
- **Note:** Version bump only for package @saltcorn/server
11
+ * Builder:
12
+ - Add ability to set custom `id` on containers. This is useful for scroll targets
13
+ - Add animations tab to containers. All animations are activated on scroll
14
+ - Tabs and multi-step actions implement a new interface that lets you move, delete and
15
+ add steps/tabs.
16
+
17
+ * Workflows:
18
+ - APIResponse step type. Provide the API response
19
+ - Stop step type. Stop workflow immediately
20
+ - EditViewForm step type: run a form from an Edit view, add respnse to context
21
+ - Call other workflows in a workflow step. Control subcontext for called workflow
22
+ - Error handling. SetErrorHandler step type, which set the step invoked on errors
23
+ - ForLoop step type for loops over arrays.
24
+ - Varius UX improvements for editing workflows
25
+ - Integrate copilot, if installed, in workflow editing
26
+
27
+ * sbadmin2 theme - Color update: dark side bar, darker primary blue
28
+
29
+ * AppChange event, runs every time a view, table, trigger, page or configuration value
30
+ is changed.
31
+
32
+ * Mobile builder:
33
+ - PJAX view loading.
34
+
35
+ ### Fixes
36
+
37
+ * fix query string build on check_state_field (#2948). Author: St0rml
38
+
39
+ ### Translations
40
+
41
+ * Update Polish translation. Author: skaskiewicz
42
+
43
+ ## 1.1.0 - Released 19 December 2024
44
+
45
+ * Workflows: a new type of trigger composed of steps, with durable execution and
46
+ a context for sharing information between steps. Workflows can include user interaction
47
+ including asking for user input in specified form fields.
48
+
49
+ * Workflow rooms: a new view for chatbot-style interactions with workflows
50
+
51
+ * HTTPS proxy: set an HTTPS proxy with the HTTPS_PROXY environment variable.
52
+
53
+ * Edit view: option to allocate new table row when running with a specified row. This is
54
+ useful when the Edit row includes embedded views based on relations. The allocated row can be
55
+ deleted if there are no changes.
56
+
57
+ * Acquire Let's Encrypt certificates for tenants. If Let's Encrypt is enabled in the root tenant,
58
+ newly created tenants will acquire a certificate. Certificate for existing tenants can be acquired
59
+ in that the settings for that tenant in the root tenant's list of tenants.
60
+
61
+ * Icon plugins. Plugins can now supply additional icons which can be chosen in the builder and
62
+ menu editor
63
+
64
+ * Registry editor: Edit configuration values
65
+
66
+ * Webhook action has more options: method, set reponse value, headers.
67
+
68
+ ### Security
69
+
70
+ - SameSite cookie settings
71
+ - Options to enable Content Security Policy and CORS
72
+ - Warn when loading embedded with without role. Strict enforcement in 1.1.2.
73
+ - Check table permissions when filling select dropdown options
74
+
75
+ ### Fixes
76
+
77
+ * Edit destination formulae are evaluated against the whole row, not only saved form fields
78
+ * Set user groups when admin becomes user
79
+
80
+ ### Translations
81
+
82
+ * Update Polish translation. Author: skaskiewicz
83
+
84
+ ## 1.0.0 - Released 15 November 2024
85
+
86
+ Change tracking from this point.
package/auth/testhelp.js CHANGED
@@ -383,7 +383,13 @@ const load_url_dom = async (url) => {
383
383
  .join("\n");
384
384
  }
385
385
  }
386
+ class FakeIntersectionObserver {
387
+ constructor() {}
388
+ observe() {}
389
+ }
390
+
386
391
  dom.window.XMLHttpRequest = FakeXHR;
392
+ dom.window.IntersectionObserver = FakeIntersectionObserver;
387
393
  await new Promise(function (resolve, reject) {
388
394
  dom.window.addEventListener("DOMContentLoaded", (event) => {
389
395
  resolve();
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "1.1.1-beta.2",
3
+ "version": "1.1.1-beta.3",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "1.1.1-beta.2",
11
- "@saltcorn/builder": "1.1.1-beta.2",
12
- "@saltcorn/data": "1.1.1-beta.2",
13
- "@saltcorn/admin-models": "1.1.1-beta.2",
14
- "@saltcorn/filemanager": "1.1.1-beta.2",
15
- "@saltcorn/markup": "1.1.1-beta.2",
16
- "@saltcorn/plugins-loader": "1.1.1-beta.2",
17
- "@saltcorn/sbadmin2": "1.1.1-beta.2",
10
+ "@saltcorn/base-plugin": "1.1.1-beta.3",
11
+ "@saltcorn/builder": "1.1.1-beta.3",
12
+ "@saltcorn/data": "1.1.1-beta.3",
13
+ "@saltcorn/admin-models": "1.1.1-beta.3",
14
+ "@saltcorn/filemanager": "1.1.1-beta.3",
15
+ "@saltcorn/markup": "1.1.1-beta.3",
16
+ "@saltcorn/plugins-loader": "1.1.1-beta.3",
17
+ "@saltcorn/sbadmin2": "1.1.1-beta.3",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -2024,3 +2024,33 @@ function update_time_of_week(nm) {
2024
2024
  $(`#inputh${nm}`).val(s).trigger("change");
2025
2025
  };
2026
2026
  }
2027
+
2028
+ const observer = new IntersectionObserver(
2029
+ (entries, observer) => {
2030
+ entries.forEach((entry) => {
2031
+ if (entry.isIntersecting) {
2032
+ const delay = entry.target.getAttribute("data-animate-delay"); // delay is optional
2033
+ const duration = entry.target.getAttribute("data-animate-duration"); // delay is optional
2034
+ const animationClass = entry.target.getAttribute("data-animate");
2035
+ if (animationClass) {
2036
+ if (delay) entry.target.style.animationDelay = delay + "s";
2037
+ if (duration) entry.target.style.animationDuration = duration + "s";
2038
+ entry.target.style.animationName = animationClass;
2039
+ entry.target.style.animationFillMode = "both";
2040
+ }
2041
+
2042
+ if (entry.target.getAttribute("data-animate-initial-hide") === "")
2043
+ entry.target.removeAttribute("data-animate-initial-hide");
2044
+
2045
+ observer.unobserve(entry.target);
2046
+ }
2047
+ });
2048
+ },
2049
+ {
2050
+ threshold: 0.2,
2051
+ }
2052
+ );
2053
+
2054
+ document.querySelectorAll("[data-animate]").forEach((element) => {
2055
+ observer.observe(element);
2056
+ });
@@ -632,3 +632,144 @@ i[class*=" unicode-"] {
632
632
  .mobile-toast-margin {
633
633
  margin-bottom: 1rem;
634
634
  }
635
+
636
+ @keyframes fadeIn {
637
+ from {
638
+ opacity: 0;
639
+ }
640
+ to {
641
+ opacity: 1;
642
+ }
643
+ }
644
+
645
+ @keyframes fadeInLeft {
646
+ from {
647
+ opacity: 0;
648
+ transform: translate3d(-100%, 0, 0);
649
+ }
650
+ to {
651
+ opacity: 1;
652
+ transform: translate3d(0, 0, 0);
653
+ }
654
+ }
655
+
656
+ @keyframes fadeInDown {
657
+ from {
658
+ opacity: 0;
659
+ transform: translate3d(0, -100%, 0);
660
+ }
661
+ to {
662
+ opacity: 1;
663
+ transform: translate3d(0, 0, 0);
664
+ }
665
+ }
666
+
667
+ @keyframes fadeInRight {
668
+ from {
669
+ opacity: 0;
670
+ transform: translate3d(100%, 0, 0);
671
+ }
672
+ to {
673
+ opacity: 1;
674
+ transform: translate3d(0, 0, 0);
675
+ }
676
+ }
677
+
678
+ @keyframes fadeInUp {
679
+ from {
680
+ opacity: 0;
681
+ transform: translate3d(0, 100%, 0);
682
+ }
683
+ to {
684
+ opacity: 1;
685
+ transform: translate3d(0, 0, 0);
686
+ }
687
+ }
688
+
689
+ @keyframes rollIn {
690
+ from {
691
+ opacity: 0;
692
+ transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);
693
+ }
694
+ to {
695
+ opacity: 1;
696
+ transform: translate3d(0, 0, 0);
697
+ }
698
+ }
699
+
700
+ @keyframes zoomIn {
701
+ from {
702
+ opacity: 0;
703
+ transform: scale3d(0.3, 0.3, 0.3);
704
+ }
705
+ 50% {
706
+ opacity: 1;
707
+ }
708
+ }
709
+
710
+ @keyframes zoomInUp {
711
+ from {
712
+ opacity: 0;
713
+ transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);
714
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
715
+ }
716
+ 60% {
717
+ opacity: 1;
718
+ transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);
719
+ animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
720
+ }
721
+ }
722
+
723
+ @keyframes bounce {
724
+ from,
725
+ 20%,
726
+ 53%,
727
+ to {
728
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
729
+ transform: translate3d(0, 0, 0);
730
+ }
731
+ 40%,
732
+ 43% {
733
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
734
+ transform: translate3d(0, -30px, 0) scaleY(1.1);
735
+ }
736
+ 70% {
737
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
738
+ transform: translate3d(0, -15px, 0) scaleY(1.05);
739
+ }
740
+ 80% {
741
+ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
742
+ transform: translate3d(0, 0, 0) scaleY(0.95);
743
+ }
744
+ 90% {
745
+ transform: translate3d(0, -4px, 0) scaleY(1.02);
746
+ }
747
+ }
748
+
749
+ @keyframes tada {
750
+ from {
751
+ transform: scale3d(1, 1, 1);
752
+ }
753
+ 10%,
754
+ 20% {
755
+ transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
756
+ }
757
+ 30%,
758
+ 50%,
759
+ 70%,
760
+ 90% {
761
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
762
+ }
763
+ 40%,
764
+ 60%,
765
+ 80% {
766
+ transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
767
+ }
768
+ to {
769
+ transform: scale3d(1, 1, 1);
770
+ }
771
+ }
772
+
773
+ [data-animate-initial-hide] {
774
+ opacity: 0;
775
+ }
package/routes/actions.js CHANGED
@@ -508,14 +508,19 @@ function genWorkflowDiagram(steps) {
508
508
  const stepNames = steps.map((s) => s.name);
509
509
  const nodeLines = steps.map(
510
510
  (s) => ` ${s.mmname}["\`**${s.name}**
511
- ${s.action_name}\`"]:::wfstep${s.id}`
511
+ ${s.action_name}\`"]:::wfstep${s.id}${s.only_if ? "@{ shape: hex }" : ""}`
512
512
  );
513
513
 
514
514
  nodeLines.unshift(` _Start@{ shape: circle, label: "Start" }`);
515
515
  const linkLines = [];
516
516
  let step_ix = 0;
517
517
  for (const step of steps) {
518
- if (step.initial_step) linkLines.push(` _Start --> ${step.mmname}`);
518
+ if (step.initial_step)
519
+ linkLines.push(
520
+ ` _Start-- <i class="fas fa-plus add-btw-nodes btw-nodes-${0}-${
521
+ step.name
522
+ }"></i> ---${step.mmname}`
523
+ );
519
524
  if (stepNames.includes(step.next_step)) {
520
525
  linkLines.push(
521
526
  ` ${step.mmname}-- <i class="fas fa-plus add-btw-nodes btw-nodes-${step.id}-${step.next_step}"></i> ---${step.mmnext}`
@@ -739,7 +744,9 @@ const getWorkflowStepForm = async (
739
744
  },
740
745
  });
741
746
 
742
- const builtInActionExplainers = WorkflowStep.builtInActionExplainers();
747
+ const builtInActionExplainers = WorkflowStep.builtInActionExplainers({
748
+ api_call: trigger.when_trigger == "API call",
749
+ });
743
750
  const actionsNotRequiringRow = Trigger.action_options({
744
751
  notRequireRow: true,
745
752
  noMultiStep: true,
@@ -830,7 +837,8 @@ const getWorkflowStepForm = async (
830
837
  form.hidden("wf_step_id");
831
838
  form.hidden("_after_step");
832
839
  if (before_step) form.values.wf_next_step = before_step;
833
- if (after_step) form.values._after_step = after_step;
840
+ if (after_step == "0") form.values.wf_initial_step = true;
841
+ else if (after_step) form.values._after_step = after_step;
834
842
  if (step_id) {
835
843
  const step = await WorkflowStep.findOne({ id: step_id });
836
844
  if (!step) throw new Error("Step not found");
@@ -1744,7 +1752,14 @@ const getWorkflowStepUserForm = async (run, trigger, step, req) => {
1744
1752
  null,
1745
1753
  req
1746
1754
  );
1755
+ await form.fill_fkey_options(false, undefined, req?.user);
1747
1756
  form.action = `/actions/fill-workflow-form/${run.id}`;
1757
+ if (run.context[step.configuration.response_variable])
1758
+ Object.assign(
1759
+ form.values,
1760
+ run.context[step.configuration.response_variable]
1761
+ );
1762
+
1748
1763
  return form;
1749
1764
  }
1750
1765
 
@@ -1880,8 +1895,9 @@ WORKFLOWS TODO
1880
1895
 
1881
1896
  help file to explain steps, and context
1882
1897
 
1883
- workflow actions: Stop, RunEditView, ReadFile, WriteFile, APIResponse
1898
+ workflow actions: ReadFile, WriteFile,
1884
1899
 
1900
+ EditViewForm: presets. response var can be blank
1885
1901
  other triggers can be steps
1886
1902
  interactive workflows for not logged in
1887
1903
  actions can declare which variables they inject into scope
package/routes/admin.js CHANGED
@@ -115,6 +115,8 @@ const { get_help_markup } = require("../help/index.js");
115
115
  const Docker = require("dockerode");
116
116
  const npmFetch = require("npm-registry-fetch");
117
117
  const Tag = require("@saltcorn/data/models/tag");
118
+ const MarkdownIt = require("markdown-it"),
119
+ md = new MarkdownIt();
118
120
 
119
121
  const router = new Router();
120
122
  module.exports = router;
@@ -287,6 +289,18 @@ router.get(
287
289
  })
288
290
  );
289
291
 
292
+ router.get(
293
+ "/whatsnew",
294
+ isAdmin,
295
+ error_catcher(async (req, res) => {
296
+ const fp = path.join(__dirname, "..", "CHANGELOG.md");
297
+ const fileBuf = await fs.promises.readFile(fp);
298
+ const mdContents = fileBuf.toString().replace("# Notable changes\n","");
299
+ const markup = md.render(mdContents);
300
+ res.sendWrap(`What's new in Saltcorn`, { above: [markup] });
301
+ })
302
+ );
303
+
290
304
  /**
291
305
  * @name get/backup
292
306
  * @function
@@ -1137,7 +1151,7 @@ router.get(
1137
1151
  table(
1138
1152
  tbody(
1139
1153
  tr(
1140
- th(req.__("Saltcorn version")),
1154
+ th({ valign: "top" }, req.__("Saltcorn version")),
1141
1155
  td(
1142
1156
  packagejson.version,
1143
1157
  isRoot && can_update
@@ -1177,7 +1191,15 @@ router.get(
1177
1191
  ` onError: (res) => { selectVersionError(res, '${rndid}') } });`,
1178
1192
  },
1179
1193
  req.__("Choose version")
1180
- )
1194
+ ),
1195
+ "<br>",
1196
+ a(
1197
+ {
1198
+ onclick: "ajax_modal('/admin/whatsnew')",
1199
+ href: `javascript:void(0)`,
1200
+ },
1201
+ "What's new?"
1202
+ )
1181
1203
  )
1182
1204
  ),
1183
1205
  git_commit &&
package/routes/api.js CHANGED
@@ -398,15 +398,26 @@ router.all(
398
398
  async function (err, user, info) {
399
399
  if (accessAllowed(req, user, trigger)) {
400
400
  try {
401
- const action = getState().actions[trigger.action];
401
+ let resp;
402
402
  const row = req.method === "GET" ? req.query : req.body;
403
- const resp = await action.run({
404
- configuration: trigger.configuration,
405
- body: req.body,
406
- row,
407
- req,
408
- user: user || req.user,
409
- });
403
+ if (trigger.action === "Workflow") {
404
+ resp = await trigger.runWithoutRow({
405
+ req,
406
+ interactive: true,
407
+ row,
408
+ user: user || req.user,
409
+ });
410
+ delete resp.__wf_run_id;
411
+ } else {
412
+ const action = getState().actions[trigger.action];
413
+ resp = await action.run({
414
+ configuration: trigger.configuration,
415
+ body: req.body,
416
+ row,
417
+ req,
418
+ user: user || req.user,
419
+ });
420
+ }
410
421
  if (
411
422
  (row._process_result || req.headers?.scprocessresults) &&
412
423
  resp?.goto
@@ -258,6 +258,7 @@ const pageBuilderData = async (req, context) => {
258
258
  next_button_label: "Done",
259
259
  fonts: getState().fonts,
260
260
  tables: [],
261
+ keyframes: getState().keyframes,
261
262
  };
262
263
  };
263
264
 
package/routes/tables.js CHANGED
@@ -711,6 +711,7 @@ const attribBadges = (f) => {
711
711
  "table",
712
712
  "agg_field",
713
713
  "agg_relation",
714
+ "calc_joinfields"
714
715
  ].includes(k)
715
716
  )
716
717
  return;
@@ -13,6 +13,13 @@ const load_script = (fnm) => {
13
13
  document.body.appendChild(scriptEl);
14
14
  };
15
15
 
16
+ class IntersectionObserver {
17
+ constructor() {}
18
+ observe() {}
19
+ }
20
+
21
+ window.IntersectionObserver = IntersectionObserver;
22
+
16
23
  load_script("jquery-3.6.0.min.js");
17
24
  load_script("saltcorn-common.js");
18
25
  load_script("saltcorn.js");
@@ -91,15 +98,11 @@ test("updateQueryStringParameter hash", () => {
91
98
  );
92
99
  });
93
100
  test("addQueryStringParameter", () => {
94
- expect(addQueryStringParameter("/foo", "age", 43)).toBe(
95
- "/foo?age=43"
96
- );
101
+ expect(addQueryStringParameter("/foo", "age", 43)).toBe("/foo?age=43");
97
102
  expect(addQueryStringParameter("/foo?age=43", "age", 44)).toBe(
98
103
  "/foo?age=43&age=44"
99
104
  );
100
- expect(addQueryStringParameter("/foo?age=43", "age", 43)).toBe(
101
- "/foo?age=43"
102
- );
105
+ expect(addQueryStringParameter("/foo?age=43", "age", 43)).toBe("/foo?age=43");
103
106
  });
104
107
  test("addQueryStringParameter hash", () => {
105
108
  expect(addQueryStringParameter("/foo#baz", "age", 43)).toBe(