GAS + Vue.js + Vuetify でスプレッドシートの編集履歴を表示する

main.gs

// ページを表示する
function doGet() {
 const htmlOutput = HtmlService.createTemplateFromFile("index").evaluate();
 htmlOutput
   .setTitle('スプレッドシート編集履歴')
   .addMetaTag('viewport', 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui');
 return htmlOutput;
}

// クライアントサイドから呼ぶ
function getLogSheetData() {
 const logSheet = SpreadsheetApp.getActive().getSheetByName('Log');
 const logSheetRows = logSheet.getRange(1, 1, logSheet.getLastRow(), logSheet.getLastColumn()).getDisplayValues();
 
 const columnHeadings = logSheetRows.shift();
 const editLogRows = logSheetRows;
 
 return {
   columnHeadings: columnHeadings,
   editLogRows: editLogRows
 };
}

// スプレッドシートの編集をトリガーに実行する
function insertLog(e) {
if (e.source.getSheetName() === 'Log') return;

const logSheet = SpreadsheetApp.getActive().getSheetByName('Log');

logSheet.appendRow([
  e.source.getSheetName(),
  e.user.getEmail(),
  Moment.moment().format('YYYY-MM-DD HH:mm:ss'),
]);
}

// 各ファイルの読み込みに使う
function include(filename) {
 return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

js.html

<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
<script>

Vue.component('edit-log-list', {
 template: '\
   <v-simple-table fixed-header>\
     <template v-slot:default>\
       <thead>\
         <tr>\
           <th class="text-center" v-for="columnHeading in columnHeadings">{{ columnHeading }}</th>\
         </tr>\
       </thead>\
       <tbody>\
         <tr v-for="editLogRow in editLogRows">\
           <td class="text-right" v-for="editLogCell in editLogRow">{{ editLogCell }}</td>\
         </tr>\
       </tbody>\
     </template>\
   </v-simple-table>\
 ',
 props: ['columnHeadings', 'editLogRows']
});

Vue.component('emit-button', {
 template: '\
   <v-btn large @click="onClick">{{ label }}</v-btn>\
 ',
 props: ['label'],
 methods: {
   onClick: function() {
     this.$emit('clicked');
   }
 }
});

var vm = new Vue({
 el: '#app',
 vuetify: new Vuetify(),
 data: {
   columnHeadings: [],
   editLogRows: [[]]
 },
 methods: {
   updateEditLogs: function() {
     google.script.run
       .withSuccessHandler(function(logSheetData) {
         vm.editLogRows = logSheetData.editLogRows;
       })
       .withFailureHandler(function(arg) {
         alert("Logシートからデータを取得できませんでした");
       }).getLogSheetData();
   }
 },
 created: function() {
   google.script.run
     .withSuccessHandler(function(logSheetData) {
       vm.columnHeadings = logSheetData.columnHeadings;
       vm.editLogRows = logSheetData.editLogRows;
     })
     .withFailureHandler(function(arg) {
       alert("Logシートからデータを取得できませんでした");
     }).getLogSheetData();
 }
});
</script>

index.html

<!DOCTYPE html>
<html>
 <head>
   <base target="_top">
   <?!= include('css'); ?>
 </head>
 <body>
   <div id="app">
     <v-app>
       <v-content>
         <v-container>
           <div class="d-flex justify-center mb-6">
             <emit-button @clicked="updateEditLogs" label="更新"></emit-button>
           </div>
           <div class="d-flex justify-center">
             <edit-log-list :column-headings="columnHeadings" :edit-log-rows="editLogRows"></edit-log-list>
           </div>
         </v-container>
       </v-content>
     </v-app>
   </div>
   <?!= include('js'); ?>
 </body>
</html>

css.html

<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<style>
</style>

これらのファイルをスプレッドシートと結びついたスクリプトエディタで作成し、[公開] > [ウェブアプリケーションとして導入] > [導入] という操作を経ることで、簡易的な Web アプリが完成します。

画像1

画像2

ユーザーがスプレッドシートを編集すると Log シートに「シート名」「編集者」「日時」が書き込まれます。アプリ側では、ページの読み込み時と、[更新] ボタンの押下時に Log シートの内容が取得されます。

ハマったところ

Class google.script.run でサーバーサイド (main.gs) で定義した関数をクライアントサイド (js.html) で呼び出して、その返り値をコールバック関数に渡すことができます。ただ、公式ドキュメントによれば、渡せる値には制限があるようです。今回の場合、

google.script.run
     .withSuccessHandler(function(logSheetData) {
       vm.columnHeadings = logSheetData.columnHeadings;
       vm.editLogRows = logSheetData.editLogRows;
     })
     .withFailureHandler(function(arg) {
       alert("Logシートからデータを取得できませんでした");
     }).getLogSheetData();

getLogSheetData() の返り値をコールバック関数が受け取るのですが、その返り値に Date オブジェクトが含まれていたせいで、Vue インスタンスの data プロパティの各値が null になっていました(つまりコールバック関数の引数が null になっていました)。そこで対策として getLogSheetData() 内の

const logSheetRows = logSheet.getRange(
  1, 1,
  logSheet.getLastRow(), logSheet.getLastColumn()
).getValues();

という記述を以下に変更しました。

const logSheetRows = logSheet.getRange(
 1, 1,
 logSheet.getLastRow(), logSheet.getLastColumn()
).getDisplayValues();

違いは getValues() が getDisplayValues() になっただけです。getValues() だとスプレッドシート上の日時表記が Date オブジェクトとして取得されてしまうので、getDisplayValues() ですべてを文字列として取得するようにします。

所感

GAS のスクリプトエディタで Vue.js を書くのは、このくらいの規模が(精神的に)限界かもしれません。

この記事が気に入ったらサポートをしてみませんか?