FormApp.GridItem.setRows() 在链接的 Sheet 中生成重复的列。如何预防?

FormApp.GridItem.setRows() producing duplicated columns in the linked Sheet. How to prevent it?

setRows() 运行 在现有 GridItem 上时,生成的 Form GridItem 元素没问题,但 [=72= 中的列]ed Sheet 在下一栏中转载。新列是重复的,但是有一个隐藏的 属性 表明它们属于 Form(所以我们不能删除新的列)。这是什么属性?旧列不再属于 Form。旧列可能具有现有值或以前的表单响应。

如何预防?

FormApp 应正确处理 sheet 范围,方法是查找现有列并仅添加具有新字符串数组的真正新列。

Google 表格 UI 是如何处理的: 当我们使用Forms UI时,我们可以很容易地在GridItem中添加新的行,并且link Sheet会更新而不会出现重复的列。

这是要测试的表单,请在运行输入代码之前创建一个新的linked response-Sheet:

Copy Sample Form with GAS code

这是 GAS 代码:

function reGenerateRows1() {
  const form = FormApp.getActiveForm()
  for (const item of form.getItems(FormApp.ItemType.GRID)) {
    if (item.getTitle() === 'First Question') {
      item.asGridItem()
      // repopulate from existing rows or from new string array
        .setRows(item.asGridItem().getRows())
      // .setRows(['A','B','C','D','E'])
      // .setRows(['A','B','C','D','E','F'])
      // result: Duplicates in destination sheet
      break;
    }
  }
}
function reGenerateRows2() {
  const form = FormApp.getActiveForm()
  for (const item of form.getItems(FormApp.ItemType.GRID)) {
    if (item.getTitle() === 'First Question') {
      let grid = item.asGridItem()
      let rows = grid.getRows()
      let columns = grid.getColumns()
      let title = grid.getTitle()
      form.deleteItem(item)
      // recreate GridItem and repopulate
      form.addGridItem().setTitle(title).setRows(rows).setColumns(columns)
      // result: Duplicates in destination sheet
      break;
    }
  }
}
function reGenerateRows3() {
  const form = FormApp.getActiveForm()
  for (const item of form.getItems(FormApp.ItemType.GRID)) {
    if (item.getTitle() === 'First Question') {
      let grid = item.asGridItem()
      let rows = grid.getRows()
      let columns = grid.getColumns()
      let id = item.getId()
      // repopulate from id
      form.getItemById(id).asGridItem().setRows(rows).setColumns(columns)
      // result: Duplicates in destination sheet
      break;
    }
  }
}

编辑了截图(我是新来的。无法嵌入图片。只有 imgur-linked 图片)

这是.setRows(['A','B','C','D','E','F'])的结果 原始行带有 'A','B','C'

Screenshot of resulting .setRows()

这是我希望 .setRows() 做的事情:

Screenshot of expected result

在带有链接的表格中编辑网格 sheet。

手动编辑带链接sheet表格中的网格的过程如下:

  1. 您单击项目的输入框。
  2. 您更新文本。
  3. sheet会自动替换相应栏目的标题。

但是,使用 Apps 脚本。唯一可用于更改标题的过程是使用方法 .setRows.setColumns。这与 UI 的行为非常不同。本质上,它 替换 行,因此,链接的 sheet 将生成新列以保留以前的答案。

它会创建新列,因为它无法知道哪个新标题对应于旧标题。

例如,如果您有一些行:

  • 一个
  • B
  • C

当您从 Apps 脚本获取此内容时,您必须先 getRows()

const rows = item.getRows()

这是作为 字符串列表返回的 :

['A', 'B', 'C']

所以在这里您已经丢失了一些重要信息。如果行返回为自己的 object 类型,带有内部 ID 号或其他内容,这样您就可以在该行上调用假设的 setTitle,那么这会很好,但是,这不是案.

如果您随后将行更改为:

const newRows = ['A', 'C', 'B']

然后将其传递给如下形式:

item.setRows(newRows)

它无法知道哪个项目对应于另一个项目,所以它只是重新生成整个项目。

同样,要使它的行为与在 UI 中的行为相同,行和列需要以不同方式实现。也许不如自己class。也许 FormGridColumnsFormGridRows 或类似的东西。理想情况下,这将有自己的方法,例如:

