@saltcorn/mobile-app 1.7.0-alpha.0 → 1.7.0-beta.0

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.
@@ -39,7 +39,7 @@ export const postView = async (context) => {
39
39
  const mobileCfg = state.mobileConfig;
40
40
  if (
41
41
  mobileCfg.user.role_id > view.min_role &&
42
- !(await view.authorise_post({ body, req, ...view }))
42
+ !(await view.authorize(mobileCfg.user, { action: "post", req, body }))
43
43
  ) {
44
44
  throw new saltcorn.data.utils.NotAuthorized(req.__("Not authorized"));
45
45
  }
@@ -150,7 +150,11 @@ export const getView = async (context) => {
150
150
  if (!view) throw new Error(req.__("No such view: %s", viewname));
151
151
  if (
152
152
  state.mobileConfig.user.role_id > view.min_role &&
153
- !(await view.authorise_get({ query, req, ...view }))
153
+ !(await view.authorize(state.mobileConfig.user, {
154
+ action: "get",
155
+ req,
156
+ state: query,
157
+ }))
154
158
  ) {
155
159
  const additionalInfos = `: your role: ${state.mobileConfig.user.role_id}, view min_role: ${view.min_role}`;
156
160
  throw new saltcorn.data.utils.NotAuthorized(
@@ -161,7 +165,9 @@ export const getView = async (context) => {
161
165
  if (!view.renderLocally()) {
162
166
  const response = await apiCall({
163
167
  method: "GET",
164
- path: `/view/${encodeURIComponent(viewname)}${context.query ? `?${context.query}` : ""}`,
168
+ path: `/view/${encodeURIComponent(viewname)}${
169
+ context.query ? `?${context.query}` : ""
170
+ }`,
165
171
  });
166
172
  const data = response.data;
167
173
  contents0 = data;
@@ -24,7 +24,7 @@ export const getHeaders = () => {
24
24
  from_cfg.push({ style: state.getConfig("page_custom_css", "") });
25
25
  if (state.getConfig("page_custom_html", ""))
26
26
  from_cfg.push({ headerTag: state.getConfig("page_custom_html", "") });
27
- return [...stdHeaders, ...config.pluginHeaders, ...from_cfg];
27
+ return [...stdHeaders, ...(config.pluginHeaders || []), ...from_cfg];
28
28
  };
29
29
 
30
30
  export const parseQuery = (queryStr) => {
@@ -1,5 +1,20 @@
1
1
  /*eslint-env browser*/
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, reset_spinners */
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, reset_spinners, apply_showif_fetching_urls */
3
+
4
+ // Tapping a javascript: link silently does nothing on iOS, so we run the
5
+ // code ourselves instead of relying on the link to do it.
6
+ document.addEventListener("click", function (event) {
7
+ const link =
8
+ event.target.closest && event.target.closest("a[href^='javascript:']");
9
+ if (!link) return;
10
+ event.preventDefault();
11
+ const code = link.getAttribute("href").slice("javascript:".length);
12
+ try {
13
+ (0, eval)(code);
14
+ } catch (error) {
15
+ console.error("Error executing javascript: link:", error);
16
+ }
17
+ });
3
18
 
4
19
  function combineFormAndQuery(form, query) {
5
20
  let paramsList = [];
@@ -87,7 +102,10 @@ async function formSubmit(e, urlSuffix, viewname, noSubmitCb, matchingState) {
87
102
  const urlParams = new URLSearchParams();
88
103
  const data = matchingState ? {} : null;
89
104
  for (const entry of new FormData(e).entries()) {
90
- if (entry[1] instanceof File) files[entry[0]] = entry[1];
105
+ // cordova-plugin-file replaces window.File with its own class, so a
106
+ // real picked file no longer passes "instanceof File" - Blob still
107
+ // works, since every File is also a Blob.
108
+ if (entry[1] instanceof Blob) files[entry[0]] = entry[1];
91
109
  else {
92
110
  // is there a hidden input with a filename?
93
111
  const domEl = $(e).find(
@@ -209,6 +227,37 @@ function saveAndContinueAsync(e, action) {
209
227
  });
210
228
  }
211
229
 
230
+ // not currently generated by any server markup, kept for parity with web
231
+ function saveAndContinueIfValid(e, action, event) {
232
+ setTimeout(() => {
233
+ if (
234
+ event &&
235
+ event.target &&
236
+ event.target.classList &&
237
+ event.target.classList.contains("no-form-change")
238
+ )
239
+ return;
240
+ const form = $(e).closest("form");
241
+ if (form[0].checkValidity?.() === false) {
242
+ form[0].reportValidity();
243
+ return;
244
+ }
245
+ saveAndContinue(e, action);
246
+ });
247
+ }
248
+
249
+ // auto_save's onChange - was undefined on mobile, silently dropping every edit
250
+ function saveAndContinueDelayed(e, action, event, retries = 1) {
251
+ setTimeout(() => {
252
+ if (apply_showif_fetching_urls.size > 0 && retries < 5) {
253
+ setTimeout(() => {
254
+ saveAndContinueDelayed(e, action, event, retries + 1);
255
+ }, 200);
256
+ } else saveAndContinue(e, action);
257
+ });
258
+ return false;
259
+ }
260
+
212
261
  async function saveAndContinue(e, action, k) {
213
262
  try {
214
263
  showLoadSpinner();
@@ -227,11 +276,45 @@ async function saveAndContinue(e, action, k) {
227
276
  }
228
277
  if (k) await k();
229
278
  // TODO ch error (request.responseText?)
279
+ return res;
230
280
  } finally {
231
281
  removeLoadSpinner();
232
282
  }
233
283
  }
234
284
 
285
+ // SubmitWithAjax - undefined on mobile; follows the redirect saveAndContinue skips
286
+ async function submitWithAjax(e) {
287
+ try {
288
+ const form = $(e).closest("form");
289
+ // action attr is always void(0) on mobile - use data-viewname instead
290
+ const res = await saveAndContinue(e, `/view/${form.attr("data-viewname")}`);
291
+ if (res?.error)
292
+ parent.saltcorn.mobileApp.common.showToasts([
293
+ { type: "error", msg: res.error },
294
+ ]);
295
+ const dest = res?.url_when_done || res?.redirect;
296
+ if (dest) {
297
+ const { path, query } =
298
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(dest);
299
+ // "Back to referer" round-trips a get/... route through the server's
300
+ // sanitizer, which adds a leading "/" - strip it, don't double the prefix
301
+ const stripped = path.replace(/^\//, "");
302
+ const route =
303
+ stripped.startsWith("get/") || stripped.startsWith("post/")
304
+ ? stripped
305
+ : path.length > 1
306
+ ? `get${path}`
307
+ : path;
308
+ await parent.saltcorn.mobileApp.navigation.handleRoute(
309
+ route,
310
+ query
311
+ );
312
+ }
313
+ } catch (error) {
314
+ parent.saltcorn.mobileApp.common.errorAlert(error);
315
+ }
316
+ }
317
+
235
318
  async function login(e, entryPoint, isSignup) {
236
319
  try {
237
320
  showLoadSpinner();
@@ -431,22 +514,50 @@ async function pjax_to(href, query, e) {
431
514
  await parent.saltcorn.mobileApp.navigation.handleRoute(safeHref, query);
432
515
  else
433
516
  try {
434
- const headers = {
435
- pjaxpageload: "true",
436
- };
437
- if (localizer.length) headers.localizedstate = "true";
438
- const result = await parent.saltcorn.mobileApp.api.apiCall({
439
- path: path,
440
- method: "GET",
441
- additionalHeaders: headers,
442
- });
517
+ const { isOfflineMode } =
518
+ parent.saltcorn.data.state.getState().mobileConfig;
519
+ let content;
520
+ if (isOfflineMode) {
521
+ const page = await parent.saltcorn.mobileApp.navigation.router.resolve(
522
+ {
523
+ pathname: `get${safeHref}`,
524
+ query: query,
525
+ }
526
+ );
527
+ if (page.redirect) {
528
+ const { path: redirectPath, query: redirectQuery } =
529
+ parent.saltcorn.mobileApp.navigation.splitPathQuery(
530
+ page.redirect
531
+ );
532
+ await parent.saltcorn.mobileApp.navigation.handleRoute(
533
+ redirectPath.startsWith("/") && redirectPath.length > 1
534
+ ? `get${redirectPath}`
535
+ : redirectPath,
536
+ redirectQuery
537
+ );
538
+ return;
539
+ }
540
+ content = typeof page === "string" ? page : page.content;
541
+ } else {
542
+ const headers = {
543
+ pjaxpageload: "true",
544
+ };
545
+ if (localizer.length) headers.localizedstate = "true";
546
+ const result = await parent.saltcorn.mobileApp.api.apiCall({
547
+ path: path,
548
+ method: "GET",
549
+ additionalHeaders: headers,
550
+ responseType: "text",
551
+ });
552
+ content = result.data;
553
+ }
443
554
  if (!inModal && !localizer.length) {
444
555
  // not sure for mobile
445
556
  // window.history.pushState({ url: href }, "", href);
446
557
  }
447
558
  if (inModal && !localizer.length)
448
559
  $(".sc-modal-linkout").attr("href", path);
449
- $dest.html(result.data);
560
+ $dest.html(content);
450
561
  if (localizer.length) localizer.attr("data-sc-local-state", path);
451
562
  initialize_page();
452
563
  } catch (error) {
@@ -675,17 +786,16 @@ async function make_unique_field(
675
786
  }
676
787
  }
677
788
 
678
- function openFile(fileId) {
789
+ async function openFile(fileId) {
679
790
  const config = parent.saltcorn.data.state.getState().mobileConfig;
680
791
  const serverPath = config.server_path;
681
- const token = config.jwt;
682
- const url = `${serverPath}/files/serve/${encodeURIComponent(
683
- fileId
684
- )}?jwt=${token}`;
792
+ const url = `${serverPath}/files/serve/${encodeURIComponent(fileId)}`;
685
793
  parent.cordova.InAppBrowser.open(
686
794
  url,
687
795
  "_blank",
688
- "clearcache=yes,clearsessioncache=yes,location=no,toolbar=yes,toolbarposition=top"
796
+ // No clearcache - it clears cookies app-wide (Android's cookie store
797
+ // isn't scoped per tab), which would log the main app out too.
798
+ "location=no,toolbar=yes,toolbarposition=top"
689
799
  );
690
800
  }
691
801
 
@@ -695,7 +805,9 @@ function openInAppBrowser(url, domId) {
695
805
  const ref = parent.cordova.InAppBrowser.open(
696
806
  url,
697
807
  "_blank",
698
- "clearcache=yes,clearsessioncache=yes,location=no,toolbar=yes,toolbarposition=top"
808
+ // No clearcache - it clears cookies app-wide (Android's cookie store
809
+ // isn't scoped per tab), which would log the main app out too.
810
+ "location=no,toolbar=yes,toolbarposition=top"
699
811
  );
700
812
  ref.addEventListener("exit", function () {
701
813
  $(`#${domId}`).find(".spinner-border").addClass("d-none");