Apps Scriptリファレンス: Apps Script Reference |障害・課題追跡: IssueTracker |Google Workspace: Status Dashboard - Summary

2021年12月19日日曜日

SalesforceのAPIをLambdaからたたいてみる(Developers Edition)


Lambdaを利用してSalesforceの情報をAPIで取得しようとしたときに、オープンなAPI GatewayのURLにアクセスして実行したときの備忘録です。

IP制限やアクセストークンの保存場所・更新などは考えず、リクエストの結果を取得するためだけに書いたテスト用のコードです。
本番のSalesforceで実行する場合は、それらを考慮する必要があると思います。

今回はDevelopers Editionで試しています。



index.html
var https = require('https');

var case_id = "CASE_ID";
var salesforce_api_token = "ACCESS_TOKEN";
var host = "HOST";//instance_urlのhttps://の後ろ→hoge-dev-ed.my.salesforce.com
var path = "/services/data/v47.0/sobjects/Case/" + case_id;

exports.handler = async function(event) {
  console.log(event);

 var results = await getData(); //getData()の処理が終わってから次の処理を実行する
 var json = returnJson(results);

 return json;
}

//Promise
function getData() {
 return new Promise(resolveFunc);
}

//Promise resolved
function resolveFunc(arg) {
 https.get(getOptions(), function(res) {
    var body = '';
    res.on('data', function(chunk) {
     body += chunk;
    });
    res.on('end', function(chunk) {
     arg(body);
    });
    res.on('error', function(e) {
     arg(e.message);
    });
 });
}

function getOptions() {
 var options = {
    "method": 'GET',
    "host": host,
    "path": path,
    "uri": host + path,
    "headers": {
     "Content-type": "application/json",
     "Authorization": " Bearer " + salesforce_api_token// Lambda側に設定して読む場合はprocess.env. salesforce_api_token
    }
 };
 return options;
}

//JSONを返す
function returnJson(results) {
 const response = {
    body: JSON.parse(results),
 };
 return response;
}




2021年10月20日水曜日

GASでWebアプリを作ってローカルファイルをGoogleドライブにアップしたい(アプリを開くユーザーとファイルをアップロードするユーザーを分けたい)



GASでWebアプリを作ってローカルファイルをGoogleドライブにアップしたい

以下の条件を満たすもの

条件
  1. アップロード先はGoogleドライブの共有フォルダ
  2. そのフォルダには権限が設定されている
  3. Webアプリを開いたユーザーはそのフォルダにアクセスする権限がない
  4. アップロード後、3のユーザにアップロードしたファイルだけ閲覧権限を付与する

というニッチな条件を満たすために考えました。


条件3をどうするか
「Webアプリを開いたユーザーはそのフォルダにアクセスする権限がない」

権限がないので、ファイルをアップロードする直前に権限を付与する?
  • 閲覧権限だけだとアップロードできないので編集権限も必要
  • フォルダへの編集権限を付与すると、フォルダ内にある他のファイルも閲覧、編集できる
  • そうなると4を満たせない


そこで考えた方法
「フォルダの編集権限を持ったユーザーが代わりにファイルをアップロードする」

Webアプリを開いたユーザーとは別のAdminユーザにやってもらう。
もう一つGASのプロジェクトを作るとできそう。

どうやるか考えてみる。
  1. ファイルをアップロードする処理は別のアプリを作って実行する
  2. それをライブラリとして扱う
  3. ライブラリ側アプリの実行ユーザーはアップロード先フォルダの編集権限を持っている
ライブラリでは実行ユーザーを指定できないため、doPostで考えてみる。(2022/01/03 追記)

  1. ファイルアップロードするdoPostの処理を別のプロジェクトで作る
  2. doPost側でpayloadを受け取ってドライブにアップロードする
  3. doPost側のアプリの実行者は自分(アップロード先フォルダの編集権限を持っている)

これでできそう。


そして書いたコード


画面を操作する側のGASプロジェクト


コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}

function upload_file_gs(reader_result, file_name) {
  var email = Session.getActiveUser().getEmail();// ファイルの閲覧権限を追加したいユーザー(この例では実行したユーザーを指定)
  var payload = {
    "reader_result": reader_result,
    "file_name": file_name, 
    "email": email
  }

  const options = {
    "method": "post",
    "payload": JSON.stringify(payload),
    "contentType": "application/json"
  }
  var url = "doPost側のWebアプリのURL";
  const response = UrlFetchApp.fetch(url, options).getContentText();
  return response;
}

ウェブアプリケーションのデプロイ設定



