@saltcorn/mobile-app 0.8.6-beta.1 → 0.8.6-beta.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saltcorn/mobile-app",
3
3
  "displayName": "Saltcorn mobile app",
4
- "version": "0.8.6-beta.1",
4
+ "version": "0.8.6-beta.11",
5
5
  "description": "Apache Cordova application with @saltcorn/markup",
6
6
  "main": "index.js",
7
7
  "scripts": {
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>
@@ -17,9 +18,11 @@
17
18
  <script src="js/routes/auth.js"></script>
18
19
  <script src="js/routes/delete.js"></script>
19
20
  <script src="js/routes/edit.js"></script>
21
+ <script src="js/routes/api.js"></script>
20
22
  <script src="js/routes/error.js"></script>
21
23
  <script src="js/routes/page.js"></script>
22
24
  <script src="js/routes/view.js"></script>
25
+ <script src="js/routes/sync.js"></script>
23
26
  <script src="js/routes/init.js"></script>
24
27
 
25
28
  <script src="js/mocks/response.js"></script>
@@ -124,7 +127,7 @@
124
127
  config.pluginHeaders.push(prepareHeader(header));
125
128
  }
126
129
  else if (typeof pluginHeaders === "function") {
127
- const headerResult = pluginHeaders(row.configuration);
130
+ const headerResult = pluginHeaders(row.configuration || {});
128
131
  if (Array.isArray(headerResult)) {
129
132
  for (const header of headerResult)
130
133
  config.pluginHeaders.push(prepareHeader(header));
@@ -146,29 +149,15 @@
146
149
  const initJwt = async () => {
147
150
  if (!(await saltcorn.data.db.tableExists("jwt_table"))) {
148
151
  await createJwtTable();
149
- }
150
- else {
152
+ } else {
151
153
  const jwt = await getJwt();
152
- if(jwt) {
154
+ if (jwt) {
153
155
  const state = saltcorn.data.state.getState();
154
156
  state.mobileConfig.jwt = jwt;
155
157
  }
156
158
  }
157
159
  };
158
160
 
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
161
  const initI18Next = async () => {
173
162
  const resources = {};
174
163
  for (const key of Object.keys(
@@ -188,8 +177,43 @@
188
177
  });
189
178
  };
190
179
 
180
+ // the app comes back from background
181
+ const onResume = async () => {
182
+ const state = saltcorn.data.state.getState();
183
+ const mobileConfig = state.mobileConfig;
184
+ if (mobileConfig?.allowOfflineMode) {
185
+ mobileConfig.networkState = navigator.connection.type;
186
+ if (
187
+ mobileConfig.networkState === "none" &&
188
+ !mobileConfig.isOfflineMode &&
189
+ mobileConfig.jwt
190
+ ) {
191
+ await offlineHelper.startOfflineMode();
192
+ clearHistory();
193
+ if (mobileConfig.user_id) await gotoEntryView();
194
+ else {
195
+ const decodedJwt = jwt_decode(mobileConfig.jwt);
196
+ mobileConfig.role_id = decodedJwt.user.role_id
197
+ ? decodedJwt.user.role_id
198
+ : 100;
199
+ mobileConfig.user_id = decodedJwt.user.id;
200
+ mobileConfig.user_name = decodedJwt.user.email;
201
+ mobileConfig.language = decodedJwt.user.language;
202
+ mobileConfig.isPublicUser = false;
203
+ }
204
+ addRoute({ route: entryPoint, query: undefined });
205
+ const page = await router.resolve({
206
+ pathname: mobileConfig.entry_point,
207
+ fullWrap: true,
208
+ alerts: [],
209
+ });
210
+ }
211
+ }
212
+ };
213
+
191
214
  // device is ready
192
215
  const init = async () => {
216
+ document.addEventListener("resume", onResume, false);
193
217
  const config = await readJSON(
194
218
  "config",
195
219
  `${cordova.file.applicationDirectory}www`
@@ -227,32 +251,86 @@
227
251
  setVersionTtag();
228
252
  const entryPoint = config.entry_point;
229
253
  await initI18Next();
254
+ state.mobileConfig.networkState = navigator.connection.type;
255
+ document.addEventListener(
256
+ "offline",
257
+ offlineHelper.offlineCallback,
258
+ false
259
+ );
260
+ document.addEventListener(
261
+ "online",
262
+ offlineHelper.onlineCallback,
263
+ false
264
+ );
265
+ const networkDisabled = state.mobileConfig.networkState === "none";
266
+ const jwt = state.mobileConfig.jwt;
230
267
  try {
231
- if (await checkJWT()) {
268
+ const alerts = [];
269
+ if ((networkDisabled && jwt) || (await checkJWT(jwt))) {
232
270
  const mobileConfig = state.mobileConfig;
233
271
  const decodedJwt = jwt_decode(mobileConfig.jwt);
234
272
  mobileConfig.role_id = decodedJwt.user.role_id
235
273
  ? decodedJwt.user.role_id
236
- : 10;
274
+ : 100;
275
+ mobileConfig.user_id = decodedJwt.user.id;
237
276
  mobileConfig.user_name = decodedJwt.user.email;
238
277
  mobileConfig.language = decodedJwt.user.language;
239
278
  mobileConfig.isPublicUser = false;
240
279
  await i18next.changeLanguage(mobileConfig.language);
280
+ if (mobileConfig.allowOfflineMode) {
281
+ const { offlineUser, upload_started_at, upload_ended_at } =
282
+ (await offlineHelper.getLastOfflineSession()) || {};
283
+ if (networkDisabled) {
284
+ if (offlineUser && offlineUser !== mobileConfig.user_name)
285
+ throw new Error(
286
+ `The offline mode is not available, '${offlineUser}' has not yet uploaded offline data.`
287
+ );
288
+ else {
289
+ await offlineHelper.startOfflineMode();
290
+ alerts.push({
291
+ type: "info",
292
+ msg: offlineHelper.getOfflineMsg(),
293
+ });
294
+ }
295
+ } else if (offlineUser) {
296
+ if (offlineUser === mobileConfig.user_name) {
297
+ if (upload_started_at && !upload_ended_at) {
298
+ alerts.push({
299
+ type: "warning",
300
+ msg: "Please check if your offline data is already online. An upload was started but did not finish.",
301
+ });
302
+ } else {
303
+ alerts.push({
304
+ type: "info",
305
+ msg: "You have offline data, to handle it open the Network menu.",
306
+ });
307
+ }
308
+ } else
309
+ alerts.push({
310
+ type: "warning",
311
+ msg: `'${offlineUser}' has not yet uploaded offline data.`,
312
+ });
313
+ }
314
+ }
241
315
  addRoute({ route: entryPoint, query: undefined });
242
316
  const page = await router.resolve({
243
317
  pathname: entryPoint,
244
318
  fullWrap: true,
319
+ alerts,
245
320
  });
246
321
  await replaceIframe(page.content);
247
322
  } else {
248
323
  const page = await router.resolve({
249
324
  pathname: "get/auth/login",
325
+ alerts,
250
326
  });
251
- replaceIframe(page.content);
327
+ await replaceIframe(page.content);
252
328
  }
253
329
  } catch (error) {
330
+ state.mobileConfig.inErrorState = true;
254
331
  const page = await router.resolve({
255
332
  pathname: "get/error_page",
333
+ fullWrap: true,
256
334
  alerts: [
257
335
  {
258
336
  type: "error",
@@ -261,7 +339,6 @@
261
339
  ],
262
340
  });
263
341
  await replaceIframe(page.content);
264
- console.error(error);
265
342
  }
266
343
  };
267
344
 
@@ -1,9 +1,13 @@
1
1
  /*global i18next, saltcorn*/
2
2
 
3
- function MobileRequest({ xhr = false, files = undefined, query = undefined }) {
4
- const roleId = saltcorn.data.state.getState().mobileConfig.role_id
5
- ? saltcorn.data.state.getState().mobileConfig.role_id
6
- : 10;
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) => {
@@ -0,0 +1,39 @@
1
+ /*global saltcorn, apiCall*/
2
+
3
+ // post/api/:tableName/:id
4
+ const updateTableRow = async (context) => {
5
+ const { tableName, id } = context.params;
6
+ const mobileConfig = saltcorn.data.state.getState().mobileConfig;
7
+ const user = {
8
+ id: mobileConfig.user_id,
9
+ role_id: mobileConfig.role_id || 100,
10
+ };
11
+ const table = saltcorn.data.models.Table.findOne({ name: tableName });
12
+ if (!table) throw new Error(`The table '${tableName}' does not exist.`);
13
+ if (
14
+ mobileConfig.isOfflineMode ||
15
+ mobileConfig.localTableIds.indexOf(table.id) >= 0
16
+ ) {
17
+ const row = {};
18
+ for (const [k, v] of new URLSearchParams(context.query).entries()) {
19
+ row[k] = v;
20
+ }
21
+ const errors = await saltcorn.data.web_mobile_commons.prepare_update_row(
22
+ table,
23
+ row,
24
+ id
25
+ );
26
+ if (errors.length > 0) throw new Error(errors.join(", "));
27
+ const ins_res = await table.tryUpdateRow(row, id, user);
28
+ if (ins_res.error)
29
+ throw new Error(`Update ${table.name} error: ${ins_res.error}`);
30
+ return { ins_res };
31
+ } else {
32
+ const response = await apiCall({
33
+ method: "POST",
34
+ path: `/api/${tableName}/${id}`,
35
+ body: context.query,
36
+ });
37
+ return response.data;
38
+ }
39
+ };
@@ -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 config = saltcorn.data.state.getState().mobileConfig;
72
+ const getLoginView = async (context) => {
73
+ const mobileConfig = saltcorn.data.state.getState().mobileConfig;
74
74
  return {
75
- content: renderLoginView(config.entry_point, config.version_tag),
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),
@@ -25,7 +25,7 @@ const parseQuery = (queryStr) => {
25
25
 
26
26
  const layout = () => {
27
27
  const state = saltcorn.data.state.getState();
28
- return state.getLayout({ role_id: state.mobileConfig.role_id });
28
+ return state.getLayout({ role_id: state.mobileConfig.role_id || 100 });
29
29
  };
30
30
 
31
31
  const sbAdmin2Layout = () => {
@@ -34,53 +34,87 @@ const sbAdmin2Layout = () => {
34
34
 
35
35
  const getMenu = (req) => {
36
36
  const state = saltcorn.data.state.getState();
37
- const mobileCfg = saltcorn.data.state.getState().mobileConfig;
38
- const role = mobileCfg.role_id || 10;
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.__
42
42
  );
43
- const allowSignup = state.getConfig("allow_signup");
44
- const userName = mobileCfg.user_name;
45
- const authItems = mobileCfg.isPublicUser
46
- ? [
47
- { link: "javascript:execLink('/auth/login')", label: req.__("Login") },
48
- ...(allowSignup
49
- ? [
43
+ if (mobileCfg.inErrorState) {
44
+ const entryLink = mobileCfg.entry_point?.startsWith("get")
45
+ ? mobileCfg.entry_point.substr(3)
46
+ : null;
47
+ return entryLink
48
+ ? [
49
+ {
50
+ section: "Reload",
51
+ items: [
50
52
  {
51
- link: "javascript:execLink('/auth/signup')",
52
- label: req.__("Sign up"),
53
+ link: `javascript:parent.gotoEntryView()`,
54
+ icon: "fas fa-sync",
55
+ label: "Reload",
53
56
  },
54
- ]
55
- : []),
56
- ]
57
- : [
58
- {
59
- label: req.__("User"),
60
- icon: "far fa-user",
61
- isUser: true,
62
- subitems: [
63
- { label: userName },
64
- {
65
- link: `javascript:logout();`,
66
- icon: "fas fa-sign-out-alt",
67
- label: "Logout",
68
- },
69
- ],
70
- },
71
- ];
72
- const result = [];
73
- if (extraMenu.length > 0)
57
+ ],
58
+ },
59
+ ]
60
+ : [];
61
+ } else {
62
+ const allowSignup = state.getConfig("allow_signup");
63
+ const userName = mobileCfg.user_name;
64
+ const authItems = mobileCfg.isPublicUser
65
+ ? [
66
+ {
67
+ link: "javascript:execNavbarLink('/auth/login')",
68
+ label: req.__("Login"),
69
+ },
70
+ ...(allowSignup
71
+ ? [
72
+ {
73
+ link: "javascript:execNavbarLink('/auth/signup')",
74
+ label: req.__("Sign up"),
75
+ },
76
+ ]
77
+ : []),
78
+ ]
79
+ : [
80
+ {
81
+ label: req.__("User"),
82
+ icon: "far fa-user",
83
+ isUser: true,
84
+ subitems: [
85
+ { label: userName },
86
+ {
87
+ link: `javascript:logout();`,
88
+ icon: "fas fa-sign-out-alt",
89
+ label: "Logout",
90
+ },
91
+ ],
92
+ },
93
+ ];
94
+ const result = [];
95
+ if (extraMenu.length > 0)
96
+ result.push({
97
+ section: req.__("Menu"),
98
+ items: extraMenu,
99
+ });
74
100
  result.push({
75
- section: req.__("Menu"),
76
- items: extraMenu,
101
+ section: req.__("User"),
102
+ isUser: true,
103
+ items: authItems,
77
104
  });
78
- result.push({
79
- section: req.__("User"),
80
- isUser: true,
81
- items: authItems,
82
- });
83
- return result;
105
+ if (mobileCfg.allowOfflineMode)
106
+ result.push({
107
+ section: "Network",
108
+ items: [
109
+ {
110
+ link: "javascript:execNavbarLink('/sync/sync_settings')",
111
+ icon: "fas fa-sync",
112
+ label: "Network",
113
+ },
114
+ ],
115
+ });
116
+ return result;
117
+ }
84
118
  };
85
119
 
86
120
  const prepareAlerts = (context, req) => {
@@ -1,18 +1,18 @@
1
- /*global i18next, apiCall, saltcorn*/
1
+ /*global i18next, apiCall, saltcorn, offlineHelper*/
2
2
 
3
3
  // post/delete/:name/:id
4
4
  const deleteRows = async (context) => {
5
5
  const { name, id } = context.params;
6
6
  const table = await saltcorn.data.models.Table.findOne({ name });
7
- const mobileConfig = saltcorn.data.state.getState().mobileConfig;
8
-
9
- if (mobileConfig.localTableIds.indexOf(table.id) >= 0) {
10
- if (mobileConfig.role_id <= table.min_role_write) {
7
+ const { isOfflineMode, localTableIds, role_id } =
8
+ saltcorn.data.state.getState().mobileConfig;
9
+ if (isOfflineMode || localTableIds.indexOf(table.id) >= 0) {
10
+ if (role_id <= table.min_role_write) {
11
11
  await table.deleteRows({ id });
12
- }
13
- // TODO 'table.is_owner' check?
14
- else {
15
- throw new Error(i18next.t("Not authorized"));
12
+ // TODO 'table.is_owner' check?
13
+ } else throw new Error(i18next.t("Not authorized"));
14
+ if (isOfflineMode && !(await offlineHelper.hasOfflineRows())) {
15
+ await offlineHelper.setOfflineSession(null);
16
16
  }
17
17
  } else {
18
18
  await apiCall({ method: "POST", path: `/delete/${name}/${id}` });
@@ -1,14 +1,18 @@
1
- /*global i18next, apiCall, saltcorn*/
1
+ /*global i18next, apiCall, saltcorn, offlineHelper*/
2
2
 
3
3
  // /toggle/:name/:id/:field_name
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 mobileConfig = saltcorn.data.state.getState().mobileConfig;
8
- if (mobileConfig.localTableIds.indexOf(table.id) >= 0) {
9
- if (mobileConfig.role_id > table.min_role_write)
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 && !(await offlineHelper.getLastOfflineSession()))
15
+ await offlineHelper.setOfflineSession({ offlineUser: user_name });
12
16
  } else {
13
17
  await apiCall({
14
18
  method: "POST",
@@ -1,18 +1,5 @@
1
- /*global layout, getHeaders, saltcorn*/
1
+ /*global wrapContents, MobileRequest */
2
2
 
3
3
  const getErrorView = async (context) => {
4
- const state = saltcorn.data.state.getState();
5
- const wrappedContent = layout().wrap({
6
- title: "Error",
7
- body: { above: [""] },
8
- alerts: context.alerts ,
9
- role: state.mobileConfig.role_id,
10
- headers: getHeaders(),
11
- menu: [],
12
- bodyClass: "",
13
- currentUrl: "",
14
- brand: {},
15
- });
16
-
17
- return { content: wrappedContent, title: "Error" };
4
+ return wrapContents("", "Error", context, new MobileRequest());
18
5
  };
@@ -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, getSyncSettingsView, getAskDeleteOfflineData, getAskUploadNotEnded, updateTableRow */
2
+ // TODO module namespacese
2
3
 
3
4
  const initRoutes = async () => {
4
5
  const routes = [
@@ -14,6 +15,10 @@ const initRoutes = async () => {
14
15
  path: "get/view/:viewname",
15
16
  action: getView,
16
17
  },
18
+ {
19
+ path: "post/api/:tableName/:id",
20
+ action: updateTableRow,
21
+ },
17
22
  {
18
23
  path: "post/edit/toggle/:name/:id/:field_name",
19
24
  action: postToggleField,
@@ -46,6 +51,18 @@ const initRoutes = async () => {
46
51
  path: "get/error_page",
47
52
  action: getErrorView,
48
53
  },
54
+ {
55
+ path: "get/sync/sync_settings",
56
+ action: getSyncSettingsView,
57
+ },
58
+ {
59
+ path: "get/sync/ask_upload_not_ended",
60
+ action: getAskUploadNotEnded,
61
+ },
62
+ {
63
+ path: "get/sync/ask_delete_offline_data",
64
+ action: getAskDeleteOfflineData,
65
+ },
49
66
  ];
50
67
  window.router = new window.UniversalRouter(routes);
51
68
  };