【问题标题】:Writing an 'Undo' Function for Google Spreadsheets Using GAS使用 GAS 为 Google 电子表格编写“撤消”功能
【发布时间】:2013-08-25 19:53:45
【问题描述】:

目前,Spreadsheet/Sheet/Range 类中没有用于 Google Apps 脚本的 undo() 函数。在 Issue Tracker 上打开了几个问题,我现在只能找到一个(我不知道 Triaged 是什么意思):here

有人建议使用 DriveApp 和修订历史记录的解决方法,但我环顾四周并没有发现任何东西(也许它被埋没了?)。在任何情况下,undo() 函数对于许多不同的操作都是非常必要的。我只能想到一种解决方法,但我无法让它工作(数据存储的方式,我什至不知道它是否可能)。这是一些伪 -

function onOpen () {
  // Get all values in the sheet(s)
  // Stringify this/each (matrix) using JSON.stringify
  // Store this/each stringified value as a Script or User property (character limits, ignore for now)
}

function onEdit () {
  // Get value of edited cell
  // Compare to some value (restriction, desired value, etc.)
  // If value is not what you want/expected, then:
  // -----> get the stringified value and parse it back into an object (matrix)
  // -----> get the old data of the current cell location (column, row)
  // -----> replace current cell value with the old data
  // -----> notifications, coloring cell, etc, whatever else you want
  // If the value IS what you expected, then:
  // -----> update the 'undoData' by getting all values and re-stringifying them
  //        and storing them as a new Script/User property
}

基本上,当电子表格打开时,将所有值存储为脚本/用户属性,并且仅在满足某些单元格条件(打开)时才引用它们。当您要撤消时,获取存储在当前单元格位置的旧数据,并将当前单元格的值替换为旧数据。如果不需要撤消该值,则更新存储的数据以反映对电子表格所做的更改。

到目前为止,我的代码已经失败,我认为这是因为当对象被字符串化和存储时嵌套数组结构丢失了(例如,它没有正确解析)。如果有人写过这种功能,请分享。否则,有关如何编写此内容的建议会很有帮助。

编辑:这些文档非常静态。行/列的数量不会改变,数据的位置也不会改变。如果可能的话,为临时修订历史实现 get-all-data/store-all-data 类型的函数实际上会满足我的需求。

【问题讨论】:

  • 可以通过 Drive API 访问修订,但不能通过 Apps 脚本。撤消似乎因协作而变得复杂。假设您向字段写入值,而您正在与之协作的人删除了该行。您的撤消是否应该恢复该行以写入旧值?只是一个看起来很混乱的例子。回顾修订历史在这里似乎是安全/保守的,尽管更手动。
  • 我将在问题正文中指定这不是协作文档,并且不会删除/编辑行。只能编辑一列,因为文档的其余部分使用importRange 函数来填充值。我知道协作撤消会很复杂。但是,我对如何将 Drive API 与 Google Apps Script 一起使用一无所知。我浏览了所有 GAS 文档,但没有看到任何关于修订历史的信息。

标签: google-apps-script google-sheets


【解决方案1】:

修改了组中的答案以允许用户选择多个单元格时的范围:

我使用了我称之为“双张纸”的东西。

/**
 * Test function for onEdit. Passes an event object to simulate an edit to
 * a cell in a spreadsheet.
 * Check for updates: https://stackoverflow.com/a/16089067/1677912
 */
function test_onEdit() {
  onEdit({
    user : Session.getActiveUser().getEmail(),
    source : SpreadsheetApp.getActiveSpreadsheet(),
    range : SpreadsheetApp.getActiveSpreadsheet().getActiveCell(),
    value : SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue(),
    authMode : "LIMITED"
  });
}