※グレーのコードはライブラリで考えたときのコードの備忘録(不完全)

コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}

function upload_file_gs(reader_result, file_name) {
  var email = "追加したいメールアドレス";
  var file_url = FileUpload.run(reader_result, file_name, email)
  return file_url;
}



index.html
<!DOCTYPE html>
<html>
<body>
  <form>
    <div>
      <input type="file" id="my_files" multiple>
    </div>
  </form>
  <label id="upload_label"></label>
  <div id="upload_div"></div>
  <script>
    document.getElementById("my_files").addEventListener("change", upload_files);
    var files_len = 0;
    var uploaded_len = 0;

    function upload_files() {
      var files = document.getElementById("my_files").files;
      files_len = files.length;
      for (var i = 0; i < files.length; i++) {
        uploadFile(files[i]);
        document.getElementById("upload_label").textContent = "アップロードしています...";
      }
    }

    function uploadFile(file) {
      var reader = new FileReader();
      reader.onload = function() {
        var reader_result = reader.result;
        var file_name = file.name
        google.script.run
          .withSuccessHandler(uploaded)
          .upload_file_gs(reader_result, file_name);
      }
      reader.readAsDataURL(file);
    }

    function uploaded(file_url) {
      create_link(file_url);
      uploaded_len++;
      if (uploaded_len == files_len) {
        document.getElementById("upload_label").textContent = "アップロードしました";
      }
    }

    function create_link(file_url) {
      var div = document.getElementById("upload_div");
      var br = document.createElement("br");
      var link = document.createElement("a");
      link.textContent = file_url;
      link.setAttribute("href", file_url);
      link.setAttribute("target", "_blank");
      div.appendChild(link);
      div.appendChild(br);
    }
  </script>
</body>
</html>



doPost側のGASプロジェクト


コード.gs
/************************************
payloadはeventObject.postData.contentsで取得できる
取得してGoogleドライブに保存する
************************************/
function doPost(eventObject) {
  const contents = JSON.parse(eventObject.postData.contents);
  const created = createFile(contents);  
  return ContentService.createTextOutput(created).setMimeType(ContentService.MimeType.JSON);
}

const FOLDER_ID = "GoogleドライブのフォルダID";
function createFile(contents) {
  var reader_result = contents["reader_result"];
  var file_name = contents["file_name"];
  var email = contents["email"];
  var result_split = reader_result.split(',');
  var content_type = result_split[0].split(';')[0].replace('data:', '');
  var row_data = result_split[1];
  var data = Utilities.base64Decode(row_data);
  
  var file = Utilities.newBlob(data, content_type, file_name);
  var folder = DriveApp.getFolderById(FOLDER_ID);
  var drive_file = folder.createFile(file);
  var file_url = drive_file.getUrl();
  drive_file.addViewer(email);
  return file_url;
}

ウェブアプリケーションのデプロイ設定



※グレーのコードはライブラリで考えたときのコードの備忘録(不完全)

コード.gs
var FOLDER_ID = "保存先フォルダのID";
function run(reader_result, file_name, email) {
  var result_split = reader_result.split(',');
  var content_type = result_split[0].split(';')[0].replace('data:', '');
  var row_data = result_split[1];
  var data = Utilities.base64Decode(row_data);
  
  var file = Utilities.newBlob(data, content_type, file_name);
  var folder = DriveApp.getFolderById(FOLDER_ID);
  var drive_file = folder.createFile(file);
  var file_url = drive_file.getUrl();
  drive_file.addViewer(email);
  return file_url;
}



実行手順

操作する側のWebアプリを開いて、以下のようにファイルをアップロードします。


「ファイル選択」をクリックして出てくるモーダルでローカルファイルを開きます。



「アップロードしています...」表示になります。



アップロードされたファイルへのリンクが表示されます。


関連記事


index.htmlはこれと同じです。
Google Driveに複数ファイルをアップロードしたい(submitボタン使わない)


GASでAPIを作ってPOSTでpayloadを渡したい


GASのライブラリの扱いは別途書き残しました。
Google Apps Scriptでライブラリを作って使う

2021年9月30日木曜日

データリストで選択した値を取得したい


 
inputのsearchボックスでdatalistから選択した値をconsoleに出力したい

  

デモ




コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}



index.html
<!DOCTYPE html>
<html>
  <body>
    <div id="main_div"></div>

<script>
myFunction();

function myFunction() {
  var arrays = ["ame", "yuki", "mizore"];
  var id = "tenki"
  createDatalist(arrays, id);
}

