@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.
- package/package.json +1 -2
- package/share_extension_files/Info.plist +20 -2
- package/share_extension_files/ShareViewController.swift +117 -88
- package/src/helpers/api.js +67 -35
- package/src/helpers/auth.js +127 -116
- package/src/helpers/db_schema.js +6 -4
- package/src/helpers/file_system.js +3 -0
- package/src/helpers/offline_mode.js +9 -13
- package/src/index.js +5 -5
- package/src/init.js +41 -42
- package/src/routing/mocks/request.js +1 -1
- package/src/routing/routes/notifications.js +34 -10
- package/src/routing/routes/view.js +9 -3
- package/src/routing/utils.js +1 -1
- package/www/js/iframe_view_utils.js +131 -19
|
@@ -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.
|
|
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.
|
|
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)}${
|
|
168
|
+
path: `/view/${encodeURIComponent(viewname)}${
|
|
169
|
+
context.query ? `?${context.query}` : ""
|
|
170
|
+
}`,
|
|
165
171
|
});
|
|
166
172
|
const data = response.data;
|
|
167
173
|
contents0 = data;
|
package/src/routing/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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");
|