@saltcorn/mobile-app 0.9.5-beta.8 → 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/config.xml CHANGED
@@ -12,6 +12,11 @@
12
12
  </config-file>
13
13
  <preference name="Scheme" value="http"/>
14
14
  <preference name="MixedContentMode" value="2"/>
15
+ <preference name="minSdkVersion" value="33"/>
16
+ <preference name="targetSdkVersion" value="33"/>
17
+ </platform>
18
+ <platform name="ios">
19
+ <preference name="deployment-target" value="12.0"/>
15
20
  </platform>
16
21
  <content src="index.html"/>
17
22
  <access origin="*"/>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saltcorn/mobile-app",
3
3
  "displayName": "Saltcorn mobile app",
4
- "version": "0.9.5-beta.8",
4
+ "version": "0.9.5",
5
5
  "description": "Apache Cordova application with @saltcorn/markup",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -20,10 +20,10 @@
20
20
  "ansi-regex": "4.1.1"
21
21
  },
22
22
  "dependencies": {
23
- "cordova": "^11.0.0",
23
+ "cordova": "^12.0.0",
24
24
  "cordova-sqlite-ext": "^6.0.0"
25
25
  },
26
26
  "devDependencies": {
27
- "cordova-android": "^10.1.2"
27
+ "cordova-android": "^12.0.1"
28
28
  }
29
29
  }
package/www/index.html CHANGED
@@ -141,12 +141,6 @@
141
141
  }
142
142
  };
143
143
 
