cht-conf 3.10.0 → 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 +22 -1
- package/README.md +3 -1
- package/package.json +7 -3
- package/release-notes.md +1 -1
- package/src/bin/index.js +14 -12
- package/src/cli/check-node-version.js +9 -7
- package/src/cli/shell-completion-setup.js +5 -8
- package/src/fn/check-git.js +3 -5
- package/src/fn/compile-app-settings.js +25 -8
- package/src/fn/compress-images.js +1 -0
- 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/create-users.js +4 -7
- package/src/fn/delete-forms.js +17 -16
- package/src/fn/move-contacts.js +2 -3
- 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 +9 -8
- package/src/fn/upload-docs.js +3 -4
- package/src/fn/upload-resources.js +6 -2
- package/src/fn/watch-project.js +271 -0
- package/src/lib/api.js +13 -14
- package/src/lib/convert-forms.js +46 -31
- package/src/lib/git-exec.js +1 -1
- package/src/lib/main.js +16 -22
- package/src/lib/project-paths.js +12 -0
- package/src/lib/upload-forms.js +36 -6
- package/src/lib/validate-app-settings.js +2 -2
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
|
*/
|
package/src/lib/main.js
CHANGED
|
@@ -12,7 +12,7 @@ const supportedActions = require('../cli/supported-actions');
|
|
|
12
12
|
const usage = require('../cli/usage');
|
|
13
13
|
const api = require('../lib/api');
|
|
14
14
|
|
|
15
|
-
const {
|
|
15
|
+
const { info, warn } = log;
|
|
16
16
|
const defaultActions = [
|
|
17
17
|
'check-git',
|
|
18
18
|
'compile-app-settings',
|
|
@@ -55,9 +55,9 @@ module.exports = async (argv, env) => {
|
|
|
55
55
|
warn('The "medic-conf" cli command is deprecated. Please use "cht" instead');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
if(argv.length <= 2) {
|
|
59
|
-
usage(
|
|
60
|
-
|
|
58
|
+
if (argv.length <= 2) {
|
|
59
|
+
usage();
|
|
60
|
+
throw new Error('Invalid number of arguments.');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
const cmdArgs = require('minimist')(argv.slice(2), {
|
|
@@ -69,8 +69,8 @@ module.exports = async (argv, env) => {
|
|
|
69
69
|
// General single use actions
|
|
70
70
|
//
|
|
71
71
|
if (cmdArgs.help) {
|
|
72
|
-
usage(
|
|
73
|
-
return
|
|
72
|
+
usage();
|
|
73
|
+
return;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
if (cmdArgs['shell-completion']) {
|
|
@@ -79,22 +79,21 @@ module.exports = async (argv, env) => {
|
|
|
79
79
|
|
|
80
80
|
if (cmdArgs['supported-actions']) {
|
|
81
81
|
info('Supported actions:\n', supportedActions.join('\n '));
|
|
82
|
-
return
|
|
82
|
+
return;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
if (cmdArgs.version) {
|
|
86
86
|
info(require('../../package.json').version);
|
|
87
|
-
return
|
|
87
|
+
return;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
if (cmdArgs.changelog) {
|
|
91
91
|
await open('https://github.com/medic/cht-conf/releases', { url: true });
|
|
92
|
-
return
|
|
92
|
+
return;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
if (cmdArgs.archive && !cmdArgs.destination) {
|
|
96
|
-
|
|
97
|
-
return 1;
|
|
96
|
+
throw new Error('--destination=<path to save files> is required with --archive.');
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
if (cmdArgs['accept-self-signed-certs']) {
|
|
@@ -130,8 +129,7 @@ module.exports = async (argv, env) => {
|
|
|
130
129
|
|
|
131
130
|
const unsupported = actions.filter(a => !supportedActions.includes(a));
|
|
132
131
|
if(unsupported.length) {
|
|
133
|
-
|
|
134
|
-
return 1;
|
|
132
|
+
throw new Error(`Unsupported action(s): ${unsupported.join(' ')}`);
|
|
135
133
|
}
|
|
136
134
|
|
|
137
135
|
if (cmdArgs['skip-git-check']) {
|
|
@@ -163,8 +161,7 @@ module.exports = async (argv, env) => {
|
|
|
163
161
|
if (actions.some(action => action.requiresInstance)) {
|
|
164
162
|
apiUrl = getApiUrl(cmdArgs, env);
|
|
165
163
|
if (!apiUrl) {
|
|
166
|
-
|
|
167
|
-
return 1;
|
|
164
|
+
throw new Error('Failed to obtain a url to the API');
|
|
168
165
|
}
|
|
169
166
|
}
|
|
170
167
|
|
|
@@ -183,8 +180,8 @@ module.exports = async (argv, env) => {
|
|
|
183
180
|
cmdArgs['skip-translation-check']
|
|
184
181
|
);
|
|
185
182
|
|
|
186
|
-
if (apiUrl
|
|
187
|
-
|
|
183
|
+
if (apiUrl) {
|
|
184
|
+
await api().available();
|
|
188
185
|
}
|
|
189
186
|
|
|
190
187
|
const productionUrlMatch = environment.instanceUrl && environment.instanceUrl.match(/^https:\/\/(?:[^@]*@)?(.*)\.(app|dev)\.medicmobile\.org(?:$|\/)/);
|
|
@@ -192,9 +189,8 @@ module.exports = async (argv, env) => {
|
|
|
192
189
|
if (productionUrlMatch && !expectedOptions.includes(productionUrlMatch[1])) {
|
|
193
190
|
warn(`Attempting to use project for \x1b[31m${projectName}\x1b[33m`,
|
|
194
191
|
`against non-matching instance: \x1b[31m${redactBasicAuth(environment.instanceUrl)}\x1b[33m`);
|
|
195
|
-
if(!userPrompt.keyInYN()) {
|
|
196
|
-
|
|
197
|
-
return 1;
|
|
192
|
+
if (!userPrompt.keyInYN()) {
|
|
193
|
+
throw new Error('User aborted execution.');
|
|
198
194
|
}
|
|
199
195
|
}
|
|
200
196
|
|
|
@@ -218,8 +214,6 @@ module.exports = async (argv, env) => {
|
|
|
218
214
|
if (actions.length > 1) {
|
|
219
215
|
await info('All actions completed.');
|
|
220
216
|
}
|
|
221
|
-
|
|
222
|
-
return 0;
|
|
223
217
|
};
|
|
224
218
|
|
|
225
219
|
// Exists for generic mocking purposes
|
|
@@ -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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const joi = require('@hapi/joi');
|
|
2
2
|
|
|
3
3
|
const FormsSchema = joi.object().pattern(
|
|
4
|
-
|
|
4
|
+
joi.string().min(1),
|
|
5
5
|
joi.object({
|
|
6
6
|
meta: joi.object({
|
|
7
7
|
code: joi.string().required(),
|
|
@@ -10,7 +10,7 @@ const FormsSchema = joi.object().pattern(
|
|
|
10
10
|
subject_key: joi.string()
|
|
11
11
|
}).required(),
|
|
12
12
|
fields: joi.object().pattern(
|
|
13
|
-
|
|
13
|
+
joi.string().min(1),
|
|
14
14
|
joi.object({
|
|
15
15
|
type: joi.string().required(),
|
|
16
16
|
labels: joi.object({
|