Presets: shorter index.file, added option groups and preset priority

10.8-maintenance
Ivan Efimov 2022-01-14 04:41:09 -06:00
parent 47ac2eb7af
commit 9357eae32c
8 changed files with 353 additions and 123 deletions

View File

@ -6372,9 +6372,13 @@
"message": "Do you really want to close the configurator?"
},
"dropDownSelectAll": {
"message": "[Select all]",
"message": "[Select/deselect all]",
"description": "Select all item in the drop down/multiple select"
},
"dropDownFilterDisabled": {
"message": "Select...",
"description": "Text indicating nothing is selected in the drop down/multiple select (filter disabled)"
},
"dropDownAll": {
"message": "All",
"description": "Text indicating everything is selected in the drop down/multiple select"
@ -6570,5 +6574,13 @@
"presetsTooManyPresetsFound": {
"message": "Reached the maximum limit of the shown presets number",
"description": "Message that apprears on presets tab if too many presets found"
},
"presetsOptionsPlaceholder": {
"message": "ATTENTION! Review the list of options",
"description": "Placeholder for the options list dropdown"
},
"presetsVersionMismatch": {
"message": "Preset source version mismatch.<br/>Required version: {{versionRequired}}<br/>Preset source version: {{versionSource}}<br/>Using this preset source could be dangerous.<br/>Do you want to continue?",
"description": "Placeholder for the options list dropdown"
}
}

View File

@ -137,6 +137,7 @@
<script type="text/javascript" src="./tabs/presets/PickedPreset.js"></script>
<script type="text/javascript" src="./tabs/presets/DetailedDialog/PresetsDetailedDialog.js"></script>
<script type="text/javascript" src="./tabs/presets/TitlePanel/PresetTitlePanel.js"></script>
<script type="text/javascript" src="./tabs/presets/PresetsRepoIndexed/PresetParser.js"></script>
<script type="text/javascript" src="./tabs/presets/PresetsRepoIndexed/PresetsRepoIndexed.js"></script>
<script type="text/javascript" src="./tabs/presets/PresetsRepoIndexed/PresetsGithubRepo.js"></script>
<script type="text/javascript" src="./tabs/presets/PresetsRepoIndexed/PresetsWebsiteRepo.js"></script>

View File

@ -46,6 +46,11 @@
padding-left: 20px;
}
#presets_options_panel .ms-choice {
border-color: var(--accentBorder);
border-width: 2px;
}
#presets_detailed_dialog_loading {
height: 300px;
}

View File

