1
0
mirror of https://github.com/adobe/brackets.git synced 2024-11-20 09:53:00 +01:00

Merge pull request #11184 from adobe/kai/disable-enable-extensions

Disable and enable extensions
This commit is contained in:
Rakesh Roshan 2015-07-08 13:34:37 +05:30
commit 25011360b1
11 changed files with 673 additions and 109 deletions

4
.gitignore vendored
View File

@ -20,6 +20,9 @@ Thumbs.db
/src/extensions/disabled
# ignore .disabled file for default extensions
/src/extensions/default/*/.disabled
#OSX .DS_Store files
.DS_Store
@ -34,3 +37,4 @@ Thumbs.db
# Files that can be automatically downloaded that we don't want to ship with our builds
/src/extensibility/node/node_modules/request/tests/

View File

@ -67,6 +67,7 @@ define(function (require, exports, module) {
* Extension status constants.
*/
var ENABLED = "enabled",
DISABLED = "disabled",
START_FAILED = "startFailed";
/**
@ -103,8 +104,9 @@ define(function (require, exports, module) {
/**
* Requested changes to the installed extensions.
*/
var _idsToRemove = [],
_idsToUpdate = [];
var _idsToRemove = {},
_idsToUpdate = {},
_idsToDisable = {};
PreferencesManager.stateManager.definePreference(FOLDER_AUTOINSTALL, "object", undefined);
@ -186,8 +188,9 @@ define(function (require, exports, module) {
*/
function _reset() {
exports.extensions = extensions = {};
_idsToRemove = [];
_idsToUpdate = [];
_idsToRemove = {};
_idsToUpdate = {};
_idsToDisable = {};
}
/**
@ -240,8 +243,9 @@ define(function (require, exports, module) {
* @param {string} path The local path of the loaded extension's folder.
*/
function _handleExtensionLoad(e, path) {
function setData(id, metadata) {
function setData(metadata) {
var locationType,
id = metadata.name,
userExtensionPath = ExtensionLoader.getUserExtensionPath();
if (path.indexOf(userExtensionPath) === 0) {
locationType = LOCATION_USER;
@ -265,27 +269,33 @@ define(function (require, exports, module) {
metadata: metadata,
path: path,
locationType: locationType,
status: (e.type === "loadFailed" ? START_FAILED : ENABLED)
status: (e.type === "loadFailed" ? START_FAILED : (e.type === "disabled" ? DISABLED : ENABLED))
};
synchronizeEntry(id);
loadTheme(id);
exports.trigger("statusChange", id);
}
function deduceMetadata() {
var match = path.match(/\/([^\/]+)$/),
name = (match && match[1]) || path,
metadata = { name: name, title: name };
return metadata;
}
ExtensionUtils.loadPackageJson(path)
ExtensionUtils.loadMetadata(path)
.done(function (metadata) {
setData(metadata.name, metadata);
setData(metadata);
})
.fail(function () {
.fail(function (disabled) {
// If there's no package.json, this is a legacy extension. It was successfully loaded,
// but we don't have an official ID or metadata for it, so we just create an id and
// "title" for it (which is the last segment of its pathname)
// and record that it's enabled.
var match = path.match(/\/([^\/]+)$/),
name = (match && match[1]) || path,
metadata = { name: name, title: name };
setData(name, metadata);
var metadata = deduceMetadata();
metadata.disabled = disabled;
setData(metadata);
});
}
@ -398,6 +408,58 @@ define(function (require, exports, module) {
}
return result.promise();
}
/**
* @private
*
* Disables or enables the installed extensions.
*
* @param {string} id The id of the extension to disable or enable.
* @param {boolean} enable A boolean indicating whether to enable or disable.
* @return {$.Promise} A promise that's resolved when the extension action is
* completed or rejected with an error that prevents the action from completion.
*/
function _enableOrDisable(id, enable) {
var result = new $.Deferred(),
extension = extensions[id];
if (extension && extension.installInfo) {
Package[(enable ? "enable" : "disable")](extension.installInfo.path)
.done(function () {
extension.installInfo.status = enable ? ENABLED : DISABLED;
extension.installInfo.metadata.disabled = !enable;
result.resolve();
exports.trigger("statusChange", id);
})
.fail(function (err) {
result.reject(err);
});
} else {
result.reject(StringUtils.format(Strings.EXTENSION_NOT_INSTALLED, id));
}
return result.promise();
}
/**
* Disables the installed extension with the given id.
*
* @param {string} id The id of the extension to disable.
* @return {$.Promise} A promise that's resolved when the extenion is disabled or
* rejected with an error that prevented the disabling.
*/
function disable(id) {
return _enableOrDisable(id, false);
}
/**
* Enables the installed extension with the given id.
*
* @param {string} id The id of the extension to enable.
* @return {$.Promise} A promise that's resolved when the extenion is enabled or
* rejected with an error that prevented the enabling.
*/
function enable(id) {
return _enableOrDisable(id, true);
}
/**
* Updates an installed extension with the given package file.
@ -452,7 +514,7 @@ define(function (require, exports, module) {
}
exports.trigger("statusChange", id);
}
/**
* Returns true if an extension is marked for removal.
* @param {string} id The id of the extension to check.
@ -461,7 +523,7 @@ define(function (require, exports, module) {
function isMarkedForRemoval(id) {
return !!(_idsToRemove[id]);
}
/**
* Returns true if there are any extensions marked for removal.
* @return {boolean} true if there are extensions to remove
@ -470,6 +532,46 @@ define(function (require, exports, module) {
return Object.keys(_idsToRemove).length > 0;
}
/**
* Marks an extension for disabling later, or unmarks an extension previously marked.
*
* @param {string} id The id of the extension
* @param {boolean} mark Whether to mark or unmark the extension.
*/
function markForDisabling(id, mark) {
if (mark) {
_idsToDisable[id] = true;
} else {
delete _idsToDisable[id];
}
exports.trigger("statusChange", id);
}
/**
* Returns true if an extension is mark for disabling.
*
* @param {string} id The id of the extension to check.
* @return {boolean} true if it's been mark for disabling, false otherwise.
*/
function isMarkedForDisabling(id) {
return !!(_idsToDisable[id]);
}
/**
* Returns true if there are any extensions marked for disabling.
* @return {boolean} true if there are extensions to disable
*/
function hasExtensionsToDisable() {
return Object.keys(_idsToDisable).length > 0;
}
/**
* Unmarks all the extensions that have been marked for disabling.
*/
function unmarkAllForDisabling() {
_idsToDisable = {};
}
/**
* If a downloaded package appears to be an update, mark the extension for update.
* If an extension was previously marked for removal, marking for update will
@ -542,6 +644,25 @@ define(function (require, exports, module) {
}
);
}
/**
* Disables extensions marked for disabling.
*
* If the return promise is rejected, the argument will contain an array of objects. Each
* element is an object identifying the extension failed with "item" property set to the
* extension id which has failed to be disabled and "error" property set to the error.
*
* @return {$.Promise} A promise that's resolved when all extensions marked for disabling are
* disabled or rejected if one or more extensions can't be disabled.
*/
function disableMarkedExtensions() {
return Async.doInParallel_aggregateErrors(
Object.keys(_idsToDisable),
function (id) {
return disable(id);
}
);
}
/**
* Updates extensions previously marked for update.
@ -764,7 +885,8 @@ define(function (require, exports, module) {
// Listen to extension load and loadFailed events
ExtensionLoader
.on("load", _handleExtensionLoad)
.on("loadFailed", _handleExtensionLoad);
.on("loadFailed", _handleExtensionLoad)
.on("disabled", _handleExtensionLoad);
EventDispatcher.makeEventDispatcher(exports);
@ -775,17 +897,24 @@ define(function (require, exports, module) {
exports.getExtensionURL = getExtensionURL;
exports.remove = remove;
exports.update = update;
exports.disable = disable;
exports.enable = enable;
exports.extensions = extensions;
exports.cleanupUpdates = cleanupUpdates;
exports.markForRemoval = markForRemoval;
exports.isMarkedForRemoval = isMarkedForRemoval;
exports.unmarkAllForRemoval = unmarkAllForRemoval;
exports.hasExtensionsToRemove = hasExtensionsToRemove;
exports.markForDisabling = markForDisabling;
exports.isMarkedForDisabling = isMarkedForDisabling;
exports.unmarkAllForDisabling = unmarkAllForDisabling;
exports.hasExtensionsToDisable = hasExtensionsToDisable;
exports.updateFromDownload = updateFromDownload;
exports.removeUpdate = removeUpdate;
exports.isMarkedForUpdate = isMarkedForUpdate;
exports.hasExtensionsToUpdate = hasExtensionsToUpdate;
exports.removeMarkedExtensions = removeMarkedExtensions;
exports.disableMarkedExtensions = disableMarkedExtensions;
exports.updateExtensions = updateExtensions;
exports.getAvailableUpdates = getAvailableUpdates;
exports.cleanAvailableUpdates = cleanAvailableUpdates;
@ -793,6 +922,7 @@ define(function (require, exports, module) {
exports.hasDownloadedRegistry = false;
exports.ENABLED = ENABLED;
exports.DISABLED = DISABLED;
exports.START_FAILED = START_FAILED;
exports.LOCATION_DEFAULT = LOCATION_DEFAULT;

View File

@ -63,17 +63,20 @@ define(function (require, exports, module) {
*/
function _performChanges() {
// If an extension was removed or updated, prompt the user to quit Brackets.
var hasRemovedExtensions = ExtensionManager.hasExtensionsToRemove(),
hasUpdatedExtensions = ExtensionManager.hasExtensionsToUpdate();
if (!hasRemovedExtensions && !hasUpdatedExtensions) {
var hasRemovedExtensions = ExtensionManager.hasExtensionsToRemove(),
hasUpdatedExtensions = ExtensionManager.hasExtensionsToUpdate(),
hasDisabledExtensions = ExtensionManager.hasExtensionsToDisable();
if (!hasRemovedExtensions && !hasUpdatedExtensions && !hasDisabledExtensions) {
return;
}
var buttonLabel = Strings.CHANGE_AND_RELOAD;
if (hasRemovedExtensions && !hasUpdatedExtensions) {
if (hasRemovedExtensions && !hasUpdatedExtensions && !hasDisabledExtensions) {
buttonLabel = Strings.REMOVE_AND_RELOAD;
} else if (hasUpdatedExtensions && !hasRemovedExtensions) {
} else if (hasUpdatedExtensions && !hasRemovedExtensions && !hasDisabledExtensions) {
buttonLabel = Strings.UPDATE_AND_RELOAD;
} else if (hasDisabledExtensions && !hasRemovedExtensions && !hasUpdatedExtensions) {
buttonLabel = Strings.DISABLE_AND_RELOAD;
}
var dlg = Dialogs.showModalDialog(
@ -107,62 +110,101 @@ define(function (require, exports, module) {
.text(Strings.PROCESSING_EXTENSIONS)
.append("<span class='spinner inline spin'/>");
ExtensionManager.removeMarkedExtensions()
.done(function () {
ExtensionManager.updateExtensions()
.done(function () {
dlg.close();
CommandManager.execute(Commands.APP_RELOAD);
})
.fail(function (errorArray) {
dlg.close();
// This error case should be very uncommon.
// Just let the user know that we couldn't update
// this extension and log the errors to the console.
var ids = [];
errorArray.forEach(function (errorObj) {
ids.push(errorObj.item);
if (errorObj.error && errorObj.error.forEach) {
console.error("Errors for", errorObj.item);
errorObj.error.forEach(function (error) {
console.error(Package.formatError(error));
});
} else {
console.error("Error for", errorObj.item, errorObj);
}
});
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_ERROR,
Strings.EXTENSION_MANAGER_UPDATE,
StringUtils.format(Strings.EXTENSION_MANAGER_UPDATE_ERROR, ids.join(", "))
).done(function () {
// We still have to reload even if some of the removals failed.
CommandManager.execute(Commands.APP_RELOAD);
});
});
})
var removeExtensionsPromise,
updateExtensionsPromise,
disableExtensionsPromise,
removeErrors,
updateErrors,
disableErrors;
removeExtensionsPromise = ExtensionManager.removeMarkedExtensions()
.fail(function (errorArray) {
removeErrors = errorArray;
});
updateExtensionsPromise = ExtensionManager.updateExtensions()
.fail(function (errorArray) {
updateErrors = errorArray;
});
disableExtensionsPromise = ExtensionManager.disableMarkedExtensions()
.fail(function (errorArray) {
disableErrors = errorArray;
});
Async.waitForAll([removeExtensionsPromise, updateExtensionsPromise, disableExtensionsPromise], true)
.always(function () {
dlg.close();
ExtensionManager.cleanupUpdates();
})
.done(function () {
CommandManager.execute(Commands.APP_RELOAD);
})
.fail(function () {
var ids = [],
dialogs = [];
function nextDialog() {
var dialog = dialogs.shift();
if (dialog) {
Dialogs.showModalDialog(dialog.dialog, dialog.title, dialog.message)
.done(nextDialog);
} else {
// Even in case of error condition, we still have to reload
CommandManager.execute(Commands.APP_RELOAD);
}
}
var ids = [];
errorArray.forEach(function (errorObj) {
ids.push(errorObj.item);
});
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_ERROR,
Strings.EXTENSION_MANAGER_REMOVE,
StringUtils.format(Strings.EXTENSION_MANAGER_REMOVE_ERROR, ids.join(", "))
).done(function () {
// We still have to reload even if some of the removals failed.
CommandManager.execute(Commands.APP_RELOAD);
});
if (removeErrors) {
removeErrors.forEach(function (errorObj) {
ids.push(errorObj.item);
});
dialogs.push({
dialog: DefaultDialogs.DIALOG_ID_ERROR,
title: Strings.EXTENSION_MANAGER_REMOVE,
message: StringUtils.format(Strings.EXTENSION_MANAGER_REMOVE_ERROR, ids.join(", "))
});
}
if (updateErrors) {
// This error case should be very uncommon.
// Just let the user know that we couldn't update
// this extension and log the errors to the console.
ids.length = 0;
updateErrors.forEach(function (errorObj) {
ids.push(errorObj.item);
if (errorObj.error && errorObj.error.forEach) {
console.error("Errors for", errorObj.item);
errorObj.error.forEach(function (error) {
console.error(Package.formatError(error));
});
} else {
console.error("Error for", errorObj.item, errorObj);
}
});
dialogs.push({
dialog: DefaultDialogs.DIALOG_ID_ERROR,
title: Strings.EXTENSION_MANAGER_UPDATE,
message: StringUtils.format(Strings.EXTENSION_MANAGER_UPDATE_ERROR, ids.join(", "))
});
}
if (disableErrors) {
ids.length = 0;
disableErrors.forEach(function (errorObj) {
ids.push(errorObj.item);
});
dialogs.push({
dialog: DefaultDialogs.DIALOG_ID_ERROR,
title: Strings.EXTENSION_MANAGER_DISABLE,
message: StringUtils.format(Strings.EXTENSION_MANAGER_DISABLE_ERROR, ids.join(", "))
});
}
nextDialog();
});
} else {
dlg.close();
ExtensionManager.cleanupUpdates();
ExtensionManager.unmarkAllForRemoval();
ExtensionManager.unmarkAllForDisabling();
}
});
}

View File

@ -181,6 +181,8 @@ define(function (require, exports, module) {
ExtensionManager.markForRemoval($target.attr("data-extension-id"), true);
} else if ($target.hasClass("undo-update")) {
ExtensionManager.removeUpdate($target.attr("data-extension-id"));
} else if ($target.hasClass("undo-disable")) {
ExtensionManager.markForDisabling($target.attr("data-extension-id"), false);
} else if ($target.data("toggle-desc") === "expand-desc") {
this._toggleDescription($target.attr("data-extension-id"), $target, true);
} else if ($target.data("toggle-desc") === "trunc-desc") {
@ -195,6 +197,12 @@ define(function (require, exports, module) {
})
.on("click", "button.remove", function (e) {
ExtensionManager.markForRemoval($(e.target).attr("data-extension-id"), true);
})
.on("click", "button.disable", function (e) {
ExtensionManager.markForDisabling($(e.target).attr("data-extension-id"), true);
})
.on("click", "button.enable", function (e) {
ExtensionManager.enable($(e.target).attr("data-extension-id"));
});
};
@ -221,6 +229,7 @@ define(function (require, exports, module) {
// arrays as iteration contexts.
context.isInstalled = !!entry.installInfo;
context.failedToStart = (entry.installInfo && entry.installInfo.status === ExtensionManager.START_FAILED);
context.disabled = (entry.installInfo && entry.installInfo.status === ExtensionManager.DISABLED);
context.hasVersionInfo = !!info.versions;
if (entry.registryInfo) {
@ -259,7 +268,9 @@ define(function (require, exports, module) {
}
context.isMarkedForRemoval = ExtensionManager.isMarkedForRemoval(info.metadata.name);
context.isMarkedForDisabling = ExtensionManager.isMarkedForDisabling(info.metadata.name);
context.isMarkedForUpdate = ExtensionManager.isMarkedForUpdate(info.metadata.name);
var hasPendingAction = context.isMarkedForDisabling || context.isMarkedForRemoval || context.isMarkedForUpdate;
context.showInstallButton = (this.model.source === this.model.SOURCE_REGISTRY || this.model.source === this.model.SOURCE_THEMES) && !context.updateAvailable;
context.showUpdateButton = context.updateAvailable && !context.isMarkedForUpdate && !context.isMarkedForRemoval;
@ -314,7 +325,11 @@ define(function (require, exports, module) {
}
context.removalAllowed = this.model.source === "installed" &&
!context.failedToStart && !context.isMarkedForUpdate && !context.isMarkedForRemoval;
!context.failedToStart && !hasPendingAction;
context.disablingAllowed = this.model.source === "installed" &&
!context.disabled && !hasPendingAction;
context.enablingAllowed = this.model.source === "installed" &&
context.disabled && !hasPendingAction;
// Copy over helper functions that we share with the registry app.
["lastVersionDate", "authorInfo"].forEach(function (helper) {

View File

@ -432,6 +432,48 @@ define(function (require, exports, module) {
});
}
/**
* Disables the extension at the given path.
*
* @param {string} path The absolute path to the extension to disable.
* @return {$.Promise} A promise that's resolved when the extenion is disabled, or
* rejected if there was an error.
*/
function disable(path) {
var result = new $.Deferred(),
file = FileSystem.getFileForPath(path + "/.disabled");
file.write("", function (err) {
if (err) {
result.reject(err);
} else {
result.resolve();
}
});
return result.promise();
}
/**
* Enables the extension at the given path.
*
* @param {string} path The absolute path to the extension to enable.
* @return {$.Promise} A promise that's resolved when the extenion is enable, or
* rejected if there was an error.
*/
function enable(path) {
var result = new $.Deferred(),
file = FileSystem.getFileForPath(path + "/.disabled");
file.unlink(function (err) {
if (err) {
result.reject(err);
return;
}
ExtensionLoader.loadExtension(FileUtils.getBaseName(path), { baseUrl: path }, "main")
.done(result.resolve)
.fail(result.reject);
});
return result.promise();
}
/**
* Install an extension update located at path.
* This assumes that the installation was previously attempted
@ -500,12 +542,14 @@ define(function (require, exports, module) {
// For unit tests only
exports._getNodeConnectionDeferred = _getNodeConnectionDeferred;
exports.installFromURL = installFromURL;
exports.installFromPath = installFromPath;
exports.validate = validate;
exports.install = install;
exports.remove = remove;
exports.installUpdate = installUpdate;
exports.formatError = formatError;
exports.InstallationStatuses = InstallationStatuses;
exports.installFromURL = installFromURL;
exports.installFromPath = installFromPath;
exports.validate = validate;
exports.install = install;
exports.remove = remove;
exports.disable = disable;
exports.enable = enable;
exports.installUpdate = installUpdate;
exports.formatError = formatError;
exports.InstallationStatuses = InstallationStatuses;
});

View File

@ -51,6 +51,7 @@
{{/translated}}
</td>
<td class="ext-action">
<div>
{{#showInstallButton}}
<button class="btn btn-mini install" data-extension-id="{{metadata.name}}" {{^allowInstall}}disabled{{/allowInstall}}>
{{^isInstalled}}{{Strings.INSTALL}}{{/isInstalled}}
@ -63,6 +64,19 @@
{{Strings.UPDATE}}
</button>
{{/showUpdateButton}}
{{#disablingAllowed}}
{{#showUpdateButton}}
</div><div>
{{/showUpdateButton}}
<button class="btn btn-mini disable" data-extension-id="{{metadata.name}}">
{{Strings.DISABLE}}
</button>
{{/disablingAllowed}}
{{#enablingAllowed}}
<button class="btn btn-mini enable" data-extension-id="{{metadata.name}}">
{{Strings.ENABLE}}
</button>
{{/enablingAllowed}}
{{#removalAllowed}}
<button class="btn btn-mini remove" data-extension-id="{{metadata.name}}" {{^allowRemove}}disabled title="{{Strings.CANT_REMOVE_DEV}}"{{/allowRemove}}>
{{Strings.REMOVE}}
@ -76,9 +90,13 @@
{{#isMarkedForRemoval}}
{{Strings.MARKED_FOR_REMOVAL}} (<a class="undo-remove" data-extension-id="{{metadata.name}}" href="#">{{Strings.UNDO_REMOVE}}</a>)
{{/isMarkedForRemoval}}
{{#isMarkedForDisabling}}
Marked for disabling (<a class="undo-disable" data-extension-id="{{metadata.name}}" href="#">{{Strings.UNDO_DISABLE}}</a>)
{{/isMarkedForDisabling}}
{{#isMarkedForUpdate}}
{{Strings.MARKED_FOR_UPDATE}} (<a class="undo-update" data-extension-id="{{metadata.name}}" href="#">{{Strings.UNDO_UPDATE}}</a>)
{{/isMarkedForUpdate}}
{{/isInstalled}}
</div>
</td>
</tr>

View File

@ -470,6 +470,8 @@ define({
"INSTALL" : "Install",
"UPDATE" : "Update",
"REMOVE" : "Remove",
"DISABLE" : "Disable",
"ENABLE" : "Enable",
"OVERWRITE" : "Overwrite",
"CANT_REMOVE_DEV" : "Extensions in the \"dev\" folder must be manually deleted.",
"CANT_UPDATE" : "The update isn't compatible with this version of {APP_NAME}.",
@ -538,15 +540,20 @@ define({
"EXTENSION_MANAGER_REMOVE_ERROR" : "Unable to remove one or more extensions: {0}. {APP_NAME} will still reload.",
"EXTENSION_MANAGER_UPDATE" : "Update Extension",
"EXTENSION_MANAGER_UPDATE_ERROR" : "Unable to update one or more extensions: {0}. {APP_NAME} will still reload.",
"EXTENSION_MANAGER_DISABLE" : "Disable Extension",
"EXTENSION_MANAGER_DISABLE_ERROR" : "Unable to disable one or more extensions: {0}. {APP_NAME} will still reload.",
"MARKED_FOR_REMOVAL" : "Marked for removal",
"UNDO_REMOVE" : "Undo",
"MARKED_FOR_UPDATE" : "Marked for update",
"UNDO_UPDATE" : "Undo",
"MARKED_FOR_DISABLING" : "Marked for disabling",
"UNDO_DISABLE" : "Undo",
"CHANGE_AND_RELOAD_TITLE" : "Change Extensions",
"CHANGE_AND_RELOAD_MESSAGE" : "To update or remove the marked extensions, {APP_NAME} will need to reload. You'll be prompted to save unsaved changes.",
"CHANGE_AND_RELOAD_MESSAGE" : "To update, remove or disable the marked extensions, {APP_NAME} will need to reload. You'll be prompted to save unsaved changes.",
"REMOVE_AND_RELOAD" : "Remove Extensions and Reload",
"CHANGE_AND_RELOAD" : "Change Extensions and Reload",
"UPDATE_AND_RELOAD" : "Update Extensions and Reload",
"DISABLE_AND_RELOAD" : "Disable Extensions and Reload",
"PROCESSING_EXTENSIONS" : "Processing extension changes\u2026",
"EXTENSION_NOT_INSTALLED" : "Couldn't remove extension {0} because it wasn't installed.",
"NO_EXTENSIONS" : "No extensions installed yet.<br>Click on the Available tab above to get started.",

View File

@ -1525,6 +1525,7 @@ input[type="color"],
box-shadow: inset 0 1px @bc-highlight-hard;
-webkit-font-smoothing: antialiased;
text-shadow: none;
margin: 3px 0px 3px 3px;
.dark & {
background-color: @dark-bc-btn-bg;
@ -1650,7 +1651,6 @@ input[type="color"],
color: @bc-text-alt;
font-weight: @font-weight-semibold;
text-shadow: 0 -1px 0 @bc-shadow-small;
margin-right: 3px;
.dark & {
background-color: @dark-bc-secondary-btn-bg;

View File

@ -242,7 +242,7 @@ define(function (require, exports, module) {
var promise = new $.Deferred();
// Try to load the package.json to figure out if we are loading a theme.
ExtensionUtils.loadPackageJson(config.baseUrl).always(promise.resolve);
ExtensionUtils.loadMetadata(config.baseUrl).always(promise.resolve);
return promise
.then(function (metadata) {
@ -251,12 +251,20 @@ define(function (require, exports, module) {
return;
}
return loadExtensionModule(name, config, entryPoint);
if (!metadata.disabled) {
return loadExtensionModule(name, config, entryPoint);
} else {
return new $.Deferred().reject("disabled").promise();
}
})
.then(function () {
exports.trigger("load", config.baseUrl);
}, function (err) {
exports.trigger("loadFailed", config.baseUrl);
if (err === "disabled") {
exports.trigger("disabled", config.baseUrl);
} else {
exports.trigger("loadFailed", config.baseUrl);
}
});
}

View File

@ -31,7 +31,8 @@
define(function (require, exports, module) {
"use strict";
var FileSystem = require("filesystem/FileSystem"),
var Async = require("utils/Async"),
FileSystem = require("filesystem/FileSystem"),
FileUtils = require("file/FileUtils");
/**
@ -233,26 +234,51 @@ define(function (require, exports, module) {
}
/**
* Loads the package.json file in the given extension folder.
* Loads the package.json file in the given extension folder as well as any additional
* metadata.
*
* If there's a .disabled file in the extension directory, then the content of package.json
* will be augmented with disabled property set to true. It will override whatever value of
* disabled might be set.
*
* @param {string} folder The extension folder.
* @return {$.Promise} A promise object that is resolved with the parsed contents of the package.json file,
* or rejected if there is no package.json or the contents are not valid JSON.
* or rejected if there is no package.json with the boolean indicating whether .disabled file exists.
*/
function loadPackageJson(folder) {
var file = FileSystem.getFileForPath(folder + "/package.json"),
result = new $.Deferred();
FileUtils.readAsText(file)
.done(function (text) {
function loadMetadata(folder) {
var packageJSONFile = FileSystem.getFileForPath(folder + "/package.json"),
disabledFile = FileSystem.getFileForPath(folder + "/.disabled"),
result = new $.Deferred(),
jsonPromise = new $.Deferred(),
disabledPromise = new $.Deferred(),
json,
disabled;
FileUtils.readAsText(packageJSONFile)
.then(function (text) {
try {
var json = JSON.parse(text);
result.resolve(json);
json = JSON.parse(text);
jsonPromise.resolve();
} catch (e) {
result.reject();
jsonPromise.reject();
}
})
.fail(function () {
result.reject();
.fail(jsonPromise.reject);
disabledFile.exists(function (err, exists) {
if (err) {
disabled = false;
} else {
disabled = exists;
}
disabledPromise.resolve();
});
Async.waitForAll([jsonPromise, disabledPromise])
.always(function () {
if (!json) {
result.reject(disabled);
} else {
json.disabled = disabled;
result.resolve(json);
}
});
return result.promise();
}
@ -264,5 +290,5 @@ define(function (require, exports, module) {
exports.getModuleUrl = getModuleUrl;
exports.loadFile = loadFile;
exports.loadStyleSheet = loadStyleSheet;
exports.loadPackageJson = loadPackageJson;
exports.loadMetadata = loadMetadata;
});

View File

@ -58,7 +58,7 @@ define(function (require, exports, module) {
describe("ExtensionManager", function () {
var mockId, mockSettings, origRegistryURL, origExtensionUrl, removedPath,
view, model, fakeLoadDeferred, modelDisposed;
view, model, fakeLoadDeferred, modelDisposed, disabledFilePath;
beforeEach(function () {
// Use fake URLs for the registry (useful if the registry isn't actually currently
@ -94,6 +94,17 @@ define(function (require, exports, module) {
removedPath = path;
return new $.Deferred().resolve().promise();
});
// Fake enabling/disabling
disabledFilePath = null;
spyOn(Package, "disable").andCallFake(function (path) {
disabledFilePath = path + "/.disabled";
return new $.Deferred().resolve().promise();
});
spyOn(Package, "enable").andCallFake(function (path) {
disabledFilePath = path + "/.disabled";
return new $.Deferred().resolve().promise();
});
});
afterEach(function () {
@ -105,7 +116,14 @@ define(function (require, exports, module) {
});
function mockLoadExtensions(names, fail) {
var numStatusChanges = 0;
var numStatusChanges = 0,
shouldFail = false,
shouldDisable = false;
if (typeof fail === "boolean") {
shouldFail = true;
} else if (typeof fail === "string") {
shouldDisable = true;
}
runs(function () {
ExtensionManager.on("statusChange.mock-load", function () {
numStatusChanges++;
@ -113,7 +131,7 @@ define(function (require, exports, module) {
var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
names = names || ["default/mock-extension-1", "dev/mock-extension-2", "user/mock-legacy-extension"];
names.forEach(function (name) {
ExtensionLoader.trigger(fail ? "loadFailed" : "load", mockPath + "/" + name);
ExtensionLoader.trigger(shouldFail ? "loadFailed" : (shouldDisable ? "disabled" : "load"), mockPath + "/" + name);
});
});
@ -302,6 +320,16 @@ define(function (require, exports, module) {
});
});
it("should list an extension that is installed but disabled", function () {
runs(function () {
waitsForDone(ExtensionManager.downloadRegistry(), "loading registry");
});
mockLoadExtensions(["user/mock-extension-3"], "disabled");
runs(function () {
expect(ExtensionManager.extensions["mock-extension-3"].installInfo.status).toEqual(ExtensionManager.DISABLED);
});
});
it("should set the title for a legacy extension based on its folder name", function () {
mockLoadExtensions();
runs(function () {
@ -357,6 +385,40 @@ define(function (require, exports, module) {
});
});
it("should disable an extension and raise a statusChange event", function () {
var spy = jasmine.createSpy();
runs(function () {
mockLoadExtensions(["user/mock-extension-3"]);
});
runs(function () {
ExtensionManager.on("statusChange.unit-test", spy);
waitsForDone(ExtensionManager.disable("mock-extension-3"));
});
runs(function () {
var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
expect(disabledFilePath).toBe(mockPath + "/user/mock-extension-3" + "/.disabled");
expect(spy).toHaveBeenCalledWith(jasmine.any(Object), "mock-extension-3");
expect(ExtensionManager.extensions["mock-extension-3"].installInfo.status).toEqual(ExtensionManager.DISABLED);
});
});
it("should enable an extension and raise a statusChange event", function () {
var spy = jasmine.createSpy();
runs(function () {
mockLoadExtensions(["user/mock-extension-2"], "disable");
});
runs(function () {
ExtensionManager.on("statusChange.unit-test", spy);
waitsForDone(ExtensionManager.enable("mock-extension-2"));
});
runs(function () {
var mockPath = SpecRunnerUtils.getTestPath("/spec/ExtensionManager-test-files");
expect(disabledFilePath).toBe(mockPath + "/user/mock-extension-2" + "/.disabled");
expect(spy).toHaveBeenCalledWith(jasmine.any(Object), "mock-extension-2");
expect(ExtensionManager.extensions["mock-extension-2"].installInfo.status).toEqual(ExtensionManager.ENABLED);
});
});
it("should fail when trying to remove an extension that's not installed", function () {
var finished = false;
runs(function () {
@ -372,6 +434,36 @@ define(function (require, exports, module) {
waitsFor(function () { return finished; }, "finish removal");
});
it("should fail when trying to disable an extension that's not installed", function () {
var finished = false;
runs(function () {
ExtensionManager.disable("mock-extension-3")
.done(function () {
finished = true;
expect("tried to disable a nonexistent extension").toBe(false);
})
.fail(function () {
finished = true;
});
});
waitsFor(function () { return finished; }, "finish disabling");
});
it("should fail when trying to enable an extension that's not installed", function () {
var finished = false;
runs(function () {
ExtensionManager.enable("mock-extension-3")
.done(function () {
finished = true;
expect("tried to enable a nonexistent extension").toBe(false);
})
.fail(function () {
finished = true;
});
});
waitsFor(function () { return finished; }, "finish enabling");
});
it("should calculate compatibility info for installed extensions", function () {
function fakeEntry(version) {
return { metadata: { engines: { brackets: version } } };
@ -775,6 +867,13 @@ define(function (require, exports, module) {
});
});
it("should include a newly-installed disabled extension", function () {
mockLoadExtensions(["user/another-great-extension"], "disabled");
runs(function () {
expect(model.filterSet.indexOf("another-great-extension")).toBe(1);
});
});
it("should raise an event when an extension is installed", function () {
var calledId;
runs(function () {
@ -788,6 +887,19 @@ define(function (require, exports, module) {
});
});
it("should raise an event when an extension is disabled", function () {
var calledId;
runs(function () {
model.on("change", function (e, id) {
calledId = id;
});
});
mockLoadExtensions(["user/another-great-extension"], "disabled");
runs(function () {
expect(calledId).toBe("another-great-extension");
});
});
it("should not include a removed extension", function () {
runs(function () {
waitsForDone(ExtensionManager.remove("registered-extension"));
@ -860,6 +972,74 @@ define(function (require, exports, module) {
});
});
it("should mark an extension for disabling and raise an event", function () {
var id = "registered-extension", calledId;
runs(function () {
model.on("change", function (e, id) {
calledId = id;
});
ExtensionManager.markForDisabling(id, true);
expect(calledId).toBe(id);
expect(ExtensionManager.isMarkedForDisabling(id)).toBe(true);
expect(model.filterSet.indexOf(id)).not.toBe(-1);
expect(ExtensionManager.hasExtensionsToDisable()).toBe(true);
});
});
it("should unmark an extension previously marked for disabling and raise an event", function () {
var id = "registered-extension", calledId;
runs(function () {
ExtensionManager.markForDisabling(id, true);
model.on("change", function (e, id) {
calledId = id;
});
ExtensionManager.markForDisabling(id, false);
expect(calledId).toBe(id);
expect(ExtensionManager.isMarkedForRemoval(id)).toBe(false);
expect(ExtensionManager.hasExtensionsToRemove()).toBe(false);
});
});
it("should disable extensions previously marked for disabling and not remove them from the model", function () {
var disabledIds = {}, disabledPaths = {};
runs(function () {
ExtensionManager.markForDisabling("registered-extension", true);
ExtensionManager.markForDisabling("Z-capital-extension", false);
model.on("change", function (e, id) {
disabledIds[id] = true;
disabledPaths[disabledFilePath] = true;
});
waitsForDone(ExtensionManager.disableMarkedExtensions());
});
runs(function () {
// Test the enabled extension, the extension that was unmarked for disabling, and an extension that was never marked.
expect(disabledIds["registered-extension"]).toBe(true);
expect(disabledPaths["/path/to/extensions/user/registered-extension/.disabled"]).toBe(true);
expect(model.filterSet.indexOf("registered-extension")).toBe(0);
expect(disabledIds["Z-capital-extension"]).toBeUndefined();
expect(disabledPaths["/path/to/extensions/user/Z-capital-extension/.disabled"]).toBeUndefined();
expect(disabledIds["unregistered-extension"]).toBeUndefined();
expect(disabledPaths["/path/to/extensions/user/unregistered-extension/.disabled"]).toBeUndefined();
});
});
it("should delete the .disabled file, enable the extension and raise an event", function () {
var extension = "registered-extension",
calledId;
runs(function () {
mockLoadExtensions(["registered-extension"], "disabled");
model.on("change", function (e, id) {
calledId = id;
});
});
runs(function () {
waitsForDone(ExtensionManager.enable(extension));
});
runs(function () {
expect(calledId).toBe(extension);
});
});
it("should mark an extension for update and raise an event", function () {
var id = "registered-extension", calledId;
runs(function () {
@ -1429,26 +1609,35 @@ define(function (require, exports, module) {
});
});
it("should show only items that are already installed and have a remove button for each", function () {
it("should show only items that are already installed and have remove and disable/enable buttons for each", function () {
mockLoadExtensions(["user/mock-extension-3", "user/mock-extension-4", "user/mock-legacy-extension"]);
mockLoadExtensions(["user/mock-extension-5"], "disabled");
setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
runs(function () {
expect($(".empty-message", view.$el).css("display")).toBe("none");
expect($("table", view.$el).css("display")).not.toBe("none");
_.forEach(mockRegistry, function (item) {
var $button = $("button.remove[data-extension-id=" + item.metadata.name + "]", view.$el);
var $removeButton = $("button.remove[data-extension-id=" + item.metadata.name + "]", view.$el);
if (item.metadata.name === "mock-extension-3" ||
item.metadata.name === "mock-extension-4" ||
item.metadata.name === "mock-legacy-extension") {
item.metadata.name === "mock-legacy-extension" ||
item.metadata.name === "mock-extension-5") {
expect(view).toHaveText(item.metadata.name);
expect($button.length).toBe(1);
expect($removeButton.length).toBe(1);
// should also have disable/enable button
var isDisable = item.metadata.name === "mock-extension-5" ? false : true,
$disableButton = $("button." + (isDisable ? "disable" : "enable") + "[data-extension-id=" +
item.metadata.name + "]", view.$el);
expect($disableButton.length).toBe(1);
// But no update button
var $updateButton = $("button.update[data-extension-id=" + item.metadata.name + "]", view.$el);
expect($updateButton.length).toBe(0);
} else {
expect(view).not.toHaveText(item.metadata.name);
expect($button.length).toBe(0);
expect($removeButton.length).toBe(0);
}
});
@ -1515,18 +1704,71 @@ define(function (require, exports, module) {
});
});
// Disable button action
it("should mark the given extension for disabling, hide the buttons and show the undo link", function () {
mockLoadExtensions(["user/mock-extension-3"]);
setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
runs(function () {
var $disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el),
$removeButton,
$undoLink;
$disableButton.click();
$removeButton = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
$undoLink = $("a.undo-disable[data-extension-id=mock-extension-3]", view.$el);
$disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el);
expect($removeButton.length).toBe(0);
expect($undoLink.length).toBe(1);
expect($disableButton.length).toBe(0);
expect(ExtensionManager.isMarkedForDisabling("mock-extension-3")).toBe(true);
});
});
it("should undo mark for disabling and make the buttons available again", function () {
mockLoadExtensions(["user/mock-extension-3"]);
setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
runs(function () {
var $disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el),
$removeButton,
$undoLink;
$disableButton.click();
$undoLink = $("a.undo-disable[data-extension-id=mock-extension-3]", view.$el);
$undoLink.click();
$removeButton = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
$disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el);
$undoLink = $("a.undo-disable[data-extension-id=mock-extension-3]", view.$el);
expect($removeButton.length).toBe(1);
expect($undoLink.length).toBe(0);
expect($disableButton.length).toBe(1);
expect(ExtensionManager.isMarkedForDisabling("mock-extension-3")).toBe(false);
});
});
it("should be able to disable an extension from the dev folder", function () {
mockLoadExtensions(["dev/mock-extension-6"]);
setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
runs(function () {
var $disableButton = $("button.disable[data-extension-id=mock-extension-6]", view.$el);
expect($disableButton.prop("disabled")).toBeFalsy();
$disableButton.click();
expect(ExtensionManager.isMarkedForDisabling("mock-extension-6")).toBe(true);
});
});
// 'Remove' button action
it("should mark the given extension for removal, hide the remove button, and show an undo link", function () {
mockLoadExtensions(["user/mock-extension-3"]);
setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
runs(function () {
var $button = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
$button.click();
var $removeButton = $("button.remove[data-extension-id=mock-extension-3]", view.$el),
$disableButton;
$removeButton.click();
$disableButton = $("button.disable[data-extension-id=mock-extension-3]", view.$el);
expect(ExtensionManager.isMarkedForRemoval("mock-extension-3")).toBe(true);
expect($disableButton.length).toBe(0);
var $undoLink = $("a.undo-remove[data-extension-id=mock-extension-3]", view.$el);
expect($undoLink.length).toBe(1);
$button = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
expect($button.length).toBe(0);
$removeButton = $("button.remove[data-extension-id=mock-extension-3]", view.$el);
expect($removeButton.length).toBe(0);
});
});
@ -1837,6 +2079,34 @@ define(function (require, exports, module) {
});
});
it("should not show a disabling confirmation dialog if an extension was marked and then unmarked", function () {
mockLoadExtensions(["user/mock-extension-3"]);
setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
runs(function () {
var $button = $("button.disable[data-extension-id=mock-extension-3]", view.$el),
$undoLink;
$button.click();
$undoLink = $("a.undo-disable[data-extension-id=mock-extension-3]");
$undoLink.click();
ExtensionManagerDialog._performChanges();
expect(dialogClassShown).toBeFalsy();
});
});
it("should show a disabling confirmation dialog if an extension was marked for disabling", function () {
mockLoadExtensions(["user/mock-extension-3"]);
setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel);
runs(function () {
var $button = $("button.disable[data-extension-id=mock-extension-3]", view.$el);
$button.click();
});
runs(function () {
ExtensionManagerDialog._performChanges();
expect(dialogClassShown).toBe("change-marked-extensions");
$mockDlg.triggerHandler("buttonClick", Dialogs.DIALOG_BTN_CANCEL);
});
});
it("should update extensions and quit if the user hits Update and Quit on the removal confirmation dialog", function () {
var id = "mock-extension-3",
filename = "/path/to/downloaded/mock-extension-3.zip";