@saltcorn/mobile-app 1.5.0-beta.8 → 1.5.0-rc.1
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/optional_sources/background_sync.js +91 -0
- package/optional_sources/notifications.js +172 -0
- package/package.json +1 -3
- package/src/helpers/auth.js +99 -70
- package/src/helpers/common.js +125 -27
- package/src/helpers/navigation.js +4 -4
- package/src/helpers/offline_mode.js +190 -69
- package/src/index.js +22 -2
- package/src/init.js +17 -4
- package/src/routing/index.js +3 -7
- package/src/routing/routes/auth.js +24 -24
- package/www/js/iframe_view_utils.js +30 -34
- package/build_scripts/modify_android_manifest.js +0 -53
- package/build_scripts/modify_gradle_cfg.js +0 -21
- package/src/helpers/notifications.js +0 -92
- /package/{src/.eslintrc → .eslintrc} +0 -0
package/src/helpers/common.js
CHANGED
|
@@ -4,25 +4,32 @@ import { apiCall } from "./api";
|
|
|
4
4
|
import { Camera, CameraResultType } from "@capacitor/camera";
|
|
5
5
|
import { ScreenOrientation } from "@capacitor/screen-orientation";
|
|
6
6
|
import { SendIntent } from "send-intent";
|
|
7
|
+
import { addPushSyncHandler } from "./offline_mode";
|
|
7
8
|
|
|
8
9
|
const orientationChangeListeners = new Set();
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Make the toast area shown at the bottom empty
|
|
13
|
+
*/
|
|
14
|
+
export function clearToasts() {
|
|
11
15
|
const iframe = document.getElementById("content-iframe");
|
|
12
16
|
const alertsArea =
|
|
13
17
|
iframe.contentWindow.document.getElementById("toasts-area");
|
|
14
18
|
alertsArea.innerHTML = "";
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Show toasts in the toast area at the bottom
|
|
23
|
+
* @param toasts
|
|
24
|
+
* @returns
|
|
25
|
+
*/
|
|
26
|
+
export function showToasts(toasts) {
|
|
18
27
|
if (typeof saltcorn === "undefined") {
|
|
19
28
|
console.log("Not yet initalized.");
|
|
20
|
-
console.log(
|
|
29
|
+
console.log(toasts);
|
|
21
30
|
} else {
|
|
22
31
|
const iframe = document.getElementById("content-iframe");
|
|
23
|
-
let area = iframe.contentWindow.document.getElementById(
|
|
24
|
-
toast ? "toasts-area" : "top-alert"
|
|
25
|
-
);
|
|
32
|
+
let area = iframe.contentWindow.document.getElementById("toasts-area");
|
|
26
33
|
if (!area) {
|
|
27
34
|
const areaHtml = `<div class="container">
|
|
28
35
|
<div
|
|
@@ -36,18 +43,14 @@ export function showAlerts(alerts, toast = true) {
|
|
|
36
43
|
iframe.contentWindow.document
|
|
37
44
|
.getElementById("page-inner-content")
|
|
38
45
|
.insertAdjacentHTML("beforeend", areaHtml);
|
|
39
|
-
area = iframe.contentWindow.document.getElementById(
|
|
40
|
-
toast ? "toasts-area" : "top-alert"
|
|
41
|
-
);
|
|
46
|
+
area = iframe.contentWindow.document.getElementById("toasts-area");
|
|
42
47
|
}
|
|
43
48
|
const successIds = [];
|
|
44
49
|
area.innerHTML = "";
|
|
45
|
-
for (const { type, msg, title } of
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (type === "success") successIds.push(rndid);
|
|
50
|
-
} else area.innerHTML += saltcorn.markup.alert(type, msg);
|
|
50
|
+
for (const { type, msg, title } of toasts) {
|
|
51
|
+
const rndid = `tab${Math.floor(Math.random() * 16777215).toString(16)}`;
|
|
52
|
+
area.innerHTML += saltcorn.markup.toast(type, msg, rndid, title);
|
|
53
|
+
if (type === "success") successIds.push(rndid);
|
|
51
54
|
}
|
|
52
55
|
if (successIds.length > 0) {
|
|
53
56
|
setTimeout(() => {
|
|
@@ -61,6 +64,34 @@ export function showAlerts(alerts, toast = true) {
|
|
|
61
64
|
return true;
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Make the alert area at the top empty
|
|
69
|
+
*/
|
|
70
|
+
export function clearAlerts() {
|
|
71
|
+
const iframe = document.getElementById("content-iframe");
|
|
72
|
+
const area = iframe.contentWindow.document.getElementById("alerts-area");
|
|
73
|
+
if (area) area.innerHTML = "";
|
|
74
|
+
const topAlert = iframe.contentWindow.document.getElementById("top-alert");
|
|
75
|
+
if (topAlert) topAlert.innerHTML = "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Show alerts in the alert area at the top
|
|
80
|
+
* @param alerts
|
|
81
|
+
*/
|
|
82
|
+
export function showAlerts(alerts) {
|
|
83
|
+
if (typeof saltcorn === "undefined") {
|
|
84
|
+
console.log("Not yet initalized.");
|
|
85
|
+
console.log(alerts);
|
|
86
|
+
} else {
|
|
87
|
+
for (const { type, msg, title } of alerts) {
|
|
88
|
+
const iframe = document.getElementById("content-iframe");
|
|
89
|
+
const area = iframe.contentWindow.document.getElementById("top-alert");
|
|
90
|
+
area.innerHTML += saltcorn.markup.alert(type, msg);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
export function showLoadSpinner() {
|
|
65
96
|
const iframe = document.getElementById("content-iframe");
|
|
66
97
|
if (iframe) iframe.contentWindow.showLoadSpinner();
|
|
@@ -71,17 +102,9 @@ export function removeLoadSpinner() {
|
|
|
71
102
|
if (iframe) iframe.contentWindow.removeLoadSpinner();
|
|
72
103
|
}
|
|
73
104
|
|
|
74
|
-
export function clearTopAlerts() {
|
|
75
|
-
const iframe = document.getElementById("content-iframe");
|
|
76
|
-
const area = iframe.contentWindow.document.getElementById("alerts-area");
|
|
77
|
-
if (area) area.innerHTML = "";
|
|
78
|
-
const topAlert = iframe.contentWindow.document.getElementById("top-alert");
|
|
79
|
-
if (topAlert) topAlert.innerHTML = "";
|
|
80
|
-
}
|
|
81
|
-
|
|
82
105
|
export function errorAlert(error) {
|
|
83
106
|
console.error(error);
|
|
84
|
-
|
|
107
|
+
showToasts([
|
|
85
108
|
{
|
|
86
109
|
type: "error",
|
|
87
110
|
msg: error.message ? error.message : "An error occured.",
|
|
@@ -109,7 +132,7 @@ export async function loadFileAsText(fileId) {
|
|
|
109
132
|
});
|
|
110
133
|
} catch (error) {
|
|
111
134
|
if (
|
|
112
|
-
!
|
|
135
|
+
!showToasts([
|
|
113
136
|
{
|
|
114
137
|
type: "error",
|
|
115
138
|
msg: error.message ? error.message : "An error occured.",
|
|
@@ -138,7 +161,7 @@ export async function loadEncodedFile(fileId) {
|
|
|
138
161
|
reader.readAsDataURL(response.data);
|
|
139
162
|
});
|
|
140
163
|
} catch (error) {
|
|
141
|
-
|
|
164
|
+
showToasts([
|
|
142
165
|
{
|
|
143
166
|
type: "error",
|
|
144
167
|
msg: error.message ? error.message : "An error occured.",
|
|
@@ -156,7 +179,7 @@ export async function takePhoto() {
|
|
|
156
179
|
});
|
|
157
180
|
return image.path;
|
|
158
181
|
} catch (error) {
|
|
159
|
-
|
|
182
|
+
showToasts([
|
|
160
183
|
{
|
|
161
184
|
type: "error",
|
|
162
185
|
msg: error.message ? error.message : "An error occured.",
|
|
@@ -189,3 +212,78 @@ export async function checkSendIntentReceived() {
|
|
|
189
212
|
return null;
|
|
190
213
|
}
|
|
191
214
|
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* init the push system, if available
|
|
218
|
+
*/
|
|
219
|
+
export async function tryInitPush(config) {
|
|
220
|
+
try {
|
|
221
|
+
const { initPushNotifications, addPusNotifyHandler } = await import(
|
|
222
|
+
"../helpers/notifications.js"
|
|
223
|
+
);
|
|
224
|
+
try {
|
|
225
|
+
await initPushNotifications();
|
|
226
|
+
if (saltcorn.data.utils.isPushEnabled(config.user)) addPusNotifyHandler();
|
|
227
|
+
if (config.pushSync) addPushSyncHandler();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error("Error initializing push notifications:", error);
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.log("Push notifications module not available:", error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* init background sync, if available
|
|
238
|
+
*/
|
|
239
|
+
export async function tryInitBackgroundSync(config) {
|
|
240
|
+
try {
|
|
241
|
+
const { startPeriodicBackgroundSync } = await import(
|
|
242
|
+
"../helpers/background_sync.js"
|
|
243
|
+
);
|
|
244
|
+
try {
|
|
245
|
+
if (config.syncInterval && config.syncInterval > 0)
|
|
246
|
+
await startPeriodicBackgroundSync(config.syncInterval);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error("Error initializing background sync:", error);
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.log("Background sync module not available:", error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* end the push system, if available
|
|
257
|
+
*/
|
|
258
|
+
export async function tryUnregisterPush() {
|
|
259
|
+
try {
|
|
260
|
+
const { unregisterPushNotifications } = await import(
|
|
261
|
+
"../helpers/notifications.js"
|
|
262
|
+
);
|
|
263
|
+
try {
|
|
264
|
+
await unregisterPushNotifications();
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error("Error unregistering push notifications:", error);
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.log("Push notifications module not available:", error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* stop background sync, if available
|
|
275
|
+
*/
|
|
276
|
+
export async function tryStopBackgroundSync() {
|
|
277
|
+
try {
|
|
278
|
+
const { stopPeriodicBackgroundSync } = await import(
|
|
279
|
+
"../helpers/background_sync.js"
|
|
280
|
+
);
|
|
281
|
+
try {
|
|
282
|
+
await stopPeriodicBackgroundSync();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error("Error stopping periodic background sync:", error);
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error("Push notifications module not available:", error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -3,7 +3,7 @@ import i18next from "i18next";
|
|
|
3
3
|
|
|
4
4
|
import { router } from "../routing/index";
|
|
5
5
|
import { startOfflineMode } from "./offline_mode";
|
|
6
|
-
import {
|
|
6
|
+
import { showToasts } from "./common";
|
|
7
7
|
|
|
8
8
|
export let routingHistory = [];
|
|
9
9
|
|
|
@@ -186,7 +186,7 @@ export async function handleRoute(route, query, files, data) {
|
|
|
186
186
|
await replaceIframeInnerContent(page.content);
|
|
187
187
|
else await replaceIframe(page.content, page.isFile);
|
|
188
188
|
} else {
|
|
189
|
-
|
|
189
|
+
showToasts([
|
|
190
190
|
{
|
|
191
191
|
type: "warning",
|
|
192
192
|
msg: i18next.t("%s finished without a result", {
|
|
@@ -199,7 +199,7 @@ export async function handleRoute(route, query, files, data) {
|
|
|
199
199
|
}
|
|
200
200
|
} catch (error) {
|
|
201
201
|
if (routeAdded) popRoute();
|
|
202
|
-
|
|
202
|
+
showToasts([
|
|
203
203
|
{
|
|
204
204
|
type: "error",
|
|
205
205
|
msg: `${i18next.t("In %s", {
|
|
@@ -253,7 +253,7 @@ export async function gotoEntryView() {
|
|
|
253
253
|
addRoute({ route: mobileConfig.entry_point, query: undefined });
|
|
254
254
|
await replaceIframeInnerContent(page.content);
|
|
255
255
|
} catch (error) {
|
|
256
|
-
|
|
256
|
+
showToasts([
|
|
257
257
|
{
|
|
258
258
|
type: "error",
|
|
259
259
|
msg: error.message ? error.message : "An error occured.",
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { apiCall } from "./api";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
showToasts,
|
|
6
|
+
clearToasts,
|
|
6
7
|
clearAlerts,
|
|
8
|
+
showAlerts,
|
|
7
9
|
errorAlert,
|
|
8
10
|
showLoadSpinner,
|
|
9
11
|
removeLoadSpinner,
|
|
@@ -318,6 +320,43 @@ const handleUniqueConflicts = async (uniqueConflicts, translatedIds) => {
|
|
|
318
320
|
}
|
|
319
321
|
};
|
|
320
322
|
|
|
323
|
+
/**
|
|
324
|
+
* If there was a field level data conflict, the server version is applied
|
|
325
|
+
* When it also was translated, change the untranslated row and translate it later the normal way
|
|
326
|
+
* @param {*} dataConflicts
|
|
327
|
+
* @param {*} translatedIds
|
|
328
|
+
* @param {*} alerts
|
|
329
|
+
*/
|
|
330
|
+
const handleUpdateConflicts = async (dataConflicts, translatedIds, alerts) => {
|
|
331
|
+
let hasConflicts = false;
|
|
332
|
+
for (const [tblName, updates] of Object.entries(dataConflicts)) {
|
|
333
|
+
const table = saltcorn.data.models.Table.findOne({ name: tblName });
|
|
334
|
+
const pkName = table.pk_name || "id";
|
|
335
|
+
const translations = translatedIds[tblName] || {};
|
|
336
|
+
for (const update of updates) {
|
|
337
|
+
// search a translation where the current row is the target
|
|
338
|
+
// and if found, make sure the untranslated row is updated
|
|
339
|
+
for (const [from, to] of Object.entries(translations)) {
|
|
340
|
+
if (to === update[pkName]) {
|
|
341
|
+
update[pkName] = from;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const { [pkName]: _sc_pkValue, ...rest } = update;
|
|
346
|
+
await table.updateRow(rest, _sc_pkValue);
|
|
347
|
+
hasConflicts = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (hasConflicts) {
|
|
351
|
+
alerts.push({
|
|
352
|
+
type: "info",
|
|
353
|
+
msg:
|
|
354
|
+
"Some of your changes could not be applied because the data has changed on the server. " +
|
|
355
|
+
"Your local data has been updated accordingly.",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
321
360
|
const updateSyncInfos = async (
|
|
322
361
|
offlineChanges,
|
|
323
362
|
allTranslations,
|
|
@@ -357,7 +396,7 @@ const updateSyncInfos = async (
|
|
|
357
396
|
}
|
|
358
397
|
};
|
|
359
398
|
|
|
360
|
-
const syncOfflineData = async (synchedTables, syncTimestamp) => {
|
|
399
|
+
const syncOfflineData = async (synchedTables, syncTimestamp, alerts) => {
|
|
361
400
|
const offlineChanges = await loadOfflineChanges(synchedTables);
|
|
362
401
|
if (Object.keys(offlineChanges).length === 0) return null;
|
|
363
402
|
const uploadResp = await apiCall({
|
|
@@ -365,7 +404,8 @@ const syncOfflineData = async (synchedTables, syncTimestamp) => {
|
|
|
365
404
|
path: "/sync/offline_changes",
|
|
366
405
|
body: {
|
|
367
406
|
changes: offlineChanges,
|
|
368
|
-
|
|
407
|
+
oldSyncTimestamp: await getLocalSyncTimestamp(),
|
|
408
|
+
newSyncTimestamp: syncTimestamp,
|
|
369
409
|
},
|
|
370
410
|
});
|
|
371
411
|
const { syncDir } = uploadResp.data;
|
|
@@ -377,10 +417,12 @@ const syncOfflineData = async (synchedTables, syncTimestamp) => {
|
|
|
377
417
|
path: `/sync/upload_finished?dir_name=${encodeURIComponent(syncDir)}`,
|
|
378
418
|
});
|
|
379
419
|
pollCount++;
|
|
380
|
-
const { finished, translatedIds, uniqueConflicts, error } =
|
|
420
|
+
const { finished, translatedIds, uniqueConflicts, dataConflicts, error } =
|
|
421
|
+
pollResp.data;
|
|
381
422
|
if (finished) {
|
|
382
423
|
if (error) throw new Error(error.message);
|
|
383
424
|
else {
|
|
425
|
+
await handleUpdateConflicts(dataConflicts, translatedIds, alerts);
|
|
384
426
|
await handleUniqueConflicts(uniqueConflicts, translatedIds);
|
|
385
427
|
await handleTranslatedIds(uniqueConflicts, translatedIds);
|
|
386
428
|
await updateSyncInfos(offlineChanges, translatedIds, syncTimestamp);
|
|
@@ -425,7 +467,7 @@ const checkCleanSync = async (uploadStarted, uploadStartTime, userName) => {
|
|
|
425
467
|
return false;
|
|
426
468
|
};
|
|
427
469
|
|
|
428
|
-
const
|
|
470
|
+
const getServerTime = async () => {
|
|
429
471
|
const resp = await apiCall({
|
|
430
472
|
method: "GET",
|
|
431
473
|
path: `/sync/sync_timestamp`,
|
|
@@ -433,6 +475,16 @@ const getSyncTimestamp = async () => {
|
|
|
433
475
|
return resp.data.syncTimestamp;
|
|
434
476
|
};
|
|
435
477
|
|
|
478
|
+
const setLocalSyncTimestamp = async (syncTimestamp) => {
|
|
479
|
+
const state = saltcorn.data.state.getState();
|
|
480
|
+
await state.setConfig("mobile_sync_timestamp", syncTimestamp);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const getLocalSyncTimestamp = async () => {
|
|
484
|
+
const state = saltcorn.data.state.getState();
|
|
485
|
+
return await state.getConfig("mobile_sync_timestamp");
|
|
486
|
+
};
|
|
487
|
+
|
|
436
488
|
const setSpinnerText = () => {
|
|
437
489
|
const iframeWindow = $("#content-iframe")[0].contentWindow;
|
|
438
490
|
if (iframeWindow) {
|
|
@@ -445,57 +497,83 @@ const setSpinnerText = () => {
|
|
|
445
497
|
}
|
|
446
498
|
};
|
|
447
499
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
await
|
|
486
|
-
await setUploadStarted(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
500
|
+
let syncInProgress = false;
|
|
501
|
+
|
|
502
|
+
export function isSyncInProgress() {
|
|
503
|
+
return syncInProgress;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
*
|
|
508
|
+
* @param {boolean} withWakelock try request a wakelock and show a spinner during sync (propably not in background mode)
|
|
509
|
+
* @param {boolean} switchOnline end the offline mode and go online again after sync
|
|
510
|
+
* @param {string[]} alerts output alerts collected during sync
|
|
511
|
+
*/
|
|
512
|
+
export async function sync(
|
|
513
|
+
withWakelock = true,
|
|
514
|
+
switchOnline = true,
|
|
515
|
+
alerts = []
|
|
516
|
+
) {
|
|
517
|
+
if (syncInProgress)
|
|
518
|
+
throw new Error("A synchronization is already in progress.");
|
|
519
|
+
syncInProgress = true;
|
|
520
|
+
try {
|
|
521
|
+
if (withWakelock) setSpinnerText();
|
|
522
|
+
const state = saltcorn.data.state.getState();
|
|
523
|
+
const { user } = state.mobileConfig;
|
|
524
|
+
const { offlineUser, hasOfflineData, uploadStarted, uploadStartTime } =
|
|
525
|
+
(await getLastOfflineSession()) || {};
|
|
526
|
+
if (offlineUser && hasOfflineData && offlineUser !== user.email) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`The sync is not available, '${offlineUser}' has not yet uploaded offline data.`
|
|
529
|
+
);
|
|
530
|
+
} else {
|
|
531
|
+
let syncDir = null;
|
|
532
|
+
let cleanSync = await checkCleanSync(
|
|
533
|
+
uploadStarted,
|
|
534
|
+
uploadStartTime,
|
|
535
|
+
user.email
|
|
536
|
+
);
|
|
537
|
+
const syncTimestamp = await getServerTime();
|
|
538
|
+
await setUploadStarted(true, syncTimestamp);
|
|
539
|
+
let lock = null;
|
|
540
|
+
if (withWakelock) {
|
|
541
|
+
try {
|
|
542
|
+
if (window.navigator?.wakeLock?.request)
|
|
543
|
+
lock = await window.navigator.wakeLock.request();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.log("wakeLock not available");
|
|
546
|
+
console.log(error);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
let transactionOpen = false;
|
|
550
|
+
try {
|
|
551
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
|
|
552
|
+
await saltcorn.data.db.query("BEGIN");
|
|
553
|
+
transactionOpen = true;
|
|
554
|
+
if (cleanSync) await clearLocalData(true);
|
|
555
|
+
const { synchedTables, syncInfos } = await prepare();
|
|
556
|
+
await syncRemoteDeletes(syncInfos, syncTimestamp);
|
|
557
|
+
syncDir = await syncOfflineData(synchedTables, syncTimestamp, alerts);
|
|
558
|
+
await syncRemoteData(syncInfos, syncTimestamp);
|
|
559
|
+
await setLocalSyncTimestamp(syncTimestamp);
|
|
560
|
+
if (switchOnline) await endOfflineMode(true);
|
|
561
|
+
await setUploadStarted(false);
|
|
562
|
+
await saltcorn.data.db.query("COMMIT");
|
|
563
|
+
transactionOpen = false;
|
|
564
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (transactionOpen) await saltcorn.data.db.query("ROLLBACK");
|
|
567
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
|
|
568
|
+
console.log(error);
|
|
569
|
+
throw error;
|
|
570
|
+
} finally {
|
|
571
|
+
if (syncDir) await cleanSyncDir(syncDir);
|
|
572
|
+
if (lock) await lock.release();
|
|
573
|
+
}
|
|
498
574
|
}
|
|
575
|
+
} finally {
|
|
576
|
+
syncInProgress = false;
|
|
499
577
|
}
|
|
500
578
|
}
|
|
501
579
|
|
|
@@ -576,21 +654,50 @@ export async function clearLocalData(inTransaction) {
|
|
|
576
654
|
}
|
|
577
655
|
}
|
|
578
656
|
|
|
579
|
-
export function networkChangeCallback(status) {
|
|
657
|
+
export async function networkChangeCallback(status) {
|
|
580
658
|
console.log("Network status changed", status);
|
|
581
659
|
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
582
660
|
if (status.connectionType !== "none" && mobileConfig.isOfflineMode) {
|
|
583
661
|
const iframeWindow = $("#content-iframe")[0].contentWindow;
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
)
|
|
593
|
-
|
|
662
|
+
if (mobileConfig.syncOnReconnect) {
|
|
663
|
+
if (isSyncInProgress()) {
|
|
664
|
+
console.log("Sync already in progress, skipping automatic sync");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
console.log("Network restored, starting automatic sync");
|
|
668
|
+
const toasts = [];
|
|
669
|
+
try {
|
|
670
|
+
await sync(false, true, toasts);
|
|
671
|
+
if (iframeWindow) {
|
|
672
|
+
clearAlerts();
|
|
673
|
+
showAlerts([
|
|
674
|
+
{
|
|
675
|
+
type: "info",
|
|
676
|
+
msg: "You are online again.",
|
|
677
|
+
},
|
|
678
|
+
]);
|
|
679
|
+
}
|
|
680
|
+
toasts.push({
|
|
681
|
+
type: "info",
|
|
682
|
+
msg: "Synchronized your offline data.",
|
|
683
|
+
});
|
|
684
|
+
} catch (error) {
|
|
685
|
+
console.log("Error during push sync:", error);
|
|
686
|
+
}
|
|
687
|
+
if (toasts.length > 0) showToasts(toasts);
|
|
688
|
+
} else {
|
|
689
|
+
const iframeWindow = $("#content-iframe")[0].contentWindow;
|
|
690
|
+
if (iframeWindow) {
|
|
691
|
+
clearToasts();
|
|
692
|
+
iframeWindow.notifyAlert(
|
|
693
|
+
`An internet connection is available, to end the offline mode click ${saltcorn.markup.a(
|
|
694
|
+
{
|
|
695
|
+
href: "javascript:execLink('/sync/sync_settings')",
|
|
696
|
+
},
|
|
697
|
+
"here"
|
|
698
|
+
)}`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
594
701
|
}
|
|
595
702
|
}
|
|
596
703
|
mobileConfig.networkState = status.connectionType;
|
|
@@ -602,7 +709,7 @@ export async function hasOfflineRows() {
|
|
|
602
709
|
const table = saltcorn.data.models.Table.findOne({ name: tblName });
|
|
603
710
|
const pkName = table.pk_name;
|
|
604
711
|
const { rows } = await saltcorn.data.db.query(
|
|
605
|
-
`select count(info_tbl.ref)
|
|
712
|
+
`select count(info_tbl.ref) as total
|
|
606
713
|
from "${saltcorn.data.db.sqlsanitize(
|
|
607
714
|
tblName
|
|
608
715
|
)}_sync_info" as info_tbl
|
|
@@ -610,7 +717,7 @@ export async function hasOfflineRows() {
|
|
|
610
717
|
on info_tbl.ref = data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}"
|
|
611
718
|
where info_tbl.modified_local = true`
|
|
612
719
|
);
|
|
613
|
-
if (rows?.length > 0 && parseInt(rows[0].
|
|
720
|
+
if (rows?.length > 0 && parseInt(rows[0].total) > 0) return true;
|
|
614
721
|
}
|
|
615
722
|
return false;
|
|
616
723
|
}
|
|
@@ -630,7 +737,7 @@ export async function deleteOfflineData(noFeedback) {
|
|
|
630
737
|
await clearLocalData(false);
|
|
631
738
|
await setHasOfflineData(false);
|
|
632
739
|
if (!noFeedback)
|
|
633
|
-
|
|
740
|
+
showToasts([
|
|
634
741
|
{
|
|
635
742
|
type: "info",
|
|
636
743
|
msg: "Deleted your offline data.",
|
|
@@ -643,3 +750,17 @@ export async function deleteOfflineData(noFeedback) {
|
|
|
643
750
|
if (!noFeedback) removeLoadSpinner();
|
|
644
751
|
}
|
|
645
752
|
}
|
|
753
|
+
|
|
754
|
+
export function addPushSyncHandler() {
|
|
755
|
+
const state = saltcorn.data.state.getState();
|
|
756
|
+
state.mobile_push_handler["push_sync"] = async (notification) => {
|
|
757
|
+
console.log("Push sync received:", notification);
|
|
758
|
+
const alerts = [];
|
|
759
|
+
try {
|
|
760
|
+
await sync(false, false, alerts);
|
|
761
|
+
if (alerts.length > 0) showToasts(alerts);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
console.log("Error during push sync:", error);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,13 +2,32 @@ import { init } from "./init";
|
|
|
2
2
|
import * as api from "./helpers/api";
|
|
3
3
|
import * as auth from "./helpers/auth";
|
|
4
4
|
import * as common from "./helpers/common";
|
|
5
|
-
import * as notifications from "./helpers/notifications";
|
|
6
5
|
import * as fileSystem from "./helpers/file_system";
|
|
7
6
|
import * as navigation from "./helpers/navigation";
|
|
8
7
|
import * as offlineMode from "./helpers/offline_mode";
|
|
9
8
|
import * as dbSchema from "./helpers/db_schema";
|
|
10
9
|
import { router } from "./routing/index";
|
|
11
10
|
|
|
11
|
+
// included when push notifications or push sync is enabled
|
|
12
|
+
let notifications = undefined;
|
|
13
|
+
try {
|
|
14
|
+
notifications = require("./helpers/notifications");
|
|
15
|
+
console.log("Notifications module available");
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.log("Notifications module not available");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// included when periodic background sync is enabled
|
|
21
|
+
let backgroundSync = undefined;
|
|
22
|
+
try {
|
|
23
|
+
backgroundSync = require("./helpers/background_sync");
|
|
24
|
+
console.log("Background sync module available");
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.log("Background sync module not available");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// include code placed in a mobile-app directory inside plugins
|
|
12
31
|
const plugins = {};
|
|
13
32
|
const context = require.context("./plugins-code", true, /index\.js$/);
|
|
14
33
|
context.keys().forEach((key) => {
|
|
@@ -22,10 +41,11 @@ export const mobileApp = {
|
|
|
22
41
|
api,
|
|
23
42
|
auth,
|
|
24
43
|
common,
|
|
25
|
-
notifications,
|
|
26
44
|
fileSystem,
|
|
27
45
|
navigation: { ...navigation, router },
|
|
28
46
|
offlineMode,
|
|
29
47
|
dbSchema,
|
|
48
|
+
...(notifications ? { notifications } : {}),
|
|
49
|
+
...(backgroundSync ? { backgroundSync } : {}),
|
|
30
50
|
plugins,
|
|
31
51
|
};
|