Source: src/js/views/metadataView/DatasetControlsView.js

define([
  "jquery",
  "backbone",
  "semantic",
  "models/MetricsModel",
  "views/MetricView",
  "views/citations/CitationModalView",
  "common/Utilities",
], (
  $,
  Backbone,
  Semantic,
  MetricsModel,
  MetricView,
  CitationModalView,
  Utilities,
) => {
  "use strict";

  // Semantic UI variation class names.
  const SEM_VARIATIONS = Semantic.CLASS_NAMES.variations;

  // CSS class names used in this view, grouped by semantic purpose.
  const CLASS_NAMES = {
    // Containers for each button/metric
    analyzeContainer: "dataset-analyze-container",
    citeContainer: "dataset-cite-container",
    notificationsContainer: "dataset-notifications-container",
    publishContainer: "dataset-publish-container",
    editContainer: "dataset-edit-metadata-container",
    downloadsContainer: "dataset-downloads-container",
    viewsContainer: "dataset-views-container",
    citationsContainer: "dataset-citations-container",
    reportsContainer: "dataset-reports-container",

    // Containers to separate button types
    metricsContainer: "dataset-metrics-container",
    actionsContainer: "dataset-actions-container",

    // Visual separator
    separator: "dataset-metrics-actions-separator",

    // share Feature is added to share/upload/edit/login features so repo can
    // easily hide them and make the repository ~ "read only"
    shareFeature: "share-feature",

    // Bootstrap classes
    icon: "icon",
    button: "btn",
    primaryButton: "btn-primary",
    dropdown: "dropdown",
    dropdownMenu: "dropdown-menu",
    caret: "caret",

    // Borrowed from MetricView for consistent styling
    metricButton: "metrics",
    buttonLink: "btn-link",
    buttonLinkNeutral: "btn-link--neutral",

    // Other
    iconOnLeft: "icon-on-left",
    hideBelow1200: "hide-below-1200",
    hideAbove1200: "hide-above-1200",
  };

  // Helper to get AppModel properties
  const APP_GET = (prop) => MetacatUI.appModel.get(prop);

  /**
   * @typedef {object} ButtonConfig
   * @property {string} [icon] - Icon name to display on the button.
   * @property {string} [text] - Button label text.
   * @property {string} [id] - Optional DOM id for the button element.
   * @property {string[]} [classes] - Additional CSS classes for styling.
   * @property {string} [tooltip] - Tooltip content.
   * @property {boolean} [dropdown] - True if the button renders a dropdown.
   * @property {string} container - CSS class name of the container element.
   * @property {string} render - Name of the render method on the view.
   */

  /**
   * @class DatasetControls
   * @classdesc A view that displays dataset metrics (views, downloads,
   * citations) and action buttons (download, share, cite) for a dataset
   * metadata view.
   * @classcategory Views/MetadataView
   * @augments Backbone.View
   * @class
   * @screenshot views/metadataView/DatasetControls.png // TODO
   * @since 2.36.0
   */
  const DatasetControls = Backbone.View.extend(
    /** @lends DatasetControls.prototype */
    {
      /**
       * Button configuration map keyed by logical button name.
       * @type {Object<string, ButtonConfig>}
       */
      buttons: {
        mdq: {
          icon: "dashboard",
          text: "Assessment Reports",
          textOnSmallScreen: "Reports",
          singularText: "Assessment Report",
          singularTextOnSmallScreen: "Report",
          classes: [
            CLASS_NAMES.metricButton,
            CLASS_NAMES.buttonLink,
            CLASS_NAMES.buttonLinkNeutral,
          ],
          tooltip:
            "View detailed Assessment Reports about this dataset's metadata and file information.",
          container: CLASS_NAMES.reportsContainer,
          render: "renderMDQ",
        },
        wholetale: {
          icon: "bar-chart",
          text: "Analyze",
          classes: [],
          tooltip:
            "Choose an analysis environment to interactively explore this dataset online using Whole Tale.",
          dropdown: true,
          container: CLASS_NAMES.analyzeContainer,
          render: "renderWholetale",
        },
        cite: {
          icon: "copy",
          text: "Cite",
          id: "cite-this-dataset-btn",
          classes: [],
          tooltip: "View and copy the citation for this dataset.",
          container: CLASS_NAMES.citeContainer,
          render: "renderCite",
        },
        edit: {
          icon: "pencil",
          text: "Edit",
          classes: [CLASS_NAMES.primaryButton],
          tooltip: "Edit this dataset's metadata.",
          container: CLASS_NAMES.editContainer,
          render: "renderEdit",
        },
        publish: {
          icon: "star",
          text: "Publish with DOI",
          textOnSmallScreen: "Publish",
          classes: [CLASS_NAMES.primaryButton],
          id: "publish",
          tooltip:
            "Publish this dataset's metadata with a DOI (Digital Object Identifier).",
          container: CLASS_NAMES.publishContainer,
          render: "renderPublish",
        },
        // Metrics render their own buttons, but we still configure the
        // containers here.
        downloads: {
          container: CLASS_NAMES.downloadsContainer,
          render: "renderDownloads",
        },
        views: {
          container: CLASS_NAMES.viewsContainer,
          render: "renderViews",
        },
        citations: {
          container: CLASS_NAMES.citationsContainer,
          render: "renderCitations",
        },
        // placeholder for future implementation
        notifications: {
          container: CLASS_NAMES.notificationsContainer,
          render: "renderNotifications",
          text: "Watch",
          icon: "bell",
          tooltip: "Be notified of changes to this dataset.",
        },
      },

      /**
       * References to rendered button root elements keyed by name. These will
       * be added during render().
       * @type {Object<string, HTMLElement>}
       */
      buttonEls: {},

      /**
       * Subviews created by this view (MetricView, CitationModalView, etc.).
       * These will be added during render().
       * @type {Backbone.View[]}
       */
      subviews: [],

      /**
       * Render the container structure for metrics and actions.
       * @returns {string} HTML string for the view root content.
       */
      template() {
        return `
          <span class="${CLASS_NAMES.metricsContainer}">
            <span class="${CLASS_NAMES.downloadsContainer}"></span>
            <span class="${CLASS_NAMES.viewsContainer}"></span>
            <span class="${CLASS_NAMES.citationsContainer}"></span>
            <span class="${CLASS_NAMES.separator}"></span>
            <span class="${CLASS_NAMES.reportsContainer}"></span>
          </span>
          <span class="${CLASS_NAMES.actionsContainer}">
            <span class="${CLASS_NAMES.notificationsContainer}"></span>
            <span class="${CLASS_NAMES.analyzeContainer}"></span>
            <span class="${CLASS_NAMES.citeContainer}"></span>
            <span class="${CLASS_NAMES.publishContainer}"></span>
            <span class="${CLASS_NAMES.editContainer}"></span>
          </span>
        `;
      },

      /**
       * Settings passed to the Formantic UI popup module to configure a tooltip
       * shown over the metric button.
       * @see https://fomantic-ui.com/modules/popup.html#/settings
       * @type {object|boolean}
       * @since 2.36.0
       */
      tooltipSettings: {
        variation: `${SEM_VARIATIONS.mini} ${SEM_VARIATIONS.inverted}`,
        position: "top center",
        on: "hover",
        hoverable: true,
        delay: {
          show: 500,
          hide: 40,
        },
      },

      /** @inheritdoc */
      events() {
        const events = {};
        events[`click #${this.buttons.cite.id}`] = "openCitationModal";
        events[`click #${this.buttons.publish.id}`] = "publish";
        return events;
      },

      /**
       * Initialize the DatasetControls view.
       * @param {object} [options] - Options to configure the view.
       * @param {string} [options.pid] - Dataset identifier for this view.
       * @param {SolrResults|EML211} [options.metadataModel] - Associated
       * metadata model.
       * @param {boolean} [options.hasWritePermission] - Whether the current
       * user has write permission for the dataset.
       * @param {Function} [options.publishMethod] - Function to publish the
       * dataset. It must return a Promise that resolves when publishing is
       * complete, throwing an error if publishing fails.
       */
      initialize(options = {}) {
        const { metadataModel } = options;

        const pid =
          options.pid ||
          metadataModel?.get("id") ||
          MetacatUI.appModel.get("pid");

        // Immediately create the metricsModel so external views like the
        // CanoncialDatasetHandlerView can access it.
        const metricsModel =
          options.metricsModel ||
          new MetricsModel({
            pid_list: [pid],
            type: "dataset",
          });

        /**
         * @typedef {object} DatasetControlsViewModel
         * @property {string} [pid] - The dataset identifier for this page.
         * @property {SolrResults|EML211} [metadataModel] - The associated metadata model.
         * @property {Function} [publishMethod] - Method to publish the dataset.
         * @property {boolean} [hasWritePermission] - Whether the current user has
         * write permission for the dataset.
         * @property {MetricsModel} [metricsModel] - The metrics model for this dataset.
         */
        this.viewModel = new Backbone.Model({
          pid,
          metadataModel: options.metadataModel,
          publishMethod: options.publishMethod,
          hasWritePermission: options.hasWritePermission === true,
          metricsModel,
        });
      },

      /**
       * Render the view content and all configured buttons/metrics.
       * @returns {this} This view instance for chaining.
       */
      render() {
        this.el.innerHTML = this.template();
        this.reset();

        // Re-render when the metadata model or pid changes
        this.listenTo(this.viewModel, "change", this.render);

        if (!this.viewModel.get("pid")) return this;
        const buttons = { ...this.buttons };
        Object.entries(buttons).forEach(([name, config]) => {
          try {
            this[config.render].call(this);
          } catch (error) {
            this.removeButton(name);
            // eslint-disable-next-line no-console
            console.error(`Error rendering the ${name} button:`, error);
          }
        });
        return this;
      },

      /**
       * Clear the view content and remove subviews and event listeners.
       * @param {boolean} [destroyCitationModal] If true, also destroy the
       * citation modal subview. We avoid this unless the entire view is being
       * destroyed because the modal is modified externally (see renderCite()).
       */
      reset(destroyCitationModal = false) {
        // Remove all the subviews
        this.subviews.forEach((subview) => {
          if (typeof subview.onClose === "function") {
            subview.onClose();
          }
          subview.remove();
        });
        this.subviews = [];

        // Remove all the buttons
        Object.entries(this.buttons).forEach(([name]) =>
          this.removeButton(name),
        );

        // Remove and close all subviews
        if (destroyCitationModal && this.citationModal) {
          this.citationModal.onClose();
          this.citationModal.remove();
          this.citationModal = null;
        }
        this.stopListening();
      },

      /**
       * Get the container element where a button should be inserted.
       * @param {string} name - Logical button name.
       * @returns {HTMLElement|null} The container element or null if not found.
       */
      getButtonContainer(name) {
        const configedButton = this.buttons[name];
        if (!configedButton) return null;
        const containerClass = configedButton.container;
        return this.el.querySelector(`.${containerClass}`);
      },

      /**
       * Get a previously rendered button element by name.
       * @param {string} name - Logical button name.
       * @returns {HTMLElement|null} The button element if it exists.
       */
      getExistingButtonEl(name) {
        return this.buttonEls?.[name] || null;
      },

      /**
       * Remove a button and clear its container.
       * @param {string} name - Logical button name to remove.
       */
      removeButton(name) {
        const buttonEl = this.getExistingButtonEl(name);
        const container = this.getButtonContainer(name);
        if (buttonEl) {
          $(buttonEl).popup("destroy");
          buttonEl.remove();
          delete this.buttonEls?.[name];
        }
        if (container) {
          container.innerHTML = "";
          container.style.display = "none";
        }
      },

      /**
       * Get or create the metrics model used to fetch dataset metrics.
       * @returns {MetricsModel} The metrics model instance for this view.
       */
      getMetricsModel() {
        const model = this.viewModel.get("metricsModel");

        // Check if the model's been fetched yet
        if (!model.get("synced") && !model.get("fetching")) {
          model.fetch();
        }

        return model;
      },

      /** Render the downloads metric view if enabled. */
      renderDownloads() {
        this.renderMetric.call(this, "downloads");
      },

      /** Render the views metric view if enabled. */
      renderViews() {
        this.renderMetric.call(this, "views");
      },

      /** Render the citations metric view if enabled. */
      renderCitations() {
        this.renderMetric.call(this, "citations");
      },

      /**
       * Render a specific metric widget by name and insert into its container.
       * @param {"downloads"|"views"|"citations"} name - The metric to render.
       */
      renderMetric(name) {
        const pid = this.viewModel.get("pid");

        const metricTypes = {
          downloads: APP_GET("displayDatasetDownloadMetric"),
          citations: APP_GET("displayDatasetCitationMetric"),
          views: APP_GET("displayDatasetViewMetric"),
        };

        if (!metricTypes[name]) return;

        const metrics = this.getMetricsModel();
        const metricView = new MetricView({
          metricName: name,
          model: metrics,
          pid,
        }).render();

        this.buttonEls[name] = metricView.el;
        this.subviews.push(metricView);

        const container = this.getButtonContainer(name);
        container.appendChild(metricView.el);
        // Make sure the container is visible
        container.style.display = "";
      },

      async canEdit() {
        if (this.isObsoleteOrArchived()) return false;
        return this.viewModel.get("hasWritePermission") === true;
      },

      /**
       * Render the Edit Metadata button if the user is authorized and the
       * metadata format is editable.
       */
      async renderEdit() {
        // Do not render the edit button if user does not have permission or the
        // app is configured to hide it.
        if (!APP_GET("displayDatasetEditButton")) return;
        const canEdit = await this.canEdit();

        if (!canEdit) return;

        const editableFormat = this.isEditableFormat();
        // The share-feature class is added to share/upload/edit/login features
        // so repo can easily hide them if needed
        const href = editableFormat ? this.createEditUrl() : null;
        const options = { href };
        if (!editableFormat) {
          options.disabled = true;
          options.href = "#";
          options.tooltip = "This metadata format is not editable.";
        }
        this.renderButton("edit", options);
      },

      /**
       * Render the Publish with DOI button if user is authorized and publishing
       * is allowed.
       */
      async renderPublish() {
        const canEdit = await this.canEdit();
        if (!canEdit) return;
        const canPublish = await this.canPublish();
        if (!canPublish) return;
        this.renderButton("publish");
      },

      /** Render the metadata quality report button */
      renderMDQ() {
        if (!this.canDisplayMDQ()) {
          // hide the separator if no reports are available
          const separator = this.el.querySelector(`.${CLASS_NAMES.separator}`);
          if (separator) separator.style.display = "none";
          return;
        }
        const pid = this.viewModel.get("pid");
        const mdqUrl = `${MetacatUI.root}/quality/${encodeURIComponent(pid)}`;
        // get the number of available reports from the MDQ service
        const numReports = APP_GET("mdqSuiteIds")?.length || 0;

        const options = { href: mdqUrl };
        if (numReports === 1) {
          // use singular text if only one report is available
          options.text = this.buttons.mdq.singularText;
          options.textOnSmallScreen =
            this.buttons.mdq.singularTextOnSmallScreen;
        }
        const button = this.renderButton("mdq", options);

        // show a badge w/ number of reports like the metric buttons
        const badge = document.createElement("span");
        badge.classList.add("metric-value");
        badge.textContent = numReports;
        // place it after the icon and before the text
        const iconEl = button.querySelector("i");
        iconEl.insertAdjacentElement("afterend", badge);

        // add the metric class names to the icon for consistent styling
        iconEl.classList.add("metric-icon");
      },

      /** Render the WholeTale Analyze dropdown button when configured. */
      renderWholetale() {
        if (
          !APP_GET("displayDatasetAnalyzeButton") ||
          !APP_GET("taleEnvironments")?.length
        ) {
          return;
        }
        const button = this.renderButton("wholetale");
        const dropdownMenu = this.createWholetaleMenu();
        button.appendChild(dropdownMenu);
      },

      /**
       * Show the citation modal with the ability to copy the citation text
       */
      renderCite() {
        this.renderButton("cite");
        if (!this.citationModal) {
          this.citationModal = new CitationModalView({
            model: this.viewModel.get("metadataModel"),
            createLink: true,
          }).render();
        }
        // Don't re-create the citation modal on re-render, and don't add it to
        // subviews to be destroyed during reset(), because the
        // CanonicalDatasetHandlerView modifies it. Destroying it would lose
        // changes. Eventually, the canonical citation should live in the
        // metadata model so the CitationModalView can update itself
        // automatically.
      },

      /**
       * Build the WholeTale dropdown menu element from configured environments.
       * @returns {HTMLUListElement} The populated unordered list element.
       */
      createWholetaleMenu() {
        const dropdownMenu = document.createElement("ul");
        dropdownMenu.classList.add(CLASS_NAMES.dropdownMenu);
        APP_GET("taleEnvironments").forEach((env) => {
          const menuItem = document.createElement("li");
          const link = document.createElement("a");
          link.href = this.createWholetaleUrl(env);
          link.textContent = env;
          link.target = "_blank";
          menuItem.appendChild(link);
          dropdownMenu.appendChild(menuItem);
        });
        return dropdownMenu;
      },

      // placeholder for future implementation
      renderNotifications() {
        // Don't render yet
        return null;
        // this.renderButton("notifications");
      },

      /**
       * Render a button by name into its configured container.
       * @param {string} name - Logical button key.
       * @param {object} [options] - Button option
       * @param {string} [options.href] - URL for the button link.
       * @param {boolean} [options.disabled] - Whether to disable the button.
       * overrides.
       * @returns {HTMLElement|null} The root element for the button, or null if
       * skipped.
       */
      renderButton(
        name,
        options = {
          href: null,
          disabled: false,
        },
      ) {
        // Remove the button if it already exists
        this.removeButton(name);

        // Get the container to insert the button into
        const container = this.getButtonContainer(name);
        if (!container) return null;

        // Get the button configuration
        const configedButton = this.buttons[name];
        if (!configedButton) return null;
        const opts = { ...configedButton, ...options };

        // Create the button element
        let button = document.createElement("a");
        const classes = Array.isArray(opts.classes) ? opts.classes : [];
        button.classList.add(...["btn", ...classes]);
        if (opts.id) button.id = opts.id;

        if (opts.href) button.href = opts.href;

        if (opts.disabled) button.classList.add("disabled");

        if (opts.icon) {
          const icon = document.createElement("i");
          icon.className = `${CLASS_NAMES.icon} icon-${opts.icon} ${CLASS_NAMES.iconOnLeft}`;
          button.appendChild(icon);
        }

        const { text, textOnSmallScreen } = opts;
        if (text) {
          const textSpan = document.createElement("span");
          if (textOnSmallScreen) {
            const mobileSpan = document.createElement("span");
            mobileSpan.classList.add(CLASS_NAMES.hideAbove1200);
            mobileSpan.textContent = textOnSmallScreen;
            textSpan.classList.add(CLASS_NAMES.hideBelow1200);
            textSpan.textContent = text;
            button.appendChild(mobileSpan);
          } else {
            textSpan.textContent = text;
          }
          button.appendChild(textSpan);
        }

        if (opts.tooltip) {
          $(button).popup({
            ...this.tooltipSettings,
            content: opts.tooltip,
          });
        }

        if (opts.dropdown) {
          button.setAttribute("data-toggle", "dropdown");
          button.href = "#";
          const caretSpan = document.createElement("span");
          caretSpan.classList.add("caret");
          button.appendChild(caretSpan);
          const dropdownButton = button;
          button = document.createElement("a");
          button.classList.add(CLASS_NAMES.dropdown);
          button.appendChild(dropdownButton);
        }

        // Store a reference to the button element
        if (!this.buttonEls) this.buttonEls = {};
        this.buttonEls[name] = button;

        // Insert the button into the container
        container.appendChild(button);
        // Make sure the container is visible (set to display:none; when button
        // is removed);
        container.style.display = "";

        return button;
      },

      /**
       * Update a button's inner content and classes for a given state.
       * @param {string} name Button key
       * @param {string} [text] Text label to display
       * @param {"default"|"progress"|"success"|"error"} state Visual state
       */
      updateButtonState(name, text, state = "default") {
        const buttonEl = this.getExistingButtonEl(name);
        if (!buttonEl) return;

        // Save the original HTML for restoration later
        if (!buttonEl.dataset.originalHtml) {
          buttonEl.dataset.originalHtml = buttonEl.innerHTML;
        }

        // Reset to clean state first
        buttonEl.classList.remove("disabled", "success", "error");
        buttonEl.disabled = false;

        // Choose icon/text by state
        const states = {
          progress: {
            icon: "spinner icon-spin",
            text: text || "Processing...",
          },
          success: { icon: "check", text: text || "Success!" },
          error: { icon: "exclamation-triangle", text: text || "Error" },
          default: { restore: true },
        };

        const config = states[state];
        if (!config) return;

        if (config.restore) {
          buttonEl.innerHTML = buttonEl.dataset.originalHtml;
          delete buttonEl.dataset.originalHtml;
          return;
        }

        // Apply new markup
        buttonEl.innerHTML = `<i class='icon icon-${config.icon}'></i> ${config.text}`;
        buttonEl.classList.add(state === "progress" ? "disabled" : state);
        if (state === "progress") buttonEl.disabled = true;
      },

      // --------------------------------------------------------
      // MODEL LOGIC: Methods below belong in a model (TODO)
      // --------------------------------------------------------

      /** Open the citation modal, creating and rendering it if needed. */
      openCitationModal() {
        this.citationModal?.show();
      },

      /**
       * Check if the current formatId is supported by the metadata quality
       * suite
       * @returns {boolean} True if the formatId is supported by MDQ, false
       * otherwise
       */
      formatIsMDQSupported() {
        const metadataModel = this.viewModel.get("metadataModel");
        const formatId = metadataModel.get("formatId");
        const mdqFormatIds = MetacatUI.appModel.get("mdqFormatIds") || [];

        return mdqFormatIds.some((pattern) =>
          Utilities.wildcardToRegex(pattern).test(formatId),
        );
      },

      /**
       * Determine if MDQ button should be displayed based on configuration.
       * @returns {boolean} True if MDQ can be displayed, false otherwise.
       */
      canDisplayMDQ() {
        return (
          APP_GET("mdqBaseUrl") &&
          this.formatIsMDQSupported() &&
          APP_GET("displayDatasetQualityMetric") &&
          MetacatUI.appModel.get("mdqSuiteIds")?.length > 0
        );
      },

      /**
       * Determine if this metadata can be published with a DOI, assuming the
       * user has permission to edit it and it is not obsolete or archived.
       * @returns {boolean} True if the metadata can be published, false
       * otherwise
       */
      async canPublish() {
        // The Publish feature has to be enabled for the repo & the model cannot
        // already have a DOI
        const metadata = this.viewModel.get("metadataModel");
        if (!APP_GET("enablePublishDOI") || metadata.isDOI()) {
          return false;
        }

        // Need a publish method to call
        const publishMethod = this.viewModel.get("publishMethod");
        if (typeof publishMethod !== "function") {
          return false;
        }

        // Check if only certain users and groups can publish metadata

        // Get the list of authorized publishers from the AppModel
        const authorizedPublishers = APP_GET("enablePublishDOIForSubjects");
        // If the logged-in user is one of the subjects in the list or is in a
        // group that is in the list, then this metadata can be published.
        // Otherwise, it cannot.
        if (
          Array.isArray(authorizedPublishers) &&
          authorizedPublishers.length
        ) {
          if (MetacatUI.appUserModel.hasIdentityOverlap(authorizedPublishers)) {
            return true;
          }
        } else {
          return true;
        }

        return false;
      },

      /**
       * Determine if the dataset is obsolete or archived.
       * @returns {boolean} True if obsolete or archived, otherwise false.
       */
      isObsoleteOrArchived() {
        const metadata = this.viewModel.get("metadataModel");
        if (!metadata) return false;

        if (
          (metadata.get("obsoletedBy") &&
            metadata.get("obsoletedBy").length > 0) ||
          metadata.get("archived")
        ) {
          return true;
        }

        return false;
      },

      /**
       * Determine if the metadata format is editable in this repository.
       * @returns {boolean} True if the current formatId is allowed to edit.
       */
      isEditableFormat() {
        const metadata = this.viewModel.get("metadataModel");
        if (!metadata) return false;
        const format = metadata.get("formatId");
        const editableFormats = APP_GET("editableFormats") || [];
        return editableFormats.includes(format);
      },

      /**
       * Construct a WholeTale URL for the given environment targeting this
       * dataset.
       * @param {string} env - The WholeTale environment name.
       * @returns {string} The generated WholeTale dashboard URL.
       */
      createWholetaleUrl(env) {
        const baseUrl = MetacatUI.appModel.get("d1CNBaseUrl");
        const dashboardUrl = MetacatUI.appModel.get("dashboardUrl");
        const currentUrl = encodeURIComponent(window.location.href);
        const service = MetacatUI.appModel.get("d1CNService");
        const title = encodeURIComponent(
          this.viewModel.get("metadataModel")?.get("title") ||
            "Untitled Dataset",
        );
        const queryParams = `?uri=${currentUrl}&title=${title}&api=${baseUrl}${service}`;
        return `${dashboardUrl}${queryParams}&environment=${env}`;
      },

      /**
       * Build the URL to edit this dataset in the submit workflow.
       * @returns {string} The edit URL.
       */
      createEditUrl() {
        const pid = this.viewModel.get("pid");
        return `${MetacatUI.root}/submit/${encodeURIComponent(pid)}`;
      },

      /**
       * Publish the data package with a DOI. Calls the publishMethod passed to
       * the view during initialization. Updates the button to show progress,
       * success, or error states.
       * @param {Event} event - The click event
       * @returns {Promise|null} A promise that resolves/rejects when
       * publishing is complete, or undefined if publishing did not start.
       */
      publish(event) {
        event?.preventDefault?.();

        const publishButton =
          event?.currentTarget || event?.target?.closest("a");
        if (
          !publishButton ||
          publishButton.disabled ||
          publishButton.classList.contains("disabled")
        ) {
          return null;
        }

        const pubMethod = this.viewModel.get("publishMethod");
        if (typeof pubMethod !== "function") return null;

        // Disable the publish button to prevent multiple clicks
        this.updateButtonState("publish", "Publishing...", "progress");

        return pubMethod()
          .then(() => {
            this.updateButtonState("publish", "Published!", "success");
            // after a timeout, remove the button. You can't republish
            setTimeout(() => {
              this.removeButton("publish");
            }, 1500);
          })
          .catch(() => {
            this.updateButtonState("publish", "Publish Failed", "error");
          });
      },

      /** Methods to run when the view is closed and removed */
      onClose() {
        this.reset(true);
      },

      /** @inheritdoc */
      remove() {
        this.onClose();
        return Backbone.View.prototype.remove.call(this);
      },
    },
  );

  return DatasetControls;
});