Динамическое зависимое раскрывающееся меню Google Web App

Я пытался добавить зависимое выпадающее меню 3-го и 4-го уровня, используя код из Code with Curt (https://codewithcurt.com/create-dependent-drop-down-on-google-web-app/), но я сталкиваюсь с некоторыми проблемами. В приведенном ниже коде я пытаюсь добавить 3-й уровень, но, похоже, это не работает. Это результат, которого я пытаюсь достичь. Я не уверен, есть ли самый быстрый способ загрузить раскрывающийся список из листов Google, так как этот код загружается примерно за 3 секунды, или лучший способ получить его из листов.

Вот код:

Скрипт Google Apps:

function doGet(e) {
  var htmlOutput = HtmlService.createTemplateFromFile('DependentSelect');
  var colors = getColors();
  htmlOutput.message = '';
  htmlOutput.colors = colors;
  return htmlOutput.evaluate();
}

function doPost(e) {

  Logger.log(JSON.stringify(e));

  var name = e.parameters.name.toString();
  var color = e.parameters.color.toString();
  var fruit = e.parameters.fruit.toString();
  var class = e.parameters.class.toString(); //class is a reserved word

  AddRecord(name, color, fruit, class);

  var htmlOutput = HtmlService.createTemplateFromFile('DependentSelect');
  var colors = getColors();
  htmlOutput.message = 'Record Added';
  htmlOutput.colors = colors;
  return htmlOutput.evaluate();

}

function getColors() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var lovSheet = ss.getSheetByName("LOV");
  var getLastRow = lovSheet.getLastRow();
  var return_array = [];
  for (var i = 2; i <= getLastRow; i++) {
    if (return_array.indexOf(lovSheet.getRange(i, 1).getValue()) === -1) {
      return_array.push(lovSheet.getRange(i, 1).getValue());
    }
  }


  return return_array;
}

function getFruits(color) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var lovSheet = ss.getSheetByName("LOV");
  var getLastRow = lovSheet.getLastRow();
  var return_array = [];
  for (var i = 2; i <= getLastRow; i++) {
    if (lovSheet.getRange(i, 1).getValue() === color) {
      return_array.push(lovSheet.getRange(i, 2).getValue());
    }
  }


  return return_array;
}
function getClass(fruit) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var lovSheet = ss.getSheetByName("LOV");
  var getLastRow = lovSheet.getLastRow();
  var return_array = [];
  for (var i = 2; i <= getLastRow; i++) {
    if (lovSheet.getRange(i, 2).getValue() === fruit) {
      return_array.push(lovSheet.getRange(i, 3).getValue());
    }
  }


  return return_array.sort();
}


function AddRecord(name, color, fruit, class) {
  var url = '';   //URL OF GOOGLE SHEET;
  var ss = SpreadsheetApp.openByUrl(url);
  var dataSheet = ss.getSheetByName("DATA");
  dataSheet.appendRow([name, color, fruit, class, new Date()]);
}

function getUrl() {
  var url = ScriptApp.getService().getUrl();
  return url;
}

HTML:

<!DOCTYPE html>
<html>

<head>
  <base target = "_top">
</head>