const rows = item.getRows()
rows[0].setTitle('B')

这是一个可以实施的示例,但由于尚未实施,您唯一的办法就是使用此模板提交功能请求:

File an Apps Script Feature Request

可能的解决方法

可以编写自己的函数来更新和合并 sheet 中的列,但它很快就会变得复杂。它还需要 Utilities.sleepSpreadsheetApp.flush() 的剂量,但在我的测试中似乎是可靠的。

例如,我编写了这个函数来更新和合并行,但前提是新行的元素数与现有行的元素数相同。

/**
 * Updates a gridItem's column names and merges previous answers
 * into the new columns that are created automatically.
 * @param {GridItem}
 * @param {string}
 * @param {array} - must have same number of rows as gridItem currently has
 */
function updateRows(gridItem, ssID, newRows){
  // Getting basic data from sheet
  const file = SpreadsheetApp.openById(ssID);
  const sheet = file.getSheetByName("Form Responses 1");

  const dataRange = sheet.getDataRange();
  const values = dataRange.getValues();
  const headers = values.shift()

  // Getting existing title and rows from form
  const itemTitle = gridItem.getTitle()
  const oldRows = gridItem.getRows()

  // Check to make sure newRows is the same length as the current rows
  if (oldRows.length !== newRows.length) {
    throw "new rows and old rows must be same length"
  }

  // Finding which column the old rows are in the spreadsheet
  const oldRowHeaderIndices = getIndicesOfRows(itemTitle, headers, oldRows)

  // Setting the new rows which will autogenerate new columns in the spreadsheet
  gridItem.setRows(newRows)

  // Need to sleep and flush so that the changes have time to propagate
  console.log("sleeping")
  Utilities.sleep(6000) // I found anything less than 6 seconds was unreliable
  console.log("woken up")
  SpreadsheetApp.flush()

  // Refreshing the data from the sheet
  const updatedDataRange = sheet.getDataRange();
  const updatedValues = updatedDataRange.getValues();
  const updatedHeaders = updatedValues.shift()
  
  // Finding the columns that the new rows are in
  const newRowHeaderIndices = getIndicesOfRows(itemTitle, updatedHeaders, newRows)

  // Copying all the data from old columns to new columns
  for (let i = 0; i != oldRows.length; i++) {

    const sourceRange = sheet.getRange(
      2,
      oldRowHeaderIndices[i] + 1,
      updatedValues.length,
      1
    )

    const destinationRange = sheet.getRange(
      2,
      newRowHeaderIndices[i] + 1,
      updatedValues.length,
      1
    )

    destinationRange.setValues(sourceRange.getValues())
  }

  // Deleting old columns (assuming that they are all adjacent to each other)
  sheet.deleteColumns(oldRowHeaderIndices[0] + 1, oldRowHeaderIndices.length)
}

function getIndicesOfRows(itemTitle, headers, rows){
  return rows.map((rowTitle, i) => {
    return headers.findIndex(header => {
        return header === `${itemTitle} [${rowTitle}]`
    })
  })
}

用法示例

起点:

然后你可以像这样调用函数(改编自你的代码):

function test() {
  // Get your form as usual
  const form = FormApp.getActiveForm()
  for (const item of form.getItems(FormApp.ItemType.GRID)) {
    if (item.getTitle() === 'First Question') {

      const originalRows = item.asGridItem().getRows()
      // ['A', 'B', 'C', 'D', 'E', 'F']

      // Here just appending "NEW" to the name to create a new row
      const newRows = originalRows.map(row => "NEW " + row)
      // ['NEW A', 'NEW B', 'NEW C', 'NEW D', 'NEW E', 'NEW F']

      const ssID = form.getDestinationId()

      // Call custom function
      updateRows(item.asGridItem(), ssID, newRows)

      break;
    }
  }
}

该脚本需要大约 10 秒才能 运行 并将导致:

结论

建议的功能在使用上可能非常有限。不过,如果有足够的时间,它可能会适应大多数用例。

显然,如果 Google 可以将网格行和网格列实现为 classes,那将是最好的,但这将需要一个功能请求并且 Google 接受它,如果他们觉得很多人都想要它。

参考文献