From 472270b2312bd6f7c64c39c65a4e15e03cae29bc Mon Sep 17 00:00:00 2001 From: Christian Schalk <79225645+cschalk-goog@users.noreply.github.com> Date: Tue, 15 Mar 2022 14:10:28 -0700 Subject: [PATCH 01/99] Forms api ga apps script snippets updates (#310) * Updates to Apps Script webapp for V1 GA * Updates for Forms API GA v1 Co-authored-by: Steve Bazyl --- .../AppsScriptFormsAPIWebApp/FormsAPI.gs | 20 +++++++++---------- .../demos/AppsScriptFormsAPIWebApp/Main.html | 18 ++++++++--------- forms-api/snippets/README.md | 6 ++---- forms-api/snippets/retrieve_all_responses.gs | 4 ++-- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs b/forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs index 87c170caf..50892992a 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs @@ -13,7 +13,7 @@ // limitations under the License. // Global constants. Customize as needed. -const formsAPIUrl = 'https://forms.googleapis.com/v1beta/forms/'; +const formsAPIUrl = 'https://forms.googleapis.com/v1/forms/'; const formId = ''; const topicName = 'projects/'; @@ -22,7 +22,7 @@ const topicName = 'projects/'; /** * Forms API Method: forms.create - * POST https://forms.googleapis.com/v1beta/forms + * POST https://forms.googleapis.com/v1/forms */ function create(title) { const accessToken = ScriptApp.getOAuthToken(); @@ -50,7 +50,7 @@ function create(title) { /** * Forms API Method: forms.get - * GET https://forms.googleapis.com/v1beta/forms/{formId}/responses/{responseId} + * GET https://forms.googleapis.com/v1/forms/{formId}/responses/{responseId} */ function get(formId) { const accessToken = ScriptApp.getOAuthToken(); @@ -76,7 +76,7 @@ function get(formId) { /** * Forms API Method: forms.batchUpdate - * POST https://forms.googleapis.com/v1beta/forms/{formId}:batchUpdate + * POST https://forms.googleapis.com/v1/forms/{formId}:batchUpdate */ function batchUpdate(formId) { const accessToken = ScriptApp.getOAuthToken(); @@ -111,7 +111,7 @@ function batchUpdate(formId) { /** * Forms API Method: forms.responses.get - * GET https://forms.googleapis.com/v1beta/forms/{formId}/responses/{responseId} + * GET https://forms.googleapis.com/v1/forms/{formId}/responses/{responseId} */ function responsesGet(formId, responseId) { const accessToken = ScriptApp.getOAuthToken(); @@ -137,7 +137,7 @@ function responsesGet(formId, responseId) { /** * Forms API Method: forms.responses.list - * GET https://forms.googleapis.com/v1beta/forms/{formId}/responses + * GET https://forms.googleapis.com/v1/forms/{formId}/responses */ function responsesList(formId) { const accessToken = ScriptApp.getOAuthToken(); @@ -163,7 +163,7 @@ function responsesList(formId) { /** * Forms API Method: forms.watches.create - * POST https://forms.googleapis.com/v1beta/forms/{formId}/watches + * POST https://forms.googleapis.com/v1/forms/{formId}/watches */ function createWatch(formId) { let accessToken = ScriptApp.getOAuthToken(); @@ -200,7 +200,7 @@ function createWatch(formId) { /** * Forms API Method: forms.watches.delete - * DELETE https://forms.googleapis.com/v1beta/forms/{formId}/watches/{watchId} + * DELETE https://forms.googleapis.com/v1/forms/{formId}/watches/{watchId} */ function deleteWatch(formId, watchId) { let accessToken = ScriptApp.getOAuthToken(); @@ -230,7 +230,7 @@ function deleteWatch(formId, watchId) { /** * Forms API Method: forms.watches.list - * GET https://forms.googleapis.com/v1beta/forms/{formId}/watches + * GET https://forms.googleapis.com/v1/forms/{formId}/watches */ function watchesList(formId) { Logger.log('formId is: ' + formId); @@ -255,7 +255,7 @@ function watchesList(formId) { /** * Forms API Method: forms.watches.renew - * POST https://forms.googleapis.com/v1beta/forms/{formId}/watches/{watchId}:renew + * POST https://forms.googleapis.com/v1/forms/{formId}/watches/{watchId}:renew */ function renewWatch(formId, watchId) { let accessToken = ScriptApp.getOAuthToken(); diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html b/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html index 92e30905c..a3f4b4c40 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html @@ -257,7 +257,7 @@

(spec)
+ (spec)
Form title: @@ -267,7 +267,7 @@

(spec)
+ (spec)
@@ -275,7 +275,7 @@

(spec)
+ (spec)
@@ -284,7 +284,7 @@

(spec)
+ (spec)
@@ -293,7 +293,7 @@

(spec)
+ (spec)
Response id:
@@ -303,7 +303,7 @@

(spec)
+ (spec)
@@ -312,7 +312,7 @@

(spec)
+ (spec)
Watch id:
@@ -322,7 +322,7 @@

(spec)
+ (spec)
@@ -331,7 +331,7 @@

(spec)
+ (spec)
Watch id:
diff --git a/forms-api/snippets/README.md b/forms-api/snippets/README.md index 3b4d4cca0..45799a1d1 100644 --- a/forms-api/snippets/README.md +++ b/forms-api/snippets/README.md @@ -1,6 +1,4 @@ # Forms API -The Google Forms API is currently in Restricted Beta. To use the API and these -samples prior to General Availability, your Google Cloud project must be -allowlisted. To request that your project be allowlisted, complete the -[Early Adopter Program application](https://developers.google.com/forms/api/eap). +To run, you must set up your GCP project to use the Forms API. +See: https://developers.google.com/forms/api/ diff --git a/forms-api/snippets/retrieve_all_responses.gs b/forms-api/snippets/retrieve_all_responses.gs index 688adecdd..fd9939700 100644 --- a/forms-api/snippets/retrieve_all_responses.gs +++ b/forms-api/snippets/retrieve_all_responses.gs @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ // Get OAuth Token var OAuthToken = ScriptApp.getOAuthToken(); Logger.log('OAuth token is: ' + OAuthToken); - var formsAPIUrl = 'https://forms.googleapis.com/v1beta/forms/' + formId + '/' + 'responses'; + var formsAPIUrl = 'https://forms.googleapis.com/v1/forms/' + formId + '/' + 'responses'; Logger.log('formsAPIUrl is: ' + formsAPIUrl); var options = { 'headers': { From ad7dfafea3b81aa75277f5e861f16756172cbd11 Mon Sep 17 00:00:00 2001 From: Christian Schalk <79225645+cschalk-goog@users.noreply.github.com> Date: Tue, 15 Mar 2022 14:53:39 -0700 Subject: [PATCH 02/99] Updates to Apps Script webapp for V1 GA (#309) From 36f8d4f22ad48ef009500f64849c0e2dbb5a7913 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:53:53 -0600 Subject: [PATCH 03/99] Update dependency eslint to v8.11.0 (#308) Co-authored-by: renovate[bot] --- package-lock.json | 63 +++++++++++++++++------------------------------ package.json | 2 +- yarn.lock | 25 ++++++++----------- 3 files changed, 34 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93c521756..6e4e727de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,23 +9,23 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "eslint": "8.10.0", + "eslint": "8.11.0", "eslint-config-google": "0.14.0", "eslint-plugin-async-await": "0.0.0", "eslint-plugin-googleappsscript": "1.0.4" } }, "node_modules/@eslint/eslintrc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.0.tgz", - "integrity": "sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", + "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.3.1", "globals": "^13.9.0", - "ignore": "^4.0.6", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.0.4", @@ -249,12 +249,12 @@ } }, "node_modules/eslint": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.10.0.tgz", - "integrity": "sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz", + "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.2.0", + "@eslint/eslintrc": "^1.2.1", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -379,15 +379,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/espree": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", @@ -562,9 +553,9 @@ } }, "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true, "engines": { "node": ">= 4" @@ -959,16 +950,16 @@ }, "dependencies": { "@eslint/eslintrc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.0.tgz", - "integrity": "sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", + "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.3.1", "globals": "^13.9.0", - "ignore": "^4.0.6", + "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.0.4", @@ -1133,12 +1124,12 @@ "dev": true }, "eslint": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.10.0.tgz", - "integrity": "sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz", + "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.0", + "@eslint/eslintrc": "^1.2.1", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -1173,14 +1164,6 @@ "strip-json-comments": "^3.1.0", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", - "dev": true - } } }, "eslint-config-google": { @@ -1373,9 +1356,9 @@ "dev": true }, "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, "import-fresh": { diff --git a/package.json b/package.json index f5271f959..ff92e3981 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "API" ], "devDependencies": { - "eslint": "8.10.0", + "eslint": "8.11.0", "eslint-config-google": "0.14.0", "eslint-plugin-async-await": "0.0.0", "eslint-plugin-googleappsscript": "1.0.4" diff --git a/yarn.lock b/yarn.lock index 4e27d5847..cbf7a5d07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,16 +2,16 @@ # yarn lockfile v1 -"@eslint/eslintrc@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.0.tgz" - integrity sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w== +"@eslint/eslintrc@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz" + integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== dependencies: ajv "^6.12.4" debug "^4.3.2" espree "^9.3.1" globals "^13.9.0" - ignore "^4.0.6" + ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" minimatch "^3.0.4" @@ -186,12 +186,12 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.10.0: - version "8.10.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.10.0.tgz" - integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw== +eslint@8.11.0: + version "8.11.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz" + integrity sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA== dependencies: - "@eslint/eslintrc" "^1.2.0" + "@eslint/eslintrc" "^1.2.1" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -336,11 +336,6 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" From 66dd73e30b22e670958e5ca4d9041dd3394aaa69 Mon Sep 17 00:00:00 2001 From: Rajesh Mudaliyar Date: Tue, 15 Mar 2022 14:54:31 -0700 Subject: [PATCH 04/99] Update selection.gs (#306) Co-authored-by: Priyankarp24 --- slides/selection/selection.gs | 324 ++++++++++++++++++++-------------- 1 file changed, 188 insertions(+), 136 deletions(-) diff --git a/slides/selection/selection.gs b/slides/selection/selection.gs index ce055104e..9880b285e 100644 --- a/slides/selection/selection.gs +++ b/slides/selection/selection.gs @@ -15,76 +15,88 @@ */ // [START apps_script_slides_get_selection] -var selection = SlidesApp.getActivePresentation().getSelection(); +const selection = SlidesApp.getActivePresentation().getSelection(); // [END apps_script_slides_get_selection] // [START apps_script_slides_get_current_page] -var currentPage = SlidesApp.getActivePresentation().getSelection().getCurrentPage(); +const currentPage = SlidesApp.getActivePresentation().getSelection().getCurrentPage(); // [END apps_script_slides_get_current_page] -// [START apps_script_slides_selection_type] -var selection = SlidesApp.getActivePresentation().getSelection(); -var selectionType = selection.getSelectionType(); -var currentPage; -switch (selectionType) { - case SlidesApp.SelectionType.NONE: - Logger.log('Nothing selected'); - break; - case SlidesApp.SelectionType.CURRENT_PAGE: - currentPage = selection.getCurrentPage(); - Logger.log('Selection is a page with ID: ' + currentPage.getObjectId()); - break; - case SlidesApp.SelectionType.PAGE_ELEMENT: - var pageElements = selection.getPageElementRange().getPageElements(); - Logger.log('There are ' + pageElements.length + ' page elements selected.'); - break; - case SlidesApp.SelectionType.TEXT: - var tableCellRange = selection.getTableCellRange(); - if (tableCellRange != null) { - var tableCell = tableCellRange.getTableCells()[0]; - Logger.log('Selected text is in a table at row ' + - tableCell.getRowIndex() + ', column ' + - tableCell.getColumnIndex()); - } - var textRange = selection.getTextRange(); - if (textRange.getStartIndex() == textRange.getEndIndex()) { - Logger.log('Text cursor position: ' + textRange.getStartIndex()); - } else { - Logger.log('Selection is a text range from: ' + textRange.getStartIndex() + ' to: ' + - textRange.getEndIndex() + ' is selected'); - } - break; - case SlidesApp.SelectionType.TABLE_CELL: - var tableCells = selection.getTableCellRange().getTableCells(); - var table = tableCells[0].getParentTable(); - Logger.log('There are ' + tableCells.length + ' table cells selected.'); - break; - case SlidesApp.SelectionType.PAGE: - var pages = selection.getPageRange().getPages(); - Logger.log('There are ' + pages.length + ' pages selected.'); - break; - default: - break; -} +/** + * Selection type to read the current selection in a type-appropriate way. + */ +function slidesSelectionTypes() { + // [START apps_script_slides_selection_type] + const selection = SlidesApp.getActivePresentation().getSelection(); + const selectionType = selection.getSelectionType(); + let currentPage; + switch (selectionType) { + case SlidesApp.SelectionType.NONE: + Logger.log('Nothing selected'); + break; + case SlidesApp.SelectionType.CURRENT_PAGE: + currentPage = selection.getCurrentPage(); + Logger.log('Selection is a page with ID: ' + currentPage.getObjectId()); + break; + case SlidesApp.SelectionType.PAGE_ELEMENT: + const pageElements = selection.getPageElementRange().getPageElements(); + Logger.log('There are ' + pageElements.length + ' page elements selected.'); + break; + case SlidesApp.SelectionType.TEXT: + const tableCellRange = selection.getTableCellRange(); + if (tableCellRange !== null) { + const tableCell = tableCellRange.getTableCells()[0]; + Logger.log('Selected text is in a table at row ' + + tableCell.getRowIndex() + ', column ' + + tableCell.getColumnIndex()); + } + const textRange = selection.getTextRange(); + if (textRange.getStartIndex() === textRange.getEndIndex()) { + Logger.log('Text cursor position: ' + textRange.getStartIndex()); + } else { + Logger.log('Selection is a text range from: ' + textRange.getStartIndex() + ' to: ' + + textRange.getEndIndex() + ' is selected'); + } + break; + case SlidesApp.SelectionType.TABLE_CELL: + const tableCells = selection.getTableCellRange().getTableCells(); + const table = tableCells[0].getParentTable(); + Logger.log('There are ' + tableCells.length + ' table cells selected.'); + break; + case SlidesApp.SelectionType.PAGE: + const pages = selection.getPageRange().getPages(); + Logger.log('There are ' + pages.length + ' pages selected.'); + break; + default: + break; + } // [END apps_script_slides_selection_type] - +} +/** + * Selecting the current page + */ +function slideSelect() { // [START apps_script_slides_select] // Select the first slide as the current page selection and remove any previous selection. -var selection = SlidesApp.getActivePresentation().getSelection(); -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -slide.selectAsCurrentPage(); + const selection = SlidesApp.getActivePresentation().getSelection(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + slide.selectAsCurrentPage(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.CURRENT_PAGE // selection.getCurrentPage() = slide // // [END apps_script_slides_select] - +} +/** + * Selecting a page element. + */ +function selectPageElement() { // [START apps_script_slides_select_page_element] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var pageElement = slide.getPageElements()[0]; -// Only select this page element and remove any previous selection. -pageElement.select(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const pageElement = slide.getPageElements()[0]; + // Only select this page element and remove any previous selection. + pageElement.select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT @@ -92,16 +104,20 @@ pageElement.select(); // selection.getPageElementRange().getPageElements()[0] = pageElement // // [END apps_script_slides_select_page_element] - -// [START apps_script_slides_select_multiple_page_elements] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -// First select the slide page, as the current page selection. -slide.selectAsCurrentPage(); -// Then select all the page elements in the selected slide page. -var pageElements = slide.getPageElements(); -for (var i = 0; i < pageElements.length; i++) { - pageElements[i].select(false); } +/** + * Selecting multiple page elements + */ +function selectMultiplePageElement() { +// [START apps_script_slides_select_multiple_page_elements] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + // First select the slide page, as the current page selection. + slide.selectAsCurrentPage(); + // Then select all the page elements in the selected slide page. + const pageElements = slide.getPageElements(); + for (let i = 0; i < pageElements.length; i++) { + pageElements[i].select(false); + } // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT @@ -109,22 +125,27 @@ for (var i = 0; i < pageElements.length; i++) { // selection.getPageElementRange().getPageElements() = pageElements // // [END apps_script_slides_select_multiple_page_elements] - +} +/** + *This shows how selection can be transformed by manipulating + * selected page elements. + */ +function slideTransformSelection() { // [START apps_script_slides_transform_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape1 = slide.getPageElements()[0].asShape(); -var shape2 = slide.getPageElements()[1].asShape(); -// Select both the shapes. -shape1.select(); -shape2.select(false); -// State of selection -// -// selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT -// selection.getCurrentPage() = slide -// selection.getPageElementRange().getPageElements() = [shape1, shape2] -// -// Remove one shape. -shape2.remove(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const shape1 = slide.getPageElements()[0].asShape(); + const shape2 = slide.getPageElements()[1].asShape(); + // Select both the shapes. + shape1.select(); + shape2.select(false); + // State of selection + // + // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT + // selection.getCurrentPage() = slide + // selection.getPageElementRange().getPageElements() = [shape1, shape2] + // + // Remove one shape. + shape2.remove(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT @@ -132,13 +153,17 @@ shape2.remove(); // selection.getPageElementRange().getPageElements() = [shape1] // // [END apps_script_slides_transform_selection] - -// [START apps_script_slides_range_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.getPageElements()[0].asShape(); -shape.getText().setText('Hello'); -// Range selection: Select the text range 'He'. -shape.getText().getRange(0, 2).select(); +} +/** + * Range selection within text contained in a shape. + */ +function slidesRangeSelection() { +// [START apps_script_slides_range_selection_in_shape] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const shape = slide.getPageElements()[0].asShape(); + shape.getText().setText('Hello'); + // Range selection: Select the text range 'He'. + shape.getText().getRange(0, 2).select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.TEXT @@ -147,14 +172,18 @@ shape.getText().getRange(0, 2).select(); // selection.getTextRange().getStartIndex() = 0 // selection.getTextRange().getEndIndex() = 2 // -// [END apps_script_slides_range_selection] - -// [START apps_script_slides_cursor_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.getPageElements()[0].asShape(); -shape.getText().setText('Hello'); -// Cursor selection: Place the cursor after 'H' like 'H|ello'. -shape.getText().getRange(1, 1).select(); +// [END apps_script_slides_range_selection_in_shape] +} +/** + * Cursor selection within text contained in a shape. + */ +function slidesCursorSelection() { +// [START apps_script_slides_cursor_selection_in_shape] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const shape = slide.getPageElements()[0].asShape(); + shape.getText().setText('Hello'); + // Cursor selection: Place the cursor after 'H' like 'H|ello'. + shape.getText().getRange(1, 1).select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.TEXT @@ -163,15 +192,19 @@ shape.getText().getRange(1, 1).select(); // selection.getTextRange().getStartIndex() = 1 // selection.getTextRange().getEndIndex() = 1 // -// [END apps_script_slides_cursor_selection] - -// [START apps_script_slides_range_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var table = slide.getPageElements()[0].asTable(); -var tableCell = table.getCell(0, 1); -tableCell.getText().setText('Hello'); -// Range selection: Select the text range 'He'. -tableCell.getText().getRange(0, 2).select(); +// [END apps_script_slides_cursor_selection_in_shape] +} +/** + * Range selection in table cell. + */ +function slideRangeSelection() { +// [START apps_script_slides_range_selection_in_table] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const table = slide.getPageElements()[0].asTable(); + const tableCell = table.getCell(0, 1); + tableCell.getText().setText('Hello'); + // Range selection: Select the text range 'He'. + tableCell.getText().getRange(0, 2).select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.TEXT @@ -181,15 +214,19 @@ tableCell.getText().getRange(0, 2).select(); // selection.getTextRange().getStartIndex() = 0 // selection.getTextRange().getEndIndex() = 2 // -// [END apps_script_slides_range_selection] - -// [START apps_script_slides_cursor_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var table = slide.getPageElements()[0].asTable(); -var tableCell = table.getCell(0, 1); -tableCell.getText().setText('Hello'); -// Cursor selection: Place the cursor after 'H' like 'H|ello'. -tableCell.getText().getRange(1, 1).select(); +// [END apps_script_slides_range_selection_in_table] +} +/** + * Cursor selection in table cell. + */ +function cursorSelection() { +// [START apps_script_slides_cursor_selection_in_table] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const table = slide.getPageElements()[0].asTable(); + const tableCell = table.getCell(0, 1); + tableCell.getText().setText('Hello'); + // Cursor selection: Place the cursor after 'H' like 'H|ello'. + tableCell.getText().getRange(1, 1).select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.TEXT @@ -199,25 +236,29 @@ tableCell.getText().getRange(1, 1).select(); // selection.getTextRange().getStartIndex() = 1 // selection.getTextRange().getEndIndex() = 1 // -// [END apps_script_slides_cursor_selection] - +// [END apps_script_slides_cursor_selection_in_table] +} +/** + * This shows how the selection can be transformed by editing the selected text. + */ +function selectTransformation() { // [START apps_script_slides_selection_transformation] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.getPageElements()[0].asShape(); -var textRange = shape.getText(); -textRange.setText('World'); -// Select all the text 'World'. -textRange.select(); -// State of selection -// -// selection.getSelectionType() = SlidesApp.SelectionType.TEXT -// selection.getCurrentPage() = slide -// selection.getPageElementRange().getPageElements()[0] = shape -// selection.getTextRange().getStartIndex() = 0 -// selection.getTextRange().getEndIndex() = 6 -// -// Add some text to the shape, and the selection will be transformed. -textRange.insertText(0, 'Hello '); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const shape = slide.getPageElements()[0].asShape(); + const textRange = shape.getText(); + textRange.setText('World'); + // Select all the text 'World'. + textRange.select(); + // State of selection + // + // selection.getSelectionType() = SlidesApp.SelectionType.TEXT + // selection.getCurrentPage() = slide + // selection.getPageElementRange().getPageElements()[0] = shape + // selection.getTextRange().getStartIndex() = 0 + // selection.getTextRange().getEndIndex() = 6 + // + // Add some text to the shape, and the selection will be transformed. + textRange.insertText(0, 'Hello '); // State of selection // @@ -228,17 +269,27 @@ textRange.insertText(0, 'Hello '); // selection.getTextRange().getEndIndex() = 12 // // [END apps_script_slides_selection_transformation] - +} +/** + * The following example shows how to unselect any current selections on a page + * by setting that page as the current page. + */ +function slidesUnselectingCurrentPage() { // [START apps_script_slides_unselecting] // Unselect one or more page elements already selected. // // In case one or more page elements in the first slide are selected, setting the // same (or any other) slide page as the current page would do the unselect. // -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -slide.selectAsCurrentPage(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + slide.selectAsCurrentPage(); // [END apps_script_slides_unselecting] - +} +/** + * The following example shows how to unselect any current selections on a page + * by selecting one page element, thus removing all other items from the selection. + */ +function slideUnselectingPageElements() { // [START apps_script_slides_selecting] // Unselect one or more page elements already selected. // @@ -246,6 +297,7 @@ slide.selectAsCurrentPage(); // selecting any pageElement in the first slide (or any other pageElement) would // do the unselect and select that pageElement. // -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -slide.getPageElements()[0].select(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + slide.getPageElements()[0].select(); // [END apps_script_slides_selecting] +} From 16ab0796ef26905df008e6cc366862a4026028a7 Mon Sep 17 00:00:00 2001 From: Rajesh Mudaliyar Date: Tue, 15 Mar 2022 14:54:57 -0700 Subject: [PATCH 05/99] Updated to ES6, added exception handling (#307) Co-authored-by: soheilv --- advanced/shoppingContent.gs | 128 ++++++++++++++---------- advanced/tagManager.gs | 190 ++++++++++++++++++++---------------- 2 files changed, 181 insertions(+), 137 deletions(-) diff --git a/advanced/shoppingContent.gs b/advanced/shoppingContent.gs index 69f1ad0d4..d27b41994 100644 --- a/advanced/shoppingContent.gs +++ b/advanced/shoppingContent.gs @@ -18,9 +18,9 @@ * Inserts a product into the products list. Logs the API response. */ function productInsert() { - var merchantId = 123456; // Replace this with your Merchant Center ID. + const merchantId = 123456; // Replace this with your Merchant Center ID. // Create a product resource and insert it - var productResource = { + const productResource = { 'offerId': 'book123', 'title': 'A Tale of Two Cities', 'description': 'A classic novel about the French Revolution', @@ -52,8 +52,14 @@ function productInsert() { } }; - response = ShoppingContent.Products.insert(productResource, merchantId); - Logger.log(response); // RESTful insert returns the JSON object as a response. + try { + response = ShoppingContent.Products.insert(productResource, merchantId); + // RESTful insert returns the JSON object as a response. + Logger.log(response); + } catch (e) { + // TODO (Developer) - Handle exceptions + Logger.log('Failed with error: $s', e.error); + } } // [END apps_script_shopping_product_insert] @@ -62,26 +68,31 @@ function productInsert() { * Lists the products for a given merchant. */ function productList() { - var merchantId = 123456; // Replace this with your Merchant Center ID. - var pageToken; - var pageNum = 1; - var maxResults = 10; - do { - var products = ShoppingContent.Products.list(merchantId, { - pageToken: pageToken, - maxResults: maxResults - }); - Logger.log('Page ' + pageNum); - if (products.resources) { - for (var i = 0; i < products.resources.length; i++) { - Logger.log('Item [' + i + '] ==> ' + products.resources[i]); + const merchantId = 123456; // Replace this with your Merchant Center ID. + let pageToken; + let pageNum = 1; + const maxResults = 10; + try { + do { + const products = ShoppingContent.Products.list(merchantId, { + pageToken: pageToken, + maxResults: maxResults + }); + Logger.log('Page ' + pageNum); + if (products.resources) { + for (let i = 0; i < products.resources.length; i++) { + Logger.log('Item [' + i + '] ==> ' + products.resources[i]); + } + } else { + Logger.log('No more products in account ' + merchantId); } - } else { - Logger.log('No more products in account ' + merchantId); - } - pageToken = products.nextPageToken; - pageNum++; - } while (pageToken); + pageToken = products.nextPageToken; + pageNum++; + } while (pageToken); + } catch (e) { + // TODO (Developer) - Handle exceptions + Logger.log('Failed with error: $s', e.error); + } } // [END apps_script_shopping_product_list] @@ -93,7 +104,7 @@ function productList() { * @param {object} productResource3 The third product resource. */ function custombatch(productResource1, productResource2, productResource3) { - var merchantId = 123456; // Replace this with your Merchant Center ID. + const merchantId = 123456; // Replace this with your Merchant Center ID. custombatchResource = { 'entries': [ { @@ -119,8 +130,13 @@ function custombatch(productResource1, productResource2, productResource3) { } ] }; - var response = ShoppingContent.Products.custombatch(custombatchResource); - Logger.log(response); + try { + const response = ShoppingContent.Products.custombatch(custombatchResource); + Logger.log(response); + } catch (e) { + // TODO (Developer) - Handle exceptions + Logger.log('Failed with error: $s', e.error); + } } // [END apps_script_shopping_product_batch_insert] @@ -131,37 +147,43 @@ function custombatch(productResource1, productResource2, productResource3) { */ function updateAccountTax() { // Replace this with your Merchant Center ID. - var merchantId = 123456; + const merchantId = 123456; // Replace this with the account that you are updating taxes for. - var accountId = 123456; + const accountId = 123456; - var accounttax = ShoppingContent.Accounttax.get(merchantId, accountId); - Logger.log(accounttax); + try { + const accounttax = ShoppingContent.Accounttax.get(merchantId, accountId); + Logger.log(accounttax); - var taxInfo = { - accountId: accountId, - rules: [ - { - 'useGlobalRate': true, - 'locationId': 21135, - 'shippingTaxed': true, - 'country': 'US' - }, - { - 'ratePercent': 3, - 'locationId': 21136, - 'country': 'US' - }, - { - 'ratePercent': 2, - 'locationId': 21160, - 'shippingTaxed': true, - 'country': 'US' - } - ] - }; + const taxInfo = { + accountId: accountId, + rules: [ + { + 'useGlobalRate': true, + 'locationId': 21135, + 'shippingTaxed': true, + 'country': 'US' + }, + { + 'ratePercent': 3, + 'locationId': 21136, + 'country': 'US' + }, + { + 'ratePercent': 2, + 'locationId': 21160, + 'shippingTaxed': true, + 'country': 'US' + } + ] + }; - Logger.log(ShoppingContent.Accounttax.update(taxInfo, merchantId, accountId)); + Logger.log(ShoppingContent.Accounttax + .update(taxInfo, merchantId, accountId)); + } catch (e) { + // TODO (Developer) - Handle exceptions + Logger.log('Failed with error: $s', e.error); + } } // [END apps_script_shopping_account_info] diff --git a/advanced/tagManager.gs b/advanced/tagManager.gs index 852b97c62..f088e9f06 100644 --- a/advanced/tagManager.gs +++ b/advanced/tagManager.gs @@ -15,58 +15,64 @@ */ // [START apps_script_tag_manager_create_version] /** - * Creates a container version for a particular account with the input accountPath. + * Creates a container version for a particular account + * with the input accountPath. * @param {string} accountPath The account path. * @return {string} The tag manager container version. */ function createContainerVersion(accountPath) { - var date = new Date(); + const date = new Date(); // Creates a container in the account, using the current timestamp to make // sure the container is unique. - var container = TagManager.Accounts.Containers.create( - { - 'name': 'appscript tagmanager container ' + date.getTime(), - 'usageContext': ['WEB'] - }, - accountPath); - var containerPath = container.path; - // Creates a workspace in the container to track entity changes. - var workspace = TagManager.Accounts.Containers.Workspaces.create( - {'name': 'appscript workspace', 'description': 'appscript workspace'}, - containerPath); - var workspacePath = workspace.path; - // Creates a random value variable. - var variable = TagManager.Accounts.Containers.Workspaces.Variables.create( - {'name': 'apps script variable', 'type': 'r'}, - workspacePath); - // Creates a trigger that fires on any page view. - var trigger = TagManager.Accounts.Containers.Workspaces.Triggers.create( - {'name': 'apps script trigger', 'type': 'PAGEVIEW'}, - workspacePath); - // Creates a arbitary pixel that fires the tag on all page views. - var tag = TagManager.Accounts.Containers.Workspaces.Tags.create( - { - 'name': 'apps script tag', - 'type': 'img', - 'liveOnly': false, - 'parameter': [ - {'type': 'boolean', 'key': 'useCacheBuster', 'value': 'true'}, { - 'type': 'template', - 'key': 'cacheBusterQueryParam', - 'value': 'gtmcb' - }, - {'type': 'template', 'key': 'url', 'value': '//example.com'} - ], - 'firingTriggerId': [trigger.triggerId] - }, - workspacePath); - // Creates a container version with the variabe, trigger, and tag. - var version = TagManager.Accounts.Containers.Workspaces - .create_version( - {'name': 'apps script version'}, workspacePath) - .containerVersion; - Logger.log(version); - return version; + try { + const container = TagManager.Accounts.Containers.create( + { + 'name': 'appscript tagmanager container ' + date.getTime(), + 'usageContext': ['WEB'] + }, + accountPath); + const containerPath = container.path; + // Creates a workspace in the container to track entity changes. + const workspace = TagManager.Accounts.Containers.Workspaces.create( + {'name': 'appscript workspace', 'description': 'appscript workspace'}, + containerPath); + const workspacePath = workspace.path; + // Creates a random value variable. + const variable = TagManager.Accounts.Containers.Workspaces.Variables.create( + {'name': 'apps script variable', 'type': 'r'}, + workspacePath); + // Creates a trigger that fires on any page view. + const trigger = TagManager.Accounts.Containers.Workspaces.Triggers.create( + {'name': 'apps script trigger', 'type': 'PAGEVIEW'}, + workspacePath); + // Creates a arbitary pixel that fires the tag on all page views. + const tag = TagManager.Accounts.Containers.Workspaces.Tags.create( + { + 'name': 'apps script tag', + 'type': 'img', + 'liveOnly': false, + 'parameter': [ + {'type': 'boolean', 'key': 'useCacheBuster', 'value': 'true'}, { + 'type': 'template', + 'key': 'cacheBusterQueryParam', + 'value': 'gtmcb' + }, + {'type': 'template', 'key': 'url', 'value': '//example.com'} + ], + 'firingTriggerId': [trigger.triggerId] + }, + workspacePath); + // Creates a container version with the variabe, trigger, and tag. + const version = TagManager.Accounts.Containers.Workspaces + .create_version( + {'name': 'apps script version'}, workspacePath) + .containerVersion; + Logger.log(version); + return version; + } catch (e) { + // TODO (Developer) - Handle exception + Logger.log('Failed with error: %s', e.error); + } } // [END apps_script_tag_manager_create_version] @@ -77,7 +83,7 @@ function createContainerVersion(accountPath) { * @return {string} The container path. */ function grabContainerPath(versionPath) { - var pathParts = versionPath.split('/'); + const pathParts = versionPath.split('/'); return pathParts.slice(0, 4).join('/'); } @@ -87,17 +93,22 @@ function grabContainerPath(versionPath) { * @param {object} version The container version. */ function publishVersionAndQuickPreviewDraft(version) { - var containerPath = grabContainerPath(version.path); - // Publish the input container version. - TagManager.Accounts.Containers.Versions.publish(version.path); - var workspace = TagManager.Accounts.Containers.Workspaces.create( - {'name': 'appscript workspace', 'description': 'appscript workspace'}, - containerPath); - var workspaceId = workspace.path; - // Quick previews the current container draft. - var quickPreview = TagManager.Accounts.Containers.Workspaces.quick_preview( - workspace.path); - Logger.log(quickPreview); + try { + const containerPath = grabContainerPath(version.path); + // Publish the input container version. + TagManager.Accounts.Containers.Versions.publish(version.path); + const workspace = TagManager.Accounts.Containers.Workspaces.create( + {'name': 'appscript workspace', 'description': 'appscript workspace'}, + containerPath); + const workspaceId = workspace.path; + // Quick previews the current container draft. + const quickPreview = TagManager.Accounts.Containers.Workspaces + .quick_preview(workspace.path); + Logger.log(quickPreview); + } catch (e) { + // TODO (Developer) - Handle exceptions + Logger.log('Failed with error: $s', e.error); + } } // [END apps_script_tag_manager_publish_version] @@ -108,7 +119,7 @@ function publishVersionAndQuickPreviewDraft(version) { * @return {string} The container path. */ function grabContainerPath(versionPath) { - var pathParts = versionPath.split('/'); + const pathParts = versionPath.split('/'); return pathParts.slice(0, 4).join('/'); } @@ -118,20 +129,26 @@ function grabContainerPath(versionPath) { * @param {object} version The container version object. */ function createAndReauthorizeUserEnvironment(version) { - // Creates a container version. - var containerPath = grabContainerPath(version.path); - // Creates a user environment that points to a container version. - var environment = TagManager.Accounts.Containers.Environments.create( - { - 'name': 'test_environment', - 'type': 'user', - 'containerVersionId': version.containerVersionId - }, - containerPath); - Logger.log('Original user environment: ' + environment); - // Reauthorizes the user environment that points to a container version. - TagManager.Accounts.Containers.Environments.reauthorize({}, environment.path); - Logger.log('Reauthorized user environment: ' + environment); + try { + // Creates a container version. + const containerPath = grabContainerPath(version.path); + // Creates a user environment that points to a container version. + const environment = TagManager.Accounts.Containers.Environments.create( + { + 'name': 'test_environment', + 'type': 'user', + 'containerVersionId': version.containerVersionId + }, + containerPath); + Logger.log('Original user environment: ' + environment); + // Reauthorizes the user environment that points to a container version. + TagManager.Accounts.Containers.Environments.reauthorize( + {}, environment.path); + Logger.log('Reauthorized user environment: ' + environment); + } catch (e) { + // TODO (Developer) - Handle exceptions + Logger.log('Failed with error: $s', e.error); + } } // [END apps_script_tag_manager_create_user_environment] @@ -141,20 +158,25 @@ function createAndReauthorizeUserEnvironment(version) { * @param {string} accountPath The account path. */ function logAllAccountUserPermissionsWithContainerAccess(accountPath) { - var userPermissions = + try { + const userPermissions = TagManager.Accounts.User_permissions.list(accountPath).userPermission; - for (var i = 0; i < userPermissions.length; i++) { - var userPermission = userPermissions[i]; - if ('emailAddress' in userPermission) { - var containerAccesses = userPermission.containerAccess; - for (var j = 0; j < containerAccesses.length; j++) { - var containerAccess = containerAccesses[j]; - Logger.log( - 'emailAddress:' + userPermission.emailAddress + ' containerId:' + - containerAccess.containerId + ' containerAccess:' + - containerAccess.permission); + for (let i = 0; i < userPermissions.length; i++) { + const userPermission = userPermissions[i]; + if ('emailAddress' in userPermission) { + const containerAccesses = userPermission.containerAccess; + for (let j = 0; j < containerAccesses.length; j++) { + const containerAccess = containerAccesses[j]; + Logger.log( + 'emailAddress:' + userPermission.emailAddress + + ' containerId:' + containerAccess.containerId + + ' containerAccess:' + containerAccess.permission); + } } } + } catch (e) { + // TODO (Developer) - Handle exceptions + Logger.log('Failed with error: $s', e.error); } } // [END apps_script_tag_manager_log] From 822bf784e42055c6f254944addcda0d1ef41e2dd Mon Sep 17 00:00:00 2001 From: Rajesh Mudaliyar Date: Tue, 15 Mar 2022 14:55:41 -0700 Subject: [PATCH 06/99] clean up apps-script samples (#305) Co-authored-by: Priyankarp24 --- advanced/fusionTables.gs | 62 ----- advanced/googlePlus.gs | 76 ------- advanced/googlePlusDomains.gs | 95 -------- advanced/mirror.gs | 83 ------- advanced/prediction.gs | 117 ---------- advanced/urlShortener.gs | 39 ---- calendar/README.md | 5 - calendar/vacationCalendar/appsscript.json | 15 -- calendar/vacationCalendar/vacationCalendar.gs | 186 --------------- gmail/mailmerge/mailmerge.gs | 211 ------------------ 10 files changed, 889 deletions(-) delete mode 100644 advanced/fusionTables.gs delete mode 100644 advanced/googlePlus.gs delete mode 100644 advanced/googlePlusDomains.gs delete mode 100644 advanced/mirror.gs delete mode 100644 advanced/prediction.gs delete mode 100644 advanced/urlShortener.gs delete mode 100644 calendar/README.md delete mode 100644 calendar/vacationCalendar/appsscript.json delete mode 100644 calendar/vacationCalendar/vacationCalendar.gs delete mode 100644 gmail/mailmerge/mailmerge.gs diff --git a/advanced/fusionTables.gs b/advanced/fusionTables.gs deleted file mode 100644 index 87d45f054..000000000 --- a/advanced/fusionTables.gs +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_fusion_tables_list] -/** - * This sample lists Fusion Tables that the user has access to. - */ -function listTables() { - var tables = FusionTables.Table.list(); - if (tables.items) { - for (var i = 0; i < tables.items.length; i++) { - var table = tables.items[i]; - Logger.log('Table with name "%s" and ID "%s" was found.', - table.name, table.tableId); - } - } else { - Logger.log('No tables found.'); - } -} -// [END apps_script_fusion_tables_list] - -// [START apps_script_fusion_tables_run_query] -/** - * This sample queries for the first 100 rows in the given Fusion Table and - * saves the results to a new spreadsheet. - * @param {string} tableId The table ID. - */ -function runQuery(tableId) { - var sql = 'SELECT * FROM ' + tableId + ' LIMIT 100'; - var result = FusionTables.Query.sqlGet(sql, { - hdrs: false - }); - if (result.rows) { - var spreadsheet = SpreadsheetApp.create('Fusion Table Query Results'); - var sheet = spreadsheet.getActiveSheet(); - - // Append the headers. - sheet.appendRow(result.columns); - - // Append the results. - sheet.getRange(2, 1, result.rows.length, result.columns.length) - .setValues(result.rows); - - Logger.log('Query results spreadsheet created: %s', - spreadsheet.getUrl()); - } else { - Logger.log('No rows returned.'); - } -} -// [END apps_script_fusion_tables_run_query] diff --git a/advanced/googlePlus.gs b/advanced/googlePlus.gs deleted file mode 100644 index fbf2cc7e3..000000000 --- a/advanced/googlePlus.gs +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_plus_people] -/** - * The following example demonstrates how to retrieve a list of the people - * in the user's Google+ circles. - */ -function getPeople() { - var userId = 'me'; - var people; - var pageToken; - do { - people = Plus.People.list(userId, 'visible', { - pageToken: pageToken - }); - if (people.items) { - for (var i = 0; i < people.items.length; i++) { - var person = people.items[i]; - Logger.log(person.displayName); - } - } else { - Logger.log('No people in your visible circles.'); - } - pageToken = people.nextPageToken; - } while (pageToken); -} -// [END apps_script_plus_people] - -// [START apps_script_plus_posts] -/** - * The following example demonstrates how to list a user's posts. The returned - * results contain a brief summary of the posts, including a list of comments - * made on the post. - */ -function getPosts() { - var userId = 'me'; - var posts; - var pageToken; - do { - posts = Plus.Activities.list(userId, 'public', { - maxResults: 10, - pageToken: pageToken - }); - if (posts.items) { - for (var i = 0; i < posts.items.length; i++) { - var post = posts.items[i]; - Logger.log(post.title); - var comments = Plus.Comments.list(post.id); - if (comments.items) { - for (var j = 0; j < comments.items.length; j++) { - var comment = comments.items[j]; - Logger.log(comment.actor.displayName + ': ' + - comment.object.content); - } - } - } - } else { - Logger.log('No posts found.'); - } - pageToken = posts.pageToken; - } while (pageToken); -} -// [END apps_script_plus_posts] diff --git a/advanced/googlePlusDomains.gs b/advanced/googlePlusDomains.gs deleted file mode 100644 index 3a0e8364f..000000000 --- a/advanced/googlePlusDomains.gs +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_plus_domains_profile] -/** - * The following example demonstrates how to retrieve details from a user's - * Google+ profile. - */ -function getProfile() { - var userId = 'me'; - var profile = PlusDomains.People.get(userId); - - Logger.log('ID: %s', profile.id); - Logger.log('Display name: %s', profile.displayName); - Logger.log('Image URL: %s', profile.image.url); - Logger.log('Profile URL: %s', profile.url); -} -// [END apps_script_plus_domains_profile] - -// [START apps_script_plus_domains_circle] -/** - * The following example demonstrates how to create an empty circle for a user - * within your G Suite domain. - */ -function createCircle() { - var userId = 'me'; - var circle = PlusDomains.newCircle(); - circle.displayName = 'Tech support'; - - circle = PlusDomains.Circles.insert(circle, userId); - Logger.log('Created "Tech support" circle with id: ' + circle.id); -} -// [END apps_script_plus_domains_circle] - -// [START apps_script_plus_domains_get_posts] -/** - * The following example demonstrates how to list a user's posts. The returned - * results contain a brief summary of the posts, including a title. Use the - * Activities.get() method to read the full details of a post. - */ -function getPosts() { - var userId = 'me'; - var pageToken; - var posts; - do { - posts = PlusDomains.Activities.list(userId, 'user', { - maxResults: 100, - pageToken: pageToken - }); - if (posts.items) { - for (var i = 0; i < posts.items.length; i++) { - var post = posts.items[i]; - Logger.log('ID: %s, Content: %s', post.id, post.object.content); - } - } - pageToken = posts.nextPageToken; - } while (pageToken); -} -// [END apps_script_plus_domains_get_posts] - -// [START apps_script_plus_domains_create_post] -/** - * The following example demonstrates how to create a post that is available - * to all users within your G Suite domain. - */ -function createPost() { - var userId = 'me'; - var post = { - object: { - originalContent: 'Happy Monday! #caseofthemondays' - }, - access: { - items: [{ - type: 'domain' - }], - domainRestricted: true - } - }; - - post = PlusDomains.Activities.insert(post, userId); - Logger.log('Post created with URL: %s', post.url); -} -// [END apps_script_plus_domains_create_post] diff --git a/advanced/mirror.gs b/advanced/mirror.gs deleted file mode 100644 index f0db2df5f..000000000 --- a/advanced/mirror.gs +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_mirror_timeline] -/** - * This sample inserts a new item into the timeline. - */ -function insertTimelineItem() { - const timelineItem = Mirror.newTimelineItem(); - timelineItem.text = 'Hello world!'; - - const notificationConfig = Mirror.newNotificationConfig(); - notificationConfig.level = 'AUDIO_ONLY'; - - const menuItem = Mirror.newMenuItem(); - menuItem.action = 'REPLY'; - - timelineItem.notification = notificationConfig; - timelineItem.menuItems = [menuItem]; - try { - // @see https://developers.google.com/glass/v1/reference/timeline/insert - Mirror.Timeline.insert(timelineItem); - Logger.log('Successfully inserted new item to Timeline '); - } catch (err) { - // TODO (developer)- Handle exception from the API - Logger.log('Failed with error %s', err.message); - } -} -// [END apps_script_mirror_timeline] - -// [START apps_script_mirror_contact] -/** - * This sample inserts a new contact. - * @see https://developers.google.com/glass/v1/reference/contacts/insert - */ -function insertContact() { - const contact = { - id: 'harold', - displayName: 'Harold Penguin', - imageUrls: ['https://developers.google.com/glass/images/harold.jpg'] - }; - try { - Mirror.Contacts.insert(contact); - Logger.log('Successfully inserted contact '); - } catch (err) { - // TODO (developer)- Handle exception from the API - Logger.log('Failed with error %s', err.message); - } -} -// [END apps_script_mirror_contact] - -// [START apps_script_mirror_location] -/** - * This sample prints the most recent known location of the user's Glass to the - * script editor's log. - * @see https://developers.google.com/glass/v1/reference/locations/get - */ -function printLatestLocation() { - try { - const location = Mirror.Locations.get('latest'); - - Logger.log('Location recorded on: ' + location.timestamp); - Logger.log(' > Latitude: ' + location.latitude); - Logger.log(' > Longitude: ' + location.longitude); - Logger.log(' > Accuracy: ' + location.accuracy + ' meters'); - } catch (err) { - // TODO (developer)- Handle exception from the API - Logger.log('Failed with error %s', err.message); - } -} -// [END apps_script_mirror_location] diff --git a/advanced/prediction.gs b/advanced/prediction.gs deleted file mode 100644 index 88ba2817d..000000000 --- a/advanced/prediction.gs +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_prediction_query_hosted_model] -/** - * Runs sentiment analysis across a sentence. - * Prints the sentiment label. - */ -function queryHostedModel() { - // When querying hosted models you must always use this - // specific project number. - var projectNumber = '414649711441'; - var hostedModelName = 'sample.sentiment'; - - // Query the hosted model with a positive statement. - var predictionString = 'Want to go to the park this weekend?'; - var prediction = Prediction.Hostedmodels.predict( - { - input: { - csvInstance: [predictionString] - } - }, - projectNumber, - hostedModelName); - // Logs Sentiment: positive. - Logger.log('Sentiment: ' + prediction.outputLabel); - - // Now query the hosted model with a negative statement. - predictionString = 'You are not very nice!'; - prediction = Prediction.Hostedmodels.predict( - { - input: { - csvInstance: [predictionString] - } - }, - projectNumber, - hostedModelName); - // Logs Sentiment: negative. - Logger.log('Sentiment: ' + prediction.outputLabel); -} -// [END apps_script_prediction_query_hosted_model] - -// [START apps_script_prediction_create_new_model] -/** - * Creates a new prediction model. - */ -function createNewModel() { - // Replace this value with the project number listed in the Google - // APIs Console project. - var projectNumber = 'XXXXXXXX'; - var id = 'mylanguageidmodel'; - var storageDataLocation = 'languageidsample/language_id.txt'; - - // Returns immediately. Training happens asynchronously. - var result = Prediction.Trainedmodels.insert( - { - id: id, - storageDataLocation: storageDataLocation - }, - projectNumber); - Logger.log(result); -} -// [END apps_script_prediction_create_new_model] - -// [START apps_script_prediction_query_training_status] -/** - * Gets the training status from a prediction model. - * Logs the status. - */ -function queryTrainingStatus() { - // Replace this value with the project number listed in the Google - // APIs Console project. - var projectNumber = 'XXXXXXXX'; - var id = 'mylanguageidmodel'; - - var result = Prediction.Trainedmodels.get(projectNumber, id); - Logger.log(result.trainingStatus); -} -// [END apps_script_prediction_query_training_status] - -// [START apps_script_prediction_query_trailed_model] -/** - * Gets the language from a trained language model. - * Logs the language of the sentence. - */ -function queryTrainedModel() { - // Replace this value with the project number listed in the Google - // APIs Console project. - var projectNumber = 'XXXXXXXX'; - var id = 'mylanguageidmodel'; - var query = 'Este es un mensaje de prueba de ejemplo'; - - var prediction = Prediction.Trainedmodels.predict( - { - input: - { - csvInstance: [query] - } - }, - projectNumber, - id); - // Logs Language: Spanish. - Logger.log('Language: ' + prediction.outputLabel); -} -// [END apps_script_prediction_query_trailed_model] diff --git a/advanced/urlShortener.gs b/advanced/urlShortener.gs deleted file mode 100644 index e39c85361..000000000 --- a/advanced/urlShortener.gs +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_url_shortener_shorten] -/** - * Shortens a long URL. Logs this URL. - */ -function shortenUrl() { - var url = UrlShortener.Url.insert({ - longUrl: 'http://www.example.com' - }); - Logger.log('Shortened URL is "%s".', url.id); -} -// [END apps_script_url_shortener_shorten] - -// [START apps_script_url_shortener_get_clicks] -/** - * Logs the number of clicks to a short URL over the last week. - * @param {string} shortUrl The short URL. - */ -function getClicks(shortUrl) { - var url = UrlShortener.Url.get(shortUrl, { - projection: 'ANALYTICS_CLICKS' - }); - Logger.log('The URL received %s clicks this week.', url.analytics.week.shortUrlClicks); -} -// [END apps_script_url_shortener_get_clicks] diff --git a/calendar/README.md b/calendar/README.md deleted file mode 100644 index 9174ad519..000000000 --- a/calendar/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Apps Scripts for Google Calendar - -## [Vacation Calendar](https://developers.google.com/apps-script/articles/vacation-calendar) - -This tutorial allows a user in a domain to automatically populate a team vacation calendar. diff --git a/calendar/vacationCalendar/appsscript.json b/calendar/vacationCalendar/appsscript.json deleted file mode 100644 index bc80838f3..000000000 --- a/calendar/vacationCalendar/appsscript.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [{ - "userSymbol": "AdminDirectory", - "serviceId": "admin", - "version": "directory_v1" - }, { - "userSymbol": "Calendar", - "serviceId": "calendar", - "version": "v3" - }] - }, - "exceptionLogging": "STACKDRIVER" -} diff --git a/calendar/vacationCalendar/vacationCalendar.gs b/calendar/vacationCalendar/vacationCalendar.gs deleted file mode 100644 index 534cfbcbc..000000000 --- a/calendar/vacationCalendar/vacationCalendar.gs +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_calendar_vacation] -var TEAM_CALENDAR_ID = 'ENTER_TEAM_CALENDAR_ID_HERE'; -var KEYWORDS = ['vacation', 'ooo', 'out of office']; -var MONTHS_IN_ADVANCE = 3; - -// The maximum script run time under Apps Script Pro is 30 minutes; this setting -// will be used to report when the script is about to reach that limit. -var MAX_PRO_RUNTIME_MS = 29 * 60 * 1000; - -/** - * Look through the domain users' public calendars and add any - * 'vacation' or 'out of office' events to the team calendar. - */ -function syncTeamVacationCalendar() { - // Define the calendar event date range to search. - var today = new Date(); - var futureDate = new Date(); - futureDate.setMonth(futureDate.getMonth() + MONTHS_IN_ADVANCE); - var lastRun = PropertiesService.getScriptProperties().getProperty('lastRun'); - lastRun = lastRun ? new Date(lastRun) : null; - - // Get the list of users in the domain. - var users = getDomainUsers(); - - // For each user, find events having one or more of the keywords in the event - // summary in the specified date range. Import each of those to the team - // calendar. - var count = 0; - var timeout = false; - for (var i = 0; i < users.length; i++) { - if (isTimeUp(today, new Date())) { - timeout = true; - break; - } - var user = users[i]; - var username = user.split('@')[0]; - KEYWORDS.forEach(function(keyword) { - var events = findEvents(user, keyword, today, futureDate, lastRun); - events.forEach(function(event) { - event.summary = '[' + username + '] ' + event.summary; - event.organizer = { - id: TEAM_CALENDAR_ID - }; - event.attendees = []; - Logger.log('Importing: %s', event.summary); - try { - Calendar.Events.import(event, TEAM_CALENDAR_ID); - count++; - } catch (e) { - Logger.log( - 'Error attempting to import event: %s. Skipping.', e.toString()); - } - }); - }); - } - PropertiesService.getScriptProperties().setProperty('lastRun', today); - Logger.log('Imported ' + count + ' events'); - if (timeout) { - Logger.log('Execution time about to hit quota limit; execution stopped.'); - } - var executionTime = ((new Date()).getTime() - today.getTime()) / 1000.0; - Logger.log('Total execution time (s) : ' + executionTime); ; -} - -/** - * In a given user's calendar, look for occurrences of the given keyword - * in events within the specified date range and return any such events - * found. - * @param {string} user the user's primary email String. - * @param {string} keyword the keyword String to look for. - * @param {Date} start the starting Date of the range to examine. - * @param {Date} end the ending Date of the range to examine. - * @param {Date} opt_since a Date indicating the last time this script was run. - * @return {object[]} an array of calendar event Objects. - */ -function findEvents(user, keyword, start, end, opt_since) { - var params = { - q: keyword, - timeMin: formatDate(start), - timeMax: formatDate(end), - showDeleted: true - }; - if (opt_since) { - // This prevents the script from examining events that have not been - // modified since the specified date (that is, the last time the - // script was run). - params['updatedMin'] = formatDate(opt_since); - } - var results = []; - try { - var response = Calendar.Events.list(user, params); - results = response.items.filter(function(item) { - // Filter out events where the keyword did not appear in the summary - // (that is, the keyword appeared in a different field, and are thus - // is not likely to be relevant). - if (item.summary.toLowerCase().indexOf(keyword) < 0) { - return false; - } - // If the event was created by someone other than the user, only include - // it if the user has marked it as 'accepted'. - if (item.organizer && item.organizer.email != user) { - if (!item.attendees) { - return false; - } - var matching = item.attendees.filter(function(attendee) { - return attendee.self; - }); - return matching.length > 0 && matching[0].status == 'accepted'; - } - return true; - }); - } catch (e) { - Logger.log('Error retriving events for %s, %s: %s; skipping', - user, keyword, e.toString()); - results = []; - } - return results; -} - -/** - * Return a list of the primary emails of users in this domain. - * @return {string[]} An array of user email strings. - */ -function getDomainUsers() { - var pageToken; - var page; - var userEmails = []; - do { - page = AdminDirectory.Users.list({ - customer: 'my_customer', - orderBy: 'givenName', - maxResults: 100, - pageToken: pageToken, - viewType: 'domain_public' - }); - var users = page.users; - if (users) { - userEmails = userEmails.concat(users.map(function(user) { - return user.primaryEmail; - })); - } else { - Logger.log('No domain users found.'); - } - pageToken = page.nextPageToken; - } while (pageToken); - return userEmails; -} - -/** - * Return an RFC3339 formated date String corresponding to the given - * Date object. - * @param {Date} date a Date. - * @return {string} a formatted date string. - */ -function formatDate(date) { - return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ'); -} - -/** - * Compares two Date objects and returns true if the difference - * between them is more than the maximum specified run time. - * - * @param {Date} start the first Date object. - * @param {Date} now the (later) Date object. - * @return {boolean} true if the time difference is greater than - * MAX_PROP_RUNTIME_MS (in milliseconds). - */ -function isTimeUp(start, now) { - return now.getTime() - start.getTime() > MAX_PRO_RUNTIME_MS; -} -// [END apps_script_calendar_vacation] diff --git a/gmail/mailmerge/mailmerge.gs b/gmail/mailmerge/mailmerge.gs deleted file mode 100644 index 30cc15c0c..000000000 --- a/gmail/mailmerge/mailmerge.gs +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// [START apps_script_gmail_mail_merge] -/** - * Iterates row by row in the input range and returns an array of objects. - * Each object contains all the data for a given row, indexed by its normalized column name. - * @param {Sheet} sheet The sheet object that contains the data to be processed - * @param {Range} range The exact range of cells where the data is stored - * @param {number} columnHeadersRowIndex Specifies the row number where the column names are stored. - * This argument is optional and it defaults to the row immediately above range; - * @return {object[]} An array of objects. - */ -function getRowsData(sheet, range, columnHeadersRowIndex) { - columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1; - var numColumns = range.getEndColumn() - range.getColumn() + 1; - var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns); - var headers = headersRange.getValues()[0]; - return getObjects(range.getValues(), normalizeHeaders(headers)); -} - -/** - * For every row of data in data, generates an object that contains the data. Names of - * object fields are defined in keys. - * @param {object} data JavaScript 2d array - * @param {object} keys Array of Strings that define the property names for the objects to create - * @return {object[]} A list of objects. - */ -function getObjects(data, keys) { - var objects = []; - for (var i = 0; i < data.length; ++i) { - var object = {}; - var hasData = false; - for (var j = 0; j < data[i].length; ++j) { - var cellData = data[i][j]; - if (isCellEmpty(cellData)) { - continue; - } - object[keys[j]] = cellData; - hasData = true; - } - if (hasData) { - objects.push(object); - } - } - return objects; -} - -/** - * Returns an array of normalized Strings. - * @param {string[]} headers Array of strings to normalize - * @return {string[]} An array of normalized strings. - */ -function normalizeHeaders(headers) { - var keys = []; - for (var i = 0; i < headers.length; ++i) { - var key = normalizeHeader(headers[i]); - if (key.length > 0) { - keys.push(key); - } - } - return keys; -} - -/** - * Normalizes a string, by removing all alphanumeric characters and using mixed case - * to separate words. The output will always start with a lower case letter. - * This function is designed to produce JavaScript object property names. - * @param {string} header The header to normalize. - * @return {string} The normalized header. - * @example "First Name" -> "firstName" - * @example "Market Cap (millions) -> "marketCapMillions - * @example "1 number at the beginning is ignored" -> "numberAtTheBeginningIsIgnored" - */ -function normalizeHeader(header) { - var key = ''; - var upperCase = false; - for (var i = 0; i < header.length; ++i) { - var letter = header[i]; - if (letter == ' ' && key.length > 0) { - upperCase = true; - continue; - } - if (!isAlnum(letter)) { - continue; - } - if (key.length == 0 && isDigit(letter)) { - continue; // first character must be a letter - } - if (upperCase) { - upperCase = false; - key += letter.toUpperCase(); - } else { - key += letter.toLowerCase(); - } - } - return key; -} - -/** - * Returns true if the cell where cellData was read from is empty. - * @param {string} cellData Cell data - * @return {boolean} True if the cell is empty. - */ -function isCellEmpty(cellData) { - return typeof(cellData) == 'string' && cellData == ''; -} - -/** - * Returns true if the character char is alphabetical, false otherwise. - * @param {string} char The character. - * @return {boolean} True if the char is a number. - */ -function isAlnum(char) { - return char >= 'A' && char <= 'Z' || - char >= 'a' && char <= 'z' || - isDigit(char); -} - -/** - * Returns true if the character char is a digit, false otherwise. - * @param {string} char The character. - * @return {boolean} True if the char is a digit. - */ -function isDigit(char) { - return char >= '0' && char <= '9'; -} - -/** - * Sends emails from spreadsheet rows. - */ -function sendEmails() { - var ss = SpreadsheetApp.getActiveSpreadsheet(); - var dataSheet = ss.getSheets()[0]; - // [START apps_script_gmail_email_data_range] - var dataRange = dataSheet.getRange(2, 1, dataSheet.getMaxRows() - 1, 4); - // [END apps_script_gmail_email_data_range] - - // [START apps_script_gmail_email_template] - var templateSheet = ss.getSheets()[1]; - var emailTemplate = templateSheet.getRange('A1').getValue(); - // [END apps_script_gmail_email_template] - - // [START apps_script_gmail_email_objects] - // Create one JavaScript object per row of data. - var objects = getRowsData(dataSheet, dataRange); - // [END apps_script_gmail_email_objects] - - // For every row object, create a personalized email from a template and send - // it to the appropriate person. - for (var i = 0; i < objects.length; ++i) { - // Get a row object - var rowData = objects[i]; - - // [START apps_script_gmail_email_text] - // Generate a personalized email. - // Given a template string, replace markers (for instance ${"First Name"}) with - // the corresponding value in a row object (for instance rowData.firstName). - var emailText = fillInTemplateFromObject(emailTemplate, rowData); - // [END apps_script_gmail_email_text] - var emailSubject = 'Tutorial: Simple Mail Merge'; - - // [START apps_script_gmail_send_email] - MailApp.sendEmail(rowData.emailAddress, emailSubject, emailText); - // [END apps_script_gmail_send_email] - } -} - -/** - * Replaces markers in a template string with values define in a JavaScript data object. - * @param {string} template Contains markers, for instance ${"Column name"} - * @param {object} data values to that will replace markers. - * For instance data.columnName will replace marker ${"Column name"} - * @return {string} A string without markers. If no data is found to replace a marker, - * it is simply removed. - */ -function fillInTemplateFromObject(template, data) { - var email = template; - // [START apps_script_gmail_template_vars] - // Search for all the variables to be replaced, for instance ${"Column name"} - var templateVars = template.match(/\$\{\"[^\"]+\"\}/g); - // [END apps_script_gmail_template_vars] - - // Replace variables from the template with the actual values from the data object. - // If no value is available, replace with the empty string. - for (var i = 0; templateVars && i < templateVars.length; ++i) { - // normalizeHeader ignores ${"} so we can call it directly here. - // [START apps_script_gmail_template_variable_data] - var variableData = data[normalizeHeader(templateVars[i])]; - // [END apps_script_gmail_template_variable_data] - // [START apps_script_gmail_template_replace] - email = email.replace(templateVars[i], variableData || ''); - // [END apps_script_gmail_template_replace] - } - - return email; -} -// [END apps_script_gmail_mail_merge] From 3d69059ef820ea627d3b76cd88b8a8d79064aeb2 Mon Sep 17 00:00:00 2001 From: Rajesh Mudaliyar Date: Fri, 25 Mar 2022 10:51:09 -0700 Subject: [PATCH 07/99] Apps script samples (#311) * Update classroom.gs * Update region tags * Update target.js * Update adsense.gs Co-authored-by: Priyankarp24 --- advanced/adsense.gs | 66 +++++++++++++++++------------------ advanced/classroom.gs | 4 +-- advanced/slides.gs | 24 ++++++------- apps-script/execute/target.js | 15 ++++++++ 4 files changed, 62 insertions(+), 47 deletions(-) diff --git a/advanced/adsense.gs b/advanced/adsense.gs index 3077fb1dc..3e4610927 100644 --- a/advanced/adsense.gs +++ b/advanced/adsense.gs @@ -21,13 +21,13 @@ function listAccounts() { let pageToken; do { const response = AdSense.Accounts.list({pageToken: pageToken}); - if (response.accounts) { - for (const account of response.accounts) { - Logger.log('Found account with resource name "%s" and display name "%s".', - account.name, account.displayName); - } - } else { + if (!response.accounts) { Logger.log('No accounts found.'); + return; + } + for (const account of response.accounts) { + Logger.log('Found account with resource name "%s" and display name "%s".', + account.name, account.displayName); } pageToken = response.nextPageToken; } while (pageToken); @@ -47,15 +47,15 @@ function listAdClients(accountName) { const response = AdSense.Accounts.Adclients.list(accountName, { pageToken: pageToken }); - if (response.adClients) { - for (const adClient of response.adClients) { - Logger.log('Found ad client for product "%s" with resource name "%s".', - adClient.productCode, adClient.name); - Logger.log('Reporting dimension ID: %s', - adClient.reportingDimensionId ?? 'None'); - } - } else { + if (!response.adClients) { Logger.log('No ad clients found for this account.'); + return; + } + for (const adClient of response.adClients) { + Logger.log('Found ad client for product "%s" with resource name "%s".', + adClient.productCode, adClient.name); + Logger.log('Reporting dimension ID: %s', + adClient.reportingDimensionId ?? 'None'); } pageToken = response.nextPageToken; } while (pageToken); @@ -75,13 +75,13 @@ function listAdUnits(adClientName) { pageSize: 50, pageToken: pageToken }); - if (response.adUnits) { - for (const adUnit of response.adUnits) { - Logger.log('Found ad unit with resource name "%s" and display name "%s".', - adUnit.name, adUnit.displayName); - } - } else { + if (!response.adUnits) { Logger.log('No ad units found for this ad client.'); + return; + } + for (const adUnit of response.adUnits) { + Logger.log('Found ad unit with resource name "%s" and display name "%s".', + adUnit.name, adUnit.displayName); } pageToken = response.nextPageToken; @@ -114,22 +114,22 @@ function generateReport(accountName, adClientReportingDimensionId) { orderBy: ['+DATE'] }); - if (report.rows) { - const spreadsheet = SpreadsheetApp.create('AdSense Report'); - const sheet = spreadsheet.getActiveSheet(); + if (!report.rows) { + Logger.log('No rows returned.'); + return; + } + const spreadsheet = SpreadsheetApp.create('AdSense Report'); + const sheet = spreadsheet.getActiveSheet(); - // Append the headers. - sheet.appendRow(report.headers.map((header) => header.name)); + // Append the headers. + sheet.appendRow(report.headers.map((header) => header.name)); - // Append the results. - sheet.getRange(2, 1, report.rows.length, report.headers.length) - .setValues(report.rows.map((row) => row.cells.map((cell) => cell.value))); + // Append the results. + sheet.getRange(2, 1, report.rows.length, report.headers.length) + .setValues(report.rows.map((row) => row.cells.map((cell) => cell.value))); - Logger.log('Report spreadsheet created: %s', - spreadsheet.getUrl()); - } else { - Logger.log('No rows returned.'); - } + Logger.log('Report spreadsheet created: %s', + spreadsheet.getUrl()); } /** diff --git a/advanced/classroom.gs b/advanced/classroom.gs index c23ed97fc..480cdce1b 100644 --- a/advanced/classroom.gs +++ b/advanced/classroom.gs @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// [START classroom_list_courses] +// [START apps_script_classroom_list_courses] /** * Lists 10 course names and IDs. */ @@ -41,4 +41,4 @@ function listCourses() { Logger.log('Failed with error %s', err.message); } } -// [END classroom_list_courses] +// [END apps_script_classroom_list_courses] diff --git a/advanced/slides.gs b/advanced/slides.gs index 9219b0669..526230abe 100644 --- a/advanced/slides.gs +++ b/advanced/slides.gs @@ -14,7 +14,7 @@ * limitations under the License. */ -// [START slides_create_presentation] +// [START apps_script_slides_create_presentation] /** * Create a new presentation. * @return {string} presentation Id. @@ -31,9 +31,9 @@ function createPresentation() { Logger.log('Failed with error %s', e.message); } } -// [END slides_create_presentation] +// [END apps_script_slides_create_presentation] -// [START slides_create_slide] +// [START apps_script_slides_create_slide] /** * Create a new slide. * @param {string} presentationId The presentation to add the slide to. @@ -63,9 +63,9 @@ function createSlide(presentationId) { Logger.log('Failed with error %s', e.message); } } -// [END slides_create_slide] +// [END apps_script_slides_create_slide] -// [START slides_read_page] +// [START apps_script_slides_read_page] /** * Read page element IDs. * @param {string} presentationId The presentation to read from. @@ -86,9 +86,9 @@ function readPageElementIds(presentationId, pageId) { Logger.log('Failed with error %s', e.message); } } -// [END slides_read_page] +// [END apps_script_slides_read_page] -// [START slides_add_text_box] +// [START apps_script_slides_add_text_box] /** * Add a new text box with text to a page. * @param {string} presentationId The presentation ID. @@ -144,9 +144,9 @@ function addTextBox(presentationId, pageId) { Logger.log('Failed with error %s', e.message); } } -// [END slides_add_text_box] +// [END apps_script_slides_add_text_box] -// [START slides_format_shape_text] +// [START apps_script_slides_format_shape_text] /** * Format the text in a shape. * @param {string} presentationId The presentation ID. @@ -188,9 +188,9 @@ function formatShapeText(presentationId, shapeId) { Logger.log('Failed with error %s', e.message); } } -// [END slides_format_shape_text] +// [END apps_script_slides_format_shape_text] -// [START slides_save_thumbnail] +// [START apps_script_slides_save_thumbnail] /** * Saves a thumbnail image of the current Google Slide presentation in Google Drive. * Logs the image URL. @@ -215,4 +215,4 @@ function saveThumbnailImage(i) { Logger.log('Failed with error %s', e.message); } } -// [END slides_save_thumbnail] +// [END apps_script_slides_save_thumbnail] diff --git a/apps-script/execute/target.js b/apps-script/execute/target.js index d0d7b67b8..aa79e72fc 100644 --- a/apps-script/execute/target.js +++ b/apps-script/execute/target.js @@ -1,3 +1,18 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ // [START apps_script_api_execute] /** * Return the set of folder names contained in the user's root folder as an From a59669544af7ecef74acf6e712a4426c61bb268e Mon Sep 17 00:00:00 2001 From: TrianguloY Date: Fri, 1 Apr 2022 21:32:14 +0200 Subject: [PATCH 08/99] Fix incorrect Google Developers channelId (#314) The snippet in https://developers.google.com/apps-script/advanced/youtube contains this sample code, as a way to subscribe to the Goggle Developers Youtube channel. When run, the account is subscribed to an unexpected channel: - Current, wrong: https://www.youtube.com/channel/UC9gFih9rw0zNCK3ZtoKQQyA (JennaMarbles) - Expected, correct: https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw (Google Developers) This error exists since at least 5 years ago when 9kopb notified it, but was ignored: https://github.com/googleworkspace/apps-script-samples/commit/499e6508653f6a620e34542f138d5b1a5aac69f2#r45916392 --- advanced/youtube.gs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/advanced/youtube.gs b/advanced/youtube.gs index c3e0dd8b3..9ba63a9ed 100644 --- a/advanced/youtube.gs +++ b/advanced/youtube.gs @@ -99,7 +99,7 @@ function retrieveMyUploads() { */ function addSubscription() { // Replace this channel ID with the channel ID you want to subscribe to - const channelId = 'UC9gFih9rw0zNCK3ZtoKQQyA'; + const channelId = 'UC_x5XG1OV2P6uZZ5FSM9Ttw'; const resource = { snippet: { resourceId: { From baf911638e9bd551eb18f6a4e1d2594f72441945 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Apr 2022 13:32:28 -0600 Subject: [PATCH 09/99] Update dependency eslint to v8.12.0 (#313) Co-authored-by: renovate[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e4e727de..8c38269fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "eslint": "8.11.0", + "eslint": "8.12.0", "eslint-config-google": "0.14.0", "eslint-plugin-async-await": "0.0.0", "eslint-plugin-googleappsscript": "1.0.4" @@ -249,9 +249,9 @@ } }, "node_modules/eslint": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz", - "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz", + "integrity": "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.2.1", @@ -1124,9 +1124,9 @@ "dev": true }, "eslint": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz", - "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz", + "integrity": "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==", "dev": true, "requires": { "@eslint/eslintrc": "^1.2.1", diff --git a/package.json b/package.json index ff92e3981..eed433321 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "API" ], "devDependencies": { - "eslint": "8.11.0", + "eslint": "8.12.0", "eslint-config-google": "0.14.0", "eslint-plugin-async-await": "0.0.0", "eslint-plugin-googleappsscript": "1.0.4" diff --git a/yarn.lock b/yarn.lock index cbf7a5d07..3b08c57c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -186,10 +186,10 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.11.0: - version "8.11.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz" - integrity sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA== +eslint@8.12.0: + version "8.12.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz" + integrity sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q== dependencies: "@eslint/eslintrc" "^1.2.1" "@humanwhocodes/config-array" "^0.9.2" From a36196268d66d934cb6464e97c61dd48925d59f7 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Tue, 5 Apr 2022 19:19:41 -0600 Subject: [PATCH 10/99] Enable snippet bot --- .github/snippet-bot.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/snippet-bot.yml diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml new file mode 100644 index 000000000..e69de29bb From 0efac264ceb76081722560c84ac16248f19b2164 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 11 Apr 2022 13:59:35 -0600 Subject: [PATCH 11/99] Add workflow to publish projects to apps script when merged. --- .github/scripts/clasp_push.sh | 39 ++++++++++++++++++++++++++++++++++ .github/workflows/publish.yaml | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100755 .github/scripts/clasp_push.sh create mode 100644 .github/workflows/publish.yaml diff --git a/.github/scripts/clasp_push.sh b/.github/scripts/clasp_push.sh new file mode 100755 index 000000000..f35eff8f9 --- /dev/null +++ b/.github/scripts/clasp_push.sh @@ -0,0 +1,39 @@ +#! /bin/bash +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +export LC_ALL=C.UTF-8 +export LANG=C.UTF-8 + +dirs=() + +IFS=$'\n' read -r -d '' -a dirs < <( find . -name 'build.gradle' -exec dirname '{}' \; | sort -u ) + +exit_code=0 + +for dir in "${dirs[@]}"; do + pushd "${dir}" || exit + clasp push -f + status=$? + if [ $status -ne 0 ]; then + exit_code=$status + fi + popd || exit +done + +if [ $exit_code -ne 0 ]; then + echo "Script push failed." +fi + +exit $exit_code \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 000000000..cd3d77b63 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,39 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +name: Publish Apps Script +on: + workflow_dispatch: + push: + branches: + - master +jobs: + lint: + concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: false + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3.0.0 + with: + fetch-depth: 0 + - name: Write test credentials + run: | + echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json" + env: + CLASP_CREDENTIALS: ${{secrets.CLASP_CREDENTIALS}} + - uses: actions/setup-node@v3 + with: + node-version: '14' + - run: npm install -g @google/clasp + - run: ./.github/scripts/clasp_push.sh From 355c7436da9943a0f679d9a0b4b595241bbf27cb Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 11 Apr 2022 14:10:57 -0600 Subject: [PATCH 12/99] Delint --- .github/workflows/lint.yaml | 1 + .github/workflows/publish.yaml | 1 + forms-api/demos/AppsScriptFormsAPIWebApp/README.md | 14 +++++++------- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index db03d2151..59841aebf 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +--- name: Lint on: workflow_dispatch: diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index cd3d77b63..8e2f41b46 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +--- name: Publish Apps Script on: workflow_dispatch: diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/README.md b/forms-api/demos/AppsScriptFormsAPIWebApp/README.md index 303d5cda2..71ae952cc 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/README.md +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/README.md @@ -4,15 +4,15 @@ This solution demonstrates how to interact with the new Google Forms API directl ## General setup -* Enable the Forms API for your Google Cloud project +* Enable the Forms API for your Google Cloud project ## Web app setup 1. Create a new blank Apps Script project. 1. Click **Project Settings**, then: - * Check **Show "appsscript.json" manifest file in editor**. - * Enter the project number of the Google Cloud project that has the + * Check **Show "appsscript.json" manifest file in editor**. + * Enter the project number of the Google Cloud project that has the Forms API enabled and click **Change project**. 1. Copy the contents of the Apps Script, HTML and JSON files into your @@ -20,13 +20,13 @@ This solution demonstrates how to interact with the new Google Forms API directl 1. Edit the `FormsAPI.gs` file to customize the constants. * `formId`: Choose a `formId` from an existing form. - * `topicName`: Optional, if using watches (pub/sub). + * `topicName`: Optional, if using watches (pub/sub). - Note: Further project setup is required to use the watch features. To - set up pub/sub topics, see + Note: Further project setup is required to use the watch features. To + set up pub/sub topics, see [Google Cloud Pubsub](https://cloud.google.com/pubsub/docs/building-pubsub-messaging-system) for additional details. -1. Deploy the project as a Web app, authorize access and click on the +1. Deploy the project as a Web app, authorize access and click on the deployment URL. From 0f05a1eadfe86d02287d0dac7db7b78a752278c7 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 11 Apr 2022 15:27:00 -0600 Subject: [PATCH 13/99] delint bare url in readme --- forms-api/snippets/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forms-api/snippets/README.md b/forms-api/snippets/README.md index 45799a1d1..1a6f81ce5 100644 --- a/forms-api/snippets/README.md +++ b/forms-api/snippets/README.md @@ -1,4 +1,4 @@ # Forms API To run, you must set up your GCP project to use the Forms API. -See: https://developers.google.com/forms/api/ +See: [Forms API](https://developers.google.com/forms/api/) From c4602f7bc2c9299155c589919a3c84ccae05245d Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 18 Apr 2022 14:30:48 -0600 Subject: [PATCH 14/99] Test syncing solutions source --- solutions/vacation-calendar/.clasp.json | 1 + solutions/vacation-calendar/Code.js | 179 ++++++++++++++++++++ solutions/vacation-calendar/appsscript.json | 7 + 3 files changed, 187 insertions(+) create mode 100644 solutions/vacation-calendar/.clasp.json create mode 100644 solutions/vacation-calendar/Code.js create mode 100644 solutions/vacation-calendar/appsscript.json diff --git a/solutions/vacation-calendar/.clasp.json b/solutions/vacation-calendar/.clasp.json new file mode 100644 index 000000000..b9162a3b0 --- /dev/null +++ b/solutions/vacation-calendar/.clasp.json @@ -0,0 +1 @@ +{"scriptId":"1jvPSSwJcuLzlDLDy2dr-qorjihiTNAW2H6B5k-dJxHjEPX6hMcNghzSh"} diff --git a/solutions/vacation-calendar/Code.js b/solutions/vacation-calendar/Code.js new file mode 100644 index 000000000..d9fbb8c35 --- /dev/null +++ b/solutions/vacation-calendar/Code.js @@ -0,0 +1,179 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/vacation-calendar + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Set the ID of the team calendar to add events to. You can find the calendar's +// ID on the settings page. +let TEAM_CALENDAR_ID = 'ENTER_TEAM_CALENDAR_ID_HERE'; +// Set the email address of the Google Group that contains everyone in the team. +// Ensure the group has less than 500 members to avoid timeouts. +let GROUP_EMAIL = 'ENTER_GOOGLE_GROUP_EMAIL_HERE'; + +let KEYWORDS = ['vacation', 'ooo', 'out of office', 'offline']; +let MONTHS_IN_ADVANCE = 3; + +/** + * Sets up the script to run automatically every hour. + */ +function setup() { + let triggers = ScriptApp.getProjectTriggers(); + if (triggers.length > 0) { + throw new Error('Triggers are already setup.'); + } + ScriptApp.newTrigger('sync').timeBased().everyHours(1).create(); + // Runs the first sync immediately. + sync(); +} + +/** + * Looks through the group members' public calendars and adds any + * 'vacation' or 'out of office' events to the team calendar. + */ +function sync() { + // Defines the calendar event date range to search. + let today = new Date(); + let maxDate = new Date(); + maxDate.setMonth(maxDate.getMonth() + MONTHS_IN_ADVANCE); + + // Determines the time the the script was last run. + let lastRun = PropertiesService.getScriptProperties().getProperty('lastRun'); + lastRun = lastRun ? new Date(lastRun) : null; + + // Gets the list of users in the Google Group. + let users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers(); + + // For each user, finds events having one or more of the keywords in the event + // summary in the specified date range. Imports each of those to the team + // calendar. + let count = 0; + users.forEach(function(user) { + let username = user.getEmail().split('@')[0]; + KEYWORDS.forEach(function(keyword) { + let events = findEvents(user, keyword, today, maxDate, lastRun); + events.forEach(function(event) { + importEvent(username, event); + count++; + }); // End foreach event. + }); // End foreach keyword. + }); // End foreach user. + + PropertiesService.getScriptProperties().setProperty('lastRun', today); + console.log('Imported ' + count + ' events'); +} + +/** + * Imports the given event from the user's calendar into the shared team + * calendar. + * @param {string} username The team member that is attending the event. + * @param {Calendar.Event} event The event to import. + */ +function importEvent(username, event) { + event.summary = '[' + username + '] ' + event.summary; + event.organizer = { + id: TEAM_CALENDAR_ID, + }; + event.attendees = []; + console.log('Importing: %s', event.summary); + try { + Calendar.Events.import(event, TEAM_CALENDAR_ID); + } catch (e) { + console.error('Error attempting to import event: %s. Skipping.', + e.toString()); + } +} + +/** + * In a given user's calendar, looks for occurrences of the given keyword + * in events within the specified date range and returns any such events + * found. + * @param {Session.User} user The user to retrieve events for. + * @param {string} keyword The keyword to look for. + * @param {Date} start The starting date of the range to examine. + * @param {Date} end The ending date of the range to examine. + * @param {Date} optSince A date indicating the last time this script was run. + * @return {Calendar.Event[]} An array of calendar events. + */ +function findEvents(user, keyword, start, end, optSince) { + let params = { + q: keyword, + timeMin: formatDateAsRFC3339(start), + timeMax: formatDateAsRFC3339(end), + showDeleted: true, + }; + if (optSince) { + // This prevents the script from examining events that have not been + // modified since the specified date (that is, the last time the + // script was run). + params.updatedMin = formatDateAsRFC3339(optSince); + } + let pageToken = null; + let events = []; + do { + params.pageToken = pageToken; + let response; + try { + response = Calendar.Events.list(user.getEmail(), params); + } catch (e) { + console.error('Error retriving events for %s, %s: %s; skipping', + user, keyword, e.toString()); + continue; + } + events = events.concat(response.items.filter(function(item) { + return shoudImportEvent(user, keyword, item); + })); + pageToken = response.nextPageToken; + } while (pageToken); + return events; +} + +/** + * Determines if the given event should be imported into the shared team + * calendar. + * @param {Session.User} user The user that is attending the event. + * @param {string} keyword The keyword being searched for. + * @param {Calendar.Event} event The event being considered. + * @return {boolean} True if the event should be imported. + */ +function shoudImportEvent(user, keyword, event) { + // Filters out events where the keyword did not appear in the summary + // (that is, the keyword appeared in a different field, and are thus + // is not likely to be relevant). + if (event.summary.toLowerCase().indexOf(keyword) < 0) { + return false; + } + if (!event.organizer || event.organizer.email == user.getEmail()) { + // If the user is the creator of the event, always imports it. + return true; + } + // Only imports events the user has accepted. + if (!event.attendees) return false; + let matching = event.attendees.filter(function(attendee) { + return attendee.self; + }); + return matching.length > 0 && matching[0].responseStatus == 'accepted'; +} + +/** + * Returns an RFC3339 formated date String corresponding to the given + * Date object. + * @param {Date} date a Date. + * @return {string} a formatted date string. + */ +function formatDateAsRFC3339(date) { + return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ'); +} diff --git a/solutions/vacation-calendar/appsscript.json b/solutions/vacation-calendar/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/vacation-calendar/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file From a86052465d990968d623eecb4d0645ebc33aa0fb Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 18 Apr 2022 14:49:19 -0600 Subject: [PATCH 15/99] Fix filename search in clasp script --- .github/scripts/clasp_push.sh | 2 +- .github/workflows/publish.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/clasp_push.sh b/.github/scripts/clasp_push.sh index f35eff8f9..b3dc9852e 100755 --- a/.github/scripts/clasp_push.sh +++ b/.github/scripts/clasp_push.sh @@ -18,7 +18,7 @@ export LANG=C.UTF-8 dirs=() -IFS=$'\n' read -r -d '' -a dirs < <( find . -name 'build.gradle' -exec dirname '{}' \; | sort -u ) +IFS=$'\n' read -r -d '' -a dirs < <( find . -name '.clasp.json' -exec dirname '{}' \; | sort -u ) exit_code=0 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 8e2f41b46..4a7277e41 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -19,7 +19,7 @@ on: branches: - master jobs: - lint: + publish: concurrency: group: ${{ github.head_ref || github.ref }} cancel-in-progress: false From 006b9146694ebb42b66e3de718a0e4a4b7367ded Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 18 Apr 2022 15:16:58 -0600 Subject: [PATCH 16/99] Add news sentiment solution + reorg solutions to mirror drive --- .github/scripts/clasp_push.sh | 1 + .../automations/news-sentiment/.clasp.json | 1 + solutions/automations/news-sentiment/Code.js | 256 ++++++++++++++++++ .../news-sentiment}/appsscript.json | 0 .../vacation-calendar/.clasp.json | 0 .../vacation-calendar/Code.js | 0 .../vacation-calendar/appsscript.json | 7 + 7 files changed, 265 insertions(+) create mode 100644 solutions/automations/news-sentiment/.clasp.json create mode 100644 solutions/automations/news-sentiment/Code.js rename solutions/{vacation-calendar => automations/news-sentiment}/appsscript.json (100%) rename solutions/{ => automations}/vacation-calendar/.clasp.json (100%) rename solutions/{ => automations}/vacation-calendar/Code.js (100%) create mode 100644 solutions/automations/vacation-calendar/appsscript.json diff --git a/.github/scripts/clasp_push.sh b/.github/scripts/clasp_push.sh index b3dc9852e..cef52f94d 100755 --- a/.github/scripts/clasp_push.sh +++ b/.github/scripts/clasp_push.sh @@ -24,6 +24,7 @@ exit_code=0 for dir in "${dirs[@]}"; do pushd "${dir}" || exit + echo "Publishing ${dir}" clasp push -f status=$? if [ $status -ne 0 ]; then diff --git a/solutions/automations/news-sentiment/.clasp.json b/solutions/automations/news-sentiment/.clasp.json new file mode 100644 index 000000000..80b47c971 --- /dev/null +++ b/solutions/automations/news-sentiment/.clasp.json @@ -0,0 +1 @@ +{"scriptId":"1KHPvTOwE2pd2myZmvX0mbsp8SPlhJBFotNCwflZiP01xmTasNfibG4zl"} \ No newline at end of file diff --git a/solutions/automations/news-sentiment/Code.js b/solutions/automations/news-sentiment/Code.js new file mode 100644 index 000000000..7d084f907 --- /dev/null +++ b/solutions/automations/news-sentiment/Code.js @@ -0,0 +1,256 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/news-sentiment + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Global variables +const googleAPIKey = 'YOUR_GOOGLE_API_KEY'; +const newsApiKey = 'YOUR_NEWS_API_KEY'; +const apiEndPointHdr = 'https://newsapi.org/v2/everything?q='; +const happyFace = + '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/24/smiley-1635449_1280.png\")'; +const mehFace = + '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/24/smiley-1635450_1280.png\")'; +const sadFace = + '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/25/smiley-1635454_1280.png\")'; +const happyColor = '#44f83d'; +const mehColor = '#f7f6cc'; +const sadColor = '#ff3c3d'; +const fullsheet = 'A2:D25'; +const sentimentCols = 'B2:D25'; +const articleMax = 20; +const threshold = 0.3; + +let headlines = []; +let rows = null; +let rowValues = null; +let topic = null; +let bottomRow = 0; +let ds = null; +let ss = null; +let headerRow = null; +let sentimentCol = null; +let headlineCol = null; +let scoreCol = null; + +/** + * Creates menu in the Google Sheets spreadsheet when the spreadsheet is opened. + * + */ +function onOpen() { + let ui = SpreadsheetApp.getUi(); + ui.createMenu('News Headlines Sentiments') + .addItem('Analyze News Headlines...', 'showNewsPrompt') + .addToUi(); +} + +/** + * Prompts user to enter a new headline topic. + * Calls main function AnalyzeHeadlines with entered topic. + */ +function showNewsPrompt() { + //Initializes global variables + ss = SpreadsheetApp.getActiveSpreadsheet(); + ds = ss.getSheetByName('Sheet1'); + headerRow = ds.getDataRange().getValues()[0]; + sentimentCol = headerRow.indexOf('Sentiment'); + headlineCol = headerRow.indexOf('Headlines'); + scoreCol = headerRow.indexOf('Score'); + + // Builds Menu + let ui = SpreadsheetApp.getUi(); + let result = ui.prompt( + 'Enter news topic:', + ui.ButtonSet.OK_CANCEL); + + // Processes the user's response. + let button = result.getSelectedButton(); + topic = result.getResponseText(); + if (button == ui.Button.OK) { + analyzeNewsHeadlines(); + } else if (button == ui.Button.CANCEL) { + // Shows alert if user clicked "Cancel." + ui.alert('News topic not selected!'); + } +} + +/** + * For each headline cell, calls the Natural Language API to get general sentiment and then updates + * the sentiment response column. + */ +function analyzeNewsHeadlines() { + // Clears and reformats the sheet + reformatSheet(); + + // Gets the headlines array + headlines = getHeadlinesArray(); + + // Syncs the headlines array to the sheet using a single setValues call + if (headlines.length > 0){ + ds.getRange(2, 1, headlines.length, headlineCol+1).setValues(headlines); + // Set global rowValues + rows = ds.getDataRange(); + rowValues = rows.getValues(); + getSentiments(); + } else { + ss.toast("No headlines returned for topic: " + topic + '!'); + } +} + +/** + * Fetches current headlines from the Free News API + */ +function getHeadlinesArray() { + // Fetches headlines for a given topic + let hdlnsResp = []; + let encodedtopic = encodeURIComponent(topic); + ss.toast("Getting headlines for: " + topic); + let response = UrlFetchApp.fetch(apiEndPointHdr + encodedtopic + '&apiKey=' + + newsApiKey); + let results = JSON.parse(response); + let articles = results["articles"]; + + for (let i = 0; i < articles.length && i < articleMax; i++) { + let newsStory = articles[i]['title']; + if (articles[i]['description'] !== null) { + newsStory += ': ' + articles[i]['description']; + } + // Scrubs newsStory of invalid characters + newsStory = scrub(newsStory); + + // Constructs hdlnsResp as a 2d array. This simplifies syncing to the sheet. + hdlnsResp.push(new Array(newsStory)); + } + + return hdlnsResp; +} + +/** + * For each article cell, calls the Natural Language API to get general sentiment and then updates + * the sentiment response columns. + */ +function getSentiments() { + ss.toast('Analyzing the headline sentiments...'); + + let articleCount = rows.getNumRows() - 1; + let avg = 0; + + // Gets sentiment for each row + for (let i = 1; i <= articleCount; i++) { + let headlineCell = rowValues[i][headlineCol]; + if (headlineCell) { + let sentimentData = retrieveSentiment(headlineCell); + let result = sentimentData['documentSentiment']['score']; + avg += result; + ds.getRange(i + 1, sentimentCol + 1).setBackgroundColor(getColor(result)); + ds.getRange(i + 1, sentimentCol + 1).setValue(getFace(result)); + ds.getRange(i + 1, scoreCol + 1).setValue(result); + } + } + let avgDecimal = (avg / articleCount).toFixed(2); + + // Shows news topic and average face, color and sentiment value. + bottomRow = articleCount + 3; + ds.getRange(bottomRow, 1, headlines.length, scoreCol+1).setFontWeight('bold'); + ds.getRange(bottomRow, headlineCol + 1).setValue('Topic: \"' + topic + '\"'); + ds.getRange(bottomRow, headlineCol + 2).setValue('Avg:'); + ds.getRange(bottomRow, sentimentCol + 1).setValue(getFace(avgDecimal)); + ds.getRange(bottomRow, sentimentCol + 1).setBackgroundColor(getColor(avgDecimal)); + ds.getRange(bottomRow, scoreCol + 1).setValue(avgDecimal); + ss.toast("Done!!"); +} + +/** + * Calls the Natureal Language API to get sentiment response for headline. + * + * Important note: Not all languages are supported by Google document + * sentiment analysis. + * Unsupported languages generate a "400" response: "INVALID_ARGUMENT". + */ +function retrieveSentiment(text) { + // Sets REST call options + let apiEndPoint = + 'https://language.googleapis.com/v1/documents:analyzeSentiment?key=' + + googleAPIKey; + let jsonReq = JSON.stringify({ + document: { + type: "PLAIN_TEXT", + content: text + }, + encodingType: "UTF8" + }); + + let options = { + 'method': 'post', + 'contentType': 'application/json', + 'payload': jsonReq + } + + // Makes the REST call + let response = UrlFetchApp.fetch(apiEndPoint, options); + let responseData = JSON.parse(response); + return responseData; +} + +// Helper Functions + +/** + * Removes old headlines, sentiments and reset formatting + */ +function reformatSheet() { + let range = ds.getRange(fullsheet); + range.clearContent(); + range.clearFormat(); + range.setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP); + + range = ds.getRange(sentimentCols); // Center the sentiment cols only + range.setHorizontalAlignment("center"); +} + +/** + * Returns a corresponding face based on numeric value. + */ +function getFace(value){ + if (value >= threshold) { + return happyFace; + } else if (value < threshold && value > -threshold){ + return mehFace; + } else if (value <= -threshold) { + return sadFace; + } +} + +/** + * Returns a corresponding color based on numeric value. + */ +function getColor(value){ + if (value >= threshold) { + return happyColor; + } else if (value < threshold && value > -threshold){ + return mehColor; + } else if (value <= -threshold) { + return sadColor; + } +} + +/** + * Scrubs invalid characters out of headline text. + * Can be expanded if needed. + */ +function scrub(text) { + return text.replace(/[\‘\,\“\”\"\'\’\-\n\â\€]/g, ' '); +} \ No newline at end of file diff --git a/solutions/vacation-calendar/appsscript.json b/solutions/automations/news-sentiment/appsscript.json similarity index 100% rename from solutions/vacation-calendar/appsscript.json rename to solutions/automations/news-sentiment/appsscript.json diff --git a/solutions/vacation-calendar/.clasp.json b/solutions/automations/vacation-calendar/.clasp.json similarity index 100% rename from solutions/vacation-calendar/.clasp.json rename to solutions/automations/vacation-calendar/.clasp.json diff --git a/solutions/vacation-calendar/Code.js b/solutions/automations/vacation-calendar/Code.js similarity index 100% rename from solutions/vacation-calendar/Code.js rename to solutions/automations/vacation-calendar/Code.js diff --git a/solutions/automations/vacation-calendar/appsscript.json b/solutions/automations/vacation-calendar/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/vacation-calendar/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file From 06cf5f2eaf7b696178baeffedcf409333500b3d7 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 18 Apr 2022 15:39:07 -0600 Subject: [PATCH 17/99] Add feedback analysis solution + readme files --- .../automations/feedback-analysis/.clasp.json | 1 + .../automations/feedback-analysis/README.md | 3 + .../feedback-analysis/appsscript.json | 12 ++ .../automations/feedback-analysis/code.js | 133 ++++++++++++++++++ .../automations/news-sentiment/README.md | 3 + .../automations/vacation-calendar/README.md | 3 + 6 files changed, 155 insertions(+) create mode 100644 solutions/automations/feedback-analysis/.clasp.json create mode 100644 solutions/automations/feedback-analysis/README.md create mode 100644 solutions/automations/feedback-analysis/appsscript.json create mode 100644 solutions/automations/feedback-analysis/code.js create mode 100644 solutions/automations/news-sentiment/README.md create mode 100644 solutions/automations/vacation-calendar/README.md diff --git a/solutions/automations/feedback-analysis/.clasp.json b/solutions/automations/feedback-analysis/.clasp.json new file mode 100644 index 000000000..92fa17d1d --- /dev/null +++ b/solutions/automations/feedback-analysis/.clasp.json @@ -0,0 +1 @@ +{"scriptId":"1LOheMLQDlSkvmlt8EQOGGETewdt8tKWyzxspCwqzfianqxTXjBGpAc8c"} \ No newline at end of file diff --git a/solutions/automations/feedback-analysis/README.md b/solutions/automations/feedback-analysis/README.md new file mode 100644 index 000000000..c2201845d --- /dev/null +++ b/solutions/automations/feedback-analysis/README.md @@ -0,0 +1,3 @@ +# Analyze sentiment of open-ended feedback + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/feedback-sentiment-analysis) for additional details. \ No newline at end of file diff --git a/solutions/automations/feedback-analysis/appsscript.json b/solutions/automations/feedback-analysis/appsscript.json new file mode 100644 index 000000000..cc6038b7a --- /dev/null +++ b/solutions/automations/feedback-analysis/appsscript.json @@ -0,0 +1,12 @@ +{ + "timeZone": "America/Los_Angeles", + "dependencies": { + "libraries": [{ + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "24" + }] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/feedback-analysis/code.js b/solutions/automations/feedback-analysis/code.js new file mode 100644 index 000000000..b9ce619b1 --- /dev/null +++ b/solutions/automations/feedback-analysis/code.js @@ -0,0 +1,133 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/feedback-sentiment-analysis + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Sets API key for accessing Cloud Natural Language API. +const myApiKey = 'YOUR_API_KEY'; // Replace with your API key. + +// Matches column names in Review Data sheet to variables. +let COLUMN_NAME = { + COMMENTS: 'comments', + ENTITY: 'entity_sentiment', + ID: 'id' +}; + +/** + * Creates a Demo menu in Google Spreadsheets. + */ +function onOpen() { + SpreadsheetApp.getUi() + .createMenu('Sentiment Tools') + .addItem('Mark entities and sentiment', 'markEntitySentiment') + .addToUi(); +}; + +/** +* Analyzes entities and sentiment for each comment in +* Review Data sheet and copies results into the +* Entity Sentiment Data sheet. +*/ +function markEntitySentiment() { + // Sets variables for "Review Data" sheet + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let dataSheet = ss.getSheetByName('Review Data'); + let rows = dataSheet.getDataRange(); + let numRows = rows.getNumRows(); + let values = rows.getValues(); + let headerRow = values[0]; + + // Checks to see if "Entity Sentiment Data" sheet is present, and + // if not, creates a new sheet and sets the header row. + let entitySheet = ss.getSheetByName('Entity Sentiment Data'); + if (entitySheet == null) { + ss.insertSheet('Entity Sentiment Data'); + let entitySheet = ss.getSheetByName('Entity Sentiment Data'); + let esHeaderRange = entitySheet.getRange(1,1,1,6); + let esHeader = [['Review ID','Entity','Salience','Sentiment Score', + 'Sentiment Magnitude','Number of mentions']]; + esHeaderRange.setValues(esHeader); + }; + + // Finds the column index for comments, language_detected, + // and comments_english columns. + let textColumnIdx = headerRow.indexOf(COLUMN_NAME.COMMENTS); + let entityColumnIdx = headerRow.indexOf(COLUMN_NAME.ENTITY); + let idColumnIdx = headerRow.indexOf(COLUMN_NAME.ID); + if (entityColumnIdx == -1) { + Browser.msgBox("Error: Could not find the column named " + COLUMN_NAME.ENTITY + + ". Please create an empty column with header \"entity_sentiment\" on the Review Data tab."); + return; // bail + }; + + ss.toast("Analyzing entities and sentiment..."); + for (let i = 0; i < numRows; ++i) { + let value = values[i]; + let commentEnCellVal = value[textColumnIdx]; + let entityCellVal = value[entityColumnIdx]; + let reviewId = value[idColumnIdx]; + + // Calls retrieveEntitySentiment function for each row that has a comment + // and also an empty entity_sentiment cell value. + if(commentEnCellVal && !entityCellVal) { + let nlData = retrieveEntitySentiment(commentEnCellVal); + // Pastes each entity and sentiment score into Entity Sentiment Data sheet. + let newValues = [] + for (let entity in nlData.entities) { + entity = nlData.entities [entity]; + let row = [reviewId, entity.name, entity.salience, entity.sentiment.score, + entity.sentiment.magnitude, entity.mentions.length + ]; + newValues.push(row); + } + if(newValues.length) { + entitySheet.getRange(entitySheet.getLastRow() + 1, 1, newValues.length, newValues[0].length).setValues(newValues); + } + // Pastes "complete" into entity_sentiment column to denote completion of NL API call. + dataSheet.getRange(i+1, entityColumnIdx+1).setValue("complete"); + } + } +}; + +/** + * Calls the Cloud Natural Language API with a string of text to analyze + * entities and sentiment present in the string. + * @param {String} the string for entity sentiment analysis + * @return {Object} the entities and related sentiment present in the string + */ +function retrieveEntitySentiment (line) { + let apiKey = myApiKey; + let apiEndpoint = 'https://language.googleapis.com/v1/documents:analyzeEntitySentiment?key=' + apiKey; + // Creates a JSON request, with text string, language, type and encoding + let nlData = { + document: { + language: 'en-us', + type: 'PLAIN_TEXT', + content: line + }, + encodingType: 'UTF8' + }; + // Packages all of the options and the data together for the API call. + let nlOptions = { + method : 'post', + contentType: 'application/json', + payload : JSON.stringify(nlData) + }; + // Makes the API call. + let response = UrlFetchApp.fetch(apiEndpoint, nlOptions); + return JSON.parse(response); +}; \ No newline at end of file diff --git a/solutions/automations/news-sentiment/README.md b/solutions/automations/news-sentiment/README.md new file mode 100644 index 000000000..72864f2e0 --- /dev/null +++ b/solutions/automations/news-sentiment/README.md @@ -0,0 +1,3 @@ +# Connect to an external API: Analyze news headlines + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/news-sentiment) for additional details. \ No newline at end of file diff --git a/solutions/automations/vacation-calendar/README.md b/solutions/automations/vacation-calendar/README.md new file mode 100644 index 000000000..2b2a99067 --- /dev/null +++ b/solutions/automations/vacation-calendar/README.md @@ -0,0 +1,3 @@ +# Populate a team vacation calendar + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/vacation-calendar) for additional details. \ No newline at end of file From 354ca3fd8bbaf5783dd36c33160fae3ee80f8c49 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 18 Apr 2022 18:36:19 -0600 Subject: [PATCH 18/99] Add filtering for publish command to only push changed projects (experimental) --- .github/scripts/clasp_push.sh | 18 +++++++++++++++--- .github/workflows/publish.yaml | 5 ++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/scripts/clasp_push.sh b/.github/scripts/clasp_push.sh index cef52f94d..03691ae59 100755 --- a/.github/scripts/clasp_push.sh +++ b/.github/scripts/clasp_push.sh @@ -16,21 +16,33 @@ export LC_ALL=C.UTF-8 export LANG=C.UTF-8 +function contains_changes() { + [[ "${@:2}" = "" ]] && return 0 + for f in ${@:2}; do + case $(realpath $f)/ in + "$(realpath $1)"/*) return 0;; + esac + done + return 1 +} + +changed_files=$(echo "${@:1}" | xargs realpath | xargs -I {} dirname {}| sort -u | uniq) dirs=() -IFS=$'\n' read -r -d '' -a dirs < <( find . -name '.clasp.json' -exec dirname '{}' \; | sort -u ) +IFS=$'\n' read -r -d '' -a dirs < <( find . -name '.clasp.json' -exec dirname '{}' \; | sort -u | xargs realpath ) exit_code=0 for dir in "${dirs[@]}"; do - pushd "${dir}" || exit + pushd "${dir}" > /dev/null || exit + contains_changes "$dir" "${changed_files[@]}" || continue echo "Publishing ${dir}" clasp push -f status=$? if [ $status -ne 0 ]; then exit_code=$status fi - popd || exit + popd > /dev/null || exit done if [ $exit_code -ne 0 ]; then diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 4a7277e41..cfef946a3 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -28,6 +28,9 @@ jobs: - uses: actions/checkout@v3.0.0 with: fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v18.7 - name: Write test credentials run: | echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json" @@ -37,4 +40,4 @@ jobs: with: node-version: '14' - run: npm install -g @google/clasp - - run: ./.github/scripts/clasp_push.sh + - run: ./.github/scripts/clasp_push.sh ${{ steps.changed-files.outputs.all_changed_files }} From 114bbeca4d172ccaeb6142ad4ed5893b2a02f760 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 18 Apr 2022 18:39:36 -0600 Subject: [PATCH 19/99] delint --- solutions/automations/feedback-analysis/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solutions/automations/feedback-analysis/README.md b/solutions/automations/feedback-analysis/README.md index c2201845d..e06d1e851 100644 --- a/solutions/automations/feedback-analysis/README.md +++ b/solutions/automations/feedback-analysis/README.md @@ -1,3 +1,3 @@ -# Analyze sentiment of open-ended feedback +# Analyze sentiment of open-ended feedback See [developers.google.com](https://developers.google.com/apps-script/samples/automations/feedback-sentiment-analysis) for additional details. \ No newline at end of file From d9c5d782af79657dbb2f2264f524e4fc2bc21a7b Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 18 Apr 2022 18:40:42 -0600 Subject: [PATCH 20/99] delint --- solutions/automations/news-sentiment/README.md | 2 +- solutions/automations/vacation-calendar/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/solutions/automations/news-sentiment/README.md b/solutions/automations/news-sentiment/README.md index 72864f2e0..14e247a6c 100644 --- a/solutions/automations/news-sentiment/README.md +++ b/solutions/automations/news-sentiment/README.md @@ -1,3 +1,3 @@ -# Connect to an external API: Analyze news headlines +# Connect to an external API: Analyze news headlines See [developers.google.com](https://developers.google.com/apps-script/samples/automations/news-sentiment) for additional details. \ No newline at end of file diff --git a/solutions/automations/vacation-calendar/README.md b/solutions/automations/vacation-calendar/README.md index 2b2a99067..012f9e3e0 100644 --- a/solutions/automations/vacation-calendar/README.md +++ b/solutions/automations/vacation-calendar/README.md @@ -1,3 +1,3 @@ -# Populate a team vacation calendar +# Populate a team vacation calendar See [developers.google.com](https://developers.google.com/apps-script/samples/automations/vacation-calendar) for additional details. \ No newline at end of file From 76ab5eaed38f2b6192a3f06df581b96e1857295f Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Thu, 21 Apr 2022 11:17:16 -0600 Subject: [PATCH 21/99] Add automation samples from docs --- .../automations/agenda-maker/.clasp.json | 1 + solutions/automations/agenda-maker/Code.js | 192 ++++++++ solutions/automations/agenda-maker/README.md | 3 + .../automations/agenda-maker/appsscript.json | 7 + .../aggregate-document-content/.clasp.json | 1 + .../aggregate-document-content/Code.js | 176 +++++++ .../aggregate-document-content/Menu.js | 43 ++ .../aggregate-document-content/README.md | 3 + .../aggregate-document-content/Setup.js | 149 ++++++ .../aggregate-document-content/Utilities.js | 44 ++ .../appsscript.json | 7 + .../calendar-timesheet/.clasp.json | 1 + .../automations/calendar-timesheet/Code.js | 363 ++++++++++++++ .../automations/calendar-timesheet/Page.html | 215 ++++++++ .../automations/calendar-timesheet/README.md | 3 + .../calendar-timesheet/appsscript.json | 7 + .../automations/content-signup/.clasp.json | 1 + solutions/automations/content-signup/Code.js | 129 +++++ .../automations/content-signup/README.md | 3 + .../content-signup/appsscript.json | 14 + .../course-feedback-response/.clasp.json | 1 + .../course-feedback-response/Code.js | 119 +++++ .../course-feedback-response/README.md | 3 + .../course-feedback-response/appsscript.json | 7 + .../employee-certificate/.clasp.json | 1 + .../automations/employee-certificate/Code.js | 3 + .../employee-certificate/README.md | 3 + .../employee-certificate/appsscript.json | 7 + .../equipment-requests/.clasp.json | 1 + .../automations/equipment-requests/Code.js | 212 ++++++++ .../automations/equipment-requests/README.md | 3 + .../equipment-requests/appsscript.json | 7 + .../new-equipment-request.html | 19 + .../equipment-requests/request-complete.html | 13 + .../event-session-signup/.clasp.json | 1 + .../automations/event-session-signup/Code.js | 209 ++++++++ .../event-session-signup/README.md | 3 + .../event-session-signup/appsscript.json | 7 + .../.clasp.json | 0 .../README.md | 0 .../appsscript.json | 0 .../code.js | 0 .../automations/generate-pdfs/.clasp.json | 1 + solutions/automations/generate-pdfs/Code.js | 263 ++++++++++ solutions/automations/generate-pdfs/Menu.js | 24 + solutions/automations/generate-pdfs/README.md | 3 + .../automations/generate-pdfs/Utilities.js | 44 ++ .../automations/generate-pdfs/appsscript.json | 7 + .../automations/import-csv-sheets/.clasp.json | 1 + .../automations/import-csv-sheets/Code.js | 191 ++++++++ .../automations/import-csv-sheets/README.md | 3 + .../import-csv-sheets/SampleData.js | 174 +++++++ .../import-csv-sheets/SetupSample.js | 95 ++++ .../import-csv-sheets/Utilities.js | 126 +++++ .../import-csv-sheets/appsscript.json | 7 + .../offsite-activity-signup/.clasp.json | 1 + .../offsite-activity-signup/Code.js | 458 ++++++++++++++++++ .../offsite-activity-signup/README.md | 3 + .../offsite-activity-signup/appsscript.json | 7 + .../tax-loss-harvest-alerts/.clasp.json | 1 + .../tax-loss-harvest-alerts/Code.js | 72 +++ .../tax-loss-harvest-alerts/README.md | 3 + .../tax-loss-harvest-alerts/appsscript.json | 7 + solutions/automations/timesheets/.clasp.json | 1 + solutions/automations/timesheets/Code.js | 264 ++++++++++ solutions/automations/timesheets/README.md | 3 + .../automations/timesheets/appsscript.json | 7 + .../automations/upload-files/.clasp.json | 1 + solutions/automations/upload-files/Code.js | 110 +++++ solutions/automations/upload-files/README.md | 3 + solutions/automations/upload-files/Setup.js | 100 ++++ .../automations/upload-files/appsscript.json | 7 + .../automations/youtube-tracker/.clasp.json | 1 + solutions/automations/youtube-tracker/Code.js | 126 +++++ .../automations/youtube-tracker/README.md | 3 + .../youtube-tracker/appsscript.json | 7 + .../automations/youtube-tracker/email.html | 13 + 77 files changed, 4118 insertions(+) create mode 100644 solutions/automations/agenda-maker/.clasp.json create mode 100644 solutions/automations/agenda-maker/Code.js create mode 100644 solutions/automations/agenda-maker/README.md create mode 100644 solutions/automations/agenda-maker/appsscript.json create mode 100644 solutions/automations/aggregate-document-content/.clasp.json create mode 100644 solutions/automations/aggregate-document-content/Code.js create mode 100644 solutions/automations/aggregate-document-content/Menu.js create mode 100644 solutions/automations/aggregate-document-content/README.md create mode 100644 solutions/automations/aggregate-document-content/Setup.js create mode 100644 solutions/automations/aggregate-document-content/Utilities.js create mode 100644 solutions/automations/aggregate-document-content/appsscript.json create mode 100644 solutions/automations/calendar-timesheet/.clasp.json create mode 100644 solutions/automations/calendar-timesheet/Code.js create mode 100644 solutions/automations/calendar-timesheet/Page.html create mode 100644 solutions/automations/calendar-timesheet/README.md create mode 100644 solutions/automations/calendar-timesheet/appsscript.json create mode 100644 solutions/automations/content-signup/.clasp.json create mode 100644 solutions/automations/content-signup/Code.js create mode 100644 solutions/automations/content-signup/README.md create mode 100644 solutions/automations/content-signup/appsscript.json create mode 100644 solutions/automations/course-feedback-response/.clasp.json create mode 100644 solutions/automations/course-feedback-response/Code.js create mode 100644 solutions/automations/course-feedback-response/README.md create mode 100644 solutions/automations/course-feedback-response/appsscript.json create mode 100644 solutions/automations/employee-certificate/.clasp.json create mode 100644 solutions/automations/employee-certificate/Code.js create mode 100644 solutions/automations/employee-certificate/README.md create mode 100644 solutions/automations/employee-certificate/appsscript.json create mode 100644 solutions/automations/equipment-requests/.clasp.json create mode 100644 solutions/automations/equipment-requests/Code.js create mode 100644 solutions/automations/equipment-requests/README.md create mode 100644 solutions/automations/equipment-requests/appsscript.json create mode 100644 solutions/automations/equipment-requests/new-equipment-request.html create mode 100644 solutions/automations/equipment-requests/request-complete.html create mode 100644 solutions/automations/event-session-signup/.clasp.json create mode 100644 solutions/automations/event-session-signup/Code.js create mode 100644 solutions/automations/event-session-signup/README.md create mode 100644 solutions/automations/event-session-signup/appsscript.json rename solutions/automations/{feedback-analysis => feedback-sentiment-analysis}/.clasp.json (100%) rename solutions/automations/{feedback-analysis => feedback-sentiment-analysis}/README.md (100%) rename solutions/automations/{feedback-analysis => feedback-sentiment-analysis}/appsscript.json (100%) rename solutions/automations/{feedback-analysis => feedback-sentiment-analysis}/code.js (100%) create mode 100644 solutions/automations/generate-pdfs/.clasp.json create mode 100644 solutions/automations/generate-pdfs/Code.js create mode 100644 solutions/automations/generate-pdfs/Menu.js create mode 100644 solutions/automations/generate-pdfs/README.md create mode 100644 solutions/automations/generate-pdfs/Utilities.js create mode 100644 solutions/automations/generate-pdfs/appsscript.json create mode 100644 solutions/automations/import-csv-sheets/.clasp.json create mode 100644 solutions/automations/import-csv-sheets/Code.js create mode 100644 solutions/automations/import-csv-sheets/README.md create mode 100644 solutions/automations/import-csv-sheets/SampleData.js create mode 100644 solutions/automations/import-csv-sheets/SetupSample.js create mode 100644 solutions/automations/import-csv-sheets/Utilities.js create mode 100644 solutions/automations/import-csv-sheets/appsscript.json create mode 100644 solutions/automations/offsite-activity-signup/.clasp.json create mode 100644 solutions/automations/offsite-activity-signup/Code.js create mode 100644 solutions/automations/offsite-activity-signup/README.md create mode 100644 solutions/automations/offsite-activity-signup/appsscript.json create mode 100644 solutions/automations/tax-loss-harvest-alerts/.clasp.json create mode 100644 solutions/automations/tax-loss-harvest-alerts/Code.js create mode 100644 solutions/automations/tax-loss-harvest-alerts/README.md create mode 100644 solutions/automations/tax-loss-harvest-alerts/appsscript.json create mode 100644 solutions/automations/timesheets/.clasp.json create mode 100644 solutions/automations/timesheets/Code.js create mode 100644 solutions/automations/timesheets/README.md create mode 100644 solutions/automations/timesheets/appsscript.json create mode 100644 solutions/automations/upload-files/.clasp.json create mode 100644 solutions/automations/upload-files/Code.js create mode 100644 solutions/automations/upload-files/README.md create mode 100644 solutions/automations/upload-files/Setup.js create mode 100644 solutions/automations/upload-files/appsscript.json create mode 100644 solutions/automations/youtube-tracker/.clasp.json create mode 100644 solutions/automations/youtube-tracker/Code.js create mode 100644 solutions/automations/youtube-tracker/README.md create mode 100644 solutions/automations/youtube-tracker/appsscript.json create mode 100644 solutions/automations/youtube-tracker/email.html diff --git a/solutions/automations/agenda-maker/.clasp.json b/solutions/automations/agenda-maker/.clasp.json new file mode 100644 index 000000000..4b9783746 --- /dev/null +++ b/solutions/automations/agenda-maker/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "147xVWUWmw8b010zbiDMIa3eeKATo3P2q5rJCZmY3meirC-yA_XucdZlp"} diff --git a/solutions/automations/agenda-maker/Code.js b/solutions/automations/agenda-maker/Code.js new file mode 100644 index 000000000..fa2bb7a95 --- /dev/null +++ b/solutions/automations/agenda-maker/Code.js @@ -0,0 +1,192 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/agenda-maker + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Checks if the folder for Agenda docs exists, and creates it if it doesn't. + * + * @return {*} Drive folder ID for the app. + */ +function checkFolder() { + const folders = DriveApp.getFoldersByName('Agenda Maker - App'); + // Finds the folder if it exists + while (folders.hasNext()) { + let folder = folders.next(); + if ( + folder.getDescription() == + 'Apps Script App - Do not change this description' && + folder.getOwner().getEmail() == Session.getActiveUser().getEmail() + ) { + return folder.getId(); + } + } + // If the folder doesn't exist, creates one + let folder = DriveApp.createFolder('Agenda Maker - App'); + folder.setDescription('Apps Script App - Do not change this description'); + return folder.getId(); +} + +/** + * Finds the template agenda doc, or creates one if it doesn't exist. + */ +function getTemplateId(folderId) { + const folder = DriveApp.getFolderById(folderId); + const files = folder.getFilesByName('Agenda TEMPLATE##'); + + // If there is a file, returns the ID. + while (files.hasNext()) { + const file = files.next(); + return file.getId(); + } + + // Otherwise, creates the agenda template. + // You can adjust the default template here + const doc = DocumentApp.create('Agenda TEMPLATE##'); + const body = doc.getBody(); + + body + .appendParagraph('##Attendees##') + .setHeading(DocumentApp.ParagraphHeading.HEADING1) + .editAsText() + .setBold(true); + body.appendParagraph(' ').editAsText().setBold(false); + + body + .appendParagraph('Overview') + .setHeading(DocumentApp.ParagraphHeading.HEADING1) + .editAsText() + .setBold(true); + body.appendParagraph(' '); + body.appendParagraph('- Topic 1: ').editAsText().setBold(true); + body.appendParagraph(' ').editAsText().setBold(false); + body.appendParagraph('- Topic 2: ').editAsText().setBold(true); + body.appendParagraph(' ').editAsText().setBold(false); + body.appendParagraph('- Topic 3: ').editAsText().setBold(true); + body.appendParagraph(' ').editAsText().setBold(false); + + body + .appendParagraph('Next Steps') + .setHeading(DocumentApp.ParagraphHeading.HEADING1) + .editAsText() + .setBold(true); + body.appendParagraph('- Takeaway 1: ').editAsText().setBold(true); + body.appendParagraph('- Responsible: ').editAsText().setBold(false); + body.appendParagraph('- Accountable: '); + body.appendParagraph('- Consult: '); + body.appendParagraph('- Inform: '); + body.appendParagraph(' '); + body.appendParagraph('- Takeaway 2: ').editAsText().setBold(true); + body.appendParagraph('- Responsible: ').editAsText().setBold(false); + body.appendParagraph('- Accountable: '); + body.appendParagraph('- Consult: '); + body.appendParagraph('- Inform: '); + body.appendParagraph(' '); + body.appendParagraph('- Takeaway 3: ').editAsText().setBold(true); + body.appendParagraph('- Responsible: ').editAsText().setBold(false); + body.appendParagraph('- Accountable: '); + body.appendParagraph('- Consult: '); + body.appendParagraph('- Inform: '); + + doc.saveAndClose(); + + folder.addFile(DriveApp.getFileById(doc.getId())); + + return doc.getId(); +} + +/** + * When there is a change to the calendar, searches for events that include "#agenda" + * in the decrisption. + * + */ +function onCalendarChange() { + // Gets recent events with the #agenda tag + const now = new Date(); + const events = CalendarApp.getEvents( + now, + new Date(now.getTime() + 2 * 60 * 60 * 1000000), + {search: '#agenda'}, + ); + + const folderId = checkFolder(); + const templateId = getTemplateId(folderId); + + const folder = DriveApp.getFolderById(folderId); + + // Loops through any events found + for (i = 0; i < events.length; i++) { + const event = events[i]; + + // Confirms whether the event has the #agenda tag + let description = event.getDescription(); + if (description.search('#agenda') == -1) continue; + + // Only works with events created by the owner of this calendar + if (event.isOwnedByMe()) { + // Creates a new document from the template for an agenda for this event + const newDoc = DriveApp.getFileById(templateId).makeCopy(); + newDoc.setName('Agenda for ' + event.getTitle()); + + const file = DriveApp.getFileById(newDoc.getId()); + folder.addFile(file); + + const doc = DocumentApp.openById(newDoc.getId()); + const body = doc.getBody(); + + // Fills in the template with information about the attendees from the + // calendar event + const conf = body.findText('##Attendees##'); + if (conf) { + const ref = conf.getStartOffset(); + + for (let i in event.getGuestList()) { + let guest = event.getGuestList()[i]; + + body.insertParagraph(ref + 2, guest.getEmail()); + } + body.replaceText('##Attendees##', 'Attendees'); + } + + // Replaces the tag with a link to the agenda document + const agendaUrl = 'https://docs.google.com/document/d/' + newDoc.getId(); + description = description.replace( + '#agenda', + 'Agenda Doc', + ); + event.setDescription(description); + + // Invites attendees to the Google doc so they automatically receive access to the agenda + newDoc.addEditor(newDoc.getOwner()); + + for (let i in event.getGuestList()) { + let guest = event.getGuestList()[i]; + + newDoc.addEditor(guest.getEmail()); + } + } + } + return; +} + +/** + * Creates an event-driven trigger that fires whenever there's a change to the calendar. + */ +function setUp() { + let email = Session.getActiveUser().getEmail(); + ScriptApp.newTrigger("onCalendarChange").forUserCalendar(email).onEventUpdated().create(); +} diff --git a/solutions/automations/agenda-maker/README.md b/solutions/automations/agenda-maker/README.md new file mode 100644 index 000000000..e8100c3c9 --- /dev/null +++ b/solutions/automations/agenda-maker/README.md @@ -0,0 +1,3 @@ +# Make an agenda for meetings + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/agenda-maker) for additional details. diff --git a/solutions/automations/agenda-maker/appsscript.json b/solutions/automations/agenda-maker/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/agenda-maker/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/aggregate-document-content/.clasp.json b/solutions/automations/aggregate-document-content/.clasp.json new file mode 100644 index 000000000..c264f85c1 --- /dev/null +++ b/solutions/automations/aggregate-document-content/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1YGstQLxmTcAQlSHfm0yke12Y2UgT8eVfCxrG_jGpG1dHDmFdOaHQfQZJ"} diff --git a/solutions/automations/aggregate-document-content/Code.js b/solutions/automations/aggregate-document-content/Code.js new file mode 100644 index 000000000..c042b83f1 --- /dev/null +++ b/solutions/automations/aggregate-document-content/Code.js @@ -0,0 +1,176 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/aggregate-document-content + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This file containts the main application functions that import data from + * summary documents into the body of the main document. + */ + +// Application constants +const APP_TITLE = 'Document summary importer'; // Application name +const PROJECT_FOLDER_NAME = 'Project statuses'; // Drive folder for the source files. + +// Below are the parameters used to identify which content to import from the source documents +// and which content has already been imported. +const FIND_TEXT_KEYWORDS = 'Summary'; // String that must be found in the heading above the table (case insensitive). +const APP_STYLE = DocumentApp.ParagraphHeading.HEADING3; // Style that must be applied to heading above the table. +const TEXT_COLOR = '#2e7d32'; // Color applied to heading after import to avoid duplication. + +/** + * Updates the main document, importing content from the source files. + * Uses the above parameters to locate content to be imported. + * + * Called from menu option. + */ +function performImport() { + // Gets the folder in Drive associated with this application. + const folder = getFolderByName_(PROJECT_FOLDER_NAME); + // Gets the Google Docs files found in the folder. + const files = getFiles(folder); + + // Warns the user if the folder is empty. + const ui = DocumentApp.getUi(); + if (files.length === 0) { + const msg = + `No files found in the folder '${PROJECT_FOLDER_NAME}'. + Run '${MENU.SETUP}' | '${MENU.SAMPLES}' from the menu + if you'd like to create samples files.` + ui.alert(APP_TITLE, msg, ui.ButtonSet.OK); + return; + } + + /** Processes main document */ + // Gets the active document and body section. + const docTarget = DocumentApp.getActiveDocument(); + const docTargetBody = docTarget.getBody(); + + // Appends import summary section to the end of the target document. + // Adds a horizontal line and a header with today's date and a title string. + docTargetBody.appendHorizontalRule(); + const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy'); + const headingText = `Imported: ${dateString}`; + docTargetBody.appendParagraph(headingText).setHeading(APP_STYLE); + // Appends a blank paragraph for spacing. + docTargetBody.appendParagraph(" "); + + /** Process source documents */ + // Iterates through each source document in the folder. + // Copies and pastes new updates to the main document. + let noContentList = []; + let numUpdates = 0; + for (let id of files) { + + // Opens source document; get info and body. + const docOpen = DocumentApp.openById(id); + const docName = docOpen.getName(); + const docHtml = docOpen.getUrl(); + const docBody = docOpen.getBody(); + + // Gets summary content from document and returns as object {content:content} + const content = getContent(docBody); + + // Logs if document doesn't contain content to be imported. + if (!content) { + noContentList.push(docName); + continue; + } + else { + numUpdates++ + // Inserts content into the main document. + // Appends a title/url reference link back to source document. + docTargetBody.appendParagraph('').appendText(`${docName}`).setLinkUrl(docHtml); + // Appends a single-cell table and pastes the content. + docTargetBody.appendTable(content); + } + docOpen.saveAndClose() + } + /** Provides an import summary */ + docTarget.saveAndClose(); + let msg = `Number of documents updated: ${numUpdates}` + if (noContentList.length != 0) { + msg += `\n\nThe following documents had no updates:` + for (let file of noContentList) { + msg += `\n ${file}`; + } + } + ui.alert(APP_TITLE, msg, ui.ButtonSet.OK); +} + +/** + * Updates the main document drawing content from source files. + * Uses the parameters at the top of this file to locate content to import. + * + * Called from performImport(). + */ +function getContent(body) { + + // Finds the heading paragraph with matching style, keywords and !color. + var parValidHeading; + const searchType = DocumentApp.ElementType.PARAGRAPH; + const searchHeading = APP_STYLE; + let searchResult = null; + + // Gets and loops through all paragraphs that match the style of APP_STYLE. + while (searchResult = body.findElement(searchType, searchResult)) { + let par = searchResult.getElement().asParagraph(); + if (par.getHeading() == searchHeading) { + // If heading style matches, searches for text string (case insensitive). + let findPos = par.findText('(?i)' + FIND_TEXT_KEYWORDS); + if (findPos !== null) { + + // If text color is green, then the paragraph isn't a new summary to copy. + if (par.editAsText().getForegroundColor() != TEXT_COLOR) { + parValidHeading = par; + } + } + } + } + + if (!parValidHeading) { + return; + } else { + // Updates the heading color to indicate that the summary has been imported. + let style = {}; + style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR; + parValidHeading.setAttributes(style); + parValidHeading.appendText(" [Exported]"); + + // Gets the content from the table following the valid heading. + let elemObj = parValidHeading.getNextSibling().asTable(); + let content = elemObj.copy(); + + return content; + } +} + +/** + * Gets the IDs of the Docs files within the folder that contains source files. + * + * Called from function performImport(). + */ +function getFiles(folder) { + // Only gets Docs files. + const files = folder.getFilesByType(MimeType.GOOGLE_DOCS); + let docIDs = []; + while (files.hasNext()) { + let file = files.next(); + docIDs.push(file.getId()); + } + return docIDs; +} \ No newline at end of file diff --git a/solutions/automations/aggregate-document-content/Menu.js b/solutions/automations/aggregate-document-content/Menu.js new file mode 100644 index 000000000..e124caf43 --- /dev/null +++ b/solutions/automations/aggregate-document-content/Menu.js @@ -0,0 +1,43 @@ +/** + * This file contains the functions that build the custom menu. + */ +// Menu constants for easy access to update. +const MENU = { + NAME: 'Import summaries', + IMPORT: 'Import summaries', + SETUP: 'Configure', + NEW_INSTANCE: 'Setup new instance', + TEMPLATE: 'Create starter template', + SAMPLES: 'Run demo setup with sample documents' +} + +/** + * Creates custom menu when the document is opened. + */ +function onOpen() { + const ui = DocumentApp.getUi(); + ui.createMenu(MENU.NAME) + .addItem(MENU.IMPORT, 'performImport') + .addSeparator() + .addSubMenu(ui.createMenu(MENU.SETUP) + .addItem(MENU.NEW_INSTANCE, 'setupConfig') + .addItem(MENU.TEMPLATE, 'createSampleFile') + .addSeparator() + .addItem(MENU.SAMPLES, 'setupWithSamples')) + .addItem('About', 'aboutApp') + .addToUi() +} + +/** + * About box for context and contact. + * TODO: Personalize + */ +function aboutApp() { + const msg = ` + ${APP_TITLE} + Version: 1.0 + Contact: ` + + const ui = DocumentApp.getUi(); + ui.alert("About this application", msg, ui.ButtonSet.OK); +} diff --git a/solutions/automations/aggregate-document-content/README.md b/solutions/automations/aggregate-document-content/README.md new file mode 100644 index 000000000..6e8623ded --- /dev/null +++ b/solutions/automations/aggregate-document-content/README.md @@ -0,0 +1,3 @@ +# Aggregate content from multiple documents + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/aggregate-document-content) for additional details. diff --git a/solutions/automations/aggregate-document-content/Setup.js b/solutions/automations/aggregate-document-content/Setup.js new file mode 100644 index 000000000..ba9e92416 --- /dev/null +++ b/solutions/automations/aggregate-document-content/Setup.js @@ -0,0 +1,149 @@ +/** + * This file contains functions that create the template and sample documents. + */ + +/** + * Runs full setup configuration, with option to include samples. + * + * Called from menu & setupWithSamples() + * + * @param {boolean} includeSamples - Optional, if true creates samples files. * + */ +function setupConfig(includeSamples) { + + // Gets folder to store documents in. + const folder = getFolderByName_(PROJECT_FOLDER_NAME) + + let msg = + `\nDrive Folder for Documents: '${PROJECT_FOLDER_NAME}' + \nURL: \n${folder.getUrl()}` + + // Creates sample documents for testing. + // Remove sample document creation and add your own process as needed. + if (includeSamples) { + let filesCreated = 0; + for (let doc of samples.documents) { + filesCreated += createGoogleDoc(doc, folder, true); + } + msg += `\n\nFiles Created: ${filesCreated}` + } + const ui = DocumentApp.getUi(); + ui.alert(`${APP_TITLE} [Setup]`, msg, ui.ButtonSet.OK); + +} + +/** + * Creates a single document instance in the application folder. + * Includes import settings already created [Heading | Keywords | Table] + * + * Called from menu. + */ +function createSampleFile() { + + // Creates a new Google Docs document. + const templateName = `[Template] ${APP_TITLE}`; + const doc = DocumentApp.create(templateName); + const docId = doc.getId(); + + const msg = `\nDocument created: '${templateName}' + \nURL: \n${doc.getUrl()}` + + // Adds template content to the body. + const body = doc.getBody(); + + body.setText(templateName); + body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE); + body.appendParagraph('Description').setHeading(DocumentApp.ParagraphHeading.HEADING1); + body.appendParagraph(''); + + const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy'); + body.appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`).setHeading(APP_STYLE); + body.appendTable().appendTableRow().appendTableCell('TL;DR'); + body.appendParagraph(""); + + // Gets folder to store documents in. + const folder = getFolderByName_(PROJECT_FOLDER_NAME) + + // Moves document to application folder. + DriveApp.getFileById(docId).moveTo(folder); + + const ui = DocumentApp.getUi(); + ui.alert(`${APP_TITLE} [Template]`, msg, ui.ButtonSet.OK); +} + +/** + * Configures application for demonstration by setting it up with sample documents. + * + * Called from menu | Calls setupConfig with option set to true. + */ +function setupWithSamples() { + setupConfig(true) +} + +/** + * Sample document names and demo content. + * {object} samples[] +*/ +const samples = { + 'documents': [ + { + 'name': 'Project GHI', + 'description': 'Google Workspace Add-on inventory review.', + 'content': 'Reviewed all of the currently in-use and proposed Google Workspace Add-ons. Will perform an assessment on how we can reduce overlap, reduce licensing costs, and limit security exposures. \n\nNext week\'s goal is to report findings back to the Corp Ops team.' + }, + { + 'name': 'Project DEF', + 'description': 'Improve IT networks within the main corporate building.', + 'content': 'Primarily focused on 2nd thru 5th floors in the main corporate building evaluating the network infrastructure. Benchmarking tests were performed and results are being analyzed. \n\nWill submit all findings, analysis, and recommendations next week for committee review.' + }, + { + 'name': 'Project ABC', + 'description': 'Assess existing Google Chromebook inventory and recommend upgrades where necessary.', + 'content': 'Concluded a pilot program with the Customer Service department to perform inventory and update inventory records with Chromebook hardware, Chrome OS versions, and installed apps. \n\nScheduling a work plan and seeking necessary go-forward approvals for next week.' + }, + ], + 'common': 'This sample document is configured to work with the Import summaries custom menu. For the import to work, the source documents used must contain a specific keyword (currently set to "Summary"). The keyword must reside in a paragraph with a set style (currently set to "Heading 3") that is directly followed by a single-cell table. The table contains the contents to be imported into the primary document.\n\nWhile those rules might seem precise, it\'s how the application programmatically determines what content is meant to be imported and what can be ignored. Once a summary has been imported, the script updates the heading font to a new color (currently set to Green, hex \'#2e7d32\') to ensure the app ignores it in future imports. You can change these settings in the Apps Script code.' +} + +/** + * Creates a sample document in application folder. + * Includes import settings already created [Heading | Keywords | Table]. + * Inserts demo data from samples[]. + * + * Called from menu. + */ +function createGoogleDoc(document, folder, duplicate) { + + // Checks for duplicates. + if (!duplicate) { + // Doesn't create file of same name if one already exists. + if (folder.getFilesByName(document.name).hasNext()) { + return 0 // File not created. + } + } + + // Creates a new Google Docs document. + const doc = DocumentApp.create(document.name).setName(document.name); + const docId = doc.getId(); + + // Adds boilerplate content to the body. + const body = doc.getBody(); + + body.setText(document.name); + body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE); + body.appendParagraph("Description").setHeading(DocumentApp.ParagraphHeading.HEADING1); + body.appendParagraph(document.description); + body.appendParagraph("Usage Instructions").setHeading(DocumentApp.ParagraphHeading.HEADING1); + body.appendParagraph(samples.common); + + const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy'); + body.appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`).setHeading(APP_STYLE); + body.appendTable().appendTableRow().appendTableCell(document.content); + body.appendParagraph(""); + + // Moves document to application folder. + DriveApp.getFileById(docId).moveTo(folder); + + // Returns if successfully created. + return 1 +} \ No newline at end of file diff --git a/solutions/automations/aggregate-document-content/Utilities.js b/solutions/automations/aggregate-document-content/Utilities.js new file mode 100644 index 000000000..be9b43a56 --- /dev/null +++ b/solutions/automations/aggregate-document-content/Utilities.js @@ -0,0 +1,44 @@ +/** + * This file contains common utility functions. + */ + +/** + * Returns a Drive folder located in same folder that the application document is located. + * Checks if the folder exists and returns that folder, or creates new one if not found. + * + * @param {string} folderName - Name of the Drive folder. + * @return {object} Google Drive folder + */ +function getFolderByName_(folderName) { + // Gets the Drive folder where the current document is located. + const docId = DocumentApp.getActiveDocument().getId(); + const parentFolder = DriveApp.getFileById(docId).getParents().next(); + + // Iterates subfolders to check if folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === folderName) { + return folder; + } + } + // Creates a new folder if one doesn't already exist. + return parentFolder.createFolder(folderName) + .setDescription(`Created by ${APP_TITLE} application to store documents to process`); +} + +/** + * Test function to run getFolderByName_. + * @logs details of created Google Drive folder. + */ +function test_getFolderByName() { + + // Gets the folder in Drive associated with this application. + const folder = getFolderByName_(PROJECT_FOLDER_NAME); + + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`) + // Uncomment the following to automatically delete the test folder. + // folder.setTrashed(true); +} \ No newline at end of file diff --git a/solutions/automations/aggregate-document-content/appsscript.json b/solutions/automations/aggregate-document-content/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/aggregate-document-content/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/calendar-timesheet/.clasp.json b/solutions/automations/calendar-timesheet/.clasp.json new file mode 100644 index 000000000..d8c75e26a --- /dev/null +++ b/solutions/automations/calendar-timesheet/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1WL3-mzC219UHqy_vqI1gEeoFy5Y8eeiKCZjiiPsWmVmQfVVedN5Vt7rK"} diff --git a/solutions/automations/calendar-timesheet/Code.js b/solutions/automations/calendar-timesheet/Code.js new file mode 100644 index 000000000..adda729b9 --- /dev/null +++ b/solutions/automations/calendar-timesheet/Code.js @@ -0,0 +1,363 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/calendar-timesheet + +/* +Copyright 2022 Jasper Duizendstra + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Runs when the spreadsheet is opened and adds the menu options + * to the spreadsheet menu + */ +const onOpen = () => { + SpreadsheetApp.getUi() + .createMenu('myTime') + .addItem('Sync calendar events', 'run') + .addItem('Settings', 'settings') + .addToUi(); +}; + +/** + * Opens the sidebar + */ +const settings = () => { + const html = HtmlService.createHtmlOutputFromFile('Page') + .setTitle('Settings'); + + SpreadsheetApp.getUi().showSidebar(html); +}; + +/** +* returns the settings from the script properties +*/ +const getSettings = () => { + const settings = {}; + + // get the current settings + const savedCalendarSettings = JSON.parse(PropertiesService.getScriptProperties().getProperty('calendar') || '[]'); + + // get the primary calendar + const primaryCalendar = CalendarApp.getAllCalendars() + .filter((cal) => cal.isMyPrimaryCalendar()) + .map((cal) => ({ + name: 'Primary calendar', + id: cal.getId() + })); + + // get the secondary calendars + const secundaryCalendars = CalendarApp.getAllCalendars() + .filter((cal) => cal.isOwnedByMe() && !cal.isMyPrimaryCalendar()) + .map((cal) => ({ + name: cal.getName(), + id: cal.getId() + })); + + // the current available calendars + const availableCalendars = primaryCalendar.concat(secundaryCalendars); + + // find any calendars that were removed + const unavailebleCalendars = []; + savedCalendarSettings.forEach((savedCalendarSetting) => { + if (!availableCalendars.find((availableCalendar) => availableCalendar.id === savedCalendarSetting.id)) { + unavailebleCalendars.push(savedCalendarSetting); + } + }); + + // map the current settings to the available calendars + const calendarSettings = availableCalendars.map((availableCalendar) => { + if (savedCalendarSettings.find((savedCalendar) => savedCalendar.id === availableCalendar.id)) { + availableCalendar.sync = true; + + } + return availableCalendar; + }); + + // add the calendar settings to the settings + settings.calendarSettings = calendarSettings; + + const savedFrom = PropertiesService.getScriptProperties().getProperty('syncFrom'); + settings.syncFrom = savedFrom; + + const savedTo = PropertiesService.getScriptProperties().getProperty('syncTo'); + settings.syncTo = savedTo; + + const savedIsUpdateTitle = PropertiesService.getScriptProperties().getProperty('isUpdateTitle') === 'true'; + settings.isUpdateCalendarItemTitle = savedIsUpdateTitle; + + const savedIsUseCategoriesAsCalendarItemTitle = PropertiesService.getScriptProperties().getProperty('isUseCategoriesAsCalendarItemTitle') === 'true'; + settings.isUseCategoriesAsCalendarItemTitle = savedIsUseCategoriesAsCalendarItemTitle; + + const savedIsUpdateDescription = PropertiesService.getScriptProperties().getProperty('isUpdateDescription') === 'true'; + settings.isUpdateCalendarItemDescription = savedIsUpdateDescription; + + return settings; +}; + +/** +* Saves the settings from the sidebar +*/ +const saveSettings = (settings) => { + PropertiesService.getScriptProperties().setProperty('calendar', JSON.stringify(settings.calendarSettings)); + PropertiesService.getScriptProperties().setProperty('syncFrom', settings.syncFrom); + PropertiesService.getScriptProperties().setProperty('syncTo', settings.syncTo); + PropertiesService.getScriptProperties().setProperty('isUpdateTitle', settings.isUpdateCalendarItemTitle); + PropertiesService.getScriptProperties().setProperty('isUseCategoriesAsCalendarItemTitle', settings.isUseCategoriesAsCalendarItemTitle); + PropertiesService.getScriptProperties().setProperty('isUpdateDescription', settings.isUpdateCalendarItemDescription); + return 'Settings saved'; +}; + +/** + * Builds the myTime object and runs the synchronisation + */ +const run = () => { + 'use strict'; + myTime({ + mainSpreadsheetId: SpreadsheetApp.getActiveSpreadsheet().getId(), + }).run(); +}; + +/** + * The main function used for the synchronisation + * @param {Object} par The main parameter object. + * @return {Object} The myTime Object. + */ +const myTime = (par) => { + 'use strict'; + + /** + * Format the sheet + */ + const formatSheet = () => { + // sort decending on start date + hourSheet.sort(3, false); + + // hide the technical columns + hourSheet.hideColumns(1, 2); + + // remove any extra rows + if (hourSheet.getLastRow() > 1 && hourSheet.getLastRow() < hourSheet.getMaxRows()) { + hourSheet.deleteRows(hourSheet.getLastRow() + 1, hourSheet.getMaxRows() - hourSheet.getLastRow()); + } + + // set the validation for the customers + let rule = SpreadsheetApp.newDataValidation() + .requireValueInRange(categoriesSheet.getRange('A2:A'), true) + .setAllowInvalid(true) + .build(); + hourSheet.getRange('I2:I').setDataValidation(rule); + + // set the validation for the projects + rule = SpreadsheetApp.newDataValidation() + .requireValueInRange(categoriesSheet.getRange('B2:B'), true) + .setAllowInvalid(true) + .build(); + hourSheet.getRange('J2:J').setDataValidation(rule); + + // set the validation for the tsaks + rule = SpreadsheetApp.newDataValidation() + .requireValueInRange(categoriesSheet.getRange('C2:C'), true) + .setAllowInvalid(true) + .build(); + hourSheet.getRange('K2:K').setDataValidation(rule); + + if(isUseCategoriesAsCalendarItemTitle) { + hourSheet.getRange('L2:L').setFormulaR1C1('IF(OR(R[0]C[-3]="tbd";R[0]C[-2]="tbd";R[0]C[-1]="tbd");""; CONCATENATE(R[0]C[-3];"|";R[0]C[-2];"|";R[0]C[-1];"|"))'); + } + // set the hours, month, week and number collumns + hourSheet.getRange('P2:P').setFormulaR1C1('=IF(R[0]C[-12]="";"";R[0]C[-12]-R[0]C[-13])'); + hourSheet.getRange('Q2:Q').setFormulaR1C1('=IF(R[0]C[-13]="";"";month(R[0]C[-13]))'); + hourSheet.getRange('R2:R').setFormulaR1C1('=IF(R[0]C[-14]="";"";WEEKNUM(R[0]C[-14];2))'); + hourSheet.getRange('S2:S').setFormulaR1C1('=R[0]C[-3]'); + }; + + /** + * Activate the synchronisation + */ + function run() { + console.log('Started processing hours.'); + + const processCalendar = (setting) => { + SpreadsheetApp.flush(); + + // current calendar info + const calendarName = setting.name; + const calendarId = setting.id; + + console.log(`processing ${calendarName} with the id ${calendarId} from ${syncStartDate} to ${syncEndDate}`); + + // get the calendar + const calendar = CalendarApp.getCalendarById(calendarId); + + // get the calendar events and create lookups + const events = calendar.getEvents(syncStartDate, syncEndDate); + const eventsLookup = events.reduce((jsn, event) => { + jsn[event.getId()] = event; + return jsn; + }, {}); + + // get the sheet events and create lookups + const existingEvents = hourSheet.getDataRange().getValues().slice(1); + const existingEventsLookUp = existingEvents.reduce((jsn, row, index) => { + if (row[0] !== calendarId) { + return jsn; + } + jsn[row[1]] = { + event: row, + row: index + 2 + }; + return jsn; + }, {}); + + // handle a calendar event + const handleEvent = (event) => { + const eventId = event.getId(); + + // new event + if (!existingEventsLookUp[eventId]) { + hourSheet.appendRow([ + calendarId, + eventId, + event.getStartTime(), + event.getEndTime(), + calendarName, + event.getCreators().join(','), + event.getTitle(), + event.getDescription(), + event.getTag('Client') || 'tbd', + event.getTag('Project') || 'tbd', + event.getTag('Task') || 'tbd', + (isUpdateCalendarItemTitle) ? '' : event.getTitle(), + (isUpdateCalendarItemDescription) ? '' : event.getDescription(), + event.getGuestList().map((guest) => guest.getEmail()).join(','), + event.getLocation(), + undefined, + undefined, + undefined, + undefined + ]); + return true; + } + + // existing event + const exisitingEvent = existingEventsLookUp[eventId].event; + const exisitingEventRow = existingEventsLookUp[eventId].row; + + if (event.getStartTime() - exisitingEvent[startTimeColumn - 1] !== 0) { + hourSheet.getRange(exisitingEventRow, startTimeColumn).setValue(event.getStartTime()); + } + + if (event.getEndTime() - exisitingEvent[endTimeColumn - 1] !== 0) { + hourSheet.getRange(exisitingEventRow, endTimeColumn).setValue(event.getEndTime()); + } + + if (event.getCreators().join(',') !== exisitingEvent[creatorsColumn - 1]) { + hourSheet.getRange(exisitingEventRow, creatorsColumn).setValue(event.getCreators()[0]); + } + + if (event.getGuestList().map((guest) => guest.getEmail()).join(',') !== exisitingEvent[guestListColumn - 1]) { + hourSheet.getRange(exisitingEventRow, guestListColumn).setValue(event.getGuestList().map((guest) => guest.getEmail()).join(',')); + } + + if (event.getLocation() !== exisitingEvent[locationColumn - 1]) { + hourSheet.getRange(exisitingEventRow, locationColumn).setValue(event.getLocation()); + } + + if(event.getTitle() !== exisitingEvent[titleColumn - 1]) { + if(!isUpdateCalendarItemTitle) { + hourSheet.getRange(exisitingEventRow, titleColumn).setValue(event.getTitle()); + } + if(isUpdateCalendarItemTitle) { + event.setTitle(exisitingEvent[titleColumn - 1]); + } + } + + if(event.getDescription() !== exisitingEvent[descriptionColumn - 1]) { + if(!isUpdateCalendarItemDescription) { + hourSheet.getRange(exisitingEventRow, descriptionColumn).setValue(event.getDescription()); + } + if(isUpdateCalendarItemDescription) { + event.setDescription(exisitingEvent[descriptionColumn - 1]); + } + } + + return true; + }; + + // process each event for the calendar + events.every(handleEvent); + + // remove any events in the sheet that are not in de calendar + existingEvents.every((event, index) => { + if (event[0] !== calendarId) { + return true; + }; + + if (eventsLookup[event[1]]) { + return true; + } + + if (event[3] < syncStartDate) { + return true; + } + + hourSheet.getRange(index + 2, 1, 1, 20).clear(); + return true; + }); + + return true; + }; + + // process the calendars + settings.calendarSettings.filter((calenderSetting) => calenderSetting.sync === true).every(processCalendar); + + formatSheet(); + SpreadsheetApp.setActiveSheet(hourSheet); + + console.log('Finished processing hours.'); + } + + const mainSpreadSheetId = par.mainSpreadsheetId; + const mainSpreadsheet = SpreadsheetApp.openById(mainSpreadSheetId); + const hourSheet = mainSpreadsheet.getSheetByName('Hours'); + const categoriesSheet = mainSpreadsheet.getSheetByName('Categories'); + const settings = getSettings(); + + const syncStartDate = new Date(); + syncStartDate.setDate(syncStartDate.getDate() - Number(settings.syncFrom)); + + const syncEndDate = new Date(); + syncEndDate.setDate(syncEndDate.getDate() + Number(settings.syncTo)); + + const isUpdateCalendarItemTitle = settings.isUpdateCalendarItemTitle; + const isUseCategoriesAsCalendarItemTitle = settings.isUseCategoriesAsCalendarItemTitle; + const isUpdateCalendarItemDescription = settings.isUpdateCalendarItemDescription; + + const startTimeColumn = 3; + const endTimeColumn = 4; + const creatorsColumn = 6; + const originalTitleColumn = 7; + const originalDescriptionColumn = 8; + const clientColumn = 9; + const projectColumn = 10; + const taskColumn = 11; + const titleColumn = 12; + const descriptionColumn = 13; + const guestListColumn = 14; + const locationColumn = 15; + + return Object.freeze({ + run: run, + }); +}; \ No newline at end of file diff --git a/solutions/automations/calendar-timesheet/Page.html b/solutions/automations/calendar-timesheet/Page.html new file mode 100644 index 000000000..b019fbb81 --- /dev/null +++ b/solutions/automations/calendar-timesheet/Page.html @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/solutions/automations/calendar-timesheet/README.md b/solutions/automations/calendar-timesheet/README.md new file mode 100644 index 000000000..0c90af2c2 --- /dev/null +++ b/solutions/automations/calendar-timesheet/README.md @@ -0,0 +1,3 @@ +# Record time and activities in Calendar and Sheets + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/calendar-timesheet) for additional details. diff --git a/solutions/automations/calendar-timesheet/appsscript.json b/solutions/automations/calendar-timesheet/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/calendar-timesheet/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/content-signup/.clasp.json b/solutions/automations/content-signup/.clasp.json new file mode 100644 index 000000000..0543fbf7b --- /dev/null +++ b/solutions/automations/content-signup/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1G8TfU6Rfcl76Uo4gKig7jFMYKai-V_fiUNbO12pAb25pA4_uyxN5PSvd"} diff --git a/solutions/automations/content-signup/Code.js b/solutions/automations/content-signup/Code.js new file mode 100644 index 000000000..2c0a3f3ad --- /dev/null +++ b/solutions/automations/content-signup/Code.js @@ -0,0 +1,129 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/content-signup + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// To use your own template doc, update the below variable with the URL of your own Google Doc template. +// Make sure you update the sharing settings so that 'anyone' or 'anyone in your organization' can view. +const EMAIL_TEMPLATE_DOC_URL = 'https://docs.google.com/document/d/1enes74gWsMG3dkK3SFO08apXkr0rcYBd3JHKOb2Nksk/edit?usp=sharing'; +// Update this variable to customize the email subject. +const EMAIL_SUBJECT = 'Hello, here is the content you requested'; + +// Update this variable to the content titles and URLs you want to offer. Make sure you update the form so that the content titles listed here match the content titles you list in the form. +const topicUrls = { + 'Google Calendar how-to videos': 'https://www.youtube.com/playlist?list=PLU8ezI8GYqs7IPb_UdmUNKyUCqjzGO9PJ', + 'Google Drive how-to videos': 'https://www.youtube.com/playlist?list=PLU8ezI8GYqs7Y5d1cgZm2Obq7leVtLkT4', + 'Google Docs how-to videos': 'https://www.youtube.com/playlist?list=PLU8ezI8GYqs4JKwZ-fpBP-zSoWPL8Sit7', + 'Google Sheets how-to videos': 'https://www.youtube.com/playlist?list=PLU8ezI8GYqs61ciKpXf_KkV7ZRbRHVG38', +}; + +/** + * Installs a trigger on the spreadsheet for when someone submits a form. + */ +function installTrigger() { + ScriptApp.newTrigger('onFormSubmit') + .forSpreadsheet(SpreadsheetApp.getActive()) + .onFormSubmit() + .create(); +} + +/** + * Sends a customized email for every form response. + * + * @param {Object} event - Form submit event + */ +function onFormSubmit(e) { + let responses = e.namedValues; + + // If the question title is a label, it can be accessed as an object field. + // If it has spaces or other characters, it can be accessed as a dictionary. + let timestamp = responses.Timestamp[0]; + let email = responses['Email address'][0].trim(); + let name = responses.Name[0].trim(); + let topicsString = responses.Topics[0].toLowerCase(); + + // Parse topics of interest into a list (since there are multiple items + // that are saved in the row as blob of text). + let topics = Object.keys(topicUrls).filter(function(topic) { + // indexOf searches for the topic in topicsString and returns a non-negative + // index if the topic is found, or it will return -1 if it's not found. + return topicsString.indexOf(topic.toLowerCase()) != -1; + }); + + // If there is at least one topic selected, send an email to the recipient. + let status = ''; + if (topics.length > 0) { + MailApp.sendEmail({ + to: email, + subject: EMAIL_SUBJECT, + htmlBody: createEmailBody(name, topics), + }); + status = 'Sent'; + } + else { + status = 'No topics selected'; + } + + // Append the status on the spreadsheet to the responses' row. + let sheet = SpreadsheetApp.getActiveSheet(); + let row = sheet.getActiveRange().getRow(); + let column = e.values.length + 1; + sheet.getRange(row, column).setValue(status); + + Logger.log("status=" + status + "; responses=" + JSON.stringify(responses)); +} + +/** + * Creates email body and includes the links based on topic. + * + * @param {string} recipient - The recipient's email address. + * @param {string[]} topics - List of topics to include in the email body. + * @return {string} - The email body as an HTML string. + */ +function createEmailBody(name, topics) { + let topicsHtml = topics.map(function(topic) { + let url = topicUrls[topic]; + return '
  • ' + topic + '
  • '; + }).join(''); + topicsHtml = '
      ' + topicsHtml + '
    '; + + // Make sure to update the emailTemplateDocId at the top. + let docId = DocumentApp.openByUrl(EMAIL_TEMPLATE_DOC_URL).getId(); + let emailBody = docToHtml(docId); + emailBody = emailBody.replace(/{{NAME}}/g, name); + emailBody = emailBody.replace(/{{TOPICS}}/g, topicsHtml); + return emailBody; +} + +/** + * Downloads a Google Doc as an HTML string. + * + * @param {string} docId - The ID of a Google Doc to fetch content from. + * @return {string} The Google Doc rendered as an HTML string. + */ +function docToHtml(docId) { + + // Downloads a Google Doc as an HTML string. + let url = "https://docs.google.com/feeds/download/documents/export/Export?id=" + + docId + "&exportFormat=html"; + let param = { + method: "get", + headers: {"Authorization": "Bearer " + ScriptApp.getOAuthToken()}, + muteHttpExceptions: true, + }; + return UrlFetchApp.fetch(url, param).getContentText(); +} diff --git a/solutions/automations/content-signup/README.md b/solutions/automations/content-signup/README.md new file mode 100644 index 000000000..ac50839e6 --- /dev/null +++ b/solutions/automations/content-signup/README.md @@ -0,0 +1,3 @@ +# Send curated content + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/content-signup) for additional details. diff --git a/solutions/automations/content-signup/appsscript.json b/solutions/automations/content-signup/appsscript.json new file mode 100644 index 000000000..ba13ec1c1 --- /dev/null +++ b/solutions/automations/content-signup/appsscript.json @@ -0,0 +1,14 @@ +{ + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "oauthScopes": [ + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/script.scriptapp", + "https://www.googleapis.com/auth/script.send_mail", + "https://www.googleapis.com/auth/spreadsheets.currentonly" + ], + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/course-feedback-response/.clasp.json b/solutions/automations/course-feedback-response/.clasp.json new file mode 100644 index 000000000..ae51645f3 --- /dev/null +++ b/solutions/automations/course-feedback-response/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1k75E4EdC3TcJEGGIupBANjm5duvs35ORAU1Mg2_6DNXENo827dFzmFeC"} diff --git a/solutions/automations/course-feedback-response/Code.js b/solutions/automations/course-feedback-response/Code.js new file mode 100644 index 000000000..212244a9c --- /dev/null +++ b/solutions/automations/course-feedback-response/Code.js @@ -0,0 +1,119 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/course-feedback-response + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Creates custom menu for user to run scripts. + */ +function onOpen() { + let ui = SpreadsheetApp.getUi(); + ui.createMenu('Form Reply Tool') + .addItem('Enable auto draft replies', 'installTrigger') + .addToUi(); +} + +/** + * Installs a trigger on the Spreadsheet for when a Form response is submitted. + */ +function installTrigger() { + ScriptApp.newTrigger('onFormSubmit') + .forSpreadsheet(SpreadsheetApp.getActive()) + .onFormSubmit() + .create(); +} + +/** + * Creates a draft email for every response on a form + * + * @param {Object} event - Form submit event + */ +function onFormSubmit(e) { + let responses = e.namedValues; + + // parse form response data + let timestamp = responses.Timestamp[0]; + let email = responses['Email address'][0].trim(); + + // create email body + let emailBody = createEmailBody(responses); + + // create draft email + createDraft(timestamp, email, emailBody); +} + +/** + * Creates email body and includes feedback from Google Form. + * + * @param {string} responses - The form response data + * @return {string} - The email body as an HTML string + */ +function createEmailBody(responses) { + // parse form response data + let name = responses.Name[0].trim(); + let industry = responses['What industry do you work in?'][0]; + let source = responses['How did you find out about this course?'][0]; + let rating = responses['On a scale of 1 - 5 how would you rate this course?'][0]; + let productFeedback = responses['What could be different to make it a 5 rating?'][0]; + let otherFeedback = responses['Any other feedback?'][0]; + + // create email body + let htmlBody = 'Hi ' + name + ',

    ' + + 'Thanks for responding to our course feedback questionnaire.

    ' + + 'It\'s really useful to us to help improve this course.

    ' + + 'Have a great day!

    ' + + 'Thanks,
    ' + + 'Course Team

    ' + + '****************************************************************

    ' + + 'Your feedback:

    ' + + 'What industry do you work in?

    ' + + industry + '

    ' + + 'How did you find out about this course?

    ' + + source + '

    ' + + 'On a scale of 1 - 5 how would you rate this course?

    ' + + rating + '

    ' + + 'What could be different to make it a 5 rating?

    ' + + productFeedback + '

    ' + + 'Any other feedback?

    ' + + otherFeedback + '

    '; + + return htmlBody; +} + +/** + * Create a draft email with the feedback + * + * @param {string} timestamp Timestamp for the form response + * @param {string} email Email address from the form response + * @param {string} emailBody The email body as an HTML string + */ +function createDraft(timestamp, email, emailBody) { + Logger.log('draft email create process started'); + + // create subject line + let subjectLine = 'Thanks for your course feedback! ' + timestamp; + + // create draft email + GmailApp.createDraft( + email, + subjectLine, + '', + { + htmlBody: emailBody, + } + ); +} diff --git a/solutions/automations/course-feedback-response/README.md b/solutions/automations/course-feedback-response/README.md new file mode 100644 index 000000000..5fe194863 --- /dev/null +++ b/solutions/automations/course-feedback-response/README.md @@ -0,0 +1,3 @@ +# Respond to feedback + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/course-feedback-response) for additional details. diff --git a/solutions/automations/course-feedback-response/appsscript.json b/solutions/automations/course-feedback-response/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/course-feedback-response/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/employee-certificate/.clasp.json b/solutions/automations/employee-certificate/.clasp.json new file mode 100644 index 000000000..1c8db5bb8 --- /dev/null +++ b/solutions/automations/employee-certificate/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1ImdFcF4gJjH7tERghIhPyJZQeNzo12wJ7hhltn2q0wGY4XlHR6LqbUGf"} diff --git a/solutions/automations/employee-certificate/Code.js b/solutions/automations/employee-certificate/Code.js new file mode 100644 index 000000000..f8f25941b --- /dev/null +++ b/solutions/automations/employee-certificate/Code.js @@ -0,0 +1,3 @@ +function myFunction() { + +} diff --git a/solutions/automations/employee-certificate/README.md b/solutions/automations/employee-certificate/README.md new file mode 100644 index 000000000..0b5bd18c7 --- /dev/null +++ b/solutions/automations/employee-certificate/README.md @@ -0,0 +1,3 @@ +# Send personalized appreciation certificates to employees + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/employee-certificate) for additional details. diff --git a/solutions/automations/employee-certificate/appsscript.json b/solutions/automations/employee-certificate/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/employee-certificate/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/equipment-requests/.clasp.json b/solutions/automations/equipment-requests/.clasp.json new file mode 100644 index 000000000..a4c52c62a --- /dev/null +++ b/solutions/automations/equipment-requests/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1T0G2Qr0QkHfqOK8dqjdiMRGuX2UVzkQU3BGfl2lC3wsNwkSmISbp2q6t"} diff --git a/solutions/automations/equipment-requests/Code.js b/solutions/automations/equipment-requests/Code.js new file mode 100644 index 000000000..528bb4c5c --- /dev/null +++ b/solutions/automations/equipment-requests/Code.js @@ -0,0 +1,212 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/equipment-requests + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Update this variable with the email address you want to send equipment requests to. +const REQUEST_NOTIFICATION_EMAIL = 'request_intake@example.com'; + +// Update the following variables with your own equipment options. +const AVAILABLE_LAPTOPS = [ + '15" high Performance Laptop (OS X)', + '15" high Performance Laptop (Windows)', + '15" high performance Laptop (Linux)', + '13" lightweight laptop (Windows)', +]; +const AVAILABLE_DESKTOPS = [ + 'Standard workstation (Windows)', + 'Standard workstation (Linux)', + 'High performance workstation (Windows)', + 'High performance workstation (Linux)', + 'Mac Pro (OS X)', +]; +const AVAILABLE_MONITORS = [ + 'Single 27"', + 'Single 32"', + 'Dual 24"', +]; + +// Form field titles, used for creating the form and as keys when handling +// responses. +/** + * Adds a custom menu to the spreadsheet. + */ +function onOpen() { + SpreadsheetApp.getUi().createMenu('Equipment requests') + .addItem('Set up', 'setup_') + .addItem('Clean up', 'cleanup_') + .addToUi(); +} + +/** + * Creates the form and triggers for the workflow. + */ +function setup_() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + if (ss.getFormUrl()) { + let msg = 'Form already exists. Unlink the form and try again.'; + SpreadsheetApp.getUi().alert(msg); + return; + } + let form = FormApp.create('Equipment Requests') + .setCollectEmail(true) + .setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()) + .setLimitOneResponsePerUser(false); + form.addTextItem().setTitle('Employee name').setRequired(true); + form.addTextItem().setTitle('Desk location').setRequired(true); + form.addDateItem().setTitle('Due date').setRequired(true); + form.addListItem().setTitle('Laptop').setChoiceValues(AVAILABLE_LAPTOPS); + form.addListItem().setTitle('Desktop').setChoiceValues(AVAILABLE_DESKTOPS); + form.addListItem().setTitle('Monitor').setChoiceValues(AVAILABLE_MONITORS); + + // Hide the raw form responses. + ss.getSheets().forEach(function(sheet) { + if (sheet.getFormUrl() == ss.getFormUrl()) { + sheet.hideSheet(); + } + }); + // Start workflow on each form submit + ScriptApp.newTrigger('onFormSubmit_') + .forForm(form) + .onFormSubmit() + .create(); + // Archive completed items every 5m. + ScriptApp.newTrigger('processCompletedItems_') + .timeBased() + .everyMinutes(5) + .create(); +} + +/** + * Cleans up the project (stop triggers, form submission, etc.) + */ +function cleanup_() { + let formUrl = SpreadsheetApp.getActiveSpreadsheet().getFormUrl(); + if (!formUrl) { + return; + } + ScriptApp.getProjectTriggers().forEach(function(trigger) { + ScriptApp.deleteTrigger(trigger); + }); + FormApp.openByUrl(formUrl) + .deleteAllResponses() + .setAcceptingResponses(false); +} + +/** + * Handles new form submissions to trigger the workflow. + * + * @param {Object} event - Form submit event + */ +function onFormSubmit_(event) { + let response = mapResponse_(event.response); + sendNewEquipmentRequestEmail_(response); + let equipmentDetails = Utilities.formatString('%s\n%s\n%s', + response['Laptop'], + response['Desktop'], + response['Monitor']); + let row = ['New', + '', + response['Due date'], + response['Employee name'], + response['Desk location'], + equipmentDetails, + response['email']]; + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let sheet = ss.getSheetByName('Pending requests'); + sheet.appendRow(row); +} + +/** + * Sweeps completed and cancelled requests, notifying the requestors and archiving them + * to the completed sheet. + * + * @param {Object} event + */ +function processCompletedItems_() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let pending = ss.getSheetByName('Pending requests'); + let completed = ss.getSheetByName('Completed requests'); + let rows = pending.getDataRange().getValues(); + for (let i = rows.length; i >= 2; i--) { + let row = rows[i -1]; + let status = row[0]; + if (status === 'Completed' || status == 'Cancelled') { + pending.deleteRow(i); + completed.appendRow(row); + console.log("Deleted row: " + i); + sendEquipmentRequestCompletedEmail_({ + 'Employee name': row[3], + 'Desk location': row[4], + 'email': row[6], + }); + } + }; +} + +/** + * Sends an email notification that a new equipment request has been submitted. + * + * @param {Object} request - Request details + */ +function sendNewEquipmentRequestEmail_(request) { + let template = HtmlService.createTemplateFromFile('new-equipment-request.html'); + template.request = request; + template.sheetUrl = SpreadsheetApp.getActiveSpreadsheet().getUrl(); + let msg = template.evaluate(); + MailApp.sendEmail({ + to: REQUEST_NOTIFICATION_EMAIL, + subject: 'New equipment request', + htmlBody: msg.getContent(), + }); +} + +/** + * Sends an email notifying the requestor that the request is complete. + * + * @param {Object} request - Request details + */ +function sendEquipmentRequestCompletedEmail_(request) { + let template = HtmlService.createTemplateFromFile('request-complete.html'); + template.request = request; + let msg = template.evaluate(); + MailApp.sendEmail({ + to: request.email, + subject: 'Equipment request completed', + htmlBody: msg.getContent(), + }); +} + +/** + * Converts a form response to an object keyed by the item titles. Allows easier + * access to response values. + * + * @param {FormResponse} response + * @return {Object} Form values keyed by question title + */ +function mapResponse_(response) { + let initialValue = { + email: response.getRespondentEmail(), + timestamp: response.getTimestamp(), + }; + return response.getItemResponses().reduce(function(obj, itemResponse) { + let key = itemResponse.getItem().getTitle(); + obj[key] = itemResponse.getResponse(); + return obj; + }, initialValue); +} + diff --git a/solutions/automations/equipment-requests/README.md b/solutions/automations/equipment-requests/README.md new file mode 100644 index 000000000..b6e5d1009 --- /dev/null +++ b/solutions/automations/equipment-requests/README.md @@ -0,0 +1,3 @@ +# Manage new employee equipment requests + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/equipment-requests) for additional details. diff --git a/solutions/automations/equipment-requests/appsscript.json b/solutions/automations/equipment-requests/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/equipment-requests/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/equipment-requests/new-equipment-request.html b/solutions/automations/equipment-requests/new-equipment-request.html new file mode 100644 index 000000000..1b222ffbc --- /dev/null +++ b/solutions/automations/equipment-requests/new-equipment-request.html @@ -0,0 +1,19 @@ + + + +

    + A new equipment request has been made by . +

    + +

    + Employee name:
    + Desk location name:
    + Due date:
    + Laptop model:
    + Desktop model:
    + Monitor(s):
    +

    + + See the spreadsheet to take or assign this item. + + diff --git a/solutions/automations/equipment-requests/request-complete.html b/solutions/automations/equipment-requests/request-complete.html new file mode 100644 index 000000000..d5cd55a3d --- /dev/null +++ b/solutions/automations/equipment-requests/request-complete.html @@ -0,0 +1,13 @@ + + + +

    + An equipment request has been completed. +

    + +

    + Employee name:
    + Desk location name:
    +

    + + diff --git a/solutions/automations/event-session-signup/.clasp.json b/solutions/automations/event-session-signup/.clasp.json new file mode 100644 index 000000000..8b1357cc3 --- /dev/null +++ b/solutions/automations/event-session-signup/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1RTfpaBw-RYW8PTJsidiqXHRrqaKnwMWAK_nq4LnWk9xXKGJWi_bhexRj"} diff --git a/solutions/automations/event-session-signup/Code.js b/solutions/automations/event-session-signup/Code.js new file mode 100644 index 000000000..dda1bd589 --- /dev/null +++ b/solutions/automations/event-session-signup/Code.js @@ -0,0 +1,209 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/event-session-signup + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Inserts a custom menu when the spreadsheet opens. + */ +function onOpen() { + SpreadsheetApp.getUi().createMenu('Conference') + .addItem('Set up conference', 'setUpConference_') + .addToUi(); +} + +/** + * Uses the conference data in the spreadsheet to create + * Google Calendar events, a Google Form, and a trigger that allows the script + * to react to form responses. + */ +function setUpConference_() { + let scriptProperties = PropertiesService.getScriptProperties(); + if (scriptProperties.getProperty('calId')) { + Browser.msgBox('Your conference is already set up. Look in Google Drive for your' + + ' sign-up form!'); + return; + } + let ss = SpreadsheetApp.getActive(); + let sheet = ss.getSheetByName('Conference Setup'); + let range = sheet.getDataRange(); + let values = range.getValues(); + setUpCalendar_(values, range); + setUpForm_(ss, values); + ScriptApp.newTrigger('onFormSubmit').forSpreadsheet(ss).onFormSubmit() + .create(); +} + +/** + * Creates a Google Calendar with events for each conference session in the + * spreadsheet, then writes the event IDs to the spreadsheet for future use. + * @param {Array} values Cell values for the spreadsheet range. + * @param {Range} range A spreadsheet range that contains conference data. + */ +function setUpCalendar_(values, range) { + let cal = CalendarApp.createCalendar('Conference Calendar'); + // Start at 1 to skip the header row. + for (let i = 1; i < values.length; i++) { + let session = values[i]; + let title = session[0]; + let start = joinDateAndTime_(session[1], session[2]); + let end = joinDateAndTime_(session[1], session[3]); + let options = {location: session[4], sendInvites: true}; + let event = cal.createEvent(title, start, end, options) + .setGuestsCanSeeGuests(false); + session[5] = event.getId(); + } + range.setValues(values); + + // Stores the ID for the Calendar, which is needed to retrieve events by ID. + let scriptProperties = PropertiesService.getScriptProperties(); + scriptProperties.setProperty('calId', cal.getId()); +} + +/** + * Creates a single Date object from separate date and time cells. + * + * @param {Date} date A Date object from which to extract the date. + * @param {Date} time A Date object from which to extract the time. + * @return {Date} A Date object representing the combined date and time. + */ +function joinDateAndTime_(date, time) { + date = new Date(date); + date.setHours(time.getHours()); + date.setMinutes(time.getMinutes()); + return date; +} + +/** + * Creates a Google Form that allows respondents to select which conference + * sessions they would like to attend, grouped by date and start time in the + * caller's time zone. + * + * @param {Spreadsheet} ss The spreadsheet that contains the conference data. + * @param {Array} values Cell values for the spreadsheet range. + */ +function setUpForm_(ss, values) { + // Group the sessions by date and time so that they can be passed to the form. + let schedule = {}; + // Start at 1 to skip the header row. + for (let i = 1; i < values.length; i++) { + let session = values[i]; + let day = session[1].toLocaleDateString(); + let time = session[2].toLocaleTimeString(); + if (!schedule[day]) { + schedule[day] = {}; + } + if (!schedule[day][time]) { + schedule[day][time] = []; + } + schedule[day][time].push(session[0]); + } + + // Creates the form and adds a multiple-choice question for each timeslot. + let form = FormApp.create('Conference Form'); + form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()); + form.addTextItem().setTitle('Name').setRequired(true); + form.addTextItem().setTitle('Email').setRequired(true); + Object.keys(schedule).forEach(function(day) { + let header = form.addSectionHeaderItem().setTitle('Sessions for ' + day); + Object.keys(schedule[day]).forEach(function(time) { + let item = form.addMultipleChoiceItem().setTitle(time + ' ' + day) + .setChoiceValues(schedule[day][time]); + }); + }); +} + +/** + * Sends out calendar invitations and a + * personalized Google Docs itinerary after a user responds to the form. + * + * @param {Object} e The event parameter for form submission to a spreadsheet; + * see https://developers.google.com/apps-script/understanding_events + */ +function onFormSubmit(e) { + let user = {name: e.namedValues['Name'][0], email: e.namedValues['Email'][0]}; + + // Grab the session data again so that we can match it to the user's choices. + let response = []; + let values = SpreadsheetApp.getActive().getSheetByName('Conference Setup') + .getDataRange().getValues(); + for (let i = 1; i < values.length; i++) { + let session = values[i]; + let title = session[0]; + let day = session[1].toLocaleDateString(); + let time = session[2].toLocaleTimeString(); + let timeslot = time + ' ' + day; + + // For every selection in the response, find the matching timeslot and title + // in the spreadsheet and add the session data to the response array. + if (e.namedValues[timeslot] && e.namedValues[timeslot] == title) { + response.push(session); + } + } + sendInvites_(user, response); + sendDoc_(user, response); +} + +/** + * Add the user as a guest for every session he or she selected. + * @param {object} user An object that contains the user's name and email. + * @param {Array} response An array of data for the user's session choices. + */ +function sendInvites_(user, response) { + let id = ScriptProperties.getProperty('calId'); + let cal = CalendarApp.getCalendarById(id); + for (let i = 0; i < response.length; i++) { + cal.getEventSeriesById(response[i][5]).addGuest(user.email); + } +} + +/** + * Creates and shares a personalized Google Doc that shows the user's itinerary. + * @param {object} user An object that contains the user's name and email. + * @param {Array} response An array of data for the user's session choices. + */ +function sendDoc_(user, response) { + let doc = DocumentApp.create('Conference Itinerary for ' + user.name) + .addEditor(user.email); + let body = doc.getBody(); + let table = [['Session', 'Date', 'Time', 'Location']]; + for (let i = 0; i < response.length; i++) { + table.push([response[i][0], response[i][1].toLocaleDateString(), + response[i][2].toLocaleTimeString(), response[i][4]]); + } + body.insertParagraph(0, doc.getName()) + .setHeading(DocumentApp.ParagraphHeading.HEADING1); + table = body.appendTable(table); + table.getRow(0).editAsText().setBold(true); + doc.saveAndClose(); + + // Emails a link to the Doc as well as a PDF copy. + MailApp.sendEmail({ + to: user.email, + subject: doc.getName(), + body: 'Thanks for registering! Here\'s your itinerary: ' + doc.getUrl(), + attachments: doc.getAs(MimeType.PDF), + }); +} + +/** + * Removes the calId script property so that the 'setUpConference_()' can be run again. + */ +function resetProperties(){ + let scriptProperties = PropertiesService.getScriptProperties(); + scriptProperties.deleteAllProperties(); +} diff --git a/solutions/automations/event-session-signup/README.md b/solutions/automations/event-session-signup/README.md new file mode 100644 index 000000000..cafbec79a --- /dev/null +++ b/solutions/automations/event-session-signup/README.md @@ -0,0 +1,3 @@ +# Create a sign-up for sessions at a conference + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/event-session-signup) for additional details. diff --git a/solutions/automations/event-session-signup/appsscript.json b/solutions/automations/event-session-signup/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/event-session-signup/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/feedback-analysis/.clasp.json b/solutions/automations/feedback-sentiment-analysis/.clasp.json similarity index 100% rename from solutions/automations/feedback-analysis/.clasp.json rename to solutions/automations/feedback-sentiment-analysis/.clasp.json diff --git a/solutions/automations/feedback-analysis/README.md b/solutions/automations/feedback-sentiment-analysis/README.md similarity index 100% rename from solutions/automations/feedback-analysis/README.md rename to solutions/automations/feedback-sentiment-analysis/README.md diff --git a/solutions/automations/feedback-analysis/appsscript.json b/solutions/automations/feedback-sentiment-analysis/appsscript.json similarity index 100% rename from solutions/automations/feedback-analysis/appsscript.json rename to solutions/automations/feedback-sentiment-analysis/appsscript.json diff --git a/solutions/automations/feedback-analysis/code.js b/solutions/automations/feedback-sentiment-analysis/code.js similarity index 100% rename from solutions/automations/feedback-analysis/code.js rename to solutions/automations/feedback-sentiment-analysis/code.js diff --git a/solutions/automations/generate-pdfs/.clasp.json b/solutions/automations/generate-pdfs/.clasp.json new file mode 100644 index 000000000..77e4f7125 --- /dev/null +++ b/solutions/automations/generate-pdfs/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1k9PjGdQ_G0HKEoS3np_Szfe-flmLw9gUvblQIxOfvTmS-NLeLgVUzvOa"} diff --git a/solutions/automations/generate-pdfs/Code.js b/solutions/automations/generate-pdfs/Code.js new file mode 100644 index 000000000..07e77a86b --- /dev/null +++ b/solutions/automations/generate-pdfs/Code.js @@ -0,0 +1,263 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/generate-pdfs + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// TODO: To test this solution, set EMAIL_OVERRIDE to true and set EMAIL_ADDRESS_OVERRIDE to your email address. +const EMAIL_OVERRIDE = false; +const EMAIL_ADDRESS_OVERRIDE = 'test@example.com'; + +// Application constants +const APP_TITLE = 'Generate and send PDFs'; +const OUTPUT_FOLDER_NAME = "Customer PDFs"; +const DUE_DATE_NUM_DAYS = 15 + +// Sheet name constants. Update if you change the names of the sheets. +const CUSTOMERS_SHEET_NAME = 'Customers'; +const PRODUCTS_SHEET_NAME = 'Products'; +const TRANSACTIONS_SHEET_NAME = 'Transactions'; +const INVOICES_SHEET_NAME = 'Invoices'; +const INVOICE_TEMPLATE_SHEET_NAME = 'Invoice Template'; + +// Email constants +const EMAIL_SUBJECT = 'Invoice Notification'; +const EMAIL_BODY = 'Hello!\rPlease see the attached PDF document.'; + + +/** + * Iterates through the worksheet data populating the template sheet with + * customer data, then saves each instance as a PDF document. + * + * Called by user via custom menu item. + */ +function processDocuments() { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const customersSheet = ss.getSheetByName(CUSTOMERS_SHEET_NAME); + const productsSheet = ss.getSheetByName(PRODUCTS_SHEET_NAME); + const transactionsSheet = ss.getSheetByName(TRANSACTIONS_SHEET_NAME); + const invoicesSheet = ss.getSheetByName(INVOICES_SHEET_NAME); + const invoiceTemplateSheet = ss.getSheetByName(INVOICE_TEMPLATE_SHEET_NAME); + + // Gets data from the storage sheets as objects. + const customers = dataRangeToObject(customersSheet); + const products = dataRangeToObject(productsSheet); + const transactions = dataRangeToObject(transactionsSheet); + + ss.toast('Creating Invoices', APP_TITLE, 1); + const invoices = []; + + // Iterates for each customer calling createInvoiceForCustomer routine. + customers.forEach(function (customer) { + ss.toast(`Creating Invoice for ${customer.customer_name}`, APP_TITLE, 1); + let invoice = createInvoiceForCustomer( + customer, products, transactions, invoiceTemplateSheet, ss.getId()); + invoices.push(invoice); + }); + // Writes invoices data to the sheet. + invoicesSheet.getRange(2, 1, invoices.length, invoices[0].length).setValues(invoices); +} + +/** + * Processes each customer instance with passed in data parameters. + * + * @param {object} customer - Object for the customer + * @param {object} products - Object for all the products + * @param {object} transactions - Object for all the transactions + * @param {object} invoiceTemplateSheet - Object for the invoice template sheet + * @param {string} ssId - Google Sheet ID + * Return {array} of instance customer invoice data + */ +function createInvoiceForCustomer(customer, products, transactions, templateSheet, ssId) { + let customerTransactions = transactions.filter(function (transaction) { + return transaction.customer_name == customer.customer_name; + }); + + // Clears existing data from the template. + clearTemplateSheet(); + + let lineItems = []; + let totalAmount = 0; + customerTransactions.forEach(function (lineItem) { + let lineItemProduct = products.filter(function (product) { + return product.sku_name == lineItem.sku; + })[0]; + const qty = parseInt(lineItem.licenses); + const price = parseFloat(lineItemProduct.price).toFixed(2); + const amount = parseFloat(qty * price).toFixed(2); + lineItems.push([lineItemProduct.sku_name, lineItemProduct.sku_description, '', qty, price, amount]); + totalAmount += parseFloat(amount); + }); + + // Generates a random invoice number. You can replace with your own document ID method. + const invoiceNumber = Math.floor(100000 + Math.random() * 900000); + + // Calulates dates. + const todaysDate = new Date().toDateString() + const dueDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * DUE_DATE_NUM_DAYS).toDateString() + + // Sets values in the template. + templateSheet.getRange('B10').setValue(customer.customer_name) + templateSheet.getRange('B11').setValue(customer.address) + templateSheet.getRange('F10').setValue(invoiceNumber) + templateSheet.getRange('F12').setValue(todaysDate) + templateSheet.getRange('F14').setValue(dueDate) + templateSheet.getRange(18, 2, lineItems.length, 6).setValues(lineItems); + + // Cleans up and creates PDF. + SpreadsheetApp.flush(); + Utilities.sleep(500); // Using to offset any potential latency in creating .pdf + const pdf = createPDF(ssId, templateSheet, `Invoice#${invoiceNumber}-${customer.customer_name}`); + return [invoiceNumber, todaysDate, customer.customer_name, customer.email, '', totalAmount, dueDate, pdf.getUrl(), 'No']; +} + +/** +* Resets the template sheet by clearing out customer data. +* You use this to prepare for the next iteration or to view blank +* the template for design. +* +* Called by createInvoiceForCustomer() or by the user via custom menu item. +*/ +function clearTemplateSheet() { + + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const templateSheet = ss.getSheetByName(INVOICE_TEMPLATE_SHEET_NAME); + // Clears existing data from the template. + const rngClear = templateSheet.getRangeList(['B10:B11', 'F10', 'F12', 'F14']).getRanges() + rngClear.forEach(function (cell) { + cell.clearContent(); + }); + // This sample only accounts for six rows of data 'B18:G24'. You can extend or make dynamic as necessary. + templateSheet.getRange(18, 2, 7, 6).clearContent(); +} + +/** + * Creates a PDF for the customer given sheet. + * @param {string} ssId - Id of the Google Spreadsheet + * @param {object} sheet - Sheet to be converted as PDF + * @param {string} pdfName - File name of the PDF being created + * @return {file object} PDF file as a blob + */ +function createPDF(ssId, sheet, pdfName) { + const fr = 0, fc = 0, lc = 9, lr = 27; + const url = "https://docs.google.com/spreadsheets/d/" + ssId + "/export" + + "?format=pdf&" + + "size=7&" + + "fzr=true&" + + "portrait=true&" + + "fitw=true&" + + "gridlines=false&" + + "printtitle=false&" + + "top_margin=0.5&" + + "bottom_margin=0.25&" + + "left_margin=0.5&" + + "right_margin=0.5&" + + "sheetnames=false&" + + "pagenum=UNDEFINED&" + + "attachment=true&" + + "gid=" + sheet.getSheetId() + '&' + + "r1=" + fr + "&c1=" + fc + "&r2=" + lr + "&c2=" + lc; + + const params = { method: "GET", headers: { "authorization": "Bearer " + ScriptApp.getOAuthToken() } }; + const blob = UrlFetchApp.fetch(url, params).getBlob().setName(pdfName + '.pdf'); + + // Gets the folder in Drive where the PDFs are stored. + const folder = getFolderByName_(OUTPUT_FOLDER_NAME); + + const pdfFile = folder.createFile(blob); + return pdfFile; +} + + +/** + * Sends emails with PDF as an attachment. + * Checks/Sets 'Email Sent' column to 'Yes' to avoid resending. + * + * Called by user via custom menu item. + */ +function sendEmails() { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const invoicesSheet = ss.getSheetByName(INVOICES_SHEET_NAME); + const invoicesData = invoicesSheet.getRange(1, 1, invoicesSheet.getLastRow(), invoicesSheet.getLastColumn()).getValues(); + const keysI = invoicesData.splice(0, 1)[0]; + const invoices = getObjects(invoicesData, createObjectKeys(keysI)); + ss.toast('Emailing Invoices', APP_TITLE, 1); + invoices.forEach(function (invoice, index) { + + if (invoice.email_sent != 'Yes') { + ss.toast(`Emailing Invoice for ${invoice.customer}`, APP_TITLE, 1); + + const fileId = invoice.invoice_link.match(/[-\w]{25,}(?!.*[-\w]{25,})/) + const attachment = DriveApp.getFileById(fileId); + + let recipient = invoice.email; + if (EMAIL_OVERRIDE) { + recipient = EMAIL_ADDRESS_OVERRIDE + } + + GmailApp.sendEmail(recipient, EMAIL_SUBJECT, EMAIL_BODY, { + attachments: [attachment.getAs(MimeType.PDF)], + name: APP_TITLE + }); + invoicesSheet.getRange(index + 2, 9).setValue('Yes'); + } + }); +} + +/** + * Helper function that turns sheet data range into an object. + * + * @param {SpreadsheetApp.Sheet} sheet - Sheet to process + * Return {object} of a sheet's datarange as an object + */ +function dataRangeToObject(sheet) { + const dataRange = sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn()).getValues(); + const keys = dataRange.splice(0, 1)[0]; + return getObjects(dataRange, createObjectKeys(keys)); +} + +/** + * Utility function for mapping sheet data to objects. + */ +function getObjects(data, keys) { + let objects = []; + for (let i = 0; i < data.length; ++i) { + let object = {}; + let hasData = false; + for (let j = 0; j < data[i].length; ++j) { + let cellData = data[i][j]; + if (isCellEmpty(cellData)) { + continue; + } + object[keys[j]] = cellData; + hasData = true; + } + if (hasData) { + objects.push(object); + } + } + return objects; +} +// Creates object keys for column headers. +function createObjectKeys(keys) { + return keys.map(function (key) { + return key.replace(/\W+/g, '_').toLowerCase(); + }); +} +// Returns true if the cell where cellData was read from is empty. +function isCellEmpty(cellData) { + return typeof (cellData) == "string" && cellData == ""; +} diff --git a/solutions/automations/generate-pdfs/Menu.js b/solutions/automations/generate-pdfs/Menu.js new file mode 100644 index 000000000..e162337fd --- /dev/null +++ b/solutions/automations/generate-pdfs/Menu.js @@ -0,0 +1,24 @@ +/** + * @OnlyCurrentDoc + * + * The above comment specifies that this automation will only + * attempt to read or modify the spreadsheet this script is bound to. + * The authorization request message presented to users reflects the + * limited scope. + */ + +/** + * Creates a custom menu in the Google Sheets UI when the document is opened. + * + * @param {object} e The event parameter for a simple onOpen trigger. + */ +function onOpen(e) { + +const menu = SpreadsheetApp.getUi().createMenu(APP_TITLE) + menu + .addItem('Process invoices', 'processDocuments') + .addItem('Send emails', 'sendEmails') + .addSeparator() + .addItem('Reset template', 'clearTemplateSheet') + .addToUi(); +} \ No newline at end of file diff --git a/solutions/automations/generate-pdfs/README.md b/solutions/automations/generate-pdfs/README.md new file mode 100644 index 000000000..b259bedf7 --- /dev/null +++ b/solutions/automations/generate-pdfs/README.md @@ -0,0 +1,3 @@ +# Generate and send PDFs from Google Sheets + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/generate-pdfs) for additional details. diff --git a/solutions/automations/generate-pdfs/Utilities.js b/solutions/automations/generate-pdfs/Utilities.js new file mode 100644 index 000000000..92cc01e6c --- /dev/null +++ b/solutions/automations/generate-pdfs/Utilities.js @@ -0,0 +1,44 @@ +/** + * Returns a Google Drive folder in the same location + * in Drive where the spreadsheet is located. First, it checks if the folder + * already exists and returns that folder. If the folder doesn't already + * exist, the script creates a new one. The folder's name is set by the + * "OUTPUT_FOLDER_NAME" variable from the Code.gs file. + * + * @param {string} folderName - Name of the Drive folder. + * @return {object} Google Drive Folder + */ +function getFolderByName_(folderName) { + + // Gets the Drive Folder of where the current spreadsheet is located. + const ssId = SpreadsheetApp.getActiveSpreadsheet().getId(); + const parentFolder = DriveApp.getFileById(ssId).getParents().next(); + + // Iterates the subfolders to check if the PDF folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === folderName) { + return folder; + } + } + // Creates a new folder if one does not already exist. + return parentFolder.createFolder(folderName) + .setDescription(`Created by ${APP_TITLE} application to store PDF output files`); +} + +/** + * Test function to run getFolderByName_. + * @prints a Google Drive FolderId. + */ +function test_getFolderByName() { + + // Gets the PDF folder in Drive. + const folder = getFolderByName_(OUTPUT_FOLDER_NAME); + + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rDescription: ${folder.getDescription()}`) + // To automatically delete test folder, uncomment the following code: + // folder.setTrashed(true); +} \ No newline at end of file diff --git a/solutions/automations/generate-pdfs/appsscript.json b/solutions/automations/generate-pdfs/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/generate-pdfs/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/import-csv-sheets/.clasp.json b/solutions/automations/import-csv-sheets/.clasp.json new file mode 100644 index 000000000..f6b8887eb --- /dev/null +++ b/solutions/automations/import-csv-sheets/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1ANsCqbcTeepCzPpAKRUSxavm-2bTtKhp6I-G530ddH315H-59LGofc6m"} diff --git a/solutions/automations/import-csv-sheets/Code.js b/solutions/automations/import-csv-sheets/Code.js new file mode 100644 index 000000000..2b89d8253 --- /dev/null +++ b/solutions/automations/import-csv-sheets/Code.js @@ -0,0 +1,191 @@ +// To learn more about this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/import-csv-sheets + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This file contains the main functions that import data from CSV files into a Google Spreadsheet. + */ + +// Application constants +const APP_TITLE = 'Trigger-driven CSV import [App Script Sample]'; // Application name +const APP_FOLDER = '[App Script sample] Import CSVs'; // Application primary folder +const SOURCE_FOLDER = 'Inbound CSV Files'; // Folder for the update files. +const PROCESSED_FOLDER = 'Processed CSV Files'; // Folder to hold processed files. +const SHEET_REPORT_NAME = 'Import CSVs'; // Name of destination spreadsheet. + +// Application settings +const CSV_HEADER_EXIST = true; // Set to true if CSV files have a header row, false if not. +const HANDLER_FUNCTION = 'updateApplicationSheet'; // Function called by installable trigger to run data processing. + +/** + * Installs a time-driven trigger that runs daily to import CSVs into the main application spreadsheet. + * Prior to creating a new instance, removes any existing triggers to avoid duplication. + * + * Called by setupSample() or run directly setting up the application. + */ +function installTrigger() { + + // Checks for an existing trigger to avoid creating duplicate instances. + // Removes existing if found. + const projectTriggers = ScriptApp.getProjectTriggers(); + for (var i = 0; i < projectTriggers.length; i++) { + if (projectTriggers[i].getHandlerFunction() == HANDLER_FUNCTION) { + console.log(`Existing trigger with Handler Function of '${HANDLER_FUNCTION}' removed.`); + ScriptApp.deleteTrigger(projectTriggers[i]); + } + } + // Creates the new trigger. + let newTrigger = ScriptApp.newTrigger(HANDLER_FUNCTION) + .timeBased() + .atHour(23) // Runs at 11 PM in the time zone of this script. + .everyDays(1) // Runs once per day. + .create(); + console.log(`New trigger with Handler Function of '${HANDLER_FUNCTION}' created.`); +} + +/** + * Handler function called by the trigger created with the "installTrigger" function. + * Run this directly to execute the entire automation process of the application with a trigger. + * + * Process: Iterates through CSV files located in the source folder (SOURCE_FOLDER), + * and appends them to the end of destination spreadsheet (SHEET_REPORT_NAME). + * Successfully processed CSV files are moved to the processed folder (PROCESSED_FOLDER) to avoid duplication. + * Sends summary email with status of the import. + */ +function updateApplicationSheet() { + + // Gets application & supporting folders. + const folderAppPrimary = getApplicationFolder_(APP_FOLDER); + const folderSource = getFolder_(SOURCE_FOLDER); + const folderProcessed = getFolder_(PROCESSED_FOLDER); + + // Gets the application's destination spreadsheet {Spreadsheet object} + let objSpreadSheet = getSpreadSheet_(SHEET_REPORT_NAME, folderAppPrimary) + + // Creates arrays to track every CSV file, categorized as processed sucessfully or not. + let filesProcessed = []; + let filesNotProcessed = []; + + // Gets all CSV files found in the source folder. + let cvsFiles = folderSource.getFilesByType(MimeType.CSV); + + // Iterates through each CSV file. + while (cvsFiles.hasNext()) { + + let csvFile = cvsFiles.next(); + let isSuccess; + + // Appends the unprocessed CSV data into the Google Sheets spreadsheet. + isSuccess = processCsv_(objSpreadSheet, csvFile); + + if (isSuccess) { + // Moves the processed file to the processed folder to prevent future duplicate data imports. + csvFile.moveTo(folderProcessed); + // Logs the successfully processed file to the filesProcessed array. + filesProcessed.push(csvFile.getName()); + console.log(`Successfully processed: ${csvFile.getName()}`); + + } else if (!isSuccess) { + // Doesn't move the unsuccesfully processed file so that it can be corrected and reprocessed later. + // Logs the unsuccessfully processed file to the filesNotProcessed array. + filesNotProcessed.push(csvFile.getName()); + console.log(`Not processed: ${csvFile.getName()}`); + } + } + + // Prepares summary email. + // Gets variables to link to this Apps Script project. + const scriptId = ScriptApp.getScriptId(); + const scriptUrl = DriveApp.getFileById(scriptId).getUrl(); + const scriptName = DriveApp.getFileById(scriptId).getName(); + + // Gets variables to link to the main application spreadsheet. + const sheetUrl = objSpreadSheet.getUrl() + const sheetName = objSpreadSheet.getName() + + // Gets user email and timestamp. + const emailTo = Session.getEffectiveUser().getEmail(); + const timestamp = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss zzzz"); + + // Prepares lists and counts of processed CSV files. + let processedList = ""; + const processedCount = filesProcessed.length + for (const processed of filesProcessed) { + processedList += processed + '
    ' + }; + + const unProcessedCount = filesNotProcessed.length + let unProcessedList = ""; + for (const unProcessed of filesNotProcessed) { + unProcessedList += unProcessed + '\n' + }; + + // Assembles email body as html. + const eMailBody = `${APP_TITLE} ran an automated process at ${timestamp}.

    ` + + `Files successfully updated: ${processedCount}
    ` + + `${processedList}
    ` + + `Files not updated: ${unProcessedCount}
    ` + + `${unProcessedList}
    ` + + `
    View all updates in the Google Sheets spreadsheet ` + + `${sheetName}.
    ` + + `
    *************
    ` + + `
    This email was generated by Google Apps Script. ` + + `To learn more about this application or make changes, open the script project below:
    ` + + `${scriptName}` + + MailApp.sendEmail({ + to: emailTo, + subject: `Automated email from ${APP_TITLE}`, + htmlBody: eMailBody + }); + console.log(`Email sent to ${emailTo}`); +} + +/** + * Parses CSV data into an array and appends it after the last row in the destination spreadsheet. + * + * @return {boolean} true if the update is successful, false if unexpected errors occur. + */ +function processCsv_(objSpreadSheet, csvFile) { + + try { + // Gets the first sheet of the destination spreadsheet. + let sheet = objSpreadSheet.getSheets()[0]; + + // Parses CSV file into data array. + let data = Utilities.parseCsv(csvFile.getBlob().getDataAsString()); + + // Omits header row if application variable CSV_HEADER_EXIST is set to 'true'. + if (CSV_HEADER_EXIST) { + data.splice(0, 1); + } + // Gets the row and column coordinates for next available range in the spreadsheet. + let startRow = sheet.getLastRow() + 1; + let startCol = 1; + // Determines the incoming data size. + let numRows = data.length; + let numColumns = data[0].length; + + // Appends data into the sheet. + sheet.getRange(startRow, startCol, numRows, numColumns).setValues(data); + return true; // Success. + + } catch { + return false; // Failure. Checks for CSV data file error. + } +} diff --git a/solutions/automations/import-csv-sheets/README.md b/solutions/automations/import-csv-sheets/README.md new file mode 100644 index 000000000..1334e63f2 --- /dev/null +++ b/solutions/automations/import-csv-sheets/README.md @@ -0,0 +1,3 @@ +# Import CSV data to a spreadsheet + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/import-csv-sheets) for additional details. diff --git a/solutions/automations/import-csv-sheets/SampleData.js b/solutions/automations/import-csv-sheets/SampleData.js new file mode 100644 index 000000000..61d89f4ee --- /dev/null +++ b/solutions/automations/import-csv-sheets/SampleData.js @@ -0,0 +1,174 @@ +/** + * This file contains functions to access headings and data for sample files. + * + * Sample data is stored in the variable SAMPLE_DATA. + */ + +// Fictitious sample data. +const SAMPLE_DATA = { + "headings": [ + "PropertyName", + "LeaseID", + "LeaseLocation", + "OwnerName", + "SquareFootage", + "RenewDate", + "LastAmount", + "LastPaymentDate", + "Revenue" + ], + "csvFiles": [ + { + "name": "Sample One.CSV", + "rows": [ + { + "PropertyName": "The Modern Building", + "LeaseID": "271312", + "LeaseLocation": "Mountain View CA 94045", + "OwnerName": "Yuri", + "SquareFootage": "17500", + "RenewDate": "12/15/2022", + "LastAmount": "100000", + "LastPaymentDate": "3/01/2022", + "Revenue": "12000" + }, + { + "PropertyName": "Garage @ 45", + "LeaseID": "271320", + "LeaseLocation": "Mountain View CA 94045", + "OwnerName": "Luka", + "SquareFootage": "1000", + "RenewDate": "6/2/2022", + "LastAmount": "50000", + "LastPaymentDate": "4/01/2022", + "Revenue": "20000" + }, + { + "PropertyName": "Office Park Deluxe", + "LeaseID": "271301", + "LeaseLocation": "Mountain View CA 94045", + "OwnerName": "Sasha", + "SquareFootage": "5000", + "RenewDate": "6/2/2022", + "LastAmount": "25000", + "LastPaymentDate": "4/01/2022", + "Revenue": "1200" + } + ] + }, + { + "name": "Sample Two.CSV", + "rows": [ + { + "PropertyName": "Tours Jumelles Minuscules", + "LeaseID": "271260", + "LeaseLocation": "8 Rue du Nom Fictif 341 Paris", + "OwnerName": "Lucian", + "SquareFootage": "1000000", + "RenewDate": "7/14/2022", + "LastAmount": "1250000", + "LastPaymentDate": "5/01/2022", + "Revenue": "77777" + }, + { + "PropertyName": "Barraca da Praia", + "LeaseID": "271281", + "LeaseLocation": "Avenida da Pastelaria 1903 Lisbon 1229-076", + "OwnerName": "Raha", + "SquareFootage": "1000", + "RenewDate": "6/2/2022", + "LastAmount": "50000", + "LastPaymentDate": "4/01/2022", + "Revenue": "20000" + } + ] + }, + { + "name": "Sample Three.CSV", + "rows": [ + { + "PropertyName": "Round Building in the Square", + "LeaseID": "371260", + "LeaseLocation": "8 Rue du Nom Fictif 341 Paris", + "OwnerName": "Charlie", + "SquareFootage": "75000", + "RenewDate": "8/1/2022", + "LastAmount": "250000", + "LastPaymentDate": "6/01/2022", + "Revenue": "22222" + }, + { + "PropertyName": "Square Building in the Round", + "LeaseID": "371281", + "LeaseLocation": "Avenida da Pastelaria 1903 Lisbon 1229-076", + "OwnerName": "Lee", + "SquareFootage": "10000", + "RenewDate": "6/2/2022", + "LastAmount": "5000", + "LastPaymentDate": "4/01/2022", + "Revenue": "1800" + } + ] + } + ] +} + + +/** + * Returns headings for use in destination spreadsheet and CSV files. + * @return {string[][]} array of each column heading as string. + */ +function getHeadings() { + let headings = [[]]; + for (let i in SAMPLE_DATA.headings) + headings[0].push(SAMPLE_DATA.headings[i]); + return (headings) +} + +/** + * Returns CSV file names and content to create sample CSV files. + * @return {object[]} {"file": ["name","csv"]} + */ +function getCSVFilesData() { + + let files = []; + + // Gets headings once - same for all files/rows. + let csvHeadings = ""; + for (let i in SAMPLE_DATA.headings) + csvHeadings += (SAMPLE_DATA.headings[i] + ','); + + // Gets data for each file by rows. + for (let i in SAMPLE_DATA.csvFiles) { + let sampleCSV = ""; + sampleCSV += csvHeadings; + let fileName = SAMPLE_DATA.csvFiles[i].name + for (let j in SAMPLE_DATA.csvFiles[i].rows) { + sampleCSV += '\n' + for (let k in SAMPLE_DATA.csvFiles[i].rows[j]) { + sampleCSV += SAMPLE_DATA.csvFiles[i].rows[j][k] + ',' + } + } + files.push({ name: fileName, csv: sampleCSV }) + } + return (files) +} + +/* + * Checks data functions are working as necessary. + */ +function test_getHeadings() { + let h = getHeadings() + console.log(h); + console.log(h[0].length); +} + +function test_getCSVFilesData() { + const csvFiles = getCSVFilesData(); + console.log(csvFiles) + + for (const file of csvFiles) { + console.log(file.name) + console.log(file.csv) + } +} \ No newline at end of file diff --git a/solutions/automations/import-csv-sheets/SetupSample.js b/solutions/automations/import-csv-sheets/SetupSample.js new file mode 100644 index 000000000..da9d28e47 --- /dev/null +++ b/solutions/automations/import-csv-sheets/SetupSample.js @@ -0,0 +1,95 @@ +/** + * This file contains functions that set up the folders and sample files used to demo the application. + * + * Sample data for the application is stored in the SampleData.gs file. + */ + +// Global variables for sample setup. +const INCLUDE_SAMPLE_DATA_FILES = true; // Set to true to create sample data files, false to skip. + +/** + * Runs the setup for the sample. + * 1) Creates the application folder and subfolders for unprocessed/processed CSV files. + * from global variables APP_FOLDER | SOURCE_FOLDER | PROCESSED_FOLDER + * 2) Creates the sample Sheets spreadsheet in the application folder. + * from global variable SHEET_REPORT_NAME + * 3) Creates CSV files from sample data in the unprocessed files folder. + * from variable SAMPLE_DATA in SampleData.gs. + * 4) Creates an installable trigger to run process automatically at a specified time interval. + */ +function setupSample() { + + console.log(`Application setup for: ${APP_TITLE}`) + + // Creates application folder. + const folderAppPrimary = getApplicationFolder_(APP_FOLDER); + // Creates supporting folders. + const folderSource = getFolder_(SOURCE_FOLDER); + const folderProcessed = getFolder_(PROCESSED_FOLDER); + + console.log(`Application folders: ${folderAppPrimary.getName()}, ${folderSource.getName()}, ${folderProcessed.getName()}`) + + if (INCLUDE_SAMPLE_DATA_FILES) { + + // Sets up primary destination spreadsheet + const sheet = setupPrimarySpreadsheet_(folderAppPrimary); + + // Gets the CSV files data - refer to the SampleData.gs file to view. + const csvFiles = getCSVFilesData(); + + // Processes each CSV file. + for (const file of csvFiles) { + // Creates CSV file in source folder if it doesn't exist. + if (!fileExists_(file.name, folderSource)) { + let csvFileId = DriveApp.createFile(file.name, file.csv, MimeType.CSV); + console.log(`Created Sample CSV: ${file.name}`) + csvFileId.moveTo(folderSource); + } + } + } + // Installs (or recreates) project trigger + installTrigger() + + console.log(`Setup completed for: ${APP_TITLE}`) +} + +/** + * + */ +function setupPrimarySpreadsheet_(folderAppPrimary) { + + // Creates the report destination spreadsheet if doesn't exist. + if (!fileExists_(SHEET_REPORT_NAME, folderAppPrimary)) { + + // Creates new destination spreadsheet (report) with cell size of 20 x 10. + const sheet = SpreadsheetApp.create(SHEET_REPORT_NAME, 20, 10); + + // Adds the sample data headings. + let sheetHeadings = getHeadings(); + sheet.getSheets()[0].getRange(1, 1, 1, sheetHeadings[0].length).setValues(sheetHeadings); + SpreadsheetApp.flush(); + // Moves to primary application root folder. + DriveApp.getFileById(sheet.getId()).moveTo(folderAppPrimary) + + console.log(`Created file: ${SHEET_REPORT_NAME} In folder: ${folderAppPrimary.getName()}.`) + return sheet; + } +} + +/** + * Moves sample content to Drive trash & uninstalls trigger. + * This function removes all folders and content related to this application. + */ +function removeSample() { + getApplicationFolder_(APP_FOLDER).setTrashed(true); + console.log(`'${APP_FOLDER}' contents have been moved to Drive Trash folder.`) + + // Removes existing trigger if found. + const projectTriggers = ScriptApp.getProjectTriggers(); + for (var i = 0; i < projectTriggers.length; i++) { + if (projectTriggers[i].getHandlerFunction() == HANDLER_FUNCTION) { + console.log(`Existing trigger with handler function of '${HANDLER_FUNCTION}' removed.`); + ScriptApp.deleteTrigger(projectTriggers[i]); + } + } +} \ No newline at end of file diff --git a/solutions/automations/import-csv-sheets/Utilities.js b/solutions/automations/import-csv-sheets/Utilities.js new file mode 100644 index 000000000..7e84ee23b --- /dev/null +++ b/solutions/automations/import-csv-sheets/Utilities.js @@ -0,0 +1,126 @@ +/** + * This file contains utility functions that work with application's folder and files. + */ + +/** + * Gets application destination spreadsheet from a given folder + * Returns new sample version if orignal is not found. + * + * @param {string} fileName - Name of the file to test for. + * @param {object} objFolder - Folder object in which to search. + * @return {object} Spreadsheet object. + */ +function getSpreadSheet_(fileName, objFolder) { + + let files = objFolder.getFilesByName(fileName); + + while (files.hasNext()) { + let file = files.next(); + let fileId = file.getId(); + + const existingSpreadsheet = SpreadsheetApp.openById(fileId); + return existingSpreadsheet; + } + + // If application destination spreadsheet is missing, creates a new sample version. + const folderAppPrimary = getApplicationFolder_(APP_FOLDER); + const sampleSheet = setupPrimarySpreadsheet_(folderAppPrimary); + return sampleSheet; +} + +/** + * Tests if a file exists within a given folder. + * + * @param {string} fileName - Name of the file to test for. + * @param {object} objFolder - Folder object in which to search. + * @return {boolean} true if found in folder, false if not. + */ +function fileExists_(fileName, objFolder) { + + let files = objFolder.getFilesByName(fileName); + + while (files.hasNext()) { + let file = files.next(); + console.log(`${file.getName()} already exists.`) + return true; + } + return false; +} + +/** + * Returns folder named in folderName parameter. + * Checks if folder already exists, creates it if it doesn't. + * + * @param {string} folderName - Name of the Drive folder. + * @return {object} Google Drive Folder + */ +function getFolder_(folderName) { + + // Gets the primary folder for the application. + const parentFolder = getApplicationFolder_(); + + // Iterates subfolders to check if folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === folderName) { + return folder; + } + } + // Creates a new folder if one doesn't already exist. + return parentFolder.createFolder(folderName) + .setDescription(`Supporting folder created by ${APP_TITLE}.`); +} + +/** + * Returns the primary folder as named by the APP_FOLDER variable in the Code.gs file. + * Checks if folder already exists to avoid duplication. + * Creates new instance if existing folder not found. + * + * @return {object} Google Drive Folder + */ +function getApplicationFolder_() { + + // Gets root folder, currently set to 'My Drive' + const parentFolder = DriveApp.getRootFolder(); + + // Iterates through the subfolders to check if folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === APP_FOLDER) { + return folder; + } + } + // Creates a new folder if one doesn't already exist. + return parentFolder.createFolder(APP_FOLDER) + .setDescription(`Main application folder created by ${APP_TITLE}.`); +} + +/** + * Tests getApplicationFolder_ and getFolder_ + * @logs details of created Google Drive folder. + */ +function test_getFolderByName() { + + let folder = getApplicationFolder_() + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`) + // Uncomment the following to automatically delete test folder. + // folder.setTrashed(true); + + folder = getFolder_(SOURCE_FOLDER); + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`) + // Uncomment the following to automatically delete test folder. + // folder.setTrashed(true); + + folder = getFolder_(PROCESSED_FOLDER); + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`) + // Uncomment the following to automatically delete test folder. + // folder.setTrashed(true); + + +} \ No newline at end of file diff --git a/solutions/automations/import-csv-sheets/appsscript.json b/solutions/automations/import-csv-sheets/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/import-csv-sheets/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/offsite-activity-signup/.clasp.json b/solutions/automations/offsite-activity-signup/.clasp.json new file mode 100644 index 000000000..55715ed13 --- /dev/null +++ b/solutions/automations/offsite-activity-signup/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "10clpAH4ojSXvTlZaE74rhJ6dDwwkfvi24L_AilGROca5Nds2Jy2oZmvY"} diff --git a/solutions/automations/offsite-activity-signup/Code.js b/solutions/automations/offsite-activity-signup/Code.js new file mode 100644 index 000000000..5cac3f583 --- /dev/null +++ b/solutions/automations/offsite-activity-signup/Code.js @@ -0,0 +1,458 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/offsite-activity-signup + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const NUM_ITEMS_TO_RANK = 5; +const ACTIVITIES_PER_PERSON = 2; +const NUM_TEST_USERS = 150; + +/** + * Adds custom menu items when opening the sheet. + */ +function onOpen() { + let menu = SpreadsheetApp.getUi().createMenu('Activities') + .addItem('Create form', 'buildForm_') + .addItem('Generate test data', 'generateTestData_') + .addItem('Assign activities', 'assignActivities_') + .addToUi(); +} + +/** + * Builds a form based on the "Activity Schedule" sheet. The form asks attendees to rank their top + * N choices of activities, where N is defined by NUM_ITEMS_TO_RANK. + */ +function buildForm_() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + if (ss.getFormUrl()) { + let msg = 'Form already exists. Unlink the form and try again.'; + SpreadsheetApp.getUi().alert(msg); + return; + } + let form = FormApp.create('Activity Signup') + .setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()) + .setAllowResponseEdits(true) + .setLimitOneResponsePerUser(true) + .setCollectEmail(true); + let sectionHelpText = Utilities.formatString('Please choose your top %d activities', + NUM_ITEMS_TO_RANK); + form.addSectionHeaderItem() + .setTitle('Activity choices') + .setHelpText(sectionHelpText); + + // Presents activity ranking as a form grid with each activity as a row and rank as a column. + let rows = loadActivitySchedule_(ss).map(function(activity) { + return activity.description; + }); + let columns = range_(1, NUM_ITEMS_TO_RANK).map(function(value) { + return Utilities.formatString('%s', toOrdinal_(value)); + }); + let gridValidation = FormApp.createGridValidation() + .setHelpText('Select one item per column.') + .requireLimitOneResponsePerColumn() + .build(); + form.addGridItem() + .setColumns(columns) + .setRows(rows) + .setValidation(gridValidation); + + form.addListItem() + .setTitle('Assign other activities if choices are not available?') + .setChoiceValues(['Yes', 'No']); +} + +/** + * Assigns activities using a random priority/random serial dictatorship approach. The results + * are then populated into two new sheets, one listing activities per person, the other listing + * the rosters for each activity. + * + * See https://en.wikipedia.org/wiki/Random_serial_dictatorship for additional information. + */ +function assignActivities_() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let activities = loadActivitySchedule_(ss); + let activityIds = activities.map(function(activity) { + return activity.id; + }); + let attendees = loadAttendeeResponses_(ss, activityIds); + assignWithRandomPriority_(attendees, activities, 2); + writeAttendeeAssignments_(ss, attendees); + writeActivityRosters_(ss, activities); +} + +/** + * Selects activities via random priority. + * + * @param {object[]} attendees - Array of attendees to assign activities to + * @param {object[]} activities - Array of all available activities + * @param {number} numActivitiesPerPerson - Maximum number of activities to assign + */ +function assignWithRandomPriority_(attendees, activities, numActivitiesPerPerson) { + let activitiesById = activities.reduce(function(obj, activity) { + obj[activity.id] = activity; + return obj; + }, {}); + for (let i = 0; i < numActivitiesPerPerson; ++i) { + let randomizedAttendees = shuffleArray_(attendees); + randomizedAttendees.forEach(function(attendee) { + makeChoice_(attendee, activitiesById); + }); + } +} + +/** + * Attempts to assign an activity for an attendee based on their preferences and current schedule. + * + * @param {object} attendee - Attendee looking to join an activity + * @param {object} activitiesById - Map of all available activities + */ +function makeChoice_(attendee, activitiesById) { + for (let i = 0; i < attendee.preferences.length; ++i) { + let activity = activitiesById[attendee.preferences[i]]; + if (!activity) { + continue; + } + let canJoin = checkAvailability_(attendee, activity); + if (canJoin) { + attendee.assigned.push(activity); + activity.roster.push(attendee); + break; + } + } +} + +/** + * Checks that an activity has capacity and doesn't conflict with previously assigned + * activities. + * + * @param {object} attendee - Attendee looking to join the activity + * @param {object} activity - Proposed activity + * @return {boolean} - True if attendee can join the activity + */ +function checkAvailability_(attendee, activity) { + if (activity.capacity <= activity.roster.length) { + return false; + } + let timesConflict = attendee.assigned.some(function(assignedActivity) { + return !(assignedActivity.startAt.getTime() > activity.endAt.getTime() || + activity.startAt.getTime() > assignedActivity.endAt.getTime()); + }); + return !timesConflict; +}; + +/** + * Populates a sheet with the assigned activities for each attendee. + * + * @param {Spreadsheet} ss - Spreadsheet to write to. + * @param {object[]} attendees - Array of attendees with their activity assignments + */ +function writeAttendeeAssignments_(ss, attendees) { + let sheet = findOrCreateSheetByName_(ss, 'Activities by person'); + sheet.clear(); + sheet.appendRow(['Email address', 'Activities']); + sheet.getRange('B1:1').merge(); + let rows = attendees.map(function(attendee) { + // Prefill row to ensure consistent length otherwise + // can't bulk update the sheet with range.setValues() + let row = fillArray_([], ACTIVITIES_PER_PERSON + 1, ''); + row[0] = attendee.email; + attendee.assigned.forEach(function(activity, index) { + row[index + 1] = activity.description; + }); + return row; + }); + bulkAppendRows_(sheet, rows); + sheet.setFrozenRows(1); + sheet.getRange('1:1').setFontWeight('bold'); + sheet.autoResizeColumns(1, sheet.getLastColumn()); +} + +/** + * Populates a sheet with the rosters for each activity. + * + * @param {Spreadsheet} ss - Spreadsheet to write to. + * @param {object[]} activities - Array of activities with their rosters + */ +function writeActivityRosters_(ss, activities) { + let sheet = findOrCreateSheetByName_(ss, 'Activity rosters'); + sheet.clear(); + var rows = []; + var rows = activities.map(function(activity) { + let roster = activity.roster.map(function(attendee) { + return attendee.email; + }); + return [activity.description].concat(roster); + }); + // Transpose the data so each activity is a column + rows = transpose_(rows, ''); + bulkAppendRows_(sheet, rows); + sheet.setFrozenRows(1); + sheet.getRange('1:1').setFontWeight('bold'); + sheet.autoResizeColumns(1, sheet.getLastColumn()); +} + +/** + * Loads the activity schedule. + * + * @param {Spreadsheet} ss - Spreadsheet to load from + * @return {object[]} Array of available activities. + */ +function loadActivitySchedule_(ss) { + let timeZone = ss.getSpreadsheetTimeZone(); + let sheet = ss.getSheetByName('Activity Schedule'); + let rows = sheet.getSheetValues( + sheet.getFrozenRows() + 1, 1, + sheet.getLastRow() - 1, sheet.getLastRow()); + let activities = rows.map(function(row, index) { + let name = row[0]; + let startAt = new Date(row[1]); + let endAt = new Date(row[2]); + let capacity = parseInt(row[3]); + let formattedStartAt= Utilities.formatDate(startAt, timeZone, 'EEE hh:mm a'); + let formattedEndAt = Utilities.formatDate(endAt, timeZone, 'hh:mm a'); + let description = Utilities.formatString('%s (%s-%s)', name, formattedStartAt, formattedEndAt); + return { + id: index, + name: name, + description: description, + capacity: capacity, + startAt: startAt, + endAt: endAt, + roster: [], + }; + }); + return activities; +} + +/** + * Loads the attendeee response data. + * + * @param {Spreadsheet} ss - Spreadsheet to load from + * @param {number[]} allActivityIds - Full set of available activity IDs + * @return {object[]} Array of parsed attendee respones. + */ +function loadAttendeeResponses_(ss, allActivityIds) { + let sheet = findResponseSheetForForm_(ss); + + if (!sheet || sheet.getLastRow() == 1) { + return undefined; + } + + let rows = sheet.getSheetValues( + sheet.getFrozenRows() + 1, 1, + sheet.getLastRow() - 1, sheet.getLastRow()); + let attendees = rows.map(function(row) { + let _ = row.shift(); // Ignore timestamp + let email = row.shift(); + let autoAssign = row.pop(); + // Find ranked items in the response data. + let preferences = row.reduce(function(prefs, value, index) { + let match = value.match(/(\d+).*/); + if (!match) { + return prefs; + } + let rank = parseInt(match[1]) - 1; // Convert ordinal to array index + prefs[rank] = index; + return prefs; + }, []); + if (autoAssign == 'Yes') { + // If auto assigning additional activites, append a randomized list of all the activities. + // These will then be considered as if the attendee ranked them. + let additionalChoices = shuffleArray_(allActivityIds); + preferences = preferences.concat(additionalChoices); + } + return { + email: email, + preferences: preferences, + assigned: [], + }; + }); + return attendees; +} + +/** + * Simulates a large number of users responding to the form. This enables users to quickly + * experience the full solution without having to collect sufficient form responses + * through other means. + */ +function generateTestData_() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let sheet = findResponseSheetForForm_(ss); + if (!sheet) { + let msg = 'No response sheet found. Create the form and try again.'; + SpreadsheetApp.getUi().alert(msg); + } + if (sheet.getLastRow() > 1) { + let msg = 'Response sheet is not empty, can not generate test data. ' + + 'Remove responses and try again.'; + SpreadsheetApp.getUi().alert(msg); + return; + } + + let activities = loadActivitySchedule_(ss); + let choices = fillArray_([], activities.length, ''); + range_(1, 5).forEach(function(value) { + choices[value] = toOrdinal_(value); + }); + + let rows = range_(1, NUM_TEST_USERS).map(function(value) { + let randomizedChoices = shuffleArray_(choices); + let email = Utilities.formatString('person%d@example.com', value); + return [new Date(), email].concat(randomizedChoices).concat(['Yes']); + }); + bulkAppendRows_(sheet, rows); +} + +/** + * Retrieves a sheet by name, creating it if it doesn't yet exist. + * + * @param {Spreadsheet} ss - Containing spreadsheet + * @Param {string} name - Name of sheet to return + * @return {Sheet} Sheet instance + */ +function findOrCreateSheetByName_(ss, name) { + let sheet = ss.getSheetByName(name); + if (sheet) { + return sheet; + } + return ss.insertSheet(name); +} + +/** + * Faster version of appending multiple rows via ranges. Requires all rows are equal length. + * + * @param {Sheet} sheet - Sheet to append to + * @param {Array>} rows - Rows to append + */ +function bulkAppendRows_(sheet, rows) { + let startRow = sheet.getLastRow() + 1; + let startColumn = 1; + let numRows = rows.length; + let numColumns = rows[0].length; + sheet.getRange(startRow, startColumn, numRows, numColumns).setValues(rows); +} + +/** + * Copies and randomizes an array. + * + * @param {object[]} array - Array to shuffle + * @return {object[]} randomized copy of the array + */ +function shuffleArray_(array) { + let clone = array.slice(0); + for (let i = clone.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)); + let temp = clone[i]; + clone[i] = clone[j]; + clone[j] = temp; + } + return clone; +} + +/** + * Formats an number as an ordinal. + * + * See: https://stackoverflow.com/questions/13627308/add-st-nd-rd-and-th-ordinal-suffix-to-a-number/13627586 + * + * @param {number} i - Number to format + * @return {string} Formatted string + */ +function toOrdinal_(i) { + let j = i % 10; + let k = i % 100; + if (j == 1 && k != 11) { + return i + 'st'; + } + if (j == 2 && k != 12) { + return i + 'nd'; + } + if (j == 3 && k != 13) { + return i + 'rd'; + } + return i + 'th'; +} + +/** + * Locates the sheet containing the form responses. + * + * @param {Spreadsheet} ss - Spreadsheet instance to search + * @return {Sheet} Sheet with form responses, undefined if not found. + */ +function findResponseSheetForForm_(ss) { + let formUrl = ss.getFormUrl(); + if (!ss || !formUrl) { + return undefined; + } + let sheets = ss.getSheets(); + for (let i in sheets) { + if (sheets[i].getFormUrl() === formUrl) { + return sheets[i]; + } + } + return undefined; +} + +/** + * Fills an array with a value ([].fill() not supported in Apps Script). + * + * @param {object[]} arr - Array to fill + * @param {number} length - Number of items to fill. + * @param {object} value - Value to place at each index. + * @return {object[]} the array, for chaining purposes + */ +function fillArray_(arr, length, value) { + for (let i = 0; i < length; ++i) { + arr[i] = value; + } + return arr; +} + +/** + * Creates and fills an array with numbers in the range [start, end]. + * + * @param {number} start - First value in the range, inclusive + * @param {number} end - Last value in the range, inclusive + * @return {number[]} Array of values representing the range + */ +function range_(start, end) { + let arr = [start]; + let i = start; + while (i < end) { + arr.push(i += 1); + } + return arr; +} + +/** + * Transposes a matrix/2d array. For cases where the rows are not the same length, + * `fillValue` is used where no other value would otherwise be present. + * + * @param {Array>} arr - 2D array to transpose + * @param {object} fillValue - Placeholder for undefined values created as a result + * of the transpose. Only required if rows aren't all of equal length. + * @return {Array>} New transposed array + */ +function transpose_(arr, fillValue) { + let transposed = []; + arr.forEach(function(row, rowIndex) { + row.forEach(function(col, colIndex) { + transposed[colIndex] = transposed[colIndex] || fillArray_([], arr.length, fillValue); + transposed[colIndex][rowIndex] = row[colIndex]; + }); + }); + return transposed; +} diff --git a/solutions/automations/offsite-activity-signup/README.md b/solutions/automations/offsite-activity-signup/README.md new file mode 100644 index 000000000..06e8debc0 --- /dev/null +++ b/solutions/automations/offsite-activity-signup/README.md @@ -0,0 +1,3 @@ +# Create a sign-up for an offsite + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/offsite-activity-signup) for additional details. diff --git a/solutions/automations/offsite-activity-signup/appsscript.json b/solutions/automations/offsite-activity-signup/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/offsite-activity-signup/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/tax-loss-harvest-alerts/.clasp.json b/solutions/automations/tax-loss-harvest-alerts/.clasp.json new file mode 100644 index 000000000..b9dffc2b6 --- /dev/null +++ b/solutions/automations/tax-loss-harvest-alerts/.clasp.json @@ -0,0 +1 @@ +{"scriptId":"1SVf_XAGJiwksNTMnAwtlIvkKaDou4RLsmwGTa9ipVHKgwITgwXWqMixB"} \ No newline at end of file diff --git a/solutions/automations/tax-loss-harvest-alerts/Code.js b/solutions/automations/tax-loss-harvest-alerts/Code.js new file mode 100644 index 000000000..e25098b87 --- /dev/null +++ b/solutions/automations/tax-loss-harvest-alerts/Code.js @@ -0,0 +1,72 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/tax-loss-harvest-alerts + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** +* Checks for losses in the sheet. +*/ +function checkLosses() { + // Pulls data from the spreadsheet + let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName( + "Calculations" + ); + let source = sheet.getRange("A:G"); + let data = source.getValues(); + + //Prepares the email alert content + let message = "Stocks:

    "; + + let send_message = false; + + console.log("starting loop"); + + //Loops through the cells in the spreadsheet to find cells where the stock fell below purchase price + let n = 0; + for (let i in data) { + //Skips the first row + if (n++ == 0) continue; + + //Loads the current row + let row = data[i]; + + console.log(row[1]); + console.log(row[6]); + + //Once at the end of the list, exits the loop + if (row[1] == "") break; + + //If value is below purchase price, adds stock ticker and difference to list of tax loss opportunities + if (row[6] < 0) { + message += + row[1] + + ": " + + (parseFloat(row[6].toString()) * 100).toFixed(2).toString() + + "%
    "; + send_message = true; + } + } + if (!send_message) return; + + MailApp.sendEmail({ + to: SpreadsheetApp.getActiveSpreadsheet().getOwner().getEmail(), + subject: "Tax-loss harvest", + htmlBody: message, + + }); +} + diff --git a/solutions/automations/tax-loss-harvest-alerts/README.md b/solutions/automations/tax-loss-harvest-alerts/README.md new file mode 100644 index 000000000..8143e4c2f --- /dev/null +++ b/solutions/automations/tax-loss-harvest-alerts/README.md @@ -0,0 +1,3 @@ +# Get stock price drop alerts + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/tax-loss-harvest-alerts) for additional details. \ No newline at end of file diff --git a/solutions/automations/tax-loss-harvest-alerts/appsscript.json b/solutions/automations/tax-loss-harvest-alerts/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/tax-loss-harvest-alerts/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/timesheets/.clasp.json b/solutions/automations/timesheets/.clasp.json new file mode 100644 index 000000000..5f0ff403b --- /dev/null +++ b/solutions/automations/timesheets/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1uzOldn2RjqdrbDJwxuPlcsb7twKLdW59YPS02rbEg_ajAG9XzrYF1-fH"} diff --git a/solutions/automations/timesheets/Code.js b/solutions/automations/timesheets/Code.js new file mode 100644 index 000000000..bad03efdc --- /dev/null +++ b/solutions/automations/timesheets/Code.js @@ -0,0 +1,264 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/timesheets + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Global variables representing the index of certain columns. +let COLUMN_NUMBER = { + EMAIL: 2, + HOURS_START: 4, + HOURS_END: 8, + HOURLY_PAY: 9, + TOTAL_HOURS: 10, + CALC_PAY: 11, + APPROVAL: 12, + NOTIFY: 13, +}; + +// Global variables: +let APPROVED_EMAIL_SUBJECT = 'Weekly Timesheet APPROVED'; +let REJECTED_EMAIL_SUBJECT = 'Weekly Timesheet NOT APPROVED'; +let APPROVED_EMAIL_MESSAGE = 'Your timesheet has been approved.'; +let REJECTED_EMAIL_MESSAGE = 'Your timesheet has not been approved.'; + +/** + * Creates the menu item "Timesheets" for user to run scripts on drop-down. + */ +function onOpen() { + let ui = SpreadsheetApp.getUi(); + ui.createMenu('Timesheets') + .addItem('Form setup', 'setUpForm') + .addItem('Column setup', 'columnSetup') + .addItem('Notify employees', 'checkApprovedStatusToNotify') + .addToUi(); +} + +/** + * Adds "WEEKLY PAY" column with calculated values using array formulas. + * Adds an "APPROVAL" column at the end of the sheet, containing + * drop-down menus to either approve/disapprove employee timesheets. + * Adds a "NOTIFIED STATUS" column indicating whether or not an + * employee has yet been e mailed. + */ +function columnSetup() { + let sheet = SpreadsheetApp.getActiveSheet(); + let lastCol = sheet.getLastColumn(); + let lastRow = sheet.getLastRow(); + let frozenRows = sheet.getFrozenRows(); + let beginningRow = frozenRows + 1; + let numRows = lastRow - frozenRows; + + // Calls helper functions to add new columns. + addCalculatePayColumn(sheet, beginningRow); + addApprovalColumn(sheet, beginningRow, numRows); + addNotifiedColumn(sheet, beginningRow, numRows); +} + +/** + * Adds TOTAL HOURS and CALCULATE PAY columns and automatically calculates + * every employee's weekly pay. + * + * @param {Object} sheet Spreadsheet object of current sheet. + * @param {Integer} beginningRow Index of beginning row. + */ +function addCalculatePayColumn(sheet, beginningRow) { + sheet.insertColumnAfter(COLUMN_NUMBER.HOURLY_PAY); + sheet.getRange(1, COLUMN_NUMBER.TOTAL_HOURS).setValue('TOTAL HOURS'); + sheet.getRange(1, COLUMN_NUMBER.CALC_PAY).setValue('WEEKLY PAY'); + + // Calculates weekly total hours. + sheet.getRange(beginningRow, COLUMN_NUMBER.TOTAL_HOURS) + .setFormula('=ArrayFormula(D2:D+E2:E+F2:F+G2:G+H2:H)'); + // Calculates weekly pay. + sheet.getRange(beginningRow, COLUMN_NUMBER.CALC_PAY) + .setFormula('=ArrayFormula(I2:I * J2:J)'); +} + +/** + * Adds an APPROVAL column allowing managers to approve/ + * disapprove of each employee's timesheet. + * + * @param {Object} sheet Spreadsheet object of current sheet. + * @param {Integer} beginningRow Index of beginning row. + * @param {Integer} numRows Number of rows currently in use. + */ +function addApprovalColumn(sheet, beginningRow, numRows) { + sheet.insertColumnAfter(COLUMN_NUMBER.CALC_PAY); + sheet.getRange(1, COLUMN_NUMBER.APPROVAL).setValue('APPROVAL'); + + // Make sure approval column is all drop-down menus. + let approvalColumnRange = sheet.getRange(beginningRow, COLUMN_NUMBER.APPROVAL, + numRows, 1); + let dropdownValues = ['APPROVED', 'NOT APPROVED', 'IN PROGRESS']; + let rule = SpreadsheetApp.newDataValidation().requireValueInList(dropdownValues) + .build(); + approvalColumnRange.setDataValidation(rule); + approvalColumnRange.setValue('IN PROGRESS'); +} + +/** + * Adds a NOTIFIED column allowing managers to see which employees + * have/have not yet been notified of their approval status. + * + * @param {Object} sheet Spreadsheet object of current sheet. + * @param {Integer} beginningRow Index of beginning row. + * @param {Integer} numRows Number of rows currently in use. + */ +function addNotifiedColumn(sheet, beginningRow, numRows) { + sheet.insertColumnAfter(COLUMN_NUMBER.APPROVAL); // global + sheet.getRange(1, COLUMN_NUMBER.APPROVAL + 1).setValue('NOTIFIED STATUS'); + + // Make sure notified column is all drop-down menus. + let notifiedColumnRange = sheet.getRange(beginningRow, COLUMN_NUMBER.APPROVAL + + 1, numRows, 1); + dropdownValues = ['NOTIFIED', 'PENDING']; + rule = SpreadsheetApp.newDataValidation().requireValueInList(dropdownValues) + .build(); + notifiedColumnRange.setDataValidation(rule); + notifiedColumnRange.setValue('PENDING'); +} + +/** + * Sets the notification status to NOTIFIED for employees + * who have received a notification email. + * + * @param {Object} sheet Current Spreadsheet. + * @param {Object} notifiedValues Array of notified values. + * @param {Integer} i Current status in the for loop. + * @parma {Integer} beginningRow Row where iterations began. + */ +function updateNotifiedStatus(sheet, notifiedValues, i, beginningRow) { + // Update notification status. + notifiedValues[i][0] = 'NOTIFIED'; + sheet.getRange(i + beginningRow, COLUMN_NUMBER.NOTIFY).setValue('NOTIFIED'); +} + +/** + * Checks the approval status of every employee, and calls helper functions + * to notify employees via email & update their notification status. + */ +function checkApprovedStatusToNotify() { + let sheet = SpreadsheetApp.getActiveSheet(); + let lastRow = sheet.getLastRow(); + let lastCol = sheet.getLastColumn(); + // lastCol here is the NOTIFIED column. + let frozenRows = sheet.getFrozenRows(); + let beginningRow = frozenRows + 1; + let numRows = lastRow - frozenRows; + + // Gets ranges of email, approval, and notified values for every employee. + let emailValues = sheet.getRange(beginningRow, COLUMN_NUMBER.EMAIL, numRows, 1).getValues(); + let approvalValues = sheet.getRange(beginningRow, COLUMN_NUMBER.APPROVAL, + lastRow - frozenRows, 1).getValues(); + let notifiedValues = sheet.getRange(beginningRow, COLUMN_NUMBER.NOTIFY, numRows, + 1).getValues(); + + // Traverses through employee's row. + for (let i = 0; i < numRows; i++) { + // Do not notify twice. + if (notifiedValues[i][0] == 'NOTIFIED') { + continue; + } + let emailAddress = emailValues[i][0]; + let approvalValue = approvalValues[i][0]; + + // Sends notifying emails & update status. + if (approvalValue == 'IN PROGRESS') { + continue; + } else if (approvalValue == 'APPROVED') { + MailApp.sendEmail(emailAddress, APPROVED_EMAIL_SUBJECT, APPROVED_EMAIL_MESSAGE); + updateNotifiedStatus(sheet, notifiedValues, i, beginningRow); + } else if (approvalValue == 'NOT APPROVED') { + MailApp.sendEmail(emailAddress,REJECTED_EMAIL_SUBJECT, REJECTED_EMAIL_MESSAGE); + updateNotifiedStatus(sheet, notifiedValues, i, beginningRow); + } + } +} + +/** + * Set up the Timesheets Responses form, & link the form's trigger to + * send manager an email when a new request is submitted. + */ +function setUpForm() { + let sheet = SpreadsheetApp.getActiveSpreadsheet(); + if (sheet.getFormUrl()) { + let msg = 'Form already exists. Unlink the form and try again.'; + SpreadsheetApp.getUi().alert(msg); + return; + } + + // Create the form. + let form = FormApp.create('Weekly Timesheets') + .setCollectEmail(true) + .setDestination(FormApp.DestinationType.SPREADSHEET, sheet.getId()) + .setLimitOneResponsePerUser(false); + form.addTextItem().setTitle('Employee Name:').setRequired(true); + form.addTextItem().setTitle('Monday Hours:').setRequired(true); + form.addTextItem().setTitle('Tuesday Hours:').setRequired(true); + form.addTextItem().setTitle('Wednesday Hours:').setRequired(true); + form.addTextItem().setTitle('Thursday Hours:').setRequired(true); + form.addTextItem().setTitle('Friday Hours:').setRequired(true); + form.addTextItem().setTitle('HourlyWage:').setRequired(true); + + // Set up on form submit trigger. + ScriptApp.newTrigger('onFormSubmit') + .forForm(form) + .onFormSubmit() + .create(); +} + +/** + * Handle new form submissions to trigger the workflow. + * + * @param {Object} event Form submit event + */ +function onFormSubmit(event) { + let response = getResponsesByName(event.response); + + // Load form responses into a new row. + let row = ['New', + '', + response['Emoloyee Email:'], + response['Employee Name:'], + response['Monday Hours:'], + response['Tuesday Hours:'], + response['Wednesday Hours:'], + response['Thursday Hours:'], + response['Friday Hours:'], + response['Hourly Wage:']]; + let sheet = SpreadsheetApp.getActiveSpreadsheet(); + sheet.appendRow(row); +} + +/** + * Converts a form response to an object keyed by the item titles. Allows easier + * access to response values. + * + * @param {FormResponse} response + * @return {Object} Form values keyed by question title + */ +function getResponsesByName(response) { + let initialValue = { + email: response.getRespondentEmail(), + timestamp: response.getTimestamp(), + }; + return response.getItemResponses().reduce(function(obj, itemResponse) { + let key = itemResponse.getItem().getTitle(); + obj[key] = itemResponse.getResponse(); + return obj; + }, initialValue); +} \ No newline at end of file diff --git a/solutions/automations/timesheets/README.md b/solutions/automations/timesheets/README.md new file mode 100644 index 000000000..100b7ea38 --- /dev/null +++ b/solutions/automations/timesheets/README.md @@ -0,0 +1,3 @@ +# Collect and review timesheets from employees + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/timesheets) for additional details. diff --git a/solutions/automations/timesheets/appsscript.json b/solutions/automations/timesheets/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/timesheets/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/upload-files/.clasp.json b/solutions/automations/upload-files/.clasp.json new file mode 100644 index 000000000..a32236750 --- /dev/null +++ b/solutions/automations/upload-files/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "17LvIKDgTm-jvsJ6PGIxxOSvszs8cdzGN18oP2trzH55N0RMEpJc-GvJG"} diff --git a/solutions/automations/upload-files/Code.js b/solutions/automations/upload-files/Code.js new file mode 100644 index 000000000..7c65da6c6 --- /dev/null +++ b/solutions/automations/upload-files/Code.js @@ -0,0 +1,110 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/upload-files + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// TODO Before you start using this sample, you must run the setUp() +// function in the Setup.gs file. + +// Application constants +const APP_TITLE = "Upload files to Drive from Forms"; +const APP_FOLDER_NAME = "Upload files to Drive (File responses)"; + +// Identifies the subfolder form item +const APP_SUBFOLDER_ITEM = "Subfolder"; +const APP_SUBFOLDER_NONE = ""; + + +/** + * Gets the file uploads from a form response and moves files to the corresponding subfolder. + * + * @param {object} event - Form submit. + */ +function onFormSubmit(e) { + try { + // Gets the application root folder. + var destFolder = getFolder_(APP_FOLDER_NAME); + + // Gets all form responses. + let itemResponses = e.response.getItemResponses(); + + // Determines the subfolder to route the file to, if any. + var subFolderName; + let dest = itemResponses.filter((itemResponse) => + itemResponse.getItem().getTitle().toString() === APP_SUBFOLDER_ITEM); + + // Gets the destination subfolder name, but ignores if APP_SUBFOLDER_NONE was selected; + if (dest.length > 0) { + if (dest[0].getResponse() != APP_SUBFOLDER_NONE) { + subFolderName = dest[0].getResponse(); + } + } + // Gets the subfolder or creates it if it doesn't exist. + if (subFolderName != undefined) { + destFolder = getSubFolder_(destFolder, subFolderName) + } + console.log(`Destination folder to use: + Name: ${destFolder.getName()} + ID: ${destFolder.getId()} + URL: ${destFolder.getUrl()}`) + + // Gets the file upload response as an array to allow for multiple files. + let fileUploads = itemResponses.filter((itemResponse) => itemResponse.getItem().getType().toString() === "FILE_UPLOAD") + .map((itemResponse) => itemResponse.getResponse()) + .reduce((a, b) => [...a, ...b], []); + + // Moves the files to the destination folder. + if (fileUploads.length > 0) { + fileUploads.forEach((fileId) => { + DriveApp.getFileById(fileId).moveTo(destFolder); + console.log(`File Copied: ${fileId}`) + }); + } + } + catch (err) { + console.log(err); + } +} + + +/** + * Returns a Drive folder under the passed in objParentFolder parent + * folder. Checks if folder of same name exists before creating, returning + * the existing folder or the newly created one if not found. + * + * @param {object} objParentFolder - Drive folder as an object. + * @param {string} subFolderName - Name of subfolder to create/return. + * @return {object} Drive folder + */ +function getSubFolder_(objParentFolder, subFolderName) { + + // Iterates subfolders of parent folder to check if folder already exists. + const subFolders = objParentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === subFolderName) { + return folder; + } + } + // Creates a new folder if one doesn't already exist. + return objParentFolder.createFolder(subFolderName) + .setDescription(`Created by ${APP_TITLE} application to store uploaded Forms files.`); +} + + diff --git a/solutions/automations/upload-files/README.md b/solutions/automations/upload-files/README.md new file mode 100644 index 000000000..975bc219b --- /dev/null +++ b/solutions/automations/upload-files/README.md @@ -0,0 +1,3 @@ +# Upload files to Google Drive from Google Forms + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/upload-files) for additional details. diff --git a/solutions/automations/upload-files/Setup.js b/solutions/automations/upload-files/Setup.js new file mode 100644 index 000000000..0589c8acc --- /dev/null +++ b/solutions/automations/upload-files/Setup.js @@ -0,0 +1,100 @@ +// TODO You must run the setUp() function before you start using this sample. + +/** + * The setUp() function performs the following: + * - Creates a Google Drive folder named by the APP_FOLDER_NAME + * variable in the Code.gs file. + * - Creates a trigger to handle onFormSubmit events. + */ +function setUp() { + // Ensures the root destination folder exists. + const appFolder = getFolder_(APP_FOLDER_NAME); + if (appFolder !== null) { + console.log(`Application folder setup. + Name: ${appFolder.getName()} + ID: ${appFolder.getId()} + URL: ${appFolder.getUrl()}`) + } + else { + console.log(`Could not setup application folder.`) + } + // Calls the function that creates the Forms onSubmit trigger. + installTrigger_(); +} + +/** + * Returns a folder to store uploaded files in the same location + * in Drive where the form is located. First, it checks if the folder + * already exists, and creates it if it doesn't. + * + * @param {string} folderName - Name of the Drive folder. + * @return {object} Google Drive Folder + */ +function getFolder_(folderName) { + + // Gets the Drive folder where the form is located. + const ssId = FormApp.getActiveForm().getId(); + const parentFolder = DriveApp.getFileById(ssId).getParents().next(); + + // Iterates through the subfolders to check if folder already exists. + // The script checks for the folder name specified in the APP_FOLDER_NAME variable. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === folderName) { + return folder; + } + } + // Creates a new folder if one doesn't already exist. + return parentFolder.createFolder(folderName) + .setDescription(`Created by ${APP_TITLE} application to store uploaded files.`); +} + +/** + * Installs trigger to capture onFormSubmit event when a form is submitted. + * Ensures that the trigger is only installed once. + * Called by setup(). + */ +function installTrigger_() { + // Ensures existing trigger doesn't already exist. + let propTriggerId = PropertiesService.getScriptProperties().getProperty('triggerUniqueId') + if (propTriggerId !== null) { + const triggers = ScriptApp.getProjectTriggers(); + for (let t in triggers) { + if (triggers[t].getUniqueId() === propTriggerId) { + console.log(`Trigger with the following unique ID already exists: ${propTriggerId}`); + return; + } + } + } + // Creates the trigger if one doesn't exist. + let triggerUniqueId = ScriptApp.newTrigger('onFormSubmit') + .forForm(FormApp.getActiveForm()) + .onFormSubmit() + .create() + .getUniqueId(); + PropertiesService.getScriptProperties().setProperty('triggerUniqueId', triggerUniqueId); + console.log(`Trigger with the following unique ID was created: ${triggerUniqueId}`); +} + +/** + * Removes all script properties and triggers for the project. + * Use primarily to test setup routines. + */ +function removeTriggersAndScriptProperties() { + PropertiesService.getScriptProperties().deleteAllProperties(); + // Removes all triggers associated with project. + const triggers = ScriptApp.getProjectTriggers(); + for (let t in triggers) { + ScriptApp.deleteTrigger(triggers[t]); + } +} + +/** + * Removes all form responses to reset the form. + */ +function deleteAllResponses() { + FormApp.getActiveForm().deleteAllResponses(); +} \ No newline at end of file diff --git a/solutions/automations/upload-files/appsscript.json b/solutions/automations/upload-files/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/upload-files/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/youtube-tracker/.clasp.json b/solutions/automations/youtube-tracker/.clasp.json new file mode 100644 index 000000000..3e74246bd --- /dev/null +++ b/solutions/automations/youtube-tracker/.clasp.json @@ -0,0 +1 @@ +{"scriptId":"15WP4FukVYk_4zy21j0_13GftPH7J8lpdtemYcy_168TYKsAQ4x-pAeQz"} \ No newline at end of file diff --git a/solutions/automations/youtube-tracker/Code.js b/solutions/automations/youtube-tracker/Code.js new file mode 100644 index 000000000..b99827d4d --- /dev/null +++ b/solutions/automations/youtube-tracker/Code.js @@ -0,0 +1,126 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/youtube-tracker + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Sets preferences for email notification. Choose 'Y' to send emails, 'N' to skip emails. +const EMAIL_ON = 'Y'; + +// Matches column names in Video sheet to variables. If the column names change, update these variables. +const COLUMN_NAME = { + VIDEO: 'Video Link', + TITLE: 'Video Title', +}; + +/** + * Gets YouTube video details and statistics for all + * video URLs listed in 'Video Link' column in each + * sheet. Sends email summary, based on preferences above, + * when videos have new comments or replies. + */ +function markVideos() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets(); + + // Runs through process for each tab in Spreadsheet. + sheets.forEach(function(dataSheet) { + let tabName = dataSheet.getName(); + let range = dataSheet.getDataRange(); + let numRows = range.getNumRows(); + let rows = range.getValues(); + let headerRow = rows[0]; + + // Finds the column indices. + let videoColumnIdx = headerRow.indexOf(COLUMN_NAME.VIDEO); + let titleColumnIdx = headerRow.indexOf(COLUMN_NAME.TITLE); + + // Creates empty array to collect data for email table. + let emailContent = []; + + // Processes each row in spreadsheet. + for (let i = 1; i < numRows; ++i) { + let row = rows[i]; + // Extracts video ID. + let videoId = extractVideoIdFromUrl(row[videoColumnIdx]) + // Processes each row that contains a video ID. + if(!videoId) { + continue; + } + // Calls getVideoDetails function and extracts target data for the video. + let detailsResponse = getVideoDetails(videoId); + let title = detailsResponse.items[0].snippet.title; + let publishDate = detailsResponse.items[0].snippet.publishedAt; + let publishDateFormatted = new Date(publishDate); + let views = detailsResponse.items[0].statistics.viewCount; + let likes = detailsResponse.items[0].statistics.likeCount; + let comments = detailsResponse.items[0].statistics.commentCount; + let channel = detailsResponse.items[0].snippet.channelTitle; + + // Collects title, publish date, channel, views, comments, likes details and pastes into tab. + let detailsRow = [title,publishDateFormatted,channel,views,comments,likes]; + dataSheet.getRange(i+1,titleColumnIdx+1,1,6).setValues([detailsRow]); + + // Determines if new count of comments/replies is greater than old count of comments/replies. + let addlCommentCount = comments - row[titleColumnIdx+4]; + + // Adds video title, link, and additional comment count to table if new counts > old counts. + if (addlCommentCount > 0) { + let emailRow = [title,row[videoColumnIdx],addlCommentCount] + emailContent.push(emailRow); + } + } + // Sends notification email if Content is not empty. + if (emailContent.length > 0 && EMAIL_ON == 'Y') { + sendEmailNotificationTemplate(emailContent, tabName); + } + }); +} + +/** + * Gets video details for YouTube videos + * using YouTube advanced service. + */ +function getVideoDetails(videoId) { + let part = "snippet,statistics"; + let response = YouTube.Videos.list(part, + {'id': videoId}); + return response; +} + +/** + * Extracts YouTube video ID from url. + * (h/t https://stackoverflow.com/a/3452617) + */ +function extractVideoIdFromUrl(url) { + let videoId = url.split('v=')[1]; + let ampersandPosition = videoId.indexOf('&'); + if (ampersandPosition != -1) { + videoId = videoId.substring(0, ampersandPosition); + } + return videoId; +} + +/** + * Assembles notification email with table of video details. + * (h/t https://stackoverflow.com/questions/37863392/making-table-in-google-apps-script-from-array) + */ +function sendEmailNotificationTemplate(content, emailAddress) { + let template = HtmlService.createTemplateFromFile('email'); + template.content = content; + let msg = template.evaluate(); + MailApp.sendEmail(emailAddress,'New comments or replies on YouTube',msg.getContent(),{htmlBody:msg.getContent()}); +} \ No newline at end of file diff --git a/solutions/automations/youtube-tracker/README.md b/solutions/automations/youtube-tracker/README.md new file mode 100644 index 000000000..7c80c2ad0 --- /dev/null +++ b/solutions/automations/youtube-tracker/README.md @@ -0,0 +1,3 @@ +# Track YouTube video views and comments + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/youtube-tracker) for additional details. \ No newline at end of file diff --git a/solutions/automations/youtube-tracker/appsscript.json b/solutions/automations/youtube-tracker/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/youtube-tracker/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/youtube-tracker/email.html b/solutions/automations/youtube-tracker/email.html new file mode 100644 index 000000000..a8e9f9c97 --- /dev/null +++ b/solutions/automations/youtube-tracker/email.html @@ -0,0 +1,13 @@ + +Hello,

    You have new comments and/or replies on videos:

    + + + + + + + + +
    Video TitleLinkNumber of new replies and comments
    + + From 0a5dd0196bbf2150e576e6dc86b730aabb603323 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Thu, 21 Apr 2022 13:56:36 -0600 Subject: [PATCH 22/99] Clean up some linter warnings --- .github/scripts/clasp_push.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/scripts/clasp_push.sh b/.github/scripts/clasp_push.sh index 03691ae59..cc6d79547 100755 --- a/.github/scripts/clasp_push.sh +++ b/.github/scripts/clasp_push.sh @@ -17,10 +17,10 @@ export LC_ALL=C.UTF-8 export LANG=C.UTF-8 function contains_changes() { - [[ "${@:2}" = "" ]] && return 0 - for f in ${@:2}; do - case $(realpath $f)/ in - "$(realpath $1)"/*) return 0;; + [[ "${*:2}" = "" ]] && return 0 + for f in "${@:2}"; do + case $(realpath "$f")/ in + $(realpath "$1")/*) return 0;; esac done return 1 @@ -37,7 +37,7 @@ for dir in "${dirs[@]}"; do pushd "${dir}" > /dev/null || exit contains_changes "$dir" "${changed_files[@]}" || continue echo "Publishing ${dir}" - clasp push -f + echo clasp push -f status=$? if [ $status -ne 0 ]; then exit_code=$status From 5bad8117a7268fed3b4471eee80f9366d46bfd36 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Thu, 21 Apr 2022 15:15:08 -0600 Subject: [PATCH 23/99] Add more solutions from docs --- .../automations/bracket-maker/.clasp.json | 1 + solutions/automations/bracket-maker/Code.js | 133 +++++++++++ solutions/automations/bracket-maker/README.md | 3 + .../automations/bracket-maker/appsscript.json | 7 + solutions/automations/mail-merge/.clasp.json | 1 + solutions/automations/mail-merge/Code.js | 210 +++++++++++++++++ solutions/automations/mail-merge/README.md | 3 + .../automations/mail-merge/appsscript.json | 7 + .../chat-bots/schedule-meetings/.clasp.json | 1 + solutions/chat-bots/schedule-meetings/Code.js | 190 +++++++++++++++ .../chat-bots/schedule-meetings/Dialog.js | 194 +++++++++++++++ .../chat-bots/schedule-meetings/README.md | 4 + .../chat-bots/schedule-meetings/Utilities.js | 58 +++++ .../schedule-meetings/appsscript.json | 8 + .../calculate-driving-distance/.clasp.json | 1 + .../calculate-driving-distance/Code.js | 223 ++++++++++++++++++ .../calculate-driving-distance/README.md | 4 + .../appsscript.json | 7 + .../summarize-sheets-data/.clasp.json | 1 + .../summarize-sheets-data/Code.js | 83 +++++++ .../summarize-sheets-data/README.md | 4 + .../summarize-sheets-data/appsscript.json | 7 + .../custom-functions/tier-pricing/.clasp.json | 1 + .../custom-functions/tier-pricing/Code.js | 51 ++++ .../custom-functions/tier-pricing/README.md | 4 + .../tier-pricing/appsscript.json | 7 + 26 files changed, 1213 insertions(+) create mode 100644 solutions/automations/bracket-maker/.clasp.json create mode 100644 solutions/automations/bracket-maker/Code.js create mode 100644 solutions/automations/bracket-maker/README.md create mode 100644 solutions/automations/bracket-maker/appsscript.json create mode 100644 solutions/automations/mail-merge/.clasp.json create mode 100644 solutions/automations/mail-merge/Code.js create mode 100644 solutions/automations/mail-merge/README.md create mode 100644 solutions/automations/mail-merge/appsscript.json create mode 100644 solutions/chat-bots/schedule-meetings/.clasp.json create mode 100644 solutions/chat-bots/schedule-meetings/Code.js create mode 100644 solutions/chat-bots/schedule-meetings/Dialog.js create mode 100644 solutions/chat-bots/schedule-meetings/README.md create mode 100644 solutions/chat-bots/schedule-meetings/Utilities.js create mode 100644 solutions/chat-bots/schedule-meetings/appsscript.json create mode 100644 solutions/custom-functions/calculate-driving-distance/.clasp.json create mode 100644 solutions/custom-functions/calculate-driving-distance/Code.js create mode 100644 solutions/custom-functions/calculate-driving-distance/README.md create mode 100644 solutions/custom-functions/calculate-driving-distance/appsscript.json create mode 100644 solutions/custom-functions/summarize-sheets-data/.clasp.json create mode 100644 solutions/custom-functions/summarize-sheets-data/Code.js create mode 100644 solutions/custom-functions/summarize-sheets-data/README.md create mode 100644 solutions/custom-functions/summarize-sheets-data/appsscript.json create mode 100644 solutions/custom-functions/tier-pricing/.clasp.json create mode 100644 solutions/custom-functions/tier-pricing/Code.js create mode 100644 solutions/custom-functions/tier-pricing/README.md create mode 100644 solutions/custom-functions/tier-pricing/appsscript.json diff --git a/solutions/automations/bracket-maker/.clasp.json b/solutions/automations/bracket-maker/.clasp.json new file mode 100644 index 000000000..92fd0ae7f --- /dev/null +++ b/solutions/automations/bracket-maker/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1LkY5nKFdBg2Q9-oIUcZsxRuESvgIcFHGobveNeQ5CpTgV6GgpTUQeOIB"} diff --git a/solutions/automations/bracket-maker/Code.js b/solutions/automations/bracket-maker/Code.js new file mode 100644 index 000000000..fccb6a7f7 --- /dev/null +++ b/solutions/automations/bracket-maker/Code.js @@ -0,0 +1,133 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/bracket-maker + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const RANGE_PLAYER1 = 'FirstPlayer'; +const SHEET_PLAYERS = 'Players'; +const SHEET_BRACKET = 'Bracket'; +const CONNECTOR_WIDTH = 15; + +/** + * Adds a custom menu item to run the script. + */ +function onOpen() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + ss.addMenu('Bracket maker', + [{name: 'Create bracket', functionName: 'createBracket'}]); +} + +/** + * Creates the brackets based on the data provided on the players. + */ +function createBracket() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let rangePlayers = ss.getRangeByName(RANGE_PLAYER1); + let sheetControl = ss.getSheetByName(SHEET_PLAYERS); + let sheetResults = ss.getSheetByName(SHEET_BRACKET); + + // Gets the players from column A. Assumes the entire column is filled. + rangePlayers = rangePlayers.offset(0, 0, sheetControl.getMaxRows() - + rangePlayers.getRowIndex() + 1, 1); + let players = rangePlayers.getValues(); + + // Figures out how many players there are by skipping the empty cells. + let numPlayers = 0; + for (let i = 0; i < players.length; i++) { + if (!players[i][0] || players[i][0].length == 0) { + break; + } + numPlayers++; + } + players = players.slice(0, numPlayers); + + // Provides some error checking in case there are too many or too few players/teams. + if (numPlayers > 64) { + Browser.msgBox('Sorry, this script can only create brackets for 64 or fewer players.'); + return; // Early exit + } + + if (numPlayers < 3) { + Browser.msgBox('Sorry, you must have at least 3 players.'); + return; // Early exit + } + + // Clears the 'Bracket' sheet and all formatting. + sheetResults.clear(); + + let upperPower = Math.ceil(Math.log(numPlayers) / Math.log(2)); + + // Calculates the number that is a power of 2 and lower than numPlayers. + let countNodesUpperBound = Math.pow(2, upperPower); + + // Calculates the number that is a power of 2 and higher than numPlayers. + let countNodesLowerBound = countNodesUpperBound / 2; + + // Determines the number of nodes that will not show in the 1st level. + let countNodesHidden = numPlayers - countNodesLowerBound; + + // Enters the players for the 1st round. + let currentPlayer = 0; + for (let i = 0; i < countNodesLowerBound; i++) { + if (i < countNodesHidden) { + // Must be on the first level + let rng = sheetResults.getRange(i * 4 + 1, 1); + setBracketItem_(rng, players); + setBracketItem_(rng.offset(2, 0, 1, 1), players); + setConnector_(sheetResults, rng.offset(0, 1, 3, 1)); + setBracketItem_(rng.offset(1, 2, 1, 1)); + } else { + // This player gets a bye. + setBracketItem_(sheetResults.getRange(i * 4 + 2, 3), players); + } + } + + // Fills in the rest of the bracket. + upperPower--; + for (let i = 0; i < upperPower; i++) { + let pow1 = Math.pow(2, i + 1); + let pow2 = Math.pow(2, i + 2); + let pow3 = Math.pow(2, i + 3); + for (let j = 0; j < Math.pow(2, upperPower - i - 1); j++) { + setBracketItem_(sheetResults.getRange((j * pow3) + pow2, i * 2 + 5)); + setConnector_(sheetResults, sheetResults.getRange((j * pow3) + pow1, i * 2 + 4, pow2 + 1, 1)); + } + } +} + +/** + * Sets the value of an item in the bracket and the color. + * @param {Range} rng The Spreadsheet Range. + * @param {string[]} players The list of players. + */ +function setBracketItem_(rng, players) { + if (players) { + let rand = Math.ceil(Math.random() * players.length); + rng.setValue(players.splice(rand - 1, 1)[0][0]); + } + rng.setBackgroundColor('yellow'); +} + +/** + * Sets the color and width for connector cells. + * @param {Sheet} sheet The spreadsheet to setup. + * @param {Range} rng The spreadsheet range. + */ +function setConnector_(sheet, rng) { + sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH); + rng.setBackgroundColor('green'); +} \ No newline at end of file diff --git a/solutions/automations/bracket-maker/README.md b/solutions/automations/bracket-maker/README.md new file mode 100644 index 000000000..920c6b3c2 --- /dev/null +++ b/solutions/automations/bracket-maker/README.md @@ -0,0 +1,3 @@ +# Create a tournament bracket + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/bracket-maker) for additional details. diff --git a/solutions/automations/bracket-maker/appsscript.json b/solutions/automations/bracket-maker/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/bracket-maker/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/mail-merge/.clasp.json b/solutions/automations/mail-merge/.clasp.json new file mode 100644 index 000000000..7f25c6014 --- /dev/null +++ b/solutions/automations/mail-merge/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1evL25lW9fLN43j6gGBJWtLq4GncLkdgoxxSVCawc8dWNoLoravNebAih"} diff --git a/solutions/automations/mail-merge/Code.js b/solutions/automations/mail-merge/Code.js new file mode 100644 index 000000000..b5951421b --- /dev/null +++ b/solutions/automations/mail-merge/Code.js @@ -0,0 +1,210 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/mail-merge + +/* +Copyright 2022 Martin Hawksey + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @OnlyCurrentDoc +*/ + +/** + * Change these to match the column names you are using for email + * recipient addresses and email sent column. +*/ +const RECIPIENT_COL = "Recipient"; +const EMAIL_SENT_COL = "Email Sent"; + +/** + * Creates the menu item "Mail Merge" for user to run scripts on drop-down. + */ +function onOpen() { + const ui = SpreadsheetApp.getUi(); + ui.createMenu('Mail Merge') + .addItem('Send Emails', 'sendEmails') + .addToUi(); +} + +/** + * Sends emails from sheet data. + * @param {string} subjectLine (optional) for the email draft message + * @param {Sheet} sheet to read data from +*/ +function sendEmails(subjectLine, sheet=SpreadsheetApp.getActiveSheet()) { + // option to skip browser prompt if you want to use this code in other projects + if (!subjectLine){ + subjectLine = Browser.inputBox("Mail Merge", + "Type or copy/paste the subject line of the Gmail " + + "draft message you would like to mail merge with:", + Browser.Buttons.OK_CANCEL); + + if (subjectLine === "cancel" || subjectLine == ""){ + // If no subject line, finishes up + return; + } + } + + // Gets the draft Gmail message to use as a template + const emailTemplate = getGmailTemplateFromDrafts_(subjectLine); + + // Gets the data from the passed sheet + const dataRange = sheet.getDataRange(); + // Fetches displayed values for each row in the Range HT Andrew Roberts + // https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490 + // @see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues + const data = dataRange.getDisplayValues(); + + // Assumes row 1 contains our column headings + const heads = data.shift(); + + // Gets the index of the column named 'Email Status' (Assumes header names are unique) + // @see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions + const emailSentColIdx = heads.indexOf(EMAIL_SENT_COL); + + // Converts 2d array into an object array + // See https://stackoverflow.com/a/22917499/1027723 + // For a pretty version, see https://mashe.hawksey.info/?p=17869/#comment-184945 + const obj = data.map(r => (heads.reduce((o, k, i) => (o[k] = r[i] || '', o), {}))); + + // Creates an array to record sent emails + const out = []; + + // Loops through all the rows of data + obj.forEach(function(row, rowIdx){ + // Only sends emails if email_sent cell is blank and not hidden by a filter + if (row[EMAIL_SENT_COL] == ''){ + try { + const msgObj = fillInTemplateFromObject_(emailTemplate.message, row); + + // See https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object) + // If you need to send emails with unicode/emoji characters change GmailApp for MailApp + // Uncomment advanced parameters as needed (see docs for limitations) + GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, { + htmlBody: msgObj.html, + // bcc: 'a.bbc@email.com', + // cc: 'a.cc@email.com', + // from: 'an.alias@email.com', + // name: 'name of the sender', + // replyTo: 'a.reply@email.com', + // noReply: true, // if the email should be sent from a generic no-reply email address (not available to gmail.com users) + attachments: emailTemplate.attachments, + inlineImages: emailTemplate.inlineImages + }); + // Edits cell to record email sent date + out.push([new Date()]); + } catch(e) { + // modify cell to record error + out.push([e.message]); + } + } else { + out.push([row[EMAIL_SENT_COL]]); + } + }); + + // Updates the sheet with new data + sheet.getRange(2, emailSentColIdx+1, out.length).setValues(out); + + /** + * Get a Gmail draft message by matching the subject line. + * @param {string} subject_line to search for draft message + * @return {object} containing the subject, plain and html message body and attachments + */ + function getGmailTemplateFromDrafts_(subject_line){ + try { + // get drafts + const drafts = GmailApp.getDrafts(); + // filter the drafts that match subject line + const draft = drafts.filter(subjectFilter_(subject_line))[0]; + // get the message object + const msg = draft.getMessage(); + + // Handles inline images and attachments so they can be included in the merge + // Based on https://stackoverflow.com/a/65813881/1027723 + // Gets all attachments and inline image attachments + const allInlineImages = draft.getMessage().getAttachments({includeInlineImages: true,includeAttachments:false}); + const attachments = draft.getMessage().getAttachments({includeInlineImages: false}); + const htmlBody = msg.getBody(); + + // Creates an inline image object with the image name as key + // (can't rely on image index as array based on insert order) + const img_obj = allInlineImages.reduce((obj, i) => (obj[i.getName()] = i, obj) ,{}); + + //Regexp searches for all img string positions with cid + const imgexp = RegExp(']+>', 'g'); + const matches = [...htmlBody.matchAll(imgexp)]; + + //Initiates the allInlineImages object + const inlineImagesObj = {}; + // built an inlineImagesObj from inline image matches + matches.forEach(match => inlineImagesObj[match[1]] = img_obj[match[2]]); + + return {message: {subject: subject_line, text: msg.getPlainBody(), html:htmlBody}, + attachments: attachments, inlineImages: inlineImagesObj }; + } catch(e) { + throw new Error("Oops - can't find Gmail draft"); + } + + /** + * Filter draft objects with the matching subject linemessage by matching the subject line. + * @param {string} subject_line to search for draft message + * @return {object} GmailDraft object + */ + function subjectFilter_(subject_line){ + return function(element) { + if (element.getMessage().getSubject() === subject_line) { + return element; + } + } + } + } + + /** + * Fill template string with data object + * @see https://stackoverflow.com/a/378000/1027723 + * @param {string} template string containing {{}} markers which are replaced with data + * @param {object} data object used to replace {{}} markers + * @return {object} message replaced with data + */ + function fillInTemplateFromObject_(template, data) { + // We have two templates one for plain text and the html body + // Stringifing the object means we can do a global replace + let template_string = JSON.stringify(template); + + // Token replacement + template_string = template_string.replace(/{{[^{}]+}}/g, key => { + return escapeData_(data[key.replace(/[{}]+/g, "")] || ""); + }); + return JSON.parse(template_string); + } + + /** + * Escape cell data to make JSON safe + * @see https://stackoverflow.com/a/9204218/1027723 + * @param {string} str to escape JSON special characters from + * @return {string} escaped string + */ + function escapeData_(str) { + return str + .replace(/[\\]/g, '\\\\') + .replace(/[\"]/g, '\\\"') + .replace(/[\/]/g, '\\/') + .replace(/[\b]/g, '\\b') + .replace(/[\f]/g, '\\f') + .replace(/[\n]/g, '\\n') + .replace(/[\r]/g, '\\r') + .replace(/[\t]/g, '\\t'); + }; +} diff --git a/solutions/automations/mail-merge/README.md b/solutions/automations/mail-merge/README.md new file mode 100644 index 000000000..82e2b8f98 --- /dev/null +++ b/solutions/automations/mail-merge/README.md @@ -0,0 +1,3 @@ +# Create a mail merge with Gmail & Google Sheets + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/mail-merge) for additional details. diff --git a/solutions/automations/mail-merge/appsscript.json b/solutions/automations/mail-merge/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/mail-merge/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/chat-bots/schedule-meetings/.clasp.json b/solutions/chat-bots/schedule-meetings/.clasp.json new file mode 100644 index 000000000..34c4cfc7f --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1NdhQ_nXfEUUhWcWKiY6WJjeunY70a1W9vnFdS7BCLPMFreSaHaOS3ucM"} diff --git a/solutions/chat-bots/schedule-meetings/Code.js b/solutions/chat-bots/schedule-meetings/Code.js new file mode 100644 index 000000000..42b7f3f45 --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/Code.js @@ -0,0 +1,190 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/chat-bots/schedule-meetings + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Application constants +const BOTNAME = 'Chat Meeting Scheduler'; +const SLASHCOMMAND = { + HELP: 1, // /help + DIALOG: 2, // /schedule_Meeting +}; + +/** + * Responds to an ADDED_TO_SPACE event in Google Chat. + * Called when the bot is added to a space. The bot can either be directly added to the space + * or added by a @mention. If the bot is added by a @mention, the event object includes a message property. + * Returns a Message object, which is usually a welcome message informing users about the bot. + * + * @param {Object} event The event object from Google Chat + */ +function onAddToSpace(event) { + let message = ''; + + // Personalizes the message depending on how the bot is called. + if (event.space.singleUserBotDm) { + message = `Hi ${event.user.displayName}!`; + } else { + const spaceName = event.space.displayName ? event.space.displayName : "this chat"; + message = `Hi! Thank you for adding me to ${spaceName}`; + } + + // Lets users know what they can do and how they can get help. + message = message + '/nI can quickly schedule a meeting for you with just a few clicks.' + + 'Try me out by typing */schedule_Meeting*. ' + + '/nTo learn what else I can do, type */help*.' + + return { "text": message }; +} + +/** + * Responds to a MESSAGE event triggered in Chat. + * Called when the bot is already in the space and the user invokes it via @mention or / command. + * Returns a message object containing the bot's response. For this bot, the response is either the + * help text or the dialog to schedule a meeting. + * + * @param {object} event The event object from Google Chat + * @return {object} JSON-formatted response as text or Card message + */ +function onMessage(event) { + + // Handles regular onMessage logic. + // Evaluates if and handles for all slash commands. + if (event.message.slashCommand) { + switch (event.message.slashCommand.commandId) { + + case SLASHCOMMAND.DIALOG: // Displays meeting dialog for /schedule_Meeting. + + // TODO update this with your own logic to set meeting recipients, subjects, etc (e.g. a group email). + return getInputFormAsDialog_({ + invitee: '', + startTime: getTopOfHourDateString_(), + duration: 30, + subject: 'Status Stand-up', + body: 'Scheduling a quick status stand-up meeting.' + }); + + case SLASHCOMMAND.HELP: // Responds with help text for /help. + return getHelpTextResponse_(); + + /* TODO Add other use cases here. E.g: + case SLASHCOMMAND.NEW_FEATURE: // Your Feature Here + getDialogForAddContact(message); + */ + + } + } + else { + // Returns text if users didn't invoke a slash command. + return { text: 'No action taken - use Slash Commands.' } + } +} + +/** + * Responds to a CARD_CLICKED event triggered in Chat. + * @param {object} event the event object from Chat + * @return {object} JSON-formatted response + * @see https://developers.google.com/chat/api/guides/message-formats/events + */ +function onCardClick(event) { + if (event.action.actionMethodName === 'handleFormSubmit') { + const recipients = getFieldValue_(event.common.formInputs, 'email'); + const subject = getFieldValue_(event.common.formInputs, 'subject'); + const body = getFieldValue_(event.common.formInputs, 'body'); + + // Assumes dialog card inputs for date and times are in the correct format. mm/dd/yyy HH:MM + const dateTimeInput = getFieldValue_(event.common.formInputs, 'date'); + const startTime = getStartTimeAsDateObject_(dateTimeInput); + const duration = Number(getFieldValue_(event.common.formInputs, 'duration')); + + // Handles instances of missing or invalid input parameters. + const errors = []; + + if (!recipients) { + errors.push('Missing or invalid recipient email address.'); + } + if (!subject) { + errors.push('Missing subject line.'); + } + if (!body) { + errors.push('Missing event description.'); + } + if (!startTime) { + errors.push('Missing or invalid start time.'); + } + if (!duration || isNaN(duration)) { + errors.push('Missing or invalid duration'); + } + if (errors.length) { + // Redisplays the form if missing or invalid inputs exist. + return getInputFormAsDialog_({ + errors, + invitee: recipients, + startTime: dateTimeInput, + duration, + subject, + body + }); + } + + // Calculates the end time via duration. + const endTime = new Date(startTime.valueOf()); + endTime.setMinutes(endTime.getMinutes() + duration); + + // Creates calendar event with notification. + const calendar = CalendarApp.getDefaultCalendar() + const scheduledEvent = calendar.createEvent(subject, + startTime, + endTime, + { + guests: recipients, + sendInvites: true, + description: body + '\nThis meeting scheduled by a Google Chat App!' + }); + + // Gets a link to the Calendar event. + const url = getCalendarEventURL_(scheduledEvent, calendar) + + return getConfirmationDialog_(url); + + } else if (event.action.actionMethodName === 'closeDialog') { + + // Returns this dialog as success. + return { + actionResponse: { + type: 'DIALOG', + dialog_action: { + actionStatus: 'OK' + } + } + } + } +} + +/** + * Responds with help text about this chat bot. + * @return {string} The help text as seen below + */ +function getHelpTextResponse_() { + const help = `*${BOTNAME}* lets you quickly create meetings from Google Chat. Here\'s a list of all its commands: + \`/schedule_Meeting\` Opens a dialog with editable, preset parameters to create a meeting event + \`/help\` Displays this help message + + Learn more about creating Google Chat bots at https://developers.google.com/chat.` + + return { 'text': help } +} diff --git a/solutions/chat-bots/schedule-meetings/Dialog.js b/solutions/chat-bots/schedule-meetings/Dialog.js new file mode 100644 index 000000000..3aea08df5 --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/Dialog.js @@ -0,0 +1,194 @@ +/** +* Form input dialog as JSON. +* @return {object} JSON-formatted cards for the dialog. +*/ +function getInputFormAsDialog_(options) { + const form = getForm_(options); + return { + 'actionResponse': { + 'type': 'DIALOG', + 'dialogAction': { + 'dialog': { + 'body': form + } + } + } + }; +} + +/** +* Form JSON to collect inputs regarding the meeting. +* @return {object} JSON-formatted cards. +*/ +function getForm_(options) { + const sections = []; + + // If errors present, display additional section with validation messages. + if (options.errors && options.errors.length) { + let errors = options.errors.reduce((str, err) => `${str}• ${err}
    `, ''); + errors = `Errors:
    ${errors}`; + const errorSection = { + 'widgets': [ + { + textParagraph: { + text: errors + } + } + ] + } + sections.push(errorSection); + } + let formSection = { + 'header': 'Schedule meeting and send email to invited participants', + 'widgets': [ + { + 'textInput': { + 'label': 'Event Title', + 'type': 'SINGLE_LINE', + 'name': 'subject', + 'value': options.subject + } + }, + { + 'textInput': { + 'label': 'Invitee Email Address', + 'type': 'SINGLE_LINE', + 'name': 'email', + 'value': options.invitee, + 'hintText': 'Add team group email' + } + }, + { + 'textInput': { + 'label': 'Description', + 'type': 'MULTIPLE_LINE', + 'name': 'body', + 'value': options.body + } + }, + { + 'textInput': { + 'label': 'Meeting start date & time', + 'type': 'SINGLE_LINE', + 'name': 'date', + 'value': options.startTime, + 'hintText': 'mm/dd/yyyy H:MM' + } + }, + { + 'selectionInput': { + 'type': 'DROPDOWN', + 'label': 'Meeting Duration', + 'name': 'duration', + 'items': [ + { + 'text': '15 minutes', + 'value': '15', + 'selected': options.duration === 15 + }, + { + 'text': '30 minutes', + 'value': '30', + 'selected': options.duration === 30 + }, + { + 'text': '45 minutes', + 'value': '45', + 'selected': options.duration === 45 + }, + { + 'text': '1 Hour', + 'value': '60', + 'selected': options.duration === 60 + }, + { + 'text': '1.5 Hours', + 'value': '90', + 'selected': options.duration === 90 + }, + { + 'text': '2 Hours', + 'value': '120', + 'selected': options.duration === 120 + } + ] + } + } + ], + 'collapsible': false + }; + sections.push(formSection); + const card = { + 'sections': sections, + 'name': 'Google Chat Scheduled Meeting', + 'fixedFooter': { + 'primaryButton': { + 'text': 'Submit', + 'onClick': { + 'action': { + 'function': 'handleFormSubmit' + } + }, + 'altText': 'Submit' + } + } + }; + return card; +} + +/** +* Confirmation dialog after a calendar event is created successfully. +* @param {string} url The Google Calendar Event url for link button +* @return {object} JSON-formatted cards for the dialog +*/ +function getConfirmationDialog_(url) { + return { + 'actionResponse': { + 'type': 'DIALOG', + 'dialogAction': { + 'dialog': { + 'body': { + 'sections': [ + { + 'widgets': [ + { + 'textParagraph': { + 'text': 'Meeting created successfully!' + }, + 'horizontalAlignment': 'CENTER' + }, + { + 'buttonList': { + 'buttons': [ + { + 'text': 'Open Calendar Event', + 'onClick': { + 'openLink': { + 'url': url + } + } + } + + ] + }, + 'horizontalAlignment': 'CENTER' + } + ] + } + ], + 'fixedFooter': { + 'primaryButton': { + 'text': 'OK', + 'onClick': { + 'action': { + 'function': 'closeDialog' + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/solutions/chat-bots/schedule-meetings/README.md b/solutions/chat-bots/schedule-meetings/README.md new file mode 100644 index 000000000..000aed7af --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/README.md @@ -0,0 +1,4 @@ +# Schedule meetings from Google Chat + +See [developers.google.com](https://developers.google.com/apps-script/samples/chat-apps/schedule-meetings) for additional details. + diff --git a/solutions/chat-bots/schedule-meetings/Utilities.js b/solutions/chat-bots/schedule-meetings/Utilities.js new file mode 100644 index 000000000..cb76fb44d --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/Utilities.js @@ -0,0 +1,58 @@ +/** +* Helper function that gets the field value from the given form input. +* @return {string} +*/ +function getFieldValue_(formInputs, fieldName) { + return formInputs[fieldName][''].stringInputs.value[0]; +} + +// Regular expression to validate the date/time input. +const DATE_TIME_PATTERN = /\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d\d/; + +/** +* Casts date and time from string to Date object. +* @return {date} +*/ +function getStartTimeAsDateObject_(dateTimeStr) { + if (!dateTimeStr || !dateTimeStr.match(DATE_TIME_PATTERN)) { + return null; + } + + const parts = dateTimeStr.split(' '); + const [month, day, year] = parts[0].split('/').map(Number); + const [hour, minute] = parts[1].split(':').map(Number); + + + Session.getScriptTimeZone() + + return new Date(year, month - 1, day, hour, minute) +} + +/** +* Gets the current date and time for the upcoming top of the hour (e.g. 01/25/2022 18:00). +* @return {string} date/time in mm/dd/yyy HH:MM format needed for use by Calendar +*/ +function getTopOfHourDateString_() { + const date = new Date(); + date.setHours(date.getHours() + 1); + date.setMinutes(0, 0, 0); + // Adding the date as string might lead to an incorrect response due to time zone adjustments. + return Utilities.formatDate(date, Session.getScriptTimeZone(), 'MM/dd/yyyy H:mm'); +} + + +/** +* Creates the URL for the Google Calendar event. +* +* @param {object} event The Google Calendar Event instance +* @param {object} cal The associated Google Calendar +* @return {string} URL in the form of 'https://www.google.com/calendar/event?eid={event-id}' +*/ +function getCalendarEventURL_(event, cal) { + const baseCalUrl = 'https://www.google.com/calendar'; + // Joins Calendar Event Id with Calendar Id, then base64 encode to derive the event URL. + let encodedId = Utilities.base64Encode(event.getId().split('@')[0] + " " + cal.getId()).replace(/\=/g, ''); + encodedId = `/event?eid=${encodedId}`; + return (baseCalUrl + encodedId); + +} \ No newline at end of file diff --git a/solutions/chat-bots/schedule-meetings/appsscript.json b/solutions/chat-bots/schedule-meetings/appsscript.json new file mode 100644 index 000000000..40869f567 --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/appsscript.json @@ -0,0 +1,8 @@ +{ + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "chat": { + "addToSpaceFallbackMessage": "Thank you for adding this Chat Bot!" + } +} \ No newline at end of file diff --git a/solutions/custom-functions/calculate-driving-distance/.clasp.json b/solutions/custom-functions/calculate-driving-distance/.clasp.json new file mode 100644 index 000000000..6fb6a8c9b --- /dev/null +++ b/solutions/custom-functions/calculate-driving-distance/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1_cfhZv-VJBekzu1V4mFD1C5ggRaUumWw9rUz0NaLED6XD4_yHB-eJ01a"} diff --git a/solutions/custom-functions/calculate-driving-distance/Code.js b/solutions/custom-functions/calculate-driving-distance/Code.js new file mode 100644 index 000000000..f6ba8cacc --- /dev/null +++ b/solutions/custom-functions/calculate-driving-distance/Code.js @@ -0,0 +1,223 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/custom-functions/calculate-driving-distance + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @OnlyCurrentDoc Limits the script to only accessing the current sheet. + */ + +/** + * A special function that runs when the spreadsheet is open, used to add a + * custom menu to the spreadsheet. + */ +function onOpen() { + try { + const spreadsheet = SpreadsheetApp.getActive(); + const menuItems = [ + {name: 'Prepare sheet...', functionName: 'prepareSheet_'}, + {name: 'Generate step-by-step...', functionName: 'generateStepByStep_'} + ]; + spreadsheet.addMenu('Directions', menuItems); + } catch (e) { + // TODO (Developer) - Handle Exception + Logger.log('Failed with error: %s' + e.error); + } +} + +/** + * A custom function that converts meters to miles. + * + * @param {Number} meters The distance in meters. + * @return {Number} The distance in miles. + */ +function metersToMiles(meters) { + if (typeof meters !== 'number') { + return null; + } + return meters / 1000 * 0.621371; +} + +/** + * A custom function that gets the driving distance between two addresses. + * + * @param {String} origin The starting address. + * @param {String} destination The ending address. + * @return {Number} The distance in meters. + */ +function drivingDistance(origin, destination) { + const directions = getDirections_(origin, destination); + return directions.routes[0].legs[0].distance.value; +} + +/** + * A function that adds headers and some initial data to the spreadsheet. + */ +function prepareSheet_() { + try { + const sheet = SpreadsheetApp.getActiveSheet().setName('Settings'); + const headers = [ + 'Start Address', + 'End Address', + 'Driving Distance (meters)', + 'Driving Distance (miles)']; + const initialData = [ + '350 5th Ave, New York, NY 10118', + '405 Lexington Ave, New York, NY 10174']; + sheet.getRange('A1:D1').setValues([headers]).setFontWeight('bold'); + sheet.getRange('A2:B2').setValues([initialData]); + sheet.setFrozenRows(1); + sheet.autoResizeColumns(1, 4); + } catch (e) { + // TODO (Developer) - Handle Exception + Logger.log('Failed with error: %s' + e.error); + } +} + +/** + * Creates a new sheet containing step-by-step directions between the two + * addresses on the "Settings" sheet that the user selected. + */ +function generateStepByStep_() { + try { + const spreadsheet = SpreadsheetApp.getActive(); + const settingsSheet = spreadsheet.getSheetByName('Settings'); + settingsSheet.activate(); + + // Prompt the user for a row number. + const selectedRow = Browser + .inputBox('Generate step-by-step', 'Please enter the row number of' + + ' the' + ' addresses to use' + ' (for example, "2"):', + Browser.Buttons.OK_CANCEL); + if (selectedRow === 'cancel') { + return; + } + const rowNumber = Number(selectedRow); + if (isNaN(rowNumber) || rowNumber < 2 || + rowNumber > settingsSheet.getLastRow()) { + Browser.msgBox('Error', + Utilities.formatString('Row "%s" is not valid.', selectedRow), + Browser.Buttons.OK); + return; + } + + + // Retrieve the addresses in that row. + const row = settingsSheet.getRange(rowNumber, 1, 1, 2); + const rowValues = row.getValues(); + const origin = rowValues[0][0]; + const destination = rowValues[0][1]; + if (!origin || !destination) { + Browser.msgBox('Error', 'Row does not contain two addresses.', + Browser.Buttons.OK); + return; + } + + // Get the raw directions information. + const directions = getDirections_(origin, destination); + + // Create a new sheet and append the steps in the directions. + const sheetName = 'Driving Directions for Row ' + rowNumber; + let directionsSheet = spreadsheet.getSheetByName(sheetName); + if (directionsSheet) { + directionsSheet.clear(); + directionsSheet.activate(); + } else { + directionsSheet = + spreadsheet.insertSheet(sheetName, spreadsheet.getNumSheets()); + } + const sheetTitle = Utilities + .formatString('Driving Directions from %s to %s', origin, destination); + const headers = [ + [sheetTitle, '', ''], + ['Step', 'Distance (Meters)', 'Distance (Miles)'] + ]; + const newRows = []; + for (const step of directions.routes[0].legs[0].steps) { + // Remove HTML tags from the instructions. + const instructions = step.html_instructions + .replace(/
    |/g, '\n').replace(/<.*?>/g, ''); + newRows.push([ + instructions, + step.distance.value + ]); + } + directionsSheet.getRange(1, 1, headers.length, 3).setValues(headers); + directionsSheet.getRange(headers.length + 1, 1, newRows.length, 2) + .setValues(newRows); + directionsSheet.getRange(headers.length + 1, 3, newRows.length, 1) + .setFormulaR1C1('=METERSTOMILES(R[0]C[-1])'); + + // Format the new sheet. + directionsSheet.getRange('A1:C1').merge().setBackground('#ddddee'); + directionsSheet.getRange('A1:2').setFontWeight('bold'); + directionsSheet.setColumnWidth(1, 500); + directionsSheet.getRange('B2:C').setVerticalAlignment('top'); + directionsSheet.getRange('C2:C').setNumberFormat('0.00'); + const stepsRange = directionsSheet.getDataRange() + .offset(2, 0, directionsSheet.getLastRow() - 2); + setAlternatingRowBackgroundColors_(stepsRange, '#ffffff', '#eeeeee'); + directionsSheet.setFrozenRows(2); + SpreadsheetApp.flush(); + } catch (e) { + // TODO (Developer) - Handle Exception + Logger.log('Failed with error: %s' + e.error); + } +} + +/** + * Sets the background colors for alternating rows within the range. + * @param {Range} range The range to change the background colors of. + * @param {string} oddColor The color to apply to odd rows (relative to the + * start of the range). + * @param {string} evenColor The color to apply to even rows (relative to the + * start of the range). + */ +function setAlternatingRowBackgroundColors_(range, oddColor, evenColor) { + const backgrounds = []; + for (let row = 1; row <= range.getNumRows(); row++) { + const rowBackgrounds = []; + for (let column = 1; column <= range.getNumColumns(); column++) { + if (row % 2 === 0) { + rowBackgrounds.push(evenColor); + } else { + rowBackgrounds.push(oddColor); + } + } + backgrounds.push(rowBackgrounds); + } + range.setBackgrounds(backgrounds); +} + +/** + * A shared helper function used to obtain the full set of directions + * information between two addresses. Uses the Apps Script Maps Service. + * + * @param {String} origin The starting address. + * @param {String} destination The ending address. + * @return {Object} The directions response object. + */ +function getDirections_(origin, destination) { + const directionFinder = Maps.newDirectionFinder(); + directionFinder.setOrigin(origin); + directionFinder.setDestination(destination); + const directions = directionFinder.getDirections(); + if (directions.status !== 'OK') { + throw directions.error_message; + } + return directions; +} \ No newline at end of file diff --git a/solutions/custom-functions/calculate-driving-distance/README.md b/solutions/custom-functions/calculate-driving-distance/README.md new file mode 100644 index 000000000..3648623a0 --- /dev/null +++ b/solutions/custom-functions/calculate-driving-distance/README.md @@ -0,0 +1,4 @@ +# Calculate driving distance & convert meters to miles + +See [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/calculate-driving-distance) for additional details. + diff --git a/solutions/custom-functions/calculate-driving-distance/appsscript.json b/solutions/custom-functions/calculate-driving-distance/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/custom-functions/calculate-driving-distance/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/custom-functions/summarize-sheets-data/.clasp.json b/solutions/custom-functions/summarize-sheets-data/.clasp.json new file mode 100644 index 000000000..a53755e8a --- /dev/null +++ b/solutions/custom-functions/summarize-sheets-data/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1NN-ROSZO3ZsfiVUlCdmNqggpCQuGNtgO_r0nehV0s5mkZJN2bcMTri-7"} diff --git a/solutions/custom-functions/summarize-sheets-data/Code.js b/solutions/custom-functions/summarize-sheets-data/Code.js new file mode 100644 index 000000000..70ac671bc --- /dev/null +++ b/solutions/custom-functions/summarize-sheets-data/Code.js @@ -0,0 +1,83 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/custom-functions/summarize-sheets-data + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Gets summary data from other sheets. The sheets you want to summarize must have columns with headers that match the names of the columns this function summarizes data from. + * + * @return {string} Summary data from other sheets. + * @customfunction + */ + +// The following sheets are ignored. Add additional constants for other sheets that should be ignored. +const READ_ME_SHEET_NAME = "ReadMe"; +const PM_SHEET_NAME = "Summary"; + +/** + * Reads data ranges for each sheet. Filters and counts based on 'Status' columns. To improve performance, the script uses arrays + * until all summary data is gathered. Then the script writes the summary array starting at the cell of the custom function. + */ +function getSheetsData() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let sheets = ss.getSheets(); + let outputArr = []; + + // For each sheet, summarizes the data and pushes to a temporary array. + for (let s in sheets) { + // Gets sheet name. + let sheetNm = sheets[s].getName(); + // Skips ReadMe and Summary sheets. + if (sheetNm === READ_ME_SHEET_NAME || sheetNm === PM_SHEET_NAME) { continue; } + // Gets sheets data. + let values = sheets[s].getDataRange().getValues(); + // Gets the first row of the sheet which is the header row. + let headerRowValues = values[0]; + // Finds the columns with the heading names 'Owner Name' and 'Status' and gets the index value of each. + // Using 'indexOf()' to get the position of each column prevents the script from breaking if the columns change positions in a sheet. + let columnOwner = headerRowValues.indexOf("Owner Name"); + let columnStatus = headerRowValues.indexOf("Status"); + // Removes header row. + values.splice(0,1); + // Gets the 'Owner Name' column value by retrieving the first data row in the array. + let owner = values[0][columnOwner]; + // Counts the total number of tasks. + let taskCnt = values.length; + // Counts the number of tasks that have the 'Complete' status. + // If the options you want to count in your spreadsheet differ, update the strings below to match the text of each option. + // To add more options, copy the line below and update the string to the new text. + let completeCnt = filterByPosition(values,'Complete', columnStatus).length; + // Counts the number of tasks that have the 'In-Progress' status. + let inProgressCnt = filterByPosition(values,'In-Progress', columnStatus).length; + // Counts the number of tasks that have the 'Scheduled' status. + let scheduledCnt = filterByPosition(values,'Scheduled', columnStatus).length; + // Counts the number of tasks that have the 'Overdue' status. + let overdueCnt = filterByPosition(values,'Overdue', columnStatus).length; + // Builds the output array. + outputArr.push([owner,taskCnt,completeCnt,inProgressCnt,scheduledCnt,overdueCnt,sheetNm]); + } + // Writes the output array. + return outputArr; +} + +/** + * Below is a helper function that filters a 2-dimenstional array. + */ +function filterByPosition(array, find, position) { + return array.filter(innerArray => innerArray[position] === find); +} + diff --git a/solutions/custom-functions/summarize-sheets-data/README.md b/solutions/custom-functions/summarize-sheets-data/README.md new file mode 100644 index 000000000..4ae6b97ac --- /dev/null +++ b/solutions/custom-functions/summarize-sheets-data/README.md @@ -0,0 +1,4 @@ +# Summarize data from multiple sheets + +See [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/summarize-sheets-data) for additional details. + diff --git a/solutions/custom-functions/summarize-sheets-data/appsscript.json b/solutions/custom-functions/summarize-sheets-data/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/custom-functions/summarize-sheets-data/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/custom-functions/tier-pricing/.clasp.json b/solutions/custom-functions/tier-pricing/.clasp.json new file mode 100644 index 000000000..c8264b479 --- /dev/null +++ b/solutions/custom-functions/tier-pricing/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1-ql7ECe91XZgWu-hW_UZBx8mhuTtQQj0yNITYh8yQCOuHxLEjxtTngGB"} diff --git a/solutions/custom-functions/tier-pricing/Code.js b/solutions/custom-functions/tier-pricing/Code.js new file mode 100644 index 000000000..9fca30cf0 --- /dev/null +++ b/solutions/custom-functions/tier-pricing/Code.js @@ -0,0 +1,51 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/custom-functions/tier-pricing + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Calculates the tiered pricing discount. + * + * You must provide a value to calculate its discount. The value can be a string or a reference + * to a cell that contains a string. + * You must provide a data table range, for example, $B$4:$D$7, that includes the + * tier start, end, and percent columns. If your table has headers, don't include + * the headers in the range. + * + * @param {string} value The value to calculate the discount for, which can be a string or a + * reference to a cell that contains a string. + * @param {string} table The tier table data range using A1 notation. + * @return number The total discount amount for the value. + * @customfunction + * + */ +function tierPrice(value, table) { + let total = 0; + // Creates an array for each row of the table and loops through each array. + for (let [start, end, percent] of table) { + // Checks if the value is less than the starting value of the tier. If it is less, the loop stops. + if (value < start) { + break; + } + // Calculates the portion of the value to be multiplied by the tier's percent value. + let amount = Math.min(value, end) - start; + // Multiplies the amount by the tier's percent value and adds the product to the total. + total += amount * percent; + } + return total; +} + \ No newline at end of file diff --git a/solutions/custom-functions/tier-pricing/README.md b/solutions/custom-functions/tier-pricing/README.md new file mode 100644 index 000000000..edbadc063 --- /dev/null +++ b/solutions/custom-functions/tier-pricing/README.md @@ -0,0 +1,4 @@ +# Calculate a tiered pricing discount + +See [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/tier-pricing) for additional details. + diff --git a/solutions/custom-functions/tier-pricing/appsscript.json b/solutions/custom-functions/tier-pricing/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/custom-functions/tier-pricing/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file From d909babd56aa798ef37c7e3a71b9e083265a4a4a Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Thu, 21 Apr 2022 15:29:52 -0600 Subject: [PATCH 24/99] Delint html file, delete old bracketmaker sample --- sheets/bracketmaker/bracketmaker.gs | 145 ------------------ .../automations/youtube-tracker/email.html | 27 ++-- 2 files changed, 17 insertions(+), 155 deletions(-) delete mode 100644 sheets/bracketmaker/bracketmaker.gs diff --git a/sheets/bracketmaker/bracketmaker.gs b/sheets/bracketmaker/bracketmaker.gs deleted file mode 100644 index e5ebe1eb9..000000000 --- a/sheets/bracketmaker/bracketmaker.gs +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// [START apps_script_bracketmaker] -// This script works with the Brackets Test spreadsheet to create a tournament bracket -// given a list of players or teams. - -var RANGE_PLAYER1 = 'FirstPlayer'; -var SHEET_PLAYERS = 'Players'; -var SHEET_BRACKET = 'Bracket'; -var CONNECTOR_WIDTH = 15; - -/** - * This method adds a custom menu item to run the script - */ -function onOpen() { - var ss = SpreadsheetApp.getActiveSpreadsheet(); - ss.addMenu('Bracket Maker', - [{name: 'Create Bracket', functionName: 'createBracket'}]); -} - -/** - * This method creates the brackets based on the data provided on the players - */ -function createBracket() { - var ss = SpreadsheetApp.getActiveSpreadsheet(); - // [START apps_script_bracketmaker_range_players_1] - var rangePlayers = ss.getRangeByName(RANGE_PLAYER1); - // [END apps_script_bracketmaker_range_players_1] - var sheetControl = ss.getSheetByName(SHEET_PLAYERS); - var sheetResults = ss.getSheetByName(SHEET_BRACKET); - - // [START apps_script_bracketmaker_range_players_2] - // Get the players from column A. We assume the entire column is filled here. - rangePlayers = rangePlayers.offset(0, 0, sheetControl.getMaxRows() - - rangePlayers.getRowIndex() + 1, 1); - var players = rangePlayers.getValues(); - // [END apps_script_bracketmaker_range_players_2] - - // [START apps_script_bracketmaker_num_players_1] - // Now figure out how many players there are(ie don't count the empty cells) - var numPlayers = 0; - for (var i = 0; i < players.length; i++) { - if (!players[i][0] || players[i][0].length == 0) { - break; - } - numPlayers++; - } - players = players.slice(0, numPlayers); - // [END apps_script_bracketmaker_num_players_1] - - // Provide some error checking in case there are too many or too few players/teams. - if (numPlayers > 64) { - Browser.msgBox('Sorry, this script can only create brackets for 64 or fewer players.'); - return; // Early exit - } - - // [START apps_script_bracketmaker_num_players_2] - if (numPlayers < 3) { - Browser.msgBox('Sorry, you must have at least 3 players.'); - return; // Early exit - } - - // First clear the results sheet and all formatting - sheetResults.clear(); - // [END apps_script_bracketmaker_num_players_2] - - var upperPower = Math.ceil(Math.log(numPlayers) / Math.log(2)); - - // Find out what is the number that is a power of 2 and lower than numPlayers. - var countNodesUpperBound = Math.pow(2, upperPower); - - // Find out what is the number that is a power of 2 and higher than numPlayers. - var countNodesLowerBound = countNodesUpperBound / 2; - - // This is the number of nodes that will not show in the 1st level. - var countNodesHidden = numPlayers - countNodesLowerBound; - - // Enter the players for the 1st round - var currentPlayer = 0; - for (var i = 0; i < countNodesLowerBound; i++) { - if (i < countNodesHidden) { - // Must be on the first level - var rng = sheetResults.getRange(i * 4 + 1, 1); - setBracketItem_(rng, players); - setBracketItem_(rng.offset(2, 0, 1, 1), players); - setConnector_(sheetResults, rng.offset(0, 1, 3, 1)); - setBracketItem_(rng.offset(1, 2, 1, 1)); - } else { - // This player gets a bye - setBracketItem_(sheetResults.getRange(i * 4 + 2, 3), players); - } - } - - // Now fill in the rest of the bracket - upperPower--; - for (var i = 0; i < upperPower; i++) { - var pow1 = Math.pow(2, i + 1); - var pow2 = Math.pow(2, i + 2); - var pow3 = Math.pow(2, i + 3); - for (var j = 0; j < Math.pow(2, upperPower - i - 1); j++) { - setBracketItem_(sheetResults.getRange((j * pow3) + pow2, i * 2 + 5)); - setConnector_(sheetResults, sheetResults.getRange((j * pow3) + pow1, i * 2 + 4, pow2 + 1, 1)); - } - } -} - -// [START apps_script_bracketmaker_set_bracket_item] -/** - * Sets the value of an item in the bracket and the color. - * @param {Range} rng The Spreadsheet Range. - * @param {string[]} players The list of players. - */ -function setBracketItem_(rng, players) { - if (players) { - var rand = Math.ceil(Math.random() * players.length); - rng.setValue(players.splice(rand - 1, 1)[0][0]); - } - rng.setBackgroundColor('yellow'); -} -// [END apps_script_bracketmaker_set_bracket_item] - -/** - * Sets the color and width for connector cells. - * @param {Sheet} sheet The spreadsheet to setup. - * @param {Range} rng The spreadsheet range. - */ -function setConnector_(sheet, rng) { - sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH); - rng.setBackgroundColor('green'); -} -// [END apps_script_bracketmaker] diff --git a/solutions/automations/youtube-tracker/email.html b/solutions/automations/youtube-tracker/email.html index a8e9f9c97..4b3851cd3 100644 --- a/solutions/automations/youtube-tracker/email.html +++ b/solutions/automations/youtube-tracker/email.html @@ -1,13 +1,20 @@ -Hello,

    You have new comments and/or replies on videos:

    - - - - - - - - -
    Video TitleLinkNumber of new replies and comments
    + + Hello,

    You have new comments and/or replies on videos:

    + + + + + + + + + + + + + +
    Video TitleLinkNumber of new replies and comments
    + From 341743ebf0b9e19e8f0197b95907afa47916657b Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Thu, 21 Apr 2022 15:33:14 -0600 Subject: [PATCH 25/99] Import add-on solution --- solutions/add-on/share-macro/.clasp.json | 1 + solutions/add-on/share-macro/Code.js | 165 ++++++++++++++ solutions/add-on/share-macro/README.md | 4 + solutions/add-on/share-macro/UI.js | 217 +++++++++++++++++++ solutions/add-on/share-macro/appsscript.json | 28 +++ 5 files changed, 415 insertions(+) create mode 100644 solutions/add-on/share-macro/.clasp.json create mode 100644 solutions/add-on/share-macro/Code.js create mode 100644 solutions/add-on/share-macro/README.md create mode 100644 solutions/add-on/share-macro/UI.js create mode 100644 solutions/add-on/share-macro/appsscript.json diff --git a/solutions/add-on/share-macro/.clasp.json b/solutions/add-on/share-macro/.clasp.json new file mode 100644 index 000000000..b92f05bc2 --- /dev/null +++ b/solutions/add-on/share-macro/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1BsbWOAbLADGoLtp5P9oqctZMiqT5EFh_R-CufxAV9y1hvVSAMO35Azu9"} diff --git a/solutions/add-on/share-macro/Code.js b/solutions/add-on/share-macro/Code.js new file mode 100644 index 000000000..3d5bb47e0 --- /dev/null +++ b/solutions/add-on/share-macro/Code.js @@ -0,0 +1,165 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.devsite.corp.google.com/apps-script/add-ons/share-macro + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Uses Apps Script API to copy source Apps Script project + * to destination Google Spreadsheet container. + * + * @param {string} sourceScriptId - Script ID of the source project. + * @param {string} targetSpreadsheetUrl - URL if the target spreadsheet. + * @return {Card[]} - Card indicating successful copy. + */ +function shareMacro_(sourceScriptId, targetSpreadsheetUrl) { + + // Gets the source project content using the Apps Script API. + const sourceProject = APPS_SCRIPT_API.get(sourceScriptId); + const sourceFiles = APPS_SCRIPT_API.getContent(sourceScriptId); + + // Opens the target spreadsheet and gets its ID. + const parentSSId = SpreadsheetApp.openByUrl(targetSpreadsheetUrl).getId(); + + // Creates an Apps Script project that's bound to the target spreadsheet. + const targetProjectObj = APPS_SCRIPT_API.create(sourceProject.title, parentSSId); + + // Updates the Apps Script project with the source project content. + APPS_SCRIPT_API.updateContent(targetProjectObj.scriptId, sourceFiles); + +} + +/** + * Function that encapsulates Apps Script API project manipulation. +*/ +const APPS_SCRIPT_API = { + accessToken: ScriptApp.getOAuthToken(), + + /* APPS_SCRIPT_API.get + * Gets Apps Script source project. + * @param {string} scriptId - Script ID of the source project. + * @return {Object} - JSON representation of source project. + */ + get: function (scriptId) { + const url = ('https://script.googleapis.com/v1/projects/' + scriptId); + const options = { + "method": 'get', + "headers": { + "Authorization": "Bearer " + this.accessToken + }, + "muteHttpExceptions": true, + }; + const res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() == 200) { + return JSON.parse(res); + } else { + console.log('An error occurred gettting the project details'); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + } + }, + + /* APPS_SCRIPT_API.create + * Creates new Apps Script project in the target spreadsheet. + * @param {string} title - Name of Apps Script project. + * @param {string} parentId - Internal ID of target spreadsheet. + * @return {Object} - JSON representation completed project creation. + */ + create: function (title, parentId) { + const url = 'https://script.googleapis.com/v1/projects'; + const options = { + "headers": { + "Authorization": "Bearer " + this.accessToken, + "Content-Type": "application/json" + }, + "muteHttpExceptions": true, + "method": "POST", + "payload": { "title": title } + } + if (parentId) { + options.payload.parentId = parentId; + } + options.payload = JSON.stringify(options.payload); + let res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() == 200) { + res = JSON.parse(res); + return res; + } else { + console.log("An error occurred while creating the project"); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + } + }, + /* APPS_SCRIPT_API.getContent + * Gets the content of the source Apps Script project. + * @param {string} scriptId - Script ID of the source project. + * @return {Object} - JSON representation of Apps Script project content. + */ + getContent: function (scriptId) { + const url = "https://script.googleapis.com/v1/projects/" + scriptId + "/content"; + const options = { + "method": 'get', + "headers": { + "Authorization": "Bearer " + this.accessToken + }, + "muteHttpExceptions": true, + }; + let res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() == 200) { + res = JSON.parse(res); + return res['files']; + } else { + console.log('An error occurred obtaining the content from the source script'); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + } + }, + + /* APPS_SCRIPT_API.updateContent + * Updates (copies) content from source to target Apps Script project. + * @param {string} scriptId - Script ID of the source project. + * @param {Object} files - JSON representation of Apps Script project content. + * @return {boolean} - Result status of the function. + */ + updateContent: function (scriptId, files) { + const url = "https://script.googleapis.com/v1/projects/" + scriptId + "/content"; + const options = { + "method": 'put', + "headers": { + "Authorization": "Bearer " + this.accessToken + }, + "contentType": "application/json", + "payload": JSON.stringify({ "files": files }), + "muteHttpExceptions": true, + }; + let res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() == 200) { + return true; + } else { + console.log(`An error occurred updating content of script ${scriptId}`); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + } + } +} \ No newline at end of file diff --git a/solutions/add-on/share-macro/README.md b/solutions/add-on/share-macro/README.md new file mode 100644 index 000000000..c070b8721 --- /dev/null +++ b/solutions/add-on/share-macro/README.md @@ -0,0 +1,4 @@ +# Copy macros to other spreadsheets + +See [developers.google.com](https://developers.google.com/apps-script/add-ons/share-macro) for additional details. + diff --git a/solutions/add-on/share-macro/UI.js b/solutions/add-on/share-macro/UI.js new file mode 100644 index 000000000..1e351dc29 --- /dev/null +++ b/solutions/add-on/share-macro/UI.js @@ -0,0 +1,217 @@ +// Change application logo here (and in manifest) as desired. +const ADDON_LOGO = 'https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png'; + +/** + * Callback function for rendering the main card. + * @return {CardService.Card} The card to show the user. + */ +function onHomepage(e) { + return createSelectionCard(e); +} + +/** + * Builds the primary card interface used to collect user inputs. + * + * @param {Object} e - Add-on event object. + * @param {string} sourceScriptId - Script ID of the source project. + * @param {string} targetSpreadsheetUrl - URL of the target spreadsheet. + * @param {string[]} errors - Array of error messages. + * + * @return {CardService.Card} The card to show to the user for inputs. + */ +function createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors) { + + // Configures card header. + let cardHeader = CardService.newCardHeader() + .setTitle('Share macros with other spreadheets!') + .setImageUrl(ADDON_LOGO) + .setImageStyle(CardService.ImageStyle.SQUARE); + + // If form errors exist, configures section with error messages. + let showErrors = false; + + if (errors && errors.length) { + showErrors = true; + let msg = errors.reduce((str, err) => `${str}• ${err}
    `, ''); + msg = `Form submission errors:
    ${msg}`; + + // Builds error message section. + sectionErrors = CardService.newCardSection() + .addWidget(CardService.newDecoratedText() + .setText(msg) + .setWrapText(true)); + } + + // Configures source project section. + let sectionSource = CardService.newCardSection() + .addWidget(CardService.newDecoratedText() + .setText('Source macro
    The Apps Script project to copy')) + + .addWidget(CardService.newTextInput() + .setFieldName('sourceScriptId') + .setValue(sourceScriptId || '') + .setTitle('Script ID of the source macro') + .setHint('You must have at least edit permission for the source spreadsheet to access its script project')) + + .addWidget(CardService.newTextButton() + .setText('Find the script ID') + .setOpenLink(CardService.newOpenLink() + .setUrl('https://developers.google.com/apps-script/api/samples/execute') + .setOpenAs(CardService.OpenAs.FULL_SIZE) + .setOnClose(CardService.OnClose.NOTHING))); + + // Configures target spreadsheet section. + let sectionTarget = CardService.newCardSection() + .addWidget(CardService.newDecoratedText() + .setText('Target spreadsheet')) + + .addWidget(CardService.newTextInput() + .setFieldName('targetSpreadsheetUrl') + .setValue(targetSpreadsheetUrl || '') + .setHint('You must have at least edit permission for the target spreadsheet') + .setTitle('Target spreadsheet URL')); + + // Configures help section. + let sectionHelp = CardService.newCardSection() + .addWidget(CardService.newDecoratedText() + .setText('NOTE: ' + + 'The Apps Script API must be turned on.') + .setWrapText(true)) + + .addWidget(CardService.newTextButton() + .setText('Turn on Apps Script API') + .setOpenLink(CardService.newOpenLink() + .setUrl('https://script.google.com/home/usersettings') + .setOpenAs(CardService.OpenAs.FULL_SIZE) + .setOnClose(CardService.OnClose.NOTHING))); + + // Configures card footer with action to copy the macro. + var cardFooter = CardService.newFixedFooter() + .setPrimaryButton(CardService.newTextButton() + .setText('Share macro') + .setOnClickAction(CardService.newAction() + .setFunctionName('onClickFunction_'))); + + // Begins building the card. + let builder = CardService.newCardBuilder() + .setHeader(cardHeader); + + // Adds error section if applicable. + if (showErrors) { + builder.addSection(sectionErrors) + } + + // Adds final sections & footer. + builder + .addSection(sectionSource) + .addSection(sectionTarget) + .addSection(sectionHelp) + .setFixedFooter(cardFooter); + + return builder.build(); +} + +/** + * Action handler that validates user inputs and calls shareMacro_ + * function to copy Apps Script project to target spreadsheet. + * + * @param {Object} e - Add-on event object. + * + * @return {CardService.Card} Responds with either a success or error card. + */ +function onClickFunction_(e) { + + const sourceScriptId = e.formInput.sourceScriptId; + const targetSpreadsheetUrl = e.formInput.targetSpreadsheetUrl; + + // Validates inputs for errors. + const errors = []; + + // Pushes an error message if the Script ID parameter is missing. + if (!sourceScriptId) { + errors.push('Missing script ID'); + } else { + + // Gets the Apps Script project if the Script ID parameter is valid. + const sourceProject = APPS_SCRIPT_API.get(sourceScriptId); + if (!sourceProject) { + // Pushes an error message if the Script ID parameter isn't valid. + errors.push('Invalid script ID'); + } + } + + // Pushes an error message if the spreadsheet URL is missing. + if (!targetSpreadsheetUrl) { + errors.push('Missing Spreadsheet URL'); + } else + try { + // Tests for valid spreadsheet URL to get the spreadsheet ID. + const ssId = SpreadsheetApp.openByUrl(targetSpreadsheetUrl).getId(); + } catch (err) { + // Pushes an error message if the spreadsheet URL parameter isn't valid. + errors.push('Invalid spreadsheet URL'); + } + + if (errors && errors.length) { + // Redisplays form if inputs are missing or invalid. + return createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors); + + } else { + // Calls shareMacro function to copy the project. + shareMacro_(sourceScriptId, targetSpreadsheetUrl); + + // Creates a success card to display to users. + return buildSuccessCard(e, targetSpreadsheetUrl); + } +} + +/** + * Builds success card to inform user & let them open the spreadsheet. + * + * @param {Object} e - Add-on event object. + * @param {string} targetSpreadsheetUrl - URL of the target spreadsheet. + * + * @return {CardService.Card} Returns success card. + */function buildSuccessCard(e, targetSpreadsheetUrl) { + + // Configures card header. + let cardHeader = CardService.newCardHeader() + .setTitle('Share macros with other spreadsheets!') + .setImageUrl(ADDON_LOGO) + .setImageStyle(CardService.ImageStyle.SQUARE); + + // Configures card body section with success message and open button. + let sectionBody1 = CardService.newCardSection() + .addWidget(CardService.newTextParagraph() + .setText('Sharing process is complete!')) + .addWidget(CardService.newTextButton() + .setText('Open spreadsheet') + .setOpenLink(CardService.newOpenLink() + .setUrl(targetSpreadsheetUrl) + .setOpenAs(CardService.OpenAs.FULL_SIZE) + .setOnClose(CardService.OnClose.RELOAD_ADD_ON))); + let sectionBody2 = CardService.newCardSection() + .addWidget(CardService.newTextParagraph() + .setText('If you don\'t see the copied project in your target spreadsheet,' + + ' make sure you turned on the Apps Script API in the Apps Script dashboard.')) + .addWidget(CardService.newTextButton() + .setText("Check API") + .setOpenLink(CardService.newOpenLink() + .setUrl('https://script.google.com/home/usersettings') + .setOpenAs(CardService.OpenAs.FULL_SIZE) + .setOnClose(CardService.OnClose.RELOAD_ADD_ON))); + + // Configures the card footer with action to start new process. + let cardFooter = CardService.newFixedFooter() + .setPrimaryButton(CardService.newTextButton() + .setText('Share another') + .setOnClickAction(CardService.newAction() + .setFunctionName('onHomepage'))); + + return builder = CardService.newCardBuilder() + .setHeader(cardHeader) + .addSection(sectionBody1) + .addSection(sectionBody2) + .setFixedFooter(cardFooter) + .build(); + } \ No newline at end of file diff --git a/solutions/add-on/share-macro/appsscript.json b/solutions/add-on/share-macro/appsscript.json new file mode 100644 index 000000000..0a9e3f14f --- /dev/null +++ b/solutions/add-on/share-macro/appsscript.json @@ -0,0 +1,28 @@ +{ + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/script.projects" + ], + "urlFetchWhitelist": [ + "https://script.googleapis.com/" + ], + "addOns": { + "common": { + "name": "Share Macro", + "logoUrl": "https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png", + "layoutProperties": { + "primaryColor": "#188038", + "secondaryColor": "#34a853" + }, + "homepageTrigger": { + "runFunction": "onHomepage" + } + }, + "sheets": {} + } +} \ No newline at end of file From f8662118af051cb093c63de2fec5f868e7c467d9 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Fri, 22 Apr 2022 12:41:46 -0600 Subject: [PATCH 26/99] Add clean sheets solution --- .../editor-add-on/clean-sheet/.clasp.json | 1 + solutions/editor-add-on/clean-sheet/Code.js | 246 ++++++++++++++++++ solutions/editor-add-on/clean-sheet/Menu.js | 44 ++++ solutions/editor-add-on/clean-sheet/README.md | 4 + .../editor-add-on/clean-sheet/appsscript.json | 7 + 5 files changed, 302 insertions(+) create mode 100644 solutions/editor-add-on/clean-sheet/.clasp.json create mode 100644 solutions/editor-add-on/clean-sheet/Code.js create mode 100644 solutions/editor-add-on/clean-sheet/Menu.js create mode 100644 solutions/editor-add-on/clean-sheet/README.md create mode 100644 solutions/editor-add-on/clean-sheet/appsscript.json diff --git a/solutions/editor-add-on/clean-sheet/.clasp.json b/solutions/editor-add-on/clean-sheet/.clasp.json new file mode 100644 index 000000000..2cae3aa10 --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "10bxhn6eGypm20dgRcTbUCbzP4Bz0dyYR6IZTNEA2gIXXxwoy8Zqs06yr"} diff --git a/solutions/editor-add-on/clean-sheet/Code.js b/solutions/editor-add-on/clean-sheet/Code.js new file mode 100644 index 000000000..deb3136c3 --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/Code.js @@ -0,0 +1,246 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/add-ons/clean-sheet + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Application Constants +const APP_TITLE = 'Clean sheet'; + +/** + * Identifies and deletes empty rows in selected range of active sheet. + * + * Cells that contain space characters are treated as non-empty. + * The entire row, including the cells outside of the selected range, + * must be empty to be deleted. + * + * Called from menu option. + */ +function deleteEmptyRows() { + + const sheet = SpreadsheetApp.getActiveSheet(); + + // Gets active selection and dimensions. + let activeRange = sheet.getActiveRange(); + const rowCount = activeRange.getHeight(); + const firstActiveRow = activeRange.getRow(); + const columnCount = sheet.getMaxColumns(); + + // Tests that the selection is a valid range. + if (rowCount < 1) { + showMessage('Select a valid range.'); + return; + } + // Tests active range isn't too large to process. Enforces limit set to 10k. + if (rowCount > 10000) { + showMessage("Selected range too large. Select up to 10,000 rows at one time."); + return; + } + + // Utilizes an array of values for efficient processing to determine blank rows. + const activeRangeValues = sheet.getRange(firstActiveRow, 1, rowCount, columnCount).getValues(); + + // Checks if array is all empty values. + const valueFilter = value => value !== ''; + const isRowEmpty = (row) => { + return row.filter(valueFilter).length === 0; + } + + // Maps the range values as an object with value (to test) and corresponding row index (with offset from selection). + const rowsToDelete = activeRangeValues.map((row, index) => ({ row, offset: index + activeRange.getRowIndex() })) + .filter(item => isRowEmpty(item.row)) // Test to filter out non-empty rows. + .map(item => item.offset); //Remap to include just the row indexes that will be removed. + + // Combines a sorted, ascending list of indexes into a set of ranges capturing consecutive values as start/end ranges. + // Combines sequential empty rows for faster processing. + const rangesToDelete = rowsToDelete.reduce((ranges, index) => { + const currentRange = ranges[ranges.length - 1]; + if (currentRange && index === currentRange[1] + 1) { + currentRange[1] = index; + return ranges; + } + ranges.push([index, index]); + return ranges; + }, []); + + // Sends a list of row indexes to be deleted to the console. + console.log(rangesToDelete); + + // Deletes the rows using REVERSE order to ensure proper indexing is used. + rangesToDelete.reverse().forEach(([start, end]) => sheet.deleteRows(start, end - start + 1)); + SpreadsheetApp.flush(); +} + +/** + * Removes blank columns in a selected range. + * + * Cells containing Space characters are treated as non-empty. + * The entire column, including cells outside of the selected range, + * must be empty to be deleted. + * + * Called from menu option. + */ +function deleteEmptyColumns() { + + const sheet = SpreadsheetApp.getActiveSheet(); + + // Gets active selection and dimensions. + let activeRange = sheet.getActiveRange(); + const rowCountMax = sheet.getMaxRows(); + const columnWidth = activeRange.getWidth(); + const firstActiveColumn = activeRange.getColumn(); + + // Tests that the selection is a valid range. + if (columnWidth < 1) { + showMessage('Select a valid range.'); + return; + } + // Tests active range is not too large to process. Enforces limit set to 1k. + if (columnWidth > 1000) { + showMessage("Selected range too large. Select up to 10,000 rows at one time."); + return; + } + + // Utilizes an array of values for efficient processing to determine blank columns. + const activeRangeValues = sheet.getRange(1, firstActiveColumn, rowCountMax, columnWidth).getValues(); + + // Transposes the array of range values so it can be processed in order of columns. + const activeRangeValuesTransposed = activeRangeValues[0].map((_, colIndex) => activeRangeValues.map(row => row[colIndex])); + + // Checks if array is all empty values. + const valueFilter = value => value !== ''; + const isColumnEmpty = (column) => { + return column.filter(valueFilter).length === 0; + } + + // Maps the range values as an object with value (to test) and corresponding column index (with offset from selection). + const columnsToDelete = activeRangeValuesTransposed.map((column, index) => ({ column, offset: index + firstActiveColumn})) + .filter(item => isColumnEmpty(item.column)) // Test to filter out non-empty rows. + .map(item => item.offset); //Remap to include just the column indexes that will be removed. + + // Combines a sorted, ascending list of indexes into a set of ranges capturing consecutive values as start/end ranges. + // Combines sequential empty columns for faster processing. + const rangesToDelete = columnsToDelete.reduce((ranges, index) => { + const currentRange = ranges[ranges.length - 1]; + if (currentRange && index === currentRange[1] + 1) { + currentRange[1] = index; + return ranges; + } + ranges.push([index, index]); + return ranges; + }, []); + + // Sends a list of column indexes to be deleted to the console. + console.log(rangesToDelete); + + // Deletes the columns using REVERSE order to ensure proper indexing is used. + rangesToDelete.reverse().forEach(([start, end]) => sheet.deleteColumns(start, end - start + 1)); + SpreadsheetApp.flush(); +} + +/** + * Trims all of the unused rows and columns outside of selected data range. + * + * Called from menu option. + */ +function cropSheet() { + const dataRange = SpreadsheetApp.getActiveSheet().getDataRange(); + const sheet = dataRange.getSheet(); + + let numRows = dataRange.getNumRows(); + let numColumns = dataRange.getNumColumns(); + + const maxRows = sheet.getMaxRows(); + const maxColumns = sheet.getMaxColumns(); + + const numFrozenRows = sheet.getFrozenRows(); + const numFrozenColumns = sheet.getFrozenColumns(); + + // If last data row is less than maximium row, then deletes rows after the last data row. + if (numRows < maxRows) { + numRows = Math.max(numRows, numFrozenRows + 1); // Don't crop empty frozen rows. + sheet.deleteRows(numRows + 1, maxRows - numRows); + } + + // If last data column is less than maximium column, then deletes columns after the last data column. + if (numColumns < maxColumns) { + numColumns = Math.max(numColumns, numFrozenColumns + 1); // Don't crop empty frozen columns. + sheet.deleteColumns(numColumns + 1, maxColumns - numColumns); + } +} + +/** + * Copies value of active cell to the blank cells beneath it. + * Stops at last row of the sheet's data range if only blank cells are encountered. + * + * Called from menu option. + */ +function fillDownData() { + + const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); + + // Gets sheet's active cell and confirms it's not empty. + const activeCell = sheet.getActiveCell(); + const activeCellValue = activeCell.getValue(); + + if (!activeCellValue) { + showMessage("The active cell is empty. Nothing to fill."); + return; + } + + // Gets coordinates of active cell. + const column = activeCell.getColumn(); + const row = activeCell.getRow(); + + // Gets entire data range of the sheet. + const dataRange = sheet.getDataRange(); + const dataRangeRows = dataRange.getNumRows(); + + // Gets trimmed range starting from active cell to the end of sheet data range. + const searchRange = dataRange.offset(row - 1, column - 1, dataRangeRows - row + 1, 1) + const searchValues = searchRange.getDisplayValues(); + + // Find the number of empty rows below the active cell. + let i = 1; // Start at 1 to skip the ActiveCell. + while (searchValues[i] && searchValues[i][0] == "") { i++; } + + // If blanks exist, fill the range with values. + if (i > 1) { + const fillRange = searchRange.offset(0, 0, i, 1).setValue(activeCellValue) + //sheet.setActiveRange(fillRange) // Uncomment to test affected range. + } + else { + showMessage("There are no empty cells below the Active Cell to fill."); + } +} + +/** + * A helper function to display messages to user. + * + * @param {string} message - Message to be displayed. + * @param {string} caller - {Optional} text to append to title. + */ +function showMessage(message, caller) { + + // Sets the title using the APP_TITLE variable; adds optional caller string. + const title = APP_TITLE + if (caller != null) { + title += ` : ${caller}` + }; + + const ui = SpreadsheetApp.getUi(); + ui.alert(title, message, ui.ButtonSet.OK); +} diff --git a/solutions/editor-add-on/clean-sheet/Menu.js b/solutions/editor-add-on/clean-sheet/Menu.js new file mode 100644 index 000000000..71abafad3 --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/Menu.js @@ -0,0 +1,44 @@ +/** + * Creates a menu entry in the Google Sheets Extensions menu when the document is opened. + * + * @param {object} e The event parameter for a simple onOpen trigger. + */ +function onOpen(e) { + // Builds a menu that displays under the Extensions menu in Sheets. + let menu = SpreadsheetApp.getUi().createAddonMenu() + + menu + .addItem('Delete blank rows (from selected rows only)', 'deleteEmptyRows') + .addItem('Delete blank columns (from selected columns only)', 'deleteEmptyColumns') + .addItem('Crop sheet to data range', 'cropSheet') + .addSeparator() + .addItem('Fill in blank rows below', 'fillDownData') + .addSeparator() + .addItem('About', 'aboutApp') + .addToUi(); +} + +/** + * Runs when the add-on is installed; calls onOpen() to ensure menu creation and + * any other initializion work is done immediately. This method is only used by + * the desktop add-on and is never called by the mobile version. + * + * @param {object} e The event parameter for a simple onInstall trigger. + */ +function onInstall(e) { + onOpen(e); +} + +/** + * About box for context and developer contact information. + * TODO: Personalize + */ +function aboutApp() { + const msg = ` + Name: ${APP_TITLE} + Version: 1.0 + Contact: ` + + const ui = SpreadsheetApp.getUi(); + ui.alert("About this application", msg, ui.ButtonSet.OK); +} \ No newline at end of file diff --git a/solutions/editor-add-on/clean-sheet/README.md b/solutions/editor-add-on/clean-sheet/README.md new file mode 100644 index 000000000..11a3932ca --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/README.md @@ -0,0 +1,4 @@ +# Clean up data in a spreadsheet + +See [developers.google.com](https://developers.google.com/apps-script/add-ons/clean-sheet) for additional details. + diff --git a/solutions/editor-add-on/clean-sheet/appsscript.json b/solutions/editor-add-on/clean-sheet/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file From 095524194cbabc1fbd57dd053f56884af137b93c Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 25 Apr 2022 16:11:45 -0600 Subject: [PATCH 27/99] Simplify eslint config/dependencies + delint --- .eslintrc.js | 9 +- advanced/docs.gs | 2 +- data-studio/auth.gs | 6 +- package-lock.json | 13 - package.json | 1 - yarn.lock | 1115 +++++++++++++++++++++---------------------- 6 files changed, 564 insertions(+), 582 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index eb416789c..d7e926559 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,10 +10,11 @@ module.exports = { rules: { 'comma-dangle': ['error', 'never'], 'max-len': ['error', { code: 100 }], - camelcase: 'off', // Off for destructuring - 'async-await/space-after-async': 2, - 'async-await/space-after-await': 2, - eqeqeq: 2, + 'camelcase': ['error', { + 'ignoreDestructuring': true, + 'ignoreImports': true, + 'allow': ['access_type', 'redirect_uris'], + }], 'guard-for-in': 'off', 'no-var': 'off', // ES3 'no-unused-vars': 'off' // functions aren't used. diff --git a/advanced/docs.gs b/advanced/docs.gs index 76763dbb3..9ba571109 100644 --- a/advanced/docs.gs +++ b/advanced/docs.gs @@ -95,7 +95,7 @@ function insertAndStyleText(documentId, text) { startIndex: 1, endIndex: text.length + 1 }, - text_style: { + textStyle: { fontSize: { magnitude: 12, unit: 'PT' diff --git a/data-studio/auth.gs b/data-studio/auth.gs index 0d7104fdf..5035107ee 100644 --- a/data-studio/auth.gs +++ b/data-studio/auth.gs @@ -133,9 +133,9 @@ function resetAuth() { * Resets the auth service. */ function resetAuth() { - var user_tokenProperties = PropertiesService.getUserProperties(); - user_tokenProperties.deleteProperty('dscc.username'); - user_tokenProperties.deleteProperty('dscc.password'); + var userTokenProperties = PropertiesService.getUserProperties(); + userTokenProperties.deleteProperty('dscc.username'); + userTokenProperties.deleteProperty('dscc.password'); } // [END apps_script_data_studio_auth_reset_user_token] diff --git a/package-lock.json b/package-lock.json index 8c38269fb..165fab447 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "devDependencies": { "eslint": "8.12.0", "eslint-config-google": "0.14.0", - "eslint-plugin-async-await": "0.0.0", "eslint-plugin-googleappsscript": "1.0.4" } }, @@ -312,12 +311,6 @@ "eslint": ">=5.16.0" } }, - "node_modules/eslint-plugin-async-await": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-async-await/-/eslint-plugin-async-await-0.0.0.tgz", - "integrity": "sha1-DyrhejgUeAY11I8kCd+eN4mMoJ8=", - "dev": true - }, "node_modules/eslint-plugin-googleappsscript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz", @@ -1173,12 +1166,6 @@ "dev": true, "requires": {} }, - "eslint-plugin-async-await": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-async-await/-/eslint-plugin-async-await-0.0.0.tgz", - "integrity": "sha1-DyrhejgUeAY11I8kCd+eN4mMoJ8=", - "dev": true - }, "eslint-plugin-googleappsscript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz", diff --git a/package.json b/package.json index eed433321..42aedd29c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "devDependencies": { "eslint": "8.12.0", "eslint-config-google": "0.14.0", - "eslint-plugin-async-await": "0.0.0", "eslint-plugin-googleappsscript": "1.0.4" }, "scripts": { diff --git a/yarn.lock b/yarn.lock index 3b08c57c8..da2097706 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,575 +3,570 @@ "@eslint/eslintrc@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz" - integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.3.1" - globals "^13.9.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" + "integrity" "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==" + "resolved" "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz" + "version" "1.2.1" + dependencies: + "ajv" "^6.12.4" + "debug" "^4.3.2" + "espree" "^9.3.1" + "globals" "^13.9.0" + "ignore" "^5.2.0" + "import-fresh" "^3.2.1" + "js-yaml" "^4.1.0" + "minimatch" "^3.0.4" + "strip-json-comments" "^3.1.1" "@humanwhocodes/config-array@^0.9.2": - version "0.9.2" - resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz" - integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA== + "integrity" "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==" + "resolved" "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz" + "version" "0.9.2" dependencies: "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" + "debug" "^4.1.1" + "minimatch" "^3.0.4" "@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -acorn-jsx@^5.3.1: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn@^8.7.0: - version "8.7.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== - -ajv@^6.10.0, ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -chalk@^4.0.0: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -debug@^4.1.1, debug@^4.3.2: - version "4.3.3" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-config-google@0.14.0: - version "0.14.0" - resolved "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz" - integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== - -eslint-plugin-async-await@0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/eslint-plugin-async-await/-/eslint-plugin-async-await-0.0.0.tgz" - integrity sha1-DyrhejgUeAY11I8kCd+eN4mMoJ8= - -eslint-plugin-googleappsscript@1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz" - integrity sha512-Z6w1EMw0z0VOUvI0PFquigNSkYTgB+pVb2O2KYedDHnllwrgjjNBOLxsqPmtwmjOtFqCu7TuL0hJQwxVnp5OzQ== - dependencies: - requireindex "~1.1.0" + "integrity" "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "resolved" "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" + "version" "1.2.1" + +"acorn-jsx@^5.3.1": + "integrity" "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" + "resolved" "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + "version" "5.3.2" + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", "acorn@^8.7.0": + "integrity" "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" + "resolved" "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz" + "version" "8.7.0" + +"ajv@^6.10.0", "ajv@^6.12.4": + "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" + "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + "version" "6.12.6" + dependencies: + "fast-deep-equal" "^3.1.1" + "fast-json-stable-stringify" "^2.0.0" + "json-schema-traverse" "^0.4.1" + "uri-js" "^4.2.2" + +"ansi-regex@^5.0.1": + "integrity" "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + "version" "5.0.1" + +"ansi-styles@^4.1.0": + "integrity" "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" + "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "color-convert" "^2.0.1" + +"argparse@^2.0.1": + "integrity" "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "resolved" "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + "version" "2.0.1" + +"balanced-match@^1.0.0": + "integrity" "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + "version" "1.0.2" + +"brace-expansion@^1.1.7": + "integrity" "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" + "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + "version" "1.1.11" + dependencies: + "balanced-match" "^1.0.0" + "concat-map" "0.0.1" + +"callsites@^3.0.0": + "integrity" "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "resolved" "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + "version" "3.1.0" + +"chalk@^4.0.0": + "integrity" "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" + "resolved" "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + "version" "4.1.2" + dependencies: + "ansi-styles" "^4.1.0" + "supports-color" "^7.1.0" + +"color-convert@^2.0.1": + "integrity" "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" + "resolved" "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + "version" "2.0.1" + dependencies: + "color-name" "~1.1.4" + +"color-name@~1.1.4": + "integrity" "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + "version" "1.1.4" + +"concat-map@0.0.1": + "integrity" "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "resolved" "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + "version" "0.0.1" + +"cross-spawn@^7.0.2": + "integrity" "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==" + "resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + "version" "7.0.3" + dependencies: + "path-key" "^3.1.0" + "shebang-command" "^2.0.0" + "which" "^2.0.1" + +"debug@^4.1.1", "debug@^4.3.2": + "integrity" "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==" + "resolved" "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" + "version" "4.3.3" + dependencies: + "ms" "2.1.2" + +"deep-is@^0.1.3": + "integrity" "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "resolved" "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + "version" "0.1.4" + +"doctrine@^3.0.0": + "integrity" "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==" + "resolved" "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "esutils" "^2.0.2" + +"escape-string-regexp@^4.0.0": + "integrity" "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + "resolved" "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + "version" "4.0.0" + +"eslint-config-google@0.14.0": + "integrity" "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==" + "resolved" "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz" + "version" "0.14.0" + +"eslint-plugin-googleappsscript@1.0.4": + "integrity" "sha512-Z6w1EMw0z0VOUvI0PFquigNSkYTgB+pVb2O2KYedDHnllwrgjjNBOLxsqPmtwmjOtFqCu7TuL0hJQwxVnp5OzQ==" + "resolved" "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz" + "version" "1.0.4" + dependencies: + "requireindex" "~1.1.0" -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== +"eslint-scope@^7.1.1": + "integrity" "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==" + "resolved" "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" + "version" "7.1.1" + dependencies: + "esrecurse" "^4.3.0" + "estraverse" "^5.2.0" + +"eslint-utils@^3.0.0": + "integrity" "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==" + "resolved" "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" + "version" "3.0.0" + dependencies: + "eslint-visitor-keys" "^2.0.0" + +"eslint-visitor-keys@^2.0.0": + "integrity" "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" + "resolved" "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + "version" "2.1.0" -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +"eslint-visitor-keys@^3.3.0": + "integrity" "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" + "resolved" "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" + "version" "3.3.0" -eslint@8.12.0: - version "8.12.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz" - integrity sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q== +"eslint@>=5", "eslint@>=5.16.0", "eslint@8.12.0": + "integrity" "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==" + "resolved" "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz" + "version" "8.12.0" dependencies: "@eslint/eslintrc" "^1.2.1" "@humanwhocodes/config-array" "^0.9.2" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^9.3.1: - version "9.3.1" - resolved "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz" - integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== - dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.3.0" - -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flatted@^3.1.0: - version "3.2.4" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz" - integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@^7.1.3: - version "7.2.0" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^13.6.0, globals@^13.9.0: - version "13.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz" - integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== - dependencies: - type-fest "^0.20.2" - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-glob@^4.0.0, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -requireindex@~1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz" - integrity sha1-5UBLgVV+91225JxacgBIk/4D4WI= - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + "ajv" "^6.10.0" + "chalk" "^4.0.0" + "cross-spawn" "^7.0.2" + "debug" "^4.3.2" + "doctrine" "^3.0.0" + "escape-string-regexp" "^4.0.0" + "eslint-scope" "^7.1.1" + "eslint-utils" "^3.0.0" + "eslint-visitor-keys" "^3.3.0" + "espree" "^9.3.1" + "esquery" "^1.4.0" + "esutils" "^2.0.2" + "fast-deep-equal" "^3.1.3" + "file-entry-cache" "^6.0.1" + "functional-red-black-tree" "^1.0.1" + "glob-parent" "^6.0.1" + "globals" "^13.6.0" + "ignore" "^5.2.0" + "import-fresh" "^3.0.0" + "imurmurhash" "^0.1.4" + "is-glob" "^4.0.0" + "js-yaml" "^4.1.0" + "json-stable-stringify-without-jsonify" "^1.0.1" + "levn" "^0.4.1" + "lodash.merge" "^4.6.2" + "minimatch" "^3.0.4" + "natural-compare" "^1.4.0" + "optionator" "^0.9.1" + "regexpp" "^3.2.0" + "strip-ansi" "^6.0.1" + "strip-json-comments" "^3.1.0" + "text-table" "^0.2.0" + "v8-compile-cache" "^2.0.3" + +"espree@^9.3.1": + "integrity" "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==" + "resolved" "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz" + "version" "9.3.1" + dependencies: + "acorn" "^8.7.0" + "acorn-jsx" "^5.3.1" + "eslint-visitor-keys" "^3.3.0" + +"esquery@^1.4.0": + "integrity" "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==" + "resolved" "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" + "version" "1.4.0" + dependencies: + "estraverse" "^5.1.0" + +"esrecurse@^4.3.0": + "integrity" "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==" + "resolved" "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + "version" "4.3.0" + dependencies: + "estraverse" "^5.2.0" + +"estraverse@^5.1.0", "estraverse@^5.2.0": + "integrity" "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "resolved" "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + "version" "5.3.0" + +"esutils@^2.0.2": + "integrity" "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + "resolved" "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + "version" "2.0.3" + +"fast-deep-equal@^3.1.1", "fast-deep-equal@^3.1.3": + "integrity" "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "resolved" "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + "version" "3.1.3" + +"fast-json-stable-stringify@^2.0.0": + "integrity" "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "resolved" "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + "version" "2.1.0" + +"fast-levenshtein@^2.0.6": + "integrity" "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + "resolved" "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + "version" "2.0.6" + +"file-entry-cache@^6.0.1": + "integrity" "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==" + "resolved" "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + "version" "6.0.1" + dependencies: + "flat-cache" "^3.0.4" + +"flat-cache@^3.0.4": + "integrity" "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==" + "resolved" "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" + "version" "3.0.4" + dependencies: + "flatted" "^3.1.0" + "rimraf" "^3.0.2" + +"flatted@^3.1.0": + "integrity" "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==" + "resolved" "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz" + "version" "3.2.4" + +"fs.realpath@^1.0.0": + "integrity" "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + "version" "1.0.0" + +"functional-red-black-tree@^1.0.1": + "integrity" "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + "resolved" "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" + "version" "1.0.1" + +"glob-parent@^6.0.1": + "integrity" "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==" + "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + "version" "6.0.2" + dependencies: + "is-glob" "^4.0.3" + +"glob@^7.1.3": + "integrity" "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==" + "resolved" "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" + "version" "7.2.0" + dependencies: + "fs.realpath" "^1.0.0" + "inflight" "^1.0.4" + "inherits" "2" + "minimatch" "^3.0.4" + "once" "^1.3.0" + "path-is-absolute" "^1.0.0" + +"globals@^13.6.0", "globals@^13.9.0": + "integrity" "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==" + "resolved" "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz" + "version" "13.12.0" + dependencies: + "type-fest" "^0.20.2" + +"has-flag@^4.0.0": + "integrity" "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "resolved" "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + "version" "4.0.0" + +"ignore@^5.2.0": + "integrity" "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" + "resolved" "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" + "version" "5.2.0" + +"import-fresh@^3.0.0", "import-fresh@^3.2.1": + "integrity" "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==" + "resolved" "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + "version" "3.3.0" + dependencies: + "parent-module" "^1.0.0" + "resolve-from" "^4.0.0" + +"imurmurhash@^0.1.4": + "integrity" "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "resolved" "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + "version" "0.1.4" + +"inflight@^1.0.4": + "integrity" "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" + "resolved" "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + "version" "1.0.6" + dependencies: + "once" "^1.3.0" + "wrappy" "1" + +"inherits@2": + "integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + "version" "2.0.4" + +"is-extglob@^2.1.1": + "integrity" "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "resolved" "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + "version" "2.1.1" + +"is-glob@^4.0.0", "is-glob@^4.0.3": + "integrity" "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==" + "resolved" "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + "version" "4.0.3" + dependencies: + "is-extglob" "^2.1.1" + +"isexe@^2.0.0": + "integrity" "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "resolved" "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + "version" "2.0.0" + +"js-yaml@^4.1.0": + "integrity" "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==" + "resolved" "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + "version" "4.1.0" + dependencies: + "argparse" "^2.0.1" + +"json-schema-traverse@^0.4.1": + "integrity" "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + "version" "0.4.1" + +"json-stable-stringify-without-jsonify@^1.0.1": + "integrity" "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + "resolved" "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + "version" "1.0.1" + +"levn@^0.4.1": + "integrity" "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==" + "resolved" "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + "version" "0.4.1" + dependencies: + "prelude-ls" "^1.2.1" + "type-check" "~0.4.0" + +"lodash.merge@^4.6.2": + "integrity" "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + "version" "4.6.2" + +"minimatch@^3.0.4": + "integrity" "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==" + "resolved" "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + "version" "3.0.4" + dependencies: + "brace-expansion" "^1.1.7" + +"ms@2.1.2": + "integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + "version" "2.1.2" + +"natural-compare@^1.4.0": + "integrity" "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + "resolved" "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + "version" "1.4.0" + +"once@^1.3.0": + "integrity" "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" + "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + "version" "1.4.0" + dependencies: + "wrappy" "1" + +"optionator@^0.9.1": + "integrity" "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==" + "resolved" "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" + "version" "0.9.1" + dependencies: + "deep-is" "^0.1.3" + "fast-levenshtein" "^2.0.6" + "levn" "^0.4.1" + "prelude-ls" "^1.2.1" + "type-check" "^0.4.0" + "word-wrap" "^1.2.3" + +"parent-module@^1.0.0": + "integrity" "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==" + "resolved" "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + "version" "1.0.1" + dependencies: + "callsites" "^3.0.0" + +"path-is-absolute@^1.0.0": + "integrity" "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "resolved" "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + "version" "1.0.1" + +"path-key@^3.1.0": + "integrity" "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "resolved" "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + "version" "3.1.1" + +"prelude-ls@^1.2.1": + "integrity" "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + "resolved" "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + "version" "1.2.1" + +"punycode@^2.1.0": + "integrity" "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "resolved" "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + "version" "2.1.1" + +"regexpp@^3.2.0": + "integrity" "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" + "resolved" "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + "version" "3.2.0" + +"requireindex@~1.1.0": + "integrity" "sha1-5UBLgVV+91225JxacgBIk/4D4WI=" + "resolved" "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz" + "version" "1.1.0" + +"resolve-from@^4.0.0": + "integrity" "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "resolved" "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + "version" "4.0.0" + +"rimraf@^3.0.2": + "integrity" "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==" + "resolved" "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + "version" "3.0.2" + dependencies: + "glob" "^7.1.3" + +"shebang-command@^2.0.0": + "integrity" "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==" + "resolved" "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + "version" "2.0.0" + dependencies: + "shebang-regex" "^3.0.0" + +"shebang-regex@^3.0.0": + "integrity" "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "resolved" "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + "version" "3.0.0" + +"strip-ansi@^6.0.1": + "integrity" "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==" + "resolved" "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + "version" "6.0.1" + dependencies: + "ansi-regex" "^5.0.1" + +"strip-json-comments@^3.1.0", "strip-json-comments@^3.1.1": + "integrity" "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + "resolved" "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + "version" "3.1.1" + +"supports-color@^7.1.0": + "integrity" "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" + "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + "version" "7.2.0" + dependencies: + "has-flag" "^4.0.0" + +"text-table@^0.2.0": + "integrity" "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + "resolved" "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + "version" "0.2.0" + +"type-check@^0.4.0", "type-check@~0.4.0": + "integrity" "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==" + "resolved" "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + "version" "0.4.0" + dependencies: + "prelude-ls" "^1.2.1" + +"type-fest@^0.20.2": + "integrity" "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" + "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + "version" "0.20.2" + +"uri-js@^4.2.2": + "integrity" "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==" + "resolved" "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + "version" "4.4.1" + dependencies: + "punycode" "^2.1.0" + +"v8-compile-cache@^2.0.3": + "integrity" "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" + "resolved" "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" + "version" "2.3.0" + +"which@^2.0.1": + "integrity" "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==" + "resolved" "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + "version" "2.0.2" + dependencies: + "isexe" "^2.0.0" + +"word-wrap@^1.2.3": + "integrity" "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + "resolved" "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" + "version" "1.2.3" + +"wrappy@1": + "integrity" "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + "version" "1.0.2" From a44837cd77f8ed570c05e5b3efbae0dbcb3e6c16 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Mon, 25 Apr 2022 16:56:59 -0600 Subject: [PATCH 28/99] Remove unnecessary plugin --- .eslintrc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index d7e926559..81d458100 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,7 +20,6 @@ module.exports = { 'no-unused-vars': 'off' // functions aren't used. }, plugins: [ - 'async-await', 'googleappsscript' ] } From 428ed635b940feed886124fd9d26699f7809e2cd Mon Sep 17 00:00:00 2001 From: Kara <62033369+kar320@users.noreply.github.com> Date: Wed, 11 May 2022 13:22:52 -0600 Subject: [PATCH 29/99] Update remove duplicates from using vars to lets (#320) Updating this sample to be republished as a library quickstart. --- sheets/removingDuplicates/removingDuplicates.gs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sheets/removingDuplicates/removingDuplicates.gs b/sheets/removingDuplicates/removingDuplicates.gs index b403c8f64..4047564ee 100644 --- a/sheets/removingDuplicates/removingDuplicates.gs +++ b/sheets/removingDuplicates/removingDuplicates.gs @@ -20,16 +20,16 @@ */ function removeDuplicates() { // [START apps_script_sheets_sheet] - var sheet = SpreadsheetApp.getActiveSheet(); - var data = sheet.getDataRange().getValues(); + let sheet = SpreadsheetApp.getActiveSheet(); + let data = sheet.getDataRange().getValues(); // [END apps_script_sheets_sheet] // [START apps_script_sheets_new_data] - var newData = []; + let newData = []; // [END apps_script_sheets_new_data] - for (var i in data) { - var row = data[i]; - var duplicate = false; - for (var j in newData) { + for (let i in data) { + let row = data[i]; + let duplicate = false; + for (let j in newData) { if (row.join() == newData[j].join()) { duplicate = true; } From 26ce74195372b7b8f3a7e7706c51ac62c21e5995 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 14:11:34 -0600 Subject: [PATCH 30/99] Update actions/checkout action to v3.0.2 (#317) Co-authored-by: renovate[bot] --- .github/workflows/lint.yaml | 2 +- .github/workflows/publish.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 59841aebf..02bff403b 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -28,7 +28,7 @@ jobs: cancel-in-progress: true runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3.0.0 + - uses: actions/checkout@v3.0.2 with: fetch-depth: 0 - uses: github/super-linter/slim@v4.9.0 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index cfef946a3..8cd707f1d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -25,7 +25,7 @@ jobs: cancel-in-progress: false runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3.0.0 + - uses: actions/checkout@v3.0.2 with: fetch-depth: 0 - name: Get changed files From b0b099b89ea88ba4ee6883460136065f744d84d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 14:11:46 -0600 Subject: [PATCH 31/99] Update dependency eslint to v8.15.0 (#316) Co-authored-by: renovate[bot] --- package-lock.json | 90 ++-- package.json | 2 +- yarn.lock | 1116 ++++++++++++++++++++++----------------------- 3 files changed, 604 insertions(+), 604 deletions(-) diff --git a/package-lock.json b/package-lock.json index 165fab447..66316dd99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,25 +9,25 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "eslint": "8.12.0", + "eslint": "8.15.0", "eslint-config-google": "0.14.0", "eslint-plugin-googleappsscript": "1.0.4" } }, "node_modules/@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", + "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", + "espree": "^9.3.2", "globals": "^13.9.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { @@ -55,9 +55,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -248,12 +248,12 @@ } }, "node_modules/eslint": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz", - "integrity": "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", + "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.2.3", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -264,7 +264,7 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.3.2", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -280,7 +280,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -373,13 +373,13 @@ } }, "node_modules/espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", "dev": true, "dependencies": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -666,9 +666,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -943,19 +943,19 @@ }, "dependencies": { "@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", + "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", + "espree": "^9.3.2", "globals": "^13.9.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, @@ -977,9 +977,9 @@ "dev": true }, "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true }, "acorn-jsx": { @@ -1117,12 +1117,12 @@ "dev": true }, "eslint": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz", - "integrity": "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", + "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.2.3", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -1133,7 +1133,7 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.3.2", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -1149,7 +1149,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -1209,13 +1209,13 @@ "dev": true }, "espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", "dev": true, "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" } }, @@ -1439,9 +1439,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" diff --git a/package.json b/package.json index 42aedd29c..651291797 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "API" ], "devDependencies": { - "eslint": "8.12.0", + "eslint": "8.15.0", "eslint-config-google": "0.14.0", "eslint-plugin-googleappsscript": "1.0.4" }, diff --git a/yarn.lock b/yarn.lock index da2097706..ed09d4fdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,571 +2,571 @@ # yarn lockfile v1 -"@eslint/eslintrc@^1.2.1": - "integrity" "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==" - "resolved" "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz" - "version" "1.2.1" - dependencies: - "ajv" "^6.12.4" - "debug" "^4.3.2" - "espree" "^9.3.1" - "globals" "^13.9.0" - "ignore" "^5.2.0" - "import-fresh" "^3.2.1" - "js-yaml" "^4.1.0" - "minimatch" "^3.0.4" - "strip-json-comments" "^3.1.1" +"@eslint/eslintrc@^1.2.3": + version "1.2.3" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz" + integrity sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.2" + globals "^13.9.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" "@humanwhocodes/config-array@^0.9.2": - "integrity" "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==" - "resolved" "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz" - "version" "0.9.2" + version "0.9.2" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz" + integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA== dependencies: "@humanwhocodes/object-schema" "^1.2.1" - "debug" "^4.1.1" - "minimatch" "^3.0.4" + debug "^4.1.1" + minimatch "^3.0.4" "@humanwhocodes/object-schema@^1.2.1": - "integrity" "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" - "resolved" "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - "version" "1.2.1" - -"acorn-jsx@^5.3.1": - "integrity" "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" - "resolved" "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - "version" "5.3.2" - -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", "acorn@^8.7.0": - "integrity" "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==" - "resolved" "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz" - "version" "8.7.0" - -"ajv@^6.10.0", "ajv@^6.12.4": - "integrity" "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==" - "resolved" "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - "version" "6.12.6" - dependencies: - "fast-deep-equal" "^3.1.1" - "fast-json-stable-stringify" "^2.0.0" - "json-schema-traverse" "^0.4.1" - "uri-js" "^4.2.2" - -"ansi-regex@^5.0.1": - "integrity" "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - "resolved" "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - "version" "5.0.1" - -"ansi-styles@^4.1.0": - "integrity" "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==" - "resolved" "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - "version" "4.3.0" - dependencies: - "color-convert" "^2.0.1" - -"argparse@^2.0.1": - "integrity" "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - "resolved" "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - "version" "2.0.1" - -"balanced-match@^1.0.0": - "integrity" "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - "resolved" "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - "version" "1.0.2" - -"brace-expansion@^1.1.7": - "integrity" "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==" - "resolved" "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - "version" "1.1.11" - dependencies: - "balanced-match" "^1.0.0" - "concat-map" "0.0.1" - -"callsites@^3.0.0": - "integrity" "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - "resolved" "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - "version" "3.1.0" - -"chalk@^4.0.0": - "integrity" "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==" - "resolved" "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - "version" "4.1.2" - dependencies: - "ansi-styles" "^4.1.0" - "supports-color" "^7.1.0" - -"color-convert@^2.0.1": - "integrity" "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==" - "resolved" "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - "version" "2.0.1" - dependencies: - "color-name" "~1.1.4" - -"color-name@~1.1.4": - "integrity" "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - "resolved" "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - "version" "1.1.4" - -"concat-map@0.0.1": - "integrity" "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - "resolved" "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - "version" "0.0.1" - -"cross-spawn@^7.0.2": - "integrity" "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==" - "resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - "version" "7.0.3" - dependencies: - "path-key" "^3.1.0" - "shebang-command" "^2.0.0" - "which" "^2.0.1" - -"debug@^4.1.1", "debug@^4.3.2": - "integrity" "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==" - "resolved" "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" - "version" "4.3.3" - dependencies: - "ms" "2.1.2" - -"deep-is@^0.1.3": - "integrity" "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" - "resolved" "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" - "version" "0.1.4" - -"doctrine@^3.0.0": - "integrity" "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==" - "resolved" "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - "version" "3.0.0" - dependencies: - "esutils" "^2.0.2" - -"escape-string-regexp@^4.0.0": - "integrity" "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - "resolved" "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - "version" "4.0.0" - -"eslint-config-google@0.14.0": - "integrity" "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==" - "resolved" "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz" - "version" "0.14.0" - -"eslint-plugin-googleappsscript@1.0.4": - "integrity" "sha512-Z6w1EMw0z0VOUvI0PFquigNSkYTgB+pVb2O2KYedDHnllwrgjjNBOLxsqPmtwmjOtFqCu7TuL0hJQwxVnp5OzQ==" - "resolved" "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz" - "version" "1.0.4" - dependencies: - "requireindex" "~1.1.0" - -"eslint-scope@^7.1.1": - "integrity" "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==" - "resolved" "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - "version" "7.1.1" + version "1.2.1" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.7.1: + version "8.7.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.1, debug@^4.3.2: + version "4.3.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-google@0.14.0: + version "0.14.0" + resolved "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz" + integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== + +eslint-plugin-googleappsscript@1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz" + integrity sha512-Z6w1EMw0z0VOUvI0PFquigNSkYTgB+pVb2O2KYedDHnllwrgjjNBOLxsqPmtwmjOtFqCu7TuL0hJQwxVnp5OzQ== + dependencies: + requireindex "~1.1.0" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== dependencies: - "esrecurse" "^4.3.0" - "estraverse" "^5.2.0" - -"eslint-utils@^3.0.0": - "integrity" "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==" - "resolved" "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - "version" "3.0.0" - dependencies: - "eslint-visitor-keys" "^2.0.0" - -"eslint-visitor-keys@^2.0.0": - "integrity" "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==" - "resolved" "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - "version" "2.1.0" - -"eslint-visitor-keys@^3.3.0": - "integrity" "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" - "resolved" "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" - "version" "3.3.0" + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -"eslint@>=5", "eslint@>=5.16.0", "eslint@8.12.0": - "integrity" "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==" - "resolved" "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz" - "version" "8.12.0" +eslint@8.15.0: + version "8.15.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz" + integrity sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA== dependencies: - "@eslint/eslintrc" "^1.2.1" + "@eslint/eslintrc" "^1.2.3" "@humanwhocodes/config-array" "^0.9.2" - "ajv" "^6.10.0" - "chalk" "^4.0.0" - "cross-spawn" "^7.0.2" - "debug" "^4.3.2" - "doctrine" "^3.0.0" - "escape-string-regexp" "^4.0.0" - "eslint-scope" "^7.1.1" - "eslint-utils" "^3.0.0" - "eslint-visitor-keys" "^3.3.0" - "espree" "^9.3.1" - "esquery" "^1.4.0" - "esutils" "^2.0.2" - "fast-deep-equal" "^3.1.3" - "file-entry-cache" "^6.0.1" - "functional-red-black-tree" "^1.0.1" - "glob-parent" "^6.0.1" - "globals" "^13.6.0" - "ignore" "^5.2.0" - "import-fresh" "^3.0.0" - "imurmurhash" "^0.1.4" - "is-glob" "^4.0.0" - "js-yaml" "^4.1.0" - "json-stable-stringify-without-jsonify" "^1.0.1" - "levn" "^0.4.1" - "lodash.merge" "^4.6.2" - "minimatch" "^3.0.4" - "natural-compare" "^1.4.0" - "optionator" "^0.9.1" - "regexpp" "^3.2.0" - "strip-ansi" "^6.0.1" - "strip-json-comments" "^3.1.0" - "text-table" "^0.2.0" - "v8-compile-cache" "^2.0.3" - -"espree@^9.3.1": - "integrity" "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==" - "resolved" "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz" - "version" "9.3.1" - dependencies: - "acorn" "^8.7.0" - "acorn-jsx" "^5.3.1" - "eslint-visitor-keys" "^3.3.0" - -"esquery@^1.4.0": - "integrity" "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==" - "resolved" "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - "version" "1.4.0" - dependencies: - "estraverse" "^5.1.0" - -"esrecurse@^4.3.0": - "integrity" "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==" - "resolved" "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - "version" "4.3.0" - dependencies: - "estraverse" "^5.2.0" - -"estraverse@^5.1.0", "estraverse@^5.2.0": - "integrity" "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - "resolved" "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - "version" "5.3.0" - -"esutils@^2.0.2": - "integrity" "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - "resolved" "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - "version" "2.0.3" - -"fast-deep-equal@^3.1.1", "fast-deep-equal@^3.1.3": - "integrity" "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - "resolved" "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - "version" "3.1.3" - -"fast-json-stable-stringify@^2.0.0": - "integrity" "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - "resolved" "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" - "version" "2.1.0" - -"fast-levenshtein@^2.0.6": - "integrity" "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - "resolved" "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - "version" "2.0.6" - -"file-entry-cache@^6.0.1": - "integrity" "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==" - "resolved" "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" - "version" "6.0.1" - dependencies: - "flat-cache" "^3.0.4" - -"flat-cache@^3.0.4": - "integrity" "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==" - "resolved" "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - "version" "3.0.4" - dependencies: - "flatted" "^3.1.0" - "rimraf" "^3.0.2" - -"flatted@^3.1.0": - "integrity" "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==" - "resolved" "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz" - "version" "3.2.4" - -"fs.realpath@^1.0.0": - "integrity" "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - "version" "1.0.0" - -"functional-red-black-tree@^1.0.1": - "integrity" "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - "resolved" "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - "version" "1.0.1" - -"glob-parent@^6.0.1": - "integrity" "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==" - "resolved" "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - "version" "6.0.2" - dependencies: - "is-glob" "^4.0.3" - -"glob@^7.1.3": - "integrity" "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==" - "resolved" "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" - "version" "7.2.0" - dependencies: - "fs.realpath" "^1.0.0" - "inflight" "^1.0.4" - "inherits" "2" - "minimatch" "^3.0.4" - "once" "^1.3.0" - "path-is-absolute" "^1.0.0" - -"globals@^13.6.0", "globals@^13.9.0": - "integrity" "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==" - "resolved" "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz" - "version" "13.12.0" - dependencies: - "type-fest" "^0.20.2" - -"has-flag@^4.0.0": - "integrity" "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - "resolved" "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - "version" "4.0.0" - -"ignore@^5.2.0": - "integrity" "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" - "resolved" "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" - "version" "5.2.0" - -"import-fresh@^3.0.0", "import-fresh@^3.2.1": - "integrity" "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==" - "resolved" "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - "version" "3.3.0" - dependencies: - "parent-module" "^1.0.0" - "resolve-from" "^4.0.0" - -"imurmurhash@^0.1.4": - "integrity" "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - "resolved" "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - "version" "0.1.4" - -"inflight@^1.0.4": - "integrity" "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=" - "resolved" "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - "version" "1.0.6" - dependencies: - "once" "^1.3.0" - "wrappy" "1" - -"inherits@2": - "integrity" "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - "resolved" "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - "version" "2.0.4" - -"is-extglob@^2.1.1": - "integrity" "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - "resolved" "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - "version" "2.1.1" - -"is-glob@^4.0.0", "is-glob@^4.0.3": - "integrity" "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==" - "resolved" "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - "version" "4.0.3" - dependencies: - "is-extglob" "^2.1.1" - -"isexe@^2.0.0": - "integrity" "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - "resolved" "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - "version" "2.0.0" - -"js-yaml@^4.1.0": - "integrity" "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==" - "resolved" "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - "version" "4.1.0" - dependencies: - "argparse" "^2.0.1" - -"json-schema-traverse@^0.4.1": - "integrity" "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - "resolved" "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - "version" "0.4.1" - -"json-stable-stringify-without-jsonify@^1.0.1": - "integrity" "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" - "resolved" "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - "version" "1.0.1" - -"levn@^0.4.1": - "integrity" "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==" - "resolved" "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - "version" "0.4.1" - dependencies: - "prelude-ls" "^1.2.1" - "type-check" "~0.4.0" - -"lodash.merge@^4.6.2": - "integrity" "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - "resolved" "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - "version" "4.6.2" - -"minimatch@^3.0.4": - "integrity" "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==" - "resolved" "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" - "version" "3.0.4" - dependencies: - "brace-expansion" "^1.1.7" - -"ms@2.1.2": - "integrity" "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - "resolved" "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - "version" "2.1.2" - -"natural-compare@^1.4.0": - "integrity" "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" - "resolved" "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - "version" "1.4.0" - -"once@^1.3.0": - "integrity" "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=" - "resolved" "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - "version" "1.4.0" - dependencies: - "wrappy" "1" - -"optionator@^0.9.1": - "integrity" "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==" - "resolved" "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - "version" "0.9.1" - dependencies: - "deep-is" "^0.1.3" - "fast-levenshtein" "^2.0.6" - "levn" "^0.4.1" - "prelude-ls" "^1.2.1" - "type-check" "^0.4.0" - "word-wrap" "^1.2.3" - -"parent-module@^1.0.0": - "integrity" "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==" - "resolved" "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - "version" "1.0.1" - dependencies: - "callsites" "^3.0.0" - -"path-is-absolute@^1.0.0": - "integrity" "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - "resolved" "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - "version" "1.0.1" - -"path-key@^3.1.0": - "integrity" "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - "resolved" "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - "version" "3.1.1" - -"prelude-ls@^1.2.1": - "integrity" "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" - "resolved" "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - "version" "1.2.1" - -"punycode@^2.1.0": - "integrity" "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - "resolved" "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" - "version" "2.1.1" - -"regexpp@^3.2.0": - "integrity" "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==" - "resolved" "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" - "version" "3.2.0" - -"requireindex@~1.1.0": - "integrity" "sha1-5UBLgVV+91225JxacgBIk/4D4WI=" - "resolved" "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz" - "version" "1.1.0" - -"resolve-from@^4.0.0": - "integrity" "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - "resolved" "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - "version" "4.0.0" - -"rimraf@^3.0.2": - "integrity" "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==" - "resolved" "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - "version" "3.0.2" - dependencies: - "glob" "^7.1.3" - -"shebang-command@^2.0.0": - "integrity" "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==" - "resolved" "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - "version" "2.0.0" - dependencies: - "shebang-regex" "^3.0.0" - -"shebang-regex@^3.0.0": - "integrity" "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - "resolved" "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - "version" "3.0.0" - -"strip-ansi@^6.0.1": - "integrity" "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==" - "resolved" "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - "version" "6.0.1" - dependencies: - "ansi-regex" "^5.0.1" - -"strip-json-comments@^3.1.0", "strip-json-comments@^3.1.1": - "integrity" "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" - "resolved" "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - "version" "3.1.1" - -"supports-color@^7.1.0": - "integrity" "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==" - "resolved" "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - "version" "7.2.0" - dependencies: - "has-flag" "^4.0.0" - -"text-table@^0.2.0": - "integrity" "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - "resolved" "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - "version" "0.2.0" - -"type-check@^0.4.0", "type-check@~0.4.0": - "integrity" "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==" - "resolved" "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - "version" "0.4.0" - dependencies: - "prelude-ls" "^1.2.1" - -"type-fest@^0.20.2": - "integrity" "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - "resolved" "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" - "version" "0.20.2" - -"uri-js@^4.2.2": - "integrity" "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==" - "resolved" "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - "version" "4.4.1" - dependencies: - "punycode" "^2.1.0" - -"v8-compile-cache@^2.0.3": - "integrity" "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==" - "resolved" "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" - "version" "2.3.0" - -"which@^2.0.1": - "integrity" "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==" - "resolved" "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - "version" "2.0.2" - dependencies: - "isexe" "^2.0.0" - -"word-wrap@^1.2.3": - "integrity" "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" - "resolved" "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - "version" "1.2.3" - -"wrappy@1": - "integrity" "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - "resolved" "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - "version" "1.0.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.2" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.6.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^9.3.2: + version "9.3.2" + resolved "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz" + integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA== + dependencies: + acorn "^8.7.1" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.4" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz" + integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.0" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.6.0, globals@^13.9.0: + version "13.12.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz" + integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== + dependencies: + type-fest "^0.20.2" + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +requireindex@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz" + integrity sha1-5UBLgVV+91225JxacgBIk/4D4WI= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= From 87c20b63da615037cc7084b5455839d62ae55de3 Mon Sep 17 00:00:00 2001 From: Steve Bazyl Date: Mon, 16 May 2022 09:54:06 -0600 Subject: [PATCH 32/99] Fix typo where wrong rule was being used --- sheets/api/spreadsheet_snippets.gs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sheets/api/spreadsheet_snippets.gs b/sheets/api/spreadsheet_snippets.gs index 620bf9af9..05a49abb2 100644 --- a/sheets/api/spreadsheet_snippets.gs +++ b/sheets/api/spreadsheet_snippets.gs @@ -402,7 +402,7 @@ Snippets.prototype.conditionalFormatting = (spreadsheetId) => { let rule2Condition = Sheets.newBooleanCondition(); rule2Condition.type = 'CUSTOM_FORMULA'; - rule2Condition.values = [rule1ConditionalValue]; + rule2Condition.values = [rule2ConditionalValue]; let rule2BooleanRule = Sheets.newBooleanRule(); rule2BooleanRule.condition = rule2Condition; From 85176ad8fcf9e142dd2b1bc00aee16b39052febc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 16 May 2022 16:31:15 -0600 Subject: [PATCH 33/99] Update tj-actions/changed-files action to v19 (#321) Co-authored-by: renovate[bot] --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 8cd707f1d..df59997d7 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -30,7 +30,7 @@ jobs: fetch-depth: 0 - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v18.7 + uses: tj-actions/changed-files@v19.3 - name: Write test credentials run: | echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json" From de03166337c781cc2ec173fc0169e24938f9a44e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 May 2022 15:10:58 -0600 Subject: [PATCH 34/99] Update tj-actions/changed-files action to v20 (#323) Co-authored-by: renovate[bot] --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index df59997d7..edf03cb93 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -30,7 +30,7 @@ jobs: fetch-depth: 0 - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v19.3 + uses: tj-actions/changed-files@v20.2 - name: Write test credentials run: | echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json" From 9f973a82e0f5db721b1f6c9a10b4a6c33469fdd8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 May 2022 15:11:14 -0600 Subject: [PATCH 35/99] Update dependency eslint to v8.16.0 (#322) Co-authored-by: renovate[bot] --- package-lock.json | 50 +++++++++++++++++++++++------------------------ package.json | 2 +- yarn.lock | 30 ++++++++++++++-------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66316dd99..c73f2ce5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,21 +9,21 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "eslint": "8.15.0", + "eslint": "8.16.0", "eslint-config-google": "0.14.0", "eslint-plugin-googleappsscript": "1.0.4" } }, "node_modules/@eslint/eslintrc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", - "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.3.2", - "globals": "^13.9.0", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -248,12 +248,12 @@ } }, "node_modules/eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", - "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz", + "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.2.3", + "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -271,7 +271,7 @@ "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -522,9 +522,9 @@ } }, "node_modules/globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -943,15 +943,15 @@ }, "dependencies": { "@eslint/eslintrc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz", - "integrity": "sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.3.2", - "globals": "^13.9.0", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1117,12 +1117,12 @@ "dev": true }, "eslint": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz", - "integrity": "sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz", + "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.3", + "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -1140,7 +1140,7 @@ "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -1328,9 +1328,9 @@ } }, "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", "dev": true, "requires": { "type-fest": "^0.20.2" diff --git a/package.json b/package.json index 651291797..78dc282a8 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "API" ], "devDependencies": { - "eslint": "8.15.0", + "eslint": "8.16.0", "eslint-config-google": "0.14.0", "eslint-plugin-googleappsscript": "1.0.4" }, diff --git a/yarn.lock b/yarn.lock index ed09d4fdb..b336d15e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,15 +2,15 @@ # yarn lockfile v1 -"@eslint/eslintrc@^1.2.3": - version "1.2.3" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.3.tgz" - integrity sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA== +"@eslint/eslintrc@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz" + integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== dependencies: ajv "^6.12.4" debug "^4.3.2" espree "^9.3.2" - globals "^13.9.0" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" @@ -181,12 +181,12 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.15.0: - version "8.15.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.15.0.tgz" - integrity sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA== +eslint@8.16.0: + version "8.16.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz" + integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA== dependencies: - "@eslint/eslintrc" "^1.2.3" + "@eslint/eslintrc" "^1.3.0" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -204,7 +204,7 @@ eslint@8.15.0: file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" glob-parent "^6.0.1" - globals "^13.6.0" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" @@ -319,10 +319,10 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^13.6.0, globals@^13.9.0: - version "13.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz" - integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== +globals@^13.15.0: + version "13.15.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz" + integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== dependencies: type-fest "^0.20.2" From b4e117d8022875687cea8ca70b9158a62e4174f0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 May 2022 15:37:37 -0600 Subject: [PATCH 36/99] Update tj-actions/changed-files action to v22 (#325) Co-authored-by: renovate[bot] --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index edf03cb93..904cb7fbb 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -30,7 +30,7 @@ jobs: fetch-depth: 0 - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v20.2 + uses: tj-actions/changed-files@v22.1 - name: Write test credentials run: | echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json" From ebc6e58581ca42e48636930ee85bab0671b840cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 May 2022 15:37:47 -0600 Subject: [PATCH 37/99] Update github/super-linter action to v4.9.4 (#312) Co-authored-by: renovate[bot] --- .github/workflows/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 02bff403b..035862cb5 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v3.0.2 with: fetch-depth: 0 - - uses: github/super-linter/slim@v4.9.0 + - uses: github/super-linter/slim@v4.9.4 env: ERROR_ON_MISSING_EXEC_BIT: true VALIDATE_JSCPD: false From a6ff51b167f43a8ca53fcd63087ae6f8be149eb5 Mon Sep 17 00:00:00 2001 From: Steve Bazyl Date: Tue, 7 Jun 2022 15:23:44 -0600 Subject: [PATCH 38/99] Fix pagination for license assignments --- advanced/adminSDK.gs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/advanced/adminSDK.gs b/advanced/adminSDK.gs index d7d7a0e81..f5c784683 100644 --- a/advanced/adminSDK.gs +++ b/advanced/adminSDK.gs @@ -245,19 +245,20 @@ function updateGroupSettings() { function getLicenseAssignments() { const productId = 'Google-Apps'; const customerId = 'example.com'; - let assignments; - let pageToken; + let assignments = []; + let pageToken = null; do { - assignments = AdminLicenseManager.LicenseAssignments - .listForProduct(productId, customerId, { - maxResults: 500, - pageToken: pageToken - }); + const response = AdminLicenseManager.LicenseAssignments.listForProduct(productId, customerId, { + maxResults: 500, + pageToken: pageToken + }); + assignments = assignments.concat(response.items); + pageToken = response.nextPageToken } while (pageToken); // Print the productId and skuId - for (const assignment of assignments.items) { - Logger.log('userId: %s, productId: %s, skuId: %s', - assignment.userId, assignment.productId, assignment.skuId); + for (const assignment of assignments) { + Logger.log('userId: %s, productId: %s, skuId: %s', + assignment.userId, assignment.productId, assignment.skuId); } } // [END apps_script_admin_sdk_get_license_assignments] From 25deba35dcee22cf5931648f31a3718783e9c783 Mon Sep 17 00:00:00 2001 From: Paul <22284856+ProgramComputer@users.noreply.github.com> Date: Tue, 7 Jun 2022 17:17:17 -0500 Subject: [PATCH 39/99] Update spreadsheet_snippets.gs (#328) * Update spreadsheet_snippets.gs _values has to be [[x],[y],[z],...] not [x,y,z...] valueInputOption is inputted as a string not an enum * Remove duplicate region --- sheets/api/spreadsheet_snippets.gs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sheets/api/spreadsheet_snippets.gs b/sheets/api/spreadsheet_snippets.gs index 05a49abb2..bc7356b90 100644 --- a/sheets/api/spreadsheet_snippets.gs +++ b/sheets/api/spreadsheet_snippets.gs @@ -147,10 +147,10 @@ Snippets.prototype.batchGetValues = (spreadsheetId, * Updates the values in the specified range * @param {string} spreadsheetId spreadsheet's ID * @param {string} range the range of cells in spreadsheet - * @param {} valueInputOption determines how the input should be interpreted + * @param {string} valueInputOption determines how the input should be interpreted * @see * https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption - * @param {list} _values list of values to input + * @param {list>} _values list of string lists to input * @returns {*} spreadsheet with updated values */ Snippets.prototype.updateValues = (spreadsheetId, range, @@ -186,10 +186,10 @@ Snippets.prototype.updateValues = (spreadsheetId, range, * Updates the values in the specified range * @param {string} spreadsheetId spreadsheet's ID * @param {string} range range of cells of the spreadsheet - * @param valueInputOption determines how the input should be interpreted + * @param {string} valueInputOption determines how the input should be interpreted * @see * https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption - * @param {list} _values list of values to input + * @param {list>} _values list of string values to input * @returns {*} spreadsheet with updated values */ Snippets.prototype.batchUpdateValues = @@ -240,7 +240,6 @@ Snippets.prototype.batchUpdateValues = */ Snippets.prototype.appendValues = (spreadsheetId, range, valueInputOption, _values) => { - // [START sheets_append_values] let values = [ [ // Cell values ... From 4490b84cb6bbd78e9707df9f6681d3d408ad38f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 16:17:28 -0600 Subject: [PATCH 40/99] Update dependency eslint to v8.17.0 (#327) Co-authored-by: renovate[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index c73f2ce5a..977f2eb9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "eslint": "8.16.0", + "eslint": "8.17.0", "eslint-config-google": "0.14.0", "eslint-plugin-googleappsscript": "1.0.4" } @@ -248,9 +248,9 @@ } }, "node_modules/eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz", - "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", @@ -1117,9 +1117,9 @@ "dev": true }, "eslint": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz", - "integrity": "sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.0", diff --git a/package.json b/package.json index 78dc282a8..2675f1381 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "API" ], "devDependencies": { - "eslint": "8.16.0", + "eslint": "8.17.0", "eslint-config-google": "0.14.0", "eslint-plugin-googleappsscript": "1.0.4" }, diff --git a/yarn.lock b/yarn.lock index b336d15e6..0902f2740 100644 --- a/yarn.lock +++ b/yarn.lock @@ -181,10 +181,10 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.16.0: - version "8.16.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.16.0.tgz" - integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA== +eslint@8.17.0: + version "8.17.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz" + integrity sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw== dependencies: "@eslint/eslintrc" "^1.3.0" "@humanwhocodes/config-array" "^0.9.2" From 2b00f96dc16716f21b5581b1cb229ab82f015ea5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 16:17:35 -0600 Subject: [PATCH 41/99] Update tj-actions/changed-files action to v22.2 (#326) Co-authored-by: renovate[bot] --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 904cb7fbb..4070b9a60 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -30,7 +30,7 @@ jobs: fetch-depth: 0 - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v22.1 + uses: tj-actions/changed-files@v22.2 - name: Write test credentials run: | echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json" From 57ae4d93d3929063b16e8cac722b9a27c1a74403 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Tue, 7 Jun 2022 16:19:09 -0600 Subject: [PATCH 42/99] Delint previous change --- advanced/adminSDK.gs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/advanced/adminSDK.gs b/advanced/adminSDK.gs index f5c784683..3e63aa4a7 100644 --- a/advanced/adminSDK.gs +++ b/advanced/adminSDK.gs @@ -249,16 +249,16 @@ function getLicenseAssignments() { let pageToken = null; do { const response = AdminLicenseManager.LicenseAssignments.listForProduct(productId, customerId, { - maxResults: 500, - pageToken: pageToken + maxResults: 500, + pageToken: pageToken }); assignments = assignments.concat(response.items); - pageToken = response.nextPageToken + pageToken = response.nextPageToken; } while (pageToken); // Print the productId and skuId for (const assignment of assignments) { - Logger.log('userId: %s, productId: %s, skuId: %s', - assignment.userId, assignment.productId, assignment.skuId); + Logger.log('userId: %s, productId: %s, skuId: %s', + assignment.userId, assignment.productId, assignment.skuId); } } // [END apps_script_admin_sdk_get_license_assignments] From e96d69ea8fcc3dd2fd1ae550ff6bfe77af7b322d Mon Sep 17 00:00:00 2001 From: Rajesh Mudaliyar Date: Fri, 17 Jun 2022 09:29:46 -0700 Subject: [PATCH 43/99] Advanced and classroom tests (#329) * Created unit tests for advanced samples adminSDK.gs and created unit tests for classroom snippet samples. * Update test_classroom_snippets.gs added doc for RUN_ALL_TESTS Co-authored-by: soheilv Co-authored-by: soheilv <97913459+soheilv@users.noreply.github.com> --- advanced/test_adminSDK.gs | 147 ++++++++++++++++++ classroom/snippets/createCourse.gs | 2 + classroom/snippets/test_classroom_snippets.gs | 90 +++++++++++ 3 files changed, 239 insertions(+) create mode 100644 advanced/test_adminSDK.gs create mode 100644 classroom/snippets/test_classroom_snippets.gs diff --git a/advanced/test_adminSDK.gs b/advanced/test_adminSDK.gs new file mode 100644 index 000000000..896726030 --- /dev/null +++ b/advanced/test_adminSDK.gs @@ -0,0 +1,147 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests listAllUsers function of adminSDK.gs + */ +function itShouldListAllUsers() { + Logger.log('> itShouldListAllUsers'); + listAllUsers(); +} + +/** + * Tests getUser function of adminSDK.gs + */ +function itShouldGetUser() { + Logger.log('> itShouldGetUser'); + getUser(); +} + +/** + * Tests addUser function of adminSDK.gs + */ +function itShouldAddUser() { + Logger.log('> itShouldAddUser'); + addUser(); +} + +/** + * Tests createAlias function of adminSDK.gs + */ +function itShouldCreateAlias() { + Logger.log('> itShouldCreateAlias'); + createAlias(); +} + +/** + * Tests listAllGroups function of adminSDK.gs + */ +function itShouldListAllGroups() { + Logger.log('> itShouldListAllGroups'); + listAllGroups(); +} + +/** + * Tests addGroupMember function of adminSDK.gs + */ +function itShouldAddGroupMember() { + Logger.log('> itShouldAddGroupMember'); + addGroupMember(); +} + +/** + * Tests migrateMessages function of adminSDK.gs + */ +function itShouldMigrateMessages() { + Logger.log('> itShouldMigrateMessages'); + migrateMessages(); +} + +/** + * Tests getGroupSettings function of adminSDK.gs + */ +function itShouldGetGroupSettings() { + Logger.log('> itShouldGetGroupSettings'); + getGroupSettings(); +} + +/** + * Tests updateGroupSettings function of adminSDK.gs + */ +function itShouldUpdateGroupSettings() { + Logger.log('> itShouldUpdateGroupSettings'); + updateGroupSettings(); +} + +/** + * Tests getLicenseAssignments function of adminSDK.gs + */ +function itShouldGetLicenseAssignments() { + Logger.log('> itShouldGetLicenseAssignments'); + getLicenseAssignments(); +} + +/** + * Tests insertLicenseAssignment function of adminSDK.gs + */ +function itShouldInsertLicenseAssignment() { + Logger.log('> itShouldInsertLicenseAssignment'); + insertLicenseAssignment(); +} + +/** + * Tests generateLoginActivityReport function of adminSDK.gs + */ +function itShouldGenerateLoginActivityReport() { + Logger.log('> itShouldGenerateLoginActivityReport'); + generateLoginActivityReport(); +} + +/** + * Tests generateUserUsageReport function of adminSDK.gs + */ +function itShouldGenerateUserUsageReport() { + Logger.log('> itShouldGenerateUserUsageReport'); + generateUserUsageReport(); +} + +/** + * Tests getSubscriptions function of adminSDK.gs + */ +function itShouldGetSubscriptions() { + Logger.log('> itShouldGetSubscriptions'); + getSubscriptions(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldListAllUsers(); + itShouldGetUser(); + itShouldAddUser(); + itShouldCreateAlias(); + itShouldListAllGroups(); + itShouldAddGroupMember(); + itShouldMigrateMessages(); + itShouldGetGroupSettings(); + itShouldUpdateGroupSettings(); + itShouldGetLicenseAssignments(); + itShouldInsertLicenseAssignment(); + itShouldGenerateLoginActivityReport(); + itShouldGenerateUserUsageReport(); + itShouldGetSubscriptions(); +} diff --git a/classroom/snippets/createCourse.gs b/classroom/snippets/createCourse.gs index 174ecef8e..c8ce90249 100644 --- a/classroom/snippets/createCourse.gs +++ b/classroom/snippets/createCourse.gs @@ -17,6 +17,7 @@ /** * Creates 10th Grade Biology Course. * @see https://developers.google.com/classroom/reference/rest/v1/courses/create + * return {string} Id of created course */ function createCourse() { let course = { @@ -33,6 +34,7 @@ function createCourse() { // Create the course using course details. course = Classroom.Courses.create(course); Logger.log('Course created: %s (%s)', course.name, course.id); + return course.id; } catch (err) { // TODO (developer) - Handle Courses.create() exception Logger.log('Failed to create course %s with an error %s', course.name, err.message); diff --git a/classroom/snippets/test_classroom_snippets.gs b/classroom/snippets/test_classroom_snippets.gs new file mode 100644 index 000000000..e75575190 --- /dev/null +++ b/classroom/snippets/test_classroom_snippets.gs @@ -0,0 +1,90 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests createCourse function of createCourse.gs + * @return {string} courseId course id of created course + */ +function itShouldCreateCourse() { + Logger.log('> itShouldCreateCourse'); + const courseId = createCourse(); + return courseId; +} + +/** + * Tests getCourse function of getCourse.gs + * @param {string} courseId course id + */ +function itShouldGetCourse(courseId) { + Logger.log('> itShouldGetCourse'); + getCourse(courseId); +} + +/** + * Tests createAlias function of createAlias.gs + */ +function itShouldCreateAlias() { + Logger.log('> itShouldCreateAlias'); + createAlias(); +} + +/** + * Tests addAlias function of addAlias.gs + * @param {string} courseId course id + */ +function itShouldAddAlias(courseId) { + Logger.log('> itShouldAddAlias'); + addAlias(courseId); +} + +/** + * Tests courseUpdate function of courseUpdate.gs + * @param {string} courseId course id + */ +function itShouldUpdateCourse(courseId) { + Logger.log('> itShouldUpdateCourse'); + courseUpdate(courseId); +} + +/** + * Tests coursePatch function of patchCourse.gs + * @param {string} courseId course id + */ +function itShouldPatchCourse(courseId) { + Logger.log('> itShouldPatchCourse'); + coursePatch(courseId); +} + +/** + * Tests listCourses function of listCourses.gs + */ +function itShouldListCourses() { + Logger.log('> itShouldListCourses'); + listCourses(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + const courseId = itShouldCreateCourse(); + itShouldGetCourse(courseId); + itShouldCreateAlias(); + itShouldAddAlias(courseId); + itShouldUpdateCourse(courseId); + itShouldPatchCourse(courseId); + itShouldListCourses(); +} From b91395957e2c88e3521218392fbcd2da19d5c6ac Mon Sep 17 00:00:00 2001 From: Rajesh Mudaliyar Date: Fri, 17 Jun 2022 09:30:05 -0700 Subject: [PATCH 44/99] Advanced Samples Tests (#330) * Created unit tests for advanced samples tagManager.gs and bigquery.gs * Update test_bigquery.gs * Fixes lint issues in test_tagManager.gs Co-authored-by: soheilv Co-authored-by: soheilv <97913459+soheilv@users.noreply.github.com> --- advanced/test_bigquery.gs | 39 ++++++++++++++++++++++ advanced/test_tagManager.gs | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 advanced/test_bigquery.gs create mode 100644 advanced/test_tagManager.gs diff --git a/advanced/test_bigquery.gs b/advanced/test_bigquery.gs new file mode 100644 index 000000000..48f721dd2 --- /dev/null +++ b/advanced/test_bigquery.gs @@ -0,0 +1,39 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests runQuery function of adminSDK.gs + */ +function itShouldRunQuery() { + Logger.log('> itShouldRunQuery'); + runQuery(); +} + +/** + * Tests loadCsv function of adminSDK.gs + */ +function itShouldLoadCsv() { + Logger.log('> itShouldLoadCsv'); + loadCsv(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldRunQuery(); + itShouldLoadCsv(); +} diff --git a/advanced/test_tagManager.gs b/advanced/test_tagManager.gs new file mode 100644 index 000000000..1cb502d61 --- /dev/null +++ b/advanced/test_tagManager.gs @@ -0,0 +1,65 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Before running tagManager tests create a test tagMAnager account +// and replace the value below with its account path +const path = 'accounts/6007387289'; + +/** + * Tests createContainerVersion function of tagManager.gs + * @param {string} accountPath Tag manager account's path + * @return {object} version The container version + */ +function itShouldCreateContainerVersion(accountPath) { + Logger.log('> itShouldCreateContainerVersion'); + const version = createContainerVersion(accountPath); + return version; +} + +/** + * Tests publishVersionAndQuickPreviewDraft function of tagManager.gs + * @param {object} version tag managers container version + */ +function itShouldPublishVersionAndQuickPreviewDraft(version) { + Logger.log('> itShouldPublishVersionAndQuickPreviewDraft'); + publishVersionAndQuickPreviewDraft(version); +} + +/** + * Tests createAndReauthorizeUserEnvironment function of tagManager.gs + * @param {object} version tag managers container version + */ +function itShouldCreateAndReauthorizeUserEnvironment(version) { + Logger.log('> itShouldCreateAndReauthorizeUserEnvironment'); + createAndReauthorizeUserEnvironment(version); +} + +/** + * Tests logAllAccountUserPermissionsWithContainerAccess function of tagManager.gs + * @param {string} accountPath Tag manager account's path + */ +function itShouldLogAllAccountUserPermissionsWithContainerAccess(accountPath) { + Logger.log('> itShouldLogAllAccountUserPermissionsWithContainerAccess'); + logAllAccountUserPermissionsWithContainerAccess(accountPath); +} +/** + * Runs all tests + */ +function RUN_ALL_TESTS() { + const version = itShouldCreateContainerVersion(path); + itShouldPublishVersionAndQuickPreviewDraft(version); + itShouldCreateAndReauthorizeUserEnvironment(version); + itShouldLogAllAccountUserPermissionsWithContainerAccess(path); +} From 757941e7f0d6d4221043e6b722f90e9f17752eb3 Mon Sep 17 00:00:00 2001 From: Rajesh Mudaliyar Date: Fri, 17 Jun 2022 09:31:54 -0700 Subject: [PATCH 45/99] Unit test 4 (#332) * Creates unit test for advanced samples: adsense.gs, analytics.gs, doubleclick.gs, shoppingContent.gs, youtubeAnalytics.gs, youtubeContentId.gs * Update test_adsense.gs Co-authored-by: soheilv Co-authored-by: Steve Bazyl --- advanced/test_adsense.gs | 52 ++++++++++++++++++++++++++ advanced/test_analytics.gs | 42 +++++++++++++++++++++ advanced/test_doubleclick.gs | 48 ++++++++++++++++++++++++ advanced/test_shoppingContent.gs | 62 +++++++++++++++++++++++++++++++ advanced/test_youtubeAnalytics.gs | 30 +++++++++++++++ advanced/test_youtubeContentId.gs | 48 ++++++++++++++++++++++++ 6 files changed, 282 insertions(+) create mode 100644 advanced/test_adsense.gs create mode 100644 advanced/test_analytics.gs create mode 100644 advanced/test_doubleclick.gs create mode 100644 advanced/test_shoppingContent.gs create mode 100644 advanced/test_youtubeAnalytics.gs create mode 100644 advanced/test_youtubeContentId.gs diff --git a/advanced/test_adsense.gs b/advanced/test_adsense.gs new file mode 100644 index 000000000..53824d97d --- /dev/null +++ b/advanced/test_adsense.gs @@ -0,0 +1,52 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Replace with correct values +const accountName = 'account name'; +const clientName = 'ad client name'; + +/** + * Tests listAccounts function of adsense.gs + */ +function itShouldListAccounts() { + Logger.log('> itShouldListAccounts'); + listAccounts(); +} + +/** + * Tests listAdClients function of adsense.gs + */ +function itShouldListAdClients() { + Logger.log('> itShouldListAdClients'); + listAdClients(accountName); +} + +/** + * Tests listAdUnits function of adsense.gs + */ +function itShouldListAdUnits() { + Logger.log('> itShouldListAdUnits'); + listAdUnits(clientName); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldListAccounts(); + itShouldListAdClients(); + itShouldListAdUnits(); +} diff --git a/advanced/test_analytics.gs b/advanced/test_analytics.gs new file mode 100644 index 000000000..19ac09a8c --- /dev/null +++ b/advanced/test_analytics.gs @@ -0,0 +1,42 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Replace with the required profileId +const profileId = 'abcd'; + +/** + * Tests listAccounts function of analytics.gs + */ +function itShouldListAccounts() { + Logger.log('> itShouldListAccounts'); + listAccounts(); +} + +/** + * Tests runReport function of analytics.gs + */ +function itShouldRunReport() { + Logger.log('> itShouldRunReport'); + runReport(profileId); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldListAccounts(); + itShouldRunReport(); +} diff --git a/advanced/test_doubleclick.gs b/advanced/test_doubleclick.gs new file mode 100644 index 000000000..84887e04a --- /dev/null +++ b/advanced/test_doubleclick.gs @@ -0,0 +1,48 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests listUserProfiles function of doubleclick.gs + */ +function itShouldListUserProfiles() { + Logger.log('> itShouldListUserProfiles'); + listUserProfiles(); +} + +/** + * Tests listActiveCampaigns function of doubleclick.gs + */ +function itShouldListActiveCampaigns() { + Logger.log('> itShouldListActiveCampaigns'); + listActiveCampaigns(); +} + +/** + * Tests createAdvertiserAndCampaign function of doubleclick.gs + */ +function itShouldCreateAdvertiserAndCampaign() { + Logger.log('> itShouldCreateAdvertiserAndCampaign'); + createAdvertiserAndCampaign(); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldListUserProfiles(); + itShouldListActiveCampaigns(); + itShouldCreateAdvertiserAndCampaign(); +} diff --git a/advanced/test_shoppingContent.gs b/advanced/test_shoppingContent.gs new file mode 100644 index 000000000..ce6b6c2e1 --- /dev/null +++ b/advanced/test_shoppingContent.gs @@ -0,0 +1,62 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Before running these tests replace the product resource variables +const productResource1 = {}; +const productResource2 = {}; +const productResource3 = {}; + +/** + * Tests productInsert function of shoppingContent.gs + */ +function itShouldProductInsert() { + Logger.log('> itShouldPproductInsert'); + productInsert(); +} + +/** + * Tests productList function of shoppingContent.gs + */ +function itShouldProductList() { + Logger.log('> itShouldProductList'); + productList(); +} + +/** + * Tests custombatch function of shoppingContent.gs + */ +function itShouldCustombatch() { + Logger.log('> itShouldCustombatch'); + custombatch(productResource1, productResource2, productResource3); +} + +/** + * Tests updateAccountTax function of shoppingContent.gs + */ +function itShouldUpdateAccountTax() { + Logger.log('> itShouldUpdateAccountTax'); + updateAccountTax(); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldProductInsert(); + itShouldProductList(); + itShouldCustombatch(); + itShouldUpdateAccountTax(); +} diff --git a/advanced/test_youtubeAnalytics.gs b/advanced/test_youtubeAnalytics.gs new file mode 100644 index 000000000..17af2dfd3 --- /dev/null +++ b/advanced/test_youtubeAnalytics.gs @@ -0,0 +1,30 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests createReport function of youtubeAnalytics.gs + */ +function itShouldCreateReport() { + Logger.log('> itShouldCreateReport'); + createReport(); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldCreateReport(); +} diff --git a/advanced/test_youtubeContentId.gs b/advanced/test_youtubeContentId.gs new file mode 100644 index 000000000..728121af5 --- /dev/null +++ b/advanced/test_youtubeContentId.gs @@ -0,0 +1,48 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests claimYourVideoWithMonetizePolicy function of youtubeContentId.gs + */ +function itShouldClaimVideoWithMonetizePolicy() { + Logger.log('> itShouldClaimVideoWithMonetizePolicy'); + claimYourVideoWithMonetizePolicy(); +} + +/** + * Tests updateAssetOwnership function of youtubeContentId.gs + */ +function itShouldUpdateAssetOwnership() { + Logger.log('> itShouldUpdateAssetOwnership'); + updateAssetOwnership(); +} + +/** + * Tests releaseClaim function of youtubeContentId.gs + */ +function itShouldReleaseClaim() { + Logger.log('> itShouldReleaseClaim'); + releaseClaim(); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldClaimVideoWithMonetizePolicy(); + itShouldUpdateAssetOwnership(); + itShouldReleaseClaim(); +} From 7446dc614a4a4e2bc86ad95b91821105cd109fd4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Jun 2022 05:16:22 +0000 Subject: [PATCH 46/99] Update tj-actions/changed-files action to v23 --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 4070b9a60..8d6fba836 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -30,7 +30,7 @@ jobs: fetch-depth: 0 - name: Get changed files id: changed-files - uses: tj-actions/changed-files@v22.2 + uses: tj-actions/changed-files@v23.1 - name: Write test credentials run: | echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json" From d640e17aceaf1ef4b6c598823159671006c02c5a Mon Sep 17 00:00:00 2001 From: Rajesh Mudaliyar Date: Wed, 6 Jul 2022 13:05:28 -0700 Subject: [PATCH 47/99] Created unit tests for jdbc.gs and advanced samples classroom.gs. (#331) Co-authored-by: soheilv --- advanced/test_classroom.gs | 30 +++++++++++++++ service/test_jdbc.gs | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 advanced/test_classroom.gs create mode 100644 service/test_jdbc.gs diff --git a/advanced/test_classroom.gs b/advanced/test_classroom.gs new file mode 100644 index 000000000..b0314c4c8 --- /dev/null +++ b/advanced/test_classroom.gs @@ -0,0 +1,30 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests listCourses function of classroom.gs + */ +function itShouldListCourses() { + Logger.log('> itShouldListCourses'); + listCourses(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldListCourses(); +} diff --git a/service/test_jdbc.gs b/service/test_jdbc.gs new file mode 100644 index 000000000..23b3c8e9a --- /dev/null +++ b/service/test_jdbc.gs @@ -0,0 +1,76 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * Tests createDatabase function of jdbc.gs + */ +function itShouldCreateDatabase() { + Logger.log('itShouldCreateDatabase'); + createDatabase(); +} + +/** + * Tests createUser function of jdbc.gs + */ +function itShouldCreateUser() { + Logger.log('itShouldCreateUser'); + createUser(); +} + +/** + * Tests createTable function of jdbc.gs + */ +function itShouldCreateTable() { + Logger.log('itShouldCreateTable'); + createTable(); +} + +/** + * Tests writeOneRecord function of jdbc.gs + */ +function itShouldWriteOneRecord() { + Logger.log('itShouldWriteOneRecord'); + writeOneRecord(); +} + +/** + * Tests writeManyRecords function of jdbc.gs + */ +function itShouldWriteManyRecords() { + Logger.log('itShouldWriteManyRecords'); + writeManyRecords(); +} + +/** + * Tests readFromTable function of jdbc.gs + */ +function itShouldReadFromTable() { + Logger.log('itShouldReadFromTable'); + readFromTable(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldCreateDatabase(); + itShouldCreateUser(); + itShouldCreateTable(); + itShouldWriteOneRecord(); + itShouldWriteManyRecords(); + itShouldReadFromTable(); +} From 934e8b5d1abcf1876d098d8abf9c497ec821236f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Jul 2022 14:05:57 -0600 Subject: [PATCH 48/99] Update dependency eslint to v8.18.0 (#333) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 977f2eb9f..7f3391426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "eslint": "8.17.0", + "eslint": "8.18.0", "eslint-config-google": "0.14.0", "eslint-plugin-googleappsscript": "1.0.4" } @@ -248,9 +248,9 @@ } }, "node_modules/eslint": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", - "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz", + "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", @@ -1117,9 +1117,9 @@ "dev": true }, "eslint": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", - "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz", + "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.0", diff --git a/package.json b/package.json index 2675f1381..d1b961580 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "API" ], "devDependencies": { - "eslint": "8.17.0", + "eslint": "8.18.0", "eslint-config-google": "0.14.0", "eslint-plugin-googleappsscript": "1.0.4" }, diff --git a/yarn.lock b/yarn.lock index 0902f2740..9bfb90d40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -181,10 +181,10 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.17.0: - version "8.17.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz" - integrity sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw== +eslint@8.18.0: + version "8.18.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz" + integrity sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA== dependencies: "@eslint/eslintrc" "^1.3.0" "@humanwhocodes/config-array" "^0.9.2" From a23d0c1a7c83e59048145cc2cf8575d097b60492 Mon Sep 17 00:00:00 2001 From: Caleb Date: Wed, 20 Jul 2022 10:52:54 -0500 Subject: [PATCH 49/99] docs: Add (err) to catch in sample code. (#338) --- service/jdbc.gs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/jdbc.gs b/service/jdbc.gs index 0921a1609..33015c0bb 100644 --- a/service/jdbc.gs +++ b/service/jdbc.gs @@ -120,7 +120,7 @@ function writeManyRecords() { const end = new Date(); Logger.log('Time elapsed: %sms for %s rows.', end - start, batch.length); - } catch { + } catch (err) { // TODO(developer) - Handle exception from the API Logger.log('Failed with an error %s', err.message); } @@ -153,7 +153,7 @@ function readFromTable() { const end = new Date(); Logger.log('Time elapsed: %sms', end - start); - } catch { + } catch (err) { // TODO(developer) - Handle exception from the API Logger.log('Failed with an error %s', err.message); } From f4497f5a76d0f0437ed45f14f7d363418e7a011e Mon Sep 17 00:00:00 2001 From: googleworkspace-bot <109114539+googleworkspace-bot@users.noreply.github.com> Date: Thu, 21 Jul 2022 11:12:51 -0600 Subject: [PATCH 50/99] chore: Synced file(s) with googleworkspace/.github (#339) * chore: Created local '.github/workflows/lint.yml' from remote 'sync-files/defaults/.github/workflows/lint.yml' * chore: Created local '.github/workflows/test.yml' from remote 'sync-files/defaults/.github/workflows/test.yml' * chore: Created local '.github/CODEOWNERS' from remote 'sync-files/defaults/.github/CODEOWNERS' * chore: Created local '.github/sync-repo-settings.yaml' from remote 'sync-files/defaults/.github/sync-repo-settings.yaml' * chore: Created local '.github/workflows/automation.yml' from remote 'sync-files/defaults/.github/workflows/automation.yml' * chore: Created local 'SECURITY.md' from remote 'SECURITY.md' --- .github/CODEOWNERS | 17 ++++++++++ .github/sync-repo-settings.yaml | 54 ++++++++++++++++++++++++++++++++ .github/workflows/automation.yml | 28 +++++++++++++++++ .github/workflows/lint.yml | 24 ++++++++++++++ .github/workflows/test.yml | 24 ++++++++++++++ SECURITY.md | 6 ++++ 6 files changed, 153 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/sync-repo-settings.yaml create mode 100644 .github/workflows/automation.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml create mode 100644 SECURITY.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..804a0939c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,17 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +.github/ @googleworkspace/workspace-devrel-dpe diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 000000000..757d7bb5e --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,54 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# .github/sync-repo-settings.yaml +# See https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings for app options. +rebaseMergeAllowed: true +squashMergeAllowed: true +mergeCommitAllowed: false +deleteBranchOnMerge: true +branchProtectionRules: + - pattern: main + isAdminEnforced: false + requiresStrictStatusChecks: false + requiredStatusCheckContexts: + # .github/workflows/test.yml with a job called "test" + - "test" + # .github/workflows/lint.yml with a job called "lint" + - "lint" + # Google bots below + - "cla/google" + - "snippet-bot check" + - "header-check" + - "conventionalcommits.org" + requiredApprovingReviewCount: 1 + requiresCodeOwnerReviews: true + - pattern: master + isAdminEnforced: false + requiresStrictStatusChecks: false + requiredStatusCheckContexts: + # .github/workflows/test.yml with a job called "test" + - "test" + # .github/workflows/lint.yml with a job called "lint" + - "lint" + # Google bots below + - "cla/google" + - "snippet-bot check" + - "header-check" + - "conventionalcommits.org" + requiredApprovingReviewCount: 1 + requiresCodeOwnerReviews: true +permissionRules: + - team: workspace-devrel-dpe + permission: admin diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml new file mode 100644 index 000000000..a5b0e865c --- /dev/null +++ b/.github/workflows/automation.yml @@ -0,0 +1,28 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Automation +on: [pull_request] +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GOOGLEWORKSPACE_BOT_TOKEN}} + steps: + - name: approve + run: gh pr review --approve "$PR_URL" + - name: merge + run: gh pr merge --auto --squash --delete-branch "$PR_URL diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..c5cb1be8e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,24 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Lint +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + echo "No lint checks"; + exit 1; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..debf46551 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Test +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + echo "No tests"; + exit 1; diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..968a1fb31 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +# Report a security issue + +To report a security issue, please use https://g.co/vulnz. We use +https://g.co/vulnz for our intake, and do coordination and disclosure here on +GitHub (including using GitHub Security Advisory). The Google Security Team will +respond within 5 working days of your report on g.co/vulnz. From 95f4e34fe469b659a1ee7c6ec951f46afccb9dbb Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Thu, 21 Jul 2022 12:13:00 -0600 Subject: [PATCH 51/99] chore: standardize workflows (#340) --- .github/workflows/lint.yaml | 45 ------------------------------------- .github/workflows/lint.yml | 30 ++++++++++++++++++------- 2 files changed, 22 insertions(+), 53 deletions(-) delete mode 100644 .github/workflows/lint.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index 035862cb5..000000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. ---- -name: Lint -on: - workflow_dispatch: - push: - branches: - - master - pull_request: - branches: - - master -jobs: - lint: - concurrency: - group: ${{ github.head_ref || github.ref }} - cancel-in-progress: true - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3.0.2 - with: - fetch-depth: 0 - - uses: github/super-linter/slim@v4.9.4 - env: - ERROR_ON_MISSING_EXEC_BIT: true - VALIDATE_JSCPD: false - VALIDATE_JAVASCRIPT_STANDARD: false - VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-node@v3 - with: - node-version: '14' - - run: npm install - - run: npm run lint diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c5cb1be8e..fd65241d3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,14 +11,28 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +--- name: Lint -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: lint: - runs-on: ubuntu-latest + concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: true + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 - - run: | - echo "No lint checks"; - exit 1; + - uses: actions/checkout@v3.0.2 + with: + fetch-depth: 0 + - uses: github/super-linter/slim@v4.9.4 + env: + ERROR_ON_MISSING_EXEC_BIT: true + VALIDATE_JSCPD: false + VALIDATE_JAVASCRIPT_STANDARD: false + VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/setup-node@v3 + with: + node-version: '14' + - run: npm install + - run: npm run lint From c027ece1d086d96bb3757128d1f57104bc1bd112 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Thu, 21 Jul 2022 12:25:28 -0600 Subject: [PATCH 52/99] chore: add license headers (#341) --- .eslintrc.js | 16 ++++++++++++++++ .github/snippet-bot.yml | 14 ++++++++++++++ docs/cursorInspector/sidebar.css.html | 16 ++++++++++++++++ docs/cursorInspector/sidebar.html | 16 ++++++++++++++++ docs/cursorInspector/sidebar.js.html | 16 ++++++++++++++++ .../demos/AppsScriptFormsAPIWebApp/Main.html | 16 ++++++++++++++++ gmail/markup/mail_template.html | 16 ++++++++++++++++ sheets/next18/LinkDialog.html | 16 ++++++++++++++++ solutions/add-on/share-macro/UI.js | 16 ++++++++++++++++ .../aggregate-document-content/Menu.js | 16 ++++++++++++++++ .../aggregate-document-content/Setup.js | 16 ++++++++++++++++ .../aggregate-document-content/Utilities.js | 16 ++++++++++++++++ .../automations/calendar-timesheet/Page.html | 16 ++++++++++++++++ .../automations/employee-certificate/Code.js | 16 ++++++++++++++++ .../new-equipment-request.html | 16 ++++++++++++++++ .../equipment-requests/request-complete.html | 16 ++++++++++++++++ solutions/automations/generate-pdfs/Menu.js | 16 ++++++++++++++++ solutions/automations/generate-pdfs/Utilities.js | 16 ++++++++++++++++ .../automations/import-csv-sheets/SampleData.js | 16 ++++++++++++++++ .../automations/import-csv-sheets/SetupSample.js | 16 ++++++++++++++++ .../automations/import-csv-sheets/Utilities.js | 16 ++++++++++++++++ solutions/automations/upload-files/Setup.js | 16 ++++++++++++++++ solutions/automations/youtube-tracker/email.html | 16 ++++++++++++++++ solutions/chat-bots/schedule-meetings/Dialog.js | 16 ++++++++++++++++ .../chat-bots/schedule-meetings/Utilities.js | 16 ++++++++++++++++ solutions/editor-add-on/clean-sheet/Menu.js | 16 ++++++++++++++++ tasks/simpleTasks/javascript.html | 16 ++++++++++++++++ tasks/simpleTasks/page.html | 16 ++++++++++++++++ tasks/simpleTasks/stylesheet.html | 16 ++++++++++++++++ templates/sheets-import/intercom.js.html | 16 ++++++++++++++++ ui/communication/basic/index.html | 16 ++++++++++++++++ ui/communication/failure/index.html | 16 ++++++++++++++++ ui/communication/private/index.html | 16 ++++++++++++++++ ui/communication/success/index.html | 16 ++++++++++++++++ ui/forms/index.html | 16 ++++++++++++++++ ui/html/printing_scriptlet.html | 16 ++++++++++++++++ ui/html/scriptlet.html | 16 ++++++++++++++++ ui/html/standard_scriptlet.html | 16 ++++++++++++++++ ui/sidebar/index.html | 16 ++++++++++++++++ ui/user/index.html | 16 ++++++++++++++++ ui/webapp/index.html | 16 ++++++++++++++++ 41 files changed, 654 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 81d458100..114dd6491 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,19 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + module.exports = { extends: 'google', parserOptions: { diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml index e69de29bb..bb488a813 100644 --- a/.github/snippet-bot.yml +++ b/.github/snippet-bot.yml @@ -0,0 +1,14 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/docs/cursorInspector/sidebar.css.html b/docs/cursorInspector/sidebar.css.html index 685176869..4f5d09fec 100644 --- a/docs/cursorInspector/sidebar.css.html +++ b/docs/cursorInspector/sidebar.css.html @@ -1,3 +1,19 @@ + + diff --git a/docs/cursorInspector/sidebar.html b/docs/cursorInspector/sidebar.html index 7880f0ea7..b21def38b 100644 --- a/docs/cursorInspector/sidebar.html +++ b/docs/cursorInspector/sidebar.html @@ -1,3 +1,19 @@ + +
    diff --git a/docs/cursorInspector/sidebar.js.html b/docs/cursorInspector/sidebar.js.html index e52c32fac..90da371a3 100644 --- a/docs/cursorInspector/sidebar.js.html +++ b/docs/cursorInspector/sidebar.js.html @@ -1,3 +1,19 @@ + + diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html b/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html index a3f4b4c40..8612e1b37 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html @@ -1,4 +1,20 @@ + + diff --git a/gmail/markup/mail_template.html b/gmail/markup/mail_template.html index 68f4e9f3b..f339fbc31 100644 --- a/gmail/markup/mail_template.html +++ b/gmail/markup/mail_template.html @@ -1,3 +1,19 @@ + + diff --git a/tasks/simpleTasks/page.html b/tasks/simpleTasks/page.html index 1322b3072..f5f9f677a 100644 --- a/tasks/simpleTasks/page.html +++ b/tasks/simpleTasks/page.html @@ -1,4 +1,20 @@ + + diff --git a/tasks/simpleTasks/stylesheet.html b/tasks/simpleTasks/stylesheet.html index 76f0567ab..02c35c0d0 100644 --- a/tasks/simpleTasks/stylesheet.html +++ b/tasks/simpleTasks/stylesheet.html @@ -1,3 +1,19 @@ + + diff --git a/templates/sheets-import/intercom.js.html b/templates/sheets-import/intercom.js.html index 1154c4048..c8fcde5cb 100644 --- a/templates/sheets-import/intercom.js.html +++ b/templates/sheets-import/intercom.js.html @@ -1,3 +1,19 @@ + + + From 097b9c69514beb56cc2f080d28b3b9d7bc55eabb Mon Sep 17 00:00:00 2001 From: Daniel Heinrich <148484595+danielatgoogle@users.noreply.github.com> Date: Fri, 27 Oct 2023 11:41:52 -0700 Subject: [PATCH 90/99] Add files via upload (#425) * Add files via upload Hello DevRel friends, Working with @gtondello, I'm uploading code samples for the upcoming Google Chat Advanced Service in Apps Script launch. These code samples pair with the following bugs and documentation changelists. * Updating chat.gs with sqrrrl's feedback * Update advanced/chat.gs Co-authored-by: Gustavo Tondello * Fixing member >>> membership in chat.gs Fixing member >>> membership per Gustavo's feedback in https://github.com/googleworkspace/apps-script-samples/pull/425#discussion_r1374625393. * Update advanced/chat.gs Co-authored-by: Gustavo Tondello * Fixing linter errors in chat.gs Fixing the following linter errors: 1. On line 107, "Expected parentheses around arrow function argument". 2. On lines, 108, 109, and 110, " Expected indentation of 10 spaces but found 8". --------- Co-authored-by: Gustavo Tondello --- advanced/chat.gs | 118 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 advanced/chat.gs diff --git a/advanced/chat.gs b/advanced/chat.gs new file mode 100644 index 000000000..9cb506934 --- /dev/null +++ b/advanced/chat.gs @@ -0,0 +1,118 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START chat_post_message_with_user_credentials] +/** + * Posts a new message to the specified space on behalf of the user. + * @param {string} spaceName The resource name of the space. + */ +function postMessageWithUserCredentials(spaceName) { + try { + const message = {'text': 'Hello world!'}; + Chat.Spaces.Messages.create(message, spaceName); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed to create message with error %s', err.message); + } +} +// [END chat_post_message_with_user_credentials] + +// [START chat_post_message_with_app_credentials] +/** + * Posts a new message to the specified space on behalf of the app. + * @param {string} spaceName The resource name of the space. + */ +function postMessageWithAppCredentials(spaceName) { + try { + // See https://developers.google.com/chat/api/guides/auth/service-accounts + // for details on how to obtain a service account OAuth token. + const appToken = getToken_(); + const message = {'text': 'Hello world!'}; + Chat.Spaces.Messages.create( + message, + spaceName, + {}, + // Authenticate with the service account token. + {'Authorization': 'Bearer ' + appToken}); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed to create message with error %s', err.message); + } +} +// [END chat_post_message_with_app_credentials] + +// [START chat_get_space] +/** + * Gets information about a Chat space. + * @param {string} spaceName The resource name of the space. + */ +function getSpace(spaceName) { + try { + const space = Chat.Spaces.get(spaceName); + console.log('Space display name: %s', space.displayName); + console.log('Space type: %s', space.spaceType); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed to get space with error %s', err.message); + } +} +// [END chat_get_space] + +// [START chat_create_space] +/** + * Creates a new Chat space. + */ +function createSpace() { + try { + const space = {'displayName': 'New Space', 'spaceType': 'SPACE'}; + Chat.Spaces.create(space); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed to create space with error %s', err.message); + } +} +// [END chat_create_space] + +// [START chat_list_memberships] +/** + * Lists all the members of a Chat space. + * @param {string} spaceName The resource name of the space. + */ +function listMemberships(spaceName) { + let response; + let pageToken = null; + try { + do { + response = Chat.Spaces.Members.list(spaceName, { + pageSize: 10, + pageToken: pageToken + }); + if (!response.memberships || response.memberships.length === 0) { + pageToken = response.nextPageToken; + continue; + } + response.memberships.forEach((membership) => console.log( + 'Member resource name: %s (type: %s)', + membership.name, + membership.member.type)); + pageToken = response.nextPageToken; + } while (pageToken); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } +} +// [END chat_list_memberships] From f8359813f6b0007eb11d7c51adde49bea4f4c978 Mon Sep 17 00:00:00 2001 From: Pierrick Voulet <6769971+PierrickVoulet@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:19:40 -0500 Subject: [PATCH 91/99] Fix #426 (#428) Fix #426 --- advanced/calendar.gs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/advanced/calendar.gs b/advanced/calendar.gs index 28f8116a0..f41a54fda 100644 --- a/advanced/calendar.gs +++ b/advanced/calendar.gs @@ -117,7 +117,7 @@ function listNext10Events() { // All-day event. const start = new Date(event.start.date); console.log('%s (%s)', event.summary, start.toLocaleDateString()); - return; + continue; } const start = new Date(event.start.dateTime); console.log('%s (%s)', event.summary, start.toLocaleString()); From 2f026784f714a69b51571aaa9868f7fe76829067 Mon Sep 17 00:00:00 2001 From: Kara <62033369+kar320@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:16:45 -0700 Subject: [PATCH 92/99] fix: Update Code.js to account for changes from Calendar API (#434) Updates the most recent version of the Vacation Calendar solution for Apps Script. --- solutions/automations/vacation-calendar/Code.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/solutions/automations/vacation-calendar/Code.js b/solutions/automations/vacation-calendar/Code.js index d00a2bee6..ef0078a20 100644 --- a/solutions/automations/vacation-calendar/Code.js +++ b/solutions/automations/vacation-calendar/Code.js @@ -97,6 +97,15 @@ function importEvent(username, event) { id: TEAM_CALENDAR_ID, }; event.attendees = []; + + // If the event is not of type 'default', it can't be imported, so it needs + // to be changed. + if (event.eventType != 'default') { + event.eventType = 'default'; + delete event.outOfOfficeProperties; + delete event.focusTimeProperties; + } + console.log('Importing: %s', event.summary); try { Calendar.Events.import(event, TEAM_CALENDAR_ID); From 17fe1a804c5b59a85971533e000c0a18d26af5fd Mon Sep 17 00:00:00 2001 From: Steve Bazyl Date: Thu, 7 Dec 2023 13:12:05 -0700 Subject: [PATCH 93/99] Fix trigger to run on push to main instead of master --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 8d6fba836..292075162 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -17,7 +17,7 @@ on: workflow_dispatch: push: branches: - - master + - main jobs: publish: concurrency: From 82c5c040044b0db0780ad6346755a042f6cbbcb3 Mon Sep 17 00:00:00 2001 From: Ken Cenerelli Date: Mon, 11 Dec 2023 10:27:15 -0800 Subject: [PATCH 94/99] Update drive.gs Advanced Drive Service samples (#433) * Update drive.gs Advanced Drive Service samples * Update the samples to work with Drive API v3. * Set page token for list revisions * Update drive.gs Fixed commit issues * Update drive.gs --------- Co-authored-by: Steve Bazyl --- advanced/drive.gs | 86 ++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/advanced/drive.gs b/advanced/drive.gs index 6dcce2256..a0ff1ab18 100644 --- a/advanced/drive.gs +++ b/advanced/drive.gs @@ -23,12 +23,12 @@ function uploadFile() { // Makes a request to fetch a URL. const image = UrlFetchApp.fetch('http://goo.gl/nd7zjB').getBlob(); let file = { - title: 'google_logo.png', + name: 'google_logo.png', mimeType: 'image/png' }; - // Insert new files to user's Drive - file = Drive.Files.insert(file, image); - console.log('ID: %s, File size (bytes): %s', file.id, file.fileSize); + // Create a file in the user's Drive. + file = Drive.Files.create(file, image, {'fields': 'id,size'}); + console.log('ID: %s, File size (bytes): %s', file.id, file.size); } catch (err) { // TODO (developer) - Handle exception console.log('Failed to upload file with error %s', err.message); @@ -49,16 +49,16 @@ function listRootFolders() { try { folders = Drive.Files.list({ q: query, - maxResults: 100, + pageSize: 100, pageToken: pageToken }); - if (!folders.items || folders.items.length === 0) { - console.log('No folders found.'); + if (!folders.files || folders.files.length === 0) { + console.log('All folders found.'); return; } - for (let i = 0; i < folders.items.length; i++) { - const folder = folders.items[i]; - console.log('%s (ID: %s)', folder.title, folder.id); + for (let i = 0; i < folders.files.length; i++) { + const folder = folders.files[i]; + console.log('%s (ID: %s)', folder.name, folder.id); } pageToken = folders.nextPageToken; } catch (err) { @@ -71,20 +71,24 @@ function listRootFolders() { // [START drive_add_custom_property] /** - * Adds a custom property to a file. Unlike Apps Script's DocumentProperties, + * Adds a custom app property to a file. Unlike Apps Script's DocumentProperties, * Drive's custom file properties can be accessed outside of Apps Script and - * by other applications (if the visibility is set to PUBLIC). - * @param {string} fileId The ID of the file to add the property to. + * by other applications; however, appProperties are only visible to the script. + * @param {string} fileId The ID of the file to add the app property to. */ -function addCustomProperty(fileId) { +function addAppProperty(fileId) { try { - const property = { - key: 'department', - value: 'Sales', - visibility: 'PUBLIC' + let file = { + 'appProperties': { + 'department': 'Sales' + } }; - // Adds a property to a file - Drive.Properties.insert(property, fileId); + // Updates a file to add an app property. + file = Drive.Files.update(file, fileId, null, {'fields': 'id,appProperties'}); + console.log( + 'ID: %s, appProperties: %s', + file.id, + JSON.stringify(file.appProperties, null, 2)); } catch (err) { // TODO (developer) - Handle exception console.log('Failed with error %s', err.message); @@ -94,29 +98,33 @@ function addCustomProperty(fileId) { // [START drive_list_revisions] /** - * Lists the revisions of a given file. Note that some properties of revisions - * are only available for certain file types. For example, Google Workspace - * application files do not consume space in Google Drive and thus list a file - * size of 0. + * Lists the revisions of a given file. * @param {string} fileId The ID of the file to list revisions for. */ function listRevisions(fileId) { - try { - const revisions = Drive.Revisions.list(fileId); - if (!revisions.items || revisions.items.length === 0) { - console.log('No revisions found.'); - return; - } - for (let i = 0; i < revisions.items.length; i++) { - const revision = revisions.items[i]; - const date = new Date(revision.modifiedDate); - console.log('Date: %s, File size (bytes): %s', date.toLocaleString(), - revision.fileSize); + let revisions; + const pageToken = null; + do { + try { + revisions = Drive.Revisions.list( + fileId, + {'fields': 'revisions(modifiedTime,size),nextPageToken'}); + if (!revisions.revisions || revisions.revisions.length === 0) { + console.log('All revisions found.'); + return; + } + for (let i = 0; i < revisions.revisions.length; i++) { + const revision = revisions.revisions[i]; + const date = new Date(revision.modifiedTime); + console.log('Date: %s, File size (bytes): %s', date.toLocaleString(), + revision.size); + } + pageToken = revisions.nextPageToken; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); } - } catch (err) { - // TODO (developer) - Handle exception - console.log('Failed with error %s', err.message); - } + } while (pageToken); } // [END drive_list_revisions] From 57b602064b4b45aecf5c26741a0fa56a7232452c Mon Sep 17 00:00:00 2001 From: Chanel Greco Date: Tue, 16 Jan 2024 18:52:18 +0100 Subject: [PATCH 95/99] fix: write OOO event type using advanced Calendar service (#437) * Sample for the AppSheet and Apps Script video tutorial * Added Calendar advanced service and updated function to add OOO event type. * Changed the blockOutCalendar function synce OOO events are new supported by the Calendar API. * Update Code.js * Update README.md * Update README.md --------- Co-authored-by: Pierrick Voulet <6769971+PierrickVoulet@users.noreply.github.com> --- solutions/automations/folder-creation/Code.js | 40 +- .../automations/folder-creation/README.md | 8 +- solutions/ooo-chat-app/Code.js | 408 ++++++++++-------- solutions/ooo-chat-app/README.md | 4 +- solutions/ooo-chat-app/appsscript.json | 33 +- 5 files changed, 268 insertions(+), 225 deletions(-) diff --git a/solutions/automations/folder-creation/Code.js b/solutions/automations/folder-creation/Code.js index 51df363e1..75e4b014c 100644 --- a/solutions/automations/folder-creation/Code.js +++ b/solutions/automations/folder-creation/Code.js @@ -1,27 +1,23 @@ -/** - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +/* +Copyright 2022 Google LLC +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ -// To learn how to use this script, refer to the video: https://youtu.be/Utl57R7I2Cs +/* +This function will create a new folder in the defined Shard Drive. +You define the Shared Drive by adding its ID on line number 26. +The parameter 'project' is passed in from the AppSheet app. +Please watch this video tutorial to see how to use this script: https://youtu.be/Utl57R7I2Cs +*/ -/** - * This function will create a new folder in the defined Shard Drive. You define - * the Shared Drive by adding its ID on line number 28. The parameter `project` - * is passed in from the AppSheet app. Please watch this video tutorial to see - * how to use this script: https://youtu.be/Utl57R7I2Cs. - */ function createNewFolder(project) { const folder = Drive.Files.insert( { diff --git a/solutions/automations/folder-creation/README.md b/solutions/automations/folder-creation/README.md index aa76b2b76..f4cefdadd 100644 --- a/solutions/automations/folder-creation/README.md +++ b/solutions/automations/folder-creation/README.md @@ -2,6 +2,10 @@ This code sample is part of a video tutorial on how to combine AppSheet and Apps Script. -You can watch the video tutorial to find out how to use the sample at https://youtu.be/Utl57R7I2Cs. +You can watch the video tutorial to find out how to use the sample. -See the [Google Apps Script Documentation](https://developers.google.com/apps-script/advanced/drive) for additional information about the advanced Google Drive services. \ No newline at end of file +

    + +

    + +See the [Google Apps Script Documentation](https://developers.google.com/apps-script/advanced/drive) for additional information about the advanced Google Drive services. diff --git a/solutions/ooo-chat-app/Code.js b/solutions/ooo-chat-app/Code.js index 1ff83e3a0..b03dde223 100644 --- a/solutions/ooo-chat-app/Code.js +++ b/solutions/ooo-chat-app/Code.js @@ -1,210 +1,246 @@ +/* +Copyright 2022 Google LLC +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + /** * Responds to an ADDED_TO_SPACE event in Chat. * @param {object} event the event object from Chat * @return {object} JSON-formatted response * @see https://developers.google.com/hangouts/chat/reference/message-formats/events */ - function onAddToSpace(event) { - let message = 'Thank you for adding me to '; - if (event.space.type === 'DM') { - message += 'a DM, ' + event.user.displayName + '!'; - } else { - message += event.space.displayName; - } - return { text: message }; - } - - /** - * Responds to a REMOVED_FROM_SPACE event in Chat. - * @param {object} event the event object from Chat - * @param {object} event the event object from Chat - * @see https://developers.google.com/hangouts/chat/reference/message-formats/events - */ - function onRemoveFromSpace(event) { - console.log('App removed from ', event.space.name); +function onAddToSpace(event) { + let message = 'Thank you for adding me to '; + if (event.space.type === 'DM') { + message += 'a DM, ' + event.user.displayName + '!'; + } else { + message += event.space.displayName; } - - - /** - * Responds to a MESSAGE event triggered in Chat. - * @param {object} event the event object from Chat - * @return {function} call the respective function - */ - function onMessage(event) { - const message = event.message; - - if (message.slashCommand) { - switch (message.slashCommand.commandId) { - case 1: // Help command - return createHelpCard(); - case 2: // Block out day command - return blockDayOut(); - case 3: // Cancel all meetings command - return cancelAllMeetings(); - case 4: // Set auto reply command - return setAutoReply(); - } + return { text: message }; +} + +/** + * Responds to a REMOVED_FROM_SPACE event in Chat. + * @param {object} event the event object from Chat + * @param {object} event the event object from Chat + * @see https://developers.google.com/hangouts/chat/reference/message-formats/events + */ +function onRemoveFromSpace(event) { + console.log('App removed from ', event.space.name); +} + + +/** + * Responds to a MESSAGE event triggered in Chat. + * @param {object} event the event object from Chat + * @return {function} call the respective function + */ +function onMessage(event) { + const message = event.message; + + if (message.slashCommand) { + switch (message.slashCommand.commandId) { + case 1: // Help command + return createHelpCard(); + case 2: // Block out day command + return blockDayOut(); + case 3: // Cancel all meetings command + return cancelAllMeetings(); + case 4: // Set auto reply command + return setAutoReply(); } } - - function createHelpCard() { - return { - "cardsV2": [ - { - "cardId": "2", - "card": { - "sections": [ - { - "header": "", - "widgets": [ - { - "decoratedText": { - "topLabel": "", - "text": "Hi! 👋 I'm here to help you with your out of office tasks.

    Here's a list of commands I understand.", - "wrapText": true - } +} + +function createHelpCard() { + return { + "cardsV2": [ + { + "cardId": "2", + "card": { + "sections": [ + { + "header": "", + "widgets": [ + { + "decoratedText": { + "topLabel": "", + "text": "Hi! 👋 I'm here to help you with your out of office tasks.

    Here's a list of commands I understand.", + "wrapText": true + } + } + ] + }, + { + "widgets": [ + { + "decoratedText": { + "topLabel": "", + "text": "/blockDayOut: I will block out your calendar for you.", + "wrapText": true } - ] - }, - { - "widgets": [ - { - "decoratedText": { - "topLabel": "", - "text": "/blockDayOut: I will block out your calendar for you.", - "wrapText": true - } - }, - { - "decoratedText": { - "topLabel": "", - "text": "/cancelAllMeetings: I will cancel all your meetings for the day.", - "wrapText": true - } - }, - { - "decoratedText": { - "topLabel": "", - "text": "/setAutoReply: Set an out of office auto reply in Gmail.", - "wrapText": true - } + }, + { + "decoratedText": { + "topLabel": "", + "text": "/cancelAllMeetings: I will cancel all your meetings for the day.", + "wrapText": true } - ] - } - ], - "header": { - "title": "OOO app", - "subtitle": "Helping you manage your OOO", - "imageUrl": "https://goo.gle/3SfMkjb", - "imageType": "SQUARE" + }, + { + "decoratedText": { + "topLabel": "", + "text": "/setAutoReply: Set an out of office auto reply in Gmail.", + "wrapText": true + } + } + ] } + ], + "header": { + "title": "OOO app", + "subtitle": "Helping you manage your OOO", + "imageUrl": "https://goo.gle/3SfMkjb", + "imageType": "SQUARE" } } - ] - } - } - - /** - * Adds an all day event to the users Google Calendar. - * @return {object} JSON-formatted response - */ - function blockDayOut() { - blockOutCalendar(); - return createResponseCard('Your calendar has been blocked out for you.') - } - - /** - * Cancels all of the users meeting for the current day. - * @return {object} JSON-formatted response - */ - function cancelAllMeetings() { - cancelMeetings(); - return createResponseCard('All your meetings have been canceled.') - } - - /** - * Sets an out of office auto reply in the users Gmail account. - * @return {object} JSON-formatted response - */ - function setAutoReply() { - turnOnAutoResponder(); - return createResponseCard('The out of office auto reply has been turned on.') - } - - - const ONE_DAY_MILLIS = 24 * 60 * 60 * 1000; - - /** - * Places an all-day meeting on the user's Calendar. - */ - function blockOutCalendar() { - CalendarApp.createAllDayEvent('I am out of office today', new Date(), new Date(Date.now() + ONE_DAY_MILLIS)); - } - - /** - * Declines all meetings for the day. - */ - - function cancelMeetings() { - const events = CalendarApp.getEventsForDay(new Date()); - - events.forEach(function(event) { - if (event.getGuestList().length > 0) { - event.setMyStatus(CalendarApp.GuestStatus.NO); } - }); + ] } - +} + +/** + * Adds an all day event to the users Google Calendar. + * @return {object} JSON-formatted response + */ +function blockDayOut() { + blockOutCalendar(); + return createResponseCard('Your calendar has been blocked out for you.') +} + +/** + * Cancels all of the users meeting for the current day. + * @return {object} JSON-formatted response + */ +function cancelAllMeetings() { + cancelMeetings(); + return createResponseCard('All your meetings have been canceled.') +} + +/** + * Sets an out of office auto reply in the users Gmail account. + * @return {object} JSON-formatted response + */ +function setAutoReply() { + turnOnAutoResponder(); + return createResponseCard('The out of office auto reply has been turned on.') +} + + + +/** + * Creates an out of office event in the user's Calendar. + */ +function blockOutCalendar() { /** - * Turns on the user's vacation response for today in Gmail. + * Helper function to get a the current date and set the time for the start and end of the event. + * @param {number} hour The hour of the day for the new date. + * @param {number} minutes The minutes of the day for the new date. + * @return {Date} The new date. */ - function turnOnAutoResponder() { - const currentTime = (new Date()).getTime(); - Gmail.Users.Settings.updateVacation({ - enableAutoReply: true, - responseSubject: 'I am out of the office today', - responseBodyHtml: 'I am out of the office today; will be back on the next business day.

    Created by OOO Chat app!', - restrictToContacts: true, - restrictToDomain: true, - startTime: currentTime, - endTime: currentTime + ONE_DAY_MILLIS - }, 'me'); + function getDateAndHours(hour, minutes) { + const date = new Date(); + date.setHours(hour); + date.setMinutes(minutes); + date.setSeconds(0); + date.setMilliseconds(0); + return date.toISOString(); + } + + const event = { + start: {dateTime: getDateAndHours(9,00)}, + end: {dateTime: getDateAndHours(17,00)}, + eventType: 'outOfOffice', + summary: 'Out of office', + outOfOfficeProperties: { + autoDeclineMode: 'declineOnlyNewConflictingInvitations', + declineMessage: 'Declined because I am taking a day of.', + } } - - function createResponseCard(responseText) { - return { - "cardsV2": [ - { - "cardId": "1", - "card": { - "sections": [ - { - "widgets": [ - { - "decoratedText": { - "topLabel": "", - "text": responseText, - "startIcon": { - "knownIcon": "NONE", - "altText": "Task done", - "iconUrl": "https://fonts.gstatic.com/s/i/short-term/web/system/1x/task_alt_gm_grey_48dp.png" - }, - "wrapText": true - } + Calendar.Events.insert(event, 'primary'); +} + +/** + * Declines all meetings for the day. + */ +function cancelMeetings() { + const events = CalendarApp.getEventsForDay(new Date()); + + events.forEach(function(event) { + if (event.getGuestList().length > 0) { + event.setMyStatus(CalendarApp.GuestStatus.NO); + } + }); +} + +/** + * Turns on the user's vacation response for today in Gmail. + */ +function turnOnAutoResponder() { + const ONE_DAY_MILLIS = 24 * 60 * 60 * 1000; + const currentTime = (new Date()).getTime(); + Gmail.Users.Settings.updateVacation({ + enableAutoReply: true, + responseSubject: 'I am out of the office today', + responseBodyHtml: 'I am out of the office today; will be back on the next business day.

    Created by OOO Chat app!', + restrictToContacts: true, + restrictToDomain: true, + startTime: currentTime, + endTime: currentTime + ONE_DAY_MILLIS + }, 'me'); +} + +function createResponseCard(responseText) { + return { + "cardsV2": [ + { + "cardId": "1", + "card": { + "sections": [ + { + "widgets": [ + { + "decoratedText": { + "topLabel": "", + "text": responseText, + "startIcon": { + "knownIcon": "NONE", + "altText": "Task done", + "iconUrl": "https://fonts.gstatic.com/s/i/short-term/web/system/1x/task_alt_gm_grey_48dp.png" + }, + "wrapText": true } - ] - } - ], - "header": { - "title": "OOO app", - "subtitle": "Helping you manage your OOO", - "imageUrl": "https://goo.gle/3SfMkjb", - "imageType": "CIRCLE" + } + ] } + ], + "header": { + "title": "OOO app", + "subtitle": "Helping you manage your OOO", + "imageUrl": "https://goo.gle/3SfMkjb", + "imageType": "CIRCLE" } } - ] - } + } + ] } - - \ No newline at end of file +} + diff --git a/solutions/ooo-chat-app/README.md b/solutions/ooo-chat-app/README.md index 2982befa1..4d72f861a 100644 --- a/solutions/ooo-chat-app/README.md +++ b/solutions/ooo-chat-app/README.md @@ -1,3 +1,5 @@ +# OOO Chat App + Sample code for a custom Google Chat app that manages your out of office tasks. -Learn more about Chat apps: https://developers.google.com/chat \ No newline at end of file +Learn more about [Chat apps](https://developers.google.com/chat). diff --git a/solutions/ooo-chat-app/appsscript.json b/solutions/ooo-chat-app/appsscript.json index ba4ac17c5..de9fa0b7b 100644 --- a/solutions/ooo-chat-app/appsscript.json +++ b/solutions/ooo-chat-app/appsscript.json @@ -1,15 +1,20 @@ { - "timeZone": "Europe/Madrid", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Gmail", - "version": "v1", - "serviceId": "gmail" - } - ] - }, - "chat": {} - } \ No newline at end of file + "timeZone": "Europe/Madrid", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + }, + { + "userSymbol": "Calendar", + "version": "v3", + "serviceId": "calendar" + } + ] + }, + "chat": {} +} \ No newline at end of file From f1d4ad595ca376384b56c69bbddc3610766686c4 Mon Sep 17 00:00:00 2001 From: Vinay Vyas <69166360+vinay-google@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:20:06 -0800 Subject: [PATCH 96/99] fix: Allowlist URLs through manifest for Preview links with Smart Chips (#441) --- solutions/add-on/book-smartchip/README.md | 5 +++++ solutions/add-on/book-smartchip/appsscript.json | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 solutions/add-on/book-smartchip/README.md diff --git a/solutions/add-on/book-smartchip/README.md b/solutions/add-on/book-smartchip/README.md new file mode 100644 index 000000000..46a058d5d --- /dev/null +++ b/solutions/add-on/book-smartchip/README.md @@ -0,0 +1,5 @@ +# Preview links from Google Books with smart chips + +See +https://developers.google.com/workspace/add-ons/samples/preview-links-google-books +for additional details. diff --git a/solutions/add-on/book-smartchip/appsscript.json b/solutions/add-on/book-smartchip/appsscript.json index 4e8538061..743b393c6 100644 --- a/solutions/add-on/book-smartchip/appsscript.json +++ b/solutions/add-on/book-smartchip/appsscript.json @@ -6,6 +6,9 @@ "https://www.googleapis.com/auth/workspace.linkpreview", "https://www.googleapis.com/auth/script.external_request" ], + "urlFetchWhitelist": [ + "https://www.googleapis.com/books/v1/volumes/" + ], "addOns": { "common": { "name": "Preview Books Add-on", @@ -37,4 +40,4 @@ ] } } -} \ No newline at end of file +} From d2c90495fc4c343edc07faa8d985dcf56953b8a9 Mon Sep 17 00:00:00 2001 From: Iben1993 <164604824+Iben1993@users.noreply.github.com> Date: Sat, 23 Mar 2024 07:46:36 +0100 Subject: [PATCH 97/99] Update LICENSE --- LICENSE | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/LICENSE b/LICENSE index d64569567..6dd3ca79d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,23 +1,23 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ + Apapache engedély + 2.0. változat 2004. január 2.0. változat + http://www.apache.org/licenses/ - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + A HASZNÁLAT, A SZAPORODÁS ÉS AZ ELOSZTÁS FELTÉTELEI - 1. Definitions. + 1. Meghatárok. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. + A "engedély" a használat feltételeit, a szaporodást jelenti, + és a dokumentum 1. és 9. szakasza által meghatározott elosztás. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. + Az "engedélyor" a szerzői jog tulajdonosát vagy jogalanyt jelenti, amelyet a szerzői jog tulajdonosa vagy jogalany + a szerzői jogi tulajdonos, aki az engedélyt biztosítja. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or + A "jogi sértés" a jogalany és minden + más olyan jogalanyok, amelyek ellenőrzik vagy közös jellegűek + az a jogalany ellenőrzése. E meghatározás érdekében, + Az "ellenőrzés" azt jelenti (i) a hatalom, közvetlen vagy közvetett, hogy a + az ilyen jogalany irányítása vagy kezelése, akár szerződéssel, akár otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. From 608416858c6d21ad2438d3494f1b3fa7ee9644de Mon Sep 17 00:00:00 2001 From: Iben1993 <164604824+Iben1993@users.noreply.github.com> Date: Sat, 23 Mar 2024 09:05:51 +0100 Subject: [PATCH 98/99] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 6dd3ca79d..4c7a704c1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ - +B5690EEEBB952194 Apapache engedély 2.0. változat 2004. január 2.0. változat http://www.apache.org/licenses/ From 62f0374601d8fbce450e967d47b0094a8c2dbff0 Mon Sep 17 00:00:00 2001 From: Iben1993 <164604824+Iben1993@users.noreply.github.com> Date: Sat, 23 Mar 2024 12:53:55 +0100 Subject: [PATCH 99/99] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a56b53039..92d6209db 100644 --- a/README.md +++ b/README.md @@ -151,3 +151,4 @@ npm run lint ``` This command will fix simple errors. +