diff --git a/src/extensibility/ExtensionManager.js b/src/extensibility/ExtensionManager.js index b6cc2d8ef01..5997d482abc 100644 --- a/src/extensibility/ExtensionManager.js +++ b/src/extensibility/ExtensionManager.js @@ -108,6 +108,9 @@ define(function (require, exports, module) { _idsToDisable = {}; PreferencesManager.stateManager.definePreference(FOLDER_AUTOINSTALL, "object", undefined); + PreferencesManager.definePreference("extensions.sort", "string", "publishedDate", { + description: Strings.SORT_EXTENSION_METHOD + }); /** * @private diff --git a/src/extensibility/ExtensionManagerDialog.js b/src/extensibility/ExtensionManagerDialog.js index acfd61873fb..855b91211d4 100644 --- a/src/extensibility/ExtensionManagerDialog.js +++ b/src/extensibility/ExtensionManagerDialog.js @@ -41,7 +41,8 @@ define(function (require, exports, module) { KeyEvent = require("utils/KeyEvent"), ExtensionManager = require("extensibility/ExtensionManager"), ExtensionManagerView = require("extensibility/ExtensionManagerView").ExtensionManagerView, - ExtensionManagerViewModel = require("extensibility/ExtensionManagerViewModel"); + ExtensionManagerViewModel = require("extensibility/ExtensionManagerViewModel"), + PreferencesManager = require("preferences/PreferencesManager"); var dialogTemplate = require("text!htmlContent/extension-manager-dialog.html"); @@ -372,6 +373,11 @@ define(function (require, exports, module) { if (models[_activeTabIndex]) { $modalDlg.scrollTop(models[_activeTabIndex].scrollPos || 0); clearSearch(); + if (_activeTabIndex === 2) { + $(".ext-sort-group").hide(); + } else { + $(".ext-sort-group").show(); + } } } @@ -460,6 +466,18 @@ define(function (require, exports, module) { $modalDlg.scrollTop(0); }); }).on("click", ".search-clear", clearSearch); + + // Sort the extension list based on the current selected sorting criteria + $dlg.on("change", ".sort-extensions", function (e) { + var sortBy = $(this).val(); + PreferencesManager.set("extensions.sort", sortBy); + models.forEach(function (model, index) { + if (index <= 1) { + model._setSortedExtensionList(ExtensionManager.extensions, index === 1); + views[index].filter($(".search").val()); + } + }); + }); // Disable the search field when there are no items in the model models.forEach(function (model, index) { @@ -480,6 +498,11 @@ define(function (require, exports, module) { } else { // Otherwise show the first tab $dlg.find(".nav-tabs a:first").tab("show"); } + if ($activeTab.hasClass("installed")) { + $(".ext-sort-group").hide(); + } else { + $(".ext-sort-group").show(); + } }); // Handle the 'Install from URL' button. diff --git a/src/extensibility/ExtensionManagerView.js b/src/extensibility/ExtensionManagerView.js index 0bac5cdde1a..2a981ff6c68 100644 --- a/src/extensibility/ExtensionManagerView.js +++ b/src/extensibility/ExtensionManagerView.js @@ -36,7 +36,8 @@ define(function (require, exports, module) { LanguageManager = require("language/LanguageManager"), Mustache = require("thirdparty/mustache/mustache"), PathUtils = require("thirdparty/path-utils/path-utils"), - itemTemplate = require("text!htmlContent/extension-manager-view-item.html"); + itemTemplate = require("text!htmlContent/extension-manager-view-item.html"), + PreferencesManager = require("preferences/PreferencesManager"); /** @@ -71,6 +72,7 @@ define(function (require, exports, module) { this._$infoMessage = $("
") .appendTo(this.$el).html(this.model.infoMessage); this._$table = $("").appendTo(this.$el); + $(".sort-extensions").val(PreferencesManager.get("extensions.sort")); this.model.initialize().done(function () { self._setupEventHandlers(); @@ -248,6 +250,7 @@ define(function (require, exports, module) { var installWarningBase = context.requiresNewer ? Strings.EXTENSION_LATEST_INCOMPATIBLE_NEWER : Strings.EXTENSION_LATEST_INCOMPATIBLE_OLDER; context.installWarning = StringUtils.format(installWarningBase, entry.registryInfo.versions[entry.registryInfo.versions.length - 1].version, latestVerCompatInfo.compatibleVersion); } + context.downloadCount = entry.registryInfo.totalDownloads; } else { // We should only get here when viewing the Installed tab and some extensions don't exist in the registry // (or registry is offline). These flags *should* always be ignored in that scenario, but just in case... diff --git a/src/extensibility/ExtensionManagerViewModel.js b/src/extensibility/ExtensionManagerViewModel.js index 90d30fe512e..cb86d1327ee 100644 --- a/src/extensibility/ExtensionManagerViewModel.js +++ b/src/extensibility/ExtensionManagerViewModel.js @@ -28,10 +28,11 @@ define(function (require, exports, module) { var _ = require("thirdparty/lodash"); - var ExtensionManager = require("extensibility/ExtensionManager"), - registry_utils = require("extensibility/registry_utils"), - EventDispatcher = require("utils/EventDispatcher"), - Strings = require("strings"); + var ExtensionManager = require("extensibility/ExtensionManager"), + registry_utils = require("extensibility/registry_utils"), + EventDispatcher = require("utils/EventDispatcher"), + Strings = require("strings"), + PreferencesManager = require("preferences/PreferencesManager"); /** * @private @@ -292,6 +293,20 @@ define(function (require, exports, module) { }); }; + ExtensionManagerViewModel.prototype._setSortedExtensionList = function (extensions, isTheme) { + this.filterSet = this.sortedFullSet = registry_utils.sortRegistry(extensions, "registryInfo", PreferencesManager.get("extensions.sort")) + .filter(function (entry) { + if (!isTheme) { + return entry.registryInfo && !entry.registryInfo.metadata.theme; + } else { + return entry.registryInfo && entry.registryInfo.metadata.theme; + } + }) + .map(function (entry) { + return entry.registryInfo.metadata.name; + }); + }; + /** * The model for the ExtensionManagerView that is responsible for handling registry-based extensions. * This extends ExtensionManagerViewModel. @@ -329,13 +344,7 @@ define(function (require, exports, module) { self.extensions = ExtensionManager.extensions; // Sort the registry by last published date and store the sorted list of IDs. - self.sortedFullSet = registry_utils.sortRegistry(self.extensions, "registryInfo") - .filter(function (entry) { - return entry.registryInfo !== undefined && entry.registryInfo.metadata.theme === undefined; - }) - .map(function (entry) { - return entry.registryInfo.metadata.name; - }); + self._setSortedExtensionList(ExtensionManager.extensions, false); self._setInitialFilter(); }) .fail(function () { @@ -536,13 +545,7 @@ define(function (require, exports, module) { self.extensions = ExtensionManager.extensions; // Sort the registry by last published date and store the sorted list of IDs. - self.sortedFullSet = registry_utils.sortRegistry(self.extensions, "registryInfo") - .filter(function (entry) { - return entry.registryInfo !== undefined && entry.registryInfo.metadata.theme; - }) - .map(function (entry) { - return entry.registryInfo.metadata.name; - }); + self._setSortedExtensionList(ExtensionManager.extensions, true); self._setInitialFilter(); }) .fail(function () { @@ -570,4 +573,4 @@ define(function (require, exports, module) { exports.RegistryViewModel = RegistryViewModel; exports.ThemesViewModel = ThemesViewModel; exports.InstalledViewModel = InstalledViewModel; -}); +}); \ No newline at end of file diff --git a/src/extensibility/registry_utils.js b/src/extensibility/registry_utils.js index 57be44c6e29..3ee9b76b6df 100644 --- a/src/extensibility/registry_utils.js +++ b/src/extensibility/registry_utils.js @@ -120,7 +120,7 @@ define(function (require, exports, module) { * we should look at the top level of the object. * @return {Array} Sorted array of registry entries. */ - exports.sortRegistry = function (registry, subkey) { + exports.sortRegistry = function (registry, subkey, sortBy) { function getPublishTime(entry) { if (entry.versions) { return new Date(entry.versions[entry.versions.length - 1].published).getTime(); @@ -136,8 +136,16 @@ define(function (require, exports, module) { sortedEntries.push(registry[key]); }); sortedEntries.sort(function (entry1, entry2) { - return getPublishTime((subkey && entry2[subkey]) || entry2) - - getPublishTime((subkey && entry1[subkey]) || entry1); + if (sortBy !== "publishedDate") { + if (entry1.registryInfo && entry2.registryInfo) { + return entry2.registryInfo.totalDownloads - entry1.registryInfo.totalDownloads; + } else { + return Number.NEGATIVE_INFINITY; + } + } else { + return getPublishTime((subkey && entry2[subkey]) || entry2) - + getPublishTime((subkey && entry1[subkey]) || entry1); + } }); return sortedEntries; diff --git a/src/htmlContent/extension-manager-dialog.html b/src/htmlContent/extension-manager-dialog.html index ebc3162bb60..53ab9562bff 100644 --- a/src/htmlContent/extension-manager-dialog.html +++ b/src/htmlContent/extension-manager-dialog.html @@ -9,6 +9,11 @@

  • {{Strings.EXTENSIONS_INSTALLED_TITLE}}
  • + {{Strings.EXTENSIONS_SORT_BY}} +
    diff --git a/src/htmlContent/extension-manager-view-item.html b/src/htmlContent/extension-manager-view-item.html index bb3782e5ed1..6b15d72bce4 100644 --- a/src/htmlContent/extension-manager-view-item.html +++ b/src/htmlContent/extension-manager-view-item.html @@ -8,6 +8,9 @@ {{#hasVersionInfo}} — {{lastVersionDate}} {{/hasVersionInfo}} + {{#downloadCount}} +

    {{downloadCount}}

    + {{/downloadCount}} - + \ No newline at end of file diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 21adacbbcab..6e15cfaf9f8 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -503,6 +503,7 @@ define({ "INSTALL_CANCELED" : "Installation canceled.", "VIEW_COMPLETE_DESCRIPTION" : "View complete description", "VIEW_TRUNCATED_DESCRIPTION" : "View truncated description", + "SORT_EXTENSION_METHOD" : "Sort Extensions using downloadCount or publishedDate", // These must match the error codes in ExtensionsDomain.Errors.* : "INVALID_ZIP_FILE" : "The downloaded content is not a valid zip file.", "MISSING_PACKAGE_JSON" : "The package has no package.json file.", @@ -580,6 +581,9 @@ define({ "EXTENSIONS_AVAILABLE_TITLE" : "Available", "EXTENSIONS_THEMES_TITLE" : "Themes", "EXTENSIONS_UPDATES_TITLE" : "Updates", + "EXTENSIONS_SORT_BY" : "Sort By", + "EXTENSIONS_LAST_UPDATED" : "Last Updated", + "EXTENSIONS_DOWNLOADS" : "Downloads", "INLINE_EDITOR_NO_MATCHES" : "No matches available.", "INLINE_EDITOR_HIDDEN_MATCHES" : "All matches are collapsed. Expand the files listed at right to view matches.", diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index 7bed48f66ae..0fd283bc034 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -1183,6 +1183,17 @@ a[href^="http"] { opacity: 0.3; } } + .sort-extensions-title { + float: left; + margin-right: 5px; + margin-top: 5px; + } + .sort-extensions { + float: left; + margin-right: 10px; + width: auto; + padding-right: 18px; + } } .modal-body { height: 400px; diff --git a/src/styles/images/download-icon.svg b/src/styles/images/download-icon.svg new file mode 100644 index 00000000000..74927919666 --- /dev/null +++ b/src/styles/images/download-icon.svg @@ -0,0 +1,19 @@ + + + + + S_Download_18_N + + + + \ No newline at end of file diff --git a/test/spec/ExtensionManager-test-files/mockRegistry.json b/test/spec/ExtensionManager-test-files/mockRegistry.json index 259639f7071..b61ec791c10 100644 --- a/test/spec/ExtensionManager-test-files/mockRegistry.json +++ b/test/spec/ExtensionManager-test-files/mockRegistry.json @@ -20,7 +20,8 @@ "published": "2013-04-10T18:21:27.058Z", "brackets": ">0.20.0" } - ] + ], + "totalDownloads": 10 }, "long-desc-extension": { "metadata": { @@ -42,7 +43,8 @@ "version": "1.0.0", "published": "2013-04-10T18:26:20.553Z" } - ] + ], + "totalDownloads": 184 }, "mock-extension-1": { "metadata": { @@ -65,7 +67,8 @@ "version": "1.0.0", "published": "2013-04-11T18:26:20.553Z" } - ] + ], + "totalDownloads": 190 }, "mock-extension-2": { "metadata": { @@ -79,7 +82,8 @@ "version": "1.0.0", "published": "2013-04-11T18:26:20.553Z" } - ] + ], + "totalDownloads": 88 }, "mock-extension-3": { "metadata": { @@ -93,7 +97,8 @@ "version": "1.0.0", "published": "2013-04-11T18:26:20.553Z" } - ] + ], + "totalDownloads": 34 }, "mock-extension-4": { "metadata": { @@ -107,7 +112,8 @@ "version": "1.0.0", "published": "2013-04-11T18:26:20.553Z" } - ] + ], + "totalDownloads": 20 }, "malicious-script-extension": { "metadata": { @@ -122,7 +128,8 @@ "version": "1.0.0", "published": "2013-04-10T18:26:24.956Z" } - ] + ], + "totalDownloads": 781 }, "select-parent": { "metadata": { @@ -147,7 +154,8 @@ "published": "2013-04-10T18:26:47.580Z", "brackets": ">0.22.0" } - ] + ], + "totalDownloads": 101 }, "basic-valid-extension": { "metadata": { @@ -165,7 +173,8 @@ "version": "3.0.0", "published": "2013-04-10T18:29:11.907Z" } - ] + ], + "totalDownloads": 130 }, "everyscrub": { "metadata": { @@ -179,6 +188,7 @@ "version": "0.23.0", "published": "2013-04-11T17:22:38.033Z" } - ] + ], + "totalDownloads": 101 } } \ No newline at end of file diff --git a/test/spec/ExtensionManager-test-files/mockRegistryForSearch.json b/test/spec/ExtensionManager-test-files/mockRegistryForSearch.json index a58761d98e8..2e64ad07643 100644 --- a/test/spec/ExtensionManager-test-files/mockRegistryForSearch.json +++ b/test/spec/ExtensionManager-test-files/mockRegistryForSearch.json @@ -19,7 +19,8 @@ "published": "2013-04-10T18:21:27.058Z", "brackets": ">0.20.0" } - ] + ], + "totalDownloads": 10 }, "item-2": { "metadata": { @@ -36,7 +37,8 @@ "version": "1.0.0", "published": "2013-04-10T18:26:20.553Z" } - ] + ], + "totalDownloads": 5 }, "item-3": { "metadata": { @@ -53,7 +55,8 @@ "version": "1.0.0", "published": "2013-04-10T13:26:20.553Z" } - ] + ], + "totalDownloads": 11 }, "item-4": { "metadata": { @@ -71,7 +74,8 @@ "version": "1.0.0", "published": "2013-04-10T15:26:20.553Z" } - ] + ], + "totalDownloads": 12 }, "item-5": { "metadata": { @@ -89,7 +93,8 @@ "version": "1.0.0", "published": "2013-04-16T18:26:20.553Z" } - ] + ], + "totalDownloads": 2 }, "item-6": { "metadata": { @@ -111,6 +116,7 @@ "version": "1.0.0", "published": "2013-04-15T18:26:20.553Z" } - ] + ], + "totalDownloads": 100 } } \ No newline at end of file diff --git a/test/spec/ExtensionManager-test-files/mockRegistryThemes.json b/test/spec/ExtensionManager-test-files/mockRegistryThemes.json index 6a62a6636af..35386535b2a 100644 --- a/test/spec/ExtensionManager-test-files/mockRegistryThemes.json +++ b/test/spec/ExtensionManager-test-files/mockRegistryThemes.json @@ -188,7 +188,8 @@ "version": "0.0.1", "published": "2014-08-31T04:05:47.778Z" } - ] + ], + "totalDownloads": 80 }, "theme-2": { "metadata": { @@ -205,6 +206,7 @@ "version": "1.0.1", "published": "2014-07-31T04:05:47.778Z" } - ] + ], + "totalDownloads": 99 } } diff --git a/test/spec/ExtensionManager-test.js b/test/spec/ExtensionManager-test.js index 52bc8df2e8b..13682a7cee3 100644 --- a/test/spec/ExtensionManager-test.js +++ b/test/spec/ExtensionManager-test.js @@ -48,6 +48,7 @@ define(function (require, exports, module) { Strings = require("strings"), StringUtils = require("utils/StringUtils"), LocalizationUtils = require("utils/LocalizationUtils"), + PreferencesManager = require("preferences/PreferencesManager"), mockRegistryText = require("text!spec/ExtensionManager-test-files/mockRegistry.json"), mockRegistryThemesText = require("text!spec/ExtensionManager-test-files/mockRegistryThemes.json"), mockRegistryForSearch = require("text!spec/ExtensionManager-test-files/mockRegistryForSearch.json"), @@ -734,7 +735,15 @@ define(function (require, exports, module) { expect(model.extensions).toEqual(ExtensionManager.extensions); }); + it("should start with the full set sorted in reverse download count order", function () { + PreferencesManager.set("extensions.sort", "downloadCount"); + model._setSortedExtensionList(ExtensionManager.extensions, false); + expect(model.filterSet).toEqual(["item-6", "item-4", "item-3", "find-uniq1-in-name", "item-2", "item-5"]); + }); + it("should start with the full set sorted in reverse publish date order", function () { + PreferencesManager.set("extensions.sort", "publishedDate"); + model._setSortedExtensionList(ExtensionManager.extensions, false); expect(model.filterSet).toEqual(["item-5", "item-6", "item-2", "find-uniq1-in-name", "item-4", "item-3"]); }); @@ -829,6 +838,12 @@ define(function (require, exports, module) { it("should start with the full set sorted in reverse publish date order", function () { expect(model.filterSet).toEqual(["theme-1", "theme-2"]); }); + + it("should start with the full set sorted in reverse download count order", function () { + PreferencesManager.set("extensions.sort", "downloadCount"); + model._setSortedExtensionList(ExtensionManager.extensions, true); + expect(model.filterSet).toEqual(["theme-2", "theme-1"]); + }); }); @@ -1282,7 +1297,8 @@ define(function (require, exports, module) { // Simple fields [item.metadata.version, - item.metadata.author && item.metadata.author.name] + item.metadata.author && item.metadata.author.name, + item.totalDownloads] .forEach(function (value) { if (value) { expect(view).toHaveText(value);
    {{#showInstallButton}} @@ -99,4 +102,4 @@ {{/isInstalled}}