<body>
  <script>
    function GetFruit(color) 
    {
    
    google.script.run.withSuccessHandler(function(ar) 
    {

    console.info(ar);
    
    fruit.length = 0;
    
    let option = document.createElement("option");
    option.value = "";
    option.text = "";
    fruit.appendChild(option);
    
    ar.forEach(function(item, index) 
    {    
      let option = document.createElement("option");
      option.value = item;
      option.text = item;
      fruit.appendChild(option);    
    });
    
    }).getFruits(color);
    
    };

 function getClass(queue)
{

google.script.run.withSuccessHandler(function(ar) 
{

console.info(ar);

class.length = 0;

let option = document.createElement("option");
option.value = "";
option.text = "";
class.appendChild(option);

ar.forEach(function(item, index) 
{    
  let option = document.createElement("option");
  option.value = item;
  option.text = item;
  class.appendChild(option);    
});

}).getClass(queue);

};
  </script>


  <h1>Web App Dependent Drop Down</h1>
  <?var url = getUrl();?>
  <form method = "post" action = "<?= url ?>">
    <label style = "font-size: 20px" >Name</label><br>
    <input type = "text" name = "name" style = "font-size: 20px" /><br><br>
    <label style = "font-size: 20px" >Colors</label><br>
    <select name = "color" style = "font-size: 20px" onchange = "GetFruit(this.value)" >
      <option value = "" ></option>
      <? for(var i = 0; i < colors.length; i++) { ?>      
      <option value = "<?= colors[i] ?>" ><?= colors[i] ?></option>
      <? } ?>
      </select><br><br>
    <label style = "font-size: 20px" >Fruit</label><br>
    <select name = "fruit" id = "fruit" style = "font-size: 20px" >
      </select><br><br>
    <label style = "font-size: 20px" >Class</label><br>
    <select name = "location" id = "location" style = "font-size: 20px" >
  <option value = "" selected disabled>Select Class</option>
  </select><br><br>

    <label style = "font-size: 20px" >Brand</label><br>
    <select name = "location" id = "location" style = "font-size: 20px" >
  <option value = "" selected disabled>Select Brand</option>
  </select><br><br>
    <input type = "submit" name = "submitButton" value = "Submit" style = "font-size: 20px" />
    <span style = "font-size: 20px" ><?= message ?></span>
  </form>
</body>

</html>

В своем вопросе вы говорите I've been trying to add 3rd and 4th level dependent dropdown. И в вашем текущем сценарии кажется, что ваши текущие проблемы — это I'm trying to add a 3rd level, but it doesn't seem to work. и I'm not sure if there's a fastest way to load the dropdown from Google sheets. И, когда я увидел ваш сценарий показа, на вашей стороне HTML, кажется, что getClass не вызывается. И, я не могу найти скрипт для 4-го уровня. Кроме того, я не могу представить вашу электронную таблицу. Прошу прощения за это. Могу я уточнить детали вашего вопроса?

Tanaike 15.10.2022 02:56

Привет, @Tanaike, спасибо за уделенное время. Я еще не включил сценарий для 4-го уровня, так как я все еще пытаюсь понять 3-й уровень, но я также хочу иметь раскрывающийся список 4-го уровня. Что касается моего листа, у меня есть только два листа: «ДАННЫЕ» и «LOV» для раскрывающегося списка, он имеет 4 столбца (цвет, фрукты, класс, бренд). Пример данных строки: Цвет: Красный, Фрукт: Яблоко, Класс: A, Марка: X).

coders_key 15.10.2022 03:25

Спасибо за ответ. Из вашего ответа я предложил модифицированный сценарий в качестве ответа. Не могли бы вы подтвердить это? Если я неправильно понял ваш вопрос и это было бесполезно, приношу свои извинения. К сожалению, кажется, что на текущем этапе изображение не может быть загружено. Итак, теперь я не мог загрузить тестовую ситуацию в виде изображения. Прошу прощения за это.

Tanaike 15.10.2022 07:49

Привет @Tanaike. Приносим извинения за поздний ответ. Удивительно, это загружается намного быстрее. Большое спасибо. Единственная проблема, которую я заметил, - это раскрывающийся список уровня 4, который является брендом, он не меняется динамически в зависимости от выбора класса.

coders_key 18.10.2022 02:10

О The only issue I've noticed is the level 4 dropdown which is the brand, it does not dynamically change based on Class selection., когда «Класс 1» изменяется на «Класс 2», значение «Бренд» изменяется. Прошу прощения за это. Чтобы правильно понять это, можете ли вы предоставить образец электронной таблицы, включая предложенный мной сценарий для ее правильной репликации? Этим я хотел бы подтвердить это.

Tanaike 18.10.2022 02:38
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
226
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы можете использовать следующие GAS и HTML:

Скрипт Google Apps