function createDatalist(arrays, id) {
  var main_div = document.getElementById('main_div');
  var input = document.createElement('input');
  input.setAttribute("type", "search");
  input.setAttribute("id", id);
  input.setAttribute("list", id + "_list");
  input.onchange = function() {
    console.log(this.value);
  }
  main_div.appendChild(input);

  var datalist = document.createElement('datalist');
  datalist.setAttribute("id", id + "_list");
  for (var i = 0; i < arrays.length; i++) {
    var option = document.createElement('option');
    option.setAttribute("value", arrays[i]);
    datalist.appendChild(option);
  }
  main_div.appendChild(datalist);
}

</script>
  </body>
</html>





2021年8月15日日曜日

Service CloudのCaseをクローズするアクションを追加したい


ここでは

ケースをクローズする「Close Case」アクションを追加したときの手順を書いて行きます。


状況(status)にClosedを表示する方法はこちら↓



以下の道を通って追加できました。

設定 > オブジェクトマネージャー > ケース > ボタン、リンク、およびアクション > 定義済み設定項目値 > ケースページレイアウト > Case Layout > 


FYI
クローズケースクイックアクションを作成する



順番にやっていきます。


まずは左上の歯車アイコンから「設定」を開きます。


「オブジェクトマネージャ」を検索して選択します。


「ケース」を選択します。


「ボタン、リンク、およびアクション」 を選択します。


「新規アクション」を選択します。


「アクション種別」と「表示ラベル」を入力して保存します。
「名前」は自動で入ります。


「保存」を選択します。


定義済み項目値で「新規」を選択します。


項目名で「状況」を選択、特定値で「Closed」を選択して保存します。


ケースページレイアウトで「Case Layout」を選択します。


「モバイルおよび Lightning のアクション」を選択して、「Close Case」を「Salesforce モバイルおよび Lightning Experience アクション」までドラッグ&ドロップして、保存します。


これでケースのフィードに「Close Case」アクションが追加できました。

追加したいボタンを削除するときは、ドラッグ&ドロップを逆に移動して保存すると、ボタン、リンク、およびアクションで削除できるようになる。


参考

クローズケースクイックアクションを作成する

Lightning Experience のケースページレイアウトへのクイックアクションの追加

クローズケースの状況項目を表示するか非表示にするかを選択

ケースのクローズ

ケースの状況選択リスト値に「クローズ」オプションを割り当てられない

Service CloudでCaseの状況(status)に選択肢を追加したい


ここでは

ケースの状況に「Resolved」を追加してみます。





ケース 状況 選択リスト値で新規に「Resolve」を追加します。

設定 > 
オブジェクトマネージャー > 
ケース > 
項目とリレーション > 
状況 > 
ケース 状況 選択リスト値



以下順番にやっていきます。


オブジェクトマネージャ


ケース

項目とリレーション > 状況

ケース 状況 選択リスト値 > 新規

Resolvedを入力 > 保存


これで状況の選択肢に




補足

Closedと同じ完了フラグをつけるには

Resolvedの編集を選択

完了フラグをチェックして保存


完了フラグのついた状況項目を表示するには

「クローズケースの状況項目を表示」 をチェックする必要があるようです。

その方法は以下の記事に書きました。


Service Cloudでケースの状況にClosedを表示したい


以下のように設定すると表示できました。

設定 > サポート設定 > 編集 > クローズケースの状況項目を表示 をチェック > 保存



デフォルトではCaseの状況で「Closed」が選択肢にありません。

このように表示して選択できるようにしたい。



順番にやっていきます。


右上の歯車から設定を選択します。


サポート設定で「クローズケースの状況項目を表示」にチェックが入っていないので


編集を選択します。


クローズケースの状況項目を表示 をチェックして保存します。


これで「Closed」が選択できるようになりました。


参考

クローズケースの状況項目を表示するか非表示にするかを選択

ケースのクローズ

ケースの状況選択リスト値に「クローズ」オプションを割り当てられない

Salesforce APIでCase IDの一覧を取得したい


SOQLを利用して

SELECT+id+from+Case

というクエリでCase IDの一覧を取得してみます。



ここでは
で書いたtoken.gsもトークンをリフレッシュする際に使うため、別ファイルで保存しておきます。

token.gsと今回書くget.gsは別ファイルで持っておく。


事前準備

PropertiesService の UserProperties に access_token を保存しておく
保存する例



get.gs
function getCaseIds() {
  var query = "SELECT id from Case";
  var url = getUserProperty("instance_url") + "/services/data/v47.0/query?q=" + query;// SOQL クエリを実行する
  var response = UrlFetchApp.fetch(url, optionsGet());
  if(response.getResponseCode() === 401) {
    runRefresh();// アクセストークン切れで401ならトークンを更新する
    response = UrlFetchApp.fetch(url, optionsGet());// 再度リクエストを送る
  }
  Logger.log(response);
}

