commit
b366486aaa
|
@ -0,0 +1,69 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const DEV_CONFIG_FILE = '.moodleapp-dev-config';
|
||||
|
||||
/**
|
||||
* Class to read and write dev-config data from a file.
|
||||
*/
|
||||
class DevConfig {
|
||||
|
||||
constructor() {
|
||||
this.loadFileData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a setting.
|
||||
*
|
||||
* @param name Name of the setting to get.
|
||||
* @param defaultValue Value to use if not found.
|
||||
*/
|
||||
get(name, defaultValue) {
|
||||
return typeof this.config[name] != 'undefined' ? this.config[name] : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load file data to memory.
|
||||
*/
|
||||
loadFileData() {
|
||||
if (!fs.existsSync(DEV_CONFIG_FILE)) {
|
||||
this.config = {};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.config = JSON.parse(fs.readFileSync(DEV_CONFIG_FILE));
|
||||
} catch (error) {
|
||||
console.error('Error reading dev config file.', error);
|
||||
this.config = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save some settings.
|
||||
*
|
||||
* @param settings Object with the settings to save.
|
||||
*/
|
||||
save(settings) {
|
||||
this.config = Object.assign(this.config, settings);
|
||||
|
||||
// Save the data in the dev file.
|
||||
fs.writeFileSync(DEV_CONFIG_FILE, JSON.stringify(this.config, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new DevConfig();
|
|
@ -0,0 +1,237 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const exec = require('child_process').exec;
|
||||
const fs = require('fs');
|
||||
const DevConfig = require('./dev-config');
|
||||
const Utils = require('./utils');
|
||||
|
||||
/**
|
||||
* Class to run git commands.
|
||||
*/
|
||||
class Git {
|
||||
|
||||
/**
|
||||
* Create a patch.
|
||||
*
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param saveTo Path to the file to save the patch to. If not defined, the patch contents will be returned.
|
||||
* @return Promise resolved when done. If saveTo not provided, it will return the patch contents.
|
||||
*/
|
||||
createPatch(range, saveTo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`git format-patch ${range} --stdout`, (err, result) => {
|
||||
if (err) {
|
||||
reject(err || 'Cannot create patch.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!saveTo) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save it to a file.
|
||||
const directory = saveTo.substring(0, saveTo.lastIndexOf('/'));
|
||||
if (directory && directory != '.' && directory != '..' && !fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory);
|
||||
}
|
||||
fs.writeFileSync(saveTo, result);
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current branch.
|
||||
*
|
||||
* @return Promise resolved with the branch name.
|
||||
*/
|
||||
getCurrentBranch() {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('git branch --show-current', (err, branch) => {
|
||||
if (branch) {
|
||||
resolve(branch.replace('\n', ''));
|
||||
} else {
|
||||
reject (err || 'Current branch not found.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HEAD commit for a certain branch.
|
||||
*
|
||||
* @param branch Name of the branch.
|
||||
* @param branchData Parsed branch data. If not provided it will be calculated.
|
||||
* @return HEAD commit.
|
||||
*/
|
||||
async getHeadCommit(branch, branchData) {
|
||||
if (!branchData) {
|
||||
// Parse the branch to get the project and issue number.
|
||||
branchData = Utils.parseBranch(branch);
|
||||
}
|
||||
|
||||
// Loop over the last commits to find the first commit messages that doesn't belong to the issue.
|
||||
const commitsString = await this.log(50, branch, '%s_____%H');
|
||||
const commits = commitsString.split('\n');
|
||||
commits.pop(); // Remove last element, it's an empty string.
|
||||
|
||||
for (let i = 0; i < commits.length; i++) {
|
||||
const commit = commits[i];
|
||||
const match = Utils.getIssueFromCommitMessage(commit) == branchData.issue;
|
||||
|
||||
if (i === 0 && !match) {
|
||||
// Most recent commit doesn't belong to the issue. Stop looking.
|
||||
break;
|
||||
}
|
||||
|
||||
if (!match) {
|
||||
// The commit does not match any more, we found it!
|
||||
return commit.split('_____')[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find the commit using the commit names, get the last commit in the integration branch.
|
||||
const remote = DevConfig.get('upstreamRemote', 'origin');
|
||||
console.log(`Head commit not found using commit messages. Get last commit from ${remote}/integration`);
|
||||
const hashes = await this.hashes(1, `${remote}/integration`);
|
||||
|
||||
return hashes[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of a certain remote.
|
||||
*
|
||||
* @param remote Remote name.
|
||||
* @return Promise resolved with the remote URL.
|
||||
*/
|
||||
getRemoteUrl(remote) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`git remote get-url ${remote}`, (err, url) => {
|
||||
if (url) {
|
||||
resolve(url.replace('\n', ''));
|
||||
} else {
|
||||
reject (err || 'Remote not found.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latest hashes from git log.
|
||||
*
|
||||
* @param count Number of commits to display.
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param format Pretty-print the contents of the commit logs in a given format.
|
||||
* @return Promise resolved with the list of hashes.
|
||||
*/
|
||||
async hashes(count, range, format) {
|
||||
format = format || '%H';
|
||||
|
||||
const hashList = await this.log(count, range, format);
|
||||
|
||||
const hashes = hashList.split('\n');
|
||||
hashes.pop(); // Remove last element, it's an empty string.
|
||||
|
||||
return hashes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the log command and returns the raw output.
|
||||
*
|
||||
* @param count Number of commits to display.
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param format Pretty-print the contents of the commit logs in a given format.
|
||||
* @param path Show only commits that are enough to explain how the files that match the specified paths came to be.
|
||||
* @return Promise resolved with the result.
|
||||
*/
|
||||
log(count, range, format, path) {
|
||||
if (typeof count == 'undefined') {
|
||||
count = 10;
|
||||
}
|
||||
|
||||
let command = 'git log';
|
||||
|
||||
if (count > 0) {
|
||||
command += ` -n ${count} `;
|
||||
}
|
||||
if (format) {
|
||||
command += ` --format=${format} `;
|
||||
}
|
||||
if (range){
|
||||
command += ` ${range} `;
|
||||
}
|
||||
if (path) {
|
||||
command += ` -- ${path}`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (err, result, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latest titles of the commit messages.
|
||||
*
|
||||
* @param count Number of commits to display.
|
||||
* @param range Show only commits in the specified revision range.
|
||||
* @param path Show only commits that are enough to explain how the files that match the specified paths came to be.
|
||||
* @return Promise resolved with the list of titles.
|
||||
*/
|
||||
async messages(count, range, path) {
|
||||
count = typeof count != 'undefined' ? count : 10;
|
||||
|
||||
const messageList = await this.log(count, range, '%s', path);
|
||||
|
||||
const messages = messageList.split('\n');
|
||||
messages.pop(); // Remove last element, it's an empty string.
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a branch.
|
||||
*
|
||||
* @param remote Remote to use.
|
||||
* @param branch Branch to push.
|
||||
* @param force Whether to force the push.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
push(remote, branch, force) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let command = `git push ${remote} ${branch}`;
|
||||
if (force) {
|
||||
command += ' -f';
|
||||
}
|
||||
|
||||
exec(command, (err, result, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Git();
|
|
@ -0,0 +1,475 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const exec = require('child_process').exec;
|
||||
const https = require('https');
|
||||
const inquirer = require('inquirer');
|
||||
const fs = require('fs');
|
||||
const request = require('request'); // This lib is deprecated, but it was the only way I found to make upload files work.
|
||||
const DevConfig = require('./dev-config');
|
||||
const Git = require('./git');
|
||||
const Url = require('./url');
|
||||
const Utils = require('./utils');
|
||||
|
||||
const apiVersion = 2;
|
||||
|
||||
/**
|
||||
* Class to interact with Jira.
|
||||
*/
|
||||
class Jira {
|
||||
|
||||
/**
|
||||
* Ask the password to the user.
|
||||
*
|
||||
* @return Promise resolved with the password.
|
||||
*/
|
||||
async askPassword() {
|
||||
const data = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
message: `Please enter the password for the username ${this.username}.`,
|
||||
},
|
||||
]);
|
||||
|
||||
return data.password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user the tracker data.
|
||||
*
|
||||
* @return Promise resolved with the data, rejected if cannot get.
|
||||
*/
|
||||
async askTrackerData() {
|
||||
const data = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'url',
|
||||
message: 'Please enter the tracker URL.',
|
||||
default: 'https://tracker.moodle.org/',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Please enter your tracker username.',
|
||||
},
|
||||
]);
|
||||
|
||||
DevConfig.save({
|
||||
'tracker.url': data.url,
|
||||
'tracker.username': data.username,
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build URL to perform requests to Jira.
|
||||
*
|
||||
* @param uri URI to add the the Jira URL.
|
||||
* @return URL.
|
||||
*/
|
||||
buildRequestUrl(uri) {
|
||||
return Utils.concatenatePaths([this.url, this.uri, '/rest/api/', apiVersion, uri]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an attachment.
|
||||
*
|
||||
* @param attachmentId Attachment ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async deleteAttachment(attachmentId) {
|
||||
const response = await this.request(`attachment/${attachmentId}`, 'DELETE');
|
||||
|
||||
if (response.status != 204) {
|
||||
throw new Error('Could not delete the attachment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the issue info from jira server using a REST API call.
|
||||
*
|
||||
* @param key Key to identify the issue. E.g. MOBILE-1234.
|
||||
* @param fields Fields to get.
|
||||
* @return Promise resolved with the issue data.
|
||||
*/
|
||||
async getIssue(key, fields) {
|
||||
fields = fields || '*all,-comment';
|
||||
|
||||
await this.init(); // Initialize data if needed.
|
||||
|
||||
const response = await this.request(`issue/${key}`, 'GET', {'fields': fields, 'expand': 'names'});
|
||||
|
||||
if (response.status == 404) {
|
||||
throw new Error('Issue could not be found.');
|
||||
} else if (response.status != 200) {
|
||||
throw new Error('The tracker is not available.')
|
||||
}
|
||||
|
||||
const issue = response.data;
|
||||
issue.named = {};
|
||||
|
||||
// Populate the named fields in a separate key. Allows us to easily find them without knowing the field ID.
|
||||
const nameList = issue.names || {};
|
||||
for (const fieldKey in issue.fields) {
|
||||
if (nameList[fieldKey]) {
|
||||
issue.named[nameList[fieldKey]] = issue.fields[fieldKey];
|
||||
}
|
||||
}
|
||||
|
||||
return issue
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the version info from the jira server using a rest api call.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async getServerInfo() {
|
||||
const response = await this.request('serverInfo');
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error(`Unexpected response code: ${response.status}`, response);
|
||||
}
|
||||
|
||||
this.version = response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracker data to push an issue.
|
||||
*
|
||||
* @return Promise resolved with the data.
|
||||
*/
|
||||
async getTrackerData() {
|
||||
// Check dev-config file first.
|
||||
let data = this.getTrackerDataFromDevConfig();
|
||||
|
||||
if (data) {
|
||||
console.log('Using tracker data from dev-config file');
|
||||
return data;
|
||||
}
|
||||
|
||||
// Try to use mdk now.
|
||||
try {
|
||||
data = await this.getTrackerDataFromMdk();
|
||||
|
||||
console.log('Using tracker data from mdk');
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
// MDK not available or not configured. Ask for the data.
|
||||
const data = await this.askTrackerData();
|
||||
|
||||
data.fromInput = true;
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracker data from dev config file.
|
||||
*
|
||||
* @return Data, undefined if cannot get.
|
||||
*/
|
||||
getTrackerDataFromDevConfig() {
|
||||
const url = DevConfig.get('tracker.url');
|
||||
const username = DevConfig.get('tracker.username');
|
||||
|
||||
if (url && username) {
|
||||
return {
|
||||
url,
|
||||
username,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracker URL and username from mdk.
|
||||
*
|
||||
* @return Promise resolved with the data, rejected if cannot get.
|
||||
*/
|
||||
getTrackerDataFromMdk() {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec('mdk config show tracker.url', (err, url) => {
|
||||
if (!url) {
|
||||
reject(err || 'URL not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
exec('mdk config show tracker.username', (err, username) => {
|
||||
if (username) {
|
||||
resolve({
|
||||
url: url.replace('\n', ''),
|
||||
username: username.replace('\n', ''),
|
||||
});
|
||||
} else {
|
||||
reject(err | 'Username not found.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize some data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async init() {
|
||||
if (this.initialized) {
|
||||
// Already initialized.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tracker URL and username.
|
||||
const trackerData = await this.getTrackerData();
|
||||
|
||||
this.url = trackerData.url;
|
||||
this.username = trackerData.username;
|
||||
|
||||
const parsed = Url.parse(this.url);
|
||||
this.ssl = parsed.protocol == 'https';
|
||||
this.host = parsed.domain;
|
||||
this.uri = parsed.path;
|
||||
|
||||
// Get the password.
|
||||
const keytar = require('keytar');
|
||||
|
||||
this.password = await keytar.getPassword('mdk-jira-password', this.username); // Use same service name as mdk.
|
||||
|
||||
if (!this.password) {
|
||||
// Ask the user.
|
||||
this.password = await this.askPassword();
|
||||
}
|
||||
|
||||
while (!this.initialized) {
|
||||
try {
|
||||
await this.getServerInfo();
|
||||
|
||||
this.initialized = true;
|
||||
keytar.setPassword('mdk-jira-password', this.username, this.password);
|
||||
} catch (error) {
|
||||
console.log('Error connecting to the server. Please make sure you entered the data correctly.', error);
|
||||
if (trackerData.fromInput) {
|
||||
// User entered the data manually, ask him again.
|
||||
trackerData = await this.askTrackerData();
|
||||
|
||||
this.url = trackerData.url;
|
||||
this.username = trackerData.username;
|
||||
}
|
||||
|
||||
this.password = await this.askPassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a certain issue could be a security issue.
|
||||
*
|
||||
* @param key Key to identify the issue. E.g. MOBILE-1234.
|
||||
* @return Promise resolved with boolean: whether it's a security issue.
|
||||
*/
|
||||
async isSecurityIssue(key) {
|
||||
const issue = await this.getIssue(key, 'security');
|
||||
|
||||
return issue.fields && !!issue.fields.security;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the server and returns the data.
|
||||
*
|
||||
* @param uri URI to add the the Jira URL.
|
||||
* @param method Method to use. Defaults to 'GET'.
|
||||
* @param params Params to send as GET params (in the URL).
|
||||
* @param data JSON string with the data to send as POST/PUT params.
|
||||
* @param headers Headers to send.
|
||||
* @return Promise resolved with the result.
|
||||
*/
|
||||
request(uri, method, params, data, headers) {
|
||||
uri = uri || '';
|
||||
method = (method || 'GET').toUpperCase();
|
||||
data = data || '';
|
||||
params = params || {};
|
||||
headers = headers || {};
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Build the request URL.
|
||||
const url = Url.addParamsToUrl(this.buildRequestUrl(uri), params);
|
||||
|
||||
// Initialize the request.
|
||||
const options = {
|
||||
method: method,
|
||||
auth: `${this.username}:${this.password}`,
|
||||
headers: headers,
|
||||
};
|
||||
const request = https.request(url, options);
|
||||
|
||||
// Add data.
|
||||
if (data) {
|
||||
request.write(data);
|
||||
}
|
||||
|
||||
// Treat response.
|
||||
request.on('response', (response) => {
|
||||
// Read the result.
|
||||
let result = '';
|
||||
response.on('data', (chunk) => {
|
||||
result += chunk;
|
||||
});
|
||||
response.on('end', () => {
|
||||
try {
|
||||
result = JSON.parse(result);
|
||||
} catch (error) {
|
||||
// Leave it as text.
|
||||
}
|
||||
|
||||
resolve({
|
||||
status: response.statusCode,
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', (e) => {
|
||||
reject(e);
|
||||
});
|
||||
|
||||
// Send the request.
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a set of fields for a certain issue in Jira.
|
||||
*
|
||||
* @param key Key to identify the issue. E.g. MOBILE-1234.
|
||||
* @param updates Object with the fields to update.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async setCustomFields(key, updates) {
|
||||
const issue = await this.getIssue(key);
|
||||
const update = {'fields': {}};
|
||||
|
||||
// Detect which fields have changed.
|
||||
for (const updateName in updates) {
|
||||
const updateValue = updates[updateName];
|
||||
const remoteValue = issue.named[updateName];
|
||||
|
||||
if (!remoteValue || remoteValue != updateValue) {
|
||||
// Map the label of the field with the field code.
|
||||
let fieldKey;
|
||||
for (const key in issue.names) {
|
||||
if (issue.names[key] == updateName) {
|
||||
fieldKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fieldKey) {
|
||||
throw new Error(`Could not find the field named ${updateName}.`);
|
||||
}
|
||||
|
||||
update.fields[fieldKey] = updateValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(update.fields).length) {
|
||||
// No fields to update.
|
||||
console.log('No updates required.')
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.request(`issue/${key}`, 'PUT', null, JSON.stringify(update));
|
||||
|
||||
if (response.status != 204) {
|
||||
throw new Error(`Issue was not updated: ${response.status}`, response.data);
|
||||
}
|
||||
|
||||
console.log('Issue updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a new attachment to an issue.
|
||||
*
|
||||
* @param key Key to identify the issue. E.g. MOBILE-1234.
|
||||
* @param filePath Path to the file to upload.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async upload(key, filePath) {
|
||||
|
||||
const uri = `issue/${key}/attachments`;
|
||||
const headers = {
|
||||
'X-Atlassian-Token': 'nocheck',
|
||||
}
|
||||
|
||||
const response = await this.uploadFile(uri, 'file', filePath, headers);
|
||||
|
||||
if (response.status != 200) {
|
||||
throw new Error('Could not upload file to Jira issue');
|
||||
}
|
||||
|
||||
console.log('File successfully uploaded.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to Jira.
|
||||
*
|
||||
* @param uri URI to add the the Jira URL.
|
||||
* @param fieldName Name of the form field where to put the file.
|
||||
* @param filePath Path to the file.
|
||||
* @param headers Headers.
|
||||
* @return Promise resolved with the result.
|
||||
*/
|
||||
async uploadFile(uri, fieldName, filePath, headers) {
|
||||
uri = uri || '';
|
||||
headers = headers || {};
|
||||
headers['Content-Type'] = 'multipart/form-data';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Add the file to the form data.
|
||||
const formData = {};
|
||||
formData[fieldName] = {
|
||||
value: fs.createReadStream(filePath),
|
||||
options: {
|
||||
filename: filePath.substr(filePath.lastIndexOf('/') + 1),
|
||||
contentType: 'multipart/form-data',
|
||||
},
|
||||
};
|
||||
|
||||
// Perform the request.
|
||||
const options = {
|
||||
url: this.buildRequestUrl(uri),
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
auth: {
|
||||
user: this.username,
|
||||
pass: this.password,
|
||||
},
|
||||
formData: formData,
|
||||
};
|
||||
|
||||
request(options, (err, httpResponse, body) => {
|
||||
resolve({
|
||||
status: httpResponse.statusCode,
|
||||
data: body,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Jira();
|
|
@ -0,0 +1,190 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const gulp = require('gulp');
|
||||
const slash = require('gulp-slash');
|
||||
const clipEmptyFiles = require('gulp-clip-empty-files');
|
||||
const through = require('through');
|
||||
const bufferFrom = require('buffer-from');
|
||||
const File = require('vinyl');
|
||||
const pathLib = require('path');
|
||||
|
||||
/**
|
||||
* Task to build the language files into a single file per language.
|
||||
*/
|
||||
class BuildLangTask {
|
||||
|
||||
/**
|
||||
* Copy a property from one object to another, adding a prefix to the key if needed.
|
||||
*
|
||||
* @param target Object to copy the properties to.
|
||||
* @param source Object to copy the properties from.
|
||||
* @param prefix Prefix to add to the keys.
|
||||
*/
|
||||
addProperties(target, source, prefix) {
|
||||
for (let property in source) {
|
||||
target[prefix + property] = source[property];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task.
|
||||
*
|
||||
* @param language Language to treat.
|
||||
* @param langPaths Paths to the possible language files.
|
||||
* @param done Function to call when done.
|
||||
*/
|
||||
run(language, langPaths, done) {
|
||||
const filename = language + '.json';
|
||||
const data = {};
|
||||
let firstFile = null;
|
||||
const self = this;
|
||||
|
||||
const paths = langPaths.map((path) => {
|
||||
if (path.slice(-1) != '/') {
|
||||
path = path + '/';
|
||||
}
|
||||
|
||||
return path + language + '.json';
|
||||
});
|
||||
|
||||
gulp.src(paths, { allowEmpty: true })
|
||||
.pipe(slash())
|
||||
.pipe(clipEmptyFiles())
|
||||
.pipe(through(function(file) {
|
||||
if (!firstFile) {
|
||||
firstFile = file;
|
||||
}
|
||||
|
||||
return self.treatFile(file, data);
|
||||
}, function() {
|
||||
/* This implementation is based on gulp-jsoncombine module.
|
||||
* https://github.com/reflog/gulp-jsoncombine */
|
||||
if (firstFile) {
|
||||
const joinedPath = pathLib.join(firstFile.base, language + '.json');
|
||||
|
||||
const joinedFile = new File({
|
||||
cwd: firstFile.cwd,
|
||||
base: firstFile.base,
|
||||
path: joinedPath,
|
||||
contents: self.treatMergedData(data),
|
||||
});
|
||||
|
||||
this.emit('data', joinedFile);
|
||||
}
|
||||
|
||||
this.emit('end');
|
||||
}))
|
||||
.pipe(gulp.dest(pathLib.join('./src/assets', 'lang')))
|
||||
.on('end', done);
|
||||
}
|
||||
|
||||
/**
|
||||
* Treats a file to merge JSONs. This function is based on gulp-jsoncombine module.
|
||||
* https://github.com/reflog/gulp-jsoncombine
|
||||
*
|
||||
* @param file File treated.
|
||||
* @param data Object where to store the data.
|
||||
*/
|
||||
treatFile(file, data) {
|
||||
if (file.isNull() || file.isStream()) {
|
||||
return; // ignore
|
||||
}
|
||||
|
||||
try {
|
||||
let path = file.path;
|
||||
let length = 9;
|
||||
|
||||
let srcPos = path.lastIndexOf('/src/app/');
|
||||
if (srcPos < 0) {
|
||||
// It's probably a Windows environment.
|
||||
srcPos = path.lastIndexOf('\\src\\app\\');
|
||||
}
|
||||
if (srcPos < 0) {
|
||||
length = 5;
|
||||
srcPos = path.lastIndexOf('/src/');
|
||||
if (srcPos < 0) {
|
||||
// It's probably a Windows environment.
|
||||
srcPos = path.lastIndexOf('\\src\\');
|
||||
}
|
||||
}
|
||||
path = path.substr(srcPos + length);
|
||||
|
||||
data[path] = JSON.parse(file.contents.toString());
|
||||
} catch (err) {
|
||||
console.log('Error parsing JSON: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Treats the merged JSON data, adding prefixes depending on the component.
|
||||
*
|
||||
* @param data Merged data.
|
||||
* @return Buffer with the treated data.
|
||||
*/
|
||||
treatMergedData(data) {
|
||||
const merged = {};
|
||||
const mergedOrdered = {};
|
||||
|
||||
for (let filepath in data) {
|
||||
|
||||
const pathSplit = filepath.split(/[\/\\]/);
|
||||
let prefix;
|
||||
|
||||
pathSplit.pop();
|
||||
|
||||
switch (pathSplit[0]) {
|
||||
case 'lang':
|
||||
prefix = 'core';
|
||||
break;
|
||||
case 'core':
|
||||
if (pathSplit[1] == 'lang') {
|
||||
// Not used right now.
|
||||
prefix = 'core';
|
||||
} else {
|
||||
prefix = 'core.' + pathSplit[1];
|
||||
}
|
||||
break;
|
||||
case 'addon':
|
||||
// Remove final item 'lang'.
|
||||
pathSplit.pop();
|
||||
// Remove first item 'addon'.
|
||||
pathSplit.shift();
|
||||
|
||||
// For subplugins. We'll use plugin_subfolder_subfolder2_...
|
||||
// E.g. 'mod_assign_feedback_comments'.
|
||||
prefix = 'addon.' + pathSplit.join('_');
|
||||
break;
|
||||
case 'assets':
|
||||
prefix = 'assets.' + pathSplit[1];
|
||||
break;
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
this.addProperties(merged, data[filepath], prefix + '.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Force ordering by string key.
|
||||
Object.keys(merged).sort().forEach((key) => {
|
||||
mergedOrdered[key] = merged[key];
|
||||
});
|
||||
|
||||
return bufferFrom(JSON.stringify(mergedOrdered, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BuildLangTask;
|
|
@ -0,0 +1,280 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const gulp = require('gulp');
|
||||
const inquirer = require('inquirer');
|
||||
const DevConfig = require('./dev-config');
|
||||
const Git = require('./git');
|
||||
const Jira = require('./jira');
|
||||
const Utils = require('./utils');
|
||||
|
||||
/**
|
||||
* Task to push a git branch and update tracker issue.
|
||||
*/
|
||||
class PushTask {
|
||||
|
||||
/**
|
||||
* Ask the user whether he wants to continue.
|
||||
*
|
||||
* @return Promise resolved with boolean: true if he wants to continue.
|
||||
*/
|
||||
async askConfirmContinue() {
|
||||
const answer = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'confirm',
|
||||
message: 'Are you sure you want to continue?',
|
||||
default: 'n',
|
||||
},
|
||||
]);
|
||||
|
||||
return answer.confirm == 'y';
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a patch to the tracker and remove the previous one.
|
||||
*
|
||||
* @param branch Branch name.
|
||||
* @param branchData Parsed branch data.
|
||||
* @param remote Remote used.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async pushPatch(branch, branchData, remote) {
|
||||
const headCommit = await Git.getHeadCommit(branch, branchData);
|
||||
|
||||
if (!headCommit) {
|
||||
throw new Error('Head commit not resolved, abort pushing patch.');
|
||||
}
|
||||
|
||||
// Create the patch file.
|
||||
const fileName = branch + '.patch';
|
||||
const tmpPatchPath = `./tmp/${fileName}`;
|
||||
|
||||
await Git.createPatch(`${headCommit}...${branch}`, tmpPatchPath);
|
||||
console.log('Git patch created');
|
||||
|
||||
// Check if there is an attachment with same name in the issue.
|
||||
const issue = await Jira.getIssue(branchData.issue, 'attachment');
|
||||
|
||||
let existingAttachmentId;
|
||||
const attachments = (issue.fields && issue.fields.attachment) || [];
|
||||
for (const i in attachments) {
|
||||
if (attachments[i].filename == fileName) {
|
||||
// Found an existing attachment with the same name, we keep track of it.
|
||||
existingAttachmentId = attachments[i].id;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Push the patch to the tracker.
|
||||
console.log(`Uploading patch ${fileName} to the tracker...`);
|
||||
await Jira.upload(branchData.issue, tmpPatchPath);
|
||||
|
||||
if (existingAttachmentId) {
|
||||
// On success, deleting file that was there before.
|
||||
try {
|
||||
console.log('Deleting older patch...')
|
||||
await Jira.deleteAttachment(existingAttachmentId);
|
||||
} catch (error) {
|
||||
console.log('Could not delete older attachment.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the task.
|
||||
*
|
||||
* @param args Command line arguments.
|
||||
* @param done Function to call when done.
|
||||
*/
|
||||
async run(args, done) {
|
||||
try {
|
||||
const remote = args.remote || DevConfig.get('upstreamRemote', 'origin');
|
||||
let branch = args.branch;
|
||||
const force = !!args.force;
|
||||
|
||||
if (!branch) {
|
||||
branch = await Git.getCurrentBranch();
|
||||
}
|
||||
|
||||
if (!branch) {
|
||||
throw new Error('Cannot determine the current branch. Please make sure youu aren\'t in detached HEAD state');
|
||||
} else if (branch == 'HEAD') {
|
||||
throw new Error('Cannot push HEAD branch');
|
||||
}
|
||||
|
||||
// Parse the branch to get the project and issue number.
|
||||
const branchData = Utils.parseBranch(branch);
|
||||
const keepRunning = await this.validateCommitMessages(branchData);
|
||||
|
||||
if (!keepRunning) {
|
||||
// Last commit not valid, stop.
|
||||
console.log('Exiting...');
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.patch) {
|
||||
// Check if it's a security issue to force patch mode.
|
||||
try {
|
||||
args.patch = await Jira.isSecurityIssue(branchData.issue);
|
||||
|
||||
if (args.patch) {
|
||||
console.log(`${branchData.issue} appears to be a security issue, switching to patch mode...`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Could not check if ${branchData.issue} is a security issue.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.patch) {
|
||||
// Create and upload a patch file.
|
||||
await this.pushPatch(branch, branchData, remote);
|
||||
} else {
|
||||
// Push the branch.
|
||||
console.log(`Pushing branch ${branch} to remote ${remote}...`);
|
||||
await Git.push(remote, branch, force);
|
||||
|
||||
// Update tracker info.
|
||||
console.log(`Branch pushed, update tracker info...`);
|
||||
await this.updateTrackerGitInfo(branch, branchData, remote);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update git info in the tracker issue.
|
||||
*
|
||||
* @param branch Branch name.
|
||||
* @param branchData Parsed branch data.
|
||||
* @param remote Remote used.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async updateTrackerGitInfo(branch, branchData, remote) {
|
||||
// Get the repository data for the project.
|
||||
let repositoryUrl = DevConfig.get(branchData.project + '.repositoryUrl');
|
||||
let diffUrlTemplate = DevConfig.get(branchData.project + '.diffUrlTemplate', '');
|
||||
|
||||
if (!repositoryUrl) {
|
||||
// Calculate the repositoryUrl based on the remote URL.
|
||||
repositoryUrl = await Git.getRemoteUrl(remote);
|
||||
}
|
||||
|
||||
// Make sure the repository URL uses the regular format.
|
||||
repositoryUrl = repositoryUrl.replace(/^(git@|git:\/\/)/, 'https://')
|
||||
.replace(/\.git$/, '')
|
||||
.replace('github.com:', 'github.com/');
|
||||
|
||||
if (!diffUrlTemplate) {
|
||||
diffUrlTemplate = Utils.concatenatePaths([repositoryUrl, 'compare/%headcommit%...%branch%']);
|
||||
}
|
||||
|
||||
// Now create the git URL for the repository.
|
||||
const repositoryGitUrl = repositoryUrl.replace(/^https?:\/\//, 'git://') + '.git';
|
||||
|
||||
// Search HEAD commit to put in the diff URL.
|
||||
console.log ('Searching for head commit...');
|
||||
let headCommit = await Git.getHeadCommit(branch, branchData);
|
||||
|
||||
if (!headCommit) {
|
||||
throw new Error('Head commit not resolved, aborting update of tracker fields');
|
||||
}
|
||||
|
||||
headCommit = headCommit.substr(0, 10);
|
||||
console.log(`Head commit resolved to ${headCommit}`);
|
||||
|
||||
// Calculate last properties needed.
|
||||
const diffUrl = diffUrlTemplate.replace('%branch%', branch).replace('%headcommit%', headCommit);
|
||||
const fieldRepositoryUrl = DevConfig.get('tracker.fieldnames.repositoryurl', 'Pull from Repository');
|
||||
const fieldBranch = DevConfig.get('tracker.fieldnames.branch', 'Pull Master Branch');
|
||||
const fieldDiffUrl = DevConfig.get('tracker.fieldnames.diffurl', 'Pull Master Diff URL');
|
||||
|
||||
// Update tracker fields.
|
||||
const updates = {};
|
||||
updates[fieldRepositoryUrl] = repositoryGitUrl;
|
||||
updates[fieldBranch] = branch;
|
||||
updates[fieldDiffUrl] = diffUrl;
|
||||
|
||||
console.log('Setting tracker fields...');
|
||||
await Jira.setCustomFields(branchData.issue, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate commit messages comparing them with the branch name.
|
||||
*
|
||||
* @param branchData Parsed branch data.
|
||||
* @return True if value is ok or the user wants to continue anyway, false to stop.
|
||||
*/
|
||||
async validateCommitMessages(branchData) {
|
||||
const messages = await Git.messages(30);
|
||||
|
||||
let numConsecutive = 0;
|
||||
let wrongCommitCandidate = null;
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
const issue = Utils.getIssueFromCommitMessage(message);
|
||||
|
||||
if (!issue || issue != branchData.issue) {
|
||||
if (i === 0) {
|
||||
// Last commit is wrong, it shouldn't happen. Ask the user if he wants to continue.
|
||||
if (!issue) {
|
||||
console.log('The issue number could not be found in the last commit message.');
|
||||
console.log(`Commit: ${message}`);
|
||||
} else if (issue != branchData.issue) {
|
||||
console.log('The issue number in the last commit does not match the branch being pushed to.');
|
||||
console.log(`Branch: ${branchData.issue} vs. commit: ${issue}`);
|
||||
}
|
||||
|
||||
return this.askConfirmContinue();
|
||||
}
|
||||
|
||||
numConsecutive++;
|
||||
if (numConsecutive > 2) {
|
||||
// 3 consecutive commits with different branch, probably the branch commits are over. Everything OK.
|
||||
return true;
|
||||
|
||||
// Don't treat a merge pull request commit as a wrong commit between right commits.
|
||||
// The current push could be a quick fix after a merge.
|
||||
} else if (!wrongCommitCandidate && message.indexOf('Merge pull request') == -1) {
|
||||
wrongCommitCandidate = {
|
||||
message: message,
|
||||
issue: issue,
|
||||
index: i,
|
||||
};
|
||||
}
|
||||
} else if (wrongCommitCandidate) {
|
||||
// We've found a commit with the branch name after a commit with a different branch. Probably wrong commit.
|
||||
if (!wrongCommitCandidate.issue) {
|
||||
console.log('The issue number could not be found in one of the commit messages.');
|
||||
console.log(`Commit: ${wrongCommitCandidate.message}`);
|
||||
} else {
|
||||
console.log('The issue number in a certain commit does not match the branch being pushed to.');
|
||||
console.log(`Branch: ${branchData.issue} vs. commit: ${wrongCommitCandidate.issue}`);
|
||||
console.log(`Commit message: ${wrongCommitCandidate.message}`);
|
||||
}
|
||||
|
||||
return this.askConfirmContinue();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PushTask;
|
|
@ -0,0 +1,79 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* Class with helper functions for urls.
|
||||
*/
|
||||
class Url {
|
||||
|
||||
/**
|
||||
* Add params to a URL.
|
||||
*
|
||||
* @param url URL to add the params to.
|
||||
* @param params Object with the params to add.
|
||||
* @return URL with params.
|
||||
*/
|
||||
static addParamsToUrl(url, params) {
|
||||
let separator = url.indexOf('?') != -1 ? '&' : '?';
|
||||
|
||||
for (const key in params) {
|
||||
let value = params[key];
|
||||
|
||||
// Ignore objects.
|
||||
if (typeof value != 'object') {
|
||||
url += separator + key + '=' + value;
|
||||
separator = '&';
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse parts of a url, using an implicit protocol if it is missing from the url.
|
||||
*
|
||||
* @param url Url.
|
||||
* @return Url parts.
|
||||
*/
|
||||
static parse(url) {
|
||||
// Parse url with regular expression taken from RFC 3986: https://tools.ietf.org/html/rfc3986#appendix-B.
|
||||
const match = url.trim().match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = match[4] || '';
|
||||
|
||||
// Get the credentials and the port from the host.
|
||||
const [domainAndPort, credentials] = host.split('@').reverse();
|
||||
const [domain, port] = domainAndPort.split(':');
|
||||
const [username, password] = credentials ? credentials.split(':') : [];
|
||||
|
||||
// Prepare parts replacing empty strings with undefined.
|
||||
return {
|
||||
protocol: match[2] || undefined,
|
||||
domain: domain || undefined,
|
||||
port: port || undefined,
|
||||
credentials: credentials || undefined,
|
||||
username: username || undefined,
|
||||
password: password || undefined,
|
||||
path: match[5] || undefined,
|
||||
query: match[7] || undefined,
|
||||
fragment: match[9] || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Url;
|
|
@ -0,0 +1,119 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const DevConfig = require('./dev-config');
|
||||
const DEFAULT_ISSUE_REGEX = '^(MOBILE)[-_]([0-9]+)';
|
||||
|
||||
/**
|
||||
* Class with some utility functions.
|
||||
*/
|
||||
class Utils {
|
||||
/**
|
||||
* Concatenate several paths, adding a slash between them if needed.
|
||||
*
|
||||
* @param paths List of paths.
|
||||
* @return Concatenated path.
|
||||
*/
|
||||
static concatenatePaths(paths) {
|
||||
if (!paths.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Remove all slashes between paths.
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
if (!paths[i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
paths[i] = String(paths[i]).replace(/\/+$/g, '');
|
||||
} else if (i === paths.length - 1) {
|
||||
paths[i] = String(paths[i]).replace(/^\/+/g, '');
|
||||
} else {
|
||||
paths[i] = String(paths[i]).replace(/^\/+|\/+$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty paths.
|
||||
paths = paths.filter(path => !!path);
|
||||
|
||||
return paths.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get command line arguments.
|
||||
*
|
||||
* @return Object with command line arguments.
|
||||
*/
|
||||
static getCommandLineArguments() {
|
||||
|
||||
let args = {}, opt, thisOpt, curOpt;
|
||||
for (let a = 0; a < process.argv.length; a++) {
|
||||
|
||||
thisOpt = process.argv[a].trim();
|
||||
opt = thisOpt.replace(/^\-+/, '');
|
||||
|
||||
if (opt === thisOpt) {
|
||||
// argument value
|
||||
if (curOpt) {
|
||||
args[curOpt] = opt;
|
||||
}
|
||||
curOpt = null;
|
||||
}
|
||||
else {
|
||||
// Argument name.
|
||||
curOpt = opt;
|
||||
args[curOpt] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a commit message, return the issue name (e.g. MOBILE-1234).
|
||||
*
|
||||
* @param commit Commit message.
|
||||
* @return Issue name.
|
||||
*/
|
||||
static getIssueFromCommitMessage(commit) {
|
||||
const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i');
|
||||
const matches = commit.match(regex);
|
||||
|
||||
return matches && matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a branch name to extract some data.
|
||||
*
|
||||
* @param branch Branch name to parse.
|
||||
* @return Data.
|
||||
*/
|
||||
static parseBranch(branch) {
|
||||
const regex = new RegExp(DevConfig.get('wording.branchRegex', DEFAULT_ISSUE_REGEX), 'i');
|
||||
|
||||
const matches = branch.match(regex);
|
||||
if (!matches || matches.length < 3) {
|
||||
throw new Error(`Error parsing branch ${branch}`);
|
||||
}
|
||||
|
||||
return {
|
||||
issue: matches[0],
|
||||
project: matches[1],
|
||||
issueNumber: matches[2],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Utils;
|
|
@ -0,0 +1,48 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
const BuildLangTask = require('./gulp/task-build-lang');
|
||||
const PushTask = require('./gulp/task-push');
|
||||
const Utils = require('./gulp/utils');
|
||||
const gulp = require('gulp');
|
||||
|
||||
const paths = {
|
||||
lang: [
|
||||
'./src/app/lang/',
|
||||
'./src/app/core/**/lang/',
|
||||
'./src/app/addon/**/lang/',
|
||||
'./src/app/**/**/lang/',
|
||||
'./src/assets/countries/',
|
||||
'./src/assets/mimetypes/'
|
||||
]
|
||||
};
|
||||
|
||||
const args = Utils.getCommandLineArguments();
|
||||
|
||||
// Build the language files into a single file per language.
|
||||
gulp.task('lang', (done) => {
|
||||
new BuildLangTask().run('en', paths.lang, done);
|
||||
});
|
||||
|
||||
gulp.task('push', (done) => {
|
||||
new PushTask().run(args, done);
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.parallel('lang'));
|
||||
|
||||
gulp.task('watch', () => {
|
||||
const langsPaths = paths.lang.map(path => path + 'en.json');
|
||||
|
||||
gulp.watch(langsPaths, { interval: 500 }, gulp.parallel('lang'));
|
||||
});
|
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
|
@ -25,7 +25,8 @@
|
|||
"test:ci": "jest -ci --runInBand --verbose",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "ng lint"
|
||||
"lint": "ng lint",
|
||||
"ionic:serve:before": "npx gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/common": "~10.0.0",
|
||||
|
@ -42,7 +43,6 @@
|
|||
"@ionic-native/file-opener": "^5.28.0",
|
||||
"@ionic-native/file-transfer": "^5.28.0",
|
||||
"@ionic-native/geolocation": "^5.28.0",
|
||||
"@ionic-native/globalization": "^5.28.0",
|
||||
"@ionic-native/http": "^5.28.0",
|
||||
"@ionic-native/in-app-browser": "^5.28.0",
|
||||
"@ionic-native/ionic-webview": "^5.28.0",
|
||||
|
@ -79,7 +79,6 @@
|
|||
"cordova-plugin-file-opener2": "^3.0.5",
|
||||
"cordova-plugin-file-transfer": "1.7.1",
|
||||
"cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff",
|
||||
"cordova-plugin-globalization": "1.11.0",
|
||||
"cordova-plugin-inappbrowser": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle",
|
||||
"cordova-plugin-ionic-keyboard": "2.1.3",
|
||||
"cordova-plugin-ionic-webview": "git+https://github.com/moodlemobile/cordova-plugin-ionic-webview.git#500-moodle",
|
||||
|
@ -134,6 +133,13 @@
|
|||
"eslint-plugin-prefer-arrow": "^1.2.2",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"faker": "^5.1.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-clip-empty-files": "^0.1.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-flatten": "^0.4.0",
|
||||
"gulp-htmlmin": "^5.0.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-slash": "^1.1.3",
|
||||
"jest": "^26.5.0",
|
||||
"jest-preset-angular": "^8.3.1",
|
||||
"ts-jest": "^26.4.1",
|
||||
|
|
|
@ -12,11 +12,40 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CoreLangProvider } from '@services/lang';
|
||||
import { CoreEvents, CoreEventsProvider } from '@services/events';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: 'app.component.html',
|
||||
styleUrls: ['app.component.scss'],
|
||||
})
|
||||
export class AppComponent { }
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private langProvider: CoreLangProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
CoreEvents.instance.on(CoreEventsProvider.LOGOUT, () => {
|
||||
// Go to sites page when user is logged out.
|
||||
// Due to DeepLinker, we need to use the ViewCtrl instead of name.
|
||||
// Otherwise some pages are re-created when they shouldn't.
|
||||
// TODO
|
||||
// CoreApp.instance.getRootNavController().setRoot(CoreLoginSitesPage);
|
||||
|
||||
// Unload lang custom strings.
|
||||
this.langProvider.clearCustomStrings();
|
||||
|
||||
// Remove version classes from body.
|
||||
// TODO
|
||||
// this.removeVersionClass();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -156,8 +156,14 @@ export class SQLiteDB {
|
|||
* @param tableCheck Check constraint for the table.
|
||||
* @return SQL query.
|
||||
*/
|
||||
buildCreateTableSql(name: string, columns: SQLiteDBColumnSchema[], primaryKeys?: string[], uniqueKeys?: string[][],
|
||||
foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string): string {
|
||||
buildCreateTableSql(
|
||||
name: string,
|
||||
columns: SQLiteDBColumnSchema[],
|
||||
primaryKeys?: string[],
|
||||
uniqueKeys?: string[][],
|
||||
foreignKeys?: SQLiteDBForeignKeySchema[],
|
||||
tableCheck?: string,
|
||||
): string {
|
||||
const columnsSql = [];
|
||||
let sql = `CREATE TABLE IF NOT EXISTS ${name} (`;
|
||||
|
||||
|
@ -258,7 +264,7 @@ export class SQLiteDB {
|
|||
async countRecords(table: string, conditions?: SQLiteDBRecordValues): Promise<number> {
|
||||
const selectAndParams = this.whereClause(conditions);
|
||||
|
||||
return this.countRecordsSelect(table, selectAndParams[0], selectAndParams[1]);
|
||||
return this.countRecordsSelect(table, selectAndParams.sql, selectAndParams.params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -270,8 +276,12 @@ export class SQLiteDB {
|
|||
* @param countItem The count string to be used in the SQL call. Default is COUNT('x').
|
||||
* @return Promise resolved with the count of records returned from the specified criteria.
|
||||
*/
|
||||
async countRecordsSelect(table: string, select: string = '', params?: SQLiteDBRecordValue[],
|
||||
countItem: string = 'COUNT(\'x\')'): Promise<number> {
|
||||
async countRecordsSelect(
|
||||
table: string,
|
||||
select: string = '',
|
||||
params?: SQLiteDBRecordValue[],
|
||||
countItem: string = 'COUNT(\'x\')',
|
||||
): Promise<number> {
|
||||
if (select) {
|
||||
select = 'WHERE ' + select;
|
||||
}
|
||||
|
@ -308,8 +318,14 @@ export class SQLiteDB {
|
|||
* @param tableCheck Check constraint for the table.
|
||||
* @return Promise resolved when success.
|
||||
*/
|
||||
async createTable(name: string, columns: SQLiteDBColumnSchema[], primaryKeys?: string[], uniqueKeys?: string[][],
|
||||
foreignKeys?: SQLiteDBForeignKeySchema[], tableCheck?: string): Promise<void> {
|
||||
async createTable(
|
||||
name: string,
|
||||
columns: SQLiteDBColumnSchema[],
|
||||
primaryKeys?: string[],
|
||||
uniqueKeys?: string[][],
|
||||
foreignKeys?: SQLiteDBForeignKeySchema[],
|
||||
tableCheck?: string,
|
||||
): Promise<void> {
|
||||
const sql = this.buildCreateTableSql(name, columns, primaryKeys, uniqueKeys, foreignKeys, tableCheck);
|
||||
|
||||
await this.execute(sql);
|
||||
|
@ -358,7 +374,7 @@ export class SQLiteDB {
|
|||
|
||||
const selectAndParams = this.whereClause(conditions);
|
||||
|
||||
return this.deleteRecordsSelect(table, selectAndParams[0], selectAndParams[1]);
|
||||
return this.deleteRecordsSelect(table, selectAndParams.sql, selectAndParams.params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -372,7 +388,7 @@ export class SQLiteDB {
|
|||
async deleteRecordsList(table: string, field: string, values: SQLiteDBRecordValue[]): Promise<number> {
|
||||
const selectAndParams = this.whereClauseList(field, values);
|
||||
|
||||
return this.deleteRecordsSelect(table, selectAndParams[0], selectAndParams[1]);
|
||||
return this.deleteRecordsSelect(table, selectAndParams.sql, selectAndParams.params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -483,7 +499,7 @@ export class SQLiteDB {
|
|||
async getField(table: string, field: string, conditions?: SQLiteDBRecordValues): Promise<SQLiteDBRecordValue> {
|
||||
const selectAndParams = this.whereClause(conditions);
|
||||
|
||||
return this.getFieldSelect(table, field, selectAndParams[0], selectAndParams[1]);
|
||||
return this.getFieldSelect(table, field, selectAndParams.sql, selectAndParams.params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -495,8 +511,12 @@ export class SQLiteDB {
|
|||
* @param params Array of sql parameters.
|
||||
* @return Promise resolved with the field's value.
|
||||
*/
|
||||
async getFieldSelect(table: string, field: string, select: string = '', params?: SQLiteDBRecordValue[]):
|
||||
Promise<SQLiteDBRecordValue> {
|
||||
async getFieldSelect(
|
||||
table: string,
|
||||
field: string,
|
||||
select: string = '',
|
||||
params?: SQLiteDBRecordValue[],
|
||||
): Promise<SQLiteDBRecordValue> {
|
||||
if (select) {
|
||||
select = 'WHERE ' + select;
|
||||
}
|
||||
|
@ -529,8 +549,11 @@ export class SQLiteDB {
|
|||
* meaning return empty. Other values will become part of the returned SQL fragment.
|
||||
* @return A list containing the constructed sql fragment and an array of parameters.
|
||||
*/
|
||||
getInOrEqual(items: SQLiteDBRecordValue | SQLiteDBRecordValue[], equal: boolean = true, onEmptyItems?: SQLiteDBRecordValue):
|
||||
SQLiteDBQueryParams {
|
||||
getInOrEqual(
|
||||
items: SQLiteDBRecordValue | SQLiteDBRecordValue[],
|
||||
equal: boolean = true,
|
||||
onEmptyItems?: SQLiteDBRecordValue,
|
||||
): SQLiteDBQueryParams {
|
||||
let sql = '';
|
||||
let params: SQLiteDBRecordValue[];
|
||||
|
||||
|
@ -581,7 +604,7 @@ export class SQLiteDB {
|
|||
getRecord<T = unknown>(table: string, conditions?: SQLiteDBRecordValues, fields: string = '*'): Promise<T> {
|
||||
const selectAndParams = this.whereClause(conditions);
|
||||
|
||||
return this.getRecordSelect<T>(table, selectAndParams[0], selectAndParams[1], fields);
|
||||
return this.getRecordSelect<T>(table, selectAndParams.sql, selectAndParams.params, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -593,8 +616,12 @@ export class SQLiteDB {
|
|||
* @param fields A comma separated list of fields to return.
|
||||
* @return Promise resolved with the record, rejected if not found.
|
||||
*/
|
||||
getRecordSelect<T = unknown>(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'):
|
||||
Promise<T> {
|
||||
getRecordSelect<T = unknown>(
|
||||
table: string,
|
||||
select: string = '',
|
||||
params: SQLiteDBRecordValue[] = [],
|
||||
fields: string = '*',
|
||||
): Promise<T> {
|
||||
if (select) {
|
||||
select = ' WHERE ' + select;
|
||||
}
|
||||
|
@ -633,11 +660,17 @@ export class SQLiteDB {
|
|||
* @param limitNum Return a subset comprising this many records in total.
|
||||
* @return Promise resolved with the records.
|
||||
*/
|
||||
getRecords<T = unknown>(table: string, conditions?: SQLiteDBRecordValues, sort: string = '', fields: string = '*',
|
||||
limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
|
||||
getRecords<T = unknown>(
|
||||
table: string,
|
||||
conditions?: SQLiteDBRecordValues,
|
||||
sort: string = '',
|
||||
fields: string = '*',
|
||||
limitFrom: number = 0,
|
||||
limitNum: number = 0,
|
||||
): Promise<T[]> {
|
||||
const selectAndParams = this.whereClause(conditions);
|
||||
|
||||
return this.getRecordsSelect<T>(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum);
|
||||
return this.getRecordsSelect<T>(table, selectAndParams.sql, selectAndParams.params, sort, fields, limitFrom, limitNum);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -652,11 +685,18 @@ export class SQLiteDB {
|
|||
* @param limitNum Return a subset comprising this many records in total.
|
||||
* @return Promise resolved with the records.
|
||||
*/
|
||||
getRecordsList<T = unknown>(table: string, field: string, values: SQLiteDBRecordValue[], sort: string = '',
|
||||
fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
|
||||
getRecordsList<T = unknown>(
|
||||
table: string,
|
||||
field: string,
|
||||
values: SQLiteDBRecordValue[],
|
||||
sort: string = '',
|
||||
fields: string = '*',
|
||||
limitFrom: number = 0,
|
||||
limitNum: number = 0,
|
||||
): Promise<T[]> {
|
||||
const selectAndParams = this.whereClauseList(field, values);
|
||||
|
||||
return this.getRecordsSelect<T>(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum);
|
||||
return this.getRecordsSelect<T>(table, selectAndParams.sql, selectAndParams.params, sort, fields, limitFrom, limitNum);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -671,8 +711,15 @@ export class SQLiteDB {
|
|||
* @param limitNum Return a subset comprising this many records in total.
|
||||
* @return Promise resolved with the records.
|
||||
*/
|
||||
getRecordsSelect<T = unknown>(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], sort: string = '',
|
||||
fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
|
||||
getRecordsSelect<T = unknown>(
|
||||
table: string,
|
||||
select: string = '',
|
||||
params: SQLiteDBRecordValue[] = [],
|
||||
sort: string = '',
|
||||
fields: string = '*',
|
||||
limitFrom: number = 0,
|
||||
limitNum: number = 0,
|
||||
): Promise<T[]> {
|
||||
if (select) {
|
||||
select = ' WHERE ' + select;
|
||||
}
|
||||
|
@ -694,8 +741,12 @@ export class SQLiteDB {
|
|||
* @param limitNum Return a subset comprising this many records.
|
||||
* @return Promise resolved with the records.
|
||||
*/
|
||||
async getRecordsSql<T = unknown>(sql: string, params?: SQLiteDBRecordValue[], limitFrom?: number, limitNum?: number):
|
||||
Promise<T[]> {
|
||||
async getRecordsSql<T = unknown>(
|
||||
sql: string,
|
||||
params?: SQLiteDBRecordValue[],
|
||||
limitFrom?: number,
|
||||
limitNum?: number,
|
||||
): Promise<T[]> {
|
||||
const limits = this.normaliseLimitFromNum(limitFrom, limitNum);
|
||||
|
||||
if (limits[0] || limits[1]) {
|
||||
|
@ -746,6 +797,8 @@ export class SQLiteDB {
|
|||
}))
|
||||
.then((db: SQLiteObject) => {
|
||||
this.db = db;
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -758,7 +811,7 @@ export class SQLiteDB {
|
|||
*/
|
||||
async insertRecord(table: string, data: SQLiteDBRecordValues): Promise<number> {
|
||||
const sqlAndParams = this.getSqlInsertQuery(table, data);
|
||||
const result = await this.execute(sqlAndParams[0], sqlAndParams[1]);
|
||||
const result = await this.execute(sqlAndParams.sql, sqlAndParams.params);
|
||||
|
||||
return result.insertId;
|
||||
}
|
||||
|
@ -772,7 +825,7 @@ export class SQLiteDB {
|
|||
*/
|
||||
async insertRecords(table: string, dataObjects: SQLiteDBRecordValues[]): Promise<void> {
|
||||
if (!Array.isArray(dataObjects)) {
|
||||
throw new CoreError('Invalid parameter supplied to insertRecords, it should be an array.');
|
||||
throw new CoreError('Invalid parameter supplied to insertRecords, it should be an array.');
|
||||
}
|
||||
|
||||
const statements = dataObjects.map((dataObject) => {
|
||||
|
@ -793,11 +846,15 @@ export class SQLiteDB {
|
|||
* @param fields A comma separated list of fields to return.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async insertRecordsFrom(table: string, source: string, conditions?: SQLiteDBRecordValues, fields: string = '*'):
|
||||
Promise<void> {
|
||||
async insertRecordsFrom(
|
||||
table: string,
|
||||
source: string,
|
||||
conditions?: SQLiteDBRecordValues,
|
||||
fields: string = '*',
|
||||
): Promise<void> {
|
||||
const selectAndParams = this.whereClause(conditions);
|
||||
const select = selectAndParams[0] ? 'WHERE ' + selectAndParams[0] : '';
|
||||
const params = selectAndParams[1];
|
||||
const select = selectAndParams.sql ? 'WHERE ' + selectAndParams.sql : '';
|
||||
const params = selectAndParams.params;
|
||||
|
||||
await this.execute(`INSERT INTO ${table} SELECT ${fields} FROM ${source} ${select}`, params);
|
||||
}
|
||||
|
@ -913,7 +970,7 @@ export class SQLiteDB {
|
|||
async updateRecords(table: string, data: SQLiteDBRecordValues, conditions?: SQLiteDBRecordValues): Promise<number> {
|
||||
const whereAndParams = this.whereClause(conditions);
|
||||
|
||||
return this.updateRecordsWhere(table, data, whereAndParams[0], whereAndParams[1]);
|
||||
return this.updateRecordsWhere(table, data, whereAndParams.sql, whereAndParams.params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -925,8 +982,12 @@ export class SQLiteDB {
|
|||
* @param whereParams Params for the where clause.
|
||||
* @return Promise resolved with the number of affected rows.
|
||||
*/
|
||||
async updateRecordsWhere(table: string, data: SQLiteDBRecordValues, where?: string, whereParams?: SQLiteDBRecordValue[]):
|
||||
Promise<number> {
|
||||
async updateRecordsWhere(
|
||||
table: string,
|
||||
data: SQLiteDBRecordValues,
|
||||
where?: string,
|
||||
whereParams?: SQLiteDBRecordValue[],
|
||||
): Promise<number> {
|
||||
this.formatDataToInsert(data);
|
||||
if (!data || !Object.keys(data).length) {
|
||||
// No fields to update, consider it's done.
|
||||
|
|
|
@ -26,20 +26,21 @@ import { Component, Input, OnChanges, OnDestroy, ElementRef, SimpleChange } from
|
|||
styleUrls: ['icon.scss'],
|
||||
})
|
||||
export class CoreIconComponent implements OnChanges, OnDestroy {
|
||||
|
||||
// Common params.
|
||||
@Input() name: string;
|
||||
@Input('color') color?: string;
|
||||
@Input('slash') slash?: boolean; // Display a red slash over the icon.
|
||||
@Input() color?: string;
|
||||
@Input() slash?: boolean; // Display a red slash over the icon.
|
||||
|
||||
// Ionicons params.
|
||||
@Input('isActive') isActive?: boolean;
|
||||
@Input('md') md?: string;
|
||||
@Input('ios') ios?: string;
|
||||
@Input() isActive?: boolean;
|
||||
@Input() md?: string;
|
||||
@Input() ios?: string;
|
||||
|
||||
// FontAwesome params.
|
||||
@Input('fixed-width') fixedWidth: string;
|
||||
@Input('fixed-width') fixedWidth: boolean;
|
||||
|
||||
@Input('label') ariaLabel?: string;
|
||||
@Input() label?: string;
|
||||
@Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false.
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
@ -66,18 +67,15 @@ export class CoreIconComponent implements OnChanges, OnDestroy {
|
|||
if (this.name.startsWith('fa-')) {
|
||||
this.newElement.classList.add('fa');
|
||||
this.newElement.classList.add(this.name);
|
||||
if (this.isTrueProperty(this.fixedWidth)) {
|
||||
if (this.fixedWidth) {
|
||||
this.newElement.classList.add('fa-fw');
|
||||
}
|
||||
if (this.color) {
|
||||
this.newElement.classList.add('fa-' + this.color);
|
||||
}
|
||||
}
|
||||
|
||||
!this.ariaLabel && this.newElement.setAttribute('aria-hidden', 'true');
|
||||
!this.ariaLabel && this.newElement.setAttribute('role', 'presentation');
|
||||
this.ariaLabel && this.newElement.setAttribute('aria-label', this.ariaLabel);
|
||||
this.ariaLabel && this.newElement.setAttribute('title', this.ariaLabel);
|
||||
!this.label && this.newElement.setAttribute('aria-hidden', 'true');
|
||||
!this.label && this.newElement.setAttribute('role', 'presentation');
|
||||
this.label && this.newElement.setAttribute('aria-label', this.label);
|
||||
this.label && this.newElement.setAttribute('title', this.label);
|
||||
|
||||
const attrs = this.element.attributes;
|
||||
for (let i = attrs.length - 1; i >= 0; i--) {
|
||||
|
@ -91,8 +89,7 @@ export class CoreIconComponent implements OnChanges, OnDestroy {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
} else if (attrs[i].name != 'name') {
|
||||
this.newElement.setAttribute(attrs[i].name, attrs[i].value);
|
||||
}
|
||||
}
|
||||
|
@ -108,22 +105,6 @@ export class CoreIconComponent implements OnChanges, OnDestroy {
|
|||
oldElement.parentElement.replaceChild(this.newElement, oldElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the value is true or on.
|
||||
*
|
||||
* @param val value to be checked.
|
||||
* @return If has a value equivalent to true.
|
||||
*/
|
||||
isTrueProperty(val: any): boolean {
|
||||
if (typeof val === 'string') {
|
||||
val = val.toLowerCase().trim();
|
||||
|
||||
return (val === 'true' || val === 'on' || val === '');
|
||||
}
|
||||
|
||||
return !!val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
|
@ -132,4 +113,5 @@ export class CoreIconComponent implements OnChanges, OnDestroy {
|
|||
this.newElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import { File } from '@ionic-native/file/ngx';
|
|||
import { FileOpener } from '@ionic-native/file-opener/ngx';
|
||||
import { FileTransfer } from '@ionic-native/file-transfer/ngx';
|
||||
import { Geolocation } from '@ionic-native/geolocation/ngx';
|
||||
import { Globalization } from '@ionic-native/globalization/ngx';
|
||||
import { HTTP } from '@ionic-native/http/ngx';
|
||||
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
|
||||
import { WebView } from '@ionic-native/ionic-webview/ngx';
|
||||
|
@ -59,7 +58,6 @@ import { Zip } from '@ionic-native/zip/ngx';
|
|||
FileOpener,
|
||||
FileTransfer,
|
||||
Geolocation,
|
||||
Globalization,
|
||||
HTTP,
|
||||
InAppBrowser,
|
||||
Keyboard,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"yourenteredsite": "Connect to your site"
|
||||
}
|
|
@ -16,17 +16,18 @@ import { NgModule } from '@angular/core';
|
|||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreLoginRoutingModule } from './login-routing.module';
|
||||
import { CoreLoginInitPage } from './pages/init/init.page';
|
||||
import { CoreLoginSitePage } from './pages/site/site.page';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
CoreLoginRoutingModule,
|
||||
TranslateModule.forChild(),
|
||||
],
|
||||
declarations: [
|
||||
CoreLoginInitPage,
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<ion-content>
|
||||
Site page.
|
||||
{{ 'core.login.yourenteredsite' | translate }}
|
||||
</ion-content>
|
||||
|
|
|
@ -81,7 +81,7 @@ export class CoreInitDelegate {
|
|||
ordered.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
ordered = ordered.map((data: CoreInitHandler) => ({
|
||||
func: this.prepareProcess.bind(this, data),
|
||||
function: this.prepareProcess.bind(this, data),
|
||||
blocking: !!data.blocking,
|
||||
}));
|
||||
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import CoreConfigConstants from '@app/config.json';
|
||||
import { LangChangeEvent } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@services/app';
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { makeSingleton, Translate, Platform, Globalization } from '@singletons/core.singletons';
|
||||
import { makeSingleton, Translate, Platform } from '@singletons/core.singletons';
|
||||
|
||||
import * as moment from 'moment';
|
||||
|
||||
|
@ -39,22 +40,33 @@ export class CoreLangProvider {
|
|||
Translate.instance.setDefaultLang(this.fallbackLanguage);
|
||||
Translate.instance.use(this.defaultLanguage);
|
||||
|
||||
Platform.instance.ready().then(() => {
|
||||
if (CoreAppProvider.isAutomated()) {
|
||||
// Force current language to English when Behat is running.
|
||||
this.changeCurrentLanguage('en');
|
||||
this.initLanguage();
|
||||
|
||||
return;
|
||||
}
|
||||
Translate.instance.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||
document.documentElement.setAttribute('lang', event.lang);
|
||||
|
||||
this.getCurrentLanguage().then((language) => {
|
||||
this.changeCurrentLanguage(language);
|
||||
});
|
||||
let dir = Translate.instance.instant('core.thisdirection');
|
||||
dir = dir.indexOf('rtl') != -1 ? 'rtl' : 'ltr';
|
||||
document.documentElement.setAttribute('dir', dir);
|
||||
});
|
||||
}
|
||||
|
||||
Translate.instance.onLangChange.subscribe(() => {
|
||||
// @todo: Set platform lang and dir.
|
||||
});
|
||||
/**
|
||||
* Init language.
|
||||
*/
|
||||
protected async initLanguage(): Promise<void> {
|
||||
await Platform.instance.ready();
|
||||
|
||||
let language: string;
|
||||
|
||||
if (CoreAppProvider.isAutomated()) {
|
||||
// Force current language to English when Behat is running.
|
||||
language = 'en';
|
||||
} else {
|
||||
language = await this.getCurrentLanguage();
|
||||
}
|
||||
|
||||
return this.changeCurrentLanguage(language);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -221,43 +233,45 @@ export class CoreLangProvider {
|
|||
return this.currentLanguage;
|
||||
}
|
||||
|
||||
this.currentLanguage = await this.detectLanguage();
|
||||
|
||||
return this.currentLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current language from settings, or detect the browser one.
|
||||
*
|
||||
* @return Promise resolved with the selected language.
|
||||
*/
|
||||
protected async detectLanguage(): Promise<string> {
|
||||
// Get current language from config (user might have changed it).
|
||||
return CoreConfig.instance.get<string>('current_language').then((language) => language).catch(() => {
|
||||
// User hasn't defined a language. If default language is forced, use it.
|
||||
if (CoreConfigConstants.default_lang && CoreConfigConstants.forcedefaultlanguage) {
|
||||
return CoreConfigConstants.default_lang;
|
||||
try {
|
||||
return await CoreConfig.instance.get<string>('current_language');
|
||||
} catch (e) {
|
||||
// Try will return, ignore errors here to avoid nesting.
|
||||
}
|
||||
|
||||
// User hasn't defined a language. If default language is forced, use it.
|
||||
if (CoreConfigConstants.default_lang && CoreConfigConstants.forcedefaultlanguage) {
|
||||
return CoreConfigConstants.default_lang;
|
||||
}
|
||||
|
||||
// No forced language, try to get current language from browser.
|
||||
let preferredLanguage = navigator.language.toLowerCase();
|
||||
if (preferredLanguage.indexOf('-') > -1) {
|
||||
// Language code defined by locale has a dash, like en-US or es-ES. Check if it's supported.
|
||||
if (CoreConfigConstants.languages && typeof CoreConfigConstants.languages[preferredLanguage] == 'undefined') {
|
||||
// Code is NOT supported. Fallback to language without dash. E.g. 'en-US' would fallback to 'en'.
|
||||
preferredLanguage = preferredLanguage.substr(0, preferredLanguage.indexOf('-'));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// No forced language, try to get current language from cordova globalization.
|
||||
return Globalization.instance.getPreferredLanguage().then((result) => {
|
||||
let language = result.value.toLowerCase();
|
||||
if (language.indexOf('-') > -1) {
|
||||
// Language code defined by locale has a dash, like en-US or es-ES. Check if it's supported.
|
||||
if (CoreConfigConstants.languages && typeof CoreConfigConstants.languages[language] == 'undefined') {
|
||||
// Code is NOT supported. Fallback to language without dash. E.g. 'en-US' would fallback to 'en'.
|
||||
language = language.substr(0, language.indexOf('-'));
|
||||
}
|
||||
}
|
||||
if (typeof CoreConfigConstants.languages[preferredLanguage] == 'undefined') {
|
||||
// Language not supported, use default language.
|
||||
return this.defaultLanguage;
|
||||
}
|
||||
|
||||
if (typeof CoreConfigConstants.languages[language] == 'undefined') {
|
||||
// Language not supported, use default language.
|
||||
return this.defaultLanguage;
|
||||
}
|
||||
|
||||
return language;
|
||||
}).catch(() =>
|
||||
// Error getting locale. Use default language.
|
||||
this.defaultLanguage);
|
||||
} catch (err) {
|
||||
// Error getting locale. Use default language.
|
||||
return Promise.resolve(this.defaultLanguage);
|
||||
}
|
||||
}).then((language) => {
|
||||
this.currentLanguage = language; // Save it for later.
|
||||
|
||||
return language;
|
||||
});
|
||||
return preferredLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,7 +30,6 @@ import { File as FileService } from '@ionic-native/file/ngx';
|
|||
import { FileOpener as FileOpenerService } from '@ionic-native/file-opener/ngx';
|
||||
import { FileTransfer as FileTransferService } from '@ionic-native/file-transfer/ngx';
|
||||
import { Geolocation as GeolocationService } from '@ionic-native/geolocation/ngx';
|
||||
import { Globalization as GlobalizationService } from '@ionic-native/globalization/ngx';
|
||||
import { HTTP } from '@ionic-native/http/ngx';
|
||||
import { InAppBrowser as InAppBrowserService } from '@ionic-native/in-app-browser/ngx';
|
||||
import { WebView as WebViewService } from '@ionic-native/ionic-webview/ngx';
|
||||
|
@ -78,7 +77,6 @@ export class File extends makeSingleton(FileService) {}
|
|||
export class FileOpener extends makeSingleton(FileOpenerService) {}
|
||||
export class FileTransfer extends makeSingleton(FileTransferService) {}
|
||||
export class Geolocation extends makeSingleton(GeolocationService) {}
|
||||
export class Globalization extends makeSingleton(GlobalizationService) {}
|
||||
export class InAppBrowser extends makeSingleton(InAppBrowserService) {}
|
||||
export class Keyboard extends makeSingleton(KeyboardService) {}
|
||||
export class LocalNotifications extends makeSingleton(LocalNotificationsService) {}
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
{
|
||||
"AD": "Andorra",
|
||||
"AE": "United Arab Emirates",
|
||||
"AF": "Afghanistan",
|
||||
"AG": "Antigua and Barbuda",
|
||||
"AI": "Anguilla",
|
||||
"AL": "Albania",
|
||||
"AM": "Armenia",
|
||||
"AO": "Angola",
|
||||
"AQ": "Antarctica",
|
||||
"AR": "Argentina",
|
||||
"AS": "American Samoa",
|
||||
"AT": "Austria",
|
||||
"AU": "Australia",
|
||||
"AW": "Aruba",
|
||||
"AX": "Åland Islands",
|
||||
"AZ": "Azerbaijan",
|
||||
"BA": "Bosnia and Herzegovina",
|
||||
"BB": "Barbados",
|
||||
"BD": "Bangladesh",
|
||||
"BE": "Belgium",
|
||||
"BF": "Burkina Faso",
|
||||
"BG": "Bulgaria",
|
||||
"BH": "Bahrain",
|
||||
"BI": "Burundi",
|
||||
"BJ": "Benin",
|
||||
"BL": "Saint Barthélemy",
|
||||
"BM": "Bermuda",
|
||||
"BN": "Brunei Darussalam",
|
||||
"BO": "Bolivia (Plurinational State of)",
|
||||
"BQ": "Bonaire, Sint Eustatius and Saba",
|
||||
"BR": "Brazil",
|
||||
"BS": "Bahamas",
|
||||
"BT": "Bhutan",
|
||||
"BV": "Bouvet Island",
|
||||
"BW": "Botswana",
|
||||
"BY": "Belarus",
|
||||
"BZ": "Belize",
|
||||
"CA": "Canada",
|
||||
"CC": "Cocos (Keeling) Islands",
|
||||
"CD": "Congo (the Democratic Republic of the)",
|
||||
"CF": "Central African Republic",
|
||||
"CG": "Congo",
|
||||
"CH": "Switzerland",
|
||||
"CI": "Côte d'Ivoire",
|
||||
"CK": "Cook Islands",
|
||||
"CL": "Chile",
|
||||
"CM": "Cameroon",
|
||||
"CN": "China",
|
||||
"CO": "Colombia",
|
||||
"CR": "Costa Rica",
|
||||
"CU": "Cuba",
|
||||
"CV": "Cabo Verde",
|
||||
"CW": "Curaçao",
|
||||
"CX": "Christmas Island",
|
||||
"CY": "Cyprus",
|
||||
"CZ": "Czechia",
|
||||
"DE": "Germany",
|
||||
"DJ": "Djibouti",
|
||||
"DK": "Denmark",
|
||||
"DM": "Dominica",
|
||||
"DO": "Dominican Republic",
|
||||
"DZ": "Algeria",
|
||||
"EC": "Ecuador",
|
||||
"EE": "Estonia",
|
||||
"EG": "Egypt",
|
||||
"EH": "Western Sahara",
|
||||
"ER": "Eritrea",
|
||||
"ES": "Spain",
|
||||
"ET": "Ethiopia",
|
||||
"FI": "Finland",
|
||||
"FJ": "Fiji",
|
||||
"FK": "Falkland Islands (Malvinas)",
|
||||
"FM": "Micronesia (Federated States of)",
|
||||
"FO": "Faroe Islands",
|
||||
"FR": "France",
|
||||
"GA": "Gabon",
|
||||
"GB": "United Kingdom",
|
||||
"GD": "Grenada",
|
||||
"GE": "Georgia",
|
||||
"GF": "French Guiana",
|
||||
"GG": "Guernsey",
|
||||
"GH": "Ghana",
|
||||
"GI": "Gibraltar",
|
||||
"GL": "Greenland",
|
||||
"GM": "Gambia",
|
||||
"GN": "Guinea",
|
||||
"GP": "Guadeloupe",
|
||||
"GQ": "Equatorial Guinea",
|
||||
"GR": "Greece",
|
||||
"GS": "South Georgia and the South Sandwich Islands",
|
||||
"GT": "Guatemala",
|
||||
"GU": "Guam",
|
||||
"GW": "Guinea-Bissau",
|
||||
"GY": "Guyana",
|
||||
"HK": "Hong Kong",
|
||||
"HM": "Heard Island and McDonald Islands",
|
||||
"HN": "Honduras",
|
||||
"HR": "Croatia",
|
||||
"HT": "Haiti",
|
||||
"HU": "Hungary",
|
||||
"ID": "Indonesia",
|
||||
"IE": "Ireland",
|
||||
"IL": "Israel",
|
||||
"IM": "Isle of Man",
|
||||
"IN": "India",
|
||||
"IO": "British Indian Ocean Territory",
|
||||
"IQ": "Iraq",
|
||||
"IR": "Iran (Islamic Republic of)",
|
||||
"IS": "Iceland",
|
||||
"IT": "Italy",
|
||||
"JE": "Jersey",
|
||||
"JM": "Jamaica",
|
||||
"JO": "Jordan",
|
||||
"JP": "Japan",
|
||||
"KE": "Kenya",
|
||||
"KG": "Kyrgyzstan",
|
||||
"KH": "Cambodia",
|
||||
"KI": "Kiribati",
|
||||
"KM": "Comoros",
|
||||
"KN": "Saint Kitts and Nevis",
|
||||
"KP": "Korea (the Democratic People's Republic of)",
|
||||
"KR": "Korea (the Republic of)",
|
||||
"KW": "Kuwait",
|
||||
"KY": "Cayman Islands",
|
||||
"KZ": "Kazakhstan",
|
||||
"LA": "Lao People's Democratic Republic",
|
||||
"LB": "Lebanon",
|
||||
"LC": "Saint Lucia",
|
||||
"LI": "Liechtenstein",
|
||||
"LK": "Sri Lanka",
|
||||
"LR": "Liberia",
|
||||
"LS": "Lesotho",
|
||||
"LT": "Lithuania",
|
||||
"LU": "Luxembourg",
|
||||
"LV": "Latvia",
|
||||
"LY": "Libya",
|
||||
"MA": "Morocco",
|
||||
"MC": "Monaco",
|
||||
"MD": "Moldova (the Republic of)",
|
||||
"ME": "Montenegro",
|
||||
"MF": "Saint Martin (French part)",
|
||||
"MG": "Madagascar",
|
||||
"MH": "Marshall Islands",
|
||||
"MK": "North Macedonia",
|
||||
"ML": "Mali",
|
||||
"MM": "Myanmar",
|
||||
"MN": "Mongolia",
|
||||
"MO": "Macao",
|
||||
"MP": "Northern Mariana Islands",
|
||||
"MQ": "Martinique",
|
||||
"MR": "Mauritania",
|
||||
"MS": "Montserrat",
|
||||
"MT": "Malta",
|
||||
"MU": "Mauritius",
|
||||
"MV": "Maldives",
|
||||
"MW": "Malawi",
|
||||
"MX": "Mexico",
|
||||
"MY": "Malaysia",
|
||||
"MZ": "Mozambique",
|
||||
"NA": "Namibia",
|
||||
"NC": "New Caledonia",
|
||||
"NE": "Niger",
|
||||
"NF": "Norfolk Island",
|
||||
"NG": "Nigeria",
|
||||
"NI": "Nicaragua",
|
||||
"NL": "Netherlands",
|
||||
"NO": "Norway",
|
||||
"NP": "Nepal",
|
||||
"NR": "Nauru",
|
||||
"NU": "Niue",
|
||||
"NZ": "New Zealand",
|
||||
"OM": "Oman",
|
||||
"PA": "Panama",
|
||||
"PE": "Peru",
|
||||
"PF": "French Polynesia",
|
||||
"PG": "Papua New Guinea",
|
||||
"PH": "Philippines",
|
||||
"PK": "Pakistan",
|
||||
"PL": "Poland",
|
||||
"PM": "Saint Pierre and Miquelon",
|
||||
"PN": "Pitcairn",
|
||||
"PR": "Puerto Rico",
|
||||
"PS": "Palestine, State of",
|
||||
"PT": "Portugal",
|
||||
"PW": "Palau",
|
||||
"PY": "Paraguay",
|
||||
"QA": "Qatar",
|
||||
"RE": "Réunion",
|
||||
"RO": "Romania",
|
||||
"RS": "Serbia",
|
||||
"RU": "Russian Federation",
|
||||
"RW": "Rwanda",
|
||||
"SA": "Saudi Arabia",
|
||||
"SB": "Solomon Islands",
|
||||
"SC": "Seychelles",
|
||||
"SD": "Sudan",
|
||||
"SE": "Sweden",
|
||||
"SG": "Singapore",
|
||||
"SH": "Saint Helena, Ascension and Tristan da Cunha",
|
||||
"SI": "Slovenia",
|
||||
"SJ": "Svalbard and Jan Mayen",
|
||||
"SK": "Slovakia",
|
||||
"SL": "Sierra Leone",
|
||||
"SM": "San Marino",
|
||||
"SN": "Senegal",
|
||||
"SO": "Somalia",
|
||||
"SR": "Suriname",
|
||||
"SS": "South Sudan",
|
||||
"ST": "Sao Tome and Principe",
|
||||
"SV": "El Salvador",
|
||||
"SX": "Sint Maarten (Dutch part)",
|
||||
"SY": "Syrian Arab Republic",
|
||||
"SZ": "Eswatini",
|
||||
"TC": "Turks and Caicos Islands",
|
||||
"TD": "Chad",
|
||||
"TF": "French Southern Territories",
|
||||
"TG": "Togo",
|
||||
"TH": "Thailand",
|
||||
"TJ": "Tajikistan",
|
||||
"TK": "Tokelau",
|
||||
"TL": "Timor-Leste",
|
||||
"TM": "Turkmenistan",
|
||||
"TN": "Tunisia",
|
||||
"TO": "Tonga",
|
||||
"TR": "Turkey",
|
||||
"TT": "Trinidad and Tobago",
|
||||
"TV": "Tuvalu",
|
||||
"TW": "Taiwan",
|
||||
"TZ": "Tanzania, the United Republic of",
|
||||
"UA": "Ukraine",
|
||||
"UG": "Uganda",
|
||||
"UM": "United States Minor Outlying Islands",
|
||||
"US": "United States",
|
||||
"UY": "Uruguay",
|
||||
"UZ": "Uzbekistan",
|
||||
"VA": "Holy See",
|
||||
"VC": "Saint Vincent and the Grenadines",
|
||||
"VE": "Venezuela (Bolivarian Republic of)",
|
||||
"VG": "Virgin Islands (British)",
|
||||
"VI": "Virgin Islands (U.S.)",
|
||||
"VN": "Viet Nam",
|
||||
"VU": "Vanuatu",
|
||||
"WF": "Wallis and Futuna",
|
||||
"WS": "Samoa",
|
||||
"YE": "Yemen",
|
||||
"YT": "Mayotte",
|
||||
"ZA": "South Africa",
|
||||
"ZM": "Zambia",
|
||||
"ZW": "Zimbabwe"
|
||||
}
|
|
@ -0,0 +1,304 @@
|
|||
{
|
||||
"assets.countries.AD": "Andorra",
|
||||
"assets.countries.AE": "United Arab Emirates",
|
||||
"assets.countries.AF": "Afghanistan",
|
||||
"assets.countries.AG": "Antigua and Barbuda",
|
||||
"assets.countries.AI": "Anguilla",
|
||||
"assets.countries.AL": "Albania",
|
||||
"assets.countries.AM": "Armenia",
|
||||
"assets.countries.AO": "Angola",
|
||||
"assets.countries.AQ": "Antarctica",
|
||||
"assets.countries.AR": "Argentina",
|
||||
"assets.countries.AS": "American Samoa",
|
||||
"assets.countries.AT": "Austria",
|
||||
"assets.countries.AU": "Australia",
|
||||
"assets.countries.AW": "Aruba",
|
||||
"assets.countries.AX": "Åland Islands",
|
||||
"assets.countries.AZ": "Azerbaijan",
|
||||
"assets.countries.BA": "Bosnia and Herzegovina",
|
||||
"assets.countries.BB": "Barbados",
|
||||
"assets.countries.BD": "Bangladesh",
|
||||
"assets.countries.BE": "Belgium",
|
||||
"assets.countries.BF": "Burkina Faso",
|
||||
"assets.countries.BG": "Bulgaria",
|
||||
"assets.countries.BH": "Bahrain",
|
||||
"assets.countries.BI": "Burundi",
|
||||
"assets.countries.BJ": "Benin",
|
||||
"assets.countries.BL": "Saint Barthélemy",
|
||||
"assets.countries.BM": "Bermuda",
|
||||
"assets.countries.BN": "Brunei Darussalam",
|
||||
"assets.countries.BO": "Bolivia (Plurinational State of)",
|
||||
"assets.countries.BQ": "Bonaire, Sint Eustatius and Saba",
|
||||
"assets.countries.BR": "Brazil",
|
||||
"assets.countries.BS": "Bahamas",
|
||||
"assets.countries.BT": "Bhutan",
|
||||
"assets.countries.BV": "Bouvet Island",
|
||||
"assets.countries.BW": "Botswana",
|
||||
"assets.countries.BY": "Belarus",
|
||||
"assets.countries.BZ": "Belize",
|
||||
"assets.countries.CA": "Canada",
|
||||
"assets.countries.CC": "Cocos (Keeling) Islands",
|
||||
"assets.countries.CD": "Congo (the Democratic Republic of the)",
|
||||
"assets.countries.CF": "Central African Republic",
|
||||
"assets.countries.CG": "Congo",
|
||||
"assets.countries.CH": "Switzerland",
|
||||
"assets.countries.CI": "Côte d'Ivoire",
|
||||
"assets.countries.CK": "Cook Islands",
|
||||
"assets.countries.CL": "Chile",
|
||||
"assets.countries.CM": "Cameroon",
|
||||
"assets.countries.CN": "China",
|
||||
"assets.countries.CO": "Colombia",
|
||||
"assets.countries.CR": "Costa Rica",
|
||||
"assets.countries.CU": "Cuba",
|
||||
"assets.countries.CV": "Cabo Verde",
|
||||
"assets.countries.CW": "Curaçao",
|
||||
"assets.countries.CX": "Christmas Island",
|
||||
"assets.countries.CY": "Cyprus",
|
||||
"assets.countries.CZ": "Czechia",
|
||||
"assets.countries.DE": "Germany",
|
||||
"assets.countries.DJ": "Djibouti",
|
||||
"assets.countries.DK": "Denmark",
|
||||
"assets.countries.DM": "Dominica",
|
||||
"assets.countries.DO": "Dominican Republic",
|
||||
"assets.countries.DZ": "Algeria",
|
||||
"assets.countries.EC": "Ecuador",
|
||||
"assets.countries.EE": "Estonia",
|
||||
"assets.countries.EG": "Egypt",
|
||||
"assets.countries.EH": "Western Sahara",
|
||||
"assets.countries.ER": "Eritrea",
|
||||
"assets.countries.ES": "Spain",
|
||||
"assets.countries.ET": "Ethiopia",
|
||||
"assets.countries.FI": "Finland",
|
||||
"assets.countries.FJ": "Fiji",
|
||||
"assets.countries.FK": "Falkland Islands (Malvinas)",
|
||||
"assets.countries.FM": "Micronesia (Federated States of)",
|
||||
"assets.countries.FO": "Faroe Islands",
|
||||
"assets.countries.FR": "France",
|
||||
"assets.countries.GA": "Gabon",
|
||||
"assets.countries.GB": "United Kingdom",
|
||||
"assets.countries.GD": "Grenada",
|
||||
"assets.countries.GE": "Georgia",
|
||||
"assets.countries.GF": "French Guiana",
|
||||
"assets.countries.GG": "Guernsey",
|
||||
"assets.countries.GH": "Ghana",
|
||||
"assets.countries.GI": "Gibraltar",
|
||||
"assets.countries.GL": "Greenland",
|
||||
"assets.countries.GM": "Gambia",
|
||||
"assets.countries.GN": "Guinea",
|
||||
"assets.countries.GP": "Guadeloupe",
|
||||
"assets.countries.GQ": "Equatorial Guinea",
|
||||
"assets.countries.GR": "Greece",
|
||||
"assets.countries.GS": "South Georgia and the South Sandwich Islands",
|
||||
"assets.countries.GT": "Guatemala",
|
||||
"assets.countries.GU": "Guam",
|
||||
"assets.countries.GW": "Guinea-Bissau",
|
||||
"assets.countries.GY": "Guyana",
|
||||
"assets.countries.HK": "Hong Kong",
|
||||
"assets.countries.HM": "Heard Island and McDonald Islands",
|
||||
"assets.countries.HN": "Honduras",
|
||||
"assets.countries.HR": "Croatia",
|
||||
"assets.countries.HT": "Haiti",
|
||||
"assets.countries.HU": "Hungary",
|
||||
"assets.countries.ID": "Indonesia",
|
||||
"assets.countries.IE": "Ireland",
|
||||
"assets.countries.IL": "Israel",
|
||||
"assets.countries.IM": "Isle of Man",
|
||||
"assets.countries.IN": "India",
|
||||
"assets.countries.IO": "British Indian Ocean Territory",
|
||||
"assets.countries.IQ": "Iraq",
|
||||
"assets.countries.IR": "Iran (Islamic Republic of)",
|
||||
"assets.countries.IS": "Iceland",
|
||||
"assets.countries.IT": "Italy",
|
||||
"assets.countries.JE": "Jersey",
|
||||
"assets.countries.JM": "Jamaica",
|
||||
"assets.countries.JO": "Jordan",
|
||||
"assets.countries.JP": "Japan",
|
||||
"assets.countries.KE": "Kenya",
|
||||
"assets.countries.KG": "Kyrgyzstan",
|
||||
"assets.countries.KH": "Cambodia",
|
||||
"assets.countries.KI": "Kiribati",
|
||||
"assets.countries.KM": "Comoros",
|
||||
"assets.countries.KN": "Saint Kitts and Nevis",
|
||||
"assets.countries.KP": "Korea (the Democratic People's Republic of)",
|
||||
"assets.countries.KR": "Korea (the Republic of)",
|
||||
"assets.countries.KW": "Kuwait",
|
||||
"assets.countries.KY": "Cayman Islands",
|
||||
"assets.countries.KZ": "Kazakhstan",
|
||||
"assets.countries.LA": "Lao People's Democratic Republic",
|
||||
"assets.countries.LB": "Lebanon",
|
||||
"assets.countries.LC": "Saint Lucia",
|
||||
"assets.countries.LI": "Liechtenstein",
|
||||
"assets.countries.LK": "Sri Lanka",
|
||||
"assets.countries.LR": "Liberia",
|
||||
"assets.countries.LS": "Lesotho",
|
||||
"assets.countries.LT": "Lithuania",
|
||||
"assets.countries.LU": "Luxembourg",
|
||||
"assets.countries.LV": "Latvia",
|
||||
"assets.countries.LY": "Libya",
|
||||
"assets.countries.MA": "Morocco",
|
||||
"assets.countries.MC": "Monaco",
|
||||
"assets.countries.MD": "Moldova (the Republic of)",
|
||||
"assets.countries.ME": "Montenegro",
|
||||
"assets.countries.MF": "Saint Martin (French part)",
|
||||
"assets.countries.MG": "Madagascar",
|
||||
"assets.countries.MH": "Marshall Islands",
|
||||
"assets.countries.MK": "North Macedonia",
|
||||
"assets.countries.ML": "Mali",
|
||||
"assets.countries.MM": "Myanmar",
|
||||
"assets.countries.MN": "Mongolia",
|
||||
"assets.countries.MO": "Macao",
|
||||
"assets.countries.MP": "Northern Mariana Islands",
|
||||
"assets.countries.MQ": "Martinique",
|
||||
"assets.countries.MR": "Mauritania",
|
||||
"assets.countries.MS": "Montserrat",
|
||||
"assets.countries.MT": "Malta",
|
||||
"assets.countries.MU": "Mauritius",
|
||||
"assets.countries.MV": "Maldives",
|
||||
"assets.countries.MW": "Malawi",
|
||||
"assets.countries.MX": "Mexico",
|
||||
"assets.countries.MY": "Malaysia",
|
||||
"assets.countries.MZ": "Mozambique",
|
||||
"assets.countries.NA": "Namibia",
|
||||
"assets.countries.NC": "New Caledonia",
|
||||
"assets.countries.NE": "Niger",
|
||||
"assets.countries.NF": "Norfolk Island",
|
||||
"assets.countries.NG": "Nigeria",
|
||||
"assets.countries.NI": "Nicaragua",
|
||||
"assets.countries.NL": "Netherlands",
|
||||
"assets.countries.NO": "Norway",
|
||||
"assets.countries.NP": "Nepal",
|
||||
"assets.countries.NR": "Nauru",
|
||||
"assets.countries.NU": "Niue",
|
||||
"assets.countries.NZ": "New Zealand",
|
||||
"assets.countries.OM": "Oman",
|
||||
"assets.countries.PA": "Panama",
|
||||
"assets.countries.PE": "Peru",
|
||||
"assets.countries.PF": "French Polynesia",
|
||||
"assets.countries.PG": "Papua New Guinea",
|
||||
"assets.countries.PH": "Philippines",
|
||||
"assets.countries.PK": "Pakistan",
|
||||
"assets.countries.PL": "Poland",
|
||||
"assets.countries.PM": "Saint Pierre and Miquelon",
|
||||
"assets.countries.PN": "Pitcairn",
|
||||
"assets.countries.PR": "Puerto Rico",
|
||||
"assets.countries.PS": "Palestine, State of",
|
||||
"assets.countries.PT": "Portugal",
|
||||
"assets.countries.PW": "Palau",
|
||||
"assets.countries.PY": "Paraguay",
|
||||
"assets.countries.QA": "Qatar",
|
||||
"assets.countries.RE": "Réunion",
|
||||
"assets.countries.RO": "Romania",
|
||||
"assets.countries.RS": "Serbia",
|
||||
"assets.countries.RU": "Russian Federation",
|
||||
"assets.countries.RW": "Rwanda",
|
||||
"assets.countries.SA": "Saudi Arabia",
|
||||
"assets.countries.SB": "Solomon Islands",
|
||||
"assets.countries.SC": "Seychelles",
|
||||
"assets.countries.SD": "Sudan",
|
||||
"assets.countries.SE": "Sweden",
|
||||
"assets.countries.SG": "Singapore",
|
||||
"assets.countries.SH": "Saint Helena, Ascension and Tristan da Cunha",
|
||||
"assets.countries.SI": "Slovenia",
|
||||
"assets.countries.SJ": "Svalbard and Jan Mayen",
|
||||
"assets.countries.SK": "Slovakia",
|
||||
"assets.countries.SL": "Sierra Leone",
|
||||
"assets.countries.SM": "San Marino",
|
||||
"assets.countries.SN": "Senegal",
|
||||
"assets.countries.SO": "Somalia",
|
||||
"assets.countries.SR": "Suriname",
|
||||
"assets.countries.SS": "South Sudan",
|
||||
"assets.countries.ST": "Sao Tome and Principe",
|
||||
"assets.countries.SV": "El Salvador",
|
||||
"assets.countries.SX": "Sint Maarten (Dutch part)",
|
||||
"assets.countries.SY": "Syrian Arab Republic",
|
||||
"assets.countries.SZ": "Eswatini",
|
||||
"assets.countries.TC": "Turks and Caicos Islands",
|
||||
"assets.countries.TD": "Chad",
|
||||
"assets.countries.TF": "French Southern Territories",
|
||||
"assets.countries.TG": "Togo",
|
||||
"assets.countries.TH": "Thailand",
|
||||
"assets.countries.TJ": "Tajikistan",
|
||||
"assets.countries.TK": "Tokelau",
|
||||
"assets.countries.TL": "Timor-Leste",
|
||||
"assets.countries.TM": "Turkmenistan",
|
||||
"assets.countries.TN": "Tunisia",
|
||||
"assets.countries.TO": "Tonga",
|
||||
"assets.countries.TR": "Turkey",
|
||||
"assets.countries.TT": "Trinidad and Tobago",
|
||||
"assets.countries.TV": "Tuvalu",
|
||||
"assets.countries.TW": "Taiwan",
|
||||
"assets.countries.TZ": "Tanzania, the United Republic of",
|
||||
"assets.countries.UA": "Ukraine",
|
||||
"assets.countries.UG": "Uganda",
|
||||
"assets.countries.UM": "United States Minor Outlying Islands",
|
||||
"assets.countries.US": "United States",
|
||||
"assets.countries.UY": "Uruguay",
|
||||
"assets.countries.UZ": "Uzbekistan",
|
||||
"assets.countries.VA": "Holy See",
|
||||
"assets.countries.VC": "Saint Vincent and the Grenadines",
|
||||
"assets.countries.VE": "Venezuela (Bolivarian Republic of)",
|
||||
"assets.countries.VG": "Virgin Islands (British)",
|
||||
"assets.countries.VI": "Virgin Islands (U.S.)",
|
||||
"assets.countries.VN": "Viet Nam",
|
||||
"assets.countries.VU": "Vanuatu",
|
||||
"assets.countries.WF": "Wallis and Futuna",
|
||||
"assets.countries.WS": "Samoa",
|
||||
"assets.countries.YE": "Yemen",
|
||||
"assets.countries.YT": "Mayotte",
|
||||
"assets.countries.ZA": "South Africa",
|
||||
"assets.countries.ZM": "Zambia",
|
||||
"assets.countries.ZW": "Zimbabwe",
|
||||
"assets.mimetypes.application/epub_zip": "EPUB ebook",
|
||||
"assets.mimetypes.application/msword": "Word document",
|
||||
"assets.mimetypes.application/pdf": "PDF document",
|
||||
"assets.mimetypes.application/vnd.moodle.backup": "Moodle backup",
|
||||
"assets.mimetypes.application/vnd.ms-excel": "Excel spreadsheet",
|
||||
"assets.mimetypes.application/vnd.ms-excel.sheet.macroEnabled.12": "Excel 2007 macro-enabled workbook",
|
||||
"assets.mimetypes.application/vnd.ms-powerpoint": "Powerpoint presentation",
|
||||
"assets.mimetypes.application/vnd.oasis.opendocument.spreadsheet": "OpenDocument Spreadsheet",
|
||||
"assets.mimetypes.application/vnd.oasis.opendocument.spreadsheet-template": "OpenDocument Spreadsheet template",
|
||||
"assets.mimetypes.application/vnd.oasis.opendocument.text": "OpenDocument Text document",
|
||||
"assets.mimetypes.application/vnd.oasis.opendocument.text-template": "OpenDocument Text template",
|
||||
"assets.mimetypes.application/vnd.oasis.opendocument.text-web": "OpenDocument Web page template",
|
||||
"assets.mimetypes.application/vnd.openxmlformats-officedocument.presentationml.presentation": "Powerpoint 2007 presentation",
|
||||
"assets.mimetypes.application/vnd.openxmlformats-officedocument.presentationml.slideshow": "Powerpoint 2007 slideshow",
|
||||
"assets.mimetypes.application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Excel 2007 spreadsheet",
|
||||
"assets.mimetypes.application/vnd.openxmlformats-officedocument.spreadsheetml.template": "Excel 2007 template",
|
||||
"assets.mimetypes.application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Word 2007 document",
|
||||
"assets.mimetypes.application/x-iwork-keynote-sffkey": "iWork Keynote presentation",
|
||||
"assets.mimetypes.application/x-iwork-numbers-sffnumbers": "iWork Numbers spreadsheet",
|
||||
"assets.mimetypes.application/x-iwork-pages-sffpages": "iWork Pages document",
|
||||
"assets.mimetypes.application/x-javascript": "JavaScript source",
|
||||
"assets.mimetypes.application/x-mspublisher": "Publisher document",
|
||||
"assets.mimetypes.application/x-shockwave-flash": "Flash animation",
|
||||
"assets.mimetypes.application/xhtml_xml": "XHTML document",
|
||||
"assets.mimetypes.archive": "Archive ({{$a.EXT}})",
|
||||
"assets.mimetypes.audio": "Audio file ({{$a.EXT}})",
|
||||
"assets.mimetypes.default": "{{$a.mimetype}}",
|
||||
"assets.mimetypes.document/unknown": "File",
|
||||
"assets.mimetypes.group:archive": "Archive files",
|
||||
"assets.mimetypes.group:audio": "Audio files",
|
||||
"assets.mimetypes.group:document": "Document files",
|
||||
"assets.mimetypes.group:html_audio": "Audio files natively supported by browsers",
|
||||
"assets.mimetypes.group:html_track": "HTML track files",
|
||||
"assets.mimetypes.group:html_video": "Video files natively supported by browsers",
|
||||
"assets.mimetypes.group:image": "Image files",
|
||||
"assets.mimetypes.group:presentation": "Presentation files",
|
||||
"assets.mimetypes.group:sourcecode": "Source code",
|
||||
"assets.mimetypes.group:spreadsheet": "Spreadsheet files",
|
||||
"assets.mimetypes.group:video": "Video files",
|
||||
"assets.mimetypes.group:web_audio": "Audio files used on the web",
|
||||
"assets.mimetypes.group:web_file": "Web files",
|
||||
"assets.mimetypes.group:web_image": "Image files used on the web",
|
||||
"assets.mimetypes.group:web_video": "Video files used on the web",
|
||||
"assets.mimetypes.image": "Image ({{$a.MIMETYPE2}})",
|
||||
"assets.mimetypes.image/vnd.microsoft.icon": "Windows icon",
|
||||
"assets.mimetypes.text/css": "Cascading Style-Sheet",
|
||||
"assets.mimetypes.text/csv": "Comma-separated values",
|
||||
"assets.mimetypes.text/html": "HTML document",
|
||||
"assets.mimetypes.text/plain": "Text file",
|
||||
"assets.mimetypes.text/rtf": "RTF document",
|
||||
"assets.mimetypes.text/vtt": "Web Video Text Track",
|
||||
"assets.mimetypes.video": "Video file ({{$a.EXT}})",
|
||||
"core.login.yourenteredsite": "Connect to your site"
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"application/epub_zip": "EPUB ebook",
|
||||
"application/msword": "Word document",
|
||||
"application/pdf": "PDF document",
|
||||
"application/vnd.moodle.backup": "Moodle backup",
|
||||
"application/vnd.ms-excel": "Excel spreadsheet",
|
||||
"application/vnd.ms-excel.sheet.macroEnabled.12": "Excel 2007 macro-enabled workbook",
|
||||
"application/vnd.ms-powerpoint": "Powerpoint presentation",
|
||||
"application/vnd.oasis.opendocument.spreadsheet": "OpenDocument Spreadsheet",
|
||||
"application/vnd.oasis.opendocument.spreadsheet-template": "OpenDocument Spreadsheet template",
|
||||
"application/vnd.oasis.opendocument.text": "OpenDocument Text document",
|
||||
"application/vnd.oasis.opendocument.text-template": "OpenDocument Text template",
|
||||
"application/vnd.oasis.opendocument.text-web": "OpenDocument Web page template",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "Powerpoint 2007 presentation",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow": "Powerpoint 2007 slideshow",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Excel 2007 spreadsheet",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template": "Excel 2007 template",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Word 2007 document",
|
||||
"application/x-iwork-keynote-sffkey": "iWork Keynote presentation",
|
||||
"application/x-iwork-numbers-sffnumbers": "iWork Numbers spreadsheet",
|
||||
"application/x-iwork-pages-sffpages": "iWork Pages document",
|
||||
"application/x-javascript": "JavaScript source",
|
||||
"application/x-mspublisher": "Publisher document",
|
||||
"application/x-shockwave-flash": "Flash animation",
|
||||
"application/xhtml_xml": "XHTML document",
|
||||
"archive": "Archive ({{$a.EXT}})",
|
||||
"audio": "Audio file ({{$a.EXT}})",
|
||||
"default": "{{$a.mimetype}}",
|
||||
"document/unknown": "File",
|
||||
"group:archive": "Archive files",
|
||||
"group:audio": "Audio files",
|
||||
"group:document": "Document files",
|
||||
"group:html_audio": "Audio files natively supported by browsers",
|
||||
"group:html_track": "HTML track files",
|
||||
"group:html_video": "Video files natively supported by browsers",
|
||||
"group:image": "Image files",
|
||||
"group:presentation": "Presentation files",
|
||||
"group:sourcecode": "Source code",
|
||||
"group:spreadsheet": "Spreadsheet files",
|
||||
"group:video": "Video files",
|
||||
"group:web_audio": "Audio files used on the web",
|
||||
"group:web_file": "Web files",
|
||||
"group:web_image": "Image files used on the web",
|
||||
"group:web_video": "Video files used on the web",
|
||||
"image": "Image ({{$a.MIMETYPE2}})",
|
||||
"image/vnd.microsoft.icon": "Windows icon",
|
||||
"text/css": "Cascading Style-Sheet",
|
||||
"text/csv": "Comma-separated values",
|
||||
"text/html": "HTML document",
|
||||
"text/plain": "Text file",
|
||||
"text/rtf": "RTF document",
|
||||
"text/vtt": "Web Video Text Track",
|
||||
"video": "Video file ({{$a.EXT}})"
|
||||
}
|
Loading…
Reference in New Issue