@ -125,24 +125,59 @@ class PresetsDetailedDialog {
_createOptionsSelect(options) {
options.forEach(option => {
let selectedString = "selected=\"selected\"";
if (!option.checked) {
selectedString = "";
if (!option.childs) {
this._addOption(this._domOptionsSelect, option, false);
} else {
this._addOptionGroup(this._domOptionsSelect, option);
}
this._domOptionsSelect.append(`<option value="${option.name}" ${selectedString}>${option.name}</option>`);
});
this._domOptionsSelect.multipleSelect({
placeholder: i18n.getMessage("dropDownAll"),
placeholder: i18n.getMessage("presetsOptionsPlaceholder"),
formatSelectAll () { return i18n.getMessage("dropDownSelectAll"); },
formatAllSelected() { return i18n.getMessage("dropDownAll"); },
onClick: () => this._optionsSelectionChanged(),
onCheckAll: () => this._optionsSelectionChanged(),
onUncheckAll: () => this._optionsSelectionChanged(),
hideOptgroupCheckboxes: true,
singleRadio: true,
selectAll: false,
styler: function (row) {
let style = "";
if (row.type === 'optgroup') {
style = 'font-weight: bold;';
} else if (row.classes.includes("optionHasParent")) {
style = 'padding-left: 22px;';
}
return style;
},
});
}
_addOptionGroup(parentElement, optionGroup) {
const optionGroupElement = $(`<optgroup label="${optionGroup.name}"></optgroup>`);
optionGroup.childs.forEach(option => {
this._addOption(optionGroupElement, option, true);
});
parentElement.append(optionGroupElement);
}
_addOption(parentElement, option, hasParent) {
let selectedString = "selected=\"selected\"";
if (!option.checked) {
selectedString = "";
}
let classString = "";
if (hasParent) {
classString = "class=\"optionHasParent\"";
}
parentElement.append(`<option value="${option.name}" ${selectedString} ${classString}>${option.name}</option>`);
}
_optionsSelectionChanged() {
this._updateFinalCliText();
}

View File

@ -0,0 +1,224 @@
'use strict';
class PresetParser {
constructor(settings) {
this._settings = settings;
}
readPresetProperties(preset, strings) {
const propertiesToRead = ["description", "discussion", "warning", "disclaimer", "include_warning", "include_disclaimer", "discussion"];
const propertiesMetadata = {};
preset.options = [];
propertiesToRead.forEach(propertyName => {
// metadata of each property, name, type, optional true/false; example:
// keywords: {type: MetadataTypes.WORDS_ARRAY, optional: true}
propertiesMetadata[propertyName] = this._settings.presetsFileMetadata[propertyName];
preset[propertyName] = undefined;
});
preset._currentOptionGroup = undefined;
for (const line of strings) {
if (line.startsWith(this._settings.MetapropertyDirective)) {
this._parseAttributeLine(preset, line, propertiesMetadata);
}
}
delete preset._currentOptionGroup;
}
_parseAttributeLine(preset, line, propertiesMetadata) {
line = line.slice(this._settings.MetapropertyDirective.length).trim(); // (#$ DESCRIPTION: foo) -> (DESCRIPTION: foo)
const lowCaseLine = line.toLowerCase();
let isProperty = false;
for (const propertyName in propertiesMetadata) {
const lineBeginning = `${propertyName.toLowerCase()}:`; // "description:"
if (lowCaseLine.startsWith(lineBeginning)) {
line = line.slice(lineBeginning.length).trim(); // (Title: foo) -> (foo)
this._parseProperty(preset, line, propertyName);
isProperty = true;
}
}
if (!isProperty && lowCaseLine.startsWith(this._settings.OptionsDirectives.OPTION_DIRECTIVE)) {
this._parseOptionDirective(preset, line);
}
}
_parseOptionDirective(preset, line) {
const lowCaseLine = line.toLowerCase();
if (lowCaseLine.startsWith(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE)) {
const option = this._getOption(line);
if (!preset._currentOptionGroup) {
preset.options.push(option);
} else {
preset._currentOptionGroup.childs.push(option);
}
} else if (lowCaseLine.startsWith(this._settings.OptionsDirectives.BEGIN_OPTION_GROUP_DIRECTIVE)) {
const optionGroup = this._getOptionGroup(line);
preset._currentOptionGroup = optionGroup;
preset.options.push(optionGroup);
} else if (lowCaseLine.startsWith(this._settings.OptionsDirectives.END_OPTION_GROUP_DIRECTIVE)) {
preset._currentOptionGroup = undefined;
}
}
_getOption(line) {
const directiveRemoved = line.slice(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE.length).trim();
const directiveRemovedLowCase = directiveRemoved.toLowerCase();
const OptionChecked = this._isOptionChecked(directiveRemovedLowCase);
const regExpRemoveChecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_CHECKED), 'gi');
const regExpRemoveUnchecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_UNCHECKED), 'gi');
let optionName = directiveRemoved.replace(regExpRemoveChecked, "");
optionName = optionName.replace(regExpRemoveUnchecked, "").trim();
return {
name: optionName.slice(1).trim(),
checked: OptionChecked,
};
}
_getOptionGroup(line) {
const directiveRemoved = line.slice(this._settings.OptionsDirectives.BEGIN_OPTION_GROUP_DIRECTIVE.length).trim();
return {
name: directiveRemoved.slice(1).trim(),
childs: [],
};
}
_isOptionChecked(lowCaseLine) {
return lowCaseLine.includes(this._settings.OptionsDirectives.OPTION_CHECKED);
}
_parseProperty(preset, line, propertyName) {
switch(this._settings.presetsFileMetadata[propertyName].type) {
case this._settings.MetadataTypes.STRING_ARRAY:
this._processArrayProperty(preset, line, propertyName);
break;
case this._settings.MetadataTypes.STRING:
this._processStringProperty(preset, line, propertyName);
break;
case this._settings.MetadataTypes.FILE_PATH:
this._processStringProperty(preset, line, propertyName);
break;
case this._settings.MetadataTypes.FILE_PATH_ARRAY:
this._processArrayProperty(preset, line, propertyName);
break;
default:
this.console.err(`Parcing preset: unknown property type '${this._settings.presetsFileMetadata[property].type}' for the property '${propertyName}'`);
}
}
_processArrayProperty(preset, line, propertyName) {
if (!preset[propertyName]) {
preset[propertyName] = [];
}
preset[propertyName].push(line);
}
_processStringProperty(preset, line, propertyName) {
preset[propertyName] = line;
}
_getOptionName(line) {
const directiveRemoved = line.slice(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE.length).trim();
const regExpRemoveChecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_CHECKED +":"), 'gi');
const regExpRemoveUnchecked = new RegExp(this._escapeRegex(this._settings.OptionsDirectives.OPTION_UNCHECKED +":"), 'gi');
let optionName = directiveRemoved.replace(regExpRemoveChecked, "");
optionName = optionName.replace(regExpRemoveUnchecked, "").trim();
return optionName;
}
_escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
removeUncheckedOptions(strings, checkedOptions) {
let resultStrings = [];
let isCurrentOptionExcluded = false;
const lowerCasedCheckedOptions = checkedOptions.map(optionName => optionName.toLowerCase());
strings.forEach(str => {
if (this._isLineAttribute(str)) {
const line = this._removeAttributeDirective(str);
if (this._isOptionBegin(line)) {
const optionNameLowCase = this._getOptionName(line).toLowerCase();
if (!lowerCasedCheckedOptions.includes(optionNameLowCase)) {
isCurrentOptionExcluded = true;
}
} else if (this._isOptionEnd(line)) {
isCurrentOptionExcluded = false;
}
} else if (!isCurrentOptionExcluded) {
resultStrings.push(str);
}
});
resultStrings = this._removeExcessiveEmptyLines(resultStrings);
return resultStrings;
}
_removeExcessiveEmptyLines(strings) {
// removes empty lines if there are two or more in a row leaving just one empty line
const result = [];
let lastStringEmpty = false;
strings.forEach(str => {
if ("" !== str || !lastStringEmpty) {
result.push(str);
}
if ("" === str) {
lastStringEmpty = true;
} else {
lastStringEmpty = false;
}
});
return result;
}
_isLineAttribute(line) {
return line.trim().startsWith(this._settings.MetapropertyDirective);
}
_isOptionBegin(line) {
const lowCaseLine = line.toLowerCase();
return lowCaseLine.startsWith(this._settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE);
}
_isOptionEnd(line) {
const lowCaseLine = line.toLowerCase();
return lowCaseLine.startsWith(this._settings.OptionsDirectives.END_OPTION_DIRECTIVE);
}
_removeAttributeDirective(line) {
return line.trim().slice(this._settings.MetapropertyDirective.length).trim();
}
isIncludeFound(strings) {
for (const str of strings) {
const match = PresetParser._sRegExpInclude.exec(str);
if (match !== null) {
return true;
}
}
return false;
}
}
// Reg exp extracts file/path.txt from # include: file/path.txt
PresetParser._sRegExpInclude = /^#\$[ ]+?INCLUDE:[ ]+?(?<filePath>\S+$)/;

