NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
/* eslint-disable no-undef */ /* eslint-disable no-empty */ // ==UserScript== // @name CPQ Custom Query // @version 0.24 // @description GUI Tool to help get results of multi datatables // @author Obada Kadri // @license MIT // @match *://*.bigmachines.com/commerce/display_company_profile.jsp* // @require https://code.jquery.com/jquery-3.3.1.min.js // @require https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.bundle.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.11.1/typeahead.jquery.min.js // @require https://cdn.datatables.net/v/bs4/dt-1.10.18/b-1.5.4/b-flash-1.5.4/b-html5-1.5.4/datatables.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js // @require https://cdn.jsdelivr.net/npm/alasql@0.4 // @grant GM_addStyle // ==/UserScript== // Constants const IM_MAPPING = { _part_custom_field1: "Inventory Item Id", _part_custom_field2: "Inventory Category", _part_custom_field4: "User Item Type", _part_custom_field8: "Primary Unit of Measure", _part_custom_field25: " Primary UOM Description", _part_custom_field3: "Item Status", _part_custom_field12: "Lifecycle Phase", _part_custom_field5: "Minimum Quantity", _part_custom_field9: "GL ID", _part_custom_field10: "Price Periodicity", _part_custom_field11: "Active Price List", _part_custom_field21: "Price End Date", _part_custom_field13: "Dynamic Formula", _part_custom_field14: "Flexible Price", _part_custom_field15: "Description Override Enabled", _part_custom_field18: "Discount Category", _part_custom_field20: "Discretionary Discount Category", _part_custom_field19: "Restricted Products Category", _part_custom_field22: "Application Enabled", _part_custom_field23: "Renewal Status", _part_custom_field24: "eSource Price List", _part_custom_field26: "Product Class", _part_custom_field27: "Part Type", _part_custom_field16: "FEC TL", _part_custom_field17: "Government Regulation", _part_custom_field28: "SKU Classification", _part_custom_field29: "Professional Service Bill Type", _part_custom_field30: "Term Length(MO)", _part_custom_field278: "Term Length Editable", _part_custom_field279: "Third Party", _part_custom_field280: "Third Party Pass Through Code", }; // Defaults and presets let datatables = []; let isTokensPopulated = false; let isModalShown = false; let selectedTables = []; let resultsTable; // Don't run on login page let notLoginPage = !document.querySelector("#login-form .login-links"); if (notLoginPage) { $("head").append("<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\">"); $("head").append("<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/typeahead.js-bootstrap-css/1.2.1/typeaheadjs.min.css\">"); $("head").append("<link rel=\"stylesheet\" href=\"https://cdn.datatables.net/v/bs4/dt-1.10.18/b-1.5.4/b-flash-1.5.4/b-html5-1.5.4/datatables.min.css\">"); /** * Appends the main button and modal to the CPQ body */ let showModal = function () { "use strict"; let modalHtml = ` <!-- Modal --> <div class="modal fade" id="customQueryModal" tabindex="-1" role="dialog" aria-labelledby="customQueryModalLabel" style="padding: 0;"> <div class="modal-dialog modal-xl" role="document"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title" id="exampleModalLabel">CPQ Custom Query</h3> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <div id="data-progress" class="progress"></div> <h5 id="title"></h5> <div id="instructions"></div> <div id="select-tables" class="step d-none"> <input type="text" class="token-input form-control" autocomplete="off" placeholder="Type datatable name and select from the valid suggestions" id="datatables-token-field" /> </div> <div id="select-filters" class="step d-none"> <table class="table table-striped" id="table-filters"> <thead> <tr> <th>Column</th> <th>Comparator</th> <th>Value</th> </tr> </thead> <tbody> </tbody> </table> </div> <div id="select-relationships" class="step d-none"> <div id="relationship-tables"></div> </div> <div id="select-columns" class="step d-none"> <table id="table-select-columns" class="table table-striped"> <thead></thead> <tbody></tbody> </table> </div> <div id="display-results" class="step d-none"> <div class="mx-auto d-none" id="calculating-results" style="width: 200px;"> <div align="center">Calculating Results...</div> <img src="/img/processingBar.gif" alt="Calculating Results..." /> </div> <div id="results-table" class="d-none"> <div id="join-type"> <label for="join-type">Join Type</label><br /> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="join-type" id="join-type-1" value="INNER JOIN" checked="true"> <label class="form-check-label" for="join-type-1">INNER JOIN</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="join-type" id="join-type-2" value="LEFT JOIN"> <label class="form-check-label" for="join-type-2">LEFT JOIN</label> </div> </div> <table id="table-results" class="table table-striped"></table> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <button type="button" id="btn-reset" class="btn btn-light btn-step d-none">Start Over</button> <button type="button" id="btn-next" class="btn btn-primary btn-step">Next</button> </div> </div> </div> </div> `; let okToolBar = document.querySelector("ul.ok-tool-bar"); if (!okToolBar) { okToolBar = document.createElement("ul"); okToolBar.className = "ok-tool-bar"; $("body").append(okToolBar); } let showCustomQryBtn = document.createElement("li"); let text = document.createTextNode("Custom Query"); showCustomQryBtn.appendChild(text); showCustomQryBtn.className = "custom-query-btn"; showCustomQryBtn.dataset.toggle = "modal"; showCustomQryBtn.dataset.target = "#customQueryModal"; //--- Add nodes to page okToolBar.append(showCustomQryBtn); $("body").append(modalHtml); }; showModal(); let gotToStep = function (stepIdx, doReset) { if (doReset) { resetAll(); } $(".step").addClass("d-none"); $("#" + steps[stepIdx].id).removeClass("d-none"); $(".btn-step").removeClass("d-none"); $("#btn-next").data("stepIdx", stepIdx + 1); if (stepIdx === 0) { $("#btn-reset").addClass("d-none"); } if (stepIdx === steps.length - 1) { $("#btn-next").addClass("d-none"); } // Set title and instructions $("h5#title").html(steps[stepIdx].title); $("#instructions").html(steps[stepIdx].instructions); steps[stepIdx].renderFn(stepIdx); }; let getApiTableName = function (tableName) { let result = tableName; if (tableName === "_parts") { result = "parts"; } else if (tableName.length) { result = "custom" + tableName[0].toUpperCase() + tableName.slice(1); } return result; }; let getTableMetaData = function (tableName) { let dMetaData = new $.Deferred(); let metaData = { columns: undefined, rowCount: undefined, }; // https://cpqav.bigmachines.com/rest/v14/parts?limit=1&totalResults=true let apiTableName = getApiTableName(tableName); $.get("/rest/v14/metadata-catalog/" + apiTableName, function (data) { metaData.columns = Object.keys(data.definitions[apiTableName].properties) .filter(c => c !== "id") .sort((a, b) => a.replace(/\d+/g, m => "0".substring(m.length - 1) + m).localeCompare(b.replace(/\d+/g, m => "0".substring(m.length - 1) + m))); if (Object.values(metaData).every(v => v !== undefined)) { dMetaData.resolve(metaData); } }); $.get(`/rest/v14/${apiTableName}?limit=1&totalResults=true`, function (data) { metaData.rowCount = data.totalResults; if (Object.values(metaData).every(v => v !== undefined)) { dMetaData.resolve(metaData); } }); return dMetaData.promise(); }; let substringMatcher = function(strs) { return function findMatches(q, cb) { let matches; // an array that will be populated with substring matches matches = []; // regex used to determine if a string contains the substring `q` try { substrRegex = new RegExp(q, "i"); // iterate through the pool of strings and for any string that // contains the substring `q`, add it to the `matches` array $.each(strs, function(i, str) { if (substrRegex.test(str)) { matches.push(str); } }); cb(matches); } catch (error) { console.warn(`Invalid table name "${q}"`); } }; }; let renderSelectTables = function () { if (!isTokensPopulated && datatables.length) { $("#btn-next").prop("disabled", selectedTables.length === 0); $("#datatables-token-field").typeahead({ hint: false, highlight: true, minLength: 2 }, { limit: 100, name: "datatables", source: substringMatcher(datatables) }); $("#datatables-token-field").bind("typeahead:select", function(ev, suggestion) { let newTable = {}; newTable.name = suggestion; if (!selectedTables.find(t => t.name === newTable.name)) { let maxId = selectedTables.length ? Math.max(...selectedTables.map(t => t.id)) : 0; newTable.id = maxId + 1; $(ev.target).prop("disabled", true); $(ev.target).focus(); getTableMetaData(newTable.name).then(function (metaData) { $(ev.target).prop("disabled", false); $(ev.target).typeahead("val", ""); // Add part number column on the top const partNumColumns = metaData.columns.filter(c => isPartNumCol(c)); // Append the sorted list of the columns newTable.columns = [ ...partNumColumns, ...metaData.columns .filter(c => !isPartNumCol(c)) .sort((a, b) => { let colA = IM_MAPPING[a] || a; let colB = IM_MAPPING[b] || b; return colA.localeCompare(colB); }) ]; newTable.rowCount = metaData.rowCount; selectedTables.push(newTable); console.info(selectedTables); $("#btn-next").prop("disabled", selectedTables.length === 0); // Start downloading table data getTableData(newTable).then(function (data) { newTable.data = data; }); }); } else { $(ev.target).typeahead("val", ""); } }); isTokensPopulated = true; } }; let isPartNumCol = function (colName) { return colName.toLowerCase().indexOf("part") >= 0 && colName.toLowerCase().indexOf("number") >= 0; }; let renderSelectRelationships = function (stepIdx) { if (selectedTables.length > 1) { let isAllRelationsSelected = function () { return selectedTables.every(function (t, i) { let left = $(`input[type=radio][name=left-${t.name}-${i}]`); let right = $(`input[type=radio][name=right-${t.name}-${i}]`); let selectedLeft = $(`input[type=radio][name=left-${t.name}-${i}]:checked`); let selectedRight = $(`input[type=radio][name=right-${t.name}-${i}]:checked`); return (left.length == 0 || selectedLeft.length) && (right.length == 0 || selectedRight.length); }); }; $("#btn-next").prop("disabled", !isAllRelationsSelected()); $("#relationship-tables").empty(); selectedTables.forEach(function (rightTable, index) { if (index > 0) { let leftTable = selectedTables[index - 1]; let relationshipTable = $(`<table class="table"><tr><th>${leftTable.name}</th><th>${rightTable.name}</th></tr></table>`); let contentRow = $("<tr>"); let leftContent = $("<td>"); leftTable.columns.forEach(col => { let colClass = isPartNumCol(col) ? "part-num" : ""; leftContent.append(`<div class="form-check ${colClass}"><input class="form-check-input" type="radio" name="left-${leftTable.name}-${leftTable.id}" value="${col}" id="left-${leftTable.name}-${leftTable.id}-${col}" ${col === leftTable.leftRelation ? "checked" : ""}><label class="form-check-label" for="left-${leftTable.name}-${leftTable.id}-${col}">${IM_MAPPING[col] ? IM_MAPPING[col] : col}</label></div>`); }); contentRow.append(leftContent); let rightContent = $("<td>"); rightTable.columns.forEach(col => { let colClass = isPartNumCol(col) ? "part-num" : ""; rightContent.append(`<div class="form-check ${colClass}"><input class="form-check-input" type="radio" name="right-${rightTable.name}-${rightTable.id}" value="${col}" id="right-${rightTable.name}-${rightTable.id}-${col}" ${col === rightTable.rightRelation ? "checked" : ""}><label class="form-check-label" for="right-${rightTable.name}-${rightTable.id}-${col}">${IM_MAPPING[col] ? IM_MAPPING[col] : col}</label></div>`); }); contentRow.append(rightContent); relationshipTable.append(contentRow); $("#relationship-tables").append(relationshipTable); $(`input[type=radio][name=left-${leftTable.name}-${leftTable.id}]`).on("change", function (e) { selectedTables[index - 1].leftRelation = $(e.target).val(); console.info(selectedTables); $("#btn-next").prop("disabled", !isAllRelationsSelected()); }); $(`input[type=radio][name=right-${rightTable.name}-${rightTable.id}]`).on("change", function (e) { selectedTables[index].rightRelation = $(e.target).val(); console.info(selectedTables); $("#btn-next").prop("disabled", !isAllRelationsSelected()); }); } }); } else { gotToStep(stepIdx + 1); } }; let renderSelectColumns = function () { if (selectedTables.length) { let tableNames = selectedTables.map(function (t) { return t.name; }); let thead = $("#table-select-columns thead"); thead.empty(); let theadRow = $("<tr>"); tableNames.forEach(function (tableName) { theadRow.append(`<th>${tableName}</th>`); }); thead.append(theadRow); let numOfRows = Math.max(...selectedTables.map(function (t) { return t.columns.length; })); let tbody = $("#table-select-columns tbody"); tbody.empty(); for (let i = 0; i < numOfRows; i++) { let rowData = selectedTables.map(function (t) { return { tableId: t.id, tableName: t.name, column: t.columns[i] || "", isChecked: t.displayColumns && t.displayColumns.indexOf(t.columns[i]) >= 0 }; }); let tbodyRow = $("<tr>"); rowData.forEach(function (cellData) { if (cellData.column) { let colClass = isPartNumCol(cellData.column) ? "part-num" : ""; tbodyRow.append(`<td class="${colClass}"> <div class="form-check"> <input class="form-check-input checkbox-column-display" type="checkbox" data-table-name="${cellData.tableName}" data-table-id="${cellData.tableId}" value="${cellData.column}" id="${cellData.tableName}-${cellData.tableId}-${cellData.column}" ${cellData.isChecked ? "checked" : ""}> <label class="form-check-label" for="${cellData.tableName}-${cellData.tableId}-${cellData.column}"> ${IM_MAPPING[cellData.column] ? IM_MAPPING[cellData.column] : cellData.column} </label> </div> </td>`); } else { tbodyRow.append("<td></td>"); } }); tbody.append(tbodyRow); $("input[type=checkbox].checkbox-column-display").change(function (e) { let datatable = selectedTables.find(t => t.id === $(e.target).data("tableId")); if (this.checked && (!datatable.displayColumns || datatable.displayColumns.indexOf($(e.target).val()) < 0)) { datatable.displayColumns = datatable.displayColumns || []; datatable.displayColumns.push($(e.target).val()); console.info(selectedTables); } else if (!this.checked && datatable.displayColumns && datatable.displayColumns.indexOf($(e.target).val()) >= 0) { datatable.displayColumns = datatable.displayColumns.filter(dc => dc !== $(e.target).val()); console.info(selectedTables); } }); } } else { gotToStep(0); } }; let removeDatatable = function (tableId) { let index = selectedTables.findIndex(t => t.id === tableId); selectedTables.splice(index, 1); console.info(selectedTables); $(`#data-progress #table-${tableId}-progress`).remove(); updateProgress(); }; let updateProgress = function () { $("#data-progress").removeClass("d-none"); const progressClass = ["", "bg-success", "bg-info", "bg-warning", "bg-danger"]; if (selectedTables.every(t => t.totalResults)) { let totalResults = selectedTables.reduceRight((acc, cur) => acc + cur.totalResults, 0); selectedTables.forEach(function (table, index) { let existingProgressSection = $(`#data-progress #table-${table.id}-progress`); let newProgressSection = $(`<div id="table-${table.id}-progress" class="progress-bar ${progressClass[index % progressClass.length]}" data-table-id="${table.id}" title="Click to remove ${table.name} table"></div>`); progressSection = existingProgressSection.length ? existingProgressSection : newProgressSection; progressSection.css("width", `${100 * table.collectedResults / totalResults}%`); progressSection.text(`${table.name} ${table.collectedResults} / ${table.totalResults}`); // Only for new progress sections if (!existingProgressSection.length) { progressSection.on("click", function (e) { let datatableId = Number($(e.target).data("tableId")); removeDatatable(datatableId); }); progressSection.addClass(progressClass[index]); $("#data-progress").append(progressSection); } }); } }; let getTableData = function (table, offset, allData, deferred) { let dTableData = Object.is(deferred, undefined) ? new $.Deferred() : deferred; offset = offset || 0; let query = ""; if (table.filters) { // {$and:[{"part_number":{$like:"B8%25"}},{"part_type":{$eq:"Phased"}}]} let filterCols = Object.keys(table.filters); let queryArr = filterCols.map(function (filterCol) { let filter = table.filters[filterCol]; return `{"${filterCol}":{$${filter.comparator.toLocaleLowerCase()}:"${filter.value.replace(/%/g, "%25")}"}}`; }); query = `{$and:[${queryArr.join(",")}]}`; } let apiTableName = getApiTableName(table.name); let url = `/rest/v14/${apiTableName}?q=${query}&totalResults=true&offset=${offset}`; allData = allData || []; $.get(url, function (data) { // Update progress let selectedTable = selectedTables.find(t => t.id === table.id); selectedTable.collectedResults = Math.min(offset + 1000, data.totalResults); selectedTable.totalResults = data.totalResults; updateProgress(); if (data.hasMore) { let nextOffset = offset + 1000; allData = allData.concat(data.items); getTableData(table, nextOffset, allData, dTableData); } else { console.log(selectedTable); dTableData.resolve(allData.concat(data.items)); } }) .fail(function () { dTableData.reject(); }); return dTableData.promise(); }; let constructResults = function (joinType) { let dConstructedResults = new $.Deferred(); // let entryIdx = 0; let results = []; if (selectedTables.length && selectedTables.every(t => t.data)) { // Result will be basically a copy of the first table data, and we will fill in the blanks from the other tables // let parts = selectedTables[0].data; // let hwPartAttributes = selectedTables[1].data; // let qResult = alasql("SELECT * FROM ? AS parts JOIN ? AS hwPartAttributes ON parts.partNumber = hwPartAttributes.part_number", [parts, hwPartAttributes]); // console.log(qResult); const minResultsCount = Math.min(...selectedTables.map(t => t.totalResults)); if (minResultsCount > 0) { selectedTables.forEach(function (table, index) { if (index === 0) { results = table.data; } else { let leftTable = selectedTables[index - 1]; let tableData = table.data; results = alasql(`SELECT * FROM ? AS l ${joinType} ? AS r ON l.${leftTable.leftRelation} = r.${table.rightRelation}`, [results, tableData]); console.log(`SELECT * FROM ? AS l ${joinType} ? AS r ON l.${leftTable.leftRelation} = r.${table.rightRelation}`, results); } }); } dConstructedResults.resolve(results); } return dConstructedResults.promise(); }; let getResults = function (joinType, dResults) { dResults = dResults || new $.Deferred(); if (selectedTables.every(t => t.data)) { //$("#data-progress").addClass("d-none"); $("#calculating-results").removeClass("d-none"); console.info("Data pull completed: ", selectedTables); setTimeout(function () { constructResults(joinType).then(results => { dResults.resolve(results); }); }, 100); } else { setTimeout(() => getResults(joinType, dResults), 500); } return dResults.promise(); }; let renderResultsTable = function (results) { console.info("Results: ", results); let displayColumns = []; selectedTables.forEach(t => { if (t.displayColumns) { t.displayColumns.forEach(c => { //let columnName = c + (t.id > 1 ? t.id : ""); return displayColumns.push({ title: IM_MAPPING[c] ? IM_MAPPING[c] : c, columnName: c }); }); } }); let displayResults = results.map(r => displayColumns.map(c => r[c.columnName] != null ? r[c.columnName] : "")); if (resultsTable) { resultsTable.destroy(); resultsTable = null; $("#table-results").empty(); } $("#results-table").removeClass("d-none"); resultsTable = $("#table-results").DataTable({ dom: "Bfrtip", columns: displayColumns, data: displayResults, buttons: [ "csv" ], language: { emptyTable: "No Results. Please check criteria and try again" }, fnInitComplete : function() { $("#table-results").append("<tfoot></tfoot>"); $("#table-results thead tr").clone().appendTo($("#table-results tfoot")); $("#table-results tfoot th").each( function (i) { var title = $(this).text(); $(this).html(` <div class="input-group mb-3"> <div class="input-group-prepend"> <select class="col-search-comparator form-control" data-index="${i}"> <option value="LIKE">LIKE</option> <option value="NOT LIKE">NOT LIKE</option> <option value="EQUALS">EQUALS</option> <option value="NOT EQUALS">NOT EQUALS</option> <option value="IS NULL">IS NULL</option> <option value="IS NOT NULL">IS NOT NULL</option> </select> </div> <input type="text" class="form-control col-search-input" placeholder="Search ${title}" data-index="${i}" /> </div> `); } ); $("#table-results tfoot tr").appendTo($("#table-results thead")); } }); $("#calculating-results").addClass("d-none"); $( resultsTable.table().container() ).on( "keyup change", "input.col-search-input, select.col-search-comparator", function () { $(this).closest(".input-group").find(".col-search-input").removeAttr("disabled"); let colIndex = $(this).data("index"); let comparator = $(this).closest(".input-group").find(".col-search-comparator").val(); let value = $(this).closest(".input-group").find(".col-search-input").val(); let isSmart = true; let isRegex = false; if (comparator === "NOT LIKE") { isSmart = false; isRegex = true; value = `^((?!${value}).)*$`; } else if (comparator === "EQUALS") { isSmart = false; isRegex = true; value = `^${value}$`; } else if (comparator === "NOT EQUALS") { isSmart = false; isRegex = true; value = `^(?!${value}$).*$`; } else if (comparator === "IS NULL") { $(this).closest(".input-group").find(".col-search-input").prop("disabled", true); isSmart = false; isRegex = true; value = "^$"; } else if (comparator === "IS NOT NULL") { $(this).closest(".input-group").find(".col-search-input").prop("disabled", true); isSmart = false; isRegex = true; value = "^(?!$).*$"; } resultsTable .column(colIndex) .search(value, isRegex, isSmart) .draw(); } ); }; let renderDisplayResults = function () { if (selectedTables.length) { let joinType = $("input[name='join-type']:checked").val(); $("#results-table").addClass("d-none"); getResults(joinType).then(renderResultsTable); if (selectedTables.length > 1) { $("#join-type").removeClass("d-none"); $("input[name='join-type']").change(function() { joinType = $("input[name='join-type']:checked").val(); $("#calculating-results").removeClass("d-none"); $("#results-table").addClass("d-none"); getResults(joinType).then(renderResultsTable); }); } else { $("#join-type").addClass("d-none"); } } else { gotToStep(0); } }; let resetAll = function () { try { $("#table-filters tbody").empty(); $("#relationship-tables").empty(); $("#table-select-columns thead").empty(); $("#table-select-columns tbody").empty(); if (resultsTable) { resultsTable.destroy(); resultsTable = null; $("#table-results").empty(); } $("input[name='join-type'][value='INNER JOIN']").prop('checked', true); } catch (error) {} }; $("#customQueryModal").on("shown.bs.modal", function () { isModalShown = true; gotToStep(0); }); $("#customQueryModal").on("hidden.bs.modal", function () { isModalShown = false; resetAll(); }); $(".btn-step").on("click", function (e) { let stepIdx = $(this).data("stepIdx") || 0; gotToStep(stepIdx, e.target.id === "btn-reset"); }); // Get all table names let iframe = document.createElement("iframe"); iframe.src = "/admin/help/functions/select_data_help.jsp"; iframe.style.display = "none"; document.body.appendChild(iframe); iframe.onload = function () { let iframeDocument = iframe.contentDocument || iframe.contentWindow.document; datatables = Array.from(iframeDocument.querySelectorAll("#table-list ul li a")).map(function (a) { return a.innerText; // return { // name: a.innerText // }; }); if (isModalShown) { gotToStep(0); } }; // steps array is used to show/hide current step, should match the step id in the DOM let steps = [{ id: "select-tables", renderFn: renderSelectTables, title: "Select the tables", instructions: "Select the tables that need to be joined. <strong>The order of selection matters</strong> as you can only filter the results of the first table. Later you can set the relationships of the rest of the tables will determine how they will be INNER-JOINED<br /><strong>Note:</strong> Item Master table is <strong>_parts</strong>" }, { id: "select-relationships", renderFn: renderSelectRelationships, title: "Select Relationships", instructions: "Select how each of the tables selected in the first step is related to the one that follows it" }, { id: "select-columns", renderFn: renderSelectColumns, title: "Select display columns", instructions: "Select the columns that will be displayed in the results for further analysis. These will be the columns of the final combined table" }, { id: "display-results", renderFn: renderDisplayResults, title: "Results", instructions: "Below are the results of the combined tables based on the filters and relationships provided. You can filter the data using the search field, and you can download the results in CSV format." } ]; // Styling Stuff let bm_css_src = ` ul.ok-tool-bar { position: fixed; bottom: 0; right: 10%; margin: 0; } ul.ok-tool-bar li { display: inline-block; padding: 3px 6px; margin: 0 3px; font-size: 11px; text-align: center; white-space: nowrap; vertical-align: middle; -ms-touch-action: manipulation; cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; background-image: none; border: 1px solid transparent; border-radius: 5px 5px 0 0; } li.custom-query-btn { color: #fff; background-color: #22614E; border-color: #22614E; } h3.header { font-size: 1.5em; margin: .5em 0 1em 0; } .ct-label.ct-horizontal { transform: rotate(-90deg); padding: 5px 0; white-space: nowrap; width: 1em !important; margin: 5px auto; } #customQueryModal .modal-xl { margin: 0; width: 100%; max-width:100%; } body { margin: auto; } div.token-input-dropdown { z-index: 99999 !important; } .part-num { font-weight: bold; } #instructions { color: #6c757d; font-size: 12px; margin-bottom: 1em; } #data-progress { height: 3rem; } #data-progress .progress-bar { cursor: pointer; opacity: 0.7; } #data-progress .progress-bar:hover { opacity: 1; background-color: red !important; } table.dataTable thead tr:not(:first-child) th:before, table.dataTable thead tr:not(:first-child) th:after { display: none; } .dataTables_wrapper { max-width: 100%; overflow-x: auto; } table.dataTable th { min-width: 250px; } .tt-selectable { cursor: pointer; } `; // eslint-disable-next-line no-undef GM_addStyle(bm_css_src); }