(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
    typeof define === 'function' && define.amd ? define(['exports'], factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Djblets = global.Djblets || {}));
})(this, (function (exports) { 'use strict';

    var _class$1, _class2$1, _class3$1;
    /**
     * Extension management support.
     */





    /**
     * Attributes for information on an installed extension.
     *
     * Version Added:
     *     4.0
     */

    /**
     * Attributes for controlling the extension manager.
     *
     * Version Added:
     *     4.0
     */
    /**
     * Represents an installed extension listed in the Manage Extensions list.
     *
     * This stores the various information about the extension that we'll display
     * to the user, and offers actions for enabling or disabling the extension.
     */
    let InstalledExtension = Spina.spina(_class$1 = class InstalledExtension extends Spina.BaseModel {
      static defaults = {
        author: null,
        authorURL: null,
        configURL: null,
        dbURL: null,
        enabled: false,
        loadError: null,
        loadable: true,
        name: null,
        summary: null,
        version: null
      };

      /**
       * Enable the extension.
       *
       * This will submit a request to the server to enable this extension.
       *
       * Returns:
       *     Promise:
       *     A promise that will be resolved when the request to enable the
       *     extension completes.
       */
      enable() {
        return new Promise((resolve, reject) => {
          this.save({
            enabled: true
          }, {
            wait: true,
            error: (model, xhr) => {
              this.set({
                canEnable: !xhr.errorRsp.needs_reload,
                loadError: xhr.errorRsp.load_error,
                loadable: false
              });
              reject(new Error(xhr.errorText));
            },
            success: () => resolve()
          });
        });
      }

      /**
       * Disable the extension.
       *
       * This will submit a request to the server to disable this extension.
       *
       * Returns:
       *     Promise:
       *     A promise that will be resolved when the request to enable the
       *     extension completes.
       */
      disable() {
        return new Promise((resolve, reject) => {
          this.save({
            enabled: false
          }, {
            wait: true,
            error: xhr => reject(new Error(xhr.errorText)),
            success: () => resolve()
          });
        });
      }

      /**
       * Return a JSON payload for requests sent to the server.
       *
       * Returns:
       *     object:
       *     A payload that will be serialized for making the API request.
       */
      toJSON() {
        return {
          enabled: this.get('enabled')
        };
      }

      /**
       * Parse a JSON payload from the server.
       *
       * Args:
       *     rsp (object):
       *         The payload from the server.
       *
       * Returns:
       *     object:
       *     The parsed response.
       */
      parse(rsp) {
        if (rsp.stat !== undefined) {
          rsp = rsp.extension;
        }
        const id = rsp.class_name;
        const configLink = rsp.links['admin-configure'];
        const dbLink = rsp.links['admin-database'];
        this.url = `${this.collection.url}${id}/`;
        return {
          author: rsp.author,
          authorURL: rsp.author_url,
          canDisable: rsp.can_disable,
          canEnable: rsp.can_enable,
          configURL: configLink ? configLink.href : null,
          dbURL: dbLink ? dbLink.href : null,
          enabled: rsp.enabled,
          id: id,
          loadError: rsp.load_error,
          loadable: rsp.loadable,
          name: rsp.name,
          summary: rsp.summary,
          version: rsp.version
        };
      }

      /**
       * Perform AJAX requests against the server-side API.
       *
       * Args:
       *     method (string):
       *         The HTTP method to use.
       *
       *     model (InstalledExtension):
       *         The extension object being synced.
       *
       *     options (object):
       *         Options for the sync operation.
       */
      sync(method, model, options) {
        return Backbone.sync.call(this, method, model, _.defaults({
          contentType: 'application/x-www-form-urlencoded',
          data: model.toJSON(),
          processData: true,
          error: (xhr, textStatus, errorThrown) => {
            let rsp;
            let text;
            try {
              rsp = $.parseJSON(xhr.responseText);
              text = rsp.err.msg;
            } catch (e) {
              text = 'HTTP ' + xhr.status + ' ' + xhr.statusText;
              rsp = {
                canEnable: false,
                loadError: text
              };
            }
            if (_.isFunction(options.error)) {
              xhr.errorText = text;
              xhr.errorRsp = rsp;
              options.error(xhr, textStatus, errorThrown);
            }
          }
        }, options));
      }
    }) || _class$1;
    /**
     * A collection of installed extensions.
     *
     * This stores the list of installed extensions, and allows fetching from
     * the API.
     */
    let InstalledExtensionCollection = Spina.spina(_class2$1 = class InstalledExtensionCollection extends Spina.BaseCollection {
      static model = InstalledExtension;

      /**
       * Parse the response from the server.
       *
       * Args:
       *     rsp (object):
       *         The response from the server.
       *
       * Returns:
       *     object:
       *     The parsed data from the response.
       */
      parse(rsp) {
        return rsp.extensions;
      }
    }) || _class2$1;
    /**
     * Manages installed extensions.
     *
     * This stores a collection of installed extensions, and provides
     * functionality for loading the current list from the server.
     */
    let ExtensionManager = Spina.spina(_class3$1 = class ExtensionManager extends Spina.BaseModel {
      static defaults = {
        apiRoot: null
      };

      /**********************
       * Instance variables *
       **********************/

      /**
       * A collection of all installed extensions.
       */

      /**
       * Initialize the manager.
       */
      initialize() {
        this.installedExtensions = new InstalledExtensionCollection();
        this.installedExtensions.url = this.get('apiRoot');
      }

      /**
       * Load the extensions list.
       */
      load() {
        this.trigger('loading');
        this.installedExtensions.fetch({
          success: () => this.trigger('loaded')
        });
      }
    }) || _class3$1;

    var _class, _class2, _class3;
    /**
     * Displays the interface showing all installed extensions.
     */





    /**
     * Attributes for the ExtensionItem model.
     *
     * Version Added:
     *     5.0
     */
    /**
     * An item in the list of registered extensions.
     *
     * This will contain information on the extension and actions for toggling
     * the enabled state, reloading the extension, or configuring the extension.
     */
    let ExtensionItem = Spina.spina(_class = class ExtensionItem extends Djblets.ConfigFormsListItem {
      static defaults = {
        extension: null
      };

      /**
       * Initialize the item.
       *
       * This will set up the initial state and then listen for any changes
       * to the extension's state (caused by enabling/disabling/reloading the
       * extension).
       *
       * Args:
       *     attributes (ListItemConstructorAttrs, optional):
       *         Attributes for the model.
       */
      initialize(attributes = {}) {
        super.initialize(attributes);
        this.#updateActions();
        this.#updateItemState();
        this.listenTo(this.get('extension'), 'change:loadable change:loadError change:enabled', () => {
          this.#updateItemState();
          this.#updateActions();
        });
      }

      /**
       * Update the actions for the extension.
       *
       * If the extension is disabled, this will add an Enabled action.
       *
       * If it's enabled, but has a load error, it will add a Reload action.
       *
       * If it's enabled, it will provide actions for Configure and Database,
       * if enabled by the extension, along with a Disable action.
       */
      #updateActions() {
        const extension = this.get('extension');
        const actions = [];
        if (!extension) {
          return;
        }
        if (!extension.get('loadable')) {
          /* Add an action for reloading the extension. */
          actions.push({
            id: 'reload',
            label: gettext("Reload")
          });
        } else if (extension.get('enabled')) {
          /*
           * Show all the actions for enabled extensions.
           *
           * Note that the order used is here to ensure visual alignment
           * for most-frequently-used options.
           */
          const configURL = extension.get('configURL');
          const dbURL = extension.get('dbURL');
          if (dbURL) {
            actions.push({
              id: 'database',
              label: gettext("Database"),
              url: dbURL
            });
          }
          if (configURL) {
            actions.push({
              id: 'configure',
              label: gettext("Configure"),
              primary: true,
              url: configURL
            });
          }
          if (extension.get('canDisable')) {
            actions.push({
              danger: true,
              id: 'disable',
              label: gettext("Disable")
            });
          }
        } else {
          /* Add an action for enabling a disabled extension. */
          if (extension.get('canEnable')) {
            actions.push({
              id: 'enable',
              label: gettext("Enable"),
              primary: true
            });
          }
        }
        this.setActions(actions);
      }

      /**
       * Update the state of this item.
       *
       * This will set the "error", "enabled", or "disabled" state of the
       * item, depending on the corresponding state in the extension.
       */
      #updateItemState() {
        const extension = this.get('extension');
        let itemState;
        if (!extension.get('loadable')) {
          itemState = 'error';
        } else if (extension.get('enabled')) {
          itemState = 'enabled';
        } else {
          itemState = 'disabled';
        }
        this.set('itemState', itemState);
      }
    }) || _class;
    /**
     * Displays an extension in the Manage Extensions list.
     *
     * This will show information about the extension, and provide links for
     * enabling/disabling the extension, and (depending on the extension's
     * capabilities) configuring it or viewing its database.
     */
    let ExtensionItemView = Spina.spina(_class2 = class ExtensionItemView extends Djblets.ConfigFormsTableItemView {
      static className = 'djblets-c-extension-item djblets-c-config-forms-list__item';
      static actionHandlers = {
        'disable': '_onDisableClicked',
        'enable': '_onEnableClicked',
        'reload': '_onReloadClicked'
      };
      static template = _.template(`<td class="djblets-c-config-forms-list__item-main">
 <div class="djblets-c-extension-item__header">
  <h3 class="djblets-c-extension-item__name"><%- name %></h3>
  <span class="djblets-c-extension-item__version"><%- version %></span>
  <div class="djblets-c-extension-item__author">
   <% if (authorURL) { %>
    <a href="<%- authorURL %>"><%- author %></a>
   <% } else { %>
    <%- author %>
   <% } %>
  </div>
 </div>
 <p class="djblets-c-extension-item__description">
  <%- summary %>
 </p>
 <% if (!loadable) { %>
  <pre class="djblets-c-extension-item__load-error"><%-
    loadError %></pre>
 <% } %>
</td>
<td class="djblets-c-config-forms-list__item-state"></td>
<td></td>`);

      /**
       * Return context data for rendering the item's template.
       *
       * Returns:
       *     object:
       *     Context data for the render.
       */
      getRenderContext() {
        return this.model.get('extension').attributes;
      }

      /**
       * Handle a click on the Disable action.
       *
       * This will make an asynchronous request to disable the extension.
       *
       * Returns:
       *     Promise:
       *     A promise for the disable request. This will resolve once the
       *     API has handled the request.
       */
      _onDisableClicked() {
        return this.model.get('extension').disable().catch(error => {
          alert(interpolate(gettext("Failed to disable the extension: %(value1)s."), {
            "value1": error.message
          }, true));
        });
      }

      /**
       * Handle a click on the Enable action.
       *
       * This will make an asynchronous request to enable the extension.
       *
       * Returns:
       *     Promise:
       *     A promise for the enable request. This will resolve once the
       *     API has handled the request.
       */
      _onEnableClicked() {
        return this.model.get('extension').enable().catch(error => {
          alert(interpolate(gettext("Failed to enable the extension: %(value1)s."), {
            "value1": error.message
          }, true));
        });
      }

      /**
       * Handle a click on the Reload action.
       *
       * This will trigger an event on the item that tells the extension
       * manager to perform a full reload of all extensions, this one included.
       *
       * Returns:
       *     Promise:
       *     A promise for the enable request. This will never resolve, in
       *     practice, but is returned to enable the action's spinner until
       *     the page reloads.
       */
      _onReloadClicked() {
        return new Promise(() => this.model.trigger('needsReload'));
      }
    }) || _class2;
    /**
     * Displays the interface showing all installed extensions.
     *
     * This loads the list of installed extensions and displays each in a list.
     */
    let ExtensionManagerView = Spina.spina(_class3 = class ExtensionManagerView extends Spina.BaseView {
      static events = {
        'click .djblets-c-extensions__reload': '_reloadFull'
      };

      /**********************
       * Instance variables *
       **********************/

      /** The extension list model. */

      /** The extension list view. */

      /**
       * Initialize the view.
       */
      initialize() {
        this.list = new Djblets.ConfigFormsList({}, {
          collection: new Djblets.ConfigFormsListItems([], {
            model: ExtensionItem
          })
        });
      }

      /**
       * Render the view.
       */
      onInitialRender() {
        const model = this.model;
        const list = this.list;
        this.listView = new Djblets.ConfigFormsTableView({
          ItemView: ExtensionItemView,
          el: this.$('.djblets-c-config-forms-list'),
          model: list
        });
        this.listView.render().$el.removeAttr('aria-busy').addClass('-all-items-are-multiline');
        this.listenTo(model, 'loading', () => list.collection.reset());
        this.listenTo(model, 'loaded', this._onLoaded);
        model.load();
      }

      /**
       * Handler for when the list of extensions is loaded.
       *
       * Renders each extension in the list. If the list is empty, this will
       * display that there are no extensions installed.
       */
      _onLoaded() {
        const items = this.list.collection;
        this.model.installedExtensions.each(extension => {
          const item = items.add({
            extension: extension
          });
          this.listenTo(item, 'needsReload', this._reloadFull);
        });
      }

      /**
       * Perform a full reload of the list of extensions on the server.
       *
       * This submits our form, which is set in the template to tell the
       * ExtensionManager to do a full reload.
       */
      _reloadFull() {
        this.el.submit();
      }
    }) || _class3;

    exports.ExtensionManager = ExtensionManager;
    exports.ExtensionManagerView = ExtensionManagerView;

}));
//# sourceMappingURL=index.js.map