function optionsGet() {
  return {
    "method" : "GET",
    "headers" : {
      "Authorization": "Bearer " + getUserProperty("access_token")
    },
    "muteHttpExceptions": true
  }
}




getData() を実行すると、ログにCase IDの一覧が出力されます。


関連記事



Salesforce APIで使うアクセストークンを取得してリフレッシュ、リボークもしたい(Google Apps Script)


SalesforceのAPIをGoogle Apps Scriptで利用するときの備忘録です。

アクセストークンを取得→保存して、その後リフレッシュとリボークするコードを書きました。

動作環境:Developers Edition



STEP1: GAS
Webアプリを作成する→作り方


STEP2: Salesforce
接続アプリケーションを作成する→作り方


STEP3: GAS
アクセストークンを取得する
UserPropertiesに保存する
リフレッシュトークンでアクセストークンを更新する
リボークしてトークンを無効にする
→この記事


STEP4: GAS
APIにリクエストを送って結果を取得する→TBD


コードの実行

1. Salesforce上で接続アプリケーションを作成した際に発行される「コンシューマ鍵」「コンシューマの秘密」と、GASをWebアプリとして公開したURLを、グローバルの変数に貼り付ける

2. getMyUrl() を実行してログに出たURLにブラウザでアクセスする

3. UserPropertiesにトークン情報が保存される

4. runRefresh() でアクセストークンが更新される

5. runRevoke() でトークン情報を無効にする



token.gs
/************************************
グローバル
************************************/
var client_id = "コンシューマ鍵";
var client_secret = "コンシューマの秘密"
var redirect_uri = "このGASプロジェクトをWebアプリとして公開したURL";

var token_url = "https://login.salesforce.com/services/oauth2/token";

/************************************
//認可コード取得URL
************************************/
function getMyUrl() {
  var url = "https://login.salesforce.com/services/oauth2/authorize?" +// sandboxの場合はinstance_url 
      "response_type=code" + "&" + 
      "client_id=" + client_id + "&" +
      "redirect_uri=" + redirect_uri + "&" +
      "state=mystate";
  Logger.log(url);
}

/************************************
doGet
************************************/
function doGet(e) {// 認可コード取得URLを開いたときに動く処理
  var response = getAccessToken(e);
  setUserProperties( JSON.parse(response));
  return ContentService.createTextOutput(response);// ブラウザに表示する
}

/************************************
getAccessToken
************************************/
function getAccessToken(e) {// 認可コードを利用してトークン情報を取得して返す
  var code = e["parameter"]["code"];
  var payload = {
    "grant_type": "authorization_code",
    "client_id": client_id,
    "client_secret": client_secret,
    "code": code,
    "redirect_uri": redirect_uri
  }
  
  var options = {
    "method": "post",
    "contentType": "application/x-www-form-urlencoded",
    "payload": payload
  };
  var response = UrlFetchApp.fetch(token_url, options);
  return response;
}

/************************************
runRefresh
************************************/
function runRefresh() {// refresh_tokenを使って更新したトークン情報を返す
  var payload = {
    "grant_type": "refresh_token",
    "refresh_token": getUserProperty("refresh_token"),
    "client_id": client_id,
    "client_secret": client_secret
  }
  var options = {
    "method": "post",
    "contentType": "application/x-www-form-urlencoded",
    "payload": payload
  }
  var response = UrlFetchApp.fetch(token_url, options);
  Logger.log(response);
  setUserProperties(JSON.parse(response));
}

/************************************
revokeToken
************************************/
function revokeToken() {
  var url = "https://login.salesforce.com/services/oauth2/revoke";
  var options = {
    "method": "get",
    "contentType": "application/x-www-form-urlencoded",
    "payload": {
      "token": getUserProperty("access_token") // access_tokenをrevokeする場合
      //token: getProp("refresh_token")// refresh_tokenをrevokeする場合
      }
  }
  var response = UrlFetchApp.fetch(url, options);
  Logger.log(response);
}

/************************************
PropertiesService
************************************/
function setUserProperty(key, value) {// ユーザのプロパティに値をひとつ保存する
  PropertiesService.getUserProperties().setProperty(key, value);
}

function setUserProperties(jobj) {// ユーザのプロパティに値をまとめて保存する
  PropertiesService.getUserProperties().setProperties(jobj);
}

