@saltcorn/mobile-app 1.1.0-beta.8 → 1.1.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.
Files changed (40) hide show
  1. package/.babelrc +3 -0
  2. package/build_scripts/modify_android_manifest.js +47 -0
  3. package/build_scripts/modify_gradle_cfg.js +21 -0
  4. package/package.json +21 -12
  5. package/src/.eslintrc +21 -0
  6. package/src/helpers/api.js +43 -0
  7. package/src/helpers/auth.js +191 -0
  8. package/src/helpers/common.js +189 -0
  9. package/{www/js/utils/table_utils.js → src/helpers/db_schema.js} +18 -40
  10. package/src/helpers/file_system.js +102 -0
  11. package/{www/js/utils/global_utils.js → src/helpers/navigation.js} +189 -332
  12. package/src/helpers/offline_mode.js +645 -0
  13. package/src/index.js +20 -0
  14. package/src/init.js +424 -0
  15. package/src/routing/index.js +98 -0
  16. package/{www/js → src/routing}/mocks/request.js +5 -5
  17. package/{www/js → src/routing}/mocks/response.js +1 -1
  18. package/{www/js → src/routing}/routes/api.js +10 -15
  19. package/{www/js → src/routing}/routes/auth.js +12 -6
  20. package/{www/js → src/routing}/routes/delete.js +9 -6
  21. package/{www/js → src/routing}/routes/edit.js +9 -6
  22. package/src/routing/routes/error.js +6 -0
  23. package/{www/js → src/routing}/routes/fields.js +7 -2
  24. package/{www/js → src/routing}/routes/page.js +15 -10
  25. package/{www/js → src/routing}/routes/sync.js +17 -8
  26. package/{www/js → src/routing}/routes/view.js +20 -15
  27. package/{www/js/routes/common.js → src/routing/utils.js} +18 -13
  28. package/unsecure-default-key.jks +0 -0
  29. package/webpack.config.js +31 -0
  30. package/www/data/encoded_site_logo.js +1 -0
  31. package/www/index.html +23 -493
  32. package/www/js/{utils/iframe_view_utils.js → iframe_view_utils.js} +193 -274
  33. package/config.xml +0 -27
  34. package/res/icon/android/icon.png +0 -0
  35. package/res/screen/android/splash-icon.png +0 -0
  36. package/res/screen/ios/Default@2x~universal~anyany.png +0 -0
  37. package/www/js/routes/error.js +0 -5
  38. package/www/js/routes/init.js +0 -76
  39. package/www/js/utils/file_helpers.js +0 -108
  40. package/www/js/utils/offline_mode_helper.js +0 -625