function onEdit() {
  // This script prevents cells from being updated. When a user edits a cell on the master sheet,
  // it is checked against the same cell on a helper sheet. If the value on the helper sheet is
  // empty, the new value is stored on both sheets.
  // If the value on the helper sheet is not empty, it is copied to the cell on the master sheet,
  // effectively undoing the change.
  // The exception is that the first few rows and the first few columns can be left free to edit by
  // changing the firstDataRow and firstDataColumn variables below to greater than 1.
  // To create the helper sheet, go to the master sheet and click the arrow in the sheet's tab at
  // the tab bar at the bottom of the browser window and choose Duplicate, then rename the new sheet
  // to Helper.
  // To change a value that was entered previously, empty the corresponding cell on the helper sheet,
  // then edit the cell on the master sheet.
  // You can hide the helper sheet by clicking the arrow in the sheet's tab at the tab bar at the
  // bottom of the browser window and choosing Hide Sheet from the pop-up menu, and when necessary,
  // unhide it by choosing View > Hidden sheets > Helper.
  // See https://productforums.google.com/d/topic/docs/gnrD6_XtZT0/discussion

  // modify these variables per your requirements
  var masterSheetName = "Master" // sheet where the cells are protected from updates
  var helperSheetName = "Helper" // sheet where the values are copied for later checking

  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var masterSheet = ss.getActiveSheet();
  if (masterSheet.getName() != masterSheetName) return;

  var masterRange = masterSheet.getActiveRange();

  var helperSheet = ss.getSheetByName(helperSheetName);
  var helperRange = helperSheet.getRange(masterRange.getA1Notation());
  var newValue = masterRange.getValues();
  var oldValue = helperRange.getValues();
  Logger.log("newValue " + newValue);
  Logger.log("oldValue " + oldValue);
      Logger.log(typeof(oldValue));
  if (oldValue == "" || isEmptyArrays(oldValue)) {
    helperRange.setValues(newValue);
  } else {
    Logger.log(oldValue);
    masterRange.setValues(oldValue);

  }
}

// In case the user pasted multiple cells this will be checked
function isEmptyArrays(oldValues) {
  if(oldValues.constructor === Array && oldValues.length > 0) {
    for(var i=0;i<oldValues.length;i++) {
      if(oldValues[i].length > 0 && (oldValues[i][0] != "")) {
          return false; 
      }
    }
  }
  return true;
}

【讨论】:

    【解决方案2】:

    当我需要保护工作表但允许通过边栏进行编辑时,我遇到了类似的问题。我的解决方案是准备两张纸(一张隐藏)。如果您编辑第一个工作表,这将触发 onEdit 过程并重新加载第二个工作表中的值。如果您取消隐藏并编辑第二个工作表,它会从第一个工作表重新加载。完美运行,大量删除数据并观看其自我修复非常有趣!

    【讨论】:

    • 这不是一个坏主意!我不敢相信我没有早点想到这一点:P
    【解决方案3】:

    只要不添加或删除行和列,就可以将行号和列号作为存储在 ScriptDb 中的历史值的索引。

    function onEdit(e) {
      // Exit if outside validation range
      // Column 3 (C) for this example
      var row = e.range.getRow();
      var col = e.range.getColumn();
      if (col !== 3) return;
      if (row <= 1) return; // skip headers
    
      var db = ScriptDb.getMyDb();
    
      // Query database for history on this cell
      var dbResult = db.query({type:"undoHistory",
                           row:row,
                           col:col});
      if (dbResult.getSize() > 0) {
        // Found historic value
        var historicObject = dbResult.next();
      }
      else {
        // First change for this cell; seed historic value
        historicObject = db.save({type:"undoHistory",
                                  row:row,
                                  col:col,
                                  value:''});
      }
    
      // Validate the change.
      if (valueValid(e.value,row,col)) {
        // update script db with this value
        historicObject.value = e.value;
        db.save(historicObject);
      }
      else {
        // undo the change.
        e.range.getSheet()
               .getRange(row,col)
               .setValue(historicObject.value);
      }
    }
    

    您需要提供一个函数来验证您的数据值。同样,在此示例中,我们只关心一列中的数据,因此验证非常简单。例如,如果您需要对不同的列执行不同类型的验证,那么您可以在 col 参数上使用 switch

    /**
     * Test validity of edited value. Return true if it
     * checks out, false if it doesn't.
     */
    function valueValid( value, row, col ) {
      var valid = false;
    
      // Simple validation rule: must be a number between 1 and 5.
      if (value >= 1 && value <= 5)
        valid = true;
    
      return valid;
    }
    

    合作

    此撤消功能适用于协作编辑的电子表格,尽管在脚本数据库中存储历史值存在竞争条件。如果多个用户同时对一个单元格进行第一次编辑,则数据库最终可能会出现多个代表该单元格的对象。在随后的更改中,使用 query() 并选择仅选择第一个结果可确保仅选择其中一个倍数。

    如果这成为一个问题,可以通过将函数包含在 Lock 中来解决。

    【讨论】:

    • Dang Mogsdad,再次成为 GAS 的重头戏!我完全忘记了 ScriptDB 的存在。这真的不是一个巨大的子项目,只是一种有助于我的工作流管理系统的必要功能,而且没有接触到直接的undo 电话让我卡了一分钟。我去看看ScriptDB,再次感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-17
    • 2014-12-06
    • 1970-01-01
    相关资源
    最近更新 更多