User:SunAfterRain/merge.js

From Wikidata
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*global jQuery, mediaWiki, OO, wikibase*/
/*!
 * https://www.wikidata.org/w/index.php?title=MediaWiki:Gadget-Merge.js&oldid=1042304347
 * merge.js - Script to merge Wikidata items
 * @authors User:Ebrahim, User:Ricordisamoa, User:Fomafix, User:Bene*, User:Petr Matas, User:Matěj Suchánek
 * @license CC-Zero
 */
// See also: MediaWiki:Gadget-EmptyDetect.js and MediaWiki:Gadget-RfDHelper.js
//mw.loader.load('//zh.wikipedia.org/w/load.php?modules=ext.gadget.site-lib');
function wgUXS (wg, hans, hant, cn, tw, hk, sg, zh, mo, my) {
    var ret = {
        'zh': zh || hans || hant || cn || tw || hk || sg || mo || my,
        'zh-hans': hans || cn || sg || my,
        'zh-hant': hant || tw || hk || mo,
        'zh-cn': cn || hans || sg || my,
        'zh-sg': sg || hans || cn || my,
        'zh-tw': tw || hant || hk || mo,
        'zh-hk': hk || hant || mo || tw,
        'zh-mo': mo || hant || hk || tw
    };
    return ret[wg] || zh || hans || hant || cn || tw || hk || sg || mo || my; //保證每一語言有值
}

function wgULS (hans, hant, cn, tw, hk, sg, zh, mo, my) {
    return wgUXS(mw.config.get('wgUserLanguage'), hans, hant, cn, tw, hk, sg, zh, mo, my);
}

