cht-conf 3.10.2 → 3.11.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/.eslintignore +2 -1
- package/.releaserc.json +1 -1
- package/CHANGELOG.md +8 -1
- package/README.md +1 -1
- package/package.json +6 -3
- package/release-notes.md +1 -1
- package/src/fn/compile-app-settings.js +25 -8
- package/src/fn/compress-svgs.js +1 -1
- package/src/fn/convert-app-forms.js +13 -6
- package/src/fn/convert-collect-forms.js +11 -4
- package/src/fn/convert-contact-forms.js +62 -59
- package/src/fn/delete-forms.js +17 -16
- package/src/fn/upload-app-forms.js +11 -4
- package/src/fn/upload-app-settings.js +28 -22
- package/src/fn/upload-collect-forms.js +10 -5
- package/src/fn/upload-contact-forms.js +13 -6
- package/src/fn/upload-custom-translations.js +7 -4
- package/src/fn/upload-resources.js +6 -2
- package/src/fn/watch-project.js +271 -0
- package/src/lib/convert-forms.js +46 -31
- package/src/lib/git-exec.js +1 -1
- package/src/lib/project-paths.js +12 -0
- package/src/lib/upload-forms.js +36 -6
package/.eslintignore
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
**/build
|
|
1
|
+
**/build
|
|
2
|
+
test/data/**
|
package/.releaserc.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [3.11.0](https://github.com/medic/cht-conf/compare/v3.10.2...v3.11.0) (2022-04-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **#477:** adds watch-project action ([7a3efc5](https://github.com/medic/cht-conf/commit/7a3efc5efcbd98d8a6b03e801945201ed17ac00f)), closes [#477](https://github.com/medic/cht-conf/issues/477)
|
|
7
|
+
|
|
1
8
|
## [3.10.2](https://github.com/medic/cht-conf/compare/v3.10.1...v3.10.2) (2022-03-29)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -199,7 +206,7 @@ As of this release no docs on the server will be touched unless some configurati
|
|
|
199
206
|
|
|
200
207
|
### New action to bulk-edit contacts
|
|
201
208
|
|
|
202
|
-
You can now bulk edit contacts at once by providing a CSV. For more information, read [the documentation](https://github.com/medic/cht-conf/blob/
|
|
209
|
+
You can now bulk edit contacts at once by providing a CSV. For more information, read [the documentation](https://github.com/medic/cht-conf/blob/main/README.md#editing-contacts-across-the-hierarchy).
|
|
203
210
|
|
|
204
211
|
[#297](https://github.com/medic/cht-conf/issues/297)
|
|
205
212
|
|
package/README.md
CHANGED
|
@@ -317,7 +317,7 @@ Breaking| perf(#2): remove reporting rates feature <br/> BREAKING CHANGE: report
|
|
|
317
317
|
|
|
318
318
|
### Releasing betas
|
|
319
319
|
|
|
320
|
-
1. Checkout `
|
|
320
|
+
1. Checkout the default branch, for example `main`
|
|
321
321
|
1. Run `npm version --no-git-tag-version <major>.<minor>.<patch>-beta.1`. This will only update the versions in `package.json` and `package-lock.json`. It will not create a git tag and not create an associated commit.
|
|
322
322
|
1. Run `npm publish --tag beta`. This will publish your beta tag to npm's beta channel.
|
|
323
323
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cht-conf",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "Configure CHT deployments",
|
|
5
5
|
"main": "./src/lib/main.js",
|
|
6
6
|
"engines": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"clean": "rm -rf ./build/",
|
|
11
|
-
"eslint": "eslint src/**/*.js test/*.js test/**/*.js",
|
|
11
|
+
"eslint": "eslint 'src/**/*.js' test/*.js 'test/**/*.js'",
|
|
12
12
|
"docker-start-couchdb": "npm run docker-stop-couchdb && docker run -d -p 6984:5984 --rm --name cht-conf-couchdb couchdb:2.3.1 && sh test/scripts/wait_for_response_code.sh 6984 200 CouchDB",
|
|
13
13
|
"docker-stop-couchdb": "docker stop cht-conf-couchdb || true",
|
|
14
14
|
"test": "npm run eslint && npm run docker-start-couchdb && npm run clean && mkdir -p build/test && cp -r test/data build/test/data && cd build/test && nyc --reporter=html mocha --forbid-only ../../test/**/*.spec.js && cd ../.. && npm run docker-stop-couchdb",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@hapi/joi": "^16.1.8",
|
|
39
39
|
"@medic/translation-checker": "^1.0.1",
|
|
40
|
+
"@parcel/watcher": "^2.0.5",
|
|
40
41
|
"canonical-json": "0.0.4",
|
|
41
42
|
"csv-parse": "^4.16.0",
|
|
42
43
|
"dom-compare": "^0.6.0",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"pouchdb-core": "^7.2.2",
|
|
57
58
|
"pouchdb-mapreduce": "^7.2.2",
|
|
58
59
|
"properties": "^1.2.1",
|
|
60
|
+
"queue-promise": "^2.2.1",
|
|
59
61
|
"readline-sync": "^1.4.10",
|
|
60
62
|
"redact-basic-auth": "^1.0.1",
|
|
61
63
|
"request": "^2.88.2",
|
|
@@ -86,6 +88,7 @@
|
|
|
86
88
|
"pouchdb-adapter-memory": "^7.2.2",
|
|
87
89
|
"rewire": "^5.0.0",
|
|
88
90
|
"semantic-release": "^18.0.0",
|
|
89
|
-
"sinon": "^7.3.2"
|
|
91
|
+
"sinon": "^7.3.2",
|
|
92
|
+
"fs-extra": "8.1.0"
|
|
90
93
|
}
|
|
91
94
|
}
|
package/release-notes.md
CHANGED
|
@@ -134,7 +134,7 @@ As of this release no docs on the server will be touched unless some configurati
|
|
|
134
134
|
|
|
135
135
|
### New action to bulk-edit contacts
|
|
136
136
|
|
|
137
|
-
You can now bulk edit contacts at once by providing a CSV. For more information, read [the documentation](https://github.com/medic/cht-conf/blob/
|
|
137
|
+
You can now bulk edit contacts at once by providing a CSV. For more information, read [the documentation](https://github.com/medic/cht-conf/blob/main/README.md#editing-contacts-across-the-hierarchy).
|
|
138
138
|
|
|
139
139
|
[#297](https://github.com/medic/cht-conf/issues/297)
|
|
140
140
|
|
|
@@ -8,6 +8,21 @@ const parseTargets = require('../lib/parse-targets');
|
|
|
8
8
|
const { warn } = require('../lib/log');
|
|
9
9
|
const parsePurge = require('../lib/parse-purge');
|
|
10
10
|
const validateAppSettings = require('../lib/validate-app-settings');
|
|
11
|
+
const { APP_SETTINGS_DIR_PATH, APP_SETTINGS_JSON_PATH } = require('../lib/project-paths');
|
|
12
|
+
|
|
13
|
+
// we can't used named capture groups yet
|
|
14
|
+
const JS_FILE_MATCHER = /^(.+)(\.js)$/; // 2 groups get the file name and extension
|
|
15
|
+
const JSON_FILE_MATCHER = /^(.+)(\.json)$/;
|
|
16
|
+
const configFileMatcher = (fileName) => {
|
|
17
|
+
const jsFileMatchResult = fileName.match(JS_FILE_MATCHER);
|
|
18
|
+
// the first element is always the whole matched string, then our first file name group
|
|
19
|
+
if (jsFileMatchResult) return jsFileMatchResult[1];
|
|
20
|
+
|
|
21
|
+
const jsonFileMatchResult = fileName.match(JSON_FILE_MATCHER);
|
|
22
|
+
if (jsonFileMatchResult) return jsonFileMatchResult[1];
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
11
26
|
|
|
12
27
|
const compileAppSettings = async () => {
|
|
13
28
|
const options = parseExtraArgs(environment.extraArgs);
|
|
@@ -23,7 +38,7 @@ const compileAppSettings = async () => {
|
|
|
23
38
|
appSettings = await compileAppSettingsForProject(projectDir, options);
|
|
24
39
|
}
|
|
25
40
|
|
|
26
|
-
fs.writeJson(path.join(projectDir,
|
|
41
|
+
fs.writeJson(path.join(projectDir, APP_SETTINGS_JSON_PATH), appSettings);
|
|
27
42
|
};
|
|
28
43
|
|
|
29
44
|
const compileAppSettingsForProject = async (projectDir, options) => {
|
|
@@ -41,8 +56,8 @@ const compileAppSettingsForProject = async (projectDir, options) => {
|
|
|
41
56
|
|
|
42
57
|
const readOptionalJson = path => fs.exists(path) ? fs.readJson(path) : undefined;
|
|
43
58
|
let appSettings;
|
|
44
|
-
const baseSettingsPath = path.join(projectDir,
|
|
45
|
-
const appSettingsPath = path.join(projectDir,
|
|
59
|
+
const baseSettingsPath = path.join(projectDir, `${APP_SETTINGS_DIR_PATH}/base_settings.json`);
|
|
60
|
+
const appSettingsPath = path.join(projectDir, APP_SETTINGS_JSON_PATH);
|
|
46
61
|
const esLintFilePath = path.join(projectDir, '.eslintrc');
|
|
47
62
|
|
|
48
63
|
// Fail if no eslintrc file is found
|
|
@@ -56,10 +71,10 @@ const compileAppSettingsForProject = async (projectDir, options) => {
|
|
|
56
71
|
if (fs.exists(baseSettingsPath)) {
|
|
57
72
|
// using modular config so should override anything already defined in app_settings.json
|
|
58
73
|
appSettings = fs.readJson(baseSettingsPath);
|
|
59
|
-
if(appSettings.forms) {
|
|
74
|
+
if (appSettings.forms) {
|
|
60
75
|
warn('forms should be defined in a separate <config_repo>/app_settings/forms.json file.');
|
|
61
76
|
}
|
|
62
|
-
if(appSettings.schedules) {
|
|
77
|
+
if (appSettings.schedules) {
|
|
63
78
|
warn('schedules should be defined in a separate <config_repo>/app_settings/schedules.json file.');
|
|
64
79
|
}
|
|
65
80
|
const formSettings = readOptionalJson(path.join(projectDir, 'app_settings/forms.json'));
|
|
@@ -107,7 +122,7 @@ function applyTransforms(app_settings, inherited) {
|
|
|
107
122
|
rules.forEach(k => {
|
|
108
123
|
const parts = k.split('.');
|
|
109
124
|
let t = target;
|
|
110
|
-
while(parts.length > 1) {
|
|
125
|
+
while (parts.length > 1) {
|
|
111
126
|
t = t[parts[0]];
|
|
112
127
|
parts.shift();
|
|
113
128
|
}
|
|
@@ -122,7 +137,7 @@ function applyTransforms(app_settings, inherited) {
|
|
|
122
137
|
.forEach(k => {
|
|
123
138
|
const parts = k.split('.');
|
|
124
139
|
let t = target;
|
|
125
|
-
while(parts.length > 1) {
|
|
140
|
+
while (parts.length > 1) {
|
|
126
141
|
t = t[parts[0]];
|
|
127
142
|
parts.shift();
|
|
128
143
|
}
|
|
@@ -146,7 +161,7 @@ function applyTransforms(app_settings, inherited) {
|
|
|
146
161
|
.forEach(k => {
|
|
147
162
|
const parts = k.split('.');
|
|
148
163
|
let t = target;
|
|
149
|
-
while(parts.length > 1) {
|
|
164
|
+
while (parts.length > 1) {
|
|
150
165
|
t = t[parts[0]];
|
|
151
166
|
parts.shift();
|
|
152
167
|
}
|
|
@@ -178,5 +193,7 @@ const parseExtraArgs = (extraArgs = []) => {
|
|
|
178
193
|
|
|
179
194
|
module.exports = {
|
|
180
195
|
requiresInstance: false,
|
|
196
|
+
APP_SETTINGS_DIR_PATH,
|
|
197
|
+
configFileMatcher,
|
|
181
198
|
execute: compileAppSettings
|
|
182
199
|
};
|
package/src/fn/compress-svgs.js
CHANGED
|
@@ -16,6 +16,6 @@ module.exports = {
|
|
|
16
16
|
.then(() => svgo.optimize(fs.read(path), { path }))
|
|
17
17
|
.then(result => fs.write(path, result.data))
|
|
18
18
|
.then(() => trace('Compressed', path))
|
|
19
|
-
.then(() => warn('Make sure you compare the content of your SVG files before merging to the
|
|
19
|
+
.then(() => warn('Make sure you compare the content of your SVG files before merging to the default branch - sometimes optimising can change their contents!')),
|
|
20
20
|
Promise.resolve())
|
|
21
21
|
};
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
const convertForms = require('../lib/convert-forms');
|
|
1
|
+
const convertForms = require('../lib/convert-forms').execute;
|
|
2
2
|
const environment = require('../lib/environment');
|
|
3
|
+
const { APP_FORMS_PATH } = require('../lib/project-paths');
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
execute: () => convertForms(environment.pathToProject, 'app', {
|
|
5
|
+
const convertAppForms = (forms) => {
|
|
6
|
+
return convertForms(environment.pathToProject, 'app', {
|
|
7
7
|
enketo: true,
|
|
8
|
-
forms:
|
|
8
|
+
forms: forms,
|
|
9
9
|
transformer: xml => xml.replace('</instance>', '</instance>\n <instance id="contact-summary"/>'),
|
|
10
|
-
})
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
requiresInstance: false,
|
|
15
|
+
convertAppForms,
|
|
16
|
+
APP_FORMS_PATH,
|
|
17
|
+
execute: () => convertAppForms(environment.extraArgs)
|
|
11
18
|
};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
const convertForms = require('../lib/convert-forms');
|
|
1
|
+
const convertForms = require('../lib/convert-forms').execute;
|
|
2
2
|
const environment = require('../lib/environment');
|
|
3
|
+
const { COLLECT_FORMS_PATH } = require('../lib/project-paths');
|
|
4
|
+
|
|
5
|
+
const convertCollectForms = (forms) => {
|
|
6
|
+
return convertForms(environment.pathToProject, 'collect', {
|
|
7
|
+
forms: forms,
|
|
8
|
+
});
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
module.exports = {
|
|
5
12
|
requiresInstance: false,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
COLLECT_FORMS_PATH,
|
|
14
|
+
convertCollectForms,
|
|
15
|
+
execute: () => convertCollectForms(environment.extraArgs)
|
|
9
16
|
};
|
|
@@ -1,92 +1,95 @@
|
|
|
1
|
-
const convertForms = require('../lib/convert-forms');
|
|
1
|
+
const convertForms = require('../lib/convert-forms').execute;
|
|
2
2
|
const environment = require('../lib/environment');
|
|
3
3
|
const fs = require('../lib/sync-fs');
|
|
4
|
+
const { CONTACT_FORMS_PATH } = require('../lib/project-paths');
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
const dir = `${environment.pathToProject}
|
|
6
|
+
const convertContactForm = (forms) => {
|
|
7
|
+
const dir = `${environment.pathToProject}/${CONTACT_FORMS_PATH}`;
|
|
7
8
|
const placeTypesJson = `${dir}/place-types.json`;
|
|
8
9
|
|
|
9
10
|
let PLACE_TYPES;
|
|
10
|
-
if(fs.exists(placeTypesJson)) {
|
|
11
|
+
if (fs.exists(placeTypesJson)) {
|
|
11
12
|
PLACE_TYPES = fs.readJson(placeTypesJson);
|
|
12
13
|
Object.keys(PLACE_TYPES)
|
|
13
14
|
.forEach(type => {
|
|
14
|
-
fs.copy(`${dir}/PLACE_TYPE-create.xlsx`, `${dir}/${type}-create.xlsx`,{ overwrite: false });
|
|
15
|
+
fs.copy(`${dir}/PLACE_TYPE-create.xlsx`, `${dir}/${type}-create.xlsx`, { overwrite: false });
|
|
15
16
|
fs.copy(`${dir}/PLACE_TYPE-edit.xlsx`, `${dir}/${type}-edit.xlsx`, { overwrite: false });
|
|
16
17
|
});
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
return convertForms(environment.pathToProject, 'contact', {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
enketo: true,
|
|
22
|
+
force_data_node: 'data',
|
|
23
|
+
forms: forms,
|
|
24
|
+
transformer: (xml, path) => {
|
|
25
|
+
const type = path.replace(/.*\/(.*?)(-(create|edit))?\.xml/, '$1');
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
if (PLACE_TYPES) {
|
|
28
|
+
xml = xml
|
|
29
|
+
.replace(/PLACE_TYPE/g, type)
|
|
30
|
+
.replace(/PLACE_NAME/g, PLACE_TYPES[type]);
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
// The ordering of elements in the <model> has an arcane affect on the
|
|
34
|
+
// order that docs are saved in the database when processing a form.
|
|
35
|
+
// Move the main doc's element down to the bottom.
|
|
36
|
+
// For templated PLACE_TYPE forms, shifting must be done _after_ templating.
|
|
37
|
+
if (xml.includes('</inputs>')) {
|
|
38
|
+
let matchedBlock;
|
|
39
|
+
const matcher = new RegExp(`\\s*<${type}>[\\s\\S]*</${type}>\\s*(\\r|\\n)`);
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
xml = xml.replace(matcher, match => {
|
|
42
|
+
matchedBlock = match;
|
|
43
|
+
return '\n';
|
|
44
|
+
});
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
46
|
+
if (matchedBlock) {
|
|
47
|
+
xml = xml.replace(/<\/inputs>(\r|\n)/, '</inputs>' + matchedBlock);
|
|
48
48
|
}
|
|
49
|
+
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
if (xml.includes('/data/init/custom_place_name')) {
|
|
52
|
+
let matchedBlock;
|
|
53
|
+
xml = xml.replace(/\s*<input ref="\/data\/init\/custom_place_name">[^]*?<\/input>/, match => {
|
|
54
|
+
matchedBlock = match;
|
|
55
|
+
return '';
|
|
56
|
+
});
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
58
|
+
if (matchedBlock) {
|
|
59
|
+
const targetMatcher = new RegExp(`\\s*<input ref="/data/${type}/external_id">\\s*(\\r|\\n)`);
|
|
60
|
+
xml = xml.replace(targetMatcher, match => matchedBlock + match);
|
|
61
61
|
}
|
|
62
|
+
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
if (xml.includes('ref="/data/contact"')) {
|
|
65
|
+
const groupRegex = name => new RegExp(`(\\s*)<group(\\s.*)?\\sref="${name}"(\\s.*)?>[^]*?</group>`);
|
|
66
|
+
let matchedBlock;
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
if (xml.match(groupRegex('/data/init'))) {
|
|
69
|
+
xml = xml.replace(groupRegex('/data/contact'), match => {
|
|
70
|
+
matchedBlock = match;
|
|
71
|
+
return '';
|
|
72
|
+
});
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
74
|
+
if (matchedBlock) {
|
|
75
|
+
const stripTrailingGroup = s => s.replace(/[\r\n\s]*<\/group>$/, '');
|
|
76
|
+
xml = xml.replace(groupRegex('/data/init'), match => {
|
|
77
|
+
return stripTrailingGroup(match) +
|
|
78
|
+
stripTrailingGroup(matchedBlock).replace(/\n/g, '\n ') +
|
|
79
|
+
'\n </group>\n </group>';
|
|
80
|
+
});
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
return xml;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
87
88
|
};
|
|
88
89
|
|
|
89
90
|
module.exports = {
|
|
90
91
|
requiresInstance: false,
|
|
91
|
-
|
|
92
|
+
convertContactForm,
|
|
93
|
+
CONTACT_FORMS_PATH,
|
|
94
|
+
execute: () => convertContactForm(environment.extraArgs)
|
|
92
95
|
};
|
package/src/fn/delete-forms.js
CHANGED
|
@@ -2,22 +2,23 @@ const environment = require('../lib/environment');
|
|
|
2
2
|
const { info, warn } = require('../lib/log');
|
|
3
3
|
const pouch = require('../lib/db');
|
|
4
4
|
|
|
5
|
+
const deleteForms = (forms) => {
|
|
6
|
+
if (!forms || !forms.length) {
|
|
7
|
+
warn('No forms specified for deleting.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const db = pouch();
|
|
11
|
+
return Promise.all(forms.map(formName => {
|
|
12
|
+
const docId = `form:${formName}`;
|
|
13
|
+
return db.get(docId)
|
|
14
|
+
.then(doc => db.remove(doc))
|
|
15
|
+
.then(() => info('Deleted form:', formName))
|
|
16
|
+
.catch(e => warn(`Failed to remove form with doc ID ${docId}`, e));
|
|
17
|
+
}));
|
|
18
|
+
};
|
|
19
|
+
|
|
5
20
|
module.exports = {
|
|
6
21
|
requiresInstance: true,
|
|
7
|
-
execute: () =>
|
|
8
|
-
|
|
9
|
-
const { extraArgs } = environment;
|
|
10
|
-
if (!extraArgs || !extraArgs.length) {
|
|
11
|
-
warn('No forms specified for deleting.');
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return Promise.all(extraArgs.map(formName => {
|
|
16
|
-
const docId = `form:${formName}`;
|
|
17
|
-
return db.get(docId)
|
|
18
|
-
.then(doc => db.remove(doc))
|
|
19
|
-
.then(() => info('Deleted form:', formName))
|
|
20
|
-
.catch(e => warn(`Failed to remove form with doc ID ${docId}`, e));
|
|
21
|
-
}));
|
|
22
|
-
}
|
|
22
|
+
execute: () => deleteForms(environment.extraArgs),
|
|
23
|
+
deleteForms,
|
|
23
24
|
};
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
const environment = require('../lib/environment');
|
|
2
|
-
const uploadForms = require('../lib/upload-forms');
|
|
2
|
+
const uploadForms = require('../lib/upload-forms').execute;
|
|
3
|
+
const { APP_FORMS_PATH } = require('../lib/project-paths');
|
|
4
|
+
|
|
5
|
+
const uploadAppForms = (forms) => {
|
|
6
|
+
return uploadForms(environment.pathToProject, 'app', {
|
|
7
|
+
forms: forms,
|
|
8
|
+
});
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
module.exports = {
|
|
5
12
|
requiresInstance: true,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
uploadAppForms,
|
|
14
|
+
APP_FORMS_PATH: APP_FORMS_PATH,
|
|
15
|
+
execute: () => uploadAppForms(environment.extraArgs)
|
|
9
16
|
};
|
|
@@ -2,29 +2,35 @@ const api = require('../lib/api');
|
|
|
2
2
|
const environment = require('../lib/environment');
|
|
3
3
|
const fs = require('../lib/sync-fs');
|
|
4
4
|
const { info } = require('../lib/log');
|
|
5
|
+
const { APP_SETTINGS_DIR_PATH, APP_SETTINGS_JSON_PATH } = require('../lib/project-paths');
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw new Error(json.error);
|
|
17
|
-
}
|
|
7
|
+
const uploadAppSettings = (api) => {
|
|
8
|
+
const settings = fs.read(`${environment.pathToProject}/${APP_SETTINGS_JSON_PATH}`);
|
|
9
|
+
return api.updateAppSettings(settings)
|
|
10
|
+
.then(JSON.parse)
|
|
11
|
+
.then(json => {
|
|
12
|
+
// As per https://github.com/medic/cht-core/issues/3674, this endpoint
|
|
13
|
+
// will return 200 even when upload fails.
|
|
14
|
+
if (!json.success) {
|
|
15
|
+
throw new Error(json.error);
|
|
16
|
+
}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
18
|
+
if ('updated' in json) {
|
|
19
|
+
// the `updated` param was added in 3.9
|
|
20
|
+
// https://github.com/medic/cht-core/issues/6315
|
|
21
|
+
if (!json.updated) {
|
|
22
|
+
info('Settings not updated - no changes detected');
|
|
23
|
+
} else {
|
|
24
|
+
info('Settings updated successfully');
|
|
27
25
|
}
|
|
28
|
-
}
|
|
29
|
-
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
uploadAppSettings,
|
|
32
|
+
APP_SETTINGS_DIR_PATH,
|
|
33
|
+
APP_SETTINGS_JSON_PATH,
|
|
34
|
+
requiresInstance: true,
|
|
35
|
+
execute: () => uploadAppSettings(api())
|
|
30
36
|
};
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
const environment = require('../lib/environment');
|
|
2
|
-
const uploadForms = require('../lib/upload-forms');
|
|
2
|
+
const uploadForms = require('../lib/upload-forms').execute;
|
|
3
|
+
|
|
4
|
+
const uploadCollectForms = (forms) => {
|
|
5
|
+
return uploadForms(environment.pathToProject, 'collect', {
|
|
6
|
+
forms: forms,
|
|
7
|
+
default_context: { collect: true },
|
|
8
|
+
});
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
module.exports = {
|
|
5
12
|
requiresInstance: true,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
default_context: { collect:true },
|
|
9
|
-
})
|
|
13
|
+
uploadCollectForms,
|
|
14
|
+
execute: () => uploadCollectForms(environment.extraArgs)
|
|
10
15
|
};
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
const environment = require('../lib/environment');
|
|
2
|
-
const uploadForms = require('../lib/upload-forms');
|
|
2
|
+
const uploadForms = require('../lib/upload-forms').execute;
|
|
3
|
+
const CONTACT_FORMS_PATH = require('../lib/project-paths');
|
|
4
|
+
|
|
5
|
+
const uploadContactForms = (forms) => {
|
|
6
|
+
return uploadForms(environment.pathToProject, 'contact', {
|
|
7
|
+
id_prefix: 'contact:',
|
|
8
|
+
forms: forms,
|
|
9
|
+
default_context: { person: false, place: false },
|
|
10
|
+
});
|
|
11
|
+
};
|
|
3
12
|
|
|
4
13
|
module.exports = {
|
|
5
14
|
requiresInstance: true,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
default_context: { person:false, place:false },
|
|
10
|
-
})
|
|
15
|
+
uploadContactForms,
|
|
16
|
+
CONTACT_FORMS_PATH,
|
|
17
|
+
execute: () => uploadContactForms(environment.extraArgs)
|
|
11
18
|
};
|
|
@@ -13,10 +13,12 @@ const {
|
|
|
13
13
|
TranslationException
|
|
14
14
|
} = require('@medic/translation-checker');
|
|
15
15
|
|
|
16
|
+
const { TRANSLATIONS_DIR_PATH } = require('../lib/project-paths');
|
|
17
|
+
|
|
16
18
|
const execute = async () => {
|
|
17
19
|
const db = pouch(environment.apiUrl);
|
|
18
20
|
|
|
19
|
-
const dir = `${environment.pathToProject}
|
|
21
|
+
const dir = `${environment.pathToProject}/${TRANSLATIONS_DIR_PATH}`;
|
|
20
22
|
|
|
21
23
|
let fileNames;
|
|
22
24
|
let formatErrorsFound = 0;
|
|
@@ -85,7 +87,7 @@ const execute = async () => {
|
|
|
85
87
|
languageName = 'TODO: please ask admin to set this in settings UI';
|
|
86
88
|
} else {
|
|
87
89
|
let languageNativeName = iso639.getNativeName(languageCode);
|
|
88
|
-
if (languageNativeName !== languageName){
|
|
90
|
+
if (languageNativeName !== languageName) {
|
|
89
91
|
languageName = `${languageNativeName} (${languageName})`;
|
|
90
92
|
}
|
|
91
93
|
}
|
|
@@ -95,7 +97,7 @@ const execute = async () => {
|
|
|
95
97
|
let doc;
|
|
96
98
|
try {
|
|
97
99
|
doc = await db.get(id);
|
|
98
|
-
} catch(e) {
|
|
100
|
+
} catch (e) {
|
|
99
101
|
if (e.status === 404) {
|
|
100
102
|
doc = await newDocFor(fileName, db, languageName, languageCode);
|
|
101
103
|
}
|
|
@@ -127,7 +129,7 @@ function parse(filePath, options) {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
function overwriteProperties(doc, props) {
|
|
130
|
-
if(doc.generic) {
|
|
132
|
+
if (doc.generic) {
|
|
131
133
|
// 3.4.0 translation structure
|
|
132
134
|
doc.custom = props;
|
|
133
135
|
} else if (doc.values) {
|
|
@@ -179,5 +181,6 @@ async function genericTranslationsStructure(db) {
|
|
|
179
181
|
|
|
180
182
|
module.exports = {
|
|
181
183
|
requiresInstance: true,
|
|
184
|
+
TRANSLATIONS_DIR_PATH,
|
|
182
185
|
execute
|
|
183
186
|
};
|
|
@@ -6,13 +6,17 @@ const processJson = (json) => {
|
|
|
6
6
|
resources: json
|
|
7
7
|
};
|
|
8
8
|
};
|
|
9
|
+
const { RESOURCE_CONFIG_PATH, RESOURCES_DIR_PATH } = require('../lib/project-paths');
|
|
9
10
|
|
|
10
11
|
module.exports = {
|
|
11
12
|
requiresInstance: true,
|
|
13
|
+
RESOURCE_CONFIG_PATH,
|
|
14
|
+
RESOURCES_DIR_PATH,
|
|
12
15
|
execute: () => {
|
|
13
|
-
const configurationPath = `${environment.pathToProject}
|
|
14
|
-
const directoryPath = `${environment.pathToProject}
|
|
16
|
+
const configurationPath = `${environment.pathToProject}/${RESOURCE_CONFIG_PATH}`;
|
|
17
|
+
const directoryPath = `${environment.pathToProject}/${RESOURCES_DIR_PATH}`;
|
|
15
18
|
|
|
16
19
|
return uploadConfigurationDocs(configurationPath, directoryPath, 'resources', processJson);
|
|
17
20
|
}
|
|
18
21
|
};
|
|
22
|
+
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const environment = require('../lib/environment');
|
|
2
|
+
const process = require('process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { error, warn, info } = require('../lib/log');
|
|
6
|
+
const Queue = require('queue-promise');
|
|
7
|
+
const watcher = require('@parcel/watcher');
|
|
8
|
+
const { uploadAppForms } = require('./upload-app-forms');
|
|
9
|
+
const { uploadContactForms } = require('./upload-contact-forms');
|
|
10
|
+
const { uploadCollectForms } = require('./upload-collect-forms');
|
|
11
|
+
const convertForms = require('../lib/convert-forms');
|
|
12
|
+
const uploadForms = require('../lib/upload-forms');
|
|
13
|
+
const { deleteForms } = require('../fn/delete-forms');
|
|
14
|
+
const { convertAppForms, APP_FORMS_PATH } = require('./convert-app-forms');
|
|
15
|
+
const { convertContactForm, CONTACT_FORMS_PATH } = require('./convert-contact-forms');
|
|
16
|
+
const { convertCollectForms, COLLECT_FORMS_PATH } = require('./convert-collect-forms');
|
|
17
|
+
const { uploadAppSettings, APP_SETTINGS_DIR_PATH, APP_SETTINGS_JSON_PATH } = require('./upload-app-settings');
|
|
18
|
+
const { execute: compileAppSettings, configFileMatcher } = require('./compile-app-settings');
|
|
19
|
+
const { execute: uploadCustomTranslations, TRANSLATIONS_DIR_PATH } = require('./upload-custom-translations');
|
|
20
|
+
const { execute: uploadResources, RESOURCES_DIR_PATH, RESOURCE_CONFIG_PATH } = require('./upload-resources');
|
|
21
|
+
|
|
22
|
+
const watcherEvents = {
|
|
23
|
+
CreateEvent: 'create',
|
|
24
|
+
DeleteEvent: 'delete',
|
|
25
|
+
UpdateEvent: 'update'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const uploadInitialState = async (api) => {
|
|
29
|
+
await uploadResources();
|
|
30
|
+
await uploadAppForms(environment.extraArgs);
|
|
31
|
+
await uploadContactForms(environment.extraArgs);
|
|
32
|
+
await uploadCustomTranslations();
|
|
33
|
+
await uploadAppSettings(api);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let fsEventsSubscription;
|
|
37
|
+
let eventQueue;
|
|
38
|
+
|
|
39
|
+
const cleanUp = async () => {
|
|
40
|
+
if (fsEventsSubscription) {
|
|
41
|
+
await fsEventsSubscription.unsubscribe();
|
|
42
|
+
fsEventsSubscription = undefined;
|
|
43
|
+
}
|
|
44
|
+
if (eventQueue) {
|
|
45
|
+
await eventQueue.clear();
|
|
46
|
+
eventQueue = undefined;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const waitForKillSignal = () => {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
process.on('SIGINT', async () => {
|
|
53
|
+
await cleanUp();
|
|
54
|
+
resolve();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const deleteForm = (fileName, formDir) => {
|
|
60
|
+
const form = uploadForms.formFileMatcher(fileName) || convertForms.formFileMatcher(fileName);
|
|
61
|
+
if (!form) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const absFormDirPath = path.join(environment.pathToProject, formDir);
|
|
65
|
+
const formFiles = fs.readdirSync(absFormDirPath).map(file => (uploadForms.formFileMatcher(file) || convertForms.formFileMatcher(file))).filter(_form => _form === form);
|
|
66
|
+
const canDelete = formFiles.length <= 0;
|
|
67
|
+
if (canDelete) {
|
|
68
|
+
eventQueue.enqueue(async () => {
|
|
69
|
+
await deleteForms([form]);
|
|
70
|
+
return fileName;
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
warn(`You have to delete all form files (.xlsx, .properties.json, .xml) associated with the ${form} form to delete it from ${environment.apiUrl}`);
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const processAppForm = (eventType, fileName) => {
|
|
79
|
+
if (eventType === watcherEvents.DeleteEvent && deleteForm(fileName, APP_FORMS_PATH)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
let form = uploadForms.formFileMatcher(fileName);
|
|
83
|
+
if (form) {
|
|
84
|
+
eventQueue.enqueue(async () => {
|
|
85
|
+
await uploadAppForms([form]);
|
|
86
|
+
return fileName;
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
form = convertForms.formFileMatcher(fileName);
|
|
92
|
+
if (form) {
|
|
93
|
+
eventQueue.enqueue(async () => {
|
|
94
|
+
await convertAppForms([form]);
|
|
95
|
+
return fileName;
|
|
96
|
+
});
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const processAppFormMedia = (formMediaDir, fileName) => {
|
|
103
|
+
const form = uploadForms.formMediaMatcher(formMediaDir);
|
|
104
|
+
if (form) {
|
|
105
|
+
eventQueue.enqueue(async () => {
|
|
106
|
+
await uploadAppForms([form]);
|
|
107
|
+
return fileName;
|
|
108
|
+
});
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const processContactForm = (eventType, fileName) => {
|
|
115
|
+
if (eventType === watcherEvents.DeleteEvent && deleteForm(fileName, CONTACT_FORMS_PATH)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
let form = convertForms.formFileMatcher(fileName);
|
|
119
|
+
if (form) {
|
|
120
|
+
eventQueue.enqueue(async () => {
|
|
121
|
+
await convertContactForm([form]);
|
|
122
|
+
return fileName;
|
|
123
|
+
});
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
form = uploadForms.formFileMatcher(fileName);
|
|
128
|
+
if (form) {
|
|
129
|
+
eventQueue.enqueue(async () => {
|
|
130
|
+
await uploadContactForms([form]);
|
|
131
|
+
return fileName;
|
|
132
|
+
});
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const processCollectForm = (eventType, fileName) => {
|
|
139
|
+
if (eventType === watcherEvents.DeleteEvent && deleteForm(fileName, COLLECT_FORMS_PATH)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
let form = uploadForms.formFileMatcher(fileName);
|
|
143
|
+
if (form) {
|
|
144
|
+
eventQueue.enqueue(async () => {
|
|
145
|
+
await uploadCollectForms([form]);
|
|
146
|
+
return fileName;
|
|
147
|
+
});
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
form = convertForms.formFileMatcher(fileName);
|
|
152
|
+
if (form) {
|
|
153
|
+
eventQueue.enqueue(async () => {
|
|
154
|
+
await convertCollectForms([form]);
|
|
155
|
+
return fileName;
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const processConfigFiles = (api, fileName) => {
|
|
163
|
+
if (fileName === APP_SETTINGS_JSON_PATH) {
|
|
164
|
+
eventQueue.enqueue(async () => {
|
|
165
|
+
await uploadAppSettings(api);
|
|
166
|
+
return fileName;
|
|
167
|
+
});
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (configFileMatcher(fileName)) {
|
|
171
|
+
eventQueue.enqueue(async () => {
|
|
172
|
+
await compileAppSettings();
|
|
173
|
+
return fileName;
|
|
174
|
+
});
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const watchProject = {
|
|
181
|
+
watch: async (api, blockFn, callback = {}, uploadStateOnStart = false) => {
|
|
182
|
+
if (uploadStateOnStart) {
|
|
183
|
+
await uploadInitialState(api);
|
|
184
|
+
info('Initial State Uploaded');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
eventQueue = new Queue({ concurrent: 1, start: true });
|
|
188
|
+
|
|
189
|
+
fsEventsSubscription = await watcher.subscribe(environment.pathToProject, async (err, events) => {
|
|
190
|
+
if (err) {
|
|
191
|
+
error(err);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (const event of events) {
|
|
195
|
+
const changePath = event.path;
|
|
196
|
+
const parsedPath = path.parse(changePath);
|
|
197
|
+
const fileName = parsedPath.base;
|
|
198
|
+
|
|
199
|
+
if (parsedPath.dir === path.join(environment.pathToProject, APP_FORMS_PATH)) {
|
|
200
|
+
processAppForm(event.type, fileName);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (parsedPath.dir.startsWith(path.join(environment.pathToProject, APP_FORMS_PATH))
|
|
205
|
+
&& path.parse(parsedPath.dir).dir.endsWith(APP_FORMS_PATH)) { // check if the directory's immediate parent is forms/app
|
|
206
|
+
const dirName = path.parse(parsedPath.dir).base;
|
|
207
|
+
processAppFormMedia(dirName, fileName);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (parsedPath.dir === path.join(environment.pathToProject, CONTACT_FORMS_PATH)) {
|
|
212
|
+
processContactForm(event.type, fileName);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (parsedPath.dir === path.join(environment.pathToProject, COLLECT_FORMS_PATH)) {
|
|
217
|
+
processCollectForm(event.type, fileName);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if (parsedPath.dir === path.join(environment.pathToProject, TRANSLATIONS_DIR_PATH)) {
|
|
223
|
+
eventQueue.enqueue(async () => {
|
|
224
|
+
await uploadCustomTranslations();
|
|
225
|
+
return fileName;
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (parsedPath.dir === path.join(environment.pathToProject, APP_SETTINGS_DIR_PATH)) {
|
|
231
|
+
processConfigFiles(api, fileName);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (parsedPath.dir === environment.pathToProject && parsedPath.base !== RESOURCE_CONFIG_PATH) {
|
|
236
|
+
processConfigFiles(api, fileName);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (parsedPath.base === RESOURCE_CONFIG_PATH || parsedPath.dir === path.join(environment.pathToProject, RESOURCES_DIR_PATH)) {
|
|
241
|
+
eventQueue.enqueue(async () => {
|
|
242
|
+
await uploadResources();
|
|
243
|
+
return fileName;
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
eventQueue.on('resolve', file => {
|
|
251
|
+
if (callback) {
|
|
252
|
+
callback(file);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
eventQueue.on('reject', err => error('watch-mode: Error while processing change,', err));
|
|
256
|
+
|
|
257
|
+
info('watching', environment.pathToProject, 'for changes');
|
|
258
|
+
await blockFn();
|
|
259
|
+
},
|
|
260
|
+
close: async () => {
|
|
261
|
+
info('stopping watchers');
|
|
262
|
+
await cleanUp();
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const api = require('../lib/api');
|
|
267
|
+
module.exports = {
|
|
268
|
+
watchProject,
|
|
269
|
+
requiresInstance: true,
|
|
270
|
+
execute: () => watchProject.watch(api(), waitForKillSignal, false, true)
|
|
271
|
+
};
|
package/src/lib/convert-forms.js
CHANGED
|
@@ -20,25 +20,34 @@ E
|
|
|
20
20
|
E pip uninstall pyxform-medic
|
|
21
21
|
E` + INSTALLATION_INSTRUCTIONS;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const FORM_EXTENSION = '.xlsx';
|
|
24
|
+
const formFileMatcher = (fileName) => {
|
|
25
|
+
if (fileName.endsWith(FORM_EXTENSION) &&
|
|
26
|
+
!fileName.startsWith('~$') // ignore Excel "owner files"
|
|
27
|
+
&& fileName !== 'PLACE_TYPE-create.xlsx' && fileName !== 'PLACE_TYPE-edit.xlsx') {
|
|
28
|
+
return fileName.slice(0, fileName.length - FORM_EXTENSION.length);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const execute = async (projectDir, subDirectory, options) => {
|
|
34
|
+
if (!options) options = {};
|
|
25
35
|
|
|
26
36
|
const formsDir = getFormDir(projectDir, subDirectory);
|
|
27
37
|
|
|
28
|
-
if(!fs.exists(formsDir)) {
|
|
38
|
+
if (!fs.exists(formsDir)) {
|
|
29
39
|
warn(`Forms dir not found: ${formsDir}`);
|
|
30
40
|
return Promise.resolve();
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
const filesToConvert = argsFormFilter(formsDir,
|
|
34
|
-
.filter(name =>
|
|
35
|
-
.filter(name => name !== 'PLACE_TYPE-create.xlsx' && name !== 'PLACE_TYPE-edit.xlsx');
|
|
43
|
+
const filesToConvert = argsFormFilter(formsDir, FORM_EXTENSION, options)
|
|
44
|
+
.filter(name => formFileMatcher(name));
|
|
36
45
|
|
|
37
46
|
for (let xls of filesToConvert) {
|
|
38
47
|
const originalSourcePath = `${formsDir}/${xls}`;
|
|
39
48
|
let sourcePath;
|
|
40
49
|
|
|
41
|
-
if(options.force_data_node) {
|
|
50
|
+
if (options.force_data_node) {
|
|
42
51
|
const temporaryPath = `${fs.mkdtemp()}/${options.force_data_node}.xlsx`;
|
|
43
52
|
fs.copy(originalSourcePath, temporaryPath);
|
|
44
53
|
sourcePath = temporaryPath;
|
|
@@ -54,15 +63,21 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
54
63
|
}
|
|
55
64
|
};
|
|
56
65
|
|
|
66
|
+
module.exports = {
|
|
67
|
+
SUPPORTED_EXTENSIONS: [FORM_EXTENSION],
|
|
68
|
+
formFileMatcher,
|
|
69
|
+
execute
|
|
70
|
+
};
|
|
71
|
+
|
|
57
72
|
const xls2xform = (sourcePath, targetPath) =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
exec([XLS2XFORM, '--skip_validate', sourcePath, targetPath])
|
|
74
|
+
.catch(e => {
|
|
75
|
+
if (executableAvailable()) {
|
|
76
|
+
if (e.includes('unrecognized arguments: --skip_validate')) {
|
|
77
|
+
throw new Error('Your xls2xform installation appears to be out of date.' + UPDATE_INSTRUCTIONS);
|
|
78
|
+
} else throw e;
|
|
79
|
+
} else throw new Error('There was a problem executing xls2xform. It may not be installed.' + INSTALLATION_INSTRUCTIONS);
|
|
80
|
+
});
|
|
66
81
|
|
|
67
82
|
// here we fix the form content in arcane ways. Seeing as we have out own fork
|
|
68
83
|
// of pyxform, we should probably be doing this fixing there.
|
|
@@ -72,33 +87,33 @@ const fixXml = (path, hiddenFields, transformer, enketo) => {
|
|
|
72
87
|
// future changes to the output of xls2xform.
|
|
73
88
|
let xml = fs.read(path)
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
// The following copies behaviour from old bash scripts, and will create a
|
|
91
|
+
// second <meta> element if one already existed. We may want to actually
|
|
92
|
+
// merge the two instead.
|
|
93
|
+
.replace(/<inputs>/, META_XML_SECTION)
|
|
79
94
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
// XLSForm does not allow converting a field without a label, so we use
|
|
96
|
+
// the placeholder NO_LABEL.
|
|
97
|
+
.replace(/NO_LABEL/g, '')
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
// No comment.
|
|
100
|
+
.replace(/.*DELETE_THIS_LINE.*(\r|\n)/g, '')
|
|
101
|
+
;
|
|
87
102
|
|
|
88
103
|
// Enketo _may_ not work with forms which define a default language - see
|
|
89
104
|
// https://github.com/medic/cht-core/issues/3174
|
|
90
|
-
if(enketo) xml = xml.replace(/ default="true\(\)"/g, '');
|
|
105
|
+
if (enketo) xml = xml.replace(/ default="true\(\)"/g, '');
|
|
91
106
|
|
|
92
|
-
if(hiddenFields) {
|
|
107
|
+
if (hiddenFields) {
|
|
93
108
|
const r = new RegExp(`<(${hiddenFields.join('|')})(/?)>`, 'g');
|
|
94
109
|
xml = xml.replace(r, '<$1 tag="hidden"$2>');
|
|
95
110
|
}
|
|
96
111
|
|
|
97
|
-
if(transformer) xml = transformer(xml, path);
|
|
112
|
+
if (transformer) xml = transformer(xml, path);
|
|
98
113
|
|
|
99
114
|
|
|
100
115
|
// Check for deprecations
|
|
101
|
-
if(xml.includes('repeat-relevant')) {
|
|
116
|
+
if (xml.includes('repeat-relevant')) {
|
|
102
117
|
warn('From webapp version 2.14.0, repeat-relevant is no longer required. See https://github.com/medic/cht-core/issues/3449 for more info.');
|
|
103
118
|
}
|
|
104
119
|
|
|
@@ -106,7 +121,7 @@ const fixXml = (path, hiddenFields, transformer, enketo) => {
|
|
|
106
121
|
};
|
|
107
122
|
|
|
108
123
|
function getHiddenFields(propsJson) {
|
|
109
|
-
if(!fs.exists(propsJson)) return [];
|
|
124
|
+
if (!fs.exists(propsJson)) return [];
|
|
110
125
|
else return fs.readJson(propsJson).hidden_fields;
|
|
111
126
|
}
|
|
112
127
|
|
|
@@ -116,7 +131,7 @@ function executableAvailable() {
|
|
|
116
131
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
117
132
|
});
|
|
118
133
|
return true;
|
|
119
|
-
} catch(e) {
|
|
134
|
+
} catch (e) {
|
|
120
135
|
return false;
|
|
121
136
|
}
|
|
122
137
|
}
|
package/src/lib/git-exec.js
CHANGED
|
@@ -64,7 +64,7 @@ module.exports.getDefaultRemote = async () => {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Returns the upstream of the current branch,
|
|
67
|
-
* in the form of repo/branch, eg. 'origin/
|
|
67
|
+
* in the form of repo/branch, eg. 'origin/main'.
|
|
68
68
|
*
|
|
69
69
|
* Returns `null` if there are no upstreams repos configured.
|
|
70
70
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
// Paths Relative to the Project
|
|
3
|
+
module.exports = {
|
|
4
|
+
APP_SETTINGS_DIR_PATH: 'app_settings',
|
|
5
|
+
APP_SETTINGS_JSON_PATH: 'app_settings.json',
|
|
6
|
+
APP_FORMS_PATH: path.join('forms', 'app'),
|
|
7
|
+
CONTACT_FORMS_PATH: path.join('forms', 'contact'),
|
|
8
|
+
COLLECT_FORMS_PATH: path.join('forms', 'collect'),
|
|
9
|
+
TRANSLATIONS_DIR_PATH: 'translations',
|
|
10
|
+
RESOURCE_CONFIG_PATH: 'resources.json',
|
|
11
|
+
RESOURCES_DIR_PATH: 'resources'
|
|
12
|
+
};
|
package/src/lib/upload-forms.js
CHANGED
|
@@ -16,18 +16,40 @@ const {
|
|
|
16
16
|
const validateForms = require('./validate-forms');
|
|
17
17
|
|
|
18
18
|
const SUPPORTED_PROPERTIES = ['context', 'icon', 'title', 'xml2sms', 'subject_key', 'hidden_fields'];
|
|
19
|
+
const FORM_EXTENSTION = '.xml';
|
|
20
|
+
const FORM_PROPERTIES_EXTENSION = '.properties.json';
|
|
21
|
+
const FORM_MEDIA_MATCHER = /(.+)-media$/;
|
|
22
|
+
const SUPPORTED_EXTENSIONS = [FORM_EXTENSTION, FORM_PROPERTIES_EXTENSION];
|
|
23
|
+
|
|
24
|
+
const formFileMatcher = (fileName) => {
|
|
25
|
+
if (fileName.endsWith(FORM_EXTENSTION)) {
|
|
26
|
+
return fileName.slice(0, fileName.length - FORM_EXTENSTION.length);
|
|
27
|
+
}
|
|
28
|
+
if (fileName.endsWith(FORM_PROPERTIES_EXTENSION)) {
|
|
29
|
+
return fileName.slice(0, fileName.length - FORM_PROPERTIES_EXTENSION.length);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const formMediaMatcher = (formMediaDir) => {
|
|
35
|
+
const matchResult = formMediaDir.match(FORM_MEDIA_MATCHER);
|
|
36
|
+
if (matchResult) {
|
|
37
|
+
return matchResult[1]; // return the captured form name
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
19
41
|
|
|
20
|
-
|
|
42
|
+
const execute = async (projectDir, subDirectory, options) => {
|
|
21
43
|
await validateForms(projectDir, subDirectory, options);
|
|
22
44
|
const db = pouch();
|
|
23
45
|
if (!options) options = {};
|
|
24
46
|
const formsDir = getFormDir(projectDir, subDirectory);
|
|
25
|
-
if(!fs.exists(formsDir)) {
|
|
47
|
+
if (!fs.exists(formsDir)) {
|
|
26
48
|
log.info(`Forms dir not found: ${formsDir}`);
|
|
27
49
|
return;
|
|
28
50
|
}
|
|
29
51
|
|
|
30
|
-
const fileNames = argsFormFilter(formsDir,
|
|
52
|
+
const fileNames = argsFormFilter(formsDir, FORM_EXTENSTION, options);
|
|
31
53
|
for (const fileName of fileNames) {
|
|
32
54
|
log.info(`Preparing form for upload: ${fileName}…`);
|
|
33
55
|
|
|
@@ -35,7 +57,7 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
35
57
|
const baseDocId = (options.id_prefix || '') + baseFileName.replace(/-/g, ':');
|
|
36
58
|
|
|
37
59
|
const mediaDirExists = fs.exists(mediaDir);
|
|
38
|
-
if(!mediaDirExists) {
|
|
60
|
+
if (!mediaDirExists) {
|
|
39
61
|
log.info(`No media directory found at ${mediaDir} for form ${xformPath}`);
|
|
40
62
|
}
|
|
41
63
|
|
|
@@ -48,7 +70,7 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
48
70
|
};
|
|
49
71
|
|
|
50
72
|
const internalId = readIdFrom(xml);
|
|
51
|
-
if(internalId !== baseDocId) log.warn('DEPRECATED', 'Form:', fileName, 'Bad ID set in XML. Expected:', baseDocId, 'but saw:', internalId, ' Support for setting these values differently will be dropped. Please see https://github.com/medic/cht-core/issues/3342.');
|
|
73
|
+
if (internalId !== baseDocId) log.warn('DEPRECATED', 'Form:', fileName, 'Bad ID set in XML. Expected:', baseDocId, 'but saw:', internalId, ' Support for setting these values differently will be dropped. Please see https://github.com/medic/cht-core/issues/3342.');
|
|
52
74
|
|
|
53
75
|
const docId = `form:${baseDocId}`;
|
|
54
76
|
const doc = {
|
|
@@ -60,7 +82,7 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
60
82
|
context: options.default_context,
|
|
61
83
|
};
|
|
62
84
|
|
|
63
|
-
const propertiesPath = `${formsDir}/${baseFileName}
|
|
85
|
+
const propertiesPath = `${formsDir}/${baseFileName}${FORM_PROPERTIES_EXTENSION}`;
|
|
64
86
|
updateFromPropertiesFile(doc, propertiesPath);
|
|
65
87
|
|
|
66
88
|
doc._attachments = mediaDirExists ? attachmentsFromDir(mediaDir) : {};
|
|
@@ -80,6 +102,14 @@ module.exports = async (projectDir, subDirectory, options) => {
|
|
|
80
102
|
}
|
|
81
103
|
};
|
|
82
104
|
|
|
105
|
+
module.exports = {
|
|
106
|
+
SUPPORTED_EXTENSIONS,
|
|
107
|
+
FORM_MEDIA_MATCHER,
|
|
108
|
+
formFileMatcher,
|
|
109
|
+
formMediaMatcher,
|
|
110
|
+
execute
|
|
111
|
+
};
|
|
112
|
+
|
|
83
113
|
const updateFromPropertiesFile = (doc, path) => {
|
|
84
114
|
if (fs.exists(path)) {
|
|
85
115
|
|