function getUserProperty(key) {// ユーザのプロパティから値をひとつ取得する
  return PropertiesService.getUserProperties().getProperty(key);
}

function getUserProperties() {// ユーザのプロパティから値をまとめて取得する
  return PropertiesService.getUserProperties().getProperties();
}




関連記事



2021年8月10日火曜日

配列内の最頻値を求めたい


統計学の勉強中に最頻値を求めようとしてちょっと手こずりました。

書き方は色々あると思います。
ライブラリを使う手もあると思います。


ここでは
与えられた配列から最頻値を出力するまでのプロセスを思うままに書きました。



コード.gsでやること

values = [1,3,2,3,4,2]

という配列内で最も頻度の多い値を

[2, 3]

の形で取得したくて書いたコードです。



コード.gs
/************************************
これを実行する
************************************/
function getMode(){
  const values =[1,3,2,3,4,2];
  const arrays = returnCountValues(values);
  const countKeyObj = returnCountKeyObj(arrays);
  const countValues = returnObjToArray(countKeyObj);
  const mode = returnMode(countValues);
  Logger.log(countValues);
  Logger.log(mode);
}

/************************************
values = [1,3,2,3,4,2]
を
[[1.0, 1.0], [2.0, 2.0], [3.0, 2.0], [4.0, 1.0]]
にして返す
***********************************/
function returnCountValues(values) {
  let arrays = [];
  let count = 1;
  let currentIndex = 0;
  const sortedValues = values.sort();
  for(let i = 0; i < sortedValues.length; i++) {
    if(sortedValues[i] === sortedValues[i-1]) {
      count++;
      arrays[currentIndex-1] = [sortedValues[i], count];
    } else {
      count = 1;
      arrays.push([sortedValues[i], count])
      currentIndex++;
    }
  }
  return arrays;
}

/************************************
arrays = [[1.0, 1.0], [2.0, 2.0], [3.0, 2.0], [4.0, 1.0]]
を
{1=[1.0, 4.0], 2=[2.0, 3.0]}
にして返す
************************************/
function returnCountKeyObj(arrays) {
  let obj = {};
  for(let i = 0; i < arrays.length; i++) {
    const array = arrays[i];
    if(obj[array[1]]) {
      obj[array[1]].push(array[0])
    } else {
      obj[array[1]] = [array[0]];
    }
  }
  return obj;
}

/************************************
obj = {1=[1.0, 4.0], 2=[2.0, 3.0]}
を
[[1, [1.0, 4.0]], [2, [2.0, 3.0]]]
にして返す
************************************/
function returnObjToArray(obj) {
  return Object.entries(obj);
}

/************************************
values = [[1, [1.0, 4.0]], [2, [2.0, 3.0]]]
から
末尾にある最頻値
[2.0, 3.0]
を返す
************************************/
function returnMode(values) {
  const tail = values[values.length - 1];
  const mode = tail[1];
  return mode
}



実行結果









配列内で重複する値の発生数を取得する(二次元配列で返す)


const values = [1, 2, 3, 2] のそれぞれの数値の件数を取得したい

このように
[[1, 1], [2, 2], [3, 1]]

1は1つ
2は2つ
3は1つ



コード.gs
function getCountValues() {
  const values = [1, 2, 3, 2];
  const countValues = returnCountValues(values);
  Logger.log(countValues);
}

function returnCountValues(values) {
  const sortedValues = values.sort();
  const unique = sortedValues.filter(returnUnique);

  let arrays = [];
  for(let i = 0; i < unique.length; i++) {
    const value = unique[i];
    const count = findDup(value, values);
    arrays.push([value, count]);
  }
  return arrays;
}

function findDup(value, values) {
  let count = 0;
  for(let i = 0; i < values.length; i++) {
    if(values[i] === value) {
      count++;
    }
  }
  return count;
}

function returnUnique(value, index, array){
  var valueIndex = array.indexOf(value);
  var result = valueIndex === index;
  Logger.log([array, "の中で", value, "は", valueIndex, "番目にあって、現在のindexは", index, "なのでuniqueの配列への追加は", result])
  return result;
}
意訳
これを実行する
valuesを決める
returnCountValuesに渡して
結果をログに出す


valuesの要素ごとの件数を返す
values並べ替える→1,2,2,3
それをユニークにする→1,2,3

結果を入れる配列を用意する
uniqueの数だけ繰り返す
uniqueをひとつずつ取り出して
values内の重複を探して
arraysに追加する

完成したarraysを返す


重複を見つける
countの初期値を0にしておく
valuesの数だけ繰り返す
もしvalues内にvalueと同じ値があれば
countに1追加していく


