@saltcorn/mobile-app 1.1.2-beta.9 → 1.1.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saltcorn/mobile-app",
3
3
  "displayName": "Saltcorn mobile app",
4
- "version": "1.1.2-beta.9",
4
+ "version": "1.1.2",
5
5
  "description": "Saltcorn mobile app for Android and iOS",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -51,7 +51,12 @@ export async function safeRows(table, rows) {
51
51
  export async function updateScTables(tablesJSON, skipScPlugins = true) {
52
52
  await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
53
53
  for (const { table, rows } of tablesJSON.sc_tables) {
54
- if (skipScPlugins && table === "_sc_plugins") continue;
54
+ if (
55
+ (skipScPlugins && table === "_sc_plugins") ||
56
+ table === "_sc_workflow_runs" ||
57
+ table === "_sc_workflow_trace"
58
+ )
59
+ continue;
55
60
  if (table === "_sc_tables") await dropDeletedTables(rows);
56
61
  await saltcorn.data.db.deleteWhere(table);
57
62
  await saltcorn.data.db.insertRows(table, await safeRows(table, rows));
@@ -56,6 +56,35 @@ function getDirEntryCordova(directory) {
56
56
  });
57
57
  }
58
58
 
59
+ export async function readJSONCordova(fileName, dirName) {
60
+ const text = await readTextCordova(fileName, dirName);
61
+ return JSON.parse(text);
62
+ }
63
+
64
+ export async function readTextCordova(fileName, dirName) {
65
+ const dirEntry = await getDirEntryCordova(dirName);
66
+ return new Promise((resolve, reject) => {
67
+ dirEntry.getFile(
68
+ fileName,
69
+ { create: false, exclusive: false },
70
+ function (fileEntry) {
71
+ fileEntry.file(function (file) {
72
+ let reader = new FileReader();
73
+ reader.onloadend = function (e) {
74
+ resolve(this.result);
75
+ };
76
+ reader.readAsText(file);
77
+ });
78
+ },
79
+ function (err) {
80
+ console.log(`unable to read ${fileName}`);
81
+ console.log(err);
82
+ reject(err);
83
+ }
84
+ );
85
+ });
86
+ }
87
+
59
88
  export async function readBinaryCordova(fileName, dirName) {
60
89
  const dirEntry = await getDirEntryCordova(dirName);
61
90
  return new Promise((resolve, reject) => {
package/src/init.js CHANGED
@@ -1,4 +1,4 @@
1
- /*global saltcorn, Capacitor */
1
+ /*global saltcorn, Capacitor, cordova */
2
2
 
3
3
  import {
4
4
  startOfflineMode,
@@ -23,6 +23,7 @@ import {
23
23
  addRoute,
24
24
  } from "./helpers/navigation.js";
25
25
  import { checkSendIntentReceived } from "./helpers/common.js";
26
+ import { readJSONCordova, readTextCordova } from "./helpers/file_system.js";
26
27
 
27
28
  import i18next from "i18next";
28
29
  import i18nextSprintfPostProcessor from "i18next-sprintf-postprocessor";
@@ -156,10 +157,22 @@ const initJwt = async () => {
156
157
  }
157
158
  };
158
159
 
159
- const initI18Next = async (allLanguages) => {
160
+ const initI18Next = async () => {
161
+ const resources = {};
162
+ for (const key of Object.keys(
163
+ saltcorn.data.models.config.available_languages
164
+ )) {
165
+ const localeFile = await readJSONCordova(
166
+ `${key}.json`,
167
+ `${cordova.file.applicationDirectory}public/data/locales`
168
+ );
169
+ resources[key] = {
170
+ translation: localeFile,
171
+ };
172
+ }
160
173
  await i18next.use(i18nextSprintfPostProcessor).init({
161
174
  lng: "en",
162
- allLanguages,
175
+ resources,
163
176
  });
164
177
  };
165
178
 
@@ -291,26 +304,52 @@ const postShare = async (shareData) => {
291
304
  return await replaceIframe(page.content);
292
305
  };
293
306
 
307
+ const readSchemaIfNeeded = async () => {
308
+ let tablesJSON = null;
309
+ const { created_at } = await readJSONCordova(
310
+ "tables_created_at.json",
311
+ `${cordova.file.applicationDirectory}${"public"}/data`
312
+ );
313
+ const updateNeeded = await dbUpdateNeeded(created_at);
314
+ if (updateNeeded) {
315
+ tablesJSON = await readJSONCordova(
316
+ "tables.json",
317
+ `${cordova.file.applicationDirectory}${"public"}/data`
318
+ );
319
+ }
320
+ return { updateNeeded, tablesJSON };
321
+ };
322
+
323
+ const readSiteLogo = async (state) => {
324
+ try {
325
+ const base64 = await readTextCordova(
326
+ "encoded_site_logo.txt",
327
+ `${cordova.file.applicationDirectory}public/data`
328
+ );
329
+ return base64;
330
+ } catch (error) {
331
+ console.log(
332
+ `Unable to read the site logo file: ${
333
+ error.message ? error.message : "Unknown error"
334
+ }`
335
+ );
336
+ return "";
337
+ }
338
+ };
339
+
294
340
  // device is ready
295
- export async function init({
296
- mobileConfig,
297
- tablesSchema,
298
- schemaCreatedAt,
299
- translations,
300
- siteLogo,
301
- }) {
341
+ export async function init(mobileConfig) {
302
342
  try {
303
343
  const lastLocation = takeLastLocation();
304
344
  document.addEventListener("resume", onResume, false);
305
- const { created_at } = schemaCreatedAt;
306
345
  await addScripts(mobileConfig.version_tag);
307
346
  saltcorn.data.db.connectObj.version_tag = mobileConfig.version_tag;
308
347
 
309
348
  await saltcorn.data.db.init();
310
- const updateNeeded = await dbUpdateNeeded(created_at);
349
+ const { updateNeeded, tablesJSON } = await readSchemaIfNeeded();
311
350
  if (updateNeeded) {
312
351
  // update '_sc_plugins' first because of loadPlugins()
313
- await updateScPlugins(tablesSchema);
352
+ await updateScPlugins(tablesJSON);
314
353
  }
315
354
  const state = saltcorn.data.state.getState();
316
355
  state.mobileConfig = mobileConfig;
@@ -318,7 +357,9 @@ export async function init({
318
357
  state.registerPlugin("base", saltcorn.base_plugin);
319
358
  state.registerPlugin("sbadmin2", saltcorn.sbadmin2);
320
359
  collectPluginHeaders(await loadPlugins(state));
321
- if (updateNeeded) await updateDb(tablesSchema);
360
+ if (updateNeeded) {
361
+ await updateDb(tablesJSON);
362
+ }
322
363
  await createSyncInfoTables(mobileConfig.synchedTables);
323
364
  await initJwt();
324
365
  await state.refresh_tables();
@@ -331,8 +372,8 @@ export async function init({
331
372
  );
332
373
  await state.setConfig("base_url", mobileConfig.server_path);
333
374
  const entryPoint = mobileConfig.entry_point;
334
- await initI18Next(translations);
335
- state.mobileConfig.encodedSiteLogo = siteLogo;
375
+ await initI18Next();
376
+ state.mobileConfig.encodedSiteLogo = await readSiteLogo();
336
377
  state.mobileConfig.networkState = (
337
378
  await Network.getStatus()
338
379
  ).connectionType;
@@ -14,6 +14,11 @@ import {
14
14
  } from "./routes/sync";
15
15
  import { postView, postViewRoute, getView } from "./routes/view";
16
16
  import { postShare } from "./routes/notifications";
17
+ import {
18
+ postResumeWorkflow,
19
+ getFillWorkflowForm,
20
+ postFillWorkflowForm,
21
+ } from "./routes/actions";
17
22
 
18
23
  const routes = [
19
24
  // api
@@ -98,6 +103,19 @@ const routes = [
98
103
  path: "get/view/:viewname",
99
104
  action: getView,
100
105
  },
106
+ // actions
107
+ {
108
+ path: "post/actions/resume-workflow/:id",
109
+ action: postResumeWorkflow,
110
+ },
111
+ {
112
+ path: "get/actions/fill-workflow-form/:id",
113
+ action: getFillWorkflowForm,
114
+ },
115
+ {
116
+ path: "post/actions/fill-workflow-form/:id",
117
+ action: postFillWorkflowForm,
118
+ },
101
119
  ];
102
120
 
103
121
  export const router = new UniversalRouter(routes);
@@ -0,0 +1,111 @@
1
+ /*global saltcorn */
2
+
3
+ import { MobileRequest } from "../mocks/request";
4
+ import { apiCall } from "../../helpers/api";
5
+
6
+ export const postResumeWorkflow = async (context) => {
7
+ const { id } = context.params;
8
+ const { isOfflineMode } = saltcorn.data.state.getState().mobileConfig;
9
+ if (isOfflineMode) {
10
+ const req = new MobileRequest(context);
11
+ const run = await saltcorn.data.models.WorkflowRun.findOne({ id });
12
+ if (run.started_by !== req.user?.id)
13
+ throw new saltcorn.data.utils.NotAuthorized(req.__("Not authorized"));
14
+ const trigger = await saltcorn.data.models.Trigger.findOne({
15
+ id: run.trigger_id,
16
+ });
17
+ const runResult = await run.run({
18
+ user: req.user,
19
+ interactive: true,
20
+ trace: trigger.configuration?.save_traces,
21
+ });
22
+ if (
23
+ runResult &&
24
+ typeof runResult === "object" &&
25
+ Object.keys(runResult).length
26
+ )
27
+ return { success: "ok", ...runResult };
28
+ const retDirs = await run.popReturnDirectives();
29
+ return { success: "ok", ...retDirs };
30
+ } else {
31
+ const response = await apiCall({
32
+ method: "POST",
33
+ path: `/actions/resume-workflow/${id}`,
34
+ });
35
+ return response.data;
36
+ }
37
+ };
38
+
39
+ export const getFillWorkflowForm = async (context) => {
40
+ const { id } = context.params;
41
+ const { isOfflineMode } = saltcorn.data.state.getState().mobileConfig;
42
+ if (isOfflineMode) {
43
+ const req = new MobileRequest();
44
+ const run = await saltcorn.data.models.WorkflowRun.findOne({ id });
45
+ if (!run.user_allowed_to_fill_form(req.user))
46
+ throw new saltcorn.data.utils.NotAuthorized(req.__("Not authorized"));
47
+ const trigger = await saltcorn.data.models.Trigger.findOne({
48
+ id: run.trigger_id,
49
+ });
50
+ const step = await saltcorn.data.models.WorkflowStep.findOne({
51
+ trigger_id: trigger.id,
52
+ name: run.current_step_name,
53
+ });
54
+ const form = await saltcorn.data.web_mobile_commons.getWorkflowStepUserForm(
55
+ run,
56
+ trigger,
57
+ step,
58
+ req
59
+ );
60
+ return saltcorn.markup.renderForm(form, false);
61
+ } else {
62
+ const response = await apiCall({
63
+ method: "GET",
64
+ path: `/actions/fill-workflow-form/${id}`,
65
+ });
66
+ return response.data;
67
+ }
68
+ };
69
+
70
+ export const postFillWorkflowForm = async (context) => {
71
+ const { id } = context.params;
72
+ const { isOfflineMode } = saltcorn.data.state.getState().mobileConfig;
73
+ if (isOfflineMode) {
74
+ const req = new MobileRequest(context);
75
+ const run = await saltcorn.data.models.WorkflowRun.findOne({ id });
76
+ if (!run.user_allowed_to_fill_form(req.user))
77
+ throw new saltcorn.data.utils.NotAuthorized(req.__("Not authorized"));
78
+ const trigger = await saltcorn.data.models.Trigger.findOne({
79
+ id: run.trigger_id,
80
+ });
81
+ const step = await saltcorn.data.models.WorkflowStep.findOne({
82
+ trigger_id: trigger.id,
83
+ name: run.current_step_name,
84
+ });
85
+ const form = await saltcorn.data.web_mobile_commons.getWorkflowStepUserForm(
86
+ run,
87
+ trigger,
88
+ step,
89
+ req
90
+ );
91
+ form.validate(req.body);
92
+ if (form.hasErrors) {
93
+ return { error: req.__("Errors in form") }; // TODO not sure
94
+ } else {
95
+ await run.provide_form_input(form.values);
96
+ const runres = await run.run({
97
+ user: req.user,
98
+ trace: trigger.configuration?.save_traces,
99
+ interactive: true,
100
+ });
101
+ const retDirs = await run.popReturnDirectives();
102
+ return { success: "ok", ...runres, ...retDirs };
103
+ }
104
+ } else {
105
+ const response = await apiCall({
106
+ method: "POST",
107
+ path: `/actions/fill-workflow-form/${id}`,
108
+ });
109
+ return response.data;
110
+ }
111
+ };
@@ -4,10 +4,11 @@ import { MobileRequest } from "../mocks/request";
4
4
  import { MobileResponse } from "../mocks/response";
5
5
  import { parseQuery, wrapContents } from "../utils";
6
6
  import { loadFileAsText } from "../../helpers/common";
7
+ import { apiCall } from "../../helpers/api";
7
8
 
8
9
  // post/page/:pagename/action/:rndid
9
10
  export const postPageAction = async (context) => {
10
- const { user } = saltcorn.data.state.getState().mobileConfig;
11
+ const { user, isOfflineMode } = saltcorn.data.state.getState().mobileConfig;
11
12
  const req = new MobileRequest({ xhr: context.xhr });
12
13
  const { page_name, rndid } = context.params;
13
14
  const page = await saltcorn.data.models.Page.findOne({ name: page_name });
@@ -21,12 +22,21 @@ export const postPageAction = async (context) => {
21
22
  if (segment.rndid === rndid) col = segment;
22
23
  },
23
24
  });
24
- const result = await saltcorn.data.plugin_helper.run_action_column({
25
- col,
26
- referrer: "",
27
- req,
28
- });
29
- return result || {};
25
+
26
+ if (isOfflineMode) {
27
+ const result = await saltcorn.data.plugin_helper.run_action_column({
28
+ col,
29
+ referrer: "",
30
+ req,
31
+ });
32
+ return result || {};
33
+ } else {
34
+ const response = await apiCall({
35
+ method: "POST",
36
+ path: `/page/${page_name}/action/${rndid}`,
37
+ });
38
+ return response.data || {};
39
+ }
30
40
  };
31
41
 
32
42
  const findPageOrGroup = (pagename) => {
@@ -5,6 +5,8 @@ import { MobileResponse } from "../mocks/response";
5
5
  import { parseQuery, wrapContents } from "../utils";
6
6
  import { setHasOfflineData } from "../../helpers/offline_mode";
7
7
  import { routingHistory } from "../../helpers/navigation";
8
+ import { apiCall } from "../../helpers/api";
9
+
8
10
  /**
9
11
  *
10
12
  * @param {*} context
@@ -90,13 +92,25 @@ export const postViewRoute = async (context) => {
90
92
  const { user, isOfflineMode } = state.mobileConfig;
91
93
  if (user.role_id > view.min_role)
92
94
  throw new saltcorn.data.utils.NotAuthorized(req.__("Not authorized"));
93
- await view.runRoute(
94
- context.params.route,
95
- context.data,
96
- res,
97
- { req, res },
98
- view.isRemoteTable()
99
- );
95
+
96
+ if (!isOfflineMode && view.viewtemplateObj?.name === "WorkflowRoom") {
97
+ const response = await apiCall({
98
+ method: "POST",
99
+ path: `/view/${encodeURIComponent(view.name)}/${encodeURIComponent(
100
+ context.params.route
101
+ )}`,
102
+ body: context.data,
103
+ });
104
+ if (response.data.success === "ok") return response.data;
105
+ else throw new Error(`Unable to run route ${context.params.route}`);
106
+ } else
107
+ await view.runRoute(
108
+ context.params.route,
109
+ context.data,
110
+ res,
111
+ { req, res },
112
+ view.isRemoteTable()
113
+ );
100
114
  if (isOfflineMode) await setHasOfflineData(true);
101
115
  const wrapped = res.getWrapHtml();
102
116
  if (wrapped)
File without changes
package/www/index.html CHANGED
@@ -5,11 +5,7 @@
5
5
  name="viewport"
6
6
  content="width=device-width, initial-scale=1, maximum-scale=1"
7
7
  />
8
- <script src="data/tables.js"></script>
9
- <script src="data/tables_created_at.js"></script>
10
8
  <script src="data/config.js"></script>
11
- <script src="data/translations.js"></script>
12
- <script src="data/encoded_site_logo.js"></script>
13
9
  <script type="module" src="./dist/bundle.js"></script>
14
10
  <script src="js/iframe_view_utils.js"></script>
15
11
 
@@ -18,15 +14,9 @@
18
14
  if (window.saltcorn) window.saltcorn.mobileApp = mobileApp;
19
15
  else window.saltcorn = { mobileApp };
20
16
 
21
- document.addEventListener("deviceready", () =>
22
- saltcorn.mobileApp.init({
23
- mobileConfig: _sc_mobile_config,
24
- tablesSchema: _sc_tables,
25
- schemaCreatedAt: _sc_tables_created_at,
26
- translations: _sc_translations,
27
- siteLogo: _sc_site_logo,
28
- })
29
- );
17
+ document.addEventListener("deviceready", () => {
18
+ saltcorn.mobileApp.init(_sc_mobile_config);
19
+ });
30
20
  document.addEventListener("backbutton", async () => {
31
21
  await saltcorn.mobileApp.navigation.goBack(1, true);
32
22
  });
@@ -1,5 +1,5 @@
1
1
  /*eslint-env browser*/
2
- /*global $, KTDrawer, submitWithEmptyAction, is_paging_param, bootstrap, common_done, unique_field_from_rows, inline_submit_success, get_current_state_url, initialize_page */
2
+ /*global $, KTDrawer, reload_embedded_view, submitWithEmptyAction, is_paging_param, bootstrap, common_done, unique_field_from_rows, inline_submit_success, get_current_state_url, initialize_page */
3
3
 
4
4
  function combineFormAndQuery(form, query) {
5
5
  let paramsList = [];
@@ -124,6 +124,55 @@ async function formSubmit(e, urlSuffix, viewname, noSubmitCb, matchingState) {
124
124
  }
125
125
  }
126
126
 
127
+ async function ajaxSubmitForm(e, force_no_reload, event) {
128
+ const form = $(e).closest("form");
129
+ const action = form.attr("action");
130
+ if (event) event.preventDefault();
131
+ try {
132
+ const { isOfflineMode } =
133
+ parent.saltcorn.data.state.getState().mobileConfig;
134
+ let responseData = null;
135
+ if (isOfflineMode) {
136
+ const { path, query } =
137
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(action);
138
+ const formData = new FormData(form[0]);
139
+ const data = {};
140
+ for (const [k, v] of formData.entries()) data[k] = v;
141
+ responseData = await parent.saltcorn.mobileApp.navigation.router.resolve({
142
+ pathname: `post${path}`,
143
+ query: query,
144
+ body: data,
145
+ });
146
+ } else {
147
+ const response = await parent.saltcorn.mobileApp.api.apiCall({
148
+ path: action,
149
+ method: "POST",
150
+ body: new FormData(form[0]),
151
+ });
152
+ responseData = response.data;
153
+ }
154
+ var no_reload = $("#scmodal").hasClass("no-submit-reload");
155
+ const on_close_reload_view = $("#scmodal").attr(
156
+ "data-on-close-reload-view"
157
+ );
158
+ $("#scmodal").modal("hide");
159
+ if (on_close_reload_view) {
160
+ const viewE = $(`[data-sc-embed-viewname="${on_close_reload_view}"]`);
161
+ if (viewE.length) reload_embedded_view(on_close_reload_view);
162
+ else if (!force_no_reload)
163
+ await parent.saltcorn.mobileApp.navigation.reload();
164
+ } else if (!force_no_reload && !no_reload)
165
+ await parent.saltcorn.mobileApp.navigation.reload();
166
+ else common_done(responseData, form.attr("data-viewname"));
167
+ } catch (error) {
168
+ // var title = request.getResponseHeader("Page-Title");
169
+ // if (title) $("#scmodal .modal-title").html(decodeURIComponent(title));
170
+ // var body = request.responseText;
171
+ // if (body) $("#scmodal .modal-body").html(body);
172
+ parent.saltcorn.mobileApp.common.errorAlert(error);
173
+ }
174
+ }
175
+
127
176
  async function inline_local_submit(e, opts1) {
128
177
  try {
129
178
  e.preventDefault();
@@ -481,7 +530,7 @@ async function mobile_modal(url, opts = {}) {
481
530
  query: query,
482
531
  alerts: [],
483
532
  });
484
- const modalContent = page.content;
533
+ const modalContent = typeof page === "string" ? page : page.content;
485
534
  const title = page.title;
486
535
  if (title) $("#scmodal .modal-title").html(title);
487
536
  $("#scmodal .modal-body").html(modalContent);
@@ -502,6 +551,10 @@ function closeModal() {
502
551
  $("#scmodal").modal("toggle");
503
552
  }
504
553
 
554
+ async function ajax_post(url, args) {
555
+ await local_post(url, args);
556
+ }
557
+
505
558
  async function local_post(url, args) {
506
559
  try {
507
560
  showLoadSpinner();
@@ -519,6 +572,10 @@ async function local_post(url, args) {
519
572
  }
520
573
  }
521
574
 
575
+ async function ajax_post_json(url, data, cb) {
576
+ await local_post_json(url, data, cb);
577
+ }
578
+
522
579
  async function local_post_json(url, data, cb) {
523
580
  try {
524
581
  showLoadSpinner();
@@ -679,6 +736,8 @@ async function clear_state(omit_fields_str, e) {
679
736
  }
680
737
  }
681
738
 
739
+ let last_route_viewname;
740
+
682
741
  async function view_post(viewnameOrElem, route, data, onDone, sendState) {
683
742
  const viewname =
684
743
  typeof viewnameOrElem === "string"
@@ -686,6 +745,7 @@ async function view_post(viewnameOrElem, route, data, onDone, sendState) {
686
745
  : $(viewnameOrElem)
687
746
  .closest("[data-sc-embed-viewname]")
688
747
  .attr("data-sc-embed-viewname");
748
+ last_route_viewname = viewname;
689
749
  const buildQuery = () => {
690
750
  const query = parent.saltcorn.mobileApp.navigation.currentQuery();
691
751
  return query ? `?${query}` : "";
@@ -1 +0,0 @@
1
- var _sc_site_logo = "";