View File

@ -14,96 +14,25 @@ class PresetsRepoIndexed {
loadIndex() {
return fetch(this._urlRaw + "index.json", {cache: "no-cache"})
.then(res => res.json())
.then(out => this._index = out);
}
removeUncheckedOptions(strings, checkedOptions) {
let resultStrings = [];
let isCurrentOptionExcluded = false;
const lowerCasedCheckedOptions = checkedOptions.map(optionName => optionName.toLowerCase());
strings.forEach(str => {
if (this._isLineAttribute(str)) {
const line = this._removeAttributeDirective(str);
if (this._isOptionBegin(line)) {
const optionNameLowCase = this._getOptionName(line).toLowerCase();
if (!lowerCasedCheckedOptions.includes(optionNameLowCase)) {
isCurrentOptionExcluded = true;
}
} else if (this._isOptionEnd(line)) {
isCurrentOptionExcluded = false;
}
} else if (!isCurrentOptionExcluded) {
resultStrings.push(str);
}
});
resultStrings = this._removeExcessiveEmptyLines(resultStrings);
return resultStrings;
}
_removeExcessiveEmptyLines(strings) {
// removes empty lines if there are two or more in a row leaving just one empty line
const result = [];
let lastStringEmpty = false;
strings.forEach(str => {
if ("" !== str || !lastStringEmpty) {
result.push(str);
}
if ("" === str) {
lastStringEmpty = true;
} else {
lastStringEmpty = false;
}
});
return result;
}
_isLineAttribute(line) {
return line.trim().startsWith(PresetsRepoIndexed._sCliAttributeDirective);
}
_isOptionBegin(line) {
const lowCaseLine = line.toLowerCase();
return lowCaseLine.startsWith(this._index.settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE);
}
_isOptionEnd(line) {
const lowCaseLine = line.toLowerCase();
return lowCaseLine.startsWith(this._index.settings.OptionsDirectives.END_OPTION_DIRECTIVE);
}
_getOptionName(line) {
const directiveRemoved = line.slice(this._index.settings.OptionsDirectives.BEGIN_OPTION_DIRECTIVE.length).trim();
const regExpRemoveChecked = new RegExp(this._escapeRegex(this._index.settings.OptionsDirectives.OPTION_CHECKED +":"), 'gi');
const regExpRemoveUnchecked = new RegExp(this._escapeRegex(this._index.settings.OptionsDirectives.OPTION_UNCHECKED +":"), 'gi');
let optionName = directiveRemoved.replace(regExpRemoveChecked, "");
optionName = optionName.replace(regExpRemoveUnchecked, "").trim();
return optionName;
}
_escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
_removeAttributeDirective(line) {
return line.trim().slice(PresetsRepoIndexed._sCliAttributeDirective.length).trim();
.then(out => {
this._index = out;
this._settings = this._index.settings;
this._PresetParser = new PresetParser(this._index.settings);
});
}
getPresetOnlineLink(preset) {
return this._urlViewOnline + preset.fullPath;
}
removeUncheckedOptions(strings, checkedOptions) {
return this._PresetParser.removeUncheckedOptions(strings, checkedOptions);
}
_parceInclude(strings, includeRowIndexes, promises)
{
for (let i = 0; i < strings.length; i++) {
const match = PresetsRepoIndexed._sRegExpInclude.exec(strings[i]);
const match = PresetParser._sRegExpInclude.exec(strings[i]);
if (match !== null) {
includeRowIndexes.push(i);
@ -131,7 +60,7 @@ class PresetsRepoIndexed {
}
_executeIncludeNested(strings) {
const isIncludeFound = this._isIncludeFound(strings);
const isIncludeFound = this._PresetParser.isIncludeFound(strings);
if (isIncludeFound) {
return this._executeIncludeOnce(strings)
@ -141,18 +70,6 @@ class PresetsRepoIndexed {
}
}
_isIncludeFound(strings) {
for (const str of strings) {
const match = PresetsRepoIndexed._sRegExpInclude.exec(str);
if (match !== null) {
return true;
}
}
return false;
}
loadPreset(preset) {
const promiseMainText = this._loadPresetText(this._urlRaw + preset.fullPath);
@ -160,6 +77,7 @@ class PresetsRepoIndexed {
.then(text => {
let strings = text.split("\n");
strings = strings.map(str => str.trim());
this._PresetParser.readPresetProperties(preset, strings);
return strings;
})
.then(strings => this._executeIncludeNested(strings))
@ -215,9 +133,3 @@ class PresetsRepoIndexed {
});
}
}
PresetsRepoIndexed._sCliCommentDirective = "#";
PresetsRepoIndexed._sCliAttributeDirective = "#$";
// Reg exp extracts file/path.txt from # include: file/path.txt
PresetsRepoIndexed._sRegExpInclude = /^#\$[ ]+?INCLUDE:[ ]+?(?<filePath>\S+$)/;

View File

@ -287,11 +287,25 @@
}
/* hack-fix for multiple-select with small font size. Originally checkboxes are not ligned with the text options when the font size is smaller than 0.7em */
.tab-presets
.ms-drop input[type="radio"], .ms-drop input[type="checkbox"] {
.tab-presets .ms-drop input[type="radio"], .ms-drop input[type="checkbox"] {
margin-top: 0.1rem !important;
}
/* hack-fix for multiple-select. Places "X" button vertically in the middle for the current font. "X" icon clears the current selection. */
.tab-presets .ms-choice>div.icon-close {
padding-top: 3px;
}
/* hack-fix for multiple-select. Prevents "X" button from changing color on hover. "X" icon clears the current selection */
.tab-presets .ms-choice > div.icon-close:hover::before {
color: rgb(136, 136, 136);
}
/* hack-fix for multiple-select. Makes "X" button bigger. "X" icon clears the current selection */
.tab-presets .ms-choice>div.icon-close {
font-size: 16px;
}
.presets_filter_table_wrapper {
display: grid;
grid-gap: 5px;

View File

@ -4,6 +4,7 @@ TABS.presets = {
presetsRepo: null,
cliEngine: null,
pickedPresetList: [],
majorVersion: 1,
};
TABS.presets.initialize = function (callback) {
@ -292,7 +293,9 @@ TABS.presets.tryLoadPresets = function() {
this._divGlobalLoading.toggle(true);
this._domWarningNotOfficialSource.toggle(!this.presetsSourcesDialog.isOfficialActive);
this.presetsRepo.loadIndex().then(() => {
this.presetsRepo.loadIndex()
.then(() => this.checkPresetSourceVersion())
.then(() => {
this.prepareFilterFields();
this._divGlobalLoading.toggle(false);
this._divMainContent.toggle(true);
@ -303,13 +306,35 @@ TABS.presets.tryLoadPresets = function() {
});
};
TABS.presets.checkPresetSourceVersion = function() {
return new Promise((resolve, reject) => {
if (this.majorVersion === this.presetsRepo.index.majorVersion) {
resolve();
} else {
const versionRequired = `${this.majorVersion}.X`;
const versionSource = `${this.presetsRepo.index.majorVersion}.${this.presetsRepo.index.minorVersion}`;
const dialogSettings = {
title: i18n.getMessage("presetsWarningDialogTitle"),
text: i18n.getMessage("presetsVersionMismatch", {"versionRequired": versionRequired, "versionSource":versionSource}),
buttonYesText: i18n.getMessage("yes"),
buttonNoText: i18n.getMessage("no"),
buttonYesCallback: () => resolve(),
buttonNoCallback: () => reject("Prset source version mismatch"),
};
GUI.showYesNoDialog(dialogSettings);
}
});
};
TABS.presets.prepareFilterFields = function() {
this._freezeSearch = true;
this.prepareFilterSelectField(this._selectCategory, this.presetsRepo.index.uniqueValues.category);
this.prepareFilterSelectField(this._selectKeyword, this.presetsRepo.index.uniqueValues.keywords);
this.prepareFilterSelectField(this._selectAuthor, this.presetsRepo.index.uniqueValues.author);
this.prepareFilterSelectField(this._selectFirmwareVersion, this.presetsRepo.index.uniqueValues.firmware_version);
this.prepareFilterSelectField(this._selectStatus, this.presetsRepo.index.settings.PresetStatusEnum);
this.prepareFilterSelectField(this._selectCategory, this.presetsRepo.index.uniqueValues.category, 3);
this.prepareFilterSelectField(this._selectKeyword, this.presetsRepo.index.uniqueValues.keywords, 3);
this.prepareFilterSelectField(this._selectAuthor, this.presetsRepo.index.uniqueValues.author, 1);
this.prepareFilterSelectField(this._selectFirmwareVersion, this.presetsRepo.index.uniqueValues.firmware_version, 2);
this.prepareFilterSelectField(this._selectStatus, this.presetsRepo.index.settings.PresetStatusEnum, 2);
this.preselectFilterFields();
this._inputTextFilter.on('input', () => this.updateSearchResults());
@ -320,8 +345,6 @@ TABS.presets.prepareFilterFields = function() {
};
TABS.presets.preselectFilterFields = function() {
this._selectCategory.multipleSelect('setSelects', ["TUNE", "RC_SMOOTHING", "RC_LINK", "RATES"]);
const currentVersion = FC.CONFIG.flightControllerVersion;
const selectedVersions = [];
@ -334,15 +357,17 @@ TABS.presets.preselectFilterFields = function() {
this._selectFirmwareVersion.multipleSelect('setSelects', selectedVersions);
};
TABS.presets.prepareFilterSelectField = function(domSelectElement, selectOptions) {
TABS.presets.prepareFilterSelectField = function(domSelectElement, selectOptions, minimumCountSelected) {
domSelectElement.multipleSelect("destroy");
domSelectElement.multipleSelect({
data: selectOptions,
placeholder: i18n.getMessage("dropDownAll"),
showClear: true,
minimumCountSelected : minimumCountSelected,
placeholder: i18n.getMessage("dropDownFilterDisabled"),
onClick: () => { this.updateSearchResults(); },
onCheckAll: () => { this.updateSearchResults(); },
onUncheckAll: () => { this.updateSearchResults(); },
formatSelectAll () { return i18n.getMessage("dropDownSelectAll"); },
formatSelectAll() { return i18n.getMessage("dropDownSelectAll"); },
formatAllSelected() { return i18n.getMessage("dropDownAll"); },
});
};
@ -411,6 +436,8 @@ TABS.presets.getFitPresets = function(searchParams) {
}
}
result.sort((a, b) => (a.priority > b.priority) ? -1 : 1);
return result;
};
@ -478,10 +505,10 @@ TABS.presets.isPresetFitSearchFirmwareVersions = function(preset, searchParams)
TABS.presets.isPresetFitSearchString = function(preset, searchParams) {
if (searchParams.searchString)
{
if (searchParams.searchString) {
const allKeywords = preset.keywords.join(" ");
const totalLine = [preset.description, allKeywords, preset.title, preset.author].join("\n").toLowerCase().replace("''", "\"");
const allVersions = preset.firmware_version.join(" ");
const totalLine = [preset.description, allKeywords, preset.title, preset.author, allVersions, preset.category].join("\n").toLowerCase().replace("''", "\"");
const allWords = searchParams.searchString.toLowerCase().replace("''", "\"").split(" ");
for (const word of allWords) {