@saltcorn/mobile-app 0.8.6-beta.1 → 0.8.6-beta.3
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 -1
- package/www/index.html +84 -21
- package/www/js/mocks/request.js +9 -4
- package/www/js/routes/auth.js +11 -6
- package/www/js/routes/common.js +13 -2
- package/www/js/routes/delete.js +2 -1
- package/www/js/routes/edit.js +7 -3
- package/www/js/routes/init.js +10 -1
- package/www/js/routes/sync.js +85 -0
- package/www/js/routes/view.js +8 -6
- package/www/js/utils/global_utils.js +70 -12
- package/www/js/utils/iframe_view_utils.js +159 -25
- package/www/js/utils/offline_mode_helper.js +146 -0
- package/www/js/utils/table_utils.js +6 -3
package/package.json
CHANGED
package/www/index.html
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<script src="js/utils/iframe_view_utils.js"></script>
|
|
11
11
|
<script src="js/utils/file_helpers.js"></script>
|
|
12
12
|
<script src="js/utils/table_utils.js"></script>
|
|
13
|
+
<script src="js/utils/offline_mode_helper.js"></script>
|
|
13
14
|
|
|
14
15
|
<script src="js/mocks/request.js"></script>
|
|
15
16
|
<script src="js/mocks/response.js"></script>
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
<script src="js/routes/error.js"></script>
|
|
21
22
|
<script src="js/routes/page.js"></script>
|
|
22
23
|
<script src="js/routes/view.js"></script>
|
|
24
|
+
<script src="js/routes/sync.js"></script>
|
|
23
25
|
<script src="js/routes/init.js"></script>
|
|
24
26
|
|
|
25
27
|
<script src="js/mocks/response.js"></script>
|
|
@@ -124,7 +126,7 @@
|
|
|
124
126
|
config.pluginHeaders.push(prepareHeader(header));
|
|
125
127
|
}
|
|
126
128
|
else if (typeof pluginHeaders === "function") {
|
|
127
|
-
const headerResult = pluginHeaders(row.configuration);
|
|
129
|
+
const headerResult = pluginHeaders(row.configuration || {});
|
|
128
130
|
if (Array.isArray(headerResult)) {
|
|
129
131
|
for (const header of headerResult)
|
|
130
132
|
config.pluginHeaders.push(prepareHeader(header));
|
|
@@ -146,29 +148,15 @@
|
|
|
146
148
|
const initJwt = async () => {
|
|
147
149
|
if (!(await saltcorn.data.db.tableExists("jwt_table"))) {
|
|
148
150
|
await createJwtTable();
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
+
} else {
|
|
151
152
|
const jwt = await getJwt();
|
|
152
|
-
if(jwt) {
|
|
153
|
+
if (jwt) {
|
|
153
154
|
const state = saltcorn.data.state.getState();
|
|
154
155
|
state.mobileConfig.jwt = jwt;
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
};
|
|
158
159
|
|
|
159
|
-
const checkJWT = async () => {
|
|
160
|
-
const state = saltcorn.data.state.getState();
|
|
161
|
-
const jwt = state.mobileConfig.jwt;
|
|
162
|
-
if(jwt && jwt !== "undefined") {
|
|
163
|
-
const response = await apiCall({
|
|
164
|
-
method: "GET",
|
|
165
|
-
path: "/auth/authenticated",
|
|
166
|
-
});
|
|
167
|
-
return response.data.authenticated;
|
|
168
|
-
}
|
|
169
|
-
else return false;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
160
|
const initI18Next = async () => {
|
|
173
161
|
const resources = {};
|
|
174
162
|
for (const key of Object.keys(
|
|
@@ -188,8 +176,38 @@
|
|
|
188
176
|
});
|
|
189
177
|
};
|
|
190
178
|
|
|
179
|
+
const tryDownloadServerData = async (alerts) => {
|
|
180
|
+
try {
|
|
181
|
+
await offlineHelper.downloadServerData();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
alerts.push({
|
|
184
|
+
type: "warning",
|
|
185
|
+
msg: `Unable to download the server data: ${
|
|
186
|
+
error.message ? error.message : "unknown error"
|
|
187
|
+
}`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// the app comes back from background
|
|
193
|
+
const onResume = async () => {
|
|
194
|
+
const state = saltcorn.data.state.getState();
|
|
195
|
+
const mobileConfig = state.mobileConfig;
|
|
196
|
+
if (mobileConfig.allowOfflineMode) {
|
|
197
|
+
mobileConfig.networkState = navigator.connection.type;
|
|
198
|
+
if (
|
|
199
|
+
mobileConfig.networkState === "none" &&
|
|
200
|
+
!mobileConfig.isOfflineMode
|
|
201
|
+
) {
|
|
202
|
+
// starts the offline mode, if needed
|
|
203
|
+
await gotoEntryView();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
191
208
|
// device is ready
|
|
192
209
|
const init = async () => {
|
|
210
|
+
document.addEventListener("resume", onResume, false);
|
|
193
211
|
const config = await readJSON(
|
|
194
212
|
"config",
|
|
195
213
|
`${cordova.file.applicationDirectory}www`
|
|
@@ -227,28 +245,74 @@
|
|
|
227
245
|
setVersionTtag();
|
|
228
246
|
const entryPoint = config.entry_point;
|
|
229
247
|
await initI18Next();
|
|
248
|
+
state.mobileConfig.networkState = navigator.connection.type;
|
|
249
|
+
document.addEventListener(
|
|
250
|
+
"offline",
|
|
251
|
+
offlineHelper.offlineCallback,
|
|
252
|
+
false
|
|
253
|
+
);
|
|
254
|
+
document.addEventListener(
|
|
255
|
+
"online",
|
|
256
|
+
offlineHelper.onlineCallback,
|
|
257
|
+
false
|
|
258
|
+
);
|
|
259
|
+
const networkDisabled = state.mobileConfig.networkState === "none";
|
|
260
|
+
const jwt = state.mobileConfig.jwt;
|
|
230
261
|
try {
|
|
231
|
-
|
|
262
|
+
const alerts = [];
|
|
263
|
+
if ((networkDisabled && jwt) || (await checkJWT(jwt))) {
|
|
232
264
|
const mobileConfig = state.mobileConfig;
|
|
233
265
|
const decodedJwt = jwt_decode(mobileConfig.jwt);
|
|
234
266
|
mobileConfig.role_id = decodedJwt.user.role_id
|
|
235
267
|
? decodedJwt.user.role_id
|
|
236
|
-
:
|
|
268
|
+
: 100;
|
|
269
|
+
mobileConfig.user_id = decodedJwt.user.id;
|
|
237
270
|
mobileConfig.user_name = decodedJwt.user.email;
|
|
238
271
|
mobileConfig.language = decodedJwt.user.language;
|
|
239
272
|
mobileConfig.isPublicUser = false;
|
|
240
273
|
await i18next.changeLanguage(mobileConfig.language);
|
|
274
|
+
if (mobileConfig.allowOfflineMode) {
|
|
275
|
+
const userWithOfflineData = await offlineHelper.lastOfflineUser();
|
|
276
|
+
if (networkDisabled) {
|
|
277
|
+
if (
|
|
278
|
+
userWithOfflineData &&
|
|
279
|
+
userWithOfflineData !== mobileConfig.user_name
|
|
280
|
+
)
|
|
281
|
+
throw new Error(
|
|
282
|
+
`The offline mode is not available, '${userWithOfflineData}' has not yet uploaded offline data.`
|
|
283
|
+
);
|
|
284
|
+
else {
|
|
285
|
+
await offlineHelper.startOfflineMode();
|
|
286
|
+
alerts.push({ type: "info", msg: "You are in offline mode" });
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
if (userWithOfflineData) {
|
|
290
|
+
if (userWithOfflineData === mobileConfig.user_name)
|
|
291
|
+
alerts.push({
|
|
292
|
+
type: "info",
|
|
293
|
+
msg: "You have offline data, open the Sync menu to handle it.",
|
|
294
|
+
});
|
|
295
|
+
else
|
|
296
|
+
alerts.push({
|
|
297
|
+
type: "warning",
|
|
298
|
+
msg: `'${userWithOfflineData}' has not yet uploaded offline data.`,
|
|
299
|
+
});
|
|
300
|
+
} else await tryDownloadServerData(alerts);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
241
303
|
addRoute({ route: entryPoint, query: undefined });
|
|
242
304
|
const page = await router.resolve({
|
|
243
305
|
pathname: entryPoint,
|
|
244
306
|
fullWrap: true,
|
|
307
|
+
alerts,
|
|
245
308
|
});
|
|
246
309
|
await replaceIframe(page.content);
|
|
247
310
|
} else {
|
|
248
311
|
const page = await router.resolve({
|
|
249
312
|
pathname: "get/auth/login",
|
|
313
|
+
alerts,
|
|
250
314
|
});
|
|
251
|
-
replaceIframe(page.content);
|
|
315
|
+
await replaceIframe(page.content);
|
|
252
316
|
}
|
|
253
317
|
} catch (error) {
|
|
254
318
|
const page = await router.resolve({
|
|
@@ -261,7 +325,6 @@
|
|
|
261
325
|
],
|
|
262
326
|
});
|
|
263
327
|
await replaceIframe(page.content);
|
|
264
|
-
console.error(error);
|
|
265
328
|
}
|
|
266
329
|
};
|
|
267
330
|
|
package/www/js/mocks/request.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/*global i18next, saltcorn*/
|
|
2
2
|
|
|
3
|
-
function MobileRequest({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
function MobileRequest({
|
|
4
|
+
xhr = false,
|
|
5
|
+
files = undefined,
|
|
6
|
+
query = undefined,
|
|
7
|
+
} = {}) {
|
|
8
|
+
const cfg = saltcorn.data.state.getState().mobileConfig;
|
|
9
|
+
const roleId = cfg.role_id ? cfg.role_id : 100;
|
|
10
|
+
const userId = cfg.user_id ? cfg.user_id : undefined;
|
|
7
11
|
const flashMessages = [];
|
|
8
12
|
|
|
9
13
|
return {
|
|
@@ -21,6 +25,7 @@ function MobileRequest({ xhr = false, files = undefined, query = undefined }) {
|
|
|
21
25
|
return mobileCfg?.language ? mobileCfg.language : "en";
|
|
22
26
|
},
|
|
23
27
|
user: {
|
|
28
|
+
id: userId,
|
|
24
29
|
role_id: roleId,
|
|
25
30
|
},
|
|
26
31
|
flash: (type, msg) => {
|
package/www/js/routes/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*global sbAdmin2Layout, apiCall, removeJwt, saltcorn*/
|
|
1
|
+
/*global sbAdmin2Layout, apiCall, removeJwt, saltcorn, clearHistory*/
|
|
2
2
|
|
|
3
3
|
const prepareAuthForm = () => {
|
|
4
4
|
return new saltcorn.data.models.Form({
|
|
@@ -35,7 +35,7 @@ const getAuthLinks = (current, entryPoint) => {
|
|
|
35
35
|
return links;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
const renderLoginView = (entryPoint, versionTag) => {
|
|
38
|
+
const renderLoginView = (entryPoint, versionTag, alerts = []) => {
|
|
39
39
|
const form = prepareAuthForm(entryPoint);
|
|
40
40
|
form.onSubmit = `javascript:loginFormSubmit(this, '${entryPoint}')`;
|
|
41
41
|
form.submitLabel = "Login";
|
|
@@ -43,7 +43,7 @@ const renderLoginView = (entryPoint, versionTag) => {
|
|
|
43
43
|
title: "login",
|
|
44
44
|
form: form,
|
|
45
45
|
authLinks: getAuthLinks("login", entryPoint),
|
|
46
|
-
alerts
|
|
46
|
+
alerts,
|
|
47
47
|
headers: [
|
|
48
48
|
{ css: `static_assets/${versionTag}/saltcorn.css` },
|
|
49
49
|
{ script: "js/utils/iframe_view_utils.js" },
|
|
@@ -69,10 +69,14 @@ const renderSignupView = (entryPoint, versionTag) => {
|
|
|
69
69
|
});
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
const getLoginView = async () => {
|
|
73
|
-
const
|
|
72
|
+
const getLoginView = async (context) => {
|
|
73
|
+
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
74
74
|
return {
|
|
75
|
-
content: renderLoginView(
|
|
75
|
+
content: renderLoginView(
|
|
76
|
+
mobileConfig.entry_point,
|
|
77
|
+
mobileConfig.version_tag,
|
|
78
|
+
context.alerts ? context.alerts : []
|
|
79
|
+
),
|
|
76
80
|
replaceIframe: true,
|
|
77
81
|
};
|
|
78
82
|
};
|
|
@@ -90,6 +94,7 @@ const logoutAction = async () => {
|
|
|
90
94
|
const response = await apiCall({ method: "GET", path: "/auth/logout" });
|
|
91
95
|
if (response.data.success) {
|
|
92
96
|
await removeJwt();
|
|
97
|
+
clearHistory();
|
|
93
98
|
config.jwt = undefined;
|
|
94
99
|
return {
|
|
95
100
|
content: renderLoginView(config.entry_point, config.version_tag),
|
package/www/js/routes/common.js
CHANGED
|
@@ -34,8 +34,8 @@ const sbAdmin2Layout = () => {
|
|
|
34
34
|
|
|
35
35
|
const getMenu = (req) => {
|
|
36
36
|
const state = saltcorn.data.state.getState();
|
|
37
|
-
const mobileCfg =
|
|
38
|
-
const role = mobileCfg.role_id ||
|
|
37
|
+
const mobileCfg = state.mobileConfig;
|
|
38
|
+
const role = mobileCfg.role_id || 100;
|
|
39
39
|
const extraMenu = saltcorn.data.web_mobile_commons.get_extra_menu(
|
|
40
40
|
role,
|
|
41
41
|
req.__
|
|
@@ -80,6 +80,17 @@ const getMenu = (req) => {
|
|
|
80
80
|
isUser: true,
|
|
81
81
|
items: authItems,
|
|
82
82
|
});
|
|
83
|
+
if (mobileCfg.allowOfflineMode)
|
|
84
|
+
result.push({
|
|
85
|
+
section: "Sync",
|
|
86
|
+
items: [
|
|
87
|
+
{
|
|
88
|
+
link: "javascript:execLink('/sync/sync_settings')",
|
|
89
|
+
icon: "fas fa-sync",
|
|
90
|
+
label: "Sync",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
83
94
|
return result;
|
|
84
95
|
};
|
|
85
96
|
|
package/www/js/routes/delete.js
CHANGED
|
@@ -5,7 +5,8 @@ const deleteRows = async (context) => {
|
|
|
5
5
|
const { name, id } = context.params;
|
|
6
6
|
const table = await saltcorn.data.models.Table.findOne({ name });
|
|
7
7
|
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
8
|
-
|
|
8
|
+
if (mobileConfig.isOfflineMode)
|
|
9
|
+
throw new Error(i18next.t("Deletes are not supported in offline mode."));
|
|
9
10
|
if (mobileConfig.localTableIds.indexOf(table.id) >= 0) {
|
|
10
11
|
if (mobileConfig.role_id <= table.min_role_write) {
|
|
11
12
|
await table.deleteRows({ id });
|
package/www/js/routes/edit.js
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
const postToggleField = async (context) => {
|
|
5
5
|
const { name, id, field_name } = context.params;
|
|
6
6
|
const table = await saltcorn.data.models.Table.findOne({ name });
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const state = saltcorn.data.state.getState();
|
|
8
|
+
const { isOfflineMode, localTableIds, user_name, role_id } =
|
|
9
|
+
state.mobileConfig;
|
|
10
|
+
if (isOfflineMode || localTableIds.indexOf(table.id) >= 0) {
|
|
11
|
+
if (role_id > table.min_role_write)
|
|
10
12
|
throw new Error(i18next.t("Not authorized"));
|
|
11
13
|
await table.toggleBool(+id, field_name);
|
|
14
|
+
if (isOfflineMode)
|
|
15
|
+
await state.setConfig("user_with_offline_data", user_name);
|
|
12
16
|
} else {
|
|
13
17
|
await apiCall({
|
|
14
18
|
method: "POST",
|
package/www/js/routes/init.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/*global postView, postViewRoute, getView, postToggleField, deleteRows, postPageAction, getPage, getLoginView, logoutAction, getSignupView, getErrorView, window*/
|
|
1
|
+
/*global postView, postViewRoute, getView, postToggleField, deleteRows, postPageAction, getPage, getLoginView, logoutAction, getSignupView, getErrorView, window, getSyncView, getSyncModalContent, getAskOverwriteDialog, getSyncSettingsView*/
|
|
2
|
+
// TODO module namespacese
|
|
2
3
|
|
|
3
4
|
const initRoutes = async () => {
|
|
4
5
|
const routes = [
|
|
@@ -46,6 +47,14 @@ const initRoutes = async () => {
|
|
|
46
47
|
path: "get/error_page",
|
|
47
48
|
action: getErrorView,
|
|
48
49
|
},
|
|
50
|
+
{
|
|
51
|
+
path: "get/sync/sync_settings",
|
|
52
|
+
action: getSyncSettingsView,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: "get/sync/ask_overwrite",
|
|
56
|
+
action: getAskOverwriteDialog,
|
|
57
|
+
},
|
|
49
58
|
];
|
|
50
59
|
window.router = new window.UniversalRouter(routes);
|
|
51
60
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*global saltcorn, wrapContents, MobileRequest, */
|
|
2
|
+
|
|
3
|
+
const getSyncSettingsView = (context) => {
|
|
4
|
+
const content = saltcorn.markup.div(
|
|
5
|
+
{ class: "container" },
|
|
6
|
+
saltcorn.markup.div(
|
|
7
|
+
{ class: "row" },
|
|
8
|
+
saltcorn.markup.div(
|
|
9
|
+
{ class: "col-9" },
|
|
10
|
+
saltcorn.markup.div(
|
|
11
|
+
{ class: "fs-6 fw-bold text-decoration-underline" },
|
|
12
|
+
"Upload offline data"
|
|
13
|
+
),
|
|
14
|
+
saltcorn.markup.div(
|
|
15
|
+
"Upload the data from your last offline session to the server."
|
|
16
|
+
)
|
|
17
|
+
),
|
|
18
|
+
saltcorn.markup.div(
|
|
19
|
+
{ class: "col-3" },
|
|
20
|
+
saltcorn.markup.button(
|
|
21
|
+
{
|
|
22
|
+
class: "btn btn-primary",
|
|
23
|
+
type: "button",
|
|
24
|
+
onClick: "callUploadSync()",
|
|
25
|
+
},
|
|
26
|
+
saltcorn.markup.i({ class: "fas fa-sync" })
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
),
|
|
30
|
+
saltcorn.markup.hr(),
|
|
31
|
+
saltcorn.markup.div(
|
|
32
|
+
{ class: "row" },
|
|
33
|
+
saltcorn.markup.div(
|
|
34
|
+
{ class: "col-9" },
|
|
35
|
+
saltcorn.markup.div(
|
|
36
|
+
{ class: "fs-6 fw-bold text-decoration-underline" },
|
|
37
|
+
"Download server data"
|
|
38
|
+
),
|
|
39
|
+
saltcorn.markup.div(
|
|
40
|
+
"Download the latest data for your next offline session."
|
|
41
|
+
)
|
|
42
|
+
),
|
|
43
|
+
saltcorn.markup.div(
|
|
44
|
+
{ class: "col-3" },
|
|
45
|
+
saltcorn.markup.button(
|
|
46
|
+
{
|
|
47
|
+
class: "btn btn-primary",
|
|
48
|
+
type: "button",
|
|
49
|
+
onClick: "callDownloadSync()",
|
|
50
|
+
},
|
|
51
|
+
saltcorn.markup.i({ class: "fas fa-sync" })
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
),
|
|
55
|
+
saltcorn.markup.hr()
|
|
56
|
+
);
|
|
57
|
+
return wrapContents(content, "Sync Settings", context, new MobileRequest());
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// get/sync/ask_overwrite
|
|
61
|
+
const getAskOverwriteDialog = (context) => {
|
|
62
|
+
const content = saltcorn.markup.div(
|
|
63
|
+
saltcorn.markup.div(
|
|
64
|
+
{ class: "mb-3 h6" },
|
|
65
|
+
"This replaces your offline data."
|
|
66
|
+
),
|
|
67
|
+
saltcorn.markup.button(
|
|
68
|
+
{
|
|
69
|
+
class: "btn btn-secondary me-2",
|
|
70
|
+
type: "button",
|
|
71
|
+
"data-bs-dismiss": "modal",
|
|
72
|
+
},
|
|
73
|
+
"Close"
|
|
74
|
+
),
|
|
75
|
+
saltcorn.markup.button(
|
|
76
|
+
{
|
|
77
|
+
class: "btn btn-primary close",
|
|
78
|
+
type: "button",
|
|
79
|
+
onClick: "closeModal(); callDownloadSync(true)",
|
|
80
|
+
},
|
|
81
|
+
"Download anyway"
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
return wrapContents(content, "Warning", context, new MobileRequest());
|
|
85
|
+
};
|
package/www/js/routes/view.js
CHANGED
|
@@ -18,8 +18,9 @@ const postView = async (context) => {
|
|
|
18
18
|
const req = new MobileRequest({ xhr: context.xhr, files: context.files });
|
|
19
19
|
const res = new MobileResponse();
|
|
20
20
|
const state = saltcorn.data.state.getState();
|
|
21
|
+
const mobileCfg = state.mobileConfig;
|
|
21
22
|
if (
|
|
22
|
-
|
|
23
|
+
mobileCfg.role_id > view.min_role &&
|
|
23
24
|
!(await view.authorise_post({ body, req, ...view }))
|
|
24
25
|
) {
|
|
25
26
|
throw new Error(req.__("Not authorized"));
|
|
@@ -34,6 +35,8 @@ const postView = async (context) => {
|
|
|
34
35
|
},
|
|
35
36
|
view.isRemoteTable()
|
|
36
37
|
);
|
|
38
|
+
if (mobileCfg.isOfflineMode)
|
|
39
|
+
await state.setConfig("user_with_offline_data", mobileCfg.user_name);
|
|
37
40
|
return res.getJson();
|
|
38
41
|
};
|
|
39
42
|
|
|
@@ -48,9 +51,8 @@ const postViewRoute = async (context) => {
|
|
|
48
51
|
const req = new MobileRequest({ xhr: context.xhr });
|
|
49
52
|
const res = new MobileResponse();
|
|
50
53
|
const state = saltcorn.data.state.getState();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
+
const { role_id, isOfflineMode, user_name } = state.mobileConfig;
|
|
55
|
+
if (role_id > view.min_role) throw new Error(req.__("Not authorized"));
|
|
54
56
|
await view.runRoute(
|
|
55
57
|
context.params.route,
|
|
56
58
|
context.data,
|
|
@@ -58,6 +60,7 @@ const postViewRoute = async (context) => {
|
|
|
58
60
|
{ req, res },
|
|
59
61
|
view.isRemoteTable()
|
|
60
62
|
);
|
|
63
|
+
if (isOfflineMode) await state.setConfig("user_with_offline_data", user_name);
|
|
61
64
|
return res.getJson();
|
|
62
65
|
};
|
|
63
66
|
|
|
@@ -75,9 +78,8 @@ const getView = async (context) => {
|
|
|
75
78
|
if (
|
|
76
79
|
state.mobileConfig.role_id > view.min_role &&
|
|
77
80
|
!(await view.authorise_get({ query, req, ...view }))
|
|
78
|
-
)
|
|
81
|
+
)
|
|
79
82
|
throw new Error(req.__("Not authorized"));
|
|
80
|
-
}
|
|
81
83
|
const contents = await view.run_possibly_on_page(
|
|
82
84
|
query,
|
|
83
85
|
req,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*global axios, write, cordova, router, getDirEntry, saltcorn, document, FileReader, navigator*/
|
|
1
|
+
/*global offlineHelper, axios, write, cordova, router, getDirEntry, saltcorn, document, FileReader, navigator*/
|
|
2
2
|
|
|
3
3
|
let routingHistory = [];
|
|
4
4
|
|
|
@@ -16,6 +16,14 @@ function addRoute(routeEntry) {
|
|
|
16
16
|
routingHistory.push(routeEntry);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function clearHistory() {
|
|
20
|
+
routingHistory = [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function popRoute() {
|
|
24
|
+
routingHistory.pop();
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
async function apiCall({ method, path, params, body, responseType }) {
|
|
20
28
|
const config = saltcorn.data.state.getState().mobileConfig;
|
|
21
29
|
const serverPath = config.server_path;
|
|
@@ -43,6 +51,13 @@ async function apiCall({ method, path, params, body, responseType }) {
|
|
|
43
51
|
}
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
function clearAlerts() {
|
|
55
|
+
const iframe = document.getElementById("content-iframe");
|
|
56
|
+
const alertsArea =
|
|
57
|
+
iframe.contentWindow.document.getElementById("alerts-area");
|
|
58
|
+
alertsArea.innerHTML = "";
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
function showAlerts(alerts) {
|
|
47
62
|
const iframe = document.getElementById("content-iframe");
|
|
48
63
|
const alertsArea =
|
|
@@ -136,27 +151,58 @@ async function replaceIframeInnerContent(content) {
|
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
async function gotoEntryView() {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
155
|
+
try {
|
|
156
|
+
if (
|
|
157
|
+
mobileConfig.networkState === "none" &&
|
|
158
|
+
mobileConfig.allowOfflineMode &&
|
|
159
|
+
!mobileConfig.isOfflineMode
|
|
160
|
+
)
|
|
161
|
+
await offlineHelper.startOfflineMode();
|
|
162
|
+
const page = await router.resolve({
|
|
163
|
+
pathname: mobileConfig.entry_point,
|
|
164
|
+
alerts: mobileConfig.isOfflineMode
|
|
165
|
+
? [{ type: "info", msg: "You are in offline mode" }]
|
|
166
|
+
: [],
|
|
167
|
+
});
|
|
168
|
+
addRoute({ route: mobileConfig.entry_point, query: undefined });
|
|
169
|
+
await replaceIframeInnerContent(page.content);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
showAlerts([
|
|
172
|
+
{
|
|
173
|
+
type: "error",
|
|
174
|
+
msg: error.message ? error.message : "An error occured.",
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
146
178
|
}
|
|
147
179
|
|
|
148
180
|
async function handleRoute(route, query, files) {
|
|
181
|
+
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
149
182
|
try {
|
|
183
|
+
if (
|
|
184
|
+
mobileConfig.networkState === "none" &&
|
|
185
|
+
mobileConfig.allowOfflineMode &&
|
|
186
|
+
!mobileConfig.isOfflineMode
|
|
187
|
+
)
|
|
188
|
+
await offlineHelper.startOfflineMode();
|
|
150
189
|
if (route === "/") return await gotoEntryView();
|
|
151
190
|
addRoute({ route, query });
|
|
152
191
|
const page = await router.resolve({
|
|
153
192
|
pathname: route,
|
|
154
193
|
query: query,
|
|
155
194
|
files: files,
|
|
195
|
+
alerts: mobileConfig.isOfflineMode
|
|
196
|
+
? [{ type: "info", msg: "You are in offline mode" }]
|
|
197
|
+
: [],
|
|
156
198
|
});
|
|
157
199
|
if (page.redirect) {
|
|
158
|
-
|
|
159
|
-
|
|
200
|
+
if (page.redirect.startsWith("http://localhost")) {
|
|
201
|
+
await gotoEntryView();
|
|
202
|
+
} else {
|
|
203
|
+
const { path, query } = splitPathQuery(page.redirect);
|
|
204
|
+
await handleRoute(path, query);
|
|
205
|
+
}
|
|
160
206
|
} else if (page.content) {
|
|
161
207
|
if (!page.replaceIframe) await replaceIframeInnerContent(page.content);
|
|
162
208
|
else await replaceIframe(page.content);
|
|
@@ -168,12 +214,14 @@ async function handleRoute(route, query, files) {
|
|
|
168
214
|
msg: error.message ? error.message : "An error occured.",
|
|
169
215
|
},
|
|
170
216
|
]);
|
|
171
|
-
console.error(error);
|
|
172
217
|
}
|
|
173
218
|
}
|
|
174
219
|
|
|
175
220
|
async function goBack(steps = 1, exitOnFirstPage = false) {
|
|
176
|
-
if (
|
|
221
|
+
if (
|
|
222
|
+
routingHistory.length === 0 ||
|
|
223
|
+
(exitOnFirstPage && routingHistory.length === 1)
|
|
224
|
+
) {
|
|
177
225
|
navigator.app.exitApp();
|
|
178
226
|
} else if (routingHistory.length <= steps) {
|
|
179
227
|
routingHistory = [];
|
|
@@ -198,3 +246,13 @@ function errorAlert(error) {
|
|
|
198
246
|
]);
|
|
199
247
|
console.error(error);
|
|
200
248
|
}
|
|
249
|
+
|
|
250
|
+
async function checkJWT(jwt) {
|
|
251
|
+
if (jwt && jwt !== "undefined") {
|
|
252
|
+
const response = await apiCall({
|
|
253
|
+
method: "GET",
|
|
254
|
+
path: "/auth/authenticated",
|
|
255
|
+
});
|
|
256
|
+
return response.data.authenticated;
|
|
257
|
+
} else return false;
|
|
258
|
+
}
|
|
@@ -97,27 +97,47 @@ async function login(e, entryPoint, isSignup) {
|
|
|
97
97
|
if (typeof loginResult === "string") {
|
|
98
98
|
// use it as a token
|
|
99
99
|
const decodedJwt = parent.jwt_decode(loginResult);
|
|
100
|
-
const
|
|
101
|
-
config
|
|
100
|
+
const state = parent.saltcorn.data.state.getState();
|
|
101
|
+
const config = state.mobileConfig;
|
|
102
|
+
config.role_id = decodedJwt.user.role_id ? decodedJwt.user.role_id : 100;
|
|
102
103
|
config.user_name = decodedJwt.user.email;
|
|
104
|
+
config.user_id = decodedJwt.user.id;
|
|
103
105
|
config.language = decodedJwt.user.language;
|
|
104
106
|
config.isPublicUser = false;
|
|
107
|
+
config.isOfflineMode = false;
|
|
105
108
|
await parent.setJwt(loginResult);
|
|
106
109
|
config.jwt = loginResult;
|
|
107
110
|
await parent.i18next.changeLanguage(config.language);
|
|
111
|
+
const alerts = [];
|
|
112
|
+
if (config.allowOfflineMode) {
|
|
113
|
+
const userWithOfflineData = await parent.offlineHelper.lastOfflineUser();
|
|
114
|
+
if (!userWithOfflineData) await parent.offlineHelper.downloadServerData();
|
|
115
|
+
else {
|
|
116
|
+
if (userWithOfflineData === config.user_name) {
|
|
117
|
+
alerts.push({
|
|
118
|
+
type: "info",
|
|
119
|
+
msg: "You have offline data, open the Sync menu to handle it.",
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
alerts.push({
|
|
123
|
+
type: "warning",
|
|
124
|
+
msg: `'${userWithOfflineData}' has not yet uploaded offline data.`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
alerts.push({
|
|
130
|
+
type: "success",
|
|
131
|
+
msg: parent.i18next.t("Welcome, %s!", {
|
|
132
|
+
postProcess: "sprintf",
|
|
133
|
+
sprintf: [config.user_name],
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
108
136
|
parent.addRoute({ route: entryPoint, query: undefined });
|
|
109
137
|
const page = await parent.router.resolve({
|
|
110
138
|
pathname: entryPoint,
|
|
111
139
|
fullWrap: true,
|
|
112
|
-
alerts
|
|
113
|
-
{
|
|
114
|
-
type: "success",
|
|
115
|
-
msg: parent.i18next.t("Welcome, %s!", {
|
|
116
|
-
postProcess: "sprintf",
|
|
117
|
-
sprintf: [config.user_name],
|
|
118
|
-
}),
|
|
119
|
-
},
|
|
120
|
-
],
|
|
140
|
+
alerts,
|
|
121
141
|
});
|
|
122
142
|
await parent.replaceIframe(page.content);
|
|
123
143
|
} else if (loginResult?.alerts) {
|
|
@@ -132,7 +152,7 @@ async function publicLogin(entryPoint) {
|
|
|
132
152
|
const loginResult = await loginRequest({ isPublic: true });
|
|
133
153
|
if (typeof loginResult === "string") {
|
|
134
154
|
const config = parent.saltcorn.data.state.getState().mobileConfig;
|
|
135
|
-
config.role_id =
|
|
155
|
+
config.role_id = 100;
|
|
136
156
|
config.user_name = "public";
|
|
137
157
|
config.language = "en";
|
|
138
158
|
config.isPublicUser = true;
|
|
@@ -312,9 +332,9 @@ async function gopage(n, pagesize, viewIdentifier, extra) {
|
|
|
312
332
|
);
|
|
313
333
|
}
|
|
314
334
|
|
|
315
|
-
function mobile_modal(url, opts = {}) {
|
|
335
|
+
async function mobile_modal(url, opts = {}) {
|
|
316
336
|
if ($("#scmodal").length === 0) {
|
|
317
|
-
$("body").append(`<div id="scmodal"
|
|
337
|
+
$("body").append(`<div id="scmodal" class="modal">
|
|
318
338
|
<div class="modal-dialog">
|
|
319
339
|
<div class="modal-content">
|
|
320
340
|
<div class="modal-header">
|
|
@@ -333,18 +353,41 @@ function mobile_modal(url, opts = {}) {
|
|
|
333
353
|
var modal = bootstrap.Modal.getInstance(myModalEl);
|
|
334
354
|
modal.dispose();
|
|
335
355
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
356
|
+
try {
|
|
357
|
+
const { path, query } = parent.splitPathQuery(url);
|
|
358
|
+
// submitReload ?
|
|
359
|
+
const mobileConfig = parent.saltcorn.data.state.getState().mobileConfig;
|
|
360
|
+
if (
|
|
361
|
+
mobileConfig.networkState === "none" &&
|
|
362
|
+
mobileConfig.allowOfflineMode &&
|
|
363
|
+
!mobileConfig.isOfflineMode
|
|
364
|
+
)
|
|
365
|
+
await parent.offlineHelper.startOfflineMode();
|
|
366
|
+
const page = await parent.router.resolve({
|
|
367
|
+
pathname: `get${path}`,
|
|
368
|
+
query: query,
|
|
369
|
+
alerts: mobileConfig.isOfflineMode
|
|
370
|
+
? [{ type: "info", msg: "You are in offline mode" }]
|
|
371
|
+
: [],
|
|
347
372
|
});
|
|
373
|
+
const modalContent = page.content;
|
|
374
|
+
const title = page.title;
|
|
375
|
+
if (title) $("#scmodal .modal-title").html(title);
|
|
376
|
+
$("#scmodal .modal-body").html(modalContent);
|
|
377
|
+
new bootstrap.Modal($("#scmodal")).show();
|
|
378
|
+
// onOpen onClose initialize_page?
|
|
379
|
+
} catch (error) {
|
|
380
|
+
parent.showAlerts([
|
|
381
|
+
{
|
|
382
|
+
type: "error",
|
|
383
|
+
msg: error.message ? error.message : "An error occured.",
|
|
384
|
+
},
|
|
385
|
+
]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function closeModal() {
|
|
390
|
+
$("#scmodal").modal("toggle");
|
|
348
391
|
}
|
|
349
392
|
|
|
350
393
|
async function local_post(url, args) {
|
|
@@ -481,6 +524,97 @@ async function view_post(viewname, route, data, onDone) {
|
|
|
481
524
|
}
|
|
482
525
|
}
|
|
483
526
|
|
|
527
|
+
async function callUploadSync() {
|
|
528
|
+
if (!(await parent.offlineHelper.lastOfflineUser())) {
|
|
529
|
+
parent.showAlerts([
|
|
530
|
+
{
|
|
531
|
+
type: "error",
|
|
532
|
+
msg: "You don't have any offline data.",
|
|
533
|
+
},
|
|
534
|
+
]);
|
|
535
|
+
} else {
|
|
536
|
+
showLoadSpinner();
|
|
537
|
+
try {
|
|
538
|
+
await parent.offlineHelper.uploadLocalData();
|
|
539
|
+
await parent.offlineHelper.endOfflineMode();
|
|
540
|
+
parent.clearAlerts();
|
|
541
|
+
parent.showAlerts([
|
|
542
|
+
{
|
|
543
|
+
type: "info",
|
|
544
|
+
msg: "Sucessfully uploaded your local data.",
|
|
545
|
+
},
|
|
546
|
+
]);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
parent.errorAlert(error);
|
|
549
|
+
} finally {
|
|
550
|
+
removeLoadSpinner();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function callDownloadSync(force = false) {
|
|
556
|
+
const lastOfflineUser = await parent.offlineHelper.lastOfflineUser();
|
|
557
|
+
const { user_name } = parent.saltcorn.data.state.getState().mobileConfig;
|
|
558
|
+
if (lastOfflineUser === user_name && !force) {
|
|
559
|
+
await mobile_modal("/sync/ask_overwrite");
|
|
560
|
+
} else if (lastOfflineUser && !force) {
|
|
561
|
+
parent.showAlerts([
|
|
562
|
+
{
|
|
563
|
+
type: "error",
|
|
564
|
+
msg: `The user '${lastOfflineUser}' has offline data, the download is not available.`,
|
|
565
|
+
},
|
|
566
|
+
]);
|
|
567
|
+
} else {
|
|
568
|
+
showLoadSpinner();
|
|
569
|
+
try {
|
|
570
|
+
await parent.offlineHelper.downloadServerData();
|
|
571
|
+
await parent.offlineHelper.endOfflineMode();
|
|
572
|
+
parent.clearAlerts();
|
|
573
|
+
parent.showAlerts([
|
|
574
|
+
{
|
|
575
|
+
type: "info",
|
|
576
|
+
msg: "Sucessfully updated your local data.",
|
|
577
|
+
},
|
|
578
|
+
]);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
parent.errorAlert(error);
|
|
581
|
+
} finally {
|
|
582
|
+
removeLoadSpinner();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function showLoadSpinner() {
|
|
588
|
+
if ($("#scspinner").length === 0) {
|
|
589
|
+
$("body").append(`
|
|
590
|
+
<div
|
|
591
|
+
id="scspinner"
|
|
592
|
+
style="position: absolute;
|
|
593
|
+
top: 0px;
|
|
594
|
+
width: 100%;
|
|
595
|
+
height: 100%;
|
|
596
|
+
z-index: 9999;"
|
|
597
|
+
>
|
|
598
|
+
<div
|
|
599
|
+
class="spinner-border"
|
|
600
|
+
role="status"
|
|
601
|
+
style="position: absolute;
|
|
602
|
+
left: 50%;
|
|
603
|
+
top: 50%;
|
|
604
|
+
height:60px;
|
|
605
|
+
width:60px;
|
|
606
|
+
margin:0px auto;"
|
|
607
|
+
>
|
|
608
|
+
<span class="visually-hidden">Loading...</span>
|
|
609
|
+
</div>
|
|
610
|
+
</div>`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function removeLoadSpinner() {
|
|
615
|
+
$("#scspinner").remove();
|
|
616
|
+
}
|
|
617
|
+
|
|
484
618
|
function reload_on_init() {
|
|
485
619
|
console.log("not yet supported");
|
|
486
620
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*global $, apiCall, saltcorn, apiCall, navigator, clearAlerts*/
|
|
2
|
+
|
|
3
|
+
var offlineHelper = (() => {
|
|
4
|
+
const loadServerData = async () => {
|
|
5
|
+
const response = await apiCall({
|
|
6
|
+
method: "GET",
|
|
7
|
+
path: "/sync/table_data",
|
|
8
|
+
});
|
|
9
|
+
return response.data;
|
|
10
|
+
};
|
|
11
|
+
const updateLocalData = async (data) => {
|
|
12
|
+
try {
|
|
13
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
|
|
14
|
+
await saltcorn.data.db.query("BEGIN TRANSACTION");
|
|
15
|
+
// replace all data
|
|
16
|
+
for (const [k, v] of Object.entries(data)) {
|
|
17
|
+
await saltcorn.data.db.query(
|
|
18
|
+
`delete from "${saltcorn.data.db.sqlsanitize(k)}"`
|
|
19
|
+
);
|
|
20
|
+
for (const row of v.rows) {
|
|
21
|
+
await saltcorn.data.db.insert(k, row);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
await saltcorn.data.db.query("COMMIT TRANSACTION");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
await saltcorn.data.db.query("ROLLBACK TRANSACTION");
|
|
27
|
+
throw error;
|
|
28
|
+
} finally {
|
|
29
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const loadLocalData = async () => {
|
|
34
|
+
const result = {};
|
|
35
|
+
const { user_id } = saltcorn.data.state.getState().mobileConfig;
|
|
36
|
+
const user = await saltcorn.data.models.User.findOne({ id: user_id });
|
|
37
|
+
if (!user) throw new Error(`The user with id '${user_id}' does not exist.`);
|
|
38
|
+
for (const table of await saltcorn.data.models.Table.find()) {
|
|
39
|
+
// ignore min_role_read, one can insert a row which is not readable
|
|
40
|
+
// but that invisible row should be synched
|
|
41
|
+
const rows =
|
|
42
|
+
user.role_id > table.min_role_write
|
|
43
|
+
? (await table.getRows()).filter((row) => table.is_owner(user, row))
|
|
44
|
+
: await table.getRows();
|
|
45
|
+
result[table.name] = rows;
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
const sendToServer = async (localData) => {
|
|
50
|
+
const response = await apiCall({
|
|
51
|
+
method: "POST",
|
|
52
|
+
path: "/sync/table_data",
|
|
53
|
+
body: {
|
|
54
|
+
data: localData,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return response.data;
|
|
58
|
+
};
|
|
59
|
+
const applyTranslatedIds = async (translateIds) => {
|
|
60
|
+
try {
|
|
61
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
|
|
62
|
+
await saltcorn.data.db.query("BEGIN TRANSACTION");
|
|
63
|
+
for (const [k, v] of Object.entries(translateIds)) {
|
|
64
|
+
const table = saltcorn.data.models.Table.findOne({ name: k });
|
|
65
|
+
for (const { from, to } of v) {
|
|
66
|
+
await table.updateRow({ id: to }, from);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await saltcorn.data.db.query("COMMIT TRANSACTION");
|
|
70
|
+
} catch (error) {
|
|
71
|
+
await saltcorn.data.db.query("ROLLBACK TRANSACTION");
|
|
72
|
+
throw error;
|
|
73
|
+
} finally {
|
|
74
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
startOfflineMode: async () => {
|
|
80
|
+
const state = saltcorn.data.state.getState();
|
|
81
|
+
const mobileConfig = state.mobileConfig;
|
|
82
|
+
const oldOfflineUser = await offlineHelper.lastOfflineUser();
|
|
83
|
+
if (oldOfflineUser && oldOfflineUser !== mobileConfig.user_name) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`The offline mode is not available, '${oldOfflineUser}' has not yet uploaded offline data.`
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
mobileConfig.isOfflineMode = true;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
endOfflineMode: async () => {
|
|
92
|
+
const state = saltcorn.data.state.getState();
|
|
93
|
+
const mobileConfig = state.mobileConfig;
|
|
94
|
+
mobileConfig.isOfflineMode = false;
|
|
95
|
+
await state.setConfig("user_with_offline_data", "");
|
|
96
|
+
},
|
|
97
|
+
lastOfflineUser: async () => {
|
|
98
|
+
const state = saltcorn.data.state.getState();
|
|
99
|
+
return await state.getConfig("user_with_offline_data");
|
|
100
|
+
},
|
|
101
|
+
uploadLocalData: async () => {
|
|
102
|
+
const lastOfflineUser = await offlineHelper.lastOfflineUser();
|
|
103
|
+
if (!lastOfflineUser) throw new Error("You don't have any offline data.");
|
|
104
|
+
const { user_name } = saltcorn.data.state.getState().mobileConfig;
|
|
105
|
+
if (lastOfflineUser !== user_name)
|
|
106
|
+
throw new Error(
|
|
107
|
+
`The upload is not available, '${lastOfflineUser}' has not yet uploaded offline data.`
|
|
108
|
+
);
|
|
109
|
+
const fromSqlite = await loadLocalData();
|
|
110
|
+
const { translateIds } = await sendToServer(fromSqlite);
|
|
111
|
+
if (translateIds && Object.keys(translateIds).length > 0)
|
|
112
|
+
await applyTranslatedIds(translateIds);
|
|
113
|
+
},
|
|
114
|
+
downloadServerData: async () => {
|
|
115
|
+
const fromServer = await loadServerData();
|
|
116
|
+
await updateLocalData(fromServer);
|
|
117
|
+
},
|
|
118
|
+
offlineCallback: async () => {
|
|
119
|
+
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
120
|
+
mobileConfig.networkState = navigator.connection.type;
|
|
121
|
+
},
|
|
122
|
+
onlineCallback: async () => {
|
|
123
|
+
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
124
|
+
if (mobileConfig.isOfflineMode) {
|
|
125
|
+
const iframeWindow = $("#content-iframe")[0].contentWindow;
|
|
126
|
+
if (iframeWindow) {
|
|
127
|
+
if (await offlineHelper.lastOfflineUser()) {
|
|
128
|
+
iframeWindow.notifyAlert(
|
|
129
|
+
`An internet connection is available, to handle your offline data Click ${saltcorn.markup.a(
|
|
130
|
+
{
|
|
131
|
+
href: "javascript:execLink('/sync/sync_settings')",
|
|
132
|
+
},
|
|
133
|
+
"here"
|
|
134
|
+
)}`
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
clearAlerts();
|
|
138
|
+
iframeWindow.notifyAlert("You are online again.");
|
|
139
|
+
mobileConfig.isOfflineMode = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
mobileConfig.networkState = navigator.connection.type;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
})();
|
|
@@ -46,18 +46,21 @@ async function updateUserDefinedTables() {
|
|
|
46
46
|
const existingTables = await saltcorn.data.db.listUserDefinedTables();
|
|
47
47
|
const tables = await saltcorn.data.models.Table.find();
|
|
48
48
|
for (const table of tables) {
|
|
49
|
+
const sanitized = saltcorn.data.db.sqlsanitize(table.name);
|
|
49
50
|
if (
|
|
50
51
|
table.name !== "users" &&
|
|
51
|
-
!existingTables.find((row) => row.name ===
|
|
52
|
+
!existingTables.find((row) => row.name === sanitized)
|
|
52
53
|
) {
|
|
53
54
|
// CREATE TABLE without inserting into _sc_tables
|
|
54
55
|
await saltcorn.data.models.Table.create(table.name, {}, table.id);
|
|
55
56
|
}
|
|
56
57
|
const existingFields = (
|
|
57
|
-
await saltcorn.data.db.query(`PRAGMA table_info('${
|
|
58
|
+
await saltcorn.data.db.query(`PRAGMA table_info('${sanitized}')`)
|
|
58
59
|
).rows.map((row) => row.name);
|
|
59
60
|
for (const field of await table.getFields()) {
|
|
60
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
existingFields.indexOf(saltcorn.data.db.sqlsanitize(field.name)) < 0
|
|
63
|
+
) {
|
|
61
64
|
// field is new
|
|
62
65
|
await saltcorn.data.models.Field.create(field, false, field.id);
|
|
63
66
|
}
|