@saltcorn/mobile-app 0.8.6-beta.9 → 0.8.7-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.xml +1 -0
- package/package.json +1 -1
- package/res/icon/android/icon.png +0 -0
- package/www/index.html +2 -0
- package/www/js/routes/common.js +2 -1
- package/www/js/utils/file_helpers.js +24 -0
- package/www/js/utils/global_utils.js +19 -10
- package/www/js/utils/iframe_view_utils.js +58 -3
- package/www/js/utils/table_utils.js +5 -2
- package/www/splash_page.html +3 -0
package/config.xml
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<name>SaltcornMobileApp</name>
|
|
4
4
|
<description>Apache Cordova application with @saltcorn/markup</description>
|
|
5
5
|
<platform name="android">
|
|
6
|
+
<icon density="ldpi" foreground="res/icon/android/icon.png" background="res/icon/android/icon.png" />
|
|
6
7
|
<edit-config file="app/src/main/AndroidManifest.xml" target="/manifest/application" mode="merge">
|
|
7
8
|
<application android:usesCleartextTraffic="true"/>
|
|
8
9
|
</edit-config>
|
package/package.json
CHANGED
|
Binary file
|
package/www/index.html
CHANGED
|
@@ -179,6 +179,7 @@
|
|
|
179
179
|
|
|
180
180
|
// the app comes back from background
|
|
181
181
|
const onResume = async () => {
|
|
182
|
+
if (typeof saltcorn === "undefined") return;
|
|
182
183
|
const state = saltcorn.data.state.getState();
|
|
183
184
|
const mobileConfig = state.mobileConfig;
|
|
184
185
|
if (mobileConfig?.allowOfflineMode) {
|
|
@@ -367,6 +368,7 @@
|
|
|
367
368
|
overflow: hidden;
|
|
368
369
|
z-index: 999999;
|
|
369
370
|
"
|
|
371
|
+
src="splash_page.html"
|
|
370
372
|
>
|
|
371
373
|
<p>Your browser does not support iframes.</p>
|
|
372
374
|
</iframe>
|
package/www/js/routes/common.js
CHANGED
|
@@ -131,7 +131,8 @@ const wrapContents = (contents, title, context, req) => {
|
|
|
131
131
|
role: state.mobileConfig.role_id,
|
|
132
132
|
menu: getMenu(req),
|
|
133
133
|
headers: getHeaders(),
|
|
134
|
-
|
|
134
|
+
// TODO logo as BASE 64 image
|
|
135
|
+
brand: { name: state.getConfig("site_name") || "Saltcorn" },
|
|
135
136
|
bodyClass: "",
|
|
136
137
|
currentUrl: "",
|
|
137
138
|
})
|
|
@@ -47,6 +47,30 @@ async function readJSON(fileName, dirName) {
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
async function readBinary(fileName, dirName) {
|
|
51
|
+
const dirEntry = await getDirEntry(dirName);
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
dirEntry.getFile(
|
|
54
|
+
fileName,
|
|
55
|
+
{ create: false, exclusive: false },
|
|
56
|
+
function (fileEntry) {
|
|
57
|
+
fileEntry.file(function (file) {
|
|
58
|
+
let reader = new FileReader();
|
|
59
|
+
reader.onloadend = function (e) {
|
|
60
|
+
resolve(this.result);
|
|
61
|
+
};
|
|
62
|
+
reader.readAsArrayBuffer(file);
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
function (err) {
|
|
66
|
+
console.log(`unable to read ${fileName}`);
|
|
67
|
+
console.log(err);
|
|
68
|
+
reject(err);
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
async function write(fileName, dirName, content) {
|
|
51
75
|
const dirEntry = await getDirEntry(dirName);
|
|
52
76
|
return new Promise((resolve, reject) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*global window, offlineHelper, axios, write, cordova, router, getDirEntry, saltcorn, document, FileReader, navigator*/
|
|
1
|
+
/*global window, offlineHelper, axios, write, cordova, router, getDirEntry, saltcorn, document, FileReader, navigator, splashConfig*/
|
|
2
2
|
|
|
3
3
|
let routingHistory = [];
|
|
4
4
|
|
|
@@ -25,7 +25,10 @@ function popRoute() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async function apiCall({ method, path, params, body, responseType, timeout }) {
|
|
28
|
-
const config =
|
|
28
|
+
const config =
|
|
29
|
+
typeof saltcorn !== "undefined"
|
|
30
|
+
? saltcorn.data.state.getState().mobileConfig
|
|
31
|
+
: splashConfig;
|
|
29
32
|
const serverPath = config.server_path;
|
|
30
33
|
const url = `${serverPath}${path}`;
|
|
31
34
|
const headers = {
|
|
@@ -60,12 +63,17 @@ function clearAlerts() {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
function showAlerts(alerts) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
alertsArea
|
|
66
|
+
if (typeof saltcorn === "undefined") {
|
|
67
|
+
console.log("Not yet initalized.");
|
|
68
|
+
console.log(alerts);
|
|
69
|
+
} else {
|
|
70
|
+
const iframe = document.getElementById("content-iframe");
|
|
71
|
+
const alertsArea =
|
|
72
|
+
iframe.contentWindow.document.getElementById("alerts-area");
|
|
73
|
+
alertsArea.innerHTML = "";
|
|
74
|
+
for (const { type, msg } of alerts) {
|
|
75
|
+
alertsArea.innerHTML += saltcorn.markup.alert(type, msg);
|
|
76
|
+
}
|
|
69
77
|
}
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -194,9 +202,10 @@ async function handleRoute(route, query, files) {
|
|
|
194
202
|
await gotoEntryView();
|
|
195
203
|
} else {
|
|
196
204
|
if (route === "/") return await gotoEntryView();
|
|
197
|
-
|
|
205
|
+
const safeRoute = route ? route : currentLocation();
|
|
206
|
+
addRoute({ route: safeRoute, query });
|
|
198
207
|
const page = await router.resolve({
|
|
199
|
-
pathname:
|
|
208
|
+
pathname: safeRoute,
|
|
200
209
|
query: query,
|
|
201
210
|
files: files,
|
|
202
211
|
alerts: mobileConfig.isOfflineMode
|
|
@@ -35,13 +35,26 @@ async function execNavbarLink(url) {
|
|
|
35
35
|
* @param {*} urlSuffix
|
|
36
36
|
* @returns
|
|
37
37
|
*/
|
|
38
|
-
async function formSubmit(e, urlSuffix, viewname) {
|
|
39
|
-
e.submit();
|
|
38
|
+
async function formSubmit(e, urlSuffix, viewname, noSubmitCb) {
|
|
39
|
+
if (!noSubmitCb) e.submit();
|
|
40
40
|
const files = {};
|
|
41
41
|
const urlParams = new URLSearchParams();
|
|
42
42
|
for (const entry of new FormData(e).entries()) {
|
|
43
43
|
if (entry[1] instanceof File) files[entry[0]] = entry[1];
|
|
44
|
-
else
|
|
44
|
+
else {
|
|
45
|
+
// is there a hidden input with a filename?
|
|
46
|
+
const domEl = $(e).find(
|
|
47
|
+
`[name='${entry[0]}'][mobile-camera-input='true']`
|
|
48
|
+
);
|
|
49
|
+
if (domEl.length > 0) {
|
|
50
|
+
const tokens = entry[1].split("/");
|
|
51
|
+
const fileName = tokens[tokens.length - 1];
|
|
52
|
+
const directory = tokens.splice(0, tokens.length - 1).join("/");
|
|
53
|
+
// read and add file to submit
|
|
54
|
+
const binary = await parent.readBinary(fileName, directory);
|
|
55
|
+
files[entry[0]] = new File([binary], fileName);
|
|
56
|
+
} else urlParams.append(entry[0], entry[1]);
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
const queryStr = urlParams.toString();
|
|
47
60
|
await parent.handleRoute(`post${urlSuffix}${viewname}`, queryStr, files);
|
|
@@ -771,6 +784,48 @@ function removeLoadSpinner() {
|
|
|
771
784
|
$("#scspinner").remove();
|
|
772
785
|
}
|
|
773
786
|
|
|
787
|
+
/**
|
|
788
|
+
* is called when an input with capture=camera is used
|
|
789
|
+
* It takes a picture with the camera plugin, saves the file, and adds the filename as a hidden input.
|
|
790
|
+
* @param {*} fieldName
|
|
791
|
+
*/
|
|
792
|
+
async function getPicture(fieldName) {
|
|
793
|
+
const cameraOptions = {
|
|
794
|
+
quality: 50,
|
|
795
|
+
encodingType: parent.Camera.EncodingType.JPEG,
|
|
796
|
+
destinationType: parent.Camera.DestinationType.FILE_URI,
|
|
797
|
+
};
|
|
798
|
+
const getPictureWithPromise = () => {
|
|
799
|
+
return new Promise((resolve, reject) => {
|
|
800
|
+
parent.navigator.camera.getPicture(
|
|
801
|
+
(imageDate) => {
|
|
802
|
+
return resolve(imageDate);
|
|
803
|
+
},
|
|
804
|
+
(message) => {
|
|
805
|
+
return reject(message);
|
|
806
|
+
},
|
|
807
|
+
cameraOptions
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
};
|
|
811
|
+
try {
|
|
812
|
+
const form = $(`#cptbtn${fieldName}`).closest("form");
|
|
813
|
+
const onsubmit = form.attr("onsubmit");
|
|
814
|
+
form.attr("onsubmit", "javascript:void(0)");
|
|
815
|
+
const fileURI = await getPictureWithPromise();
|
|
816
|
+
form.attr("onsubmit", onsubmit);
|
|
817
|
+
const inputId = `input${fieldName}`;
|
|
818
|
+
form.find(`#${inputId}`).remove();
|
|
819
|
+
form.append(
|
|
820
|
+
`<input class="d-none" id="${inputId}" name="${fieldName}" value="${fileURI}" mobile-camera-input="true" />`
|
|
821
|
+
);
|
|
822
|
+
const tokens = fileURI.split("/");
|
|
823
|
+
$(`#cpt-file-name-${fieldName}`).text(tokens[tokens.length - 1]);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
parent.errorAlert(error);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
774
829
|
function reload_on_init() {
|
|
775
830
|
console.log("not yet supported");
|
|
776
831
|
}
|
|
@@ -34,7 +34,10 @@ async function updateScTables(tablesJSON, skipScPlugins = true) {
|
|
|
34
34
|
// pick fields that really exist
|
|
35
35
|
const insertRow = {};
|
|
36
36
|
for (const safeField of existingFields) {
|
|
37
|
-
|
|
37
|
+
const fromRow = row[safeField];
|
|
38
|
+
if (fromRow !== null && fromRow !== undefined) {
|
|
39
|
+
insertRow[safeField] = fromRow;
|
|
40
|
+
}
|
|
38
41
|
}
|
|
39
42
|
await saltcorn.data.db.insert(table, insertRow);
|
|
40
43
|
}
|
|
@@ -67,7 +70,7 @@ async function updateUserDefinedTables() {
|
|
|
67
70
|
const existingFields = (
|
|
68
71
|
await saltcorn.data.db.query(`PRAGMA table_info('${sanitized}')`)
|
|
69
72
|
).rows.map((row) => row.name);
|
|
70
|
-
for (const field of
|
|
73
|
+
for (const field of table.getFields()) {
|
|
71
74
|
if (
|
|
72
75
|
existingFields.indexOf(saltcorn.data.db.sqlsanitize(field.name)) < 0
|
|
73
76
|
) {
|