144
- const setVersionTtag = () => {
145
- const config = saltcorn.data.state.getState().mobileConfig;
146
- let iframe = document.getElementById("content-iframe");
147
- iframe.contentWindow._sc_version_tag = config.version_tag;
148
- };
149
-
150
144
  const initJwt = async () => {
151
145
  if (!(await saltcorn.data.db.tableExists("jwt_table"))) {
152
146
  await createJwtTable();
@@ -228,12 +222,15 @@
228
222
  if (mobileConfig.user_id) await gotoEntryView();
229
223
  else {
230
224
  const decodedJwt = jwt_decode(mobileConfig.jwt);
225
+ mobileConfig.user = decodedJwt.user;
226
+ // TODO remove these, use 'user' everywhere
231
227
  mobileConfig.role_id = decodedJwt.user.role_id
232
228
  ? decodedJwt.user.role_id
233
229
  : 100;
234
230
  mobileConfig.user_id = decodedJwt.user.id;
235
231
  mobileConfig.user_name = decodedJwt.user.email;
236
232
  mobileConfig.language = decodedJwt.user.language;
233
+
237
234
  mobileConfig.isPublicUser = false;
238
235
  }
239
236
  addRoute({ route: entryPoint, query: undefined });
@@ -342,7 +339,6 @@
342
339
  );
343
340
  await state.setConfig("base_url", config.server_path);
344
341
  await initRoutes();
345
- setVersionTtag();
346
342
  const entryPoint = config.entry_point;
347
343
  await initI18Next();
348
344
  await readSiteLogo(state);
@@ -364,12 +360,15 @@
364
360
  if ((networkDisabled && jwt) || (await checkJWT(jwt))) {
365
361
  const mobileConfig = state.mobileConfig;
366
362
  const decodedJwt = jwt_decode(mobileConfig.jwt);
363
+ mobileConfig.user = decodedJwt.user;
364
+ // TODO remove these, use 'user' everywhere
367
365
  mobileConfig.role_id = decodedJwt.user.role_id
368
366
  ? decodedJwt.user.role_id
369
367
  : 100;
370
368
  mobileConfig.user_id = decodedJwt.user.id;
371
369
  mobileConfig.user_name = decodedJwt.user.email;
372
370
  mobileConfig.language = decodedJwt.user.language;
371
+
373
372
  mobileConfig.isPublicUser = false;
374
373
  await i18next.changeLanguage(mobileConfig.language);
375
374
  if (mobileConfig.allowOfflineMode) {
@@ -419,9 +418,12 @@
419
418
  if (page.content) await replaceIframe(page.content, page.isFile);
420
419
  } else if (isPublicJwt(jwt)) {
421
420
  const config = state.mobileConfig;
421
+ config.user = { role_id: 100, user_name: "public", language: "en" };
422
+ // TODO remove these, use 'user' everywhere
422
423
  config.role_id = 100;
423
424
  config.user_name = "public";
424
425
  config.language = "en";
426
+
425
427
  config.isPublicUser = true;
426
428
  i18next.changeLanguage(config.language);
427
429
  addRoute({ route: entryPoint, query: undefined });
@@ -35,10 +35,7 @@ function MobileRequest({
35
35
  const mobileCfg = saltcorn.data.state.getState().mobileConfig;
36
36
  return mobileCfg?.language ? mobileCfg.language : "en";
37
37
  },
38
- user: {
39
- id: userId,
40
- role_id: roleId,
41
- },
38
+ user: cfg.user,
42
39
  flash: (type, msg) => {
43
40
  flashMessages.push({ type, msg });
44
41
  },
@@ -1,4 +1,4 @@
1
- /*global sbAdmin2Layout, apiCall, removeJwt, saltcorn, clearHistory*/
1
+ /*global sbAdmin2Layout, apiCall, removeJwt, saltcorn, clearHistory, MobileRequest, MobileResponse, getHeaders*/
2
2
 
3
3
  const prepareAuthForm = () => {
4
4
  return new saltcorn.data.models.Form({
@@ -35,11 +35,40 @@ const getAuthLinks = (current, entryPoint) => {
35
35
  return links;
36
36
  };
37
37
 
38
- const renderLoginView = (entryPoint, versionTag, alerts = []) => {
38
+ const renderLoginView = async (entryPoint, versionTag, alerts = []) => {
39
+ const state = saltcorn.data.state.getState();
39
40
  const form = prepareAuthForm(entryPoint);
40
41
  form.onSubmit = `javascript:loginFormSubmit(this, '${entryPoint}')`;
41
42
  form.submitLabel = "Login";
42
- return sbAdmin2Layout().authWrap({
43
+ const layout = sbAdmin2Layout();
44
+ const login_form_name = state.getConfig("login_form", "");
45
+ if (login_form_name) {
46
+ const login_form = saltcorn.data.models.View.findOne({
47
+ name: login_form_name,
48
+ });
49
+ if (login_form) {
50
+ const req = new MobileRequest();
51
+ const res = new MobileResponse();
52
+ const resp = await login_form.run_possibly_on_page({}, req, res);
53
+ if (login_form.default_render_page) {
54
+ return layout.wrap({
55
+ title: "Login",
56
+ no_menu: true,
57
+ body: resp,
58
+ alerts: [],
59
+ role: req.user ? req.user.role_id : 100,
60
+ req,
61
+ headers: getHeaders(),
62
+ brand: {
63
+ name: state.getConfig("site_name") || "Saltcorn",
64
+ logo: state.mobileConfig.encodedSiteLogo,
65
+ },
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ return layout.authWrap({
43
72
  title: "login",
44
73
  form: form,
45
74
  authLinks: getAuthLinks("login", entryPoint),
@@ -72,7 +101,7 @@ const renderSignupView = (entryPoint, versionTag) => {
72
101
  const getLoginView = async (context) => {
73
102
  const mobileConfig = saltcorn.data.state.getState().mobileConfig;
74
103
  return {
75
- content: renderLoginView(
104
+ content: await renderLoginView(
76
105
  mobileConfig.entry_point,
77
106
  mobileConfig.version_tag,
78
107
  context.alerts ? context.alerts : []
@@ -97,7 +126,7 @@ const logoutAction = async () => {
97
126
  clearHistory();
98
127
  config.jwt = undefined;
99
128
  return {
100
- content: renderLoginView(config.entry_point, config.version_tag),
129
+ content: await renderLoginView(config.entry_point, config.version_tag),
101
130
  };
102
131
  } else {
103
132
  console.log("unable to logout");
@@ -1,7 +1,8 @@
1
1
  /*global saltcorn, offlineHelper*/
2
2
 
3
3
  const getHeaders = () => {
4
- const config = saltcorn.data.state.getState().mobileConfig;
4
+ const state = saltcorn.data.state.getState();
5
+ const config = state.mobileConfig;
5
6
  const versionTag = config.version_tag;
6
7
  const stdHeaders = [
7
8
  { css: `static_assets/${versionTag}/saltcorn.css` },
@@ -9,7 +10,13 @@ const getHeaders = () => {
9
10
  { script: `static_assets/${versionTag}/dayjs.min.js` },
10
11
  { script: "js/utils/iframe_view_utils.js" },
11
12
  ];
12
- return [...stdHeaders, ...config.pluginHeaders];
13
+
14
+ let from_cfg = [];
15
+ if (state.getConfig("page_custom_css", ""))
16
+ from_cfg.push({ style: state.getConfig("page_custom_css", "") });
17
+ if (state.getConfig("page_custom_html", ""))
18
+ from_cfg.push({ headerTag: state.getConfig("page_custom_html", "") });
19
+ return [...stdHeaders, ...config.pluginHeaders, ...from_cfg];
13
20
  };
14
21
 
15
22
  const parseQuery = (queryStr) => {
@@ -81,15 +81,21 @@ const runPageGroup = async (pageGroup, state, context, { req, res }) => {
81
81
  // get/page/pagename
82
82
  const getPage = async (context) => {
83
83
  const state = saltcorn.data.state.getState();
84
- const req = new MobileRequest({ xhr: context.xhr });
84
+ const query = context.query ? parseQuery(context.query) : {};
85
+ const req = new MobileRequest({ xhr: context.xhr, query: query });
85
86
  const res = new MobileResponse();
86
87
  const { page_name } = context.params;
87
88
  const { page, pageGroup } = findPageOrGroup(page_name);
88
89
  let contents = null;
89
- if (page) contents = await runPage(page, state, context, { req, res });
90
- else if (pageGroup)
91
- contents = await runPageGroup(pageGroup, state, context, { req, res });
92
- else throw new Error(req.__("Page %s not found", page_name));
90
+ state.queriesCache = {};
91
+ try {
92
+ if (page) contents = await runPage(page, state, context, { req, res });
93
+ else if (pageGroup)
94
+ contents = await runPageGroup(pageGroup, state, context, { req, res });
95
+ else throw new Error(req.__("Page %s not found", page_name));
96
+ } finally {
97
+ state.queriesCache = null;
98
+ }
93
99
  if (contents.html_file) {
94
100
  if (state.mobileConfig?.isOfflineMode)
95
101
  throw new Error(req.__("Offline mode: cannot load file"));
@@ -137,12 +137,18 @@ const getView = async (context) => {
137
137
  req.__("Not authorized") + additionalInfos
138
138
  );
139
139
  }
140
- const contents = await view.run_possibly_on_page(
141
- query,
142
- req,
143
- res,
144
- view.isRemoteTable()
145
- );
140
+ state.queriesCache = {};
141
+ let contents = null;
142
+ try {
143
+ contents = await view.run_possibly_on_page(
144
+ query,
145
+ req,
146
+ res,
147
+ view.isRemoteTable()
148
+ );
149
+ } finally {
150
+ state.queriesCache = null;
151
+ }
146
152
  const wrapped = res.getWrapHtml();
147
153
  if (wrapped)
148
154
  return wrapContents(
@@ -11,9 +11,25 @@ function currentLocation() {
11
11
  return routingHistory[index].route;
12
12
  }
13
13
 
14
- function currentQuery() {
14
+ function currentQuery(skipPosts = false) {
15
15
  if (routingHistory.length == 0) return undefined;
16
- return routingHistory[routingHistory.length - 1].query;
16
+ let index = routingHistory.length - 1;
17
+ if (skipPosts)
18
+ while (index > 0 && routingHistory[index].route.startsWith("post/")) {
19
+ index--;
20
+ }
21
+ return routingHistory[index].query;
22
+ }
23
+
24
+ function addQueryParam(key, value) {
25
+ let query = currentQuery();
26
+ if (!query) {
27
+ routingHistory[routingHistory.length - 1].query = `${key}=${value}`;
28
+ } else {
29
+ const parsed = new URLSearchParams(query);
30
+ parsed.set(key, value);
31
+ routingHistory[routingHistory.length - 1].query = parsed.toString();
32
+ }
17
33
  }
18
34
 
19
35
  function addRoute(routeEntry) {
@@ -176,9 +192,7 @@ function splitPathQuery(url) {
176
192
 
177
193
  async function replaceIframe(content, isFile = false) {
178
194
  const iframe = document.getElementById("content-iframe");
179
- await write("content.html", `${cordova.file.dataDirectory}`, content);
180
- const url = await getDirEntry(`${cordova.file.dataDirectory}content.html`);
181
- iframe.src = url.toURL();
195
+ iframe.srcdoc = content;
182
196
  if (isFile) {
183
197
  iframe.setAttribute("is-html-file", true);
184
198
  await new Promise((resolve, reject) => {
@@ -242,6 +256,7 @@ async function replaceIframeInnerContent(content) {
242
256
  if (scmodal) {
243
257
  scmodal.modal("hide");
244
258
  }
259
+ iframe.contentWindow.scrollTo(0, 0);
245
260
  iframe.contentWindow.initialize_page();
246
261
  }
247
262
 
@@ -366,7 +381,7 @@ async function handleRoute(route, query, files, data) {
366
381
  async function reload() {
367
382
  const currentRoute = currentLocation();
368
383
  if (!currentRoute) await gotoEntryView();
369
- await handleRoute(currentRoute, currentQuery());
384
+ await handleRoute(currentRoute, currentQuery(true));
370
385
  }
371
386
 
372
387
  async function goBack(steps = 1, exitOnFirstPage = false) {
@@ -1,5 +1,5 @@
1
1
  /*eslint-env browser*/
2
- /*global $, submitWithEmptyAction, is_paging_param, bootstrap, common_done, unique_field_from_rows, inline_submit_success*/
2
+ /*global $, KTDrawer, submitWithEmptyAction, is_paging_param, bootstrap, common_done, unique_field_from_rows, inline_submit_success*/
3
3
 
4
4
  function combineFormAndQuery(form, query) {
5
5
  let paramsList = [];
@@ -27,15 +27,34 @@ async function execLink(url, linkSrc) {
27
27
  } else
28
28
  try {
29
29
  showLoadSpinner();
30
- const { path, query } = parent.splitPathQuery(url);
31
- await parent.handleRoute(`get${path}`, query);
30
+ if (url.startsWith("javascript:")) eval(url.substring(11));
31
+ else {
32
+ const { path, query } = parent.splitPathQuery(url);
33
+ await parent.handleRoute(`get${path}`, query);
34
+ }
32
35
  } finally {
33
36
  removeLoadSpinner();
34
37
  }
35
38
  }
36
39
 
40
+ async function runUrl(url, method = "get") {
41
+ const { path, query } = parent.splitPathQuery(url);
42
+ const page = await parent.router.resolve({
43
+ pathname: `${method}${path}`,
44
+ query: query,
45
+ });
46
+ return page.content;
47
+ }
48
+
37
49
  async function execNavbarLink(url) {
38
50
  $(".navbar-toggler").click();
51
+ if (typeof KTDrawer === "function") {
52
+ const aside = $("#kt_aside")[0];
53
+ if (aside) {
54
+ const kAside = KTDrawer.getInstance(aside);
55
+ kAside.hide();
56
+ }
57
+ }
39
58
  execLink(url);
40
59
  }
41
60
 
@@ -186,14 +205,10 @@ async function login(e, entryPoint, isSignup) {
186
205
  config.user_name = decodedJwt.user.email;
187
206
  config.user_id = decodedJwt.user.id;
188
207
  config.language = decodedJwt.user.language;
208
+ config.user = decodedJwt.user;
189
209
  config.isPublicUser = false;
190
210
  config.isOfflineMode = false;
191
- await parent.insertUser({
192
- id: config.user_id,
193
- email: config.user_name,
194
- role_id: config.role_id,
195
- language: config.language,
196
- });
211
+ await parent.insertUser(config.user);
197
212
  await parent.setJwt(loginResult);
198
213
  config.jwt = loginResult;
199
214
  await parent.i18next.changeLanguage(config.language);
@@ -245,9 +260,16 @@ async function publicLogin(entryPoint) {
245
260
  const loginResult = await loginRequest({ isPublic: true });
246
261
  if (typeof loginResult === "string") {
247
262
  const config = parent.saltcorn.data.state.getState().mobileConfig;
263
+ config.user = {
264
+ role_id: 100,
265
+ user_name: "public",
266
+ language: "en",
267
+ };
268
+ // TODO remove these, use 'user' everywhere
248
269
  config.role_id = 100;
249
270
  config.user_name = "public";
250
271
  config.language = "en";
272
+
251
273
  config.isPublicUser = true;
252
274
  await parent.setJwt(loginResult);
253
275
  config.jwt = loginResult;
@@ -321,7 +343,13 @@ async function signupFormSubmit(e, entryView) {
321
343
 
322
344
  async function loginFormSubmit(e, entryView) {
323
345
  try {
324
- await login(e, entryView, false);
346
+ let safeEntryView = entryView;
347
+ if (!safeEntryView) {
348
+ const config = parent.saltcorn.data.state.getState().mobileConfig;
349
+ if (!config.entry_point) throw new Error("Unable to find an entry-point");
350
+ safeEntryView = config.entry_point;
351
+ }
352
+ await login(e, safeEntryView, false);
325
353
  } catch (error) {
326
354
  parent.errorAlert(error);
327
355
  }
@@ -464,6 +492,10 @@ async function gopage(n, pagesize, viewIdentifier, extra) {
464
492
  );
465
493
  }
466
494
 
495
+ function ajax_modal(url, opts = {}) {
496
+ mobile_modal(url, opts);
497
+ }
498
+
467
499
  async function mobile_modal(url, opts = {}) {
468
500
  if ($("#scmodal").length === 0) {
469
501
  $("body").append(`<div id="scmodal" class="modal">
@@ -473,6 +505,13 @@ async function mobile_modal(url, opts = {}) {
473
505
  <h5 class="modal-title">Modal title</h5>
474
506
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
475
507
  </button>
508
+ <div
509
+ id="modal-toasts-area"
510
+ class="toast-container position-fixed top-0 start-50 p-0"
511
+ style: "z-index: 7000;"
512
+ aria-live="polite"
513
+ aria-atomic="true">
514
+ </div>
476
515
  </div>
477
516
  <div class="modal-body">
478
517
  <p>Modal body text goes here.</p>
@@ -857,10 +896,10 @@ function showLoadSpinner() {
857
896
  $("body").append(`
858
897
  <div
859
898
  id="scspinner"
860
- style="position: absolute;
861
- top: 0px;
862
- width: 100%;
863
- height: 100%;
899
+ style="position: fixed;
900
+ top: 50%;
901
+ left: 50%;
902
+ transform: translate(-50%, -50%);
864
903
  z-index: 9999;"
865
904
  >
866
905
  <div