Add operation category display in popovers
- Add 'Show operation category in popover' option (enabled by default) - Display category names (excluding Favourites) in operation popovers - Make category names clickable to open the category in operations list - Clicking a category link clears search and expands the category - Add UI test for category popover functionality - Addresses issue gchq/CyberChef#1654
This commit is contained in:
parent
002ed911b1
commit
f15ad10b94
@ -56,7 +56,8 @@ class HTMLOperation {
|
|||||||
|
|
||||||
if (this.description) {
|
if (this.description) {
|
||||||
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";
|
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";
|
||||||
const content = Utils.escapeHtml(this.description + infoLink);
|
const categoryInfo = this.getCategoryInfo();
|
||||||
|
const content = Utils.escapeHtml(this.description + infoLink + categoryInfo);
|
||||||
|
|
||||||
html += ` data-container='body' data-toggle='popover' data-placement='right'
|
html += ` data-container='body' data-toggle='popover' data-placement='right'
|
||||||
data-content="${content}" data-html='true' data-trigger='hover'
|
data-content="${content}" data-html='true' data-trigger='hover'
|
||||||
@ -75,6 +76,39 @@ class HTMLOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the category information for this operation as an HTML string.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getCategoryInfo() {
|
||||||
|
if (!this.app.options.showOpCategories) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all categories this operation belongs to, excluding Favourites
|
||||||
|
const categories = [];
|
||||||
|
for (let i = 0; i < this.app.categories.length; i++) {
|
||||||
|
const cat = this.app.categories[i];
|
||||||
|
if (cat.name !== "Favourites" && cat.ops.includes(this.name)) {
|
||||||
|
categories.push(cat.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the category links
|
||||||
|
const categoryLinks = categories.map(catName => {
|
||||||
|
const catId = "cat" + catName.replace(/[\s/\-:_]/g, "");
|
||||||
|
return `<a class="op-category-link" data-category="${catId}">${catName}</a>`;
|
||||||
|
}).join(", ");
|
||||||
|
|
||||||
|
return `<hr>Category: ${categoryLinks}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the operation in HTML as a full operation with ingredients.
|
* Renders the operation in HTML as a full operation with ingredients.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -232,6 +232,8 @@ class Manager {
|
|||||||
this.addDynamicListener(".option-item input[type=checkbox]#wordWrap", "change", this.options.setWordWrap, this.options);
|
this.addDynamicListener(".option-item input[type=checkbox]#wordWrap", "change", this.options.setWordWrap, this.options);
|
||||||
this.addDynamicListener(".option-item input[type=checkbox]#useMetaKey", "change", this.bindings.updateKeybList, this.bindings);
|
this.addDynamicListener(".option-item input[type=checkbox]#useMetaKey", "change", this.bindings.updateKeybList, this.bindings);
|
||||||
this.addDynamicListener(".option-item input[type=checkbox]#showCatCount", "change", this.ops.setCatCount, this.ops);
|
this.addDynamicListener(".option-item input[type=checkbox]#showCatCount", "change", this.ops.setCatCount, this.ops);
|
||||||
|
this.addDynamicListener(".option-item input[type=checkbox]#showOpCategories", "change", this.ops.toggleOpCategories, this.ops);
|
||||||
|
this.addDynamicListener(".op-category-link", "click", this.ops.categoryLinkClick, this.ops);
|
||||||
this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options);
|
this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options);
|
||||||
this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options);
|
this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options);
|
||||||
this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options);
|
this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options);
|
||||||
|
|||||||
@ -529,6 +529,13 @@
|
|||||||
Show the number of operations in each category
|
Show the number of operations in each category
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox option-item">
|
||||||
|
<label for="showOpCategories">
|
||||||
|
<input type="checkbox" option="showOpCategories" id="showOpCategories" checked>
|
||||||
|
Show operation category in popover
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>
|
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>
|
||||||
|
|||||||
@ -53,6 +53,7 @@ function main() {
|
|||||||
imagePreview: true,
|
imagePreview: true,
|
||||||
syncTabs: true,
|
syncTabs: true,
|
||||||
showCatCount: false,
|
showCatCount: false,
|
||||||
|
showOpCategories: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
document.removeEventListener("DOMContentLoaded", main, false);
|
document.removeEventListener("DOMContentLoaded", main, false);
|
||||||
|
|||||||
@ -326,6 +326,68 @@ class OperationsWaiter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for toggling the display of operation categories in popovers.
|
||||||
|
* Repopulates the operations list to show/hide category information.
|
||||||
|
*/
|
||||||
|
toggleOpCategories() {
|
||||||
|
// Repopulate operations list to apply the option change
|
||||||
|
this.app.populateOperationsList();
|
||||||
|
this.manager.recipe.initialiseOperationDragNDrop();
|
||||||
|
|
||||||
|
// Refresh search results if search is active
|
||||||
|
const searchInput = document.getElementById("search");
|
||||||
|
if (searchInput && searchInput.value) {
|
||||||
|
this.searchOperations({target: searchInput});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for clicking a category link in an operation popover.
|
||||||
|
* Clears the search and opens the clicked category.
|
||||||
|
*
|
||||||
|
* @param {event} e
|
||||||
|
*/
|
||||||
|
categoryLinkClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const categoryId = e.target.dataset.category;
|
||||||
|
if (!categoryId) return;
|
||||||
|
|
||||||
|
// Hide all popovers
|
||||||
|
$("[data-toggle=popover]").popover("hide");
|
||||||
|
|
||||||
|
// Clear search
|
||||||
|
const searchInput = document.getElementById("search");
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear search results
|
||||||
|
const searchResults = document.getElementById("search-results");
|
||||||
|
if (searchResults) {
|
||||||
|
while (searchResults.firstChild) {
|
||||||
|
try {
|
||||||
|
$(searchResults.firstChild).popover("dispose");
|
||||||
|
} catch (err) {}
|
||||||
|
searchResults.removeChild(searchResults.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all categories and open the target one
|
||||||
|
$("#categories .collapse").collapse("hide");
|
||||||
|
$(`#${categoryId}`).collapse("show");
|
||||||
|
|
||||||
|
// Scroll the category into view
|
||||||
|
const categoryElement = document.getElementById(categoryId);
|
||||||
|
if (categoryElement) {
|
||||||
|
categoryElement.scrollIntoView({behavior: "smooth", block: "nearest"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OperationsWaiter;
|
export default OperationsWaiter;
|
||||||
|
|||||||
@ -252,6 +252,79 @@ module.exports = {
|
|||||||
.waitForElementVisible("//ul[@id='search-results']//b[text()='MD5']", 1000);
|
.waitForElementVisible("//ul[@id='search-results']//b[text()='MD5']", 1000);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"Operation category in popover": browser => {
|
||||||
|
const op = "//ul[@id='search-results']//li[contains(@class, 'operation') and contains(., 'MD5')]";
|
||||||
|
|
||||||
|
// Search for an operation
|
||||||
|
browser
|
||||||
|
.useCss()
|
||||||
|
.clearValue("#search")
|
||||||
|
.setValue("#search", "md5")
|
||||||
|
.useXpath()
|
||||||
|
.waitForElementVisible(op, 1000);
|
||||||
|
|
||||||
|
// Hover over the operation to show popover
|
||||||
|
browser
|
||||||
|
.moveToElement(op, 10, 10)
|
||||||
|
.useCss()
|
||||||
|
.waitForElementVisible(".popover-body", 1000);
|
||||||
|
|
||||||
|
// Assert that Category line appears in popover
|
||||||
|
browser
|
||||||
|
.expect.element(".popover-body").text.to.contain("Category:");
|
||||||
|
|
||||||
|
// Click the category link
|
||||||
|
browser
|
||||||
|
.click(".popover-body .op-category-link");
|
||||||
|
|
||||||
|
// Assert that search is cleared
|
||||||
|
browser
|
||||||
|
.useCss()
|
||||||
|
.expect.element("#search").value.to.equal("");
|
||||||
|
|
||||||
|
// Assert that the Hashing category is now visible
|
||||||
|
browser
|
||||||
|
.expect.element("#catHashing").to.be.visible;
|
||||||
|
|
||||||
|
// Toggle the option off
|
||||||
|
browser
|
||||||
|
.click("#options");
|
||||||
|
|
||||||
|
browser
|
||||||
|
.waitForElementVisible("#options-modal", 1000)
|
||||||
|
.click("#showOpCategories")
|
||||||
|
.pause(500)
|
||||||
|
.click("#options-modal .modal-footer .btn-secondary[data-dismiss='modal']")
|
||||||
|
.waitForElementNotVisible("#options-modal", 1000);
|
||||||
|
|
||||||
|
// Search again and verify category line doesn't appear
|
||||||
|
browser
|
||||||
|
.clearValue("#search")
|
||||||
|
.setValue("#search", "md5")
|
||||||
|
.useXpath()
|
||||||
|
.waitForElementVisible(op, 1000)
|
||||||
|
.moveToElement(op, 10, 10)
|
||||||
|
.useCss()
|
||||||
|
.waitForElementVisible(".popover-body", 1000);
|
||||||
|
|
||||||
|
// Assert that Category line does not appear
|
||||||
|
browser
|
||||||
|
.expect.element(".popover-body").text.to.not.contain("Category:");
|
||||||
|
|
||||||
|
// Reset option back to enabled
|
||||||
|
browser
|
||||||
|
.click("#options")
|
||||||
|
.waitForElementVisible("#options-modal", 1000)
|
||||||
|
.click("#showOpCategories")
|
||||||
|
.pause(500)
|
||||||
|
.click("#options-modal .modal-footer .btn-secondary[data-dismiss='modal']")
|
||||||
|
.waitForElementNotVisible("#options-modal", 1000);
|
||||||
|
|
||||||
|
// Clear search
|
||||||
|
browser
|
||||||
|
.clearValue("#search");
|
||||||
|
},
|
||||||
|
|
||||||
"Alert bar": browser => {
|
"Alert bar": browser => {
|
||||||
// Bake nothing to create an empty output which can be copied
|
// Bake nothing to create an empty output which can be copied
|
||||||
utils.clear(browser);
|
utils.clear(browser);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user