(function ($, mw, OO) {
    'use strict';
    var entityId = mw.config.get('wbEntityId'), api = new mw.Api(wgULS('',''));
    var messages = {
    	conflictMessage : wgULS('存在跨语言冲突:','存在跨語言衝突:'),
        conflictWithMessage : wgULS('和','和'),
        createRedirect : wgULS('创建重定向','建立重定向'),
        creatingRedirect : wgULS('创建重定向中......','建立重定向中......'),
        errorWhile : wgULS('“$1”时出错:','「$1」時出錯:'),
        invalidInput : wgULS('目前只有一个Qid是有效的','目前只有一個Qid是有效的'),
        lowestEntityId : wgULS('始终合并到较旧的实体(取消选中以合并到“要和此项合并的”实体)','始終合併到較舊的實體(取消選中以合併到「要和此項合併的數據項」實體)'),
        merge : wgULS('合并','合併'),
        mergePendingNotification : wgULS('Merge.js已经运行,请前往其他需要合并的项。','Merge.js已經運行,請前往其他需要合併的項。'),
        mergeProcess : wgULS('开始合并','開始合併'),
        mergeSummary : wgULS('将以下文本添加到自动生成的编辑摘要中:','將以下文本添加到自動生成的編輯摘要中:'),
        mergeThisEntity : wgULS('合并此项','合併此項'),
        mergeWithInput : wgULS('要和此项合并的数据项:','要和此項合併的數據項:'),
        mergeWithProgress : wgULS('合并','合併'),
        mergeWizard : wgULS('合并数据项','合併數據項'),
        pleaseWait : wgULS('请稍候......','請稍後......'),
        postpone : wgULS('和其他项合并','和其他項合併'),
        postponeTitle : wgULS('储存此项编号以和其他项合并','儲存此項編號以和其他項合併'),
        reportError : wgULS('请将上述错误报告到[[这里]],包括合并的来源及目的地','請將上述錯誤報告到[[這裡]],包括合併的來源及目的地'),
        selectForMerging : wgULS('选择合并项','選擇合併項'),
        selectForMergingTitle : wgULS('记住这个项目是要合并的两个项目中的第二个项目。','記住這個項目是要合併的兩個計畫中的第二個項目。'),
        unwatchOption : wgULS('从监视列表移除重复项(若可能)','從監視列表移除重複項(若可能)'),
        unwatching : wgULS('正在从监视列表移除重复项......','正在從監視列表移除重複項......'),
        loadMergeDestination: wgULS('合并成功时加载目标项','合併成功時載入目標項'),
        loadingMergeDestination: wgULS('正在合并中...','正在合併中...'),
    };
    
  /**
   * Retrieve items by id
   */
  function getItems(ids) {
    return api.get({
      action: 'wbgetentities',
      ids: ids.join('|')
    }).then(function (data) {
      return Object.keys(data.entities).map(function (x) { return data.entities[x]; });
    });
  }

  /**
   * Set a Storage to postpone merge and deletion
   */
  function mergePending(id) {
    mw.storage.set('merge-pending-id', id);
    mw.notify($.parseHTML(messages.mergePendingNotification));
  }

  /**
   * ...and reset this Storage
   */
  function removePending() {
    mw.storage.remove('merge-pending-id');
  }

  /**
   * Check if items can be merged
   */
  function detectConflicts(items) {
    var all = {},
      conflicts = {};
    items.forEach(function (item) {
      if (!item.sitelinks) { return; }
      Object.keys(item.sitelinks).forEach(function (dbName) {
        if (all[dbName] && all[dbName].sitelinks[dbName].title !== item.sitelinks[dbName].title) {
          if (!conflicts[dbName]) {
            conflicts[dbName] = [all[dbName]];
          }
          conflicts[dbName].push(item);
        }
        all[dbName] = item;
      });
    });
    return conflicts;
  }

  /**
   * Create a redirect
   */
  function createRedirect(fromId, toId) {
    // ugly hack, we have to clear the entity first before creating the redirect...
    return api.postWithEditToken({
      action: 'wbeditentity',
      id: fromId,
      clear: true,
      summary: 'Clearing item to prepare for redirect',
      data: '{}'
    }).then(function () {
      return api.postWithEditToken({
        action: 'wbcreateredirect',
        from: fromId,
        to: toId
      });
    });
  }

  /**
   * Moving logic
   */
  function mergeApi(from, to, mergeSummary) {
    var data;
    if (from.indexOf('L') === 0) {
      data = {
        action: 'wblmergelexemes',
        source: from,
        target: to,
      };
    } else {
      data = {
        action: 'wbmergeitems',
        fromid: from,
        toid: to,
        ignoreconflicts: 'description', // ignore descriptions conflicts as old version of merge did
      };
    }
    data.summary = '[[User:Sunny00217/merge.js]] ' + mergeSummary;
    return api.postWithEditToken(data);
  }

  /**
   * @class Merger
   * @mixins OO.EventEmitter
   *
   * @constructor
   */
  function Merger(mergeItems, mergeSummary, alwaysLowestId, unwatch, mergeCreateRedirect, loadMergeDestination) {
    OO.EventEmitter.call(this);
    this.mergeItems = mergeItems;
    this.mergeSummary = mergeSummary;
    if (/^\w/.test(this.mergeSummary)) {
      this.mergeSummary = ' ' + this.mergeSummary;
    }
    this.alwaysLowestId = alwaysLowestId;
    this.unwatch = unwatch;
    this.mergeCreateRedirect = mergeCreateRedirect;
    this.loadMergeDestination = loadMergeDestination;
  }
  OO.mixinClass(Merger, OO.EventEmitter);

  /**
   * Merge process
   */
  Merger.prototype.merger = function (from, to) {
    var self = this;
    self.emit('progress', messages.mergeWithInput + ' ' + to);
    var redirected;
    var deferred = mergeApi(from, to, self.mergeSummary)
        .then(function (data) {
            redirected = data.redirected;
            return $.Deferred().resolve();
        });

    if (self.unwatch) {
      deferred = deferred.then(function () {
        self.emit('progress', messages.unwatching);
        return api.unwatch(from);
      });
    }

    if (self.mergeCreateRedirect) {
      deferred = deferred.then(function () {
        if (redirected) { // don't create redirect if already redirected
          return $.Deferred().resolve();
        }
        self.emit('progress', messages.creatingRedirect);
        return createRedirect(from, to);
      });
    }

    deferred.then(function () {
      if (self.loadMergeDestination) {
        self.emit('progress', messages.loadingMergeDestination);
        var target = mw.config.get('wgPageName').replace(from, to);
        // Purge (via API), then reload.
        // XXX: Do we even need to purge? Why?
        api.post({
          action: 'purge',
          titles: target
        }).then(function () {
          window.location = mw.util.getUrl(target);
        });
      } else {
        self.emit('success');
      }
    }, function (code, result) {
      self.emit('error', result.error.extradata[0] || result.error.info);
    });
  };

  /**
   * Merge button action, pre-merge checks
   */
  Merger.prototype.merge = function () {
    var self = this,
      itemsNames = [$.trim(self.mergeItems).toUpperCase(), entityId],
      isAllQ = itemsNames.every(function (x) { return /^Q\d*$/i.test(x); }),
      isAllL = itemsNames.every(function (x) { return /^L\d*$/i.test(x); });
    if (!(isAllQ || isAllL)) {
      $('#merge-input-validation-message').text(messages.invalidInput);
      return;
    }
    self.emit('progress', messages.pleaseWait);
    getItems(itemsNames).then(function (items) {
      // duplicate item if just an item is returned
      // if item was being merged to itself this could conflict error that also useful for debugging conflict detector
      if (items.length === 1) {
        items = items.concat(items);
      }

      var conflicts = detectConflicts(items),
        message;
      if (Object.keys(conflicts).length === 0) {
        if (self.alwaysLowestId) {
          items.sort(function (x, y) { return +x.id.replace(/^[QL]/i, '') - y.id.replace(/^[QL]/i, ''); }); // sort by Qid _only_if_specified_
        }
        self.merger(items[1].id, items[0].id);
      } else {
        message = Object.keys(conflicts).map(function (i) {
          var x = conflicts[i];
          return '<br>' + messages.conflictMessage + i + ':' + x.map(function (y, j) {
            return ' [[' + x[j].id + ']] ' + messages.conflictWithMessage +
              ' [[' + i + ':' + y.sitelinks[i].title + ']]';
          }).join(',');
        }).join('').replace(/\[\[([^\]\:]*?)\:([^\]]*?)\]\]/g, function (x, y, z) {
          return mw.html.element( 'a', { href: wikibase.sites.getSite(y).getUrlTo(z) }, y + ':' + z );
        });
        self.emit('error', message, true);
      }
    });
  };

  /**
   * @class MergeDialog
   * @extends OO.ui.ProcessDialog
   *
   * @constructor
   * @param {Object} config Configuration options
   * @cfg {string} entityId Entity ID
   */
  function MergeDialog(config) {
    MergeDialog.parent.call(this, config);
    this.entityId = config.entityId;
  }
  OO.inheritClass(MergeDialog, OO.ui.ProcessDialog);

  MergeDialog.static.name = 'mergeDialog';
  MergeDialog.static.title = messages.mergeWizard;
  MergeDialog.static.size = 'medium';
  MergeDialog.static.actions = [
    {
      action: 'postpone',
      label: messages.postpone,
      title: messages.postponeTitle,
      flags: 'progressive'
    },
    {
      action: 'merge',
      label: messages.merge,
      title: messages.mergeProcess,
      flags: ['primary', 'constructive']
    },
    {
      action: 'cancel',
      label: mw.msg('ooui-dialog-message-reject'),
      flags: 'safe'
    }
  ];

  /**
   * @inheritdoc
   */
  MergeDialog.prototype.initialize = function () {
    MergeDialog.parent.prototype.initialize.apply(this, arguments);
    var fieldset = new OO.ui.FieldsetLayout({});
    this.entitySelector = new OO.ui.TextInputWidget({
      value: this.entityId
    });
    fieldset.addItems([
      new OO.ui.FieldLayout(
        this.entitySelector,
        {
          align: 'left',
          label: messages.mergeWithInput
        }
      )
    ]);
    fieldset.$element.append($('<span>', {
      id: 'merge-input-validation-message',
      style: 'color: red;'
    }));
    this.mergeSummary = new OO.ui.TextInputWidget({});
    fieldset.addItems([
      new OO.ui.FieldLayout(
        this.mergeSummary,
        {
          align: 'left',
          label: messages.mergeSummary
        }
      )
    ]);
    this.mergeAlwaysLowestId = new OO.ui.CheckboxInputWidget({
      selected: true
    });
    fieldset.addItems([
      new OO.ui.FieldLayout(
        this.mergeAlwaysLowestId,
        {
          align: 'inline',
          label: messages.lowestEntityId
        }
      )
    ]);
    this.mergeCreateRedirect = new OO.ui.CheckboxInputWidget({
      selected: true,
      disabled: true
    });
    fieldset.addItems([
      new OO.ui.FieldLayout(
        this.mergeCreateRedirect,
        {
          align: 'inline',
          label: messages.createRedirect
        }
      )
    ]);
    this.mergeUnwatch = new OO.ui.CheckboxInputWidget({
      selected: mw.storage.get('merge-unwatch') === 'true'
    });
    fieldset.addItems([
      new OO.ui.FieldLayout(
        this.mergeUnwatch,
        {
          align: 'inline',
          label: messages.unwatchOption
        }
      )
    ]);
    this.loadMergeDestination = new OO.ui.CheckboxInputWidget({
      selected: mw.storage.get('merge-load-destination') !== 'false'
    });
    fieldset.addItems([
      new OO.ui.FieldLayout(
        this.loadMergeDestination,
        {
          align: 'inline',
          label: messages.loadMergeDestination
        }
      )
    ]);
    var content = new OO.ui.PanelLayout({
      padded: true,
      expanded: false
    });
    content.$element.append(fieldset.$element);
    this.$body.append(content.$element);
    var self = this;
    this.actions.once('add', function () {
      self.actions.setAbilities({ postpone: self.entityId === '' });
    });
    this.$element.prop('lang', $('html').prop('lang'));
  };

  /**
   * @inheritdoc
   */
  MergeDialog.prototype.getReadyProcess = function (data) {
    return MergeDialog.parent.prototype.getReadyProcess.call(this, data)
      .next(function () {
        // focus "Merge with" field:
        // https://www.wikidata.org/wiki/?oldid=333747825#Request:_Improvements_for_keyboard_navigation
        this.entitySelector.focus();
      }, this);
  };

  /**
   * Save options in storage
   */
  MergeDialog.prototype.saveOptions = function () {
    mw.storage.set('merge-always-lowest-id', this.mergeAlwaysLowestId.isSelected().toString());
    mw.storage.set('merge-unwatch', this.mergeUnwatch.isSelected().toString());
    mw.storage.set('merge-create-redirect', this.mergeCreateRedirect.isSelected().toString());
    mw.storage.set('merge-load-destination', this.loadMergeDestination.isSelected().toString());
  };

  MergeDialog.prototype.merge = function () {
    var self = this;
    this.saveOptions();
    removePending();
    var merger = new Merger(
      this.entitySelector.getValue(),
      this.mergeSummary.getValue(),
      this.mergeAlwaysLowestId.isSelected(),
      this.mergeUnwatch.isSelected(),
      this.mergeCreateRedirect.isSelected(),
      this.loadMergeDestination.isSelected()
    );
    merger.on('progress', function () {
      self.displayProgress.apply(self, arguments);
    });
    merger.on('error', function () {
      self.displayError.apply(self, arguments);
    });
    merger.on('success', function () {
      self.close();
      $('#ca-merge-queue-process, #ca-merge, #ca-merge-select').remove();
    });
    merger.merge();
  };

  MergeDialog.prototype.postpone = function () {
    this.saveOptions();
    mergePending(entityId);
    this.close();
  };

  /**
   * @inheritdoc
   */
  MergeDialog.prototype.getActionProcess = function (action) {
    if (action === 'merge') {
      return new OO.ui.Process(this.merge, this);
    }
    if (action === 'postpone') {
      return new OO.ui.Process(this.postpone, this);
    }
    if (action === 'cancel') {
      return new OO.ui.Process(this.close, this);
    }
    return MergeDialog.parent.prototype.getActionProcess.call(this, action);
  };

  /**
   * Display progress on form dialog
   */
  MergeDialog.prototype.displayProgress = function (message) {
    if (this.$progressMessage) {
      this.$progressMessage.text(message);
      this.updateSize();
      return;
    }
    this.$body.children().hide();
    this.actions.forEach(null, function (action) {
      action.setDisabled(true); // disable buttons
    });
    this.$progressMessage = $('<span>').text(message);
    this.pushPending();
    $('<div>').css({
      'text-align': 'center',
      'margin': '3em 0',
      'font-size': '120%'
    }).append(
      this.$progressMessage
    ).appendTo(this.$body);
    this.updateSize();
  };

  /**
   * Display error on form dialog
   */
  MergeDialog.prototype.displayError = function (error, hideReportLink) {
    var reportLink;
    this.$body.children().hide();
    while (this.isPending()) {
      this.popPending();
    }
    this.actions.forEach(null, function (action) {
      // reenable 'cancel' button, disable all other buttons
      // https://www.wikidata.org/wiki/?oldid=323115101#Close.2FCancel
      action.setDisabled(action.getAction() !== 'cancel');
    });
    if (hideReportLink === true) {
      reportLink = '';
    } else {
      reportLink = '<p>' + messages.reportError.replace(/\[\[(.*)\]\]/, '<a href="//www.wikidata.org/w/index.php?title=MediaWiki_talk:Gadget-Merge.js&action=edit&section=new" target="_blank">$1</a>') + '</p>';
    }
    this.$body.append($('<div>', {
      style: 'color: #990000; margin-top: 0.4em;',
      html: '<p>' + messages.errorWhile.replace(/\$1/, this.$progressMessage.text()) + ' ' + error + '</p>' + reportLink
    }));
    this.updateSize();
  };

  /**
   * Dialog creator and launcher
   */
  function launchDialog(id) {
    if (typeof id !== 'string') {
      id = '';
    }
    var dialog = new MergeDialog({
      entityId: id
    });
    var windowManager = new OO.ui.WindowManager();
    $('body').append(windowManager.$element);
    windowManager.addWindows([dialog]);
    windowManager.openWindow(dialog);
  }

  // Initialization
  if (entityId !== null &&
      [0, 146].indexOf(mw.config.get('wgNamespaceNumber')) !== -1 &&
      mw.config.get('wgAction') === 'view') {
    $(window).on('focus storage', function () {
      $('#ca-merge-queue-process').remove();
      if (mw.storage.get('merge-pending-id') !== null &&
          mw.storage.get('merge-pending-id') !== '' &&
          mw.storage.get('merge-pending-id') !== entityId) {
        $('#p-views ul')[$(document).prop('dir') === 'rtl' ? 'append' : 'prepend']($('<li>', {
          id: 'ca-merge-queue-process'
        }).append($('<a>', {
          href: '#',
          title: 'process the postponed merge'
        }).append($('<img>', {
          src: '//upload.wikimedia.org/wikipedia/commons/thumb/1/10/Pictogram_voting_merge.svg/26px-Pictogram_voting_merge.svg.png',
          alt: 'merge icon'
        }))).click(function (event) {
          event.preventDefault();
          launchDialog(mw.storage.get('merge-pending-id'));
        }));
      }
    });
    $(function () {
      $('#ca-merge-queue-process, #ca-merge, #ca-merge-select').remove();
      $(mw.util.addPortletLink(
        'p-cactions',
        '#',
        messages.mergeWithProgress,
        'ca-merge',
        messages.mergeThisEntity
      )).click(function (event) {
        event.preventDefault();
        launchDialog();
      });

      $(mw.util.addPortletLink(
        'p-cactions',
        '#',
        messages.selectForMerging,
        'ca-merge-select',
        messages.selectForMergingTitle
      )).click(function (event) {
        event.preventDefault();
        mergePending(entityId);
      });
    });
  }

  // Export section
  // currently just for [[MediaWiki:Gadget-EmptyDetect.js]], just launchDialog is exposed
  window.mergeTool = {
    launchDialog: launchDialog
  };
}(jQuery, mediaWiki, OO));