NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Gitlab review extension // @version 0.7.0 // @description File approving for gitlab! // @author V. Vasin // @include /^https?:\/\/[^/]*gitlab.[^/]+\// // @run-at document-body // @license MIT // @copyright 2019, ds3v (https://openuserjs.org//users/ds3v) // @icon https://about.gitlab.com/ico/favicon-192x192.png // @updateURL https://openuserjs.org/meta/ds3v/Gitlab_review_extension.meta.js // @require https://www.gstatic.com/firebasejs/3.6.2/firebase.js // @require https://raw.githubusercontent.com/emn178/js-sha256/730f41e84b76d3525b5a9b505cea549ab0c2a287/build/sha256.min.js // ==/UserScript== (function(window) { 'use strict'; const STYLES =` .files .diff-file.approved .approve-btn { background : #afa; color: black; } .files.hide-approved .diff-file.approved, .files.hide-filtered .diff-file.filtered-file, .files.approve-script-applied.hide-approved .diff-file.approved, .files.approve-script-applied.hide-filtered .diff-file.filtered-file, .files.approve-script-applied .diff-file.filtered-file .diff-content, .files .diff-file.filtered-file .diff-content, .files .diff-file.filtered-file .approve-btn, .files .diff-file.filtered-file .bottom-approve-btn, .files .diff-file.approved .bottom-approve-btn, .files .diff-file.approved .diff-content { display: none; } .files .diff-file .bottom-approve-btn { margin-top: -75px; margin-left: calc(100% - 45px); opacity: 0.2; background: #afa; color: #000; border: 1px solid #555; box-shadow: 0 0 3px white; } .files .diff-file .bottom-approve-btn:hover { opacity: 0.9 } .files .diff-file .file-actions .btn.unhide-btn { display: none; } .files .diff-file.filtered-file .file-actions .btn.unhide-btn { display: inline-block; } .files .diff-file { position:relative; } .files.approve-script-applied .diff-file { display: block; } #utilHolder #approvedCnt { color: green; } #utilHolder #filteredCnt { color: blue; } #utilHolder #filteredCnt:after, #utilHolder #filteredCnt:before { content : '/'; color: gray; display: inline-block; width: 30px; text-align: center; } #utilHolder { position: fixed; top: 3px; left: calc(50% - 210px); z-index: 9999; background: #fff; border: 1px solid #eee; width: 420px; border-radius: 3px; } #utilHolder #progressCnt:before { content : 'Review progress: ' } #utilHolder #progressCnt { margin : 0 15px 0 0; color : red; font-weight: bold; } #utilHolder #progressCnt.half { color: #aa4500; } #utilHolder #progressCnt.done { color : green; } #utilHolder #setting-editor-button { float: left; margin: 5px 10px; font-size: 20px; cursor: pointer; } .hideFiltered, .hideApproved { margin-right:15px; color: black; cursor: pointer; } .hideApproved:before, .hideFiltered:before { margin-right: 5px; } .hideApproved.fa-eye-slash, .hideFiltered.fa-eye-slash { color: gray; } .settings-editor { position: fixed; left: calc(50% - 210px); top : 75px; z-index: 9999; background: #fff; border: 1px solid #eee; width: 420px; border-radius: 3px; padding : 15px; box-shadow: 0 0 5px black; } .settings-editor label { margin : 5px 0; display: block; } .settings-editor .repo-type { display: block; } .settings-editor .repo-type::after { margin : 5px; content: "Use public Firebase" } .settings-editor .buttons { text-align: center; } .settings-editor .buttons .btn { width: 100px; margin : 5px 10px; } .settings-editor .inner-settings { padding-left:20px; } `; let $; function scrollTo(element) { if (localStorage.noscroll === "true") { return; } setTimeout(() => { const offset = element.offset(); if (offset) { $('html, body').animate({scrollTop: offset.top - 155}, 300); } }, 100); } class Statistic { constructor(parent) { this.progress = $("<span id='progressCnt' title='Progress'>0%</span>").appendTo(parent); this.approved = $("<span id='approvedCnt' title='Approved'>0</span>").appendTo(parent); this.filtered = $("<span id='filteredCnt' title='Filtered'>0</span>").appendTo(parent); this.total = $("<span id='totalCnt' title='Total'>0</span>").appendTo(parent); } update() { const files = $('.files .diff-file'), total = files.length || 1, approved = files.filter('.approved').length, filtered = files.filter('.filtered-file:not(.approved)').length, processed = filtered + approved; this.total.text(total); this.approved.text(approved); this.filtered.text(filtered); this.progress .text((processed * 100 / total).toFixed(1) + "%") .toggleClass("half", processed / total >= 0.5) .toggleClass("done", processed === total); } } class Filters { constructor(settings) { this.settings = settings; this.preFilters = [ this.filterByPattern ]; this.fileFilters = [ this.filterMoved, this.filterByContent ]; this.contentFilters = [ this.filterEmptyChanges, this.filterLineBreakAtTheEnd, this.filterByHighlightedDiff, this.filterImportChanges, this.filterPackageChanges ]; } preFilter(changedFile) { const failedFilter = this.preFilters.find(f => f.call(this, changedFile)); if (failedFilter) { console.debug( "File '%c" + changedFile.fileName.replace(/.*\/(.*)$/, "$1") + "%c' prefiltered by %c" + failedFilter.name, "color: red", "color: black", "color: blue; font-weight:bold" ); } return !!failedFilter; } filter(changedFile) { const failedFilter = this.fileFilters.find(f => f.call(this, changedFile)); if (failedFilter) { console.log( "File '%c" + changedFile.fileName.replace(/.*\/(.*)$/, "$1") + "%c' filtered by %c" + failedFilter.name, "color: red", "color: black", "color: blue; font-weight:bold" ); } return !!failedFilter; } filterByPattern(changedFile) { return this.settings.filters.some(p => changedFile.fileName.indexOf(p) > -1); } filterMoved(changedFile) { return changedFile.element.find(".nothing-here-block:contains('File moved')").length > 0; } filterByContent(changedFile) { return changedFile.changedLines.every(line => this.contentFilters.some(f => f.call(this, line))); } filterByHighlightedDiff(line) { return ( this.settings.filterEmptyChanges && line.highlightedDiff.length > 0 && line.highlightedDiff.replace(/[\s;]+/g, "").length === 0 ); } filterEmptyChanges(line) { return this.settings.filterEmptyChanges && line.content.length === 0; } filterLineBreakAtTheEnd(line) { return this.settings.filterEmptyChanges && line.content === "}"; } filterImportChanges(line) { return this.settings.filterImportChanges && line.content.indexOf("import ") === 0; } filterPackageChanges(line) { return this.settings.filterImportChanges && line.content.indexOf("package ") === 0; } } class SettingsEditorButton { constructor(parent, settings) { const editor = new SettingsEditor(settings); this.button = $("<span id='setting-editor-button' class='btn fa fa-cogs'/>") .appendTo(parent) .click(() => editor.show()); } } class ReviewPanel { constructor(settings) { this.panel = $("<div id='utilHolder'/>"); this.settingsButton = new SettingsEditorButton(this.panel, settings); this.statistic = new Statistic(this.panel); this.controls = new ReviewPanelControls(this.panel); } update() { this.statistic.update(); this.controls.update(); } inject() { this.panel.appendTo('body'); this.update(); } } class ReviewPanelControls { constructor(parent) { const holder = $("<div/>").appendTo(parent); this.hideFilteredBtn = $("<label class='hideFiltered fa fa-eye'> Filtered</label>") .appendTo(holder) .click(() => this.toggleFiltered()); this.hideApprovedBtn = $("<label class='hideApproved fa fa-eye'> Approved</label>") .appendTo(holder) .click(() => this.toggleApproved()); } toggleFiltered() { localStorage.hideFiltered = localStorage.hideFiltered !== "true"; this.update(); } toggleApproved() { localStorage.hideApproved = localStorage.hideApproved !== "true"; this.update(); } update() { const hideFiltered = localStorage.hideFiltered === undefined ? true : localStorage.hideFiltered === "true"; const hideApproved = localStorage.hideApproved === "true"; localStorage.hideFiltered = hideFiltered; localStorage.hideApproved = hideApproved; this.hideFilteredBtn.toggleClass("fa-eye-slash", hideFiltered); this.hideApprovedBtn.toggleClass("fa-eye-slash", hideApproved); $('.files') .toggleClass("hide-filtered", hideFiltered) .toggleClass("hide-approved", hideApproved); } } class MergeRequestInjector { constructor(userInfo, filters, reviewPanel, approveStateStorage, unhideStateStorage) { this.reviewPanel = reviewPanel; this.filters = filters; this.approveStateStorage = approveStateStorage; this.unhideStateStorage = unhideStateStorage; this.userInfo = userInfo; } inject() { this.injectStyles(); this.injectActionsToFileList(); this.reviewPanel.inject(); } injectActionsToFileList() { if ($(".files.approve-script-applied").length) { return; } $('.files .diff-file').each((i, element) => this.injectActionsToFileElement($(element))); $('.files').addClass("approve-script-applied"); } injectActionsToFileElement(fileElement) { const changedFile = new ChangedFile(fileElement); const controller = new StateController(this.approveStateStorage, changedFile); changedFile .listener(() => this.reviewPanel.update()) .listener(() => controller.onChanges()); this.injectActionsToChangedFile(changedFile); this.applyFilters(changedFile) } applyFilters(changedFile) { const prefiltered = this.filters.preFilter(changedFile); changedFile.setFiltered(prefiltered); if (!prefiltered) { changedFile.waitContent(() => changedFile.setFiltered(this.filters.filter(changedFile))); } } injectActionsToChangedFile(changedFile) { changedFile.addAction(new ApproveButton(changedFile, this.userInfo.plain(), false)); changedFile.addAction(new ApproverIcon(changedFile)); changedFile.addAction(new UnhideFilteredButton(this.unhideStateStorage, changedFile)); //changedFile.addActionToDiffContent(new ApproveButton(changedFile, this.userInfo.plain(), true)); } injectStyles() { $('<style>') .html(STYLES) .appendTo("head"); } } class StateController { constructor(stateStorage, file) { this.file = file; this.stateStorage = stateStorage; this.storeId = file.fileId; this.init(); this.lastApprover = undefined; } init() { this.stateStorage.watch(this.storeId, state => { const lastState = state || {}; this.lastApprover = lastState.user; this.lastChecksum = lastState.checksum; this.file.setApprove(this.lastChecksum, this.lastApprover); }); } onChanges() { const approver = this.file.getApprover(); const checksum = this.file.getApprovedChecksum(); if (this.lastChecksum === checksum && this.lastApprover === approver) { return; } if (this.file.isApproved()) { this.stateStorage.set(this.storeId, { checksum : checksum, user : approver }); this.lastApprover = approver; this.lastChecksum = checksum; } else { this.stateStorage.set(this.storeId, {}); this.lastApprover = undefined; this.lastChecksum = undefined; } } } class ApproverIcon { constructor(file) { this.element = $("<img class='avatar avatar-inline s24'>").hide(); this.file = file; } refresh() { const approved = this.file.isApproved(); console.log(this.file); if (approved) { const approver = this.file.getApprover() this.element .attr("title", "Approved by " + approver.name) .attr("src", approver.avatar) .attr("srcset", approver.avatar.replace("uploads/user", "uploads/-/system/user")); } this.element.toggle(approved); } } class ApproveButton { constructor(changedFile, userInfo, bottom) { this.element = $("<span class='btn fa approve-btn fa-check'/>") .toggleClass("bottom-approve-btn", !!bottom) .click(() => changedFile.toggleApprover(userInfo)); } } class UserInfo { constructor() { const link = $(".profile-link"); const avatarUrl = $(".header-user-avatar").eq(0).attr("src"); this.name = link.data("user"); this.url = link.attr("href"); this.avatar = avatarUrl && (avatarUrl.indexOf(location.host) == -1 ? avatarUrl : avatarUrl.replace(/[^:]+:\/\/[^/]+/, "")) || ""; } isAuthorized() { return !!this.name; } plain() { return { name : this.name, url : this.url, avatar : this.avatar, }; } } class UnhideFilteredButton { constructor(stateStorage, file) { this.element = $("<span class='btn fa unhide-btn fa-eye'/>") .click(() => this.unfilter()); this.file = file; this.storeId = 'unhidden/' + this.file.fileName; this.stateStorage = stateStorage; this.init(); } init() { this.stateStorage.watch(this.storeId, value => { if (value === "true") { this.file.disableFilter(); } }); } unfilter() { this.file.setFiltered(false) this.file.scrollTo(); } refresh() { const filtered = !!this.file.isFiltered; this.stateStorage.set(this.storeId, "" + filtered); } } class ChangedFile { constructor(fileElement) { this.fileName = $('.file-title-name', fileElement).text().replace(/[\n\r]+/gi, "").trim(); this.fileId = sha256(this.fileName); this.element = fileElement; this._filterDisabled = false; this._filtered = false; this._approver = undefined; this._approvedChecksum = undefined; this.listeners = []; this.actions = []; this.actionsToolbar = fileElement.find(".file-actions"); this.refresh(); } listener(listener) { this.listeners.push(listener); return this; } setFiltered(state) { if (this._filterDisabled || state == this._filtered) { return } this._filtered = state; this.element.toggleClass("filtered-file", state); this.refresh(); } disableFilter() { this.setFiltered(false); this._filterDisabled = true; } setApprove(approvedChecksum, approver) { if (approvedChecksum === this._approvedChecksum) { return } this._approver = approver; this._approvedChecksum = approvedChecksum; this.updateView(); } toggleApprover(approver) { if (this.isApproved()) { this.setApprove(undefined, approver); } else { this.setApprove(this.checksum, approver); } } isApproved() { return !!(this._approver && this._approvedChecksum && this._approvedChecksum === this.checksum) } updateView() { const approved = this._approver && this._approvedChecksum && this._approvedChecksum === this.checksum; this.element.toggleClass("approved", !!approved); this.listeners.forEach(listener => listener()); this.actions .filter(action => typeof action.refresh === "function") .forEach(action => action.refresh()) } getApprover() { return this._approver; } getApprovedChecksum() { return this._approvedChecksum; } initChecksum() { const newLines = this.changedLines.filter(l => l.element.is(".new .line")).map(l => l.content).join("\n"); const oldLines = this.changedLines.filter(l => l.element.is(".old .line")).map(l => l.content).join("\n"); this.checksum = sha256(oldLines) + " - " + sha256(newLines); } isLoaded() { return this.element.find(".diff-content i.fa.fa-spinner, .diff-collapsed").length === 0; } waitContent(callback) { if (this.isLoaded()) { this.refresh(); callback(); } else { setTimeout(() => this.waitContent(callback), 500); } } refresh() { this.diffContent = this.element.find(".diff-content"); this.changedLines = [...this.diffContent.find(".new.line_content, .old.line_content")] .map(element => $(element).find(".line")) .map(line => { return { element: line, content: line.text().trim(), highlightedDiff: line.find(".idiff").text().trim() }; }); this.initChecksum(this.element); this.updateView(); } addAction(action) { this.actions.push(action); this.actionsToolbar.prepend(action.element); } addActionToDiffContent(action) { this.actions.push(action); this.diffContent.after(action.element); } toggleClass(className, value) { this.element.toggleClass(className, value); } scrollTo() { scrollTo(this.element); } scrollToNext() { let target = this.element.nextAll(":not(.filtered-file, .approved)"); if (!target || !target.length) { target = this.element.prevAll(":not(.filtered-file, .approved)"); if (!target || !target.length) { target = $("body"); } } scrollTo(target.eq(0)); } } class Gitlab { constructor(settings, stateStorageFactory) { this.stateStorageFactory = stateStorageFactory; this.filters = new Filters(settings); this.reviewPanel = new ReviewPanel(settings); this.stateStorages = {}; window.gt=this; } detectMergeRequestId(href) { const urlData = /\/([^/]+)\/merge_requests\/(\d+)/.exec(href); if (!urlData || urlData.length < 3) { return undefined; } return sha256(location.host) + "/" + urlData[1] + "/" + urlData[2]; } waitForInjecting() { if (!this.mergeRequestId) { return; } // && $('.files .diff-file .diff-content i.fa.fa-spinner').length === 0 const preloaderShown = $(".loading").is(":visible"); const files = $('.files .diff-file').length; if (files > 0) { console.info("Injecting Gitlab. Found " + files + " files"); const userInfo = new UserInfo(); new MergeRequestInjector( userInfo, this.filters, this.reviewPanel, this.stateStorages[this.mergeRequestId], new LocalStateStorage(this.mergeRequestId) ).inject(); } else { console.debug("Waiting for injecting, files: " + files + ", preloader: " + preloaderShown); setTimeout(this.waitForInjecting.bind(this), 500); } } mergeRequestDiffDetected(mergeRequestId) { if (mergeRequestId) { this.mergeRequestId = mergeRequestId; this.stateStorages[mergeRequestId] = this.stateStorages[mergeRequestId] || this.stateStorageFactory.create(mergeRequestId); this.waitForInjecting(); } } checkForMergeRequest() { if (/\/[^/]+\/[^/]+\/merge_requests\/\d+\/diffs.*/.test(location.href)) { let params = location.search.split(/[?&]/).filter(s => s); let newParams = []; if (params.indexOf("expand_all_diffs=1") === -1) { newParams.push("expand_all_diffs=1"); } if (params.indexOf("w=1") === -1) { newParams.push("w=1"); } if (params.indexOf("expanded=1") === -1) { newParams.push("expanded=1"); } if (newParams.length) { newParams.unshift(...params); location.search = newParams.join("&"); } else { this.mergeRequestDiffDetected(this.detectMergeRequestId(location.href)); } } else { setTimeout(() => this.checkForMergeRequest(), 1000); } } init() { this.checkForMergeRequest(); } } class DBStateStorage { constructor(database, prefix) { this.database = database; this.prefix = prefix.replace(/[\.#$\[\]]+/gi, "_"); this.resolvers = {}; this.repo = {}; this.database.ref(this.prefix).on('value', snapshot => { const prevRepo = this.repo; this.repo = snapshot.val() || {}; Object.keys(this.resolvers).forEach(key => { const prevChecksum = prevRepo[key] && prevRepo[key].checksum; const currChecksum = this.repo[key] && this.repo[key].checksum; if (prevChecksum != currChecksum) { this.resolvers[key](this.repo[key]); } }); }); } key(subkey) { return this.prefix + "/" + subkey.replace(/[\.#$\[\]\\\/]+/gi, "_"); } watch(subkey, resolve) { this.resolvers[subkey] = resolve; if (this.repo[subkey]) { resolve(this.repo[subkey]); } } set(key, value) { setTimeout(() => { this.database.ref().update({ [this.key(key)] : value }); }, 1); } } class DBStateStorageFactory { constructor(settings) { DBStateStorageFactory.init(settings); } create(prefix) { return new DBStateStorage(firebase.database(), prefix); } static init(settings) { if (!DBStateStorageFactory.initiated) { DBStateStorageFactory.initiated = true; localStorage.removeItem("firebase:previous_websocket_failure"); firebase.initializeApp({ apiKey: settings.fireBaseApiKey, databaseURL: settings.fireBaseDBUrl }); } } } class LocalStateStorage { constructor(prefix) { this.prefix = prefix; } watch(key, resolve) { var value = localStorage[this.prefix + "/" + key]; resolve(value === undefined ? value : JSON.parse(value)); } set(key, value) { localStorage[this.prefix + "/" + key] = JSON.stringify(value); } } class LocalStateStorageFactory { create(prefix) { return new LocalStateStorage(prefix); } } class Main { static init() { const settings = new Settings(); new Gitlab(settings, this.createStateStorage(settings)).init(); } static createStateStorage(settings) { if (settings.stateStorage === "local") { console.info("Local state storage"); return new LocalStateStorageFactory(); } console.info("DB state storage"); return new DBStateStorageFactory(settings); } } class Settings { constructor() { function get(name) { return localStorage[name] || Settings.defaults[name]; } function getBool(name, trueValue="true") { return get(name) === trueValue; } this.stateStorage = get("stateStorage"); this.isPublicFireBase = ( getBool("fireBaseDBUrl", Settings.defaults.fireBaseDBUrl) && getBool("fireBaseApiKey", Settings.defaults.fireBaseApiKey) ); this.fireBaseApiKey = get("fireBaseApiKey"); this.fireBaseDBUrl = get("fireBaseDBUrl"); this.filterPackageChanges = getBool("filterPackageChanges"); this.filterImportChanges = getBool("filterImportChanges"); this.filterEmptyChanges = getBool("filterEmptyChanges"); this.filters = JSON.parse(get("fileNameFilters")).filter(a => a && a.length > 2); this.enableFileNameFilter = this.filters.length !== 0 && getBool("enableFileNameFilter"); } update() { localStorage.stateStorage = this.stateStorage; localStorage.fireBaseApiKey = this.fireBaseApiKey; localStorage.fireBaseDBUrl = this.fireBaseDBUrl; localStorage.filterPackageChanges = this.filterPackageChanges; localStorage.filterImportChanges = this.filterImportChanges; localStorage.filterEmptyChanges = this.filterEmptyChanges; localStorage.enableFileNameFilter = this.enableFileNameFilter; localStorage.fileNameFilters = JSON.stringify(this.filters); } } Settings.defaults = { stateStorage : "db", filterPackageChanges : "true", filterImportChanges : "true", filterEmptyChanges : "true", enableFileNameFilter : "false", fileNameFilters : "[]", fireBaseApiKey : "AIzaSyDh6mkW61kb8HbmRZ6gZQjeuZugWTDpLto", fireBaseDBUrl : "https://public-gitlab-review.firebaseio.com" }; class SettingsEditor { constructor(settings) { function checkbox(holder, toggleable) { const checkbox = $("<input type='checkbox'>") .appendTo(holder) .wrap("<div class='checkbox'></div>") .wrap("<label></label>"); return toggleable ? checkbox.change(() => toggleable.toggle(checkbox.is(":checked"))) : checkbox; } const editor = $("<div class='settings-editor'>").hide(); const repoHolder = $("<div class='repo-holder'/>").appendTo(editor); const filtersHolder = $("<div>").appendTo(editor); const buttons = $("<div class='buttons'>").appendTo(editor); const saveBtn = $("<button class='btn btn-primary'>Save</button>") .appendTo(buttons) .click(() => this.save()); const cancelBtn = $("<button class='btn btn-default'>Cancel</button>") .appendTo(buttons) .click(() => this.close()); this.settings = settings; this.editor = editor; this.firebaseSettings = $("<div/>").addClass("inner-settings"); this.useDbCheckbox = checkbox(repoHolder, this.firebaseSettings).after("Use firebase DB for sharing approves"); this.firebaseSettings.appendTo(repoHolder); this.urlInput = $("<input class='form-control'>") .appendTo(this.firebaseSettings) .wrap("<label>Firebase DB url: </label>"); this.apiKeyInput = $("<input class='form-control'>") .appendTo(this.firebaseSettings) .wrap("<label>Firebase Api key: </label>"); this.filterImportChanges = checkbox(filtersHolder).after("Filter for 'import' changes"); this.filterEmptyChanges = checkbox(filtersHolder).after("Filter for 'empty' changes"); this.fileNameFilterHolder = $("<div/>").addClass("inner-settings"); this.enableFileNameFilter = checkbox(filtersHolder, this.fileNameFilterHolder).after("Filter by file names"); this.fileNameFilterHolder.appendTo(filtersHolder); this.filtersInput = $('<textarea rows=7 class="form-control"></textarea>') .appendTo(this.fileNameFilterHolder) .wrap("<label>File name filters(exclude): </label>"); } show() { this.update(); this.editor.show().appendTo("body"); } update() { this.urlInput.val(this.settings.fireBaseDBUrl); this.apiKeyInput.val(this.settings.fireBaseApiKey); this.filtersInput.val(this.settings.filters.join("\n")); const isDatabaseStateStorage = this.settings.stateStorage === "db"; this.useDbCheckbox.prop("checked", isDatabaseStateStorage); this.firebaseSettings.toggle(isDatabaseStateStorage); this.filterEmptyChanges.prop("checked", this.settings.filterEmptyChanges); this.filterImportChanges.prop("checked", this.settings.filterImportChanges); this.enableFileNameFilter.prop("checked", this.settings.enableFileNameFilter); this.fileNameFilterHolder.toggle(this.settings.enableFileNameFilter); } save() { this.settings.fireBaseDBUrl = this.urlInput.val(); this.settings.fireBaseApiKey = this.apiKeyInput.val(); this.settings.filters = this.filtersInput.val() .split("\n") .map(s => s.trim()) .filter(s => s.length > 0) .filter((v, i, arr) => arr.indexOf(v) === i); this.settings.stateStorage = this.useDbCheckbox.is(":checked") ? "db" : "local"; this.settings.filterImportChanges = this.filterImportChanges.is(":checked"); this.settings.filterPackageChanges = this.filterImportChanges.is(":checked"); this.settings.filterEmptyChanges = this.filterEmptyChanges.is(":checked"); this.settings.enableFileNameFilter = this.enableFileNameFilter.is(":checked"); this.settings.update(); this.close(); } close() { this.editor.hide(); } } function init() { if (!window.$) { setTimeout(init, 200); } else { $ = window.$; Main.init(); } } init(); })(window.document.defaultView);