@@ -0,0 +1,645 @@
1
+ /*global saltcorn, $*/
2
+
3
+ import { apiCall } from "./api";
4
+ import {
5
+ showAlerts,
6
+ clearAlerts,
7
+ errorAlert,
8
+ showLoadSpinner,
9
+ removeLoadSpinner,
10
+ } from "./common";
11
+
12
+ const setUploadStarted = async (started, time) => {
13
+ const state = saltcorn.data.state.getState();
14
+ const oldSession = await state.getConfig("last_offline_session");
15
+ const newSession = { ...oldSession };
16
+ newSession.uploadStarted = started;
17
+ if (started) newSession.uploadStartTime = time;
18
+ else newSession.uploadStartTime = null;
19
+ await state.setConfig("last_offline_session", newSession);
20
+ };
21
+
22
+ const maxLastModified = async (tblName) => {
23
+ const result = await saltcorn.data.db.query(
24
+ `select max(last_modified) "max" from "${saltcorn.data.db.sqlsanitize(
25
+ tblName
26
+ )}_sync_info"`
27
+ );
28
+ return result.rows?.length > 0 ? new Date(result.rows[0].max) : null;
29
+ };
30
+
31
+ const prepare = async () => {
32
+ const state = saltcorn.data.state.getState();
33
+ const { synchedTables } = state.mobileConfig;
34
+ const syncInfos = {};
35
+ for (const tblName of synchedTables) {
36
+ const syncInfo = { maxLoadedId: 0 };
37
+ const maxLm = await maxLastModified(tblName);
38
+ if (maxLm) syncInfo.syncFrom = maxLm.valueOf();
39
+ syncInfos[tblName] = syncInfo;
40
+ }
41
+ return { synchedTables, syncInfos };
42
+ };
43
+
44
+ const insertRemoteData = async (table, rows, syncTimestamp) => {
45
+ const tblName = table.name;
46
+ const pkName = table.pk_name;
47
+ for (const row of rows) {
48
+ const {
49
+ _sync_info_tbl_ref_,
50
+ _sync_info_tbl_last_modified_,
51
+ _sync_info_tbl_deleted_,
52
+ ...rest
53
+ } = row;
54
+ const ref = _sync_info_tbl_ref_,
55
+ last_modified = _sync_info_tbl_last_modified_;
56
+ await saltcorn.data.db.insert(tblName, rest, { replace: true });
57
+ const infos = await saltcorn.data.db.select(
58
+ `${saltcorn.data.db.sqlsanitize(tblName)}_sync_info`,
59
+ {
60
+ ref: rest[pkName],
61
+ }
62
+ );
63
+ if (infos.length > 0) {
64
+ await saltcorn.data.db.query(
65
+ `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
66
+ set last_modified=${last_modified}, deleted=false, modified_local = false
67
+ where ref=${rest[pkName]}`
68
+ );
69
+ } else {
70
+ await saltcorn.data.db.insert(
71
+ `${saltcorn.data.db.sqlsanitize(tblName)}_sync_info`,
72
+ {
73
+ ref,
74
+ last_modified: syncTimestamp,
75
+ deleted: false,
76
+ modified_local: false,
77
+ }
78
+ );
79
+ }
80
+ }
81
+ };
82
+
83
+ const syncRemoteData = async (syncInfos, syncTimestamp) => {
84
+ let iterations = 200;
85
+ let hasMoreData = true;
86
+ const idToTable = {};
87
+ const getTable = (tblName) => {
88
+ if (!idToTable[tblName])
89
+ idToTable[tblName] = saltcorn.data.models.Table.findOne({
90
+ name: tblName,
91
+ });
92
+ return idToTable[tblName];
93
+ };
94
+ while (hasMoreData && --iterations > 0) {
95
+ hasMoreData = false;
96
+ const loadResp = await apiCall({
97
+ method: "POST",
98
+ path: "/sync/load_changes",
99
+ body: {
100
+ syncInfos,
101
+ loadUntil: syncTimestamp,
102
+ },
103
+ });
104
+ for (const [tblName, { rows, maxLoadedId }] of Object.entries(
105
+ loadResp.data
106
+ )) {
107
+ if (rows?.length > 0) {
108
+ const table = getTable(tblName);
109
+ hasMoreData = true;
110
+ await insertRemoteData(table, rows, syncTimestamp);
111
+ syncInfos[tblName].maxLoadedId = maxLoadedId;
112
+ }
113
+ }
114
+ }
115
+ };
116
+
117
+ const prepDeletes = async (table, deletes) => {
118
+ let result = [...deletes];
119
+ const tblName = table.name;
120
+ const pkName = table.pk_name;
121
+ // don't delete if it's local modifed or unsynched
122
+ const tblConflicts = await saltcorn.data.db.query(
123
+ `select ref from "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
124
+ where ref in (${deletes.map(({ ref }) => ref).join(",")}) and
125
+ (last_modified is null or modified_local = true)`
126
+ );
127
+ if (tblConflicts.rows.length > 0) {
128
+ // make it an insert, so that it re-appears on the server
129
+ const conflicts = tblConflicts.rows.map(({ ref }) => ref);
130
+ await saltcorn.data.db.query(
131
+ `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
132
+ set last_modified = null, modified_local = true
133
+ where ref in (${conflicts.join(",")})`
134
+ );
135
+ const conflictsSet = new Set(conflicts);
136
+ result = result.filter((del) => !conflictsSet.has(del.ref));
137
+ }
138
+
139
+ // don't delete if it's referenced by offline data
140
+ for (const field of await saltcorn.data.models.Field.find({
141
+ reftable_name: tblName,
142
+ })) {
143
+ const srcTbl = saltcorn.data.models.Table.findOne(field.table_id);
144
+ const { synchedTables } = saltcorn.data.state.getState().mobileConfig;
145
+ if (synchedTables.indexOf(srcTbl.name) >= 0) {
146
+ const fkConflicts = await saltcorn.data.db.query(
147
+ `select data_tbl."${saltcorn.data.db.sqlsanitize(
148
+ field.name
149
+ )}" from "${saltcorn.data.db.sqlsanitize(
150
+ srcTbl.name
151
+ )}" as data_tbl join "${saltcorn.data.db.sqlsanitize(
152
+ srcTbl.name
153
+ )}_sync_info" as info_tbl
154
+ on data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}" = info_tbl.ref
155
+ where data_tbl."${saltcorn.data.db.sqlsanitize(
156
+ field.name
157
+ )}" in (${result.map(({ ref }) => ref).join(",")})
158
+ and (info_tbl.last_modified is null or info_tbl.modified_local = true)`
159
+ );
160
+ if (fkConflicts.rows.length > 0) {
161
+ // make it an insert
162
+ const conflicts = fkConflicts.rows.map(
163
+ (conflict) => conflict[field.name]
164
+ );
165
+ const conflictsSet = new Set(conflicts);
166
+ result = result.filter((del) => !conflictsSet.has(del.ref));
167
+ await saltcorn.data.db.query(
168
+ `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
169
+ set last_modified = null, modified_local = true
170
+ where ref in (${conflicts.join(",")})`
171
+ );
172
+ }
173
+ }
174
+ }
175
+ return result;
176
+ };
177
+
178
+ const applyDeletes = async (allDeletes, syncTimestamp) => {
179
+ for (const [tblName, deletes] of Object.entries(allDeletes)) {
180
+ if (deletes.length > 0) {
181
+ const table = saltcorn.data.models.Table.findOne({ name: tblName });
182
+ const pkName = table.pk_name;
183
+ const safeDeletes = await prepDeletes(table, deletes);
184
+ if (safeDeletes.length > 0) {
185
+ const delIds = safeDeletes.map(({ ref }) => ref).join(",");
186
+ await saltcorn.data.db.query(
187
+ `delete from "${saltcorn.data.db.sqlsanitize(
188
+ tblName
189
+ )}" where "${saltcorn.data.db.sqlsanitize(pkName)}" in (${delIds})`
190
+ );
191
+ await saltcorn.data.db.query(
192
+ `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
193
+ set deleted = true, last_modified = ${syncTimestamp}, modified_local = false
194
+ where ref in (${delIds}) and deleted = false`
195
+ );
196
+ }
197
+ }
198
+ }
199
+ };
200
+
201
+ const syncRemoteDeletes = async (syncInfos, syncTimestamp) => {
202
+ const delResp = await apiCall({
203
+ method: "POST",
204
+ path: "/sync/deletes",
205
+ body: {
206
+ syncTimestamp,
207
+ syncInfos,
208
+ },
209
+ });
210
+ const { deletes } = delResp.data;
211
+ await applyDeletes(deletes, syncTimestamp);
212
+ };
213
+
214
+ const loadOfflineChanges = async (synchedTbls) => {
215
+ const result = {};
216
+ for (const synchedTbl of synchedTbls) {
217
+ const table = saltcorn.data.models.Table.findOne({ name: synchedTbl });
218
+ const pkName = table.pk_name;
219
+ const localModified = await saltcorn.data.db.query(
220
+ `select
221
+ info_tbl.ref as _sync_info_ref_, info_tbl.last_modified as _sync_info_last_modified_,
222
+ info_tbl.deleted as _sync_info_deleted_, info_tbl.modified_local as _sync_info_modified_local_,
223
+ data_tbl.*
224
+ from "${saltcorn.data.db.sqlsanitize(
225
+ synchedTbl
226
+ )}_sync_info" as info_tbl left join "${saltcorn.data.db.sqlsanitize(
227
+ synchedTbl
228
+ )}" as data_tbl
229
+ on info_tbl.ref = data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}"
230
+ where info_tbl.modified_local = true`
231
+ );
232
+ const inserts = [];
233
+ const updates = [];
234
+ const deletes = [];
235
+ for (const row of localModified.rows) {
236
+ const {
237
+ _sync_info_ref_,
238
+ _sync_info_last_modified_,
239
+ _sync_info_deleted_,
240
+ _sync_info_modified_local_,
241
+ ...rest
242
+ } = row;
243
+ const ref = _sync_info_ref_,
244
+ last_modified = _sync_info_last_modified_,
245
+ deleted = _sync_info_deleted_;
246
+ if (deleted)
247
+ deletes.push({
248
+ [pkName]: ref,
249
+ last_modified,
250
+ });
251
+ else if (rest[pkName]) {
252
+ if (!last_modified) inserts.push(rest);
253
+ else updates.push(rest);
254
+ }
255
+ }
256
+ const changes = {};
257
+ if (inserts.length > 0) changes.inserts = inserts;
258
+ if (updates.length > 0) changes.updates = updates;
259
+ if (deletes.length > 0) changes.deletes = deletes;
260
+ if (Object.keys(changes).length > 0) result[synchedTbl] = changes;
261
+ }
262
+ return result;
263
+ };
264
+
265
+ const handleTranslatedIds = async (allUniqueConflicts, allTranslations) => {
266
+ const idToTable = {};
267
+ for (const [tblName, translations] of Object.entries(allTranslations)) {
268
+ const fks = await saltcorn.data.models.Field.find({
269
+ reftable_name: tblName,
270
+ });
271
+ const uniqueConflicts = (allUniqueConflicts[tblName] =
272
+ allUniqueConflicts[tblName] || []);
273
+ const table = saltcorn.data.models.Table.findOne({ name: tblName });
274
+ const transArr = Array.from(Object.entries(translations));
275
+ transArr.sort((a, b) => parseInt(b[1]) - parseInt(a[1]));
276
+ for (const [from, to] of transArr) {
277
+ if (!uniqueConflicts.find((conf) => conf[table.pk_name] === to)) {
278
+ await saltcorn.data.db.update(tblName, { [table.pk_name]: to }, from);
279
+ await saltcorn.data.db.query(
280
+ `update "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
281
+ set ref = ${to}
282
+ where ref = ${from} and deleted = false`
283
+ );
284
+ }
285
+ for (const fk of fks) {
286
+ if (!idToTable[fk.table_id])
287
+ idToTable[fk.table_id] = saltcorn.data.models.Table.findOne(
288
+ fk.table_id
289
+ );
290
+ const refTable = idToTable[fk.table_id];
291
+ await saltcorn.data.db.query(
292
+ `update "${saltcorn.data.db.sqlsanitize(
293
+ refTable.name
294
+ )}" set "${saltcorn.data.db.sqlsanitize(fk.name)}" = ${to}
295
+ where "${fk.name}" = ${from}`
296
+ );
297
+ }
298
+ }
299
+ }
300
+ };
301
+
302
+ const handleUniqueConflicts = async (uniqueConflicts, translatedIds) => {
303
+ for (const [tblName, conflicts] of Object.entries(uniqueConflicts)) {
304
+ const table = saltcorn.data.models.Table.findOne({ name: tblName });
305
+ const pkName = table.pk_name || "id";
306
+ const translated = translatedIds[tblName] || {};
307
+ for (const conflict of conflicts) {
308
+ for (const [from, to] of Object.entries(translated)) {
309
+ if (to === conflict[pkName]) {
310
+ await table.deleteRows({ [pkName]: from });
311
+ await saltcorn.data.db.deleteWhere(`${table.name}_sync_info`, {
312
+ ref: from,
313
+ });
314
+ }
315
+ }
316
+ await saltcorn.data.db.insert(tblName, conflict, { replace: true });
317
+ }
318
+ }
319
+ };
320
+
321
+ const updateSyncInfos = async (
322
+ offlineChanges,
323
+ allTranslations,
324
+ syncTimestamp
325
+ ) => {
326
+ const update = async (tblName, changes, deleted = false) => {
327
+ const table = saltcorn.data.models.Table.findOne({ name: tblName });
328
+ const pkName = table.pk_name;
329
+ const translated = allTranslations[tblName];
330
+ const refIds = Array.from(
331
+ new Set(
332
+ changes.map((change) =>
333
+ deleted
334
+ ? change[pkName]
335
+ : translated?.[change[pkName]] || change[pkName]
336
+ )
337
+ )
338
+ );
339
+ const values = refIds.map(
340
+ (ref) => `(${ref}, ${syncTimestamp}, ${deleted}, false)`
341
+ );
342
+ await saltcorn.data.db.query(
343
+ `delete from "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
344
+ where ref in (${refIds.join(",")})`
345
+ );
346
+ await saltcorn.data.db.query(
347
+ `insert into "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
348
+ (ref, last_modified, deleted, modified_local)
349
+ values ${values.join(",")}
350
+ `
351
+ );
352
+ };
353
+ for (const [tblName, tblChanges] of Object.entries(offlineChanges)) {
354
+ if (tblChanges.inserts) await update(tblName, tblChanges.inserts);
355
+ if (tblChanges.updates) await update(tblName, tblChanges.updates);
356
+ if (tblChanges.deletes) await update(tblName, tblChanges.deletes, true);
357
+ }
358
+ };
359
+
360
+ const syncOfflineData = async (synchedTables, syncTimestamp) => {
361
+ const offlineChanges = await loadOfflineChanges(synchedTables);
362
+ if (Object.keys(offlineChanges).length === 0) return null;
363
+ const uploadResp = await apiCall({
364
+ method: "POST",
365
+ path: "/sync/offline_changes",
366
+ body: {
367
+ changes: offlineChanges,
368
+ syncTimestamp,
369
+ },
370
+ });
371
+ const { syncDir } = uploadResp.data;
372
+ let pollCount = 0;
373
+ while (pollCount < 60) {
374
+ await saltcorn.data.utils.sleep(1000);
375
+ const pollResp = await apiCall({
376
+ method: "GET",
377
+ path: `/sync/upload_finished?dir_name=${encodeURIComponent(syncDir)}`,
378
+ });
379
+ pollCount++;
380
+ const { finished, translatedIds, uniqueConflicts, error } = pollResp.data;
381
+ if (finished) {
382
+ if (error) throw new Error(error.message);
383
+ else {
384
+ await handleUniqueConflicts(uniqueConflicts, translatedIds);
385
+ await handleTranslatedIds(uniqueConflicts, translatedIds);
386
+ await updateSyncInfos(offlineChanges, translatedIds, syncTimestamp);
387
+ return syncDir;
388
+ }
389
+ } else console.log(`poll for syncResult '${syncTimestamp}': ${pollCount}`);
390
+ }
391
+ throw new Error("Unable to get the translatedIds");
392
+ };
393
+
394
+ const cleanSyncDir = async (syncDir) => {
395
+ try {
396
+ await apiCall({
397
+ method: "POST",
398
+ path: "/sync/clean_sync_dir",
399
+ body: {
400
+ dir_name: syncDir,
401
+ },
402
+ });
403
+ } catch (error) {
404
+ console.log(`Unable to clean ${syncDir}`);
405
+ console.log(error);
406
+ }
407
+ };
408
+
409
+ /*
410
+ * check if the server did an upload which didnt't came back to the app
411
+ * the phone shut down or the app was stopped in between
412
+ * Then the data is already uploaded and to avoid conflicts, we do a clean full sync
413
+ */
414
+ const checkCleanSync = async (uploadStarted, uploadStartTime, userName) => {
415
+ if (uploadStarted) {
416
+ const oldSyncDir = `${uploadStartTime}_${userName}`;
417
+ const resp = await apiCall({
418
+ method: "GET",
419
+ path: `/sync/upload_finished?dir_name=${encodeURIComponent(oldSyncDir)}`,
420
+ });
421
+ const { finished, error } = resp.data;
422
+ if (finished && !error) return true;
423
+ else await cleanSyncDir(oldSyncDir);
424
+ }
425
+ return false;
426
+ };
427
+
428
+ const getSyncTimestamp = async () => {
429
+ const resp = await apiCall({
430
+ method: "GET",
431
+ path: `/sync/sync_timestamp`,
432
+ });
433
+ return resp.data.syncTimestamp;
434
+ };
435
+
436
+ const setSpinnerText = () => {
437
+ const iframeWindow = $("#content-iframe")[0].contentWindow;
438
+ if (iframeWindow) {
439
+ const spinnerText =
440
+ iframeWindow.document.getElementById("scspinner-text-id");
441
+ if (spinnerText) {
442
+ spinnerText.innerHTML = "Syncing, please don't turn off";
443
+ spinnerText.classList.remove("d-none");
444
+ }
445
+ }
446
+ };
447
+
448
+ export async function sync() {
449
+ setSpinnerText();
450
+ const state = saltcorn.data.state.getState();
451
+ const { user } = state.mobileConfig;
452
+ const { offlineUser, hasOfflineData, uploadStarted, uploadStartTime } =
453
+ (await getLastOfflineSession()) || {};
454
+ if (offlineUser && hasOfflineData && offlineUser !== user.email) {
455
+ throw new Error(
456
+ `The sync is not available, '${offlineUser}' has not yet uploaded offline data.`
457
+ );
458
+ } else {
459
+ let syncDir = null;
460
+ let cleanSync = await checkCleanSync(
461
+ uploadStarted,
462
+ uploadStartTime,
463
+ user.email
464
+ );
465
+ const syncTimestamp = await getSyncTimestamp();
466
+ await setUploadStarted(true, syncTimestamp);
467
+ let lock = null;
468
+ try {
469
+ if (window.navigator?.wakeLock?.request)
470
+ lock = await window.navigator.wakeLock.request();
471
+ } catch (error) {
472
+ console.log("wakeLock not available");
473
+ console.log(error);
474
+ }
475
+ let transactionOpen = false;
476
+ try {
477
+ await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
478
+ await saltcorn.data.db.query("BEGIN");
479
+ transactionOpen = true;
480
+ if (cleanSync) await clearLocalData(true);
481
+ const { synchedTables, syncInfos } = await prepare();
482
+ await syncRemoteDeletes(syncInfos, syncTimestamp);
483
+ syncDir = await syncOfflineData(synchedTables, syncTimestamp);
484
+ await syncRemoteData(syncInfos, syncTimestamp);
485
+ await endOfflineMode(true);
486
+ await setUploadStarted(false);
487
+ await saltcorn.data.db.query("COMMIT");
488
+ transactionOpen = false;
489
+ await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
490
+ } catch (error) {
491
+ if (transactionOpen) await saltcorn.data.db.query("ROLLBACK");
492
+ await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
493
+ console.log(error);
494
+ throw error;
495
+ } finally {
496
+ if (syncDir) await cleanSyncDir(syncDir);
497
+ if (lock) await lock.release();
498
+ }
499
+ }
500
+ }
501
+
502
+ export async function startOfflineMode() {
503
+ const state = saltcorn.data.state.getState();
504
+ const mobileConfig = state.mobileConfig;
505
+ const oldSession = await getLastOfflineSession();
506
+ if (!oldSession) {
507
+ await setOfflineSession({
508
+ offlineUser: mobileConfig.user.email,
509
+ });
510
+ } else if (
511
+ oldSession.offlineUser &&
512
+ oldSession.offlineUser !== mobileConfig.user.email
513
+ ) {
514
+ if (oldSession.hasOfflineData)
515
+ throw new Error(
516
+ `The offline mode is not available, '${oldSession.offlineUser}' has not yet uploaded offline data.`
517
+ );
518
+ } else if (oldSession.uploadStarted) {
519
+ throw new Error(
520
+ `A previous Synchronization did not finish. Please ${
521
+ mobileConfig.networkState === "none" ? "go online and " : ""
522
+ } try it again.`
523
+ );
524
+ } else {
525
+ await setOfflineSession({
526
+ offlineUser: mobileConfig.user.email,
527
+ });
528
+ }
529
+ mobileConfig.isOfflineMode = true;
530
+ }
531
+
532
+ export async function endOfflineMode(endSession = false) {
533
+ const state = saltcorn.data.state.getState();
534
+ state.mobileConfig.isOfflineMode = false;
535
+ const oldSession = await getLastOfflineSession();
536
+ if ((!oldSession?.uploadStarted && !(await hasOfflineRows())) || endSession)
537
+ await state.setConfig("last_offline_session", null);
538
+ }
539
+
540
+ export async function getLastOfflineSession() {
541
+ const state = saltcorn.data.state.getState();
542
+ return await state.getConfig("last_offline_session");
543
+ }
544
+
545
+ export async function setOfflineSession(sessObj) {
546
+ const state = saltcorn.data.state.getState();
547
+ await state.setConfig("last_offline_session", sessObj);
548
+ }
549
+
550
+ export async function setHasOfflineData(hasOfflineData) {
551
+ const offlineSession = await getLastOfflineSession();
552
+ if (offlineSession?.hasOfflineData !== hasOfflineData) {
553
+ offlineSession.hasOfflineData = hasOfflineData;
554
+ await setOfflineSession(offlineSession);
555
+ }
556
+ }
557
+
558
+ export async function clearLocalData(inTransaction) {
559
+ try {
560
+ await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
561
+ if (!inTransaction) await saltcorn.data.db.query("BEGIN");
562
+ const { synchedTables } = saltcorn.data.state.getState().mobileConfig;
563
+ for (const tblName of synchedTables) {
564
+ const table = saltcorn.data.models.Table.findOne({ name: tblName });
565
+ await table.deleteRows();
566
+ await saltcorn.data.db.deleteWhere(`${table.name}_sync_info`, {});
567
+ }
568
+ if (!inTransaction) await saltcorn.data.db.query("COMMIT");
569
+ await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
570
+ } catch (error) {
571
+ if (!inTransaction) {
572
+ await saltcorn.data.db.query("ROLLBACK");
573
+ await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
574
+ }
575
+ throw error;
576
+ }
577
+ }
578
+
579
+ export function networkChangeCallback(status) {
580
+ console.log("Network status changed", status);
581
+ const mobileConfig = saltcorn.data.state.getState().mobileConfig;
582
+ if (status.connectionType !== "none" && mobileConfig.isOfflineMode) {
583
+ const iframeWindow = $("#content-iframe")[0].contentWindow;
584
+ if (iframeWindow) {
585
+ clearAlerts();
586
+ iframeWindow.notifyAlert(
587
+ `An internet connection is available, to end the offline mode click ${saltcorn.markup.a(
588
+ {
589
+ href: "javascript:execLink('/sync/sync_settings')",
590
+ },
591
+ "here"
592
+ )}`
593
+ );
594
+ }
595
+ }
596
+ mobileConfig.networkState = status.connectionType;
597
+ }
598
+
599
+ export async function hasOfflineRows() {
600
+ const { synchedTables } = saltcorn.data.state.getState().mobileConfig;
601
+ for (const tblName of synchedTables) {
602
+ const table = saltcorn.data.models.Table.findOne({ name: tblName });
603
+ const pkName = table.pk_name;
604
+ const { rows } = await saltcorn.data.db.query(
605
+ `select count(info_tbl.ref)
606
+ from "${saltcorn.data.db.sqlsanitize(
607
+ tblName
608
+ )}_sync_info" as info_tbl
609
+ join "${saltcorn.data.db.sqlsanitize(tblName)}" as data_tbl
610
+ on info_tbl.ref = data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}"
611
+ where info_tbl.modified_local = true`
612
+ );
613
+ if (rows?.length > 0 && parseInt(rows[0].count) > 0) return true;
614
+ }
615
+ return false;
616
+ }
617
+
618
+ export function getOfflineMsg() {
619
+ const { networkState } = saltcorn.data.state.getState().mobileConfig;
620
+ return networkState === "none"
621
+ ? "You are offline."
622
+ : "You are offline, an internet connection is available.";
623
+ }
624
+
625
+ export async function deleteOfflineData(noFeedback) {
626
+ const mobileConfig = saltcorn.data.state.getState().mobileConfig;
627
+ try {
628
+ mobileConfig.inLoadState = true;
629
+ if (!noFeedback) showLoadSpinner();
630
+ await clearLocalData(false);
631
+ await setHasOfflineData(false);
632
+ if (!noFeedback)
633
+ showAlerts([
634
+ {
635
+ type: "info",
636
+ msg: "Deleted your offline data.",
637
+ },
638
+ ]);
639
+ } catch (error) {
640
+ errorAlert(error);
641
+ } finally {
642
+ mobileConfig.inLoadState = false;
643
+ if (!noFeedback) removeLoadSpinner();
644
+ }
645
+ }
package/src/index.js ADDED
@@ -0,0 +1,20 @@
1
+ import { init } from "./init";
2
+ import * as api from "./helpers/api";
3
+ import * as auth from "./helpers/auth";
4
+ import * as common from "./helpers/common";
5
+ import * as fileSystem from "./helpers/file_system";
6
+ import * as navigation from "./helpers/navigation";
7
+ import * as offlineMode from "./helpers/offline_mode";
8
+ import * as dbSchema from "./helpers/db_schema";
9
+ import { router } from "./routing/index";
10
+
11
+ export const mobileApp = {
12
+ init,
13
+ api,
14
+ auth,
15
+ common,
16
+ fileSystem,
17
+ navigation: { ...navigation, router },
18
+ offlineMode,
19
+ dbSchema,
20
+ };