countを返す


ユニークを返す
arrayの中でvalueが何番目にあるか
それが現在のvalueの位置と同じならtrue, 違うならfalse
ログに出してみる
を返してtrueだけのunique配列を作る


実行結果

[[1.0, 1.0], [2.0, 2.0], [3.0, 1.0]]

returnUniqueの中で何をやっているのかログで確認しています。




別の書き方


コード.gs
function getCountValues(){
  const values = [1,2,3,2,2,2,3,3,3];
  const arrays = returnCountValues(values);
  Logger.log(arrays);
}

function returnCountValues(values) {
  let arrays = [];
  let count = 1;
  let currentIndex = 0;
  const sortedValues = values.sort();
  for(let i = 0; i < sortedValues.length; i++) {
    if(sortedValues[i] === sortedValues[i-1]) {
      count++;
      arrays[currentIndex-1] = [sortedValues[i], count];
    } else {
      count = 1;
      arrays.push([sortedValues[i], count])
      currentIndex++;
    }
  }
  return arrays;
}

実行結果


2021年7月25日日曜日

現在日時から四半期の開始日と終了日を知りたい


今の日時を取得して、現在の四半期の開始日と終了日を知りたくて書いたコードです。



コード.gs
function myFunction() {
  const month = new Date().getMonth() + 1;// monthは0始まりなので+1
  const quater = judgeQuater(month);
  Logger.log([month, quater]);
}

/************************************
受け取った月からそのQuaterのStartDateと EndDateとQuaterを返す
************************************/
function judgeQuater(month) {
  switch(month) {
    case 7:
    case 8:
    case 9:
      return ["07-01", "09-30", "Q1"];
    case 10:
    case 11:
    case 12:
      return ["10-01", "12-31", "Q2"];
    case 1:
    case 2:
    case 3:
      return ["01-01", "03-31", "Q3"];
    case 4:
    case 5:
    case 6:
      return ["04-01", "06-30", "Q4"];
  }
}




ログ

2021年6月19日土曜日

Googel Apps Scriptでスプレッドシートの句読点や指定文字のあとに改行を入れて置換したい


コメントの実行プロセスに書いたとおり

選択範囲内の指定文字の後に改行を入れてセルを上書きしたくて書いたコードです。



コード.gs
/************************************
formatText()を実行する

実行プロセス
1. 選択範囲の値を取得する
2. 一行ずつテキストを置換して配列に追加する
3. 選択範囲の値を置換したテキストで上書きする
************************************/
function formatText() {
  Logger.log("start");
  const values = targetRange().getValues();
  let newValues = [];
  for(let i = 0; i < values.length; i++) {
    const text = values[i][0];
    const newText = replaceText(text);
    newValues.push([newText]);
  }
  targetRange().setValues(newValues);
  Logger.log("success!");
}

/************************************
テキストの指定文字を置換して返す
************************************/
function replaceText(text) {
  return text.replace(/、/g, "、\n").replace(/。/g, "。\n").replace(/を/g, "を\n");
}

/************************************
現在選択されている範囲を取得して返す
************************************/
function targetRange() {
  return SpreadsheetApp.getActiveSheet().getSelection().getActiveRange();
}



2021年6月18日金曜日

マウス座標がどれだけ変化したか差分を取得したい









コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}
意訳
この機能がやること
指定したHTMLファイルを表示する




index.html
<!DOCTYPE html>
<html>
<body>
  <label>X</label><input type="text" id="movedX" value=0>
  <br>
  <label>Y</label><input type="text" id="movedY" value=0>
  <br>
  <label id="rightLeft"></label>
  <br>
  <label id="upDown"></label>
<script>
window.onmousemove = mouseMoved;
 
function mouseMoved(e) {
  elem("movedX").value = e.movementX;
  elem("movedY").value = e.movementY;

  let rightLeft;
  if(e.movementX > 0) {
    rightLeft = "right";
  } else {
    rightLeft = "left";
  }

  let upDown;
  if(e.movementY > 0) {
    upDown = "down";
  } else {
    upDown = "up";
  }

  elem("rightLeft").textContent = rightLeft;
  elem("upDown").textContent = upDown;
}

function elem(id) {
  return document.getElementById(id);
}
</script>
</body>
</html>



2021年5月23日日曜日

マイドライブを指定してファイルを保存したい(DriveApp.getRootFolder())


以前書いた

CSVデータを作ってGoogleドライブに保存する

では、特定のフォルダを指定して保存しました。

ここでは「マイドライブ」に保存するコードを書きました。


save_as_csv()を実行すると

マイドライブにvar dataで設定したデータが保存されて

そのファイルのURLがログに出力されます。



コード.gs
function save_as_csv() {
  var data = "でーた1, でーた2\nでーた3";
  create_file(data);
}

function create_file(data) {
  var content_type = "text/csv";
  var file_name = "FILE_NAME";
  var blob = Utilities.newBlob("", content_type, file_name);
  var file = blob.setDataFromString(data, "UTF-8");
  var folder = DriveApp.getRootFolder();
  var file = folder.createFile(file);
  var fileUrl = file.getUrl();
  Logger.log(fileUrl);
}




2021年5月5日水曜日

JavaScriptのanimate()でアニメーションを試してみたい


Elementの透明度と位置をアニメーションさせたい。

Element.animate() で実装できました。


デモ




コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}
意訳
この機能がやること
指定したHTMLファイルを表示する




index.html
<!DOCTYPE html>
<html>

<body>
<textarea id="ta">hello</textarea>
<script>

animation();
function animation() {
  elem("ta").animate({
    "opacity": [ 0, 1 ], // [ フレーム 1, フレーム 2 ]
    "color": [ "#fff", "#000" ] // [ フレーム 1, フレーム 2 ]
    },
    {
      "duration": 2000,
      "iterations": Infinity
    })

  elem("ta").animate({
    "marginLeft": ["0px", "100px"]
    },
    {
      "duration": 1000,
      "easing": "ease",
      "iterations": Infinity
    })
  
  // 移動後の位置を設定しないと元に戻る
  elem("ta").style.marginLeft = "100px";
}

function elem(id) {
  return document.getElementById(id);
}

</script>
</body>
</html>



参考

Element.animate()

Animation

Keyframe Formats

EffectTiming

2021年5月4日火曜日

Google Apps ScriptでURLの表示を変更したい google.script.history.replace()


google.script.history.replace()

で実現できました。


こういうことを実現したい

HtmlServiceのWebアプリのURL
https://script.google.com/macros/s/ID/exec

ページ遷移を発生させずパラメーターを追加したい
https://script.google.com/macros/s/ID/exec?id=1&value=hello



URLの表示を変更したくて調べてみると、History API を取り扱う が見つかりました。
試してみると、history.pushState() や history.replaceState() では変更できなかった。

さらに調べてみると Class google.script.history (Client-side API) が見つかりました。

google.script.history.push() と google.script.history.replace() で実現できました。
pushとreplaceの違いはリファレンスを参照してください。

今回は replace() を使ったコードを書きました。



コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}
意訳
この機能がやること
指定したHTMLファイルを表示する




index.html
<!DOCTYPE html>
<html>
<body>

<script>

replaceParam();
function replaceParam() {
  var params = {
    "id": 1,
    "value": "hello"
  };
  google.script.history.replace("", params, "");
}
</script>

</body>
</html>




参考

Class google.script.history (Client-side API) 

History API を取り扱う


2021年4月30日金曜日

[Spreadsheet] 列ごとに値をユニークにしてオブジェクト化したい(2列目は配列で持つ)


以下のような処理を実現したくて書きました。


スプレッドシートのA、B列に以下のようなデータがある時
prefecturecity
東京新宿
東京新宿
東京渋谷
東京渋谷
東京品川
東京品川
東京品川
東京品川
神奈川横浜
神奈川横浜
神奈川横浜
神奈川横浜
神奈川川崎
神奈川川崎


それぞれの列で重複する値をユニークにして
以下のようなオブジェクトにしたい。

 {東京=[新宿, 渋谷, 品川], 神奈川=[横浜, 川崎]}

A列はオブジェクトのkey。
B列は配列で持つ。



コード.gs
function myFunction() {
  var categoriesObj = createCategoriesObj();
  Logger.log(categoriesObj);
}
/************************************
オブジェクトを作成する
************************************/
function createCategoriesObj() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var lastRow = sheet.getLastRow();
  var categories = sheet.getRange(1, 1, lastRow, 1).getValues();
  var subCategories = sheet.getRange(1, 2, lastRow, 1).getValues();
  
  // ヘッダ行の要素は取り除いておく
  categories.shift();
  subCategories.shift();
  
  // ここからオブジェクトを作っていく
  var obj = {};
  var array = [];
  for(var i = 0; i < categories.length; i++) {
    var category = categories[i][0];
    var categoryPrev = categories[i-1];
    if(obj[category] !== obj[categoryPrev]) {//categoryの値が変わった時に配列を初期化したい
      array = [];// 初期化しないと要素が追加され続ける
    }
    array.push(subCategories[i][0]);
    obj[category] = array.filter(returnUnique);// subCategoriesの値が重複してもここでuniqueにする
  }
  return obj;
}

