commit
0af73bd413
|
@ -1,69 +0,0 @@
|
||||||
// (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();
|
|
237
gulp/git.js
237
gulp/git.js
|
@ -1,237 +0,0 @@
|
||||||
// (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);
|
|
||||||
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();
|
|
476
gulp/jira.js
476
gulp/jira.js
|
@ -1,476 +0,0 @@
|
||||||
// (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 trackerData = await this.askTrackerData();
|
|
||||||
|
|
||||||
trackerData.fromInput = true;
|
|
||||||
|
|
||||||
return trackerData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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', (error, username) => {
|
|
||||||
if (username) {
|
|
||||||
resolve({
|
|
||||||
url: url.replace('\n', ''),
|
|
||||||
username: username.replace('\n', ''),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject(error || 'Username not found.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize some data.
|
|
||||||
*
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
if (this.initialized) {
|
|
||||||
// Already initialized.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tracker URL and username.
|
|
||||||
let 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 buildRequest = https.request(url, options);
|
|
||||||
|
|
||||||
// Add data.
|
|
||||||
if (data) {
|
|
||||||
buildRequest.write(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Treat response.
|
|
||||||
buildRequest.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,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
buildRequest.on('error', (e) => {
|
|
||||||
reject(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send the request.
|
|
||||||
buildRequest.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a set of fields for a certain issue in Jira.
|
|
||||||
*
|
|
||||||
* @param issueId Key to identify the issue. E.g. MOBILE-1234.
|
|
||||||
* @param updates Object with the fields to update.
|
|
||||||
* @return Promise resolved when done.
|
|
||||||
*/
|
|
||||||
async setCustomFields(issueId, updates) {
|
|
||||||
const issue = await this.getIssue(issueId);
|
|
||||||
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 id in issue.names) {
|
|
||||||
if (issue.names[id] == updateName) {
|
|
||||||
fieldKey = id;
|
|
||||||
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) => {
|
|
||||||
// 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();
|
|
|
@ -1,280 +0,0 @@
|
||||||
// (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;
|
|
79
gulp/url.js
79
gulp/url.js
|
@ -1,79 +0,0 @@
|
||||||
// (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;
|
|
120
gulp/utils.js
120
gulp/utils.js
|
@ -1,120 +0,0 @@
|
||||||
// (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 = {};
|
|
||||||
let curOpt;
|
|
||||||
|
|
||||||
for (const argument of process.argv) {
|
|
||||||
const thisOpt = argument.trim();
|
|
||||||
const option = thisOpt.replace(/^\-+/, '');
|
|
||||||
|
|
||||||
if (option === thisOpt) {
|
|
||||||
// argument value
|
|
||||||
if (curOpt) {
|
|
||||||
args[curOpt] = option;
|
|
||||||
}
|
|
||||||
curOpt = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Argument name.
|
|
||||||
curOpt = option;
|
|
||||||
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;
|
|
|
@ -16,9 +16,7 @@ const BuildLangTask = require('./gulp/task-build-lang');
|
||||||
const BuildBehatPluginTask = require('./gulp/task-build-behat-plugin');
|
const BuildBehatPluginTask = require('./gulp/task-build-behat-plugin');
|
||||||
const BuildEnvTask = require('./gulp/task-build-env');
|
const BuildEnvTask = require('./gulp/task-build-env');
|
||||||
const BuildIconsJsonTask = require('./gulp/task-build-icons-json');
|
const BuildIconsJsonTask = require('./gulp/task-build-icons-json');
|
||||||
const PushTask = require('./gulp/task-push');
|
|
||||||
const OverrideLangTask = require('./gulp/task-override-lang');
|
const OverrideLangTask = require('./gulp/task-override-lang');
|
||||||
const Utils = require('./gulp/utils');
|
|
||||||
const gulp = require('gulp');
|
const gulp = require('gulp');
|
||||||
|
|
||||||
const paths = {
|
const paths = {
|
||||||
|
@ -31,8 +29,6 @@ const paths = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const args = Utils.getCommandLineArguments();
|
|
||||||
|
|
||||||
// Build the language files into a single file per language.
|
// Build the language files into a single file per language.
|
||||||
gulp.task('lang', (done) => {
|
gulp.task('lang', (done) => {
|
||||||
new BuildLangTask().run(paths.lang, done);
|
new BuildLangTask().run(paths.lang, done);
|
||||||
|
@ -59,10 +55,6 @@ if (BuildBehatPluginTask.isBehatConfigured()) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
gulp.task('push', (done) => {
|
|
||||||
new PushTask().run(args, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
'default',
|
'default',
|
||||||
gulp.parallel([
|
gulp.parallel([
|
||||||
|
|
|
@ -141,7 +141,6 @@
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-preset-angular": "^13.1.4",
|
"jest-preset-angular": "^13.1.4",
|
||||||
"jsonc-parser": "^2.3.1",
|
"jsonc-parser": "^2.3.1",
|
||||||
"keytar": "^7.2.0",
|
|
||||||
"minimatch": "^9.0.3",
|
"minimatch": "^9.0.3",
|
||||||
"native-run": "^2.0.0",
|
"native-run": "^2.0.0",
|
||||||
"patch-package": "^6.5.0",
|
"patch-package": "^6.5.0",
|
||||||
|
@ -11688,21 +11687,6 @@
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/decompress-response": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"mimic-response": "^3.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dedent": {
|
"node_modules/dedent": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||||
|
@ -12029,15 +12013,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/detect-newline": {
|
"node_modules/detect-newline": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||||
|
@ -13338,15 +13313,6 @@
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/expand-template": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expand-tilde": {
|
"node_modules/expand-tilde": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
|
||||||
|
@ -14500,12 +14466,6 @@
|
||||||
"assert-plus": "^1.0.0"
|
"assert-plus": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/github-from-package": {
|
|
||||||
"version": "0.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
|
||||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
@ -19897,17 +19857,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz",
|
||||||
"integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg=="
|
"integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg=="
|
||||||
},
|
},
|
||||||
"node_modules/keytar": {
|
|
||||||
"version": "7.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
|
|
||||||
"integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
|
||||||
"node-addon-api": "^4.3.0",
|
|
||||||
"prebuild-install": "^7.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
@ -21178,18 +21127,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-response": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/min-document": {
|
"node_modules/min-document": {
|
||||||
"version": "2.19.0",
|
"version": "2.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||||
|
@ -21472,12 +21409,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp-classic": {
|
|
||||||
"version": "0.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
|
||||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.29.4",
|
"version": "2.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
|
@ -21652,12 +21583,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/napi-build-utils": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/native-run": {
|
"node_modules/native-run": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.0.tgz",
|
||||||
|
@ -21845,24 +21770,6 @@
|
||||||
"lower-case": "^1.1.1"
|
"lower-case": "^1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-abi": {
|
|
||||||
"version": "3.51.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz",
|
|
||||||
"integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"semver": "^7.3.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-addon-api": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/node-forge": {
|
"node_modules/node-forge": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
||||||
|
@ -24054,32 +23961,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||||
},
|
},
|
||||||
"node_modules/prebuild-install": {
|
|
||||||
"version": "7.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
|
|
||||||
"integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"detect-libc": "^2.0.0",
|
|
||||||
"expand-template": "^2.0.3",
|
|
||||||
"github-from-package": "0.0.0",
|
|
||||||
"minimist": "^1.2.3",
|
|
||||||
"mkdirp-classic": "^0.5.3",
|
|
||||||
"napi-build-utils": "^1.0.1",
|
|
||||||
"node-abi": "^3.3.0",
|
|
||||||
"pump": "^3.0.0",
|
|
||||||
"rc": "^1.2.7",
|
|
||||||
"simple-get": "^4.0.0",
|
|
||||||
"tar-fs": "^2.0.0",
|
|
||||||
"tunnel-agent": "^0.6.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"prebuild-install": "bin.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
@ -25938,51 +25819,6 @@
|
||||||
"tail": "^0.4.0"
|
"tail": "^0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-concat": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/simple-get": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"decompress-response": "^6.0.0",
|
|
||||||
"once": "^1.3.1",
|
|
||||||
"simple-concat": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/simple-plist": {
|
"node_modules/simple-plist": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz",
|
||||||
|
@ -26948,24 +26784,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-fs": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"chownr": "^1.1.1",
|
|
||||||
"mkdirp-classic": "^0.5.2",
|
|
||||||
"pump": "^3.0.0",
|
|
||||||
"tar-stream": "^2.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tar-fs/node_modules/chownr": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/tar-stream": {
|
"node_modules/tar-stream": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
|
|
@ -176,7 +176,6 @@
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-preset-angular": "^13.1.4",
|
"jest-preset-angular": "^13.1.4",
|
||||||
"jsonc-parser": "^2.3.1",
|
"jsonc-parser": "^2.3.1",
|
||||||
"keytar": "^7.2.0",
|
|
||||||
"minimatch": "^9.0.3",
|
"minimatch": "^9.0.3",
|
||||||
"native-run": "^2.0.0",
|
"native-run": "^2.0.0",
|
||||||
"patch-package": "^6.5.0",
|
"patch-package": "^6.5.0",
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col size="auto" *ngIf="sort.enabled">
|
<ion-col size="auto" *ngIf="sort.enabled">
|
||||||
<core-combobox [label]="'core.sortby' | translate" [selection]="sort.selected" (onChange)="sortCourses($event)"
|
<core-combobox [label]="'core.sortby' | translate" [selection]="sort.selected" (onChange)="sortCourses($event)"
|
||||||
icon="fas-arrow-down-short-wide">
|
icon="fas-arrow-down-short-wide" class="no-border">
|
||||||
<ion-select-option class="ion-text-wrap" value="fullname">
|
<ion-select-option class="ion-text-wrap" value="fullname">
|
||||||
{{'addon.block_myoverview.title' | translate}}
|
{{'addon.block_myoverview.title' | translate}}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
|
|
|
@ -10,24 +10,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-button,
|
ion-button,
|
||||||
core-combobox ::ng-deep ion-button {
|
core-combobox ::ng-deep ion-select {
|
||||||
--border-width: 0;
|
|
||||||
--a11y-min-target-size: 40px;
|
--a11y-min-target-size: 40px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-button {
|
||||||
|
--border-width: 0;
|
||||||
|
|
||||||
.select-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
core-combobox ::ng-deep ion-select {
|
|
||||||
margin: 0;
|
|
||||||
--a11y-min-target-size: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-searchbar {
|
ion-searchbar {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
--height: 40px;
|
--height: 40px;
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
:host .core-block-content ::ng-deep {
|
:host .core-block-content ::ng-deep {
|
||||||
|
ion-label {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
.tag_cloud {
|
.tag_cloud {
|
||||||
text-align: center;
|
|
||||||
ul.inline-list {
|
ul.inline-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
-webkit-padding-start: 0;
|
-webkit-padding-start: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
padding: .2em;
|
padding: .2em;
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
</ion-col>
|
</ion-col>
|
||||||
<ion-col size="auto">
|
<ion-col size="auto">
|
||||||
<core-combobox [label]="'core.sortby' | translate" [formControl]="sort" (onChange)="sortChanged($event)"
|
<core-combobox [label]="'core.sortby' | translate" [formControl]="sort" (onChange)="sortChanged($event)"
|
||||||
icon="fas-arrow-down-short-wide">
|
icon="fas-arrow-down-short-wide" class="no-border">
|
||||||
<ion-select-option *ngFor="let option of sortOptions" class="ion-text-wrap" [value]="option.value">
|
<ion-select-option *ngFor="let option of sortOptions" class="ion-text-wrap" [value]="option.value">
|
||||||
{{ option.name | translate }}
|
{{ option.name | translate }}
|
||||||
</ion-select-option>
|
</ion-select-option>
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
@ -5,30 +5,94 @@
|
||||||
display: block;
|
display: block;
|
||||||
@include margin-horizontal(var(--ion-safe-area-left), var(--ion-safe-area-right));
|
@include margin-horizontal(var(--ion-safe-area-left), var(--ion-safe-area-right));
|
||||||
|
|
||||||
|
&.no-border {
|
||||||
|
--core-combobox-border-width: 0px;
|
||||||
|
|
||||||
|
ion-select.combobox-icon-only {
|
||||||
|
--padding-start: 8px;
|
||||||
|
|
||||||
|
&::part(icon) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(label) {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
margin: var(--icon-margin);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ion-select,
|
ion-select,
|
||||||
ion-button {
|
ion-button {
|
||||||
--icon-margin: 0 4px;
|
--icon-margin: 0 4px;
|
||||||
--background: var(--core-combobox-background);
|
|
||||||
--background-hover: black;
|
|
||||||
--background-activated: black;
|
|
||||||
--background-focused: black;
|
|
||||||
--background-hover-opacity: .04;
|
|
||||||
|
|
||||||
|
--background: var(--core-combobox-background);
|
||||||
|
|
||||||
|
--border-color: var(--core-combobox-border-color);
|
||||||
|
--border-style: solid;
|
||||||
|
--border-width: var(--core-combobox-border-width);
|
||||||
|
--border-radius: var(--core-combobox-radius);
|
||||||
|
|
||||||
|
--box-shadow: var(--core-combobox-box-shadow);
|
||||||
|
|
||||||
|
--padding-start: 16px;
|
||||||
|
--padding-end: 8px;
|
||||||
|
--padding-top: 8px;
|
||||||
|
--padding-bottom: 8px;
|
||||||
|
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--color);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-height: var(--a11y-min-target-size);
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
@include core-focus-style();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-select {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
border-style: var(--border-style);
|
||||||
|
border-width: var(--border-width);
|
||||||
|
border-radius: var(--core-combobox-radius);
|
||||||
|
margin: 8px;
|
||||||
|
|
||||||
|
&.combobox-icon-only {
|
||||||
|
&::part(text) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(label) {
|
||||||
|
position: absolute;
|
||||||
|
margin-inline: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(icon) {
|
||||||
|
margin: var(--icon-margin);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-button {
|
||||||
--color: var(--core-combobox-color);
|
--color: var(--core-combobox-color);
|
||||||
--color-activated: var(--core-combobox-color);
|
--color-activated: var(--core-combobox-color);
|
||||||
--color-focused: currentcolor;
|
--color-focused: currentcolor;
|
||||||
--color-hover: currentcolor;
|
--color-hover: currentcolor;
|
||||||
|
|
||||||
--border-color: var(--core-combobox-border-color);
|
--background-hover: black;
|
||||||
--border-style: solid;
|
--background-activated: black;
|
||||||
--border-width: var(--core-combobox-border-width);
|
--background-focused: black;
|
||||||
--border-radius: var(--radius-xs);
|
--background-hover-opacity: .04;
|
||||||
|
|
||||||
--box-shadow: var(--core-combobox-box-shadow);
|
|
||||||
|
|
||||||
--padding-top: 8px;
|
|
||||||
--padding-end: 8px;
|
|
||||||
--padding-bottom: 8px;
|
|
||||||
|
|
||||||
&.md {
|
&.md {
|
||||||
--background-activated-opacity: 0;
|
--background-activated-opacity: 0;
|
||||||
|
@ -39,97 +103,36 @@
|
||||||
--background-activated-opacity: .12;
|
--background-activated-opacity: .12;
|
||||||
--background-focused-opacity: .15;
|
--background-focused-opacity: .15;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ion-button {
|
|
||||||
--padding-start: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-select {
|
|
||||||
--padding-start: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-select,
|
|
||||||
ion-button {
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--color);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
min-height: var(--a11y-min-target-size);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:focus-within {
|
|
||||||
@include core-focus-style();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-select {
|
|
||||||
border-color: var(--border-color);
|
|
||||||
border-style: var(--border-style);
|
|
||||||
border-width: var(--border-width);
|
|
||||||
border-radius: var(--core-combobox-radius);
|
|
||||||
margin: 8px;
|
|
||||||
|
|
||||||
&::part(icon) {
|
|
||||||
margin: var(--icon-margin);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
@include button-state();
|
|
||||||
transition: var(--transition);
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover::after {
|
|
||||||
color: var(--color-hover);
|
|
||||||
background: var(--background-hover);
|
|
||||||
opacity: var(--background-hover-opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus::after,
|
|
||||||
&:focus-within::after {
|
|
||||||
color: var(--color-focused);
|
|
||||||
background: var(--background-focused);
|
|
||||||
opacity: var(--background-focused-opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[hidden] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-button {
|
|
||||||
border-radius: var(--core-combobox-radius);
|
|
||||||
margin: 4px 8px;
|
|
||||||
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
&::part(native) {
|
|
||||||
text-transform: none;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 20px;
|
|
||||||
border-radius: var(--core-combobox-radius);
|
border-radius: var(--core-combobox-radius);
|
||||||
|
margin: 4px 8px;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&::part(native) {
|
||||||
|
text-transform: none;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-radius: var(--core-combobox-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-text {
|
||||||
|
@include margin-horizontal(null, auto);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.sr-only {
|
||||||
|
@include sr-only();
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ion-activated {
|
||||||
|
--color: var(--color-activated);
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
margin: var(--icon-margin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-text {
|
|
||||||
@include margin-horizontal(null, auto);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.sr-only {
|
|
||||||
@include sr-only();
|
|
||||||
}
|
|
||||||
|
|
||||||
&.ion-activated {
|
|
||||||
--color: var(--color-activated);
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-icon {
|
|
||||||
margin: var(--icon-margin);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,10 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { ModalOptions } from '@ionic/core';
|
import { ModalOptions } from '@ionic/core';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { IonSelect } from '@ionic/angular';
|
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,8 +50,6 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
})
|
})
|
||||||
export class CoreComboboxComponent implements ControlValueAccessor {
|
export class CoreComboboxComponent implements ControlValueAccessor {
|
||||||
|
|
||||||
@ViewChild(IonSelect) select?: IonSelect;
|
|
||||||
|
|
||||||
@Input() interface: 'popover' | 'modal' = 'popover';
|
@Input() interface: 'popover' | 'modal' = 'popover';
|
||||||
@Input() label = Translate.instant('core.show'); // Aria label.
|
@Input() label = Translate.instant('core.show'); // Aria label.
|
||||||
@Input() disabled = false;
|
@Input() disabled = false;
|
||||||
|
@ -112,30 +109,25 @@ export class CoreComboboxComponent implements ControlValueAccessor {
|
||||||
/**
|
/**
|
||||||
* Shows combobox modal.
|
* Shows combobox modal.
|
||||||
*
|
*
|
||||||
* @param event Event.
|
|
||||||
* @returns Promise resolved when done.
|
* @returns Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async openSelect(event?: UIEvent): Promise<void> {
|
async openModal(): Promise<void> {
|
||||||
this.touch();
|
this.touch();
|
||||||
|
|
||||||
if (this.interface === 'modal') {
|
if (this.expanded || !this.modalOptions) {
|
||||||
if (this.expanded || !this.modalOptions) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
this.expanded = true;
|
||||||
this.expanded = true;
|
|
||||||
|
|
||||||
if (this.listboxId) {
|
if (this.listboxId) {
|
||||||
this.modalOptions.id = this.listboxId;
|
this.modalOptions.id = this.listboxId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await CoreDomUtils.openModal(this.modalOptions);
|
const data = await CoreDomUtils.openModal(this.modalOptions);
|
||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
this.onValueChanged(data);
|
this.onValueChanged(data);
|
||||||
}
|
|
||||||
} else if (this.select) {
|
|
||||||
this.select.open(event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
<ion-button (click)="openSelect($event)" *ngIf="interface !== 'modal' && icon" [disabled]="disabled">
|
|
||||||
<ion-icon [name]="icon" [attr.aria-label]="label" slot="start" />
|
|
||||||
<div class="select-icon" role="presentation" aria-hidden="true">
|
|
||||||
<div class="select-icon-inner"></div>
|
|
||||||
</div>
|
|
||||||
</ion-button>
|
|
||||||
<ion-select *ngIf="interface !== 'modal'" class="ion-text-start" [(ngModel)]="selection" (ngModelChange)="onValueChanged(selection)"
|
<ion-select *ngIf="interface !== 'modal'" class="ion-text-start" [(ngModel)]="selection" (ngModelChange)="onValueChanged(selection)"
|
||||||
[interface]="interface" [attr.aria-label]="label" [disabled]="disabled" [hidden]="!!icon">
|
[interface]="interface" [disabled]="disabled" [class.combobox-icon-only]="icon" [interfaceOptions]="{alignment: 'start', arrow: false}">
|
||||||
|
<div slot="label">
|
||||||
|
<span class="sr-only" *ngIf="label">{{ label }}</span>
|
||||||
|
<ion-icon *ngIf="icon" [name]="icon" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
|
|
||||||
<ion-button *ngIf="interface === 'modal'" aria-haspopup="listbox" [attr.aria-controls]="listboxId" [attr.aria-owns]="listboxId"
|
<ion-button *ngIf="interface === 'modal'" aria-haspopup="listbox" [attr.aria-controls]="listboxId" [attr.aria-owns]="listboxId"
|
||||||
[attr.aria-expanded]="expanded" (click)="openSelect()" [disabled]="disabled" expand="block" role="combobox">
|
[attr.aria-expanded]="expanded" (click)="openModal()" [disabled]="disabled" expand="block" role="combobox">
|
||||||
<ion-icon *ngIf="icon" [name]="icon" slot="start" aria-hidden="true" />
|
<ion-icon *ngIf="icon" [name]="icon" slot="start" aria-hidden="true" />
|
||||||
<span class="sr-only" *ngIf="label">{{ label }}:</span>
|
<span class="sr-only" *ngIf="label">{{ label }},</span>
|
||||||
<div class="select-text">
|
<div class="select-text">
|
||||||
<slot name="text">{{selection}}</slot>
|
<slot name="text">{{selection}}</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,19 +30,14 @@ export class CoreFileUploaderAlbumHandlerService implements CoreFileUploaderHand
|
||||||
priority = 2000;
|
priority = 2000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the handler is enabled on a site level.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Promise resolved with true if enabled.
|
|
||||||
*/
|
*/
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
return CorePlatform.isMobile();
|
return CorePlatform.isMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of mimetypes, return the ones that are supported by the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param mimetypes List of mimetypes.
|
|
||||||
* @returns Supported mimetypes.
|
|
||||||
*/
|
*/
|
||||||
getSupportedMimetypes(mimetypes: string[]): string[] {
|
getSupportedMimetypes(mimetypes: string[]): string[] {
|
||||||
// Album allows picking images and videos.
|
// Album allows picking images and videos.
|
||||||
|
@ -50,15 +45,13 @@ export class CoreFileUploaderAlbumHandlerService implements CoreFileUploaderHand
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the data to display the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Data.
|
|
||||||
*/
|
*/
|
||||||
getData(): CoreFileUploaderHandlerData {
|
getData(): CoreFileUploaderHandlerData {
|
||||||
return {
|
return {
|
||||||
title: 'core.fileuploader.photoalbums',
|
title: 'core.fileuploader.photoalbums',
|
||||||
class: 'core-fileuploader-album-handler',
|
class: 'core-fileuploader-album-handler',
|
||||||
icon: 'images', // Cannot use font-awesome in action sheet.
|
icon: 'fas-images',
|
||||||
action: async (
|
action: async (
|
||||||
maxSize?: number,
|
maxSize?: number,
|
||||||
upload?: boolean,
|
upload?: boolean,
|
||||||
|
|
|
@ -30,19 +30,14 @@ export class CoreFileUploaderAudioHandlerService implements CoreFileUploaderHand
|
||||||
priority = 1600;
|
priority = 1600;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the handler is enabled on a site level.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Promise resolved with true if enabled.
|
|
||||||
*/
|
*/
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
return CorePlatform.isMobile() || (CoreApp.canGetUserMedia() && CoreApp.canRecordMedia());
|
return CorePlatform.isMobile() || (CoreApp.canGetUserMedia() && CoreApp.canRecordMedia());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of mimetypes, return the ones that are supported by the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param mimetypes List of mimetypes.
|
|
||||||
* @returns Supported mimetypes.
|
|
||||||
*/
|
*/
|
||||||
getSupportedMimetypes(mimetypes: string[]): string[] {
|
getSupportedMimetypes(mimetypes: string[]): string[] {
|
||||||
if (CorePlatform.isIOS()) {
|
if (CorePlatform.isIOS()) {
|
||||||
|
@ -66,15 +61,13 @@ export class CoreFileUploaderAudioHandlerService implements CoreFileUploaderHand
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the data to display the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Data.
|
|
||||||
*/
|
*/
|
||||||
getData(): CoreFileUploaderHandlerData {
|
getData(): CoreFileUploaderHandlerData {
|
||||||
return {
|
return {
|
||||||
title: 'core.fileuploader.audio',
|
title: 'core.fileuploader.audio',
|
||||||
class: 'core-fileuploader-audio-handler',
|
class: 'core-fileuploader-audio-handler',
|
||||||
icon: 'mic', // Cannot use font-awesome in action sheet.
|
icon: 'fas-microphone',
|
||||||
action: async (
|
action: async (
|
||||||
maxSize?: number,
|
maxSize?: number,
|
||||||
upload?: boolean,
|
upload?: boolean,
|
||||||
|
|
|
@ -31,19 +31,14 @@ export class CoreFileUploaderCameraHandlerService implements CoreFileUploaderHan
|
||||||
priority = 1800;
|
priority = 1800;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the handler is enabled on a site level.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Promise resolved with true if enabled.
|
|
||||||
*/
|
*/
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
return CorePlatform.isMobile() || CoreApp.canGetUserMedia();
|
return CorePlatform.isMobile() || CoreApp.canGetUserMedia();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of mimetypes, return the ones that are supported by the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param mimetypes List of mimetypes.
|
|
||||||
* @returns Supported mimetypes.
|
|
||||||
*/
|
*/
|
||||||
getSupportedMimetypes(mimetypes: string[]): string[] {
|
getSupportedMimetypes(mimetypes: string[]): string[] {
|
||||||
// Camera only supports JPEG and PNG.
|
// Camera only supports JPEG and PNG.
|
||||||
|
@ -51,15 +46,13 @@ export class CoreFileUploaderCameraHandlerService implements CoreFileUploaderHan
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the data to display the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Data.
|
|
||||||
*/
|
*/
|
||||||
getData(): CoreFileUploaderHandlerData {
|
getData(): CoreFileUploaderHandlerData {
|
||||||
return {
|
return {
|
||||||
title: 'core.fileuploader.camera',
|
title: 'core.fileuploader.camera',
|
||||||
class: 'core-fileuploader-camera-handler',
|
class: 'core-fileuploader-camera-handler',
|
||||||
icon: 'camera', // Cannot use font-awesome in action sheet.
|
icon: 'fas-camera',
|
||||||
action: async (
|
action: async (
|
||||||
maxSize?: number,
|
maxSize?: number,
|
||||||
upload?: boolean,
|
upload?: boolean,
|
||||||
|
|
|
@ -31,34 +31,27 @@ export class CoreFileUploaderFileHandlerService implements CoreFileUploaderHandl
|
||||||
priority = 1200;
|
priority = 1200;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the handler is enabled on a site level.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Promise resolved with true if enabled.
|
|
||||||
*/
|
*/
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of mimetypes, return the ones that are supported by the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param mimetypes List of mimetypes.
|
|
||||||
* @returns Supported mimetypes.
|
|
||||||
*/
|
*/
|
||||||
getSupportedMimetypes(mimetypes: string[]): string[] {
|
getSupportedMimetypes(mimetypes: string[]): string[] {
|
||||||
return mimetypes;
|
return mimetypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the data to display the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Data.
|
|
||||||
*/
|
*/
|
||||||
getData(): CoreFileUploaderHandlerData {
|
getData(): CoreFileUploaderHandlerData {
|
||||||
const handler: CoreFileUploaderHandlerData = {
|
const handler: CoreFileUploaderHandlerData = {
|
||||||
title: 'core.fileuploader.file',
|
title: 'core.fileuploader.file',
|
||||||
class: 'core-fileuploader-file-handler',
|
class: 'core-fileuploader-file-handler',
|
||||||
icon: 'folder', // Cannot use font-awesome in action sheet.
|
icon: 'fas-file-lines',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (CorePlatform.isMobile()) {
|
if (CorePlatform.isMobile()) {
|
||||||
|
|
|
@ -30,19 +30,14 @@ export class CoreFileUploaderVideoHandlerService implements CoreFileUploaderHand
|
||||||
priority = 1400;
|
priority = 1400;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the handler is enabled on a site level.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Promise resolved with true if enabled.
|
|
||||||
*/
|
*/
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
return CorePlatform.isMobile() || (CoreApp.canGetUserMedia() && CoreApp.canRecordMedia());
|
return CorePlatform.isMobile() || (CoreApp.canGetUserMedia() && CoreApp.canRecordMedia());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of mimetypes, return the ones that are supported by the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param mimetypes List of mimetypes.
|
|
||||||
* @returns Supported mimetypes.
|
|
||||||
*/
|
*/
|
||||||
getSupportedMimetypes(mimetypes: string[]): string[] {
|
getSupportedMimetypes(mimetypes: string[]): string[] {
|
||||||
if (CorePlatform.isIOS()) {
|
if (CorePlatform.isIOS()) {
|
||||||
|
@ -66,15 +61,13 @@ export class CoreFileUploaderVideoHandlerService implements CoreFileUploaderHand
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the data to display the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Data.
|
|
||||||
*/
|
*/
|
||||||
getData(): CoreFileUploaderHandlerData {
|
getData(): CoreFileUploaderHandlerData {
|
||||||
return {
|
return {
|
||||||
title: 'core.fileuploader.video',
|
title: 'core.fileuploader.video',
|
||||||
class: 'core-fileuploader-video-handler',
|
class: 'core-fileuploader-video-handler',
|
||||||
icon: 'videocam', // Cannot use font-awesome in action sheet.
|
icon: 'fas-video',
|
||||||
action: async (
|
action: async (
|
||||||
maxSize?: number,
|
maxSize?: number,
|
||||||
upload?: boolean,
|
upload?: boolean,
|
||||||
|
|
|
@ -32,34 +32,27 @@ export class CoreSharedFilesUploadHandlerService implements CoreFileUploaderHand
|
||||||
priority = 1300;
|
priority = 1300;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the handler is enabled on a site level.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns True or promise resolved with true if enabled.
|
|
||||||
*/
|
*/
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
return CorePlatform.isIOS();
|
return CorePlatform.isIOS();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of mimetypes, return the ones that are supported by the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @param mimetypes List of mimetypes.
|
|
||||||
* @returns Supported mimetypes.
|
|
||||||
*/
|
*/
|
||||||
getSupportedMimetypes(mimetypes: string[]): string[] {
|
getSupportedMimetypes(mimetypes: string[]): string[] {
|
||||||
return mimetypes;
|
return mimetypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the data to display the handler.
|
* @inheritdoc
|
||||||
*
|
|
||||||
* @returns Data.
|
|
||||||
*/
|
*/
|
||||||
getData(): CoreFileUploaderHandlerData {
|
getData(): CoreFileUploaderHandlerData {
|
||||||
return {
|
return {
|
||||||
title: 'core.sharedfiles.sharedfiles',
|
title: 'core.sharedfiles.sharedfiles',
|
||||||
class: 'core-sharedfiles-fileuploader-handler',
|
class: 'core-sharedfiles-fileuploader-handler',
|
||||||
icon: 'folder', // Cannot use font-awesome in action sheet.
|
icon: 'fas-folder',
|
||||||
action: (
|
action: (
|
||||||
maxSize?: number,
|
maxSize?: number,
|
||||||
upload?: boolean,
|
upload?: boolean,
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class CoreUserProfileMailHandlerService implements CoreUserProfileHandler
|
||||||
*/
|
*/
|
||||||
getDisplayData(): CoreUserProfileHandlerData {
|
getDisplayData(): CoreUserProfileHandlerData {
|
||||||
return {
|
return {
|
||||||
icon: 'mail',
|
icon: 'fas-envelope',
|
||||||
title: 'core.user.sendemail',
|
title: 'core.user.sendemail',
|
||||||
class: 'core-user-profile-mail',
|
class: 'core-user-profile-mail',
|
||||||
action: (event, user): void => {
|
action: (event, user): void => {
|
||||||
|
|
|
@ -1027,7 +1027,6 @@ input[type=radio],
|
||||||
--border-width: 2px;
|
--border-width: 2px;
|
||||||
--outer-border-width: 2px;
|
--outer-border-width: 2px;
|
||||||
--border-style: solid;
|
--border-style: solid;
|
||||||
--inner-border-radius: 50%;
|
|
||||||
--size: 20px;
|
--size: 20px;
|
||||||
|
|
||||||
&:not(.ion-color) {
|
&:not(.ion-color) {
|
||||||
|
@ -1045,10 +1044,10 @@ input[type=radio],
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios ion-radio {
|
.ios ion-radio {
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
|
|
||||||
&::part(container) {
|
&::part(container) {
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
border-width: var(--outer-border-width);
|
border-width: var(--outer-border-width);
|
||||||
|
@ -1155,6 +1154,13 @@ ion-select {
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-select-popover {
|
ion-select-popover {
|
||||||
|
ion-list ion-radio-group ion-item.select-interface-option ion-radio.hydrated::part(container) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-item {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
ion-item.core-select-option-border-bottom {
|
ion-item.core-select-option-border-bottom {
|
||||||
border-bottom: 1px solid var(--stroke);
|
border-bottom: 1px solid var(--stroke);
|
||||||
}
|
}
|
||||||
|
@ -1645,6 +1651,7 @@ ion-item.item {
|
||||||
// Change default outline.
|
// Change default outline.
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
@include core-focus-style();
|
@include core-focus-style();
|
||||||
|
border-radius: inherit;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue