From 18babc95cb7fcb2dc8ed964c9c1bfdd8eab75457 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 16:25:38 -0700 Subject: [PATCH 01/12] build: switch to biomejs --- .eslintignore | 2 - .eslintrc.js | 50 --- biome.json | 20 + package.json | 11 +- pnpm-lock.yaml | 760 ++++----------------------------- pnpm-workspace.yaml | 1 + wasm/hello-world/biome.json | 9 - wasm/hello-world/package.json | 3 +- wasm/image-add-on/biome.json | 9 - wasm/image-add-on/package.json | 3 +- wasm/python/biome.json | 9 - wasm/python/package.json | 3 +- 12 files changed, 114 insertions(+), 766 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.js create mode 100644 biome.json delete mode 100644 wasm/hello-world/biome.json delete mode 100644 wasm/image-add-on/biome.json delete mode 100644 wasm/python/biome.json diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index f1f3d7c64..000000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -moment.gs -wasm/** \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index d9cfc9029..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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: { - ecmaVersion: 2020 - }, - env: { - 'node': true, - 'googleappsscript/googleappsscript': true - }, - rules: { - 'comma-dangle': ['error', 'never'], - 'max-len': ['error', {code: 100}], - '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. - }, - plugins: ['googleappsscript'], - overrides: [ - { - files: ['scripts/**/*.js'], - parserOptions: { - sourceType: 'module' - } - } - ] -}; diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..0f254acda --- /dev/null +++ b/biome.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "files": { + "ignore": ["moment.gs", "**/dist", "**/target", "**/pkg", "**/node_modules"] + } +} diff --git a/package.json b/package.json index cbe7e4766..d26850c71 100644 --- a/package.json +++ b/package.json @@ -16,20 +16,15 @@ "devDependencies": { "@types/google-apps-script": "^2.0.7", "@types/node": "^24.10.1", - "eslint": "8.18.0", - "eslint-config-google": "0.14.0", - "eslint-plugin-googleappsscript": "1.0.4", + "@biomejs/biome": "1.9.4", "tsx": "^4.20.6", "typescript": "^5.9.3" }, "scripts": { - "lint": "./node_modules/.bin/eslint **/*.gs", - "fix": "./node_modules/.bin/eslint **/*.gs --fix --quiet", + "lint": "biome check .", + "format": "biome check --write .", "check": "tsx .github/scripts/check-gs.ts" }, - "eslintIgnore": [ - "**/node_modules/**" - ], "type": "module", "packageManager": "pnpm@10.15.1", "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93dc263fb..6ff5402ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,21 +8,15 @@ importers: .: devDependencies: + '@biomejs/biome': + specifier: 1.9.4 + version: 1.9.4 '@types/google-apps-script': specifier: ^2.0.7 version: 2.0.7 '@types/node': specifier: ^24.10.1 version: 24.10.1 - eslint: - specifier: 8.18.0 - version: 8.18.0 - eslint-config-google: - specifier: 0.14.0 - version: 0.14.0(eslint@8.18.0) - eslint-plugin-googleappsscript: - specifier: 1.0.4 - version: 1.0.4 tsx: specifier: ^4.20.6 version: 4.20.6 @@ -32,6 +26,59 @@ importers: packages: + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -188,351 +235,33 @@ packages: cpu: [x64] os: [win32] - '@eslint/eslintrc@1.4.1': - resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@humanwhocodes/config-array@0.9.5': - resolution: {integrity: sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/object-schema@1.2.1': - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - deprecated: Use @eslint/object-schema instead - '@types/google-apps-script@2.0.7': resolution: {integrity: sha512-Yd5ham8GJNPoKSMt9Ep+B5IJzmfsvSNkOF1DWGQDyz66+1O/Jw/TEe6fltJcTKRvNlCCH5ZaGmmYDOgadA8QYQ==} '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-config-google@0.14.0: - resolution: {integrity: sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==} - engines: {node: '>=0.10.0'} - peerDependencies: - eslint: '>=5.16.0' - - eslint-plugin-googleappsscript@1.0.4: - resolution: {integrity: sha512-Z6w1EMw0z0VOUvI0PFquigNSkYTgB+pVb2O2KYedDHnllwrgjjNBOLxsqPmtwmjOtFqCu7TuL0hJQwxVnp5OzQ==} - engines: {node: '>=0.10.0'} - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-utils@3.0.0: - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - - eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.18.0: - resolution: {integrity: sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - functional-red-black-tree@1.0.1: - resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - - requireindex@1.1.0: - resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} - engines: {node: '>=0.10.5'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - tsx@4.20.6: resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} engines: {node: '>=18.0.0'} hasBin: true - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -541,25 +270,42 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} +snapshots: + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true - v8-compile-cache@2.4.0: - resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + '@biomejs/cli-darwin-x64@1.9.4': + optional: true - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} + '@biomejs/cli-linux-arm64@1.9.4': + optional: true - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true -snapshots: + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true '@esbuild/aix-ppc64@0.25.12': optional: true @@ -639,95 +385,12 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@eslint/eslintrc@1.4.1': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/config-array@0.9.5': - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/object-schema@1.2.1': {} - '@types/google-apps-script@2.0.7': {} '@types/node@24.10.1': dependencies: undici-types: 7.16.0 - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - argparse@2.0.1: {} - - balanced-match@1.0.2: {} - - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - callsites@3.1.0: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concat-map@0.0.1: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -757,244 +420,15 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - escape-string-regexp@4.0.0: {} - - eslint-config-google@0.14.0(eslint@8.18.0): - dependencies: - eslint: 8.18.0 - - eslint-plugin-googleappsscript@1.0.4: - dependencies: - requireindex: 1.1.0 - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-utils@3.0.0(eslint@8.18.0): - dependencies: - eslint: 8.18.0 - eslint-visitor-keys: 2.1.0 - - eslint-visitor-keys@2.1.0: {} - - eslint-visitor-keys@3.4.3: {} - - eslint@8.18.0: - dependencies: - '@eslint/eslintrc': 1.4.1 - '@humanwhocodes/config-array': 0.9.5 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-utils: 3.0.0(eslint@8.18.0) - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - functional-red-black-tree: 1.0.1 - glob-parent: 6.0.2 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - js-yaml: 4.1.1 - 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.4 - regexpp: 3.2.0 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - v8-compile-cache: 2.4.0 - transitivePeerDependencies: - - supports-color - - espree@9.6.1: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 3.4.3 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - - flat-cache@3.2.0: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.3.3: {} - - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true - functional-red-black-tree@1.0.1: {} - get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - - has-flag@4.0.0: {} - - ignore@5.3.2: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - isexe@2.0.0: {} - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lodash.merge@4.6.2: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - ms@2.1.3: {} - - natural-compare@1.4.0: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-is-absolute@1.0.1: {} - - path-key@3.1.1: {} - - prelude-ls@1.2.1: {} - - punycode@2.3.1: {} - - regexpp@3.2.0: {} - - requireindex@1.1.0: {} - - resolve-from@4.0.0: {} - resolve-pkg-maps@1.0.0: {} - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - text-table@0.2.0: {} - tsx@4.20.6: dependencies: esbuild: 0.25.12 @@ -1002,26 +436,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-fest@0.20.2: {} - typescript@5.9.3: {} undici-types@7.16.0: {} - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - v8-compile-cache@2.4.0: {} - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - wrappy@1.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index efc037aa8..d649e815b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ onlyBuiltDependencies: + - '@biomejs/biome' - esbuild diff --git a/wasm/hello-world/biome.json b/wasm/hello-world/biome.json deleted file mode 100644 index c9a226d97..000000000 --- a/wasm/hello-world/biome.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "formatter": { - "indentStyle": "space" - }, - "files": { - "ignore": ["node_modules", "dist", "target", "pkg"] - } -} diff --git a/wasm/hello-world/package.json b/wasm/hello-world/package.json index 9e4fdd3c8..20e48b496 100644 --- a/wasm/hello-world/package.json +++ b/wasm/hello-world/package.json @@ -8,7 +8,7 @@ "build:wasm": "wireit", "clean": "rm -rf dist pkg target", "deploy": "wireit", - "format": "biome check --apply .; cargo fmt", + "format": "cargo fmt", "start": "wireit" }, "wireit": { @@ -71,7 +71,6 @@ "author": "Justin Poehnelt ", "license": "Apache-2.0", "devDependencies": { - "@biomejs/biome": "^1.5.3", "@google/clasp": "^2.4.2", "esbuild": "^0.20.1", "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", diff --git a/wasm/image-add-on/biome.json b/wasm/image-add-on/biome.json deleted file mode 100644 index c9a226d97..000000000 --- a/wasm/image-add-on/biome.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "formatter": { - "indentStyle": "space" - }, - "files": { - "ignore": ["node_modules", "dist", "target", "pkg"] - } -} diff --git a/wasm/image-add-on/package.json b/wasm/image-add-on/package.json index dbf711def..a0c15d8cf 100644 --- a/wasm/image-add-on/package.json +++ b/wasm/image-add-on/package.json @@ -8,7 +8,7 @@ "build:wasm": "wireit", "clean": "rm -rf dist pkg target", "deploy": "wireit", - "format": "biome check --apply .; cargo fmt", + "format": "cargo fmt", "start": "wireit" }, "wireit": { @@ -72,7 +72,6 @@ "author": "Justin Poehnelt ", "license": "Apache-2.0", "devDependencies": { - "@biomejs/biome": "^1.5.3", "@google/clasp": "^2.4.2", "esbuild": "^0.20.1", "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", diff --git a/wasm/python/biome.json b/wasm/python/biome.json deleted file mode 100644 index c9a226d97..000000000 --- a/wasm/python/biome.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "formatter": { - "indentStyle": "space" - }, - "files": { - "ignore": ["node_modules", "dist", "target", "pkg"] - } -} diff --git a/wasm/python/package.json b/wasm/python/package.json index 0cec99642..4bc79474a 100644 --- a/wasm/python/package.json +++ b/wasm/python/package.json @@ -8,7 +8,7 @@ "build:wasm": "wireit", "clean": "rm -rf dist pkg target", "deploy": "wireit", - "format": "biome check --apply .; cargo fmt", + "format": "cargo fmt", "start": "wireit" }, "wireit": { @@ -72,7 +72,6 @@ "author": "Justin Poehnelt ", "license": "Apache-2.0", "devDependencies": { - "@biomejs/biome": "^1.5.3", "@google/clasp": "^2.4.2", "esbuild": "^0.20.1", "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", From 431594faa4a8605a670f6d255b05f3ab6c196245 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 16:26:48 -0700 Subject: [PATCH 02/12] style: biome check --write --- .gemini/settings.json | 2 +- .github/linters/.htmlhintrc | 46 +- .github/scripts/check-gs.ts | 411 +++++----- .vscode/extensions.json | 6 +- .vscode/settings.json | 8 +- ai/autosummarize/appsscript.json | 42 +- ai/autosummarize/gemini.js | 142 ++-- ai/autosummarize/main.js | 193 +++-- ai/autosummarize/summarize.js | 238 +++--- ai/custom-func-ai-agent/AiVertex.js | 150 ++-- ai/custom-func-ai-agent/Code.js | 19 +- ai/custom-func-ai-agent/appsscript.json | 24 +- ai/custom-func-ai-studio/Code.js | 11 +- ai/custom-func-ai-studio/appsscript.json | 11 +- ai/custom-func-ai-studio/gemini.js | 95 +-- ai/custom_func_vertex/Code.js | 10 +- ai/custom_func_vertex/aiVertex.js | 140 ++-- ai/custom_func_vertex/appsscript.json | 24 +- ai/devdocs-link-preview/Cards.js | 157 ++-- ai/devdocs-link-preview/Helpers.js | 12 +- ai/devdocs-link-preview/Main.js | 70 +- ai/devdocs-link-preview/Vertex.js | 154 ++-- ai/devdocs-link-preview/appsscript.json | 138 ++-- ai/drive-rename/ai.js | 126 ++-- ai/drive-rename/appsscript.json | 86 +-- ai/drive-rename/drive.js | 148 ++-- ai/drive-rename/main.js | 65 +- ai/drive-rename/ui.js | 468 ++++++------ ai/email-classifier/appsscript.json | 72 +- ai/gmail-sentiment-analysis/appsscript.json | 52 +- ai/standup-chat-app/appsscript.json | 72 +- ai/standup-chat-app/db.js | 99 ++- ai/standup-chat-app/gemini.js | 38 +- ai/standup-chat-app/main.js | 146 ++-- ai/standup-chat-app/memoize.js | 33 +- apps-script/execute/target.js | 16 +- biome.json | 36 +- chat/advanced-service/appsscript.json | 58 +- chat/quickstart/appsscript.json | 28 +- data-studio/appsscript.json | 45 +- data-studio/appsscript2.json | 22 +- .../AppsScriptFormsAPIWebApp/appsscript.json | 36 +- gmail-sentiment-analysis/.clasp.json | 2 +- gmail-sentiment-analysis/appsscript.json | 48 +- gmail/add-ons/appsscript.json | 37 +- package.json | 62 +- picker/appsscript.json | 34 +- sheets/next18/appsscript.json | 36 +- slides/SpeakerNotesScript/appscript.json | 12 +- solutions/add-on/book-smartchip/.clasp.json | 2 +- solutions/add-on/book-smartchip/Code.js | 96 +-- .../add-on/book-smartchip/appsscript.json | 80 +- solutions/add-on/share-macro/.clasp.json | 2 +- solutions/add-on/share-macro/Code.js | 263 +++---- solutions/add-on/share-macro/UI.js | 402 +++++----- solutions/add-on/share-macro/appsscript.json | 52 +- .../attendance-chat-app/final/appsscript.json | 23 +- .../step-3/appsscript.json | 9 +- .../step-4/appsscript.json | 9 +- .../step-5/appsscript.json | 9 +- .../step-6/appsscript.json | 23 +- .../automations/agenda-maker/.clasp.json | 2 +- solutions/automations/agenda-maker/Code.js | 295 ++++---- .../automations/agenda-maker/appsscript.json | 11 +- .../aggregate-document-content/.clasp.json | 2 +- .../aggregate-document-content/Code.js | 252 +++---- .../aggregate-document-content/Menu.js | 49 +- .../aggregate-document-content/Setup.js | 252 ++++--- .../aggregate-document-content/Utilities.js | 52 +- .../appsscript.json | 11 +- .../automations/bracket-maker/.clasp.json | 2 +- solutions/automations/bracket-maker/Code.js | 182 ++--- .../automations/bracket-maker/appsscript.json | 11 +- .../calendar-timesheet/.clasp.json | 2 +- .../automations/calendar-timesheet/Code.js | 703 ++++++++++-------- .../calendar-timesheet/appsscript.json | 11 +- .../automations/content-signup/.clasp.json | 2 +- solutions/automations/content-signup/Code.js | 151 ++-- .../content-signup/appsscript.json | 26 +- .../course-feedback-response/.clasp.json | 2 +- .../course-feedback-response/Code.js | 123 +-- .../course-feedback-response/appsscript.json | 11 +- .../employee-certificate/.clasp.json | 2 +- .../automations/employee-certificate/Code.js | 193 ++--- .../employee-certificate/appsscript.json | 11 +- .../equipment-requests/.clasp.json | 2 +- .../automations/equipment-requests/Code.js | 243 +++--- .../equipment-requests/appsscript.json | 11 +- .../event-session-signup/.clasp.json | 2 +- .../automations/event-session-signup/Code.js | 251 ++++--- .../event-session-signup/appsscript.json | 11 +- .../feedback-sentiment-analysis/.clasp.json | 2 +- .../appsscript.json | 24 +- .../feedback-sentiment-analysis/code.js | 215 +++--- solutions/automations/folder-creation/Code.js | 20 +- .../folder-creation/appscript.json | 26 +- .../automations/generate-pdfs/.clasp.json | 2 +- solutions/automations/generate-pdfs/Code.js | 412 +++++----- solutions/automations/generate-pdfs/Menu.js | 17 +- .../automations/generate-pdfs/Utilities.js | 53 +- .../automations/generate-pdfs/appsscript.json | 11 +- .../automations/import-csv-sheets/.clasp.json | 2 +- .../automations/import-csv-sheets/Code.js | 285 +++---- .../import-csv-sheets/SampleData.js | 284 ++++--- .../import-csv-sheets/SetupSample.js | 147 ++-- .../import-csv-sheets/Utilities.js | 163 ++-- .../import-csv-sheets/appsscript.json | 11 +- solutions/automations/mail-merge/.clasp.json | 2 +- solutions/automations/mail-merge/Code.js | 377 +++++----- .../automations/mail-merge/appsscript.json | 11 +- .../automations/news-sentiment/.clasp.json | 2 +- solutions/automations/news-sentiment/Code.js | 295 ++++---- .../news-sentiment/appsscript.json | 11 +- .../offsite-activity-signup/.clasp.json | 2 +- .../offsite-activity-signup/Code.js | 532 ++++++------- .../offsite-activity-signup/appsscript.json | 11 +- .../tax-loss-harvest-alerts/.clasp.json | 2 +- .../tax-loss-harvest-alerts/Code.js | 81 +- .../tax-loss-harvest-alerts/appsscript.json | 11 +- solutions/automations/timesheets/.clasp.json | 2 +- solutions/automations/timesheets/Code.js | 325 ++++---- .../automations/timesheets/appsscript.json | 11 +- solutions/automations/upload-files/Code.js | 125 ++-- solutions/automations/upload-files/Setup.js | 123 +-- .../automations/upload-files/appsscript.json | 11 +- .../automations/vacation-calendar/.clasp.json | 2 +- .../automations/vacation-calendar/Code.js | 266 +++---- .../vacation-calendar/appsscript.json | 11 +- .../automations/youtube-tracker/.clasp.json | 2 +- solutions/automations/youtube-tracker/Code.js | 165 ++-- .../youtube-tracker/appsscript.json | 11 +- .../calculate-driving-distance/.clasp.json | 2 +- .../calculate-driving-distance/Code.js | 299 ++++---- .../appsscript.json | 11 +- .../summarize-sheets-data/.clasp.json | 2 +- .../summarize-sheets-data/Code.js | 105 +-- .../summarize-sheets-data/appsscript.json | 11 +- .../custom-functions/tier-pricing/.clasp.json | 2 +- .../custom-functions/tier-pricing/Code.js | 39 +- .../tier-pricing/appsscript.json | 11 +- .../editor-add-on/clean-sheet/.clasp.json | 2 +- solutions/editor-add-on/clean-sheet/Code.js | 376 +++++----- solutions/editor-add-on/clean-sheet/Menu.js | 43 +- .../editor-add-on/clean-sheet/appsscript.json | 11 +- solutions/ooo-assistant/.clasp.json | 2 +- solutions/ooo-assistant/appsscript.json | 81 +- solutions/ooo-chat-app/Code.js | 2 +- solutions/ooo-chat-app/appsscript.json | 38 +- solutions/schedule-meetings/.clasp.json | 2 +- solutions/schedule-meetings/Code.js | 244 +++--- solutions/schedule-meetings/Dialog.js | 365 +++++---- solutions/schedule-meetings/Utilities.js | 87 +-- solutions/schedule-meetings/appsscript.json | 14 +- tasks/simpleTasks/appsscript.json | 20 +- tsconfig.json | 35 +- wasm/hello-world/.clasp.json | 4 +- wasm/hello-world/build.js | 37 +- wasm/hello-world/package.json | 138 ++-- wasm/hello-world/polyfill.js | 4 +- wasm/hello-world/src/appsscript.json | 8 +- wasm/hello-world/src/main.js | 4 +- wasm/hello-world/src/test.js | 101 +-- wasm/hello-world/src/wasm.js | 8 +- wasm/image-add-on/.clasp.json | 4 +- wasm/image-add-on/build.js | 26 +- wasm/image-add-on/package.json | 141 ++-- wasm/image-add-on/polyfill.js | 4 +- wasm/image-add-on/src/add-on.js | 362 ++++----- wasm/image-add-on/src/appsscript.json | 66 +- wasm/image-add-on/src/main.js | 22 +- wasm/image-add-on/src/test.js | 99 +-- wasm/image-add-on/src/wasm.js | 22 +- wasm/python/.clasp.json | 4 +- wasm/python/build.js | 26 +- wasm/python/package.json | 139 ++-- wasm/python/polyfill.js | 4 +- wasm/python/src/appsscript.json | 26 +- wasm/python/src/main.js | 10 +- wasm/python/src/test.js | 10 +- wasm/python/src/wasm.js | 10 +- 180 files changed, 7707 insertions(+), 7269 deletions(-) diff --git a/.gemini/settings.json b/.gemini/settings.json index ec18cbfba..500f4c4a6 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -14,4 +14,4 @@ "run_shell_command(pnpm test)" ] } -} \ No newline at end of file +} diff --git a/.github/linters/.htmlhintrc b/.github/linters/.htmlhintrc index 70391a462..78fe47573 100644 --- a/.github/linters/.htmlhintrc +++ b/.github/linters/.htmlhintrc @@ -1,25 +1,25 @@ { - "tagname-lowercase": true, - "attr-lowercase": true, - "attr-value-double-quotes": true, - "attr-value-not-empty": false, - "attr-no-duplication": true, - "doctype-first": false, - "tag-pair": true, - "tag-self-close": false, - "spec-char-escape": false, - "id-unique": true, - "src-not-empty": true, - "title-require": false, - "alt-require": true, - "doctype-html5": true, - "id-class-value": false, - "style-disabled": false, - "inline-style-disabled": false, - "inline-script-disabled": false, - "space-tab-mixed-disabled": "space", - "id-class-ad-disabled": false, - "href-abs-or-rel": false, - "attr-unsafe-chars": true, - "head-script-disabled": false + "tagname-lowercase": true, + "attr-lowercase": true, + "attr-value-double-quotes": true, + "attr-value-not-empty": false, + "attr-no-duplication": true, + "doctype-first": false, + "tag-pair": true, + "tag-self-close": false, + "spec-char-escape": false, + "id-unique": true, + "src-not-empty": true, + "title-require": false, + "alt-require": true, + "doctype-html5": true, + "id-class-value": false, + "style-disabled": false, + "inline-style-disabled": false, + "inline-script-disabled": false, + "space-tab-mixed-disabled": "space", + "id-class-ad-disabled": false, + "href-abs-or-rel": false, + "attr-unsafe-chars": true, + "head-script-disabled": false } diff --git a/.github/scripts/check-gs.ts b/.github/scripts/check-gs.ts index 2f2332e65..3afc61f33 100644 --- a/.github/scripts/check-gs.ts +++ b/.github/scripts/check-gs.ts @@ -16,226 +16,241 @@ /// +import { exec } from "child_process"; import { - readdirSync, - statSync, - existsSync, - rmSync, - mkdirSync, - copyFileSync, - writeFileSync -} from 'fs'; -import {join, relative, dirname, resolve, sep} from 'path'; -import {exec} from 'child_process'; -import {promisify} from 'util'; + copyFileSync, + existsSync, + mkdirSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "fs"; +import { dirname, join, relative, resolve, sep } from "path"; +import { promisify } from "util"; const execAsync = promisify(exec); -const TEMP_ROOT = '.tsc_check'; +const TEMP_ROOT = ".tsc_check"; interface Project { - files: string[]; - name: string; - path: string; + files: string[]; + name: string; + path: string; } interface CheckResult { - name: string; - success: boolean; - output: string; + name: string; + success: boolean; + output: string; } // Helper to recursively find all files with a specific extension -function findFiles(dir: string, extension: string, fileList: string[] = []): string[] { - const files = readdirSync(dir); - for (const file of files) { - if (file.endsWith('.js')) continue; - const filePath = join(dir, file); - const stat = statSync(filePath); - if (stat.isDirectory()) { - if (file !== 'node_modules' && file !== '.git' && file !== TEMP_ROOT) { - findFiles(filePath, extension, fileList); - } - } else if (file.endsWith(extension)) { - fileList.push(filePath); - } - } - return fileList; +function findFiles( + dir: string, + extension: string, + fileList: string[] = [], +): string[] { + const files = readdirSync(dir); + for (const file of files) { + if (file.endsWith(".js")) continue; + const filePath = join(dir, file); + const stat = statSync(filePath); + if (stat.isDirectory()) { + if (file !== "node_modules" && file !== ".git" && file !== TEMP_ROOT) { + findFiles(filePath, extension, fileList); + } + } else if (file.endsWith(extension)) { + fileList.push(filePath); + } + } + return fileList; } // Find all directories containing appsscript.json function findProjectRoots(rootDir: string): string[] { - return findFiles(rootDir, 'appsscript.json').map((f) => dirname(f)); + return findFiles(rootDir, "appsscript.json").map((f) => dirname(f)); } -function createProjects(rootDir: string, projectRoots: string[], allGsFiles: string[]): Project[] { - // Holds files that belong to a formal Apps Script project (defined by the presence of appsscript.json). - const projectGroups = new Map(); - - // Holds "orphan" files that do not belong to any defined Apps Script project (no appsscript.json found). - const looseGroups = new Map(); - - // Initialize project groups - for (const p of projectRoots) { - projectGroups.set(p, []); - } - - for (const file of allGsFiles) { - let assigned = false; - let currentDir = dirname(file); - - while (currentDir.startsWith(rootDir) && currentDir !== rootDir) { - if (projectGroups.has(currentDir)) { - projectGroups.get(currentDir)?.push(file); - assigned = true; - break; - } - currentDir = dirname(currentDir); - } - - if (!assigned) { - const dir = dirname(file); - if (!looseGroups.has(dir)) { - looseGroups.set(dir, []); - } - looseGroups.get(dir)?.push(file); - } - } - - const projects: Project[] = []; - projectGroups.forEach((files, dir) => { - if (files.length > 0) { - projects.push({ - files, - name: `Project: ${relative(rootDir, dir)}`, - path: relative(rootDir, dir) - }); - } - }); - looseGroups.forEach((files, dir) => { - if (files.length > 0) { - projects.push({ - files, - name: `Loose Project: ${relative(rootDir, dir)}`, - path: relative(rootDir, dir) - }); - } - }); - - return projects; +function createProjects( + rootDir: string, + projectRoots: string[], + allGsFiles: string[], +): Project[] { + // Holds files that belong to a formal Apps Script project (defined by the presence of appsscript.json). + const projectGroups = new Map(); + + // Holds "orphan" files that do not belong to any defined Apps Script project (no appsscript.json found). + const looseGroups = new Map(); + + // Initialize project groups + for (const p of projectRoots) { + projectGroups.set(p, []); + } + + for (const file of allGsFiles) { + let assigned = false; + let currentDir = dirname(file); + + while (currentDir.startsWith(rootDir) && currentDir !== rootDir) { + if (projectGroups.has(currentDir)) { + projectGroups.get(currentDir)?.push(file); + assigned = true; + break; + } + currentDir = dirname(currentDir); + } + + if (!assigned) { + const dir = dirname(file); + if (!looseGroups.has(dir)) { + looseGroups.set(dir, []); + } + looseGroups.get(dir)?.push(file); + } + } + + const projects: Project[] = []; + projectGroups.forEach((files, dir) => { + if (files.length > 0) { + projects.push({ + files, + name: `Project: ${relative(rootDir, dir)}`, + path: relative(rootDir, dir), + }); + } + }); + looseGroups.forEach((files, dir) => { + if (files.length > 0) { + projects.push({ + files, + name: `Loose Project: ${relative(rootDir, dir)}`, + path: relative(rootDir, dir), + }); + } + }); + + return projects; } -async function checkProject(project: Project, rootDir: string): Promise { - const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, '_'); - const projectTempDir = join(TEMP_ROOT, projectNameSafe); - - // Synchronous setup is fine as it's fast and avoids race conditions on mkdir if we were sharing dirs (we aren't) - mkdirSync(projectTempDir, {recursive: true}); - - for (const file of project.files) { - const fileRelPath = relative(rootDir, file); - const destPath = join(projectTempDir, fileRelPath.replace(/\.gs$/, '.js')); - const destDir = dirname(destPath); - mkdirSync(destDir, {recursive: true}); - copyFileSync(file, destPath); - } - - const tsConfig = { - extends: '../../tsconfig.json', - compilerOptions: { - noEmit: true, - allowJs: true, - checkJs: true, - typeRoots: [resolve(rootDir, 'node_modules/@types')] - }, - include: ['**/*.js'] - }; - - writeFileSync( - join(projectTempDir, 'tsconfig.json'), - JSON.stringify(tsConfig, null, 2) - ); - - try { - await execAsync(`tsc -p "${projectTempDir}"`, {cwd: rootDir}); - return {name: project.name, success: true, output: ''}; - } catch (e: any) { - const rawOutput = (e.stdout || '') + (e.stderr || ''); - - const rewritten = rawOutput.split('\n').map((line: string) => { - if (line.includes(projectTempDir)) { - let newLine = line.split(projectTempDir + sep).pop(); - if (!newLine) { - return line; - } - newLine = newLine.replace(/\.js(:|\()/g, '.gs$1'); - return newLine; - } - return line; - }).join('\n'); - - return {name: project.name, success: false, output: rewritten}; - } +async function checkProject( + project: Project, + rootDir: string, +): Promise { + const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, "_"); + const projectTempDir = join(TEMP_ROOT, projectNameSafe); + + // Synchronous setup is fine as it's fast and avoids race conditions on mkdir if we were sharing dirs (we aren't) + mkdirSync(projectTempDir, { recursive: true }); + + for (const file of project.files) { + const fileRelPath = relative(rootDir, file); + const destPath = join(projectTempDir, fileRelPath.replace(/\.gs$/, ".js")); + const destDir = dirname(destPath); + mkdirSync(destDir, { recursive: true }); + copyFileSync(file, destPath); + } + + const tsConfig = { + extends: "../../tsconfig.json", + compilerOptions: { + noEmit: true, + allowJs: true, + checkJs: true, + typeRoots: [resolve(rootDir, "node_modules/@types")], + }, + include: ["**/*.js"], + }; + + writeFileSync( + join(projectTempDir, "tsconfig.json"), + JSON.stringify(tsConfig, null, 2), + ); + + try { + await execAsync(`tsc -p "${projectTempDir}"`, { cwd: rootDir }); + return { name: project.name, success: true, output: "" }; + } catch (e: any) { + const rawOutput = (e.stdout || "") + (e.stderr || ""); + + const rewritten = rawOutput + .split("\n") + .map((line: string) => { + if (line.includes(projectTempDir)) { + let newLine = line.split(projectTempDir + sep).pop(); + if (!newLine) { + return line; + } + newLine = newLine.replace(/\.js(:|\()/g, ".gs$1"); + return newLine; + } + return line; + }) + .join("\n"); + + return { name: project.name, success: false, output: rewritten }; + } } async function main() { - try { - const rootDir = resolve('.'); - const args = process.argv.slice(2); - const searchArg = args.find(arg => arg !== '--'); - - // 1. Discovery - const projectRoots = findProjectRoots(rootDir); - const allGsFiles = findFiles(rootDir, '.gs'); - - // 2. Grouping - const projects = createProjects(rootDir, projectRoots, allGsFiles); - - // 3. Filtering - const projectsToCheck = projects.filter(p => { - return !searchArg || p.path.startsWith(searchArg); - }); - - if (projectsToCheck.length === 0) { - console.log('No projects found matching the search path.'); - return; - } - - // 4. Setup - if (existsSync(TEMP_ROOT)) { - rmSync(TEMP_ROOT, {recursive: true, force: true}); - } - mkdirSync(TEMP_ROOT); - - console.log(`Checking ${projectsToCheck.length} projects in parallel...`); - - // 5. Parallel Execution - const results = await Promise.all(projectsToCheck.map(p => checkProject(p, rootDir))); - - // 6. Reporting - let hasError = false; - for (const result of results) { - if (!result.success) { - hasError = true; - console.log(`\n--- Failed: ${result.name} ---`); - console.log(result.output); - } - } - - if (hasError) { - console.error('\nOne or more checks failed.'); - process.exit(1); - } else { - console.log('\nAll checks passed.'); - } - - } catch (err) { - console.error('Unexpected error:', err); - process.exit(1); - } finally { - if (existsSync(TEMP_ROOT)) { - rmSync(TEMP_ROOT, {recursive: true, force: true}); - } - } + try { + const rootDir = resolve("."); + const args = process.argv.slice(2); + const searchArg = args.find((arg) => arg !== "--"); + + // 1. Discovery + const projectRoots = findProjectRoots(rootDir); + const allGsFiles = findFiles(rootDir, ".gs"); + + // 2. Grouping + const projects = createProjects(rootDir, projectRoots, allGsFiles); + + // 3. Filtering + const projectsToCheck = projects.filter((p) => { + return !searchArg || p.path.startsWith(searchArg); + }); + + if (projectsToCheck.length === 0) { + console.log("No projects found matching the search path."); + return; + } + + // 4. Setup + if (existsSync(TEMP_ROOT)) { + rmSync(TEMP_ROOT, { recursive: true, force: true }); + } + mkdirSync(TEMP_ROOT); + + console.log(`Checking ${projectsToCheck.length} projects in parallel...`); + + // 5. Parallel Execution + const results = await Promise.all( + projectsToCheck.map((p) => checkProject(p, rootDir)), + ); + + // 6. Reporting + let hasError = false; + for (const result of results) { + if (!result.success) { + hasError = true; + console.log(`\n--- Failed: ${result.name} ---`); + console.log(result.output); + } + } + + if (hasError) { + console.error("\nOne or more checks failed."); + process.exit(1); + } else { + console.log("\nAll checks passed."); + } + } catch (err) { + console.error("Unexpected error:", err); + process.exit(1); + } finally { + if (existsSync(TEMP_ROOT)) { + rmSync(TEMP_ROOT, { recursive: true, force: true }); + } + } } main(); diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4a9deaa43..7a3cec339 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,3 @@ { - "recommendations": [ - "google-workspace.google-workspace-developer-tools" - ] -} \ No newline at end of file + "recommendations": ["google-workspace.google-workspace-developer-tools"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 4dd808d53..4c76c318a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "files.associations": { - "*.gs": "javascript" - } -} \ No newline at end of file + "files.associations": { + "*.gs": "javascript" + } +} diff --git a/ai/autosummarize/appsscript.json b/ai/autosummarize/appsscript.json index 5aed4b13d..a4f3c4538 100644 --- a/ai/autosummarize/appsscript.json +++ b/ai/autosummarize/appsscript.json @@ -1,22 +1,22 @@ { - "timeZone": "America/Denver", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "43", - "developmentMode": false - } - ], - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v3", - "serviceId": "drive" - } - ] - } -} \ No newline at end of file + "timeZone": "America/Denver", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43", + "developmentMode": false + } + ], + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + } +} diff --git a/ai/autosummarize/gemini.js b/ai/autosummarize/gemini.js index 35bbb65bd..aecf83589 100644 --- a/ai/autosummarize/gemini.js +++ b/ai/autosummarize/gemini.js @@ -15,18 +15,20 @@ limitations under the License. */ function scriptPropertyWithDefault(key, defaultValue = undefined) { - const scriptProperties = PropertiesService.getScriptProperties(); - const value = scriptProperties.getProperty(key); - if (value) { - return value; - } - return defaultValue; + const scriptProperties = PropertiesService.getScriptProperties(); + const value = scriptProperties.getProperty(key); + if (value) { + return value; + } + return defaultValue; } -const VERTEX_AI_LOCATION = scriptPropertyWithDefault('project_location', 'us-central1'); -const MODEL_ID = scriptPropertyWithDefault('model_id', 'gemini-pro-vision'); -const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault('service_account_key'); - +const VERTEX_AI_LOCATION = scriptPropertyWithDefault( + "project_location", + "us-central1", +); +const MODEL_ID = scriptPropertyWithDefault("model_id", "gemini-pro-vision"); +const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault("service_account_key"); /** * Packages prompt and necessary settings, then sends a request to @@ -38,75 +40,79 @@ const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault('service_account_key'); * @param {string} options.tokens The number of tokens to limit to the prompt. */ function getAiSummary(parts, options = {}) { - options = Object.assign({}, { temperature: 0.1, tokens: 8192}, options ?? {}) - const request = { - "contents": [ - { - "role": "user", - "parts": parts, - } - ], - "generationConfig": { - "temperature": options.temperature, - "topK": 1, - "topP": 1, - "maxOutputTokens": options.tokens, - "stopSequences": [] - }, - } + options = Object.assign( + {}, + { temperature: 0.1, tokens: 8192 }, + options ?? {}, + ); + const request = { + contents: [ + { + role: "user", + parts: parts, + }, + ], + generationConfig: { + temperature: options.temperature, + topK: 1, + topP: 1, + maxOutputTokens: options.tokens, + stopSequences: [], + }, + }; + + const credentials = credentialsForVertexAI(); - const credentials = credentialsForVertexAI(); + const fetchOptions = { + method: "POST", + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify(request), + }; - const fetchOptions = { - method: 'POST', - headers: { - 'Authorization': `Bearer ${credentials.accessToken}` - }, - contentType: 'application/json', - muteHttpExceptions: true, - payload: JSON.stringify(request) - } + const url = + `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + + `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; + const response = UrlFetchApp.fetch(url, fetchOptions); - const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + - `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent` - const response = UrlFetchApp.fetch(url, fetchOptions); + const responseCode = response.getResponseCode(); + if (responseCode >= 400) { + throw new Error(`Unable to process file: Error code ${responseCode}`); + } - - const responseCode = response.getResponseCode(); - if (responseCode >= 400) { - throw new Error(`Unable to process file: Error code ${responseCode}`); - } - - const responseText = response.getContentText(); - const parsedResponse = JSON.parse(responseText); - if (parsedResponse.error) { - throw new Error(parsedResponse.error.message); - } - const text = parsedResponse.candidates[0].content.parts[0].text - return text + const responseText = response.getContentText(); + const parsedResponse = JSON.parse(responseText); + if (parsedResponse.error) { + throw new Error(parsedResponse.error.message); + } + const text = parsedResponse.candidates[0].content.parts[0].text; + return text; } /** * Gets credentials required to call Vertex API using a Service Account. * Requires use of Service Account Key stored with project - * + * * @return {!Object} Containing the Cloud Project Id and the access token. */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } - const parsedCredentials = JSON.parse(credentials); - const service = OAuth2.createService("Vertex") - .setTokenUrl('https://oauth2.googleapis.com/token') - .setPrivateKey(parsedCredentials['private_key']) - .setIssuer(parsedCredentials['client_email']) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials['project_id'], - accessToken: service.getAccessToken(), - } + const parsedCredentials = JSON.parse(credentials); + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials["private_key"]) + .setIssuer(parsedCredentials["client_email"]) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials["project_id"], + accessToken: service.getAccessToken(), + }; } diff --git a/ai/autosummarize/main.js b/ai/autosummarize/main.js index c6ebe7b39..c61db3175 100644 --- a/ai/autosummarize/main.js +++ b/ai/autosummarize/main.js @@ -20,116 +20,145 @@ limitations under the License. * @param {object} e The event parameter for a simple onOpen trigger. */ function onOpen(e) { - SpreadsheetApp.getUi().createAddonMenu() - .addItem('📄 Open AutoSummarize AI', 'showSidebar') - .addSeparator() - .addItem('❎ Quick summary', 'doAutoSummarizeAI') - .addItem('❌ Remove all summaries', 'removeAllSummaries') - .addToUi(); + SpreadsheetApp.getUi() + .createAddonMenu() + .addItem("📄 Open AutoSummarize AI", "showSidebar") + .addSeparator() + .addItem("❎ Quick summary", "doAutoSummarizeAI") + .addItem("❌ Remove all summaries", "removeAllSummaries") + .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 + * 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. + * @param {object} e The event parameter for a simple onInstall trigger. */ function onInstall(e) { - onOpen(e); + onOpen(e); } /** * Opens sidebar in Sheets with AutoSummarize AI interface. */ function showSidebar() { - const ui = HtmlService.createHtmlOutputFromFile('sidebar') - .setTitle('AutoSummarize AI'); - SpreadsheetApp.getUi().showSidebar(ui); + const ui = + HtmlService.createHtmlOutputFromFile("sidebar").setTitle( + "AutoSummarize AI", + ); + SpreadsheetApp.getUi().showSidebar(ui); } - /** - * Deletes all of the AutoSummarize AI created sheets + * Deletes all of the AutoSummarize AI created sheets * i.e. any sheets with prefix of 'AutoSummarize AI' */ function removeAllSummaries() { - const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); - const allSheets = spreadsheet.getSheets(); + const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); + const allSheets = spreadsheet.getSheets(); - allSheets.forEach(function (sheet) { - const sheetName = sheet.getName(); - // Check if the sheet name starts with "AutoSummarize AI" - if (sheetName.startsWith("AutoSummarize AI")) { - spreadsheet.deleteSheet(sheet) - } - }); + allSheets.forEach((sheet) => { + const sheetName = sheet.getName(); + // Check if the sheet name starts with "AutoSummarize AI" + if (sheetName.startsWith("AutoSummarize AI")) { + spreadsheet.deleteSheet(sheet); + } + }); } /** * Wrapper function for add-on. */ -function doAutoSummarizeAI(customPrompt1, customPrompt2, temperature = .1, tokens = 2048) { - // Get selected cell values. - console.log("Getting selection..."); - let selection = SpreadsheetApp.getSelection() - .getActiveRange() - .getRichTextValues() - .map(value => { - if (value[0].getLinkUrl()) { - return value[0].getLinkUrl(); - } - return value[0].getText(); - }); +function doAutoSummarizeAI( + customPrompt1, + customPrompt2, + temperature = 0.1, + tokens = 2048, +) { + // Get selected cell values. + console.log("Getting selection..."); + const selection = SpreadsheetApp.getSelection() + .getActiveRange() + .getRichTextValues() + .map((value) => { + if (value[0].getLinkUrl()) { + return value[0].getLinkUrl(); + } + return value[0].getText(); + }); + + // Get AI summary + const data = summarizeFiles( + selection, + customPrompt1, + customPrompt2, + temperature, + tokens, + ); - // Get AI summary - const data = summarizeFiles(selection, customPrompt1, customPrompt2, temperature, tokens); - - // Add and format a new new sheet. - const now = new Date(); - const nowFormatted = Utilities.formatDate(now, now.getTimezoneOffset().toString(), "MM/dd HH:mm"); - let sheetName = `AutoSummarize AI (${nowFormatted})`; - if (SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)) { - sheetName = `AutoSummarize AI (${nowFormatted}:${now.getSeconds()})`; - } - let aiSheet = SpreadsheetApp.getActiveSpreadsheet() - .insertSheet() - .setName(sheetName); - let aiSheetHeaderStyle = SpreadsheetApp.newTextStyle() - .setFontSize(12) - .setBold(true) - .setFontFamily("Google Sans") - .setForegroundColor("#ffffff") - .build(); - let aiSheetValuesStyle = SpreadsheetApp.newTextStyle() - .setFontSize(10) - .setBold(false) - .setFontFamily("Google Sans") - .setForegroundColor("#000000") - .build(); - aiSheet.getRange("A1:E1") - .setBackground("#434343") - .setTextStyle(aiSheetHeaderStyle) - .setValues([["Link", "Title",`Summary from Gemini AI [Temperature: ${temperature}]`, `Custom Prompt #1: ${customPrompt1}`, `Custom Prompt #2: ${customPrompt2}`]]) - .setWrap(true); - aiSheet.setColumnWidths(1, 1, 100); - aiSheet.setColumnWidths(2, 1, 300); - aiSheet.setColumnWidths(3, 3, 300); + // Add and format a new new sheet. + const now = new Date(); + const nowFormatted = Utilities.formatDate( + now, + now.getTimezoneOffset().toString(), + "MM/dd HH:mm", + ); + let sheetName = `AutoSummarize AI (${nowFormatted})`; + if (SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)) { + sheetName = `AutoSummarize AI (${nowFormatted}:${now.getSeconds()})`; + } + const aiSheet = SpreadsheetApp.getActiveSpreadsheet() + .insertSheet() + .setName(sheetName); + const aiSheetHeaderStyle = SpreadsheetApp.newTextStyle() + .setFontSize(12) + .setBold(true) + .setFontFamily("Google Sans") + .setForegroundColor("#ffffff") + .build(); + const aiSheetValuesStyle = SpreadsheetApp.newTextStyle() + .setFontSize(10) + .setBold(false) + .setFontFamily("Google Sans") + .setForegroundColor("#000000") + .build(); + aiSheet + .getRange("A1:E1") + .setBackground("#434343") + .setTextStyle(aiSheetHeaderStyle) + .setValues([ + [ + "Link", + "Title", + `Summary from Gemini AI [Temperature: ${temperature}]`, + `Custom Prompt #1: ${customPrompt1}`, + `Custom Prompt #2: ${customPrompt2}`, + ], + ]) + .setWrap(true); + aiSheet.setColumnWidths(1, 1, 100); + aiSheet.setColumnWidths(2, 1, 300); + aiSheet.setColumnWidths(3, 3, 300); - // Copy results - aiSheet - .getRange(`A2:E${data.length + 1}`) - .setValues(data); + // Copy results + aiSheet.getRange(`A2:E${data.length + 1}`).setValues(data); - aiSheet.getRange(`A2:E${data.length + 1}`) - .setBackground("#ffffff") - .setTextStyle(aiSheetValuesStyle) - .setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP) - .setVerticalAlignment("top"); - aiSheet.getRange(`C2:E${data.length + 1}`) - .setBackground("#efefef") - .setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP); + aiSheet + .getRange(`A2:E${data.length + 1}`) + .setBackground("#ffffff") + .setTextStyle(aiSheetValuesStyle) + .setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP) + .setVerticalAlignment("top"); + aiSheet + .getRange(`C2:E${data.length + 1}`) + .setBackground("#efefef") + .setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP); - aiSheet.deleteColumns(8, 19); - aiSheet.deleteRows(aiSheet.getLastRow() + 1, aiSheet.getMaxRows() - aiSheet.getLastRow()); + aiSheet.deleteColumns(8, 19); + aiSheet.deleteRows( + aiSheet.getLastRow() + 1, + aiSheet.getMaxRows() - aiSheet.getLastRow(), + ); } diff --git a/ai/autosummarize/summarize.js b/ai/autosummarize/summarize.js index fae7101eb..1dfe67bed 100644 --- a/ai/autosummarize/summarize.js +++ b/ai/autosummarize/summarize.js @@ -16,130 +16,160 @@ limitations under the License. /** * Exports a Google Doc/Sheet/Slide to the requested format. - * + * * @param {string} fileId - ID of file to export * @param {string} targetType - MIME type to export as * @return Base64 encoded file content */ function exportFile(fileId, targetType = "application/pdf") { - const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(targetType)}&supportsAllDrives=true`; + const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(targetType)}&supportsAllDrives=true`; - const requestOptions = { - headers: { - Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, - }, - }; - const response = UrlFetchApp.fetch(exportUrl, requestOptions); - const blob = response.getBlob(); + const requestOptions = { + headers: { + Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, + }, + }; + const response = UrlFetchApp.fetch(exportUrl, requestOptions); + const blob = response.getBlob(); - return Utilities.base64Encode(blob.getBytes()); + return Utilities.base64Encode(blob.getBytes()); } /** * Downloads a binary file from Drive. - * + * * @param {string} fileId - ID of file to export * @param {string} targetType - MIME type to export as * @return Base64 encoded file content */ function downloadFile(fileId) { - const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&supportsAllDrives=true`; + const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&supportsAllDrives=true`; - const requestOptions = { - headers: { - Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, - }, - }; - const response = UrlFetchApp.fetch(exportUrl, requestOptions); - const blob = response.getBlob(); + const requestOptions = { + headers: { + Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, + }, + }; + const response = UrlFetchApp.fetch(exportUrl, requestOptions); + const blob = response.getBlob(); - return Utilities.base64Encode(blob.getBytes()); + return Utilities.base64Encode(blob.getBytes()); } /** * Main function for AutoSummarize AI process. */ -function summarizeFiles(sourceSheetLinks, customPrompt1, customPrompt2, temperature, tokens) { - return sourceSheetLinks.map(function (fileUrl) { - console.log("Processing:", fileUrl); - - let fileName = ""; - let summary = ""; - let customPrompt1Response = ""; - let customPrompt2Response = ""; - - if (!fileUrl) { - return ["", fileName, summary, customPrompt1Response, customPrompt2Response]; - } - try { - const promptParts = [ - { - text: 'Summarize the following document.', - }, - { - text: 'Return your response as a single paragraph. Reformat any lists as part of the paragraph. Output only the single paragraph as plain text. Do not use more than 3 sentences. Do not use markdown.' - } - ] - let fileIdMatchPattern = new RegExp("/d/(.*?)/", "gi"); - let fileId = fileIdMatchPattern.exec(fileUrl)[1]; - - // Get file title and type. - let currentFile = Drive.Files.get(fileId, { "supportsAllDrives": true }); - let fileMimeType = currentFile.mimeType; - fileName = currentFile.name; - - console.log(`Processing ${fileName} (ID: ${fileId})...`); - - // Add file content to the prompt - switch(fileMimeType) { - case "application/vnd.google-apps.presentation": - case "application/vnd.google-apps.document": - case "application/vnd.google-apps.spreadsheet": - promptParts.push({ - inlineData: { - mimeType: 'application/pdf', - data: exportFile(fileId, 'application/pdf'), - } - }) - break; - case "application/pdf": - case "image/gif": - case "image/jpeg": - case "image/png": - promptParts.push({ - inlineData: { - mimeType: fileMimeType, - data: downloadFile(fileId), - } - }) - break; - default: - console.log(`Unsupported file type: ${fileMimeType}`); - return [fileUrl, fileName, summary, customPrompt1Response, customPrompt2Response]; - } - - // Prompt for summary - let geminiOptions = { - temperature, - tokens, - }; - summary = getAiSummary(promptParts, geminiOptions); - - // If any custom prompts, request those too - if (customPrompt1) { - promptParts[0].text = customPrompt1; - customPrompt1Response = getAiSummary(promptParts, geminiOptions); - } - if (customPrompt2) { - promptParts[0].text = customPrompt2; - customPrompt2Response = getAiSummary(promptParts, geminiOptions); - } - - return [fileUrl, fileName, summary, customPrompt1Response, customPrompt2Response]; - } catch (e) { - // Add error row values if anything else goes wrong. - console.log(e); - return [fileUrl, fileName, "Something went wrong. Make sure you have access to this row's link.", "", ""]; - } - }); -} \ No newline at end of file +function summarizeFiles( + sourceSheetLinks, + customPrompt1, + customPrompt2, + temperature, + tokens, +) { + return sourceSheetLinks.map((fileUrl) => { + console.log("Processing:", fileUrl); + + let fileName = ""; + let summary = ""; + let customPrompt1Response = ""; + let customPrompt2Response = ""; + + if (!fileUrl) { + return [ + "", + fileName, + summary, + customPrompt1Response, + customPrompt2Response, + ]; + } + try { + const promptParts = [ + { + text: "Summarize the following document.", + }, + { + text: "Return your response as a single paragraph. Reformat any lists as part of the paragraph. Output only the single paragraph as plain text. Do not use more than 3 sentences. Do not use markdown.", + }, + ]; + const fileIdMatchPattern = /\/d\/(.*?)\//gi; + const fileId = fileIdMatchPattern.exec(fileUrl)[1]; + + // Get file title and type. + const currentFile = Drive.Files.get(fileId, { supportsAllDrives: true }); + const fileMimeType = currentFile.mimeType; + fileName = currentFile.name; + + console.log(`Processing ${fileName} (ID: ${fileId})...`); + + // Add file content to the prompt + switch (fileMimeType) { + case "application/vnd.google-apps.presentation": + case "application/vnd.google-apps.document": + case "application/vnd.google-apps.spreadsheet": + promptParts.push({ + inlineData: { + mimeType: "application/pdf", + data: exportFile(fileId, "application/pdf"), + }, + }); + break; + case "application/pdf": + case "image/gif": + case "image/jpeg": + case "image/png": + promptParts.push({ + inlineData: { + mimeType: fileMimeType, + data: downloadFile(fileId), + }, + }); + break; + default: + console.log(`Unsupported file type: ${fileMimeType}`); + return [ + fileUrl, + fileName, + summary, + customPrompt1Response, + customPrompt2Response, + ]; + } + + // Prompt for summary + const geminiOptions = { + temperature, + tokens, + }; + summary = getAiSummary(promptParts, geminiOptions); + + // If any custom prompts, request those too + if (customPrompt1) { + promptParts[0].text = customPrompt1; + customPrompt1Response = getAiSummary(promptParts, geminiOptions); + } + if (customPrompt2) { + promptParts[0].text = customPrompt2; + customPrompt2Response = getAiSummary(promptParts, geminiOptions); + } + + return [ + fileUrl, + fileName, + summary, + customPrompt1Response, + customPrompt2Response, + ]; + } catch (e) { + // Add error row values if anything else goes wrong. + console.log(e); + return [ + fileUrl, + fileName, + "Something went wrong. Make sure you have access to this row's link.", + "", + "", + ]; + } + }); +} diff --git a/ai/custom-func-ai-agent/AiVertex.js b/ai/custom-func-ai-agent/AiVertex.js index aa58d0836..286896d7d 100644 --- a/ai/custom-func-ai-agent/AiVertex.js +++ b/ai/custom-func-ai-agent/AiVertex.js @@ -14,10 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -const LOCATION = PropertiesService.getScriptProperties().getProperty('LOCATION'); -const GEMINI_MODEL_ID = PropertiesService.getScriptProperties().getProperty('GEMINI_MODEL_ID'); -const REASONING_ENGINE_ID = PropertiesService.getScriptProperties().getProperty('REASONING_ENGINE_ID'); -const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty('SERVICE_ACCOUNT_KEY'); +const LOCATION = + PropertiesService.getScriptProperties().getProperty("LOCATION"); +const GEMINI_MODEL_ID = + PropertiesService.getScriptProperties().getProperty("GEMINI_MODEL_ID"); +const REASONING_ENGINE_ID = PropertiesService.getScriptProperties().getProperty( + "REASONING_ENGINE_ID", +); +const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty( + "SERVICE_ACCOUNT_KEY", +); const credentials = credentialsForVertexAI(); @@ -25,87 +31,89 @@ const credentials = credentialsForVertexAI(); * @param {string} statement The statement to fact-check. */ function requestLlmAuditorAdkAiAgent(statement) { - return UrlFetchApp.fetch( - `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/reasoningEngines/${REASONING_ENGINE_ID}:streamQuery?alt=sse`, - { - method: 'post', - headers: { 'Authorization': `Bearer ${credentials.accessToken}` }, - contentType: 'application/json', - muteHttpExceptions: true, - payload: JSON.stringify({ - "class_method": "async_stream_query", - "input": { - "user_id": "google_sheets_custom_function_fact_check", - "message": statement, - } - }) - } - ).getContentText(); + return UrlFetchApp.fetch( + `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/reasoningEngines/${REASONING_ENGINE_ID}:streamQuery?alt=sse`, + { + method: "post", + headers: { Authorization: `Bearer ${credentials.accessToken}` }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify({ + class_method: "async_stream_query", + input: { + user_id: "google_sheets_custom_function_fact_check", + message: statement, + }, + }), + }, + ).getContentText(); } /** * @param {string} prompt The Gemini prompt to use. */ function requestOutputFormatting(prompt) { - const response = UrlFetchApp.fetch( - `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/publishers/google/models/${GEMINI_MODEL_ID}:generateContent`, - { - method: 'post', - headers: { 'Authorization': `Bearer ${credentials.accessToken}` }, - contentType: 'application/json', - muteHttpExceptions: true, - payload: JSON.stringify({ - "contents": [{ - "role": "user", - "parts": [{ "text": prompt }] - }], - "generationConfig": { "temperature": 0.1, "maxOutputTokens": 2048 }, - "safetySettings": [ - { - "category": "HARM_CATEGORY_HARASSMENT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "threshold": "BLOCK_NONE" - } - ] - }) - } - ); - return JSON.parse(response).candidates[0].content.parts[0].text + const response = UrlFetchApp.fetch( + `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/publishers/google/models/${GEMINI_MODEL_ID}:generateContent`, + { + method: "post", + headers: { Authorization: `Bearer ${credentials.accessToken}` }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify({ + contents: [ + { + role: "user", + parts: [{ text: prompt }], + }, + ], + generationConfig: { temperature: 0.1, maxOutputTokens: 2048 }, + safetySettings: [ + { + category: "HARM_CATEGORY_HARASSMENT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_HATE_SPEECH", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_DANGEROUS_CONTENT", + threshold: "BLOCK_NONE", + }, + ], + }), + }, + ); + return JSON.parse(response).candidates[0].content.parts[0].text; } /** * Gets credentials required to call Vertex API using a Service Account. * Requires use of Service Account Key stored with project. - * + * * @return {!Object} Containing the Google Cloud project ID and the access token. */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } - const parsedCredentials = JSON.parse(credentials); + const parsedCredentials = JSON.parse(credentials); - const service = OAuth2.createService("Vertex") - .setTokenUrl('https://oauth2.googleapis.com/token') - .setPrivateKey(parsedCredentials['private_key']) - .setIssuer(parsedCredentials['client_email']) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials['project_id'], - accessToken: service.getAccessToken(), - } + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials["private_key"]) + .setIssuer(parsedCredentials["client_email"]) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials["project_id"], + accessToken: service.getAccessToken(), + }; } diff --git a/ai/custom-func-ai-agent/Code.js b/ai/custom-func-ai-agent/Code.js index d18674f6a..ba4fa90dc 100644 --- a/ai/custom-func-ai-agent/Code.js +++ b/ai/custom-func-ai-agent/Code.js @@ -16,21 +16,24 @@ limitations under the License. /** * Passes a statement to fact-check and, optionally, output formatting instructions. - * + * * @param {string} statement The statement to fact-check as a string or single cell * reference (data ranges are not supported). * @param {string} outputFormat The instructions as a string or single cell reference * (data ranges are not supported). - * + * * @return The generated and formatted verdict * @customfunction */ function FACT_CHECK(statement, outputFormat) { - if (!outputFormat || outputFormat == "") { - outputFormat = 'Summarize it. Only keep the verdict result and main arguments. ' - + 'Do not reiterate the fact being checked. Remove all markdown. ' - + 'State the verdit result in a first paragraph in a few words and the rest of the summary in a second paragraph.'; - } + if (!outputFormat || outputFormat == "") { + outputFormat = + "Summarize it. Only keep the verdict result and main arguments. " + + "Do not reiterate the fact being checked. Remove all markdown. " + + "State the verdit result in a first paragraph in a few words and the rest of the summary in a second paragraph."; + } - return requestOutputFormatting(`Here is a fact checking result: ${requestLlmAuditorAdkAiAgent(statement)}.\n\n${outputFormat}`); + return requestOutputFormatting( + `Here is a fact checking result: ${requestLlmAuditorAdkAiAgent(statement)}.\n\n${outputFormat}`, + ); } diff --git a/ai/custom-func-ai-agent/appsscript.json b/ai/custom-func-ai-agent/appsscript.json index d1a41c7c1..8f76020e2 100644 --- a/ai/custom-func-ai-agent/appsscript.json +++ b/ai/custom-func-ai-agent/appsscript.json @@ -1,12 +1,14 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "libraries": [{ - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "43" - }] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/ai/custom-func-ai-studio/Code.js b/ai/custom-func-ai-studio/Code.js index bb887a139..0d85eb3c3 100644 --- a/ai/custom-func-ai-studio/Code.js +++ b/ai/custom-func-ai-studio/Code.js @@ -14,16 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ - /** * Passes a prompt and a data range to Gemini AI. - * + * * @param {range} range The range of cells. * @param {string} prompt The text prompt as a string or single cell reference. * @return The Gemini response. * @customfunction */ -function gemini(range,prompt) { - prompt = `For the range of cells ${range}, ${prompt}` - return getAiSummary(prompt); -} \ No newline at end of file +function gemini(range, prompt) { + prompt = `For the range of cells ${range}, ${prompt}`; + return getAiSummary(prompt); +} diff --git a/ai/custom-func-ai-studio/appsscript.json b/ai/custom-func-ai-studio/appsscript.json index f119e8cc1..551eff89b 100644 --- a/ai/custom-func-ai-studio/appsscript.json +++ b/ai/custom-func-ai-studio/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/ai/custom-func-ai-studio/gemini.js b/ai/custom-func-ai-studio/gemini.js index 70f3a3067..5a567da10 100644 --- a/ai/custom-func-ai-studio/gemini.js +++ b/ai/custom-func-ai-studio/gemini.js @@ -23,50 +23,57 @@ limitations under the License. * @return {string} Result of Gemini AI in string format. */ function getAiSummary(prompt) { - const data = { - "contents": [{ - "parts": [{ - "text": prompt - }] - }], - "generationConfig": { - "temperature": 0.2, - "topK": 1, - "topP": 1, - "maxOutputTokens": 2048, - "stopSequences": [] - }, - "safetySettings": [ - { - "category": "HARM_CATEGORY_HARASSMENT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "threshold": "BLOCK_NONE" - } - ] - }; - const options = { - 'method': 'post', - 'contentType': 'application/json', - 'payload': JSON.stringify(data) // Convert the JavaScript object to a JSON string. - }; + const data = { + contents: [ + { + parts: [ + { + text: prompt, + }, + ], + }, + ], + generationConfig: { + temperature: 0.2, + topK: 1, + topP: 1, + maxOutputTokens: 2048, + stopSequences: [], + }, + safetySettings: [ + { + category: "HARM_CATEGORY_HARASSMENT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_HATE_SPEECH", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_DANGEROUS_CONTENT", + threshold: "BLOCK_NONE", + }, + ], + }; + const options = { + method: "post", + contentType: "application/json", + payload: JSON.stringify(data), // Convert the JavaScript object to a JSON string. + }; - const apiKey = PropertiesService.getScriptProperties().getProperty('api_key'); - let response = UrlFetchApp.fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=' + apiKey, options); + const apiKey = PropertiesService.getScriptProperties().getProperty("api_key"); + const response = UrlFetchApp.fetch( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=" + + apiKey, + options, + ); - const payload = JSON.parse(response.getContentText()); - const text = payload.candidates[0].content.parts[0].text; + const payload = JSON.parse(response.getContentText()); + const text = payload.candidates[0].content.parts[0].text; - return text; - -} \ No newline at end of file + return text; +} diff --git a/ai/custom_func_vertex/Code.js b/ai/custom_func_vertex/Code.js index e8fb881eb..4207ecf53 100644 --- a/ai/custom_func_vertex/Code.js +++ b/ai/custom_func_vertex/Code.js @@ -1,12 +1,12 @@ /** * Passes a prompt and a data range to Gemini AI. - * + * * @param {range} range The range of cells. * @param {string} prompt The text prompt as a string or single cell reference. * @return The Gemini response. * @customfunction */ -function gemini(range,prompt) { - prompt = `For the table of data: ${range}, Answer the following: ${prompt}. Do not use formatting. Remove all markdown.` - return getAiSummary(prompt); -} \ No newline at end of file +function gemini(range, prompt) { + prompt = `For the table of data: ${range}, Answer the following: ${prompt}. Do not use formatting. Remove all markdown.`; + return getAiSummary(prompt); +} diff --git a/ai/custom_func_vertex/aiVertex.js b/ai/custom_func_vertex/aiVertex.js index 708582614..c288e09b4 100644 --- a/ai/custom_func_vertex/aiVertex.js +++ b/ai/custom_func_vertex/aiVertex.js @@ -14,9 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -const VERTEX_AI_LOCATION = PropertiesService.getScriptProperties().getProperty('project_location'); -const MODEL_ID = PropertiesService.getScriptProperties().getProperty('model_id'); -const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty('service_account_key'); +const VERTEX_AI_LOCATION = + PropertiesService.getScriptProperties().getProperty("project_location"); +const MODEL_ID = + PropertiesService.getScriptProperties().getProperty("model_id"); +const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty( + "service_account_key", +); /** * Packages prompt and necessary settings, then sends a request to @@ -26,83 +30,87 @@ const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty( * @param prompt - String representing your prompt for Gemini AI. */ function getAiSummary(prompt) { + const request = { + contents: [ + { + role: "user", + parts: [ + { + text: prompt, + }, + ], + }, + ], + generationConfig: { + temperature: 0.1, + maxOutputTokens: 2048, + }, + safetySettings: [ + { + category: "HARM_CATEGORY_HARASSMENT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_HATE_SPEECH", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_DANGEROUS_CONTENT", + threshold: "BLOCK_NONE", + }, + ], + }; - const request = { - "contents": [{ - "role": "user", - "parts": [{ - "text": prompt - }] - }], - "generationConfig": { - "temperature": 0.1, - "maxOutputTokens": 2048, - }, - "safetySettings": [ - { - "category": "HARM_CATEGORY_HARASSMENT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "threshold": "BLOCK_NONE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "threshold": "BLOCK_NONE" - } - ] - }; + const credentials = credentialsForVertexAI(); - const credentials = credentialsForVertexAI(); + const fetchOptions = { + method: "post", + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify(request), + }; - const fetchOptions = { - method: 'post', - headers: { - 'Authorization': `Bearer ${credentials.accessToken}` - }, - contentType: 'application/json', - muteHttpExceptions: true, - payload: JSON.stringify(request) - } + const url = + `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/` + + `locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; - const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/` - + `locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent` + const response = UrlFetchApp.fetch(url, fetchOptions); - const response = UrlFetchApp.fetch(url, fetchOptions); + const payload = JSON.parse(response); + const text = payload.candidates[0].content.parts[0].text; - const payload = JSON.parse(response); - const text = payload.candidates[0].content.parts[0].text - - return text + return text; } /** * Gets credentials required to call Vertex API using a Service Account. * Requires use of Service Account Key stored with project - * + * * @return {!Object} Containing the Cloud Project Id and the access token. */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } - const parsedCredentials = JSON.parse(credentials); + const parsedCredentials = JSON.parse(credentials); - const service = OAuth2.createService("Vertex") - .setTokenUrl('https://oauth2.googleapis.com/token') - .setPrivateKey(parsedCredentials['private_key']) - .setIssuer(parsedCredentials['client_email']) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials['project_id'], - accessToken: service.getAccessToken(), - } + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials["private_key"]) + .setIssuer(parsedCredentials["client_email"]) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials["project_id"], + accessToken: service.getAccessToken(), + }; } diff --git a/ai/custom_func_vertex/appsscript.json b/ai/custom_func_vertex/appsscript.json index d1a41c7c1..8f76020e2 100644 --- a/ai/custom_func_vertex/appsscript.json +++ b/ai/custom_func_vertex/appsscript.json @@ -1,12 +1,14 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "libraries": [{ - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "43" - }] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/ai/devdocs-link-preview/Cards.js b/ai/devdocs-link-preview/Cards.js index 037fef365..37d978720 100644 --- a/ai/devdocs-link-preview/Cards.js +++ b/ai/devdocs-link-preview/Cards.js @@ -16,100 +16,99 @@ limitations under the License. /** * Creates the Card to display documentation summary to user. - * + * * @param {string} pageTitle Title of the page/card section. * @param {string} summary Page summary to display. * @return {!Card} */ function buildCard(pageTitle, summary, showRating = true) { - - let cardHeader = CardService.newCardHeader() - .setTitle('About this page'); - - let summarySection = CardService.newCardSection() - .addWidget(CardService.newTextParagraph().setText(summary)); - - let feedbackSection = CardService.newCardSection() - .setHeader('Rate this summary'); - - if (showRating) { - let thumbsUpAction = CardService.newAction() - .setFunctionName('onRatingClicked') - .setParameters({ - 'key': 'upVotes', - 'title': pageTitle, - 'pageSummary': summary - }); - - let thumbsDownAction = CardService.newAction() - .setFunctionName('onRatingClicked') - .setParameters({ - 'key': 'downVotes', - 'title': pageTitle, - 'pageSummary': summary - }); - - let thumbsUpButton = CardService.newImageButton() - .setIconUrl( - 'https://fonts.gstatic.com/s/i/googlematerialicons/thumb_up_alt/v11/gm_blue-24dp/1x/gm_thumb_up_alt_gm_blue_24dp.png' - ) - .setAltText('Looks good') - .setOnClickAction(thumbsUpAction); - - let thumbsDownButton = CardService.newImageButton() - .setIconUrl( - 'https://fonts.gstatic.com/s/i/googlematerialicons/thumb_down_alt/v11/gm_blue-24dp/1x/gm_thumb_down_alt_gm_blue_24dp.png' - ) - .setAltText('Not great') - .setOnClickAction(thumbsDownAction); - - let ratingButtons = CardService.newButtonSet() - .addButton(thumbsUpButton) - .addButton(thumbsDownButton); - feedbackSection.addWidget(ratingButtons) - } else { - feedbackSection.addWidget(CardService.newTextParagraph().setText("Thank you for your feedback.")) - } - - - let card = CardService.newCardBuilder() - .setHeader(cardHeader) - .addSection(summarySection) - .addSection(feedbackSection) - .build(); - return card; + const cardHeader = CardService.newCardHeader().setTitle("About this page"); + + const summarySection = CardService.newCardSection().addWidget( + CardService.newTextParagraph().setText(summary), + ); + + const feedbackSection = + CardService.newCardSection().setHeader("Rate this summary"); + + if (showRating) { + const thumbsUpAction = CardService.newAction() + .setFunctionName("onRatingClicked") + .setParameters({ + key: "upVotes", + title: pageTitle, + pageSummary: summary, + }); + + const thumbsDownAction = CardService.newAction() + .setFunctionName("onRatingClicked") + .setParameters({ + key: "downVotes", + title: pageTitle, + pageSummary: summary, + }); + + const thumbsUpButton = CardService.newImageButton() + .setIconUrl( + "https://fonts.gstatic.com/s/i/googlematerialicons/thumb_up_alt/v11/gm_blue-24dp/1x/gm_thumb_up_alt_gm_blue_24dp.png", + ) + .setAltText("Looks good") + .setOnClickAction(thumbsUpAction); + + const thumbsDownButton = CardService.newImageButton() + .setIconUrl( + "https://fonts.gstatic.com/s/i/googlematerialicons/thumb_down_alt/v11/gm_blue-24dp/1x/gm_thumb_down_alt_gm_blue_24dp.png", + ) + .setAltText("Not great") + .setOnClickAction(thumbsDownAction); + + const ratingButtons = CardService.newButtonSet() + .addButton(thumbsUpButton) + .addButton(thumbsDownButton); + feedbackSection.addWidget(ratingButtons); + } else { + feedbackSection.addWidget( + CardService.newTextParagraph().setText("Thank you for your feedback."), + ); + } + + const card = CardService.newCardBuilder() + .setHeader(cardHeader) + .addSection(summarySection) + .addSection(feedbackSection) + .build(); + return card; } /** * Creates a Card to let user know an error has occurred. - * + * * @return {!Card} */ function buildErrorCard() { - let cardHeader = CardService.newCardHeader() - .setTitle('Uh oh! Something went wrong.') + const cardHeader = CardService.newCardHeader().setTitle( + "Uh oh! Something went wrong.", + ); - let errorMessage = CardService.newTextParagraph() - .setText( - 'It looks like Gemini got stage fright.'); + const errorMessage = CardService.newTextParagraph().setText( + "It looks like Gemini got stage fright.", + ); - let tryAgainButton = CardService.newTextButton() - .setText('Try again') - .setTextButtonStyle(CardService.TextButtonStyle.TEXT) - .setOnClickAction( CardService.newAction() - .setFunctionName('onLinkPreview')); + const tryAgainButton = CardService.newTextButton() + .setText("Try again") + .setTextButtonStyle(CardService.TextButtonStyle.TEXT) + .setOnClickAction(CardService.newAction().setFunctionName("onLinkPreview")); - let buttonList = CardService.newButtonSet() - .addButton(tryAgainButton); + const buttonList = CardService.newButtonSet().addButton(tryAgainButton); - let mainSection = CardService.newCardSection() - .addWidget(errorMessage) - .addWidget(buttonList); + const mainSection = CardService.newCardSection() + .addWidget(errorMessage) + .addWidget(buttonList); - let errorCard = CardService.newCardBuilder() - .setHeader(cardHeader) - .addSection(mainSection) - .build(); + const errorCard = CardService.newCardBuilder() + .setHeader(cardHeader) + .addSection(mainSection) + .build(); - return errorCard; -} \ No newline at end of file + return errorCard; +} diff --git a/ai/devdocs-link-preview/Helpers.js b/ai/devdocs-link-preview/Helpers.js index d3ef585a8..f01d10d86 100644 --- a/ai/devdocs-link-preview/Helpers.js +++ b/ai/devdocs-link-preview/Helpers.js @@ -18,10 +18,10 @@ limitations under the License. * Wraper around script properties to allow for a default value if unset. */ function scriptPropertyWithDefault(key, defaultValue = undefined) { - const scriptProperties = PropertiesService.getScriptProperties(); - const value = scriptProperties.getProperty(key); - if (value) { - return value; - } - return defaultValue; + const scriptProperties = PropertiesService.getScriptProperties(); + const value = scriptProperties.getProperty(key); + if (value) { + return value; + } + return defaultValue; } diff --git a/ai/devdocs-link-preview/Main.js b/ai/devdocs-link-preview/Main.js index db7c23da2..dc4942d89 100644 --- a/ai/devdocs-link-preview/Main.js +++ b/ai/devdocs-link-preview/Main.js @@ -16,54 +16,54 @@ limitations under the License. /** * Creates a link preview card for Google developer documentation links. - * + * * @param {!Object} event * @return {!Card} */ function onLinkPreview(event) { - const hostApp = event.hostApp; - if (!event[hostApp].matchedUrl.url) { - return; - } - const url = event[hostApp].matchedUrl.url; - try { - const info = getPageSummary(url); - const card = buildCard(info.title, info.summary); - const linkPreview = CardService.newLinkPreview() - .setPreviewCard(card) - .setTitle(info.title) - .setLinkPreviewTitle(info.title); - return linkPreview; - } catch (error) { - // Log the error - console.error("Error occurred:", error); - const errorCard = buildErrorCard(); - return CardService.newActionResponseBuilder() - .setNavigation(CardService.newNavigation().updateCard(errorCard)) - .build(); - } + const hostApp = event.hostApp; + if (!event[hostApp].matchedUrl.url) { + return; + } + const url = event[hostApp].matchedUrl.url; + try { + const info = getPageSummary(url); + const card = buildCard(info.title, info.summary); + const linkPreview = CardService.newLinkPreview() + .setPreviewCard(card) + .setTitle(info.title) + .setLinkPreviewTitle(info.title); + return linkPreview; + } catch (error) { + // Log the error + console.error("Error occurred:", error); + const errorCard = buildErrorCard(); + return CardService.newActionResponseBuilder() + .setNavigation(CardService.newNavigation().updateCard(errorCard)) + .build(); + } } /** * Action handler for a good rating . - * + * * @param {!Object} e The event passed from click action. * @return {!Card} */ function onRatingClicked(e) { - let key = e.parameters.key; - let title = e.parameters.title; - let pageSummary = e.parameters.pageSummary; + const key = e.parameters.key; + const title = e.parameters.title; + const pageSummary = e.parameters.pageSummary; - const properties = PropertiesService.getScriptProperties(); - let rating = Number(properties.getProperty(key) ?? 0); - properties.setProperty(key, ++rating); + const properties = PropertiesService.getScriptProperties(); + let rating = Number(properties.getProperty(key) ?? 0); + properties.setProperty(key, ++rating); - let card = buildCard(title, pageSummary, false); - let linkPreview = CardService.newLinkPreview() - .setPreviewCard(card) - .setTitle(title) - .setLinkPreviewTitle(title); + const card = buildCard(title, pageSummary, false); + const linkPreview = CardService.newLinkPreview() + .setPreviewCard(card) + .setTitle(title) + .setLinkPreviewTitle(title); - return linkPreview; + return linkPreview; } diff --git a/ai/devdocs-link-preview/Vertex.js b/ai/devdocs-link-preview/Vertex.js index c5631b486..9e110e894 100644 --- a/ai/devdocs-link-preview/Vertex.js +++ b/ai/devdocs-link-preview/Vertex.js @@ -14,99 +14,101 @@ See the License for the specific language governing permissions and limitations under the License. */ -const VERTEX_AI_LOCATION = scriptPropertyWithDefault('project_location', 'us-central1'); -const MODEL_ID = scriptPropertyWithDefault('model_id', 'gemini-2.5-flash'); -const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault('service_account_key'); +const VERTEX_AI_LOCATION = scriptPropertyWithDefault( + "project_location", + "us-central1", +); +const MODEL_ID = scriptPropertyWithDefault("model_id", "gemini-2.5-flash"); +const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault("service_account_key"); /** * Invokes Gemini to extrac the title and summary of a given URL. Responses may be cached. */ function getPageSummary(targetUrl) { - let cachedResponse = CacheService.getScriptCache().get(targetUrl); - if (cachedResponse) { - return JSON.parse(cachedResponse); - } + const cachedResponse = CacheService.getScriptCache().get(targetUrl); + if (cachedResponse) { + return JSON.parse(cachedResponse); + } - const request = { - contents: [ - { - role: "user", - parts: [ - { - text: targetUrl - } - ] - } - ], - systemInstruction: { - parts: [ - { - text: `You are a Google Developers documentation expert. In 2-3 sentences, create a short description of what the following web page is about based on the snippet of HTML from the page. Make the summary scannable. Don't repeat the URL in the description. Use proper grammar. Make the description easy to read. Only include the description in your response, exclude any conversational parts of the response. Make sure you use the most recent Google product names. Output the response as JSON with the page title as "title" and the summary as "summary"` - } - ] - }, - generationConfig: { - temperature: .2, - candidateCount: 1, - maxOutputTokens: 2048 - } - } + const request = { + contents: [ + { + role: "user", + parts: [ + { + text: targetUrl, + }, + ], + }, + ], + systemInstruction: { + parts: [ + { + text: `You are a Google Developers documentation expert. In 2-3 sentences, create a short description of what the following web page is about based on the snippet of HTML from the page. Make the summary scannable. Don't repeat the URL in the description. Use proper grammar. Make the description easy to read. Only include the description in your response, exclude any conversational parts of the response. Make sure you use the most recent Google product names. Output the response as JSON with the page title as "title" and the summary as "summary"`, + }, + ], + }, + generationConfig: { + temperature: 0.2, + candidateCount: 1, + maxOutputTokens: 2048, + }, + }; - const credentials = credentialsForVertexAI(); + const credentials = credentialsForVertexAI(); - const fetchOptions = { - method: 'POST', - headers: { - 'Authorization': `Bearer ${credentials.accessToken}` - }, - contentType: 'application/json', - muteHttpExceptions: true, - payload: JSON.stringify(request) - } + const fetchOptions = { + method: "POST", + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify(request), + }; - const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + - `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent` - const response = UrlFetchApp.fetch(url, fetchOptions); + const url = + `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + + `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; + const response = UrlFetchApp.fetch(url, fetchOptions); - const responseText = response.getContentText(); - console.log(responseText); - if (response.getResponseCode() >= 400) { - console.log(responseText); - throw new Error("Unable to generate preview,"); - } - const parsedResponse = JSON.parse(responseText); - let modelResponse = parsedResponse.candidates[0].content.parts[0].text; - const jsonMatch = modelResponse.match(/(?<=^`{3}json$)([\s\S]*)(?=^`{3}$)/gm); - if (!jsonMatch) { - throw new Error("Unable to generate preview,"); - } - CacheService.getScriptCache().put(targetUrl, jsonMatch); - return JSON.parse(jsonMatch[0]); + const responseText = response.getContentText(); + console.log(responseText); + if (response.getResponseCode() >= 400) { + console.log(responseText); + throw new Error("Unable to generate preview,"); + } + const parsedResponse = JSON.parse(responseText); + const modelResponse = parsedResponse.candidates[0].content.parts[0].text; + const jsonMatch = modelResponse.match(/(?<=^`{3}json$)([\s\S]*)(?=^`{3}$)/gm); + if (!jsonMatch) { + throw new Error("Unable to generate preview,"); + } + CacheService.getScriptCache().put(targetUrl, jsonMatch); + return JSON.parse(jsonMatch[0]); } - - /** * Gets credentials required to call Vertex API using a Service Account. * Requires use of Service Account Key stored with project - * + * * @return {!Object} Containing the Cloud Project Id and the access token. */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } - const parsedCredentials = JSON.parse(credentials); - const service = OAuth2.createService("Vertex") - .setTokenUrl('https://oauth2.googleapis.com/token') - .setPrivateKey(parsedCredentials['private_key']) - .setIssuer(parsedCredentials['client_email']) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials['project_id'], - accessToken: service.getAccessToken(), - } + const parsedCredentials = JSON.parse(credentials); + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials["private_key"]) + .setIssuer(parsedCredentials["client_email"]) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials["project_id"], + accessToken: service.getAccessToken(), + }; } diff --git a/ai/devdocs-link-preview/appsscript.json b/ai/devdocs-link-preview/appsscript.json index 1cdbefadc..7d9c7f482 100644 --- a/ai/devdocs-link-preview/appsscript.json +++ b/ai/devdocs-link-preview/appsscript.json @@ -1,70 +1,70 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "43" - } - ] - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/workspace.linkpreview", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/cloud-platform" - ], - "addOns": { - "common": { - "name": "DevDocs Previews", - "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png", - "layoutProperties": { - "primaryColor": "#1A73E8" - } - }, - "docs": { - "linkPreviewTriggers": [ - { - "patterns": [ - { - "hostPattern": "developers.google.*" - } - ], - "runFunction": "onLinkPreview", - "labelText": "Page title", - "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" - } - ] - }, - "sheets": { - "linkPreviewTriggers": [ - { - "patterns": [ - { - "hostPattern": "developers.google.*" - } - ], - "runFunction": "onLinkPreview", - "labelText": "Page title", - "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" - } - ] - }, - "slides": { - "linkPreviewTriggers": [ - { - "patterns": [ - { - "hostPattern": "developers.google.*" - } - ], - "runFunction": "onLinkPreview", - "labelText": "Page title", - "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" - } - ] - } - } -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43" + } + ] + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/workspace.linkpreview", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/cloud-platform" + ], + "addOns": { + "common": { + "name": "DevDocs Previews", + "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png", + "layoutProperties": { + "primaryColor": "#1A73E8" + } + }, + "docs": { + "linkPreviewTriggers": [ + { + "patterns": [ + { + "hostPattern": "developers.google.*" + } + ], + "runFunction": "onLinkPreview", + "labelText": "Page title", + "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" + } + ] + }, + "sheets": { + "linkPreviewTriggers": [ + { + "patterns": [ + { + "hostPattern": "developers.google.*" + } + ], + "runFunction": "onLinkPreview", + "labelText": "Page title", + "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" + } + ] + }, + "slides": { + "linkPreviewTriggers": [ + { + "patterns": [ + { + "hostPattern": "developers.google.*" + } + ], + "runFunction": "onLinkPreview", + "labelText": "Page title", + "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" + } + ] + } + } +} diff --git a/ai/drive-rename/ai.js b/ai/drive-rename/ai.js index e88f324b2..9e6dbd081 100644 --- a/ai/drive-rename/ai.js +++ b/ai/drive-rename/ai.js @@ -14,9 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -const VERTEX_AI_LOCATION = PropertiesService.getScriptProperties().getProperty('project_location'); -const MODEL_ID = PropertiesService.getScriptProperties().getProperty('model_id'); -const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty('service_account_key'); +const VERTEX_AI_LOCATION = + PropertiesService.getScriptProperties().getProperty("project_location"); +const MODEL_ID = + PropertiesService.getScriptProperties().getProperty("model_id"); +const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty( + "service_account_key", +); const STANDARD_PROMPT = ` @@ -38,69 +42,69 @@ const STANDARD_PROMPT = ` * @param prompt - String representing your prompt for Gemini AI. */ function getAiSummary(prompt) { - - const request = { - "contents": [ - { - "role": "user", - "parts": [{ - text: STANDARD_PROMPT, - }, - { - "text": prompt - }] - } - ], - "generationConfig": { - "temperature": .2, - "maxOutputTokens": 2048, - "response_mime_type": "application/json" - } - } - - const credentials = credentialsForVertexAI(); - - const fetchOptions = { - method: 'POST', - headers: { - 'Authorization': `Bearer ${credentials.accessToken}` - }, - contentType: 'application/json', - payload: JSON.stringify(request) - } - - const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent` - - const response = UrlFetchApp.fetch(url, fetchOptions); - - const payload = JSON.parse(response.getContentText()); - const jsonPayload = JSON.parse(payload.candidates[0].content.parts[0].text) - - return jsonPayload - + const request = { + contents: [ + { + role: "user", + parts: [ + { + text: STANDARD_PROMPT, + }, + { + text: prompt, + }, + ], + }, + ], + generationConfig: { + temperature: 0.2, + maxOutputTokens: 2048, + response_mime_type: "application/json", + }, + }; + + const credentials = credentialsForVertexAI(); + + const fetchOptions = { + method: "POST", + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + contentType: "application/json", + payload: JSON.stringify(request), + }; + + const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; + + const response = UrlFetchApp.fetch(url, fetchOptions); + + const payload = JSON.parse(response.getContentText()); + const jsonPayload = JSON.parse(payload.candidates[0].content.parts[0].text); + + return jsonPayload; } /** * Gets credentials required to call Vertex API using a Service Account. * - * + * */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } - - const parsedCredentials = JSON.parse(credentials); - - const service = OAuth2.createService("Vertex") - .setTokenUrl('https://oauth2.googleapis.com/token') - .setPrivateKey(parsedCredentials['private_key']) - .setIssuer(parsedCredentials['client_email']) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials['project_id'], - accessToken: service.getAccessToken(), - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } + + const parsedCredentials = JSON.parse(credentials); + + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials["private_key"]) + .setIssuer(parsedCredentials["client_email"]) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials["project_id"], + accessToken: service.getAccessToken(), + }; } diff --git a/ai/drive-rename/appsscript.json b/ai/drive-rename/appsscript.json index 72b2f849e..8d80189a1 100644 --- a/ai/drive-rename/appsscript.json +++ b/ai/drive-rename/appsscript.json @@ -1,44 +1,44 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "serviceId": "drive", - "version": "v3" - } - ] - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/drive.addons.metadata.readonly", - "https://www.googleapis.com/auth/drive.file", - "https://www.googleapis.com/auth/drive", - "https://www.googleapis.com/auth/drive.readonly", - "https://www.googleapis.com/auth/documents" - ], - "urlFetchWhitelist": [ - "https://*.googleusercontent.com/", - "https://*.googleapis.com/" - ], - "addOns": { - "common": { - "name": "Name with Intelligence", - "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png", - "layoutProperties": { - "primaryColor": "#4285f4", - "secondaryColor": "#3f8bca" - } - }, - "drive": { - "homepageTrigger": { - "runFunction": "onHomepageOpened" - }, - "onItemsSelectedTrigger": { - "runFunction": "onDriveItemsSelected" - } - } - } -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "serviceId": "drive", + "version": "v3" + } + ] + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/drive.addons.metadata.readonly", + "https://www.googleapis.com/auth/drive.file", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/documents" + ], + "urlFetchWhitelist": [ + "https://*.googleusercontent.com/", + "https://*.googleapis.com/" + ], + "addOns": { + "common": { + "name": "Name with Intelligence", + "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png", + "layoutProperties": { + "primaryColor": "#4285f4", + "secondaryColor": "#3f8bca" + } + }, + "drive": { + "homepageTrigger": { + "runFunction": "onHomepageOpened" + }, + "onItemsSelectedTrigger": { + "runFunction": "onDriveItemsSelected" + } + } + } +} diff --git a/ai/drive-rename/drive.js b/ai/drive-rename/drive.js index 7d84a850f..38458ab57 100644 --- a/ai/drive-rename/drive.js +++ b/ai/drive-rename/drive.js @@ -16,123 +16,111 @@ limitations under the License. /** * Renames a file based on user selection / updates card. - * + * * @param {!Event} e Add-on event context * @return {!Card} */ function renameFile(e) { - - const newName = e.formInput.names - const id = e.drive.activeCursorItem.id - DriveApp.getFileById(id).setName(newName) - - const eUpdated = - { - hostApp: 'drive', - drive: - { - selectedItems: [[Object]], - activeCursorItem: - { - title: newName, - id: id, - iconUrl: e.drive.activeCursorItem.iconUrl, - mimeType: e.drive.activeCursorItem.mimeType - }, - commonEventObject: { hostApp: 'DRIVE', platform: 'WEB' }, - clientPlatform: 'web' - } - } - - return onCardUpdate(eUpdated) - + const newName = e.formInput.names; + const id = e.drive.activeCursorItem.id; + DriveApp.getFileById(id).setName(newName); + + const eUpdated = { + hostApp: "drive", + drive: { + selectedItems: [[Object]], + activeCursorItem: { + title: newName, + id: id, + iconUrl: e.drive.activeCursorItem.iconUrl, + mimeType: e.drive.activeCursorItem.mimeType, + }, + commonEventObject: { hostApp: "DRIVE", platform: "WEB" }, + clientPlatform: "web", + }, + }; + + return onCardUpdate(eUpdated); } /** * Redraws the same card to force AI to refresh its data. - * + * * @param {!Event} e Add-on event context * @return {!Card} */ function updateCard(e) { - - const id = e.drive.activeCursorItem.id - - const eConverted = - { - hostApp: 'drive', - drive: - { - selectedItems: [[Object]], - activeCursorItem: - { - title: DriveApp.getFileById(id).getName(), - id: id, - iconUrl: e.drive.activeCursorItem.iconUrl, - mimeType: e.drive.activeCursorItem.mimeType - }, - commonEventObject: { hostApp: 'DRIVE', platform: 'WEB' }, - clientPlatform: 'web' - } - } - - return onCardUpdate(eConverted) + const id = e.drive.activeCursorItem.id; + + const eConverted = { + hostApp: "drive", + drive: { + selectedItems: [[Object]], + activeCursorItem: { + title: DriveApp.getFileById(id).getName(), + id: id, + iconUrl: e.drive.activeCursorItem.iconUrl, + mimeType: e.drive.activeCursorItem.mimeType, + }, + commonEventObject: { hostApp: "DRIVE", platform: "WEB" }, + clientPlatform: "web", + }, + }; + + return onCardUpdate(eConverted); } /** * Fetches the body of given document, using DocumentApp. - * + * * @param {string} id The Google Document file ID. * @return {string} The body of the Google Document. */ function getDocumentBody(id) { + var doc = DocumentApp.openById(id); + var body = doc.getBody(); + var text = body.getText(); - var doc = DocumentApp.openById(id); - var body = doc.getBody(); - var text = body.getText(); - - return text; + return text; } /** * Fetches the body of given document, using DocsApi. - * + * * @param {string} id The Google Document file ID. * @return {string} The body of the Google Document. */ function getDocAPIBody(id) { - - // Call DOC API REST endpoint to get the file - let url = `https://docs.googleapis.com/v1/documents/${id}`; - - var response = UrlFetchApp.fetch(url, { - method: 'GET', - headers: { - Authorization: 'Bearer ' + ScriptApp.getOAuthToken(), - }, - muteHttpExceptions: true - }); - - if (response.getResponseCode() !== 200) { - throw new Error(`Drive API returned error \ + // Call DOC API REST endpoint to get the file + const url = `https://docs.googleapis.com/v1/documents/${id}`; + + var response = UrlFetchApp.fetch(url, { + method: "GET", + headers: { + Authorization: "Bearer " + ScriptApp.getOAuthToken(), + }, + muteHttpExceptions: true, + }); + + if (response.getResponseCode() !== 200) { + throw new Error(`Drive API returned error \ ${response.getResponseCode()} :\ ${response.getContentText()}`); - } + } - let file = response.getContentText(); - let data = JSON.parse(file); + const file = response.getContentText(); + const data = JSON.parse(file); - return data.body.content; + return data.body.content; } /** * Sends the given document to the trash folder. - * + * * @param {!Event} e Add-on event context */ function moveFileToTrash(e) { - - const id = e.drive.activeCursorItem.id - const file = DriveApp.getFileById(id); - file.setTrashed(true); -} \ No newline at end of file + const id = e.drive.activeCursorItem.id; + const file = DriveApp.getFileById(id); + file.setTrashed(true); +} diff --git a/ai/drive-rename/main.js b/ai/drive-rename/main.js index 418202d19..677cfa144 100644 --- a/ai/drive-rename/main.js +++ b/ai/drive-rename/main.js @@ -16,56 +16,53 @@ limitations under the License. /** * Main entry point for add-on when opened. - * + * * @param e - Add-on event context */ function onHomepageOpened(e) { - const card = buildHomePage(); + const card = buildHomePage(); - return { - action: { - navigations: [ - { - pushCard: card - } - ] - } - }; + return { + action: { + navigations: [ + { + pushCard: card, + }, + ], + }, + }; } /** * Handles selection of a file in Google Drive. - * + * * @param e - Add-on event context */ function onDriveItemsSelected(e) { - - return { - action: { - navigations: [ - { - pushCard: buildSelectionPage(e) - } - ] - } - } + return { + action: { + navigations: [ + { + pushCard: buildSelectionPage(e), + }, + ], + }, + }; } - /** * Handles the update of the card on demand. - * + * * @param e - (Modified) add-on event context */ function onCardUpdate(e) { - - return { - action: { - navigations: [ - { - updateCard: buildSelectionPage(e) - } - ] - } - } + return { + action: { + navigations: [ + { + updateCard: buildSelectionPage(e), + }, + ], + }, + }; } diff --git a/ai/drive-rename/ui.js b/ai/drive-rename/ui.js index ddbfafc5f..65f7467e0 100644 --- a/ai/drive-rename/ui.js +++ b/ai/drive-rename/ui.js @@ -14,263 +14,247 @@ See the License for the specific language governing permissions and limitations under the License. */ -const ICO_HEADER = "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png" -const ICON_RENAME = "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-18dp/2x/gm_drive_file_rename_outline_googblue_18dp.png" -const ICON_RETRY = "https://fonts.gstatic.com/s/i/googlematerialicons/refresh/v16/googblue-18dp/2x/gm_refresh_googblue_18dp.png" -const ICON_DELETE = "https://fonts.gstatic.com/s/i/googlematerialicons/delete/v17/black-18dp/2x/gm_delete_black_18dp.png" +const ICO_HEADER = + "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png"; +const ICON_RENAME = + "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-18dp/2x/gm_drive_file_rename_outline_googblue_18dp.png"; +const ICON_RETRY = + "https://fonts.gstatic.com/s/i/googlematerialicons/refresh/v16/googblue-18dp/2x/gm_refresh_googblue_18dp.png"; +const ICON_DELETE = + "https://fonts.gstatic.com/s/i/googlematerialicons/delete/v17/black-18dp/2x/gm_delete_black_18dp.png"; /** * Builds the card for the selected active item. - * + * * @param e - Add-on event context */ function buildSelectionPage(e) { - - const selected = e.drive.activeCursorItem - - // Check if Google Doc type, respond unsupported if not - if (selected.mimeType != "application/vnd.google-apps.document") { - return { - sections: [ - { - widgets: [ - { - textParagraph: { - text: "Note: currently only Google Docs file types are supported." - }, - } - ] - } - ], - "header": buildHeader() - }; - } - - // Get document body - const docBody = getDocumentBody(selected.id); - - - // Create widgets starting with Title - const widgets = [ - { - textParagraph: { - text: `${selected.title}` - }, - } - ]; - - // Check if doc is empty before calling AI - if (docBody.length > 1) { - - // Get AI data - const aiResponse = getAiSummary(docBody); - - console.log('RESPONSE') - - console.log(aiResponse) - - - // Add the Summary text - widgets.push({ - "decoratedText": { - "topLabel": "Summary", - "text": aiResponse.summary, - "wrapText": true - } - }, - ) - - // Divider - widgets.push({ "divider": {} },) - - // Create an object of items - const items = []; - aiResponse.names.forEach(name => { - items.push({ - "text": name, - "value": name, - "selected": false - },) - - }); - - // Set first item as selected - items[0].selected = true - - - // Add the Radio button of 'names' as items - widgets.push( - { - "selectionInput": { - "name": "names", - "label": "Select a new name", - "type": "RADIO_BUTTON", - "items": items - } - }, - ) - - // Create the 'Rename' button - widgets.push( - { - "buttonList": { - "buttons": [ - { - "text": "Rename", - "icon": { - "iconUrl": ICON_RENAME, - "altText": "Rename" - }, - "onClick": { - "action": { - "function": "renameFile", - "parameters": [ - { - "key": "id", - "value": selected.id - } - ], - "loadIndicator": "SPINNER" - } - } - }, { - "text": "", - "icon": { - "iconUrl": ICON_RETRY, - "altText": "Retry" - }, - "onClick": { - "action": { - "function": "updateCard", - "parameters": [ - { - "key": "id", - "value": selected.id - } - ], - "loadIndicator": "SPINNER" - } - } - } - ] - }, - "horizontalAlignment": "CENTER", - } - ) - - } // end if - - // Don't call AI, but offer to delete - else { - - // Add the Summary text - widgets.push({ - "decoratedText": { - "topLabel": "Summary", - "text": "Empty document", - "wrapText": true - } - }, - ) - - // Divider - widgets.push({ "divider": {} },) - - // Create the 'Delete' button - widgets.push( - { - "buttonList": { - "buttons": [ - { - "text": "Move to trash", - "icon": { - "iconUrl": ICON_DELETE, - "altText": "Move to trash" - }, - "onClick": { - "action": { - "function": "moveFileToTrash", - "parameters": [ - { - "key": "id", - "value": selected.id - } - ], - "loadIndicator": "SPINNER" - } - }, - "color": { - "red": 0.961, - "green": 0.6, - "blue": 0.667, - "alpha": 1 - } - } - ] - }, - "horizontalAlignment": "CENTER", - } - ) - } // end else - - - return { - sections: [ - { - widgets - }, - ], - "header": buildHeader() - }; + const selected = e.drive.activeCursorItem; + + // Check if Google Doc type, respond unsupported if not + if (selected.mimeType != "application/vnd.google-apps.document") { + return { + sections: [ + { + widgets: [ + { + textParagraph: { + text: "Note: currently only Google Docs file types are supported.", + }, + }, + ], + }, + ], + header: buildHeader(), + }; + } + + // Get document body + const docBody = getDocumentBody(selected.id); + + // Create widgets starting with Title + const widgets = [ + { + textParagraph: { + text: `${selected.title}`, + }, + }, + ]; + + // Check if doc is empty before calling AI + if (docBody.length > 1) { + // Get AI data + const aiResponse = getAiSummary(docBody); + + console.log("RESPONSE"); + + console.log(aiResponse); + + // Add the Summary text + widgets.push({ + decoratedText: { + topLabel: "Summary", + text: aiResponse.summary, + wrapText: true, + }, + }); + + // Divider + widgets.push({ divider: {} }); + + // Create an object of items + const items = []; + aiResponse.names.forEach((name) => { + items.push({ + text: name, + value: name, + selected: false, + }); + }); + + // Set first item as selected + items[0].selected = true; + + // Add the Radio button of 'names' as items + widgets.push({ + selectionInput: { + name: "names", + label: "Select a new name", + type: "RADIO_BUTTON", + items: items, + }, + }); + + // Create the 'Rename' button + widgets.push({ + buttonList: { + buttons: [ + { + text: "Rename", + icon: { + iconUrl: ICON_RENAME, + altText: "Rename", + }, + onClick: { + action: { + function: "renameFile", + parameters: [ + { + key: "id", + value: selected.id, + }, + ], + loadIndicator: "SPINNER", + }, + }, + }, + { + text: "", + icon: { + iconUrl: ICON_RETRY, + altText: "Retry", + }, + onClick: { + action: { + function: "updateCard", + parameters: [ + { + key: "id", + value: selected.id, + }, + ], + loadIndicator: "SPINNER", + }, + }, + }, + ], + }, + horizontalAlignment: "CENTER", + }); + } // end if + + // Don't call AI, but offer to delete + else { + // Add the Summary text + widgets.push({ + decoratedText: { + topLabel: "Summary", + text: "Empty document", + wrapText: true, + }, + }); + + // Divider + widgets.push({ divider: {} }); + + // Create the 'Delete' button + widgets.push({ + buttonList: { + buttons: [ + { + text: "Move to trash", + icon: { + iconUrl: ICON_DELETE, + altText: "Move to trash", + }, + onClick: { + action: { + function: "moveFileToTrash", + parameters: [ + { + key: "id", + value: selected.id, + }, + ], + loadIndicator: "SPINNER", + }, + }, + color: { + red: 0.961, + green: 0.6, + blue: 0.667, + alpha: 1, + }, + }, + ], + }, + horizontalAlignment: "CENTER", + }); + } // end else + + return { + sections: [ + { + widgets, + }, + ], + header: buildHeader(), + }; } - - /** * Builds the header for the Add-on Cards. */ function buildHeader() { - - const header = { - "title": "Name with Intelligence", - "subtitle": `"Untitled documents" no more!`, // Better Doc names w/ Gemini AI", - "imageUrl": ICO_HEADER, - "imageType": "SQUARE" - }; - return header + const header = { + title: "Name with Intelligence", + subtitle: `"Untitled documents" no more!`, // Better Doc names w/ Gemini AI", + imageUrl: ICO_HEADER, + imageType: "SQUARE", + }; + return header; } /** * Builds the home page card. */ function buildHomePage() { - - const widgets = [ - { - textParagraph: { - text: "Name with Intelligence enables you to quickly rename any Google Doc using suggestions provided via Google Gemini." - }, - }, - { "divider": {} }, - { - textParagraph: { - text: "👉 To use, select a Google Doc to rename. Then choose a new name from the list of AI generated names provided for you. A quick summary of the file is also provided by Google Gemini to help you make your decision.", - }, - }, - { "divider": {} }, - { - textParagraph: { - text: "Note: currently only Google Docs file types are supported.", - }, - } - ]; - - return { - sections: [ - { - widgets - }, - ], - "header": buildHeader() - }; -} \ No newline at end of file + const widgets = [ + { + textParagraph: { + text: "Name with Intelligence enables you to quickly rename any Google Doc using suggestions provided via Google Gemini.", + }, + }, + { divider: {} }, + { + textParagraph: { + text: "👉 To use, select a Google Doc to rename. Then choose a new name from the list of AI generated names provided for you. A quick summary of the file is also provided by Google Gemini to help you make your decision.", + }, + }, + { divider: {} }, + { + textParagraph: { + text: "Note: currently only Google Docs file types are supported.", + }, + }, + ]; + + return { + sections: [ + { + widgets, + }, + ], + header: buildHeader(), + }; +} diff --git a/ai/email-classifier/appsscript.json b/ai/email-classifier/appsscript.json index baa8ca4ce..2c42da7c0 100644 --- a/ai/email-classifier/appsscript.json +++ b/ai/email-classifier/appsscript.json @@ -1,38 +1,38 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Gmail", - "version": "v1", - "serviceId": "gmail" - }, - { - "userSymbol": "Sheets", - "version": "v4", - "serviceId": "sheets" - } - ] - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/gmail.addons.execute", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/spreadsheets" - ], - "addOns": { - "common": { - "name": "Email Classifier", - "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/label_important/v20/googblue-24dp/1x/gm_label_important_googblue_24dp.png" - }, - "gmail": { - "homepageTrigger": { - "runFunction": "onHomepageTrigger", - "enabled": true - } - } - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + }, + { + "userSymbol": "Sheets", + "version": "v4", + "serviceId": "sheets" + } + ] + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/gmail.addons.execute", + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/spreadsheets" + ], + "addOns": { + "common": { + "name": "Email Classifier", + "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/label_important/v20/googblue-24dp/1x/gm_label_important_googblue_24dp.png" + }, + "gmail": { + "homepageTrigger": { + "runFunction": "onHomepageTrigger", + "enabled": true + } + } + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/ai/gmail-sentiment-analysis/appsscript.json b/ai/gmail-sentiment-analysis/appsscript.json index 80b231e9a..4634adc28 100644 --- a/ai/gmail-sentiment-analysis/appsscript.json +++ b/ai/gmail-sentiment-analysis/appsscript.json @@ -1,27 +1,27 @@ { - "timeZone": "Europe/Madrid", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "version": "43", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" - } - ] - }, - "addOns": { - "common": { - "name": "Productivity toolbox", - "logoUrl": "https://icons.iconarchive.com/icons/roundicons/100-free-solid/64/spy-icon.png", - "useLocaleFromApp": true - }, - "gmail": { - "homepageTrigger": { - "runFunction": "onHomepage", - "enabled": true - } - } - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "Europe/Madrid", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "version": "43", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" + } + ] + }, + "addOns": { + "common": { + "name": "Productivity toolbox", + "logoUrl": "https://icons.iconarchive.com/icons/roundicons/100-free-solid/64/spy-icon.png", + "useLocaleFromApp": true + }, + "gmail": { + "homepageTrigger": { + "runFunction": "onHomepage", + "enabled": true + } + } + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/ai/standup-chat-app/appsscript.json b/ai/standup-chat-app/appsscript.json index 0d1efd07a..8c7f15e7a 100644 --- a/ai/standup-chat-app/appsscript.json +++ b/ai/standup-chat-app/appsscript.json @@ -1,37 +1,37 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Chat", - "serviceId": "chat", - "version": "v1" - }, - { - "userSymbol": "AdminDirectory", - "serviceId": "admin", - "version": "directory_v1" - } - ] - }, - "webapp": { - "executeAs": "USER_ACCESSING", - "access": "DOMAIN" - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/chat.messages", - "https://www.googleapis.com/auth/spreadsheets", - "https://www.googleapis.com/auth/admin.directory.user.readonly", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/chat.spaces.create", - "https://www.googleapis.com/auth/chat.spaces", - "https://www.googleapis.com/auth/chat.spaces.readonly", - "https://www.googleapis.com/auth/chat.spaces.create", - "https://www.googleapis.com/auth/chat.delete", - "https://www.googleapis.com/auth/chat.memberships", - "https://www.googleapis.com/auth/chat.memberships.app", - "https://www.googleapis.com/auth/userinfo.email" - ] -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Chat", + "serviceId": "chat", + "version": "v1" + }, + { + "userSymbol": "AdminDirectory", + "serviceId": "admin", + "version": "directory_v1" + } + ] + }, + "webapp": { + "executeAs": "USER_ACCESSING", + "access": "DOMAIN" + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/chat.messages", + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/admin.directory.user.readonly", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/chat.spaces.create", + "https://www.googleapis.com/auth/chat.spaces", + "https://www.googleapis.com/auth/chat.spaces.readonly", + "https://www.googleapis.com/auth/chat.spaces.create", + "https://www.googleapis.com/auth/chat.delete", + "https://www.googleapis.com/auth/chat.memberships", + "https://www.googleapis.com/auth/chat.memberships.app", + "https://www.googleapis.com/auth/userinfo.email" + ] +} diff --git a/ai/standup-chat-app/db.js b/ai/standup-chat-app/db.js index f8c003d58..3f65ae832 100644 --- a/ai/standup-chat-app/db.js +++ b/ai/standup-chat-app/db.js @@ -34,65 +34,60 @@ limitations under the License. * @property {string} space.name */ - class DB { - /** - * params {String} spreadsheetId - */ - constructor(spreadsheetId) { - this.spreadsheetId = spreadsheetId; - this.sheetName = "Messages"; - - } - - /** - * @returns {SpreadsheetApp.Sheet} - */ - get sheet() { - const spreadsheet = SpreadsheetApp.openById(this.spreadsheetId); - let sheet = spreadsheet.getSheetByName(this.sheetName); - - // create if it does not exist - if (sheet == undefined) { - sheet = spreadsheet.insertSheet(); - sheet.setName(this.sheetName) - } - - return sheet; - } - - /** - * @returns {Message|undefined} - */ - get last() { - const lastRow = this.sheet.getLastRow() - if (lastRow === 0) return; - return JSON.parse(this.sheet.getSheetValues(lastRow, 1, 1, 2)[0][1]); - } - - - /** - * @params {Chat_v1.Chat.V1.Schema.Message} message - */ - append(message) { - this.sheet.appendRow([message.name, JSON.stringify(message, null, 2)]); - } - + /** + * params {String} spreadsheetId + */ + constructor(spreadsheetId) { + this.spreadsheetId = spreadsheetId; + this.sheetName = "Messages"; + } + + /** + * @returns {SpreadsheetApp.Sheet} + */ + get sheet() { + const spreadsheet = SpreadsheetApp.openById(this.spreadsheetId); + let sheet = spreadsheet.getSheetByName(this.sheetName); + + // create if it does not exist + if (sheet == undefined) { + sheet = spreadsheet.insertSheet(); + sheet.setName(this.sheetName); + } + + return sheet; + } + + /** + * @returns {Message|undefined} + */ + get last() { + const lastRow = this.sheet.getLastRow(); + if (lastRow === 0) return; + return JSON.parse(this.sheet.getSheetValues(lastRow, 1, 1, 2)[0][1]); + } + + /** + * @params {Chat_v1.Chat.V1.Schema.Message} message + */ + append(message) { + this.sheet.appendRow([message.name, JSON.stringify(message, null, 2)]); + } } - /** * Test function for DB Object */ function testDB() { - const db = new DB(SPREADSHEET_ID); + const db = new DB(SPREADSHEET_ID); - let thread = db.last; - if (thread == undefined) return; - console.log(thread) + let thread = db.last; + if (thread == undefined) return; + console.log(thread); - db.rowOffset = 1; - thread = db.last; - if (thread == undefined) return; - console.log(thread) + db.rowOffset = 1; + thread = db.last; + if (thread == undefined) return; + console.log(thread); } diff --git a/ai/standup-chat-app/gemini.js b/ai/standup-chat-app/gemini.js index 98d5195d5..45d84d606 100644 --- a/ai/standup-chat-app/gemini.js +++ b/ai/standup-chat-app/gemini.js @@ -16,26 +16,28 @@ limitations under the License. /** * Makes a simple content-only call to Gemini AI. - * + * * @param {string} text Prompt to pass to Gemini API. * @param {string} API_KEY Developer API Key enabled to call Gemini. - * - * @return {string} Response from AI call. + * + * @return {string} Response from AI call. */ function generateContent(text, API_KEY) { - const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`; + const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`; - return JSON.parse(UrlFetchApp.fetch(url, { - method: "POST", - headers: { - "content-type": "application/json" - }, - payload: JSON.stringify({ - contents: [{ - parts: [ - {text} - ] - }] - }), - }).getContentText()) -} \ No newline at end of file + return JSON.parse( + UrlFetchApp.fetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + }, + payload: JSON.stringify({ + contents: [ + { + parts: [{ text }], + }, + ], + }), + }).getContentText(), + ); +} diff --git a/ai/standup-chat-app/main.js b/ai/standup-chat-app/main.js index 812191dfa..1271c7594 100644 --- a/ai/standup-chat-app/main.js +++ b/ai/standup-chat-app/main.js @@ -18,101 +18,109 @@ limitations under the License. * Update global variables for your project settings * */ const API_KEY = PropertiesService.getScriptProperties().getProperty("API_KEY"); -const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty("SPREADSHEET_ID"); // e.g. "1O0IW7fW1QeFLa7tIrv_h7_PlSUTB6kd0miQO_sXo7p0" -const SPACE_NAME = PropertiesService.getScriptProperties().getProperty("SPACE_NAME"); // e.g. "spaces/AAAABCa12Cc" +const SPREADSHEET_ID = + PropertiesService.getScriptProperties().getProperty("SPREADSHEET_ID"); // e.g. "1O0IW7fW1QeFLa7tIrv_h7_PlSUTB6kd0miQO_sXo7p0" +const SPACE_NAME = + PropertiesService.getScriptProperties().getProperty("SPACE_NAME"); // e.g. "spaces/AAAABCa12Cc" const SUMMARY_HEADER = `\n\n*Gemini Generated Summary*\n\n`; - /** - * Sends the message to create new standup instance. + * Sends the message to create new standup instance. * Called by trigger on interval of standup, e.g. Weekly - * + * * @return {string} The thread name of the message sent. */ function standup() { - const db = new DB(SPREADSHEET_ID); + const db = new DB(SPREADSHEET_ID); - const last = db.last; + const last = db.last; - let text = ` Please share your weekly update here.\n\n*Source Code*: `; + let text = ` Please share your weekly update here.\n\n*Source Code*: `; - if (last) { - text += `\n*Last Week*: <${linkToThread(last)}|View thread>`; - } + if (last) { + text += `\n*Last Week*: <${linkToThread(last)}|View thread>`; + } - const message = Chat.Spaces.Messages.create({ - text, - }, PropertiesService.getScriptProperties().getProperty("spaceName") // Demo replaces => SPACE_NAME - ); + const message = Chat.Spaces.Messages.create( + { + text, + }, + PropertiesService.getScriptProperties().getProperty("spaceName"), // Demo replaces => SPACE_NAME + ); - db.append(message); + db.append(message); - console.log(`Thread Name: ${message.thread.name}`) - return message.thread.name + console.log(`Thread Name: ${message.thread.name}`); + return message.thread.name; } /** * Uses AI to create a summary of messages for a stand up period. - * Called by trigger on interval required to summarize, e.g. Hourly - * + * Called by trigger on interval required to summarize, e.g. Hourly + * * @return n/a */ function summarize() { - const db = new DB(SPREADSHEET_ID); - const last = db.last; - - if (last == undefined) return; - - const filter = `thread.name=${last.thread.name}`; - let { messages } = Chat.Spaces.Messages.list(PropertiesService.getScriptProperties().getProperty("spaceName"), { filter }); // Demo replaces => SPACE_NAME - - messages = (messages ?? []) - .slice(1) - .filter(message => message.slashCommand === undefined) - - if (messages.length === 0) { - return; - } - - const history = messages - .map(({ sender, text }) => `${cachedGetSenderDisplayName(sender)}: ${text}`) - .join('/n'); - - const response = generateContent( - `Summarize the following weekly tasks and discussion per team member in a single concise sentence for each individual with an extra newline between members, but without using markdown or any special character except for newlines: ${history}`, - API_KEY); - const summary = response.candidates[0].content?.parts[0].text; - - if (summary == undefined) { - return; - } - - Chat.Spaces.Messages.update({ - text: last.formattedText + SUMMARY_HEADER + summary.replace("**", "*") - }, - last.name, - { update_mask: "text" } - ); - + const db = new DB(SPREADSHEET_ID); + const last = db.last; + + if (last == undefined) return; + + const filter = `thread.name=${last.thread.name}`; + let { messages } = Chat.Spaces.Messages.list( + PropertiesService.getScriptProperties().getProperty("spaceName"), + { filter }, + ); // Demo replaces => SPACE_NAME + + messages = (messages ?? []) + .slice(1) + .filter((message) => message.slashCommand === undefined); + + if (messages.length === 0) { + return; + } + + const history = messages + .map(({ sender, text }) => `${cachedGetSenderDisplayName(sender)}: ${text}`) + .join("/n"); + + const response = generateContent( + `Summarize the following weekly tasks and discussion per team member in a single concise sentence for each individual with an extra newline between members, but without using markdown or any special character except for newlines: ${history}`, + API_KEY, + ); + const summary = response.candidates[0].content?.parts[0].text; + + if (summary == undefined) { + return; + } + + Chat.Spaces.Messages.update( + { + text: last.formattedText + SUMMARY_HEADER + summary.replace("**", "*"), + }, + last.name, + { update_mask: "text" }, + ); } /** * Gets the display name from AdminDirectory Services. - * - * @param {!Object} sender + * + * @param {!Object} sender * @return {string} User name on success | 'Unknown' if not. */ function getSenderDisplayName(sender) { - try { - const user = AdminDirectory.Users.get( - sender.name.replace("users/", ""), - { projection: 'BASIC', viewType: 'domain_public' }); - return user.name.displayName ?? user.name.fullName; - } catch (e) { - console.error("Unable to get display name"); - return "Unknown" - }; + try { + const user = AdminDirectory.Users.get(sender.name.replace("users/", ""), { + projection: "BASIC", + viewType: "domain_public", + }); + return user.name.displayName ?? user.name.fullName; + } catch (e) { + console.error("Unable to get display name"); + return "Unknown"; + } } const cachedGetSenderDisplayName = memoize(getSenderDisplayName); @@ -122,6 +130,6 @@ const cachedGetSenderDisplayName = memoize(getSenderDisplayName); * @returns {String} */ function linkToThread(message) { - // https://chat.google.com/room/SPACE/THREAD/ - return `https://chat.google.com/room/${message.space.name.split("/").pop()}/${message.thread.name.split("/").pop()}`; + // https://chat.google.com/room/SPACE/THREAD/ + return `https://chat.google.com/room/${message.space.name.split("/").pop()}/${message.thread.name.split("/").pop()}`; } diff --git a/ai/standup-chat-app/memoize.js b/ai/standup-chat-app/memoize.js index 6d9a6f259..01f6076d6 100644 --- a/ai/standup-chat-app/memoize.js +++ b/ai/standup-chat-app/memoize.js @@ -24,9 +24,9 @@ limitations under the License. * @returns {string} The base64 encoded hash of the string. */ function hash(str, algorithm = Utilities.DigestAlgorithm.MD5) { - const digest = Utilities.computeDigest(algorithm, str); + const digest = Utilities.computeDigest(algorithm, str); - return Utilities.base64Encode(digest); + return Utilities.base64Encode(digest); } /** @@ -47,20 +47,19 @@ function hash(str, algorithm = Utilities.DigestAlgorithm.MD5) { * cached(4, 5, 6); // A new result will be calculated and cached */ function memoize(func, ttl = 600, cache = CacheService.getScriptCache()) { - return (...args) => { + return (...args) => { + // consider a more robust input to the hash function to handler complex + // types such as functions, dates, and regex + const key = hash(JSON.stringify([func.toString(), ...args])); - // consider a more robust input to the hash function to handler complex - // types such as functions, dates, and regex - const key = hash(JSON.stringify([func.toString(), ...args])); + const cached = cache.get(key); - const cached = cache.get(key); - - if (cached != null) { - return JSON.parse(cached); - } else { - const result = func(...args); - cache.put(key, JSON.stringify(result), ttl); - return result; - } - }; -} \ No newline at end of file + if (cached != null) { + return JSON.parse(cached); + } else { + const result = func(...args); + cache.put(key, JSON.stringify(result), ttl); + return result; + } + }; +} diff --git a/apps-script/execute/target.js b/apps-script/execute/target.js index aa79e72fc..2f791cf53 100644 --- a/apps-script/execute/target.js +++ b/apps-script/execute/target.js @@ -20,13 +20,13 @@ * @return {Object} A set of folder names keyed by folder ID. */ function getFoldersUnderRoot() { - const root = DriveApp.getRootFolder(); - const folders = root.getFolders(); - const folderSet = {}; - while (folders.hasNext()) { - const folder = folders.next(); - folderSet[folder.getId()] = folder.getName(); - } - return folderSet; + const root = DriveApp.getRootFolder(); + const folders = root.getFolders(); + const folderSet = {}; + while (folders.hasNext()) { + const folder = folders.next(); + folderSet[folder.getId()] = folder.getName(); + } + return folderSet; } // [END apps_script_api_execute] diff --git a/biome.json b/biome.json index 0f254acda..0d0a563b5 100644 --- a/biome.json +++ b/biome.json @@ -1,20 +1,20 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "formatter": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "files": { - "ignore": ["moment.gs", "**/dist", "**/target", "**/pkg", "**/node_modules"] - } + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "files": { + "ignore": ["moment.gs", "**/dist", "**/target", "**/pkg", "**/node_modules"] + } } diff --git a/chat/advanced-service/appsscript.json b/chat/advanced-service/appsscript.json index 7f1d4d24f..14852ca1c 100644 --- a/chat/advanced-service/appsscript.json +++ b/chat/advanced-service/appsscript.json @@ -1,29 +1,33 @@ { - "timeZone": "America/New_York", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [ - "https://www.googleapis.com/auth/chat.spaces", - "https://www.googleapis.com/auth/chat.spaces.create", - "https://www.googleapis.com/auth/chat.spaces.readonly", - "https://www.googleapis.com/auth/chat.memberships", - "https://www.googleapis.com/auth/chat.memberships.app", - "https://www.googleapis.com/auth/chat.memberships.readonly", - "https://www.googleapis.com/auth/chat.messages", - "https://www.googleapis.com/auth/chat.messages.create", - "https://www.googleapis.com/auth/chat.messages.readonly" - ], - "chat": {}, - "dependencies": { - "enabledAdvancedServices": [{ - "userSymbol": "Chat", - "version": "v1", - "serviceId": "chat" - }], - "libraries": [{ - "userSymbol": "OAuth2", - "version": "43", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" - }] - } + "timeZone": "America/New_York", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/chat.spaces", + "https://www.googleapis.com/auth/chat.spaces.create", + "https://www.googleapis.com/auth/chat.spaces.readonly", + "https://www.googleapis.com/auth/chat.memberships", + "https://www.googleapis.com/auth/chat.memberships.app", + "https://www.googleapis.com/auth/chat.memberships.readonly", + "https://www.googleapis.com/auth/chat.messages", + "https://www.googleapis.com/auth/chat.messages.create", + "https://www.googleapis.com/auth/chat.messages.readonly" + ], + "chat": {}, + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Chat", + "version": "v1", + "serviceId": "chat" + } + ], + "libraries": [ + { + "userSymbol": "OAuth2", + "version": "43", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" + } + ] + } } diff --git a/chat/quickstart/appsscript.json b/chat/quickstart/appsscript.json index 73c0edcf7..a25813a03 100644 --- a/chat/quickstart/appsscript.json +++ b/chat/quickstart/appsscript.json @@ -1,16 +1,16 @@ { - "timeZone": "America/New_York", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [ - "https://www.googleapis.com/auth/chat.spaces.readonly" - ], - "chat": {}, - "dependencies": { - "enabledAdvancedServices": [{ - "userSymbol": "Chat", - "version": "v1", - "serviceId": "chat" - }] - } + "timeZone": "America/New_York", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": ["https://www.googleapis.com/auth/chat.spaces.readonly"], + "chat": {}, + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Chat", + "version": "v1", + "serviceId": "chat" + } + ] + } } diff --git a/data-studio/appsscript.json b/data-studio/appsscript.json index 358a778e8..f505294eb 100644 --- a/data-studio/appsscript.json +++ b/data-studio/appsscript.json @@ -1,24 +1,25 @@ { - "dataStudio": { - "name": "Nucleus by Hooli", - "company": "Hooli Inc.", - "companyUrl": "https://hooli.xyz", - "logoUrl": "https://hooli.xyz/middle-out-optimized/nucleus/logo.png", - "addonUrl": "https://hooli.xyz/data-studio-connector", - "supportUrl": "https://hooli.xyz/data-studio-connector/support", - "description": "Nucleus by Hooli connector lets you connect to your data in Data Studio using Nucleus middle out optimization. You will need an account on hooli.xyz to use this connector. Create your account at https://hooli.xyz/signup", - "shortDescription": "Connect to your data using Nucleus middle out optimization", - "privacyPolicyUrl": "https://hooli.xyz/privacy", - "termsOfServiceUrl": "https://hooli.xyz/tos", - "authType": ["NONE"], - "feeType": ["PAID"], - "sources": ["HOOLI_CHAT_LOG", "ENDFRAME_SERVER_STREAM", "RETINABYTE_USER_ANALYTICS"], - "templates": { - "default": "872223s89f5fdkjnd983kjf" - } - }, - "urlFetchWhitelist": [ - "https://api.hooli.xyz/", - "https://hooli.xyz/" - ] + "dataStudio": { + "name": "Nucleus by Hooli", + "company": "Hooli Inc.", + "companyUrl": "https://hooli.xyz", + "logoUrl": "https://hooli.xyz/middle-out-optimized/nucleus/logo.png", + "addonUrl": "https://hooli.xyz/data-studio-connector", + "supportUrl": "https://hooli.xyz/data-studio-connector/support", + "description": "Nucleus by Hooli connector lets you connect to your data in Data Studio using Nucleus middle out optimization. You will need an account on hooli.xyz to use this connector. Create your account at https://hooli.xyz/signup", + "shortDescription": "Connect to your data using Nucleus middle out optimization", + "privacyPolicyUrl": "https://hooli.xyz/privacy", + "termsOfServiceUrl": "https://hooli.xyz/tos", + "authType": ["NONE"], + "feeType": ["PAID"], + "sources": [ + "HOOLI_CHAT_LOG", + "ENDFRAME_SERVER_STREAM", + "RETINABYTE_USER_ANALYTICS" + ], + "templates": { + "default": "872223s89f5fdkjnd983kjf" + } + }, + "urlFetchWhitelist": ["https://api.hooli.xyz/", "https://hooli.xyz/"] } diff --git a/data-studio/appsscript2.json b/data-studio/appsscript2.json index 5b1d2c601..705398e48 100644 --- a/data-studio/appsscript2.json +++ b/data-studio/appsscript2.json @@ -1,12 +1,12 @@ { - "dataStudio": { - "name": "npm Downloads - Build Guide", - "logoUrl": "https://raw.githubusercontent.com/npm/logos/master/%22npm%22%20lockup/npm-logo-simplifed-with-white-space.png", - "company": "Build Guide User", - "companyUrl": "https://developers.google.com/datastudio/", - "addonUrl": "https://github.com/google/datastudio/tree/master/community-connectors/npm-downloads", - "supportUrl": "https://github.com/google/datastudio/issues", - "description": "Get npm package download counts.", - "sources": ["npm"] - } -} \ No newline at end of file + "dataStudio": { + "name": "npm Downloads - Build Guide", + "logoUrl": "https://raw.githubusercontent.com/npm/logos/master/%22npm%22%20lockup/npm-logo-simplifed-with-white-space.png", + "company": "Build Guide User", + "companyUrl": "https://developers.google.com/datastudio/", + "addonUrl": "https://github.com/google/datastudio/tree/master/community-connectors/npm-downloads", + "supportUrl": "https://github.com/google/datastudio/issues", + "description": "Get npm package download counts.", + "sources": ["npm"] + } +} diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json b/forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json index 796a53b3e..50427a683 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json @@ -1,19 +1,19 @@ { - "timeZone": "America/New_York", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": {}, - "webapp": { - "executeAs": "USER_DEPLOYING", - "access": "MYSELF" - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/drive", - "https://www.googleapis.com/auth/drive.readonly", - "https://www.googleapis.com/auth/forms.body", - "https://www.googleapis.com/auth/forms.body.readonly", - "https://www.googleapis.com/auth/forms.responses.readonly", - "https://www.googleapis.com/auth/userinfo.email" - ] -} \ No newline at end of file + "timeZone": "America/New_York", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": {}, + "webapp": { + "executeAs": "USER_DEPLOYING", + "access": "MYSELF" + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/forms.body", + "https://www.googleapis.com/auth/forms.body.readonly", + "https://www.googleapis.com/auth/forms.responses.readonly", + "https://www.googleapis.com/auth/userinfo.email" + ] +} diff --git a/gmail-sentiment-analysis/.clasp.json b/gmail-sentiment-analysis/.clasp.json index 8206fa91c..ba34351b4 100644 --- a/gmail-sentiment-analysis/.clasp.json +++ b/gmail-sentiment-analysis/.clasp.json @@ -1,3 +1,3 @@ { - "scriptId": "1Z2gfvr0oYn68ppDtQbv0qIuKKVWhvwOTr-gCE0GFKVjNk8NDlpfJAGAr" + "scriptId": "1Z2gfvr0oYn68ppDtQbv0qIuKKVWhvwOTr-gCE0GFKVjNk8NDlpfJAGAr" } diff --git a/gmail-sentiment-analysis/appsscript.json b/gmail-sentiment-analysis/appsscript.json index c1085b5f1..fa056ed5c 100644 --- a/gmail-sentiment-analysis/appsscript.json +++ b/gmail-sentiment-analysis/appsscript.json @@ -1,25 +1,25 @@ { - "timeZone": "America/Toronto", - "oauthScopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/gmail.addons.execute", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/userinfo.email" - ], - "addOns": { - "common": { - "name": "Sentiment Analysis", - "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/sentiment_extremely_dissatisfied/v6/black-24dp/1x/gm_sentiment_extremely_dissatisfied_black_24dp.png" - }, - "gmail": { - "homepageTrigger": { - "runFunction": "onHomepageTrigger", - "enabled": true - } - } - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/Toronto", + "oauthScopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/gmail.addons.execute", + "https://www.googleapis.com/auth/gmail.labels", + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/userinfo.email" + ], + "addOns": { + "common": { + "name": "Sentiment Analysis", + "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/sentiment_extremely_dissatisfied/v6/black-24dp/1x/gm_sentiment_extremely_dissatisfied_black_24dp.png" + }, + "gmail": { + "homepageTrigger": { + "runFunction": "onHomepageTrigger", + "enabled": true + } + } + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/gmail/add-ons/appsscript.json b/gmail/add-ons/appsscript.json index 8984bbe94..36f94c724 100644 --- a/gmail/add-ons/appsscript.json +++ b/gmail/add-ons/appsscript.json @@ -1,21 +1,20 @@ { - "oauthScopes": [ - "https://www.googleapis.com/auth/gmail.addons.execute", - "https://www.googleapis.com/auth/gmail.addons.current.message.metadata", - "https://www.googleapis.com/auth/gmail.modify" - ], - "gmail": { - "name": "Gmail Add-on Quickstart - QuickLabels", - "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/label_googblue_24dp.png", - "contextualTriggers": [{ - "unconditional": { - }, - "onTriggerFunction": "buildAddOn" - }], - "openLinkUrlPrefixes": [ - "https://mail.google.com/" - ], - "primaryColor": "#4285F4", - "secondaryColor": "#4285F4" - } + "oauthScopes": [ + "https://www.googleapis.com/auth/gmail.addons.execute", + "https://www.googleapis.com/auth/gmail.addons.current.message.metadata", + "https://www.googleapis.com/auth/gmail.modify" + ], + "gmail": { + "name": "Gmail Add-on Quickstart - QuickLabels", + "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/label_googblue_24dp.png", + "contextualTriggers": [ + { + "unconditional": {}, + "onTriggerFunction": "buildAddOn" + } + ], + "openLinkUrlPrefixes": ["https://mail.google.com/"], + "primaryColor": "#4285F4", + "secondaryColor": "#4285F4" + } } diff --git a/package.json b/package.json index d26850c71..c29860601 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,33 @@ { - "name": "googleworkspace-apps-script-samples", - "version": "1.0.0", - "description": "Apps Script samples for [Google Workspace](https://developers.google.com/apps-script/) docs.", - "license": "MIT", - "private": true, - "keywords": [ - "Google Workspace", - "Apps Script", - "Calendar", - "Drive", - "Sheets", - "Slides", - "API" - ], - "devDependencies": { - "@types/google-apps-script": "^2.0.7", - "@types/node": "^24.10.1", - "@biomejs/biome": "1.9.4", - "tsx": "^4.20.6", - "typescript": "^5.9.3" - }, - "scripts": { - "lint": "biome check .", - "format": "biome check --write .", - "check": "tsx .github/scripts/check-gs.ts" - }, - "type": "module", - "packageManager": "pnpm@10.15.1", - "engines": { - "node": ">=20" - } + "name": "googleworkspace-apps-script-samples", + "version": "1.0.0", + "description": "Apps Script samples for [Google Workspace](https://developers.google.com/apps-script/) docs.", + "license": "MIT", + "private": true, + "keywords": [ + "Google Workspace", + "Apps Script", + "Calendar", + "Drive", + "Sheets", + "Slides", + "API" + ], + "devDependencies": { + "@types/google-apps-script": "^2.0.7", + "@types/node": "^24.10.1", + "@biomejs/biome": "1.9.4", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + }, + "scripts": { + "lint": "biome check .", + "format": "biome check --write .", + "check": "tsx .github/scripts/check-gs.ts" + }, + "type": "module", + "packageManager": "pnpm@10.15.1", + "engines": { + "node": ">=20" + } } diff --git a/picker/appsscript.json b/picker/appsscript.json index bc86e7be2..5e2059224 100644 --- a/picker/appsscript.json +++ b/picker/appsscript.json @@ -1,18 +1,18 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [ - "https://www.googleapis.com/auth/script.container.ui", - "https://www.googleapis.com/auth/drive.file" - ], - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v3", - "serviceId": "drive" - } - ] - } - } \ No newline at end of file + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/script.container.ui", + "https://www.googleapis.com/auth/drive.file" + ], + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + } +} diff --git a/sheets/next18/appsscript.json b/sheets/next18/appsscript.json index cccf3c15e..6ba0b64f6 100644 --- a/sheets/next18/appsscript.json +++ b/sheets/next18/appsscript.json @@ -1,19 +1,21 @@ { - "timeZone": "America/New_York", - "dependencies": { - "libraries": [{ - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "26" - }] - }, - "exceptionLogging": "STACKDRIVER", - "oauthScopes": [ - "https://www.googleapis.com/auth/script.container.ui", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/spreadsheets.currentonly", - "https://www.googleapis.com/auth/drive", - "https://www.googleapis.com/auth/documents", - "https://www.googleapis.com/auth/presentations" - ] + "timeZone": "America/New_York", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "26" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "oauthScopes": [ + "https://www.googleapis.com/auth/script.container.ui", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/spreadsheets.currentonly", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/presentations" + ] } diff --git a/slides/SpeakerNotesScript/appscript.json b/slides/SpeakerNotesScript/appscript.json index 696cd61b5..ce8138e2c 100644 --- a/slides/SpeakerNotesScript/appscript.json +++ b/slides/SpeakerNotesScript/appscript.json @@ -1,8 +1,8 @@ { - "timeZone": "America/New_York", - "oauthScopes": [ - "https://www.googleapis.com/auth/documents", - "https://www.googleapis.com/auth/presentations.currentonly" - ], - "exceptionLogging": "STACKDRIVER" + "timeZone": "America/New_York", + "oauthScopes": [ + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/presentations.currentonly" + ], + "exceptionLogging": "STACKDRIVER" } diff --git a/solutions/add-on/book-smartchip/.clasp.json b/solutions/add-on/book-smartchip/.clasp.json index 490e7e41d..14c6f8945 100644 --- a/solutions/add-on/book-smartchip/.clasp.json +++ b/solutions/add-on/book-smartchip/.clasp.json @@ -1 +1 @@ -{"scriptId":"14tK6PD4C624ivRyGk-S6eYCbYJnDfA24xeP0Jhb1U8sPgAvZXeZm5gpb"} +{ "scriptId": "14tK6PD4C624ivRyGk-S6eYCbYJnDfA24xeP0Jhb1U8sPgAvZXeZm5gpb" } diff --git a/solutions/add-on/book-smartchip/Code.js b/solutions/add-on/book-smartchip/Code.js index f054c4d18..7f7360049 100644 --- a/solutions/add-on/book-smartchip/Code.js +++ b/solutions/add-on/book-smartchip/Code.js @@ -1,53 +1,53 @@ function getBook(id) { - const apiKey = 'YOUR_API_KEY'; // Replace with your API key - const apiEndpoint = `https://www.googleapis.com/books/v1/volumes/${id}?key=${apiKey}&country=US`; - const response = UrlFetchApp.fetch(apiEndpoint); - return JSON.parse(response); + const apiKey = "YOUR_API_KEY"; // Replace with your API key + const apiEndpoint = `https://www.googleapis.com/books/v1/volumes/${id}?key=${apiKey}&country=US`; + const response = UrlFetchApp.fetch(apiEndpoint); + return JSON.parse(response); } function bookLinkPreview(event) { - if (event.docs.matchedUrl.url) { - const segments = event.docs.matchedUrl.url.split('/'); - const volumeID = segments[segments.length - 1]; - - const bookData = getBook(volumeID); - const bookTitle = bookData.volumeInfo.title; - const bookDescription = bookData.volumeInfo.description; - const bookImage = bookData.volumeInfo.imageLinks.small; - const bookAuthors = bookData.volumeInfo.authors; - const bookPageCount = bookData.volumeInfo.pageCount; - - const previewHeader = CardService.newCardHeader() - .setSubtitle('By ' + bookAuthors) - .setTitle(bookTitle); - - const previewPages = CardService.newDecoratedText() - .setTopLabel('Page count') - .setText(bookPageCount); - - const previewDescription = CardService.newDecoratedText() - .setTopLabel('About this book') - .setText(bookDescription).setWrapText(true); - - const previewImage = CardService.newImage() - .setAltText('Image of book cover') - .setImageUrl(bookImage); - - const buttonBook = CardService.newTextButton() - .setText('View book') - .setOpenLink(CardService.newOpenLink() - .setUrl(event.docs.matchedUrl.url)); - - const cardSectionBook = CardService.newCardSection() - .addWidget(previewImage) - .addWidget(previewPages) - .addWidget(CardService.newDivider()) - .addWidget(previewDescription) - .addWidget(buttonBook); - - return CardService.newCardBuilder() - .setHeader(previewHeader) - .addSection(cardSectionBook) - .build(); - } + if (event.docs.matchedUrl.url) { + const segments = event.docs.matchedUrl.url.split("/"); + const volumeID = segments[segments.length - 1]; + + const bookData = getBook(volumeID); + const bookTitle = bookData.volumeInfo.title; + const bookDescription = bookData.volumeInfo.description; + const bookImage = bookData.volumeInfo.imageLinks.small; + const bookAuthors = bookData.volumeInfo.authors; + const bookPageCount = bookData.volumeInfo.pageCount; + + const previewHeader = CardService.newCardHeader() + .setSubtitle("By " + bookAuthors) + .setTitle(bookTitle); + + const previewPages = CardService.newDecoratedText() + .setTopLabel("Page count") + .setText(bookPageCount); + + const previewDescription = CardService.newDecoratedText() + .setTopLabel("About this book") + .setText(bookDescription) + .setWrapText(true); + + const previewImage = CardService.newImage() + .setAltText("Image of book cover") + .setImageUrl(bookImage); + + const buttonBook = CardService.newTextButton() + .setText("View book") + .setOpenLink(CardService.newOpenLink().setUrl(event.docs.matchedUrl.url)); + + const cardSectionBook = CardService.newCardSection() + .addWidget(previewImage) + .addWidget(previewPages) + .addWidget(CardService.newDivider()) + .addWidget(previewDescription) + .addWidget(buttonBook); + + return CardService.newCardBuilder() + .setHeader(previewHeader) + .addSection(cardSectionBook) + .build(); + } } diff --git a/solutions/add-on/book-smartchip/appsscript.json b/solutions/add-on/book-smartchip/appsscript.json index 743b393c6..a54803910 100644 --- a/solutions/add-on/book-smartchip/appsscript.json +++ b/solutions/add-on/book-smartchip/appsscript.json @@ -1,43 +1,41 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [ - "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", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/library-icon.png", - "layoutProperties": { - "primaryColor": "#dd4b39" - } - }, - "docs": { - "linkPreviewTriggers": [ - { - "runFunction": "bookLinkPreview", - "patterns": [ - { - "hostPattern": "*.google.*", - "pathPrefix": "books" - }, - { - "hostPattern": "*.google.*", - "pathPrefix": "books/edition" - } - ], - "labelText": "Book", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/book-icon.png", - "localizedLabelText": { - "es": "Libros" - } - } - ] - } - } + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "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", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/library-icon.png", + "layoutProperties": { + "primaryColor": "#dd4b39" + } + }, + "docs": { + "linkPreviewTriggers": [ + { + "runFunction": "bookLinkPreview", + "patterns": [ + { + "hostPattern": "*.google.*", + "pathPrefix": "books" + }, + { + "hostPattern": "*.google.*", + "pathPrefix": "books/edition" + } + ], + "labelText": "Book", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/book-icon.png", + "localizedLabelText": { + "es": "Libros" + } + } + ] + } + } } diff --git a/solutions/add-on/share-macro/.clasp.json b/solutions/add-on/share-macro/.clasp.json index b92f05bc2..ed5586705 100644 --- a/solutions/add-on/share-macro/.clasp.json +++ b/solutions/add-on/share-macro/.clasp.json @@ -1 +1 @@ -{"scriptId": "1BsbWOAbLADGoLtp5P9oqctZMiqT5EFh_R-CufxAV9y1hvVSAMO35Azu9"} +{ "scriptId": "1BsbWOAbLADGoLtp5P9oqctZMiqT5EFh_R-CufxAV9y1hvVSAMO35Azu9" } diff --git a/solutions/add-on/share-macro/Code.js b/solutions/add-on/share-macro/Code.js index 715a869b4..0bf32ad24 100644 --- a/solutions/add-on/share-macro/Code.js +++ b/solutions/add-on/share-macro/Code.js @@ -18,147 +18,152 @@ limitations under the License. */ /** - * Uses Apps Script API to copy source Apps Script project + * 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. */ 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); - // 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(); - // 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); + // 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. -*/ + * Function that encapsulates Apps Script API project manipulation. + */ const APPS_SCRIPT_API = { - accessToken: ScriptApp.getOAuthToken(), + 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.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.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; - } - } -} + /* 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, + }; + const 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; + } + }, +}; diff --git a/solutions/add-on/share-macro/UI.js b/solutions/add-on/share-macro/UI.js index 1aec692c2..0d7bf799a 100644 --- a/solutions/add-on/share-macro/UI.js +++ b/solutions/add-on/share-macro/UI.js @@ -15,219 +15,253 @@ */ // Change application logo here (and in manifest) as desired. -const ADDON_LOGO = 'https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png'; +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); + 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. - * + * @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(); + // Configures card header. + const 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. + const 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. + const 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. + const 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. + const 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); - } + 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 + */ function buildSuccessCard(e, targetSpreadsheetUrl) { + // Configures card header. + const 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. + const 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), + ), + ); + const 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. + const 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()); +} diff --git a/solutions/add-on/share-macro/appsscript.json b/solutions/add-on/share-macro/appsscript.json index 0a9e3f14f..419507996 100644 --- a/solutions/add-on/share-macro/appsscript.json +++ b/solutions/add-on/share-macro/appsscript.json @@ -1,28 +1,26 @@ { - "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 + "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": {} + } +} diff --git a/solutions/attendance-chat-app/final/appsscript.json b/solutions/attendance-chat-app/final/appsscript.json index 4482a3a30..c42874c42 100644 --- a/solutions/attendance-chat-app/final/appsscript.json +++ b/solutions/attendance-chat-app/final/appsscript.json @@ -1,12 +1,13 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [{ - "userSymbol": "Gmail", - "serviceId": "gmail", - "version": "v1" - }] - }, - "chat": { - } -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "serviceId": "gmail", + "version": "v1" + } + ] + }, + "chat": {} +} diff --git a/solutions/attendance-chat-app/step-3/appsscript.json b/solutions/attendance-chat-app/step-3/appsscript.json index 9e2a1d673..205a036c2 100644 --- a/solutions/attendance-chat-app/step-3/appsscript.json +++ b/solutions/attendance-chat-app/step-3/appsscript.json @@ -1,6 +1,5 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - }, - "chat": {} -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "chat": {} +} diff --git a/solutions/attendance-chat-app/step-4/appsscript.json b/solutions/attendance-chat-app/step-4/appsscript.json index 9e2a1d673..205a036c2 100644 --- a/solutions/attendance-chat-app/step-4/appsscript.json +++ b/solutions/attendance-chat-app/step-4/appsscript.json @@ -1,6 +1,5 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - }, - "chat": {} -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "chat": {} +} diff --git a/solutions/attendance-chat-app/step-5/appsscript.json b/solutions/attendance-chat-app/step-5/appsscript.json index 9e2a1d673..205a036c2 100644 --- a/solutions/attendance-chat-app/step-5/appsscript.json +++ b/solutions/attendance-chat-app/step-5/appsscript.json @@ -1,6 +1,5 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - }, - "chat": {} -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "chat": {} +} diff --git a/solutions/attendance-chat-app/step-6/appsscript.json b/solutions/attendance-chat-app/step-6/appsscript.json index 4482a3a30..c42874c42 100644 --- a/solutions/attendance-chat-app/step-6/appsscript.json +++ b/solutions/attendance-chat-app/step-6/appsscript.json @@ -1,12 +1,13 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [{ - "userSymbol": "Gmail", - "serviceId": "gmail", - "version": "v1" - }] - }, - "chat": { - } -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "serviceId": "gmail", + "version": "v1" + } + ] + }, + "chat": {} +} diff --git a/solutions/automations/agenda-maker/.clasp.json b/solutions/automations/agenda-maker/.clasp.json index 4b9783746..69e60f551 100644 --- a/solutions/automations/agenda-maker/.clasp.json +++ b/solutions/automations/agenda-maker/.clasp.json @@ -1 +1 @@ -{"scriptId": "147xVWUWmw8b010zbiDMIa3eeKATo3P2q5rJCZmY3meirC-yA_XucdZlp"} +{ "scriptId": "147xVWUWmw8b010zbiDMIa3eeKATo3P2q5rJCZmY3meirC-yA_XucdZlp" } diff --git a/solutions/automations/agenda-maker/Code.js b/solutions/automations/agenda-maker/Code.js index fa2bb7a95..0e40c3b7a 100644 --- a/solutions/automations/agenda-maker/Code.js +++ b/solutions/automations/agenda-maker/Code.js @@ -23,90 +23,90 @@ limitations under the License. * @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(); + const folders = DriveApp.getFoldersByName("Agenda Maker - App"); + // Finds the folder if it exists + while (folders.hasNext()) { + const 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 + const 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(); + 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(); } /** @@ -115,78 +115,81 @@ function getTemplateId(folderId) { * */ 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; + // 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 (const i in event.getGuestList()) { + const 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 (const i in event.getGuestList()) { + const 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(); + const email = Session.getActiveUser().getEmail(); + ScriptApp.newTrigger("onCalendarChange") + .forUserCalendar(email) + .onEventUpdated() + .create(); } diff --git a/solutions/automations/agenda-maker/appsscript.json b/solutions/automations/agenda-maker/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/agenda-maker/appsscript.json +++ b/solutions/automations/agenda-maker/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/aggregate-document-content/.clasp.json b/solutions/automations/aggregate-document-content/.clasp.json index c264f85c1..fbd3f90d5 100644 --- a/solutions/automations/aggregate-document-content/.clasp.json +++ b/solutions/automations/aggregate-document-content/.clasp.json @@ -1 +1 @@ -{"scriptId": "1YGstQLxmTcAQlSHfm0yke12Y2UgT8eVfCxrG_jGpG1dHDmFdOaHQfQZJ"} +{ "scriptId": "1YGstQLxmTcAQlSHfm0yke12Y2UgT8eVfCxrG_jGpG1dHDmFdOaHQfQZJ" } diff --git a/solutions/automations/aggregate-document-content/Code.js b/solutions/automations/aggregate-document-content/Code.js index c042b83f1..e2946fd5a 100644 --- a/solutions/automations/aggregate-document-content/Code.js +++ b/solutions/automations/aggregate-document-content/Code.js @@ -17,160 +17,162 @@ 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. +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 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. +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}'. + // 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); + 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. + const noContentList = []; + let numUpdates = 0; + for (const 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 (const 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; - } + // 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))) { + const par = searchResult.getElement().asParagraph(); + if (par.getHeading() == searchHeading) { + // If heading style matches, searches for text string (case insensitive). + const 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. + const style = {}; + style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR; + parValidHeading.setAttributes(style); + parValidHeading.appendText(" [Exported]"); + + // Gets the content from the table following the valid heading. + const elemObj = parValidHeading.getNextSibling().asTable(); + const 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 + // Only gets Docs files. + const files = folder.getFilesByType(MimeType.GOOGLE_DOCS); + const docIDs = []; + while (files.hasNext()) { + const file = files.next(); + docIDs.push(file.getId()); + } + return docIDs; +} diff --git a/solutions/automations/aggregate-document-content/Menu.js b/solutions/automations/aggregate-document-content/Menu.js index 58a6b59fb..ee69f00c1 100644 --- a/solutions/automations/aggregate-document-content/Menu.js +++ b/solutions/automations/aggregate-document-content/Menu.js @@ -14,34 +14,37 @@ * limitations under the License. */ -/** +/** * 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' -} + 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() + 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(); } /** @@ -49,11 +52,11 @@ function onOpen() { * TODO: Personalize */ function aboutApp() { - const msg = ` + const msg = ` ${APP_TITLE} Version: 1.0 - Contact: ` + Contact: `; - const ui = DocumentApp.getUi(); - ui.alert("About this application", msg, ui.ButtonSet.OK); + const ui = DocumentApp.getUi(); + ui.alert("About this application", msg, ui.ButtonSet.OK); } diff --git a/solutions/automations/aggregate-document-content/Setup.js b/solutions/automations/aggregate-document-content/Setup.js index ef13ba0cb..c87765446 100644 --- a/solutions/automations/aggregate-document-content/Setup.js +++ b/solutions/automations/aggregate-document-content/Setup.js @@ -14,152 +14,170 @@ * limitations under the License. */ -/** +/** * 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. * + * + * @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); - + // 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 (const 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. + * + * 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); + // 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. + * + * Called from menu | Calls setupConfig with option set to true. */ function setupWithSamples() { - setupConfig(true) + setupConfig(true); } -/** - * Sample document names and demo content. +/** + * 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.' -} + 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. + * + * 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 + // 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; +} diff --git a/solutions/automations/aggregate-document-content/Utilities.js b/solutions/automations/aggregate-document-content/Utilities.js index 721260de6..a9cf3bfbc 100644 --- a/solutions/automations/aggregate-document-content/Utilities.js +++ b/solutions/automations/aggregate-document-content/Utilities.js @@ -14,7 +14,7 @@ * limitations under the License. */ -/** +/** * This file contains common utility functions. */ @@ -22,27 +22,30 @@ * 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. + * @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(); + // 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(); + // Iterates subfolders to check if folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + const 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`); + // 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`, + ); } /** @@ -50,11 +53,12 @@ function getFolderByName_(folderName) { * @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); - // 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 + 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); +} diff --git a/solutions/automations/aggregate-document-content/appsscript.json b/solutions/automations/aggregate-document-content/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/aggregate-document-content/appsscript.json +++ b/solutions/automations/aggregate-document-content/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/bracket-maker/.clasp.json b/solutions/automations/bracket-maker/.clasp.json index 92fd0ae7f..7fccc8c7b 100644 --- a/solutions/automations/bracket-maker/.clasp.json +++ b/solutions/automations/bracket-maker/.clasp.json @@ -1 +1 @@ -{"scriptId": "1LkY5nKFdBg2Q9-oIUcZsxRuESvgIcFHGobveNeQ5CpTgV6GgpTUQeOIB"} +{ "scriptId": "1LkY5nKFdBg2Q9-oIUcZsxRuESvgIcFHGobveNeQ5CpTgV6GgpTUQeOIB" } diff --git a/solutions/automations/bracket-maker/Code.js b/solutions/automations/bracket-maker/Code.js index fccb6a7f7..dccabe418 100644 --- a/solutions/automations/bracket-maker/Code.js +++ b/solutions/automations/bracket-maker/Code.js @@ -17,96 +17,106 @@ 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 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'}]); + const 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)); - } - } + const ss = SpreadsheetApp.getActiveSpreadsheet(); + let rangePlayers = ss.getRangeByName(RANGE_PLAYER1); + const sheetControl = ss.getSheetByName(SHEET_PLAYERS); + const 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. + const countNodesUpperBound = Math.pow(2, upperPower); + + // Calculates the number that is a power of 2 and higher than numPlayers. + const countNodesLowerBound = countNodesUpperBound / 2; + + // Determines the number of nodes that will not show in the 1st level. + const countNodesHidden = numPlayers - countNodesLowerBound; + + // Enters the players for the 1st round. + const currentPlayer = 0; + for (let i = 0; i < countNodesLowerBound; i++) { + if (i < countNodesHidden) { + // Must be on the first level + const 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++) { + const pow1 = Math.pow(2, i + 1); + const pow2 = Math.pow(2, i + 2); + const 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), + ); + } + } } /** @@ -115,11 +125,11 @@ function createBracket() { * @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'); + if (players) { + const rand = Math.ceil(Math.random() * players.length); + rng.setValue(players.splice(rand - 1, 1)[0][0]); + } + rng.setBackgroundColor("yellow"); } /** @@ -128,6 +138,6 @@ function setBracketItem_(rng, players) { * @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 + sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH); + rng.setBackgroundColor("green"); +} diff --git a/solutions/automations/bracket-maker/appsscript.json b/solutions/automations/bracket-maker/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/bracket-maker/appsscript.json +++ b/solutions/automations/bracket-maker/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/calendar-timesheet/.clasp.json b/solutions/automations/calendar-timesheet/.clasp.json index d8c75e26a..61688f402 100644 --- a/solutions/automations/calendar-timesheet/.clasp.json +++ b/solutions/automations/calendar-timesheet/.clasp.json @@ -1 +1 @@ -{"scriptId": "1WL3-mzC219UHqy_vqI1gEeoFy5Y8eeiKCZjiiPsWmVmQfVVedN5Vt7rK"} +{ "scriptId": "1WL3-mzC219UHqy_vqI1gEeoFy5Y8eeiKCZjiiPsWmVmQfVVedN5Vt7rK" } diff --git a/solutions/automations/calendar-timesheet/Code.js b/solutions/automations/calendar-timesheet/Code.js index adda729b9..1d4fe96d0 100644 --- a/solutions/automations/calendar-timesheet/Code.js +++ b/solutions/automations/calendar-timesheet/Code.js @@ -22,110 +22,146 @@ limitations under the License. * to the spreadsheet menu */ const onOpen = () => { - SpreadsheetApp.getUi() - .createMenu('myTime') - .addItem('Sync calendar events', 'run') - .addItem('Settings', 'settings') - .addToUi(); + 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'); + const html = + HtmlService.createHtmlOutputFromFile("Page").setTitle("Settings"); - SpreadsheetApp.getUi().showSidebar(html); + SpreadsheetApp.getUi().showSidebar(html); }; /** -* returns the settings from the script properties -*/ + * 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; + 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 -*/ + * 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'; + 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(); + myTime({ + mainSpreadsheetId: SpreadsheetApp.getActiveSpreadsheet().getId(), + }).run(); }; /** @@ -134,230 +170,277 @@ const run = () => { * @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 + /** + * 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, + }); +}; diff --git a/solutions/automations/calendar-timesheet/appsscript.json b/solutions/automations/calendar-timesheet/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/calendar-timesheet/appsscript.json +++ b/solutions/automations/calendar-timesheet/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/content-signup/.clasp.json b/solutions/automations/content-signup/.clasp.json index 0543fbf7b..25a651130 100644 --- a/solutions/automations/content-signup/.clasp.json +++ b/solutions/automations/content-signup/.clasp.json @@ -1 +1 @@ -{"scriptId": "1G8TfU6Rfcl76Uo4gKig7jFMYKai-V_fiUNbO12pAb25pA4_uyxN5PSvd"} +{ "scriptId": "1G8TfU6Rfcl76Uo4gKig7jFMYKai-V_fiUNbO12pAb25pA4_uyxN5PSvd" } diff --git a/solutions/automations/content-signup/Code.js b/solutions/automations/content-signup/Code.js index e48e64cd7..967cbde23 100644 --- a/solutions/automations/content-signup/Code.js +++ b/solutions/automations/content-signup/Code.js @@ -19,72 +19,76 @@ 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'; +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'; +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', + "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(); + 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); - - console.log("status=" + status + "; responses=" + JSON.stringify(responses)); + const 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. + const timestamp = responses.Timestamp[0]; + const email = responses["Email address"][0].trim(); + const name = responses.Name[0].trim(); + const 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). + const topics = Object.keys(topicUrls).filter((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. + const sheet = SpreadsheetApp.getActiveSheet(); + const row = sheet.getActiveRange().getRow(); + const column = e.values.length + 1; + sheet.getRange(row, column).setValue(status); + + console.log("status=" + status + "; responses=" + JSON.stringify(responses)); } /** @@ -95,35 +99,38 @@ function onFormSubmit(e) { * @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; + let topicsHtml = topics + .map((topic) => { + const url = topicUrls[topic]; + return '
  • ' + topic + "
  • "; + }) + .join(""); + topicsHtml = "
      " + topicsHtml + "
    "; + + // Make sure to update the emailTemplateDocId at the top. + const 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(); + // Downloads a Google Doc as an HTML string. + const url = + "https://docs.google.com/feeds/download/documents/export/Export?id=" + + docId + + "&exportFormat=html"; + const param = { + method: "get", + headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() }, + muteHttpExceptions: true, + }; + return UrlFetchApp.fetch(url, param).getContentText(); } diff --git a/solutions/automations/content-signup/appsscript.json b/solutions/automations/content-signup/appsscript.json index ba13ec1c1..668c2f68f 100644 --- a/solutions/automations/content-signup/appsscript.json +++ b/solutions/automations/content-signup/appsscript.json @@ -1,14 +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 + "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" +} diff --git a/solutions/automations/course-feedback-response/.clasp.json b/solutions/automations/course-feedback-response/.clasp.json index ae51645f3..8a4805751 100644 --- a/solutions/automations/course-feedback-response/.clasp.json +++ b/solutions/automations/course-feedback-response/.clasp.json @@ -1 +1 @@ -{"scriptId": "1k75E4EdC3TcJEGGIupBANjm5duvs35ORAU1Mg2_6DNXENo827dFzmFeC"} +{ "scriptId": "1k75E4EdC3TcJEGGIupBANjm5duvs35ORAU1Mg2_6DNXENo827dFzmFeC" } diff --git a/solutions/automations/course-feedback-response/Code.js b/solutions/automations/course-feedback-response/Code.js index 99dbad61f..8902e2d24 100644 --- a/solutions/automations/course-feedback-response/Code.js +++ b/solutions/automations/course-feedback-response/Code.js @@ -21,20 +21,20 @@ 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(); + const 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(); + ScriptApp.newTrigger("onFormSubmit") + .forSpreadsheet(SpreadsheetApp.getActive()) + .onFormSubmit() + .create(); } /** @@ -43,17 +43,17 @@ function installTrigger() { * @param {Object} event - Form submit event */ function onFormSubmit(e) { - let responses = e.namedValues; + const responses = e.namedValues; - // parse form response data - let timestamp = responses.Timestamp[0]; - let email = responses['Email address'][0].trim(); + // parse form response data + const timestamp = responses.Timestamp[0]; + const email = responses["Email address"][0].trim(); - // create email body - let emailBody = createEmailBody(responses); + // create email body + const emailBody = createEmailBody(responses); - // create draft email - createDraft(timestamp, email, emailBody); + // create draft email + createDraft(timestamp, email, emailBody); } /** @@ -63,35 +63,45 @@ function onFormSubmit(e) { * @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; + // parse form response data + const name = responses.Name[0].trim(); + const industry = responses["What industry do you work in?"][0]; + const source = responses["How did you find out about this course?"][0]; + const rating = + responses["On a scale of 1 - 5 how would you rate this course?"][0]; + const productFeedback = + responses["What could be different to make it a 5 rating?"][0]; + const otherFeedback = responses["Any other feedback?"][0]; + + // create email body + const 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; } /** @@ -102,18 +112,13 @@ function createEmailBody(responses) { * @param {string} emailBody The email body as an HTML string */ function createDraft(timestamp, email, emailBody) { - console.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, - } - ); + console.log("draft email create process started"); + + // create subject line + const subjectLine = "Thanks for your course feedback! " + timestamp; + + // create draft email + GmailApp.createDraft(email, subjectLine, "", { + htmlBody: emailBody, + }); } diff --git a/solutions/automations/course-feedback-response/appsscript.json b/solutions/automations/course-feedback-response/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/course-feedback-response/appsscript.json +++ b/solutions/automations/course-feedback-response/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/employee-certificate/.clasp.json b/solutions/automations/employee-certificate/.clasp.json index 89f032f45..93049a3bc 100644 --- a/solutions/automations/employee-certificate/.clasp.json +++ b/solutions/automations/employee-certificate/.clasp.json @@ -1 +1 @@ -{"scriptId": "1f0EhMh_a2Jtq3DS96ZWeG2XviJ-XHSStB2B3mVXODPz3KyojS7nFRzV-"} +{ "scriptId": "1f0EhMh_a2Jtq3DS96ZWeG2XviJ-XHSStB2B3mVXODPz3KyojS7nFRzV-" } diff --git a/solutions/automations/employee-certificate/Code.js b/solutions/automations/employee-certificate/Code.js index 8d2a63ad3..c073ee2bc 100644 --- a/solutions/automations/employee-certificate/Code.js +++ b/solutions/automations/employee-certificate/Code.js @@ -17,20 +17,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -const slideTemplateId = 'PRESENTATION_ID'; -const tempFolderId = 'FOLDER_ID'; // Create an empty folder in Google Drive +const slideTemplateId = "PRESENTATION_ID"; +const tempFolderId = "FOLDER_ID"; // Create an empty folder in Google Drive /** * Creates a custom menu "Appreciation" in the spreadsheet * with drop-down options to create and send certificates */ function onOpen() { - const ui = SpreadsheetApp.getUi(); - ui.createMenu('Appreciation') - .addItem('Create certificates', 'createCertificates') - .addSeparator() - .addItem('Send certificates', 'sendCertificates') - .addToUi(); + const ui = SpreadsheetApp.getUi(); + ui.createMenu("Appreciation") + .addItem("Create certificates", "createCertificates") + .addSeparator() + .addItem("Send certificates", "sendCertificates") + .addToUi(); } /** @@ -38,48 +38,56 @@ function onOpen() { * and stores every individual Slides doc on Google Drive */ function createCertificates() { - // Load the Google Slide template file - const template = DriveApp.getFileById(slideTemplateId); - - // Get all employee data from the spreadsheet and identify the headers - const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); - const values = sheet.getDataRange().getValues(); - const headers = values[0]; - const empNameIndex = headers.indexOf('Employee Name'); - const dateIndex = headers.indexOf('Date'); - const managerNameIndex = headers.indexOf('Manager Name'); - const titleIndex = headers.indexOf('Title'); - const compNameIndex = headers.indexOf('Company Name'); - const empEmailIndex = headers.indexOf('Employee Email'); - const empSlideIndex = headers.indexOf('Employee Slide'); - const statusIndex = headers.indexOf('Status'); - - // Iterate through each row to capture individual details - for (let i = 1; i < values.length; i++) { - const rowData = values[i]; - const empName = rowData[empNameIndex]; - const date = rowData[dateIndex]; - const managerName = rowData[managerNameIndex]; - const title = rowData[titleIndex]; - const compName = rowData[compNameIndex]; - - // Make a copy of the Slide template and rename it with employee name - const tempFolder = DriveApp.getFolderById(tempFolderId); - const empSlideId = template.makeCopy(tempFolder).setName(empName).getId(); - const empSlide = SlidesApp.openById(empSlideId).getSlides()[0]; - - // Replace placeholder values with actual employee related details - empSlide.replaceAllText('Employee Name', empName); - empSlide.replaceAllText('Date', 'Date: ' + Utilities.formatDate(date, Session.getScriptTimeZone(), 'MMMM dd, yyyy')); - empSlide.replaceAllText('Your Name', managerName); - empSlide.replaceAllText('Title', title); - empSlide.replaceAllText('Company Name', compName); - - // Update the spreadsheet with the new Slide Id and status - sheet.getRange(i + 1, empSlideIndex + 1).setValue(empSlideId); - sheet.getRange(i + 1, statusIndex + 1).setValue('CREATED'); - SpreadsheetApp.flush(); - } + // Load the Google Slide template file + const template = DriveApp.getFileById(slideTemplateId); + + // Get all employee data from the spreadsheet and identify the headers + const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); + const values = sheet.getDataRange().getValues(); + const headers = values[0]; + const empNameIndex = headers.indexOf("Employee Name"); + const dateIndex = headers.indexOf("Date"); + const managerNameIndex = headers.indexOf("Manager Name"); + const titleIndex = headers.indexOf("Title"); + const compNameIndex = headers.indexOf("Company Name"); + const empEmailIndex = headers.indexOf("Employee Email"); + const empSlideIndex = headers.indexOf("Employee Slide"); + const statusIndex = headers.indexOf("Status"); + + // Iterate through each row to capture individual details + for (let i = 1; i < values.length; i++) { + const rowData = values[i]; + const empName = rowData[empNameIndex]; + const date = rowData[dateIndex]; + const managerName = rowData[managerNameIndex]; + const title = rowData[titleIndex]; + const compName = rowData[compNameIndex]; + + // Make a copy of the Slide template and rename it with employee name + const tempFolder = DriveApp.getFolderById(tempFolderId); + const empSlideId = template.makeCopy(tempFolder).setName(empName).getId(); + const empSlide = SlidesApp.openById(empSlideId).getSlides()[0]; + + // Replace placeholder values with actual employee related details + empSlide.replaceAllText("Employee Name", empName); + empSlide.replaceAllText( + "Date", + "Date: " + + Utilities.formatDate( + date, + Session.getScriptTimeZone(), + "MMMM dd, yyyy", + ), + ); + empSlide.replaceAllText("Your Name", managerName); + empSlide.replaceAllText("Title", title); + empSlide.replaceAllText("Company Name", compName); + + // Update the spreadsheet with the new Slide Id and status + sheet.getRange(i + 1, empSlideIndex + 1).setValue(empSlideId); + sheet.getRange(i + 1, statusIndex + 1).setValue("CREATED"); + SpreadsheetApp.flush(); + } } /** @@ -87,45 +95,48 @@ function createCertificates() { * with a PDF attachment of their appreciation certificate */ function sendCertificates() { - // Get all employee data from the spreadsheet and identify the headers - const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); - const values = sheet.getDataRange().getValues(); - const headers = values[0]; - const empNameIndex = headers.indexOf('Employee Name'); - const dateIndex = headers.indexOf('Date'); - const managerNameIndex = headers.indexOf('Manager Name'); - const titleIndex = headers.indexOf('Title'); - const compNameIndex = headers.indexOf('Company Name'); - const empEmailIndex = headers.indexOf('Employee Email'); - const empSlideIndex = headers.indexOf('Employee Slide'); - const statusIndex = headers.indexOf('Status'); - - // Iterate through each row to capture individual details - for (let i = 1; i < values.length; i++) { - const rowData = values[i]; - const empName = rowData[empNameIndex]; - const date = rowData[dateIndex]; - const managerName = rowData[managerNameIndex]; - const title = rowData[titleIndex]; - const compName = rowData[compNameIndex]; - const empSlideId = rowData[empSlideIndex]; - const empEmail = rowData[empEmailIndex]; - - // Load the employee's personalized Google Slide file - const attachment = DriveApp.getFileById(empSlideId); - - // Setup the required parameters and send them the email - const senderName = 'CertBot'; - const subject = empName + ', you\'re awesome!'; - const body = 'Please find your employee appreciation certificate attached.' + - '\n\n' + compName + ' team'; - GmailApp.sendEmail(empEmail, subject, body, { - attachments: [attachment.getAs(MimeType.PDF)], - name: senderName - }); - - // Update the spreadsheet with email status - sheet.getRange(i + 1, statusIndex + 1).setValue('SENT'); - SpreadsheetApp.flush(); - } + // Get all employee data from the spreadsheet and identify the headers + const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); + const values = sheet.getDataRange().getValues(); + const headers = values[0]; + const empNameIndex = headers.indexOf("Employee Name"); + const dateIndex = headers.indexOf("Date"); + const managerNameIndex = headers.indexOf("Manager Name"); + const titleIndex = headers.indexOf("Title"); + const compNameIndex = headers.indexOf("Company Name"); + const empEmailIndex = headers.indexOf("Employee Email"); + const empSlideIndex = headers.indexOf("Employee Slide"); + const statusIndex = headers.indexOf("Status"); + + // Iterate through each row to capture individual details + for (let i = 1; i < values.length; i++) { + const rowData = values[i]; + const empName = rowData[empNameIndex]; + const date = rowData[dateIndex]; + const managerName = rowData[managerNameIndex]; + const title = rowData[titleIndex]; + const compName = rowData[compNameIndex]; + const empSlideId = rowData[empSlideIndex]; + const empEmail = rowData[empEmailIndex]; + + // Load the employee's personalized Google Slide file + const attachment = DriveApp.getFileById(empSlideId); + + // Setup the required parameters and send them the email + const senderName = "CertBot"; + const subject = empName + ", you're awesome!"; + const body = + "Please find your employee appreciation certificate attached." + + "\n\n" + + compName + + " team"; + GmailApp.sendEmail(empEmail, subject, body, { + attachments: [attachment.getAs(MimeType.PDF)], + name: senderName, + }); + + // Update the spreadsheet with email status + sheet.getRange(i + 1, statusIndex + 1).setValue("SENT"); + SpreadsheetApp.flush(); + } } diff --git a/solutions/automations/employee-certificate/appsscript.json b/solutions/automations/employee-certificate/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/employee-certificate/appsscript.json +++ b/solutions/automations/employee-certificate/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/equipment-requests/.clasp.json b/solutions/automations/equipment-requests/.clasp.json index a4c52c62a..d097f4a6d 100644 --- a/solutions/automations/equipment-requests/.clasp.json +++ b/solutions/automations/equipment-requests/.clasp.json @@ -1 +1 @@ -{"scriptId": "1T0G2Qr0QkHfqOK8dqjdiMRGuX2UVzkQU3BGfl2lC3wsNwkSmISbp2q6t"} +{ "scriptId": "1T0G2Qr0QkHfqOK8dqjdiMRGuX2UVzkQU3BGfl2lC3wsNwkSmISbp2q6t" } diff --git a/solutions/automations/equipment-requests/Code.js b/solutions/automations/equipment-requests/Code.js index 528bb4c5c..9dd92e360 100644 --- a/solutions/automations/equipment-requests/Code.js +++ b/solutions/automations/equipment-requests/Code.js @@ -18,27 +18,23 @@ 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'; +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)', + '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"', + "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. @@ -46,65 +42,61 @@ const AVAILABLE_MONITORS = [ * Adds a custom menu to the spreadsheet. */ function onOpen() { - SpreadsheetApp.getUi().createMenu('Equipment requests') - .addItem('Set up', 'setup_') - .addItem('Clean up', 'cleanup_') - .addToUi(); + 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(); + const ss = SpreadsheetApp.getActiveSpreadsheet(); + if (ss.getFormUrl()) { + const msg = "Form already exists. Unlink the form and try again."; + SpreadsheetApp.getUi().alert(msg); + return; + } + const 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((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); + const formUrl = SpreadsheetApp.getActiveSpreadsheet().getFormUrl(); + if (!formUrl) { + return; + } + ScriptApp.getProjectTriggers().forEach((trigger) => { + ScriptApp.deleteTrigger(trigger); + }); + FormApp.openByUrl(formUrl).deleteAllResponses().setAcceptingResponses(false); } /** @@ -113,22 +105,26 @@ function cleanup_() { * @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); + const response = mapResponse_(event.response); + sendNewEquipmentRequestEmail_(response); + const equipmentDetails = Utilities.formatString( + "%s\n%s\n%s", + response["Laptop"], + response["Desktop"], + response["Monitor"], + ); + const row = [ + "New", + "", + response["Due date"], + response["Employee name"], + response["Desk location"], + equipmentDetails, + response["email"], + ]; + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheet = ss.getSheetByName("Pending requests"); + sheet.appendRow(row); } /** @@ -138,24 +134,24 @@ function onFormSubmit_(event) { * @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], - }); - } - }; + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const pending = ss.getSheetByName("Pending requests"); + const completed = ss.getSheetByName("Completed requests"); + const rows = pending.getDataRange().getValues(); + for (let i = rows.length; i >= 2; i--) { + const row = rows[i - 1]; + const 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], + }); + } + } } /** @@ -164,15 +160,17 @@ function processCompletedItems_() { * @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(), - }); + const template = HtmlService.createTemplateFromFile( + "new-equipment-request.html", + ); + template.request = request; + template.sheetUrl = SpreadsheetApp.getActiveSpreadsheet().getUrl(); + const msg = template.evaluate(); + MailApp.sendEmail({ + to: REQUEST_NOTIFICATION_EMAIL, + subject: "New equipment request", + htmlBody: msg.getContent(), + }); } /** @@ -181,14 +179,14 @@ function sendNewEquipmentRequestEmail_(request) { * @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(), - }); + const template = HtmlService.createTemplateFromFile("request-complete.html"); + template.request = request; + const msg = template.evaluate(); + MailApp.sendEmail({ + to: request.email, + subject: "Equipment request completed", + htmlBody: msg.getContent(), + }); } /** @@ -199,14 +197,13 @@ function sendEquipmentRequestCompletedEmail_(request) { * @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); + const initialValue = { + email: response.getRespondentEmail(), + timestamp: response.getTimestamp(), + }; + return response.getItemResponses().reduce((obj, itemResponse) => { + const key = itemResponse.getItem().getTitle(); + obj[key] = itemResponse.getResponse(); + return obj; + }, initialValue); } - diff --git a/solutions/automations/equipment-requests/appsscript.json b/solutions/automations/equipment-requests/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/equipment-requests/appsscript.json +++ b/solutions/automations/equipment-requests/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/event-session-signup/.clasp.json b/solutions/automations/event-session-signup/.clasp.json index 8b1357cc3..1242d5272 100644 --- a/solutions/automations/event-session-signup/.clasp.json +++ b/solutions/automations/event-session-signup/.clasp.json @@ -1 +1 @@ -{"scriptId": "1RTfpaBw-RYW8PTJsidiqXHRrqaKnwMWAK_nq4LnWk9xXKGJWi_bhexRj"} +{ "scriptId": "1RTfpaBw-RYW8PTJsidiqXHRrqaKnwMWAK_nq4LnWk9xXKGJWi_bhexRj" } diff --git a/solutions/automations/event-session-signup/Code.js b/solutions/automations/event-session-signup/Code.js index dda1bd589..24fc6c6f4 100644 --- a/solutions/automations/event-session-signup/Code.js +++ b/solutions/automations/event-session-signup/Code.js @@ -21,9 +21,10 @@ limitations under the License. * Inserts a custom menu when the spreadsheet opens. */ function onOpen() { - SpreadsheetApp.getUi().createMenu('Conference') - .addItem('Set up conference', 'setUpConference_') - .addToUi(); + SpreadsheetApp.getUi() + .createMenu("Conference") + .addItem("Set up conference", "setUpConference_") + .addToUi(); } /** @@ -32,20 +33,24 @@ function onOpen() { * 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(); + const 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; + } + const ss = SpreadsheetApp.getActive(); + const sheet = ss.getSheetByName("Conference Setup"); + const range = sheet.getDataRange(); + const values = range.getValues(); + setUpCalendar_(values, range); + setUpForm_(ss, values); + ScriptApp.newTrigger("onFormSubmit") + .forSpreadsheet(ss) + .onFormSubmit() + .create(); } /** @@ -55,23 +60,24 @@ function setUpConference_() { * @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()); + const cal = CalendarApp.createCalendar("Conference Calendar"); + // Start at 1 to skip the header row. + for (let i = 1; i < values.length; i++) { + const session = values[i]; + const title = session[0]; + const start = joinDateAndTime_(session[1], session[2]); + const end = joinDateAndTime_(session[1], session[3]); + const options = { location: session[4], sendInvites: true }; + const 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. + const scriptProperties = PropertiesService.getScriptProperties(); + scriptProperties.setProperty("calId", cal.getId()); } /** @@ -82,10 +88,10 @@ function setUpCalendar_(values, range) { * @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; + date = new Date(date); + date.setHours(time.getHours()); + date.setMinutes(time.getMinutes()); + return date; } /** @@ -97,34 +103,36 @@ function joinDateAndTime_(date, time) { * @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]); - }); - }); + // Group the sessions by date and time so that they can be passed to the form. + const schedule = {}; + // Start at 1 to skip the header row. + for (let i = 1; i < values.length; i++) { + const session = values[i]; + const day = session[1].toLocaleDateString(); + const 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. + const 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((day) => { + const header = form.addSectionHeaderItem().setTitle("Sessions for " + day); + Object.keys(schedule[day]).forEach((time) => { + const item = form + .addMultipleChoiceItem() + .setTitle(time + " " + day) + .setChoiceValues(schedule[day][time]); + }); + }); } /** @@ -135,27 +143,32 @@ function setUpForm_(ss, values) { * 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); + const 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. + const response = []; + const values = SpreadsheetApp.getActive() + .getSheetByName("Conference Setup") + .getDataRange() + .getValues(); + for (let i = 1; i < values.length; i++) { + const session = values[i]; + const title = session[0]; + const day = session[1].toLocaleDateString(); + const time = session[2].toLocaleTimeString(); + const 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); } /** @@ -164,11 +177,11 @@ function onFormSubmit(e) { * @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); - } + const id = ScriptProperties.getProperty("calId"); + const cal = CalendarApp.getCalendarById(id); + for (let i = 0; i < response.length; i++) { + cal.getEventSeriesById(response[i][5]).addGuest(user.email); + } } /** @@ -177,33 +190,39 @@ function sendInvites_(user, response) { * @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), - }); + const doc = DocumentApp.create( + "Conference Itinerary for " + user.name, + ).addEditor(user.email); + const 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(); +function resetProperties() { + const scriptProperties = PropertiesService.getScriptProperties(); + scriptProperties.deleteAllProperties(); } diff --git a/solutions/automations/event-session-signup/appsscript.json b/solutions/automations/event-session-signup/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/event-session-signup/appsscript.json +++ b/solutions/automations/event-session-signup/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/feedback-sentiment-analysis/.clasp.json b/solutions/automations/feedback-sentiment-analysis/.clasp.json index 92fa17d1d..b17a7fcd7 100644 --- a/solutions/automations/feedback-sentiment-analysis/.clasp.json +++ b/solutions/automations/feedback-sentiment-analysis/.clasp.json @@ -1 +1 @@ -{"scriptId":"1LOheMLQDlSkvmlt8EQOGGETewdt8tKWyzxspCwqzfianqxTXjBGpAc8c"} \ No newline at end of file +{ "scriptId": "1LOheMLQDlSkvmlt8EQOGGETewdt8tKWyzxspCwqzfianqxTXjBGpAc8c" } diff --git a/solutions/automations/feedback-sentiment-analysis/appsscript.json b/solutions/automations/feedback-sentiment-analysis/appsscript.json index cc6038b7a..4dd859db7 100644 --- a/solutions/automations/feedback-sentiment-analysis/appsscript.json +++ b/solutions/automations/feedback-sentiment-analysis/appsscript.json @@ -1,12 +1,14 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "libraries": [{ - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "24" - }] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "24" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/feedback-sentiment-analysis/code.js b/solutions/automations/feedback-sentiment-analysis/code.js index b9ce619b1..27f9a0968 100644 --- a/solutions/automations/feedback-sentiment-analysis/code.js +++ b/solutions/automations/feedback-sentiment-analysis/code.js @@ -18,90 +18,113 @@ limitations under the License. */ // Sets API key for accessing Cloud Natural Language API. -const myApiKey = 'YOUR_API_KEY'; // Replace with your API key. +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' +const 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(); -}; + 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. -*/ + * 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"); - } - } -}; + // Sets variables for "Review Data" sheet + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const dataSheet = ss.getSheetByName("Review Data"); + const rows = dataSheet.getDataRange(); + const numRows = rows.getNumRows(); + const values = rows.getValues(); + const 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. + const entitySheet = ss.getSheetByName("Entity Sentiment Data"); + if (entitySheet == null) { + ss.insertSheet("Entity Sentiment Data"); + const entitySheet = ss.getSheetByName("Entity Sentiment Data"); + const esHeaderRange = entitySheet.getRange(1, 1, 1, 6); + const 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. + const textColumnIdx = headerRow.indexOf(COLUMN_NAME.COMMENTS); + const entityColumnIdx = headerRow.indexOf(COLUMN_NAME.ENTITY); + const 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) { + const value = values[i]; + const commentEnCellVal = value[textColumnIdx]; + const entityCellVal = value[entityColumnIdx]; + const reviewId = value[idColumnIdx]; + + // Calls retrieveEntitySentiment function for each row that has a comment + // and also an empty entity_sentiment cell value. + if (commentEnCellVal && !entityCellVal) { + const nlData = retrieveEntitySentiment(commentEnCellVal); + // Pastes each entity and sentiment score into Entity Sentiment Data sheet. + const newValues = []; + for (let entity in nlData.entities) { + entity = nlData.entities[entity]; + const 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 @@ -109,25 +132,27 @@ function markEntitySentiment() { * @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 +function retrieveEntitySentiment(line) { + const apiKey = myApiKey; + const apiEndpoint = + "https://language.googleapis.com/v1/documents:analyzeEntitySentiment?key=" + + apiKey; + // Creates a JSON request, with text string, language, type and encoding + const 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. + const nlOptions = { + method: "post", + contentType: "application/json", + payload: JSON.stringify(nlData), + }; + // Makes the API call. + const response = UrlFetchApp.fetch(apiEndpoint, nlOptions); + return JSON.parse(response); +} diff --git a/solutions/automations/folder-creation/Code.js b/solutions/automations/folder-creation/Code.js index 75e4b014c..d7e678c0a 100644 --- a/solutions/automations/folder-creation/Code.js +++ b/solutions/automations/folder-creation/Code.js @@ -19,15 +19,15 @@ Please watch this video tutorial to see how to use this script: https://youtu.be */ function createNewFolder(project) { - const folder = Drive.Files.insert( - { - parents: [{ id: 'ADD YOUR SHARED DRIVE FOLDER ID HERE' }], - title: project, - mimeType: "application/vnd.google-apps.folder", - }, - null, - { supportsAllDrives: true } - ); + const folder = Drive.Files.insert( + { + parents: [{ id: "ADD YOUR SHARED DRIVE FOLDER ID HERE" }], + title: project, + mimeType: "application/vnd.google-apps.folder", + }, + null, + { supportsAllDrives: true }, + ); - return folder.alternateLink; + return folder.alternateLink; } diff --git a/solutions/automations/folder-creation/appscript.json b/solutions/automations/folder-creation/appscript.json index de9678f42..0389303b6 100644 --- a/solutions/automations/folder-creation/appscript.json +++ b/solutions/automations/folder-creation/appscript.json @@ -1,14 +1,14 @@ { - "timeZone": "Europe/Madrid", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v2", - "serviceId": "drive" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" - } \ No newline at end of file + "timeZone": "Europe/Madrid", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v2", + "serviceId": "drive" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/generate-pdfs/.clasp.json b/solutions/automations/generate-pdfs/.clasp.json index 77e4f7125..99e72b31f 100644 --- a/solutions/automations/generate-pdfs/.clasp.json +++ b/solutions/automations/generate-pdfs/.clasp.json @@ -1 +1 @@ -{"scriptId": "1k9PjGdQ_G0HKEoS3np_Szfe-flmLw9gUvblQIxOfvTmS-NLeLgVUzvOa"} +{ "scriptId": "1k9PjGdQ_G0HKEoS3np_Szfe-flmLw9gUvblQIxOfvTmS-NLeLgVUzvOa" } diff --git a/solutions/automations/generate-pdfs/Code.js b/solutions/automations/generate-pdfs/Code.js index 07e77a86b..2f8e178b6 100644 --- a/solutions/automations/generate-pdfs/Code.js +++ b/solutions/automations/generate-pdfs/Code.js @@ -19,129 +19,165 @@ 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'; +const EMAIL_ADDRESS_OVERRIDE = "test@example.com"; // Application constants -const APP_TITLE = 'Generate and send PDFs'; +const APP_TITLE = "Generate and send PDFs"; const OUTPUT_FOLDER_NAME = "Customer PDFs"; -const DUE_DATE_NUM_DAYS = 15 +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'; +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.'; - +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 + * 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); + 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((customer) => { + ss.toast(`Creating Invoice for ${customer.customer_name}`, APP_TITLE, 1); + const 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 + * @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']; +function createInvoiceForCustomer( + customer, + products, + transactions, + templateSheet, + ssId, +) { + const customerTransactions = transactions.filter( + (transaction) => transaction.customer_name == customer.customer_name, + ); + + // Clears existing data from the template. + clearTemplateSheet(); + + const lineItems = []; + let totalAmount = 0; + customerTransactions.forEach((lineItem) => { + const lineItemProduct = products.filter( + (product) => product.sku_name == lineItem.sku, + )[0]; + const qty = Number.parseInt(lineItem.licenses); + const price = Number.parseFloat(lineItemProduct.price).toFixed(2); + const amount = Number.parseFloat(qty * price).toFixed(2); + lineItems.push([ + lineItemProduct.sku_name, + lineItemProduct.sku_description, + "", + qty, + price, + amount, + ]); + totalAmount += Number.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. -*/ + * 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(); + 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((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(); } /** @@ -152,112 +188,132 @@ function clearTemplateSheet() { * @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; + 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'); - } - }); + 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((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. - * + * 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 + * 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)); + 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; + const objects = []; + for (let i = 0; i < data.length; ++i) { + const object = {}; + let hasData = false; + for (let j = 0; j < data[i].length; ++j) { + const 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(); - }); + return keys.map((key) => 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 == ""; + return typeof cellData == "string" && cellData == ""; } diff --git a/solutions/automations/generate-pdfs/Menu.js b/solutions/automations/generate-pdfs/Menu.js index 00fad4705..ed0b92730 100644 --- a/solutions/automations/generate-pdfs/Menu.js +++ b/solutions/automations/generate-pdfs/Menu.js @@ -29,12 +29,11 @@ * @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 + const menu = SpreadsheetApp.getUi().createMenu(APP_TITLE); + menu + .addItem("Process invoices", "processDocuments") + .addItem("Send emails", "sendEmails") + .addSeparator() + .addItem("Reset template", "clearTemplateSheet") + .addToUi(); +} diff --git a/solutions/automations/generate-pdfs/Utilities.js b/solutions/automations/generate-pdfs/Utilities.js index 4e7e60908..22f61942c 100644 --- a/solutions/automations/generate-pdfs/Utilities.js +++ b/solutions/automations/generate-pdfs/Utilities.js @@ -15,34 +15,36 @@ */ /** - * Returns a Google Drive folder in the same location + * 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. + * @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(); - // 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()) { + const folder = subFolders.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`); + // 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`, + ); } /** @@ -50,11 +52,12 @@ function getFolderByName_(folderName) { * @prints a Google Drive FolderId. */ function test_getFolderByName() { + // Gets the PDF folder in Drive. + const folder = getFolderByName_(OUTPUT_FOLDER_NAME); - // 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 + console.log( + `Name: ${folder.getName()}\rID: ${folder.getId()}\rDescription: ${folder.getDescription()}`, + ); + // To automatically delete test folder, uncomment the following code: + // folder.setTrashed(true); +} diff --git a/solutions/automations/generate-pdfs/appsscript.json b/solutions/automations/generate-pdfs/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/generate-pdfs/appsscript.json +++ b/solutions/automations/generate-pdfs/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/import-csv-sheets/.clasp.json b/solutions/automations/import-csv-sheets/.clasp.json index f6b8887eb..a49e9df63 100644 --- a/solutions/automations/import-csv-sheets/.clasp.json +++ b/solutions/automations/import-csv-sheets/.clasp.json @@ -1 +1 @@ -{"scriptId": "1ANsCqbcTeepCzPpAKRUSxavm-2bTtKhp6I-G530ddH315H-59LGofc6m"} +{ "scriptId": "1ANsCqbcTeepCzPpAKRUSxavm-2bTtKhp6I-G530ddH315H-59LGofc6m" } diff --git a/solutions/automations/import-csv-sheets/Code.js b/solutions/automations/import-csv-sheets/Code.js index 2b89d8253..25d09a2a3 100644 --- a/solutions/automations/import-csv-sheets/Code.js +++ b/solutions/automations/import-csv-sheets/Code.js @@ -1,4 +1,4 @@ -// To learn more about this script, refer to the documentation: +// To learn more about this script, refer to the documentation: // https://developers.google.com/apps-script/samples/automations/import-csv-sheets /* @@ -17,175 +17,178 @@ 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. +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. +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.`); + // 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. + const 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}`); + // 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} + const objSpreadSheet = getSpreadSheet_(SHEET_REPORT_NAME, folderAppPrimary); + + // Creates arrays to track every CSV file, categorized as processed sucessfully or not. + const filesProcessed = []; + const filesNotProcessed = []; + + // Gets all CSV files found in the source folder. + const cvsFiles = folderSource.getFilesByType(MimeType.CSV); + + // Iterates through each CSV file. + while (cvsFiles.hasNext()) { + const 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. - } + try { + // Gets the first sheet of the destination spreadsheet. + const sheet = objSpreadSheet.getSheets()[0]; + + // Parses CSV file into data array. + const 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. + const startRow = sheet.getLastRow() + 1; + const startCol = 1; + // Determines the incoming data size. + const numRows = data.length; + const 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/SampleData.js b/solutions/automations/import-csv-sheets/SampleData.js index 4fb3afec1..8a20d79e6 100644 --- a/solutions/automations/import-csv-sheets/SampleData.js +++ b/solutions/automations/import-csv-sheets/SampleData.js @@ -14,131 +14,130 @@ * limitations under the License. */ -/** +/** * 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" - } - ] - } - ] -} - + 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) + const headings = [[]]; + for (const i in SAMPLE_DATA.headings) + headings[0].push(SAMPLE_DATA.headings[i]); + return headings; } /** @@ -146,45 +145,44 @@ function getHeadings() { * @return {object[]} {"file": ["name","csv"]} */ function getCSVFilesData() { + const files = []; - 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 headings once - same for all files/rows. + let csvHeadings = ""; + for (const 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) + // Gets data for each file by rows. + for (const i in SAMPLE_DATA.csvFiles) { + let sampleCSV = ""; + sampleCSV += csvHeadings; + const fileName = SAMPLE_DATA.csvFiles[i].name; + for (const j in SAMPLE_DATA.csvFiles[i].rows) { + sampleCSV += "\n"; + for (const 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); + const h = getHeadings(); + console.log(h); + console.log(h[0].length); } function test_getCSVFilesData() { - const csvFiles = getCSVFilesData(); - console.log(csvFiles) + 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 + for (const file of csvFiles) { + console.log(file.name); + console.log(file.csv); + } +} diff --git a/solutions/automations/import-csv-sheets/SetupSample.js b/solutions/automations/import-csv-sheets/SetupSample.js index f7670293e..b6e828761 100644 --- a/solutions/automations/import-csv-sheets/SetupSample.js +++ b/solutions/automations/import-csv-sheets/SetupSample.js @@ -14,9 +14,9 @@ * limitations under the License. */ -/** +/** * 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. */ @@ -24,72 +24,79 @@ const INCLUDE_SAMPLE_DATA_FILES = true; // Set to true to create sample data files, false to skip. /** - * Runs the setup for the sample. + * 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 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}`) + 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)) { + const 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; - } + // 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. + const 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; + } } /** @@ -97,15 +104,19 @@ function setupPrimarySpreadsheet_(folderAppPrimary) { * 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 + 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]); + } + } +} diff --git a/solutions/automations/import-csv-sheets/Utilities.js b/solutions/automations/import-csv-sheets/Utilities.js index f37b81a20..e339dcf4e 100644 --- a/solutions/automations/import-csv-sheets/Utilities.js +++ b/solutions/automations/import-csv-sheets/Utilities.js @@ -14,83 +14,81 @@ * limitations under the License. */ -/** +/** * 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. - * + * 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) { + const files = objFolder.getFilesByName(fileName); - let files = objFolder.getFilesByName(fileName); + while (files.hasNext()) { + const file = files.next(); + const fileId = file.getId(); - while (files.hasNext()) { - let file = files.next(); - let fileId = file.getId(); + const existingSpreadsheet = SpreadsheetApp.openById(fileId); + return existingSpreadsheet; + } - 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; + // 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; + const files = objFolder.getFilesByName(fileName); + + while (files.hasNext()) { + const file = files.next(); + console.log(`${file.getName()} already exists.`); + return true; + } + return false; } -/** - * Returns folder named in folderName parameter. +/** + * 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. + * @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}.`); + // 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()) { + const 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. @@ -98,23 +96,23 @@ function getFolder_(folderName) { * @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}.`); + // 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()) { + const 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}.`); } /** @@ -122,21 +120,24 @@ function getApplicationFolder_() { * @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 + 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); +} diff --git a/solutions/automations/import-csv-sheets/appsscript.json b/solutions/automations/import-csv-sheets/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/import-csv-sheets/appsscript.json +++ b/solutions/automations/import-csv-sheets/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/mail-merge/.clasp.json b/solutions/automations/mail-merge/.clasp.json index 7f25c6014..0ba082557 100644 --- a/solutions/automations/mail-merge/.clasp.json +++ b/solutions/automations/mail-merge/.clasp.json @@ -1 +1 @@ -{"scriptId": "1evL25lW9fLN43j6gGBJWtLq4GncLkdgoxxSVCawc8dWNoLoravNebAih"} +{ "scriptId": "1evL25lW9fLN43j6gGBJWtLq4GncLkdgoxxSVCawc8dWNoLoravNebAih" } diff --git a/solutions/automations/mail-merge/Code.js b/solutions/automations/mail-merge/Code.js index 7784dd31f..1356e94a3 100644 --- a/solutions/automations/mail-merge/Code.js +++ b/solutions/automations/mail-merge/Code.js @@ -16,195 +16,216 @@ 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 + * Change these to match the column names you are using for email * recipient addresses and email sent column. -*/ -const RECIPIENT_COL = "Recipient"; + */ +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(); + 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.bcc@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'); - }; + */ +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((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.bcc@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 = /]+>/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 (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/appsscript.json b/solutions/automations/mail-merge/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/mail-merge/appsscript.json +++ b/solutions/automations/mail-merge/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/news-sentiment/.clasp.json b/solutions/automations/news-sentiment/.clasp.json index 80b47c971..ef27e3156 100644 --- a/solutions/automations/news-sentiment/.clasp.json +++ b/solutions/automations/news-sentiment/.clasp.json @@ -1 +1 @@ -{"scriptId":"1KHPvTOwE2pd2myZmvX0mbsp8SPlhJBFotNCwflZiP01xmTasNfibG4zl"} \ No newline at end of file +{ "scriptId": "1KHPvTOwE2pd2myZmvX0mbsp8SPlhJBFotNCwflZiP01xmTasNfibG4zl" } diff --git a/solutions/automations/news-sentiment/Code.js b/solutions/automations/news-sentiment/Code.js index 7d084f907..9f41b35f1 100644 --- a/solutions/automations/news-sentiment/Code.js +++ b/solutions/automations/news-sentiment/Code.js @@ -18,20 +18,20 @@ 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 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\")'; + '=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\")'; + '=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'; + '=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; @@ -49,43 +49,41 @@ 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(); + const ui = SpreadsheetApp.getUi(); + ui.createMenu("News Headlines Sentiments") + .addItem("Analyze News Headlines...", "showNewsPrompt") + .addToUi(); } /** - * Prompts user to enter a new headline topic. + * 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'); + //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); + // Builds Menu + const ui = SpreadsheetApp.getUi(); + const 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!'); - } + // Processes the user's response. + const 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!"); + } } /** @@ -93,158 +91,163 @@ function showNewsPrompt() { * the sentiment response column. */ function analyzeNewsHeadlines() { - // Clears and reformats the sheet - reformatSheet(); + // Clears and reformats the sheet + reformatSheet(); - // Gets the headlines array - headlines = getHeadlinesArray(); + // 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 + '!'); - } + // 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"]; + // Fetches headlines for a given topic + const hdlnsResp = []; + const encodedtopic = encodeURIComponent(topic); + ss.toast("Getting headlines for: " + topic); + const response = UrlFetchApp.fetch( + apiEndPointHdr + encodedtopic + "&apiKey=" + newsApiKey, + ); + const results = JSON.parse(response); + const 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); + 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)); - } + // Constructs hdlnsResp as a 2d array. This simplifies syncing to the sheet. + hdlnsResp.push(new Array(newsStory)); + } - return hdlnsResp; + return hdlnsResp; } /** - * For each article cell, calls the Natural Language API to get general sentiment and then updates + * 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...'); + ss.toast("Analyzing the headline sentiments..."); - let articleCount = rows.getNumRows() - 1; - let avg = 0; + const 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!!"); + // Gets sentiment for each row + for (let i = 1; i <= articleCount; i++) { + const headlineCell = rowValues[i][headlineCol]; + if (headlineCell) { + const sentimentData = retrieveSentiment(headlineCell); + const 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); + } + } + const 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. + * + * 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" - }); + // Sets REST call options + const apiEndPoint = + "https://language.googleapis.com/v1/documents:analyzeSentiment?key=" + + googleAPIKey; + const jsonReq = JSON.stringify({ + document: { + type: "PLAIN_TEXT", + content: text, + }, + encodingType: "UTF8", + }); - let options = { - 'method': 'post', - 'contentType': 'application/json', - 'payload': jsonReq - } + const 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; + // Makes the REST call + const response = UrlFetchApp.fetch(apiEndPoint, options); + const responseData = JSON.parse(response); + return responseData; } -// Helper Functions +// 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); + 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"); + 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; - } +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; - } +function getColor(value) { + if (value >= threshold) { + return happyColor; + } else if (value < threshold && value > -threshold) { + return mehColor; + } else if (value <= -threshold) { + return sadColor; + } } /** @@ -252,5 +255,5 @@ function getColor(value){ * Can be expanded if needed. */ function scrub(text) { - return text.replace(/[\‘\,\“\”\"\'\’\-\n\â\€]/g, ' '); -} \ No newline at end of file + return text.replace(/[\‘\,\“\”\"\'\’\-\n\â\€]/g, " "); +} diff --git a/solutions/automations/news-sentiment/appsscript.json b/solutions/automations/news-sentiment/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/news-sentiment/appsscript.json +++ b/solutions/automations/news-sentiment/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/offsite-activity-signup/.clasp.json b/solutions/automations/offsite-activity-signup/.clasp.json index 55715ed13..be18c2d02 100644 --- a/solutions/automations/offsite-activity-signup/.clasp.json +++ b/solutions/automations/offsite-activity-signup/.clasp.json @@ -1 +1 @@ -{"scriptId": "10clpAH4ojSXvTlZaE74rhJ6dDwwkfvi24L_AilGROca5Nds2Jy2oZmvY"} +{ "scriptId": "10clpAH4ojSXvTlZaE74rhJ6dDwwkfvi24L_AilGROca5Nds2Jy2oZmvY" } diff --git a/solutions/automations/offsite-activity-signup/Code.js b/solutions/automations/offsite-activity-signup/Code.js index 5cac3f583..421d88640 100644 --- a/solutions/automations/offsite-activity-signup/Code.js +++ b/solutions/automations/offsite-activity-signup/Code.js @@ -25,11 +25,12 @@ 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(); + const menu = SpreadsheetApp.getUi() + .createMenu("Activities") + .addItem("Create form", "buildForm_") + .addItem("Generate test data", "generateTestData_") + .addItem("Assign activities", "assignActivities_") + .addToUi(); } /** @@ -37,42 +38,47 @@ function onOpen() { * 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); + const ss = SpreadsheetApp.getActiveSpreadsheet(); + if (ss.getFormUrl()) { + const msg = "Form already exists. Unlink the form and try again."; + SpreadsheetApp.getUi().alert(msg); + return; + } + const form = FormApp.create("Activity Signup") + .setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()) + .setAllowResponseEdits(true) + .setLimitOneResponsePerUser(true) + .setCollectEmail(true); + const 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); + // Presents activity ranking as a form grid with each activity as a row and rank as a column. + const rows = loadActivitySchedule_(ss).map( + (activity) => activity.description, + ); + const columns = range_(1, NUM_ITEMS_TO_RANK).map((value) => + Utilities.formatString("%s", toOrdinal_(value)), + ); + const 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']); + form + .addListItem() + .setTitle("Assign other activities if choices are not available?") + .setChoiceValues(["Yes", "No"]); } /** @@ -83,15 +89,13 @@ function buildForm_() { * 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); + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const activities = loadActivitySchedule_(ss); + const activityIds = activities.map((activity) => activity.id); + const attendees = loadAttendeeResponses_(ss, activityIds); + assignWithRandomPriority_(attendees, activities, 2); + writeAttendeeAssignments_(ss, attendees); + writeActivityRosters_(ss, activities); } /** @@ -101,17 +105,21 @@ function assignActivities_() { * @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); - }); - } +function assignWithRandomPriority_( + attendees, + activities, + numActivitiesPerPerson, +) { + const activitiesById = activities.reduce((obj, activity) => { + obj[activity.id] = activity; + return obj; + }, {}); + for (let i = 0; i < numActivitiesPerPerson; ++i) { + const randomizedAttendees = shuffleArray_(attendees); + randomizedAttendees.forEach((attendee) => { + makeChoice_(attendee, activitiesById); + }); + } } /** @@ -121,18 +129,18 @@ function assignWithRandomPriority_(attendees, activities, numActivitiesPerPerson * @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; - } - } + for (let i = 0; i < attendee.preferences.length; ++i) { + const activity = activitiesById[attendee.preferences[i]]; + if (!activity) { + continue; + } + const canJoin = checkAvailability_(attendee, activity); + if (canJoin) { + attendee.assigned.push(activity); + activity.roster.push(attendee); + break; + } + } } /** @@ -144,15 +152,18 @@ function makeChoice_(attendee, activitiesById) { * @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; -}; + if (activity.capacity <= activity.roster.length) { + return false; + } + const timesConflict = attendee.assigned.some( + (assignedActivity) => + !( + assignedActivity.startAt.getTime() > activity.endAt.getTime() || + activity.startAt.getTime() > assignedActivity.endAt.getTime() + ), + ); + return !timesConflict; +} /** * Populates a sheet with the assigned activities for each attendee. @@ -161,24 +172,24 @@ function checkAvailability_(attendee, activity) { * @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()); + const sheet = findOrCreateSheetByName_(ss, "Activities by person"); + sheet.clear(); + sheet.appendRow(["Email address", "Activities"]); + sheet.getRange("B1:1").merge(); + const rows = attendees.map((attendee) => { + // Prefill row to ensure consistent length otherwise + // can't bulk update the sheet with range.setValues() + const row = fillArray_([], ACTIVITIES_PER_PERSON + 1, ""); + row[0] = attendee.email; + attendee.assigned.forEach((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()); } /** @@ -188,21 +199,19 @@ function writeAttendeeAssignments_(ss, attendees) { * @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()); + const sheet = findOrCreateSheetByName_(ss, "Activity rosters"); + sheet.clear(); + var rows = []; + var rows = activities.map((activity) => { + const roster = activity.roster.map((attendee) => 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()); } /** @@ -212,30 +221,42 @@ function writeActivityRosters_(ss, activities) { * @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; + const timeZone = ss.getSpreadsheetTimeZone(); + const sheet = ss.getSheetByName("Activity Schedule"); + const rows = sheet.getSheetValues( + sheet.getFrozenRows() + 1, + 1, + sheet.getLastRow() - 1, + sheet.getLastRow(), + ); + const activities = rows.map((row, index) => { + const name = row[0]; + const startAt = new Date(row[1]); + const endAt = new Date(row[2]); + const capacity = Number.parseInt(row[3]); + const formattedStartAt = Utilities.formatDate( + startAt, + timeZone, + "EEE hh:mm a", + ); + const formattedEndAt = Utilities.formatDate(endAt, timeZone, "hh:mm a"); + const 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; } /** @@ -246,42 +267,45 @@ function loadActivitySchedule_(ss) { * @return {object[]} Array of parsed attendee respones. */ function loadAttendeeResponses_(ss, allActivityIds) { - let sheet = findResponseSheetForForm_(ss); + const sheet = findResponseSheetForForm_(ss); - if (!sheet || sheet.getLastRow() == 1) { - return undefined; - } + 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; + const rows = sheet.getSheetValues( + sheet.getFrozenRows() + 1, + 1, + sheet.getLastRow() - 1, + sheet.getLastRow(), + ); + const attendees = rows.map((row) => { + const _ = row.shift(); // Ignore timestamp + const email = row.shift(); + const autoAssign = row.pop(); + // Find ranked items in the response data. + let preferences = row.reduce((prefs, value, index) => { + const match = value.match(/(\d+).*/); + if (!match) { + return prefs; + } + const rank = Number.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. + const additionalChoices = shuffleArray_(allActivityIds); + preferences = preferences.concat(additionalChoices); + } + return { + email: email, + preferences: preferences, + assigned: [], + }; + }); + return attendees; } /** @@ -290,31 +314,32 @@ function loadAttendeeResponses_(ss, allActivityIds) { * 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; - } + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheet = findResponseSheetForForm_(ss); + if (!sheet) { + const msg = "No response sheet found. Create the form and try again."; + SpreadsheetApp.getUi().alert(msg); + } + if (sheet.getLastRow() > 1) { + const 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); - }); + const activities = loadActivitySchedule_(ss); + const choices = fillArray_([], activities.length, ""); + range_(1, 5).forEach((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); + const rows = range_(1, NUM_TEST_USERS).map((value) => { + const randomizedChoices = shuffleArray_(choices); + const email = Utilities.formatString("person%d@example.com", value); + return [new Date(), email].concat(randomizedChoices).concat(["Yes"]); + }); + bulkAppendRows_(sheet, rows); } /** @@ -325,11 +350,11 @@ function generateTestData_() { * @return {Sheet} Sheet instance */ function findOrCreateSheetByName_(ss, name) { - let sheet = ss.getSheetByName(name); - if (sheet) { - return sheet; - } - return ss.insertSheet(name); + const sheet = ss.getSheetByName(name); + if (sheet) { + return sheet; + } + return ss.insertSheet(name); } /** @@ -339,11 +364,11 @@ function findOrCreateSheetByName_(ss, name) { * @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); + const startRow = sheet.getLastRow() + 1; + const startColumn = 1; + const numRows = rows.length; + const numColumns = rows[0].length; + sheet.getRange(startRow, startColumn, numRows, numColumns).setValues(rows); } /** @@ -353,14 +378,14 @@ function bulkAppendRows_(sheet, rows) { * @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; + const clone = array.slice(0); + for (let i = clone.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = clone[i]; + clone[i] = clone[j]; + clone[j] = temp; + } + return clone; } /** @@ -372,18 +397,18 @@ function shuffleArray_(array) { * @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'; + const j = i % 10; + const 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"; } /** @@ -393,17 +418,17 @@ function toOrdinal_(i) { * @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; + const formUrl = ss.getFormUrl(); + if (!ss || !formUrl) { + return undefined; + } + const sheets = ss.getSheets(); + for (const i in sheets) { + if (sheets[i].getFormUrl() === formUrl) { + return sheets[i]; + } + } + return undefined; } /** @@ -415,10 +440,10 @@ function findResponseSheetForForm_(ss) { * @return {object[]} the array, for chaining purposes */ function fillArray_(arr, length, value) { - for (let i = 0; i < length; ++i) { - arr[i] = value; - } - return arr; + for (let i = 0; i < length; ++i) { + arr[i] = value; + } + return arr; } /** @@ -429,12 +454,12 @@ function fillArray_(arr, length, value) { * @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; + const arr = [start]; + let i = start; + while (i < end) { + arr.push((i += 1)); + } + return arr; } /** @@ -447,12 +472,13 @@ function range_(start, end) { * @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; + const transposed = []; + arr.forEach((row, rowIndex) => { + row.forEach((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/appsscript.json b/solutions/automations/offsite-activity-signup/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/offsite-activity-signup/appsscript.json +++ b/solutions/automations/offsite-activity-signup/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/tax-loss-harvest-alerts/.clasp.json b/solutions/automations/tax-loss-harvest-alerts/.clasp.json index b9dffc2b6..657ae7722 100644 --- a/solutions/automations/tax-loss-harvest-alerts/.clasp.json +++ b/solutions/automations/tax-loss-harvest-alerts/.clasp.json @@ -1 +1 @@ -{"scriptId":"1SVf_XAGJiwksNTMnAwtlIvkKaDou4RLsmwGTa9ipVHKgwITgwXWqMixB"} \ No newline at end of file +{ "scriptId": "1SVf_XAGJiwksNTMnAwtlIvkKaDou4RLsmwGTa9ipVHKgwITgwXWqMixB" } diff --git a/solutions/automations/tax-loss-harvest-alerts/Code.js b/solutions/automations/tax-loss-harvest-alerts/Code.js index e25098b87..95ef2e1d1 100644 --- a/solutions/automations/tax-loss-harvest-alerts/Code.js +++ b/solutions/automations/tax-loss-harvest-alerts/Code.js @@ -17,56 +17,53 @@ See the License for the specific language governing permissions and limitations under the License. */ -/** -* Checks for losses in the sheet. -*/ +/** + * 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(); + // Pulls data from the spreadsheet + const sheet = + SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Calculations"); + const source = sheet.getRange("A:G"); + const data = source.getValues(); - //Prepares the email alert content - let message = "Stocks:

    "; + //Prepares the email alert content + let message = "Stocks:

    "; - let send_message = false; + let send_message = false; - console.log("starting loop"); + 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; + //Loops through the cells in the spreadsheet to find cells where the stock fell below purchase price + let n = 0; + for (const i in data) { + //Skips the first row + if (n++ == 0) continue; - //Loads the current row - let row = data[i]; + //Loads the current row + const row = data[i]; - console.log(row[1]); - console.log(row[6]); + console.log(row[1]); + console.log(row[6]); - //Once at the end of the list, exits the loop - if (row[1] == "") break; + //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; + //If value is below purchase price, adds stock ticker and difference to list of tax loss opportunities + if (row[6] < 0) { + message += + row[1] + + ": " + + (Number.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, - - }); + MailApp.sendEmail({ + to: SpreadsheetApp.getActiveSpreadsheet().getOwner().getEmail(), + subject: "Tax-loss harvest", + htmlBody: message, + }); } - diff --git a/solutions/automations/tax-loss-harvest-alerts/appsscript.json b/solutions/automations/tax-loss-harvest-alerts/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/tax-loss-harvest-alerts/appsscript.json +++ b/solutions/automations/tax-loss-harvest-alerts/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/timesheets/.clasp.json b/solutions/automations/timesheets/.clasp.json index 5f0ff403b..72039fb19 100644 --- a/solutions/automations/timesheets/.clasp.json +++ b/solutions/automations/timesheets/.clasp.json @@ -1 +1 @@ -{"scriptId": "1uzOldn2RjqdrbDJwxuPlcsb7twKLdW59YPS02rbEg_ajAG9XzrYF1-fH"} +{ "scriptId": "1uzOldn2RjqdrbDJwxuPlcsb7twKLdW59YPS02rbEg_ajAG9XzrYF1-fH" } diff --git a/solutions/automations/timesheets/Code.js b/solutions/automations/timesheets/Code.js index bad03efdc..32f40b069 100644 --- a/solutions/automations/timesheets/Code.js +++ b/solutions/automations/timesheets/Code.js @@ -18,54 +18,54 @@ 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, +const 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.'; +const APPROVED_EMAIL_SUBJECT = "Weekly Timesheet APPROVED"; +const REJECTED_EMAIL_SUBJECT = "Weekly Timesheet NOT APPROVED"; +const APPROVED_EMAIL_MESSAGE = "Your timesheet has been approved."; +const 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(); + const 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 "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; + const sheet = SpreadsheetApp.getActiveSheet(); + const lastCol = sheet.getLastColumn(); + const lastRow = sheet.getLastRow(); + const frozenRows = sheet.getFrozenRows(); + const beginningRow = frozenRows + 1; + const numRows = lastRow - frozenRows; - // Calls helper functions to add new columns. - addCalculatePayColumn(sheet, beginningRow); - addApprovalColumn(sheet, beginningRow, numRows); - addNotifiedColumn(sheet, beginningRow, numRows); + // Calls helper functions to add new columns. + addCalculatePayColumn(sheet, beginningRow); + addApprovalColumn(sheet, beginningRow, numRows); + addNotifiedColumn(sheet, beginningRow, numRows); } /** @@ -76,16 +76,18 @@ function columnSetup() { * @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'); + 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)'); + // 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)"); } /** @@ -97,17 +99,22 @@ function addCalculatePayColumn(sheet, beginningRow) { * @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'); + 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'); + // Make sure approval column is all drop-down menus. + const approvalColumnRange = sheet.getRange( + beginningRow, + COLUMN_NUMBER.APPROVAL, + numRows, + 1, + ); + const dropdownValues = ["APPROVED", "NOT APPROVED", "IN PROGRESS"]; + const rule = SpreadsheetApp.newDataValidation() + .requireValueInList(dropdownValues) + .build(); + approvalColumnRange.setDataValidation(rule); + approvalColumnRange.setValue("IN PROGRESS"); } /** @@ -119,17 +126,22 @@ function addApprovalColumn(sheet, beginningRow, numRows) { * @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'); + 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'); + // Make sure notified column is all drop-down menus. + const 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"); } /** @@ -142,83 +154,92 @@ function addNotifiedColumn(sheet, beginningRow, numRows) { * @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'); + // 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; + const sheet = SpreadsheetApp.getActiveSheet(); + const lastRow = sheet.getLastRow(); + const lastCol = sheet.getLastColumn(); + // lastCol here is the NOTIFIED column. + const frozenRows = sheet.getFrozenRows(); + const beginningRow = frozenRows + 1; + const 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(); + // Gets ranges of email, approval, and notified values for every employee. + const emailValues = sheet + .getRange(beginningRow, COLUMN_NUMBER.EMAIL, numRows, 1) + .getValues(); + const approvalValues = sheet + .getRange(beginningRow, COLUMN_NUMBER.APPROVAL, lastRow - frozenRows, 1) + .getValues(); + const 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]; + // Traverses through employee's row. + for (let i = 0; i < numRows; i++) { + // Do not notify twice. + if (notifiedValues[i][0] == "NOTIFIED") { + continue; + } + const emailAddress = emailValues[i][0]; + const 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); - } - } + // 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 +/** + * 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(); + const sheet = SpreadsheetApp.getActiveSpreadsheet(); + if (sheet.getFormUrl()) { + const msg = "Form already exists. Unlink the form and try again."; + SpreadsheetApp.getUi().alert(msg); + return; + } + + // Create the form. + const 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(); } /** @@ -227,21 +248,23 @@ function setUpForm() { * @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); + const response = getResponsesByName(event.response); + + // Load form responses into a new row. + const 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:"], + ]; + const sheet = SpreadsheetApp.getActiveSpreadsheet(); + sheet.appendRow(row); } /** @@ -252,13 +275,13 @@ function onFormSubmit(event) { * @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 + const initialValue = { + email: response.getRespondentEmail(), + timestamp: response.getTimestamp(), + }; + return response.getItemResponses().reduce((obj, itemResponse) => { + const key = itemResponse.getItem().getTitle(); + obj[key] = itemResponse.getResponse(); + return obj; + }, initialValue); +} diff --git a/solutions/automations/timesheets/appsscript.json b/solutions/automations/timesheets/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/timesheets/appsscript.json +++ b/solutions/automations/timesheets/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/upload-files/Code.js b/solutions/automations/upload-files/Code.js index 50e523954..11a455a28 100644 --- a/solutions/automations/upload-files/Code.js +++ b/solutions/automations/upload-files/Code.js @@ -18,7 +18,7 @@ limitations under the License. */ // [START apps_script_upload_files] -// TODO Before you start using this sample, you must run the setUp() +// TODO Before you start using this sample, you must run the setUp() // function in the Setup.gs file. // Application constants @@ -29,62 +29,65 @@ const APP_FOLDER_NAME = "Upload files to Drive (File responses)"; 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: + try { + // Gets the application root folder. + var destFolder = getFolder_(APP_FOLDER_NAME); + + // Gets all form responses. + const itemResponses = e.response.getItemResponses(); + + // Determines the subfolder to route the file to, if any. + var subFolderName; + const 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); - } + URL: ${destFolder.getUrl()}`); + + // Gets the file upload response as an array to allow for multiple files. + const 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 + * 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. @@ -92,20 +95,22 @@ function onFormSubmit(e) { * @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.`); + // Iterates subfolders of parent folder to check if folder already exists. + const subFolders = objParentFolder.getFolders(); + while (subFolders.hasNext()) { + const 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.`, + ); } // [END apps_script_upload_files] diff --git a/solutions/automations/upload-files/Setup.js b/solutions/automations/upload-files/Setup.js index 2ad4ef5cf..01d96d239 100644 --- a/solutions/automations/upload-files/Setup.js +++ b/solutions/automations/upload-files/Setup.js @@ -17,56 +17,57 @@ // [START apps_script_upload_files_setup] // 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. + // 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_(); + 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. + * @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(); - // 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(); + // 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()) { + const 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.`); + // 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.`, + ); } /** @@ -75,25 +76,33 @@ function getFolder_(folderName) { * 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}`); + // Ensures existing trigger doesn't already exist. + const propTriggerId = + PropertiesService.getScriptProperties().getProperty("triggerUniqueId"); + if (propTriggerId !== null) { + const triggers = ScriptApp.getProjectTriggers(); + for (const 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. + const 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}`, + ); } /** @@ -101,19 +110,19 @@ function installTrigger_() { * 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]); - } + PropertiesService.getScriptProperties().deleteAllProperties(); + // Removes all triggers associated with project. + const triggers = ScriptApp.getProjectTriggers(); + for (const t in triggers) { + ScriptApp.deleteTrigger(triggers[t]); + } } /** * Removes all form responses to reset the form. */ function deleteAllResponses() { - FormApp.getActiveForm().deleteAllResponses(); + FormApp.getActiveForm().deleteAllResponses(); } // [END apps_script_upload_files_setup] diff --git a/solutions/automations/upload-files/appsscript.json b/solutions/automations/upload-files/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/upload-files/appsscript.json +++ b/solutions/automations/upload-files/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/vacation-calendar/.clasp.json b/solutions/automations/vacation-calendar/.clasp.json index b9162a3b0..708fde255 100644 --- a/solutions/automations/vacation-calendar/.clasp.json +++ b/solutions/automations/vacation-calendar/.clasp.json @@ -1 +1 @@ -{"scriptId":"1jvPSSwJcuLzlDLDy2dr-qorjihiTNAW2H6B5k-dJxHjEPX6hMcNghzSh"} +{ "scriptId": "1jvPSSwJcuLzlDLDy2dr-qorjihiTNAW2H6B5k-dJxHjEPX6hMcNghzSh" } diff --git a/solutions/automations/vacation-calendar/Code.js b/solutions/automations/vacation-calendar/Code.js index 6705810ff..8cf64e1f7 100644 --- a/solutions/automations/vacation-calendar/Code.js +++ b/solutions/automations/vacation-calendar/Code.js @@ -19,29 +19,29 @@ 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'; +const 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. // Change to an array in order to add indirect members frrm multiple groups, for example: // let GROUP_EMAIL = ['ENTER_GOOGLE_GROUP_EMAIL_HERE', 'ENTER_ANOTHER_GOOGLE_GROUP_EMAIL_HERE']; -let GROUP_EMAIL = 'ENTER_GOOGLE_GROUP_EMAIL_HERE'; +const GROUP_EMAIL = "ENTER_GOOGLE_GROUP_EMAIL_HERE"; -let ONLY_DIRECT_MEMBERS = false; +const ONLY_DIRECT_MEMBERS = false; -let KEYWORDS = ['vacation', 'ooo', 'out of office', 'offline']; -let MONTHS_IN_ADVANCE = 3; +const KEYWORDS = ["vacation", "ooo", "out of office", "offline"]; +const 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(); + const 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(); } /** @@ -49,38 +49,38 @@ function setup() { * '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 = getAllMembers(GROUP_EMAIL); - if (ONLY_DIRECT_MEMBERS){ - users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers(); - } else if (Array.isArray(GROUP_EMAIL)) { - users = getUsersFromGroups(GROUP_EMAIL); - } - - // 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]; - let events = findEvents(user, today, maxDate, lastRun); - events.forEach(function(event) { - importEvent(username, event); - count++; - }); // End foreach event. - }); // End foreach user. - - PropertiesService.getScriptProperties().setProperty('lastRun', today); - console.log('Imported ' + count + ' events'); + // Defines the calendar event date range to search. + const today = new Date(); + const 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 = getAllMembers(GROUP_EMAIL); + if (ONLY_DIRECT_MEMBERS) { + users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers(); + } else if (Array.isArray(GROUP_EMAIL)) { + users = getUsersFromGroups(GROUP_EMAIL); + } + + // 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((user) => { + const username = user.getEmail().split("@")[0]; + const events = findEvents(user, today, maxDate, lastRun); + events.forEach((event) => { + importEvent(username, event); + count++; + }); // End foreach event. + }); // End foreach user. + + PropertiesService.getScriptProperties().setProperty("lastRun", today); + console.log("Imported " + count + " events"); } /** @@ -90,27 +90,29 @@ function sync() { * @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 = []; - - // 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); - } catch (e) { - console.error('Error attempting to import event: %s. Skipping.', - e.toString()); - } + event.summary = "[" + username + "] " + event.summary; + event.organizer = { + 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); + } catch (e) { + console.error( + "Error attempting to import event: %s. Skipping.", + e.toString(), + ); + } } /** @@ -125,34 +127,38 @@ function importEvent(username, event) { * @return {Calendar.Event[]} An array of calendar events. */ function findEvents(user, start, end, optSince) { - let params = { - eventTypes: "outOfOffice", - 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); - pageToken = response.nextPageToken; - } while (pageToken); - return events; + const params = { + eventTypes: "outOfOffice", + 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); + pageToken = response.nextPageToken; + } while (pageToken); + return events; } /** @@ -162,49 +168,49 @@ function findEvents(user, start, end, optSince) { * @return {string} a formatted date string. */ function formatDateAsRFC3339(date) { - return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ'); + return Utilities.formatDate(date, "UTC", "yyyy-MM-dd'T'HH:mm:ssZ"); } /** -* Get both direct and indirect members (and delete duplicates). -* @param {string} the e-mail address of the group. -* @return {object} direct and indirect members. -*/ + * Get both direct and indirect members (and delete duplicates). + * @param {string} the e-mail address of the group. + * @return {object} direct and indirect members. + */ function getAllMembers(groupEmail) { - var group = GroupsApp.getGroupByEmail(groupEmail); - var users = group.getUsers(); - var childGroups = group.getGroups(); - for (var i = 0; i < childGroups.length; i++) { - var childGroup = childGroups[i]; - users = users.concat(getAllMembers(childGroup.getEmail())); - } - // Remove duplicate members - var uniqueUsers = []; - var userEmails = {}; - for (var i = 0; i < users.length; i++) { - var user = users[i]; - if (!userEmails[user.getEmail()]) { - uniqueUsers.push(user); - userEmails[user.getEmail()] = true; - } - } - return uniqueUsers; + var group = GroupsApp.getGroupByEmail(groupEmail); + var users = group.getUsers(); + var childGroups = group.getGroups(); + for (var i = 0; i < childGroups.length; i++) { + var childGroup = childGroups[i]; + users = users.concat(getAllMembers(childGroup.getEmail())); + } + // Remove duplicate members + var uniqueUsers = []; + var userEmails = {}; + for (var i = 0; i < users.length; i++) { + var user = users[i]; + if (!userEmails[user.getEmail()]) { + uniqueUsers.push(user); + userEmails[user.getEmail()] = true; + } + } + return uniqueUsers; } /** -* Get indirect members from multiple groups (and delete duplicates). -* @param {array} the e-mail addresses of multiple groups. -* @return {object} indirect members of multiple groups. -*/ + * Get indirect members from multiple groups (and delete duplicates). + * @param {array} the e-mail addresses of multiple groups. + * @return {object} indirect members of multiple groups. + */ function getUsersFromGroups(groupEmails) { - let users = []; - for (let groupEmail of groupEmails) { - let groupUsers = GroupsApp.getGroupByEmail(groupEmail).getUsers(); - for (let user of groupUsers) { - if (!users.some(u => u.getEmail() === user.getEmail())) { - users.push(user); - } - } - } - return users; + const users = []; + for (const groupEmail of groupEmails) { + const groupUsers = GroupsApp.getGroupByEmail(groupEmail).getUsers(); + for (const user of groupUsers) { + if (!users.some((u) => u.getEmail() === user.getEmail())) { + users.push(user); + } + } + } + return users; } diff --git a/solutions/automations/vacation-calendar/appsscript.json b/solutions/automations/vacation-calendar/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/vacation-calendar/appsscript.json +++ b/solutions/automations/vacation-calendar/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/automations/youtube-tracker/.clasp.json b/solutions/automations/youtube-tracker/.clasp.json index 3e74246bd..ec95a3593 100644 --- a/solutions/automations/youtube-tracker/.clasp.json +++ b/solutions/automations/youtube-tracker/.clasp.json @@ -1 +1 @@ -{"scriptId":"15WP4FukVYk_4zy21j0_13GftPH7J8lpdtemYcy_168TYKsAQ4x-pAeQz"} \ No newline at end of file +{ "scriptId": "15WP4FukVYk_4zy21j0_13GftPH7J8lpdtemYcy_168TYKsAQ4x-pAeQz" } diff --git a/solutions/automations/youtube-tracker/Code.js b/solutions/automations/youtube-tracker/Code.js index b99827d4d..ff0319a8c 100644 --- a/solutions/automations/youtube-tracker/Code.js +++ b/solutions/automations/youtube-tracker/Code.js @@ -18,87 +18,95 @@ limitations under the License. */ // Sets preferences for email notification. Choose 'Y' to send emails, 'N' to skip emails. -const EMAIL_ON = 'Y'; +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', + 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, + * 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); - } - }); + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets(); + + // Runs through process for each tab in Spreadsheet. + sheets.forEach((dataSheet) => { + const tabName = dataSheet.getName(); + const range = dataSheet.getDataRange(); + const numRows = range.getNumRows(); + const rows = range.getValues(); + const headerRow = rows[0]; + + // Finds the column indices. + const videoColumnIdx = headerRow.indexOf(COLUMN_NAME.VIDEO); + const titleColumnIdx = headerRow.indexOf(COLUMN_NAME.TITLE); + + // Creates empty array to collect data for email table. + const emailContent = []; + + // Processes each row in spreadsheet. + for (let i = 1; i < numRows; ++i) { + const row = rows[i]; + // Extracts video ID. + const 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. + const detailsResponse = getVideoDetails(videoId); + const title = detailsResponse.items[0].snippet.title; + const publishDate = detailsResponse.items[0].snippet.publishedAt; + const publishDateFormatted = new Date(publishDate); + const views = detailsResponse.items[0].statistics.viewCount; + const likes = detailsResponse.items[0].statistics.likeCount; + const comments = detailsResponse.items[0].statistics.commentCount; + const channel = detailsResponse.items[0].snippet.channelTitle; + + // Collects title, publish date, channel, views, comments, likes details and pastes into tab. + const 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. + const addlCommentCount = comments - row[titleColumnIdx + 4]; + + // Adds video title, link, and additional comment count to table if new counts > old counts. + if (addlCommentCount > 0) { + const 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; + const part = "snippet,statistics"; + const response = YouTube.Videos.list(part, { id: videoId }); + return response; } /** @@ -106,21 +114,26 @@ function getVideoDetails(videoId) { * (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; + let videoId = url.split("v=")[1]; + const ampersandPosition = videoId.indexOf("&"); + if (ampersandPosition != -1) { + videoId = videoId.substring(0, ampersandPosition); + } + return videoId; } /** - * Assembles notification email with table of video details. + * 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 + const template = HtmlService.createTemplateFromFile("email"); + template.content = content; + const msg = template.evaluate(); + MailApp.sendEmail( + emailAddress, + "New comments or replies on YouTube", + msg.getContent(), + { htmlBody: msg.getContent() }, + ); +} diff --git a/solutions/automations/youtube-tracker/appsscript.json b/solutions/automations/youtube-tracker/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/automations/youtube-tracker/appsscript.json +++ b/solutions/automations/youtube-tracker/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/custom-functions/calculate-driving-distance/.clasp.json b/solutions/custom-functions/calculate-driving-distance/.clasp.json index 6fb6a8c9b..ce08ddcf7 100644 --- a/solutions/custom-functions/calculate-driving-distance/.clasp.json +++ b/solutions/custom-functions/calculate-driving-distance/.clasp.json @@ -1 +1 @@ -{"scriptId": "1_cfhZv-VJBekzu1V4mFD1C5ggRaUumWw9rUz0NaLED6XD4_yHB-eJ01a"} +{ "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 index aaf9ceebd..83725642a 100644 --- a/solutions/custom-functions/calculate-driving-distance/Code.js +++ b/solutions/custom-functions/calculate-driving-distance/Code.js @@ -26,17 +26,17 @@ limitations under the License. * 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 - console.log('Failed with error: %s' + e.error); - } + 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 + console.log("Failed with error: %s" + e.error); + } } /** @@ -46,10 +46,10 @@ function onOpen() { * @return {Number} The distance in miles. */ function metersToMiles(meters) { - if (typeof meters !== 'number') { - return null; - } - return meters / 1000 * 0.621371; + if (typeof meters !== "number") { + return null; + } + return (meters / 1000) * 0.621371; } /** @@ -60,32 +60,34 @@ function metersToMiles(meters) { * @return {Number} The distance in meters. */ function drivingDistance(origin, destination) { - const directions = getDirections_(origin, destination); - return directions.routes[0].legs[0].distance.value; + 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 - console.log('Failed with error: %s' + e.error); - } + 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 + console.log("Failed with error: %s" + e.error); + } } /** @@ -93,90 +95,107 @@ function prepareSheet_() { * 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 - console.log('Failed with error: %s' + e.error); - } + 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 + console.log("Failed with error: %s" + e.error); + } } /** @@ -188,19 +207,19 @@ function generateStepByStep_() { * 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); + 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); } /** @@ -212,12 +231,12 @@ function setAlternatingRowBackgroundColors_(range, oddColor, evenColor) { * @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; + const directionFinder = Maps.newDirectionFinder(); + directionFinder.setOrigin(origin); + directionFinder.setDestination(destination); + const directions = directionFinder.getDirections(); + if (directions.status !== "OK") { + throw directions.error_message; + } + return directions; } diff --git a/solutions/custom-functions/calculate-driving-distance/appsscript.json b/solutions/custom-functions/calculate-driving-distance/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/custom-functions/calculate-driving-distance/appsscript.json +++ b/solutions/custom-functions/calculate-driving-distance/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/custom-functions/summarize-sheets-data/.clasp.json b/solutions/custom-functions/summarize-sheets-data/.clasp.json index a53755e8a..5226736fc 100644 --- a/solutions/custom-functions/summarize-sheets-data/.clasp.json +++ b/solutions/custom-functions/summarize-sheets-data/.clasp.json @@ -1 +1 @@ -{"scriptId": "1NN-ROSZO3ZsfiVUlCdmNqggpCQuGNtgO_r0nehV0s5mkZJN2bcMTri-7"} +{ "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 index 70ac671bc..61d9237f5 100644 --- a/solutions/custom-functions/summarize-sheets-data/Code.js +++ b/solutions/custom-functions/summarize-sheets-data/Code.js @@ -19,7 +19,7 @@ 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 */ @@ -29,55 +29,76 @@ 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 + * 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 = []; + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheets = ss.getSheets(); + const 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; + // For each sheet, summarizes the data and pushes to a temporary array. + for (const s in sheets) { + // Gets sheet name. + const sheetNm = sheets[s].getName(); + // Skips ReadMe and Summary sheets. + if (sheetNm === READ_ME_SHEET_NAME || sheetNm === PM_SHEET_NAME) { + continue; + } + // Gets sheets data. + const values = sheets[s].getDataRange().getValues(); + // Gets the first row of the sheet which is the header row. + const 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. + const columnOwner = headerRowValues.indexOf("Owner Name"); + const 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. + const owner = values[0][columnOwner]; + // Counts the total number of tasks. + const 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. + const completeCnt = filterByPosition( + values, + "Complete", + columnStatus, + ).length; + // Counts the number of tasks that have the 'In-Progress' status. + const inProgressCnt = filterByPosition( + values, + "In-Progress", + columnStatus, + ).length; + // Counts the number of tasks that have the 'Scheduled' status. + const scheduledCnt = filterByPosition( + values, + "Scheduled", + columnStatus, + ).length; + // Counts the number of tasks that have the 'Overdue' status. + const 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); + return array.filter((innerArray) => innerArray[position] === find); } - diff --git a/solutions/custom-functions/summarize-sheets-data/appsscript.json b/solutions/custom-functions/summarize-sheets-data/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/custom-functions/summarize-sheets-data/appsscript.json +++ b/solutions/custom-functions/summarize-sheets-data/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/custom-functions/tier-pricing/.clasp.json b/solutions/custom-functions/tier-pricing/.clasp.json index c8264b479..4aba2ab87 100644 --- a/solutions/custom-functions/tier-pricing/.clasp.json +++ b/solutions/custom-functions/tier-pricing/.clasp.json @@ -1 +1 @@ -{"scriptId": "1-ql7ECe91XZgWu-hW_UZBx8mhuTtQQj0yNITYh8yQCOuHxLEjxtTngGB"} +{ "scriptId": "1-ql7ECe91XZgWu-hW_UZBx8mhuTtQQj0yNITYh8yQCOuHxLEjxtTngGB" } diff --git a/solutions/custom-functions/tier-pricing/Code.js b/solutions/custom-functions/tier-pricing/Code.js index 9fca30cf0..6da7b5b93 100644 --- a/solutions/custom-functions/tier-pricing/Code.js +++ b/solutions/custom-functions/tier-pricing/Code.js @@ -18,34 +18,33 @@ limitations under the License. */ /** - * Calculates the tiered pricing discount. - * + * 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 + * 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 + * + * @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; + let total = 0; + // Creates an array for each row of the table and loops through each array. + for (const [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. + const 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/appsscript.json b/solutions/custom-functions/tier-pricing/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/custom-functions/tier-pricing/appsscript.json +++ b/solutions/custom-functions/tier-pricing/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/editor-add-on/clean-sheet/.clasp.json b/solutions/editor-add-on/clean-sheet/.clasp.json index 2cae3aa10..2a9d2408a 100644 --- a/solutions/editor-add-on/clean-sheet/.clasp.json +++ b/solutions/editor-add-on/clean-sheet/.clasp.json @@ -1 +1 @@ -{"scriptId": "10bxhn6eGypm20dgRcTbUCbzP4Bz0dyYR6IZTNEA2gIXXxwoy8Zqs06yr"} +{ "scriptId": "10bxhn6eGypm20dgRcTbUCbzP4Bz0dyYR6IZTNEA2gIXXxwoy8Zqs06yr" } diff --git a/solutions/editor-add-on/clean-sheet/Code.js b/solutions/editor-add-on/clean-sheet/Code.js index deb3136c3..105a2b2a3 100644 --- a/solutions/editor-add-on/clean-sheet/Code.js +++ b/solutions/editor-add-on/clean-sheet/Code.js @@ -18,229 +18,247 @@ limitations under the License. */ // Application Constants -const APP_TITLE = 'Clean sheet'; +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(); + const sheet = SpreadsheetApp.getActiveSheet(); + + // Gets active selection and dimensions. + const 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(); + const sheet = SpreadsheetApp.getActiveSheet(); + + // Gets active selection and dimensions. + const 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(); + const dataRange = SpreadsheetApp.getActiveSheet().getDataRange(); + const sheet = dataRange.getSheet(); - let numRows = dataRange.getNumRows(); - let numColumns = dataRange.getNumColumns(); + let numRows = dataRange.getNumRows(); + let numColumns = dataRange.getNumColumns(); - const maxRows = sheet.getMaxRows(); - const maxColumns = sheet.getMaxColumns(); + const maxRows = sheet.getMaxRows(); + const maxColumns = sheet.getMaxColumns(); - const numFrozenRows = sheet.getFrozenRows(); - const numFrozenColumns = sheet.getFrozenColumns(); + 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 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); - } + // 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. + * 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."); - } + 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); + // 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 index fb6feca40..451fc12fb 100644 --- a/solutions/editor-add-on/clean-sheet/Menu.js +++ b/solutions/editor-add-on/clean-sheet/Menu.js @@ -17,32 +17,35 @@ /** * 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. + * @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() + // Builds a menu that displays under the Extensions menu in Sheets. + const 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(); + 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 + * 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. + * @param {object} e The event parameter for a simple onInstall trigger. */ function onInstall(e) { - onOpen(e); + onOpen(e); } /** @@ -50,11 +53,11 @@ function onInstall(e) { * TODO: Personalize */ function aboutApp() { - const msg = ` + const msg = ` Name: ${APP_TITLE} Version: 1.0 - Contact: ` + Contact: `; - const ui = SpreadsheetApp.getUi(); - ui.alert("About this application", msg, ui.ButtonSet.OK); -} \ No newline at end of file + const ui = SpreadsheetApp.getUi(); + ui.alert("About this application", msg, ui.ButtonSet.OK); +} diff --git a/solutions/editor-add-on/clean-sheet/appsscript.json b/solutions/editor-add-on/clean-sheet/appsscript.json index 3cf1d247d..333cc15cb 100644 --- a/solutions/editor-add-on/clean-sheet/appsscript.json +++ b/solutions/editor-add-on/clean-sheet/appsscript.json @@ -1,7 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": { - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" -} \ No newline at end of file + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} diff --git a/solutions/ooo-assistant/.clasp.json b/solutions/ooo-assistant/.clasp.json index 747d3bffa..e10a8d490 100644 --- a/solutions/ooo-assistant/.clasp.json +++ b/solutions/ooo-assistant/.clasp.json @@ -1 +1 @@ -{"scriptId": "16L_UmGrkrDKYWrfw9YlnUnnnWOMBEWywyPrZDZIQqKF17Q97RtZeinqn"} +{ "scriptId": "16L_UmGrkrDKYWrfw9YlnUnnnWOMBEWywyPrZDZIQqKF17Q97RtZeinqn" } diff --git a/solutions/ooo-assistant/appsscript.json b/solutions/ooo-assistant/appsscript.json index a95705ef3..116f55c66 100644 --- a/solutions/ooo-assistant/appsscript.json +++ b/solutions/ooo-assistant/appsscript.json @@ -1,40 +1,45 @@ { - "timeZone": "America/New_York", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "enabledAdvancedServices": [{ - "userSymbol": "Gmail", - "version": "v1", - "serviceId": "gmail" - }, - { - "userSymbol": "Calendar", - "version": "v3", - "serviceId": "calendar" - }] - }, - "addOns": { - "common": { - "name": "OOO Assistant", - "logoUrl": "https://goo.gle/3SfMkjb", - "homepageTrigger": { - "runFunction": "onHomepage" - }, - "universalActions": [{ - "label": "Block day out", - "runFunction": "blockDayOut" - }, { - "label": "Set auto reply", - "runFunction": "setAutoReply" - }] - }, - "chat": {}, - "calendar": {}, - "gmail": {}, - "drive": {}, - "docs": {}, - "sheets": {}, - "slides": {} - } + "timeZone": "America/New_York", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + }, + { + "userSymbol": "Calendar", + "version": "v3", + "serviceId": "calendar" + } + ] + }, + "addOns": { + "common": { + "name": "OOO Assistant", + "logoUrl": "https://goo.gle/3SfMkjb", + "homepageTrigger": { + "runFunction": "onHomepage" + }, + "universalActions": [ + { + "label": "Block day out", + "runFunction": "blockDayOut" + }, + { + "label": "Set auto reply", + "runFunction": "setAutoReply" + } + ] + }, + "chat": {}, + "calendar": {}, + "gmail": {}, + "drive": {}, + "docs": {}, + "sheets": {}, + "slides": {} + } } diff --git a/solutions/ooo-chat-app/Code.js b/solutions/ooo-chat-app/Code.js index b03dde223..196f1de6e 100644 --- a/solutions/ooo-chat-app/Code.js +++ b/solutions/ooo-chat-app/Code.js @@ -184,7 +184,7 @@ function blockOutCalendar() { function cancelMeetings() { const events = CalendarApp.getEventsForDay(new Date()); - events.forEach(function(event) { + events.forEach((event) => { if (event.getGuestList().length > 0) { event.setMyStatus(CalendarApp.GuestStatus.NO); } diff --git a/solutions/ooo-chat-app/appsscript.json b/solutions/ooo-chat-app/appsscript.json index de9fa0b7b..94cbe5fcc 100644 --- a/solutions/ooo-chat-app/appsscript.json +++ b/solutions/ooo-chat-app/appsscript.json @@ -1,20 +1,20 @@ { - "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 + "timeZone": "Europe/Madrid", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + }, + { + "userSymbol": "Calendar", + "version": "v3", + "serviceId": "calendar" + } + ] + }, + "chat": {} +} diff --git a/solutions/schedule-meetings/.clasp.json b/solutions/schedule-meetings/.clasp.json index 34c4cfc7f..d07e68bf6 100644 --- a/solutions/schedule-meetings/.clasp.json +++ b/solutions/schedule-meetings/.clasp.json @@ -1 +1 @@ -{"scriptId": "1NdhQ_nXfEUUhWcWKiY6WJjeunY70a1W9vnFdS7BCLPMFreSaHaOS3ucM"} +{ "scriptId": "1NdhQ_nXfEUUhWcWKiY6WJjeunY70a1W9vnFdS7BCLPMFreSaHaOS3ucM" } diff --git a/solutions/schedule-meetings/Code.js b/solutions/schedule-meetings/Code.js index bec500809..9805fc02a 100644 --- a/solutions/schedule-meetings/Code.js +++ b/solutions/schedule-meetings/Code.js @@ -18,37 +18,41 @@ limitations under the License. */ // Application constants -const APPNAME = 'Chat Meeting Scheduler'; +const APPNAME = "Chat Meeting Scheduler"; const SLASHCOMMAND = { - HELP: 1, // /help - DIALOG: 2, // /schedule_Meeting + HELP: 1, // /help + DIALOG: 2, // /schedule_Meeting }; /** * Responds to an ADDED_TO_SPACE event in Google Chat. * Called when the Chat app is added to a space. The Chat app can either be directly added to the space - * or added by a @mention. If the Chat app is added by a @mention, the event object includes a message property. + * or added by a @mention. If the Chat app 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 Chat app. * * @param {Object} event The event object from Google Chat */ function onAddToSpace(event) { - let message = ''; - - // Personalizes the message depending on how the Chat app 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 }; + let message = ""; + + // Personalizes the message depending on how the Chat app 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 }; } /** @@ -56,42 +60,37 @@ function onAddToSpace(event) { * Called when the Chat app is already in the space and the user invokes it via @mention or / command. * Returns a message object containing the Chat app's response. For this Chat app, 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: + // 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.' } - } + } + } else { + // Returns text if users didn't invoke a slash command. + return { text: "No action taken - use Slash Commands." }; + } } /** @@ -101,78 +100,75 @@ function onMessage(event) { * @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' - } - } - } - } + 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", + }, + }, + }; + } } /** @@ -180,11 +176,11 @@ function onCardClick(event) { * @return {string} The help text as seen below */ function getHelpTextResponse_() { - const help = `*${APPNAME}* lets you quickly create meetings from Google Chat. Here\'s a list of all its commands: + const help = `*${APPNAME}* 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 apps at https://developers.google.com/chat.` + Learn more about creating Google Chat apps at https://developers.google.com/chat.`; - return { 'text': help } + return { text: help }; } diff --git a/solutions/schedule-meetings/Dialog.js b/solutions/schedule-meetings/Dialog.js index c7731683e..3a3143f93 100644 --- a/solutions/schedule-meetings/Dialog.js +++ b/solutions/schedule-meetings/Dialog.js @@ -15,196 +15,195 @@ */ /** -* Form input dialog as JSON. -* @return {object} JSON-formatted cards for the dialog. -*/ + * 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 - } - } - } - }; + 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. -*/ + * Form JSON to collect inputs regarding the meeting. + * @return {object} JSON-formatted cards. + */ function getForm_(options) { - const sections = []; + 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; + // 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); + } + const 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 -*/ + * 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 + 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", + }, + }, + }, + }, + }, + }, + }, + }, + }; +} diff --git a/solutions/schedule-meetings/Utilities.js b/solutions/schedule-meetings/Utilities.js index dd08ea31b..c42627d83 100644 --- a/solutions/schedule-meetings/Utilities.js +++ b/solutions/schedule-meetings/Utilities.js @@ -15,60 +15,63 @@ */ /** -* Helper function that gets the field value from the given form input. -* @return {string} -*/ + * Helper function that gets the field value from the given form input. + * @return {string} + */ function getFieldValue_(formInputs, fieldName) { - return formInputs[fieldName][''].stringInputs.value[0]; + 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} -*/ + * Casts date and time from string to Date object. + * @return {date} + */ function getStartTimeAsDateObject_(dateTimeStr) { - if (!dateTimeStr || !dateTimeStr.match(DATE_TIME_PATTERN)) { - return null; - } + 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); - 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) + 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 -*/ +/** + * 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'); + 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}' -*/ +/** + * 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 + 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; +} diff --git a/solutions/schedule-meetings/appsscript.json b/solutions/schedule-meetings/appsscript.json index 113372528..f30cd857c 100644 --- a/solutions/schedule-meetings/appsscript.json +++ b/solutions/schedule-meetings/appsscript.json @@ -1,8 +1,8 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "chat": { - "addToSpaceFallbackMessage": "Thank you for adding this Chat App!" - } -} \ No newline at end of file + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "chat": { + "addToSpaceFallbackMessage": "Thank you for adding this Chat App!" + } +} diff --git a/tasks/simpleTasks/appsscript.json b/tasks/simpleTasks/appsscript.json index 63398b622..43a2b4918 100644 --- a/tasks/simpleTasks/appsscript.json +++ b/tasks/simpleTasks/appsscript.json @@ -1,11 +1,13 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [{ - "userSymbol": "Tasks", - "serviceId": "tasks", - "version": "v1" - }] - }, - "exceptionLogging": "STACKDRIVER" + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Tasks", + "serviceId": "tasks", + "version": "v1" + } + ] + }, + "exceptionLogging": "STACKDRIVER" } diff --git a/tsconfig.json b/tsconfig.json index 3ec62e0e4..732f2ff98 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,15 @@ { - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "noEmit": true, - "target": "es2019", - "module": "commonjs", - "alwaysStrict": true, - "lib": [ - "es2019" - ], - "types": [ - "google-apps-script" - ], - "strict": true, - "noImplicitAny": true - }, - "include": [ - "**/*.gs", - ".github/scripts/check-gs.ts" - ] -} \ No newline at end of file + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "target": "es2019", + "module": "commonjs", + "alwaysStrict": true, + "lib": ["es2019"], + "types": ["google-apps-script"], + "strict": true, + "noImplicitAny": true + }, + "include": ["**/*.gs", ".github/scripts/check-gs.ts"] +} diff --git a/wasm/hello-world/.clasp.json b/wasm/hello-world/.clasp.json index 0b59e587d..92ce01f19 100644 --- a/wasm/hello-world/.clasp.json +++ b/wasm/hello-world/.clasp.json @@ -1,4 +1,4 @@ { - "scriptId": "1xt1CvoUyFAzfoCdkwCHXBXzu3oaNz2a6iNsPW2GA6rOAsyBv66r4TarA", - "rootDir": "./dist" + "scriptId": "1xt1CvoUyFAzfoCdkwCHXBXzu3oaNz2a6iNsPW2GA6rOAsyBv66r4TarA", + "rootDir": "./dist" } diff --git a/wasm/hello-world/build.js b/wasm/hello-world/build.js index 708b80d39..a965ca7c8 100644 --- a/wasm/hello-world/build.js +++ b/wasm/hello-world/build.js @@ -15,37 +15,30 @@ */ import fs from "fs"; +import path from "path"; import esbuild from "esbuild"; import { wasmLoader } from "esbuild-plugin-wasm"; -import path from "path"; const outdir = "dist"; const sourceRoot = "src"; await esbuild.build({ - entryPoints: ["./src/wasm.js"], - bundle: true, - outdir, - sourceRoot, - platform: "neutral", - format: "esm", - plugins: [wasmLoader({ mode: "embedded" })], - inject: ["polyfill.js"], - minify: true, - banner: { js: "// Generated code DO NOT EDIT\n" }, + entryPoints: ["./src/wasm.js"], + bundle: true, + outdir, + sourceRoot, + platform: "neutral", + format: "esm", + plugins: [wasmLoader({ mode: "embedded" })], + inject: ["polyfill.js"], + minify: true, + banner: { js: "// Generated code DO NOT EDIT\n" }, }); -const passThroughFiles = [ - "main.js", - "test.js", - "appsscript.json", -]; +const passThroughFiles = ["main.js", "test.js", "appsscript.json"]; await Promise.all( - passThroughFiles.map(async (file) => - fs.promises.copyFile( - path.join(sourceRoot, file), - path.join(outdir, file) - ) - ) + passThroughFiles.map(async (file) => + fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), + ), ); diff --git a/wasm/hello-world/package.json b/wasm/hello-world/package.json index 20e48b496..5d723d194 100644 --- a/wasm/hello-world/package.json +++ b/wasm/hello-world/package.json @@ -1,84 +1,58 @@ { - "name": "example", - "version": "0.1.0", - "description": "An example integration of WASM with Rust into Apps Script", - "scripts": { - "build": "wireit", - "build:rust": "wireit", - "build:wasm": "wireit", - "clean": "rm -rf dist pkg target", - "deploy": "wireit", - "format": "cargo fmt", - "start": "wireit" - }, - "wireit": { - "build": { - "command": "node build.js", - "dependencies": [ - "build:wasm" - ], - "files": [ - "src/*.js", - "*.js", - "package.json" - ], - "output": [ - "dist" - ] - }, - "build:rust": { - "command": "cargo build --release --target wasm32-unknown-unknown", - "output": [ - "./target/wasm32-unknown-unknown/release/example.wasm" - ], - "files": [ - "Cargo.lock", - "Cargo.toml", - "src/**/*.rs", - "package.json" - ] - }, - "build:wasm": { - "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", - "dependencies": [ - "build:rust" - ], - "files": [ - "./target/wasm32-unknown-unknown/release/example_bg.wasm", - "package.json" - ], - "output": [ - "src/pkg" - ] - }, - "start": { - "command": "node dist/index.js", - "dependencies": [ - "build" - ] - }, - "deploy": { - "command": "clasp push -f", - "dependencies": [ - "build" - ], - "files": [ - ".clasp.json", - ".claspignore" - ] - } - }, - "author": "Justin Poehnelt ", - "license": "Apache-2.0", - "devDependencies": { - "@google/clasp": "^2.4.2", - "esbuild": "^0.20.1", - "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", - "vitest": "^1.3.1", - "wireit": "^0.14.4" - }, - "dependencies": { - "fastestsmallesttextencoderdecoder": "^1.0.22" - }, - "type": "module" + "name": "example", + "version": "0.1.0", + "description": "An example integration of WASM with Rust into Apps Script", + "scripts": { + "build": "wireit", + "build:rust": "wireit", + "build:wasm": "wireit", + "clean": "rm -rf dist pkg target", + "deploy": "wireit", + "format": "cargo fmt", + "start": "wireit" + }, + "wireit": { + "build": { + "command": "node build.js", + "dependencies": ["build:wasm"], + "files": ["src/*.js", "*.js", "package.json"], + "output": ["dist"] + }, + "build:rust": { + "command": "cargo build --release --target wasm32-unknown-unknown", + "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], + "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] + }, + "build:wasm": { + "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", + "dependencies": ["build:rust"], + "files": [ + "./target/wasm32-unknown-unknown/release/example_bg.wasm", + "package.json" + ], + "output": ["src/pkg"] + }, + "start": { + "command": "node dist/index.js", + "dependencies": ["build"] + }, + "deploy": { + "command": "clasp push -f", + "dependencies": ["build"], + "files": [".clasp.json", ".claspignore"] + } + }, + "author": "Justin Poehnelt ", + "license": "Apache-2.0", + "devDependencies": { + "@google/clasp": "^2.4.2", + "esbuild": "^0.20.1", + "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", + "vitest": "^1.3.1", + "wireit": "^0.14.4" + }, + "dependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22" + }, + "type": "module" } diff --git a/wasm/hello-world/polyfill.js b/wasm/hello-world/polyfill.js index e2f434747..c78434025 100644 --- a/wasm/hello-world/polyfill.js +++ b/wasm/hello-world/polyfill.js @@ -15,6 +15,6 @@ */ export { - TextEncoder, - TextDecoder, + TextEncoder, + TextDecoder, } from "fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js"; diff --git a/wasm/hello-world/src/appsscript.json b/wasm/hello-world/src/appsscript.json index 3ea28e5ae..d51e5c9d8 100644 --- a/wasm/hello-world/src/appsscript.json +++ b/wasm/hello-world/src/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/Denver", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Denver", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/wasm/hello-world/src/main.js b/wasm/hello-world/src/main.js index 801cd8791..df55df936 100644 --- a/wasm/hello-world/src/main.js +++ b/wasm/hello-world/src/main.js @@ -15,6 +15,6 @@ */ async function main() { - const name = "world"; - console.log(await hello_(name)); + const name = "world"; + console.log(await hello_(name)); } diff --git a/wasm/hello-world/src/test.js b/wasm/hello-world/src/test.js index f87ccb334..8c2fd3186 100644 --- a/wasm/hello-world/src/test.js +++ b/wasm/hello-world/src/test.js @@ -15,74 +15,75 @@ */ async function test() { - await assert(hello_("world"), "Hello, world from Rust!"); + await assert(hello_("world"), "Hello, world from Rust!"); } async function assert(a, b, message) { - const aVal = await a; - const bVal = await b; + const aVal = await a; + const bVal = await b; - if (aVal !== bVal) { - throw message ?? `'${aVal}' !== '${bVal}'`; - } + if (aVal !== bVal) { + throw message ?? `'${aVal}' !== '${bVal}'`; + } } async function latency(func, iterations, argsFunc = () => []) { - const executionTimes = []; + const executionTimes = []; - for (let i = 0; i < iterations; i++) { - const args = argsFunc(); + for (let i = 0; i < iterations; i++) { + const args = argsFunc(); - const startTime = Date.now(); - let endTime; + const startTime = Date.now(); + let endTime; - try { - await func(...args); - endTime = Date.now(); - } catch (e) { - endTime = Infinity; - console.error(e); - continue; - } + try { + await func(...args); + endTime = Date.now(); + } catch (e) { + endTime = Number.POSITIVE_INFINITY; + console.error(e); + continue; + } - executionTimes.push(endTime - startTime); - } + executionTimes.push(endTime - startTime); + } - // Calculate statistics - const min = Math.min(...executionTimes); - const max = Math.max(...executionTimes); - const totalTime = executionTimes.reduce((sum, time) => sum + time, 0); - const average = totalTime / iterations; + // Calculate statistics + const min = Math.min(...executionTimes); + const max = Math.max(...executionTimes); + const totalTime = executionTimes.reduce((sum, time) => sum + time, 0); + const average = totalTime / iterations; - return { - min: min, - max: max, - average: average, - totalTime, - // times: executionTimes // Array of all execution times - }; + return { + min: min, + max: max, + average: average, + totalTime, + // times: executionTimes // Array of all execution times + }; } async function benchmark() { - await hello_("world"); // Warmup - - console.log(await latency(hello_, 100, () => [generateRandomString(10)])); - console.log(await latency(hello_, 100, () => [generateRandomString(100)])); - console.log(await latency(hello_, 100, () => [generateRandomString(1000)])); - console.log(await latency(hello_, 100, () => [generateRandomString(10000)])); - console.log(await latency(hello_, 100, () => [generateRandomString(100000)])); - console.log(await latency(hello_, 30, () => [generateRandomString(1000000)])); + await hello_("world"); // Warmup + + console.log(await latency(hello_, 100, () => [generateRandomString(10)])); + console.log(await latency(hello_, 100, () => [generateRandomString(100)])); + console.log(await latency(hello_, 100, () => [generateRandomString(1000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(10000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(100000)])); + console.log(await latency(hello_, 30, () => [generateRandomString(1000000)])); } function generateRandomString(length = 1024) { - // Choose your desired character set - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; + // Choose your desired character set + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; - let result = ''; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } + let result = ""; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } - return result; -} \ No newline at end of file + return result; +} diff --git a/wasm/hello-world/src/wasm.js b/wasm/hello-world/src/wasm.js index 560a02217..6495e781c 100644 --- a/wasm/hello-world/src/wasm.js +++ b/wasm/hello-world/src/wasm.js @@ -20,12 +20,12 @@ * @returns */ async function hello_(name) { - const wasm = await import("./pkg/example_bg.wasm"); - const { __wbg_set_wasm, hello } = await import("./pkg/example_bg.js"); + const wasm = await import("./pkg/example_bg.wasm"); + const { __wbg_set_wasm, hello } = await import("./pkg/example_bg.js"); - __wbg_set_wasm(wasm); + __wbg_set_wasm(wasm); - return hello(name); + return hello(name); } globalThis.hello_ = hello_; diff --git a/wasm/image-add-on/.clasp.json b/wasm/image-add-on/.clasp.json index ae82bd7ba..6f4e159ea 100644 --- a/wasm/image-add-on/.clasp.json +++ b/wasm/image-add-on/.clasp.json @@ -1,4 +1,4 @@ { - "scriptId": "1gP1tiV1KkhVbMADIA_M-d4IJP1GNXwU7-7MundfqlESmSAdo0sC_Nml4", - "rootDir": "./dist" + "scriptId": "1gP1tiV1KkhVbMADIA_M-d4IJP1GNXwU7-7MundfqlESmSAdo0sC_Nml4", + "rootDir": "./dist" } diff --git a/wasm/image-add-on/build.js b/wasm/image-add-on/build.js index 78cd3bcc8..fb86dac14 100644 --- a/wasm/image-add-on/build.js +++ b/wasm/image-add-on/build.js @@ -23,22 +23,22 @@ const outdir = "dist"; const sourceRoot = "src"; await esbuild.build({ - entryPoints: ["./src/wasm.js"], - bundle: true, - outdir, - sourceRoot, - platform: "neutral", - format: "esm", - plugins: [wasmLoader({ mode: "embedded" })], - inject: ["polyfill.js"], - minify: true, - banner: { js: "// Generated code DO NOT EDIT\n" }, + entryPoints: ["./src/wasm.js"], + bundle: true, + outdir, + sourceRoot, + platform: "neutral", + format: "esm", + plugins: [wasmLoader({ mode: "embedded" })], + inject: ["polyfill.js"], + minify: true, + banner: { js: "// Generated code DO NOT EDIT\n" }, }); const passThroughFiles = ["main.js", "test.js", "appsscript.json", "add-on.js"]; await Promise.all( - passThroughFiles.map(async (file) => - fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), - ), + passThroughFiles.map(async (file) => + fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), + ), ); diff --git a/wasm/image-add-on/package.json b/wasm/image-add-on/package.json index a0c15d8cf..35ab9d6f0 100644 --- a/wasm/image-add-on/package.json +++ b/wasm/image-add-on/package.json @@ -1,86 +1,59 @@ { - "name": "example", - "version": "0.1.0", - "description": "An example integration of WASM with Rust into Apps Script", - "scripts": { - "build": "wireit", - "build:rust": "wireit", - "build:wasm": "wireit", - "clean": "rm -rf dist pkg target", - "deploy": "wireit", - "format": "cargo fmt", - "start": "wireit" - }, - "wireit": { - "build": { - "command": "node build.js", - "dependencies": [ - "build:wasm" - ], - "files": [ - "src/*.js", - "src/*.json", - "*.js", - "package.json" - ], - "output": [ - "dist" - ] - }, - "build:rust": { - "command": "cargo build --release --target wasm32-unknown-unknown", - "output": [ - "./target/wasm32-unknown-unknown/release/example.wasm" - ], - "files": [ - "Cargo.lock", - "Cargo.toml", - "src/**/*.rs", - "package.json" - ] - }, - "build:wasm": { - "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", - "dependencies": [ - "build:rust" - ], - "files": [ - "./target/wasm32-unknown-unknown/release/example_bg.wasm", - "package.json" - ], - "output": [ - "src/pkg" - ] - }, - "start": { - "command": "node dist/index.js", - "dependencies": [ - "build" - ] - }, - "deploy": { - "command": "clasp push -f", - "dependencies": [ - "build" - ], - "files": [ - ".clasp.json", - ".claspignore" - ] - } - }, - "author": "Justin Poehnelt ", - "license": "Apache-2.0", - "devDependencies": { - "@google/clasp": "^2.4.2", - "esbuild": "^0.20.1", - "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", - "vitest": "^1.3.1", - "wireit": "^0.14.4" - }, - "dependencies": { - "@types/google-apps-script": "^1.0.82", - "fastestsmallesttextencoderdecoder": "^1.0.22" - }, - "type": "module" + "name": "example", + "version": "0.1.0", + "description": "An example integration of WASM with Rust into Apps Script", + "scripts": { + "build": "wireit", + "build:rust": "wireit", + "build:wasm": "wireit", + "clean": "rm -rf dist pkg target", + "deploy": "wireit", + "format": "cargo fmt", + "start": "wireit" + }, + "wireit": { + "build": { + "command": "node build.js", + "dependencies": ["build:wasm"], + "files": ["src/*.js", "src/*.json", "*.js", "package.json"], + "output": ["dist"] + }, + "build:rust": { + "command": "cargo build --release --target wasm32-unknown-unknown", + "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], + "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] + }, + "build:wasm": { + "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", + "dependencies": ["build:rust"], + "files": [ + "./target/wasm32-unknown-unknown/release/example_bg.wasm", + "package.json" + ], + "output": ["src/pkg"] + }, + "start": { + "command": "node dist/index.js", + "dependencies": ["build"] + }, + "deploy": { + "command": "clasp push -f", + "dependencies": ["build"], + "files": [".clasp.json", ".claspignore"] + } + }, + "author": "Justin Poehnelt ", + "license": "Apache-2.0", + "devDependencies": { + "@google/clasp": "^2.4.2", + "esbuild": "^0.20.1", + "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", + "vitest": "^1.3.1", + "wireit": "^0.14.4" + }, + "dependencies": { + "@types/google-apps-script": "^1.0.82", + "fastestsmallesttextencoderdecoder": "^1.0.22" + }, + "type": "module" } diff --git a/wasm/image-add-on/polyfill.js b/wasm/image-add-on/polyfill.js index e2f434747..c78434025 100644 --- a/wasm/image-add-on/polyfill.js +++ b/wasm/image-add-on/polyfill.js @@ -15,6 +15,6 @@ */ export { - TextEncoder, - TextDecoder, + TextEncoder, + TextDecoder, } from "fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js"; diff --git a/wasm/image-add-on/src/add-on.js b/wasm/image-add-on/src/add-on.js index a99ff01cc..da3080059 100644 --- a/wasm/image-add-on/src/add-on.js +++ b/wasm/image-add-on/src/add-on.js @@ -15,126 +15,126 @@ */ const COLORS = { - RED: "#EA4335", + RED: "#EA4335", }; const properties = PropertiesService.getUserProperties(); async function card(items) { - const builder = CardService.newCardBuilder(); - - const { quality, format, width, height } = loadSettings(); - - const controls = CardService.newCardSection() - .addWidget( - CardService.newSelectionInput() - .setFieldName("quality") - .setTitle("Quality") - .setType(CardService.SelectionInputType.RADIO_BUTTON) - .addItem("Low", "low", quality === "low") - .addItem("Medium", "medium", quality === "medium") - .addItem("High", "high", quality === "high") - ) - .addWidget( - CardService.newTextInput() - .setFieldName("height") - .setTitle("Height") - .setMultiline(false) - .setValue(height ?? "") - ) - .addWidget( - CardService.newTextInput() - .setFieldName("width") - .setTitle("Width") - .setMultiline(false) - .setValue(width ?? "") - ) - .addWidget( - CardService.newTextButton() - .setBackgroundColor(COLORS.RED) - .setText("Apply Settings") - .setOnClickAction( - CardService.newAction() - .setFunctionName("updateSettings") - .setParameters({}) - .setLoadIndicator(CardService.LoadIndicator.SPINNER) - ) - ) - .setCollapsible(true) - .setNumUncollapsibleWidgets(0); - - builder.addSection(controls); - - const sections = await Promise.all( - ( - items ?? - JSON.parse( - PropertiesService.getUserProperties().getProperty("selectedItems") - ) - ) - .filter((item) => item.mimeType.startsWith("image")) - .map(async (item) => { - const section = CardService.newCardSection(); - - const bytes = DriveApp.getFileById(item.id).getBlob().getBytes(); - - const newBytes = await compress_(bytes, { - quality: qualityToInt(quality), - format: item.mimeType.split("/").pop(), - width: parseInt(width ?? "0"), - height: parseInt(height ?? "0"), - }); - - const dataUrl = `data:${item.mimeType};base64,${Utilities.base64Encode( - newBytes - )}`; - - section.addWidget(CardService.newImage().setImageUrl(dataUrl)); - - section.addWidget( - CardService.newDecoratedText().setText(bytesToText(newBytes.length)) - ); - - section.addWidget( - CardService.newButtonSet() - .addButton( - CardService.newTextButton() - .setBackgroundColor(COLORS.RED) - .setText("Save") - .setOnClickAction( - CardService.newAction() - .setFunctionName("save") - .setParameters({ - bytes: Utilities.base64Encode(newBytes), - action: "save", - item: JSON.stringify(item), - }) - ) - ) - .addButton( - CardService.newTextButton() - .setBackgroundColor(COLORS.RED) - .setText("Save Copy") - .setOnClickAction( - CardService.newAction() - .setFunctionName("save") - .setParameters({ - bytes: Utilities.base64Encode(newBytes), - action: "save-as", - item: JSON.stringify(item), - }) - ) - ) - ); - return section; - }) - ); - - for (const section of sections) { - builder.addSection(section); - } - - return builder; + const builder = CardService.newCardBuilder(); + + const { quality, format, width, height } = loadSettings(); + + const controls = CardService.newCardSection() + .addWidget( + CardService.newSelectionInput() + .setFieldName("quality") + .setTitle("Quality") + .setType(CardService.SelectionInputType.RADIO_BUTTON) + .addItem("Low", "low", quality === "low") + .addItem("Medium", "medium", quality === "medium") + .addItem("High", "high", quality === "high"), + ) + .addWidget( + CardService.newTextInput() + .setFieldName("height") + .setTitle("Height") + .setMultiline(false) + .setValue(height ?? ""), + ) + .addWidget( + CardService.newTextInput() + .setFieldName("width") + .setTitle("Width") + .setMultiline(false) + .setValue(width ?? ""), + ) + .addWidget( + CardService.newTextButton() + .setBackgroundColor(COLORS.RED) + .setText("Apply Settings") + .setOnClickAction( + CardService.newAction() + .setFunctionName("updateSettings") + .setParameters({}) + .setLoadIndicator(CardService.LoadIndicator.SPINNER), + ), + ) + .setCollapsible(true) + .setNumUncollapsibleWidgets(0); + + builder.addSection(controls); + + const sections = await Promise.all( + ( + items ?? + JSON.parse( + PropertiesService.getUserProperties().getProperty("selectedItems"), + ) + ) + .filter((item) => item.mimeType.startsWith("image")) + .map(async (item) => { + const section = CardService.newCardSection(); + + const bytes = DriveApp.getFileById(item.id).getBlob().getBytes(); + + const newBytes = await compress_(bytes, { + quality: qualityToInt(quality), + format: item.mimeType.split("/").pop(), + width: Number.parseInt(width ?? "0"), + height: Number.parseInt(height ?? "0"), + }); + + const dataUrl = `data:${item.mimeType};base64,${Utilities.base64Encode( + newBytes, + )}`; + + section.addWidget(CardService.newImage().setImageUrl(dataUrl)); + + section.addWidget( + CardService.newDecoratedText().setText(bytesToText(newBytes.length)), + ); + + section.addWidget( + CardService.newButtonSet() + .addButton( + CardService.newTextButton() + .setBackgroundColor(COLORS.RED) + .setText("Save") + .setOnClickAction( + CardService.newAction() + .setFunctionName("save") + .setParameters({ + bytes: Utilities.base64Encode(newBytes), + action: "save", + item: JSON.stringify(item), + }), + ), + ) + .addButton( + CardService.newTextButton() + .setBackgroundColor(COLORS.RED) + .setText("Save Copy") + .setOnClickAction( + CardService.newAction() + .setFunctionName("save") + .setParameters({ + bytes: Utilities.base64Encode(newBytes), + action: "save-as", + item: JSON.stringify(item), + }), + ), + ), + ); + return section; + }), + ); + + for (const section of sections) { + builder.addSection(section); + } + + return builder; } /** @@ -148,11 +148,11 @@ async function card(items) { * @return {Card} */ async function onItemsSelectedTrigger(e) { - PropertiesService.getUserProperties().setProperty( - "selectedItems", - JSON.stringify(e.drive.selectedItems) - ); - return (await card(e.drive.selectedItems)).build(); + PropertiesService.getUserProperties().setProperty( + "selectedItems", + JSON.stringify(e.drive.selectedItems), + ); + return (await card(e.drive.selectedItems)).build(); } /** @@ -165,89 +165,89 @@ async function onItemsSelectedTrigger(e) { * @return {DriveItemsSelectedActionResponse} */ function onRequestFileScopeButtonClicked(e) { - const idToRequest = e.parameters.id; - return CardService.newDriveItemsSelectedActionResponseBuilder() - .requestFileScope(idToRequest) - .build(); + const idToRequest = e.parameters.id; + return CardService.newDriveItemsSelectedActionResponseBuilder() + .requestFileScope(idToRequest) + .build(); } function onFileScopeGrantedTrigger(e) { - console.info("after granting item"); - console.info(e); - const builder = CardService.newCardBuilder(); - return builder.build(); + console.info("after granting item"); + console.info(e); + const builder = CardService.newCardBuilder(); + return builder.build(); } function onHomePageTrigger() { - return CardService.newCardBuilder() - .setHeader(CardService.newCardHeader().setTitle("Drive Image Compress")) - .addSection( - CardService.newCardSection().addWidget( - CardService.newTextParagraph().setText( - "Select one or more files in Drive to compress the image." - ) - ) - ) - .build(); + return CardService.newCardBuilder() + .setHeader(CardService.newCardHeader().setTitle("Drive Image Compress")) + .addSection( + CardService.newCardSection().addWidget( + CardService.newTextParagraph().setText( + "Select one or more files in Drive to compress the image.", + ), + ), + ) + .build(); } function bytesToText(bytes) { - const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; - if (bytes === 0) return "0 Byte"; - const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); - return `${Math.round(bytes / 1024 ** i)} ${sizes[i]}`; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + if (bytes === 0) return "0 Byte"; + const i = Number.parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); + return `${Math.round(bytes / 1024 ** i)} ${sizes[i]}`; } async function save(...args) { - console.log(args); - return CardService.newActionResponseBuilder() - .setNavigation(CardService.newNavigation().popToRoot()) - .build(); + console.log(args); + return CardService.newActionResponseBuilder() + .setNavigation(CardService.newNavigation().popToRoot()) + .build(); } async function updateSettings(e) { - console.log({ e }); - const { formInput } = e; - - persistSettings(formInput); - - return CardService.newActionResponseBuilder() - .setNavigation( - CardService.newNavigation() - .popToRoot() - .updateCard((await card()).build()) - ) - .build(); + console.log({ e }); + const { formInput } = e; + + persistSettings(formInput); + + return CardService.newActionResponseBuilder() + .setNavigation( + CardService.newNavigation() + .popToRoot() + .updateCard((await card()).build()), + ) + .build(); } function persistSettings(settings) { - properties.setProperty( - "settings", - JSON.stringify({ - ...loadSettings, - ...settings, - }) - ); + properties.setProperty( + "settings", + JSON.stringify({ + ...loadSettings, + ...settings, + }), + ); } function loadSettings() { - const defaults = { - quality: "medium", - }; - - return { - ...defaults, - ...JSON.parse(properties.getProperty("settings") ?? "{}"), - }; + const defaults = { + quality: "medium", + }; + + return { + ...defaults, + ...JSON.parse(properties.getProperty("settings") ?? "{}"), + }; } function qualityToInt(quality) { - switch (quality) { - case "low": - return 50; - case "medium": - return 80; - case "high": - return 90; - } + switch (quality) { + case "low": + return 50; + case "medium": + return 80; + case "high": + return 90; + } } diff --git a/wasm/image-add-on/src/appsscript.json b/wasm/image-add-on/src/appsscript.json index afaead7b4..1d8c9fed0 100644 --- a/wasm/image-add-on/src/appsscript.json +++ b/wasm/image-add-on/src/appsscript.json @@ -1,35 +1,35 @@ { - "timeZone": "America/Denver", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v3", - "serviceId": "drive" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "addOns": { - "common": { - "logoUrl": "https://ssl.gstatic.com/docs/script/images/logo/script-64.png", - "name": "Drive Image Compress Add-on", - "universalActions": [] - }, - "drive": { - "homepageTrigger": { - "runFunction": "onHomePageTrigger", - "enabled": true - }, - "onItemsSelectedTrigger": { - "runFunction": "onItemsSelectedTrigger" - } - } - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/script.locale", - "https://www.googleapis.com/auth/drive.addons.metadata.readonly", - "https://www.googleapis.com/auth/drive" - ] + "timeZone": "America/Denver", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "addOns": { + "common": { + "logoUrl": "https://ssl.gstatic.com/docs/script/images/logo/script-64.png", + "name": "Drive Image Compress Add-on", + "universalActions": [] + }, + "drive": { + "homepageTrigger": { + "runFunction": "onHomePageTrigger", + "enabled": true + }, + "onItemsSelectedTrigger": { + "runFunction": "onItemsSelectedTrigger" + } + } + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/script.locale", + "https://www.googleapis.com/auth/drive.addons.metadata.readonly", + "https://www.googleapis.com/auth/drive" + ] } diff --git a/wasm/image-add-on/src/main.js b/wasm/image-add-on/src/main.js index 19c6bcc9e..2a117326a 100644 --- a/wasm/image-add-on/src/main.js +++ b/wasm/image-add-on/src/main.js @@ -17,18 +17,18 @@ const QUALITY = 80; async function main() { - const iterator = DriveApp.getFilesByType("image/jpeg"); + const iterator = DriveApp.getFilesByType("image/jpeg"); - while (iterator.hasNext()) { - const file = iterator.next(); - const bytes = file.getBlob().getBytes(); + while (iterator.hasNext()) { + const file = iterator.next(); + const bytes = file.getBlob().getBytes(); - const dataUrl = await compress_(bytes, QUALITY); + const dataUrl = await compress_(bytes, QUALITY); - if (dataUrl) { - console.log(dataUrl); - } else { - console.warn("failed to decode image"); - } - } + if (dataUrl) { + console.log(dataUrl); + } else { + console.warn("failed to decode image"); + } + } } diff --git a/wasm/image-add-on/src/test.js b/wasm/image-add-on/src/test.js index f0832b375..a481eeb09 100644 --- a/wasm/image-add-on/src/test.js +++ b/wasm/image-add-on/src/test.js @@ -17,70 +17,71 @@ async function test() {} async function assert(a, b, message) { - const aVal = await a; - const bVal = await b; + const aVal = await a; + const bVal = await b; - if (aVal !== bVal) { - throw message ?? `'${aVal}' !== '${bVal}'`; - } + if (aVal !== bVal) { + throw message ?? `'${aVal}' !== '${bVal}'`; + } } async function latency(func, iterations, argsFunc = () => []) { - const executionTimes = []; + const executionTimes = []; - for (let i = 0; i < iterations; i++) { - const args = argsFunc(); + for (let i = 0; i < iterations; i++) { + const args = argsFunc(); - const startTime = Date.now(); - let endTime; + const startTime = Date.now(); + let endTime; - try { - await func(...args); - endTime = Date.now(); - } catch (e) { - endTime = Infinity; - console.error(e); - continue; - } + try { + await func(...args); + endTime = Date.now(); + } catch (e) { + endTime = Number.POSITIVE_INFINITY; + console.error(e); + continue; + } - executionTimes.push(endTime - startTime); - } + executionTimes.push(endTime - startTime); + } - // Calculate statistics - const min = Math.min(...executionTimes); - const max = Math.max(...executionTimes); - const totalTime = executionTimes.reduce((sum, time) => sum + time, 0); - const average = totalTime / iterations; + // Calculate statistics + const min = Math.min(...executionTimes); + const max = Math.max(...executionTimes); + const totalTime = executionTimes.reduce((sum, time) => sum + time, 0); + const average = totalTime / iterations; - return { - min: min, - max: max, - average: average, - totalTime, - // times: executionTimes // Array of all execution times - }; + return { + min: min, + max: max, + average: average, + totalTime, + // times: executionTimes // Array of all execution times + }; } async function benchmark() { - await hello_("world"); // Warmup - - console.log(await latency(hello_, 100, () => [generateRandomString(10)])); - console.log(await latency(hello_, 100, () => [generateRandomString(100)])); - console.log(await latency(hello_, 100, () => [generateRandomString(1000)])); - console.log(await latency(hello_, 100, () => [generateRandomString(10000)])); - console.log(await latency(hello_, 100, () => [generateRandomString(100000)])); - console.log(await latency(hello_, 30, () => [generateRandomString(1000000)])); + await hello_("world"); // Warmup + + console.log(await latency(hello_, 100, () => [generateRandomString(10)])); + console.log(await latency(hello_, 100, () => [generateRandomString(100)])); + console.log(await latency(hello_, 100, () => [generateRandomString(1000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(10000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(100000)])); + console.log(await latency(hello_, 30, () => [generateRandomString(1000000)])); } function generateRandomString(length = 1024) { - // Choose your desired character set - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; + // Choose your desired character set + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; - let result = ''; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } + let result = ""; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } - return result; -} \ No newline at end of file + return result; +} diff --git a/wasm/image-add-on/src/wasm.js b/wasm/image-add-on/src/wasm.js index 64afda151..7f1d544aa 100644 --- a/wasm/image-add-on/src/wasm.js +++ b/wasm/image-add-on/src/wasm.js @@ -15,23 +15,23 @@ */ async function compress_(bytes, { quality, format, width, height }) { - const wasm = await import("./pkg/example_bg.wasm"); - const { __wbg_set_wasm, compress } = await import("./pkg/example_bg.js"); + const wasm = await import("./pkg/example_bg.wasm"); + const { __wbg_set_wasm, compress } = await import("./pkg/example_bg.js"); - __wbg_set_wasm(wasm); + __wbg_set_wasm(wasm); - width = width || 0; - height = height || 0; + width = width || 0; + height = height || 0; - console.log({ quality, format, width, height }); + console.log({ quality, format, width, height }); - const result = compress(bytes, quality, format, width, height); + const result = compress(bytes, quality, format, width, height); - if (typeof result === "string") { - throw new Error(result); - } + if (typeof result === "string") { + throw new Error(result); + } - return result; + return result; } globalThis.compress_ = compress_; diff --git a/wasm/python/.clasp.json b/wasm/python/.clasp.json index 5e29e1288..5ee8a1774 100644 --- a/wasm/python/.clasp.json +++ b/wasm/python/.clasp.json @@ -1,4 +1,4 @@ { - "scriptId": "1_tU8IFkT1ZZ-b08YFeC8umntrH92WVQ27jvUmsCo1W4ZqKKqcytBLdcn", - "rootDir": "./dist" + "scriptId": "1_tU8IFkT1ZZ-b08YFeC8umntrH92WVQ27jvUmsCo1W4ZqKKqcytBLdcn", + "rootDir": "./dist" } diff --git a/wasm/python/build.js b/wasm/python/build.js index c0ea8f299..a965ca7c8 100644 --- a/wasm/python/build.js +++ b/wasm/python/build.js @@ -23,22 +23,22 @@ const outdir = "dist"; const sourceRoot = "src"; await esbuild.build({ - entryPoints: ["./src/wasm.js"], - bundle: true, - outdir, - sourceRoot, - platform: "neutral", - format: "esm", - plugins: [wasmLoader({ mode: "embedded" })], - inject: ["polyfill.js"], - minify: true, - banner: { js: "// Generated code DO NOT EDIT\n" }, + entryPoints: ["./src/wasm.js"], + bundle: true, + outdir, + sourceRoot, + platform: "neutral", + format: "esm", + plugins: [wasmLoader({ mode: "embedded" })], + inject: ["polyfill.js"], + minify: true, + banner: { js: "// Generated code DO NOT EDIT\n" }, }); const passThroughFiles = ["main.js", "test.js", "appsscript.json"]; await Promise.all( - passThroughFiles.map(async (file) => - fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), - ), + passThroughFiles.map(async (file) => + fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), + ), ); diff --git a/wasm/python/package.json b/wasm/python/package.json index 4bc79474a..73e8eeab5 100644 --- a/wasm/python/package.json +++ b/wasm/python/package.json @@ -1,85 +1,58 @@ { - "name": "example", - "version": "0.1.0", - "description": "An example integration of WASM with Rust into Apps Script", - "scripts": { - "build": "wireit", - "build:rust": "wireit", - "build:wasm": "wireit", - "clean": "rm -rf dist pkg target", - "deploy": "wireit", - "format": "cargo fmt", - "start": "wireit" - }, - "wireit": { - "build": { - "command": "node build.js", - "dependencies": [ - "build:wasm" - ], - "files": [ - "src/*.js", - "src/*.json", - "*.js", - "package.json" - ], - "output": [ - "dist" - ] - }, - "build:rust": { - "command": "cargo build --release --target wasm32-unknown-unknown", - "output": [ - "./target/wasm32-unknown-unknown/release/example.wasm" - ], - "files": [ - "Cargo.lock", - "Cargo.toml", - "src/**/*.rs", - "package.json" - ] - }, - "build:wasm": { - "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", - "dependencies": [ - "build:rust" - ], - "files": [ - "./target/wasm32-unknown-unknown/release/example_bg.wasm", - "package.json" - ], - "output": [ - "src/pkg" - ] - }, - "start": { - "command": "node dist/index.js", - "dependencies": [ - "build" - ] - }, - "deploy": { - "command": "clasp push -f", - "dependencies": [ - "build" - ], - "files": [ - ".clasp.json", - ".claspignore" - ] - } - }, - "author": "Justin Poehnelt ", - "license": "Apache-2.0", - "devDependencies": { - "@google/clasp": "^2.4.2", - "esbuild": "^0.20.1", - "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", - "vitest": "^1.3.1", - "wireit": "^0.14.4" - }, - "dependencies": { - "fastestsmallesttextencoderdecoder": "^1.0.22" - }, - "type": "module" + "name": "example", + "version": "0.1.0", + "description": "An example integration of WASM with Rust into Apps Script", + "scripts": { + "build": "wireit", + "build:rust": "wireit", + "build:wasm": "wireit", + "clean": "rm -rf dist pkg target", + "deploy": "wireit", + "format": "cargo fmt", + "start": "wireit" + }, + "wireit": { + "build": { + "command": "node build.js", + "dependencies": ["build:wasm"], + "files": ["src/*.js", "src/*.json", "*.js", "package.json"], + "output": ["dist"] + }, + "build:rust": { + "command": "cargo build --release --target wasm32-unknown-unknown", + "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], + "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] + }, + "build:wasm": { + "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", + "dependencies": ["build:rust"], + "files": [ + "./target/wasm32-unknown-unknown/release/example_bg.wasm", + "package.json" + ], + "output": ["src/pkg"] + }, + "start": { + "command": "node dist/index.js", + "dependencies": ["build"] + }, + "deploy": { + "command": "clasp push -f", + "dependencies": ["build"], + "files": [".clasp.json", ".claspignore"] + } + }, + "author": "Justin Poehnelt ", + "license": "Apache-2.0", + "devDependencies": { + "@google/clasp": "^2.4.2", + "esbuild": "^0.20.1", + "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", + "vitest": "^1.3.1", + "wireit": "^0.14.4" + }, + "dependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22" + }, + "type": "module" } diff --git a/wasm/python/polyfill.js b/wasm/python/polyfill.js index e2f434747..c78434025 100644 --- a/wasm/python/polyfill.js +++ b/wasm/python/polyfill.js @@ -15,6 +15,6 @@ */ export { - TextEncoder, - TextDecoder, + TextEncoder, + TextDecoder, } from "fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js"; diff --git a/wasm/python/src/appsscript.json b/wasm/python/src/appsscript.json index 97141a6f6..bd143bff8 100644 --- a/wasm/python/src/appsscript.json +++ b/wasm/python/src/appsscript.json @@ -1,15 +1,15 @@ { - "timeZone": "America/Denver", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v3", - "serviceId": "drive" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [] + "timeZone": "America/Denver", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [] } diff --git a/wasm/python/src/main.js b/wasm/python/src/main.js index f9a50f877..c790bf1be 100644 --- a/wasm/python/src/main.js +++ b/wasm/python/src/main.js @@ -23,11 +23,11 @@ * @customfunction */ async function PYTHON(code = "args", ...args) { - const result = await python_(`${code}`, ...args); + const result = await python_(`${code}`, ...args); - if (result instanceof Error) { - throw result; - } + if (result instanceof Error) { + throw result; + } - return result; + return result; } diff --git a/wasm/python/src/test.js b/wasm/python/src/test.js index 5b02a51b3..745604b7e 100644 --- a/wasm/python/src/test.js +++ b/wasm/python/src/test.js @@ -17,10 +17,10 @@ async function test() {} async function assert(a, b, message) { - const aVal = await a; - const bVal = await b; + const aVal = await a; + const bVal = await b; - if (aVal !== bVal) { - throw message ?? `'${aVal}' !== '${bVal}'`; - } + if (aVal !== bVal) { + throw message ?? `'${aVal}' !== '${bVal}'`; + } } diff --git a/wasm/python/src/wasm.js b/wasm/python/src/wasm.js index 8ffe281bc..0c0e36d22 100644 --- a/wasm/python/src/wasm.js +++ b/wasm/python/src/wasm.js @@ -15,7 +15,7 @@ */ globalThis.crypto = { - getRandomValues: (array) => array.map(() => Math.floor(Math.random() * 256)), + getRandomValues: (array) => array.map(() => Math.floor(Math.random() * 256)), }; /** @@ -24,12 +24,12 @@ globalThis.crypto = { * @returns */ async function python_(source, ...args) { - const wasm = await import("./pkg/example_bg.wasm"); - const { __wbg_set_wasm, python } = await import("./pkg/example_bg.js"); + const wasm = await import("./pkg/example_bg.wasm"); + const { __wbg_set_wasm, python } = await import("./pkg/example_bg.js"); - __wbg_set_wasm(wasm); + __wbg_set_wasm(wasm); - return await python(source, args); + return await python(source, args); } globalThis.python_ = python_; From f1a0d3f4e2753fabaf0286f7006447d7dd262293 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 16:27:16 -0700 Subject: [PATCH 03/12] style: biome check --write --unsafe --- .github/scripts/check-gs.ts | 8 +- ai/autosummarize/gemini.js | 6 +- ai/custom-func-ai-agent/AiVertex.js | 6 +- ai/custom-func-ai-agent/Code.js | 2 +- ai/custom-func-ai-studio/gemini.js | 3 +- ai/custom_func_vertex/aiVertex.js | 6 +- ai/devdocs-link-preview/Vertex.js | 6 +- ai/drive-rename/ai.js | 6 +- ai/drive-rename/drive.js | 10 +-- ai/drive-rename/ui.js | 2 +- ai/standup-chat-app/db.js | 6 +- ai/standup-chat-app/main.js | 6 +- ai/standup-chat-app/memoize.js | 7 +- solutions/add-on/book-smartchip/Code.js | 2 +- solutions/add-on/share-macro/Code.js | 74 +++++++++---------- solutions/add-on/share-macro/UI.js | 17 ++--- solutions/automations/agenda-maker/Code.js | 12 +-- .../aggregate-document-content/Code.js | 54 +++++++------- solutions/automations/bracket-maker/Code.js | 12 +-- solutions/automations/content-signup/Code.js | 15 ++-- .../course-feedback-response/Code.js | 29 +------- .../automations/employee-certificate/Code.js | 19 ++--- .../automations/equipment-requests/Code.js | 14 ++-- .../automations/event-session-signup/Code.js | 16 ++-- .../feedback-sentiment-analysis/code.js | 10 +-- solutions/automations/generate-pdfs/Code.js | 50 +++---------- .../automations/import-csv-sheets/Code.js | 21 ++---- .../import-csv-sheets/SampleData.js | 4 +- .../import-csv-sheets/SetupSample.js | 4 +- solutions/automations/mail-merge/Code.js | 14 ++-- solutions/automations/news-sentiment/Code.js | 38 +++++----- .../offsite-activity-signup/Code.js | 22 +++--- .../tax-loss-harvest-alerts/Code.js | 10 +-- solutions/automations/timesheets/Code.js | 9 +-- solutions/automations/upload-files/Code.js | 8 +- solutions/automations/upload-files/Setup.js | 2 +- .../automations/vacation-calendar/Code.js | 28 +++---- solutions/automations/youtube-tracker/Code.js | 4 +- .../calculate-driving-distance/Code.js | 10 +-- solutions/editor-add-on/clean-sheet/Code.js | 4 +- solutions/ooo-chat-app/Code.js | 2 +- solutions/schedule-meetings/Code.js | 18 ++--- solutions/schedule-meetings/Dialog.js | 2 +- solutions/schedule-meetings/Utilities.js | 2 +- wasm/hello-world/build.js | 4 +- wasm/image-add-on/build.js | 4 +- wasm/python/build.js | 4 +- 47 files changed, 258 insertions(+), 354 deletions(-) diff --git a/.github/scripts/check-gs.ts b/.github/scripts/check-gs.ts index 3afc61f33..6712405ed 100644 --- a/.github/scripts/check-gs.ts +++ b/.github/scripts/check-gs.ts @@ -16,7 +16,7 @@ /// -import { exec } from "child_process"; +import { exec } from "node:child_process"; import { copyFileSync, existsSync, @@ -25,9 +25,9 @@ import { rmSync, statSync, writeFileSync, -} from "fs"; -import { dirname, join, relative, resolve, sep } from "path"; -import { promisify } from "util"; +} from "node:fs"; +import { dirname, join, relative, resolve, sep } from "node:path"; +import { promisify } from "node:util"; const execAsync = promisify(exec); const TEMP_ROOT = ".tsc_check"; diff --git a/ai/autosummarize/gemini.js b/ai/autosummarize/gemini.js index aecf83589..d3856f2be 100644 --- a/ai/autosummarize/gemini.js +++ b/ai/autosummarize/gemini.js @@ -107,12 +107,12 @@ function credentialsForVertexAI() { const parsedCredentials = JSON.parse(credentials); const service = OAuth2.createService("Vertex") .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials["private_key"]) - .setIssuer(parsedCredentials["client_email"]) + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) .setPropertyStore(PropertiesService.getScriptProperties()) .setScope("https://www.googleapis.com/auth/cloud-platform"); return { - projectId: parsedCredentials["project_id"], + projectId: parsedCredentials.project_id, accessToken: service.getAccessToken(), }; } diff --git a/ai/custom-func-ai-agent/AiVertex.js b/ai/custom-func-ai-agent/AiVertex.js index 286896d7d..f84c69f1a 100644 --- a/ai/custom-func-ai-agent/AiVertex.js +++ b/ai/custom-func-ai-agent/AiVertex.js @@ -108,12 +108,12 @@ function credentialsForVertexAI() { const service = OAuth2.createService("Vertex") .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials["private_key"]) - .setIssuer(parsedCredentials["client_email"]) + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) .setPropertyStore(PropertiesService.getScriptProperties()) .setScope("https://www.googleapis.com/auth/cloud-platform"); return { - projectId: parsedCredentials["project_id"], + projectId: parsedCredentials.project_id, accessToken: service.getAccessToken(), }; } diff --git a/ai/custom-func-ai-agent/Code.js b/ai/custom-func-ai-agent/Code.js index ba4fa90dc..1dd44ffd6 100644 --- a/ai/custom-func-ai-agent/Code.js +++ b/ai/custom-func-ai-agent/Code.js @@ -26,7 +26,7 @@ limitations under the License. * @customfunction */ function FACT_CHECK(statement, outputFormat) { - if (!outputFormat || outputFormat == "") { + if (!outputFormat || outputFormat === "") { outputFormat = "Summarize it. Only keep the verdict result and main arguments. " + "Do not reiterate the fact being checked. Remove all markdown. " + diff --git a/ai/custom-func-ai-studio/gemini.js b/ai/custom-func-ai-studio/gemini.js index 5a567da10..7a595af6a 100644 --- a/ai/custom-func-ai-studio/gemini.js +++ b/ai/custom-func-ai-studio/gemini.js @@ -67,8 +67,7 @@ function getAiSummary(prompt) { const apiKey = PropertiesService.getScriptProperties().getProperty("api_key"); const response = UrlFetchApp.fetch( - "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=" + - apiKey, + `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`, options, ); diff --git a/ai/custom_func_vertex/aiVertex.js b/ai/custom_func_vertex/aiVertex.js index c288e09b4..2777aa62c 100644 --- a/ai/custom_func_vertex/aiVertex.js +++ b/ai/custom_func_vertex/aiVertex.js @@ -105,12 +105,12 @@ function credentialsForVertexAI() { const service = OAuth2.createService("Vertex") .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials["private_key"]) - .setIssuer(parsedCredentials["client_email"]) + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) .setPropertyStore(PropertiesService.getScriptProperties()) .setScope("https://www.googleapis.com/auth/cloud-platform"); return { - projectId: parsedCredentials["project_id"], + projectId: parsedCredentials.project_id, accessToken: service.getAccessToken(), }; } diff --git a/ai/devdocs-link-preview/Vertex.js b/ai/devdocs-link-preview/Vertex.js index 9e110e894..0a4b017f2 100644 --- a/ai/devdocs-link-preview/Vertex.js +++ b/ai/devdocs-link-preview/Vertex.js @@ -103,12 +103,12 @@ function credentialsForVertexAI() { const parsedCredentials = JSON.parse(credentials); const service = OAuth2.createService("Vertex") .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials["private_key"]) - .setIssuer(parsedCredentials["client_email"]) + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) .setPropertyStore(PropertiesService.getScriptProperties()) .setScope("https://www.googleapis.com/auth/cloud-platform"); return { - projectId: parsedCredentials["project_id"], + projectId: parsedCredentials.project_id, accessToken: service.getAccessToken(), }; } diff --git a/ai/drive-rename/ai.js b/ai/drive-rename/ai.js index 9e6dbd081..3c106a69a 100644 --- a/ai/drive-rename/ai.js +++ b/ai/drive-rename/ai.js @@ -99,12 +99,12 @@ function credentialsForVertexAI() { const service = OAuth2.createService("Vertex") .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials["private_key"]) - .setIssuer(parsedCredentials["client_email"]) + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) .setPropertyStore(PropertiesService.getScriptProperties()) .setScope("https://www.googleapis.com/auth/cloud-platform"); return { - projectId: parsedCredentials["project_id"], + projectId: parsedCredentials.project_id, accessToken: service.getAccessToken(), }; } diff --git a/ai/drive-rename/drive.js b/ai/drive-rename/drive.js index 38458ab57..5d1487dac 100644 --- a/ai/drive-rename/drive.js +++ b/ai/drive-rename/drive.js @@ -77,9 +77,9 @@ function updateCard(e) { * @return {string} The body of the Google Document. */ function getDocumentBody(id) { - var doc = DocumentApp.openById(id); - var body = doc.getBody(); - var text = body.getText(); + const doc = DocumentApp.openById(id); + const body = doc.getBody(); + const text = body.getText(); return text; } @@ -94,10 +94,10 @@ function getDocAPIBody(id) { // Call DOC API REST endpoint to get the file const url = `https://docs.googleapis.com/v1/documents/${id}`; - var response = UrlFetchApp.fetch(url, { + const response = UrlFetchApp.fetch(url, { method: "GET", headers: { - Authorization: "Bearer " + ScriptApp.getOAuthToken(), + Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, }, muteHttpExceptions: true, }); diff --git a/ai/drive-rename/ui.js b/ai/drive-rename/ui.js index 65f7467e0..b1c57e94c 100644 --- a/ai/drive-rename/ui.js +++ b/ai/drive-rename/ui.js @@ -32,7 +32,7 @@ function buildSelectionPage(e) { const selected = e.drive.activeCursorItem; // Check if Google Doc type, respond unsupported if not - if (selected.mimeType != "application/vnd.google-apps.document") { + if (selected.mimeType !== "application/vnd.google-apps.document") { return { sections: [ { diff --git a/ai/standup-chat-app/db.js b/ai/standup-chat-app/db.js index 3f65ae832..cd486ebdd 100644 --- a/ai/standup-chat-app/db.js +++ b/ai/standup-chat-app/db.js @@ -51,7 +51,7 @@ class DB { let sheet = spreadsheet.getSheetByName(this.sheetName); // create if it does not exist - if (sheet == undefined) { + if (sheet === undefined) { sheet = spreadsheet.insertSheet(); sheet.setName(this.sheetName); } @@ -83,11 +83,11 @@ function testDB() { const db = new DB(SPREADSHEET_ID); let thread = db.last; - if (thread == undefined) return; + if (thread === undefined) return; console.log(thread); db.rowOffset = 1; thread = db.last; - if (thread == undefined) return; + if (thread === undefined) return; console.log(thread); } diff --git a/ai/standup-chat-app/main.js b/ai/standup-chat-app/main.js index 1271c7594..3cf5c327d 100644 --- a/ai/standup-chat-app/main.js +++ b/ai/standup-chat-app/main.js @@ -23,7 +23,7 @@ const SPREADSHEET_ID = const SPACE_NAME = PropertiesService.getScriptProperties().getProperty("SPACE_NAME"); // e.g. "spaces/AAAABCa12Cc" -const SUMMARY_HEADER = `\n\n*Gemini Generated Summary*\n\n`; +const SUMMARY_HEADER = "\n\n*Gemini Generated Summary*\n\n"; /** * Sends the message to create new standup instance. @@ -65,7 +65,7 @@ function summarize() { const db = new DB(SPREADSHEET_ID); const last = db.last; - if (last == undefined) return; + if (last === undefined) return; const filter = `thread.name=${last.thread.name}`; let { messages } = Chat.Spaces.Messages.list( @@ -91,7 +91,7 @@ function summarize() { ); const summary = response.candidates[0].content?.parts[0].text; - if (summary == undefined) { + if (summary === undefined) { return; } diff --git a/ai/standup-chat-app/memoize.js b/ai/standup-chat-app/memoize.js index 01f6076d6..5414a2931 100644 --- a/ai/standup-chat-app/memoize.js +++ b/ai/standup-chat-app/memoize.js @@ -56,10 +56,9 @@ function memoize(func, ttl = 600, cache = CacheService.getScriptCache()) { if (cached != null) { return JSON.parse(cached); - } else { - const result = func(...args); - cache.put(key, JSON.stringify(result), ttl); - return result; } + const result = func(...args); + cache.put(key, JSON.stringify(result), ttl); + return result; }; } diff --git a/solutions/add-on/book-smartchip/Code.js b/solutions/add-on/book-smartchip/Code.js index 7f7360049..6bea50e00 100644 --- a/solutions/add-on/book-smartchip/Code.js +++ b/solutions/add-on/book-smartchip/Code.js @@ -18,7 +18,7 @@ function bookLinkPreview(event) { const bookPageCount = bookData.volumeInfo.pageCount; const previewHeader = CardService.newCardHeader() - .setSubtitle("By " + bookAuthors) + .setSubtitle(`By ${bookAuthors}`) .setTitle(bookTitle); const previewPages = CardService.newDecoratedText() diff --git a/solutions/add-on/share-macro/Code.js b/solutions/add-on/share-macro/Code.js index 0bf32ad24..33bf095ce 100644 --- a/solutions/add-on/share-macro/Code.js +++ b/solutions/add-on/share-macro/Code.js @@ -54,24 +54,23 @@ const APPS_SCRIPT_API = { * @return {Object} - JSON representation of source project. */ get: function (scriptId) { - const url = "https://script.googleapis.com/v1/projects/" + scriptId; + const url = `https://script.googleapis.com/v1/projects/${scriptId}`; const options = { method: "get", headers: { - Authorization: "Bearer " + this.accessToken, + Authorization: `Bearer ${this.accessToken}`, }, muteHttpExceptions: true, }; const res = UrlFetchApp.fetch(url, options); - if (res.getResponseCode() == 200) { + 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; } + 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 @@ -84,7 +83,7 @@ const APPS_SCRIPT_API = { const url = "https://script.googleapis.com/v1/projects"; const options = { headers: { - Authorization: "Bearer " + this.accessToken, + Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json", }, muteHttpExceptions: true, @@ -96,16 +95,15 @@ const APPS_SCRIPT_API = { } options.payload = JSON.stringify(options.payload); let res = UrlFetchApp.fetch(url, options); - if (res.getResponseCode() == 200) { + 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; } + 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. @@ -113,28 +111,26 @@ const APPS_SCRIPT_API = { * @return {Object} - JSON representation of Apps Script project content. */ getContent: function (scriptId) { - const url = - "https://script.googleapis.com/v1/projects/" + scriptId + "/content"; + const url = `https://script.googleapis.com/v1/projects/${scriptId}/content`; const options = { method: "get", headers: { - Authorization: "Bearer " + this.accessToken, + Authorization: `Bearer ${this.accessToken}`, }, muteHttpExceptions: true, }; let res = UrlFetchApp.fetch(url, options); - if (res.getResponseCode() == 200) { + 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; + return res.files; } + 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 @@ -144,26 +140,24 @@ const APPS_SCRIPT_API = { * @return {boolean} - Result status of the function. */ updateContent: function (scriptId, files) { - const url = - "https://script.googleapis.com/v1/projects/" + scriptId + "/content"; + const url = `https://script.googleapis.com/v1/projects/${scriptId}/content`; const options = { method: "put", headers: { - Authorization: "Bearer " + this.accessToken, + Authorization: `Bearer ${this.accessToken}`, }, contentType: "application/json", payload: JSON.stringify({ files: files }), muteHttpExceptions: true, }; const res = UrlFetchApp.fetch(url, options); - if (res.getResponseCode() == 200) { + 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; } + console.log(`An error occurred updating content of script ${scriptId}`); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; }, }; diff --git a/solutions/add-on/share-macro/UI.js b/solutions/add-on/share-macro/UI.js index 0d7bf799a..a1f5a2871 100644 --- a/solutions/add-on/share-macro/UI.js +++ b/solutions/add-on/share-macro/UI.js @@ -46,7 +46,7 @@ function createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors) { // If form errors exist, configures section with error messages. let showErrors = false; - if (errors && errors.length) { + if (errors?.length) { showErrors = true; let msg = errors.reduce((str, err) => `${str}• ${err}
    `, ""); msg = `Form submission errors:
    ${msg}`; @@ -127,7 +127,7 @@ function createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors) { ); // Configures card footer with action to copy the macro. - var cardFooter = CardService.newFixedFooter().setPrimaryButton( + const cardFooter = CardService.newFixedFooter().setPrimaryButton( CardService.newTextButton() .setText("Share macro") .setOnClickAction( @@ -192,16 +192,15 @@ function onClickFunction_(e) { errors.push("Invalid spreadsheet URL"); } - if (errors && errors.length) { + if (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); } + // Calls shareMacro function to copy the project. + shareMacro_(sourceScriptId, targetSpreadsheetUrl); + + // Creates a success card to display to users. + return buildSuccessCard(e, targetSpreadsheetUrl); } /** diff --git a/solutions/automations/agenda-maker/Code.js b/solutions/automations/agenda-maker/Code.js index 0e40c3b7a..c24172dd2 100644 --- a/solutions/automations/agenda-maker/Code.js +++ b/solutions/automations/agenda-maker/Code.js @@ -28,9 +28,9 @@ function checkFolder() { while (folders.hasNext()) { const folder = folders.next(); if ( - folder.getDescription() == + folder.getDescription() === "Apps Script App - Do not change this description" && - folder.getOwner().getEmail() == Session.getActiveUser().getEmail() + folder.getOwner().getEmail() === Session.getActiveUser().getEmail() ) { return folder.getId(); } @@ -134,13 +134,13 @@ function onCalendarChange() { // Confirms whether the event has the #agenda tag let description = event.getDescription(); - if (description.search("#agenda") == -1) continue; + 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()); + newDoc.setName(`Agenda for ${event.getTitle()}`); const file = DriveApp.getFileById(newDoc.getId()); folder.addFile(file); @@ -163,10 +163,10 @@ function onCalendarChange() { } // Replaces the tag with a link to the agenda document - const agendaUrl = "https://docs.google.com/document/d/" + newDoc.getId(); + const agendaUrl = `https://docs.google.com/document/d/${newDoc.getId()}`; description = description.replace( "#agenda", - "Agenda Doc", + `Agenda Doc`, ); event.setDescription(description); diff --git a/solutions/automations/aggregate-document-content/Code.js b/solutions/automations/aggregate-document-content/Code.js index e2946fd5a..227e35f40 100644 --- a/solutions/automations/aggregate-document-content/Code.js +++ b/solutions/automations/aggregate-document-content/Code.js @@ -91,24 +91,23 @@ function performImport() { 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); } + 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:`; + if (noContentList.length !== 0) { + msg += "\n\nThe following documents had no updates:"; for (const file of noContentList) { msg += `\n ${file}`; } @@ -124,7 +123,7 @@ function performImport() { */ function getContent(body) { // Finds the heading paragraph with matching style, keywords and !color. - var parValidHeading; + let parValidHeading; const searchType = DocumentApp.ElementType.PARAGRAPH; const searchHeading = APP_STYLE; let searchResult = null; @@ -132,12 +131,12 @@ function getContent(body) { // Gets and loops through all paragraphs that match the style of APP_STYLE. while ((searchResult = body.findElement(searchType, searchResult))) { const par = searchResult.getElement().asParagraph(); - if (par.getHeading() == searchHeading) { + if (par.getHeading() === searchHeading) { // If heading style matches, searches for text string (case insensitive). - const findPos = par.findText("(?i)" + FIND_TEXT_KEYWORDS); + const 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) { + if (par.editAsText().getForegroundColor() !== TEXT_COLOR) { parValidHeading = par; } } @@ -146,19 +145,18 @@ function getContent(body) { if (!parValidHeading) { return; - } else { - // Updates the heading color to indicate that the summary has been imported. - const style = {}; - style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR; - parValidHeading.setAttributes(style); - parValidHeading.appendText(" [Exported]"); - - // Gets the content from the table following the valid heading. - const elemObj = parValidHeading.getNextSibling().asTable(); - const content = elemObj.copy(); - - return content; } + // Updates the heading color to indicate that the summary has been imported. + const style = {}; + style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR; + parValidHeading.setAttributes(style); + parValidHeading.appendText(" [Exported]"); + + // Gets the content from the table following the valid heading. + const elemObj = parValidHeading.getNextSibling().asTable(); + const content = elemObj.copy(); + + return content; } /** diff --git a/solutions/automations/bracket-maker/Code.js b/solutions/automations/bracket-maker/Code.js index dccabe418..e27a093c8 100644 --- a/solutions/automations/bracket-maker/Code.js +++ b/solutions/automations/bracket-maker/Code.js @@ -53,7 +53,7 @@ function createBracket() { // 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) { + if (!players[i][0] || players[i][0].length === 0) { break; } numPlayers++; @@ -79,7 +79,7 @@ function createBracket() { let upperPower = Math.ceil(Math.log(numPlayers) / Math.log(2)); // Calculates the number that is a power of 2 and lower than numPlayers. - const countNodesUpperBound = Math.pow(2, upperPower); + const countNodesUpperBound = 2 ** upperPower; // Calculates the number that is a power of 2 and higher than numPlayers. const countNodesLowerBound = countNodesUpperBound / 2; @@ -106,10 +106,10 @@ function createBracket() { // Fills in the rest of the bracket. upperPower--; for (let i = 0; i < upperPower; i++) { - const pow1 = Math.pow(2, i + 1); - const pow2 = Math.pow(2, i + 2); - const pow3 = Math.pow(2, i + 3); - for (let j = 0; j < Math.pow(2, upperPower - i - 1); j++) { + const pow1 = 2 ** (i + 1); + const pow2 = 2 ** (i + 2); + const pow3 = 2 ** (i + 3); + for (let j = 0; j < 2 ** (upperPower - i - 1); j++) { setBracketItem_(sheetResults.getRange(j * pow3 + pow2, i * 2 + 5)); setConnector_( sheetResults, diff --git a/solutions/automations/content-signup/Code.js b/solutions/automations/content-signup/Code.js index 967cbde23..95c493d88 100644 --- a/solutions/automations/content-signup/Code.js +++ b/solutions/automations/content-signup/Code.js @@ -66,7 +66,7 @@ function onFormSubmit(e) { const topics = Object.keys(topicUrls).filter((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; + return topicsString.indexOf(topic.toLowerCase()) !== -1; }); // If there is at least one topic selected, send an email to the recipient. @@ -88,7 +88,7 @@ function onFormSubmit(e) { const column = e.values.length + 1; sheet.getRange(row, column).setValue(status); - console.log("status=" + status + "; responses=" + JSON.stringify(responses)); + console.log(`status=${status}; responses=${JSON.stringify(responses)}`); } /** @@ -102,10 +102,10 @@ function createEmailBody(name, topics) { let topicsHtml = topics .map((topic) => { const url = topicUrls[topic]; - return '
  • ' + topic + "
  • "; + return `
  • ${topic}
  • `; }) .join(""); - topicsHtml = "
      " + topicsHtml + "
    "; + topicsHtml = `
      ${topicsHtml}
    `; // Make sure to update the emailTemplateDocId at the top. const docId = DocumentApp.openByUrl(EMAIL_TEMPLATE_DOC_URL).getId(); @@ -123,13 +123,10 @@ function createEmailBody(name, topics) { */ function docToHtml(docId) { // Downloads a Google Doc as an HTML string. - const url = - "https://docs.google.com/feeds/download/documents/export/Export?id=" + - docId + - "&exportFormat=html"; + const url = `https://docs.google.com/feeds/download/documents/export/Export?id=${docId}&exportFormat=html`; const param = { method: "get", - headers: { Authorization: "Bearer " + ScriptApp.getOAuthToken() }, + headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, muteHttpExceptions: true, }; return UrlFetchApp.fetch(url, param).getContentText(); diff --git a/solutions/automations/course-feedback-response/Code.js b/solutions/automations/course-feedback-response/Code.js index 8902e2d24..3ab733d18 100644 --- a/solutions/automations/course-feedback-response/Code.js +++ b/solutions/automations/course-feedback-response/Code.js @@ -74,32 +74,7 @@ function createEmailBody(responses) { const otherFeedback = responses["Any other feedback?"][0]; // create email body - const 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 + - "

    "; + const 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; } @@ -115,7 +90,7 @@ function createDraft(timestamp, email, emailBody) { console.log("draft email create process started"); // create subject line - const subjectLine = "Thanks for your course feedback! " + timestamp; + const subjectLine = `Thanks for your course feedback! ${timestamp}`; // create draft email GmailApp.createDraft(email, subjectLine, "", { diff --git a/solutions/automations/employee-certificate/Code.js b/solutions/automations/employee-certificate/Code.js index c073ee2bc..d9a0a5ba1 100644 --- a/solutions/automations/employee-certificate/Code.js +++ b/solutions/automations/employee-certificate/Code.js @@ -72,12 +72,11 @@ function createCertificates() { empSlide.replaceAllText("Employee Name", empName); empSlide.replaceAllText( "Date", - "Date: " + - Utilities.formatDate( - date, - Session.getScriptTimeZone(), - "MMMM dd, yyyy", - ), + `Date: ${Utilities.formatDate( + date, + Session.getScriptTimeZone(), + "MMMM dd, yyyy", + )}`, ); empSlide.replaceAllText("Your Name", managerName); empSlide.replaceAllText("Title", title); @@ -124,12 +123,8 @@ function sendCertificates() { // Setup the required parameters and send them the email const senderName = "CertBot"; - const subject = empName + ", you're awesome!"; - const body = - "Please find your employee appreciation certificate attached." + - "\n\n" + - compName + - " team"; + const subject = `${empName}, you're awesome!`; + const body = `Please find your employee appreciation certificate attached.\n\n${compName} team`; GmailApp.sendEmail(empEmail, subject, body, { attachments: [attachment.getAs(MimeType.PDF)], name: senderName, diff --git a/solutions/automations/equipment-requests/Code.js b/solutions/automations/equipment-requests/Code.js index 9dd92e360..e1651f3c9 100644 --- a/solutions/automations/equipment-requests/Code.js +++ b/solutions/automations/equipment-requests/Code.js @@ -72,7 +72,7 @@ function setup_() { // Hide the raw form responses. ss.getSheets().forEach((sheet) => { - if (sheet.getFormUrl() == ss.getFormUrl()) { + if (sheet.getFormUrl() === ss.getFormUrl()) { sheet.hideSheet(); } }); @@ -109,9 +109,9 @@ function onFormSubmit_(event) { sendNewEquipmentRequestEmail_(response); const equipmentDetails = Utilities.formatString( "%s\n%s\n%s", - response["Laptop"], - response["Desktop"], - response["Monitor"], + response.Laptop, + response.Desktop, + response.Monitor, ); const row = [ "New", @@ -120,7 +120,7 @@ function onFormSubmit_(event) { response["Employee name"], response["Desk location"], equipmentDetails, - response["email"], + response.email, ]; const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName("Pending requests"); @@ -141,10 +141,10 @@ function processCompletedItems_() { for (let i = rows.length; i >= 2; i--) { const row = rows[i - 1]; const status = row[0]; - if (status === "Completed" || status == "Cancelled") { + if (status === "Completed" || status === "Cancelled") { pending.deleteRow(i); completed.appendRow(row); - console.log("Deleted row: " + i); + console.log(`Deleted row: ${i}`); sendEquipmentRequestCompletedEmail_({ "Employee name": row[3], "Desk location": row[4], diff --git a/solutions/automations/event-session-signup/Code.js b/solutions/automations/event-session-signup/Code.js index 24fc6c6f4..8b69ef15e 100644 --- a/solutions/automations/event-session-signup/Code.js +++ b/solutions/automations/event-session-signup/Code.js @@ -125,11 +125,11 @@ function setUpForm_(ss, values) { form.addTextItem().setTitle("Name").setRequired(true); form.addTextItem().setTitle("Email").setRequired(true); Object.keys(schedule).forEach((day) => { - const header = form.addSectionHeaderItem().setTitle("Sessions for " + day); + const header = form.addSectionHeaderItem().setTitle(`Sessions for ${day}`); Object.keys(schedule[day]).forEach((time) => { const item = form .addMultipleChoiceItem() - .setTitle(time + " " + day) + .setTitle(`${time} ${day}`) .setChoiceValues(schedule[day][time]); }); }); @@ -144,8 +144,8 @@ function setUpForm_(ss, values) { */ function onFormSubmit(e) { const user = { - name: e.namedValues["Name"][0], - email: e.namedValues["Email"][0], + 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. @@ -159,11 +159,11 @@ function onFormSubmit(e) { const title = session[0]; const day = session[1].toLocaleDateString(); const time = session[2].toLocaleTimeString(); - const timeslot = time + " " + day; + const 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) { + if (e.namedValues[timeslot] && e.namedValues[timeslot] === title) { response.push(session); } } @@ -191,7 +191,7 @@ function sendInvites_(user, response) { */ function sendDoc_(user, response) { const doc = DocumentApp.create( - "Conference Itinerary for " + user.name, + `Conference Itinerary for ${user.name}`, ).addEditor(user.email); const body = doc.getBody(); let table = [["Session", "Date", "Time", "Location"]]; @@ -214,7 +214,7 @@ function sendDoc_(user, response) { MailApp.sendEmail({ to: user.email, subject: doc.getName(), - body: "Thanks for registering! Here's your itinerary: " + doc.getUrl(), + body: `Thanks for registering! Here's your itinerary: ${doc.getUrl()}`, attachments: doc.getAs(MimeType.PDF), }); } diff --git a/solutions/automations/feedback-sentiment-analysis/code.js b/solutions/automations/feedback-sentiment-analysis/code.js index 27f9a0968..929c9cc96 100644 --- a/solutions/automations/feedback-sentiment-analysis/code.js +++ b/solutions/automations/feedback-sentiment-analysis/code.js @@ -76,11 +76,9 @@ function markEntitySentiment() { const textColumnIdx = headerRow.indexOf(COLUMN_NAME.COMMENTS); const entityColumnIdx = headerRow.indexOf(COLUMN_NAME.ENTITY); const idColumnIdx = headerRow.indexOf(COLUMN_NAME.ID); - if (entityColumnIdx == -1) { + 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.', + `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 } @@ -134,9 +132,7 @@ function markEntitySentiment() { */ function retrieveEntitySentiment(line) { const apiKey = myApiKey; - const apiEndpoint = - "https://language.googleapis.com/v1/documents:analyzeEntitySentiment?key=" + - apiKey; + const apiEndpoint = `https://language.googleapis.com/v1/documents:analyzeEntitySentiment?key=${apiKey}`; // Creates a JSON request, with text string, language, type and encoding const nlData = { document: { diff --git a/solutions/automations/generate-pdfs/Code.js b/solutions/automations/generate-pdfs/Code.js index 2f8e178b6..6e4f8395d 100644 --- a/solutions/automations/generate-pdfs/Code.js +++ b/solutions/automations/generate-pdfs/Code.js @@ -95,7 +95,7 @@ function createInvoiceForCustomer( ssId, ) { const customerTransactions = transactions.filter( - (transaction) => transaction.customer_name == customer.customer_name, + (transaction) => transaction.customer_name === customer.customer_name, ); // Clears existing data from the template. @@ -105,7 +105,7 @@ function createInvoiceForCustomer( let totalAmount = 0; customerTransactions.forEach((lineItem) => { const lineItemProduct = products.filter( - (product) => product.sku_name == lineItem.sku, + (product) => product.sku_name === lineItem.sku, )[0]; const qty = Number.parseInt(lineItem.licenses); const price = Number.parseFloat(lineItemProduct.price).toFixed(2); @@ -188,47 +188,19 @@ function clearTemplateSheet() { * @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 fr = 0; + const fc = 0; + const lc = 9; + const 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() }, + headers: { authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, }; const blob = UrlFetchApp.fetch(url, params) .getBlob() - .setName(pdfName + ".pdf"); + .setName(`${pdfName}.pdf`); // Gets the folder in Drive where the PDFs are stored. const folder = getFolderByName_(OUTPUT_FOLDER_NAME); @@ -253,7 +225,7 @@ function sendEmails() { const invoices = getObjects(invoicesData, createObjectKeys(keysI)); ss.toast("Emailing Invoices", APP_TITLE, 1); invoices.forEach((invoice, index) => { - if (invoice.email_sent != "Yes") { + 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,})/); @@ -315,5 +287,5 @@ function createObjectKeys(keys) { } // Returns true if the cell where cellData was read from is empty. function isCellEmpty(cellData) { - return typeof cellData == "string" && cellData == ""; + return typeof cellData === "string" && cellData === ""; } diff --git a/solutions/automations/import-csv-sheets/Code.js b/solutions/automations/import-csv-sheets/Code.js index 25d09a2a3..f59184bdd 100644 --- a/solutions/automations/import-csv-sheets/Code.js +++ b/solutions/automations/import-csv-sheets/Code.js @@ -42,8 +42,8 @@ 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) { + for (let i = 0; i < projectTriggers.length; i++) { + if (projectTriggers[i].getHandlerFunction() === HANDLER_FUNCTION) { console.log( `Existing trigger with Handler Function of '${HANDLER_FUNCTION}' removed.`, ); @@ -130,28 +130,17 @@ function updateApplicationSheet() { let processedList = ""; const processedCount = filesProcessed.length; for (const processed of filesProcessed) { - processedList += processed + "
    "; + processedList += `${processed}
    `; } const unProcessedCount = filesNotProcessed.length; let unProcessedList = ""; for (const unProcessed of filesNotProcessed) { - unProcessedList += unProcessed + "\n"; + 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}`; + 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, diff --git a/solutions/automations/import-csv-sheets/SampleData.js b/solutions/automations/import-csv-sheets/SampleData.js index 8a20d79e6..0b7709fb2 100644 --- a/solutions/automations/import-csv-sheets/SampleData.js +++ b/solutions/automations/import-csv-sheets/SampleData.js @@ -150,7 +150,7 @@ function getCSVFilesData() { // Gets headings once - same for all files/rows. let csvHeadings = ""; for (const i in SAMPLE_DATA.headings) - csvHeadings += SAMPLE_DATA.headings[i] + ","; + csvHeadings += `${SAMPLE_DATA.headings[i]},`; // Gets data for each file by rows. for (const i in SAMPLE_DATA.csvFiles) { @@ -160,7 +160,7 @@ function getCSVFilesData() { for (const j in SAMPLE_DATA.csvFiles[i].rows) { sampleCSV += "\n"; for (const k in SAMPLE_DATA.csvFiles[i].rows[j]) { - sampleCSV += SAMPLE_DATA.csvFiles[i].rows[j][k] + ","; + sampleCSV += `${SAMPLE_DATA.csvFiles[i].rows[j][k]},`; } } files.push({ name: fileName, csv: sampleCSV }); diff --git a/solutions/automations/import-csv-sheets/SetupSample.js b/solutions/automations/import-csv-sheets/SetupSample.js index b6e828761..ea2f5bf28 100644 --- a/solutions/automations/import-csv-sheets/SetupSample.js +++ b/solutions/automations/import-csv-sheets/SetupSample.js @@ -111,8 +111,8 @@ function removeSample() { // Removes existing trigger if found. const projectTriggers = ScriptApp.getProjectTriggers(); - for (var i = 0; i < projectTriggers.length; i++) { - if (projectTriggers[i].getHandlerFunction() == HANDLER_FUNCTION) { + for (let i = 0; i < projectTriggers.length; i++) { + if (projectTriggers[i].getHandlerFunction() === HANDLER_FUNCTION) { console.log( `Existing trigger with handler function of '${HANDLER_FUNCTION}' removed.`, ); diff --git a/solutions/automations/mail-merge/Code.js b/solutions/automations/mail-merge/Code.js index 1356e94a3..ffb6f5aca 100644 --- a/solutions/automations/mail-merge/Code.js +++ b/solutions/automations/mail-merge/Code.js @@ -51,7 +51,7 @@ function sendEmails(subjectLine, sheet = SpreadsheetApp.getActiveSheet()) { Browser.Buttons.OK_CANCEL, ); - if (subjectLine === "cancel" || subjectLine == "") { + if (subjectLine === "cancel" || subjectLine === "") { // If no subject line, finishes up return; } @@ -87,7 +87,7 @@ function sendEmails(subjectLine, sheet = SpreadsheetApp.getActiveSheet()) { // Loops through all the rows of data obj.forEach((row, rowIdx) => { // Only sends emails if email_sent cell is blank and not hidden by a filter - if (row[EMAIL_SENT_COL] == "") { + if (row[EMAIL_SENT_COL] === "") { try { const msgObj = fillInTemplateFromObject_(emailTemplate.message, row); @@ -136,12 +136,10 @@ function sendEmails(subjectLine, sheet = SpreadsheetApp.getActiveSheet()) { // 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 allInlineImages = draft.getMessage().getAttachments({ + includeInlineImages: true, + includeAttachments: false, + }); const attachments = draft .getMessage() .getAttachments({ includeInlineImages: false }); diff --git a/solutions/automations/news-sentiment/Code.js b/solutions/automations/news-sentiment/Code.js index 9f41b35f1..0c2e1f4a8 100644 --- a/solutions/automations/news-sentiment/Code.js +++ b/solutions/automations/news-sentiment/Code.js @@ -78,9 +78,9 @@ function showNewsPrompt() { // Processes the user's response. const button = result.getSelectedButton(); topic = result.getResponseText(); - if (button == ui.Button.OK) { + if (button === ui.Button.OK) { analyzeNewsHeadlines(); - } else if (button == ui.Button.CANCEL) { + } else if (button === ui.Button.CANCEL) { // Shows alert if user clicked "Cancel." ui.alert("News topic not selected!"); } @@ -105,7 +105,7 @@ function analyzeNewsHeadlines() { rowValues = rows.getValues(); getSentiments(); } else { - ss.toast("No headlines returned for topic: " + topic + "!"); + ss.toast(`No headlines returned for topic: ${topic}!`); } } @@ -116,17 +116,17 @@ function getHeadlinesArray() { // Fetches headlines for a given topic const hdlnsResp = []; const encodedtopic = encodeURIComponent(topic); - ss.toast("Getting headlines for: " + topic); + ss.toast(`Getting headlines for: ${topic}`); const response = UrlFetchApp.fetch( - apiEndPointHdr + encodedtopic + "&apiKey=" + newsApiKey, + `${apiEndPointHdr + encodedtopic}&apiKey=${newsApiKey}`, ); const results = JSON.parse(response); - const articles = results["articles"]; + const 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"]; + let newsStory = articles[i].title; + if (articles[i].description !== null) { + newsStory += `: ${articles[i].description}`; } // Scrubs newsStory of invalid characters newsStory = scrub(newsStory); @@ -153,7 +153,7 @@ function getSentiments() { const headlineCell = rowValues[i][headlineCol]; if (headlineCell) { const sentimentData = retrieveSentiment(headlineCell); - const result = sentimentData["documentSentiment"]["score"]; + const result = sentimentData.documentSentiment.score; avg += result; ds.getRange(i + 1, sentimentCol + 1).setBackgroundColor(getColor(result)); ds.getRange(i + 1, sentimentCol + 1).setValue(getFace(result)); @@ -167,7 +167,7 @@ function getSentiments() { ds.getRange(bottomRow, 1, headlines.length, scoreCol + 1).setFontWeight( "bold", ); - ds.getRange(bottomRow, headlineCol + 1).setValue('Topic: "' + topic + '"'); + 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( @@ -186,9 +186,7 @@ function getSentiments() { */ function retrieveSentiment(text) { // Sets REST call options - const apiEndPoint = - "https://language.googleapis.com/v1/documents:analyzeSentiment?key=" + - googleAPIKey; + const apiEndPoint = `https://language.googleapis.com/v1/documents:analyzeSentiment?key=${googleAPIKey}`; const jsonReq = JSON.stringify({ document: { type: "PLAIN_TEXT", @@ -230,9 +228,11 @@ function reformatSheet() { function getFace(value) { if (value >= threshold) { return happyFace; - } else if (value < threshold && value > -threshold) { + } + if (value < threshold && value > -threshold) { return mehFace; - } else if (value <= -threshold) { + } + if (value <= -threshold) { return sadFace; } } @@ -243,9 +243,11 @@ function getFace(value) { function getColor(value) { if (value >= threshold) { return happyColor; - } else if (value < threshold && value > -threshold) { + } + if (value < threshold && value > -threshold) { return mehColor; - } else if (value <= -threshold) { + } + if (value <= -threshold) { return sadColor; } } diff --git a/solutions/automations/offsite-activity-signup/Code.js b/solutions/automations/offsite-activity-signup/Code.js index 421d88640..5e24fb9fd 100644 --- a/solutions/automations/offsite-activity-signup/Code.js +++ b/solutions/automations/offsite-activity-signup/Code.js @@ -201,8 +201,8 @@ function writeAttendeeAssignments_(ss, attendees) { function writeActivityRosters_(ss, activities) { const sheet = findOrCreateSheetByName_(ss, "Activity rosters"); sheet.clear(); - var rows = []; - var rows = activities.map((activity) => { + const rows = []; + let rows = activities.map((activity) => { const roster = activity.roster.map((attendee) => attendee.email); return [activity.description].concat(roster); }); @@ -269,7 +269,7 @@ function loadActivitySchedule_(ss) { function loadAttendeeResponses_(ss, allActivityIds) { const sheet = findResponseSheetForForm_(ss); - if (!sheet || sheet.getLastRow() == 1) { + if (!sheet || sheet.getLastRow() === 1) { return undefined; } @@ -293,7 +293,7 @@ function loadAttendeeResponses_(ss, allActivityIds) { prefs[rank] = index; return prefs; }, []); - if (autoAssign == "Yes") { + 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. const additionalChoices = shuffleArray_(allActivityIds); @@ -399,16 +399,16 @@ function shuffleArray_(array) { function toOrdinal_(i) { const j = i % 10; const k = i % 100; - if (j == 1 && k != 11) { - return i + "st"; + if (j === 1 && k !== 11) { + return `${i}st`; } - if (j == 2 && k != 12) { - return i + "nd"; + if (j === 2 && k !== 12) { + return `${i}nd`; } - if (j == 3 && k != 13) { - return i + "rd"; + if (j === 3 && k !== 13) { + return `${i}rd`; } - return i + "th"; + return `${i}th`; } /** diff --git a/solutions/automations/tax-loss-harvest-alerts/Code.js b/solutions/automations/tax-loss-harvest-alerts/Code.js index 95ef2e1d1..5c8cd0f9d 100644 --- a/solutions/automations/tax-loss-harvest-alerts/Code.js +++ b/solutions/automations/tax-loss-harvest-alerts/Code.js @@ -38,7 +38,7 @@ function checkLosses() { let n = 0; for (const i in data) { //Skips the first row - if (n++ == 0) continue; + if (n++ === 0) continue; //Loads the current row const row = data[i]; @@ -47,15 +47,11 @@ function checkLosses() { console.log(row[6]); //Once at the end of the list, exits the loop - if (row[1] == "") break; + 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] + - ": " + - (Number.parseFloat(row[6].toString()) * 100).toFixed(2).toString() + - "%
    "; + message += `${row[1]}: ${(Number.parseFloat(row[6].toString()) * 100).toFixed(2).toString()}%
    `; send_message = true; } } diff --git a/solutions/automations/timesheets/Code.js b/solutions/automations/timesheets/Code.js index 32f40b069..5c67d2bb0 100644 --- a/solutions/automations/timesheets/Code.js +++ b/solutions/automations/timesheets/Code.js @@ -186,23 +186,22 @@ function checkApprovedStatusToNotify() { // Traverses through employee's row. for (let i = 0; i < numRows; i++) { // Do not notify twice. - if (notifiedValues[i][0] == "NOTIFIED") { + if (notifiedValues[i][0] === "NOTIFIED") { continue; } const emailAddress = emailValues[i][0]; const approvalValue = approvalValues[i][0]; // Sends notifying emails & update status. - if (approvalValue == "IN PROGRESS") { - continue; - } else if (approvalValue == "APPROVED") { + if (approvalValue === "IN PROGRESS") { + } else if (approvalValue === "APPROVED") { MailApp.sendEmail( emailAddress, APPROVED_EMAIL_SUBJECT, APPROVED_EMAIL_MESSAGE, ); updateNotifiedStatus(sheet, notifiedValues, i, beginningRow); - } else if (approvalValue == "NOT APPROVED") { + } else if (approvalValue === "NOT APPROVED") { MailApp.sendEmail( emailAddress, REJECTED_EMAIL_SUBJECT, diff --git a/solutions/automations/upload-files/Code.js b/solutions/automations/upload-files/Code.js index 11a455a28..7427453b2 100644 --- a/solutions/automations/upload-files/Code.js +++ b/solutions/automations/upload-files/Code.js @@ -37,13 +37,13 @@ const APP_SUBFOLDER_NONE = ""; function onFormSubmit(e) { try { // Gets the application root folder. - var destFolder = getFolder_(APP_FOLDER_NAME); + let destFolder = getFolder_(APP_FOLDER_NAME); // Gets all form responses. const itemResponses = e.response.getItemResponses(); // Determines the subfolder to route the file to, if any. - var subFolderName; + let subFolderName; const dest = itemResponses.filter( (itemResponse) => itemResponse.getItem().getTitle().toString() === APP_SUBFOLDER_ITEM, @@ -51,12 +51,12 @@ function onFormSubmit(e) { // Gets the destination subfolder name, but ignores if APP_SUBFOLDER_NONE was selected; if (dest.length > 0) { - if (dest[0].getResponse() != APP_SUBFOLDER_NONE) { + 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) { + if (subFolderName !== undefined) { destFolder = getSubFolder_(destFolder, subFolderName); } console.log(`Destination folder to use: diff --git a/solutions/automations/upload-files/Setup.js b/solutions/automations/upload-files/Setup.js index 01d96d239..25d3c3f77 100644 --- a/solutions/automations/upload-files/Setup.js +++ b/solutions/automations/upload-files/Setup.js @@ -32,7 +32,7 @@ function setUp() { ID: ${appFolder.getId()} URL: ${appFolder.getUrl()}`); } else { - console.log(`Could not setup application folder.`); + console.log("Could not setup application folder."); } // Calls the function that creates the Forms onSubmit trigger. installTrigger_(); diff --git a/solutions/automations/vacation-calendar/Code.js b/solutions/automations/vacation-calendar/Code.js index 8cf64e1f7..08e81b8b4 100644 --- a/solutions/automations/vacation-calendar/Code.js +++ b/solutions/automations/vacation-calendar/Code.js @@ -80,7 +80,7 @@ function sync() { }); // End foreach user. PropertiesService.getScriptProperties().setProperty("lastRun", today); - console.log("Imported " + count + " events"); + console.log(`Imported ${count} events`); } /** @@ -90,7 +90,7 @@ function sync() { * @param {Calendar.Event} event The event to import. */ function importEvent(username, event) { - event.summary = "[" + username + "] " + event.summary; + event.summary = `[${username}] ${event.summary}`; event.organizer = { id: TEAM_CALENDAR_ID, }; @@ -98,10 +98,10 @@ function importEvent(username, event) { // If the event is not of type 'default', it can't be imported, so it needs // to be changed. - if (event.eventType != "default") { + if (event.eventType !== "default") { event.eventType = "default"; - delete event.outOfOfficeProperties; - delete event.focusTimeProperties; + event.outOfOfficeProperties = undefined; + event.focusTimeProperties = undefined; } console.log("Importing: %s", event.summary); @@ -177,18 +177,18 @@ function formatDateAsRFC3339(date) { * @return {object} direct and indirect members. */ function getAllMembers(groupEmail) { - var group = GroupsApp.getGroupByEmail(groupEmail); - var users = group.getUsers(); - var childGroups = group.getGroups(); - for (var i = 0; i < childGroups.length; i++) { - var childGroup = childGroups[i]; + const group = GroupsApp.getGroupByEmail(groupEmail); + let users = group.getUsers(); + const childGroups = group.getGroups(); + for (let i = 0; i < childGroups.length; i++) { + const childGroup = childGroups[i]; users = users.concat(getAllMembers(childGroup.getEmail())); } // Remove duplicate members - var uniqueUsers = []; - var userEmails = {}; - for (var i = 0; i < users.length; i++) { - var user = users[i]; + const uniqueUsers = []; + const userEmails = {}; + for (let i = 0; i < users.length; i++) { + const user = users[i]; if (!userEmails[user.getEmail()]) { uniqueUsers.push(user); userEmails[user.getEmail()] = true; diff --git a/solutions/automations/youtube-tracker/Code.js b/solutions/automations/youtube-tracker/Code.js index ff0319a8c..484f4bcc9 100644 --- a/solutions/automations/youtube-tracker/Code.js +++ b/solutions/automations/youtube-tracker/Code.js @@ -93,7 +93,7 @@ function markVideos() { } } // Sends notification email if Content is not empty. - if (emailContent.length > 0 && EMAIL_ON == "Y") { + if (emailContent.length > 0 && EMAIL_ON === "Y") { sendEmailNotificationTemplate(emailContent, tabName); } }); @@ -116,7 +116,7 @@ function getVideoDetails(videoId) { function extractVideoIdFromUrl(url) { let videoId = url.split("v=")[1]; const ampersandPosition = videoId.indexOf("&"); - if (ampersandPosition != -1) { + if (ampersandPosition !== -1) { videoId = videoId.substring(0, ampersandPosition); } return videoId; diff --git a/solutions/custom-functions/calculate-driving-distance/Code.js b/solutions/custom-functions/calculate-driving-distance/Code.js index 83725642a..765ff45dd 100644 --- a/solutions/custom-functions/calculate-driving-distance/Code.js +++ b/solutions/custom-functions/calculate-driving-distance/Code.js @@ -35,7 +35,7 @@ function onOpen() { spreadsheet.addMenu("Directions", menuItems); } catch (e) { // TODO (Developer) - Handle Exception - console.log("Failed with error: %s" + e.error); + console.log(`Failed with error: %s${e.error}`); } } @@ -86,7 +86,7 @@ function prepareSheet_() { sheet.autoResizeColumns(1, 4); } catch (e) { // TODO (Developer) - Handle Exception - console.log("Failed with error: %s" + e.error); + console.log(`Failed with error: %s${e.error}`); } } @@ -114,7 +114,7 @@ function generateStepByStep_() { } const rowNumber = Number(selectedRow); if ( - isNaN(rowNumber) || + Number.isNaN(rowNumber) || rowNumber < 2 || rowNumber > settingsSheet.getLastRow() ) { @@ -144,7 +144,7 @@ function generateStepByStep_() { const directions = getDirections_(origin, destination); // Create a new sheet and append the steps in the directions. - const sheetName = "Driving Directions for Row " + rowNumber; + const sheetName = `Driving Directions for Row ${rowNumber}`; let directionsSheet = spreadsheet.getSheetByName(sheetName); if (directionsSheet) { directionsSheet.clear(); @@ -194,7 +194,7 @@ function generateStepByStep_() { SpreadsheetApp.flush(); } catch (e) { // TODO (Developer) - Handle Exception - console.log("Failed with error: %s" + e.error); + console.log(`Failed with error: %s${e.error}`); } } diff --git a/solutions/editor-add-on/clean-sheet/Code.js b/solutions/editor-add-on/clean-sheet/Code.js index 105a2b2a3..cb15ef1d2 100644 --- a/solutions/editor-add-on/clean-sheet/Code.js +++ b/solutions/editor-add-on/clean-sheet/Code.js @@ -233,7 +233,7 @@ function fillDownData() { // 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] == "") { + while (searchValues[i] && searchValues[i][0] === "") { i++; } @@ -254,7 +254,7 @@ function fillDownData() { */ function showMessage(message, caller) { // Sets the title using the APP_TITLE variable; adds optional caller string. - const title = APP_TITLE; + let title = APP_TITLE; if (caller != null) { title += ` : ${caller}`; } diff --git a/solutions/ooo-chat-app/Code.js b/solutions/ooo-chat-app/Code.js index 196f1de6e..450bacb79 100644 --- a/solutions/ooo-chat-app/Code.js +++ b/solutions/ooo-chat-app/Code.js @@ -20,7 +20,7 @@ limitations under the License. function onAddToSpace(event) { let message = 'Thank you for adding me to '; if (event.space.type === 'DM') { - message += 'a DM, ' + event.user.displayName + '!'; + message += `a DM, ${event.user.displayName}!`; } else { message += event.space.displayName; } diff --git a/solutions/schedule-meetings/Code.js b/solutions/schedule-meetings/Code.js index 9805fc02a..f3a606953 100644 --- a/solutions/schedule-meetings/Code.js +++ b/solutions/schedule-meetings/Code.js @@ -46,11 +46,7 @@ function onAddToSpace(event) { } // 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*."; + 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 }; } @@ -87,10 +83,9 @@ function onMessage(event) { getDialogForAddContact(message); */ } - } else { - // Returns text if users didn't invoke a slash command. - return { text: "No action taken - use Slash Commands." }; } + // Returns text if users didn't invoke a slash command. + return { text: "No action taken - use Slash Commands." }; } /** @@ -127,7 +122,7 @@ function onCardClick(event) { if (!startTime) { errors.push("Missing or invalid start time."); } - if (!duration || isNaN(duration)) { + if (!duration || Number.isNaN(duration)) { errors.push("Missing or invalid duration"); } if (errors.length) { @@ -151,14 +146,15 @@ function onCardClick(event) { const scheduledEvent = calendar.createEvent(subject, startTime, endTime, { guests: recipients, sendInvites: true, - description: body + "\nThis meeting scheduled by a Google Chat App!", + 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") { + } + if (event.action.actionMethodName === "closeDialog") { // Returns this dialog as success. return { actionResponse: { diff --git a/solutions/schedule-meetings/Dialog.js b/solutions/schedule-meetings/Dialog.js index 3a3143f93..9eba41372 100644 --- a/solutions/schedule-meetings/Dialog.js +++ b/solutions/schedule-meetings/Dialog.js @@ -40,7 +40,7 @@ function getForm_(options) { const sections = []; // If errors present, display additional section with validation messages. - if (options.errors && options.errors.length) { + if (options.errors?.length) { let errors = options.errors.reduce((str, err) => `${str}• ${err}
    `, ""); errors = `Errors:
    ${errors}`; const errorSection = { diff --git a/solutions/schedule-meetings/Utilities.js b/solutions/schedule-meetings/Utilities.js index c42627d83..88491b8be 100644 --- a/solutions/schedule-meetings/Utilities.js +++ b/solutions/schedule-meetings/Utilities.js @@ -70,7 +70,7 @@ 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(), + `${event.getId().split("@")[0]} ${cal.getId()}`, ).replace(/\=/g, ""); encodedId = `/event?eid=${encodedId}`; return baseCalUrl + encodedId; diff --git a/wasm/hello-world/build.js b/wasm/hello-world/build.js index a965ca7c8..45e2468e9 100644 --- a/wasm/hello-world/build.js +++ b/wasm/hello-world/build.js @@ -14,8 +14,8 @@ * limitations under the License. */ -import fs from "fs"; -import path from "path"; +import fs from "node:fs"; +import path from "node:path"; import esbuild from "esbuild"; import { wasmLoader } from "esbuild-plugin-wasm"; diff --git a/wasm/image-add-on/build.js b/wasm/image-add-on/build.js index fb86dac14..4b8026de6 100644 --- a/wasm/image-add-on/build.js +++ b/wasm/image-add-on/build.js @@ -14,8 +14,8 @@ * limitations under the License. */ -import fs from "fs"; -import path from "path"; +import fs from "node:fs"; +import path from "node:path"; import esbuild from "esbuild"; import { wasmLoader } from "esbuild-plugin-wasm"; diff --git a/wasm/python/build.js b/wasm/python/build.js index a965ca7c8..45e2468e9 100644 --- a/wasm/python/build.js +++ b/wasm/python/build.js @@ -14,8 +14,8 @@ * limitations under the License. */ -import fs from "fs"; -import path from "path"; +import fs from "node:fs"; +import path from "node:path"; import esbuild from "esbuild"; import { wasmLoader } from "esbuild-plugin-wasm"; From 520db0a114aa06212861bad904105df1a54521c6 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 17:08:06 -0700 Subject: [PATCH 04/12] style: manual fixes --- ai/autosummarize/gemini.js | 21 +++++++------- ai/autosummarize/main.js | 4 +-- ai/custom-func-ai-agent/Code.js | 15 +++++----- ai/custom-func-ai-studio/Code.js | 3 +- ai/custom_func_vertex/Code.js | 5 ++-- ai/drive-rename/ui.js | 4 +-- ai/standup-chat-app/db.js | 2 +- solutions/add-on/share-macro/UI.js | 7 +++-- .../aggregate-document-content/Code.js | 7 ++++- .../automations/calendar-timesheet/Code.js | 4 +-- .../automations/equipment-requests/Code.js | 8 ++--- .../automations/event-session-signup/Code.js | 22 +++++++------- solutions/automations/generate-pdfs/Code.js | 12 ++++---- .../automations/import-csv-sheets/Code.js | 7 ++--- solutions/automations/mail-merge/Code.js | 29 ++++++++++--------- solutions/automations/upload-files/Code.js | 2 +- .../automations/vacation-calendar/Code.js | 8 ++--- 17 files changed, 82 insertions(+), 78 deletions(-) diff --git a/ai/autosummarize/gemini.js b/ai/autosummarize/gemini.js index d3856f2be..b7c1e860b 100644 --- a/ai/autosummarize/gemini.js +++ b/ai/autosummarize/gemini.js @@ -37,14 +37,16 @@ const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault("service_account_key"); * * @param {string} prompt The prompt to senb to Vertex AI API. * @param {string} options.temperature The temperature setting set by user. - * @param {string} options.tokens The number of tokens to limit to the prompt. + * @param {string} options.maxOutputTokens The number of tokens to limit to the prompt. */ function getAiSummary(parts, options = {}) { - options = Object.assign( - {}, - { temperature: 0.1, tokens: 8192 }, - options ?? {}, - ); + const defaultOptions = { + temperature: 0.1, + maxOutputTokens: 8192, + topK: 1, + topP: 1, + stopSequences: [], + }; const request = { contents: [ { @@ -53,11 +55,8 @@ function getAiSummary(parts, options = {}) { }, ], generationConfig: { - temperature: options.temperature, - topK: 1, - topP: 1, - maxOutputTokens: options.tokens, - stopSequences: [], + ...defaultOptions, + ...options, }, }; diff --git a/ai/autosummarize/main.js b/ai/autosummarize/main.js index c61db3175..5b692feaf 100644 --- a/ai/autosummarize/main.js +++ b/ai/autosummarize/main.js @@ -59,13 +59,13 @@ function removeAllSummaries() { const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); const allSheets = spreadsheet.getSheets(); - allSheets.forEach((sheet) => { + for (const sheet of allSheets) { const sheetName = sheet.getName(); // Check if the sheet name starts with "AutoSummarize AI" if (sheetName.startsWith("AutoSummarize AI")) { spreadsheet.deleteSheet(sheet); } - }); + } } /** diff --git a/ai/custom-func-ai-agent/Code.js b/ai/custom-func-ai-agent/Code.js index 1dd44ffd6..a73571287 100644 --- a/ai/custom-func-ai-agent/Code.js +++ b/ai/custom-func-ai-agent/Code.js @@ -14,6 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +DEFAULT_OUTPUT_FORMAT = + "Summarize it. Only keep the verdict result and main arguments. " + + "Do not reiterate the fact being checked. Remove all markdown. " + + "State the verdit result in a first paragraph in a few words and " + + "the rest of the summary in a second paragraph."; + /** * Passes a statement to fact-check and, optionally, output formatting instructions. * @@ -25,14 +31,7 @@ limitations under the License. * @return The generated and formatted verdict * @customfunction */ -function FACT_CHECK(statement, outputFormat) { - if (!outputFormat || outputFormat === "") { - outputFormat = - "Summarize it. Only keep the verdict result and main arguments. " + - "Do not reiterate the fact being checked. Remove all markdown. " + - "State the verdit result in a first paragraph in a few words and the rest of the summary in a second paragraph."; - } - +function FACT_CHECK(statement, outputFormat = DEFAULT_OUTPUT_FORMAT) { return requestOutputFormatting( `Here is a fact checking result: ${requestLlmAuditorAdkAiAgent(statement)}.\n\n${outputFormat}`, ); diff --git a/ai/custom-func-ai-studio/Code.js b/ai/custom-func-ai-studio/Code.js index 0d85eb3c3..b9b605e39 100644 --- a/ai/custom-func-ai-studio/Code.js +++ b/ai/custom-func-ai-studio/Code.js @@ -23,6 +23,5 @@ limitations under the License. * @customfunction */ function gemini(range, prompt) { - prompt = `For the range of cells ${range}, ${prompt}`; - return getAiSummary(prompt); + return getAiSummary(`For the range of cells ${range}, ${prompt}`); } diff --git a/ai/custom_func_vertex/Code.js b/ai/custom_func_vertex/Code.js index 4207ecf53..7e9071cfb 100644 --- a/ai/custom_func_vertex/Code.js +++ b/ai/custom_func_vertex/Code.js @@ -7,6 +7,7 @@ * @customfunction */ function gemini(range, prompt) { - prompt = `For the table of data: ${range}, Answer the following: ${prompt}. Do not use formatting. Remove all markdown.`; - return getAiSummary(prompt); + return getAiSummary( + `For the table of data: ${range} Answer the following: ${prompt}. Do not use formatting. Remove all markdown.`, + ); } diff --git a/ai/drive-rename/ui.js b/ai/drive-rename/ui.js index b1c57e94c..d71395e9c 100644 --- a/ai/drive-rename/ui.js +++ b/ai/drive-rename/ui.js @@ -84,13 +84,13 @@ function buildSelectionPage(e) { // Create an object of items const items = []; - aiResponse.names.forEach((name) => { + for (const name of aiResponse.names) { items.push({ text: name, value: name, selected: false, }); - }); + } // Set first item as selected items[0].selected = true; diff --git a/ai/standup-chat-app/db.js b/ai/standup-chat-app/db.js index cd486ebdd..1a821f6c6 100644 --- a/ai/standup-chat-app/db.js +++ b/ai/standup-chat-app/db.js @@ -64,7 +64,7 @@ class DB { */ get last() { const lastRow = this.sheet.getLastRow(); - if (lastRow === 0) return; + if (lastRow === 0) return undefined; return JSON.parse(this.sheet.getSheetValues(lastRow, 1, 1, 2)[0][1]); } diff --git a/solutions/add-on/share-macro/UI.js b/solutions/add-on/share-macro/UI.js index a1f5a2871..297e1ad3a 100644 --- a/solutions/add-on/share-macro/UI.js +++ b/solutions/add-on/share-macro/UI.js @@ -257,10 +257,11 @@ function onClickFunction_(e) { .setOnClickAction(CardService.newAction().setFunctionName("onHomepage")), ); - return (builder = CardService.newCardBuilder() + const builder = CardService.newCardBuilder() .setHeader(cardHeader) .addSection(sectionBody1) .addSection(sectionBody2) - .setFixedFooter(cardFooter) - .build()); + .setFixedFooter(cardFooter); + + return builder.build(); } diff --git a/solutions/automations/aggregate-document-content/Code.js b/solutions/automations/aggregate-document-content/Code.js index 227e35f40..f5b45c15e 100644 --- a/solutions/automations/aggregate-document-content/Code.js +++ b/solutions/automations/aggregate-document-content/Code.js @@ -129,7 +129,12 @@ function getContent(body) { let searchResult = null; // Gets and loops through all paragraphs that match the style of APP_STYLE. - while ((searchResult = body.findElement(searchType, searchResult))) { + while (true) { + searchResult = body.findElement(searchType, searchResult); + if (!searchResult) { + break; + } + const par = searchResult.getElement().asParagraph(); if (par.getHeading() === searchHeading) { // If heading style matches, searches for text string (case insensitive). diff --git a/solutions/automations/calendar-timesheet/Code.js b/solutions/automations/calendar-timesheet/Code.js index 1d4fe96d0..9ef436d27 100644 --- a/solutions/automations/calendar-timesheet/Code.js +++ b/solutions/automations/calendar-timesheet/Code.js @@ -71,7 +71,7 @@ const getSettings = () => { // find any calendars that were removed const unavailebleCalendars = []; - savedCalendarSettings.forEach((savedCalendarSetting) => { + for (const savedCalendarSetting of savedCalendarSettings) { if ( !availableCalendars.find( (availableCalendar) => availableCalendar.id === savedCalendarSetting.id, @@ -79,7 +79,7 @@ const getSettings = () => { ) { unavailebleCalendars.push(savedCalendarSetting); } - }); + } // map the current settings to the available calendars const calendarSettings = availableCalendars.map((availableCalendar) => { diff --git a/solutions/automations/equipment-requests/Code.js b/solutions/automations/equipment-requests/Code.js index e1651f3c9..5d8cb0fd5 100644 --- a/solutions/automations/equipment-requests/Code.js +++ b/solutions/automations/equipment-requests/Code.js @@ -71,11 +71,11 @@ function setup_() { form.addListItem().setTitle("Monitor").setChoiceValues(AVAILABLE_MONITORS); // Hide the raw form responses. - ss.getSheets().forEach((sheet) => { + for (const sheet of ss.getSheets()) { 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. @@ -93,9 +93,9 @@ function cleanup_() { if (!formUrl) { return; } - ScriptApp.getProjectTriggers().forEach((trigger) => { + for (const trigger of ScriptApp.getProjectTriggers()) { ScriptApp.deleteTrigger(trigger); - }); + } FormApp.openByUrl(formUrl).deleteAllResponses().setAcceptingResponses(false); } diff --git a/solutions/automations/event-session-signup/Code.js b/solutions/automations/event-session-signup/Code.js index 8b69ef15e..e03d99e63 100644 --- a/solutions/automations/event-session-signup/Code.js +++ b/solutions/automations/event-session-signup/Code.js @@ -87,11 +87,11 @@ function setUpCalendar_(values, range) { * @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; +function joinDateAndTime_(date_, time) { + const processedDate = new Date(date_); + processedDate.setHours(time.getHours()); + processedDate.setMinutes(time.getMinutes()); + return processedDate; } /** @@ -124,15 +124,15 @@ function setUpForm_(ss, values) { form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()); form.addTextItem().setTitle("Name").setRequired(true); form.addTextItem().setTitle("Email").setRequired(true); - Object.keys(schedule).forEach((day) => { - const header = form.addSectionHeaderItem().setTitle(`Sessions for ${day}`); - Object.keys(schedule[day]).forEach((time) => { - const item = form + for (const day of Object.keys(schedule)) { + form.addSectionHeaderItem().setTitle(`Sessions for ${day}`); + for (const time of Object.keys(schedule[day])) { + form .addMultipleChoiceItem() .setTitle(`${time} ${day}`) .setChoiceValues(schedule[day][time]); - }); - }); + } + } } /** diff --git a/solutions/automations/generate-pdfs/Code.js b/solutions/automations/generate-pdfs/Code.js index 6e4f8395d..1944f0447 100644 --- a/solutions/automations/generate-pdfs/Code.js +++ b/solutions/automations/generate-pdfs/Code.js @@ -60,7 +60,7 @@ function processDocuments() { const invoices = []; // Iterates for each customer calling createInvoiceForCustomer routine. - customers.forEach((customer) => { + for (const customer of customers) { ss.toast(`Creating Invoice for ${customer.customer_name}`, APP_TITLE, 1); const invoice = createInvoiceForCustomer( customer, @@ -70,7 +70,7 @@ function processDocuments() { ss.getId(), ); invoices.push(invoice); - }); + } // Writes invoices data to the sheet. invoicesSheet .getRange(2, 1, invoices.length, invoices[0].length) @@ -103,7 +103,7 @@ function createInvoiceForCustomer( const lineItems = []; let totalAmount = 0; - customerTransactions.forEach((lineItem) => { + for (const lineItem of customerTransactions) { const lineItemProduct = products.filter( (product) => product.sku_name === lineItem.sku, )[0]; @@ -119,7 +119,7 @@ function createInvoiceForCustomer( amount, ]); totalAmount += Number.parseFloat(amount); - }); + } // Generates a random invoice number. You can replace with your own document ID method. const invoiceNumber = Math.floor(100000 + Math.random() * 900000); @@ -173,9 +173,9 @@ function clearTemplateSheet() { const rngClear = templateSheet .getRangeList(["B10:B11", "F10", "F12", "F14"]) .getRanges(); - rngClear.forEach((cell) => { + for (const cell of rngClear) { 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(); } diff --git a/solutions/automations/import-csv-sheets/Code.js b/solutions/automations/import-csv-sheets/Code.js index f59184bdd..0b8f94255 100644 --- a/solutions/automations/import-csv-sheets/Code.js +++ b/solutions/automations/import-csv-sheets/Code.js @@ -89,10 +89,7 @@ function updateApplicationSheet() { // Iterates through each CSV file. while (cvsFiles.hasNext()) { const csvFile = cvsFiles.next(); - let isSuccess; - - // Appends the unprocessed CSV data into the Google Sheets spreadsheet. - isSuccess = processCsv_(objSpreadSheet, csvFile); + const isSuccess = processCsv_(objSpreadSheet, csvFile); if (isSuccess) { // Moves the processed file to the processed folder to prevent future duplicate data imports. @@ -100,7 +97,7 @@ function updateApplicationSheet() { // Logs the successfully processed file to the filesProcessed array. filesProcessed.push(csvFile.getName()); console.log(`Successfully processed: ${csvFile.getName()}`); - } else if (!isSuccess) { + } else { // 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()); diff --git a/solutions/automations/mail-merge/Code.js b/solutions/automations/mail-merge/Code.js index ffb6f5aca..5b72b2d1d 100644 --- a/solutions/automations/mail-merge/Code.js +++ b/solutions/automations/mail-merge/Code.js @@ -43,22 +43,23 @@ function onOpen() { */ 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( + let processedSubjectLine = subjectLine; + if (!processedSubjectLine) { + processedSubjectLine = 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 (processedSubjectLine === "cancel" || processedSubjectLine === "") { // If no subject line, finishes up return; } } // Gets the draft Gmail message to use as a template - const emailTemplate = getGmailTemplateFromDrafts_(subjectLine); + const emailTemplate = getGmailTemplateFromDrafts_(processedSubjectLine); // Gets the data from the passed sheet const dataRange = sheet.getDataRange(); @@ -78,7 +79,10 @@ function sendEmails(subjectLine, sheet = SpreadsheetApp.getActiveSheet()) { // 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), {}), + heads.reduce((o, k, i) => { + o[k] = r[i] || ""; + return o; + }, {}), ); // Creates an array to record sent emails @@ -147,10 +151,10 @@ function sendEmails(subjectLine, sheet = SpreadsheetApp.getActiveSheet()) { // 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), - {}, - ); + const img_obj = allInlineImages.reduce((obj, i) => { + obj[i.getName()] = i; + return obj; + }, {}); //Regexp searches for all img string positions with cid const imgexp = /]+>/g; @@ -158,10 +162,9 @@ function sendEmails(subjectLine, sheet = SpreadsheetApp.getActiveSheet()) { //Initiates the allInlineImages object const inlineImagesObj = {}; - // built an inlineImagesObj from inline image matches - matches.forEach( - (match) => (inlineImagesObj[match[1]] = img_obj[match[2]]), - ); + for (const match of matches) { + inlineImagesObj[match[1]] = img_obj[match[2]]; + } return { message: { diff --git a/solutions/automations/upload-files/Code.js b/solutions/automations/upload-files/Code.js index 7427453b2..3d277a15a 100644 --- a/solutions/automations/upload-files/Code.js +++ b/solutions/automations/upload-files/Code.js @@ -71,7 +71,7 @@ function onFormSubmit(e) { itemResponse.getItem().getType().toString() === "FILE_UPLOAD", ) .map((itemResponse) => itemResponse.getResponse()) - .reduce((a, b) => [...a, ...b], []); + .reduce((a, b) => a.concat(b), []); // Moves the files to the destination folder. if (fileUploads.length > 0) { diff --git a/solutions/automations/vacation-calendar/Code.js b/solutions/automations/vacation-calendar/Code.js index 08e81b8b4..3fedc8143 100644 --- a/solutions/automations/vacation-calendar/Code.js +++ b/solutions/automations/vacation-calendar/Code.js @@ -70,14 +70,14 @@ function sync() { // summary in the specified date range. Imports each of those to the team // calendar. let count = 0; - users.forEach((user) => { + for (const user of users) { const username = user.getEmail().split("@")[0]; const events = findEvents(user, today, maxDate, lastRun); - events.forEach((event) => { + for (const event of events) { importEvent(username, event); count++; - }); // End foreach event. - }); // End foreach user. + } + } PropertiesService.getScriptProperties().setProperty("lastRun", today); console.log(`Imported ${count} events`); From 8b38ffaf924034dde90146ad11cf3f10cdad6de0 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 17:09:05 -0700 Subject: [PATCH 05/12] build: update gemini context --- GEMINI.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/GEMINI.md b/GEMINI.md index 0fa2ed216..4a0c0e162 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -7,6 +7,16 @@ This guide outlines best practices for developing Google Apps Script projects, f * For new sample directories, ensure the top-level folder is included in the [`test.yaml`](.github/workflows/test.yaml) GitHub workflow's matrix configuration. * Do not move or delete snippet tags: `[END apps_script_... ]` or `[END apps_script_... ]`. + +## Tools + +Lint and format code using [Biome](https://biomejs.dev/). + +```bash +pnpm lint +pnpm format +``` + ## Apps Script Code Best Practices Apps Script supports the V8 runtime, which enables modern ECMAScript syntax. Using these features makes your code cleaner, more readable, and less error-prone. From 5a7067db3b3ea4d68af809e4954af22d634d2ed7 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 18:48:13 -0700 Subject: [PATCH 06/12] style: more manual lint error fixes --- .github/scripts/check-gs.ts | 4 +- .../offsite-activity-signup/Code.js | 20 +- solutions/automations/upload-files/Code.js | 4 +- solutions/automations/youtube-tracker/Code.js | 4 +- solutions/editor-add-on/clean-sheet/Code.js | 12 +- solutions/ooo-chat-app/Code.js | 325 +++++++++--------- 6 files changed, 185 insertions(+), 184 deletions(-) diff --git a/.github/scripts/check-gs.ts b/.github/scripts/check-gs.ts index 6712405ed..418c81a8c 100644 --- a/.github/scripts/check-gs.ts +++ b/.github/scripts/check-gs.ts @@ -167,9 +167,9 @@ async function checkProject( ); try { - await execAsync(`tsc -p "${projectTempDir}"`, { cwd: rootDir }); + await execAsync(`tsc -p \"${projectTempDir}\"`, { cwd: rootDir }); return { name: project.name, success: true, output: "" }; - } catch (e: any) { + } catch (e: { stdout: string; stderr: string }) { const rawOutput = (e.stdout || "") + (e.stderr || ""); const rewritten = rawOutput diff --git a/solutions/automations/offsite-activity-signup/Code.js b/solutions/automations/offsite-activity-signup/Code.js index 5e24fb9fd..be3ac5d30 100644 --- a/solutions/automations/offsite-activity-signup/Code.js +++ b/solutions/automations/offsite-activity-signup/Code.js @@ -116,9 +116,9 @@ function assignWithRandomPriority_( }, {}); for (let i = 0; i < numActivitiesPerPerson; ++i) { const randomizedAttendees = shuffleArray_(attendees); - randomizedAttendees.forEach((attendee) => { + for (const attendee of randomizedAttendees) { makeChoice_(attendee, activitiesById); - }); + } } } @@ -201,7 +201,6 @@ function writeAttendeeAssignments_(ss, attendees) { function writeActivityRosters_(ss, activities) { const sheet = findOrCreateSheetByName_(ss, "Activity rosters"); sheet.clear(); - const rows = []; let rows = activities.map((activity) => { const roster = activity.roster.map((attendee) => attendee.email); return [activity.description].concat(roster); @@ -330,9 +329,9 @@ function generateTestData_() { const activities = loadActivitySchedule_(ss); const choices = fillArray_([], activities.length, ""); - range_(1, 5).forEach((value) => { + for (const value of range_(1, 5)) { choices[value] = toOrdinal_(value); - }); + } const rows = range_(1, NUM_TEST_USERS).map((value) => { const randomizedChoices = shuffleArray_(choices); @@ -457,7 +456,8 @@ function range_(start, end) { const arr = [start]; let i = start; while (i < end) { - arr.push((i += 1)); + i += 1; + arr.push(i); } return arr; } @@ -473,12 +473,12 @@ function range_(start, end) { */ function transpose_(arr, fillValue) { const transposed = []; - arr.forEach((row, rowIndex) => { - row.forEach((col, colIndex) => { + for (const [rowIndex, row] of arr.entries()) { + for (const [colIndex, col] of row.entries()) { transposed[colIndex] = transposed[colIndex] || fillArray_([], arr.length, fillValue); transposed[colIndex][rowIndex] = row[colIndex]; - }); - }); + } + } return transposed; } diff --git a/solutions/automations/upload-files/Code.js b/solutions/automations/upload-files/Code.js index 3d277a15a..2198d7f56 100644 --- a/solutions/automations/upload-files/Code.js +++ b/solutions/automations/upload-files/Code.js @@ -75,10 +75,10 @@ function onFormSubmit(e) { // Moves the files to the destination folder. if (fileUploads.length > 0) { - fileUploads.forEach((fileId) => { + for (const fileId of fileUploads) { DriveApp.getFileById(fileId).moveTo(destFolder); console.log(`File Copied: ${fileId}`); - }); + } } } catch (err) { console.log(err); diff --git a/solutions/automations/youtube-tracker/Code.js b/solutions/automations/youtube-tracker/Code.js index 484f4bcc9..08f6af555 100644 --- a/solutions/automations/youtube-tracker/Code.js +++ b/solutions/automations/youtube-tracker/Code.js @@ -37,7 +37,7 @@ function markVideos() { const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets(); // Runs through process for each tab in Spreadsheet. - sheets.forEach((dataSheet) => { + for (const dataSheet of sheets) { const tabName = dataSheet.getName(); const range = dataSheet.getDataRange(); const numRows = range.getNumRows(); @@ -96,7 +96,7 @@ function markVideos() { if (emailContent.length > 0 && EMAIL_ON === "Y") { sendEmailNotificationTemplate(emailContent, tabName); } - }); + } } /** diff --git a/solutions/editor-add-on/clean-sheet/Code.js b/solutions/editor-add-on/clean-sheet/Code.js index cb15ef1d2..7753c8a47 100644 --- a/solutions/editor-add-on/clean-sheet/Code.js +++ b/solutions/editor-add-on/clean-sheet/Code.js @@ -84,9 +84,9 @@ function deleteEmptyRows() { 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)); + for (const [start, end] of rangesToDelete.reverse()) { + sheet.deleteRows(start, end - start + 1); + } SpreadsheetApp.flush(); } @@ -159,9 +159,9 @@ function deleteEmptyColumns() { 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)); + for (const [start, end] of rangesToDelete.reverse()) { + sheet.deleteColumns(start, end - start + 1); + } SpreadsheetApp.flush(); } diff --git a/solutions/ooo-chat-app/Code.js b/solutions/ooo-chat-app/Code.js index 450bacb79..5ca78c4ff 100644 --- a/solutions/ooo-chat-app/Code.js +++ b/solutions/ooo-chat-app/Code.js @@ -18,13 +18,13 @@ limitations under the License. * @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 }; + 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 }; } /** @@ -34,87 +34,86 @@ function onAddToSpace(event) { * @see https://developers.google.com/hangouts/chat/reference/message-formats/events */ function onRemoveFromSpace(event) { - console.log('App removed from ', event.space.name); + 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(); - } - } + 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 - } - } - ] - }, - { - "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 - } - } - ] - } - ], - "header": { - "title": "OOO app", - "subtitle": "Helping you manage your OOO", - "imageUrl": "https://goo.gle/3SfMkjb", - "imageType": "SQUARE" - } - } - } - ] - } + 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, + }, + }, + { + 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, + }, + }, + ], + }, + ], + header: { + title: "OOO app", + subtitle: "Helping you manage your OOO", + imageUrl: "https://goo.gle/3SfMkjb", + imageType: "SQUARE", + }, + }, + }, + ], + }; } /** @@ -122,8 +121,8 @@ function createHelpCard() { * @return {object} JSON-formatted response */ function blockDayOut() { - blockOutCalendar(); - return createResponseCard('Your calendar has been blocked out for you.') + blockOutCalendar(); + return createResponseCard("Your calendar has been blocked out for you."); } /** @@ -131,8 +130,8 @@ function blockDayOut() { * @return {object} JSON-formatted response */ function cancelAllMeetings() { - cancelMeetings(); - return createResponseCard('All your meetings have been canceled.') + cancelMeetings(); + return createResponseCard("All your meetings have been canceled."); } /** @@ -140,107 +139,109 @@ function cancelAllMeetings() { * @return {object} JSON-formatted response */ function setAutoReply() { - turnOnAutoResponder(); - return createResponseCard('The out of office auto reply has been turned on.') + 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() { - /** - * 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 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.', - } - } - Calendar.Events.insert(event, 'primary'); + /** + * 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 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, 0) }, + end: { dateTime: getDateAndHours(17, 0) }, + eventType: "outOfOffice", + summary: "Out of office", + outOfOfficeProperties: { + autoDeclineMode: "declineOnlyNewConflictingInvitations", + declineMessage: "Declined because I am taking a day of.", + }, + }; + Calendar.Events.insert(event, "primary"); } /** * Declines all meetings for the day. */ function cancelMeetings() { - const events = CalendarApp.getEventsForDay(new Date()); + const events = CalendarApp.getEventsForDay(new Date()); - events.forEach((event) => { - if (event.getGuestList().length > 0) { - event.setMyStatus(CalendarApp.GuestStatus.NO); - } - }); + for (const event of events) { + 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'); + 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" - } - } - } - ] - } + 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", + }, + }, + }, + ], + }; } - From 0dc0d7a6bfdd7dead2883692784880d29d0fd0cc Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 18:51:30 -0700 Subject: [PATCH 07/12] fix: ts unknown --- .github/scripts/check-gs.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/scripts/check-gs.ts b/.github/scripts/check-gs.ts index 418c81a8c..6e10292e8 100644 --- a/.github/scripts/check-gs.ts +++ b/.github/scripts/check-gs.ts @@ -169,8 +169,9 @@ async function checkProject( try { await execAsync(`tsc -p \"${projectTempDir}\"`, { cwd: rootDir }); return { name: project.name, success: true, output: "" }; - } catch (e: { stdout: string; stderr: string }) { - const rawOutput = (e.stdout || "") + (e.stderr || ""); + } catch (e) { + const err = e as { stdout?: string; stderr?: string }; + const rawOutput = (err.stdout ?? "") + (err.stderr || ""); const rewritten = rawOutput .split("\n") From 347529f5cbfdf8e82092de98d88690abd5b36d36 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 18:56:06 -0700 Subject: [PATCH 08/12] style: 2 space indentation --- .gemini/settings.json | 30 +- .github/linters/.htmlhintrc | 46 +- .github/scripts/check-gs.ts | 406 ++++----- .vscode/extensions.json | 2 +- .vscode/settings.json | 6 +- ai/autosummarize/appsscript.json | 40 +- ai/autosummarize/gemini.js | 134 +-- ai/autosummarize/main.js | 210 ++--- ai/autosummarize/summarize.js | 258 +++--- ai/custom-func-ai-agent/AiVertex.js | 144 ++-- ai/custom-func-ai-agent/Code.js | 14 +- ai/custom-func-ai-agent/appsscript.json | 24 +- ai/custom-func-ai-studio/Code.js | 2 +- ai/custom-func-ai-studio/appsscript.json | 8 +- ai/custom-func-ai-studio/gemini.js | 98 +-- ai/custom_func_vertex/Code.js | 6 +- ai/custom_func_vertex/aiVertex.js | 138 ++-- ai/custom_func_vertex/appsscript.json | 24 +- ai/devdocs-link-preview/Cards.js | 150 ++-- ai/devdocs-link-preview/Helpers.js | 12 +- ai/devdocs-link-preview/Main.js | 66 +- ai/devdocs-link-preview/Vertex.js | 146 ++-- ai/devdocs-link-preview/appsscript.json | 136 +-- ai/drive-rename/ai.js | 120 +-- ai/drive-rename/appsscript.json | 84 +- ai/drive-rename/drive.js | 124 +-- ai/drive-rename/main.js | 56 +- ai/drive-rename/ui.js | 406 ++++----- ai/email-classifier/appsscript.json | 72 +- ai/gmail-sentiment-analysis/appsscript.json | 50 +- ai/standup-chat-app/appsscript.json | 70 +- ai/standup-chat-app/db.js | 84 +- ai/standup-chat-app/gemini.js | 32 +- ai/standup-chat-app/main.js | 138 ++-- ai/standup-chat-app/memoize.js | 28 +- apps-script/execute/target.js | 16 +- biome.json | 39 +- chat/advanced-service/appsscript.json | 62 +- chat/quickstart/appsscript.json | 28 +- data-studio/appsscript.json | 46 +- data-studio/appsscript2.json | 20 +- .../AppsScriptFormsAPIWebApp/appsscript.json | 34 +- gmail-sentiment-analysis/.clasp.json | 2 +- gmail-sentiment-analysis/appsscript.json | 46 +- gmail/add-ons/appsscript.json | 36 +- package.json | 62 +- picker/appsscript.json | 32 +- sheets/next18/appsscript.json | 38 +- slides/SpeakerNotesScript/appscript.json | 12 +- solutions/add-on/book-smartchip/Code.js | 96 +-- .../add-on/book-smartchip/appsscript.json | 78 +- solutions/add-on/share-macro/Code.js | 246 +++--- solutions/add-on/share-macro/UI.js | 368 ++++----- solutions/add-on/share-macro/appsscript.json | 48 +- .../attendance-chat-app/final/appsscript.json | 22 +- .../step-3/appsscript.json | 6 +- .../step-4/appsscript.json | 6 +- .../step-5/appsscript.json | 6 +- .../step-6/appsscript.json | 22 +- solutions/automations/agenda-maker/Code.js | 298 +++---- .../automations/agenda-maker/appsscript.json | 8 +- .../aggregate-document-content/Code.js | 240 +++--- .../aggregate-document-content/Menu.js | 46 +- .../aggregate-document-content/Setup.js | 228 ++--- .../aggregate-document-content/Utilities.js | 50 +- .../appsscript.json | 8 +- solutions/automations/bracket-maker/Code.js | 184 ++--- .../automations/bracket-maker/appsscript.json | 8 +- .../automations/calendar-timesheet/Code.js | 776 +++++++++--------- .../calendar-timesheet/appsscript.json | 8 +- solutions/automations/content-signup/Code.js | 144 ++-- .../content-signup/appsscript.json | 24 +- .../course-feedback-response/Code.js | 74 +- .../course-feedback-response/appsscript.json | 8 +- .../automations/employee-certificate/Code.js | 190 ++--- .../employee-certificate/appsscript.json | 8 +- .../automations/equipment-requests/Code.js | 234 +++--- .../equipment-requests/appsscript.json | 8 +- .../automations/event-session-signup/Code.js | 268 +++--- .../event-session-signup/appsscript.json | 8 +- .../appsscript.json | 24 +- .../feedback-sentiment-analysis/code.js | 204 ++--- solutions/automations/folder-creation/Code.js | 20 +- .../folder-creation/appscript.json | 24 +- solutions/automations/generate-pdfs/Code.js | 330 ++++---- solutions/automations/generate-pdfs/Menu.js | 14 +- .../automations/generate-pdfs/Utilities.js | 50 +- .../automations/generate-pdfs/appsscript.json | 8 +- .../automations/import-csv-sheets/Code.js | 236 +++--- .../import-csv-sheets/SampleData.js | 274 +++---- .../import-csv-sheets/SetupSample.js | 126 +-- .../import-csv-sheets/Utilities.js | 146 ++-- .../import-csv-sheets/appsscript.json | 8 +- solutions/automations/mail-merge/Code.js | 378 ++++----- .../automations/mail-merge/appsscript.json | 8 +- solutions/automations/news-sentiment/Code.js | 264 +++--- .../news-sentiment/appsscript.json | 8 +- .../offsite-activity-signup/Code.js | 552 ++++++------- .../offsite-activity-signup/appsscript.json | 8 +- .../tax-loss-harvest-alerts/Code.js | 64 +- .../tax-loss-harvest-alerts/appsscript.json | 8 +- solutions/automations/timesheets/Code.js | 312 +++---- .../automations/timesheets/appsscript.json | 8 +- solutions/automations/upload-files/Code.js | 120 +-- solutions/automations/upload-files/Setup.js | 124 +-- .../automations/upload-files/appsscript.json | 8 +- .../automations/vacation-calendar/Code.js | 246 +++--- .../vacation-calendar/appsscript.json | 8 +- solutions/automations/youtube-tracker/Code.js | 168 ++-- .../youtube-tracker/appsscript.json | 8 +- .../calculate-driving-distance/Code.js | 318 +++---- .../appsscript.json | 8 +- .../summarize-sheets-data/Code.js | 122 +-- .../summarize-sheets-data/appsscript.json | 8 +- .../custom-functions/tier-pricing/Code.js | 26 +- .../tier-pricing/appsscript.json | 8 +- solutions/editor-add-on/clean-sheet/Code.js | 376 ++++----- solutions/editor-add-on/clean-sheet/Menu.js | 36 +- .../editor-add-on/clean-sheet/appsscript.json | 8 +- solutions/ooo-assistant/appsscript.json | 86 +- solutions/ooo-chat-app/Code.js | 322 ++++---- solutions/ooo-chat-app/appsscript.json | 36 +- solutions/schedule-meetings/Code.js | 224 ++--- solutions/schedule-meetings/Dialog.js | 342 ++++---- solutions/schedule-meetings/Utilities.js | 50 +- solutions/schedule-meetings/appsscript.json | 12 +- tasks/simpleTasks/appsscript.json | 22 +- tsconfig.json | 26 +- wasm/hello-world/.clasp.json | 4 +- wasm/hello-world/build.js | 26 +- wasm/hello-world/package.json | 112 +-- wasm/hello-world/polyfill.js | 4 +- wasm/hello-world/src/appsscript.json | 8 +- wasm/hello-world/src/main.js | 4 +- wasm/hello-world/src/test.js | 98 +-- wasm/hello-world/src/wasm.js | 8 +- wasm/image-add-on/.clasp.json | 4 +- wasm/image-add-on/build.js | 26 +- wasm/image-add-on/package.json | 114 +-- wasm/image-add-on/polyfill.js | 4 +- wasm/image-add-on/src/add-on.js | 362 ++++---- wasm/image-add-on/src/appsscript.json | 66 +- wasm/image-add-on/src/main.js | 22 +- wasm/image-add-on/src/test.js | 96 +-- wasm/image-add-on/src/wasm.js | 22 +- wasm/python/.clasp.json | 4 +- wasm/python/build.js | 26 +- wasm/python/package.json | 112 +-- wasm/python/polyfill.js | 4 +- wasm/python/src/appsscript.json | 26 +- wasm/python/src/main.js | 10 +- wasm/python/src/test.js | 10 +- wasm/python/src/wasm.js | 10 +- 153 files changed, 7263 insertions(+), 7260 deletions(-) diff --git a/.gemini/settings.json b/.gemini/settings.json index 500f4c4a6..4f41c59f3 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,17 +1,17 @@ { - "mcpServers": { - "workspace-developer": { - "httpUrl": "https://workspace-developer.goog/mcp", - "trust": true - } - }, - "tools": { - "allowed": [ - "run_shell_command(pnpm install)", - "run_shell_command(pnpm format)", - "run_shell_command(pnpm lint)", - "run_shell_command(pnpm check)", - "run_shell_command(pnpm test)" - ] - } + "mcpServers": { + "workspace-developer": { + "httpUrl": "https://workspace-developer.goog/mcp", + "trust": true + } + }, + "tools": { + "allowed": [ + "run_shell_command(pnpm install)", + "run_shell_command(pnpm format)", + "run_shell_command(pnpm lint)", + "run_shell_command(pnpm check)", + "run_shell_command(pnpm test)" + ] + } } diff --git a/.github/linters/.htmlhintrc b/.github/linters/.htmlhintrc index 78fe47573..70391a462 100644 --- a/.github/linters/.htmlhintrc +++ b/.github/linters/.htmlhintrc @@ -1,25 +1,25 @@ { - "tagname-lowercase": true, - "attr-lowercase": true, - "attr-value-double-quotes": true, - "attr-value-not-empty": false, - "attr-no-duplication": true, - "doctype-first": false, - "tag-pair": true, - "tag-self-close": false, - "spec-char-escape": false, - "id-unique": true, - "src-not-empty": true, - "title-require": false, - "alt-require": true, - "doctype-html5": true, - "id-class-value": false, - "style-disabled": false, - "inline-style-disabled": false, - "inline-script-disabled": false, - "space-tab-mixed-disabled": "space", - "id-class-ad-disabled": false, - "href-abs-or-rel": false, - "attr-unsafe-chars": true, - "head-script-disabled": false + "tagname-lowercase": true, + "attr-lowercase": true, + "attr-value-double-quotes": true, + "attr-value-not-empty": false, + "attr-no-duplication": true, + "doctype-first": false, + "tag-pair": true, + "tag-self-close": false, + "spec-char-escape": false, + "id-unique": true, + "src-not-empty": true, + "title-require": false, + "alt-require": true, + "doctype-html5": true, + "id-class-value": false, + "style-disabled": false, + "inline-style-disabled": false, + "inline-script-disabled": false, + "space-tab-mixed-disabled": "space", + "id-class-ad-disabled": false, + "href-abs-or-rel": false, + "attr-unsafe-chars": true, + "head-script-disabled": false } diff --git a/.github/scripts/check-gs.ts b/.github/scripts/check-gs.ts index 6e10292e8..7a526b036 100644 --- a/.github/scripts/check-gs.ts +++ b/.github/scripts/check-gs.ts @@ -18,13 +18,13 @@ import { exec } from "node:child_process"; import { - copyFileSync, - existsSync, - mkdirSync, - readdirSync, - rmSync, - statSync, - writeFileSync, + copyFileSync, + existsSync, + mkdirSync, + readdirSync, + rmSync, + statSync, + writeFileSync, } from "node:fs"; import { dirname, join, relative, resolve, sep } from "node:path"; import { promisify } from "node:util"; @@ -33,225 +33,225 @@ const execAsync = promisify(exec); const TEMP_ROOT = ".tsc_check"; interface Project { - files: string[]; - name: string; - path: string; + files: string[]; + name: string; + path: string; } interface CheckResult { - name: string; - success: boolean; - output: string; + name: string; + success: boolean; + output: string; } // Helper to recursively find all files with a specific extension function findFiles( - dir: string, - extension: string, - fileList: string[] = [], + dir: string, + extension: string, + fileList: string[] = [], ): string[] { - const files = readdirSync(dir); - for (const file of files) { - if (file.endsWith(".js")) continue; - const filePath = join(dir, file); - const stat = statSync(filePath); - if (stat.isDirectory()) { - if (file !== "node_modules" && file !== ".git" && file !== TEMP_ROOT) { - findFiles(filePath, extension, fileList); - } - } else if (file.endsWith(extension)) { - fileList.push(filePath); - } - } - return fileList; + const files = readdirSync(dir); + for (const file of files) { + if (file.endsWith(".js")) continue; + const filePath = join(dir, file); + const stat = statSync(filePath); + if (stat.isDirectory()) { + if (file !== "node_modules" && file !== ".git" && file !== TEMP_ROOT) { + findFiles(filePath, extension, fileList); + } + } else if (file.endsWith(extension)) { + fileList.push(filePath); + } + } + return fileList; } // Find all directories containing appsscript.json function findProjectRoots(rootDir: string): string[] { - return findFiles(rootDir, "appsscript.json").map((f) => dirname(f)); + return findFiles(rootDir, "appsscript.json").map((f) => dirname(f)); } function createProjects( - rootDir: string, - projectRoots: string[], - allGsFiles: string[], + rootDir: string, + projectRoots: string[], + allGsFiles: string[], ): Project[] { - // Holds files that belong to a formal Apps Script project (defined by the presence of appsscript.json). - const projectGroups = new Map(); - - // Holds "orphan" files that do not belong to any defined Apps Script project (no appsscript.json found). - const looseGroups = new Map(); - - // Initialize project groups - for (const p of projectRoots) { - projectGroups.set(p, []); - } - - for (const file of allGsFiles) { - let assigned = false; - let currentDir = dirname(file); - - while (currentDir.startsWith(rootDir) && currentDir !== rootDir) { - if (projectGroups.has(currentDir)) { - projectGroups.get(currentDir)?.push(file); - assigned = true; - break; - } - currentDir = dirname(currentDir); - } - - if (!assigned) { - const dir = dirname(file); - if (!looseGroups.has(dir)) { - looseGroups.set(dir, []); - } - looseGroups.get(dir)?.push(file); - } - } - - const projects: Project[] = []; - projectGroups.forEach((files, dir) => { - if (files.length > 0) { - projects.push({ - files, - name: `Project: ${relative(rootDir, dir)}`, - path: relative(rootDir, dir), - }); - } - }); - looseGroups.forEach((files, dir) => { - if (files.length > 0) { - projects.push({ - files, - name: `Loose Project: ${relative(rootDir, dir)}`, - path: relative(rootDir, dir), - }); - } - }); - - return projects; + // Holds files that belong to a formal Apps Script project (defined by the presence of appsscript.json). + const projectGroups = new Map(); + + // Holds "orphan" files that do not belong to any defined Apps Script project (no appsscript.json found). + const looseGroups = new Map(); + + // Initialize project groups + for (const p of projectRoots) { + projectGroups.set(p, []); + } + + for (const file of allGsFiles) { + let assigned = false; + let currentDir = dirname(file); + + while (currentDir.startsWith(rootDir) && currentDir !== rootDir) { + if (projectGroups.has(currentDir)) { + projectGroups.get(currentDir)?.push(file); + assigned = true; + break; + } + currentDir = dirname(currentDir); + } + + if (!assigned) { + const dir = dirname(file); + if (!looseGroups.has(dir)) { + looseGroups.set(dir, []); + } + looseGroups.get(dir)?.push(file); + } + } + + const projects: Project[] = []; + projectGroups.forEach((files, dir) => { + if (files.length > 0) { + projects.push({ + files, + name: `Project: ${relative(rootDir, dir)}`, + path: relative(rootDir, dir), + }); + } + }); + looseGroups.forEach((files, dir) => { + if (files.length > 0) { + projects.push({ + files, + name: `Loose Project: ${relative(rootDir, dir)}`, + path: relative(rootDir, dir), + }); + } + }); + + return projects; } async function checkProject( - project: Project, - rootDir: string, + project: Project, + rootDir: string, ): Promise { - const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, "_"); - const projectTempDir = join(TEMP_ROOT, projectNameSafe); - - // Synchronous setup is fine as it's fast and avoids race conditions on mkdir if we were sharing dirs (we aren't) - mkdirSync(projectTempDir, { recursive: true }); - - for (const file of project.files) { - const fileRelPath = relative(rootDir, file); - const destPath = join(projectTempDir, fileRelPath.replace(/\.gs$/, ".js")); - const destDir = dirname(destPath); - mkdirSync(destDir, { recursive: true }); - copyFileSync(file, destPath); - } - - const tsConfig = { - extends: "../../tsconfig.json", - compilerOptions: { - noEmit: true, - allowJs: true, - checkJs: true, - typeRoots: [resolve(rootDir, "node_modules/@types")], - }, - include: ["**/*.js"], - }; - - writeFileSync( - join(projectTempDir, "tsconfig.json"), - JSON.stringify(tsConfig, null, 2), - ); - - try { - await execAsync(`tsc -p \"${projectTempDir}\"`, { cwd: rootDir }); - return { name: project.name, success: true, output: "" }; - } catch (e) { - const err = e as { stdout?: string; stderr?: string }; - const rawOutput = (err.stdout ?? "") + (err.stderr || ""); - - const rewritten = rawOutput - .split("\n") - .map((line: string) => { - if (line.includes(projectTempDir)) { - let newLine = line.split(projectTempDir + sep).pop(); - if (!newLine) { - return line; - } - newLine = newLine.replace(/\.js(:|\()/g, ".gs$1"); - return newLine; - } - return line; - }) - .join("\n"); - - return { name: project.name, success: false, output: rewritten }; - } + const projectNameSafe = project.name.replace(/[^a-zA-Z0-9]/g, "_"); + const projectTempDir = join(TEMP_ROOT, projectNameSafe); + + // Synchronous setup is fine as it's fast and avoids race conditions on mkdir if we were sharing dirs (we aren't) + mkdirSync(projectTempDir, { recursive: true }); + + for (const file of project.files) { + const fileRelPath = relative(rootDir, file); + const destPath = join(projectTempDir, fileRelPath.replace(/\.gs$/, ".js")); + const destDir = dirname(destPath); + mkdirSync(destDir, { recursive: true }); + copyFileSync(file, destPath); + } + + const tsConfig = { + extends: "../../tsconfig.json", + compilerOptions: { + noEmit: true, + allowJs: true, + checkJs: true, + typeRoots: [resolve(rootDir, "node_modules/@types")], + }, + include: ["**/*.js"], + }; + + writeFileSync( + join(projectTempDir, "tsconfig.json"), + JSON.stringify(tsConfig, null, 2), + ); + + try { + await execAsync(`tsc -p \"${projectTempDir}\"`, { cwd: rootDir }); + return { name: project.name, success: true, output: "" }; + } catch (e) { + const err = e as { stdout?: string; stderr?: string }; + const rawOutput = (err.stdout ?? "") + (err.stderr || ""); + + const rewritten = rawOutput + .split("\n") + .map((line: string) => { + if (line.includes(projectTempDir)) { + let newLine = line.split(projectTempDir + sep).pop(); + if (!newLine) { + return line; + } + newLine = newLine.replace(/\.js(:|\()/g, ".gs$1"); + return newLine; + } + return line; + }) + .join("\n"); + + return { name: project.name, success: false, output: rewritten }; + } } async function main() { - try { - const rootDir = resolve("."); - const args = process.argv.slice(2); - const searchArg = args.find((arg) => arg !== "--"); - - // 1. Discovery - const projectRoots = findProjectRoots(rootDir); - const allGsFiles = findFiles(rootDir, ".gs"); - - // 2. Grouping - const projects = createProjects(rootDir, projectRoots, allGsFiles); - - // 3. Filtering - const projectsToCheck = projects.filter((p) => { - return !searchArg || p.path.startsWith(searchArg); - }); - - if (projectsToCheck.length === 0) { - console.log("No projects found matching the search path."); - return; - } - - // 4. Setup - if (existsSync(TEMP_ROOT)) { - rmSync(TEMP_ROOT, { recursive: true, force: true }); - } - mkdirSync(TEMP_ROOT); - - console.log(`Checking ${projectsToCheck.length} projects in parallel...`); - - // 5. Parallel Execution - const results = await Promise.all( - projectsToCheck.map((p) => checkProject(p, rootDir)), - ); - - // 6. Reporting - let hasError = false; - for (const result of results) { - if (!result.success) { - hasError = true; - console.log(`\n--- Failed: ${result.name} ---`); - console.log(result.output); - } - } - - if (hasError) { - console.error("\nOne or more checks failed."); - process.exit(1); - } else { - console.log("\nAll checks passed."); - } - } catch (err) { - console.error("Unexpected error:", err); - process.exit(1); - } finally { - if (existsSync(TEMP_ROOT)) { - rmSync(TEMP_ROOT, { recursive: true, force: true }); - } - } + try { + const rootDir = resolve("."); + const args = process.argv.slice(2); + const searchArg = args.find((arg) => arg !== "--"); + + // 1. Discovery + const projectRoots = findProjectRoots(rootDir); + const allGsFiles = findFiles(rootDir, ".gs"); + + // 2. Grouping + const projects = createProjects(rootDir, projectRoots, allGsFiles); + + // 3. Filtering + const projectsToCheck = projects.filter((p) => { + return !searchArg || p.path.startsWith(searchArg); + }); + + if (projectsToCheck.length === 0) { + console.log("No projects found matching the search path."); + return; + } + + // 4. Setup + if (existsSync(TEMP_ROOT)) { + rmSync(TEMP_ROOT, { recursive: true, force: true }); + } + mkdirSync(TEMP_ROOT); + + console.log(`Checking ${projectsToCheck.length} projects in parallel...`); + + // 5. Parallel Execution + const results = await Promise.all( + projectsToCheck.map((p) => checkProject(p, rootDir)), + ); + + // 6. Reporting + let hasError = false; + for (const result of results) { + if (!result.success) { + hasError = true; + console.log(`\n--- Failed: ${result.name} ---`); + console.log(result.output); + } + } + + if (hasError) { + console.error("\nOne or more checks failed."); + process.exit(1); + } else { + console.log("\nAll checks passed."); + } + } catch (err) { + console.error("Unexpected error:", err); + process.exit(1); + } finally { + if (existsSync(TEMP_ROOT)) { + rmSync(TEMP_ROOT, { recursive: true, force: true }); + } + } } main(); diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 7a3cec339..6eff0d8f6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["google-workspace.google-workspace-developer-tools"] + "recommendations": ["google-workspace.google-workspace-developer-tools"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c76c318a..f033bf1cb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "files.associations": { - "*.gs": "javascript" - } + "files.associations": { + "*.gs": "javascript" + } } diff --git a/ai/autosummarize/appsscript.json b/ai/autosummarize/appsscript.json index a4f3c4538..1cc12b980 100644 --- a/ai/autosummarize/appsscript.json +++ b/ai/autosummarize/appsscript.json @@ -1,22 +1,22 @@ { - "timeZone": "America/Denver", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "43", - "developmentMode": false - } - ], - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v3", - "serviceId": "drive" - } - ] - } + "timeZone": "America/Denver", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43", + "developmentMode": false + } + ], + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + } } diff --git a/ai/autosummarize/gemini.js b/ai/autosummarize/gemini.js index b7c1e860b..399ef10af 100644 --- a/ai/autosummarize/gemini.js +++ b/ai/autosummarize/gemini.js @@ -15,17 +15,17 @@ limitations under the License. */ function scriptPropertyWithDefault(key, defaultValue = undefined) { - const scriptProperties = PropertiesService.getScriptProperties(); - const value = scriptProperties.getProperty(key); - if (value) { - return value; - } - return defaultValue; + const scriptProperties = PropertiesService.getScriptProperties(); + const value = scriptProperties.getProperty(key); + if (value) { + return value; + } + return defaultValue; } const VERTEX_AI_LOCATION = scriptPropertyWithDefault( - "project_location", - "us-central1", + "project_location", + "us-central1", ); const MODEL_ID = scriptPropertyWithDefault("model_id", "gemini-pro-vision"); const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault("service_account_key"); @@ -40,55 +40,55 @@ const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault("service_account_key"); * @param {string} options.maxOutputTokens The number of tokens to limit to the prompt. */ function getAiSummary(parts, options = {}) { - const defaultOptions = { - temperature: 0.1, - maxOutputTokens: 8192, - topK: 1, - topP: 1, - stopSequences: [], - }; - const request = { - contents: [ - { - role: "user", - parts: parts, - }, - ], - generationConfig: { - ...defaultOptions, - ...options, - }, - }; + const defaultOptions = { + temperature: 0.1, + maxOutputTokens: 8192, + topK: 1, + topP: 1, + stopSequences: [], + }; + const request = { + contents: [ + { + role: "user", + parts: parts, + }, + ], + generationConfig: { + ...defaultOptions, + ...options, + }, + }; - const credentials = credentialsForVertexAI(); + const credentials = credentialsForVertexAI(); - const fetchOptions = { - method: "POST", - headers: { - Authorization: `Bearer ${credentials.accessToken}`, - }, - contentType: "application/json", - muteHttpExceptions: true, - payload: JSON.stringify(request), - }; + const fetchOptions = { + method: "POST", + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify(request), + }; - const url = - `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + - `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; - const response = UrlFetchApp.fetch(url, fetchOptions); + const url = + `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + + `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; + const response = UrlFetchApp.fetch(url, fetchOptions); - const responseCode = response.getResponseCode(); - if (responseCode >= 400) { - throw new Error(`Unable to process file: Error code ${responseCode}`); - } + const responseCode = response.getResponseCode(); + if (responseCode >= 400) { + throw new Error(`Unable to process file: Error code ${responseCode}`); + } - const responseText = response.getContentText(); - const parsedResponse = JSON.parse(responseText); - if (parsedResponse.error) { - throw new Error(parsedResponse.error.message); - } - const text = parsedResponse.candidates[0].content.parts[0].text; - return text; + const responseText = response.getContentText(); + const parsedResponse = JSON.parse(responseText); + if (parsedResponse.error) { + throw new Error(parsedResponse.error.message); + } + const text = parsedResponse.candidates[0].content.parts[0].text; + return text; } /** @@ -98,20 +98,20 @@ function getAiSummary(parts, options = {}) { * @return {!Object} Containing the Cloud Project Id and the access token. */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } - const parsedCredentials = JSON.parse(credentials); - const service = OAuth2.createService("Vertex") - .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials.private_key) - .setIssuer(parsedCredentials.client_email) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials.project_id, - accessToken: service.getAccessToken(), - }; + const parsedCredentials = JSON.parse(credentials); + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials.project_id, + accessToken: service.getAccessToken(), + }; } diff --git a/ai/autosummarize/main.js b/ai/autosummarize/main.js index 5b692feaf..7c9f77117 100644 --- a/ai/autosummarize/main.js +++ b/ai/autosummarize/main.js @@ -20,13 +20,13 @@ limitations under the License. * @param {object} e The event parameter for a simple onOpen trigger. */ function onOpen(e) { - SpreadsheetApp.getUi() - .createAddonMenu() - .addItem("📄 Open AutoSummarize AI", "showSidebar") - .addSeparator() - .addItem("❎ Quick summary", "doAutoSummarizeAI") - .addItem("❌ Remove all summaries", "removeAllSummaries") - .addToUi(); + SpreadsheetApp.getUi() + .createAddonMenu() + .addItem("📄 Open AutoSummarize AI", "showSidebar") + .addSeparator() + .addItem("❎ Quick summary", "doAutoSummarizeAI") + .addItem("❌ Remove all summaries", "removeAllSummaries") + .addToUi(); } /** @@ -37,18 +37,18 @@ function onOpen(e) { * @param {object} e The event parameter for a simple onInstall trigger. */ function onInstall(e) { - onOpen(e); + onOpen(e); } /** * Opens sidebar in Sheets with AutoSummarize AI interface. */ function showSidebar() { - const ui = - HtmlService.createHtmlOutputFromFile("sidebar").setTitle( - "AutoSummarize AI", - ); - SpreadsheetApp.getUi().showSidebar(ui); + const ui = + HtmlService.createHtmlOutputFromFile("sidebar").setTitle( + "AutoSummarize AI", + ); + SpreadsheetApp.getUi().showSidebar(ui); } /** @@ -56,109 +56,109 @@ function showSidebar() { * i.e. any sheets with prefix of 'AutoSummarize AI' */ function removeAllSummaries() { - const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); - const allSheets = spreadsheet.getSheets(); + const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); + const allSheets = spreadsheet.getSheets(); - for (const sheet of allSheets) { - const sheetName = sheet.getName(); - // Check if the sheet name starts with "AutoSummarize AI" - if (sheetName.startsWith("AutoSummarize AI")) { - spreadsheet.deleteSheet(sheet); - } - } + for (const sheet of allSheets) { + const sheetName = sheet.getName(); + // Check if the sheet name starts with "AutoSummarize AI" + if (sheetName.startsWith("AutoSummarize AI")) { + spreadsheet.deleteSheet(sheet); + } + } } /** * Wrapper function for add-on. */ function doAutoSummarizeAI( - customPrompt1, - customPrompt2, - temperature = 0.1, - tokens = 2048, + customPrompt1, + customPrompt2, + temperature = 0.1, + tokens = 2048, ) { - // Get selected cell values. - console.log("Getting selection..."); - const selection = SpreadsheetApp.getSelection() - .getActiveRange() - .getRichTextValues() - .map((value) => { - if (value[0].getLinkUrl()) { - return value[0].getLinkUrl(); - } - return value[0].getText(); - }); + // Get selected cell values. + console.log("Getting selection..."); + const selection = SpreadsheetApp.getSelection() + .getActiveRange() + .getRichTextValues() + .map((value) => { + if (value[0].getLinkUrl()) { + return value[0].getLinkUrl(); + } + return value[0].getText(); + }); - // Get AI summary - const data = summarizeFiles( - selection, - customPrompt1, - customPrompt2, - temperature, - tokens, - ); + // Get AI summary + const data = summarizeFiles( + selection, + customPrompt1, + customPrompt2, + temperature, + tokens, + ); - // Add and format a new new sheet. - const now = new Date(); - const nowFormatted = Utilities.formatDate( - now, - now.getTimezoneOffset().toString(), - "MM/dd HH:mm", - ); - let sheetName = `AutoSummarize AI (${nowFormatted})`; - if (SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)) { - sheetName = `AutoSummarize AI (${nowFormatted}:${now.getSeconds()})`; - } - const aiSheet = SpreadsheetApp.getActiveSpreadsheet() - .insertSheet() - .setName(sheetName); - const aiSheetHeaderStyle = SpreadsheetApp.newTextStyle() - .setFontSize(12) - .setBold(true) - .setFontFamily("Google Sans") - .setForegroundColor("#ffffff") - .build(); - const aiSheetValuesStyle = SpreadsheetApp.newTextStyle() - .setFontSize(10) - .setBold(false) - .setFontFamily("Google Sans") - .setForegroundColor("#000000") - .build(); - aiSheet - .getRange("A1:E1") - .setBackground("#434343") - .setTextStyle(aiSheetHeaderStyle) - .setValues([ - [ - "Link", - "Title", - `Summary from Gemini AI [Temperature: ${temperature}]`, - `Custom Prompt #1: ${customPrompt1}`, - `Custom Prompt #2: ${customPrompt2}`, - ], - ]) - .setWrap(true); - aiSheet.setColumnWidths(1, 1, 100); - aiSheet.setColumnWidths(2, 1, 300); - aiSheet.setColumnWidths(3, 3, 300); + // Add and format a new new sheet. + const now = new Date(); + const nowFormatted = Utilities.formatDate( + now, + now.getTimezoneOffset().toString(), + "MM/dd HH:mm", + ); + let sheetName = `AutoSummarize AI (${nowFormatted})`; + if (SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName)) { + sheetName = `AutoSummarize AI (${nowFormatted}:${now.getSeconds()})`; + } + const aiSheet = SpreadsheetApp.getActiveSpreadsheet() + .insertSheet() + .setName(sheetName); + const aiSheetHeaderStyle = SpreadsheetApp.newTextStyle() + .setFontSize(12) + .setBold(true) + .setFontFamily("Google Sans") + .setForegroundColor("#ffffff") + .build(); + const aiSheetValuesStyle = SpreadsheetApp.newTextStyle() + .setFontSize(10) + .setBold(false) + .setFontFamily("Google Sans") + .setForegroundColor("#000000") + .build(); + aiSheet + .getRange("A1:E1") + .setBackground("#434343") + .setTextStyle(aiSheetHeaderStyle) + .setValues([ + [ + "Link", + "Title", + `Summary from Gemini AI [Temperature: ${temperature}]`, + `Custom Prompt #1: ${customPrompt1}`, + `Custom Prompt #2: ${customPrompt2}`, + ], + ]) + .setWrap(true); + aiSheet.setColumnWidths(1, 1, 100); + aiSheet.setColumnWidths(2, 1, 300); + aiSheet.setColumnWidths(3, 3, 300); - // Copy results - aiSheet.getRange(`A2:E${data.length + 1}`).setValues(data); + // Copy results + aiSheet.getRange(`A2:E${data.length + 1}`).setValues(data); - aiSheet - .getRange(`A2:E${data.length + 1}`) - .setBackground("#ffffff") - .setTextStyle(aiSheetValuesStyle) - .setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP) - .setVerticalAlignment("top"); - aiSheet - .getRange(`C2:E${data.length + 1}`) - .setBackground("#efefef") - .setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP); + aiSheet + .getRange(`A2:E${data.length + 1}`) + .setBackground("#ffffff") + .setTextStyle(aiSheetValuesStyle) + .setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP) + .setVerticalAlignment("top"); + aiSheet + .getRange(`C2:E${data.length + 1}`) + .setBackground("#efefef") + .setWrapStrategy(SpreadsheetApp.WrapStrategy.WRAP); - aiSheet.deleteColumns(8, 19); - aiSheet.deleteRows( - aiSheet.getLastRow() + 1, - aiSheet.getMaxRows() - aiSheet.getLastRow(), - ); + aiSheet.deleteColumns(8, 19); + aiSheet.deleteRows( + aiSheet.getLastRow() + 1, + aiSheet.getMaxRows() - aiSheet.getLastRow(), + ); } diff --git a/ai/autosummarize/summarize.js b/ai/autosummarize/summarize.js index 1dfe67bed..d032e52bc 100644 --- a/ai/autosummarize/summarize.js +++ b/ai/autosummarize/summarize.js @@ -22,17 +22,17 @@ limitations under the License. * @return Base64 encoded file content */ function exportFile(fileId, targetType = "application/pdf") { - const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(targetType)}&supportsAllDrives=true`; + const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(targetType)}&supportsAllDrives=true`; - const requestOptions = { - headers: { - Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, - }, - }; - const response = UrlFetchApp.fetch(exportUrl, requestOptions); - const blob = response.getBlob(); + const requestOptions = { + headers: { + Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, + }, + }; + const response = UrlFetchApp.fetch(exportUrl, requestOptions); + const blob = response.getBlob(); - return Utilities.base64Encode(blob.getBytes()); + return Utilities.base64Encode(blob.getBytes()); } /** @@ -43,133 +43,133 @@ function exportFile(fileId, targetType = "application/pdf") { * @return Base64 encoded file content */ function downloadFile(fileId) { - const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&supportsAllDrives=true`; + const exportUrl = `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&supportsAllDrives=true`; - const requestOptions = { - headers: { - Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, - }, - }; - const response = UrlFetchApp.fetch(exportUrl, requestOptions); - const blob = response.getBlob(); + const requestOptions = { + headers: { + Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, + }, + }; + const response = UrlFetchApp.fetch(exportUrl, requestOptions); + const blob = response.getBlob(); - return Utilities.base64Encode(blob.getBytes()); + return Utilities.base64Encode(blob.getBytes()); } /** * Main function for AutoSummarize AI process. */ function summarizeFiles( - sourceSheetLinks, - customPrompt1, - customPrompt2, - temperature, - tokens, + sourceSheetLinks, + customPrompt1, + customPrompt2, + temperature, + tokens, ) { - return sourceSheetLinks.map((fileUrl) => { - console.log("Processing:", fileUrl); - - let fileName = ""; - let summary = ""; - let customPrompt1Response = ""; - let customPrompt2Response = ""; - - if (!fileUrl) { - return [ - "", - fileName, - summary, - customPrompt1Response, - customPrompt2Response, - ]; - } - try { - const promptParts = [ - { - text: "Summarize the following document.", - }, - { - text: "Return your response as a single paragraph. Reformat any lists as part of the paragraph. Output only the single paragraph as plain text. Do not use more than 3 sentences. Do not use markdown.", - }, - ]; - const fileIdMatchPattern = /\/d\/(.*?)\//gi; - const fileId = fileIdMatchPattern.exec(fileUrl)[1]; - - // Get file title and type. - const currentFile = Drive.Files.get(fileId, { supportsAllDrives: true }); - const fileMimeType = currentFile.mimeType; - fileName = currentFile.name; - - console.log(`Processing ${fileName} (ID: ${fileId})...`); - - // Add file content to the prompt - switch (fileMimeType) { - case "application/vnd.google-apps.presentation": - case "application/vnd.google-apps.document": - case "application/vnd.google-apps.spreadsheet": - promptParts.push({ - inlineData: { - mimeType: "application/pdf", - data: exportFile(fileId, "application/pdf"), - }, - }); - break; - case "application/pdf": - case "image/gif": - case "image/jpeg": - case "image/png": - promptParts.push({ - inlineData: { - mimeType: fileMimeType, - data: downloadFile(fileId), - }, - }); - break; - default: - console.log(`Unsupported file type: ${fileMimeType}`); - return [ - fileUrl, - fileName, - summary, - customPrompt1Response, - customPrompt2Response, - ]; - } - - // Prompt for summary - const geminiOptions = { - temperature, - tokens, - }; - summary = getAiSummary(promptParts, geminiOptions); - - // If any custom prompts, request those too - if (customPrompt1) { - promptParts[0].text = customPrompt1; - customPrompt1Response = getAiSummary(promptParts, geminiOptions); - } - if (customPrompt2) { - promptParts[0].text = customPrompt2; - customPrompt2Response = getAiSummary(promptParts, geminiOptions); - } - - return [ - fileUrl, - fileName, - summary, - customPrompt1Response, - customPrompt2Response, - ]; - } catch (e) { - // Add error row values if anything else goes wrong. - console.log(e); - return [ - fileUrl, - fileName, - "Something went wrong. Make sure you have access to this row's link.", - "", - "", - ]; - } - }); + return sourceSheetLinks.map((fileUrl) => { + console.log("Processing:", fileUrl); + + let fileName = ""; + let summary = ""; + let customPrompt1Response = ""; + let customPrompt2Response = ""; + + if (!fileUrl) { + return [ + "", + fileName, + summary, + customPrompt1Response, + customPrompt2Response, + ]; + } + try { + const promptParts = [ + { + text: "Summarize the following document.", + }, + { + text: "Return your response as a single paragraph. Reformat any lists as part of the paragraph. Output only the single paragraph as plain text. Do not use more than 3 sentences. Do not use markdown.", + }, + ]; + const fileIdMatchPattern = /\/d\/(.*?)\//gi; + const fileId = fileIdMatchPattern.exec(fileUrl)[1]; + + // Get file title and type. + const currentFile = Drive.Files.get(fileId, { supportsAllDrives: true }); + const fileMimeType = currentFile.mimeType; + fileName = currentFile.name; + + console.log(`Processing ${fileName} (ID: ${fileId})...`); + + // Add file content to the prompt + switch (fileMimeType) { + case "application/vnd.google-apps.presentation": + case "application/vnd.google-apps.document": + case "application/vnd.google-apps.spreadsheet": + promptParts.push({ + inlineData: { + mimeType: "application/pdf", + data: exportFile(fileId, "application/pdf"), + }, + }); + break; + case "application/pdf": + case "image/gif": + case "image/jpeg": + case "image/png": + promptParts.push({ + inlineData: { + mimeType: fileMimeType, + data: downloadFile(fileId), + }, + }); + break; + default: + console.log(`Unsupported file type: ${fileMimeType}`); + return [ + fileUrl, + fileName, + summary, + customPrompt1Response, + customPrompt2Response, + ]; + } + + // Prompt for summary + const geminiOptions = { + temperature, + tokens, + }; + summary = getAiSummary(promptParts, geminiOptions); + + // If any custom prompts, request those too + if (customPrompt1) { + promptParts[0].text = customPrompt1; + customPrompt1Response = getAiSummary(promptParts, geminiOptions); + } + if (customPrompt2) { + promptParts[0].text = customPrompt2; + customPrompt2Response = getAiSummary(promptParts, geminiOptions); + } + + return [ + fileUrl, + fileName, + summary, + customPrompt1Response, + customPrompt2Response, + ]; + } catch (e) { + // Add error row values if anything else goes wrong. + console.log(e); + return [ + fileUrl, + fileName, + "Something went wrong. Make sure you have access to this row's link.", + "", + "", + ]; + } + }); } diff --git a/ai/custom-func-ai-agent/AiVertex.js b/ai/custom-func-ai-agent/AiVertex.js index f84c69f1a..7148abe41 100644 --- a/ai/custom-func-ai-agent/AiVertex.js +++ b/ai/custom-func-ai-agent/AiVertex.js @@ -15,14 +15,14 @@ limitations under the License. */ const LOCATION = - PropertiesService.getScriptProperties().getProperty("LOCATION"); + PropertiesService.getScriptProperties().getProperty("LOCATION"); const GEMINI_MODEL_ID = - PropertiesService.getScriptProperties().getProperty("GEMINI_MODEL_ID"); + PropertiesService.getScriptProperties().getProperty("GEMINI_MODEL_ID"); const REASONING_ENGINE_ID = PropertiesService.getScriptProperties().getProperty( - "REASONING_ENGINE_ID", + "REASONING_ENGINE_ID", ); const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty( - "SERVICE_ACCOUNT_KEY", + "SERVICE_ACCOUNT_KEY", ); const credentials = credentialsForVertexAI(); @@ -31,65 +31,65 @@ const credentials = credentialsForVertexAI(); * @param {string} statement The statement to fact-check. */ function requestLlmAuditorAdkAiAgent(statement) { - return UrlFetchApp.fetch( - `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/reasoningEngines/${REASONING_ENGINE_ID}:streamQuery?alt=sse`, - { - method: "post", - headers: { Authorization: `Bearer ${credentials.accessToken}` }, - contentType: "application/json", - muteHttpExceptions: true, - payload: JSON.stringify({ - class_method: "async_stream_query", - input: { - user_id: "google_sheets_custom_function_fact_check", - message: statement, - }, - }), - }, - ).getContentText(); + return UrlFetchApp.fetch( + `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/reasoningEngines/${REASONING_ENGINE_ID}:streamQuery?alt=sse`, + { + method: "post", + headers: { Authorization: `Bearer ${credentials.accessToken}` }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify({ + class_method: "async_stream_query", + input: { + user_id: "google_sheets_custom_function_fact_check", + message: statement, + }, + }), + }, + ).getContentText(); } /** * @param {string} prompt The Gemini prompt to use. */ function requestOutputFormatting(prompt) { - const response = UrlFetchApp.fetch( - `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/publishers/google/models/${GEMINI_MODEL_ID}:generateContent`, - { - method: "post", - headers: { Authorization: `Bearer ${credentials.accessToken}` }, - contentType: "application/json", - muteHttpExceptions: true, - payload: JSON.stringify({ - contents: [ - { - role: "user", - parts: [{ text: prompt }], - }, - ], - generationConfig: { temperature: 0.1, maxOutputTokens: 2048 }, - safetySettings: [ - { - category: "HARM_CATEGORY_HARASSMENT", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_HATE_SPEECH", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_DANGEROUS_CONTENT", - threshold: "BLOCK_NONE", - }, - ], - }), - }, - ); - return JSON.parse(response).candidates[0].content.parts[0].text; + const response = UrlFetchApp.fetch( + `https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${LOCATION}/publishers/google/models/${GEMINI_MODEL_ID}:generateContent`, + { + method: "post", + headers: { Authorization: `Bearer ${credentials.accessToken}` }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify({ + contents: [ + { + role: "user", + parts: [{ text: prompt }], + }, + ], + generationConfig: { temperature: 0.1, maxOutputTokens: 2048 }, + safetySettings: [ + { + category: "HARM_CATEGORY_HARASSMENT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_HATE_SPEECH", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_DANGEROUS_CONTENT", + threshold: "BLOCK_NONE", + }, + ], + }), + }, + ); + return JSON.parse(response).candidates[0].content.parts[0].text; } /** @@ -99,21 +99,21 @@ function requestOutputFormatting(prompt) { * @return {!Object} Containing the Google Cloud project ID and the access token. */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } - const parsedCredentials = JSON.parse(credentials); + const parsedCredentials = JSON.parse(credentials); - const service = OAuth2.createService("Vertex") - .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials.private_key) - .setIssuer(parsedCredentials.client_email) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials.project_id, - accessToken: service.getAccessToken(), - }; + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials.project_id, + accessToken: service.getAccessToken(), + }; } diff --git a/ai/custom-func-ai-agent/Code.js b/ai/custom-func-ai-agent/Code.js index a73571287..a6815dadf 100644 --- a/ai/custom-func-ai-agent/Code.js +++ b/ai/custom-func-ai-agent/Code.js @@ -15,10 +15,10 @@ limitations under the License. */ DEFAULT_OUTPUT_FORMAT = - "Summarize it. Only keep the verdict result and main arguments. " + - "Do not reiterate the fact being checked. Remove all markdown. " + - "State the verdit result in a first paragraph in a few words and " + - "the rest of the summary in a second paragraph."; + "Summarize it. Only keep the verdict result and main arguments. " + + "Do not reiterate the fact being checked. Remove all markdown. " + + "State the verdit result in a first paragraph in a few words and " + + "the rest of the summary in a second paragraph."; /** * Passes a statement to fact-check and, optionally, output formatting instructions. @@ -32,7 +32,7 @@ DEFAULT_OUTPUT_FORMAT = * @customfunction */ function FACT_CHECK(statement, outputFormat = DEFAULT_OUTPUT_FORMAT) { - return requestOutputFormatting( - `Here is a fact checking result: ${requestLlmAuditorAdkAiAgent(statement)}.\n\n${outputFormat}`, - ); + return requestOutputFormatting( + `Here is a fact checking result: ${requestLlmAuditorAdkAiAgent(statement)}.\n\n${outputFormat}`, + ); } diff --git a/ai/custom-func-ai-agent/appsscript.json b/ai/custom-func-ai-agent/appsscript.json index 8f76020e2..bf02fb9fb 100644 --- a/ai/custom-func-ai-agent/appsscript.json +++ b/ai/custom-func-ai-agent/appsscript.json @@ -1,14 +1,14 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "43" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Los_Angeles", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/ai/custom-func-ai-studio/Code.js b/ai/custom-func-ai-studio/Code.js index b9b605e39..54d90540d 100644 --- a/ai/custom-func-ai-studio/Code.js +++ b/ai/custom-func-ai-studio/Code.js @@ -23,5 +23,5 @@ limitations under the License. * @customfunction */ function gemini(range, prompt) { - return getAiSummary(`For the range of cells ${range}, ${prompt}`); + return getAiSummary(`For the range of cells ${range}, ${prompt}`); } diff --git a/ai/custom-func-ai-studio/appsscript.json b/ai/custom-func-ai-studio/appsscript.json index 551eff89b..882ca10a4 100644 --- a/ai/custom-func-ai-studio/appsscript.json +++ b/ai/custom-func-ai-studio/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/ai/custom-func-ai-studio/gemini.js b/ai/custom-func-ai-studio/gemini.js index 7a595af6a..4a78d6d20 100644 --- a/ai/custom-func-ai-studio/gemini.js +++ b/ai/custom-func-ai-studio/gemini.js @@ -23,56 +23,56 @@ limitations under the License. * @return {string} Result of Gemini AI in string format. */ function getAiSummary(prompt) { - const data = { - contents: [ - { - parts: [ - { - text: prompt, - }, - ], - }, - ], - generationConfig: { - temperature: 0.2, - topK: 1, - topP: 1, - maxOutputTokens: 2048, - stopSequences: [], - }, - safetySettings: [ - { - category: "HARM_CATEGORY_HARASSMENT", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_HATE_SPEECH", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_DANGEROUS_CONTENT", - threshold: "BLOCK_NONE", - }, - ], - }; - const options = { - method: "post", - contentType: "application/json", - payload: JSON.stringify(data), // Convert the JavaScript object to a JSON string. - }; + const data = { + contents: [ + { + parts: [ + { + text: prompt, + }, + ], + }, + ], + generationConfig: { + temperature: 0.2, + topK: 1, + topP: 1, + maxOutputTokens: 2048, + stopSequences: [], + }, + safetySettings: [ + { + category: "HARM_CATEGORY_HARASSMENT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_HATE_SPEECH", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_DANGEROUS_CONTENT", + threshold: "BLOCK_NONE", + }, + ], + }; + const options = { + method: "post", + contentType: "application/json", + payload: JSON.stringify(data), // Convert the JavaScript object to a JSON string. + }; - const apiKey = PropertiesService.getScriptProperties().getProperty("api_key"); - const response = UrlFetchApp.fetch( - `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`, - options, - ); + const apiKey = PropertiesService.getScriptProperties().getProperty("api_key"); + const response = UrlFetchApp.fetch( + `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${apiKey}`, + options, + ); - const payload = JSON.parse(response.getContentText()); - const text = payload.candidates[0].content.parts[0].text; + const payload = JSON.parse(response.getContentText()); + const text = payload.candidates[0].content.parts[0].text; - return text; + return text; } diff --git a/ai/custom_func_vertex/Code.js b/ai/custom_func_vertex/Code.js index 7e9071cfb..448a2eb33 100644 --- a/ai/custom_func_vertex/Code.js +++ b/ai/custom_func_vertex/Code.js @@ -7,7 +7,7 @@ * @customfunction */ function gemini(range, prompt) { - return getAiSummary( - `For the table of data: ${range} Answer the following: ${prompt}. Do not use formatting. Remove all markdown.`, - ); + return getAiSummary( + `For the table of data: ${range} Answer the following: ${prompt}. Do not use formatting. Remove all markdown.`, + ); } diff --git a/ai/custom_func_vertex/aiVertex.js b/ai/custom_func_vertex/aiVertex.js index 2777aa62c..9f8fe54d0 100644 --- a/ai/custom_func_vertex/aiVertex.js +++ b/ai/custom_func_vertex/aiVertex.js @@ -15,11 +15,11 @@ limitations under the License. */ const VERTEX_AI_LOCATION = - PropertiesService.getScriptProperties().getProperty("project_location"); + PropertiesService.getScriptProperties().getProperty("project_location"); const MODEL_ID = - PropertiesService.getScriptProperties().getProperty("model_id"); + PropertiesService.getScriptProperties().getProperty("model_id"); const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty( - "service_account_key", + "service_account_key", ); /** @@ -30,63 +30,63 @@ const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty( * @param prompt - String representing your prompt for Gemini AI. */ function getAiSummary(prompt) { - const request = { - contents: [ - { - role: "user", - parts: [ - { - text: prompt, - }, - ], - }, - ], - generationConfig: { - temperature: 0.1, - maxOutputTokens: 2048, - }, - safetySettings: [ - { - category: "HARM_CATEGORY_HARASSMENT", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_HATE_SPEECH", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", - threshold: "BLOCK_NONE", - }, - { - category: "HARM_CATEGORY_DANGEROUS_CONTENT", - threshold: "BLOCK_NONE", - }, - ], - }; + const request = { + contents: [ + { + role: "user", + parts: [ + { + text: prompt, + }, + ], + }, + ], + generationConfig: { + temperature: 0.1, + maxOutputTokens: 2048, + }, + safetySettings: [ + { + category: "HARM_CATEGORY_HARASSMENT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_HATE_SPEECH", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", + threshold: "BLOCK_NONE", + }, + { + category: "HARM_CATEGORY_DANGEROUS_CONTENT", + threshold: "BLOCK_NONE", + }, + ], + }; - const credentials = credentialsForVertexAI(); + const credentials = credentialsForVertexAI(); - const fetchOptions = { - method: "post", - headers: { - Authorization: `Bearer ${credentials.accessToken}`, - }, - contentType: "application/json", - muteHttpExceptions: true, - payload: JSON.stringify(request), - }; + const fetchOptions = { + method: "post", + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify(request), + }; - const url = - `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/` + - `locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; + const url = + `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/` + + `locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; - const response = UrlFetchApp.fetch(url, fetchOptions); + const response = UrlFetchApp.fetch(url, fetchOptions); - const payload = JSON.parse(response); - const text = payload.candidates[0].content.parts[0].text; + const payload = JSON.parse(response); + const text = payload.candidates[0].content.parts[0].text; - return text; + return text; } /** @@ -96,21 +96,21 @@ function getAiSummary(prompt) { * @return {!Object} Containing the Cloud Project Id and the access token. */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } - const parsedCredentials = JSON.parse(credentials); + const parsedCredentials = JSON.parse(credentials); - const service = OAuth2.createService("Vertex") - .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials.private_key) - .setIssuer(parsedCredentials.client_email) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials.project_id, - accessToken: service.getAccessToken(), - }; + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials.project_id, + accessToken: service.getAccessToken(), + }; } diff --git a/ai/custom_func_vertex/appsscript.json b/ai/custom_func_vertex/appsscript.json index 8f76020e2..bf02fb9fb 100644 --- a/ai/custom_func_vertex/appsscript.json +++ b/ai/custom_func_vertex/appsscript.json @@ -1,14 +1,14 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "43" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Los_Angeles", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/ai/devdocs-link-preview/Cards.js b/ai/devdocs-link-preview/Cards.js index 37d978720..48b87b9ba 100644 --- a/ai/devdocs-link-preview/Cards.js +++ b/ai/devdocs-link-preview/Cards.js @@ -22,62 +22,62 @@ limitations under the License. * @return {!Card} */ function buildCard(pageTitle, summary, showRating = true) { - const cardHeader = CardService.newCardHeader().setTitle("About this page"); - - const summarySection = CardService.newCardSection().addWidget( - CardService.newTextParagraph().setText(summary), - ); - - const feedbackSection = - CardService.newCardSection().setHeader("Rate this summary"); - - if (showRating) { - const thumbsUpAction = CardService.newAction() - .setFunctionName("onRatingClicked") - .setParameters({ - key: "upVotes", - title: pageTitle, - pageSummary: summary, - }); - - const thumbsDownAction = CardService.newAction() - .setFunctionName("onRatingClicked") - .setParameters({ - key: "downVotes", - title: pageTitle, - pageSummary: summary, - }); - - const thumbsUpButton = CardService.newImageButton() - .setIconUrl( - "https://fonts.gstatic.com/s/i/googlematerialicons/thumb_up_alt/v11/gm_blue-24dp/1x/gm_thumb_up_alt_gm_blue_24dp.png", - ) - .setAltText("Looks good") - .setOnClickAction(thumbsUpAction); - - const thumbsDownButton = CardService.newImageButton() - .setIconUrl( - "https://fonts.gstatic.com/s/i/googlematerialicons/thumb_down_alt/v11/gm_blue-24dp/1x/gm_thumb_down_alt_gm_blue_24dp.png", - ) - .setAltText("Not great") - .setOnClickAction(thumbsDownAction); - - const ratingButtons = CardService.newButtonSet() - .addButton(thumbsUpButton) - .addButton(thumbsDownButton); - feedbackSection.addWidget(ratingButtons); - } else { - feedbackSection.addWidget( - CardService.newTextParagraph().setText("Thank you for your feedback."), - ); - } - - const card = CardService.newCardBuilder() - .setHeader(cardHeader) - .addSection(summarySection) - .addSection(feedbackSection) - .build(); - return card; + const cardHeader = CardService.newCardHeader().setTitle("About this page"); + + const summarySection = CardService.newCardSection().addWidget( + CardService.newTextParagraph().setText(summary), + ); + + const feedbackSection = + CardService.newCardSection().setHeader("Rate this summary"); + + if (showRating) { + const thumbsUpAction = CardService.newAction() + .setFunctionName("onRatingClicked") + .setParameters({ + key: "upVotes", + title: pageTitle, + pageSummary: summary, + }); + + const thumbsDownAction = CardService.newAction() + .setFunctionName("onRatingClicked") + .setParameters({ + key: "downVotes", + title: pageTitle, + pageSummary: summary, + }); + + const thumbsUpButton = CardService.newImageButton() + .setIconUrl( + "https://fonts.gstatic.com/s/i/googlematerialicons/thumb_up_alt/v11/gm_blue-24dp/1x/gm_thumb_up_alt_gm_blue_24dp.png", + ) + .setAltText("Looks good") + .setOnClickAction(thumbsUpAction); + + const thumbsDownButton = CardService.newImageButton() + .setIconUrl( + "https://fonts.gstatic.com/s/i/googlematerialicons/thumb_down_alt/v11/gm_blue-24dp/1x/gm_thumb_down_alt_gm_blue_24dp.png", + ) + .setAltText("Not great") + .setOnClickAction(thumbsDownAction); + + const ratingButtons = CardService.newButtonSet() + .addButton(thumbsUpButton) + .addButton(thumbsDownButton); + feedbackSection.addWidget(ratingButtons); + } else { + feedbackSection.addWidget( + CardService.newTextParagraph().setText("Thank you for your feedback."), + ); + } + + const card = CardService.newCardBuilder() + .setHeader(cardHeader) + .addSection(summarySection) + .addSection(feedbackSection) + .build(); + return card; } /** @@ -86,29 +86,29 @@ function buildCard(pageTitle, summary, showRating = true) { * @return {!Card} */ function buildErrorCard() { - const cardHeader = CardService.newCardHeader().setTitle( - "Uh oh! Something went wrong.", - ); + const cardHeader = CardService.newCardHeader().setTitle( + "Uh oh! Something went wrong.", + ); - const errorMessage = CardService.newTextParagraph().setText( - "It looks like Gemini got stage fright.", - ); + const errorMessage = CardService.newTextParagraph().setText( + "It looks like Gemini got stage fright.", + ); - const tryAgainButton = CardService.newTextButton() - .setText("Try again") - .setTextButtonStyle(CardService.TextButtonStyle.TEXT) - .setOnClickAction(CardService.newAction().setFunctionName("onLinkPreview")); + const tryAgainButton = CardService.newTextButton() + .setText("Try again") + .setTextButtonStyle(CardService.TextButtonStyle.TEXT) + .setOnClickAction(CardService.newAction().setFunctionName("onLinkPreview")); - const buttonList = CardService.newButtonSet().addButton(tryAgainButton); + const buttonList = CardService.newButtonSet().addButton(tryAgainButton); - const mainSection = CardService.newCardSection() - .addWidget(errorMessage) - .addWidget(buttonList); + const mainSection = CardService.newCardSection() + .addWidget(errorMessage) + .addWidget(buttonList); - const errorCard = CardService.newCardBuilder() - .setHeader(cardHeader) - .addSection(mainSection) - .build(); + const errorCard = CardService.newCardBuilder() + .setHeader(cardHeader) + .addSection(mainSection) + .build(); - return errorCard; + return errorCard; } diff --git a/ai/devdocs-link-preview/Helpers.js b/ai/devdocs-link-preview/Helpers.js index f01d10d86..d3ef585a8 100644 --- a/ai/devdocs-link-preview/Helpers.js +++ b/ai/devdocs-link-preview/Helpers.js @@ -18,10 +18,10 @@ limitations under the License. * Wraper around script properties to allow for a default value if unset. */ function scriptPropertyWithDefault(key, defaultValue = undefined) { - const scriptProperties = PropertiesService.getScriptProperties(); - const value = scriptProperties.getProperty(key); - if (value) { - return value; - } - return defaultValue; + const scriptProperties = PropertiesService.getScriptProperties(); + const value = scriptProperties.getProperty(key); + if (value) { + return value; + } + return defaultValue; } diff --git a/ai/devdocs-link-preview/Main.js b/ai/devdocs-link-preview/Main.js index dc4942d89..9a43e4114 100644 --- a/ai/devdocs-link-preview/Main.js +++ b/ai/devdocs-link-preview/Main.js @@ -21,27 +21,27 @@ limitations under the License. * @return {!Card} */ function onLinkPreview(event) { - const hostApp = event.hostApp; - if (!event[hostApp].matchedUrl.url) { - return; - } - const url = event[hostApp].matchedUrl.url; - try { - const info = getPageSummary(url); - const card = buildCard(info.title, info.summary); - const linkPreview = CardService.newLinkPreview() - .setPreviewCard(card) - .setTitle(info.title) - .setLinkPreviewTitle(info.title); - return linkPreview; - } catch (error) { - // Log the error - console.error("Error occurred:", error); - const errorCard = buildErrorCard(); - return CardService.newActionResponseBuilder() - .setNavigation(CardService.newNavigation().updateCard(errorCard)) - .build(); - } + const hostApp = event.hostApp; + if (!event[hostApp].matchedUrl.url) { + return; + } + const url = event[hostApp].matchedUrl.url; + try { + const info = getPageSummary(url); + const card = buildCard(info.title, info.summary); + const linkPreview = CardService.newLinkPreview() + .setPreviewCard(card) + .setTitle(info.title) + .setLinkPreviewTitle(info.title); + return linkPreview; + } catch (error) { + // Log the error + console.error("Error occurred:", error); + const errorCard = buildErrorCard(); + return CardService.newActionResponseBuilder() + .setNavigation(CardService.newNavigation().updateCard(errorCard)) + .build(); + } } /** @@ -51,19 +51,19 @@ function onLinkPreview(event) { * @return {!Card} */ function onRatingClicked(e) { - const key = e.parameters.key; - const title = e.parameters.title; - const pageSummary = e.parameters.pageSummary; + const key = e.parameters.key; + const title = e.parameters.title; + const pageSummary = e.parameters.pageSummary; - const properties = PropertiesService.getScriptProperties(); - let rating = Number(properties.getProperty(key) ?? 0); - properties.setProperty(key, ++rating); + const properties = PropertiesService.getScriptProperties(); + let rating = Number(properties.getProperty(key) ?? 0); + properties.setProperty(key, ++rating); - const card = buildCard(title, pageSummary, false); - const linkPreview = CardService.newLinkPreview() - .setPreviewCard(card) - .setTitle(title) - .setLinkPreviewTitle(title); + const card = buildCard(title, pageSummary, false); + const linkPreview = CardService.newLinkPreview() + .setPreviewCard(card) + .setTitle(title) + .setLinkPreviewTitle(title); - return linkPreview; + return linkPreview; } diff --git a/ai/devdocs-link-preview/Vertex.js b/ai/devdocs-link-preview/Vertex.js index 0a4b017f2..2e3f59e3a 100644 --- a/ai/devdocs-link-preview/Vertex.js +++ b/ai/devdocs-link-preview/Vertex.js @@ -15,8 +15,8 @@ limitations under the License. */ const VERTEX_AI_LOCATION = scriptPropertyWithDefault( - "project_location", - "us-central1", + "project_location", + "us-central1", ); const MODEL_ID = scriptPropertyWithDefault("model_id", "gemini-2.5-flash"); const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault("service_account_key"); @@ -25,67 +25,67 @@ const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault("service_account_key"); * Invokes Gemini to extrac the title and summary of a given URL. Responses may be cached. */ function getPageSummary(targetUrl) { - const cachedResponse = CacheService.getScriptCache().get(targetUrl); - if (cachedResponse) { - return JSON.parse(cachedResponse); - } + const cachedResponse = CacheService.getScriptCache().get(targetUrl); + if (cachedResponse) { + return JSON.parse(cachedResponse); + } - const request = { - contents: [ - { - role: "user", - parts: [ - { - text: targetUrl, - }, - ], - }, - ], - systemInstruction: { - parts: [ - { - text: `You are a Google Developers documentation expert. In 2-3 sentences, create a short description of what the following web page is about based on the snippet of HTML from the page. Make the summary scannable. Don't repeat the URL in the description. Use proper grammar. Make the description easy to read. Only include the description in your response, exclude any conversational parts of the response. Make sure you use the most recent Google product names. Output the response as JSON with the page title as "title" and the summary as "summary"`, - }, - ], - }, - generationConfig: { - temperature: 0.2, - candidateCount: 1, - maxOutputTokens: 2048, - }, - }; + const request = { + contents: [ + { + role: "user", + parts: [ + { + text: targetUrl, + }, + ], + }, + ], + systemInstruction: { + parts: [ + { + text: `You are a Google Developers documentation expert. In 2-3 sentences, create a short description of what the following web page is about based on the snippet of HTML from the page. Make the summary scannable. Don't repeat the URL in the description. Use proper grammar. Make the description easy to read. Only include the description in your response, exclude any conversational parts of the response. Make sure you use the most recent Google product names. Output the response as JSON with the page title as "title" and the summary as "summary"`, + }, + ], + }, + generationConfig: { + temperature: 0.2, + candidateCount: 1, + maxOutputTokens: 2048, + }, + }; - const credentials = credentialsForVertexAI(); + const credentials = credentialsForVertexAI(); - const fetchOptions = { - method: "POST", - headers: { - Authorization: `Bearer ${credentials.accessToken}`, - }, - contentType: "application/json", - muteHttpExceptions: true, - payload: JSON.stringify(request), - }; + const fetchOptions = { + method: "POST", + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + contentType: "application/json", + muteHttpExceptions: true, + payload: JSON.stringify(request), + }; - const url = - `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + - `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; - const response = UrlFetchApp.fetch(url, fetchOptions); + const url = + `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}` + + `/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; + const response = UrlFetchApp.fetch(url, fetchOptions); - const responseText = response.getContentText(); - console.log(responseText); - if (response.getResponseCode() >= 400) { - console.log(responseText); - throw new Error("Unable to generate preview,"); - } - const parsedResponse = JSON.parse(responseText); - const modelResponse = parsedResponse.candidates[0].content.parts[0].text; - const jsonMatch = modelResponse.match(/(?<=^`{3}json$)([\s\S]*)(?=^`{3}$)/gm); - if (!jsonMatch) { - throw new Error("Unable to generate preview,"); - } - CacheService.getScriptCache().put(targetUrl, jsonMatch); - return JSON.parse(jsonMatch[0]); + const responseText = response.getContentText(); + console.log(responseText); + if (response.getResponseCode() >= 400) { + console.log(responseText); + throw new Error("Unable to generate preview,"); + } + const parsedResponse = JSON.parse(responseText); + const modelResponse = parsedResponse.candidates[0].content.parts[0].text; + const jsonMatch = modelResponse.match(/(?<=^`{3}json$)([\s\S]*)(?=^`{3}$)/gm); + if (!jsonMatch) { + throw new Error("Unable to generate preview,"); + } + CacheService.getScriptCache().put(targetUrl, jsonMatch); + return JSON.parse(jsonMatch[0]); } /** @@ -95,20 +95,20 @@ function getPageSummary(targetUrl) { * @return {!Object} Containing the Cloud Project Id and the access token. */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } - const parsedCredentials = JSON.parse(credentials); - const service = OAuth2.createService("Vertex") - .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials.private_key) - .setIssuer(parsedCredentials.client_email) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials.project_id, - accessToken: service.getAccessToken(), - }; + const parsedCredentials = JSON.parse(credentials); + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials.project_id, + accessToken: service.getAccessToken(), + }; } diff --git a/ai/devdocs-link-preview/appsscript.json b/ai/devdocs-link-preview/appsscript.json index 7d9c7f482..e7f18696e 100644 --- a/ai/devdocs-link-preview/appsscript.json +++ b/ai/devdocs-link-preview/appsscript.json @@ -1,70 +1,70 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "43" - } - ] - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/workspace.linkpreview", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/cloud-platform" - ], - "addOns": { - "common": { - "name": "DevDocs Previews", - "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png", - "layoutProperties": { - "primaryColor": "#1A73E8" - } - }, - "docs": { - "linkPreviewTriggers": [ - { - "patterns": [ - { - "hostPattern": "developers.google.*" - } - ], - "runFunction": "onLinkPreview", - "labelText": "Page title", - "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" - } - ] - }, - "sheets": { - "linkPreviewTriggers": [ - { - "patterns": [ - { - "hostPattern": "developers.google.*" - } - ], - "runFunction": "onLinkPreview", - "labelText": "Page title", - "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" - } - ] - }, - "slides": { - "linkPreviewTriggers": [ - { - "patterns": [ - { - "hostPattern": "developers.google.*" - } - ], - "runFunction": "onLinkPreview", - "labelText": "Page title", - "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" - } - ] - } - } + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "43" + } + ] + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/workspace.linkpreview", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/cloud-platform" + ], + "addOns": { + "common": { + "name": "DevDocs Previews", + "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png", + "layoutProperties": { + "primaryColor": "#1A73E8" + } + }, + "docs": { + "linkPreviewTriggers": [ + { + "patterns": [ + { + "hostPattern": "developers.google.*" + } + ], + "runFunction": "onLinkPreview", + "labelText": "Page title", + "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" + } + ] + }, + "sheets": { + "linkPreviewTriggers": [ + { + "patterns": [ + { + "hostPattern": "developers.google.*" + } + ], + "runFunction": "onLinkPreview", + "labelText": "Page title", + "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" + } + ] + }, + "slides": { + "linkPreviewTriggers": [ + { + "patterns": [ + { + "hostPattern": "developers.google.*" + } + ], + "runFunction": "onLinkPreview", + "labelText": "Page title", + "logoUrl": "https://www.gstatic.com/images/branding/productlogos/google_developers/v8/web-24dp/logo_google_developers_color_1x_web_24dp.png" + } + ] + } + } } diff --git a/ai/drive-rename/ai.js b/ai/drive-rename/ai.js index 3c106a69a..6ca5694f5 100644 --- a/ai/drive-rename/ai.js +++ b/ai/drive-rename/ai.js @@ -15,11 +15,11 @@ limitations under the License. */ const VERTEX_AI_LOCATION = - PropertiesService.getScriptProperties().getProperty("project_location"); + PropertiesService.getScriptProperties().getProperty("project_location"); const MODEL_ID = - PropertiesService.getScriptProperties().getProperty("model_id"); + PropertiesService.getScriptProperties().getProperty("model_id"); const SERVICE_ACCOUNT_KEY = PropertiesService.getScriptProperties().getProperty( - "service_account_key", + "service_account_key", ); const STANDARD_PROMPT = ` @@ -42,46 +42,46 @@ const STANDARD_PROMPT = ` * @param prompt - String representing your prompt for Gemini AI. */ function getAiSummary(prompt) { - const request = { - contents: [ - { - role: "user", - parts: [ - { - text: STANDARD_PROMPT, - }, - { - text: prompt, - }, - ], - }, - ], - generationConfig: { - temperature: 0.2, - maxOutputTokens: 2048, - response_mime_type: "application/json", - }, - }; - - const credentials = credentialsForVertexAI(); - - const fetchOptions = { - method: "POST", - headers: { - Authorization: `Bearer ${credentials.accessToken}`, - }, - contentType: "application/json", - payload: JSON.stringify(request), - }; - - const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; - - const response = UrlFetchApp.fetch(url, fetchOptions); - - const payload = JSON.parse(response.getContentText()); - const jsonPayload = JSON.parse(payload.candidates[0].content.parts[0].text); - - return jsonPayload; + const request = { + contents: [ + { + role: "user", + parts: [ + { + text: STANDARD_PROMPT, + }, + { + text: prompt, + }, + ], + }, + ], + generationConfig: { + temperature: 0.2, + maxOutputTokens: 2048, + response_mime_type: "application/json", + }, + }; + + const credentials = credentialsForVertexAI(); + + const fetchOptions = { + method: "POST", + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + contentType: "application/json", + payload: JSON.stringify(request), + }; + + const url = `https://${VERTEX_AI_LOCATION}-aiplatform.googleapis.com/v1/projects/${credentials.projectId}/locations/${VERTEX_AI_LOCATION}/publishers/google/models/${MODEL_ID}:generateContent`; + + const response = UrlFetchApp.fetch(url, fetchOptions); + + const payload = JSON.parse(response.getContentText()); + const jsonPayload = JSON.parse(payload.candidates[0].content.parts[0].text); + + return jsonPayload; } /** @@ -90,21 +90,21 @@ function getAiSummary(prompt) { * */ function credentialsForVertexAI() { - const credentials = SERVICE_ACCOUNT_KEY; - if (!credentials) { - throw new Error("service_account_key script property must be set."); - } - - const parsedCredentials = JSON.parse(credentials); - - const service = OAuth2.createService("Vertex") - .setTokenUrl("https://oauth2.googleapis.com/token") - .setPrivateKey(parsedCredentials.private_key) - .setIssuer(parsedCredentials.client_email) - .setPropertyStore(PropertiesService.getScriptProperties()) - .setScope("https://www.googleapis.com/auth/cloud-platform"); - return { - projectId: parsedCredentials.project_id, - accessToken: service.getAccessToken(), - }; + const credentials = SERVICE_ACCOUNT_KEY; + if (!credentials) { + throw new Error("service_account_key script property must be set."); + } + + const parsedCredentials = JSON.parse(credentials); + + const service = OAuth2.createService("Vertex") + .setTokenUrl("https://oauth2.googleapis.com/token") + .setPrivateKey(parsedCredentials.private_key) + .setIssuer(parsedCredentials.client_email) + .setPropertyStore(PropertiesService.getScriptProperties()) + .setScope("https://www.googleapis.com/auth/cloud-platform"); + return { + projectId: parsedCredentials.project_id, + accessToken: service.getAccessToken(), + }; } diff --git a/ai/drive-rename/appsscript.json b/ai/drive-rename/appsscript.json index 8d80189a1..50d082ce3 100644 --- a/ai/drive-rename/appsscript.json +++ b/ai/drive-rename/appsscript.json @@ -1,44 +1,44 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "serviceId": "drive", - "version": "v3" - } - ] - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/drive.addons.metadata.readonly", - "https://www.googleapis.com/auth/drive.file", - "https://www.googleapis.com/auth/drive", - "https://www.googleapis.com/auth/drive.readonly", - "https://www.googleapis.com/auth/documents" - ], - "urlFetchWhitelist": [ - "https://*.googleusercontent.com/", - "https://*.googleapis.com/" - ], - "addOns": { - "common": { - "name": "Name with Intelligence", - "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png", - "layoutProperties": { - "primaryColor": "#4285f4", - "secondaryColor": "#3f8bca" - } - }, - "drive": { - "homepageTrigger": { - "runFunction": "onHomepageOpened" - }, - "onItemsSelectedTrigger": { - "runFunction": "onDriveItemsSelected" - } - } - } + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "serviceId": "drive", + "version": "v3" + } + ] + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/drive.addons.metadata.readonly", + "https://www.googleapis.com/auth/drive.file", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/documents" + ], + "urlFetchWhitelist": [ + "https://*.googleusercontent.com/", + "https://*.googleapis.com/" + ], + "addOns": { + "common": { + "name": "Name with Intelligence", + "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png", + "layoutProperties": { + "primaryColor": "#4285f4", + "secondaryColor": "#3f8bca" + } + }, + "drive": { + "homepageTrigger": { + "runFunction": "onHomepageOpened" + }, + "onItemsSelectedTrigger": { + "runFunction": "onDriveItemsSelected" + } + } + } } diff --git a/ai/drive-rename/drive.js b/ai/drive-rename/drive.js index 5d1487dac..5bc0ef9e9 100644 --- a/ai/drive-rename/drive.js +++ b/ai/drive-rename/drive.js @@ -21,26 +21,26 @@ limitations under the License. * @return {!Card} */ function renameFile(e) { - const newName = e.formInput.names; - const id = e.drive.activeCursorItem.id; - DriveApp.getFileById(id).setName(newName); - - const eUpdated = { - hostApp: "drive", - drive: { - selectedItems: [[Object]], - activeCursorItem: { - title: newName, - id: id, - iconUrl: e.drive.activeCursorItem.iconUrl, - mimeType: e.drive.activeCursorItem.mimeType, - }, - commonEventObject: { hostApp: "DRIVE", platform: "WEB" }, - clientPlatform: "web", - }, - }; - - return onCardUpdate(eUpdated); + const newName = e.formInput.names; + const id = e.drive.activeCursorItem.id; + DriveApp.getFileById(id).setName(newName); + + const eUpdated = { + hostApp: "drive", + drive: { + selectedItems: [[Object]], + activeCursorItem: { + title: newName, + id: id, + iconUrl: e.drive.activeCursorItem.iconUrl, + mimeType: e.drive.activeCursorItem.mimeType, + }, + commonEventObject: { hostApp: "DRIVE", platform: "WEB" }, + clientPlatform: "web", + }, + }; + + return onCardUpdate(eUpdated); } /** @@ -50,24 +50,24 @@ function renameFile(e) { * @return {!Card} */ function updateCard(e) { - const id = e.drive.activeCursorItem.id; - - const eConverted = { - hostApp: "drive", - drive: { - selectedItems: [[Object]], - activeCursorItem: { - title: DriveApp.getFileById(id).getName(), - id: id, - iconUrl: e.drive.activeCursorItem.iconUrl, - mimeType: e.drive.activeCursorItem.mimeType, - }, - commonEventObject: { hostApp: "DRIVE", platform: "WEB" }, - clientPlatform: "web", - }, - }; - - return onCardUpdate(eConverted); + const id = e.drive.activeCursorItem.id; + + const eConverted = { + hostApp: "drive", + drive: { + selectedItems: [[Object]], + activeCursorItem: { + title: DriveApp.getFileById(id).getName(), + id: id, + iconUrl: e.drive.activeCursorItem.iconUrl, + mimeType: e.drive.activeCursorItem.mimeType, + }, + commonEventObject: { hostApp: "DRIVE", platform: "WEB" }, + clientPlatform: "web", + }, + }; + + return onCardUpdate(eConverted); } /** @@ -77,11 +77,11 @@ function updateCard(e) { * @return {string} The body of the Google Document. */ function getDocumentBody(id) { - const doc = DocumentApp.openById(id); - const body = doc.getBody(); - const text = body.getText(); + const doc = DocumentApp.openById(id); + const body = doc.getBody(); + const text = body.getText(); - return text; + return text; } /** @@ -91,27 +91,27 @@ function getDocumentBody(id) { * @return {string} The body of the Google Document. */ function getDocAPIBody(id) { - // Call DOC API REST endpoint to get the file - const url = `https://docs.googleapis.com/v1/documents/${id}`; - - const response = UrlFetchApp.fetch(url, { - method: "GET", - headers: { - Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, - }, - muteHttpExceptions: true, - }); - - if (response.getResponseCode() !== 200) { - throw new Error(`Drive API returned error \ + // Call DOC API REST endpoint to get the file + const url = `https://docs.googleapis.com/v1/documents/${id}`; + + const response = UrlFetchApp.fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${ScriptApp.getOAuthToken()}`, + }, + muteHttpExceptions: true, + }); + + if (response.getResponseCode() !== 200) { + throw new Error(`Drive API returned error \ ${response.getResponseCode()} :\ ${response.getContentText()}`); - } + } - const file = response.getContentText(); - const data = JSON.parse(file); + const file = response.getContentText(); + const data = JSON.parse(file); - return data.body.content; + return data.body.content; } /** @@ -120,7 +120,7 @@ function getDocAPIBody(id) { * @param {!Event} e Add-on event context */ function moveFileToTrash(e) { - const id = e.drive.activeCursorItem.id; - const file = DriveApp.getFileById(id); - file.setTrashed(true); + const id = e.drive.activeCursorItem.id; + const file = DriveApp.getFileById(id); + file.setTrashed(true); } diff --git a/ai/drive-rename/main.js b/ai/drive-rename/main.js index 677cfa144..fb9ab06d7 100644 --- a/ai/drive-rename/main.js +++ b/ai/drive-rename/main.js @@ -20,17 +20,17 @@ limitations under the License. * @param e - Add-on event context */ function onHomepageOpened(e) { - const card = buildHomePage(); + const card = buildHomePage(); - return { - action: { - navigations: [ - { - pushCard: card, - }, - ], - }, - }; + return { + action: { + navigations: [ + { + pushCard: card, + }, + ], + }, + }; } /** @@ -39,15 +39,15 @@ function onHomepageOpened(e) { * @param e - Add-on event context */ function onDriveItemsSelected(e) { - return { - action: { - navigations: [ - { - pushCard: buildSelectionPage(e), - }, - ], - }, - }; + return { + action: { + navigations: [ + { + pushCard: buildSelectionPage(e), + }, + ], + }, + }; } /** @@ -56,13 +56,13 @@ function onDriveItemsSelected(e) { * @param e - (Modified) add-on event context */ function onCardUpdate(e) { - return { - action: { - navigations: [ - { - updateCard: buildSelectionPage(e), - }, - ], - }, - }; + return { + action: { + navigations: [ + { + updateCard: buildSelectionPage(e), + }, + ], + }, + }; } diff --git a/ai/drive-rename/ui.js b/ai/drive-rename/ui.js index d71395e9c..f59f379e8 100644 --- a/ai/drive-rename/ui.js +++ b/ai/drive-rename/ui.js @@ -15,13 +15,13 @@ limitations under the License. */ const ICO_HEADER = - "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png"; + "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-48dp/2x/gm_drive_file_rename_outline_googblue_48dp.png"; const ICON_RENAME = - "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-18dp/2x/gm_drive_file_rename_outline_googblue_18dp.png"; + "https://fonts.gstatic.com/s/i/googlematerialicons/drive_file_rename_outline/v12/googblue-18dp/2x/gm_drive_file_rename_outline_googblue_18dp.png"; const ICON_RETRY = - "https://fonts.gstatic.com/s/i/googlematerialicons/refresh/v16/googblue-18dp/2x/gm_refresh_googblue_18dp.png"; + "https://fonts.gstatic.com/s/i/googlematerialicons/refresh/v16/googblue-18dp/2x/gm_refresh_googblue_18dp.png"; const ICON_DELETE = - "https://fonts.gstatic.com/s/i/googlematerialicons/delete/v17/black-18dp/2x/gm_delete_black_18dp.png"; + "https://fonts.gstatic.com/s/i/googlematerialicons/delete/v17/black-18dp/2x/gm_delete_black_18dp.png"; /** * Builds the card for the selected active item. @@ -29,232 +29,232 @@ const ICON_DELETE = * @param e - Add-on event context */ function buildSelectionPage(e) { - const selected = e.drive.activeCursorItem; + const selected = e.drive.activeCursorItem; - // Check if Google Doc type, respond unsupported if not - if (selected.mimeType !== "application/vnd.google-apps.document") { - return { - sections: [ - { - widgets: [ - { - textParagraph: { - text: "Note: currently only Google Docs file types are supported.", - }, - }, - ], - }, - ], - header: buildHeader(), - }; - } + // Check if Google Doc type, respond unsupported if not + if (selected.mimeType !== "application/vnd.google-apps.document") { + return { + sections: [ + { + widgets: [ + { + textParagraph: { + text: "Note: currently only Google Docs file types are supported.", + }, + }, + ], + }, + ], + header: buildHeader(), + }; + } - // Get document body - const docBody = getDocumentBody(selected.id); + // Get document body + const docBody = getDocumentBody(selected.id); - // Create widgets starting with Title - const widgets = [ - { - textParagraph: { - text: `${selected.title}`, - }, - }, - ]; + // Create widgets starting with Title + const widgets = [ + { + textParagraph: { + text: `${selected.title}`, + }, + }, + ]; - // Check if doc is empty before calling AI - if (docBody.length > 1) { - // Get AI data - const aiResponse = getAiSummary(docBody); + // Check if doc is empty before calling AI + if (docBody.length > 1) { + // Get AI data + const aiResponse = getAiSummary(docBody); - console.log("RESPONSE"); + console.log("RESPONSE"); - console.log(aiResponse); + console.log(aiResponse); - // Add the Summary text - widgets.push({ - decoratedText: { - topLabel: "Summary", - text: aiResponse.summary, - wrapText: true, - }, - }); + // Add the Summary text + widgets.push({ + decoratedText: { + topLabel: "Summary", + text: aiResponse.summary, + wrapText: true, + }, + }); - // Divider - widgets.push({ divider: {} }); + // Divider + widgets.push({ divider: {} }); - // Create an object of items - const items = []; - for (const name of aiResponse.names) { - items.push({ - text: name, - value: name, - selected: false, - }); - } + // Create an object of items + const items = []; + for (const name of aiResponse.names) { + items.push({ + text: name, + value: name, + selected: false, + }); + } - // Set first item as selected - items[0].selected = true; + // Set first item as selected + items[0].selected = true; - // Add the Radio button of 'names' as items - widgets.push({ - selectionInput: { - name: "names", - label: "Select a new name", - type: "RADIO_BUTTON", - items: items, - }, - }); + // Add the Radio button of 'names' as items + widgets.push({ + selectionInput: { + name: "names", + label: "Select a new name", + type: "RADIO_BUTTON", + items: items, + }, + }); - // Create the 'Rename' button - widgets.push({ - buttonList: { - buttons: [ - { - text: "Rename", - icon: { - iconUrl: ICON_RENAME, - altText: "Rename", - }, - onClick: { - action: { - function: "renameFile", - parameters: [ - { - key: "id", - value: selected.id, - }, - ], - loadIndicator: "SPINNER", - }, - }, - }, - { - text: "", - icon: { - iconUrl: ICON_RETRY, - altText: "Retry", - }, - onClick: { - action: { - function: "updateCard", - parameters: [ - { - key: "id", - value: selected.id, - }, - ], - loadIndicator: "SPINNER", - }, - }, - }, - ], - }, - horizontalAlignment: "CENTER", - }); - } // end if + // Create the 'Rename' button + widgets.push({ + buttonList: { + buttons: [ + { + text: "Rename", + icon: { + iconUrl: ICON_RENAME, + altText: "Rename", + }, + onClick: { + action: { + function: "renameFile", + parameters: [ + { + key: "id", + value: selected.id, + }, + ], + loadIndicator: "SPINNER", + }, + }, + }, + { + text: "", + icon: { + iconUrl: ICON_RETRY, + altText: "Retry", + }, + onClick: { + action: { + function: "updateCard", + parameters: [ + { + key: "id", + value: selected.id, + }, + ], + loadIndicator: "SPINNER", + }, + }, + }, + ], + }, + horizontalAlignment: "CENTER", + }); + } // end if - // Don't call AI, but offer to delete - else { - // Add the Summary text - widgets.push({ - decoratedText: { - topLabel: "Summary", - text: "Empty document", - wrapText: true, - }, - }); + // Don't call AI, but offer to delete + else { + // Add the Summary text + widgets.push({ + decoratedText: { + topLabel: "Summary", + text: "Empty document", + wrapText: true, + }, + }); - // Divider - widgets.push({ divider: {} }); + // Divider + widgets.push({ divider: {} }); - // Create the 'Delete' button - widgets.push({ - buttonList: { - buttons: [ - { - text: "Move to trash", - icon: { - iconUrl: ICON_DELETE, - altText: "Move to trash", - }, - onClick: { - action: { - function: "moveFileToTrash", - parameters: [ - { - key: "id", - value: selected.id, - }, - ], - loadIndicator: "SPINNER", - }, - }, - color: { - red: 0.961, - green: 0.6, - blue: 0.667, - alpha: 1, - }, - }, - ], - }, - horizontalAlignment: "CENTER", - }); - } // end else + // Create the 'Delete' button + widgets.push({ + buttonList: { + buttons: [ + { + text: "Move to trash", + icon: { + iconUrl: ICON_DELETE, + altText: "Move to trash", + }, + onClick: { + action: { + function: "moveFileToTrash", + parameters: [ + { + key: "id", + value: selected.id, + }, + ], + loadIndicator: "SPINNER", + }, + }, + color: { + red: 0.961, + green: 0.6, + blue: 0.667, + alpha: 1, + }, + }, + ], + }, + horizontalAlignment: "CENTER", + }); + } // end else - return { - sections: [ - { - widgets, - }, - ], - header: buildHeader(), - }; + return { + sections: [ + { + widgets, + }, + ], + header: buildHeader(), + }; } /** * Builds the header for the Add-on Cards. */ function buildHeader() { - const header = { - title: "Name with Intelligence", - subtitle: `"Untitled documents" no more!`, // Better Doc names w/ Gemini AI", - imageUrl: ICO_HEADER, - imageType: "SQUARE", - }; - return header; + const header = { + title: "Name with Intelligence", + subtitle: `"Untitled documents" no more!`, // Better Doc names w/ Gemini AI", + imageUrl: ICO_HEADER, + imageType: "SQUARE", + }; + return header; } /** * Builds the home page card. */ function buildHomePage() { - const widgets = [ - { - textParagraph: { - text: "Name with Intelligence enables you to quickly rename any Google Doc using suggestions provided via Google Gemini.", - }, - }, - { divider: {} }, - { - textParagraph: { - text: "👉 To use, select a Google Doc to rename. Then choose a new name from the list of AI generated names provided for you. A quick summary of the file is also provided by Google Gemini to help you make your decision.", - }, - }, - { divider: {} }, - { - textParagraph: { - text: "Note: currently only Google Docs file types are supported.", - }, - }, - ]; + const widgets = [ + { + textParagraph: { + text: "Name with Intelligence enables you to quickly rename any Google Doc using suggestions provided via Google Gemini.", + }, + }, + { divider: {} }, + { + textParagraph: { + text: "👉 To use, select a Google Doc to rename. Then choose a new name from the list of AI generated names provided for you. A quick summary of the file is also provided by Google Gemini to help you make your decision.", + }, + }, + { divider: {} }, + { + textParagraph: { + text: "Note: currently only Google Docs file types are supported.", + }, + }, + ]; - return { - sections: [ - { - widgets, - }, - ], - header: buildHeader(), - }; + return { + sections: [ + { + widgets, + }, + ], + header: buildHeader(), + }; } diff --git a/ai/email-classifier/appsscript.json b/ai/email-classifier/appsscript.json index 2c42da7c0..baa8ca4ce 100644 --- a/ai/email-classifier/appsscript.json +++ b/ai/email-classifier/appsscript.json @@ -1,38 +1,38 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Gmail", - "version": "v1", - "serviceId": "gmail" - }, - { - "userSymbol": "Sheets", - "version": "v4", - "serviceId": "sheets" - } - ] - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/gmail.addons.execute", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/spreadsheets" - ], - "addOns": { - "common": { - "name": "Email Classifier", - "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/label_important/v20/googblue-24dp/1x/gm_label_important_googblue_24dp.png" - }, - "gmail": { - "homepageTrigger": { - "runFunction": "onHomepageTrigger", - "enabled": true - } - } - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + }, + { + "userSymbol": "Sheets", + "version": "v4", + "serviceId": "sheets" + } + ] + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/gmail.addons.execute", + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/spreadsheets" + ], + "addOns": { + "common": { + "name": "Email Classifier", + "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/label_important/v20/googblue-24dp/1x/gm_label_important_googblue_24dp.png" + }, + "gmail": { + "homepageTrigger": { + "runFunction": "onHomepageTrigger", + "enabled": true + } + } + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/ai/gmail-sentiment-analysis/appsscript.json b/ai/gmail-sentiment-analysis/appsscript.json index 4634adc28..f753fa81c 100644 --- a/ai/gmail-sentiment-analysis/appsscript.json +++ b/ai/gmail-sentiment-analysis/appsscript.json @@ -1,27 +1,27 @@ { - "timeZone": "Europe/Madrid", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "version": "43", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" - } - ] - }, - "addOns": { - "common": { - "name": "Productivity toolbox", - "logoUrl": "https://icons.iconarchive.com/icons/roundicons/100-free-solid/64/spy-icon.png", - "useLocaleFromApp": true - }, - "gmail": { - "homepageTrigger": { - "runFunction": "onHomepage", - "enabled": true - } - } - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "Europe/Madrid", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "version": "43", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" + } + ] + }, + "addOns": { + "common": { + "name": "Productivity toolbox", + "logoUrl": "https://icons.iconarchive.com/icons/roundicons/100-free-solid/64/spy-icon.png", + "useLocaleFromApp": true + }, + "gmail": { + "homepageTrigger": { + "runFunction": "onHomepage", + "enabled": true + } + } + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/ai/standup-chat-app/appsscript.json b/ai/standup-chat-app/appsscript.json index 8c7f15e7a..822a6ad98 100644 --- a/ai/standup-chat-app/appsscript.json +++ b/ai/standup-chat-app/appsscript.json @@ -1,37 +1,37 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Chat", - "serviceId": "chat", - "version": "v1" - }, - { - "userSymbol": "AdminDirectory", - "serviceId": "admin", - "version": "directory_v1" - } - ] - }, - "webapp": { - "executeAs": "USER_ACCESSING", - "access": "DOMAIN" - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/chat.messages", - "https://www.googleapis.com/auth/spreadsheets", - "https://www.googleapis.com/auth/admin.directory.user.readonly", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/chat.spaces.create", - "https://www.googleapis.com/auth/chat.spaces", - "https://www.googleapis.com/auth/chat.spaces.readonly", - "https://www.googleapis.com/auth/chat.spaces.create", - "https://www.googleapis.com/auth/chat.delete", - "https://www.googleapis.com/auth/chat.memberships", - "https://www.googleapis.com/auth/chat.memberships.app", - "https://www.googleapis.com/auth/userinfo.email" - ] + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Chat", + "serviceId": "chat", + "version": "v1" + }, + { + "userSymbol": "AdminDirectory", + "serviceId": "admin", + "version": "directory_v1" + } + ] + }, + "webapp": { + "executeAs": "USER_ACCESSING", + "access": "DOMAIN" + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/chat.messages", + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/admin.directory.user.readonly", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/chat.spaces.create", + "https://www.googleapis.com/auth/chat.spaces", + "https://www.googleapis.com/auth/chat.spaces.readonly", + "https://www.googleapis.com/auth/chat.spaces.create", + "https://www.googleapis.com/auth/chat.delete", + "https://www.googleapis.com/auth/chat.memberships", + "https://www.googleapis.com/auth/chat.memberships.app", + "https://www.googleapis.com/auth/userinfo.email" + ] } diff --git a/ai/standup-chat-app/db.js b/ai/standup-chat-app/db.js index 1a821f6c6..dafdf202f 100644 --- a/ai/standup-chat-app/db.js +++ b/ai/standup-chat-app/db.js @@ -35,59 +35,59 @@ limitations under the License. */ class DB { - /** - * params {String} spreadsheetId - */ - constructor(spreadsheetId) { - this.spreadsheetId = spreadsheetId; - this.sheetName = "Messages"; - } + /** + * params {String} spreadsheetId + */ + constructor(spreadsheetId) { + this.spreadsheetId = spreadsheetId; + this.sheetName = "Messages"; + } - /** - * @returns {SpreadsheetApp.Sheet} - */ - get sheet() { - const spreadsheet = SpreadsheetApp.openById(this.spreadsheetId); - let sheet = spreadsheet.getSheetByName(this.sheetName); + /** + * @returns {SpreadsheetApp.Sheet} + */ + get sheet() { + const spreadsheet = SpreadsheetApp.openById(this.spreadsheetId); + let sheet = spreadsheet.getSheetByName(this.sheetName); - // create if it does not exist - if (sheet === undefined) { - sheet = spreadsheet.insertSheet(); - sheet.setName(this.sheetName); - } + // create if it does not exist + if (sheet === undefined) { + sheet = spreadsheet.insertSheet(); + sheet.setName(this.sheetName); + } - return sheet; - } + return sheet; + } - /** - * @returns {Message|undefined} - */ - get last() { - const lastRow = this.sheet.getLastRow(); - if (lastRow === 0) return undefined; - return JSON.parse(this.sheet.getSheetValues(lastRow, 1, 1, 2)[0][1]); - } + /** + * @returns {Message|undefined} + */ + get last() { + const lastRow = this.sheet.getLastRow(); + if (lastRow === 0) return undefined; + return JSON.parse(this.sheet.getSheetValues(lastRow, 1, 1, 2)[0][1]); + } - /** - * @params {Chat_v1.Chat.V1.Schema.Message} message - */ - append(message) { - this.sheet.appendRow([message.name, JSON.stringify(message, null, 2)]); - } + /** + * @params {Chat_v1.Chat.V1.Schema.Message} message + */ + append(message) { + this.sheet.appendRow([message.name, JSON.stringify(message, null, 2)]); + } } /** * Test function for DB Object */ function testDB() { - const db = new DB(SPREADSHEET_ID); + const db = new DB(SPREADSHEET_ID); - let thread = db.last; - if (thread === undefined) return; - console.log(thread); + let thread = db.last; + if (thread === undefined) return; + console.log(thread); - db.rowOffset = 1; - thread = db.last; - if (thread === undefined) return; - console.log(thread); + db.rowOffset = 1; + thread = db.last; + if (thread === undefined) return; + console.log(thread); } diff --git a/ai/standup-chat-app/gemini.js b/ai/standup-chat-app/gemini.js index 45d84d606..716810595 100644 --- a/ai/standup-chat-app/gemini.js +++ b/ai/standup-chat-app/gemini.js @@ -23,21 +23,21 @@ limitations under the License. * @return {string} Response from AI call. */ function generateContent(text, API_KEY) { - const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`; + const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`; - return JSON.parse( - UrlFetchApp.fetch(url, { - method: "POST", - headers: { - "content-type": "application/json", - }, - payload: JSON.stringify({ - contents: [ - { - parts: [{ text }], - }, - ], - }), - }).getContentText(), - ); + return JSON.parse( + UrlFetchApp.fetch(url, { + method: "POST", + headers: { + "content-type": "application/json", + }, + payload: JSON.stringify({ + contents: [ + { + parts: [{ text }], + }, + ], + }), + }).getContentText(), + ); } diff --git a/ai/standup-chat-app/main.js b/ai/standup-chat-app/main.js index 3cf5c327d..bc53dd0cf 100644 --- a/ai/standup-chat-app/main.js +++ b/ai/standup-chat-app/main.js @@ -19,9 +19,9 @@ limitations under the License. * */ const API_KEY = PropertiesService.getScriptProperties().getProperty("API_KEY"); const SPREADSHEET_ID = - PropertiesService.getScriptProperties().getProperty("SPREADSHEET_ID"); // e.g. "1O0IW7fW1QeFLa7tIrv_h7_PlSUTB6kd0miQO_sXo7p0" + PropertiesService.getScriptProperties().getProperty("SPREADSHEET_ID"); // e.g. "1O0IW7fW1QeFLa7tIrv_h7_PlSUTB6kd0miQO_sXo7p0" const SPACE_NAME = - PropertiesService.getScriptProperties().getProperty("SPACE_NAME"); // e.g. "spaces/AAAABCa12Cc" + PropertiesService.getScriptProperties().getProperty("SPACE_NAME"); // e.g. "spaces/AAAABCa12Cc" const SUMMARY_HEADER = "\n\n*Gemini Generated Summary*\n\n"; @@ -32,27 +32,27 @@ const SUMMARY_HEADER = "\n\n*Gemini Generated Summary*\n\n"; * @return {string} The thread name of the message sent. */ function standup() { - const db = new DB(SPREADSHEET_ID); + const db = new DB(SPREADSHEET_ID); - const last = db.last; + const last = db.last; - let text = ` Please share your weekly update here.\n\n*Source Code*: `; + let text = ` Please share your weekly update here.\n\n*Source Code*: `; - if (last) { - text += `\n*Last Week*: <${linkToThread(last)}|View thread>`; - } + if (last) { + text += `\n*Last Week*: <${linkToThread(last)}|View thread>`; + } - const message = Chat.Spaces.Messages.create( - { - text, - }, - PropertiesService.getScriptProperties().getProperty("spaceName"), // Demo replaces => SPACE_NAME - ); + const message = Chat.Spaces.Messages.create( + { + text, + }, + PropertiesService.getScriptProperties().getProperty("spaceName"), // Demo replaces => SPACE_NAME + ); - db.append(message); + db.append(message); - console.log(`Thread Name: ${message.thread.name}`); - return message.thread.name; + console.log(`Thread Name: ${message.thread.name}`); + return message.thread.name; } /** @@ -62,46 +62,46 @@ function standup() { * @return n/a */ function summarize() { - const db = new DB(SPREADSHEET_ID); - const last = db.last; - - if (last === undefined) return; - - const filter = `thread.name=${last.thread.name}`; - let { messages } = Chat.Spaces.Messages.list( - PropertiesService.getScriptProperties().getProperty("spaceName"), - { filter }, - ); // Demo replaces => SPACE_NAME - - messages = (messages ?? []) - .slice(1) - .filter((message) => message.slashCommand === undefined); - - if (messages.length === 0) { - return; - } - - const history = messages - .map(({ sender, text }) => `${cachedGetSenderDisplayName(sender)}: ${text}`) - .join("/n"); - - const response = generateContent( - `Summarize the following weekly tasks and discussion per team member in a single concise sentence for each individual with an extra newline between members, but without using markdown or any special character except for newlines: ${history}`, - API_KEY, - ); - const summary = response.candidates[0].content?.parts[0].text; - - if (summary === undefined) { - return; - } - - Chat.Spaces.Messages.update( - { - text: last.formattedText + SUMMARY_HEADER + summary.replace("**", "*"), - }, - last.name, - { update_mask: "text" }, - ); + const db = new DB(SPREADSHEET_ID); + const last = db.last; + + if (last === undefined) return; + + const filter = `thread.name=${last.thread.name}`; + let { messages } = Chat.Spaces.Messages.list( + PropertiesService.getScriptProperties().getProperty("spaceName"), + { filter }, + ); // Demo replaces => SPACE_NAME + + messages = (messages ?? []) + .slice(1) + .filter((message) => message.slashCommand === undefined); + + if (messages.length === 0) { + return; + } + + const history = messages + .map(({ sender, text }) => `${cachedGetSenderDisplayName(sender)}: ${text}`) + .join("/n"); + + const response = generateContent( + `Summarize the following weekly tasks and discussion per team member in a single concise sentence for each individual with an extra newline between members, but without using markdown or any special character except for newlines: ${history}`, + API_KEY, + ); + const summary = response.candidates[0].content?.parts[0].text; + + if (summary === undefined) { + return; + } + + Chat.Spaces.Messages.update( + { + text: last.formattedText + SUMMARY_HEADER + summary.replace("**", "*"), + }, + last.name, + { update_mask: "text" }, + ); } /** @@ -111,16 +111,16 @@ function summarize() { * @return {string} User name on success | 'Unknown' if not. */ function getSenderDisplayName(sender) { - try { - const user = AdminDirectory.Users.get(sender.name.replace("users/", ""), { - projection: "BASIC", - viewType: "domain_public", - }); - return user.name.displayName ?? user.name.fullName; - } catch (e) { - console.error("Unable to get display name"); - return "Unknown"; - } + try { + const user = AdminDirectory.Users.get(sender.name.replace("users/", ""), { + projection: "BASIC", + viewType: "domain_public", + }); + return user.name.displayName ?? user.name.fullName; + } catch (e) { + console.error("Unable to get display name"); + return "Unknown"; + } } const cachedGetSenderDisplayName = memoize(getSenderDisplayName); @@ -130,6 +130,6 @@ const cachedGetSenderDisplayName = memoize(getSenderDisplayName); * @returns {String} */ function linkToThread(message) { - // https://chat.google.com/room/SPACE/THREAD/ - return `https://chat.google.com/room/${message.space.name.split("/").pop()}/${message.thread.name.split("/").pop()}`; + // https://chat.google.com/room/SPACE/THREAD/ + return `https://chat.google.com/room/${message.space.name.split("/").pop()}/${message.thread.name.split("/").pop()}`; } diff --git a/ai/standup-chat-app/memoize.js b/ai/standup-chat-app/memoize.js index 5414a2931..fc9ad2d78 100644 --- a/ai/standup-chat-app/memoize.js +++ b/ai/standup-chat-app/memoize.js @@ -24,9 +24,9 @@ limitations under the License. * @returns {string} The base64 encoded hash of the string. */ function hash(str, algorithm = Utilities.DigestAlgorithm.MD5) { - const digest = Utilities.computeDigest(algorithm, str); + const digest = Utilities.computeDigest(algorithm, str); - return Utilities.base64Encode(digest); + return Utilities.base64Encode(digest); } /** @@ -47,18 +47,18 @@ function hash(str, algorithm = Utilities.DigestAlgorithm.MD5) { * cached(4, 5, 6); // A new result will be calculated and cached */ function memoize(func, ttl = 600, cache = CacheService.getScriptCache()) { - return (...args) => { - // consider a more robust input to the hash function to handler complex - // types such as functions, dates, and regex - const key = hash(JSON.stringify([func.toString(), ...args])); + return (...args) => { + // consider a more robust input to the hash function to handler complex + // types such as functions, dates, and regex + const key = hash(JSON.stringify([func.toString(), ...args])); - const cached = cache.get(key); + const cached = cache.get(key); - if (cached != null) { - return JSON.parse(cached); - } - const result = func(...args); - cache.put(key, JSON.stringify(result), ttl); - return result; - }; + if (cached != null) { + return JSON.parse(cached); + } + const result = func(...args); + cache.put(key, JSON.stringify(result), ttl); + return result; + }; } diff --git a/apps-script/execute/target.js b/apps-script/execute/target.js index 2f791cf53..aa79e72fc 100644 --- a/apps-script/execute/target.js +++ b/apps-script/execute/target.js @@ -20,13 +20,13 @@ * @return {Object} A set of folder names keyed by folder ID. */ function getFoldersUnderRoot() { - const root = DriveApp.getRootFolder(); - const folders = root.getFolders(); - const folderSet = {}; - while (folders.hasNext()) { - const folder = folders.next(); - folderSet[folder.getId()] = folder.getName(); - } - return folderSet; + const root = DriveApp.getRootFolder(); + const folders = root.getFolders(); + const folderSet = {}; + while (folders.hasNext()) { + const folder = folders.next(); + folderSet[folder.getId()] = folder.getName(); + } + return folderSet; } // [END apps_script_api_execute] diff --git a/biome.json b/biome.json index 0d0a563b5..0c44b47da 100644 --- a/biome.json +++ b/biome.json @@ -1,20 +1,23 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "formatter": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "files": { - "ignore": ["moment.gs", "**/dist", "**/target", "**/pkg", "**/node_modules"] - } + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "indentWidth": 2, + "indentStyle": "space", + "lineWidth": 80 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "files": { + "ignore": ["moment.gs", "**/dist", "**/target", "**/pkg", "**/node_modules"] + } } diff --git a/chat/advanced-service/appsscript.json b/chat/advanced-service/appsscript.json index 14852ca1c..cd3bb000c 100644 --- a/chat/advanced-service/appsscript.json +++ b/chat/advanced-service/appsscript.json @@ -1,33 +1,33 @@ { - "timeZone": "America/New_York", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [ - "https://www.googleapis.com/auth/chat.spaces", - "https://www.googleapis.com/auth/chat.spaces.create", - "https://www.googleapis.com/auth/chat.spaces.readonly", - "https://www.googleapis.com/auth/chat.memberships", - "https://www.googleapis.com/auth/chat.memberships.app", - "https://www.googleapis.com/auth/chat.memberships.readonly", - "https://www.googleapis.com/auth/chat.messages", - "https://www.googleapis.com/auth/chat.messages.create", - "https://www.googleapis.com/auth/chat.messages.readonly" - ], - "chat": {}, - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Chat", - "version": "v1", - "serviceId": "chat" - } - ], - "libraries": [ - { - "userSymbol": "OAuth2", - "version": "43", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" - } - ] - } + "timeZone": "America/New_York", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/chat.spaces", + "https://www.googleapis.com/auth/chat.spaces.create", + "https://www.googleapis.com/auth/chat.spaces.readonly", + "https://www.googleapis.com/auth/chat.memberships", + "https://www.googleapis.com/auth/chat.memberships.app", + "https://www.googleapis.com/auth/chat.memberships.readonly", + "https://www.googleapis.com/auth/chat.messages", + "https://www.googleapis.com/auth/chat.messages.create", + "https://www.googleapis.com/auth/chat.messages.readonly" + ], + "chat": {}, + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Chat", + "version": "v1", + "serviceId": "chat" + } + ], + "libraries": [ + { + "userSymbol": "OAuth2", + "version": "43", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF" + } + ] + } } diff --git a/chat/quickstart/appsscript.json b/chat/quickstart/appsscript.json index a25813a03..ddf26fde9 100644 --- a/chat/quickstart/appsscript.json +++ b/chat/quickstart/appsscript.json @@ -1,16 +1,16 @@ { - "timeZone": "America/New_York", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": ["https://www.googleapis.com/auth/chat.spaces.readonly"], - "chat": {}, - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Chat", - "version": "v1", - "serviceId": "chat" - } - ] - } + "timeZone": "America/New_York", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": ["https://www.googleapis.com/auth/chat.spaces.readonly"], + "chat": {}, + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Chat", + "version": "v1", + "serviceId": "chat" + } + ] + } } diff --git a/data-studio/appsscript.json b/data-studio/appsscript.json index f505294eb..798183ec1 100644 --- a/data-studio/appsscript.json +++ b/data-studio/appsscript.json @@ -1,25 +1,25 @@ { - "dataStudio": { - "name": "Nucleus by Hooli", - "company": "Hooli Inc.", - "companyUrl": "https://hooli.xyz", - "logoUrl": "https://hooli.xyz/middle-out-optimized/nucleus/logo.png", - "addonUrl": "https://hooli.xyz/data-studio-connector", - "supportUrl": "https://hooli.xyz/data-studio-connector/support", - "description": "Nucleus by Hooli connector lets you connect to your data in Data Studio using Nucleus middle out optimization. You will need an account on hooli.xyz to use this connector. Create your account at https://hooli.xyz/signup", - "shortDescription": "Connect to your data using Nucleus middle out optimization", - "privacyPolicyUrl": "https://hooli.xyz/privacy", - "termsOfServiceUrl": "https://hooli.xyz/tos", - "authType": ["NONE"], - "feeType": ["PAID"], - "sources": [ - "HOOLI_CHAT_LOG", - "ENDFRAME_SERVER_STREAM", - "RETINABYTE_USER_ANALYTICS" - ], - "templates": { - "default": "872223s89f5fdkjnd983kjf" - } - }, - "urlFetchWhitelist": ["https://api.hooli.xyz/", "https://hooli.xyz/"] + "dataStudio": { + "name": "Nucleus by Hooli", + "company": "Hooli Inc.", + "companyUrl": "https://hooli.xyz", + "logoUrl": "https://hooli.xyz/middle-out-optimized/nucleus/logo.png", + "addonUrl": "https://hooli.xyz/data-studio-connector", + "supportUrl": "https://hooli.xyz/data-studio-connector/support", + "description": "Nucleus by Hooli connector lets you connect to your data in Data Studio using Nucleus middle out optimization. You will need an account on hooli.xyz to use this connector. Create your account at https://hooli.xyz/signup", + "shortDescription": "Connect to your data using Nucleus middle out optimization", + "privacyPolicyUrl": "https://hooli.xyz/privacy", + "termsOfServiceUrl": "https://hooli.xyz/tos", + "authType": ["NONE"], + "feeType": ["PAID"], + "sources": [ + "HOOLI_CHAT_LOG", + "ENDFRAME_SERVER_STREAM", + "RETINABYTE_USER_ANALYTICS" + ], + "templates": { + "default": "872223s89f5fdkjnd983kjf" + } + }, + "urlFetchWhitelist": ["https://api.hooli.xyz/", "https://hooli.xyz/"] } diff --git a/data-studio/appsscript2.json b/data-studio/appsscript2.json index 705398e48..26b5c76dc 100644 --- a/data-studio/appsscript2.json +++ b/data-studio/appsscript2.json @@ -1,12 +1,12 @@ { - "dataStudio": { - "name": "npm Downloads - Build Guide", - "logoUrl": "https://raw.githubusercontent.com/npm/logos/master/%22npm%22%20lockup/npm-logo-simplifed-with-white-space.png", - "company": "Build Guide User", - "companyUrl": "https://developers.google.com/datastudio/", - "addonUrl": "https://github.com/google/datastudio/tree/master/community-connectors/npm-downloads", - "supportUrl": "https://github.com/google/datastudio/issues", - "description": "Get npm package download counts.", - "sources": ["npm"] - } + "dataStudio": { + "name": "npm Downloads - Build Guide", + "logoUrl": "https://raw.githubusercontent.com/npm/logos/master/%22npm%22%20lockup/npm-logo-simplifed-with-white-space.png", + "company": "Build Guide User", + "companyUrl": "https://developers.google.com/datastudio/", + "addonUrl": "https://github.com/google/datastudio/tree/master/community-connectors/npm-downloads", + "supportUrl": "https://github.com/google/datastudio/issues", + "description": "Get npm package download counts.", + "sources": ["npm"] + } } diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json b/forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json index 50427a683..4ba87b8f4 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/appsscript.json @@ -1,19 +1,19 @@ { - "timeZone": "America/New_York", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": {}, - "webapp": { - "executeAs": "USER_DEPLOYING", - "access": "MYSELF" - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/drive", - "https://www.googleapis.com/auth/drive.readonly", - "https://www.googleapis.com/auth/forms.body", - "https://www.googleapis.com/auth/forms.body.readonly", - "https://www.googleapis.com/auth/forms.responses.readonly", - "https://www.googleapis.com/auth/userinfo.email" - ] + "timeZone": "America/New_York", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": {}, + "webapp": { + "executeAs": "USER_DEPLOYING", + "access": "MYSELF" + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/forms.body", + "https://www.googleapis.com/auth/forms.body.readonly", + "https://www.googleapis.com/auth/forms.responses.readonly", + "https://www.googleapis.com/auth/userinfo.email" + ] } diff --git a/gmail-sentiment-analysis/.clasp.json b/gmail-sentiment-analysis/.clasp.json index ba34351b4..8206fa91c 100644 --- a/gmail-sentiment-analysis/.clasp.json +++ b/gmail-sentiment-analysis/.clasp.json @@ -1,3 +1,3 @@ { - "scriptId": "1Z2gfvr0oYn68ppDtQbv0qIuKKVWhvwOTr-gCE0GFKVjNk8NDlpfJAGAr" + "scriptId": "1Z2gfvr0oYn68ppDtQbv0qIuKKVWhvwOTr-gCE0GFKVjNk8NDlpfJAGAr" } diff --git a/gmail-sentiment-analysis/appsscript.json b/gmail-sentiment-analysis/appsscript.json index fa056ed5c..3de7c717f 100644 --- a/gmail-sentiment-analysis/appsscript.json +++ b/gmail-sentiment-analysis/appsscript.json @@ -1,25 +1,25 @@ { - "timeZone": "America/Toronto", - "oauthScopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/gmail.addons.execute", - "https://www.googleapis.com/auth/gmail.labels", - "https://www.googleapis.com/auth/gmail.modify", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/userinfo.email" - ], - "addOns": { - "common": { - "name": "Sentiment Analysis", - "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/sentiment_extremely_dissatisfied/v6/black-24dp/1x/gm_sentiment_extremely_dissatisfied_black_24dp.png" - }, - "gmail": { - "homepageTrigger": { - "runFunction": "onHomepageTrigger", - "enabled": true - } - } - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Toronto", + "oauthScopes": [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/gmail.addons.execute", + "https://www.googleapis.com/auth/gmail.labels", + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/userinfo.email" + ], + "addOns": { + "common": { + "name": "Sentiment Analysis", + "logoUrl": "https://fonts.gstatic.com/s/i/googlematerialicons/sentiment_extremely_dissatisfied/v6/black-24dp/1x/gm_sentiment_extremely_dissatisfied_black_24dp.png" + }, + "gmail": { + "homepageTrigger": { + "runFunction": "onHomepageTrigger", + "enabled": true + } + } + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/gmail/add-ons/appsscript.json b/gmail/add-ons/appsscript.json index 36f94c724..8aeabdae4 100644 --- a/gmail/add-ons/appsscript.json +++ b/gmail/add-ons/appsscript.json @@ -1,20 +1,20 @@ { - "oauthScopes": [ - "https://www.googleapis.com/auth/gmail.addons.execute", - "https://www.googleapis.com/auth/gmail.addons.current.message.metadata", - "https://www.googleapis.com/auth/gmail.modify" - ], - "gmail": { - "name": "Gmail Add-on Quickstart - QuickLabels", - "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/label_googblue_24dp.png", - "contextualTriggers": [ - { - "unconditional": {}, - "onTriggerFunction": "buildAddOn" - } - ], - "openLinkUrlPrefixes": ["https://mail.google.com/"], - "primaryColor": "#4285F4", - "secondaryColor": "#4285F4" - } + "oauthScopes": [ + "https://www.googleapis.com/auth/gmail.addons.execute", + "https://www.googleapis.com/auth/gmail.addons.current.message.metadata", + "https://www.googleapis.com/auth/gmail.modify" + ], + "gmail": { + "name": "Gmail Add-on Quickstart - QuickLabels", + "logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/label_googblue_24dp.png", + "contextualTriggers": [ + { + "unconditional": {}, + "onTriggerFunction": "buildAddOn" + } + ], + "openLinkUrlPrefixes": ["https://mail.google.com/"], + "primaryColor": "#4285F4", + "secondaryColor": "#4285F4" + } } diff --git a/package.json b/package.json index c29860601..d26850c71 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,33 @@ { - "name": "googleworkspace-apps-script-samples", - "version": "1.0.0", - "description": "Apps Script samples for [Google Workspace](https://developers.google.com/apps-script/) docs.", - "license": "MIT", - "private": true, - "keywords": [ - "Google Workspace", - "Apps Script", - "Calendar", - "Drive", - "Sheets", - "Slides", - "API" - ], - "devDependencies": { - "@types/google-apps-script": "^2.0.7", - "@types/node": "^24.10.1", - "@biomejs/biome": "1.9.4", - "tsx": "^4.20.6", - "typescript": "^5.9.3" - }, - "scripts": { - "lint": "biome check .", - "format": "biome check --write .", - "check": "tsx .github/scripts/check-gs.ts" - }, - "type": "module", - "packageManager": "pnpm@10.15.1", - "engines": { - "node": ">=20" - } + "name": "googleworkspace-apps-script-samples", + "version": "1.0.0", + "description": "Apps Script samples for [Google Workspace](https://developers.google.com/apps-script/) docs.", + "license": "MIT", + "private": true, + "keywords": [ + "Google Workspace", + "Apps Script", + "Calendar", + "Drive", + "Sheets", + "Slides", + "API" + ], + "devDependencies": { + "@types/google-apps-script": "^2.0.7", + "@types/node": "^24.10.1", + "@biomejs/biome": "1.9.4", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + }, + "scripts": { + "lint": "biome check .", + "format": "biome check --write .", + "check": "tsx .github/scripts/check-gs.ts" + }, + "type": "module", + "packageManager": "pnpm@10.15.1", + "engines": { + "node": ">=20" + } } diff --git a/picker/appsscript.json b/picker/appsscript.json index 5e2059224..218c29527 100644 --- a/picker/appsscript.json +++ b/picker/appsscript.json @@ -1,18 +1,18 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [ - "https://www.googleapis.com/auth/script.container.ui", - "https://www.googleapis.com/auth/drive.file" - ], - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v3", - "serviceId": "drive" - } - ] - } + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/script.container.ui", + "https://www.googleapis.com/auth/drive.file" + ], + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + } } diff --git a/sheets/next18/appsscript.json b/sheets/next18/appsscript.json index 6ba0b64f6..8ac29955f 100644 --- a/sheets/next18/appsscript.json +++ b/sheets/next18/appsscript.json @@ -1,21 +1,21 @@ { - "timeZone": "America/New_York", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "26" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "oauthScopes": [ - "https://www.googleapis.com/auth/script.container.ui", - "https://www.googleapis.com/auth/script.external_request", - "https://www.googleapis.com/auth/spreadsheets.currentonly", - "https://www.googleapis.com/auth/drive", - "https://www.googleapis.com/auth/documents", - "https://www.googleapis.com/auth/presentations" - ] + "timeZone": "America/New_York", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "26" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "oauthScopes": [ + "https://www.googleapis.com/auth/script.container.ui", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/spreadsheets.currentonly", + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/presentations" + ] } diff --git a/slides/SpeakerNotesScript/appscript.json b/slides/SpeakerNotesScript/appscript.json index ce8138e2c..696cd61b5 100644 --- a/slides/SpeakerNotesScript/appscript.json +++ b/slides/SpeakerNotesScript/appscript.json @@ -1,8 +1,8 @@ { - "timeZone": "America/New_York", - "oauthScopes": [ - "https://www.googleapis.com/auth/documents", - "https://www.googleapis.com/auth/presentations.currentonly" - ], - "exceptionLogging": "STACKDRIVER" + "timeZone": "America/New_York", + "oauthScopes": [ + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/presentations.currentonly" + ], + "exceptionLogging": "STACKDRIVER" } diff --git a/solutions/add-on/book-smartchip/Code.js b/solutions/add-on/book-smartchip/Code.js index 6bea50e00..e578b804b 100644 --- a/solutions/add-on/book-smartchip/Code.js +++ b/solutions/add-on/book-smartchip/Code.js @@ -1,53 +1,53 @@ function getBook(id) { - const apiKey = "YOUR_API_KEY"; // Replace with your API key - const apiEndpoint = `https://www.googleapis.com/books/v1/volumes/${id}?key=${apiKey}&country=US`; - const response = UrlFetchApp.fetch(apiEndpoint); - return JSON.parse(response); + const apiKey = "YOUR_API_KEY"; // Replace with your API key + const apiEndpoint = `https://www.googleapis.com/books/v1/volumes/${id}?key=${apiKey}&country=US`; + const response = UrlFetchApp.fetch(apiEndpoint); + return JSON.parse(response); } function bookLinkPreview(event) { - if (event.docs.matchedUrl.url) { - const segments = event.docs.matchedUrl.url.split("/"); - const volumeID = segments[segments.length - 1]; - - const bookData = getBook(volumeID); - const bookTitle = bookData.volumeInfo.title; - const bookDescription = bookData.volumeInfo.description; - const bookImage = bookData.volumeInfo.imageLinks.small; - const bookAuthors = bookData.volumeInfo.authors; - const bookPageCount = bookData.volumeInfo.pageCount; - - const previewHeader = CardService.newCardHeader() - .setSubtitle(`By ${bookAuthors}`) - .setTitle(bookTitle); - - const previewPages = CardService.newDecoratedText() - .setTopLabel("Page count") - .setText(bookPageCount); - - const previewDescription = CardService.newDecoratedText() - .setTopLabel("About this book") - .setText(bookDescription) - .setWrapText(true); - - const previewImage = CardService.newImage() - .setAltText("Image of book cover") - .setImageUrl(bookImage); - - const buttonBook = CardService.newTextButton() - .setText("View book") - .setOpenLink(CardService.newOpenLink().setUrl(event.docs.matchedUrl.url)); - - const cardSectionBook = CardService.newCardSection() - .addWidget(previewImage) - .addWidget(previewPages) - .addWidget(CardService.newDivider()) - .addWidget(previewDescription) - .addWidget(buttonBook); - - return CardService.newCardBuilder() - .setHeader(previewHeader) - .addSection(cardSectionBook) - .build(); - } + if (event.docs.matchedUrl.url) { + const segments = event.docs.matchedUrl.url.split("/"); + const volumeID = segments[segments.length - 1]; + + const bookData = getBook(volumeID); + const bookTitle = bookData.volumeInfo.title; + const bookDescription = bookData.volumeInfo.description; + const bookImage = bookData.volumeInfo.imageLinks.small; + const bookAuthors = bookData.volumeInfo.authors; + const bookPageCount = bookData.volumeInfo.pageCount; + + const previewHeader = CardService.newCardHeader() + .setSubtitle(`By ${bookAuthors}`) + .setTitle(bookTitle); + + const previewPages = CardService.newDecoratedText() + .setTopLabel("Page count") + .setText(bookPageCount); + + const previewDescription = CardService.newDecoratedText() + .setTopLabel("About this book") + .setText(bookDescription) + .setWrapText(true); + + const previewImage = CardService.newImage() + .setAltText("Image of book cover") + .setImageUrl(bookImage); + + const buttonBook = CardService.newTextButton() + .setText("View book") + .setOpenLink(CardService.newOpenLink().setUrl(event.docs.matchedUrl.url)); + + const cardSectionBook = CardService.newCardSection() + .addWidget(previewImage) + .addWidget(previewPages) + .addWidget(CardService.newDivider()) + .addWidget(previewDescription) + .addWidget(buttonBook); + + return CardService.newCardBuilder() + .setHeader(previewHeader) + .addSection(cardSectionBook) + .build(); + } } diff --git a/solutions/add-on/book-smartchip/appsscript.json b/solutions/add-on/book-smartchip/appsscript.json index a54803910..ab533aed5 100644 --- a/solutions/add-on/book-smartchip/appsscript.json +++ b/solutions/add-on/book-smartchip/appsscript.json @@ -1,41 +1,41 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [ - "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", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/library-icon.png", - "layoutProperties": { - "primaryColor": "#dd4b39" - } - }, - "docs": { - "linkPreviewTriggers": [ - { - "runFunction": "bookLinkPreview", - "patterns": [ - { - "hostPattern": "*.google.*", - "pathPrefix": "books" - }, - { - "hostPattern": "*.google.*", - "pathPrefix": "books/edition" - } - ], - "labelText": "Book", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/book-icon.png", - "localizedLabelText": { - "es": "Libros" - } - } - ] - } - } + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "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", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/library-icon.png", + "layoutProperties": { + "primaryColor": "#dd4b39" + } + }, + "docs": { + "linkPreviewTriggers": [ + { + "runFunction": "bookLinkPreview", + "patterns": [ + { + "hostPattern": "*.google.*", + "pathPrefix": "books" + }, + { + "hostPattern": "*.google.*", + "pathPrefix": "books/edition" + } + ], + "labelText": "Book", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/book-icon.png", + "localizedLabelText": { + "es": "Libros" + } + } + ] + } + } } diff --git a/solutions/add-on/share-macro/Code.js b/solutions/add-on/share-macro/Code.js index 33bf095ce..76b739712 100644 --- a/solutions/add-on/share-macro/Code.js +++ b/solutions/add-on/share-macro/Code.js @@ -25,139 +25,139 @@ limitations under the License. * @param {string} targetSpreadsheetUrl - URL if the target spreadsheet. */ 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); + // 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(); + // 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, - ); + // 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); + // 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(), + 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); - } - 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.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); + } + 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; - } - 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; - } - 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.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; + } + 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; + } + 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, - }; - const res = UrlFetchApp.fetch(url, options); - if (res.getResponseCode() === 200) { - return true; - } - console.log(`An error occurred updating content of script ${scriptId}`); - 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, + }; + const res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() === 200) { + return true; + } + console.log(`An error occurred updating content of script ${scriptId}`); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + }, }; diff --git a/solutions/add-on/share-macro/UI.js b/solutions/add-on/share-macro/UI.js index 297e1ad3a..0eceb8dc8 100644 --- a/solutions/add-on/share-macro/UI.js +++ b/solutions/add-on/share-macro/UI.js @@ -16,14 +16,14 @@ // Change application logo here (and in manifest) as desired. const ADDON_LOGO = - "https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png"; + "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); + return createSelectionCard(e); } /** @@ -37,120 +37,120 @@ function onHomepage(e) { * @return {CardService.Card} The card to show to the user for inputs. */ function createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors) { - // Configures card header. - const cardHeader = CardService.newCardHeader() - .setTitle("Share macros with other spreadheets!") - .setImageUrl(ADDON_LOGO) - .setImageStyle(CardService.ImageStyle.SQUARE); + // Configures card header. + const 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 form errors exist, configures section with error messages. + let showErrors = false; - if (errors?.length) { - showErrors = true; - let msg = errors.reduce((str, err) => `${str}• ${err}
    `, ""); - msg = `Form submission errors:
    ${msg}`; + if (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), - ); - } + // Builds error message section. + sectionErrors = CardService.newCardSection().addWidget( + CardService.newDecoratedText().setText(msg).setWrapText(true), + ); + } - // Configures source project section. - const sectionSource = CardService.newCardSection() - .addWidget( - CardService.newDecoratedText().setText( - "Source macro
    The Apps Script project to copy", - ), - ) + // Configures source project section. + const 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.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), - ), - ); + .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. - const sectionTarget = CardService.newCardSection() - .addWidget( - CardService.newDecoratedText().setText("Target spreadsheet"), - ) + // Configures target spreadsheet section. + const 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"), - ); + .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. - const sectionHelp = CardService.newCardSection() - .addWidget( - CardService.newDecoratedText() - .setText( - "NOTE: " + - "The Apps Script API must be turned on.", - ) - .setWrapText(true), - ) + // Configures help section. + const 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), - ), - ); + .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. - const cardFooter = CardService.newFixedFooter().setPrimaryButton( - CardService.newTextButton() - .setText("Share macro") - .setOnClickAction( - CardService.newAction().setFunctionName("onClickFunction_"), - ), - ); + // Configures card footer with action to copy the macro. + const cardFooter = CardService.newFixedFooter().setPrimaryButton( + CardService.newTextButton() + .setText("Share macro") + .setOnClickAction( + CardService.newAction().setFunctionName("onClickFunction_"), + ), + ); - // Begins building the card. - const builder = CardService.newCardBuilder().setHeader(cardHeader); + // Begins building the card. + const builder = CardService.newCardBuilder().setHeader(cardHeader); - // Adds error section if applicable. - if (showErrors) { - builder.addSection(sectionErrors); - } + // Adds error section if applicable. + if (showErrors) { + builder.addSection(sectionErrors); + } - // Adds final sections & footer. - builder - .addSection(sectionSource) - .addSection(sectionTarget) - .addSection(sectionHelp) - .setFixedFooter(cardFooter); + // Adds final sections & footer. + builder + .addSection(sectionSource) + .addSection(sectionTarget) + .addSection(sectionHelp) + .setFixedFooter(cardFooter); - return builder.build(); + return builder.build(); } /** @@ -162,45 +162,45 @@ function createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors) { * @return {CardService.Card} Responds with either a success or error card. */ function onClickFunction_(e) { - const sourceScriptId = e.formInput.sourceScriptId; - const targetSpreadsheetUrl = e.formInput.targetSpreadsheetUrl; + const sourceScriptId = e.formInput.sourceScriptId; + const targetSpreadsheetUrl = e.formInput.targetSpreadsheetUrl; - // Validates inputs for errors. - const errors = []; + // 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 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"); - } + // 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?.length) { - // Redisplays form if inputs are missing or invalid. - return createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors); - } - // Calls shareMacro function to copy the project. - shareMacro_(sourceScriptId, targetSpreadsheetUrl); + if (errors?.length) { + // Redisplays form if inputs are missing or invalid. + return createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors); + } + // Calls shareMacro function to copy the project. + shareMacro_(sourceScriptId, targetSpreadsheetUrl); - // Creates a success card to display to users. - return buildSuccessCard(e, targetSpreadsheetUrl); + // Creates a success card to display to users. + return buildSuccessCard(e, targetSpreadsheetUrl); } /** @@ -211,57 +211,57 @@ function onClickFunction_(e) { * * @return {CardService.Card} Returns success card. */ function buildSuccessCard(e, targetSpreadsheetUrl) { - // Configures card header. - const cardHeader = CardService.newCardHeader() - .setTitle("Share macros with other spreadsheets!") - .setImageUrl(ADDON_LOGO) - .setImageStyle(CardService.ImageStyle.SQUARE); + // Configures card header. + const 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. - const 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), - ), - ); - const 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 card body section with success message and open button. + const 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), + ), + ); + const 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. - const cardFooter = CardService.newFixedFooter().setPrimaryButton( - CardService.newTextButton() - .setText("Share another") - .setOnClickAction(CardService.newAction().setFunctionName("onHomepage")), - ); + // Configures the card footer with action to start new process. + const cardFooter = CardService.newFixedFooter().setPrimaryButton( + CardService.newTextButton() + .setText("Share another") + .setOnClickAction(CardService.newAction().setFunctionName("onHomepage")), + ); - const builder = CardService.newCardBuilder() - .setHeader(cardHeader) - .addSection(sectionBody1) - .addSection(sectionBody2) - .setFixedFooter(cardFooter); + const builder = CardService.newCardBuilder() + .setHeader(cardHeader) + .addSection(sectionBody1) + .addSection(sectionBody2) + .setFixedFooter(cardFooter); - return builder.build(); + return builder.build(); } diff --git a/solutions/add-on/share-macro/appsscript.json b/solutions/add-on/share-macro/appsscript.json index 419507996..e70dfe2cb 100644 --- a/solutions/add-on/share-macro/appsscript.json +++ b/solutions/add-on/share-macro/appsscript.json @@ -1,26 +1,26 @@ { - "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": {} - } + "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": {} + } } diff --git a/solutions/attendance-chat-app/final/appsscript.json b/solutions/attendance-chat-app/final/appsscript.json index c42874c42..434f00b3e 100644 --- a/solutions/attendance-chat-app/final/appsscript.json +++ b/solutions/attendance-chat-app/final/appsscript.json @@ -1,13 +1,13 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Gmail", - "serviceId": "gmail", - "version": "v1" - } - ] - }, - "chat": {} + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "serviceId": "gmail", + "version": "v1" + } + ] + }, + "chat": {} } diff --git a/solutions/attendance-chat-app/step-3/appsscript.json b/solutions/attendance-chat-app/step-3/appsscript.json index 205a036c2..02005f501 100644 --- a/solutions/attendance-chat-app/step-3/appsscript.json +++ b/solutions/attendance-chat-app/step-3/appsscript.json @@ -1,5 +1,5 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": {}, - "chat": {} + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "chat": {} } diff --git a/solutions/attendance-chat-app/step-4/appsscript.json b/solutions/attendance-chat-app/step-4/appsscript.json index 205a036c2..02005f501 100644 --- a/solutions/attendance-chat-app/step-4/appsscript.json +++ b/solutions/attendance-chat-app/step-4/appsscript.json @@ -1,5 +1,5 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": {}, - "chat": {} + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "chat": {} } diff --git a/solutions/attendance-chat-app/step-5/appsscript.json b/solutions/attendance-chat-app/step-5/appsscript.json index 205a036c2..02005f501 100644 --- a/solutions/attendance-chat-app/step-5/appsscript.json +++ b/solutions/attendance-chat-app/step-5/appsscript.json @@ -1,5 +1,5 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": {}, - "chat": {} + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "chat": {} } diff --git a/solutions/attendance-chat-app/step-6/appsscript.json b/solutions/attendance-chat-app/step-6/appsscript.json index c42874c42..434f00b3e 100644 --- a/solutions/attendance-chat-app/step-6/appsscript.json +++ b/solutions/attendance-chat-app/step-6/appsscript.json @@ -1,13 +1,13 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Gmail", - "serviceId": "gmail", - "version": "v1" - } - ] - }, - "chat": {} + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "serviceId": "gmail", + "version": "v1" + } + ] + }, + "chat": {} } diff --git a/solutions/automations/agenda-maker/Code.js b/solutions/automations/agenda-maker/Code.js index c24172dd2..f507d64ca 100644 --- a/solutions/automations/agenda-maker/Code.js +++ b/solutions/automations/agenda-maker/Code.js @@ -23,90 +23,90 @@ limitations under the License. * @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()) { - const 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 - const folder = DriveApp.createFolder("Agenda Maker - App"); - folder.setDescription("Apps Script App - Do not change this description"); - return folder.getId(); + const folders = DriveApp.getFoldersByName("Agenda Maker - App"); + // Finds the folder if it exists + while (folders.hasNext()) { + const 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 + const 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(); + 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(); } /** @@ -115,81 +115,81 @@ function getTemplateId(folderId) { * */ 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 (const i in event.getGuestList()) { - const 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 (const i in event.getGuestList()) { - const guest = event.getGuestList()[i]; - - newDoc.addEditor(guest.getEmail()); - } - } - } - return; + // 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 (const i in event.getGuestList()) { + const 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 (const i in event.getGuestList()) { + const 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() { - const email = Session.getActiveUser().getEmail(); - ScriptApp.newTrigger("onCalendarChange") - .forUserCalendar(email) - .onEventUpdated() - .create(); + const email = Session.getActiveUser().getEmail(); + ScriptApp.newTrigger("onCalendarChange") + .forUserCalendar(email) + .onEventUpdated() + .create(); } diff --git a/solutions/automations/agenda-maker/appsscript.json b/solutions/automations/agenda-maker/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/agenda-maker/appsscript.json +++ b/solutions/automations/agenda-maker/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/aggregate-document-content/Code.js b/solutions/automations/aggregate-document-content/Code.js index f5b45c15e..27e1a3536 100644 --- a/solutions/automations/aggregate-document-content/Code.js +++ b/solutions/automations/aggregate-document-content/Code.js @@ -39,80 +39,80 @@ const TEXT_COLOR = "#2e7d32"; // Color applied to heading after import to avoid * 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}'. + // 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. - const noContentList = []; - let numUpdates = 0; - for (const 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; - } - 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 (const file of noContentList) { - msg += `\n ${file}`; - } - } - ui.alert(APP_TITLE, msg, ui.ButtonSet.OK); + 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. + const noContentList = []; + let numUpdates = 0; + for (const 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; + } + 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 (const file of noContentList) { + msg += `\n ${file}`; + } + } + ui.alert(APP_TITLE, msg, ui.ButtonSet.OK); } /** @@ -122,46 +122,46 @@ function performImport() { * Called from performImport(). */ function getContent(body) { - // Finds the heading paragraph with matching style, keywords and !color. - let 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 (true) { - searchResult = body.findElement(searchType, searchResult); - if (!searchResult) { - break; - } - - const par = searchResult.getElement().asParagraph(); - if (par.getHeading() === searchHeading) { - // If heading style matches, searches for text string (case insensitive). - const 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; - } - // Updates the heading color to indicate that the summary has been imported. - const style = {}; - style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR; - parValidHeading.setAttributes(style); - parValidHeading.appendText(" [Exported]"); - - // Gets the content from the table following the valid heading. - const elemObj = parValidHeading.getNextSibling().asTable(); - const content = elemObj.copy(); - - return content; + // Finds the heading paragraph with matching style, keywords and !color. + let 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 (true) { + searchResult = body.findElement(searchType, searchResult); + if (!searchResult) { + break; + } + + const par = searchResult.getElement().asParagraph(); + if (par.getHeading() === searchHeading) { + // If heading style matches, searches for text string (case insensitive). + const 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; + } + // Updates the heading color to indicate that the summary has been imported. + const style = {}; + style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR; + parValidHeading.setAttributes(style); + parValidHeading.appendText(" [Exported]"); + + // Gets the content from the table following the valid heading. + const elemObj = parValidHeading.getNextSibling().asTable(); + const content = elemObj.copy(); + + return content; } /** @@ -170,12 +170,12 @@ function getContent(body) { * Called from function performImport(). */ function getFiles(folder) { - // Only gets Docs files. - const files = folder.getFilesByType(MimeType.GOOGLE_DOCS); - const docIDs = []; - while (files.hasNext()) { - const file = files.next(); - docIDs.push(file.getId()); - } - return docIDs; + // Only gets Docs files. + const files = folder.getFilesByType(MimeType.GOOGLE_DOCS); + const docIDs = []; + while (files.hasNext()) { + const file = files.next(); + docIDs.push(file.getId()); + } + return docIDs; } diff --git a/solutions/automations/aggregate-document-content/Menu.js b/solutions/automations/aggregate-document-content/Menu.js index ee69f00c1..dcd944b8b 100644 --- a/solutions/automations/aggregate-document-content/Menu.js +++ b/solutions/automations/aggregate-document-content/Menu.js @@ -19,32 +19,32 @@ */ // 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", + 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(); + 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(); } /** @@ -52,11 +52,11 @@ function onOpen() { * TODO: Personalize */ function aboutApp() { - const msg = ` + const msg = ` ${APP_TITLE} Version: 1.0 Contact: `; - const ui = DocumentApp.getUi(); - ui.alert("About this application", msg, ui.ButtonSet.OK); + const ui = DocumentApp.getUi(); + ui.alert("About this application", msg, ui.ButtonSet.OK); } diff --git a/solutions/automations/aggregate-document-content/Setup.js b/solutions/automations/aggregate-document-content/Setup.js index c87765446..a61225113 100644 --- a/solutions/automations/aggregate-document-content/Setup.js +++ b/solutions/automations/aggregate-document-content/Setup.js @@ -26,23 +26,23 @@ * @param {boolean} includeSamples - Optional, if true creates samples files. * */ function setupConfig(includeSamples) { - // Gets folder to store documents in. - const folder = getFolderByName_(PROJECT_FOLDER_NAME); + // Gets folder to store documents in. + const folder = getFolderByName_(PROJECT_FOLDER_NAME); - let msg = `\nDrive Folder for Documents: '${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 (const 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 sample documents for testing. + // Remove sample document creation and add your own process as needed. + if (includeSamples) { + let filesCreated = 0; + for (const 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); } /** @@ -52,43 +52,43 @@ function setupConfig(includeSamples) { * 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(); + // 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}' + 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); + // 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); } /** @@ -97,7 +97,7 @@ function createSampleFile() { * Called from menu | Calls setupConfig with option set to true. */ function setupWithSamples() { - setupConfig(true); + setupConfig(true); } /** @@ -105,29 +105,29 @@ function setupWithSamples() { * {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.', + 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.', }; /** @@ -138,46 +138,46 @@ const 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; + // 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; } diff --git a/solutions/automations/aggregate-document-content/Utilities.js b/solutions/automations/aggregate-document-content/Utilities.js index a9cf3bfbc..629d527a0 100644 --- a/solutions/automations/aggregate-document-content/Utilities.js +++ b/solutions/automations/aggregate-document-content/Utilities.js @@ -26,26 +26,26 @@ * @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(); + // 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()) { - const folder = subFolders.next(); + // Iterates subfolders to check if folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + const 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`, - ); + // 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`, + ); } /** @@ -53,12 +53,12 @@ function getFolderByName_(folderName) { * @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); + // 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); + 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); } diff --git a/solutions/automations/aggregate-document-content/appsscript.json b/solutions/automations/aggregate-document-content/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/aggregate-document-content/appsscript.json +++ b/solutions/automations/aggregate-document-content/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/bracket-maker/Code.js b/solutions/automations/bracket-maker/Code.js index e27a093c8..2e2dc3cd9 100644 --- a/solutions/automations/bracket-maker/Code.js +++ b/solutions/automations/bracket-maker/Code.js @@ -26,97 +26,97 @@ const CONNECTOR_WIDTH = 15; * Adds a custom menu item to run the script. */ function onOpen() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - ss.addMenu("Bracket maker", [ - { name: "Create bracket", functionName: "createBracket" }, - ]); + const 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() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - let rangePlayers = ss.getRangeByName(RANGE_PLAYER1); - const sheetControl = ss.getSheetByName(SHEET_PLAYERS); - const 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. - const countNodesUpperBound = 2 ** upperPower; - - // Calculates the number that is a power of 2 and higher than numPlayers. - const countNodesLowerBound = countNodesUpperBound / 2; - - // Determines the number of nodes that will not show in the 1st level. - const countNodesHidden = numPlayers - countNodesLowerBound; - - // Enters the players for the 1st round. - const currentPlayer = 0; - for (let i = 0; i < countNodesLowerBound; i++) { - if (i < countNodesHidden) { - // Must be on the first level - const 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++) { - const pow1 = 2 ** (i + 1); - const pow2 = 2 ** (i + 2); - const pow3 = 2 ** (i + 3); - for (let j = 0; j < 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), - ); - } - } + const ss = SpreadsheetApp.getActiveSpreadsheet(); + let rangePlayers = ss.getRangeByName(RANGE_PLAYER1); + const sheetControl = ss.getSheetByName(SHEET_PLAYERS); + const 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. + const countNodesUpperBound = 2 ** upperPower; + + // Calculates the number that is a power of 2 and higher than numPlayers. + const countNodesLowerBound = countNodesUpperBound / 2; + + // Determines the number of nodes that will not show in the 1st level. + const countNodesHidden = numPlayers - countNodesLowerBound; + + // Enters the players for the 1st round. + const currentPlayer = 0; + for (let i = 0; i < countNodesLowerBound; i++) { + if (i < countNodesHidden) { + // Must be on the first level + const 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++) { + const pow1 = 2 ** (i + 1); + const pow2 = 2 ** (i + 2); + const pow3 = 2 ** (i + 3); + for (let j = 0; j < 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), + ); + } + } } /** @@ -125,11 +125,11 @@ function createBracket() { * @param {string[]} players The list of players. */ function setBracketItem_(rng, players) { - if (players) { - const rand = Math.ceil(Math.random() * players.length); - rng.setValue(players.splice(rand - 1, 1)[0][0]); - } - rng.setBackgroundColor("yellow"); + if (players) { + const rand = Math.ceil(Math.random() * players.length); + rng.setValue(players.splice(rand - 1, 1)[0][0]); + } + rng.setBackgroundColor("yellow"); } /** @@ -138,6 +138,6 @@ function setBracketItem_(rng, players) { * @param {Range} rng The spreadsheet range. */ function setConnector_(sheet, rng) { - sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH); - rng.setBackgroundColor("green"); + sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH); + rng.setBackgroundColor("green"); } diff --git a/solutions/automations/bracket-maker/appsscript.json b/solutions/automations/bracket-maker/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/bracket-maker/appsscript.json +++ b/solutions/automations/bracket-maker/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/calendar-timesheet/Code.js b/solutions/automations/calendar-timesheet/Code.js index 9ef436d27..51d06b0b3 100644 --- a/solutions/automations/calendar-timesheet/Code.js +++ b/solutions/automations/calendar-timesheet/Code.js @@ -22,146 +22,146 @@ limitations under the License. * to the spreadsheet menu */ const onOpen = () => { - SpreadsheetApp.getUi() - .createMenu("myTime") - .addItem("Sync calendar events", "run") - .addItem("Settings", "settings") - .addToUi(); + 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"); + const html = + HtmlService.createHtmlOutputFromFile("Page").setTitle("Settings"); - SpreadsheetApp.getUi().showSidebar(html); + 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 = []; - for (const savedCalendarSetting of savedCalendarSettings) { - 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; + 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 = []; + for (const savedCalendarSetting of savedCalendarSettings) { + 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"; + 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 = () => { - myTime({ - mainSpreadsheetId: SpreadsheetApp.getActiveSpreadsheet().getId(), - }).run(); + myTime({ + mainSpreadsheetId: SpreadsheetApp.getActiveSpreadsheet().getId(), + }).run(); }; /** @@ -170,277 +170,277 @@ const run = () => { * @return {Object} The myTime Object. */ const myTime = (par) => { - /** - * 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, - }); + /** + * 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, + }); }; diff --git a/solutions/automations/calendar-timesheet/appsscript.json b/solutions/automations/calendar-timesheet/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/calendar-timesheet/appsscript.json +++ b/solutions/automations/calendar-timesheet/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/content-signup/Code.js b/solutions/automations/content-signup/Code.js index 95c493d88..0a351d285 100644 --- a/solutions/automations/content-signup/Code.js +++ b/solutions/automations/content-signup/Code.js @@ -20,30 +20,30 @@ 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"; + "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", + "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(); + ScriptApp.newTrigger("onFormSubmit") + .forSpreadsheet(SpreadsheetApp.getActive()) + .onFormSubmit() + .create(); } /** @@ -52,43 +52,43 @@ function installTrigger() { * @param {Object} event - Form submit event */ function onFormSubmit(e) { - const 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. - const timestamp = responses.Timestamp[0]; - const email = responses["Email address"][0].trim(); - const name = responses.Name[0].trim(); - const 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). - const topics = Object.keys(topicUrls).filter((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. - const sheet = SpreadsheetApp.getActiveSheet(); - const row = sheet.getActiveRange().getRow(); - const column = e.values.length + 1; - sheet.getRange(row, column).setValue(status); - - console.log(`status=${status}; responses=${JSON.stringify(responses)}`); + const 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. + const timestamp = responses.Timestamp[0]; + const email = responses["Email address"][0].trim(); + const name = responses.Name[0].trim(); + const 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). + const topics = Object.keys(topicUrls).filter((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. + const sheet = SpreadsheetApp.getActiveSheet(); + const row = sheet.getActiveRange().getRow(); + const column = e.values.length + 1; + sheet.getRange(row, column).setValue(status); + + console.log(`status=${status}; responses=${JSON.stringify(responses)}`); } /** @@ -99,20 +99,20 @@ function onFormSubmit(e) { * @return {string} - The email body as an HTML string. */ function createEmailBody(name, topics) { - let topicsHtml = topics - .map((topic) => { - const url = topicUrls[topic]; - return `
  • ${topic}
  • `; - }) - .join(""); - topicsHtml = `
      ${topicsHtml}
    `; - - // Make sure to update the emailTemplateDocId at the top. - const 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; + let topicsHtml = topics + .map((topic) => { + const url = topicUrls[topic]; + return `
  • ${topic}
  • `; + }) + .join(""); + topicsHtml = `
      ${topicsHtml}
    `; + + // Make sure to update the emailTemplateDocId at the top. + const 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; } /** @@ -122,12 +122,12 @@ function createEmailBody(name, topics) { * @return {string} The Google Doc rendered as an HTML string. */ function docToHtml(docId) { - // Downloads a Google Doc as an HTML string. - const url = `https://docs.google.com/feeds/download/documents/export/Export?id=${docId}&exportFormat=html`; - const param = { - method: "get", - headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, - muteHttpExceptions: true, - }; - return UrlFetchApp.fetch(url, param).getContentText(); + // Downloads a Google Doc as an HTML string. + const url = `https://docs.google.com/feeds/download/documents/export/Export?id=${docId}&exportFormat=html`; + const param = { + method: "get", + headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, + muteHttpExceptions: true, + }; + return UrlFetchApp.fetch(url, param).getContentText(); } diff --git a/solutions/automations/content-signup/appsscript.json b/solutions/automations/content-signup/appsscript.json index 668c2f68f..5e67304ad 100644 --- a/solutions/automations/content-signup/appsscript.json +++ b/solutions/automations/content-signup/appsscript.json @@ -1,14 +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" + "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" } diff --git a/solutions/automations/course-feedback-response/Code.js b/solutions/automations/course-feedback-response/Code.js index 3ab733d18..f158a970d 100644 --- a/solutions/automations/course-feedback-response/Code.js +++ b/solutions/automations/course-feedback-response/Code.js @@ -21,20 +21,20 @@ limitations under the License. * Creates custom menu for user to run scripts. */ function onOpen() { - const ui = SpreadsheetApp.getUi(); - ui.createMenu("Form Reply Tool") - .addItem("Enable auto draft replies", "installTrigger") - .addToUi(); + const 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(); + ScriptApp.newTrigger("onFormSubmit") + .forSpreadsheet(SpreadsheetApp.getActive()) + .onFormSubmit() + .create(); } /** @@ -43,17 +43,17 @@ function installTrigger() { * @param {Object} event - Form submit event */ function onFormSubmit(e) { - const responses = e.namedValues; + const responses = e.namedValues; - // parse form response data - const timestamp = responses.Timestamp[0]; - const email = responses["Email address"][0].trim(); + // parse form response data + const timestamp = responses.Timestamp[0]; + const email = responses["Email address"][0].trim(); - // create email body - const emailBody = createEmailBody(responses); + // create email body + const emailBody = createEmailBody(responses); - // create draft email - createDraft(timestamp, email, emailBody); + // create draft email + createDraft(timestamp, email, emailBody); } /** @@ -63,20 +63,20 @@ function onFormSubmit(e) { * @return {string} - The email body as an HTML string */ function createEmailBody(responses) { - // parse form response data - const name = responses.Name[0].trim(); - const industry = responses["What industry do you work in?"][0]; - const source = responses["How did you find out about this course?"][0]; - const rating = - responses["On a scale of 1 - 5 how would you rate this course?"][0]; - const productFeedback = - responses["What could be different to make it a 5 rating?"][0]; - const otherFeedback = responses["Any other feedback?"][0]; - - // create email body - const 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; + // parse form response data + const name = responses.Name[0].trim(); + const industry = responses["What industry do you work in?"][0]; + const source = responses["How did you find out about this course?"][0]; + const rating = + responses["On a scale of 1 - 5 how would you rate this course?"][0]; + const productFeedback = + responses["What could be different to make it a 5 rating?"][0]; + const otherFeedback = responses["Any other feedback?"][0]; + + // create email body + const 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; } /** @@ -87,13 +87,13 @@ function createEmailBody(responses) { * @param {string} emailBody The email body as an HTML string */ function createDraft(timestamp, email, emailBody) { - console.log("draft email create process started"); + console.log("draft email create process started"); - // create subject line - const subjectLine = `Thanks for your course feedback! ${timestamp}`; + // create subject line + const subjectLine = `Thanks for your course feedback! ${timestamp}`; - // create draft email - GmailApp.createDraft(email, subjectLine, "", { - htmlBody: emailBody, - }); + // create draft email + GmailApp.createDraft(email, subjectLine, "", { + htmlBody: emailBody, + }); } diff --git a/solutions/automations/course-feedback-response/appsscript.json b/solutions/automations/course-feedback-response/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/course-feedback-response/appsscript.json +++ b/solutions/automations/course-feedback-response/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/employee-certificate/Code.js b/solutions/automations/employee-certificate/Code.js index d9a0a5ba1..76cb52c8d 100644 --- a/solutions/automations/employee-certificate/Code.js +++ b/solutions/automations/employee-certificate/Code.js @@ -25,12 +25,12 @@ const tempFolderId = "FOLDER_ID"; // Create an empty folder in Google Drive * with drop-down options to create and send certificates */ function onOpen() { - const ui = SpreadsheetApp.getUi(); - ui.createMenu("Appreciation") - .addItem("Create certificates", "createCertificates") - .addSeparator() - .addItem("Send certificates", "sendCertificates") - .addToUi(); + const ui = SpreadsheetApp.getUi(); + ui.createMenu("Appreciation") + .addItem("Create certificates", "createCertificates") + .addSeparator() + .addItem("Send certificates", "sendCertificates") + .addToUi(); } /** @@ -38,55 +38,55 @@ function onOpen() { * and stores every individual Slides doc on Google Drive */ function createCertificates() { - // Load the Google Slide template file - const template = DriveApp.getFileById(slideTemplateId); - - // Get all employee data from the spreadsheet and identify the headers - const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); - const values = sheet.getDataRange().getValues(); - const headers = values[0]; - const empNameIndex = headers.indexOf("Employee Name"); - const dateIndex = headers.indexOf("Date"); - const managerNameIndex = headers.indexOf("Manager Name"); - const titleIndex = headers.indexOf("Title"); - const compNameIndex = headers.indexOf("Company Name"); - const empEmailIndex = headers.indexOf("Employee Email"); - const empSlideIndex = headers.indexOf("Employee Slide"); - const statusIndex = headers.indexOf("Status"); - - // Iterate through each row to capture individual details - for (let i = 1; i < values.length; i++) { - const rowData = values[i]; - const empName = rowData[empNameIndex]; - const date = rowData[dateIndex]; - const managerName = rowData[managerNameIndex]; - const title = rowData[titleIndex]; - const compName = rowData[compNameIndex]; - - // Make a copy of the Slide template and rename it with employee name - const tempFolder = DriveApp.getFolderById(tempFolderId); - const empSlideId = template.makeCopy(tempFolder).setName(empName).getId(); - const empSlide = SlidesApp.openById(empSlideId).getSlides()[0]; - - // Replace placeholder values with actual employee related details - empSlide.replaceAllText("Employee Name", empName); - empSlide.replaceAllText( - "Date", - `Date: ${Utilities.formatDate( - date, - Session.getScriptTimeZone(), - "MMMM dd, yyyy", - )}`, - ); - empSlide.replaceAllText("Your Name", managerName); - empSlide.replaceAllText("Title", title); - empSlide.replaceAllText("Company Name", compName); - - // Update the spreadsheet with the new Slide Id and status - sheet.getRange(i + 1, empSlideIndex + 1).setValue(empSlideId); - sheet.getRange(i + 1, statusIndex + 1).setValue("CREATED"); - SpreadsheetApp.flush(); - } + // Load the Google Slide template file + const template = DriveApp.getFileById(slideTemplateId); + + // Get all employee data from the spreadsheet and identify the headers + const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); + const values = sheet.getDataRange().getValues(); + const headers = values[0]; + const empNameIndex = headers.indexOf("Employee Name"); + const dateIndex = headers.indexOf("Date"); + const managerNameIndex = headers.indexOf("Manager Name"); + const titleIndex = headers.indexOf("Title"); + const compNameIndex = headers.indexOf("Company Name"); + const empEmailIndex = headers.indexOf("Employee Email"); + const empSlideIndex = headers.indexOf("Employee Slide"); + const statusIndex = headers.indexOf("Status"); + + // Iterate through each row to capture individual details + for (let i = 1; i < values.length; i++) { + const rowData = values[i]; + const empName = rowData[empNameIndex]; + const date = rowData[dateIndex]; + const managerName = rowData[managerNameIndex]; + const title = rowData[titleIndex]; + const compName = rowData[compNameIndex]; + + // Make a copy of the Slide template and rename it with employee name + const tempFolder = DriveApp.getFolderById(tempFolderId); + const empSlideId = template.makeCopy(tempFolder).setName(empName).getId(); + const empSlide = SlidesApp.openById(empSlideId).getSlides()[0]; + + // Replace placeholder values with actual employee related details + empSlide.replaceAllText("Employee Name", empName); + empSlide.replaceAllText( + "Date", + `Date: ${Utilities.formatDate( + date, + Session.getScriptTimeZone(), + "MMMM dd, yyyy", + )}`, + ); + empSlide.replaceAllText("Your Name", managerName); + empSlide.replaceAllText("Title", title); + empSlide.replaceAllText("Company Name", compName); + + // Update the spreadsheet with the new Slide Id and status + sheet.getRange(i + 1, empSlideIndex + 1).setValue(empSlideId); + sheet.getRange(i + 1, statusIndex + 1).setValue("CREATED"); + SpreadsheetApp.flush(); + } } /** @@ -94,44 +94,44 @@ function createCertificates() { * with a PDF attachment of their appreciation certificate */ function sendCertificates() { - // Get all employee data from the spreadsheet and identify the headers - const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); - const values = sheet.getDataRange().getValues(); - const headers = values[0]; - const empNameIndex = headers.indexOf("Employee Name"); - const dateIndex = headers.indexOf("Date"); - const managerNameIndex = headers.indexOf("Manager Name"); - const titleIndex = headers.indexOf("Title"); - const compNameIndex = headers.indexOf("Company Name"); - const empEmailIndex = headers.indexOf("Employee Email"); - const empSlideIndex = headers.indexOf("Employee Slide"); - const statusIndex = headers.indexOf("Status"); - - // Iterate through each row to capture individual details - for (let i = 1; i < values.length; i++) { - const rowData = values[i]; - const empName = rowData[empNameIndex]; - const date = rowData[dateIndex]; - const managerName = rowData[managerNameIndex]; - const title = rowData[titleIndex]; - const compName = rowData[compNameIndex]; - const empSlideId = rowData[empSlideIndex]; - const empEmail = rowData[empEmailIndex]; - - // Load the employee's personalized Google Slide file - const attachment = DriveApp.getFileById(empSlideId); - - // Setup the required parameters and send them the email - const senderName = "CertBot"; - const subject = `${empName}, you're awesome!`; - const body = `Please find your employee appreciation certificate attached.\n\n${compName} team`; - GmailApp.sendEmail(empEmail, subject, body, { - attachments: [attachment.getAs(MimeType.PDF)], - name: senderName, - }); - - // Update the spreadsheet with email status - sheet.getRange(i + 1, statusIndex + 1).setValue("SENT"); - SpreadsheetApp.flush(); - } + // Get all employee data from the spreadsheet and identify the headers + const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); + const values = sheet.getDataRange().getValues(); + const headers = values[0]; + const empNameIndex = headers.indexOf("Employee Name"); + const dateIndex = headers.indexOf("Date"); + const managerNameIndex = headers.indexOf("Manager Name"); + const titleIndex = headers.indexOf("Title"); + const compNameIndex = headers.indexOf("Company Name"); + const empEmailIndex = headers.indexOf("Employee Email"); + const empSlideIndex = headers.indexOf("Employee Slide"); + const statusIndex = headers.indexOf("Status"); + + // Iterate through each row to capture individual details + for (let i = 1; i < values.length; i++) { + const rowData = values[i]; + const empName = rowData[empNameIndex]; + const date = rowData[dateIndex]; + const managerName = rowData[managerNameIndex]; + const title = rowData[titleIndex]; + const compName = rowData[compNameIndex]; + const empSlideId = rowData[empSlideIndex]; + const empEmail = rowData[empEmailIndex]; + + // Load the employee's personalized Google Slide file + const attachment = DriveApp.getFileById(empSlideId); + + // Setup the required parameters and send them the email + const senderName = "CertBot"; + const subject = `${empName}, you're awesome!`; + const body = `Please find your employee appreciation certificate attached.\n\n${compName} team`; + GmailApp.sendEmail(empEmail, subject, body, { + attachments: [attachment.getAs(MimeType.PDF)], + name: senderName, + }); + + // Update the spreadsheet with email status + sheet.getRange(i + 1, statusIndex + 1).setValue("SENT"); + SpreadsheetApp.flush(); + } } diff --git a/solutions/automations/employee-certificate/appsscript.json b/solutions/automations/employee-certificate/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/employee-certificate/appsscript.json +++ b/solutions/automations/employee-certificate/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/equipment-requests/Code.js b/solutions/automations/equipment-requests/Code.js index 5d8cb0fd5..d48733ddf 100644 --- a/solutions/automations/equipment-requests/Code.js +++ b/solutions/automations/equipment-requests/Code.js @@ -22,17 +22,17 @@ 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)', + '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)", + "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"']; @@ -42,61 +42,61 @@ const AVAILABLE_MONITORS = ['Single 27"', 'Single 32"', 'Dual 24"']; * Adds a custom menu to the spreadsheet. */ function onOpen() { - SpreadsheetApp.getUi() - .createMenu("Equipment requests") - .addItem("Set up", "setup_") - .addItem("Clean up", "cleanup_") - .addToUi(); + SpreadsheetApp.getUi() + .createMenu("Equipment requests") + .addItem("Set up", "setup_") + .addItem("Clean up", "cleanup_") + .addToUi(); } /** * Creates the form and triggers for the workflow. */ function setup_() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - if (ss.getFormUrl()) { - const msg = "Form already exists. Unlink the form and try again."; - SpreadsheetApp.getUi().alert(msg); - return; - } - const 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); + const ss = SpreadsheetApp.getActiveSpreadsheet(); + if (ss.getFormUrl()) { + const msg = "Form already exists. Unlink the form and try again."; + SpreadsheetApp.getUi().alert(msg); + return; + } + const 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. - for (const sheet of ss.getSheets()) { - 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(); + // Hide the raw form responses. + for (const sheet of ss.getSheets()) { + 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_() { - const formUrl = SpreadsheetApp.getActiveSpreadsheet().getFormUrl(); - if (!formUrl) { - return; - } - for (const trigger of ScriptApp.getProjectTriggers()) { - ScriptApp.deleteTrigger(trigger); - } - FormApp.openByUrl(formUrl).deleteAllResponses().setAcceptingResponses(false); + const formUrl = SpreadsheetApp.getActiveSpreadsheet().getFormUrl(); + if (!formUrl) { + return; + } + for (const trigger of ScriptApp.getProjectTriggers()) { + ScriptApp.deleteTrigger(trigger); + } + FormApp.openByUrl(formUrl).deleteAllResponses().setAcceptingResponses(false); } /** @@ -105,26 +105,26 @@ function cleanup_() { * @param {Object} event - Form submit event */ function onFormSubmit_(event) { - const response = mapResponse_(event.response); - sendNewEquipmentRequestEmail_(response); - const equipmentDetails = Utilities.formatString( - "%s\n%s\n%s", - response.Laptop, - response.Desktop, - response.Monitor, - ); - const row = [ - "New", - "", - response["Due date"], - response["Employee name"], - response["Desk location"], - equipmentDetails, - response.email, - ]; - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const sheet = ss.getSheetByName("Pending requests"); - sheet.appendRow(row); + const response = mapResponse_(event.response); + sendNewEquipmentRequestEmail_(response); + const equipmentDetails = Utilities.formatString( + "%s\n%s\n%s", + response.Laptop, + response.Desktop, + response.Monitor, + ); + const row = [ + "New", + "", + response["Due date"], + response["Employee name"], + response["Desk location"], + equipmentDetails, + response.email, + ]; + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheet = ss.getSheetByName("Pending requests"); + sheet.appendRow(row); } /** @@ -134,24 +134,24 @@ function onFormSubmit_(event) { * @param {Object} event */ function processCompletedItems_() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const pending = ss.getSheetByName("Pending requests"); - const completed = ss.getSheetByName("Completed requests"); - const rows = pending.getDataRange().getValues(); - for (let i = rows.length; i >= 2; i--) { - const row = rows[i - 1]; - const 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], - }); - } - } + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const pending = ss.getSheetByName("Pending requests"); + const completed = ss.getSheetByName("Completed requests"); + const rows = pending.getDataRange().getValues(); + for (let i = rows.length; i >= 2; i--) { + const row = rows[i - 1]; + const 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], + }); + } + } } /** @@ -160,17 +160,17 @@ function processCompletedItems_() { * @param {Object} request - Request details */ function sendNewEquipmentRequestEmail_(request) { - const template = HtmlService.createTemplateFromFile( - "new-equipment-request.html", - ); - template.request = request; - template.sheetUrl = SpreadsheetApp.getActiveSpreadsheet().getUrl(); - const msg = template.evaluate(); - MailApp.sendEmail({ - to: REQUEST_NOTIFICATION_EMAIL, - subject: "New equipment request", - htmlBody: msg.getContent(), - }); + const template = HtmlService.createTemplateFromFile( + "new-equipment-request.html", + ); + template.request = request; + template.sheetUrl = SpreadsheetApp.getActiveSpreadsheet().getUrl(); + const msg = template.evaluate(); + MailApp.sendEmail({ + to: REQUEST_NOTIFICATION_EMAIL, + subject: "New equipment request", + htmlBody: msg.getContent(), + }); } /** @@ -179,14 +179,14 @@ function sendNewEquipmentRequestEmail_(request) { * @param {Object} request - Request details */ function sendEquipmentRequestCompletedEmail_(request) { - const template = HtmlService.createTemplateFromFile("request-complete.html"); - template.request = request; - const msg = template.evaluate(); - MailApp.sendEmail({ - to: request.email, - subject: "Equipment request completed", - htmlBody: msg.getContent(), - }); + const template = HtmlService.createTemplateFromFile("request-complete.html"); + template.request = request; + const msg = template.evaluate(); + MailApp.sendEmail({ + to: request.email, + subject: "Equipment request completed", + htmlBody: msg.getContent(), + }); } /** @@ -197,13 +197,13 @@ function sendEquipmentRequestCompletedEmail_(request) { * @return {Object} Form values keyed by question title */ function mapResponse_(response) { - const initialValue = { - email: response.getRespondentEmail(), - timestamp: response.getTimestamp(), - }; - return response.getItemResponses().reduce((obj, itemResponse) => { - const key = itemResponse.getItem().getTitle(); - obj[key] = itemResponse.getResponse(); - return obj; - }, initialValue); + const initialValue = { + email: response.getRespondentEmail(), + timestamp: response.getTimestamp(), + }; + return response.getItemResponses().reduce((obj, itemResponse) => { + const key = itemResponse.getItem().getTitle(); + obj[key] = itemResponse.getResponse(); + return obj; + }, initialValue); } diff --git a/solutions/automations/equipment-requests/appsscript.json b/solutions/automations/equipment-requests/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/equipment-requests/appsscript.json +++ b/solutions/automations/equipment-requests/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/event-session-signup/Code.js b/solutions/automations/event-session-signup/Code.js index e03d99e63..ffaaa28ef 100644 --- a/solutions/automations/event-session-signup/Code.js +++ b/solutions/automations/event-session-signup/Code.js @@ -21,10 +21,10 @@ limitations under the License. * Inserts a custom menu when the spreadsheet opens. */ function onOpen() { - SpreadsheetApp.getUi() - .createMenu("Conference") - .addItem("Set up conference", "setUpConference_") - .addToUi(); + SpreadsheetApp.getUi() + .createMenu("Conference") + .addItem("Set up conference", "setUpConference_") + .addToUi(); } /** @@ -33,24 +33,24 @@ function onOpen() { * to react to form responses. */ function setUpConference_() { - const 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; - } - const ss = SpreadsheetApp.getActive(); - const sheet = ss.getSheetByName("Conference Setup"); - const range = sheet.getDataRange(); - const values = range.getValues(); - setUpCalendar_(values, range); - setUpForm_(ss, values); - ScriptApp.newTrigger("onFormSubmit") - .forSpreadsheet(ss) - .onFormSubmit() - .create(); + const 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; + } + const ss = SpreadsheetApp.getActive(); + const sheet = ss.getSheetByName("Conference Setup"); + const range = sheet.getDataRange(); + const values = range.getValues(); + setUpCalendar_(values, range); + setUpForm_(ss, values); + ScriptApp.newTrigger("onFormSubmit") + .forSpreadsheet(ss) + .onFormSubmit() + .create(); } /** @@ -60,24 +60,24 @@ function setUpConference_() { * @param {Range} range A spreadsheet range that contains conference data. */ function setUpCalendar_(values, range) { - const cal = CalendarApp.createCalendar("Conference Calendar"); - // Start at 1 to skip the header row. - for (let i = 1; i < values.length; i++) { - const session = values[i]; - const title = session[0]; - const start = joinDateAndTime_(session[1], session[2]); - const end = joinDateAndTime_(session[1], session[3]); - const options = { location: session[4], sendInvites: true }; - const 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. - const scriptProperties = PropertiesService.getScriptProperties(); - scriptProperties.setProperty("calId", cal.getId()); + const cal = CalendarApp.createCalendar("Conference Calendar"); + // Start at 1 to skip the header row. + for (let i = 1; i < values.length; i++) { + const session = values[i]; + const title = session[0]; + const start = joinDateAndTime_(session[1], session[2]); + const end = joinDateAndTime_(session[1], session[3]); + const options = { location: session[4], sendInvites: true }; + const 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. + const scriptProperties = PropertiesService.getScriptProperties(); + scriptProperties.setProperty("calId", cal.getId()); } /** @@ -88,10 +88,10 @@ function setUpCalendar_(values, range) { * @return {Date} A Date object representing the combined date and time. */ function joinDateAndTime_(date_, time) { - const processedDate = new Date(date_); - processedDate.setHours(time.getHours()); - processedDate.setMinutes(time.getMinutes()); - return processedDate; + const processedDate = new Date(date_); + processedDate.setHours(time.getHours()); + processedDate.setMinutes(time.getMinutes()); + return processedDate; } /** @@ -103,36 +103,36 @@ function joinDateAndTime_(date_, time) { * @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. - const schedule = {}; - // Start at 1 to skip the header row. - for (let i = 1; i < values.length; i++) { - const session = values[i]; - const day = session[1].toLocaleDateString(); - const 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. - const form = FormApp.create("Conference Form"); - form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()); - form.addTextItem().setTitle("Name").setRequired(true); - form.addTextItem().setTitle("Email").setRequired(true); - for (const day of Object.keys(schedule)) { - form.addSectionHeaderItem().setTitle(`Sessions for ${day}`); - for (const time of Object.keys(schedule[day])) { - form - .addMultipleChoiceItem() - .setTitle(`${time} ${day}`) - .setChoiceValues(schedule[day][time]); - } - } + // Group the sessions by date and time so that they can be passed to the form. + const schedule = {}; + // Start at 1 to skip the header row. + for (let i = 1; i < values.length; i++) { + const session = values[i]; + const day = session[1].toLocaleDateString(); + const 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. + const form = FormApp.create("Conference Form"); + form.setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()); + form.addTextItem().setTitle("Name").setRequired(true); + form.addTextItem().setTitle("Email").setRequired(true); + for (const day of Object.keys(schedule)) { + form.addSectionHeaderItem().setTitle(`Sessions for ${day}`); + for (const time of Object.keys(schedule[day])) { + form + .addMultipleChoiceItem() + .setTitle(`${time} ${day}`) + .setChoiceValues(schedule[day][time]); + } + } } /** @@ -143,32 +143,32 @@ function setUpForm_(ss, values) { * see https://developers.google.com/apps-script/understanding_events */ function onFormSubmit(e) { - const 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. - const response = []; - const values = SpreadsheetApp.getActive() - .getSheetByName("Conference Setup") - .getDataRange() - .getValues(); - for (let i = 1; i < values.length; i++) { - const session = values[i]; - const title = session[0]; - const day = session[1].toLocaleDateString(); - const time = session[2].toLocaleTimeString(); - const 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); + const 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. + const response = []; + const values = SpreadsheetApp.getActive() + .getSheetByName("Conference Setup") + .getDataRange() + .getValues(); + for (let i = 1; i < values.length; i++) { + const session = values[i]; + const title = session[0]; + const day = session[1].toLocaleDateString(); + const time = session[2].toLocaleTimeString(); + const 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); } /** @@ -177,11 +177,11 @@ function onFormSubmit(e) { * @param {Array} response An array of data for the user's session choices. */ function sendInvites_(user, response) { - const id = ScriptProperties.getProperty("calId"); - const cal = CalendarApp.getCalendarById(id); - for (let i = 0; i < response.length; i++) { - cal.getEventSeriesById(response[i][5]).addGuest(user.email); - } + const id = ScriptProperties.getProperty("calId"); + const cal = CalendarApp.getCalendarById(id); + for (let i = 0; i < response.length; i++) { + cal.getEventSeriesById(response[i][5]).addGuest(user.email); + } } /** @@ -190,39 +190,39 @@ function sendInvites_(user, response) { * @param {Array} response An array of data for the user's session choices. */ function sendDoc_(user, response) { - const doc = DocumentApp.create( - `Conference Itinerary for ${user.name}`, - ).addEditor(user.email); - const 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), - }); + const doc = DocumentApp.create( + `Conference Itinerary for ${user.name}`, + ).addEditor(user.email); + const 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() { - const scriptProperties = PropertiesService.getScriptProperties(); - scriptProperties.deleteAllProperties(); + const scriptProperties = PropertiesService.getScriptProperties(); + scriptProperties.deleteAllProperties(); } diff --git a/solutions/automations/event-session-signup/appsscript.json b/solutions/automations/event-session-signup/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/event-session-signup/appsscript.json +++ b/solutions/automations/event-session-signup/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/feedback-sentiment-analysis/appsscript.json b/solutions/automations/feedback-sentiment-analysis/appsscript.json index 4dd859db7..5e86ad849 100644 --- a/solutions/automations/feedback-sentiment-analysis/appsscript.json +++ b/solutions/automations/feedback-sentiment-analysis/appsscript.json @@ -1,14 +1,14 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "libraries": [ - { - "userSymbol": "OAuth2", - "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", - "version": "24" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Los_Angeles", + "dependencies": { + "libraries": [ + { + "userSymbol": "OAuth2", + "libraryId": "1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF", + "version": "24" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/feedback-sentiment-analysis/code.js b/solutions/automations/feedback-sentiment-analysis/code.js index 929c9cc96..bc078556c 100644 --- a/solutions/automations/feedback-sentiment-analysis/code.js +++ b/solutions/automations/feedback-sentiment-analysis/code.js @@ -22,19 +22,19 @@ const myApiKey = "YOUR_API_KEY"; // Replace with your API key. // Matches column names in Review Data sheet to variables. const COLUMN_NAME = { - COMMENTS: "comments", - ENTITY: "entity_sentiment", - ID: "id", + 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(); + SpreadsheetApp.getUi() + .createMenu("Sentiment Tools") + .addItem("Mark entities and sentiment", "markEntitySentiment") + .addToUi(); } /** @@ -43,85 +43,85 @@ function onOpen() { * Entity Sentiment Data sheet. */ function markEntitySentiment() { - // Sets variables for "Review Data" sheet - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const dataSheet = ss.getSheetByName("Review Data"); - const rows = dataSheet.getDataRange(); - const numRows = rows.getNumRows(); - const values = rows.getValues(); - const headerRow = values[0]; + // Sets variables for "Review Data" sheet + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const dataSheet = ss.getSheetByName("Review Data"); + const rows = dataSheet.getDataRange(); + const numRows = rows.getNumRows(); + const values = rows.getValues(); + const 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. - const entitySheet = ss.getSheetByName("Entity Sentiment Data"); - if (entitySheet == null) { - ss.insertSheet("Entity Sentiment Data"); - const entitySheet = ss.getSheetByName("Entity Sentiment Data"); - const esHeaderRange = entitySheet.getRange(1, 1, 1, 6); - const esHeader = [ - [ - "Review ID", - "Entity", - "Salience", - "Sentiment Score", - "Sentiment Magnitude", - "Number of mentions", - ], - ]; - esHeaderRange.setValues(esHeader); - } + // Checks to see if "Entity Sentiment Data" sheet is present, and + // if not, creates a new sheet and sets the header row. + const entitySheet = ss.getSheetByName("Entity Sentiment Data"); + if (entitySheet == null) { + ss.insertSheet("Entity Sentiment Data"); + const entitySheet = ss.getSheetByName("Entity Sentiment Data"); + const esHeaderRange = entitySheet.getRange(1, 1, 1, 6); + const 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. - const textColumnIdx = headerRow.indexOf(COLUMN_NAME.COMMENTS); - const entityColumnIdx = headerRow.indexOf(COLUMN_NAME.ENTITY); - const 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 - } + // Finds the column index for comments, language_detected, + // and comments_english columns. + const textColumnIdx = headerRow.indexOf(COLUMN_NAME.COMMENTS); + const entityColumnIdx = headerRow.indexOf(COLUMN_NAME.ENTITY); + const 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) { - const value = values[i]; - const commentEnCellVal = value[textColumnIdx]; - const entityCellVal = value[entityColumnIdx]; - const reviewId = value[idColumnIdx]; + ss.toast("Analyzing entities and sentiment..."); + for (let i = 0; i < numRows; ++i) { + const value = values[i]; + const commentEnCellVal = value[textColumnIdx]; + const entityCellVal = value[entityColumnIdx]; + const reviewId = value[idColumnIdx]; - // Calls retrieveEntitySentiment function for each row that has a comment - // and also an empty entity_sentiment cell value. - if (commentEnCellVal && !entityCellVal) { - const nlData = retrieveEntitySentiment(commentEnCellVal); - // Pastes each entity and sentiment score into Entity Sentiment Data sheet. - const newValues = []; - for (let entity in nlData.entities) { - entity = nlData.entities[entity]; - const 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 retrieveEntitySentiment function for each row that has a comment + // and also an empty entity_sentiment cell value. + if (commentEnCellVal && !entityCellVal) { + const nlData = retrieveEntitySentiment(commentEnCellVal); + // Pastes each entity and sentiment score into Entity Sentiment Data sheet. + const newValues = []; + for (let entity in nlData.entities) { + entity = nlData.entities[entity]; + const 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"); + } + } } /** @@ -131,24 +131,24 @@ function markEntitySentiment() { * @return {Object} the entities and related sentiment present in the string */ function retrieveEntitySentiment(line) { - const apiKey = myApiKey; - const apiEndpoint = `https://language.googleapis.com/v1/documents:analyzeEntitySentiment?key=${apiKey}`; - // Creates a JSON request, with text string, language, type and encoding - const 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. - const nlOptions = { - method: "post", - contentType: "application/json", - payload: JSON.stringify(nlData), - }; - // Makes the API call. - const response = UrlFetchApp.fetch(apiEndpoint, nlOptions); - return JSON.parse(response); + const apiKey = myApiKey; + const apiEndpoint = `https://language.googleapis.com/v1/documents:analyzeEntitySentiment?key=${apiKey}`; + // Creates a JSON request, with text string, language, type and encoding + const 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. + const nlOptions = { + method: "post", + contentType: "application/json", + payload: JSON.stringify(nlData), + }; + // Makes the API call. + const response = UrlFetchApp.fetch(apiEndpoint, nlOptions); + return JSON.parse(response); } diff --git a/solutions/automations/folder-creation/Code.js b/solutions/automations/folder-creation/Code.js index d7e678c0a..1a92537d2 100644 --- a/solutions/automations/folder-creation/Code.js +++ b/solutions/automations/folder-creation/Code.js @@ -19,15 +19,15 @@ Please watch this video tutorial to see how to use this script: https://youtu.be */ function createNewFolder(project) { - const folder = Drive.Files.insert( - { - parents: [{ id: "ADD YOUR SHARED DRIVE FOLDER ID HERE" }], - title: project, - mimeType: "application/vnd.google-apps.folder", - }, - null, - { supportsAllDrives: true }, - ); + const folder = Drive.Files.insert( + { + parents: [{ id: "ADD YOUR SHARED DRIVE FOLDER ID HERE" }], + title: project, + mimeType: "application/vnd.google-apps.folder", + }, + null, + { supportsAllDrives: true }, + ); - return folder.alternateLink; + return folder.alternateLink; } diff --git a/solutions/automations/folder-creation/appscript.json b/solutions/automations/folder-creation/appscript.json index 0389303b6..febe84322 100644 --- a/solutions/automations/folder-creation/appscript.json +++ b/solutions/automations/folder-creation/appscript.json @@ -1,14 +1,14 @@ { - "timeZone": "Europe/Madrid", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v2", - "serviceId": "drive" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "Europe/Madrid", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v2", + "serviceId": "drive" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/generate-pdfs/Code.js b/solutions/automations/generate-pdfs/Code.js index 1944f0447..ddbd91c64 100644 --- a/solutions/automations/generate-pdfs/Code.js +++ b/solutions/automations/generate-pdfs/Code.js @@ -44,37 +44,37 @@ const EMAIL_BODY = "Hello!\rPlease see the attached 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); + 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); + // 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 = []; + ss.toast("Creating Invoices", APP_TITLE, 1); + const invoices = []; - // Iterates for each customer calling createInvoiceForCustomer routine. - for (const customer of customers) { - ss.toast(`Creating Invoice for ${customer.customer_name}`, APP_TITLE, 1); - const 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); + // Iterates for each customer calling createInvoiceForCustomer routine. + for (const customer of customers) { + ss.toast(`Creating Invoice for ${customer.customer_name}`, APP_TITLE, 1); + const 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); } /** @@ -88,75 +88,75 @@ function processDocuments() { * Return {array} of instance customer invoice data */ function createInvoiceForCustomer( - customer, - products, - transactions, - templateSheet, - ssId, + customer, + products, + transactions, + templateSheet, + ssId, ) { - const customerTransactions = transactions.filter( - (transaction) => transaction.customer_name === customer.customer_name, - ); + const customerTransactions = transactions.filter( + (transaction) => transaction.customer_name === customer.customer_name, + ); - // Clears existing data from the template. - clearTemplateSheet(); + // Clears existing data from the template. + clearTemplateSheet(); - const lineItems = []; - let totalAmount = 0; - for (const lineItem of customerTransactions) { - const lineItemProduct = products.filter( - (product) => product.sku_name === lineItem.sku, - )[0]; - const qty = Number.parseInt(lineItem.licenses); - const price = Number.parseFloat(lineItemProduct.price).toFixed(2); - const amount = Number.parseFloat(qty * price).toFixed(2); - lineItems.push([ - lineItemProduct.sku_name, - lineItemProduct.sku_description, - "", - qty, - price, - amount, - ]); - totalAmount += Number.parseFloat(amount); - } + const lineItems = []; + let totalAmount = 0; + for (const lineItem of customerTransactions) { + const lineItemProduct = products.filter( + (product) => product.sku_name === lineItem.sku, + )[0]; + const qty = Number.parseInt(lineItem.licenses); + const price = Number.parseFloat(lineItemProduct.price).toFixed(2); + const amount = Number.parseFloat(qty * price).toFixed(2); + lineItems.push([ + lineItemProduct.sku_name, + lineItemProduct.sku_description, + "", + qty, + price, + amount, + ]); + totalAmount += Number.parseFloat(amount); + } - // Generates a random invoice number. You can replace with your own document ID method. - const invoiceNumber = Math.floor(100000 + Math.random() * 900000); + // 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(); + // 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); + // 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", - ]; + // 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", + ]; } /** @@ -167,17 +167,17 @@ function createInvoiceForCustomer( * 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(); - for (const cell of rngClear) { - 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(); + 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(); + for (const cell of rngClear) { + 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(); } /** @@ -188,25 +188,25 @@ function clearTemplateSheet() { * @return {file object} PDF file as a blob */ function createPDF(ssId, sheet, pdfName) { - const fr = 0; - const fc = 0; - const lc = 9; - const 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 fr = 0; + const fc = 0; + const lc = 9; + const 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`); + 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); + // Gets the folder in Drive where the PDFs are stored. + const folder = getFolderByName_(OUTPUT_FOLDER_NAME); - const pdfFile = folder.createFile(blob); - return pdfFile; + const pdfFile = folder.createFile(blob); + return pdfFile; } /** @@ -216,33 +216,33 @@ function createPDF(ssId, sheet, pdfName) { * 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((invoice, index) => { - if (invoice.email_sent !== "Yes") { - ss.toast(`Emailing Invoice for ${invoice.customer}`, APP_TITLE, 1); + 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((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); + 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; - } + 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"); - } - }); + GmailApp.sendEmail(recipient, EMAIL_SUBJECT, EMAIL_BODY, { + attachments: [attachment.getAs(MimeType.PDF)], + name: APP_TITLE, + }); + invoicesSheet.getRange(index + 2, 9).setValue("Yes"); + } + }); } /** @@ -252,40 +252,40 @@ function sendEmails() { * 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)); + 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) { - const objects = []; - for (let i = 0; i < data.length; ++i) { - const object = {}; - let hasData = false; - for (let j = 0; j < data[i].length; ++j) { - const cellData = data[i][j]; - if (isCellEmpty(cellData)) { - continue; - } - object[keys[j]] = cellData; - hasData = true; - } - if (hasData) { - objects.push(object); - } - } - return objects; + const objects = []; + for (let i = 0; i < data.length; ++i) { + const object = {}; + let hasData = false; + for (let j = 0; j < data[i].length; ++j) { + const 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((key) => key.replace(/\W+/g, "_").toLowerCase()); + return keys.map((key) => 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 === ""; + return typeof cellData === "string" && cellData === ""; } diff --git a/solutions/automations/generate-pdfs/Menu.js b/solutions/automations/generate-pdfs/Menu.js index ed0b92730..b8c850f89 100644 --- a/solutions/automations/generate-pdfs/Menu.js +++ b/solutions/automations/generate-pdfs/Menu.js @@ -29,11 +29,11 @@ * @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(); + const menu = SpreadsheetApp.getUi().createMenu(APP_TITLE); + menu + .addItem("Process invoices", "processDocuments") + .addItem("Send emails", "sendEmails") + .addSeparator() + .addItem("Reset template", "clearTemplateSheet") + .addToUi(); } diff --git a/solutions/automations/generate-pdfs/Utilities.js b/solutions/automations/generate-pdfs/Utilities.js index 22f61942c..d0ef9d33c 100644 --- a/solutions/automations/generate-pdfs/Utilities.js +++ b/solutions/automations/generate-pdfs/Utilities.js @@ -25,26 +25,26 @@ * @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(); + // 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()) { - const folder = subFolders.next(); + // Iterates the subfolders to check if the PDF folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + const 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`, - ); + // 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`, + ); } /** @@ -52,12 +52,12 @@ function getFolderByName_(folderName) { * @prints a Google Drive FolderId. */ function test_getFolderByName() { - // Gets the PDF folder in Drive. - const folder = getFolderByName_(OUTPUT_FOLDER_NAME); + // 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); + console.log( + `Name: ${folder.getName()}\rID: ${folder.getId()}\rDescription: ${folder.getDescription()}`, + ); + // To automatically delete test folder, uncomment the following code: + // folder.setTrashed(true); } diff --git a/solutions/automations/generate-pdfs/appsscript.json b/solutions/automations/generate-pdfs/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/generate-pdfs/appsscript.json +++ b/solutions/automations/generate-pdfs/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/import-csv-sheets/Code.js b/solutions/automations/import-csv-sheets/Code.js index 0b8f94255..7d5a35399 100644 --- a/solutions/automations/import-csv-sheets/Code.js +++ b/solutions/automations/import-csv-sheets/Code.js @@ -39,26 +39,26 @@ const HANDLER_FUNCTION = "updateApplicationSheet"; // Function called by install * 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 (let 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. - const 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.`, - ); + // Checks for an existing trigger to avoid creating duplicate instances. + // Removes existing if found. + const projectTriggers = ScriptApp.getProjectTriggers(); + for (let 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. + const 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.`, + ); } /** @@ -71,80 +71,80 @@ function installTrigger() { * 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} - const objSpreadSheet = getSpreadSheet_(SHEET_REPORT_NAME, folderAppPrimary); - - // Creates arrays to track every CSV file, categorized as processed sucessfully or not. - const filesProcessed = []; - const filesNotProcessed = []; - - // Gets all CSV files found in the source folder. - const cvsFiles = folderSource.getFilesByType(MimeType.CSV); - - // Iterates through each CSV file. - while (cvsFiles.hasNext()) { - const csvFile = cvsFiles.next(); - const 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 { - // 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}`); + // 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} + const objSpreadSheet = getSpreadSheet_(SHEET_REPORT_NAME, folderAppPrimary); + + // Creates arrays to track every CSV file, categorized as processed sucessfully or not. + const filesProcessed = []; + const filesNotProcessed = []; + + // Gets all CSV files found in the source folder. + const cvsFiles = folderSource.getFilesByType(MimeType.CSV); + + // Iterates through each CSV file. + while (cvsFiles.hasNext()) { + const csvFile = cvsFiles.next(); + const 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 { + // 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}`); } /** @@ -153,28 +153,28 @@ function updateApplicationSheet() { * @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. - const sheet = objSpreadSheet.getSheets()[0]; - - // Parses CSV file into data array. - const 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. - const startRow = sheet.getLastRow() + 1; - const startCol = 1; - // Determines the incoming data size. - const numRows = data.length; - const 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. - } + try { + // Gets the first sheet of the destination spreadsheet. + const sheet = objSpreadSheet.getSheets()[0]; + + // Parses CSV file into data array. + const 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. + const startRow = sheet.getLastRow() + 1; + const startCol = 1; + // Determines the incoming data size. + const numRows = data.length; + const 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/SampleData.js b/solutions/automations/import-csv-sheets/SampleData.js index 0b7709fb2..8b75ef664 100644 --- a/solutions/automations/import-csv-sheets/SampleData.js +++ b/solutions/automations/import-csv-sheets/SampleData.js @@ -22,111 +22,111 @@ // 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", - }, - ], - }, - ], + 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", + }, + ], + }, + ], }; /** @@ -134,10 +134,10 @@ const SAMPLE_DATA = { * @return {string[][]} array of each column heading as string. */ function getHeadings() { - const headings = [[]]; - for (const i in SAMPLE_DATA.headings) - headings[0].push(SAMPLE_DATA.headings[i]); - return headings; + const headings = [[]]; + for (const i in SAMPLE_DATA.headings) + headings[0].push(SAMPLE_DATA.headings[i]); + return headings; } /** @@ -145,44 +145,44 @@ function getHeadings() { * @return {object[]} {"file": ["name","csv"]} */ function getCSVFilesData() { - const files = []; + const files = []; - // Gets headings once - same for all files/rows. - let csvHeadings = ""; - for (const i in SAMPLE_DATA.headings) - csvHeadings += `${SAMPLE_DATA.headings[i]},`; + // Gets headings once - same for all files/rows. + let csvHeadings = ""; + for (const i in SAMPLE_DATA.headings) + csvHeadings += `${SAMPLE_DATA.headings[i]},`; - // Gets data for each file by rows. - for (const i in SAMPLE_DATA.csvFiles) { - let sampleCSV = ""; - sampleCSV += csvHeadings; - const fileName = SAMPLE_DATA.csvFiles[i].name; - for (const j in SAMPLE_DATA.csvFiles[i].rows) { - sampleCSV += "\n"; - for (const k in SAMPLE_DATA.csvFiles[i].rows[j]) { - sampleCSV += `${SAMPLE_DATA.csvFiles[i].rows[j][k]},`; - } - } - files.push({ name: fileName, csv: sampleCSV }); - } - return files; + // Gets data for each file by rows. + for (const i in SAMPLE_DATA.csvFiles) { + let sampleCSV = ""; + sampleCSV += csvHeadings; + const fileName = SAMPLE_DATA.csvFiles[i].name; + for (const j in SAMPLE_DATA.csvFiles[i].rows) { + sampleCSV += "\n"; + for (const 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() { - const h = getHeadings(); - console.log(h); - console.log(h[0].length); + const h = getHeadings(); + console.log(h); + console.log(h[0].length); } function test_getCSVFilesData() { - const csvFiles = getCSVFilesData(); - console.log(csvFiles); + const csvFiles = getCSVFilesData(); + console.log(csvFiles); - for (const file of csvFiles) { - console.log(file.name); - console.log(file.csv); - } + for (const file of csvFiles) { + console.log(file.name); + console.log(file.csv); + } } diff --git a/solutions/automations/import-csv-sheets/SetupSample.js b/solutions/automations/import-csv-sheets/SetupSample.js index ea2f5bf28..d08e91375 100644 --- a/solutions/automations/import-csv-sheets/SetupSample.js +++ b/solutions/automations/import-csv-sheets/SetupSample.js @@ -34,69 +34,69 @@ const INCLUDE_SAMPLE_DATA_FILES = true; // Set to true to create sample data fil * 4) Creates an installable trigger to run process automatically at a specified time interval. */ function setupSample() { - console.log(`Application setup for: ${APP_TITLE}`); + 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); + // 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()}`, - ); + console.log( + `Application folders: ${folderAppPrimary.getName()}, ${folderSource.getName()}, ${folderProcessed.getName()}`, + ); - if (INCLUDE_SAMPLE_DATA_FILES) { - // Sets up primary destination spreadsheet - const sheet = setupPrimarySpreadsheet_(folderAppPrimary); + 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(); + // 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)) { - const 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(); + // 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)) { + const 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}`); + 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); + // 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. - const 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); + // Adds the sample data headings. + const 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; - } + console.log( + `Created file: ${SHEET_REPORT_NAME} In folder: ${folderAppPrimary.getName()}.`, + ); + return sheet; + } } /** @@ -104,19 +104,19 @@ function setupPrimarySpreadsheet_(folderAppPrimary) { * 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.`, - ); + 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 (let 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]); - } - } + // Removes existing trigger if found. + const projectTriggers = ScriptApp.getProjectTriggers(); + for (let 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]); + } + } } diff --git a/solutions/automations/import-csv-sheets/Utilities.js b/solutions/automations/import-csv-sheets/Utilities.js index e339dcf4e..7bbcec870 100644 --- a/solutions/automations/import-csv-sheets/Utilities.js +++ b/solutions/automations/import-csv-sheets/Utilities.js @@ -27,20 +27,20 @@ * @return {object} Spreadsheet object. */ function getSpreadSheet_(fileName, objFolder) { - const files = objFolder.getFilesByName(fileName); + const files = objFolder.getFilesByName(fileName); - while (files.hasNext()) { - const file = files.next(); - const fileId = file.getId(); + while (files.hasNext()) { + const file = files.next(); + const fileId = file.getId(); - const existingSpreadsheet = SpreadsheetApp.openById(fileId); - return existingSpreadsheet; - } + 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; + // If application destination spreadsheet is missing, creates a new sample version. + const folderAppPrimary = getApplicationFolder_(APP_FOLDER); + const sampleSheet = setupPrimarySpreadsheet_(folderAppPrimary); + return sampleSheet; } /** @@ -51,14 +51,14 @@ function getSpreadSheet_(fileName, objFolder) { * @return {boolean} true if found in folder, false if not. */ function fileExists_(fileName, objFolder) { - const files = objFolder.getFilesByName(fileName); - - while (files.hasNext()) { - const file = files.next(); - console.log(`${file.getName()} already exists.`); - return true; - } - return false; + const files = objFolder.getFilesByName(fileName); + + while (files.hasNext()) { + const file = files.next(); + console.log(`${file.getName()} already exists.`); + return true; + } + return false; } /** @@ -69,23 +69,23 @@ function fileExists_(fileName, objFolder) { * @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()) { - const 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}.`); + // 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()) { + const 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}.`); } /** @@ -96,23 +96,23 @@ function getFolder_(folderName) { * @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()) { - const 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}.`); + // 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()) { + const 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}.`); } /** @@ -120,24 +120,24 @@ function getApplicationFolder_() { * @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); + 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); } diff --git a/solutions/automations/import-csv-sheets/appsscript.json b/solutions/automations/import-csv-sheets/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/import-csv-sheets/appsscript.json +++ b/solutions/automations/import-csv-sheets/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/mail-merge/Code.js b/solutions/automations/mail-merge/Code.js index 5b72b2d1d..757716495 100644 --- a/solutions/automations/mail-merge/Code.js +++ b/solutions/automations/mail-merge/Code.js @@ -32,8 +32,8 @@ 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(); + const ui = SpreadsheetApp.getUi(); + ui.createMenu("Mail Merge").addItem("Send Emails", "sendEmails").addToUi(); } /** @@ -42,191 +42,191 @@ function onOpen() { * @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 - let processedSubjectLine = subjectLine; - if (!processedSubjectLine) { - processedSubjectLine = 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 (processedSubjectLine === "cancel" || processedSubjectLine === "") { - // If no subject line, finishes up - return; - } - } - - // Gets the draft Gmail message to use as a template - const emailTemplate = getGmailTemplateFromDrafts_(processedSubjectLine); - - // 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] || ""; - return o; - }, {}), - ); - - // Creates an array to record sent emails - const out = []; - - // Loops through all the rows of data - obj.forEach((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.bcc@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; - return obj; - }, {}); - - //Regexp searches for all img string positions with cid - const imgexp = /]+>/g; - const matches = [...htmlBody.matchAll(imgexp)]; - - //Initiates the allInlineImages object - const inlineImagesObj = {}; - for (const match of matches) { - 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 (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"); - } + // option to skip browser prompt if you want to use this code in other projects + let processedSubjectLine = subjectLine; + if (!processedSubjectLine) { + processedSubjectLine = 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 (processedSubjectLine === "cancel" || processedSubjectLine === "") { + // If no subject line, finishes up + return; + } + } + + // Gets the draft Gmail message to use as a template + const emailTemplate = getGmailTemplateFromDrafts_(processedSubjectLine); + + // 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] || ""; + return o; + }, {}), + ); + + // Creates an array to record sent emails + const out = []; + + // Loops through all the rows of data + obj.forEach((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.bcc@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; + return obj; + }, {}); + + //Regexp searches for all img string positions with cid + const imgexp = /]+>/g; + const matches = [...htmlBody.matchAll(imgexp)]; + + //Initiates the allInlineImages object + const inlineImagesObj = {}; + for (const match of matches) { + 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 (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/appsscript.json b/solutions/automations/mail-merge/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/mail-merge/appsscript.json +++ b/solutions/automations/mail-merge/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/news-sentiment/Code.js b/solutions/automations/news-sentiment/Code.js index 0c2e1f4a8..a0b6c51e7 100644 --- a/solutions/automations/news-sentiment/Code.js +++ b/solutions/automations/news-sentiment/Code.js @@ -22,11 +22,11 @@ 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")'; + '=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")'; + '=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")'; + '=IMAGE("https://cdn.pixabay.com/photo/2016/09/01/08/25/smiley-1635454_1280.png")'; const happyColor = "#44f83d"; const mehColor = "#f7f6cc"; const sadColor = "#ff3c3d"; @@ -52,10 +52,10 @@ let scoreCol = null; * */ function onOpen() { - const ui = SpreadsheetApp.getUi(); - ui.createMenu("News Headlines Sentiments") - .addItem("Analyze News Headlines...", "showNewsPrompt") - .addToUi(); + const ui = SpreadsheetApp.getUi(); + ui.createMenu("News Headlines Sentiments") + .addItem("Analyze News Headlines...", "showNewsPrompt") + .addToUi(); } /** @@ -63,27 +63,27 @@ function onOpen() { * 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"); + //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 - const ui = SpreadsheetApp.getUi(); - const result = ui.prompt("Enter news topic:", ui.ButtonSet.OK_CANCEL); + // Builds Menu + const ui = SpreadsheetApp.getUi(); + const result = ui.prompt("Enter news topic:", ui.ButtonSet.OK_CANCEL); - // Processes the user's response. - const 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!"); - } + // Processes the user's response. + const 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!"); + } } /** @@ -91,51 +91,51 @@ function showNewsPrompt() { * the sentiment response column. */ function analyzeNewsHeadlines() { - // Clears and reformats the sheet - reformatSheet(); + // Clears and reformats the sheet + reformatSheet(); - // Gets the headlines array - headlines = getHeadlinesArray(); + // 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}!`); - } + // 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 - const hdlnsResp = []; - const encodedtopic = encodeURIComponent(topic); - ss.toast(`Getting headlines for: ${topic}`); - const response = UrlFetchApp.fetch( - `${apiEndPointHdr + encodedtopic}&apiKey=${newsApiKey}`, - ); - const results = JSON.parse(response); - const articles = results.articles; + // Fetches headlines for a given topic + const hdlnsResp = []; + const encodedtopic = encodeURIComponent(topic); + ss.toast(`Getting headlines for: ${topic}`); + const response = UrlFetchApp.fetch( + `${apiEndPointHdr + encodedtopic}&apiKey=${newsApiKey}`, + ); + const results = JSON.parse(response); + const 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); + 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)); - } + // Constructs hdlnsResp as a 2d array. This simplifies syncing to the sheet. + hdlnsResp.push(new Array(newsStory)); + } - return hdlnsResp; + return hdlnsResp; } /** @@ -143,38 +143,38 @@ function getHeadlinesArray() { * the sentiment response columns. */ function getSentiments() { - ss.toast("Analyzing the headline sentiments..."); + ss.toast("Analyzing the headline sentiments..."); - const articleCount = rows.getNumRows() - 1; - let avg = 0; + const articleCount = rows.getNumRows() - 1; + let avg = 0; - // Gets sentiment for each row - for (let i = 1; i <= articleCount; i++) { - const headlineCell = rowValues[i][headlineCol]; - if (headlineCell) { - const sentimentData = retrieveSentiment(headlineCell); - const 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); - } - } - const avgDecimal = (avg / articleCount).toFixed(2); + // Gets sentiment for each row + for (let i = 1; i <= articleCount; i++) { + const headlineCell = rowValues[i][headlineCol]; + if (headlineCell) { + const sentimentData = retrieveSentiment(headlineCell); + const 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); + } + } + const 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!!"); + // 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!!"); } /** @@ -185,26 +185,26 @@ function getSentiments() { * Unsupported languages generate a "400" response: "INVALID_ARGUMENT". */ function retrieveSentiment(text) { - // Sets REST call options - const apiEndPoint = `https://language.googleapis.com/v1/documents:analyzeSentiment?key=${googleAPIKey}`; - const jsonReq = JSON.stringify({ - document: { - type: "PLAIN_TEXT", - content: text, - }, - encodingType: "UTF8", - }); + // Sets REST call options + const apiEndPoint = `https://language.googleapis.com/v1/documents:analyzeSentiment?key=${googleAPIKey}`; + const jsonReq = JSON.stringify({ + document: { + type: "PLAIN_TEXT", + content: text, + }, + encodingType: "UTF8", + }); - const options = { - method: "post", - contentType: "application/json", - payload: jsonReq, - }; + const options = { + method: "post", + contentType: "application/json", + payload: jsonReq, + }; - // Makes the REST call - const response = UrlFetchApp.fetch(apiEndPoint, options); - const responseData = JSON.parse(response); - return responseData; + // Makes the REST call + const response = UrlFetchApp.fetch(apiEndPoint, options); + const responseData = JSON.parse(response); + return responseData; } // Helper Functions @@ -213,43 +213,43 @@ function retrieveSentiment(text) { * Removes old headlines, sentiments and reset formatting */ function reformatSheet() { - let range = ds.getRange(fullsheet); - range.clearContent(); - range.clearFormat(); - range.setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP); + 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"); + 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; - } - if (value < threshold && value > -threshold) { - return mehFace; - } - if (value <= -threshold) { - return sadFace; - } + if (value >= threshold) { + return happyFace; + } + if (value < threshold && value > -threshold) { + return mehFace; + } + if (value <= -threshold) { + return sadFace; + } } /** * Returns a corresponding color based on numeric value. */ function getColor(value) { - if (value >= threshold) { - return happyColor; - } - if (value < threshold && value > -threshold) { - return mehColor; - } - if (value <= -threshold) { - return sadColor; - } + if (value >= threshold) { + return happyColor; + } + if (value < threshold && value > -threshold) { + return mehColor; + } + if (value <= -threshold) { + return sadColor; + } } /** @@ -257,5 +257,5 @@ function getColor(value) { * Can be expanded if needed. */ function scrub(text) { - return text.replace(/[\‘\,\“\”\"\'\’\-\n\â\€]/g, " "); + return text.replace(/[\‘\,\“\”\"\'\’\-\n\â\€]/g, " "); } diff --git a/solutions/automations/news-sentiment/appsscript.json b/solutions/automations/news-sentiment/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/news-sentiment/appsscript.json +++ b/solutions/automations/news-sentiment/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/offsite-activity-signup/Code.js b/solutions/automations/offsite-activity-signup/Code.js index be3ac5d30..12a23cec1 100644 --- a/solutions/automations/offsite-activity-signup/Code.js +++ b/solutions/automations/offsite-activity-signup/Code.js @@ -25,12 +25,12 @@ const NUM_TEST_USERS = 150; * Adds custom menu items when opening the sheet. */ function onOpen() { - const menu = SpreadsheetApp.getUi() - .createMenu("Activities") - .addItem("Create form", "buildForm_") - .addItem("Generate test data", "generateTestData_") - .addItem("Assign activities", "assignActivities_") - .addToUi(); + const menu = SpreadsheetApp.getUi() + .createMenu("Activities") + .addItem("Create form", "buildForm_") + .addItem("Generate test data", "generateTestData_") + .addItem("Assign activities", "assignActivities_") + .addToUi(); } /** @@ -38,47 +38,47 @@ function onOpen() { * N choices of activities, where N is defined by NUM_ITEMS_TO_RANK. */ function buildForm_() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - if (ss.getFormUrl()) { - const msg = "Form already exists. Unlink the form and try again."; - SpreadsheetApp.getUi().alert(msg); - return; - } - const form = FormApp.create("Activity Signup") - .setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()) - .setAllowResponseEdits(true) - .setLimitOneResponsePerUser(true) - .setCollectEmail(true); - const sectionHelpText = Utilities.formatString( - "Please choose your top %d activities", - NUM_ITEMS_TO_RANK, - ); - form - .addSectionHeaderItem() - .setTitle("Activity choices") - .setHelpText(sectionHelpText); + const ss = SpreadsheetApp.getActiveSpreadsheet(); + if (ss.getFormUrl()) { + const msg = "Form already exists. Unlink the form and try again."; + SpreadsheetApp.getUi().alert(msg); + return; + } + const form = FormApp.create("Activity Signup") + .setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()) + .setAllowResponseEdits(true) + .setLimitOneResponsePerUser(true) + .setCollectEmail(true); + const 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. - const rows = loadActivitySchedule_(ss).map( - (activity) => activity.description, - ); - const columns = range_(1, NUM_ITEMS_TO_RANK).map((value) => - Utilities.formatString("%s", toOrdinal_(value)), - ); - const gridValidation = FormApp.createGridValidation() - .setHelpText("Select one item per column.") - .requireLimitOneResponsePerColumn() - .build(); - form - .addGridItem() - .setColumns(columns) - .setRows(rows) - .setValidation(gridValidation); + // Presents activity ranking as a form grid with each activity as a row and rank as a column. + const rows = loadActivitySchedule_(ss).map( + (activity) => activity.description, + ); + const columns = range_(1, NUM_ITEMS_TO_RANK).map((value) => + Utilities.formatString("%s", toOrdinal_(value)), + ); + const 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"]); + form + .addListItem() + .setTitle("Assign other activities if choices are not available?") + .setChoiceValues(["Yes", "No"]); } /** @@ -89,13 +89,13 @@ function buildForm_() { * See https://en.wikipedia.org/wiki/Random_serial_dictatorship for additional information. */ function assignActivities_() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const activities = loadActivitySchedule_(ss); - const activityIds = activities.map((activity) => activity.id); - const attendees = loadAttendeeResponses_(ss, activityIds); - assignWithRandomPriority_(attendees, activities, 2); - writeAttendeeAssignments_(ss, attendees); - writeActivityRosters_(ss, activities); + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const activities = loadActivitySchedule_(ss); + const activityIds = activities.map((activity) => activity.id); + const attendees = loadAttendeeResponses_(ss, activityIds); + assignWithRandomPriority_(attendees, activities, 2); + writeAttendeeAssignments_(ss, attendees); + writeActivityRosters_(ss, activities); } /** @@ -106,20 +106,20 @@ function assignActivities_() { * @param {number} numActivitiesPerPerson - Maximum number of activities to assign */ function assignWithRandomPriority_( - attendees, - activities, - numActivitiesPerPerson, + attendees, + activities, + numActivitiesPerPerson, ) { - const activitiesById = activities.reduce((obj, activity) => { - obj[activity.id] = activity; - return obj; - }, {}); - for (let i = 0; i < numActivitiesPerPerson; ++i) { - const randomizedAttendees = shuffleArray_(attendees); - for (const attendee of randomizedAttendees) { - makeChoice_(attendee, activitiesById); - } - } + const activitiesById = activities.reduce((obj, activity) => { + obj[activity.id] = activity; + return obj; + }, {}); + for (let i = 0; i < numActivitiesPerPerson; ++i) { + const randomizedAttendees = shuffleArray_(attendees); + for (const attendee of randomizedAttendees) { + makeChoice_(attendee, activitiesById); + } + } } /** @@ -129,18 +129,18 @@ function assignWithRandomPriority_( * @param {object} activitiesById - Map of all available activities */ function makeChoice_(attendee, activitiesById) { - for (let i = 0; i < attendee.preferences.length; ++i) { - const activity = activitiesById[attendee.preferences[i]]; - if (!activity) { - continue; - } - const canJoin = checkAvailability_(attendee, activity); - if (canJoin) { - attendee.assigned.push(activity); - activity.roster.push(attendee); - break; - } - } + for (let i = 0; i < attendee.preferences.length; ++i) { + const activity = activitiesById[attendee.preferences[i]]; + if (!activity) { + continue; + } + const canJoin = checkAvailability_(attendee, activity); + if (canJoin) { + attendee.assigned.push(activity); + activity.roster.push(attendee); + break; + } + } } /** @@ -152,17 +152,17 @@ function makeChoice_(attendee, activitiesById) { * @return {boolean} - True if attendee can join the activity */ function checkAvailability_(attendee, activity) { - if (activity.capacity <= activity.roster.length) { - return false; - } - const timesConflict = attendee.assigned.some( - (assignedActivity) => - !( - assignedActivity.startAt.getTime() > activity.endAt.getTime() || - activity.startAt.getTime() > assignedActivity.endAt.getTime() - ), - ); - return !timesConflict; + if (activity.capacity <= activity.roster.length) { + return false; + } + const timesConflict = attendee.assigned.some( + (assignedActivity) => + !( + assignedActivity.startAt.getTime() > activity.endAt.getTime() || + activity.startAt.getTime() > assignedActivity.endAt.getTime() + ), + ); + return !timesConflict; } /** @@ -172,24 +172,24 @@ function checkAvailability_(attendee, activity) { * @param {object[]} attendees - Array of attendees with their activity assignments */ function writeAttendeeAssignments_(ss, attendees) { - const sheet = findOrCreateSheetByName_(ss, "Activities by person"); - sheet.clear(); - sheet.appendRow(["Email address", "Activities"]); - sheet.getRange("B1:1").merge(); - const rows = attendees.map((attendee) => { - // Prefill row to ensure consistent length otherwise - // can't bulk update the sheet with range.setValues() - const row = fillArray_([], ACTIVITIES_PER_PERSON + 1, ""); - row[0] = attendee.email; - attendee.assigned.forEach((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()); + const sheet = findOrCreateSheetByName_(ss, "Activities by person"); + sheet.clear(); + sheet.appendRow(["Email address", "Activities"]); + sheet.getRange("B1:1").merge(); + const rows = attendees.map((attendee) => { + // Prefill row to ensure consistent length otherwise + // can't bulk update the sheet with range.setValues() + const row = fillArray_([], ACTIVITIES_PER_PERSON + 1, ""); + row[0] = attendee.email; + attendee.assigned.forEach((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()); } /** @@ -199,18 +199,18 @@ function writeAttendeeAssignments_(ss, attendees) { * @param {object[]} activities - Array of activities with their rosters */ function writeActivityRosters_(ss, activities) { - const sheet = findOrCreateSheetByName_(ss, "Activity rosters"); - sheet.clear(); - let rows = activities.map((activity) => { - const roster = activity.roster.map((attendee) => 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()); + const sheet = findOrCreateSheetByName_(ss, "Activity rosters"); + sheet.clear(); + let rows = activities.map((activity) => { + const roster = activity.roster.map((attendee) => 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()); } /** @@ -220,42 +220,42 @@ function writeActivityRosters_(ss, activities) { * @return {object[]} Array of available activities. */ function loadActivitySchedule_(ss) { - const timeZone = ss.getSpreadsheetTimeZone(); - const sheet = ss.getSheetByName("Activity Schedule"); - const rows = sheet.getSheetValues( - sheet.getFrozenRows() + 1, - 1, - sheet.getLastRow() - 1, - sheet.getLastRow(), - ); - const activities = rows.map((row, index) => { - const name = row[0]; - const startAt = new Date(row[1]); - const endAt = new Date(row[2]); - const capacity = Number.parseInt(row[3]); - const formattedStartAt = Utilities.formatDate( - startAt, - timeZone, - "EEE hh:mm a", - ); - const formattedEndAt = Utilities.formatDate(endAt, timeZone, "hh:mm a"); - const 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; + const timeZone = ss.getSpreadsheetTimeZone(); + const sheet = ss.getSheetByName("Activity Schedule"); + const rows = sheet.getSheetValues( + sheet.getFrozenRows() + 1, + 1, + sheet.getLastRow() - 1, + sheet.getLastRow(), + ); + const activities = rows.map((row, index) => { + const name = row[0]; + const startAt = new Date(row[1]); + const endAt = new Date(row[2]); + const capacity = Number.parseInt(row[3]); + const formattedStartAt = Utilities.formatDate( + startAt, + timeZone, + "EEE hh:mm a", + ); + const formattedEndAt = Utilities.formatDate(endAt, timeZone, "hh:mm a"); + const 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; } /** @@ -266,45 +266,45 @@ function loadActivitySchedule_(ss) { * @return {object[]} Array of parsed attendee respones. */ function loadAttendeeResponses_(ss, allActivityIds) { - const sheet = findResponseSheetForForm_(ss); + const sheet = findResponseSheetForForm_(ss); - if (!sheet || sheet.getLastRow() === 1) { - return undefined; - } + if (!sheet || sheet.getLastRow() === 1) { + return undefined; + } - const rows = sheet.getSheetValues( - sheet.getFrozenRows() + 1, - 1, - sheet.getLastRow() - 1, - sheet.getLastRow(), - ); - const attendees = rows.map((row) => { - const _ = row.shift(); // Ignore timestamp - const email = row.shift(); - const autoAssign = row.pop(); - // Find ranked items in the response data. - let preferences = row.reduce((prefs, value, index) => { - const match = value.match(/(\d+).*/); - if (!match) { - return prefs; - } - const rank = Number.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. - const additionalChoices = shuffleArray_(allActivityIds); - preferences = preferences.concat(additionalChoices); - } - return { - email: email, - preferences: preferences, - assigned: [], - }; - }); - return attendees; + const rows = sheet.getSheetValues( + sheet.getFrozenRows() + 1, + 1, + sheet.getLastRow() - 1, + sheet.getLastRow(), + ); + const attendees = rows.map((row) => { + const _ = row.shift(); // Ignore timestamp + const email = row.shift(); + const autoAssign = row.pop(); + // Find ranked items in the response data. + let preferences = row.reduce((prefs, value, index) => { + const match = value.match(/(\d+).*/); + if (!match) { + return prefs; + } + const rank = Number.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. + const additionalChoices = shuffleArray_(allActivityIds); + preferences = preferences.concat(additionalChoices); + } + return { + email: email, + preferences: preferences, + assigned: [], + }; + }); + return attendees; } /** @@ -313,32 +313,32 @@ function loadAttendeeResponses_(ss, allActivityIds) { * through other means. */ function generateTestData_() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const sheet = findResponseSheetForForm_(ss); - if (!sheet) { - const msg = "No response sheet found. Create the form and try again."; - SpreadsheetApp.getUi().alert(msg); - } - if (sheet.getLastRow() > 1) { - const msg = - "Response sheet is not empty, can not generate test data. " + - "Remove responses and try again."; - SpreadsheetApp.getUi().alert(msg); - return; - } + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheet = findResponseSheetForForm_(ss); + if (!sheet) { + const msg = "No response sheet found. Create the form and try again."; + SpreadsheetApp.getUi().alert(msg); + } + if (sheet.getLastRow() > 1) { + const msg = + "Response sheet is not empty, can not generate test data. " + + "Remove responses and try again."; + SpreadsheetApp.getUi().alert(msg); + return; + } - const activities = loadActivitySchedule_(ss); - const choices = fillArray_([], activities.length, ""); - for (const value of range_(1, 5)) { - choices[value] = toOrdinal_(value); - } + const activities = loadActivitySchedule_(ss); + const choices = fillArray_([], activities.length, ""); + for (const value of range_(1, 5)) { + choices[value] = toOrdinal_(value); + } - const rows = range_(1, NUM_TEST_USERS).map((value) => { - const randomizedChoices = shuffleArray_(choices); - const email = Utilities.formatString("person%d@example.com", value); - return [new Date(), email].concat(randomizedChoices).concat(["Yes"]); - }); - bulkAppendRows_(sheet, rows); + const rows = range_(1, NUM_TEST_USERS).map((value) => { + const randomizedChoices = shuffleArray_(choices); + const email = Utilities.formatString("person%d@example.com", value); + return [new Date(), email].concat(randomizedChoices).concat(["Yes"]); + }); + bulkAppendRows_(sheet, rows); } /** @@ -349,11 +349,11 @@ function generateTestData_() { * @return {Sheet} Sheet instance */ function findOrCreateSheetByName_(ss, name) { - const sheet = ss.getSheetByName(name); - if (sheet) { - return sheet; - } - return ss.insertSheet(name); + const sheet = ss.getSheetByName(name); + if (sheet) { + return sheet; + } + return ss.insertSheet(name); } /** @@ -363,11 +363,11 @@ function findOrCreateSheetByName_(ss, name) { * @param {Array>} rows - Rows to append */ function bulkAppendRows_(sheet, rows) { - const startRow = sheet.getLastRow() + 1; - const startColumn = 1; - const numRows = rows.length; - const numColumns = rows[0].length; - sheet.getRange(startRow, startColumn, numRows, numColumns).setValues(rows); + const startRow = sheet.getLastRow() + 1; + const startColumn = 1; + const numRows = rows.length; + const numColumns = rows[0].length; + sheet.getRange(startRow, startColumn, numRows, numColumns).setValues(rows); } /** @@ -377,14 +377,14 @@ function bulkAppendRows_(sheet, rows) { * @return {object[]} randomized copy of the array */ function shuffleArray_(array) { - const clone = array.slice(0); - for (let i = clone.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - const temp = clone[i]; - clone[i] = clone[j]; - clone[j] = temp; - } - return clone; + const clone = array.slice(0); + for (let i = clone.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = clone[i]; + clone[i] = clone[j]; + clone[j] = temp; + } + return clone; } /** @@ -396,18 +396,18 @@ function shuffleArray_(array) { * @return {string} Formatted string */ function toOrdinal_(i) { - const j = i % 10; - const 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`; + const j = i % 10; + const 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`; } /** @@ -417,17 +417,17 @@ function toOrdinal_(i) { * @return {Sheet} Sheet with form responses, undefined if not found. */ function findResponseSheetForForm_(ss) { - const formUrl = ss.getFormUrl(); - if (!ss || !formUrl) { - return undefined; - } - const sheets = ss.getSheets(); - for (const i in sheets) { - if (sheets[i].getFormUrl() === formUrl) { - return sheets[i]; - } - } - return undefined; + const formUrl = ss.getFormUrl(); + if (!ss || !formUrl) { + return undefined; + } + const sheets = ss.getSheets(); + for (const i in sheets) { + if (sheets[i].getFormUrl() === formUrl) { + return sheets[i]; + } + } + return undefined; } /** @@ -439,10 +439,10 @@ function findResponseSheetForForm_(ss) { * @return {object[]} the array, for chaining purposes */ function fillArray_(arr, length, value) { - for (let i = 0; i < length; ++i) { - arr[i] = value; - } - return arr; + for (let i = 0; i < length; ++i) { + arr[i] = value; + } + return arr; } /** @@ -453,13 +453,13 @@ function fillArray_(arr, length, value) { * @return {number[]} Array of values representing the range */ function range_(start, end) { - const arr = [start]; - let i = start; - while (i < end) { - i += 1; - arr.push(i); - } - return arr; + const arr = [start]; + let i = start; + while (i < end) { + i += 1; + arr.push(i); + } + return arr; } /** @@ -472,13 +472,13 @@ function range_(start, end) { * @return {Array>} New transposed array */ function transpose_(arr, fillValue) { - const transposed = []; - for (const [rowIndex, row] of arr.entries()) { - for (const [colIndex, col] of row.entries()) { - transposed[colIndex] = - transposed[colIndex] || fillArray_([], arr.length, fillValue); - transposed[colIndex][rowIndex] = row[colIndex]; - } - } - return transposed; + const transposed = []; + for (const [rowIndex, row] of arr.entries()) { + for (const [colIndex, col] of row.entries()) { + transposed[colIndex] = + transposed[colIndex] || fillArray_([], arr.length, fillValue); + transposed[colIndex][rowIndex] = row[colIndex]; + } + } + return transposed; } diff --git a/solutions/automations/offsite-activity-signup/appsscript.json b/solutions/automations/offsite-activity-signup/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/offsite-activity-signup/appsscript.json +++ b/solutions/automations/offsite-activity-signup/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/tax-loss-harvest-alerts/Code.js b/solutions/automations/tax-loss-harvest-alerts/Code.js index 5c8cd0f9d..fd7d7c2ef 100644 --- a/solutions/automations/tax-loss-harvest-alerts/Code.js +++ b/solutions/automations/tax-loss-harvest-alerts/Code.js @@ -21,45 +21,45 @@ limitations under the License. * Checks for losses in the sheet. */ function checkLosses() { - // Pulls data from the spreadsheet - const sheet = - SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Calculations"); - const source = sheet.getRange("A:G"); - const data = source.getValues(); + // Pulls data from the spreadsheet + const sheet = + SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Calculations"); + const source = sheet.getRange("A:G"); + const data = source.getValues(); - //Prepares the email alert content - let message = "Stocks:

    "; + //Prepares the email alert content + let message = "Stocks:

    "; - let send_message = false; + let send_message = false; - console.log("starting loop"); + 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 (const i in data) { - //Skips the first row - if (n++ === 0) continue; + //Loops through the cells in the spreadsheet to find cells where the stock fell below purchase price + let n = 0; + for (const i in data) { + //Skips the first row + if (n++ === 0) continue; - //Loads the current row - const row = data[i]; + //Loads the current row + const row = data[i]; - console.log(row[1]); - console.log(row[6]); + console.log(row[1]); + console.log(row[6]); - //Once at the end of the list, exits the loop - if (row[1] === "") break; + //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]}: ${(Number.parseFloat(row[6].toString()) * 100).toFixed(2).toString()}%
    `; - send_message = true; - } - } - if (!send_message) return; + //If value is below purchase price, adds stock ticker and difference to list of tax loss opportunities + if (row[6] < 0) { + message += `${row[1]}: ${(Number.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, - }); + MailApp.sendEmail({ + to: SpreadsheetApp.getActiveSpreadsheet().getOwner().getEmail(), + subject: "Tax-loss harvest", + htmlBody: message, + }); } diff --git a/solutions/automations/tax-loss-harvest-alerts/appsscript.json b/solutions/automations/tax-loss-harvest-alerts/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/tax-loss-harvest-alerts/appsscript.json +++ b/solutions/automations/tax-loss-harvest-alerts/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/timesheets/Code.js b/solutions/automations/timesheets/Code.js index 5c67d2bb0..0c787e685 100644 --- a/solutions/automations/timesheets/Code.js +++ b/solutions/automations/timesheets/Code.js @@ -19,14 +19,14 @@ limitations under the License. // Global variables representing the index of certain columns. const COLUMN_NUMBER = { - EMAIL: 2, - HOURS_START: 4, - HOURS_END: 8, - HOURLY_PAY: 9, - TOTAL_HOURS: 10, - CALC_PAY: 11, - APPROVAL: 12, - NOTIFY: 13, + EMAIL: 2, + HOURS_START: 4, + HOURS_END: 8, + HOURLY_PAY: 9, + TOTAL_HOURS: 10, + CALC_PAY: 11, + APPROVAL: 12, + NOTIFY: 13, }; // Global variables: @@ -39,12 +39,12 @@ const REJECTED_EMAIL_MESSAGE = "Your timesheet has not been approved."; * Creates the menu item "Timesheets" for user to run scripts on drop-down. */ function onOpen() { - const ui = SpreadsheetApp.getUi(); - ui.createMenu("Timesheets") - .addItem("Form setup", "setUpForm") - .addItem("Column setup", "columnSetup") - .addItem("Notify employees", "checkApprovedStatusToNotify") - .addToUi(); + const ui = SpreadsheetApp.getUi(); + ui.createMenu("Timesheets") + .addItem("Form setup", "setUpForm") + .addItem("Column setup", "columnSetup") + .addItem("Notify employees", "checkApprovedStatusToNotify") + .addToUi(); } /** @@ -55,17 +55,17 @@ function onOpen() { * employee has yet been e mailed. */ function columnSetup() { - const sheet = SpreadsheetApp.getActiveSheet(); - const lastCol = sheet.getLastColumn(); - const lastRow = sheet.getLastRow(); - const frozenRows = sheet.getFrozenRows(); - const beginningRow = frozenRows + 1; - const numRows = lastRow - frozenRows; + const sheet = SpreadsheetApp.getActiveSheet(); + const lastCol = sheet.getLastColumn(); + const lastRow = sheet.getLastRow(); + const frozenRows = sheet.getFrozenRows(); + const beginningRow = frozenRows + 1; + const numRows = lastRow - frozenRows; - // Calls helper functions to add new columns. - addCalculatePayColumn(sheet, beginningRow); - addApprovalColumn(sheet, beginningRow, numRows); - addNotifiedColumn(sheet, beginningRow, numRows); + // Calls helper functions to add new columns. + addCalculatePayColumn(sheet, beginningRow); + addApprovalColumn(sheet, beginningRow, numRows); + addNotifiedColumn(sheet, beginningRow, numRows); } /** @@ -76,18 +76,18 @@ function columnSetup() { * @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"); + 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)"); + // 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)"); } /** @@ -99,22 +99,22 @@ function addCalculatePayColumn(sheet, beginningRow) { * @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"); + sheet.insertColumnAfter(COLUMN_NUMBER.CALC_PAY); + sheet.getRange(1, COLUMN_NUMBER.APPROVAL).setValue("APPROVAL"); - // Make sure approval column is all drop-down menus. - const approvalColumnRange = sheet.getRange( - beginningRow, - COLUMN_NUMBER.APPROVAL, - numRows, - 1, - ); - const dropdownValues = ["APPROVED", "NOT APPROVED", "IN PROGRESS"]; - const rule = SpreadsheetApp.newDataValidation() - .requireValueInList(dropdownValues) - .build(); - approvalColumnRange.setDataValidation(rule); - approvalColumnRange.setValue("IN PROGRESS"); + // Make sure approval column is all drop-down menus. + const approvalColumnRange = sheet.getRange( + beginningRow, + COLUMN_NUMBER.APPROVAL, + numRows, + 1, + ); + const dropdownValues = ["APPROVED", "NOT APPROVED", "IN PROGRESS"]; + const rule = SpreadsheetApp.newDataValidation() + .requireValueInList(dropdownValues) + .build(); + approvalColumnRange.setDataValidation(rule); + approvalColumnRange.setValue("IN PROGRESS"); } /** @@ -126,22 +126,22 @@ function addApprovalColumn(sheet, beginningRow, numRows) { * @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"); + 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. - const 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"); + // Make sure notified column is all drop-down menus. + const 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"); } /** @@ -154,9 +154,9 @@ function addNotifiedColumn(sheet, beginningRow, numRows) { * @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"); + // Update notification status. + notifiedValues[i][0] = "NOTIFIED"; + sheet.getRange(i + beginningRow, COLUMN_NUMBER.NOTIFY).setValue("NOTIFIED"); } /** @@ -164,52 +164,52 @@ function updateNotifiedStatus(sheet, notifiedValues, i, beginningRow) { * to notify employees via email & update their notification status. */ function checkApprovedStatusToNotify() { - const sheet = SpreadsheetApp.getActiveSheet(); - const lastRow = sheet.getLastRow(); - const lastCol = sheet.getLastColumn(); - // lastCol here is the NOTIFIED column. - const frozenRows = sheet.getFrozenRows(); - const beginningRow = frozenRows + 1; - const numRows = lastRow - frozenRows; + const sheet = SpreadsheetApp.getActiveSheet(); + const lastRow = sheet.getLastRow(); + const lastCol = sheet.getLastColumn(); + // lastCol here is the NOTIFIED column. + const frozenRows = sheet.getFrozenRows(); + const beginningRow = frozenRows + 1; + const numRows = lastRow - frozenRows; - // Gets ranges of email, approval, and notified values for every employee. - const emailValues = sheet - .getRange(beginningRow, COLUMN_NUMBER.EMAIL, numRows, 1) - .getValues(); - const approvalValues = sheet - .getRange(beginningRow, COLUMN_NUMBER.APPROVAL, lastRow - frozenRows, 1) - .getValues(); - const notifiedValues = sheet - .getRange(beginningRow, COLUMN_NUMBER.NOTIFY, numRows, 1) - .getValues(); + // Gets ranges of email, approval, and notified values for every employee. + const emailValues = sheet + .getRange(beginningRow, COLUMN_NUMBER.EMAIL, numRows, 1) + .getValues(); + const approvalValues = sheet + .getRange(beginningRow, COLUMN_NUMBER.APPROVAL, lastRow - frozenRows, 1) + .getValues(); + const 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; - } - const emailAddress = emailValues[i][0]; - const approvalValue = approvalValues[i][0]; + // Traverses through employee's row. + for (let i = 0; i < numRows; i++) { + // Do not notify twice. + if (notifiedValues[i][0] === "NOTIFIED") { + continue; + } + const emailAddress = emailValues[i][0]; + const approvalValue = approvalValues[i][0]; - // Sends notifying emails & update status. - if (approvalValue === "IN PROGRESS") { - } 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); - } - } + // Sends notifying emails & update status. + if (approvalValue === "IN PROGRESS") { + } 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); + } + } } /** @@ -217,28 +217,28 @@ function checkApprovedStatusToNotify() { * send manager an email when a new request is submitted. */ function setUpForm() { - const sheet = SpreadsheetApp.getActiveSpreadsheet(); - if (sheet.getFormUrl()) { - const msg = "Form already exists. Unlink the form and try again."; - SpreadsheetApp.getUi().alert(msg); - return; - } + const sheet = SpreadsheetApp.getActiveSpreadsheet(); + if (sheet.getFormUrl()) { + const msg = "Form already exists. Unlink the form and try again."; + SpreadsheetApp.getUi().alert(msg); + return; + } - // Create the form. - const 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); + // Create the form. + const 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(); + // Set up on form submit trigger. + ScriptApp.newTrigger("onFormSubmit").forForm(form).onFormSubmit().create(); } /** @@ -247,23 +247,23 @@ function setUpForm() { * @param {Object} event Form submit event */ function onFormSubmit(event) { - const response = getResponsesByName(event.response); + const response = getResponsesByName(event.response); - // Load form responses into a new row. - const 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:"], - ]; - const sheet = SpreadsheetApp.getActiveSpreadsheet(); - sheet.appendRow(row); + // Load form responses into a new row. + const 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:"], + ]; + const sheet = SpreadsheetApp.getActiveSpreadsheet(); + sheet.appendRow(row); } /** @@ -274,13 +274,13 @@ function onFormSubmit(event) { * @return {Object} Form values keyed by question title */ function getResponsesByName(response) { - const initialValue = { - email: response.getRespondentEmail(), - timestamp: response.getTimestamp(), - }; - return response.getItemResponses().reduce((obj, itemResponse) => { - const key = itemResponse.getItem().getTitle(); - obj[key] = itemResponse.getResponse(); - return obj; - }, initialValue); + const initialValue = { + email: response.getRespondentEmail(), + timestamp: response.getTimestamp(), + }; + return response.getItemResponses().reduce((obj, itemResponse) => { + const key = itemResponse.getItem().getTitle(); + obj[key] = itemResponse.getResponse(); + return obj; + }, initialValue); } diff --git a/solutions/automations/timesheets/appsscript.json b/solutions/automations/timesheets/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/timesheets/appsscript.json +++ b/solutions/automations/timesheets/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/upload-files/Code.js b/solutions/automations/upload-files/Code.js index 2198d7f56..0f01bbd4f 100644 --- a/solutions/automations/upload-files/Code.js +++ b/solutions/automations/upload-files/Code.js @@ -35,54 +35,54 @@ const APP_SUBFOLDER_NONE = ""; * @param {object} event - Form submit. */ function onFormSubmit(e) { - try { - // Gets the application root folder. - let destFolder = getFolder_(APP_FOLDER_NAME); - - // Gets all form responses. - const itemResponses = e.response.getItemResponses(); - - // Determines the subfolder to route the file to, if any. - let subFolderName; - const 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: + try { + // Gets the application root folder. + let destFolder = getFolder_(APP_FOLDER_NAME); + + // Gets all form responses. + const itemResponses = e.response.getItemResponses(); + + // Determines the subfolder to route the file to, if any. + let subFolderName; + const 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. - const fileUploads = itemResponses - .filter( - (itemResponse) => - itemResponse.getItem().getType().toString() === "FILE_UPLOAD", - ) - .map((itemResponse) => itemResponse.getResponse()) - .reduce((a, b) => a.concat(b), []); - - // Moves the files to the destination folder. - if (fileUploads.length > 0) { - for (const fileId of fileUploads) { - DriveApp.getFileById(fileId).moveTo(destFolder); - console.log(`File Copied: ${fileId}`); - } - } - } catch (err) { - console.log(err); - } + // Gets the file upload response as an array to allow for multiple files. + const fileUploads = itemResponses + .filter( + (itemResponse) => + itemResponse.getItem().getType().toString() === "FILE_UPLOAD", + ) + .map((itemResponse) => itemResponse.getResponse()) + .reduce((a, b) => a.concat(b), []); + + // Moves the files to the destination folder. + if (fileUploads.length > 0) { + for (const fileId of fileUploads) { + DriveApp.getFileById(fileId).moveTo(destFolder); + console.log(`File Copied: ${fileId}`); + } + } + } catch (err) { + console.log(err); + } } /** @@ -95,22 +95,22 @@ function onFormSubmit(e) { * @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()) { - const 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.`, - ); + // Iterates subfolders of parent folder to check if folder already exists. + const subFolders = objParentFolder.getFolders(); + while (subFolders.hasNext()) { + const 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.`, + ); } // [END apps_script_upload_files] diff --git a/solutions/automations/upload-files/Setup.js b/solutions/automations/upload-files/Setup.js index 25d3c3f77..2282b02a3 100644 --- a/solutions/automations/upload-files/Setup.js +++ b/solutions/automations/upload-files/Setup.js @@ -24,18 +24,18 @@ * - 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. + // 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_(); + } else { + console.log("Could not setup application folder."); + } + // Calls the function that creates the Forms onSubmit trigger. + installTrigger_(); } /** @@ -47,27 +47,27 @@ function setUp() { * @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(); + // 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()) { - const folder = subFolders.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()) { + const 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.`, - ); + // 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.`, + ); } /** @@ -76,33 +76,33 @@ function getFolder_(folderName) { * Called by setup(). */ function installTrigger_() { - // Ensures existing trigger doesn't already exist. - const propTriggerId = - PropertiesService.getScriptProperties().getProperty("triggerUniqueId"); - if (propTriggerId !== null) { - const triggers = ScriptApp.getProjectTriggers(); - for (const 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. - const 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}`, - ); + // Ensures existing trigger doesn't already exist. + const propTriggerId = + PropertiesService.getScriptProperties().getProperty("triggerUniqueId"); + if (propTriggerId !== null) { + const triggers = ScriptApp.getProjectTriggers(); + for (const 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. + const 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}`, + ); } /** @@ -110,19 +110,19 @@ function installTrigger_() { * Use primarily to test setup routines. */ function removeTriggersAndScriptProperties() { - PropertiesService.getScriptProperties().deleteAllProperties(); - // Removes all triggers associated with project. - const triggers = ScriptApp.getProjectTriggers(); - for (const t in triggers) { - ScriptApp.deleteTrigger(triggers[t]); - } + PropertiesService.getScriptProperties().deleteAllProperties(); + // Removes all triggers associated with project. + const triggers = ScriptApp.getProjectTriggers(); + for (const t in triggers) { + ScriptApp.deleteTrigger(triggers[t]); + } } /** * Removes all form responses to reset the form. */ function deleteAllResponses() { - FormApp.getActiveForm().deleteAllResponses(); + FormApp.getActiveForm().deleteAllResponses(); } // [END apps_script_upload_files_setup] diff --git a/solutions/automations/upload-files/appsscript.json b/solutions/automations/upload-files/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/upload-files/appsscript.json +++ b/solutions/automations/upload-files/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/vacation-calendar/Code.js b/solutions/automations/vacation-calendar/Code.js index 3fedc8143..26125c66e 100644 --- a/solutions/automations/vacation-calendar/Code.js +++ b/solutions/automations/vacation-calendar/Code.js @@ -35,13 +35,13 @@ const MONTHS_IN_ADVANCE = 3; * Sets up the script to run automatically every hour. */ function setup() { - const 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(); + const 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(); } /** @@ -49,38 +49,38 @@ function setup() { * 'vacation' or 'out of office' events to the team calendar. */ function sync() { - // Defines the calendar event date range to search. - const today = new Date(); - const 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 = getAllMembers(GROUP_EMAIL); - if (ONLY_DIRECT_MEMBERS) { - users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers(); - } else if (Array.isArray(GROUP_EMAIL)) { - users = getUsersFromGroups(GROUP_EMAIL); - } - - // 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; - for (const user of users) { - const username = user.getEmail().split("@")[0]; - const events = findEvents(user, today, maxDate, lastRun); - for (const event of events) { - importEvent(username, event); - count++; - } - } - - PropertiesService.getScriptProperties().setProperty("lastRun", today); - console.log(`Imported ${count} events`); + // Defines the calendar event date range to search. + const today = new Date(); + const 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 = getAllMembers(GROUP_EMAIL); + if (ONLY_DIRECT_MEMBERS) { + users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers(); + } else if (Array.isArray(GROUP_EMAIL)) { + users = getUsersFromGroups(GROUP_EMAIL); + } + + // 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; + for (const user of users) { + const username = user.getEmail().split("@")[0]; + const events = findEvents(user, today, maxDate, lastRun); + for (const event of events) { + importEvent(username, event); + count++; + } + } + + PropertiesService.getScriptProperties().setProperty("lastRun", today); + console.log(`Imported ${count} events`); } /** @@ -90,29 +90,29 @@ function sync() { * @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 = []; - - // 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"; - event.outOfOfficeProperties = undefined; - event.focusTimeProperties = undefined; - } - - 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(), - ); - } + event.summary = `[${username}] ${event.summary}`; + event.organizer = { + 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"; + event.outOfOfficeProperties = undefined; + event.focusTimeProperties = undefined; + } + + 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(), + ); + } } /** @@ -127,38 +127,38 @@ function importEvent(username, event) { * @return {Calendar.Event[]} An array of calendar events. */ function findEvents(user, start, end, optSince) { - const params = { - eventTypes: "outOfOffice", - 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); - pageToken = response.nextPageToken; - } while (pageToken); - return events; + const params = { + eventTypes: "outOfOffice", + 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); + pageToken = response.nextPageToken; + } while (pageToken); + return events; } /** @@ -168,7 +168,7 @@ function findEvents(user, start, end, optSince) { * @return {string} a formatted date string. */ function formatDateAsRFC3339(date) { - return Utilities.formatDate(date, "UTC", "yyyy-MM-dd'T'HH:mm:ssZ"); + return Utilities.formatDate(date, "UTC", "yyyy-MM-dd'T'HH:mm:ssZ"); } /** @@ -177,24 +177,24 @@ function formatDateAsRFC3339(date) { * @return {object} direct and indirect members. */ function getAllMembers(groupEmail) { - const group = GroupsApp.getGroupByEmail(groupEmail); - let users = group.getUsers(); - const childGroups = group.getGroups(); - for (let i = 0; i < childGroups.length; i++) { - const childGroup = childGroups[i]; - users = users.concat(getAllMembers(childGroup.getEmail())); - } - // Remove duplicate members - const uniqueUsers = []; - const userEmails = {}; - for (let i = 0; i < users.length; i++) { - const user = users[i]; - if (!userEmails[user.getEmail()]) { - uniqueUsers.push(user); - userEmails[user.getEmail()] = true; - } - } - return uniqueUsers; + const group = GroupsApp.getGroupByEmail(groupEmail); + let users = group.getUsers(); + const childGroups = group.getGroups(); + for (let i = 0; i < childGroups.length; i++) { + const childGroup = childGroups[i]; + users = users.concat(getAllMembers(childGroup.getEmail())); + } + // Remove duplicate members + const uniqueUsers = []; + const userEmails = {}; + for (let i = 0; i < users.length; i++) { + const user = users[i]; + if (!userEmails[user.getEmail()]) { + uniqueUsers.push(user); + userEmails[user.getEmail()] = true; + } + } + return uniqueUsers; } /** @@ -203,14 +203,14 @@ function getAllMembers(groupEmail) { * @return {object} indirect members of multiple groups. */ function getUsersFromGroups(groupEmails) { - const users = []; - for (const groupEmail of groupEmails) { - const groupUsers = GroupsApp.getGroupByEmail(groupEmail).getUsers(); - for (const user of groupUsers) { - if (!users.some((u) => u.getEmail() === user.getEmail())) { - users.push(user); - } - } - } - return users; + const users = []; + for (const groupEmail of groupEmails) { + const groupUsers = GroupsApp.getGroupByEmail(groupEmail).getUsers(); + for (const user of groupUsers) { + if (!users.some((u) => u.getEmail() === user.getEmail())) { + users.push(user); + } + } + } + return users; } diff --git a/solutions/automations/vacation-calendar/appsscript.json b/solutions/automations/vacation-calendar/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/vacation-calendar/appsscript.json +++ b/solutions/automations/vacation-calendar/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/automations/youtube-tracker/Code.js b/solutions/automations/youtube-tracker/Code.js index 08f6af555..f15e6cb84 100644 --- a/solutions/automations/youtube-tracker/Code.js +++ b/solutions/automations/youtube-tracker/Code.js @@ -22,8 +22,8 @@ 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", + VIDEO: "Video Link", + TITLE: "Video Title", }; /** @@ -33,70 +33,70 @@ const COLUMN_NAME = { * when videos have new comments or replies. */ function markVideos() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets(); - - // Runs through process for each tab in Spreadsheet. - for (const dataSheet of sheets) { - const tabName = dataSheet.getName(); - const range = dataSheet.getDataRange(); - const numRows = range.getNumRows(); - const rows = range.getValues(); - const headerRow = rows[0]; - - // Finds the column indices. - const videoColumnIdx = headerRow.indexOf(COLUMN_NAME.VIDEO); - const titleColumnIdx = headerRow.indexOf(COLUMN_NAME.TITLE); - - // Creates empty array to collect data for email table. - const emailContent = []; - - // Processes each row in spreadsheet. - for (let i = 1; i < numRows; ++i) { - const row = rows[i]; - // Extracts video ID. - const 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. - const detailsResponse = getVideoDetails(videoId); - const title = detailsResponse.items[0].snippet.title; - const publishDate = detailsResponse.items[0].snippet.publishedAt; - const publishDateFormatted = new Date(publishDate); - const views = detailsResponse.items[0].statistics.viewCount; - const likes = detailsResponse.items[0].statistics.likeCount; - const comments = detailsResponse.items[0].statistics.commentCount; - const channel = detailsResponse.items[0].snippet.channelTitle; - - // Collects title, publish date, channel, views, comments, likes details and pastes into tab. - const 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. - const addlCommentCount = comments - row[titleColumnIdx + 4]; - - // Adds video title, link, and additional comment count to table if new counts > old counts. - if (addlCommentCount > 0) { - const 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); - } - } + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets(); + + // Runs through process for each tab in Spreadsheet. + for (const dataSheet of sheets) { + const tabName = dataSheet.getName(); + const range = dataSheet.getDataRange(); + const numRows = range.getNumRows(); + const rows = range.getValues(); + const headerRow = rows[0]; + + // Finds the column indices. + const videoColumnIdx = headerRow.indexOf(COLUMN_NAME.VIDEO); + const titleColumnIdx = headerRow.indexOf(COLUMN_NAME.TITLE); + + // Creates empty array to collect data for email table. + const emailContent = []; + + // Processes each row in spreadsheet. + for (let i = 1; i < numRows; ++i) { + const row = rows[i]; + // Extracts video ID. + const 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. + const detailsResponse = getVideoDetails(videoId); + const title = detailsResponse.items[0].snippet.title; + const publishDate = detailsResponse.items[0].snippet.publishedAt; + const publishDateFormatted = new Date(publishDate); + const views = detailsResponse.items[0].statistics.viewCount; + const likes = detailsResponse.items[0].statistics.likeCount; + const comments = detailsResponse.items[0].statistics.commentCount; + const channel = detailsResponse.items[0].snippet.channelTitle; + + // Collects title, publish date, channel, views, comments, likes details and pastes into tab. + const 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. + const addlCommentCount = comments - row[titleColumnIdx + 4]; + + // Adds video title, link, and additional comment count to table if new counts > old counts. + if (addlCommentCount > 0) { + const 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); + } + } } /** @@ -104,9 +104,9 @@ function markVideos() { * using YouTube advanced service. */ function getVideoDetails(videoId) { - const part = "snippet,statistics"; - const response = YouTube.Videos.list(part, { id: videoId }); - return response; + const part = "snippet,statistics"; + const response = YouTube.Videos.list(part, { id: videoId }); + return response; } /** @@ -114,12 +114,12 @@ function getVideoDetails(videoId) { * (h/t https://stackoverflow.com/a/3452617) */ function extractVideoIdFromUrl(url) { - let videoId = url.split("v=")[1]; - const ampersandPosition = videoId.indexOf("&"); - if (ampersandPosition !== -1) { - videoId = videoId.substring(0, ampersandPosition); - } - return videoId; + let videoId = url.split("v=")[1]; + const ampersandPosition = videoId.indexOf("&"); + if (ampersandPosition !== -1) { + videoId = videoId.substring(0, ampersandPosition); + } + return videoId; } /** @@ -127,13 +127,13 @@ function extractVideoIdFromUrl(url) { * (h/t https://stackoverflow.com/questions/37863392/making-table-in-google-apps-script-from-array) */ function sendEmailNotificationTemplate(content, emailAddress) { - const template = HtmlService.createTemplateFromFile("email"); - template.content = content; - const msg = template.evaluate(); - MailApp.sendEmail( - emailAddress, - "New comments or replies on YouTube", - msg.getContent(), - { htmlBody: msg.getContent() }, - ); + const template = HtmlService.createTemplateFromFile("email"); + template.content = content; + const msg = template.evaluate(); + MailApp.sendEmail( + emailAddress, + "New comments or replies on YouTube", + msg.getContent(), + { htmlBody: msg.getContent() }, + ); } diff --git a/solutions/automations/youtube-tracker/appsscript.json b/solutions/automations/youtube-tracker/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/automations/youtube-tracker/appsscript.json +++ b/solutions/automations/youtube-tracker/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/custom-functions/calculate-driving-distance/Code.js b/solutions/custom-functions/calculate-driving-distance/Code.js index 765ff45dd..2fd506442 100644 --- a/solutions/custom-functions/calculate-driving-distance/Code.js +++ b/solutions/custom-functions/calculate-driving-distance/Code.js @@ -26,17 +26,17 @@ limitations under the License. * 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 - console.log(`Failed with error: %s${e.error}`); - } + 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 + console.log(`Failed with error: %s${e.error}`); + } } /** @@ -46,10 +46,10 @@ function onOpen() { * @return {Number} The distance in miles. */ function metersToMiles(meters) { - if (typeof meters !== "number") { - return null; - } - return (meters / 1000) * 0.621371; + if (typeof meters !== "number") { + return null; + } + return (meters / 1000) * 0.621371; } /** @@ -60,34 +60,34 @@ function metersToMiles(meters) { * @return {Number} The distance in meters. */ function drivingDistance(origin, destination) { - const directions = getDirections_(origin, destination); - return directions.routes[0].legs[0].distance.value; + 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 - console.log(`Failed with error: %s${e.error}`); - } + 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 + console.log(`Failed with error: %s${e.error}`); + } } /** @@ -95,107 +95,107 @@ function prepareSheet_() { * 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 ( - Number.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 - console.log(`Failed with error: %s${e.error}`); - } + 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 ( + Number.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 + console.log(`Failed with error: %s${e.error}`); + } } /** @@ -207,19 +207,19 @@ function generateStepByStep_() { * 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); + 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); } /** @@ -231,12 +231,12 @@ function setAlternatingRowBackgroundColors_(range, oddColor, evenColor) { * @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; + const directionFinder = Maps.newDirectionFinder(); + directionFinder.setOrigin(origin); + directionFinder.setDestination(destination); + const directions = directionFinder.getDirections(); + if (directions.status !== "OK") { + throw directions.error_message; + } + return directions; } diff --git a/solutions/custom-functions/calculate-driving-distance/appsscript.json b/solutions/custom-functions/calculate-driving-distance/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/custom-functions/calculate-driving-distance/appsscript.json +++ b/solutions/custom-functions/calculate-driving-distance/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/custom-functions/summarize-sheets-data/Code.js b/solutions/custom-functions/summarize-sheets-data/Code.js index 61d9237f5..eb744e240 100644 --- a/solutions/custom-functions/summarize-sheets-data/Code.js +++ b/solutions/custom-functions/summarize-sheets-data/Code.js @@ -33,72 +33,72 @@ const PM_SHEET_NAME = "Summary"; * until all summary data is gathered. Then the script writes the summary array starting at the cell of the custom function. */ function getSheetsData() { - const ss = SpreadsheetApp.getActiveSpreadsheet(); - const sheets = ss.getSheets(); - const outputArr = []; + const ss = SpreadsheetApp.getActiveSpreadsheet(); + const sheets = ss.getSheets(); + const outputArr = []; - // For each sheet, summarizes the data and pushes to a temporary array. - for (const s in sheets) { - // Gets sheet name. - const sheetNm = sheets[s].getName(); - // Skips ReadMe and Summary sheets. - if (sheetNm === READ_ME_SHEET_NAME || sheetNm === PM_SHEET_NAME) { - continue; - } - // Gets sheets data. - const values = sheets[s].getDataRange().getValues(); - // Gets the first row of the sheet which is the header row. - const 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. - const columnOwner = headerRowValues.indexOf("Owner Name"); - const 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. - const owner = values[0][columnOwner]; - // Counts the total number of tasks. - const 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. - const completeCnt = filterByPosition( - values, - "Complete", - columnStatus, - ).length; - // Counts the number of tasks that have the 'In-Progress' status. - const inProgressCnt = filterByPosition( - values, - "In-Progress", - columnStatus, - ).length; - // Counts the number of tasks that have the 'Scheduled' status. - const scheduledCnt = filterByPosition( - values, - "Scheduled", - columnStatus, - ).length; - // Counts the number of tasks that have the 'Overdue' status. - const 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; + // For each sheet, summarizes the data and pushes to a temporary array. + for (const s in sheets) { + // Gets sheet name. + const sheetNm = sheets[s].getName(); + // Skips ReadMe and Summary sheets. + if (sheetNm === READ_ME_SHEET_NAME || sheetNm === PM_SHEET_NAME) { + continue; + } + // Gets sheets data. + const values = sheets[s].getDataRange().getValues(); + // Gets the first row of the sheet which is the header row. + const 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. + const columnOwner = headerRowValues.indexOf("Owner Name"); + const 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. + const owner = values[0][columnOwner]; + // Counts the total number of tasks. + const 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. + const completeCnt = filterByPosition( + values, + "Complete", + columnStatus, + ).length; + // Counts the number of tasks that have the 'In-Progress' status. + const inProgressCnt = filterByPosition( + values, + "In-Progress", + columnStatus, + ).length; + // Counts the number of tasks that have the 'Scheduled' status. + const scheduledCnt = filterByPosition( + values, + "Scheduled", + columnStatus, + ).length; + // Counts the number of tasks that have the 'Overdue' status. + const 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); + return array.filter((innerArray) => innerArray[position] === find); } diff --git a/solutions/custom-functions/summarize-sheets-data/appsscript.json b/solutions/custom-functions/summarize-sheets-data/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/custom-functions/summarize-sheets-data/appsscript.json +++ b/solutions/custom-functions/summarize-sheets-data/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/custom-functions/tier-pricing/Code.js b/solutions/custom-functions/tier-pricing/Code.js index 6da7b5b93..032704cd6 100644 --- a/solutions/custom-functions/tier-pricing/Code.js +++ b/solutions/custom-functions/tier-pricing/Code.js @@ -34,17 +34,17 @@ limitations under the License. * */ function tierPrice(value, table) { - let total = 0; - // Creates an array for each row of the table and loops through each array. - for (const [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. - const 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; + let total = 0; + // Creates an array for each row of the table and loops through each array. + for (const [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. + const 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; } diff --git a/solutions/custom-functions/tier-pricing/appsscript.json b/solutions/custom-functions/tier-pricing/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/custom-functions/tier-pricing/appsscript.json +++ b/solutions/custom-functions/tier-pricing/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/editor-add-on/clean-sheet/Code.js b/solutions/editor-add-on/clean-sheet/Code.js index 7753c8a47..7bd607a85 100644 --- a/solutions/editor-add-on/clean-sheet/Code.js +++ b/solutions/editor-add-on/clean-sheet/Code.js @@ -30,64 +30,64 @@ const APP_TITLE = "Clean sheet"; * Called from menu option. */ function deleteEmptyRows() { - const sheet = SpreadsheetApp.getActiveSheet(); - - // Gets active selection and dimensions. - const 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. - for (const [start, end] of rangesToDelete.reverse()) { - sheet.deleteRows(start, end - start + 1); - } - SpreadsheetApp.flush(); + const sheet = SpreadsheetApp.getActiveSheet(); + + // Gets active selection and dimensions. + const 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. + for (const [start, end] of rangesToDelete.reverse()) { + sheet.deleteRows(start, end - start + 1); + } + SpreadsheetApp.flush(); } /** @@ -100,69 +100,69 @@ function deleteEmptyRows() { * Called from menu option. */ function deleteEmptyColumns() { - const sheet = SpreadsheetApp.getActiveSheet(); - - // Gets active selection and dimensions. - const 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. - for (const [start, end] of rangesToDelete.reverse()) { - sheet.deleteColumns(start, end - start + 1); - } - SpreadsheetApp.flush(); + const sheet = SpreadsheetApp.getActiveSheet(); + + // Gets active selection and dimensions. + const 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. + for (const [start, end] of rangesToDelete.reverse()) { + sheet.deleteColumns(start, end - start + 1); + } + SpreadsheetApp.flush(); } /** @@ -171,29 +171,29 @@ function deleteEmptyColumns() { * Called from menu option. */ function cropSheet() { - const dataRange = SpreadsheetApp.getActiveSheet().getDataRange(); - const sheet = dataRange.getSheet(); + const dataRange = SpreadsheetApp.getActiveSheet().getDataRange(); + const sheet = dataRange.getSheet(); - let numRows = dataRange.getNumRows(); - let numColumns = dataRange.getNumColumns(); + let numRows = dataRange.getNumRows(); + let numColumns = dataRange.getNumColumns(); - const maxRows = sheet.getMaxRows(); - const maxColumns = sheet.getMaxColumns(); + const maxRows = sheet.getMaxRows(); + const maxColumns = sheet.getMaxColumns(); - const numFrozenRows = sheet.getFrozenRows(); - const numFrozenColumns = sheet.getFrozenColumns(); + 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 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); - } + // 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); + } } /** @@ -203,47 +203,47 @@ function cropSheet() { * 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."); - } + 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."); + } } /** @@ -253,12 +253,12 @@ function fillDownData() { * @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. - let title = APP_TITLE; - if (caller != null) { - title += ` : ${caller}`; - } - - const ui = SpreadsheetApp.getUi(); - ui.alert(title, message, ui.ButtonSet.OK); + // Sets the title using the APP_TITLE variable; adds optional caller string. + let 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 index 451fc12fb..f0b627c1b 100644 --- a/solutions/editor-add-on/clean-sheet/Menu.js +++ b/solutions/editor-add-on/clean-sheet/Menu.js @@ -20,21 +20,21 @@ * @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. - const menu = SpreadsheetApp.getUi().createAddonMenu(); + // Builds a menu that displays under the Extensions menu in Sheets. + const 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(); + 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(); } /** @@ -45,7 +45,7 @@ function onOpen(e) { * @param {object} e The event parameter for a simple onInstall trigger. */ function onInstall(e) { - onOpen(e); + onOpen(e); } /** @@ -53,11 +53,11 @@ function onInstall(e) { * TODO: Personalize */ function aboutApp() { - const msg = ` + const msg = ` Name: ${APP_TITLE} Version: 1.0 Contact: `; - const ui = SpreadsheetApp.getUi(); - ui.alert("About this application", msg, ui.ButtonSet.OK); + const ui = SpreadsheetApp.getUi(); + ui.alert("About this application", msg, ui.ButtonSet.OK); } diff --git a/solutions/editor-add-on/clean-sheet/appsscript.json b/solutions/editor-add-on/clean-sheet/appsscript.json index 333cc15cb..e867f291e 100644 --- a/solutions/editor-add-on/clean-sheet/appsscript.json +++ b/solutions/editor-add-on/clean-sheet/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/New_York", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/New_York", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/solutions/ooo-assistant/appsscript.json b/solutions/ooo-assistant/appsscript.json index 116f55c66..c43ac48d2 100644 --- a/solutions/ooo-assistant/appsscript.json +++ b/solutions/ooo-assistant/appsscript.json @@ -1,45 +1,45 @@ { - "timeZone": "America/New_York", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Gmail", - "version": "v1", - "serviceId": "gmail" - }, - { - "userSymbol": "Calendar", - "version": "v3", - "serviceId": "calendar" - } - ] - }, - "addOns": { - "common": { - "name": "OOO Assistant", - "logoUrl": "https://goo.gle/3SfMkjb", - "homepageTrigger": { - "runFunction": "onHomepage" - }, - "universalActions": [ - { - "label": "Block day out", - "runFunction": "blockDayOut" - }, - { - "label": "Set auto reply", - "runFunction": "setAutoReply" - } - ] - }, - "chat": {}, - "calendar": {}, - "gmail": {}, - "drive": {}, - "docs": {}, - "sheets": {}, - "slides": {} - } + "timeZone": "America/New_York", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + }, + { + "userSymbol": "Calendar", + "version": "v3", + "serviceId": "calendar" + } + ] + }, + "addOns": { + "common": { + "name": "OOO Assistant", + "logoUrl": "https://goo.gle/3SfMkjb", + "homepageTrigger": { + "runFunction": "onHomepage" + }, + "universalActions": [ + { + "label": "Block day out", + "runFunction": "blockDayOut" + }, + { + "label": "Set auto reply", + "runFunction": "setAutoReply" + } + ] + }, + "chat": {}, + "calendar": {}, + "gmail": {}, + "drive": {}, + "docs": {}, + "sheets": {}, + "slides": {} + } } diff --git a/solutions/ooo-chat-app/Code.js b/solutions/ooo-chat-app/Code.js index 5ca78c4ff..3aae1a695 100644 --- a/solutions/ooo-chat-app/Code.js +++ b/solutions/ooo-chat-app/Code.js @@ -18,13 +18,13 @@ limitations under the License. * @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 }; + 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 }; } /** @@ -34,7 +34,7 @@ function onAddToSpace(event) { * @see https://developers.google.com/hangouts/chat/reference/message-formats/events */ function onRemoveFromSpace(event) { - console.log("App removed from ", event.space.name); + console.log("App removed from ", event.space.name); } /** @@ -43,77 +43,77 @@ function onRemoveFromSpace(event) { * @return {function} call the respective function */ function onMessage(event) { - const message = event.message; + 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(); - } - } + 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, - }, - }, - ], - }, - { - 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, - }, - }, - ], - }, - ], - header: { - title: "OOO app", - subtitle: "Helping you manage your OOO", - imageUrl: "https://goo.gle/3SfMkjb", - imageType: "SQUARE", - }, - }, - }, - ], - }; + 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, + }, + }, + { + 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, + }, + }, + ], + }, + ], + header: { + title: "OOO app", + subtitle: "Helping you manage your OOO", + imageUrl: "https://goo.gle/3SfMkjb", + imageType: "SQUARE", + }, + }, + }, + ], + }; } /** @@ -121,8 +121,8 @@ function createHelpCard() { * @return {object} JSON-formatted response */ function blockDayOut() { - blockOutCalendar(); - return createResponseCard("Your calendar has been blocked out for you."); + blockOutCalendar(); + return createResponseCard("Your calendar has been blocked out for you."); } /** @@ -130,8 +130,8 @@ function blockDayOut() { * @return {object} JSON-formatted response */ function cancelAllMeetings() { - cancelMeetings(); - return createResponseCard("All your meetings have been canceled."); + cancelMeetings(); + return createResponseCard("All your meetings have been canceled."); } /** @@ -139,109 +139,109 @@ function cancelAllMeetings() { * @return {object} JSON-formatted response */ function setAutoReply() { - turnOnAutoResponder(); - return createResponseCard("The out of office auto reply has been turned on."); + 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() { - /** - * 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 getDateAndHours(hour, minutes) { - const date = new Date(); - date.setHours(hour); - date.setMinutes(minutes); - date.setSeconds(0); - date.setMilliseconds(0); - return date.toISOString(); - } + /** + * 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 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, 0) }, - end: { dateTime: getDateAndHours(17, 0) }, - eventType: "outOfOffice", - summary: "Out of office", - outOfOfficeProperties: { - autoDeclineMode: "declineOnlyNewConflictingInvitations", - declineMessage: "Declined because I am taking a day of.", - }, - }; - Calendar.Events.insert(event, "primary"); + const event = { + start: { dateTime: getDateAndHours(9, 0) }, + end: { dateTime: getDateAndHours(17, 0) }, + eventType: "outOfOffice", + summary: "Out of office", + outOfOfficeProperties: { + autoDeclineMode: "declineOnlyNewConflictingInvitations", + declineMessage: "Declined because I am taking a day of.", + }, + }; + Calendar.Events.insert(event, "primary"); } /** * Declines all meetings for the day. */ function cancelMeetings() { - const events = CalendarApp.getEventsForDay(new Date()); + const events = CalendarApp.getEventsForDay(new Date()); - for (const event of events) { - if (event.getGuestList().length > 0) { - event.setMyStatus(CalendarApp.GuestStatus.NO); - } - } + for (const event of events) { + 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", - ); + 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", - }, - }, - }, - ], - }; + 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", + }, + }, + }, + ], + }; } diff --git a/solutions/ooo-chat-app/appsscript.json b/solutions/ooo-chat-app/appsscript.json index 94cbe5fcc..2d1fa08f5 100644 --- a/solutions/ooo-chat-app/appsscript.json +++ b/solutions/ooo-chat-app/appsscript.json @@ -1,20 +1,20 @@ { - "timeZone": "Europe/Madrid", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Gmail", - "version": "v1", - "serviceId": "gmail" - }, - { - "userSymbol": "Calendar", - "version": "v3", - "serviceId": "calendar" - } - ] - }, - "chat": {} + "timeZone": "Europe/Madrid", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + }, + { + "userSymbol": "Calendar", + "version": "v3", + "serviceId": "calendar" + } + ] + }, + "chat": {} } diff --git a/solutions/schedule-meetings/Code.js b/solutions/schedule-meetings/Code.js index f3a606953..ae81986aa 100644 --- a/solutions/schedule-meetings/Code.js +++ b/solutions/schedule-meetings/Code.js @@ -20,8 +20,8 @@ limitations under the License. // Application constants const APPNAME = "Chat Meeting Scheduler"; const SLASHCOMMAND = { - HELP: 1, // /help - DIALOG: 2, // /schedule_Meeting + HELP: 1, // /help + DIALOG: 2, // /schedule_Meeting }; /** @@ -33,22 +33,22 @@ const SLASHCOMMAND = { * @param {Object} event The event object from Google Chat */ function onAddToSpace(event) { - let message = ""; - - // Personalizes the message depending on how the Chat app 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 }; + let message = ""; + + // Personalizes the message depending on how the Chat app 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 }; } /** @@ -61,31 +61,31 @@ function onAddToSpace(event) { * @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: + // 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); */ - } - } - // Returns text if users didn't invoke a slash command. - return { text: "No action taken - use Slash Commands." }; + } + } + // Returns text if users didn't invoke a slash command. + return { text: "No action taken - use Slash Commands." }; } /** @@ -95,76 +95,76 @@ function onMessage(event) { * @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 || Number.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); - } - if (event.action.actionMethodName === "closeDialog") { - // Returns this dialog as success. - return { - actionResponse: { - type: "DIALOG", - dialog_action: { - actionStatus: "OK", - }, - }, - }; - } + 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 || Number.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); + } + if (event.action.actionMethodName === "closeDialog") { + // Returns this dialog as success. + return { + actionResponse: { + type: "DIALOG", + dialog_action: { + actionStatus: "OK", + }, + }, + }; + } } /** @@ -172,11 +172,11 @@ function onCardClick(event) { * @return {string} The help text as seen below */ function getHelpTextResponse_() { - const help = `*${APPNAME}* lets you quickly create meetings from Google Chat. Here\'s a list of all its commands: + const help = `*${APPNAME}* 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 apps at https://developers.google.com/chat.`; - return { text: help }; + return { text: help }; } diff --git a/solutions/schedule-meetings/Dialog.js b/solutions/schedule-meetings/Dialog.js index 9eba41372..b0f018723 100644 --- a/solutions/schedule-meetings/Dialog.js +++ b/solutions/schedule-meetings/Dialog.js @@ -19,17 +19,17 @@ * @return {object} JSON-formatted cards for the dialog. */ function getInputFormAsDialog_(options) { - const form = getForm_(options); - return { - actionResponse: { - type: "DIALOG", - dialogAction: { - dialog: { - body: form, - }, - }, - }, - }; + const form = getForm_(options); + return { + actionResponse: { + type: "DIALOG", + dialogAction: { + dialog: { + body: form, + }, + }, + }, + }; } /** @@ -37,119 +37,119 @@ function getInputFormAsDialog_(options) { * @return {object} JSON-formatted cards. */ function getForm_(options) { - const sections = []; + const sections = []; - // If errors present, display additional section with validation messages. - if (options.errors?.length) { - let errors = options.errors.reduce((str, err) => `${str}• ${err}
    `, ""); - errors = `Errors:
    ${errors}`; - const errorSection = { - widgets: [ - { - textParagraph: { - text: errors, - }, - }, - ], - }; - sections.push(errorSection); - } - const 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; + // If errors present, display additional section with validation messages. + if (options.errors?.length) { + let errors = options.errors.reduce((str, err) => `${str}• ${err}
    `, ""); + errors = `Errors:
    ${errors}`; + const errorSection = { + widgets: [ + { + textParagraph: { + text: errors, + }, + }, + ], + }; + sections.push(errorSection); + } + const 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; } /** @@ -158,52 +158,52 @@ function getForm_(options) { * @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", - }, - }, - }, - }, - }, - }, - }, - }, - }; + 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", + }, + }, + }, + }, + }, + }, + }, + }, + }; } diff --git a/solutions/schedule-meetings/Utilities.js b/solutions/schedule-meetings/Utilities.js index 88491b8be..6ecef73aa 100644 --- a/solutions/schedule-meetings/Utilities.js +++ b/solutions/schedule-meetings/Utilities.js @@ -19,7 +19,7 @@ * @return {string} */ function getFieldValue_(formInputs, fieldName) { - return formInputs[fieldName][""].stringInputs.value[0]; + return formInputs[fieldName][""].stringInputs.value[0]; } // Regular expression to validate the date/time input. @@ -30,17 +30,17 @@ const DATE_TIME_PATTERN = /\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d\d/; * @return {date} */ function getStartTimeAsDateObject_(dateTimeStr) { - if (!dateTimeStr || !dateTimeStr.match(DATE_TIME_PATTERN)) { - return null; - } + 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); + const parts = dateTimeStr.split(" "); + const [month, day, year] = parts[0].split("/").map(Number); + const [hour, minute] = parts[1].split(":").map(Number); - Session.getScriptTimeZone(); + Session.getScriptTimeZone(); - return new Date(year, month - 1, day, hour, minute); + return new Date(year, month - 1, day, hour, minute); } /** @@ -48,15 +48,15 @@ function getStartTimeAsDateObject_(dateTimeStr) { * @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", - ); + 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", + ); } /** @@ -67,11 +67,11 @@ function getTopOfHourDateString_() { * @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; + 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; } diff --git a/solutions/schedule-meetings/appsscript.json b/solutions/schedule-meetings/appsscript.json index f30cd857c..cf76895c9 100644 --- a/solutions/schedule-meetings/appsscript.json +++ b/solutions/schedule-meetings/appsscript.json @@ -1,8 +1,8 @@ { - "timeZone": "America/Los_Angeles", - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "chat": { - "addToSpaceFallbackMessage": "Thank you for adding this Chat App!" - } + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "chat": { + "addToSpaceFallbackMessage": "Thank you for adding this Chat App!" + } } diff --git a/tasks/simpleTasks/appsscript.json b/tasks/simpleTasks/appsscript.json index 43a2b4918..3442fee0f 100644 --- a/tasks/simpleTasks/appsscript.json +++ b/tasks/simpleTasks/appsscript.json @@ -1,13 +1,13 @@ { - "timeZone": "America/Los_Angeles", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Tasks", - "serviceId": "tasks", - "version": "v1" - } - ] - }, - "exceptionLogging": "STACKDRIVER" + "timeZone": "America/Los_Angeles", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Tasks", + "serviceId": "tasks", + "version": "v1" + } + ] + }, + "exceptionLogging": "STACKDRIVER" } diff --git a/tsconfig.json b/tsconfig.json index 732f2ff98..9904e82eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,15 @@ { - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "noEmit": true, - "target": "es2019", - "module": "commonjs", - "alwaysStrict": true, - "lib": ["es2019"], - "types": ["google-apps-script"], - "strict": true, - "noImplicitAny": true - }, - "include": ["**/*.gs", ".github/scripts/check-gs.ts"] + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "target": "es2019", + "module": "commonjs", + "alwaysStrict": true, + "lib": ["es2019"], + "types": ["google-apps-script"], + "strict": true, + "noImplicitAny": true + }, + "include": ["**/*.gs", ".github/scripts/check-gs.ts"] } diff --git a/wasm/hello-world/.clasp.json b/wasm/hello-world/.clasp.json index 92ce01f19..0b59e587d 100644 --- a/wasm/hello-world/.clasp.json +++ b/wasm/hello-world/.clasp.json @@ -1,4 +1,4 @@ { - "scriptId": "1xt1CvoUyFAzfoCdkwCHXBXzu3oaNz2a6iNsPW2GA6rOAsyBv66r4TarA", - "rootDir": "./dist" + "scriptId": "1xt1CvoUyFAzfoCdkwCHXBXzu3oaNz2a6iNsPW2GA6rOAsyBv66r4TarA", + "rootDir": "./dist" } diff --git a/wasm/hello-world/build.js b/wasm/hello-world/build.js index 45e2468e9..30c2fddc2 100644 --- a/wasm/hello-world/build.js +++ b/wasm/hello-world/build.js @@ -23,22 +23,22 @@ const outdir = "dist"; const sourceRoot = "src"; await esbuild.build({ - entryPoints: ["./src/wasm.js"], - bundle: true, - outdir, - sourceRoot, - platform: "neutral", - format: "esm", - plugins: [wasmLoader({ mode: "embedded" })], - inject: ["polyfill.js"], - minify: true, - banner: { js: "// Generated code DO NOT EDIT\n" }, + entryPoints: ["./src/wasm.js"], + bundle: true, + outdir, + sourceRoot, + platform: "neutral", + format: "esm", + plugins: [wasmLoader({ mode: "embedded" })], + inject: ["polyfill.js"], + minify: true, + banner: { js: "// Generated code DO NOT EDIT\n" }, }); const passThroughFiles = ["main.js", "test.js", "appsscript.json"]; await Promise.all( - passThroughFiles.map(async (file) => - fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), - ), + passThroughFiles.map(async (file) => + fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), + ), ); diff --git a/wasm/hello-world/package.json b/wasm/hello-world/package.json index 5d723d194..ddcdf6754 100644 --- a/wasm/hello-world/package.json +++ b/wasm/hello-world/package.json @@ -1,58 +1,58 @@ { - "name": "example", - "version": "0.1.0", - "description": "An example integration of WASM with Rust into Apps Script", - "scripts": { - "build": "wireit", - "build:rust": "wireit", - "build:wasm": "wireit", - "clean": "rm -rf dist pkg target", - "deploy": "wireit", - "format": "cargo fmt", - "start": "wireit" - }, - "wireit": { - "build": { - "command": "node build.js", - "dependencies": ["build:wasm"], - "files": ["src/*.js", "*.js", "package.json"], - "output": ["dist"] - }, - "build:rust": { - "command": "cargo build --release --target wasm32-unknown-unknown", - "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], - "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] - }, - "build:wasm": { - "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", - "dependencies": ["build:rust"], - "files": [ - "./target/wasm32-unknown-unknown/release/example_bg.wasm", - "package.json" - ], - "output": ["src/pkg"] - }, - "start": { - "command": "node dist/index.js", - "dependencies": ["build"] - }, - "deploy": { - "command": "clasp push -f", - "dependencies": ["build"], - "files": [".clasp.json", ".claspignore"] - } - }, - "author": "Justin Poehnelt ", - "license": "Apache-2.0", - "devDependencies": { - "@google/clasp": "^2.4.2", - "esbuild": "^0.20.1", - "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", - "vitest": "^1.3.1", - "wireit": "^0.14.4" - }, - "dependencies": { - "fastestsmallesttextencoderdecoder": "^1.0.22" - }, - "type": "module" + "name": "example", + "version": "0.1.0", + "description": "An example integration of WASM with Rust into Apps Script", + "scripts": { + "build": "wireit", + "build:rust": "wireit", + "build:wasm": "wireit", + "clean": "rm -rf dist pkg target", + "deploy": "wireit", + "format": "cargo fmt", + "start": "wireit" + }, + "wireit": { + "build": { + "command": "node build.js", + "dependencies": ["build:wasm"], + "files": ["src/*.js", "*.js", "package.json"], + "output": ["dist"] + }, + "build:rust": { + "command": "cargo build --release --target wasm32-unknown-unknown", + "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], + "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] + }, + "build:wasm": { + "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", + "dependencies": ["build:rust"], + "files": [ + "./target/wasm32-unknown-unknown/release/example_bg.wasm", + "package.json" + ], + "output": ["src/pkg"] + }, + "start": { + "command": "node dist/index.js", + "dependencies": ["build"] + }, + "deploy": { + "command": "clasp push -f", + "dependencies": ["build"], + "files": [".clasp.json", ".claspignore"] + } + }, + "author": "Justin Poehnelt ", + "license": "Apache-2.0", + "devDependencies": { + "@google/clasp": "^2.4.2", + "esbuild": "^0.20.1", + "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", + "vitest": "^1.3.1", + "wireit": "^0.14.4" + }, + "dependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22" + }, + "type": "module" } diff --git a/wasm/hello-world/polyfill.js b/wasm/hello-world/polyfill.js index c78434025..e2f434747 100644 --- a/wasm/hello-world/polyfill.js +++ b/wasm/hello-world/polyfill.js @@ -15,6 +15,6 @@ */ export { - TextEncoder, - TextDecoder, + TextEncoder, + TextDecoder, } from "fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js"; diff --git a/wasm/hello-world/src/appsscript.json b/wasm/hello-world/src/appsscript.json index d51e5c9d8..3ea28e5ae 100644 --- a/wasm/hello-world/src/appsscript.json +++ b/wasm/hello-world/src/appsscript.json @@ -1,6 +1,6 @@ { - "timeZone": "America/Denver", - "dependencies": {}, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8" + "timeZone": "America/Denver", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" } diff --git a/wasm/hello-world/src/main.js b/wasm/hello-world/src/main.js index df55df936..801cd8791 100644 --- a/wasm/hello-world/src/main.js +++ b/wasm/hello-world/src/main.js @@ -15,6 +15,6 @@ */ async function main() { - const name = "world"; - console.log(await hello_(name)); + const name = "world"; + console.log(await hello_(name)); } diff --git a/wasm/hello-world/src/test.js b/wasm/hello-world/src/test.js index 8c2fd3186..ef7d928e5 100644 --- a/wasm/hello-world/src/test.js +++ b/wasm/hello-world/src/test.js @@ -15,75 +15,75 @@ */ async function test() { - await assert(hello_("world"), "Hello, world from Rust!"); + await assert(hello_("world"), "Hello, world from Rust!"); } async function assert(a, b, message) { - const aVal = await a; - const bVal = await b; + const aVal = await a; + const bVal = await b; - if (aVal !== bVal) { - throw message ?? `'${aVal}' !== '${bVal}'`; - } + if (aVal !== bVal) { + throw message ?? `'${aVal}' !== '${bVal}'`; + } } async function latency(func, iterations, argsFunc = () => []) { - const executionTimes = []; + const executionTimes = []; - for (let i = 0; i < iterations; i++) { - const args = argsFunc(); + for (let i = 0; i < iterations; i++) { + const args = argsFunc(); - const startTime = Date.now(); - let endTime; + const startTime = Date.now(); + let endTime; - try { - await func(...args); - endTime = Date.now(); - } catch (e) { - endTime = Number.POSITIVE_INFINITY; - console.error(e); - continue; - } + try { + await func(...args); + endTime = Date.now(); + } catch (e) { + endTime = Number.POSITIVE_INFINITY; + console.error(e); + continue; + } - executionTimes.push(endTime - startTime); - } + executionTimes.push(endTime - startTime); + } - // Calculate statistics - const min = Math.min(...executionTimes); - const max = Math.max(...executionTimes); - const totalTime = executionTimes.reduce((sum, time) => sum + time, 0); - const average = totalTime / iterations; + // Calculate statistics + const min = Math.min(...executionTimes); + const max = Math.max(...executionTimes); + const totalTime = executionTimes.reduce((sum, time) => sum + time, 0); + const average = totalTime / iterations; - return { - min: min, - max: max, - average: average, - totalTime, - // times: executionTimes // Array of all execution times - }; + return { + min: min, + max: max, + average: average, + totalTime, + // times: executionTimes // Array of all execution times + }; } async function benchmark() { - await hello_("world"); // Warmup + await hello_("world"); // Warmup - console.log(await latency(hello_, 100, () => [generateRandomString(10)])); - console.log(await latency(hello_, 100, () => [generateRandomString(100)])); - console.log(await latency(hello_, 100, () => [generateRandomString(1000)])); - console.log(await latency(hello_, 100, () => [generateRandomString(10000)])); - console.log(await latency(hello_, 100, () => [generateRandomString(100000)])); - console.log(await latency(hello_, 30, () => [generateRandomString(1000000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(10)])); + console.log(await latency(hello_, 100, () => [generateRandomString(100)])); + console.log(await latency(hello_, 100, () => [generateRandomString(1000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(10000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(100000)])); + console.log(await latency(hello_, 30, () => [generateRandomString(1000000)])); } function generateRandomString(length = 1024) { - // Choose your desired character set - const characters = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - const charactersLength = characters.length; + // Choose your desired character set + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; - let result = ""; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } + let result = ""; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } - return result; + return result; } diff --git a/wasm/hello-world/src/wasm.js b/wasm/hello-world/src/wasm.js index 6495e781c..560a02217 100644 --- a/wasm/hello-world/src/wasm.js +++ b/wasm/hello-world/src/wasm.js @@ -20,12 +20,12 @@ * @returns */ async function hello_(name) { - const wasm = await import("./pkg/example_bg.wasm"); - const { __wbg_set_wasm, hello } = await import("./pkg/example_bg.js"); + const wasm = await import("./pkg/example_bg.wasm"); + const { __wbg_set_wasm, hello } = await import("./pkg/example_bg.js"); - __wbg_set_wasm(wasm); + __wbg_set_wasm(wasm); - return hello(name); + return hello(name); } globalThis.hello_ = hello_; diff --git a/wasm/image-add-on/.clasp.json b/wasm/image-add-on/.clasp.json index 6f4e159ea..ae82bd7ba 100644 --- a/wasm/image-add-on/.clasp.json +++ b/wasm/image-add-on/.clasp.json @@ -1,4 +1,4 @@ { - "scriptId": "1gP1tiV1KkhVbMADIA_M-d4IJP1GNXwU7-7MundfqlESmSAdo0sC_Nml4", - "rootDir": "./dist" + "scriptId": "1gP1tiV1KkhVbMADIA_M-d4IJP1GNXwU7-7MundfqlESmSAdo0sC_Nml4", + "rootDir": "./dist" } diff --git a/wasm/image-add-on/build.js b/wasm/image-add-on/build.js index 4b8026de6..c716d95e1 100644 --- a/wasm/image-add-on/build.js +++ b/wasm/image-add-on/build.js @@ -23,22 +23,22 @@ const outdir = "dist"; const sourceRoot = "src"; await esbuild.build({ - entryPoints: ["./src/wasm.js"], - bundle: true, - outdir, - sourceRoot, - platform: "neutral", - format: "esm", - plugins: [wasmLoader({ mode: "embedded" })], - inject: ["polyfill.js"], - minify: true, - banner: { js: "// Generated code DO NOT EDIT\n" }, + entryPoints: ["./src/wasm.js"], + bundle: true, + outdir, + sourceRoot, + platform: "neutral", + format: "esm", + plugins: [wasmLoader({ mode: "embedded" })], + inject: ["polyfill.js"], + minify: true, + banner: { js: "// Generated code DO NOT EDIT\n" }, }); const passThroughFiles = ["main.js", "test.js", "appsscript.json", "add-on.js"]; await Promise.all( - passThroughFiles.map(async (file) => - fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), - ), + passThroughFiles.map(async (file) => + fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), + ), ); diff --git a/wasm/image-add-on/package.json b/wasm/image-add-on/package.json index 35ab9d6f0..4b8113ef8 100644 --- a/wasm/image-add-on/package.json +++ b/wasm/image-add-on/package.json @@ -1,59 +1,59 @@ { - "name": "example", - "version": "0.1.0", - "description": "An example integration of WASM with Rust into Apps Script", - "scripts": { - "build": "wireit", - "build:rust": "wireit", - "build:wasm": "wireit", - "clean": "rm -rf dist pkg target", - "deploy": "wireit", - "format": "cargo fmt", - "start": "wireit" - }, - "wireit": { - "build": { - "command": "node build.js", - "dependencies": ["build:wasm"], - "files": ["src/*.js", "src/*.json", "*.js", "package.json"], - "output": ["dist"] - }, - "build:rust": { - "command": "cargo build --release --target wasm32-unknown-unknown", - "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], - "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] - }, - "build:wasm": { - "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", - "dependencies": ["build:rust"], - "files": [ - "./target/wasm32-unknown-unknown/release/example_bg.wasm", - "package.json" - ], - "output": ["src/pkg"] - }, - "start": { - "command": "node dist/index.js", - "dependencies": ["build"] - }, - "deploy": { - "command": "clasp push -f", - "dependencies": ["build"], - "files": [".clasp.json", ".claspignore"] - } - }, - "author": "Justin Poehnelt ", - "license": "Apache-2.0", - "devDependencies": { - "@google/clasp": "^2.4.2", - "esbuild": "^0.20.1", - "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", - "vitest": "^1.3.1", - "wireit": "^0.14.4" - }, - "dependencies": { - "@types/google-apps-script": "^1.0.82", - "fastestsmallesttextencoderdecoder": "^1.0.22" - }, - "type": "module" + "name": "example", + "version": "0.1.0", + "description": "An example integration of WASM with Rust into Apps Script", + "scripts": { + "build": "wireit", + "build:rust": "wireit", + "build:wasm": "wireit", + "clean": "rm -rf dist pkg target", + "deploy": "wireit", + "format": "cargo fmt", + "start": "wireit" + }, + "wireit": { + "build": { + "command": "node build.js", + "dependencies": ["build:wasm"], + "files": ["src/*.js", "src/*.json", "*.js", "package.json"], + "output": ["dist"] + }, + "build:rust": { + "command": "cargo build --release --target wasm32-unknown-unknown", + "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], + "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] + }, + "build:wasm": { + "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", + "dependencies": ["build:rust"], + "files": [ + "./target/wasm32-unknown-unknown/release/example_bg.wasm", + "package.json" + ], + "output": ["src/pkg"] + }, + "start": { + "command": "node dist/index.js", + "dependencies": ["build"] + }, + "deploy": { + "command": "clasp push -f", + "dependencies": ["build"], + "files": [".clasp.json", ".claspignore"] + } + }, + "author": "Justin Poehnelt ", + "license": "Apache-2.0", + "devDependencies": { + "@google/clasp": "^2.4.2", + "esbuild": "^0.20.1", + "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", + "vitest": "^1.3.1", + "wireit": "^0.14.4" + }, + "dependencies": { + "@types/google-apps-script": "^1.0.82", + "fastestsmallesttextencoderdecoder": "^1.0.22" + }, + "type": "module" } diff --git a/wasm/image-add-on/polyfill.js b/wasm/image-add-on/polyfill.js index c78434025..e2f434747 100644 --- a/wasm/image-add-on/polyfill.js +++ b/wasm/image-add-on/polyfill.js @@ -15,6 +15,6 @@ */ export { - TextEncoder, - TextDecoder, + TextEncoder, + TextDecoder, } from "fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js"; diff --git a/wasm/image-add-on/src/add-on.js b/wasm/image-add-on/src/add-on.js index da3080059..6ebc67372 100644 --- a/wasm/image-add-on/src/add-on.js +++ b/wasm/image-add-on/src/add-on.js @@ -15,126 +15,126 @@ */ const COLORS = { - RED: "#EA4335", + RED: "#EA4335", }; const properties = PropertiesService.getUserProperties(); async function card(items) { - const builder = CardService.newCardBuilder(); - - const { quality, format, width, height } = loadSettings(); - - const controls = CardService.newCardSection() - .addWidget( - CardService.newSelectionInput() - .setFieldName("quality") - .setTitle("Quality") - .setType(CardService.SelectionInputType.RADIO_BUTTON) - .addItem("Low", "low", quality === "low") - .addItem("Medium", "medium", quality === "medium") - .addItem("High", "high", quality === "high"), - ) - .addWidget( - CardService.newTextInput() - .setFieldName("height") - .setTitle("Height") - .setMultiline(false) - .setValue(height ?? ""), - ) - .addWidget( - CardService.newTextInput() - .setFieldName("width") - .setTitle("Width") - .setMultiline(false) - .setValue(width ?? ""), - ) - .addWidget( - CardService.newTextButton() - .setBackgroundColor(COLORS.RED) - .setText("Apply Settings") - .setOnClickAction( - CardService.newAction() - .setFunctionName("updateSettings") - .setParameters({}) - .setLoadIndicator(CardService.LoadIndicator.SPINNER), - ), - ) - .setCollapsible(true) - .setNumUncollapsibleWidgets(0); - - builder.addSection(controls); - - const sections = await Promise.all( - ( - items ?? - JSON.parse( - PropertiesService.getUserProperties().getProperty("selectedItems"), - ) - ) - .filter((item) => item.mimeType.startsWith("image")) - .map(async (item) => { - const section = CardService.newCardSection(); - - const bytes = DriveApp.getFileById(item.id).getBlob().getBytes(); - - const newBytes = await compress_(bytes, { - quality: qualityToInt(quality), - format: item.mimeType.split("/").pop(), - width: Number.parseInt(width ?? "0"), - height: Number.parseInt(height ?? "0"), - }); - - const dataUrl = `data:${item.mimeType};base64,${Utilities.base64Encode( - newBytes, - )}`; - - section.addWidget(CardService.newImage().setImageUrl(dataUrl)); - - section.addWidget( - CardService.newDecoratedText().setText(bytesToText(newBytes.length)), - ); - - section.addWidget( - CardService.newButtonSet() - .addButton( - CardService.newTextButton() - .setBackgroundColor(COLORS.RED) - .setText("Save") - .setOnClickAction( - CardService.newAction() - .setFunctionName("save") - .setParameters({ - bytes: Utilities.base64Encode(newBytes), - action: "save", - item: JSON.stringify(item), - }), - ), - ) - .addButton( - CardService.newTextButton() - .setBackgroundColor(COLORS.RED) - .setText("Save Copy") - .setOnClickAction( - CardService.newAction() - .setFunctionName("save") - .setParameters({ - bytes: Utilities.base64Encode(newBytes), - action: "save-as", - item: JSON.stringify(item), - }), - ), - ), - ); - return section; - }), - ); - - for (const section of sections) { - builder.addSection(section); - } - - return builder; + const builder = CardService.newCardBuilder(); + + const { quality, format, width, height } = loadSettings(); + + const controls = CardService.newCardSection() + .addWidget( + CardService.newSelectionInput() + .setFieldName("quality") + .setTitle("Quality") + .setType(CardService.SelectionInputType.RADIO_BUTTON) + .addItem("Low", "low", quality === "low") + .addItem("Medium", "medium", quality === "medium") + .addItem("High", "high", quality === "high"), + ) + .addWidget( + CardService.newTextInput() + .setFieldName("height") + .setTitle("Height") + .setMultiline(false) + .setValue(height ?? ""), + ) + .addWidget( + CardService.newTextInput() + .setFieldName("width") + .setTitle("Width") + .setMultiline(false) + .setValue(width ?? ""), + ) + .addWidget( + CardService.newTextButton() + .setBackgroundColor(COLORS.RED) + .setText("Apply Settings") + .setOnClickAction( + CardService.newAction() + .setFunctionName("updateSettings") + .setParameters({}) + .setLoadIndicator(CardService.LoadIndicator.SPINNER), + ), + ) + .setCollapsible(true) + .setNumUncollapsibleWidgets(0); + + builder.addSection(controls); + + const sections = await Promise.all( + ( + items ?? + JSON.parse( + PropertiesService.getUserProperties().getProperty("selectedItems"), + ) + ) + .filter((item) => item.mimeType.startsWith("image")) + .map(async (item) => { + const section = CardService.newCardSection(); + + const bytes = DriveApp.getFileById(item.id).getBlob().getBytes(); + + const newBytes = await compress_(bytes, { + quality: qualityToInt(quality), + format: item.mimeType.split("/").pop(), + width: Number.parseInt(width ?? "0"), + height: Number.parseInt(height ?? "0"), + }); + + const dataUrl = `data:${item.mimeType};base64,${Utilities.base64Encode( + newBytes, + )}`; + + section.addWidget(CardService.newImage().setImageUrl(dataUrl)); + + section.addWidget( + CardService.newDecoratedText().setText(bytesToText(newBytes.length)), + ); + + section.addWidget( + CardService.newButtonSet() + .addButton( + CardService.newTextButton() + .setBackgroundColor(COLORS.RED) + .setText("Save") + .setOnClickAction( + CardService.newAction() + .setFunctionName("save") + .setParameters({ + bytes: Utilities.base64Encode(newBytes), + action: "save", + item: JSON.stringify(item), + }), + ), + ) + .addButton( + CardService.newTextButton() + .setBackgroundColor(COLORS.RED) + .setText("Save Copy") + .setOnClickAction( + CardService.newAction() + .setFunctionName("save") + .setParameters({ + bytes: Utilities.base64Encode(newBytes), + action: "save-as", + item: JSON.stringify(item), + }), + ), + ), + ); + return section; + }), + ); + + for (const section of sections) { + builder.addSection(section); + } + + return builder; } /** @@ -148,11 +148,11 @@ async function card(items) { * @return {Card} */ async function onItemsSelectedTrigger(e) { - PropertiesService.getUserProperties().setProperty( - "selectedItems", - JSON.stringify(e.drive.selectedItems), - ); - return (await card(e.drive.selectedItems)).build(); + PropertiesService.getUserProperties().setProperty( + "selectedItems", + JSON.stringify(e.drive.selectedItems), + ); + return (await card(e.drive.selectedItems)).build(); } /** @@ -165,89 +165,89 @@ async function onItemsSelectedTrigger(e) { * @return {DriveItemsSelectedActionResponse} */ function onRequestFileScopeButtonClicked(e) { - const idToRequest = e.parameters.id; - return CardService.newDriveItemsSelectedActionResponseBuilder() - .requestFileScope(idToRequest) - .build(); + const idToRequest = e.parameters.id; + return CardService.newDriveItemsSelectedActionResponseBuilder() + .requestFileScope(idToRequest) + .build(); } function onFileScopeGrantedTrigger(e) { - console.info("after granting item"); - console.info(e); - const builder = CardService.newCardBuilder(); - return builder.build(); + console.info("after granting item"); + console.info(e); + const builder = CardService.newCardBuilder(); + return builder.build(); } function onHomePageTrigger() { - return CardService.newCardBuilder() - .setHeader(CardService.newCardHeader().setTitle("Drive Image Compress")) - .addSection( - CardService.newCardSection().addWidget( - CardService.newTextParagraph().setText( - "Select one or more files in Drive to compress the image.", - ), - ), - ) - .build(); + return CardService.newCardBuilder() + .setHeader(CardService.newCardHeader().setTitle("Drive Image Compress")) + .addSection( + CardService.newCardSection().addWidget( + CardService.newTextParagraph().setText( + "Select one or more files in Drive to compress the image.", + ), + ), + ) + .build(); } function bytesToText(bytes) { - const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; - if (bytes === 0) return "0 Byte"; - const i = Number.parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); - return `${Math.round(bytes / 1024 ** i)} ${sizes[i]}`; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + if (bytes === 0) return "0 Byte"; + const i = Number.parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); + return `${Math.round(bytes / 1024 ** i)} ${sizes[i]}`; } async function save(...args) { - console.log(args); - return CardService.newActionResponseBuilder() - .setNavigation(CardService.newNavigation().popToRoot()) - .build(); + console.log(args); + return CardService.newActionResponseBuilder() + .setNavigation(CardService.newNavigation().popToRoot()) + .build(); } async function updateSettings(e) { - console.log({ e }); - const { formInput } = e; - - persistSettings(formInput); - - return CardService.newActionResponseBuilder() - .setNavigation( - CardService.newNavigation() - .popToRoot() - .updateCard((await card()).build()), - ) - .build(); + console.log({ e }); + const { formInput } = e; + + persistSettings(formInput); + + return CardService.newActionResponseBuilder() + .setNavigation( + CardService.newNavigation() + .popToRoot() + .updateCard((await card()).build()), + ) + .build(); } function persistSettings(settings) { - properties.setProperty( - "settings", - JSON.stringify({ - ...loadSettings, - ...settings, - }), - ); + properties.setProperty( + "settings", + JSON.stringify({ + ...loadSettings, + ...settings, + }), + ); } function loadSettings() { - const defaults = { - quality: "medium", - }; - - return { - ...defaults, - ...JSON.parse(properties.getProperty("settings") ?? "{}"), - }; + const defaults = { + quality: "medium", + }; + + return { + ...defaults, + ...JSON.parse(properties.getProperty("settings") ?? "{}"), + }; } function qualityToInt(quality) { - switch (quality) { - case "low": - return 50; - case "medium": - return 80; - case "high": - return 90; - } + switch (quality) { + case "low": + return 50; + case "medium": + return 80; + case "high": + return 90; + } } diff --git a/wasm/image-add-on/src/appsscript.json b/wasm/image-add-on/src/appsscript.json index 1d8c9fed0..afaead7b4 100644 --- a/wasm/image-add-on/src/appsscript.json +++ b/wasm/image-add-on/src/appsscript.json @@ -1,35 +1,35 @@ { - "timeZone": "America/Denver", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v3", - "serviceId": "drive" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "addOns": { - "common": { - "logoUrl": "https://ssl.gstatic.com/docs/script/images/logo/script-64.png", - "name": "Drive Image Compress Add-on", - "universalActions": [] - }, - "drive": { - "homepageTrigger": { - "runFunction": "onHomePageTrigger", - "enabled": true - }, - "onItemsSelectedTrigger": { - "runFunction": "onItemsSelectedTrigger" - } - } - }, - "oauthScopes": [ - "https://www.googleapis.com/auth/script.locale", - "https://www.googleapis.com/auth/drive.addons.metadata.readonly", - "https://www.googleapis.com/auth/drive" - ] + "timeZone": "America/Denver", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "addOns": { + "common": { + "logoUrl": "https://ssl.gstatic.com/docs/script/images/logo/script-64.png", + "name": "Drive Image Compress Add-on", + "universalActions": [] + }, + "drive": { + "homepageTrigger": { + "runFunction": "onHomePageTrigger", + "enabled": true + }, + "onItemsSelectedTrigger": { + "runFunction": "onItemsSelectedTrigger" + } + } + }, + "oauthScopes": [ + "https://www.googleapis.com/auth/script.locale", + "https://www.googleapis.com/auth/drive.addons.metadata.readonly", + "https://www.googleapis.com/auth/drive" + ] } diff --git a/wasm/image-add-on/src/main.js b/wasm/image-add-on/src/main.js index 2a117326a..19c6bcc9e 100644 --- a/wasm/image-add-on/src/main.js +++ b/wasm/image-add-on/src/main.js @@ -17,18 +17,18 @@ const QUALITY = 80; async function main() { - const iterator = DriveApp.getFilesByType("image/jpeg"); + const iterator = DriveApp.getFilesByType("image/jpeg"); - while (iterator.hasNext()) { - const file = iterator.next(); - const bytes = file.getBlob().getBytes(); + while (iterator.hasNext()) { + const file = iterator.next(); + const bytes = file.getBlob().getBytes(); - const dataUrl = await compress_(bytes, QUALITY); + const dataUrl = await compress_(bytes, QUALITY); - if (dataUrl) { - console.log(dataUrl); - } else { - console.warn("failed to decode image"); - } - } + if (dataUrl) { + console.log(dataUrl); + } else { + console.warn("failed to decode image"); + } + } } diff --git a/wasm/image-add-on/src/test.js b/wasm/image-add-on/src/test.js index a481eeb09..1f172f84f 100644 --- a/wasm/image-add-on/src/test.js +++ b/wasm/image-add-on/src/test.js @@ -17,71 +17,71 @@ async function test() {} async function assert(a, b, message) { - const aVal = await a; - const bVal = await b; + const aVal = await a; + const bVal = await b; - if (aVal !== bVal) { - throw message ?? `'${aVal}' !== '${bVal}'`; - } + if (aVal !== bVal) { + throw message ?? `'${aVal}' !== '${bVal}'`; + } } async function latency(func, iterations, argsFunc = () => []) { - const executionTimes = []; + const executionTimes = []; - for (let i = 0; i < iterations; i++) { - const args = argsFunc(); + for (let i = 0; i < iterations; i++) { + const args = argsFunc(); - const startTime = Date.now(); - let endTime; + const startTime = Date.now(); + let endTime; - try { - await func(...args); - endTime = Date.now(); - } catch (e) { - endTime = Number.POSITIVE_INFINITY; - console.error(e); - continue; - } + try { + await func(...args); + endTime = Date.now(); + } catch (e) { + endTime = Number.POSITIVE_INFINITY; + console.error(e); + continue; + } - executionTimes.push(endTime - startTime); - } + executionTimes.push(endTime - startTime); + } - // Calculate statistics - const min = Math.min(...executionTimes); - const max = Math.max(...executionTimes); - const totalTime = executionTimes.reduce((sum, time) => sum + time, 0); - const average = totalTime / iterations; + // Calculate statistics + const min = Math.min(...executionTimes); + const max = Math.max(...executionTimes); + const totalTime = executionTimes.reduce((sum, time) => sum + time, 0); + const average = totalTime / iterations; - return { - min: min, - max: max, - average: average, - totalTime, - // times: executionTimes // Array of all execution times - }; + return { + min: min, + max: max, + average: average, + totalTime, + // times: executionTimes // Array of all execution times + }; } async function benchmark() { - await hello_("world"); // Warmup + await hello_("world"); // Warmup - console.log(await latency(hello_, 100, () => [generateRandomString(10)])); - console.log(await latency(hello_, 100, () => [generateRandomString(100)])); - console.log(await latency(hello_, 100, () => [generateRandomString(1000)])); - console.log(await latency(hello_, 100, () => [generateRandomString(10000)])); - console.log(await latency(hello_, 100, () => [generateRandomString(100000)])); - console.log(await latency(hello_, 30, () => [generateRandomString(1000000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(10)])); + console.log(await latency(hello_, 100, () => [generateRandomString(100)])); + console.log(await latency(hello_, 100, () => [generateRandomString(1000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(10000)])); + console.log(await latency(hello_, 100, () => [generateRandomString(100000)])); + console.log(await latency(hello_, 30, () => [generateRandomString(1000000)])); } function generateRandomString(length = 1024) { - // Choose your desired character set - const characters = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - const charactersLength = characters.length; + // Choose your desired character set + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; - let result = ""; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } + let result = ""; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } - return result; + return result; } diff --git a/wasm/image-add-on/src/wasm.js b/wasm/image-add-on/src/wasm.js index 7f1d544aa..64afda151 100644 --- a/wasm/image-add-on/src/wasm.js +++ b/wasm/image-add-on/src/wasm.js @@ -15,23 +15,23 @@ */ async function compress_(bytes, { quality, format, width, height }) { - const wasm = await import("./pkg/example_bg.wasm"); - const { __wbg_set_wasm, compress } = await import("./pkg/example_bg.js"); + const wasm = await import("./pkg/example_bg.wasm"); + const { __wbg_set_wasm, compress } = await import("./pkg/example_bg.js"); - __wbg_set_wasm(wasm); + __wbg_set_wasm(wasm); - width = width || 0; - height = height || 0; + width = width || 0; + height = height || 0; - console.log({ quality, format, width, height }); + console.log({ quality, format, width, height }); - const result = compress(bytes, quality, format, width, height); + const result = compress(bytes, quality, format, width, height); - if (typeof result === "string") { - throw new Error(result); - } + if (typeof result === "string") { + throw new Error(result); + } - return result; + return result; } globalThis.compress_ = compress_; diff --git a/wasm/python/.clasp.json b/wasm/python/.clasp.json index 5ee8a1774..5e29e1288 100644 --- a/wasm/python/.clasp.json +++ b/wasm/python/.clasp.json @@ -1,4 +1,4 @@ { - "scriptId": "1_tU8IFkT1ZZ-b08YFeC8umntrH92WVQ27jvUmsCo1W4ZqKKqcytBLdcn", - "rootDir": "./dist" + "scriptId": "1_tU8IFkT1ZZ-b08YFeC8umntrH92WVQ27jvUmsCo1W4ZqKKqcytBLdcn", + "rootDir": "./dist" } diff --git a/wasm/python/build.js b/wasm/python/build.js index 45e2468e9..30c2fddc2 100644 --- a/wasm/python/build.js +++ b/wasm/python/build.js @@ -23,22 +23,22 @@ const outdir = "dist"; const sourceRoot = "src"; await esbuild.build({ - entryPoints: ["./src/wasm.js"], - bundle: true, - outdir, - sourceRoot, - platform: "neutral", - format: "esm", - plugins: [wasmLoader({ mode: "embedded" })], - inject: ["polyfill.js"], - minify: true, - banner: { js: "// Generated code DO NOT EDIT\n" }, + entryPoints: ["./src/wasm.js"], + bundle: true, + outdir, + sourceRoot, + platform: "neutral", + format: "esm", + plugins: [wasmLoader({ mode: "embedded" })], + inject: ["polyfill.js"], + minify: true, + banner: { js: "// Generated code DO NOT EDIT\n" }, }); const passThroughFiles = ["main.js", "test.js", "appsscript.json"]; await Promise.all( - passThroughFiles.map(async (file) => - fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), - ), + passThroughFiles.map(async (file) => + fs.promises.copyFile(path.join(sourceRoot, file), path.join(outdir, file)), + ), ); diff --git a/wasm/python/package.json b/wasm/python/package.json index 73e8eeab5..05f8c7a20 100644 --- a/wasm/python/package.json +++ b/wasm/python/package.json @@ -1,58 +1,58 @@ { - "name": "example", - "version": "0.1.0", - "description": "An example integration of WASM with Rust into Apps Script", - "scripts": { - "build": "wireit", - "build:rust": "wireit", - "build:wasm": "wireit", - "clean": "rm -rf dist pkg target", - "deploy": "wireit", - "format": "cargo fmt", - "start": "wireit" - }, - "wireit": { - "build": { - "command": "node build.js", - "dependencies": ["build:wasm"], - "files": ["src/*.js", "src/*.json", "*.js", "package.json"], - "output": ["dist"] - }, - "build:rust": { - "command": "cargo build --release --target wasm32-unknown-unknown", - "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], - "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] - }, - "build:wasm": { - "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", - "dependencies": ["build:rust"], - "files": [ - "./target/wasm32-unknown-unknown/release/example_bg.wasm", - "package.json" - ], - "output": ["src/pkg"] - }, - "start": { - "command": "node dist/index.js", - "dependencies": ["build"] - }, - "deploy": { - "command": "clasp push -f", - "dependencies": ["build"], - "files": [".clasp.json", ".claspignore"] - } - }, - "author": "Justin Poehnelt ", - "license": "Apache-2.0", - "devDependencies": { - "@google/clasp": "^2.4.2", - "esbuild": "^0.20.1", - "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", - "vitest": "^1.3.1", - "wireit": "^0.14.4" - }, - "dependencies": { - "fastestsmallesttextencoderdecoder": "^1.0.22" - }, - "type": "module" + "name": "example", + "version": "0.1.0", + "description": "An example integration of WASM with Rust into Apps Script", + "scripts": { + "build": "wireit", + "build:rust": "wireit", + "build:wasm": "wireit", + "clean": "rm -rf dist pkg target", + "deploy": "wireit", + "format": "cargo fmt", + "start": "wireit" + }, + "wireit": { + "build": { + "command": "node build.js", + "dependencies": ["build:wasm"], + "files": ["src/*.js", "src/*.json", "*.js", "package.json"], + "output": ["dist"] + }, + "build:rust": { + "command": "cargo build --release --target wasm32-unknown-unknown", + "output": ["./target/wasm32-unknown-unknown/release/example.wasm"], + "files": ["Cargo.lock", "Cargo.toml", "src/**/*.rs", "package.json"] + }, + "build:wasm": { + "command": "wasm-bindgen --out-dir src/pkg --target bundler ./target/wasm32-unknown-unknown/release/example.wasm && wasm-opt src/pkg/example_bg.wasm -Oz -o src/pkg/example_bg.wasm", + "dependencies": ["build:rust"], + "files": [ + "./target/wasm32-unknown-unknown/release/example_bg.wasm", + "package.json" + ], + "output": ["src/pkg"] + }, + "start": { + "command": "node dist/index.js", + "dependencies": ["build"] + }, + "deploy": { + "command": "clasp push -f", + "dependencies": ["build"], + "files": [".clasp.json", ".claspignore"] + } + }, + "author": "Justin Poehnelt ", + "license": "Apache-2.0", + "devDependencies": { + "@google/clasp": "^2.4.2", + "esbuild": "^0.20.1", + "esbuild-plugin-wasm": "github:Tschrock/esbuild-plugin-wasm#04ce98be15b9471980c150eb142d51b20a7dd8bd", + "vitest": "^1.3.1", + "wireit": "^0.14.4" + }, + "dependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22" + }, + "type": "module" } diff --git a/wasm/python/polyfill.js b/wasm/python/polyfill.js index c78434025..e2f434747 100644 --- a/wasm/python/polyfill.js +++ b/wasm/python/polyfill.js @@ -15,6 +15,6 @@ */ export { - TextEncoder, - TextDecoder, + TextEncoder, + TextDecoder, } from "fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js"; diff --git a/wasm/python/src/appsscript.json b/wasm/python/src/appsscript.json index bd143bff8..97141a6f6 100644 --- a/wasm/python/src/appsscript.json +++ b/wasm/python/src/appsscript.json @@ -1,15 +1,15 @@ { - "timeZone": "America/Denver", - "dependencies": { - "enabledAdvancedServices": [ - { - "userSymbol": "Drive", - "version": "v3", - "serviceId": "drive" - } - ] - }, - "exceptionLogging": "STACKDRIVER", - "runtimeVersion": "V8", - "oauthScopes": [] + "timeZone": "America/Denver", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Drive", + "version": "v3", + "serviceId": "drive" + } + ] + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [] } diff --git a/wasm/python/src/main.js b/wasm/python/src/main.js index c790bf1be..f9a50f877 100644 --- a/wasm/python/src/main.js +++ b/wasm/python/src/main.js @@ -23,11 +23,11 @@ * @customfunction */ async function PYTHON(code = "args", ...args) { - const result = await python_(`${code}`, ...args); + const result = await python_(`${code}`, ...args); - if (result instanceof Error) { - throw result; - } + if (result instanceof Error) { + throw result; + } - return result; + return result; } diff --git a/wasm/python/src/test.js b/wasm/python/src/test.js index 745604b7e..5b02a51b3 100644 --- a/wasm/python/src/test.js +++ b/wasm/python/src/test.js @@ -17,10 +17,10 @@ async function test() {} async function assert(a, b, message) { - const aVal = await a; - const bVal = await b; + const aVal = await a; + const bVal = await b; - if (aVal !== bVal) { - throw message ?? `'${aVal}' !== '${bVal}'`; - } + if (aVal !== bVal) { + throw message ?? `'${aVal}' !== '${bVal}'`; + } } diff --git a/wasm/python/src/wasm.js b/wasm/python/src/wasm.js index 0c0e36d22..8ffe281bc 100644 --- a/wasm/python/src/wasm.js +++ b/wasm/python/src/wasm.js @@ -15,7 +15,7 @@ */ globalThis.crypto = { - getRandomValues: (array) => array.map(() => Math.floor(Math.random() * 256)), + getRandomValues: (array) => array.map(() => Math.floor(Math.random() * 256)), }; /** @@ -24,12 +24,12 @@ globalThis.crypto = { * @returns */ async function python_(source, ...args) { - const wasm = await import("./pkg/example_bg.wasm"); - const { __wbg_set_wasm, python } = await import("./pkg/example_bg.js"); + const wasm = await import("./pkg/example_bg.wasm"); + const { __wbg_set_wasm, python } = await import("./pkg/example_bg.js"); - __wbg_set_wasm(wasm); + __wbg_set_wasm(wasm); - return await python(source, args); + return await python(source, args); } globalThis.python_ = python_; From a76a96ea8e231d0f4b19364910136c2c67bf6ca0 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 19:05:28 -0700 Subject: [PATCH 09/12] docs: best practices --- GEMINI.md | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index 4a0c0e162..44d1d37c0 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -21,7 +21,7 @@ pnpm format Apps Script supports the V8 runtime, which enables modern ECMAScript syntax. Using these features makes your code cleaner, more readable, and less error-prone. -### 1. `let` and `const` +### `let` and `const` Use `let` and `const` instead of `var` for block-scoped variables. * **`const`**: Use for values that should not be reassigned. @@ -37,7 +37,7 @@ if (true) { // local is not accessible here ``` -### 2. Arrow Functions +### Arrow Functions Use arrow functions for concise function expressions, especially for callbacks. ```javascript @@ -45,7 +45,7 @@ const numbers = [1, 2, 3]; const squares = numbers.map(x => x * x); // [1, 4, 9] ``` -### 3. Destructuring +### Destructuring Unpack values from arrays or properties from objects into distinct variables. ```javascript @@ -56,7 +56,7 @@ const coords = [10, 20]; const [x, y] = coords; ``` -### 4. Template Literals +### Template Literals Use template literals for string interpolation and multi-line strings. ```javascript @@ -69,7 +69,7 @@ const multiLine = ` `; ``` -### 5. Default Parameters +### Default Parameters Specify default values for function parameters. ```javascript @@ -80,6 +80,23 @@ function greet(name = "Guest") { greet(); // "Hello, Guest!" ``` +### Prefer `for...of` for Iteration +While `forEach` is convenient, `for...of` loops generally offer better performance and more control (e.g., `break`, `continue`) in Apps Script, especially when dealing with large arrays. + +```javascript +const numbers = [1, 2, 3]; + +// Using forEach (less performant for large arrays) +numbers.forEach(num => { + console.log(num); +}); + +// Using for...of (preferred) +for (const num of numbers) { + console.log(num); +} +``` + ## Apps Script V8 Runtime It's important to understand that the Apps Script V8 runtime is @@ -237,7 +254,7 @@ function greet(name, age) { ### Advanced Patterns -#### 4. Custom Objects (@typedef) +#### 1. Custom Objects (@typedef) For complex objects, define a type using `@typedef`. ```javascript @@ -257,7 +274,7 @@ function processUser(config) { } ``` -#### 5. Type Casting +#### 2. Type Casting Sometimes the type checker cannot infer the type correctly. Use inline `@type` to cast. ```javascript @@ -267,7 +284,7 @@ const data = JSON.parse(jsonString); const config = data; ``` -#### 6. Arrays and Generics +#### 3. Arrays and Generics Specify array contents clearly. ```javascript @@ -280,7 +297,7 @@ function lengths(names) { } ``` -#### 7. Handling `null` and `undefined` +#### 4. Handling `null` and `undefined` Be explicit if a value can be null. ```javascript From fbb8e3b9b4c03cde3b9d41352cb7a4f35782dbde Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 19:06:58 -0700 Subject: [PATCH 10/12] fix: add file ID extraction error handling, use XmlService for robust HTML stripping, and update Gemini API parameter name. --- ai/autosummarize/summarize.js | 15 +++++++++++++-- ai/custom-func-ai-agent/Code.js | 2 +- ai/devdocs-link-preview/Vertex.js | 7 ++++--- .../calculate-driving-distance/Code.js | 8 +++++--- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/ai/autosummarize/summarize.js b/ai/autosummarize/summarize.js index d032e52bc..6e891ae76 100644 --- a/ai/autosummarize/summarize.js +++ b/ai/autosummarize/summarize.js @@ -93,7 +93,18 @@ function summarizeFiles( }, ]; const fileIdMatchPattern = /\/d\/(.*?)\//gi; - const fileId = fileIdMatchPattern.exec(fileUrl)[1]; + const match = fileIdMatchPattern.exec(fileUrl); + if (!match) { + console.log(`Could not extract file ID from URL: ${fileUrl}`); + return [ + fileUrl, + fileName, + "Could not extract file ID from URL.", + "", + "", + ]; + } + const fileId = match[1]; // Get file title and type. const currentFile = Drive.Files.get(fileId, { supportsAllDrives: true }); @@ -139,7 +150,7 @@ function summarizeFiles( // Prompt for summary const geminiOptions = { temperature, - tokens, + maxOutputTokens: tokens, }; summary = getAiSummary(promptParts, geminiOptions); diff --git a/ai/custom-func-ai-agent/Code.js b/ai/custom-func-ai-agent/Code.js index a6815dadf..4ecc70c6b 100644 --- a/ai/custom-func-ai-agent/Code.js +++ b/ai/custom-func-ai-agent/Code.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -DEFAULT_OUTPUT_FORMAT = +const DEFAULT_OUTPUT_FORMAT = "Summarize it. Only keep the verdict result and main arguments. " + "Do not reiterate the fact being checked. Remove all markdown. " + "State the verdit result in a first paragraph in a few words and " + diff --git a/ai/devdocs-link-preview/Vertex.js b/ai/devdocs-link-preview/Vertex.js index 2e3f59e3a..a843934b6 100644 --- a/ai/devdocs-link-preview/Vertex.js +++ b/ai/devdocs-link-preview/Vertex.js @@ -22,7 +22,7 @@ const MODEL_ID = scriptPropertyWithDefault("model_id", "gemini-2.5-flash"); const SERVICE_ACCOUNT_KEY = scriptPropertyWithDefault("service_account_key"); /** - * Invokes Gemini to extrac the title and summary of a given URL. Responses may be cached. + * Invokes Gemini to extract the title and summary of a given URL. Responses may be cached. */ function getPageSummary(targetUrl) { const cachedResponse = CacheService.getScriptCache().get(targetUrl); @@ -84,8 +84,9 @@ function getPageSummary(targetUrl) { if (!jsonMatch) { throw new Error("Unable to generate preview,"); } - CacheService.getScriptCache().put(targetUrl, jsonMatch); - return JSON.parse(jsonMatch[0]); + const jsonResponse = jsonMatch[0]; + CacheService.getScriptCache().put(targetUrl, jsonResponse); + return JSON.parse(jsonResponse); } /** diff --git a/solutions/custom-functions/calculate-driving-distance/Code.js b/solutions/custom-functions/calculate-driving-distance/Code.js index 2fd506442..1927bba78 100644 --- a/solutions/custom-functions/calculate-driving-distance/Code.js +++ b/solutions/custom-functions/calculate-driving-distance/Code.js @@ -167,9 +167,11 @@ function generateStepByStep_() { 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, ""); + const instructions = XmlService.parse( + `${step.html_instructions}`, + ) + .getRootElement() + .getText(); newRows.push([instructions, step.distance.value]); } directionsSheet.getRange(1, 1, headers.length, 3).setValues(headers); From 9854200f1350a3e10c19ce2c47fa254c1ae1caf9 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 21:28:16 -0700 Subject: [PATCH 11/12] refactor: declare dropdownValues and rule as constants --- solutions/automations/timesheets/Code.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solutions/automations/timesheets/Code.js b/solutions/automations/timesheets/Code.js index 0c787e685..21c941816 100644 --- a/solutions/automations/timesheets/Code.js +++ b/solutions/automations/timesheets/Code.js @@ -136,8 +136,8 @@ function addNotifiedColumn(sheet, beginningRow, numRows) { numRows, 1, ); - dropdownValues = ["NOTIFIED", "PENDING"]; - rule = SpreadsheetApp.newDataValidation() + const dropdownValues = ["NOTIFIED", "PENDING"]; + const rule = SpreadsheetApp.newDataValidation() .requireValueInList(dropdownValues) .build(); notifiedColumnRange.setDataValidation(rule); From 377735b3932974fc21392d81fdcf6a3fa53d73c3 Mon Sep 17 00:00:00 2001 From: Justin Poehnelt Date: Mon, 24 Nov 2025 21:30:05 -0700 Subject: [PATCH 12/12] chore: Add 2025 Google LLC copyright header to various files. --- .gemini/config.yaml | 14 ++++++++++++++ .github/linters/.yaml-lint.yml | 14 ++++++++++++++ ai/custom_func_vertex/Code.js | 16 ++++++++++++++++ pnpm-lock.yaml | 14 ++++++++++++++ pnpm-workspace.yaml | 14 ++++++++++++++ solutions/add-on/book-smartchip/Code.js | 16 ++++++++++++++++ wasm/hello-world/Cargo.toml | 14 ++++++++++++++ wasm/image-add-on/Cargo.toml | 14 ++++++++++++++ wasm/python/Cargo.toml | 14 ++++++++++++++ 9 files changed, 130 insertions(+) diff --git a/.gemini/config.yaml b/.gemini/config.yaml index a4814a5fd..15e5a2046 100644 --- a/.gemini/config.yaml +++ b/.gemini/config.yaml @@ -1,3 +1,17 @@ +# Copyright 2025 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. + # Config for the Gemini Pull Request Review Bot. # https://github.com/marketplace/gemini-code-assist have_fun: false diff --git a/.github/linters/.yaml-lint.yml b/.github/linters/.yaml-lint.yml index e8394fd59..25ddf219b 100644 --- a/.github/linters/.yaml-lint.yml +++ b/.github/linters/.yaml-lint.yml @@ -1,3 +1,17 @@ +# Copyright 2025 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. + --- ########################################### # These are the rules used for # diff --git a/ai/custom_func_vertex/Code.js b/ai/custom_func_vertex/Code.js index 448a2eb33..8b82a51d7 100644 --- a/ai/custom_func_vertex/Code.js +++ b/ai/custom_func_vertex/Code.js @@ -1,3 +1,19 @@ +/** + * Copyright 2025 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. + */ + /** * Passes a prompt and a data range to Gemini AI. * diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ff5402ae..814db2a44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,17 @@ +# Copyright 2025 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. + lockfileVersion: '9.0' settings: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d649e815b..40cc2b561 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,17 @@ +# Copyright 2025 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. + onlyBuiltDependencies: - '@biomejs/biome' - esbuild diff --git a/solutions/add-on/book-smartchip/Code.js b/solutions/add-on/book-smartchip/Code.js index e578b804b..d4c0603f1 100644 --- a/solutions/add-on/book-smartchip/Code.js +++ b/solutions/add-on/book-smartchip/Code.js @@ -1,3 +1,19 @@ +/** + * Copyright 2025 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. + */ + function getBook(id) { const apiKey = "YOUR_API_KEY"; // Replace with your API key const apiEndpoint = `https://www.googleapis.com/books/v1/volumes/${id}?key=${apiKey}&country=US`; diff --git a/wasm/hello-world/Cargo.toml b/wasm/hello-world/Cargo.toml index 2b8e15ec4..eecee824a 100644 --- a/wasm/hello-world/Cargo.toml +++ b/wasm/hello-world/Cargo.toml @@ -1,3 +1,17 @@ +# Copyright 2025 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. + [package] name = "example" version = "0.1.0" diff --git a/wasm/image-add-on/Cargo.toml b/wasm/image-add-on/Cargo.toml index 71edba381..a459bbefd 100644 --- a/wasm/image-add-on/Cargo.toml +++ b/wasm/image-add-on/Cargo.toml @@ -1,3 +1,17 @@ +# Copyright 2025 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. + [package] name = "example" version = "0.1.0" diff --git a/wasm/python/Cargo.toml b/wasm/python/Cargo.toml index bb2f383d9..1ddf0efd5 100644 --- a/wasm/python/Cargo.toml +++ b/wasm/python/Cargo.toml @@ -1,3 +1,17 @@ +# Copyright 2025 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. + [package] name = "example" version = "0.1.0"