Я добавил функцию onlyUnique (из другого поста SO), чтобы отфильтровать похожие записи. Я также добавил другие функции для столбцов Class и Brand. Я также изменил переменную class на classParam, так как class — зарезервированное слово.

function doGet(e) {
  var htmlOutput =  HtmlService.createTemplateFromFile('DependentSelect');
  var colors = getColors();
  var fruits = getFruits();
  var classParams = getClasses();
  var brands = getBrands();
  htmlOutput.message = '';
  htmlOutput.colors = colors;
  htmlOutput.fruits = fruits;
  htmlOutput.classParams = classParams;
  htmlOutput.brands = brands;
  return htmlOutput.evaluate();
}

function doPost(e) {
  
  Logger.log(JSON.stringify(e));
  
  var name = e.parameters.name.toString();
  var color = e.parameters.color.toString();
  var fruit = e.parameters.fruit.toString();
  var classParam = e.parameters.classParam.toString();
  var brand = e.parameters.brand.toString();
  
  AddRecord(name, color, fruit, classParam, brand);
  
  var htmlOutput =  HtmlService.createTemplateFromFile('DependentSelect');
  var colors = getColors();
  var fruits = getFruits();
  var classParams = getClasses();
  var brands = getBrands();
  htmlOutput.message = 'Record Added';
  htmlOutput.colors = colors;
  htmlOutput.fruits = fruits;
  htmlOutput.classParams = classParams;
  htmlOutput.brands = brands;

  return htmlOutput.evaluate(); 
  
}

function onlyUnique(value, index, self) {
  return self.indexOf(value) === index;
}

function getColors() { 
  var ss= SpreadsheetApp.getActiveSpreadsheet();
  var lovSheet = ss.getSheetByName("LOV"); 
  var getLastRow = lovSheet.getLastRow();
  var return_array = [];
  for(var i = 2; i <= getLastRow; i++)
  {
      if (return_array.indexOf(lovSheet.getRange(i, 1).getValue()) === -1) {
        return_array.push(lovSheet.getRange(i, 1).getValue());
      }
  }


  return return_array.filter(onlyUnique);  
}

function getFruits(color) { 
  var ss= SpreadsheetApp.getActiveSpreadsheet();
  var lovSheet = ss.getSheetByName("LOV"); 
  var getLastRow = lovSheet.getLastRow();
  var return_array = [];
  for(var i = 2; i <= getLastRow; i++)
  {
      if (lovSheet.getRange(i, 1).getValue() === color) {
        return_array.push(lovSheet.getRange(i, 2).getValue());
      }
  }


  return return_array.filter(onlyUnique);  
}

function getClasses(color, fruit) { 
  var ss= SpreadsheetApp.getActiveSpreadsheet();
  var lovSheet = ss.getSheetByName("LOV"); 
  var getLastRow = lovSheet.getLastRow();
  var return_array = [];
  for(var i = 2; i <= getLastRow; i++)
  {
      if ((lovSheet.getRange(i, 1).getValue() === color) && (lovSheet.getRange(i, 2).getValue() === fruit)) {
        return_array.push(lovSheet.getRange(i, 3).getValue());
      }
  }

  return return_array.filter(onlyUnique);  
}

function getBrands(color, fruit, classParam) { 
  var ss= SpreadsheetApp.getActiveSpreadsheet();
  var lovSheet = ss.getSheetByName("LOV"); 
  var getLastRow = lovSheet.getLastRow();
  var return_array = [];
  for(var i = 2; i <= getLastRow; i++)
  {
      if ((lovSheet.getRange(i, 1).getValue() === color) && (lovSheet.getRange(i, 2).getValue() === fruit) && (lovSheet.getRange(i, 3).getValue() === classParam)) {
        return_array.push(lovSheet.getRange(i, 4).getValue());
      }
  }


  return return_array.filter(onlyUnique);  
}

function AddRecord(name, color, fruit, classParam, brand) {
  var url = "";   //INSERT SPREADSHEET URL HERE <---------
  var ss = SpreadsheetApp.openByUrl(url);
  var dataSheet = ss.getSheetByName("DATA");
  dataSheet.appendRow([name, color, fruit, classParam, brand, new Date()]);
}

function getUrl() {
 var url = ScriptApp.getService().getUrl();
 return url;
}

HTML

<!DOCTYPE html>
<html>
  <head>
    <base target = "_top">
    <script>
    function GetFruit(color) 
    {
    
    google.script.run.withSuccessHandler(function(ar) 
    {

    console.info(ar);
    
    fruit.length = 0;
    
    let option = document.createElement("option");
    option.value = "";
    option.text = "";
    fruit.appendChild(option);
    
    ar.forEach(function(item, index) 
    {    
      let option = document.createElement("option");
      option.value = item;
      option.text = item;
      fruit.appendChild(option);    
    });
    
    }).getFruits(color);
    
    };

    function GetClass(color, fruit) 
    {
    
    google.script.run.withSuccessHandler(function(ar) 
    {

    console.info(ar);
    
    classParam.length = 0;
    
    let option = document.createElement("option");
    option.value = "";
    option.text = "";
    classParam.appendChild(option);
    
    ar.forEach(function(item, index) 
    {    
      let option = document.createElement("option");
      option.value = item;
      option.text = item;
      classParam.appendChild(option);    
    });
    
    }).getClasses(color, fruit);
    
    };

    function GetBrand(color, fruit, classParam) 
    {
    
    google.script.run.withSuccessHandler(function(ar) 
    {

    console.info(ar);
    
    brand.length = 0;
    
    let option = document.createElement("option");
    option.value = "";
    option.text = "";
    brand.appendChild(option);
    
    ar.forEach(function(item, index) 
    {    
      let option = document.createElement("option");
      option.value = item;
      option.text = item;
      brand.appendChild(option);    
    });
    
    }).getBrands(color, fruit, classParam);
    
    };
  </script>  
  </head>
  <body>
    <h1>Web App Dependent Drop Down</h1>
    <?var url = getUrl();?>
    <form method = "post" action = "<?= url ?>" >
      <!-- name -->
      <label style = "font-size: 20px" >Name</label><br>
      <input type = "text" name = "name" style = "font-size: 20px" /><br><br>

      <!-- color -->
      <label style = "font-size: 20px" >Colors</label><br>
      <select name = "color" id = "color" style = "font-size: 20px" onchange = "GetFruit(this.value)" >
      <option value = "" ></option>
      <? for(var i = 0; i < colors.length; i++) { ?>      
      <option value = "<?= colors[i] ?>" ><?= colors[i] ?></option>
      <? } ?>
      </select><br><br>

      <!-- fruit -->
      <label style = "font-size: 20px" >Fruits</label><br>
      <select name = "fruit" id = "fruit" style = "font-size: 20px" onchange = "GetClass(color.value, this.value)" >
      <option value = "" ></option>
      <? for(var i = 0; i < fruits.length; i++) { ?>      
      <option value = "<?= fruits[i] ?>" ><?= fruits[i] ?></option>
      <? } ?>
      </select><br><br>

      <!-- class -->
      <label style = "font-size: 20px" >Classes</label><br>
      <select name = "classParam" id = "classParam" style = "font-size: 20px" onchange = "GetBrand(color.value, fruit.value, this.value)" >
      <option value = "" ></option>
      <? for(var i = 0; i < classParams.length; i++) { ?>      
      <option value = "<?= classParams[i] ?>" ><?= classParams[i] ?></option>
      <? } ?>
      </select><br><br>

      <!-- brand -->
      <label style = "font-size: 20px" >Brand</label><br>
      <select name = "brand" id = "brand" style = "font-size: 20px" >
      </select><br><br>

      <input type = "submit" name = "submitButton" value = "Submit" style = "font-size: 20px" /> 
      <span style = "font-size: 20px" ><?= message ?></span>
      
    </form>
  </body>
</html>

Образец данных

Веб-приложение

Вывод

Не стесняйтесь спрашивать, есть ли у вас какие-либо вопросы относительно моего ответа.

PatrickdC 15.10.2022 05:16

Ух! Спасибо, Патрик! Ценю тебя мужик! Я только что заметил, что бренд не меняется динамически в зависимости от выбора классов.

coders_key 15.10.2022 07:01

Спасибо, что указали на ошибку. По-видимому, фильтрация для каждого столбца зависит только от первого столбца слева от него. я посмотрю на это

PatrickdC 15.10.2022 07:25

Обновлены как GAS, так и HTML. Я добавил дополнительные коды в условные операторы, а также добавил входные переменные в другие функции. Пожалуйста, протестируйте его на своей стороне.

PatrickdC 15.10.2022 07:46

Привет, ПатрикдС. Приносим извинения за задержку ответа. Ты удивительный!! Это сработало отлично. Благодарим вас за помощь.

coders_key 18.10.2022 02:16

Рад слышать. Если мой ответ помог вам, пожалуйста, не стесняйтесь принять мой ответ или проголосовать за него, чтобы другие пользователи (которые могут иметь те же проблемы, что и вы) знали, что они могут использовать этот метод, который я предоставил.

PatrickdC 19.10.2022 05:42
Ответ принят как подходящий

Я считаю, что ваша цель заключается в следующем.

  • Вы хотите снизить стоимость обработки вашего сценария.

Точки модификации:

  • Когда циклический процесс используется с использованием HTML-шаблона, стоимость процесса становится высокой. Реф

    • В этом случае для замены значений используется шаблон HTML.
  • Когда используется google.script.run, стоимость процесса становится высокой.

    • В этом случае google.script.run используется для отправки значений на сторону скрипта Google Apps вместо отправки формы.
    • Из вашего сценария показа я подумал, что значения могут не требоваться для отправки с запросом HTML. Итак, в этой модификации значения отправляются с помощью google.script.run.
  • Создание параметров в теге select выполняется в Javascript с использованием первых загруженных значений.

  • На стороне скрипта Google Apps getValue() используется в цикле. В этом случае стоимость процесса становится высокой. Реф

Когда эти моменты будут отражены в вашем сценарии показа, как насчет следующей модификации?

Сторона скрипта Google Apps:

function doGet(e) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("LOV");
  var [, ...values] = sheet.getDataRange().getDisplayValues();
  var htmlOutput = HtmlService.createTemplateFromFile('DependentSelect');
  htmlOutput.values = JSON.stringify(values);
  htmlOutput.message = '';
  return htmlOutput.evaluate();
}

function addRecord({ name, color, fruit, clas, brand }) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("DATA");
  sheet.appendRow([name, color, fruit, clas, brand, new Date()]);
}

Сторона HTML и Javascript:

<h1>Web App Dependent Drop Down</h1>
<form>
  <label style = "font-size: 20px" >Name</label><br>
  <input type = "text" name = "name" style = "font-size: 20px" /><br><br>
  <label style = "font-size: 20px" >Colors</label><br>
  <select id = "colors" name = "color" style = "font-size: 20px" onchange = "setOptions('fruit', getValues(this.value, 0))" ></select><br><br>
  <label style = "font-size: 20px" >Fruit</label><br>
  <select name = "fruit" id = "fruit" style = "font-size: 20px" onchange = "setOptions('clas', getValues(this.value, 1))" ></select><br><br>
  <label style = "font-size: 20px" >Class</label><br>
  <select name = "clas" id = "clas" style = "font-size: 20px" onchange = "setOptions('brand', getValues(this.value, 2))" ></select><br><br>
  <label style = "font-size: 20px" >Brand</label><br>
  <select name = "brand" id = "brand" style = "font-size: 20px" ></select><br><br>
  <input type = "button" name = "submitButton" value = "Submit" style = "font-size: 20px" onclick = "sample(this.parentNode)" >
  <span style = "font-size: 20px" ><?= message ?></span>
</form>
<script>
const values = JSON.parse(<?= values ?>);

function setOptions(id, v) {
  const s = document.getElementById(id);
  s.innerHTML = "";
  v.forEach(a => {
    const option = document.createElement("option");
    option.value = a;
    option.innerHTML = a;
    s.appendChild(option);
 });
}

