MediaWiki:Gadget-markreview.js: Difference between revisions

From Taranaki Lab Manual
Jump to navigation Jump to search
No edit summary
Tag: Reverted
No edit summary
Tag: Reverted
Line 1: Line 1:
/* Gadget: MarkReview — One‑click "Mark as reviewed" for page owners (updates review_ping parameter) */
/* review_ping in the template call.
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.user']).then(function () {


  function run($content) {
```javascript
    // Find .doc-meta in content or fallback to whole DOM
/* Gadget: MarkReview — One‑click "Mark Gadget: MarkReview — One‑click "Mark as reviewed" (updates review_ping) with robust injection as reviewed" (updates review_ping) with robust injection + diagnostics */
    var $docMeta = $content.find('.doc-meta').first();
mw.loader.using( + diagnostics */
    if (!$docMeta.length) {
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.user', 'mediawiki.notify']).['mediawiki.api', 'mediawiki.util', 'mediawiki.user', 'mediawiki.notify']).then(function () {
      $docMeta = $('.doc-meta').first();
    }
    if (!$docMeta.length) return; // No panel found


    var owner = $docMeta.attr('data-owner');
  function localStamp() {
    var user = mw.config.get('wgUserName');
     varthen(function () {
    var action = mw.config.get('wgAction') || 'view';
    if (action !== 'view') return;
     if (!owner || !user || owner.toLowerCase() !== user.toLowerCase()) return;


    function localStamp() {
  function localStamp() {
      var now = new Date();
    var now = new Date();
      return now.getFullYear() + '-' +
    return now.get now = new Date();
        String(now.getMonth() + 1).padStart(2, '0') + '-' +
    return now.getFullYear() + '-' +
        String(now.getDate()).padStart(2, '0') + 'T' +
      String(nowFullYear() + '-' +
        String(now.getHours()).padStart(2, '0') + ':' +
      String(now.getMonth() + 1)..getMonth() + 1).padStart(2, '0')padStart(2, '0') + '-' +
        String(now.getMinutes()).padStart(2, '0') + ':' +
      String + '-' +
        String(now.getSeconds()).padStart(2, '0');
      String(now.getDate()).padStart(2, '0')(now.getDate()).padStart(2, '0') + 'T' +
    }
      String + 'T' +
      String(now.getHours()).padStart(2, '0')(now.getHours()). + ':' +
      String(now.getMinutes()).padStart(2, '0') + ':' +
      String(now.getMinutes()).padStart(2, '0')padStart(2, '0') + ':' +
      String(now.getSeconds()).padStart(2, '0');
  }


    function doReview() {
  function findDocMeta($scope) {
      var page = mw.config.get('wgPageName');
+ ':' +
       var stamp = localStamp();
       String(now.getSeconds()).padStart(2, '0');
      var api = new mw.Api();
  }


      api.get({
  function findDocMeta($scope) {
        action: 'query',
    // Find either the table.doc-meta or any element with    // Find either the table.doc-meta .doc-meta
        prop: 'revisions',
    var $el = $scope.find or any element with .doc-meta
        titles: page,
    var $el = $scope.find('table.doc-meta').first();
        rvslots: 'main',
    if('table.doc-meta').first();
        rvprop: 'content|timestamp',
    if (!$el.length) $el = $scope.find (!$el.length) $el = $scope.find('.doc-meta').first();
        formatversion: 2
    if (!$el('.doc-meta').first();
      }).done(function (data) {
    if (!$el.length) $el = $('table.doc-meta, ..length) $el = $('table.doc-meta, .doc-meta').first();
        var pg = data.query.pages[0];
    return $el;
        if (!pg || !pg.revisions || !pg.revisions.length) {
  }
          mw.notify('Could not read page content', { type: 'error' });
          return;
        }
        var rev = pg.revisions[0];
        var content = rev.slots.main.content;
        var baseTs = rev.timestamp;


        // Match the first {{DocMeta ...}} block
  functiondoc-meta').first();
        var docmetaRe = /\{\{\s*DocMeta\b([\s\S]*?)\}\}/i;
    return $ injectButton($docel;
        var m = content.match(docmetaRe);
  }
        if (!m) {
          mw.notify('DocMeta block not found on this page', { type: 'error' });
          return;
        }


        var block = m[0];
  function injectButton($docMeta, onClick) {
        var inner = m[1];
    var $table =Meta, onClick) {
    var $table = $docMeta.is('table') ? $docMeta : $ $docMeta.is('table') ? $docMeta : $docMeta.closest('table');
    if (!$docMeta.closest('table');
    if (!$table.length) $tabletable.length) $table = $docMeta; // fallback = $docMeta; // fallback


        // Replace or insert review_ping
    // Use existing .doc-meta-actions
        var rpRe = /(\|\s*review_ping\s*=\s*)(.*)/i;
        var newInner;
        if (rpRe.test(inner)) {
          newInner = inner.replace(rpRe, '$1' + stamp);
        } else {
          newInner = inner + '\n | review_ping = ' + stamp;
        }


        var newBlock = block.replace(inner, newInner);
    // Use existing .doc-meta-actions if present, else if present, else create
        var newContent = content.replace(docmetaRe, newBlock);
    var $actions = $table create
    var $actions = $table.nextAll('.doc-meta-actions').first();
    if (!$actions.nextAll('.doc-meta-actions').first();
    if (!$actions.length) {
    .length) {
      $actions = $('<div class="doc-meta-actions"></div>').insertAfter($table);
    }


        api.postWithToken('csrf', {
$actions = $('<div class="doc-meta-actions"></div>').insertAfter($table);
          action: 'edit',
          title: page,
          text: newContent,
          summary: 'Mark as reviewed',
          minor: true,
          nocreate: true,
          basetimestamp: baseTs
        }).done(function () {
          location.reload();
        }).fail(function (err) {
          mw.notify('Review failed: ' + (err && err.error && err.error.info ? err.error.info : err), { type: 'error' });
        });
      });
     }
     }


     // Create the button
     var $btn = $('   var $btn = $('<a>')
    var $btn = $('<a>')
      .attr('href', '#')
      .text('Mark as reviewed<a>')
       .attr('href', '#')
       .attr('href', '#')
       .text('Mark as reviewed')
       .text('Mark as reviewed')
      .css({
        display: 'inline-block',
')
       .css({
       .css({
         display: 'inline-block',
         display: 'inline-block',
         marginTop: '8px',
         marginTop: '8px',
              marginTop: '8px',
         padding: '4px 8px',
         padding: '4px 8px',
        background padding: '4px 8px',
         background: '#f0f0f0',
         background: '#f0f0f0',
      : '#f0f0f0',
         border: '1px solid #ccc',
         border: '1px solid #ccc',
        border border: '1px solid #ccc',
         borderRadius: '2px',
         borderRadius: '2px',
         textDecoration: 'none'
      Radius: '2px',
         textDecoration: textDecoration: 'none'
      })
'none'
       })
       })
       .on('click', function (e) {
       .on('click', function (e) {
         e.preventDefault();
         e.prevent      .on('click', function (e) {
         doReview();
Default();
        onClick();
            e.preventDefault();
         onClick();
       });
       });


     // Inject into .doc-meta-actions or create it
     $actions.empty().append($btn);
    var $actions = $docMeta.closest('table').nextAll('.doc-meta-actions').first();
  }
     if (!$actions.length) {
 
       $actions = $('<div class="doc-meta-actions"></div>').insertAfter($docMeta.closest('table'));
  function });
 
    $actions.empty().append($btn);
  }
 
  function updateReviewParam updateReviewParam(page, stamp) {
(page, stamp) {
    var api = new mw.Api();
 
    return api.get({
        var api = new mw.Api();
 
    return api.get({
      action: 'query',
      prop: 'revisions action: 'query',
      prop: 'revisions',
      titles: page,
      rvslots',
      titles: page,
      rvslots: 'main',
      rvprop: 'content|timestamp',
      formatversion: 'main',
      rvprop: 'content|timestamp',
      formatversion: 2
    }).then(function: 2
    }).then(function (data) {
      var (data) {
      var pg = data.query.pages[0];
      if (!pg || !pg.revisions || !pg.re pg = data.query.pages[0];
      if (!pg || !pg.revisions || !pg.revisions.length) {
        return $.Deferred().reject('No revisions found').promise();
    visions.length) {
        return $.Deferred().reject('No revisions found').promise();
      }
      var rev = pg.revisions[0];
      var content }
      var rev = pg.revisions[0];
      var content = rev.slots.main = rev.slots.main.content;
      var baseTs = rev.timestamp.content;
      var baseTs = rev.timestamp;
 
      // Find the first {{Doc;
 
      // Find the first {{DocMeta ...}} blockMeta ...}} block
      var docmeta
      var docmetaRe = /\{\{\s*DocRe = /\{\{\s*DocMeta\b([\s\S]*?)Meta\b([\s\S]*?)\}\}/i;
      var m = content.match\}\}/i;
      var m = content.match(docmetaRe);
    (docmetaRe);
      if (!m) {
        return $.Deferred if (!m) {
        return $.Deferred().reject('DocMeta block not found').promise();
    ().reject('DocMeta block not found').promise();
      }
 
      var block = m[0];
      var }
 
      var block = m[0];
      var inner = m[1];
 
      // Replace or insert review_ping
      var rp inner = m[1];
 
      // Replace or insert review_ping
      var rpRe = /(\|\s*reviewRe = /(\|\s*review_ping\s*=\s*)(.*)/i;
      var newInner;
      if _ping\s*=\s*)(.*)/i;
      var newInner;
      if (rpRe.test(inner)) {
        newInner = inner.replace(rpRe.test(inner)) {
        newInner = inner.replace(rpRe, '$1' + stamp(rpRe, '$1' + stamp);
      } else {
        // Ensure a newline before inserting
      );
      } else {
        // Ensure a newline before inserting
        if (!/\n$/.test(inner)) inner += if (!/\n$/.test(inner)) inner += '\n';
        newInner = inner + ' | review_ping = '\n';
        newInner = inner + ' | review_ping = ' + stamp;
      }
 
      var new ' + stamp;
      }
 
      var newBlock = block.replace(inner, newInner);
      var newBlock = block.replace(inner, newInner);
      var newContent = contentContent = content.replace(docmetaRe, newBlock);
 
      return api.replace(docmetaRe, newBlock);
 
      return api.postWithToken('csrf', {
        action: 'edit',
.postWithToken('csrf', {
        action: 'edit',
        title: page,
        text: new        title: page,
        text: newContent,
        summary: 'Mark as reviewed',
      Content,
        summary: 'Mark as reviewed',
        minor: true,
        nocreate: true,
        basetimestamp: baseTs
      });
    });
  }
 
  function init($content) {
    var action = mw.config.get('wgAction') || 'view';
    if (action !== 'view') return;
 
    var $docMeta = findDocMeta($content);
     if (!$docMeta.length) {
       // Loud hint once per page view
      mw.notify('MarkReview: DocMeta panel not found on this page.', { type: 'warn' });
      return;
    }
 
    var owner = ($docMeta.attr('data-owner') || '').trim();
    var user = (mw.config.get('wgUserName') || '').trim();
 
    if (!owner) {
      mw.notify('MarkReview: data-owner is missing on DocMeta.', { type: 'warn' });
      return;
    }
    if (!user) {
      // Not logged in — nothing to show
      return;
    }
 
    // Normalize (case-insensitive); also accept underscores vs spaces
    var norm = function (s) { return s.replace(/_/g, ' ').toLowerCase(); };
    if (norm(owner) !== norm(user)) {
      // Add a debug hint if URL contains ?reviewdebug=1
      if (location.search.indexOf('reviewdebug=1') !== -1) {
        mw.notify('MarkReview: owner mismatch. owner=' + owner + ' user=' + user, { type: 'warn' });
      }
      return;
     }
     }
     $actions.empty().append($btn);
 
     injectButton($docMeta, function () {
      var page = mw.config.get('wgPageName');
      var stamp = localStamp();
      updateReviewParam(page, stamp).done(function () {
        // Optional success cue before reload
        location.reload();
      }).fail(function (err) {
        mw.notify('MarkReview failed: ' + (err && err.error && err.error.info ? err.error.info : err), { type: 'error' });
      });
    });
   }
   }


   // Run on initial load and after AJAX loads
   // Run on initial load and after AJAX loads
   mw.hook('wikipage.content').add(run);
   mw.hook('wikipage.content').add(init);
 
  // Also run once on DOM ready to catch any missed hook edge cases
  $(init.bind(null, $(document)));
});
});

Revision as of 09:08, 9 September 2025

/* review_ping in the template call.

```javascript
/* Gadget: MarkReview — One‑click "Mark Gadget: MarkReview — One‑click "Mark as reviewed" (updates review_ping) with robust injection as reviewed" (updates review_ping) with robust injection + diagnostics */
mw.loader.using( + diagnostics */
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.user', 'mediawiki.notify']).['mediawiki.api', 'mediawiki.util', 'mediawiki.user', 'mediawiki.notify']).then(function () {

  function localStamp() {
    varthen(function () {

  function localStamp() {
    var now = new Date();
    return now.get now = new Date();
    return now.getFullYear() + '-' +
      String(nowFullYear() + '-' +
      String(now.getMonth() + 1)..getMonth() + 1).padStart(2, '0')padStart(2, '0') + '-' +
      String + '-' +
      String(now.getDate()).padStart(2, '0')(now.getDate()).padStart(2, '0') + 'T' +
      String + 'T' +
      String(now.getHours()).padStart(2, '0')(now.getHours()). + ':' +
      String(now.getMinutes()).padStart(2, '0') + ':' +
      String(now.getMinutes()).padStart(2, '0')padStart(2, '0') + ':' +
      String(now.getSeconds()).padStart(2, '0');
  }

  function findDocMeta($scope) {
 + ':' +
      String(now.getSeconds()).padStart(2, '0');
  }

  function findDocMeta($scope) {
    // Find either the table.doc-meta or any element with    // Find either the table.doc-meta .doc-meta
    var $el = $scope.find or any element with .doc-meta
    var $el = $scope.find('table.doc-meta').first();
    if('table.doc-meta').first();
    if (!$el.length) $el = $scope.find (!$el.length) $el = $scope.find('.doc-meta').first();
    if (!$el('.doc-meta').first();
    if (!$el.length) $el = $('table.doc-meta, ..length) $el = $('table.doc-meta, .doc-meta').first();
    return $el;
  }

  functiondoc-meta').first();
    return $ injectButton($docel;
  }

  function injectButton($docMeta, onClick) {
    var $table =Meta, onClick) {
    var $table = $docMeta.is('table') ? $docMeta : $ $docMeta.is('table') ? $docMeta : $docMeta.closest('table');
    if (!$docMeta.closest('table');
    if (!$table.length) $tabletable.length) $table = $docMeta; // fallback = $docMeta; // fallback

    // Use existing .doc-meta-actions

    // Use existing .doc-meta-actions if present, else if present, else create
    var $actions = $table create
    var $actions = $table.nextAll('.doc-meta-actions').first();
    if (!$actions.nextAll('.doc-meta-actions').first();
    if (!$actions.length) {
     .length) {
      $actions = $('<div class="doc-meta-actions"></div>').insertAfter($table);
    }

 $actions = $('<div class="doc-meta-actions"></div>').insertAfter($table);
    }

    var $btn = $('    var $btn = $('<a>')
      .attr('href', '#')
      .text('Mark as reviewed<a>')
      .attr('href', '#')
      .text('Mark as reviewed')
      .css({
        display: 'inline-block',
')
      .css({
        display: 'inline-block',
        marginTop: '8px',
               marginTop: '8px',
        padding: '4px 8px',
        background padding: '4px 8px',
        background: '#f0f0f0',
       : '#f0f0f0',
        border: '1px solid #ccc',
        border border: '1px solid #ccc',
        borderRadius: '2px',
       Radius: '2px',
        textDecoration: textDecoration: 'none'
      })
 'none'
      })
      .on('click', function (e) {
        e.prevent      .on('click', function (e) {
Default();
        onClick();
             e.preventDefault();
        onClick();
      });

    $actions.empty().append($btn);
  }

  function });

    $actions.empty().append($btn);
  }

  function updateReviewParam updateReviewParam(page, stamp) {
(page, stamp) {
    var api = new mw.Api();

    return api.get({
         var api = new mw.Api();

    return api.get({
      action: 'query',
      prop: 'revisions action: 'query',
      prop: 'revisions',
      titles: page,
      rvslots',
      titles: page,
      rvslots: 'main',
      rvprop: 'content|timestamp',
      formatversion: 'main',
      rvprop: 'content|timestamp',
      formatversion: 2
    }).then(function: 2
    }).then(function (data) {
      var (data) {
      var pg = data.query.pages[0];
      if (!pg || !pg.revisions || !pg.re pg = data.query.pages[0];
      if (!pg || !pg.revisions || !pg.revisions.length) {
        return $.Deferred().reject('No revisions found').promise();
     visions.length) {
        return $.Deferred().reject('No revisions found').promise();
      }
      var rev = pg.revisions[0];
      var content }
      var rev = pg.revisions[0];
      var content = rev.slots.main = rev.slots.main.content;
      var baseTs = rev.timestamp.content;
      var baseTs = rev.timestamp;

      // Find the first {{Doc;

      // Find the first {{DocMeta ...}} blockMeta ...}} block
      var docmeta
      var docmetaRe = /\{\{\s*DocRe = /\{\{\s*DocMeta\b([\s\S]*?)Meta\b([\s\S]*?)\}\}/i;
      var m = content.match\}\}/i;
      var m = content.match(docmetaRe);
     (docmetaRe);
      if (!m) {
        return $.Deferred if (!m) {
        return $.Deferred().reject('DocMeta block not found').promise();
     ().reject('DocMeta block not found').promise();
      }

      var block = m[0];
      var }

      var block = m[0];
      var inner = m[1];

      // Replace or insert review_ping
      var rp inner = m[1];

      // Replace or insert review_ping
      var rpRe = /(\|\s*reviewRe = /(\|\s*review_ping\s*=\s*)(.*)/i;
      var newInner;
      if _ping\s*=\s*)(.*)/i;
      var newInner;
      if (rpRe.test(inner)) {
        newInner = inner.replace(rpRe.test(inner)) {
        newInner = inner.replace(rpRe, '$1' + stamp(rpRe, '$1' + stamp);
      } else {
        // Ensure a newline before inserting
       );
      } else {
        // Ensure a newline before inserting
        if (!/\n$/.test(inner)) inner += if (!/\n$/.test(inner)) inner += '\n';
        newInner = inner + ' | review_ping = '\n';
        newInner = inner + ' | review_ping = ' + stamp;
      }

      var new ' + stamp;
      }

      var newBlock = block.replace(inner, newInner);
      var newBlock = block.replace(inner, newInner);
      var newContent = contentContent = content.replace(docmetaRe, newBlock);

      return api.replace(docmetaRe, newBlock);

      return api.postWithToken('csrf', {
        action: 'edit',
.postWithToken('csrf', {
        action: 'edit',
        title: page,
        text: new        title: page,
        text: newContent,
        summary: 'Mark as reviewed',
       Content,
        summary: 'Mark as reviewed',
        minor: true,
        nocreate: true,
        basetimestamp: baseTs
      });
    });
  }

  function init($content) {
    var action = mw.config.get('wgAction') || 'view';
    if (action !== 'view') return;

    var $docMeta = findDocMeta($content);
    if (!$docMeta.length) {
      // Loud hint once per page view
      mw.notify('MarkReview: DocMeta panel not found on this page.', { type: 'warn' });
      return;
    }

    var owner = ($docMeta.attr('data-owner') || '').trim();
    var user = (mw.config.get('wgUserName') || '').trim();

    if (!owner) {
      mw.notify('MarkReview: data-owner is missing on DocMeta.', { type: 'warn' });
      return;
    }
    if (!user) {
      // Not logged in — nothing to show
      return;
    }

    // Normalize (case-insensitive); also accept underscores vs spaces
    var norm = function (s) { return s.replace(/_/g, ' ').toLowerCase(); };
    if (norm(owner) !== norm(user)) {
      // Add a debug hint if URL contains ?reviewdebug=1
      if (location.search.indexOf('reviewdebug=1') !== -1) {
        mw.notify('MarkReview: owner mismatch. owner=' + owner + ' user=' + user, { type: 'warn' });
      }
      return;
    }

    injectButton($docMeta, function () {
      var page = mw.config.get('wgPageName');
      var stamp = localStamp();
      updateReviewParam(page, stamp).done(function () {
        // Optional success cue before reload
        location.reload();
      }).fail(function (err) {
        mw.notify('MarkReview failed: ' + (err && err.error && err.error.info ? err.error.info : err), { type: 'error' });
      });
    });
  }

  // Run on initial load and after AJAX loads
  mw.hook('wikipage.content').add(init);

  // Also run once on DOM ready to catch any missed hook edge cases
  $(init.bind(null, $(document)));
});