/************************************
一次元配列の要素の値をユニークにする
************************************/
function returnUnique(value_i, index, array){
  var value_i_index = array.indexOf(value_i);// 配列の何番目に要素があるかを先頭の要素から一つずつ確認して
  var result = value_i_index === index;// 配列の要素のindexと同じなら
  return result;// その値を返して配列に追加する(違うなら追加しない→2つ目以降は存在してもindexが異なるため追加されない)
}



myFunctionを実行すると以下のようなログが出力されます。

2021年4月25日日曜日

テキストの改行を削除したい


改行を含むテキストから改行を削除したくて書いたコードです。

ここでは

text.replace(/\n/g, "")

で改行を削除しています。


デモアプリ
上のテキストエリアにテキストを貼り付けると
改行が削除されたテキストが下に出力されます。






コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}
意訳
この機能がやること
指定したHTMLファイルを表示する




index.html
<!DOCTYPE html>
<html>
<body>
  <textarea id="ta" style="width:30vw; height:30vh"></textarea>
  <br>
  <textarea id="result_ta" style="width:30vw; height:30vh"></textarea>

<script>
document.getElementById("ta").onkeyup = taKeyup;

function taKeyup() {
  var text = document.getElementById("ta").value;
  var result = text.replace(/\n/g, "");
  document.getElementById("result_ta").value = result;
}

</script>
</body>
</html>





2021年3月2日火曜日

選択したテキストを配列に入れてテキストエリアに出力したい


今回やりたいこと

アプリケーションの中で
選択したテキストを取得する。

選択したテキストを
テキストエリアに入れる。

選択したテキストを
配列に追加していく。



STEP1
「テキストを」を選択するとテキストエリアに「テキストを」が入る。


STEP2
その後、「追加して」を選択するとテキストエリアに「テキストを,追加して」が入る。



コード.gs
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index");
}
意訳
この機能がやること
指定したHTMLファイルを表示する




index.html
<!DOCTYPE html>
<html>
  <body>
    選択した範囲のテキストを配列に追加してテキストエリアに入れる。
    <textarea id="ta"></textarea>

<script>
document.onmouseup = on_mouseup;

var values = [];

function on_mouseup() {
  var text = window.getSelection();
  values.push(text.toString());
  document.getElementById("ta").value = values;
}

</script>

  </body>
</html>



JIRAのカンバンボードで次世代プロジェクトの課題が表示されないときは、サブフィルターを空にしてみる


(コードを使う話ではありません)

クラシック・次世代、両プロジェクトの課題を一つのカンバンボードに表示したい。

と思ってやってみたら
次世代プロジェクトの課題が表示されないという壁にぶつかりました。


解決方法

カンバン ボード サブフィルターに入っている
fixVersion in unreleasedVersions() OR fixVersion is EMPTY
を削除する。


今回やった手順

STEP1
ボード設定の「基本メニュー」の下の方に
カンバン ボード サブフィルター があります。


STEP2
フィルタの条件部分にマウスポインタを移動すると
右端に鉛筆アイコンが表示されます。
それをクリックして編集します。


STEP3
fixVersion in unreleasedVersions() OR fixVersion is EMPTY
という条件を削除して「更新」をクリックします。


サブフィルタがありません
という状態になります。

これで
クラシック・次世代、両プロジェクトの課題を一つのカンバンボードに表示できました。


補足

サブフィルターの設定だけでなく、「列」のマッピングも確認
  1. ボード設定の「列」を開きます
  2. 右端の「課題を含むステータス」を確認します
  3. 対象のステータスの課題がそこにあると、ボードの列に表示されません
  4. 表示したい列にドラッグ&ドロップして追加すると、ボードに表示されます





「列」のマッピングをしているのに
次世代プロジェクトの課題がボードに表示されない場合は
本投稿のカンバン ボード サブフィルターを空にすると表示されるようになるかも。


参考

Any way to include next-gen issues in a classic board ?
https://community.atlassian.com/t5/Jira-questions/Any-way-to-include-next-gen-issues-in-a-classic-board/qaq-p/1012688

Latest post

Googleドキュメントに見出しを追加したい

今回の例では、ドキュメントの末尾に「見出しD」 を追加します。 見出しA, B, C, Dのスタイルは、見出し3 ( HEADING3 ) に設定しています。  下記Code.gsの  GOOGLE_DOCUMENT_URL を設定して  addHeadingToEnd()  を...