function getValues(e, i) {
  return ["", ...new Set(values.reduce((ar, r) => (r[i] == e && ar.push(r[i + 1]), ar), []))];
}

window.onload = function() {
  setOptions("colors", ["", ...new Set(values.map(([a]) => a))]);
}

function sample(e) {
  google.script.run.addRecord(e);
}
</script>

Примечание:

! Приносим извинения за поздний ответ. Удивительно, это загружается намного быстрее. Большое спасибо. Единственная проблема, которую я заметил, - это раскрывающийся список уровня 4, который является брендом, он не меняется динамически в зависимости от выбора класса.

coders_key 18.10.2022 02:09

@coders_key Спасибо за ответ. Насчет The only issue I've noticed is the level 4 dropdown which is the brand, it does not dynamically change based on Class selection., когда я тестировал предложенный мной сценарий, я не смог воспроизвести вашу ситуацию. Прошу прощения за это. Чтобы правильно понять вашу текущую проблему it does not dynamically change based on Class selection, можете ли вы предоставить образец электронной таблицы для ее правильного воспроизведения? Этим я хотел бы подтвердить это.

Tanaike 18.10.2022 02:11

Привет, например. У меня есть этот набор данных. Когда я попытался отфильтровать пример. Красное->Яблоко->Класс 1> в раскрывающихся списках брендов показаны бренд 1, бренд X и бренд 3. Вместо просто брендов 1 и X. Красное яблоко, класс 1, бренд 1, красное яблоко, класс 1, бренд X, желтый банан, класс 1, бренд 3.

coders_key 18.10.2022 02:21

@coders_key Спасибо за ответ. К сожалению, я не смог понять ваш ответ. Могу я спросить вас о деталях вашего ответа?

Tanaike 18.10.2022 02:23

Я обновил снимок экрана в своем исходном сообщении.

coders_key 18.10.2022 02:32

@coders_key Спасибо за ответ. Когда я увидел ваше обновленное изображение, я подумал, что «Класс 1» не является «Брендом 1, Брендом 3, Брендом X» из предоставленных вами образцов данных. Я думаю, что когда это «Класс 1», это «Бренд 1, Бренд 2, Бренд X». Поэтому я не могу понять вашу текущую проблему. Прошу прощения за это. Могу я спросить вас о деталях вашей текущей проблемы и вашей цели? Этим я хотел бы подтвердить это. Кстати, когда «Класс 1» меняется на «Класс 2», значение «Бренд» меняется. Как насчет этого?

Tanaike 18.10.2022 02:34

@coders_key О! Я заметил, что предоставленные вами данные в вашем ответе были изменены только сейчас. Прошу прощения за это. Но когда «Класс 1» меняется на «Класс 2», значение «Марка» изменяется. Прошу прощения за это. Чтобы правильно понять это, можете ли вы предоставить образец электронной таблицы, включая предложенный мной сценарий для ее правильной репликации? Этим я хотел бы подтвердить это.

Tanaike 18.10.2022 02:45

Другие вопросы по теме

Похожие вопросы

Запустите скрипт Google с помощью скрипта Google
Генерировать уникальный добавочный серийный номер для нескольких дискретных участников
Как использовать разделение со столбцом, где целое число каждой ячейки хранится в виде массива
Как переместить 10 тыс. строк X 170 столбцов данных между листами, получая формулы изображений ячеек col C с помощью GAS?
Не только автоматическое заполнение ячеек, но и удаление строки при установленном флажке
Ошибка сценария Google Sheets: Исключение: диапазоны должны иметь хотя бы один диапазон
Изображения имен, экспортированные из Google Sheets с определенным идентификатором, извлеченным из соответствующей ячейки
Как увеличивать глобальную переменную в JavaScript каждый раз, когда функция запускается в скрипте Google Apps?
Как сопоставить клиентский код с исходным кодом
Можно ли получить дату изменения документа Google в скриптах Google Sheets или Apps?