diff --git a/.eslintrc.js b/.eslintrc.js index ac9cbafee..114dd6491 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,21 +1,41 @@ +/** + * 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": 6, - }, - "env": { - "node": true, - }, - "rules": { - "comma-dangle": ["error", "never"], - "max-len": ["error", {"code": 100}], - "camelcase": "off", // Off for destructuring - "async-await/space-after-async": 2, - "async-await/space-after-await": 2, - "eqeqeq": 2, - "guard-for-in": "off", - "no-var": "off", // ES3 - "no-unused-vars": "off" // functions aren't used. - }, - "plugins": ["async-await"] -}; + 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' + ] +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..804a0939c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,17 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +.github/ @googleworkspace/workspace-devrel-dpe diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8d5e0256e..3299ad7b6 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,7 @@ +# Summary + +TODO + ## Expected Behavior Sample URL: diff --git a/.github/linters/.htmlhintrc b/.github/linters/.htmlhintrc new file mode 100644 index 000000000..70391a462 --- /dev/null +++ b/.github/linters/.htmlhintrc @@ -0,0 +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 +} diff --git a/.github/linters/.yaml-lint.yml b/.github/linters/.yaml-lint.yml new file mode 100644 index 000000000..e8394fd59 --- /dev/null +++ b/.github/linters/.yaml-lint.yml @@ -0,0 +1,59 @@ +--- +########################################### +# These are the rules used for # +# linting all the yaml files in the stack # +# NOTE: # +# You can disable line with: # +# # yamllint disable-line # +########################################### +rules: + braces: + level: warning + min-spaces-inside: 0 + max-spaces-inside: 0 + min-spaces-inside-empty: 1 + max-spaces-inside-empty: 5 + brackets: + level: warning + min-spaces-inside: 0 + max-spaces-inside: 0 + min-spaces-inside-empty: 1 + max-spaces-inside-empty: 5 + colons: + level: warning + max-spaces-before: 0 + max-spaces-after: 1 + commas: + level: warning + max-spaces-before: 0 + min-spaces-after: 1 + max-spaces-after: 1 + comments: disable + comments-indentation: disable + document-end: disable + document-start: + level: warning + present: true + empty-lines: + level: warning + max: 2 + max-start: 0 + max-end: 0 + hyphens: + level: warning + max-spaces-after: 1 + indentation: + level: warning + spaces: consistent + indent-sequences: true + check-multi-line-strings: false + key-duplicates: enable + line-length: + level: warning + max: 120 + allow-non-breakable-words: true + allow-non-breakable-inline-mappings: true + new-line-at-end-of-file: disable + new-lines: + type: unix + trailing-spaces: disable \ No newline at end of file diff --git a/.github/linters/sun_checks.xml b/.github/linters/sun_checks.xml new file mode 100644 index 000000000..76d0840d0 --- /dev/null +++ b/.github/linters/sun_checks.xml @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..c08348628 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +# Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Is it been tested? +- [ ] Development testing done + +## Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have performed a peer-reviewed with team member(s) +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/.github/scripts/clasp_push.sh b/.github/scripts/clasp_push.sh new file mode 100755 index 000000000..4d0ad7288 --- /dev/null +++ b/.github/scripts/clasp_push.sh @@ -0,0 +1,52 @@ +#! /bin/bash +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +export LC_ALL=C.UTF-8 +export LANG=C.UTF-8 + +function contains_changes() { + [[ "${*:2}" = "" ]] && return 0 + for f in "${@:2}"; do + case $(realpath "$f")/ in + $(realpath "$1")/*) return 0;; + esac + done + return 1 +} + +changed_files=$(echo "${@:1}" | xargs realpath | xargs -I {} dirname {}| sort -u | uniq) +dirs=() + +IFS=$'\n' read -r -d '' -a dirs < <( find . -name '.clasp.json' -exec dirname '{}' \; | sort -u | xargs realpath ) + +exit_code=0 + +for dir in "${dirs[@]}"; do + pushd "${dir}" > /dev/null || exit + contains_changes "$dir" "${changed_files[@]}" || continue + echo "Publishing ${dir}" + clasp push -f + status=$? + if [ $status -ne 0 ]; then + exit_code=$status + fi + popd > /dev/null || exit +done + +if [ $exit_code -ne 0 ]; then + echo "Script push failed." +fi + +exit $exit_code \ No newline at end of file diff --git a/.github/snippet-bot.yml b/.github/snippet-bot.yml new file mode 100644 index 000000000..bb488a813 --- /dev/null +++ b/.github/snippet-bot.yml @@ -0,0 +1,14 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 000000000..7b363bc42 --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,41 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# .github/sync-repo-settings.yaml +# See https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings for app options. +rebaseMergeAllowed: true +squashMergeAllowed: true +mergeCommitAllowed: false +deleteBranchOnMerge: true +branchProtectionRules: + - pattern: main + isAdminEnforced: false + requiresStrictStatusChecks: false + requiredStatusCheckContexts: + # .github/workflows/test.yml with a job called "test" + - "test" + # .github/workflows/lint.yml with a job called "lint" + - "lint" + # Google bots below + - "cla/google" + - "snippet-bot check" + - "header-check" + - "conventionalcommits.org" + requiredApprovingReviewCount: 1 + requiresCodeOwnerReviews: true +permissionRules: + - team: workspace-devrel-dpe + permission: admin + - team: workspace-devrel + permission: push diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml new file mode 100644 index 000000000..95f323bfb --- /dev/null +++ b/.github/workflows/automation.yml @@ -0,0 +1,69 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +name: Automation +on: [ push, pull_request, workflow_dispatch ] +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }} + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GOOGLEWORKSPACE_BOT_TOKEN}} + steps: + - name: approve + run: gh pr review --approve "$PR_URL" + - name: merge + run: gh pr merge --auto --squash --delete-branch "$PR_URL" + default-branch-migration: + # this job helps with migrating the default branch to main + # it pushes main to master if master exists and main is the default branch + # it pushes master to main if master is the default branch + runs-on: ubuntu-latest + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + # required otherwise GitHub blocks infinite loops in pushes originating in an action + token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }} + - name: Set env + run: | + # set DEFAULT BRANCH + echo "DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')" >> "$GITHUB_ENV"; + + # set HAS_MASTER_BRANCH + if [ -n "$(git ls-remote --heads origin master)" ]; then + echo "HAS_MASTER_BRANCH=true" >> "$GITHUB_ENV" + else + echo "HAS_MASTER_BRANCH=false" >> "$GITHUB_ENV" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: configure git + run: | + git config --global user.name 'googleworkspace-bot' + git config --global user.email 'googleworkspace-bot@google.com' + - if: ${{ env.DEFAULT_BRANCH == 'main' && env.HAS_MASTER_BRANCH == 'true' }} + name: Update master branch from main + run: | + git checkout -B master + git reset --hard origin/main + git push origin master + - if: ${{ env.DEFAULT_BRANCH == 'master'}} + name: Update main branch from master + run: | + git checkout -B main + git reset --hard origin/master + git push origin main diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..fd65241d3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,38 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +name: Lint +on: [push, pull_request, workflow_dispatch] +jobs: + lint: + concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: true + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3.0.2 + with: + fetch-depth: 0 + - uses: github/super-linter/slim@v4.9.4 + env: + ERROR_ON_MISSING_EXEC_BIT: true + VALIDATE_JSCPD: false + VALIDATE_JAVASCRIPT_STANDARD: false + VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/setup-node@v3 + with: + node-version: '14' + - run: npm install + - run: npm run lint diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 000000000..8d6fba836 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,43 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +name: Publish Apps Script +on: + workflow_dispatch: + push: + branches: + - master +jobs: + publish: + concurrency: + group: ${{ github.head_ref || github.ref }} + cancel-in-progress: false + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3.0.2 + with: + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v23.1 + - name: Write test credentials + run: | + echo "${CLASP_CREDENTIALS}" > "${HOME}/.clasprc.json" + env: + CLASP_CREDENTIALS: ${{secrets.CLASP_CREDENTIALS}} + - uses: actions/setup-node@v3 + with: + node-version: '14' + - run: npm install -g @google/clasp + - run: ./.github/scripts/clasp_push.sh ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..debf46551 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Test +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + echo "No tests"; + exit 1; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6e0cfbfcc..000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: node_js -node_js: - - "node" -branches: - only: - - master -before_install: - - npm install -g -cache: - directories: - - node_modules/ -script: npm run lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 291895d0c..97072d5b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,12 +8,12 @@ have to jump a couple of legal hurdles. Please fill out either the individual or corporate Contributor License Agreement (CLA). - * If you are an individual writing original source code and you're sure you - own the intellectual property, then you'll need to sign an - [individual CLA](https://developers.google.com/open-source/cla/individual). - * If you work for a company that wants to allow you to contribute your work, - then you'll need to sign a - [corporate CLA](https://developers.google.com/open-source/cla/corporate). +* If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an + [individual CLA](https://developers.google.com/open-source/cla/individual). +* If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a + [corporate CLA](https://developers.google.com/open-source/cla/corporate). Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to @@ -21,10 +21,10 @@ accept your pull requests. ## Contributing A Patch -1. Submit an issue describing your proposed change to the repo in question. -1. The repo owner will respond to your issue promptly. +1. Submit an issue describing your proposed change to the repository in question. +1. The repository owner will respond to your issue promptly. 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). -1. Fork the desired repo, develop and test your code changes. +1. Fork the desired repository, develop and test your code changes. 1. Ensure that your code adheres to the existing style in the sample to which you are contributing. 1. Ensure that your code has an appropriate set of unit tests which all pass. 1. Submit a pull request! diff --git a/README.md b/README.md index 70619b56d..a56b53039 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Google Apps Script Samples [![Build Status](https://travis-ci.org/googleworkspace/apps-script-samples.svg?branch=master)](https://travis-ci.org/googleworkspace/apps-script-samples) +# Google Apps Script Samples Various sample code and projects for the Google Apps Script platform, a JavaScript platform in the cloud. -Learn more: https://developers.google.com/apps-script +Learn more at [developers.google.com](https://developers.google.com/apps-script). ## Google APIs @@ -28,7 +28,7 @@ align="left" width="96px"/> ### Calendar - [List upcoming events](calendar/quickstart) -- [Create a vacation calendar](calendar/vacationCalendar) +- [Create a vacation calendar](solutions/automations/vacation-calendar/Code.js) 0) { - Logger.log('Users:'); - for (i = 0; i < users.length; i++) { - var user = users[i]; - Logger.log('%s (%s)', user.primaryEmail, user.name.fullName); + try { + const response = AdminDirectory.Users.list(optionalArgs); + const users = response.users; + if (!users || users.length === 0) { + console.log('No users found.'); + return; } - } else { - Logger.log('No users found.'); + // Print the list of user's full name and email + console.log('Users:'); + for (const user of users) { + console.log('%s (%s)', user.primaryEmail, user.name.fullName); + } + } catch (err) { + // TODO (developer)- Handle exception from the Directory API + console.log('Failed with error %s', err.message); } } // [END admin_sdk_directory_quickstart] diff --git a/adminSDK/reports/quickstart.gs b/adminSDK/reports/quickstart.gs index a5c7e6d56..5b5778886 100644 --- a/adminSDK/reports/quickstart.gs +++ b/adminSDK/reports/quickstart.gs @@ -15,25 +15,31 @@ */ // [START admin_sdk_reports_quickstart] /** - * List login events for a G Suite domain. + * List login events for a Google Workspace domain. + * @see https://developers.google.com/admin-sdk/reports/reference/rest/v1/activities/list */ function listLogins() { - var userKey = 'all'; - var applicationName = 'login'; - var optionalArgs = { + const userKey = 'all'; + const applicationName = 'login'; + const optionalArgs = { maxResults: 10 }; - var response = AdminReports.Activities.list(userKey, applicationName, optionalArgs); - var activities = response.items; - if (activities && activities.length > 0) { - Logger.log('Logins:'); - for (i = 0; i < activities.length; i++) { - var activity = activities[i]; - Logger.log('%s: %s (%s)', activity.id.time, activity.actor.email, + try { + const response = AdminReports.Activities.list(userKey, applicationName, optionalArgs); + const activities = response.items; + if (!activities || activities.length === 0) { + console.log('No logins found.'); + return; + } + // Print login events + console.log('Logins:'); + for (const activity of activities) { + console.log('%s: %s (%s)', activity.id.time, activity.actor.email, activity.events[0].name); } - } else { - Logger.log('No logins found.'); + } catch (err) { + // TODO (developer)- Handle exception from the Report API + console.log('Failed with error %s', err.message); } } // [END admin_sdk_reports_quickstart] diff --git a/adminSDK/reseller/quickstart.gs b/adminSDK/reseller/quickstart.gs index dbcff972c..3e7ee0e6b 100644 --- a/adminSDK/reseller/quickstart.gs +++ b/adminSDK/reseller/quickstart.gs @@ -15,23 +15,28 @@ */ // [START admin_sdk_reseller_quickstart] /** - * List Admin SDK reseller subscriptions. + * List Admin SDK reseller. + * @see https://developers.google.com/admin-sdk/reseller/reference/rest/v1/subscriptions/list */ function listSubscriptions() { - var optionalArgs = { + const optionalArgs = { maxResults: 10 }; - var response = AdminReseller.Subscriptions.list(optionalArgs); - var subscriptions = response.subscriptions; - if (subscriptions && subscriptions.length > 0) { - Logger.log('Subscriptions:'); - for (i = 0; i < subscriptions.length; i++) { - var subscription = subscriptions[i]; - Logger.log('%s (%s, %s)', subscription.customerId, subscription.skuId, + try { + const response = AdminReseller.Subscriptions.list(optionalArgs); + const subscriptions = response.subscriptions; + if (!subscriptions || subscriptions.length === 0) { + console.log('No subscriptions found.'); + return; + } + console.log('Subscriptions:'); + for (const subscription of subscriptions) { + console.log('%s (%s, %s)', subscription.customerId, subscription.skuId, subscription.plan.planName); } - } else { - Logger.log('No subscriptions found.'); + } catch (err) { + // TODO (developer)- Handle exception from the Reseller API + console.log('Failed with error %s', err.message); } } // [END admin_sdk_reseller_quickstart] diff --git a/advanced/README.md b/advanced/README.md index 6b891373a..0efa5d004 100644 --- a/advanced/README.md +++ b/advanced/README.md @@ -4,4 +4,4 @@ This directory contains samples for using Apps Script Advanced Services. > Note: These services must be [enabled](https://developers.google.com/apps-script/guides/services/advanced#enabling_advanced_services) before running these samples. -Learn more: https://developers.google.com/apps-script/guides/services/advanced +Learn more at [developers.google.com](https://developers.google.com/apps-script/guides/services/advanced). diff --git a/advanced/adminSDK.gs b/advanced/adminSDK.gs index 3fcfcdc68..c0aed633d 100644 --- a/advanced/adminSDK.gs +++ b/advanced/adminSDK.gs @@ -16,10 +16,11 @@ // [START apps_script_admin_sdk_list_all_users] /** * Lists all the users in a domain sorted by first name. + * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list */ function listAllUsers() { - var pageToken; - var page; + let pageToken; + let page; do { page = AdminDirectory.Users.list({ domain: 'example.com', @@ -27,14 +28,14 @@ function listAllUsers() { maxResults: 100, pageToken: pageToken }); - var users = page.users; - if (users) { - for (var i = 0; i < users.length; i++) { - var user = users[i]; - Logger.log('%s (%s)', user.name.fullName, user.primaryEmail); - } - } else { - Logger.log('No users found.'); + const users = page.users; + if (!users) { + console.log('No users found.'); + return; + } + // Print the user's full name and email. + for (const user of users) { + console.log('%s (%s)', user.name.fullName, user.primaryEmail); } pageToken = page.nextPageToken; } while (pageToken); @@ -43,12 +44,19 @@ function listAllUsers() { // [START apps_script_admin_sdk_get_users] /** -* Get a user by their email address and logs all of their data as a JSON string. -*/ + * Get a user by their email address and logs all of their data as a JSON string. + * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/get + */ function getUser() { - var userEmail = 'liz@example.com'; - var user = AdminDirectory.Users.get(userEmail); - Logger.log('User data:\n %s', JSON.stringify(user, null, 2)); + // TODO (developer) - Replace userEmail value with yours + const userEmail = 'liz@example.com'; + try { + const user = AdminDirectory.Users.get(userEmail); + console.log('User data:\n %s', JSON.stringify(user, null, 2)); + } catch (err) { + // TODO (developer)- Handle exception from the API + console.log('Failed with error %s', err.message); + } } // [END apps_script_admin_sdk_get_users] @@ -59,7 +67,8 @@ function getUser() { * @see https://developers.google.com/admin-sdk/directory/v1/reference/users/insert */ function addUser() { - var user = { + let user = { + // TODO (developer) - Replace primaryEmail value with yours primaryEmail: 'liz@example.com', name: { givenName: 'Elizabeth', @@ -68,46 +77,59 @@ function addUser() { // Generate a random password string. password: Math.random().toString(36) }; - user = AdminDirectory.Users.insert(user); - Logger.log('User %s created with ID %s.', user.primaryEmail, user.id); + try { + user = AdminDirectory.Users.insert(user); + console.log('User %s created with ID %s.', user.primaryEmail, user.id); + } catch (err) { + // TODO (developer)- Handle exception from the API + console.log('Failed with error %s', err.message); + } } // [END apps_script_admin_sdk_add_user] // [START apps_script_admin_sdk_create_alias] /** * Creates an alias (nickname) for a user. + * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/users.aliases/insert */ function createAlias() { - var userEmail = 'liz@example.com'; - var alias = { + // TODO (developer) - Replace userEmail value with yours + const userEmail = 'liz@example.com'; + let alias = { alias: 'chica@example.com' }; - alias = AdminDirectory.Users.Aliases.insert(alias, userEmail); - Logger.log('Created alias %s for user %s.', alias.alias, userEmail); + try { + alias = AdminDirectory.Users.Aliases.insert(alias, userEmail); + console.log('Created alias %s for user %s.', alias.alias, userEmail); + } catch (err) { + // TODO (developer)- Handle exception from the API + console.log('Failed with error %s', err.message); + } } // [END apps_script_admin_sdk_create_alias] // [START apps_script_admin_sdk_list_all_groups] /** * Lists all the groups in the domain. + * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/groups/list */ function listAllGroups() { - var pageToken; - var page; + let pageToken; + let page; do { page = AdminDirectory.Groups.list({ domain: 'example.com', maxResults: 100, pageToken: pageToken }); - var groups = page.groups; - if (groups) { - for (var i = 0; i < groups.length; i++) { - var group = groups[i]; - Logger.log('%s (%s)', group.name, group.email); - } - } else { - Logger.log('No groups found.'); + const groups = page.groups; + if (!groups) { + console.log('No groups found.'); + return; + } + // Print group name and email. + for (const group of groups) { + console.log('%s (%s)', group.name, group.email); } pageToken = page.nextPageToken; } while (pageToken); @@ -117,16 +139,24 @@ function listAllGroups() { // [START apps_script_admin_sdk_add_group_member] /** * Adds a user to an existing group in the domain. + * @see https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/insert */ function addGroupMember() { - var userEmail = 'liz@example.com'; - var groupEmail = 'bookclub@example.com'; - var member = { + // TODO (developer) - Replace userEmail value with yours + const userEmail = 'liz@example.com'; + // TODO (developer) - Replace groupEmail value with yours + const groupEmail = 'bookclub@example.com'; + const member = { email: userEmail, role: 'MEMBER' }; - member = AdminDirectory.Members.insert(member, groupEmail); - Logger.log('User %s added as a member of group %s.', userEmail, groupEmail); + try { + AdminDirectory.Members.insert(member, groupEmail); + console.log('User %s added as a member of group %s.', userEmail, groupEmail); + } catch (err) { + // TODO (developer)- Handle exception from the API + console.log('Failed with error %s', err.message); + } } // [END apps_script_admin_sdk_add_group_member] @@ -137,11 +167,11 @@ function addGroupMember() { * (including attachments), and inserts it in a Google Group in the domain. */ function migrateMessages() { - var groupId = 'exampleGroup@example.com'; - var messagesToMigrate = getRecentMessagesContent(); - for (var i = 0; i < messagesToMigrate.length; i++) { - var messageContent = messagesToMigrate[i]; - var contentBlob = Utilities.newBlob(messageContent, 'message/rfc822'); + // TODO (developer) - Replace groupId value with yours + const groupId = 'exampleGroup@example.com'; + const messagesToMigrate = getRecentMessagesContent(); + for (const messageContent of messagesToMigrate) { + const contentBlob = Utilities.newBlob(messageContent, 'message/rfc822'); AdminGroupsMigration.Archive.insert(groupId, contentBlob); } } @@ -153,14 +183,14 @@ function migrateMessages() { * @return {Array} the messages' content. */ function getRecentMessagesContent() { - var NUM_THREADS = 3; - var NUM_MESSAGES = 3; - var threads = GmailApp.getInboxThreads(0, NUM_THREADS); - var messages = GmailApp.getMessagesForThreads(threads); - var messagesContent = []; - for (var i = 0; i < messages.length; i++) { - for (var j = 0; j < NUM_MESSAGES; j++) { - var message = messages[i][j]; + const NUM_THREADS = 3; + const NUM_MESSAGES = 3; + const threads = GmailApp.getInboxThreads(0, NUM_THREADS); + const messages = GmailApp.getMessagesForThreads(threads); + const messagesContent = []; + for (let i = 0; i < messages.length; i++) { + for (let j = 0; j < NUM_MESSAGES; j++) { + const message = messages[i][j]; if (message) { messagesContent.push(message.getRawContent()); } @@ -175,9 +205,15 @@ function getRecentMessagesContent() { * Gets a group's settings and logs them to the console. */ function getGroupSettings() { - var groupId = 'exampleGroup@example.com'; - var group = AdminGroupsSettings.Groups.get(groupId); - Logger.log(JSON.stringify(group, null, 2)); + // TODO (developer) - Replace groupId value with yours + const groupId = 'exampleGroup@example.com'; + try { + const group = AdminGroupsSettings.Groups.get(groupId); + console.log(JSON.stringify(group, null, 2)); + } catch (err) { + // TODO (developer)- Handle exception from the API + console.log('Failed with error %s', err.message); + } } // [END apps_script_admin_sdk_get_group_setting] @@ -185,12 +221,18 @@ function getGroupSettings() { /** * Updates group's settings. Here, the description is modified, but various * other settings can be changed in the same way. + * @see https://developers.google.com/admin-sdk/groups-settings/v1/reference/groups/patch */ function updateGroupSettings() { - var groupId = 'exampleGroup@example.com'; - var group = AdminGroupsSettings.newGroups(); - group.description = 'Newly changed group description'; - AdminGroupsSettings.Groups.patch(group, groupId); + const groupId = 'exampleGroup@example.com'; + try { + const group = AdminGroupsSettings.newGroups(); + group.description = 'Newly changed group description'; + AdminGroupsSettings.Groups.patch(group, groupId); + } catch (err) { + // TODO (developer)- Handle exception from the API + console.log('Failed with error %s', err.message); + } } // [END apps_script_admin_sdk_update_group_setting] @@ -201,20 +243,21 @@ function updateGroupSettings() { * list of results. */ function getLicenseAssignments() { - var productId = 'Google-Apps'; - var customerId = 'example.com'; - var assignments; - var pageToken; + const productId = 'Google-Apps'; + const customerId = 'example.com'; + let assignments = []; + let pageToken = null; do { - assignments = AdminLicenseManager.LicenseAssignments - .listForProduct(productId, customerId, { + const response = AdminLicenseManager.LicenseAssignments.listForProduct(productId, customerId, { maxResults: 500, pageToken: pageToken }); + assignments = assignments.concat(response.items); + pageToken = response.nextPageToken; } while (pageToken); - for (var i = 0; i < assignments.items.length; i++) { - var assignment = assignments.items[i]; - Logger.log('userId: %s, productId: %s, skuId: %s', + // Print the productId and skuId + for (const assignment of assignments) { + console.log('userId: %s, productId: %s, skuId: %s', assignment.userId, assignment.productId, assignment.skuId); } } @@ -224,14 +267,21 @@ function getLicenseAssignments() { /** * Insert a license assignment for a user, for a given product ID and sku ID * combination. + * For more details follow the link + * https://developers.google.com/admin-sdk/licensing/reference/rest/v1/licenseAssignments/insert */ function insertLicenseAssignment() { - var productId = 'Google-Apps'; - var skuId = 'Google-Vault'; - var userId = 'marty@hoverboard.net'; - var results = AdminLicenseManager.LicenseAssignments - .insert({userId: userId}, productId, skuId); - Logger.log(results); + const productId = 'Google-Apps'; + const skuId = 'Google-Vault'; + const userId = 'marty@hoverboard.net'; + try { + const results = AdminLicenseManager.LicenseAssignments + .insert({userId: userId}, productId, skuId); + console.log(results); + } catch (e) { + // TODO (developer) - Handle exception. + console.log('Failed with an error %s ', e.message); + } } // [END apps_script_admin_sdk_insert_license_assignment] @@ -239,16 +289,17 @@ function insertLicenseAssignment() { /** * Generates a login activity report for the last week as a spreadsheet. The * report includes the time, user, and login result. + * @see https://developers.google.com/admin-sdk/reports/reference/rest/v1/activities/list */ function generateLoginActivityReport() { - var now = new Date(); - var oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - var startTime = oneWeekAgo.toISOString(); - var endTime = now.toISOString(); + const now = new Date(); + const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const startTime = oneWeekAgo.toISOString(); + const endTime = now.toISOString(); - var rows = []; - var pageToken; - var page; + const rows = []; + let pageToken; + let page; do { page = AdminReports.Activities.list('all', 'login', { startTime: startTime, @@ -256,11 +307,10 @@ function generateLoginActivityReport() { maxResults: 500, pageToken: pageToken }); - var items = page.items; + const items = page.items; if (items) { - for (var i = 0; i < items.length; i++) { - var item = items[i]; - var row = [ + for (const item of items) { + const row = [ new Date(item.id.time), item.actor.email, item.events[0].name @@ -271,21 +321,21 @@ function generateLoginActivityReport() { pageToken = page.nextPageToken; } while (pageToken); - if (rows.length > 0) { - var spreadsheet = SpreadsheetApp.create('G Suite Login Report'); - var sheet = spreadsheet.getActiveSheet(); + if (rows.length === 0) { + console.log('No results returned.'); + return; + } + const spreadsheet = SpreadsheetApp.create('Google Workspace Login Report'); + const sheet = spreadsheet.getActiveSheet(); - // Append the headers. - var headers = ['Time', 'User', 'Login Result']; - sheet.appendRow(headers); + // Append the headers. + const headers = ['Time', 'User', 'Login Result']; + sheet.appendRow(headers); - // Append the results. - sheet.getRange(2, 1, rows.length, headers.length).setValues(rows); + // Append the results. + sheet.getRange(2, 1, rows.length, headers.length).setValues(rows); - Logger.log('Report spreadsheet created: %s', spreadsheet.getUrl()); - } else { - Logger.log('No results returned.'); - } + console.log('Report spreadsheet created: %s', spreadsheet.getUrl()); } // [END apps_script_admin_sdk_generate_login_activity_report] @@ -294,21 +344,22 @@ function generateLoginActivityReport() { * Generates a user usage report for this day last week as a spreadsheet. The * report includes the date, user, last login time, number of emails received, * and number of drive files created. + * @see https://developers.google.com/admin-sdk/reports/reference/rest/v1/userUsageReport/get */ function generateUserUsageReport() { - var today = new Date(); - var oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); - var timezone = Session.getScriptTimeZone(); - var date = Utilities.formatDate(oneWeekAgo, timezone, 'yyyy-MM-dd'); + const today = new Date(); + const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); + const timezone = Session.getScriptTimeZone(); + const date = Utilities.formatDate(oneWeekAgo, timezone, 'yyyy-MM-dd'); - var parameters = [ + const parameters = [ 'accounts:last_login_time', 'gmail:num_emails_received', 'drive:num_items_created' ]; - var rows = []; - var pageToken; - var page; + const rows = []; + let pageToken; + let page; do { page = AdminReports.UserUsageReport.get('all', date, { parameters: parameters.join(','), @@ -316,17 +367,15 @@ function generateUserUsageReport() { pageToken: pageToken }); if (page.warnings) { - for (var i = 0; i < page.warnings.length; i++) { - var warning = page.warnings[i]; - Logger.log(warning.message); + for (const warning of page.warnings) { + console.log(warning.message); } } - var reports = page.usageReports; + const reports = page.usageReports; if (reports) { - for (var i = 0; i < reports.length; i++) { - var report = reports[i]; - var parameterValues = getParameterValues(report.parameters); - var row = [ + for (const report of reports) { + const parameterValues = getParameterValues(report.parameters); + const row = [ report.date, report.entity.userEmail, parameterValues['accounts:last_login_time'], @@ -339,22 +388,22 @@ function generateUserUsageReport() { pageToken = page.nextPageToken; } while (pageToken); - if (rows.length > 0) { - var spreadsheet = SpreadsheetApp.create('G Suite User Usage Report'); - var sheet = spreadsheet.getActiveSheet(); + if (rows.length === 0) { + console.log('No results returned.'); + return; + } + const spreadsheet = SpreadsheetApp.create('Google Workspace User Usage Report'); + const sheet = spreadsheet.getActiveSheet(); - // Append the headers. - var headers = ['Date', 'User', 'Last Login', 'Num Emails Received', - 'Num Drive Files Created']; - sheet.appendRow(headers); + // Append the headers. + const headers = ['Date', 'User', 'Last Login', 'Num Emails Received', + 'Num Drive Files Created']; + sheet.appendRow(headers); - // Append the results. - sheet.getRange(2, 1, rows.length, headers.length).setValues(rows); + // Append the results. + sheet.getRange(2, 1, rows.length, headers.length).setValues(rows); - Logger.log('Report spreadsheet created: %s', spreadsheet.getUrl()); - } else { - Logger.log('No results returned.'); - } + console.log('Report spreadsheet created: %s', spreadsheet.getUrl()); } /** @@ -363,9 +412,9 @@ function generateUserUsageReport() { * @return {Object} A map from parameter names to their values. */ function getParameterValues(parameters) { - return parameters.reduce(function(result, parameter) { - var name = parameter.name; - var value; + return parameters.reduce((result, parameter) => { + const name = parameter.name; + let value; if (parameter.intValue !== undefined) { value = parameter.intValue; } else if (parameter.stringValue !== undefined) { @@ -386,20 +435,19 @@ function getParameterValues(parameters) { * Logs the list of subscriptions, including the customer ID, date created, plan * name, and the sku ID. Notice the use of page tokens to access the full list * of results. + * @see https://developers.google.com/admin-sdk/reseller/reference/rest/v1/subscriptions/list */ function getSubscriptions() { - var result; - var subscriptions; - var pageToken; + let result; + let pageToken; do { result = AdminReseller.Subscriptions.list({ pageToken: pageToken }); - for (var i = 0; i < result.subscriptions.length; i++) { - var sub = result.subscriptions[i]; - var creationDate = new Date(); + for (const sub of result.subscriptions) { + const creationDate = new Date(); creationDate.setUTCSeconds(sub.creationTime); - Logger.log('customer ID: %s, date created: %s, plan name: %s, sku id: %s', + console.log('customer ID: %s, date created: %s, plan name: %s, sku id: %s', sub.customerId, creationDate.toDateString(), sub.plan.planName, sub.skuId); } diff --git a/advanced/adsense.gs b/advanced/adsense.gs index df7b896b7..33e3488f2 100644 --- a/advanced/adsense.gs +++ b/advanced/adsense.gs @@ -17,17 +17,17 @@ /** * Lists available AdSense accounts. */ -function listAccounts () { +function listAccounts() { let pageToken; do { - const response = AdSense.Accounts.list({ pageToken: pageToken }); - if (response.accounts) { - for (const account of response.accounts) { - Logger.log('Found account with resource name "%s" and display name "%s".', + const response = AdSense.Accounts.list({pageToken: pageToken}); + if (!response.accounts) { + console.log('No accounts found.'); + return; + } + for (const account of response.accounts) { + console.log('Found account with resource name "%s" and display name "%s".', account.name, account.displayName); - } - } else { - Logger.log('No accounts found.'); } pageToken = response.nextPageToken; } while (pageToken); @@ -38,23 +38,24 @@ function listAccounts () { /** * Logs available Ad clients for an account. * - * @param {string} accountName The resource name of the account that owns the collection of ad clients. + * @param {string} accountName The resource name of the account that owns the + * collection of ad clients. */ -function listAdClients (accountName) { +function listAdClients(accountName) { let pageToken; do { const response = AdSense.Accounts.Adclients.list(accountName, { pageToken: pageToken }); - if (response.adClients) { - for (const adClient of response.adClients) { - Logger.log('Found ad client for product "%s" with resource name "%s".', + if (!response.adClients) { + console.log('No ad clients found for this account.'); + return; + } + for (const adClient of response.adClients) { + console.log('Found ad client for product "%s" with resource name "%s".', adClient.productCode, adClient.name); - Logger.log('Reporting dimension ID: %s', + console.log('Reporting dimension ID: %s', adClient.reportingDimensionId ?? 'None'); - } - } else { - Logger.log('No ad clients found for this account.'); } pageToken = response.nextPageToken; } while (pageToken); @@ -64,22 +65,23 @@ function listAdClients (accountName) { // [START apps_script_adsense_list_ad_units] /** * Lists ad units. - * @param {string} adClientName The resource name of the ad client that owns the collection of ad units. + * @param {string} adClientName The resource name of the ad client that owns the collection + * of ad units. */ -function listAdUnits (adClientName) { +function listAdUnits(adClientName) { let pageToken; do { const response = AdSense.Accounts.Adclients.Adunits.list(adClientName, { pageSize: 50, pageToken: pageToken }); - if (response.adUnits) { - for (const adUnit of response.adUnits) { - Logger.log('Found ad unit with resource name "%s" and display name "%s".', + if (!response.adUnits) { + console.log('No ad units found for this ad client.'); + return; + } + for (const adUnit of response.adUnits) { + console.log('Found ad unit with resource name "%s" and display name "%s".', adUnit.name, adUnit.displayName); - } - } else { - Logger.log('No ad units found for this ad client.'); } pageToken = response.nextPageToken; @@ -91,9 +93,10 @@ function listAdUnits (adClientName) { /** * Generates a spreadsheet report for a specific ad client in an account. * @param {string} accountName The resource name of the account. - * @param {string} adClientName The reporting dimension ID of the ad client. + * @param {string} adClientReportingDimensionId The reporting dimension ID + * of the ad client. */ -function generateReport (accountName, adClientReportingDimensionId) { +function generateReport(accountName, adClientReportingDimensionId) { // Prepare report. const today = new Date(); const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); @@ -111,22 +114,22 @@ function generateReport (accountName, adClientReportingDimensionId) { orderBy: ['+DATE'] }); - if (report.rows) { - const spreadsheet = SpreadsheetApp.create('AdSense Report'); - const sheet = spreadsheet.getActiveSheet(); + if (!report.rows) { + console.log('No rows returned.'); + return; + } + const spreadsheet = SpreadsheetApp.create('AdSense Report'); + const sheet = spreadsheet.getActiveSheet(); - // Append the headers. - sheet.appendRow(report.headers.map(header => header.name)); + // Append the headers. + sheet.appendRow(report.headers.map((header) => header.name)); - // Append the results. - sheet.getRange(2, 1, report.rows.length, report.headers.length) - .setValues(report.rows.map(row => row.cells.map(cell => cell.value))); + // Append the results. + sheet.getRange(2, 1, report.rows.length, report.headers.length) + .setValues(report.rows.map((row) => row.cells.map((cell) => cell.value))); - Logger.log('Report spreadsheet created: %s', + console.log('Report spreadsheet created: %s', spreadsheet.getUrl()); - } else { - Logger.log('No rows returned.'); - } } /** @@ -134,7 +137,7 @@ function generateReport (accountName, adClientReportingDimensionId) { * @param {string} parameter The parameter to be escaped. * @return {string} The escaped parameter. */ -function escapeFilterParameter (parameter) { +function escapeFilterParameter(parameter) { return parameter.replace('\\', '\\\\').replace(',', '\\,'); } @@ -143,8 +146,9 @@ function escapeFilterParameter (parameter) { * * @param {string} paramName the name of the date parameter * @param {Date} value the date + * @return {object} formatted date */ -function dateToJson (paramName, value) { +function dateToJson(paramName, value) { return { [paramName + '.year']: value.getFullYear(), [paramName + '.month']: value.getMonth() + 1, diff --git a/advanced/analytics.gs b/advanced/analytics.gs index a0b8fb64c..a691ce4a2 100644 --- a/advanced/analytics.gs +++ b/advanced/analytics.gs @@ -18,17 +18,23 @@ * Lists Analytics accounts. */ function listAccounts() { - var accounts = Analytics.Management.Accounts.list(); - if (accounts.items && accounts.items.length) { - for (var i = 0; i < accounts.items.length; i++) { - var account = accounts.items[i]; - Logger.log('Account: name "%s", id "%s".', account.name, account.id); + try { + const accounts = Analytics.Management.Accounts.list(); + if (!accounts.items || !accounts.items.length) { + console.log('No accounts found.'); + return; + } + + for (let i = 0; i < accounts.items.length; i++) { + const account = accounts.items[i]; + console.log('Account: name "%s", id "%s".', account.name, account.id); // List web properties in the account. listWebProperties(account.id); } - } else { - Logger.log('No accounts found.'); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } @@ -37,18 +43,23 @@ function listAccounts() { * @param {string} accountId The account ID. */ function listWebProperties(accountId) { - var webProperties = Analytics.Management.Webproperties.list(accountId); - if (webProperties.items && webProperties.items.length) { - for (var i = 0; i < webProperties.items.length; i++) { - var webProperty = webProperties.items[i]; - Logger.log('\tWeb Property: name "%s", id "%s".', webProperty.name, - webProperty.id); + try { + const webProperties = Analytics.Management.Webproperties.list(accountId); + if (!webProperties.items || !webProperties.items.length) { + console.log('\tNo web properties found.'); + return; + } + for (let i = 0; i < webProperties.items.length; i++) { + const webProperty = webProperties.items[i]; + console.log('\tWeb Property: name "%s", id "%s".', + webProperty.name, webProperty.id); // List profiles in the web property. listProfiles(accountId, webProperty.id); - } - } else { - Logger.log('\tNo web properties found.'); + } + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } @@ -61,17 +72,22 @@ function listProfiles(accountId, webPropertyId) { // Note: If you experience "Quota Error: User Rate Limit Exceeded" errors // due to the number of accounts or profiles you have, you may be able to // avoid it by adding a Utilities.sleep(1000) statement here. + try { + const profiles = Analytics.Management.Profiles.list(accountId, + webPropertyId); - var profiles = Analytics.Management.Profiles.list(accountId, - webPropertyId); - if (profiles.items && profiles.items.length) { - for (var i = 0; i < profiles.items.length; i++) { - var profile = profiles.items[i]; - Logger.log('\t\tProfile: name "%s", id "%s".', profile.name, + if (!profiles.items || !profiles.items.length) { + console.log('\t\tNo web properties found.'); + return; + } + for (let i = 0; i < profiles.items.length; i++) { + const profile = profiles.items[i]; + console.log('\t\tProfile: name "%s", id "%s".', profile.name, profile.id); } - } else { - Logger.log('\t\tNo web properties found.'); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } // [END apps_script_analytics_accounts] @@ -82,43 +98,44 @@ function listProfiles(accountId, webPropertyId) { * @param {string} profileId The profile ID. */ function runReport(profileId) { - var today = new Date(); - var oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); + const today = new Date(); + const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); - var startDate = Utilities.formatDate(oneWeekAgo, Session.getScriptTimeZone(), + const startDate = Utilities.formatDate(oneWeekAgo, Session.getScriptTimeZone(), 'yyyy-MM-dd'); - var endDate = Utilities.formatDate(today, Session.getScriptTimeZone(), + const endDate = Utilities.formatDate(today, Session.getScriptTimeZone(), 'yyyy-MM-dd'); - var tableId = 'ga:' + profileId; - var metric = 'ga:visits'; - var options = { + const tableId = 'ga:' + profileId; + const metric = 'ga:visits'; + const options = { 'dimensions': 'ga:source,ga:keyword', 'sort': '-ga:visits,ga:source', 'filters': 'ga:medium==organic', 'max-results': 25 }; - var report = Analytics.Data.Ga.get(tableId, startDate, endDate, metric, + const report = Analytics.Data.Ga.get(tableId, startDate, endDate, metric, options); - if (report.rows) { - var spreadsheet = SpreadsheetApp.create('Google Analytics Report'); - var sheet = spreadsheet.getActiveSheet(); + if (!report.rows) { + console.log('No rows returned.'); + return; + } + + const spreadsheet = SpreadsheetApp.create('Google Analytics Report'); + const sheet = spreadsheet.getActiveSheet(); - // Append the headers. - var headers = report.columnHeaders.map(function(columnHeader) { - return columnHeader.name; - }); - sheet.appendRow(headers); + // Append the headers. + const headers = report.columnHeaders.map((columnHeader) => { + return columnHeader.name; + }); + sheet.appendRow(headers); - // Append the results. - sheet.getRange(2, 1, report.rows.length, headers.length) - .setValues(report.rows); + // Append the results. + sheet.getRange(2, 1, report.rows.length, headers.length) + .setValues(report.rows); - Logger.log('Report spreadsheet created: %s', - spreadsheet.getUrl()); - } else { - Logger.log('No rows returned.'); - } + console.log('Report spreadsheet created: %s', + spreadsheet.getUrl()); } // [END apps_script_analytics_reports] diff --git a/advanced/analyticsAdmin.gs b/advanced/analyticsAdmin.gs new file mode 100644 index 000000000..973388fd5 --- /dev/null +++ b/advanced/analyticsAdmin.gs @@ -0,0 +1,37 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// [START apps_script_analyticsadmin] +/** + * Logs the Google Analytics accounts accessible by the current user. + */ +function listAccounts() { + try { + accounts = AnalyticsAdmin.Accounts.list(); + if (!accounts.items || !accounts.items.length) { + console.log('No accounts found.'); + return; + } + + for (let i = 0; i < accounts.items.length; i++) { + const account = accounts.items[i]; + console.log('Account: name "%s", displayName "%s".', account.name, account.displayName); + } + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } +} +// [END apps_script_analyticsadmin] diff --git a/advanced/analyticsData.gs b/advanced/analyticsData.gs index bd2212699..b881998d1 100644 --- a/advanced/analyticsData.gs +++ b/advanced/analyticsData.gs @@ -25,60 +25,66 @@ function runReport() { */ const propertyId = 'YOUR-GA4-PROPERTY-ID'; - var metric = AnalyticsData.newMetric(); - metric.name = 'activeUsers'; + try { + const metric = AnalyticsData.newMetric(); + metric.name = 'activeUsers'; - var dimension = AnalyticsData.newDimension(); - dimension.name = 'city'; + const dimension = AnalyticsData.newDimension(); + dimension.name = 'city'; - var dateRange = AnalyticsData.newDateRange(); - dateRange.startDate = '2020-03-31'; - dateRange.endDate = 'today'; + const dateRange = AnalyticsData.newDateRange(); + dateRange.startDate = '2020-03-31'; + dateRange.endDate = 'today'; - var request = AnalyticsData.newRunReportRequest(); - request.dimensions = [ dimension ]; - request.metrics = [ metric ]; - request.dateRanges = dateRange; + const request = AnalyticsData.newRunReportRequest(); + request.dimensions = [dimension]; + request.metrics = [metric]; + request.dateRanges = dateRange; - var report = AnalyticsData.Properties.runReport(request, - 'properties/' + propertyId); - if (report.rows) { - var spreadsheet = SpreadsheetApp.create('Google Analytics Report'); - var sheet = spreadsheet.getActiveSheet(); + const report = AnalyticsData.Properties.runReport(request, + 'properties/' + propertyId); + if (!report.rows) { + console.log('No rows returned.'); + return; + } + + const spreadsheet = SpreadsheetApp.create('Google Analytics Report'); + const sheet = spreadsheet.getActiveSheet(); // Append the headers. - var dimensionHeaders = report.dimensionHeaders.map( - function(dimensionHeader) { - return dimensionHeader.name; - }); - var metricHeaders = report.metricHeaders.map( - function(metricHeader) { - return metricHeader.name; - }); - var headers = [ ...dimensionHeaders, ...metricHeaders]; + const dimensionHeaders = report.dimensionHeaders.map( + (dimensionHeader) => { + return dimensionHeader.name; + }); + const metricHeaders = report.metricHeaders.map( + (metricHeader) => { + return metricHeader.name; + }); + const headers = [...dimensionHeaders, ...metricHeaders]; sheet.appendRow(headers); // Append the results. - var rows = report.rows.map( function(row) { - var dimensionValues = row.dimensionValues.map( - function(dimensionValue) { - return dimensionValue.value; - }); - var metricValues = row.metricValues.map( - function(metricValues) { - return metricValues.value; - }); - return [ ...dimensionValues, ...metricValues]; + const rows = report.rows.map((row) => { + const dimensionValues = row.dimensionValues.map( + (dimensionValue) => { + return dimensionValue.value; + }); + const metricValues = row.metricValues.map( + (metricValues) => { + return metricValues.value; + }); + return [...dimensionValues, ...metricValues]; }); sheet.getRange(2, 1, report.rows.length, headers.length) .setValues(rows); - Logger.log('Report spreadsheet created: %s', + console.log('Report spreadsheet created: %s', spreadsheet.getUrl()); - } else { - Logger.log('No rows returned.'); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } // [END apps_script_analyticsdata] diff --git a/advanced/bigquery.gs b/advanced/bigquery.gs index 0495317f0..a4127aa1b 100644 --- a/advanced/bigquery.gs +++ b/advanced/bigquery.gs @@ -20,17 +20,23 @@ function runQuery() { // Replace this value with the project ID listed in the Google // Cloud Platform project. - var projectId = 'XXXXXXXX'; + const projectId = 'XXXXXXXX'; - var request = { - query: 'SELECT TOP(word, 300) AS word, COUNT(*) AS word_count ' + - 'FROM publicdata:samples.shakespeare WHERE LENGTH(word) > 10;' + const request = { + // TODO (developer) - Replace query with yours + query: 'SELECT refresh_date AS Day, term AS Top_Term, rank ' + + 'FROM `bigquery-public-data.google_trends.top_terms` ' + + 'WHERE rank = 1 ' + + 'AND refresh_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 2 WEEK) ' + + 'GROUP BY Day, Top_Term, rank ' + + 'ORDER BY Day DESC;', + useLegacySql: false }; - var queryResults = BigQuery.Jobs.query(request, projectId); - var jobId = queryResults.jobReference.jobId; + let queryResults = BigQuery.Jobs.query(request, projectId); + const jobId = queryResults.jobReference.jobId; // Check on status of the Query Job. - var sleepTimeMs = 500; + let sleepTimeMs = 500; while (!queryResults.jobComplete) { Utilities.sleep(sleepTimeMs); sleepTimeMs *= 2; @@ -38,7 +44,7 @@ function runQuery() { } // Get all the rows of results. - var rows = queryResults.rows; + let rows = queryResults.rows; while (queryResults.pageToken) { queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId, { pageToken: queryResults.pageToken @@ -46,32 +52,31 @@ function runQuery() { rows = rows.concat(queryResults.rows); } - if (rows) { - var spreadsheet = SpreadsheetApp.create('BiqQuery Results'); - var sheet = spreadsheet.getActiveSheet(); + if (!rows) { + console.log('No rows returned.'); + return; + } + const spreadsheet = SpreadsheetApp.create('BigQuery Results'); + const sheet = spreadsheet.getActiveSheet(); - // Append the headers. - var headers = queryResults.schema.fields.map(function(field) { - return field.name; - }); - sheet.appendRow(headers); + // Append the headers. + const headers = queryResults.schema.fields.map(function(field) { + return field.name; + }); + sheet.appendRow(headers); - // Append the results. - var data = new Array(rows.length); - for (var i = 0; i < rows.length; i++) { - var cols = rows[i].f; - data[i] = new Array(cols.length); - for (var j = 0; j < cols.length; j++) { - data[i][j] = cols[j].v; - } + // Append the results. + const data = new Array(rows.length); + for (let i = 0; i < rows.length; i++) { + const cols = rows[i].f; + data[i] = new Array(cols.length); + for (let j = 0; j < cols.length; j++) { + data[i][j] = cols[j].v; } - sheet.getRange(2, 1, rows.length, headers.length).setValues(data); - - Logger.log('Results spreadsheet created: %s', - spreadsheet.getUrl()); - } else { - Logger.log('No rows returned.'); } + sheet.getRange(2, 1, rows.length, headers.length).setValues(data); + + console.log('Results spreadsheet created: %s', spreadsheet.getUrl()); } // [END apps_script_bigquery_run_query] @@ -82,17 +87,17 @@ function runQuery() { function loadCsv() { // Replace this value with the project ID listed in the Google // Cloud Platform project. - var projectId = 'XXXXXXXX'; + const projectId = 'XXXXXXXX'; // Create a dataset in the BigQuery UI (https://bigquery.cloud.google.com) // and enter its ID below. - var datasetId = 'YYYYYYYY'; + const datasetId = 'YYYYYYYY'; // Sample CSV file of Google Trends data conforming to the schema below. // https://docs.google.com/file/d/0BwzA1Orbvy5WMXFLaTR1Z1p2UDg/edit - var csvFileId = '0BwzA1Orbvy5WMXFLaTR1Z1p2UDg'; + const csvFileId = '0BwzA1Orbvy5WMXFLaTR1Z1p2UDg'; // Create the table. - var tableId = 'pets_' + new Date().getTime(); - var table = { + const tableId = 'pets_' + new Date().getTime(); + let table = { tableReference: { projectId: projectId, datasetId: datasetId, @@ -107,15 +112,18 @@ function loadCsv() { ] } }; - table = BigQuery.Tables.insert(table, projectId, datasetId); - Logger.log('Table created: %s', table.id); - + try { + table = BigQuery.Tables.insert(table, projectId, datasetId); + console.log('Table created: %s', table.id); + } catch (err) { + console.log('unable to create table'); + } // Load CSV data from Drive and convert to the correct format for upload. - var file = DriveApp.getFileById(csvFileId); - var data = file.getBlob().setContentType('application/octet-stream'); + const file = DriveApp.getFileById(csvFileId); + const data = file.getBlob().setContentType('application/octet-stream'); // Create the data upload job. - var job = { + const job = { configuration: { load: { destinationTable: { @@ -127,8 +135,11 @@ function loadCsv() { } } }; - job = BigQuery.Jobs.insert(job, projectId, data); - Logger.log('Load job started. Check on the status of it here: ' + - 'https://bigquery.cloud.google.com/jobs/%s', projectId); + try { + const jobResult = BigQuery.Jobs.insert(job, projectId, data); + console.log(`Load job started. Status: ${jobResult.status.state}`); + } catch (err) { + console.log('unable to insert job'); + } } // [END apps_script_bigquery_load_csv] diff --git a/advanced/calendar.gs b/advanced/calendar.gs index f67e1e41d..28f8116a0 100644 --- a/advanced/calendar.gs +++ b/advanced/calendar.gs @@ -13,40 +13,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// [START apps_script_calendar_list_calendars] +// [START calendar_list_calendars] /** * Lists the calendars shown in the user's calendar list. + * @see https://developers.google.com/calendar/api/v3/reference/calendarList/list */ function listCalendars() { - var calendars; - var pageToken; + let calendars; + let pageToken; do { calendars = Calendar.CalendarList.list({ maxResults: 100, pageToken: pageToken + }); - if (calendars.items && calendars.items.length > 0) { - for (var i = 0; i < calendars.items.length; i++) { - var calendar = calendars.items[i]; - Logger.log('%s (ID: %s)', calendar.summary, calendar.id); - } - } else { - Logger.log('No calendars found.'); + if (!calendars.items || calendars.items.length === 0) { + console.log('No calendars found.'); + return; + } + // Print the calendar id and calendar summary + for (const calendar of calendars.items) { + console.log('%s (ID: %s)', calendar.summary, calendar.id); } pageToken = calendars.nextPageToken; } while (pageToken); } -// [END apps_script_calendar_list_calendars] +// [END calendar_list_calendars] -// [START apps_script_calendar_create_event] +// [START calendar_create_event] /** * Creates an event in the user's default calendar. + * @see https://developers.google.com/calendar/api/v3/reference/events/insert */ function createEvent() { - var calendarId = 'primary'; - var start = getRelativeDate(1, 12); - var end = getRelativeDate(1, 13); - var event = { + const calendarId = 'primary'; + const start = getRelativeDate(1, 12); + const end = getRelativeDate(1, 13); + // event details for creating event. + let event = { summary: 'Lunch Meeting', location: 'The Deli', description: 'To discuss our plans for the presentation next week.', @@ -57,14 +61,19 @@ function createEvent() { dateTime: end.toISOString() }, attendees: [ - {email: 'alice@example.com'}, - {email: 'bob@example.com'} + {email: 'gduser1@workspacesample.dev'}, + {email: 'gduser2@workspacesample.dev'} ], // Red background. Use Calendar.Colors.get() for the full list. colorId: 11 }; - event = Calendar.Events.insert(event, calendarId); - Logger.log('Event ID: ' + event.id); + try { + // call method to insert/create new event in provided calandar + event = Calendar.Events.insert(event, calendarId); + console.log('Event ID: ' + event.id); + } catch (err) { + console.log('Failed with error %s', err.message); + } } /** @@ -75,7 +84,7 @@ function createEvent() { * @return {Date} The new date. */ function getRelativeDate(daysOffset, hour) { - var date = new Date(); + const date = new Date(); date.setDate(date.getDate() + daysOffset); date.setHours(hour); date.setMinutes(0); @@ -83,40 +92,40 @@ function getRelativeDate(daysOffset, hour) { date.setMilliseconds(0); return date; } -// [END apps_script_calendar_create_event] +// [END calendar_create_event] -// [START apps_script_calendar_list_events] +// [START calendar_list_events] /** * Lists the next 10 upcoming events in the user's default calendar. + * @see https://developers.google.com/calendar/api/v3/reference/events/list */ function listNext10Events() { - var calendarId = 'primary'; - var now = new Date(); - var events = Calendar.Events.list(calendarId, { + const calendarId = 'primary'; + const now = new Date(); + const events = Calendar.Events.list(calendarId, { timeMin: now.toISOString(), singleEvents: true, orderBy: 'startTime', maxResults: 10 }); - if (events.items && events.items.length > 0) { - for (var i = 0; i < events.items.length; i++) { - var event = events.items[i]; - if (event.start.date) { - // All-day event. - var start = new Date(event.start.date); - Logger.log('%s (%s)', event.summary, start.toLocaleDateString()); - } else { - var start = new Date(event.start.dateTime); - Logger.log('%s (%s)', event.summary, start.toLocaleString()); - } + if (!events.items || events.items.length === 0) { + console.log('No events found.'); + return; + } + for (const event of events.items) { + if (event.start.date) { + // All-day event. + const start = new Date(event.start.date); + console.log('%s (%s)', event.summary, start.toLocaleDateString()); + return; } - } else { - Logger.log('No events found.'); + const start = new Date(event.start.dateTime); + console.log('%s (%s)', event.summary, start.toLocaleString()); } } -// [END apps_script_calendar_list_events] +// [END calendar_list_events] -// [START apps_script_calendar_log_synced_events] +// [START calendar_log_synced_events] /** * Retrieve and log events from the given calendar that have been modified * since the last sync. If the sync token is missing or invalid, log all @@ -127,21 +136,20 @@ function listNext10Events() { * perform a full sync; if false, use the existing sync token if possible. */ function logSyncedEvents(calendarId, fullSync) { - var properties = PropertiesService.getUserProperties(); - var options = { + const properties = PropertiesService.getUserProperties(); + const options = { maxResults: 100 }; - var syncToken = properties.getProperty('syncToken'); + const syncToken = properties.getProperty('syncToken'); if (syncToken && !fullSync) { options.syncToken = syncToken; } else { // Sync events up to thirty days in the past. options.timeMin = getRelativeDate(-30, 0).toISOString(); } - // Retrieve events one page at a time. - var events; - var pageToken; + let events; + let pageToken; do { try { options.pageToken = pageToken; @@ -153,38 +161,34 @@ function logSyncedEvents(calendarId, fullSync) { properties.deleteProperty('syncToken'); logSyncedEvents(calendarId, true); return; - } else { - throw new Error(e.message); } + throw new Error(e.message); } - - if (events.items && events.items.length > 0) { - for (var i = 0; i < events.items.length; i++) { - var event = events.items[i]; - if (event.status === 'cancelled') { - console.log('Event id %s was cancelled.', event.id); - } else if (event.start.date) { - // All-day event. - var start = new Date(event.start.date); - console.log('%s (%s)', event.summary, start.toLocaleDateString()); - } else { - // Events that don't last all day; they have defined start times. - var start = new Date(event.start.dateTime); - console.log('%s (%s)', event.summary, start.toLocaleString()); - } - } - } else { + if (events.items && events.items.length === 0) { console.log('No events found.'); + return; + } + for (const event of events.items) { + if (event.status === 'cancelled') { + console.log('Event id %s was cancelled.', event.id); + return; + } + if (event.start.date) { + const start = new Date(event.start.date); + console.log('%s (%s)', event.summary, start.toLocaleDateString()); + return; + } + // Events that don't last all day; they have defined start times. + const start = new Date(event.start.dateTime); + console.log('%s (%s)', event.summary, start.toLocaleString()); } - pageToken = events.nextPageToken; } while (pageToken); - properties.setProperty('syncToken', events.nextSyncToken); } -// [END apps_script_calendar_log_synced_events] +// [END calendar_log_synced_events] -// [START apps_script_calendar_conditional_update] +// [START calendar_conditional_update] /** * Creates an event in the user's default calendar, waits 30 seconds, then * attempts to update the event's location, on the condition that the event @@ -196,10 +200,10 @@ function logSyncedEvents(calendarId, fullSync) { * to the etag of the new event when it was created. */ function conditionalUpdate() { - var calendarId = 'primary'; - var start = getRelativeDate(1, 12); - var end = getRelativeDate(1, 13); - var event = { + const calendarId = 'primary'; + const start = getRelativeDate(1, 12); + const end = getRelativeDate(1, 13); + let event = { summary: 'Lunch Meeting', location: 'The Deli', description: 'To discuss our plans for the presentation next week.', @@ -210,14 +214,14 @@ function conditionalUpdate() { dateTime: end.toISOString() }, attendees: [ - {email: 'alice@example.com'}, - {email: 'bob@example.com'} + {email: 'gduser1@workspacesample.dev'}, + {email: 'gduser2@workspacesample.dev'} ], // Red background. Use Calendar.Colors.get() for the full list. colorId: 11 }; event = Calendar.Events.insert(event, calendarId); - Logger.log('Event ID: ' + event.getId()); + console.log('Event ID: ' + event.getId()); // Wait 30 seconds to see if the event has been updated outside this script. Utilities.sleep(30 * 1000); // Try to update the event, on the condition that the event state has not @@ -225,20 +229,20 @@ function conditionalUpdate() { event.location = 'The Coffee Shop'; try { event = Calendar.Events.update( - event, - calendarId, - event.id, - {}, - {'If-Match': event.etag} + event, + calendarId, + event.id, + {}, + {'If-Match': event.etag} ); - Logger.log('Successfully updated event: ' + event.id); + console.log('Successfully updated event: ' + event.id); } catch (e) { - Logger.log('Fetch threw an exception: ' + e); + console.log('Fetch threw an exception: ' + e); } } -// [END apps_script_calendar_conditional_update] +// [END calendar_conditional_update] -// [START apps_script_calendar_conditional_fetch] +// [START calendar_conditional_fetch] /** * Creates an event in the user's default calendar, then re-fetches the event * every second, on the condition that the event has changed since the last @@ -248,10 +252,10 @@ function conditionalUpdate() { * to the etag of the last known state of the event. */ function conditionalFetch() { - var calendarId = 'primary'; - var start = getRelativeDate(1, 12); - var end = getRelativeDate(1, 13); - var event = { + const calendarId = 'primary'; + const start = getRelativeDate(1, 12); + const end = getRelativeDate(1, 13); + let event = { summary: 'Lunch Meeting', location: 'The Deli', description: 'To discuss our plans for the presentation next week.', @@ -262,23 +266,24 @@ function conditionalFetch() { dateTime: end.toISOString() }, attendees: [ - {email: 'alice@example.com'}, - {email: 'bob@example.com'} + {email: 'gduser1@workspacesample.dev'}, + {email: 'gduser2@workspacesample.dev'} ], // Red background. Use Calendar.Colors.get() for the full list. colorId: 11 }; - event = Calendar.Events.insert(event, calendarId); - Logger.log('Event ID: ' + event.getId()); - // Re-fetch the event each second, but only get a result if it has changed. - for (var i = 0; i < 30; i++) { - Utilities.sleep(1000); - try { + try { + // insert event + event = Calendar.Events.insert(event, calendarId); + console.log('Event ID: ' + event.getId()); + // Re-fetch the event each second, but only get a result if it has changed. + for (let i = 0; i < 30; i++) { + Utilities.sleep(1000); event = Calendar.Events.get(calendarId, event.id, {}, {'If-None-Match': event.etag}); - Logger.log('New event description: ' + event.description); - } catch (e) { - Logger.log('Fetch threw an exception: ' + e); + console.log('New event description: ' + event.start.dateTime); } + } catch (e) { + console.log('Fetch threw an exception: ' + e); } } -// [END apps_script_calendar_conditional_fetch] +// [END calendar_conditional_fetch] diff --git a/advanced/classroom.gs b/advanced/classroom.gs index cc9b6f3ff..1dc7c8a54 100644 --- a/advanced/classroom.gs +++ b/advanced/classroom.gs @@ -18,18 +18,27 @@ * Lists 10 course names and IDs. */ function listCourses() { - var optionalArgs = { + /** + * @see https://developers.google.com/classroom/reference/rest/v1/courses/list + */ + const optionalArgs = { pageSize: 10 + // Use other query parameters here if needed. }; - var response = Classroom.Courses.list(optionalArgs); - var courses = response.courses; - if (courses && courses.length > 0) { - for (i = 0; i < courses.length; i++) { - var course = courses[i]; - Logger.log('%s (%s)', course.name, course.id); + try { + const response = Classroom.Courses.list(optionalArgs); + const courses = response.courses; + if (!courses || courses.length === 0) { + console.log('No courses found.'); + return; } - } else { - Logger.log('No courses found.'); + // Print the course names and IDs of the available courses. + for (const course in courses) { + console.log('%s (%s)', courses[course].name, courses[course].id); + } + } catch (err) { + // TODO (developer)- Handle Courses.list() exception from Classroom API + console.log('Failed with error %s', err.message); } } // [END apps_script_classroom_list_courses] diff --git a/advanced/docs.gs b/advanced/docs.gs index c2d742a7d..c8721bf5e 100644 --- a/advanced/docs.gs +++ b/advanced/docs.gs @@ -14,59 +14,74 @@ * limitations under the License. */ -// [START apps_script_docs_create_document] +// [START docs_create_document] /** * Create a new document. + * @see https://developers.google.com/docs/api/reference/rest/v1/documents/create + * @return {string} documentId */ function createDocument() { - var document = Docs.Documents.create({'title': 'My New Document'}); - Logger.log('Created document with ID: ' + document.documentId); + try { + // Create document with title + const document = Docs.Documents.create({'title': 'My New Document'}); + console.log('Created document with ID: ' + document.documentId); + return document.documentId; + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', e.message); + } } -// [END apps_script_docs_create_document] +// [END docs_create_document] -// [START apps_script_docs_find_and_replace] +// [START docs_find_and_replace_text] /** * Performs "replace all". - * @param {string} documentId The document to perform the replace text - * operations on. - * @param {Object} findTextToReplacementMap A map from the "find text" to the - * "replace text". + * @param {string} documentId The document to perform the replace text operations on. + * @param {Object} findTextToReplacementMap A map from the "find text" to the "replace text". + * @return {Object} replies + * @see https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate */ function findAndReplace(documentId, findTextToReplacementMap) { - var requests = []; - for (var findText in findTextToReplacementMap) { - var replaceText = findTextToReplacementMap[findText]; - var request = { + const requests = []; + for (const findText in findTextToReplacementMap) { + const replaceText = findTextToReplacementMap[findText]; + const request = { replaceAllText: { containsText: { text: findText, - matchCase: true, + matchCase: true }, replaceText: replaceText } }; requests.push(request); } - - var response = Docs.Documents.batchUpdate({'requests': requests}, documentId); - var replies = response.replies; - for (var i = 0; i < replies.length; i++) { - var reply = replies[i]; - var numReplacements = reply.replaceAllText.occurrencesChanged || 0; - Logger.log('Request %s performed %s replacements.', i, numReplacements); + try { + const response = Docs.Documents.batchUpdate({'requests': requests}, documentId); + const replies = response.replies; + for (const [index] of replies.entries()) { + const numReplacements = replies[index].replaceAllText.occurrencesChanged || 0; + console.log('Request %s performed %s replacements.', index, numReplacements); + } + return replies; + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with error : %s', e.message); } } -// [END apps_script_docs_find_and_replace] +// [END docs_find_and_replace_text] -// [START apps_script_docs_insert_and_style_text] +// [START docs_insert_and_style_text] /** * Insert text at the beginning of the document and then style the inserted * text. * @param {string} documentId The document the text is inserted into. * @param {string} text The text to insert into the document. + * @return {Object} replies + * @see https://developers.google.com/docs/api/reference/rest/v1/documents/batchUpdate */ function insertAndStyleText(documentId, text) { - var requests = [{ + const requests = [{ insertText: { location: { index: 1 @@ -80,7 +95,7 @@ function insertAndStyleText(documentId, text) { startIndex: 1, endIndex: text.length + 1 }, - text_style: { + textStyle: { fontSize: { magnitude: 12, unit: 'PT' @@ -92,35 +107,48 @@ function insertAndStyleText(documentId, text) { fields: 'weightedFontFamily, fontSize' } }]; - Docs.Documents.batchUpdate({'requests': requests}, documentId); + try { + const response =Docs.Documents.batchUpdate({'requests': requests}, documentId); + return response.replies; + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with an error %s', e.message); + } } -// [END apps_script_docs_insert_and_style_text] +// [END docs_insert_and_style_text] -// [START apps_script_docs_read_first_paragraph] - /** +// [START docs_read_first_paragraph] +/** * Read the first paragraph of the body of a document. * @param {string} documentId The ID of the document to read. + * @return {Object} paragraphText + * @see https://developers.google.com/docs/api/reference/rest/v1/documents/get */ function readFirstParagraph(documentId) { - var document = Docs.Documents.get(documentId); - var bodyElements = document.body.content; - - for (var i = 0; i < bodyElements.length; i++) { - var structuralElement = bodyElements[i]; - if (structuralElement.paragraph !== null) { - var paragraphElements = structuralElement.paragraph.elements; - var paragraphText = ''; + try { + // Get the document using document ID + const document = Docs.Documents.get(documentId); + const bodyElements = document.body.content; + for (let i = 0; i < bodyElements.length; i++) { + const structuralElement = bodyElements[i]; + // Print the first paragraph text present in document + if (structuralElement.paragraph) { + const paragraphElements = structuralElement.paragraph.elements; + let paragraphText = ''; - for (var j = 0; j < paragraphElements.length; j++) { - var paragraphElement = paragraphElements[j]; - if (paragraphElement.textRun !== null) { - paragraphText += paragraphElement.textRun.content; + for (let j = 0; j < paragraphElements.length; j++) { + const paragraphElement = paragraphElements[j]; + if (paragraphElement.textRun !== null) { + paragraphText += paragraphElement.textRun.content; + } } + console.log(paragraphText); + return paragraphText; } - - Logger.log(paragraphText); - return; } + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', e.message); } } -// [END apps_script_docs_read_first_paragraph] +// [END docs_read_first_paragraph] diff --git a/advanced/doubleclick.gs b/advanced/doubleclick.gs index 3659e43fc..1d556465b 100644 --- a/advanced/doubleclick.gs +++ b/advanced/doubleclick.gs @@ -19,15 +19,20 @@ */ function listUserProfiles() { // Retrieve the list of available user profiles - var profiles = DoubleClickCampaigns.UserProfiles.list(); + try { + const profiles = DoubleClickCampaigns.UserProfiles.list(); - if (profiles.items) { - // Print out the user ID and name of each - for (var i = 0; i < profiles.items.length; i++) { - var profile = profiles.items[i]; - Logger.log('Found profile with ID %s and name "%s".', - profile.profileId, profile.userName); + if (profiles.items) { + // Print out the user ID and name of each + for (let i = 0; i < profiles.items.length; i++) { + const profile = profiles.items[i]; + console.log('Found profile with ID %s and name "%s".', + profile.profileId, profile.userName); + } } + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } // [END apps_script_doubleclick_list_user_profiles] @@ -38,25 +43,30 @@ function listUserProfiles() { * Note the use of paging tokens to retrieve the whole list. */ function listActiveCampaigns() { - var profileId = '1234567'; // Replace with your profile ID. - var fields = 'nextPageToken,campaigns(id,name)'; - var result; - var pageToken; - do { - result = DoubleClickCampaigns.Campaigns.list(profileId, { - 'archived': false, - 'fields': fields, - 'pageToken': pageToken - }); - if (result.campaigns) { - for (var i = 0; i < result.campaigns.length; i++) { - var campaign = result.campaigns[i]; - Logger.log('Found campaign with ID %s and name "%s".', - campaign.id, campaign.name); + const profileId = '1234567'; // Replace with your profile ID. + const fields = 'nextPageToken,campaigns(id,name)'; + let result; + let pageToken; + try { + do { + result = DoubleClickCampaigns.Campaigns.list(profileId, { + 'archived': false, + 'fields': fields, + 'pageToken': pageToken + }); + if (result.campaigns) { + for (let i = 0; i < result.campaigns.length; i++) { + const campaign = result.campaigns[i]; + console.log('Found campaign with ID %s and name "%s".', + campaign.id, campaign.name); + } } - } - pageToken = result.nextPageToken; - } while (pageToken); + pageToken = result.nextPageToken; + } while (pageToken); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } // [END apps_script_doubleclick_list_active_campaigns] @@ -66,36 +76,42 @@ function listActiveCampaigns() { * The campaign is set to last for one month. */ function createAdvertiserAndCampaign() { - var profileId = '1234567'; // Replace with your profile ID. + const profileId = '1234567'; // Replace with your profile ID. - var advertiser = { + const advertiser = { name: 'Example Advertiser', status: 'APPROVED' }; - var advertiserId = DoubleClickCampaigns.Advertisers - .insert(advertiser, profileId).id; - var landingPage = { - advertiserId: advertiserId, - archived: false, - name: 'Example landing page', - url: 'https://www.google.com' - }; - var landingPageId = DoubleClickCampaigns.AdvertiserLandingPages - .insert(landingPage, profileId).id; + try { + const advertiserId = DoubleClickCampaigns.Advertisers + .insert(advertiser, profileId).id; - var campaignStart = new Date(); - // End campaign after 1 month. - var campaignEnd = new Date(); - campaignEnd.setMonth(campaignEnd.getMonth() + 1); + const landingPage = { + advertiserId: advertiserId, + archived: false, + name: 'Example landing page', + url: 'https://www.google.com' + }; + const landingPageId = DoubleClickCampaigns.AdvertiserLandingPages + .insert(landingPage, profileId).id; - var campaign = { - advertiserId: advertiserId, - defaultLandingPageId: landingPageId, - name: 'Example campaign', - startDate: Utilities.formatDate(campaignStart, 'GMT', 'yyyy-MM-dd'), - endDate: Utilities.formatDate(campaignEnd, 'GMT', 'yyyy-MM-dd') - }; - DoubleClickCampaigns.Campaigns.insert(campaign, profileId); + const campaignStart = new Date(); + // End campaign after 1 month. + const campaignEnd = new Date(); + campaignEnd.setMonth(campaignEnd.getMonth() + 1); + + const campaign = { + advertiserId: advertiserId, + defaultLandingPageId: landingPageId, + name: 'Example campaign', + startDate: Utilities.formatDate(campaignStart, 'GMT', 'yyyy-MM-dd'), + endDate: Utilities.formatDate(campaignEnd, 'GMT', 'yyyy-MM-dd') + }; + DoubleClickCampaigns.Campaigns.insert(campaign, profileId); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } // [END apps_script_doubleclick_create_advertiser_and_campaign] diff --git a/advanced/drive.gs b/advanced/drive.gs index 8c848eb65..6dcce2256 100644 --- a/advanced/drive.gs +++ b/advanced/drive.gs @@ -13,84 +13,110 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// [START apps_script_drive_upload_file] + +// [START drive_upload_file] /** * Uploads a new file to the user's Drive. */ function uploadFile() { - var image = UrlFetchApp.fetch('http://goo.gl/nd7zjB').getBlob(); - var file = { - title: 'google_logo.png', - mimeType: 'image/png' - }; - file = Drive.Files.insert(file, image); - Logger.log('ID: %s, File size (bytes): %s', file.id, file.fileSize); + try { + // Makes a request to fetch a URL. + const image = UrlFetchApp.fetch('http://goo.gl/nd7zjB').getBlob(); + let file = { + title: 'google_logo.png', + mimeType: 'image/png' + }; + // Insert new files to user's Drive + file = Drive.Files.insert(file, image); + console.log('ID: %s, File size (bytes): %s', file.id, file.fileSize); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed to upload file with error %s', err.message); + } } -// [END apps_script_drive_upload_file] +// [END drive_upload_file] -// [START apps_script_drive_list_root_folders] +// [START drive_list_root_folders] /** * Lists the top-level folders in the user's Drive. */ function listRootFolders() { - var query = '"root" in parents and trashed = false and ' + - 'mimeType = "application/vnd.google-apps.folder"'; - var folders; - var pageToken; + const query = '"root" in parents and trashed = false and ' + + 'mimeType = "application/vnd.google-apps.folder"'; + let folders; + let pageToken = null; do { - folders = Drive.Files.list({ - q: query, - maxResults: 100, - pageToken: pageToken - }); - if (folders.items && folders.items.length > 0) { - for (var i = 0; i < folders.items.length; i++) { - var folder = folders.items[i]; - Logger.log('%s (ID: %s)', folder.title, folder.id); + try { + folders = Drive.Files.list({ + q: query, + maxResults: 100, + pageToken: pageToken + }); + if (!folders.items || folders.items.length === 0) { + console.log('No folders found.'); + return; + } + for (let i = 0; i < folders.items.length; i++) { + const folder = folders.items[i]; + console.log('%s (ID: %s)', folder.title, folder.id); } - } else { - Logger.log('No folders found.'); + pageToken = folders.nextPageToken; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); } - pageToken = folders.nextPageToken; } while (pageToken); } -// [END apps_script_drive_list_root_folders] +// [END drive_list_root_folders] -// [START apps_script_drive_add_custom_property] +// [START drive_add_custom_property] /** * Adds a custom property to a file. Unlike Apps Script's DocumentProperties, - * Drive's custom file properties can be accessed outside of Apps Script and by - * other applications (if the visibility is set to PUBLIC). + * Drive's custom file properties can be accessed outside of Apps Script and + * by other applications (if the visibility is set to PUBLIC). * @param {string} fileId The ID of the file to add the property to. */ function addCustomProperty(fileId) { - var property = { - key: 'department', - value: 'Sales', - visibility: 'PUBLIC' - }; - Drive.Properties.insert(property, fileId); + try { + const property = { + key: 'department', + value: 'Sales', + visibility: 'PUBLIC' + }; + // Adds a property to a file + Drive.Properties.insert(property, fileId); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } } -// [END apps_script_drive_add_custom_property] +// [END drive_add_custom_property] -// [START apps_script_drive_list_revisions] +// [START drive_list_revisions] /** * Lists the revisions of a given file. Note that some properties of revisions - * are only available for certain file types. For example, G Suite application - * files do not consume space in Google Drive and thus list a file size of 0. + * are only available for certain file types. For example, Google Workspace + * application files do not consume space in Google Drive and thus list a file + * size of 0. * @param {string} fileId The ID of the file to list revisions for. */ function listRevisions(fileId) { - var revisions = Drive.Revisions.list(fileId); - if (revisions.items && revisions.items.length > 0) { - for (var i = 0; i < revisions.items.length; i++) { - var revision = revisions.items[i]; - var date = new Date(revision.modifiedDate); - Logger.log('Date: %s, File size (bytes): %s', date.toLocaleString(), + try { + const revisions = Drive.Revisions.list(fileId); + if (!revisions.items || revisions.items.length === 0) { + console.log('No revisions found.'); + return; + } + for (let i = 0; i < revisions.items.length; i++) { + const revision = revisions.items[i]; + const date = new Date(revision.modifiedDate); + console.log('Date: %s, File size (bytes): %s', date.toLocaleString(), revision.fileSize); } - } else { - Logger.log('No revisions found.'); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); } } -// [END apps_script_drive_list_revisions] + +// [END drive_list_revisions] diff --git a/advanced/driveActivity.gs b/advanced/driveActivity.gs index 3719bc05c..3d5f8bd0f 100644 --- a/advanced/driveActivity.gs +++ b/advanced/driveActivity.gs @@ -39,6 +39,6 @@ function getUsersActivity() { } pageToken = result.nextPageToken; } while (pageToken); - Logger.log(Object.keys(users)); + console.log(Object.keys(users)); } // [END apps_script_drive_activity_get_users_activity] diff --git a/advanced/driveLabels.gs b/advanced/driveLabels.gs new file mode 100644 index 000000000..1e4ffa447 --- /dev/null +++ b/advanced/driveLabels.gs @@ -0,0 +1,124 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START apps_script_drive_labels_list_labels] +/** + * List labels available to the user. + */ +function listLabels() { + let pageToken = null; + let labels = []; + do { + try { + const response = DriveLabels.Labels.list({ + publishedOnly: true, + pageToken: pageToken + }); + pageToken = response.nextPageToken; + labels = labels.concat(response.labels); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed to list labels with error %s', err.message); + } + } while (pageToken != null); + + console.log('Found %d labels', labels.length); +} +// [END apps_script_drive_labels_list_labels] + +// [START apps_script_drive_labels_get_label] +/** + * Get a label by name. + * @param {string} labelName The label name. + */ +function getLabel(labelName) { + try { + const label = DriveLabels.Labels.get(labelName, {view: 'LABEL_VIEW_FULL'}); + const title = label.properties.title; + const fieldsLength = label.fields.length; + console.log(`Fetched label with title: '${title}' and ${fieldsLength} fields.`); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed to get label with error %s', err.message); + } +} +// [END apps_script_drive_labels_get_label] + +// [START apps_script_drive_labels_list_labels_on_drive_item] +/** + * List Labels on a Drive Item + * Fetches a Drive Item and prints all applied values along with their to their + * human-readable names. + * + * @param {string} fileId The Drive File ID + */ +function listLabelsOnDriveItem(fileId) { + try { + const appliedLabels = Drive.Files.listLabels(fileId); + + console.log('%d label(s) are applied to this file', appliedLabels.items.length); + + appliedLabels.items.forEach((appliedLabel) => { + // Resource name of the label at the applied revision. + const labelName = 'labels/' + appliedLabel.id + '@' + appliedLabel.revisionId; + + console.log('Fetching Label: %s', labelName); + const label = DriveLabels.Labels.get(labelName, {view: 'LABEL_VIEW_FULL'}); + + console.log('Label Title: %s', label.properties.title); + + Object.keys(appliedLabel.fields).forEach((fieldId) => { + const fieldValue = appliedLabel.fields[fieldId]; + const field = label.fields.find((f) => f.id == fieldId); + + console.log(`Field ID: ${field.id}, Display Name: ${field.properties.displayName}`); + switch (fieldValue.valueType) { + case 'text': + console.log('Text: %s', fieldValue.text[0]); + break; + case 'integer': + console.log('Integer: %d', fieldValue.integer[0]); + break; + case 'dateString': + console.log('Date: %s', fieldValue.dateString[0]); + break; + case 'user': + const user = fieldValue.user.map((user) => { + return `${user.emailAddress}: ${user.displayName}`; + }).join(', '); + console.log(`User: ${user}`); + break; + case 'selection': + const choices = fieldValue.selection.map((choiceId) => { + return field.selectionOptions.choices.find((choice) => choice.id === choiceId); + }); + const selection = choices.map((choice) => { + return `${choice.id}: ${choice.properties.displayName}`; + }).join(', '); + console.log(`Selection: ${selection}`); + break; + default: + console.log('Unknown: %s', fieldValue.valueType); + console.log(fieldValue.value); + } + }); + }); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } +} +// [END apps_script_drive_labels_list_labels_on_drive_item] diff --git a/advanced/fusionTables.gs b/advanced/fusionTables.gs deleted file mode 100644 index 98f500e21..000000000 --- a/advanced/fusionTables.gs +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_fusion_tables_list] -/** - * This sample lists Fusion Tables that the user has access to. - */ -function listTables() { - var tables = FusionTables.Table.list(); - if (tables.items) { - for (var i = 0; i < tables.items.length; i++) { - var table = tables.items[i]; - Logger.log('Table with name "%s" and ID "%s" was found.', - table.name, table.tableId); - } - } else { - Logger.log('No tables found.'); - } -} -// [END apps_script_fusion_tables_list] - -// [START apps_script_fusion_tables_run_query] -/** - * This sample queries for the first 100 rows in the given Fusion Table and - * saves the results to a new spreadsheet. - * @param {string} tableId The table ID. - */ -function runQuery(tableId) { - var sql = 'SELECT * FROM ' + tableId + ' LIMIT 100'; - var result = FusionTables.Query.sqlGet(sql, { - hdrs: false - }); - if (result.rows) { - var spreadsheet = SpreadsheetApp.create('Fusion Table Query Results'); - var sheet = spreadsheet.getActiveSheet(); - - // Append the headers. - sheet.appendRow(result.columns); - - // Append the results. - sheet.getRange(2, 1, result.rows.length, result.columns.length) - .setValues(result.rows); - - Logger.log('Query results spreadsheet created: %s', - spreadsheet.getUrl()); - } else { - Logger.log('No rows returned.'); - } -} -// [END apps_script_fusion_tables_run_query] diff --git a/advanced/gmail.gs b/advanced/gmail.gs index d866e05f6..a1195c6b7 100644 --- a/advanced/gmail.gs +++ b/advanced/gmail.gs @@ -13,101 +13,120 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// [START apps_script_gmail_label] +// [START gmail_label] /** * Lists the user's labels, including name, type, * ID and visibility information. */ function listLabelInfo() { - var response = - Gmail.Users.Labels.list('me'); - for (var i = 0; i < response.labels.length; i++) { - var label = response.labels[i]; - Logger.log(JSON.stringify(label)); + try { + const response = + Gmail.Users.Labels.list('me'); + for (let i = 0; i < response.labels.length; i++) { + const label = response.labels[i]; + console.log(JSON.stringify(label)); + } + } catch (err) { + console.log(err); } } -// [END apps_script_gmail_label] +// [END gmail_label] -// [START apps_script_gmail_inbox_snippets] +// [START gmail_inbox_snippets] /** * Lists, for each thread in the user's Inbox, a * snippet associated with that thread. */ function listInboxSnippets() { - var pageToken; - do { - var threadList = Gmail.Users.Threads.list('me', { - q: 'label:inbox', - pageToken: pageToken - }); - if (threadList.threads && threadList.threads.length > 0) { - threadList.threads.forEach(function(thread) { - Logger.log('Snippet: %s', thread.snippet); + try { + let pageToken; + do { + const threadList = Gmail.Users.Threads.list('me', { + q: 'label:inbox', + pageToken: pageToken }); - } - pageToken = threadList.nextPageToken; - } while (pageToken); + if (threadList.threads && threadList.threads.length > 0) { + threadList.threads.forEach(function(thread) { + console.log('Snippet: %s', thread.snippet); + }); + } + pageToken = threadList.nextPageToken; + } while (pageToken); + } catch (err) { + console.log(err); + } } -// [END apps_script_gmail_inbox_snippets] +// [END gmail_inbox_snippets] -// [START apps_script_gmail_history] +// [START gmail_history] /** * Gets a history record ID associated with the most * recently sent message, then logs all the message IDs * that have changed since that message was sent. */ function logRecentHistory() { - // Get the history ID associated with the most recent - // sent message. - var sent = Gmail.Users.Threads.list('me', { + try { + // Get the history ID associated with the most recent + // sent message. + const sent = Gmail.Users.Threads.list('me', { q: 'label:sent', maxResults: 1 - }); - if (!sent.threads || !sent.threads[0]) { - Logger.log('No sent threads found.'); - return; - } - var historyId = sent.threads[0].historyId; - - // Log the ID of each message changed since the most - // recent message was sent. - var pageToken; - var changed = []; - do { - var recordList = Gmail.Users.History.list('me', { - startHistoryId: historyId, - pageToken: pageToken }); - var history = recordList.history; - if (history && history.length > 0) { - history.forEach(function(record) { - record.messages.forEach(function(message) { - if (changed.indexOf(message.id) === -1) { - changed.push(message.id); - } - }); - }); + if (!sent.threads || !sent.threads[0]) { + console.log('No sent threads found.'); + return; } - pageToken = recordList.nextPageToken; - } while (pageToken); + const historyId = sent.threads[0].historyId; - changed.forEach(function(id) { - Logger.log('Message Changed: %s', id); - }); + // Log the ID of each message changed since the most + // recent message was sent. + let pageToken; + const changed = []; + do { + const recordList = Gmail.Users.History.list('me', { + startHistoryId: historyId, + pageToken: pageToken + }); + const history = recordList.history; + if (history && history.length > 0) { + history.forEach(function(record) { + record.messages.forEach(function(message) { + if (changed.indexOf(message.id) === -1) { + changed.push(message.id); + } + }); + }); + } + pageToken = recordList.nextPageToken; + } while (pageToken); + + changed.forEach(function(id) { + console.log('Message Changed: %s', id); + }); + } catch (err) { + console.log(err); + } } -// [END apps_script_gmail_history] +// [END gmail_history] -// [START apps_script_gmail_raw] +// [START gmail_raw] +/** + * Logs the raw message content for the most recent message in gmail. + */ function getRawMessage() { - var messageId = Gmail.Users.Messages.list('me').messages[0].id; - console.log(messageId); - var message = Gmail.Users.Messages.get('me', messageId, { - 'format': 'raw' - }); + try { + const messageId = Gmail.Users.Messages.list('me').messages[0].id; + console.log(messageId); + const message = Gmail.Users.Messages.get('me', messageId, { + 'format': 'raw' + }); - // Get raw content as base64url encoded string. - var encodedMessage = Utilities.base64Encode(message.raw); - console.log(encodedMessage); + // Get raw content as base64url encoded string. + const encodedMessage = Utilities.base64Encode(message.raw); + console.log(encodedMessage); + } catch (err) { + console.log(err); + } } -// [END apps_script_gmail_raw] +// [END gmail_raw] diff --git a/advanced/googlePlus.gs b/advanced/googlePlus.gs deleted file mode 100644 index bcafffe06..000000000 --- a/advanced/googlePlus.gs +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_plus_people] -/** - * The following example demonstrates how to retrieve a list of the people - * in the user's Google+ circles. - */ -function getPeople() { - var userId = 'me'; - var people; - var pageToken; - do { - people = Plus.People.list(userId, 'visible', { - pageToken: pageToken - }); - if (people.items) { - for (var i = 0; i < people.items.length; i++) { - var person = people.items[i]; - Logger.log(person.displayName); - } - } else { - Logger.log('No people in your visible circles.'); - } - pageToken = people.nextPageToken; - } while (pageToken); -} -// [END apps_script_plus_people] - - // [START apps_script_plus_posts] -/** - * The following example demonstrates how to list a user's posts. The returned - * results contain a brief summary of the posts, including a list of comments - * made on the post. - */ -function getPosts() { - var userId = 'me'; - var posts; - var pageToken; - do { - posts = Plus.Activities.list(userId, 'public', { - maxResults: 10, - pageToken: pageToken - }); - if (posts.items) { - for (var i = 0; i < posts.items.length; i++) { - var post = posts.items[i]; - Logger.log(post.title); - var comments = Plus.Comments.list(post.id); - if (comments.items) { - for (var j = 0; j < comments.items.length; j++) { - var comment = comments.items[j]; - Logger.log(comment.actor.displayName + ': ' + - comment.object.content); - } - } - } - } else { - Logger.log('No posts found.'); - } - pageToken = posts.pageToken; - } while (pageToken); -} -// [END apps_script_plus_posts] diff --git a/advanced/googlePlusDomains.gs b/advanced/googlePlusDomains.gs deleted file mode 100644 index 129b60e68..000000000 --- a/advanced/googlePlusDomains.gs +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_plus_domains_profile] -/** - * The following example demonstrates how to retrieve details from a user's - * Google+ profile. - */ -function getProfile() { - var userId = 'me'; - var profile = PlusDomains.People.get(userId); - - Logger.log('ID: %s', profile.id); - Logger.log('Display name: %s', profile.displayName); - Logger.log('Image URL: %s', profile.image.url); - Logger.log('Profile URL: %s', profile.url); -} -// [END apps_script_plus_domains_profile] - - // [START apps_script_plus_domains_circle] -/** - * The following example demonstrates how to create an empty circle for a user - * within your G Suite domain. - */ -function createCircle() { - var userId = 'me'; - var circle = PlusDomains.newCircle(); - circle.displayName = 'Tech support'; - - circle = PlusDomains.Circles.insert(circle, userId); - Logger.log('Created "Tech support" circle with id: ' + circle.id); -} -// [END apps_script_plus_domains_circle] - - // [START apps_script_plus_domains_get_posts] -/** - * The following example demonstrates how to list a user's posts. The returned - * results contain a brief summary of the posts, including a title. Use the - * Activities.get() method to read the full details of a post. - */ -function getPosts() { - var userId = 'me'; - var pageToken; - var posts; - do { - posts = PlusDomains.Activities.list(userId, 'user', { - maxResults: 100, - pageToken: pageToken - }); - if (posts.items) { - for (var i = 0; i < posts.items.length; i++) { - var post = posts.items[i]; - Logger.log('ID: %s, Content: %s', post.id, post.object.content); - } - } - pageToken = posts.nextPageToken; - } while (pageToken); -} -// [END apps_script_plus_domains_get_posts] - -// [START apps_script_plus_domains_create_post] -/** - * The following example demonstrates how to create a post that is available - * to all users within your G Suite domain. - */ -function createPost() { - var userId = 'me'; - var post = { - object: { - originalContent: 'Happy Monday! #caseofthemondays' - }, - access: { - items: [{ - type: 'domain' - }], - domainRestricted: true - } - }; - - post = PlusDomains.Activities.insert(post, userId); - Logger.log('Post created with URL: %s', post.url); -} -// [END apps_script_plus_domains_create_post] diff --git a/advanced/iot.gs b/advanced/iot.gs index b6b44dcf7..8087fd023 100644 --- a/advanced/iot.gs +++ b/advanced/iot.gs @@ -18,17 +18,17 @@ * Lists the registries for the configured project and region. */ function listRegistries() { - Logger.log(response); + console.log(response); var projectId = 'your-project-id'; var cloudRegion = 'us-central1'; var parent = 'projects/' + projectId + '/locations/' + cloudRegion; var response = CloudIoT.Projects.Locations.Registries.list(parent); - if (response.deviceRegistries){ + if (response.deviceRegistries) { response.deviceRegistries.forEach( - function(registry){ - Logger.log(registry.id); - }); + function(registry) { + console.log(registry.id); + }); } } // [END apps_script_iot_list_registries] @@ -46,16 +46,16 @@ function createRegistry() { var pubsubTopic = 'projects/' + projectId + '/topics/' + topic; var registry = { - eventNotificationConfigs: [{ + 'eventNotificationConfigs': [{ // From - https://console.cloud.google.com/cloudpubsub - pubsubTopicName : pubsubTopic + pubsubTopicName: pubsubTopic }], 'id': name }; var parent = 'projects/' + projectId + '/locations/' + cloudRegion; - var response = CloudIoT.Projects.Locations.Registries.create(registry, parent) - Logger.log('Created registry: ' + response.id); + var response = CloudIoT.Projects.Locations.Registries.create(registry, parent); + console.log('Created registry: ' + response.id); } // [END apps_script_iot_create_registry] @@ -71,8 +71,8 @@ function getRegistry() { var parent = 'projects/' + projectId + '/locations/' + cloudRegion; var registryName = parent + '/registries/' + name; - var response = CloudIoT.Projects.Locations.Registries.get(registryName) - Logger.log('Retrieved registry: ' + response.id); + var response = CloudIoT.Projects.Locations.Registries.get(registryName); + console.log('Retrieved registry: ' + response.id); } // [END apps_script_iot_get_registry] @@ -88,9 +88,9 @@ function deleteRegistry() { var parent = 'projects/' + projectId + '/locations/' + cloudRegion; var registryName = parent + '/registries/' + name; - var response = CloudIoT.Projects.Locations.Registries.remove(registryName) + var response = CloudIoT.Projects.Locations.Registries.remove(registryName); // Successfully removed registry if exception was not thrown. - Logger.log('Deleted registry: ' + name); + console.log('Deleted registry: ' + name); } // [END apps_script_iot_delete_registry] @@ -108,12 +108,12 @@ function listDevicesForRegistry() { var response = CloudIoT.Projects.Locations.Registries.Devices.list(registryName); - Logger.log('Registry contains the following devices: '); + console.log('Registry contains the following devices: '); if (response.devices) { response.devices.forEach( - function(device){ - Logger.log('\t' + device.id); - }); + function(device) { + console.log('\t' + device.id); + }); } } // [END apps_script_iot_list_devices] @@ -128,7 +128,7 @@ function createDevice() { var projectId = 'your-project-id'; var registry = 'your-registry-name'; - Logger.log('Creating device: ' + name + ' in Registry: ' + registry); + console.log('Creating device: ' + name + ' in Registry: ' + registry); var parent = 'projects/' + projectId + '/locations/' + cloudRegion + '/registries/' + registry; var device = { @@ -139,8 +139,8 @@ function createDevice() { } }; - var response = CloudIoT.Projects.Locations.Registries.Devices.create(device, parent) - Logger.log('Created device:' + response.name); + var response = CloudIoT.Projects.Locations.Registries.Devices.create(device, parent); + console.log('Created device:' + response.name); } // [END apps_script_iot_create_unauth_device] @@ -178,11 +178,11 @@ function createRsaDevice() { format: 'RSA_X509_PEM', key: cert } - }], + }] }; var response = CloudIoT.Projects.Locations.Registries.Devices.create(device, parent); - Logger.log('Created device:' + response.name); + console.log('Created device:' + response.name); } // [END apps_script_iot_create_rsa_device] @@ -199,9 +199,9 @@ function deleteDevice() { var parent = 'projects/' + projectId + '/locations/' + cloudRegion + '/registries/' + registry; var deviceName = parent + '/devices/' + name; - var response = CloudIoT.Projects.Locations.Registries.Devices.remove(deviceName) + var response = CloudIoT.Projects.Locations.Registries.Devices.remove(deviceName); // If no exception thrown, device was successfully removed - Logger.log('Successfully deleted device: ' + deviceName); + console.log('Successfully deleted device: ' + deviceName); } // [END apps_script_iot_delete_device] diff --git a/advanced/mirror.gs b/advanced/mirror.gs deleted file mode 100644 index 7be30550f..000000000 --- a/advanced/mirror.gs +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_mirror_timeline] -/** - * This sample inserts a new item into the timeline. - */ -function insertTimelineItem() { - var timelineItem = Mirror.newTimelineItem(); - timelineItem.text = 'Hello world!'; - - var notificationConfig = Mirror.newNotificationConfig(); - notificationConfig.level = 'AUDIO_ONLY'; - - var menuItem = Mirror.newMenuItem(); - menuItem.action = 'REPLY'; - - timelineItem.notification = notificationConfig; - timelineItem.menuItems = [menuItem]; - - Mirror.Timeline.insert(timelineItem); -} -// [END apps_script_mirror_timeline] - -// [START apps_script_mirror_contact] -/** - * This sample inserts a new contact. - */ -function insertContact() { - var contact = { - id: 'harold', - displayName: 'Harold Penguin', - imageUrls: ['https://developers.google.com/glass/images/harold.jpg'] - }; - - Mirror.Contacts.insert(contact); -} -// [END apps_script_mirror_contact] - -// [START apps_script_mirror_location] -/** - * This sample prints the most recent known location of the user's Glass to the - * script editor's log. - */ -function printLatestLocation() { - var location = Mirror.Locations.get('latest'); - - Logger.log('Location recorded on: ' + location.timestamp); - Logger.log(' > Latitude: ' + location.latitude); - Logger.log(' > Longitude: ' + location.longitude); - Logger.log(' > Accuracy: ' + location.accuracy + ' meters'); -} -// [END apps_script_mirror_location] diff --git a/advanced/people.gs b/advanced/people.gs index 1077ec88c..10cf68d09 100644 --- a/advanced/people.gs +++ b/advanced/people.gs @@ -13,40 +13,177 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// [START apps_script_people_get_connections] +// [START people_get_connections] /** * Gets a list of people in the user's contacts. + * @see https://developers.google.com/people/api/rest/v1/people.connections/list */ function getConnections() { - var people = People.People.Connections.list('people/me', { - personFields: 'names,emailAddresses' - }); - Logger.log('Connections: %s', JSON.stringify(people, null, 2)); + try { + // Get the list of connections/contacts of user's profile + const people = People.People.Connections.list('people/me', { + personFields: 'names,emailAddresses' + }); + // Print the connections/contacts + console.log('Connections: %s', JSON.stringify(people, null, 2)); + } catch (err) { + // TODO (developers) - Handle exception here + console.log('Failed to get the connection with an error %s', err.message); + } } -// [END apps_script_people_get_connections] +// [END people_get_connections] -// [START apps_script_people_get_self] +// [START people_get_self_profile] /** * Gets the own user's profile. + * @see https://developers.google.com/people/api/rest/v1/people/getBatchGet */ function getSelf() { - var people = People.People.getBatchGet({ - resourceNames: ['people/me'], - personFields: 'names,emailAddresses' - }); - Logger.log('Myself: %s', JSON.stringify(people, null, 2)); + try { + // Get own user's profile using People.getBatchGet() method + const people = People.People.getBatchGet({ + resourceNames: ['people/me'], + personFields: 'names,emailAddresses' + // Use other query parameter here if needed + }); + console.log('Myself: %s', JSON.stringify(people, null, 2)); + } catch (err) { + // TODO (developer) -Handle exception + console.log('Failed to get own profile with an error %s', err.message); + } } -// [END apps_script_people_get_self] +// [END people_get_self_profile] -// [START apps_script_people_get_account] +// [START people_get_account] /** * Gets the person information for any Google Account. * @param {string} accountId The account ID. + * @see https://developers.google.com/people/api/rest/v1/people/get */ function getAccount(accountId) { - var people = People.People.get('people/' + accountId, { - personFields: 'names,emailAddresses' - }); - Logger.log('Public Profile: %s', JSON.stringify(people, null, 2)); + try { + // Get the Account details using account ID. + const people = People.People.get('people/' + accountId, { + personFields: 'names,emailAddresses' + }); + // Print the profile details of Account. + console.log('Public Profile: %s', JSON.stringify(people, null, 2)); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed to get account with an error %s', err.message); + } } -// [END apps_script_people_get_account] +// [END people_get_account] + +// [START people_get_group] + +/** + * Gets a contact group with the given name + * @param {string} name The group name. + * @see https://developers.google.com/people/api/rest/v1/contactGroups/list + */ +function getContactGroup(name) { + try { + const people = People.ContactGroups.list(); + // Finds the contact group for the person where the name matches. + const group = people['contactGroups'].find((group) => group['name'] === name); + // Prints the contact group + console.log('Group: %s', JSON.stringify(group, null, 2)); + } catch (err) { + // TODO (developers) - Handle exception + console.log('Failed to get the contact group with an error %s', err.message); + } +} + +// [END people_get_group] + +// [START people_get_contact_by_email] + +/** + * Gets a contact by the email address. + * @param {string} email The email address. + * @see https://developers.google.com/people/api/rest/v1/people.connections/list + */ +function getContactByEmail(email) { + try { + // Gets the person with that email address by iterating over all contacts. + const people = People.People.Connections.list('people/me', { + personFields: 'names,emailAddresses' + }); + const contact = people['connections'].find((connection) => { + return connection['emailAddresses'].some((emailAddress) => emailAddress['value'] === email); + }); + // Prints the contact. + console.log('Contact: %s', JSON.stringify(contact, null, 2)); + } catch (err) { + // TODO (developers) - Handle exception + console.log('Failed to get the connection with an error %s', err.message); + } +} + +// [END people_get_contact_by_email] + +// [START people_get_full_name] +/** + * Gets the full name (given name and last name) of the contact as a string. + * @see https://developers.google.com/people/api/rest/v1/people/get + */ +function getFullName() { + try { + // Gets the person by specifying resource name/account ID + // in the first parameter of People.People.get. + // This example gets the person for the user running the script. + const people = People.People.get('people/me', {personFields: 'names'}); + // Prints the full name (given name + family name) + console.log(`${people['names'][0]['givenName']} ${people['names'][0]['familyName']}`); + } catch (err) { + // TODO (developers) - Handle exception + console.log('Failed to get the connection with an error %s', err.message); + } +} + +// [END people_get_full_name] + +// [START people_get_phone_numbers] +/** + * Gets all the phone numbers for this contact. + * @see https://developers.google.com/people/api/rest/v1/people/get + */ +function getPhoneNumbers() { + try { + // Gets the person by specifying resource name/account ID + // in the first parameter of People.People.get. + // This example gets the person for the user running the script. + const people = People.People.get('people/me', {personFields: 'phoneNumbers'}); + // Prints the phone numbers. + console.log(people['phoneNumbers']); + } catch (err) { + // TODO (developers) - Handle exception + console.log('Failed to get the connection with an error %s', err.message); + } +} + +// [END people_get_phone_numbers] + +// [START people_get_single_phone_number] +/** + * Gets a phone number by type, such as work or home. + * @see https://developers.google.com/people/api/rest/v1/people/get + */ +function getPhone() { + try { + // Gets the person by specifying resource name/account ID + // in the first parameter of People.People.get. + // This example gets the person for the user running the script. + const people = People.People.get('people/me', {personFields: 'phoneNumbers'}); + // Gets phone number by type, such as home or work. + const phoneNumber = people['phoneNumbers'].find((phone) => phone['type'] === 'home')['value']; + // Prints the phone numbers. + console.log(phoneNumber); + } catch (err) { + // TODO (developers) - Handle exception + console.log('Failed to get the connection with an error %s', err.message); + } +} + +// [END people_get_single_phone_number] diff --git a/advanced/prediction.gs b/advanced/prediction.gs deleted file mode 100644 index 88ba2817d..000000000 --- a/advanced/prediction.gs +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_prediction_query_hosted_model] -/** - * Runs sentiment analysis across a sentence. - * Prints the sentiment label. - */ -function queryHostedModel() { - // When querying hosted models you must always use this - // specific project number. - var projectNumber = '414649711441'; - var hostedModelName = 'sample.sentiment'; - - // Query the hosted model with a positive statement. - var predictionString = 'Want to go to the park this weekend?'; - var prediction = Prediction.Hostedmodels.predict( - { - input: { - csvInstance: [predictionString] - } - }, - projectNumber, - hostedModelName); - // Logs Sentiment: positive. - Logger.log('Sentiment: ' + prediction.outputLabel); - - // Now query the hosted model with a negative statement. - predictionString = 'You are not very nice!'; - prediction = Prediction.Hostedmodels.predict( - { - input: { - csvInstance: [predictionString] - } - }, - projectNumber, - hostedModelName); - // Logs Sentiment: negative. - Logger.log('Sentiment: ' + prediction.outputLabel); -} -// [END apps_script_prediction_query_hosted_model] - -// [START apps_script_prediction_create_new_model] -/** - * Creates a new prediction model. - */ -function createNewModel() { - // Replace this value with the project number listed in the Google - // APIs Console project. - var projectNumber = 'XXXXXXXX'; - var id = 'mylanguageidmodel'; - var storageDataLocation = 'languageidsample/language_id.txt'; - - // Returns immediately. Training happens asynchronously. - var result = Prediction.Trainedmodels.insert( - { - id: id, - storageDataLocation: storageDataLocation - }, - projectNumber); - Logger.log(result); -} -// [END apps_script_prediction_create_new_model] - -// [START apps_script_prediction_query_training_status] -/** - * Gets the training status from a prediction model. - * Logs the status. - */ -function queryTrainingStatus() { - // Replace this value with the project number listed in the Google - // APIs Console project. - var projectNumber = 'XXXXXXXX'; - var id = 'mylanguageidmodel'; - - var result = Prediction.Trainedmodels.get(projectNumber, id); - Logger.log(result.trainingStatus); -} -// [END apps_script_prediction_query_training_status] - -// [START apps_script_prediction_query_trailed_model] -/** - * Gets the language from a trained language model. - * Logs the language of the sentence. - */ -function queryTrainedModel() { - // Replace this value with the project number listed in the Google - // APIs Console project. - var projectNumber = 'XXXXXXXX'; - var id = 'mylanguageidmodel'; - var query = 'Este es un mensaje de prueba de ejemplo'; - - var prediction = Prediction.Trainedmodels.predict( - { - input: - { - csvInstance: [query] - } - }, - projectNumber, - id); - // Logs Language: Spanish. - Logger.log('Language: ' + prediction.outputLabel); -} -// [END apps_script_prediction_query_trailed_model] diff --git a/advanced/sheets.gs b/advanced/sheets.gs index 3875bd5f9..cac5df416 100644 --- a/advanced/sheets.gs +++ b/advanced/sheets.gs @@ -13,38 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var spreadsheetId = '1LcjJcCdM6OGrJtJEkHegV-rH5bWZ-mCukEMMCKYtUkc'; -var sheetId = 371977894; -var pivotSourceDataSheetId = 371977894; -var destinationSheetId = 1428299768; - -// [START apps_script_sheets_read_range] +// TODO (developer)- Replace the spreadsheet ID and sheet ID with yours values. +const yourspreadsheetId = '1YdrrmXSjpi4Tz-UuQ0eUKtdzQuvpzRLMoPEz3niTTVU'; +const yourpivotSourceDataSheetId = 635809130; +const yourdestinationSheetId = 83410180; +// [START sheets_read_range] /** * Read a range (A1:D5) of data values. Logs the values. * @param {string} spreadsheetId The spreadsheet ID to read from. + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get */ -function readRange(spreadsheetId) { - var response = Sheets.Spreadsheets.Values.get(spreadsheetId, 'Sheet1!A1:D5'); - Logger.log(response.values); +function readRange(spreadsheetId = yourspreadsheetId) { + try { + const response = Sheets.Spreadsheets.Values.get(spreadsheetId, 'Sheet1!A1:D5'); + if (response.values) { + console.log(response.values); + return; + } + console.log('Failed to get range of values from spreadsheet'); + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', e.message); + } } -// [END apps_script_sheets_read_range] +// [END sheets_read_range] -// [START apps_script_sheets_write_range] +// [START sheets_write_range] /** * Write to multiple, disjoint data ranges. * @param {string} spreadsheetId The spreadsheet ID to write to. + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate */ -function writeToMultipleRanges(spreadsheetId) { +function writeToMultipleRanges(spreadsheetId = yourspreadsheetId) { // Specify some values to write to the sheet. - var columnAValues = [ + const columnAValues = [ ['Item', 'Wheel', 'Door', 'Engine'] ]; - var rowValues = [ + const rowValues = [ ['Cost', 'Stocked', 'Ship Date'], ['$20.50', '4', '3/1/2016'] ]; - var request = { + const request = { 'valueInputOption': 'USER_ENTERED', 'data': [ { @@ -59,19 +69,28 @@ function writeToMultipleRanges(spreadsheetId) { } ] }; - - var response = Sheets.Spreadsheets.Values.batchUpdate(request, spreadsheetId); - Logger.log(response); + try { + const response = Sheets.Spreadsheets.Values.batchUpdate(request, spreadsheetId); + if (response) { + console.log(response); + return; + } + console.log('response null'); + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', e.message); + } } -// [END apps_script_sheets_write_range] +// [END sheets_write_range] -// [START apps_script_sheets_new_sheet] +// [START sheets_add_new_sheet] /** * Add a new sheet with some properties. * @param {string} spreadsheetId The spreadsheet ID. + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate */ -function addSheet(spreadsheetId) { - var requests = [{ +function addSheet(spreadsheetId = yourspreadsheetId) { + const requests = [{ 'addSheet': { 'properties': { 'title': 'Deposits', @@ -87,24 +106,31 @@ function addSheet(spreadsheetId) { } } }]; - - var response = + try { + const response = Sheets.Spreadsheets.batchUpdate({'requests': requests}, spreadsheetId); - Logger.log('Created sheet with ID: ' + + console.log('Created sheet with ID: ' + response.replies[0].addSheet.properties.sheetId); + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', e.message); + } } -// [END apps_script_sheets_new_sheet] +// [END sheets_add_new_sheet] -// [START apps_script_sheets_add_pivot_table] +// [START sheets_add_pivot_table] /** * Add a pivot table. * @param {string} spreadsheetId The spreadsheet ID to add the pivot table to. * @param {string} pivotSourceDataSheetId The sheet ID to get the data from. * @param {string} destinationSheetId The sheet ID to add the pivot table to. + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/batchUpdate */ function addPivotTable( - spreadsheetId, pivotSourceDataSheetId, destinationSheetId) { - var requests = [{ + spreadsheetId = yourspreadsheetId, + pivotSourceDataSheetId= yourpivotSourceDataSheetId, + destinationSheetId= yourdestinationSheetId) { + const requests = [{ 'updateCells': { 'rows': { 'values': [ @@ -164,9 +190,12 @@ function addPivotTable( 'fields': 'pivotTable' } }]; - - var response = - Sheets.Spreadsheets.batchUpdate({'requests': requests}, spreadsheetId); - // The Pivot table will appear anchored to cell A50 of the destination sheet. + try { + const response = Sheets.Spreadsheets.batchUpdate({'requests': requests}, spreadsheetId); + // The Pivot table will appear anchored to cell A50 of the destination sheet. + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', e.message); + } } -// [END apps_script_sheets_add_pivot_table] +// [END sheets_add_pivot_table] diff --git a/advanced/shoppingContent.gs b/advanced/shoppingContent.gs index 69f1ad0d4..22cc7e2c8 100644 --- a/advanced/shoppingContent.gs +++ b/advanced/shoppingContent.gs @@ -18,9 +18,9 @@ * Inserts a product into the products list. Logs the API response. */ function productInsert() { - var merchantId = 123456; // Replace this with your Merchant Center ID. + const merchantId = 123456; // Replace this with your Merchant Center ID. // Create a product resource and insert it - var productResource = { + const productResource = { 'offerId': 'book123', 'title': 'A Tale of Two Cities', 'description': 'A classic novel about the French Revolution', @@ -52,8 +52,14 @@ function productInsert() { } }; - response = ShoppingContent.Products.insert(productResource, merchantId); - Logger.log(response); // RESTful insert returns the JSON object as a response. + try { + response = ShoppingContent.Products.insert(productResource, merchantId); + // RESTful insert returns the JSON object as a response. + console.log(response); + } catch (e) { + // TODO (Developer) - Handle exceptions + console.log('Failed with error: $s', e.error); + } } // [END apps_script_shopping_product_insert] @@ -62,26 +68,31 @@ function productInsert() { * Lists the products for a given merchant. */ function productList() { - var merchantId = 123456; // Replace this with your Merchant Center ID. - var pageToken; - var pageNum = 1; - var maxResults = 10; - do { - var products = ShoppingContent.Products.list(merchantId, { - pageToken: pageToken, - maxResults: maxResults - }); - Logger.log('Page ' + pageNum); - if (products.resources) { - for (var i = 0; i < products.resources.length; i++) { - Logger.log('Item [' + i + '] ==> ' + products.resources[i]); + const merchantId = 123456; // Replace this with your Merchant Center ID. + let pageToken; + let pageNum = 1; + const maxResults = 10; + try { + do { + const products = ShoppingContent.Products.list(merchantId, { + pageToken: pageToken, + maxResults: maxResults + }); + console.log('Page ' + pageNum); + if (products.resources) { + for (let i = 0; i < products.resources.length; i++) { + console.log('Item [' + i + '] ==> ' + products.resources[i]); + } + } else { + console.log('No more products in account ' + merchantId); } - } else { - Logger.log('No more products in account ' + merchantId); - } - pageToken = products.nextPageToken; - pageNum++; - } while (pageToken); + pageToken = products.nextPageToken; + pageNum++; + } while (pageToken); + } catch (e) { + // TODO (Developer) - Handle exceptions + console.log('Failed with error: $s', e.error); + } } // [END apps_script_shopping_product_list] @@ -93,7 +104,7 @@ function productList() { * @param {object} productResource3 The third product resource. */ function custombatch(productResource1, productResource2, productResource3) { - var merchantId = 123456; // Replace this with your Merchant Center ID. + const merchantId = 123456; // Replace this with your Merchant Center ID. custombatchResource = { 'entries': [ { @@ -119,8 +130,13 @@ function custombatch(productResource1, productResource2, productResource3) { } ] }; - var response = ShoppingContent.Products.custombatch(custombatchResource); - Logger.log(response); + try { + const response = ShoppingContent.Products.custombatch(custombatchResource); + console.log(response); + } catch (e) { + // TODO (Developer) - Handle exceptions + console.log('Failed with error: $s', e.error); + } } // [END apps_script_shopping_product_batch_insert] @@ -131,37 +147,43 @@ function custombatch(productResource1, productResource2, productResource3) { */ function updateAccountTax() { // Replace this with your Merchant Center ID. - var merchantId = 123456; + const merchantId = 123456; // Replace this with the account that you are updating taxes for. - var accountId = 123456; + const accountId = 123456; - var accounttax = ShoppingContent.Accounttax.get(merchantId, accountId); - Logger.log(accounttax); + try { + const accounttax = ShoppingContent.Accounttax.get(merchantId, accountId); + console.log(accounttax); - var taxInfo = { - accountId: accountId, - rules: [ - { - 'useGlobalRate': true, - 'locationId': 21135, - 'shippingTaxed': true, - 'country': 'US' - }, - { - 'ratePercent': 3, - 'locationId': 21136, - 'country': 'US' - }, - { - 'ratePercent': 2, - 'locationId': 21160, - 'shippingTaxed': true, - 'country': 'US' - } - ] - }; + const taxInfo = { + accountId: accountId, + rules: [ + { + 'useGlobalRate': true, + 'locationId': 21135, + 'shippingTaxed': true, + 'country': 'US' + }, + { + 'ratePercent': 3, + 'locationId': 21136, + 'country': 'US' + }, + { + 'ratePercent': 2, + 'locationId': 21160, + 'shippingTaxed': true, + 'country': 'US' + } + ] + }; - Logger.log(ShoppingContent.Accounttax.update(taxInfo, merchantId, accountId)); + console.log(ShoppingContent.Accounttax + .update(taxInfo, merchantId, accountId)); + } catch (e) { + // TODO (Developer) - Handle exceptions + console.log('Failed with error: $s', e.error); + } } // [END apps_script_shopping_account_info] diff --git a/advanced/slides.gs b/advanced/slides.gs index 40a9b2227..d8532df0b 100644 --- a/advanced/slides.gs +++ b/advanced/slides.gs @@ -13,18 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var presentationId = '1K0Wd9BWgfy1doB0da_vRYmaYTUavEYVKJV_EygSiiMY'; -var pageId = 'p'; -var shapeId = 'MyTextBoxId'; // [START apps_script_slides_create_presentation] /** * Create a new presentation. + * @return {string} presentation Id. + * @see https://developers.google.com/slides/api/reference/rest/v1/presentations/create */ function createPresentation() { - var presentation = + try { + const presentation = Slides.Presentations.create({'title': 'MyNewPresentation'}); - Logger.log('Created presentation with ID: ' + presentation.presentationId); + console.log('Created presentation with ID: ' + presentation.presentationId); + return presentation.presentationId; + } catch (e) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', e.message); + } } // [END apps_script_slides_create_presentation] @@ -32,12 +37,14 @@ function createPresentation() { /** * Create a new slide. * @param {string} presentationId The presentation to add the slide to. + * @return {Object} slide + * @see https://developers.google.com/slides/api/reference/rest/v1/presentations/batchUpdate */ function createSlide(presentationId) { // You can specify the ID to use for the slide, as long as it's unique. - var pageId = Utilities.getUuid(); + const pageId = Utilities.getUuid(); - var requests = [{ + const requests = [{ 'createSlide': { 'objectId': pageId, 'insertionIndex': 1, @@ -46,9 +53,15 @@ function createSlide(presentationId) { } } }]; - var slide = + try { + const slide = Slides.Presentations.batchUpdate({'requests': requests}, presentationId); - Logger.log('Created Slide with ID: ' + slide.replies[0].createSlide.objectId); + console.log('Created Slide with ID: ' + slide.replies[0].createSlide.objectId); + return slide; + } catch (e) { + // TODO (developer) - Handle Exception + console.log('Failed with error %s', e.message); + } } // [END apps_script_slides_create_slide] @@ -57,13 +70,21 @@ function createSlide(presentationId) { * Read page element IDs. * @param {string} presentationId The presentation to read from. * @param {string} pageId The page to read from. + * @return {Object} response + * @see https://developers.google.com/slides/api/reference/rest/v1/presentations.pages/get */ function readPageElementIds(presentationId, pageId) { // You can use a field mask to limit the data the API retrieves // in a get request, or what fields are updated in an batchUpdate. - var response = Slides.Presentations.Pages.get( - presentationId, pageId, {'fields': 'pageElements.objectId'}); - Logger.log(response); + try { + const response = Slides.Presentations.Pages.get( + presentationId, pageId, {'fields': 'pageElements.objectId'}); + console.log(response); + return response; + } catch (e) { + // TODO (developer) - Handle Exception + console.log('Failed with error %s', e.message); + } } // [END apps_script_slides_read_page] @@ -72,13 +93,15 @@ function readPageElementIds(presentationId, pageId) { * Add a new text box with text to a page. * @param {string} presentationId The presentation ID. * @param {string} pageId The page ID. + * @return {Object} response + * @see https://developers.google.com/slides/api/reference/rest/v1/presentations/batchUpdate */ function addTextBox(presentationId, pageId) { // You can specify the ID to use for elements you create, // as long as the ID is unique. - var pageElementId = Utilities.getUuid(); + const pageElementId = Utilities.getUuid(); - var requests = [{ + const requests = [{ 'createShape': { 'objectId': pageElementId, 'shapeType': 'TEXT_BOX', @@ -110,10 +133,16 @@ function addTextBox(presentationId, pageId) { 'insertionIndex': 0 } }]; - var response = + try { + const response = Slides.Presentations.batchUpdate({'requests': requests}, presentationId); - Logger.log('Created Textbox with ID: ' + + console.log('Created Textbox with ID: ' + response.replies[0].createShape.objectId); + return response; + } catch (e) { + // TODO (developer) - Handle Exception + console.log('Failed with error %s', e.message); + } } // [END apps_script_slides_add_text_box] @@ -122,9 +151,11 @@ function addTextBox(presentationId, pageId) { * Format the text in a shape. * @param {string} presentationId The presentation ID. * @param {string} shapeId The shape ID. + * @return {Object} replies + * @see https://developers.google.com/slides/api/reference/rest/v1/presentations/batchUpdate */ function formatShapeText(presentationId, shapeId) { - var requests = [{ + const requests = [{ 'updateTextStyle': { 'objectId': shapeId, 'fields': 'foregroundColor,bold,italic,fontFamily,fontSize,underline', @@ -148,25 +179,40 @@ function formatShapeText(presentationId, shapeId) { } } }]; - var response = + try { + const response = Slides.Presentations.batchUpdate({'requests': requests}, presentationId); + return response.replies; + } catch (e) { + // TODO (developer) - Handle Exception + console.log('Failed with error %s', e.message); + } } // [END apps_script_slides_format_shape_text] -// [START apps_script_slides_thumbnail] +// [START apps_script_slides_save_thumbnail] /** * Saves a thumbnail image of the current Google Slide presentation in Google Drive. * Logs the image URL. * @param {number} i The zero-based slide index. 0 is the first slide. * @example saveThumbnailImage(0) + * @see https://developers.google.com/slides/api/reference/rest/v1/presentations.pages/getThumbnail */ function saveThumbnailImage(i) { - var presentation = SlidesApp.getActivePresentation(); - var thumbnail = Slides.Presentations.Pages.getThumbnail( - presentation.getId(), presentation.getSlides()[i].getObjectId()); - var response = UrlFetchApp.fetch(thumbnail.contentUrl); - var image = response.getBlob(); - var file = DriveApp.createFile(image); - Logger.log(file.getUrl()); + try { + const presentation = SlidesApp.getActivePresentation(); + // Get the thumbnail of specified page + const thumbnail = Slides.Presentations.Pages.getThumbnail( + presentation.getId(), presentation.getSlides()[i].getObjectId()); + // fetch the URL to the thumbnail image. + const response = UrlFetchApp.fetch(thumbnail.contentUrl); + const image = response.getBlob(); + // Creates a file in the root of the user's Drive from a given Blob of arbitrary data. + const file = DriveApp.createFile(image); + console.log(file.getUrl()); + } catch (e) { + // TODO (developer) - Handle Exception + console.log('Failed with error %s', e.message); + } } -// [END apps_script_slides_thumbnail] +// [END apps_script_slides_save_thumbnail] diff --git a/advanced/tagManager.gs b/advanced/tagManager.gs index 4902a183c..3d15f8d8e 100644 --- a/advanced/tagManager.gs +++ b/advanced/tagManager.gs @@ -15,58 +15,64 @@ */ // [START apps_script_tag_manager_create_version] /** - * Creates a container version for a particular account with the input accountPath. + * Creates a container version for a particular account + * with the input accountPath. * @param {string} accountPath The account path. * @return {string} The tag manager container version. */ function createContainerVersion(accountPath) { - var date = new Date(); + const date = new Date(); // Creates a container in the account, using the current timestamp to make // sure the container is unique. - var container = TagManager.Accounts.Containers.create( - { - 'name': 'appscript tagmanager container ' + date.getTime(), - 'usageContext': ['WEB'] - }, - accountPath); - var containerPath = container.path; - // Creates a workspace in the container to track entity changes. - var workspace = TagManager.Accounts.Containers.Workspaces.create( - {'name': 'appscript workspace', 'description': 'appscript workspace'}, - containerPath); - var workspacePath = workspace.path; - // Creates a random value variable. - var variable = TagManager.Accounts.Containers.Workspaces.Variables.create( - {'name': 'apps script variable', 'type': 'r'}, - workspacePath); - // Creates a trigger that fires on any page view. - var trigger = TagManager.Accounts.Containers.Workspaces.Triggers.create( - {'name': 'apps script trigger', 'type': 'PAGEVIEW'}, - workspacePath); - // Creates a arbitary pixel that fires the tag on all page views. - var tag = TagManager.Accounts.Containers.Workspaces.Tags.create( - { - 'name': 'apps script tag', - 'type': 'img', - 'liveOnly': false, - 'parameter': [ - {'type': 'boolean', 'key': 'useCacheBuster', 'value': 'true'}, { - 'type': 'template', - 'key': 'cacheBusterQueryParam', - 'value': 'gtmcb' - }, - {'type': 'template', 'key': 'url', 'value': '//example.com'} - ], - 'firingTriggerId': [trigger.triggerId] - }, - workspacePath); - // Creates a container version with the variabe, trigger, and tag. - var version = TagManager.Accounts.Containers.Workspaces - .create_version( - {'name': 'apps script version'}, workspacePath) - .containerVersion; - Logger.log(version); - return version; + try { + const container = TagManager.Accounts.Containers.create( + { + 'name': 'appscript tagmanager container ' + date.getTime(), + 'usageContext': ['WEB'] + }, + accountPath); + const containerPath = container.path; + // Creates a workspace in the container to track entity changes. + const workspace = TagManager.Accounts.Containers.Workspaces.create( + {'name': 'appscript workspace', 'description': 'appscript workspace'}, + containerPath); + const workspacePath = workspace.path; + // Creates a random value variable. + const variable = TagManager.Accounts.Containers.Workspaces.Variables.create( + {'name': 'apps script variable', 'type': 'r'}, + workspacePath); + // Creates a trigger that fires on any page view. + const trigger = TagManager.Accounts.Containers.Workspaces.Triggers.create( + {'name': 'apps script trigger', 'type': 'PAGEVIEW'}, + workspacePath); + // Creates a arbitary pixel that fires the tag on all page views. + const tag = TagManager.Accounts.Containers.Workspaces.Tags.create( + { + 'name': 'apps script tag', + 'type': 'img', + 'liveOnly': false, + 'parameter': [ + {'type': 'boolean', 'key': 'useCacheBuster', 'value': 'true'}, { + 'type': 'template', + 'key': 'cacheBusterQueryParam', + 'value': 'gtmcb' + }, + {'type': 'template', 'key': 'url', 'value': '//example.com'} + ], + 'firingTriggerId': [trigger.triggerId] + }, + workspacePath); + // Creates a container version with the variabe, trigger, and tag. + const version = TagManager.Accounts.Containers.Workspaces + .create_version( + {'name': 'apps script version'}, workspacePath) + .containerVersion; + console.log(version); + return version; + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } // [END apps_script_tag_manager_create_version] @@ -77,7 +83,7 @@ function createContainerVersion(accountPath) { * @return {string} The container path. */ function grabContainerPath(versionPath) { - var pathParts = versionPath.split('/'); + const pathParts = versionPath.split('/'); return pathParts.slice(0, 4).join('/'); } @@ -87,17 +93,22 @@ function grabContainerPath(versionPath) { * @param {object} version The container version. */ function publishVersionAndQuickPreviewDraft(version) { - var containerPath = grabContainerPath(version.path); - // Publish the input container version. - TagManager.Accounts.Containers.Versions.publish(version.path); - var workspace = TagManager.Accounts.Containers.Workspaces.create( - {'name': 'appscript workspace', 'description': 'appscript workspace'}, - containerPath); - var workspaceId = workspace.path; - // Quick previews the current container draft. - var quickPreview = TagManager.Accounts.Containers.Workspaces.quick_preview( - workspace.path); - Logger.log(quickPreview); + try { + const containerPath = grabContainerPath(version.path); + // Publish the input container version. + TagManager.Accounts.Containers.Versions.publish(version.path); + const workspace = TagManager.Accounts.Containers.Workspaces.create( + {'name': 'appscript workspace', 'description': 'appscript workspace'}, + containerPath); + const workspaceId = workspace.path; + // Quick previews the current container draft. + const quickPreview = TagManager.Accounts.Containers.Workspaces + .quick_preview(workspace.path); + console.log(quickPreview); + } catch (e) { + // TODO (Developer) - Handle exceptions + console.log('Failed with error: $s', e.error); + } } // [END apps_script_tag_manager_publish_version] @@ -108,7 +119,7 @@ function publishVersionAndQuickPreviewDraft(version) { * @return {string} The container path. */ function grabContainerPath(versionPath) { - var pathParts = versionPath.split('/'); + const pathParts = versionPath.split('/'); return pathParts.slice(0, 4).join('/'); } @@ -118,20 +129,26 @@ function grabContainerPath(versionPath) { * @param {object} version The container version object. */ function createAndReauthorizeUserEnvironment(version) { - // Creates a container version. - var containerPath = grabContainerPath(version.path); - // Creates a user environment that points to a container version. - var environment = TagManager.Accounts.Containers.Environments.create( - { - 'name': 'test_environment', - 'type': 'user', - 'containerVersionId': version.containerVersionId - }, - containerPath); - Logger.log('Original user environment: ' + environment); - // Reauthorizes the user environment that points to a container version. - TagManager.Accounts.Containers.Environments.reauthorize({}, environment.path); - Logger.log('Reauthorized user environment: ' + environment); + try { + // Creates a container version. + const containerPath = grabContainerPath(version.path); + // Creates a user environment that points to a container version. + const environment = TagManager.Accounts.Containers.Environments.create( + { + 'name': 'test_environment', + 'type': 'user', + 'containerVersionId': version.containerVersionId + }, + containerPath); + console.log('Original user environment: ' + environment); + // Reauthorizes the user environment that points to a container version. + TagManager.Accounts.Containers.Environments.reauthorize( + {}, environment.path); + console.log('Reauthorized user environment: ' + environment); + } catch (e) { + // TODO (Developer) - Handle exceptions + console.log('Failed with error: $s', e.error); + } } // [END apps_script_tag_manager_create_user_environment] @@ -141,20 +158,25 @@ function createAndReauthorizeUserEnvironment(version) { * @param {string} accountPath The account path. */ function logAllAccountUserPermissionsWithContainerAccess(accountPath) { - var userPermissions = + try { + const userPermissions = TagManager.Accounts.User_permissions.list(accountPath).userPermission; - for (var i = 0; i < userPermissions.length; i++) { - var userPermission = userPermissions[i]; - if ('emailAddress' in userPermission) { - var containerAccesses = userPermission.containerAccess; - for (var j = 0; j < containerAccesses.length; j++) { - var containerAccess = containerAccesses[j]; - Logger.log( - 'emailAddress:' + userPermission.emailAddress + ' containerId:' + - containerAccess.containerId + ' containerAccess:' + - containerAccess.permission); + for (let i = 0; i < userPermissions.length; i++) { + const userPermission = userPermissions[i]; + if ('emailAddress' in userPermission) { + const containerAccesses = userPermission.containerAccess; + for (let j = 0; j < containerAccesses.length; j++) { + const containerAccess = containerAccesses[j]; + console.log( + 'emailAddress:' + userPermission.emailAddress + + ' containerId:' + containerAccess.containerId + + ' containerAccess:' + containerAccess.permission); + } } } + } catch (e) { + // TODO (Developer) - Handle exceptions + console.log('Failed with error: $s', e.error); } } // [END apps_script_tag_manager_log] diff --git a/advanced/tasks.gs b/advanced/tasks.gs index 29e36dffb..af1e3bd9c 100644 --- a/advanced/tasks.gs +++ b/advanced/tasks.gs @@ -13,54 +13,79 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// [START apps_script_tasks_lists_task_lists] +// [START tasks_lists_task_lists] /** - * Lists tasks titles and IDs. + * Lists the titles and IDs of tasksList. + * @see https://developers.google.com/tasks/reference/rest/v1/tasklists/list */ function listTaskLists() { - var taskLists = Tasks.Tasklists.list(); - if (taskLists.items) { - for (var i = 0; i < taskLists.items.length; i++) { - var taskList = taskLists.items[i]; - Logger.log('Task list with title "%s" and ID "%s" was found.', - taskList.title, taskList.id); + try { + // Returns all the authenticated user's task lists. + const taskLists = Tasks.Tasklists.list(); + // If taskLists are available then print all tasklists. + if (!taskLists.items) { + console.log('No task lists found.'); + return; } - } else { - Logger.log('No task lists found.'); + // Print the tasklist title and tasklist id. + for (let i = 0; i < taskLists.items.length; i++) { + const taskList = taskLists.items[i]; + console.log('Task list with title "%s" and ID "%s" was found.', taskList.title, taskList.id); + } + } catch (err) { + // TODO (developer) - Handle exception from Task API + console.log('Failed with an error %s ', err.message); } } -// [END apps_script_tasks_lists_task_lists] +// [END tasks_lists_task_lists] -// [START apps_script_tasks_list_tasks] +// [START tasks_list_tasks] /** * Lists task items for a provided tasklist ID. * @param {string} taskListId The tasklist ID. + * @see https://developers.google.com/tasks/reference/rest/v1/tasks/list */ function listTasks(taskListId) { - var tasks = Tasks.Tasks.list(taskListId); - if (tasks.items) { - for (var i = 0; i < tasks.items.length; i++) { - var task = tasks.items[i]; - Logger.log('Task with title "%s" and ID "%s" was found.', - task.title, task.id); + try { + // List the task items of specified tasklist using taskList id. + const tasks = Tasks.Tasks.list(taskListId); + // If tasks are available then print all task of given tasklists. + if (!tasks.items) { + console.log('No tasks found.'); + return; + } + // Print the task title and task id of specified tasklist. + for (let i = 0; i < tasks.items.length; i++) { + const task = tasks.items[i]; + console.log('Task with title "%s" and ID "%s" was found.', task.title, task.id); } - } else { - Logger.log('No tasks found.'); + } catch (err) { + // TODO (developer) - Handle exception from Task API + console.log('Failed with an error %s', err.message); } } -// [END apps_script_tasks_list_tasks] +// [END tasks_list_tasks] -// [START apps_script_tasks_add_task] +// [START tasks_add_task] /** * Adds a task to a tasklist. * @param {string} taskListId The tasklist to add to. + * @see https://developers.google.com/tasks/reference/rest/v1/tasks/insert */ function addTask(taskListId) { - var task = { + // Task details with title and notes for inserting new task + let task = { title: 'Pick up dry cleaning', notes: 'Remember to get this done!' }; - task = Tasks.Tasks.insert(task, taskListId); - Logger.log('Task with ID "%s" was created.', task.id); + try { + // Call insert method with taskDetails and taskListId to insert Task to specified tasklist. + task = Tasks.Tasks.insert(task, taskListId); + // Print the Task ID of created task. + console.log('Task with ID "%s" was created.', task.id); + } catch (err) { + // TODO (developer) - Handle exception from Tasks.insert() of Task API + console.log('Failed with an error %s', err.message); + } } -// [END apps_script_tasks_add_task] +// [END tasks_add_task] diff --git a/advanced/test_adminSDK.gs b/advanced/test_adminSDK.gs new file mode 100644 index 000000000..fb825fe04 --- /dev/null +++ b/advanced/test_adminSDK.gs @@ -0,0 +1,147 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests listAllUsers function of adminSDK.gs + */ +function itShouldListAllUsers() { + console.log('> itShouldListAllUsers'); + listAllUsers(); +} + +/** + * Tests getUser function of adminSDK.gs + */ +function itShouldGetUser() { + console.log('> itShouldGetUser'); + getUser(); +} + +/** + * Tests addUser function of adminSDK.gs + */ +function itShouldAddUser() { + console.log('> itShouldAddUser'); + addUser(); +} + +/** + * Tests createAlias function of adminSDK.gs + */ +function itShouldCreateAlias() { + console.log('> itShouldCreateAlias'); + createAlias(); +} + +/** + * Tests listAllGroups function of adminSDK.gs + */ +function itShouldListAllGroups() { + console.log('> itShouldListAllGroups'); + listAllGroups(); +} + +/** + * Tests addGroupMember function of adminSDK.gs + */ +function itShouldAddGroupMember() { + console.log('> itShouldAddGroupMember'); + addGroupMember(); +} + +/** + * Tests migrateMessages function of adminSDK.gs + */ +function itShouldMigrateMessages() { + console.log('> itShouldMigrateMessages'); + migrateMessages(); +} + +/** + * Tests getGroupSettings function of adminSDK.gs + */ +function itShouldGetGroupSettings() { + console.log('> itShouldGetGroupSettings'); + getGroupSettings(); +} + +/** + * Tests updateGroupSettings function of adminSDK.gs + */ +function itShouldUpdateGroupSettings() { + console.log('> itShouldUpdateGroupSettings'); + updateGroupSettings(); +} + +/** + * Tests getLicenseAssignments function of adminSDK.gs + */ +function itShouldGetLicenseAssignments() { + console.log('> itShouldGetLicenseAssignments'); + getLicenseAssignments(); +} + +/** + * Tests insertLicenseAssignment function of adminSDK.gs + */ +function itShouldInsertLicenseAssignment() { + console.log('> itShouldInsertLicenseAssignment'); + insertLicenseAssignment(); +} + +/** + * Tests generateLoginActivityReport function of adminSDK.gs + */ +function itShouldGenerateLoginActivityReport() { + console.log('> itShouldGenerateLoginActivityReport'); + generateLoginActivityReport(); +} + +/** + * Tests generateUserUsageReport function of adminSDK.gs + */ +function itShouldGenerateUserUsageReport() { + console.log('> itShouldGenerateUserUsageReport'); + generateUserUsageReport(); +} + +/** + * Tests getSubscriptions function of adminSDK.gs + */ +function itShouldGetSubscriptions() { + console.log('> itShouldGetSubscriptions'); + getSubscriptions(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldListAllUsers(); + itShouldGetUser(); + itShouldAddUser(); + itShouldCreateAlias(); + itShouldListAllGroups(); + itShouldAddGroupMember(); + itShouldMigrateMessages(); + itShouldGetGroupSettings(); + itShouldUpdateGroupSettings(); + itShouldGetLicenseAssignments(); + itShouldInsertLicenseAssignment(); + itShouldGenerateLoginActivityReport(); + itShouldGenerateUserUsageReport(); + itShouldGetSubscriptions(); +} diff --git a/advanced/test_adsense.gs b/advanced/test_adsense.gs new file mode 100644 index 000000000..1b7ef1e24 --- /dev/null +++ b/advanced/test_adsense.gs @@ -0,0 +1,52 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Replace with correct values +const accountName = 'account name'; +const clientName = 'ad client name'; + +/** + * Tests listAccounts function of adsense.gs + */ +function itShouldListAccounts() { + console.log('> itShouldListAccounts'); + listAccounts(); +} + +/** + * Tests listAdClients function of adsense.gs + */ +function itShouldListAdClients() { + console.log('> itShouldListAdClients'); + listAdClients(accountName); +} + +/** + * Tests listAdUnits function of adsense.gs + */ +function itShouldListAdUnits() { + console.log('> itShouldListAdUnits'); + listAdUnits(clientName); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldListAccounts(); + itShouldListAdClients(); + itShouldListAdUnits(); +} diff --git a/advanced/test_analytics.gs b/advanced/test_analytics.gs new file mode 100644 index 000000000..505974c2b --- /dev/null +++ b/advanced/test_analytics.gs @@ -0,0 +1,42 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Replace with the required profileId +const profileId = 'abcd'; + +/** + * Tests listAccounts function of analytics.gs + */ +function itShouldListAccounts() { + console.log('> itShouldListAccounts'); + listAccounts(); +} + +/** + * Tests runReport function of analytics.gs + */ +function itShouldRunReport() { + console.log('> itShouldRunReport'); + runReport(profileId); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldListAccounts(); + itShouldRunReport(); +} diff --git a/advanced/test_bigquery.gs b/advanced/test_bigquery.gs new file mode 100644 index 000000000..d79373b2f --- /dev/null +++ b/advanced/test_bigquery.gs @@ -0,0 +1,39 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests runQuery function of adminSDK.gs + */ +function itShouldRunQuery() { + console.log('> itShouldRunQuery'); + runQuery(); +} + +/** + * Tests loadCsv function of adminSDK.gs + */ +function itShouldLoadCsv() { + console.log('> itShouldLoadCsv'); + loadCsv(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldRunQuery(); + itShouldLoadCsv(); +} diff --git a/advanced/test_calendar.gs b/advanced/test_calendar.gs new file mode 100644 index 000000000..ceec9a2a4 --- /dev/null +++ b/advanced/test_calendar.gs @@ -0,0 +1,88 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests listCalendars function of calendar.gs + */ +function itShouldListCalendars() { + console.log('> itShouldListCalendars'); + listCalendars(); +} + +/** + * Tests createEvent function of calendars.gs + */ +function itShouldCreateEvent() { + console.log('> itShouldCreateEvent'); + createEvent(); +} + +/** + * Tests gerRelativeDate function of calendar.gs + */ +function itShouldGetRelativeDate() { + console.log('> itShouldGetRelativeDate'); + console.log('no offset: ' + getRelativeDate(0, 0)); + console.log('4 hour offset: ' + getRelativeDate(0, 4)); + console.log('1 day offset: ' + getRelativeDate(1, 0)); + console.log('1 day and 3 hour off set: ' + getRelativeDate(1, 3)); +} + +/** + * Tests listNext10Events function of calendar.gs + */ +function itShouldListNext10Events() { + console.log('> itShouldListNext10Events'); + listNext10Events(); +} + +/** + * Tests logSyncedEvents function of calendar.gs + */ +function itShouldLogSyncedEvents() { + console.log('> itShouldLogSyncedEvents'); + logSyncedEvents('primary', true); + logSyncedEvents('primary', false); +} + +/** + * Tests conditionalUpdate function of calendar.gs + */ +function itShouldConditionalUpdate() { + console.log('> itShouldConditionalUpdate (takes 30 seconds)'); + conditionalUpdate(); +} + +/** + * Tests conditionalFetch function of calendar.gs + */ +function itShouldConditionalFetch() { + console.log('> itShouldConditionalFetch'); + conditionalFetch(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldListCalendars(); + itShouldCreateEvent(); + itShouldGetRelativeDate(); + itShouldListNext10Events(); + itShouldLogSyncedEvents(); + itShouldConditionalUpdate(); + itShouldConditionalFetch(); +} diff --git a/advanced/test_classroom.gs b/advanced/test_classroom.gs new file mode 100644 index 000000000..bbe2d7625 --- /dev/null +++ b/advanced/test_classroom.gs @@ -0,0 +1,30 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests listCourses function of classroom.gs + */ +function itShouldListCourses() { + console.log('> itShouldListCourses'); + listCourses(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldListCourses(); +} diff --git a/advanced/test_docs.gs b/advanced/test_docs.gs new file mode 100644 index 000000000..97bbd35ce --- /dev/null +++ b/advanced/test_docs.gs @@ -0,0 +1,106 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO (developer) - Replace with your documentId +const documentId='1EaLpBfuo3bMUeP6_P34auuQroh3bCWi6hLDppY6J6us'; +/** + * A simple exists assertion check. Expects a value to exist. Errors if DNE. + * @param {any} value A value that is expected to exist. + */ +function expectToExist(value) { + if (!value) { + console.log('DNE'); + return; + } + console.log('TEST: Exists'); +} + +/** + * A simple exists assertion check for primatives (no nested objects). + * Expects actual to equal expected. Logs the output. + * @param {any} expected The actual value. + * @param {any} actual The expected value. + */ +function expectToEqual(expected, actual) { + if (actual !== expected) { + console.log('TEST: actual: %s = expected: %s', actual, expected); + return; + } + console.log('TEST: actual: %s = expected: %s', actual, expected); +} + + +/** + * Runs all tests. + */ +function RUN_ALL_TESTS() { + itShouldCreateDocument(); + itShouldInsertTextWithStyle(); + itShouldReplaceText(); + itShouldReadFirstParagraph(); +} + +/** + * Creates a presentation. + */ +function itShouldCreateDocument() { + const documentId = createDocument(); + expectToExist(documentId); + deleteFileOnCleanup(documentId); +} + + +/** + * Insert text with style. + */ +function itShouldInsertTextWithStyle() { + const documentId = createDocument(); + expectToExist(documentId); + const text='This is the sample document'; + const replies=insertAndStyleText(documentId, text); + expectToEqual(2, replies.length); + deleteFileOnCleanup(documentId); +} + +/** + * Find and Replace the text. + */ +function itShouldReplaceText() { + const documentId = createDocument(); + expectToExist(documentId); + const text='This is the sample document'; + const response=insertAndStyleText(documentId, text); + expectToEqual(2, response.replies.length); + const findTextToReplacementMap={'sample': 'test', 'document': 'Doc'}; + const replies=findAndReplace(documentId, findTextToReplacementMap); + expectToEqual(2, replies.length); + deleteFileOnCleanup(documentId); +} + +/** + * Read first paragraph + */ +function itShouldReadFirstParagraph() { + const paragraphText=readFirstParagraph(documentId); + expectToExist(paragraphText); + expectToEqual(89, paragraphText.length); +} +/** + * Delete the file + * @param {string} id Document ID + */ +function deleteFileOnCleanup(id) { + Drive.Files.remove(id); +} diff --git a/advanced/test_doubleclick.gs b/advanced/test_doubleclick.gs new file mode 100644 index 000000000..c0efee4b8 --- /dev/null +++ b/advanced/test_doubleclick.gs @@ -0,0 +1,48 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests listUserProfiles function of doubleclick.gs + */ +function itShouldListUserProfiles() { + console.log('> itShouldListUserProfiles'); + listUserProfiles(); +} + +/** + * Tests listActiveCampaigns function of doubleclick.gs + */ +function itShouldListActiveCampaigns() { + console.log('> itShouldListActiveCampaigns'); + listActiveCampaigns(); +} + +/** + * Tests createAdvertiserAndCampaign function of doubleclick.gs + */ +function itShouldCreateAdvertiserAndCampaign() { + console.log('> itShouldCreateAdvertiserAndCampaign'); + createAdvertiserAndCampaign(); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldListUserProfiles(); + itShouldListActiveCampaigns(); + itShouldCreateAdvertiserAndCampaign(); +} diff --git a/advanced/test_drive.gs b/advanced/test_drive.gs new file mode 100644 index 000000000..f3ebd5eeb --- /dev/null +++ b/advanced/test_drive.gs @@ -0,0 +1,121 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helper functions to help test drive.gs expectToExist(...) + * @param {string} value + * To test drive.gs please add drive services + */ +function expectToExist(value) { + if (value) { + console.log('TEST: Exists'); + } else { + throw new Error('TEST: DNE'); + } +} + +/** + * Helper functions to help test drive.gs expectToEqual + * @param {string} actual + * @param {string} expected + * To test drive.gs please add drive services + */ +function expectToEqual(actual, expected) { + console.log('TEST: actual: %s = expected: %s', actual, expected); + if (actual !== expected) { + console.log('TEST: actual: %s expected: %s', actual, expected); + } +} + +/** + * Helper functions to help test drive.gs createFolder() + * + * To test drive.gs please add drive services + */ +function createTestFolder() { + DriveApp.createFolder('test1'); + DriveApp.createFolder('test2'); +} + +/** + * Helper functions to help test drive.gs getFilesByName(...) + * + * To test drive.gs please add drive services + */ +function fileCleanUp() { + DriveApp.getFilesByName('google_logo.png').next().setTrashed(true); +} + +/** + * Helper functions folderCleanUp() + * + * To test getFoldersByName() please add drive services + */ +function folderCleanUp() { + DriveApp.getFoldersByName('test1').next().setTrashed(true); + DriveApp.getFoldersByName('test2').next().setTrashed(true); +} + +/** + * drive.gs test functions below + */ + +/** + * tests drive.gs uploadFile + * @return {string} fileId The ID of the file + */ +function checkUploadFile() { + uploadFile(); + const fileId = DriveApp.getFilesByName('google_logo.png').next().getId(); + expectToExist(fileId); + return fileId; +} + +/** + * tests drive.gs listRootFolders + */ +function checkListRootFolders() { + createTestFolder(); + + const folders = DriveApp.getFolders(); + while (folders.hasNext()) { + const folder = folders.next(); + console.log(folder.getName() + ' ' + folder.getId()); + } + listRootFolders(); + folderCleanUp(); +} + +/** + * tests drive.gs addCustomProperty + * @param {string} fileId The ID of the file + */ +function checkAddCustomProperty(fileId) { + addCustomProperty(fileId); + expectToEqual(Drive.Properties.get(fileId, 'department', + {visibility: 'PUBLIC'}).value, 'Sales'); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + const fileId = checkUploadFile(); + checkListRootFolders(); + checkAddCustomProperty(fileId); + listRevisions(fileId); + fileCleanUp(); +} diff --git a/advanced/test_gmail.gs b/advanced/test_gmail.gs new file mode 100644 index 000000000..494871f66 --- /dev/null +++ b/advanced/test_gmail.gs @@ -0,0 +1,30 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Run All functions of gmail.gs + * Add gmail services to run + */ +function RUN_ALL_TESTS() { + console.log('> ltShouldListLabelInfo'); + listLabelInfo(); + console.log('> ltShouldListInboxSnippets'); + listInboxSnippets(); + console.log('> ltShouldLogRecentHistory'); + logRecentHistory(); + console.log('> ltShouldGetRawMessage'); + getRawMessage(); +} diff --git a/advanced/test_people.gs b/advanced/test_people.gs new file mode 100644 index 000000000..07e1e09e0 --- /dev/null +++ b/advanced/test_people.gs @@ -0,0 +1,29 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helper functions for sheets.gs testing + * + * to tests people.gs add people api services + */ +function RUN_ALL_TESTS() { + console.log('> itShouldGetConnections'); + getConnections(); + console.log('> itShouldGetSelf'); // Requires the scope userinfo.profile + getSelf(); + console.log('> itShouldGetAccount'); + getAccount('me'); +} diff --git a/advanced/test_sheets.gs b/advanced/test_sheets.gs new file mode 100644 index 000000000..345214c06 --- /dev/null +++ b/advanced/test_sheets.gs @@ -0,0 +1,89 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helper functions for sheets.gs testing + * to tests sheets.gs add sheets services + * + * create test spreadsheets + * @return {string} spreadsheet + */ +function createTestSpreadsheet() { + const spreadsheet = SpreadsheetApp.create('Test Spreadsheet'); + for (let i = 0; i < 3; ++i) { + spreadsheet.appendRow([1, 2, 3]); + } + return spreadsheet.getId(); +} + +/** + * populate the created spreadsheet with values + * @param {string} spreadsheetId + */ +function populateValues(spreadsheetId) { + const batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest(); + const repeatCellRequest = Sheets.newRepeatCellRequest(); + + const values = []; + for (let i = 0; i < 10; ++i) { + values[i] = []; + for (let j = 0; j < 10; ++j) { + values[i].push('Hello'); + } + } + const range = 'A1:J10'; + SpreadsheetApp.openById(spreadsheetId).getRange(range).setValues(values); + SpreadsheetApp.flush(); +} + +/** + * Functions to test sheets.gs below this line + * tests readRange function of sheets.gs + * @return {string} spreadsheet ID + */ +function itShouldReadRange() { + console.log('> itShouldReadRange'); + spreadsheetId = createTestSpreadsheet(); + populateValues(spreadsheetId); + readRange(spreadsheetId); + return spreadsheetId; +} + +/** + * tests the addPivotTable function of sheets.gs + * @param {string} spreadsheetId + */ +function itShouldAddPivotTable(spreadsheetId) { + console.log('> itShouldAddPivotTable'); + const spreadsheet = SpreadsheetApp.openById(spreadsheetId); + const sheets = spreadsheet.getSheets(); + sheetId = sheets[0].getSheetId(); + addPivotTable(spreadsheetId, sheetId, sheetId); + SpreadsheetApp.flush(); + console.log('Created pivot table'); +} + +/** + * runs all the tests + */ +function RUN_ALL_TEST() { + const spreadsheetId = itShouldReadRange(); + console.log('> itShouldWriteToMultipleRanges'); + writeToMultipleRanges(spreadsheetId); + console.log('> itShouldAddSheet'); + addSheet(spreadsheetId); + itShouldAddPivotTable(spreadsheetId); +} diff --git a/advanced/test_shoppingContent.gs b/advanced/test_shoppingContent.gs new file mode 100644 index 000000000..14cc296c2 --- /dev/null +++ b/advanced/test_shoppingContent.gs @@ -0,0 +1,62 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Before running these tests replace the product resource variables +const productResource1 = {}; +const productResource2 = {}; +const productResource3 = {}; + +/** + * Tests productInsert function of shoppingContent.gs + */ +function itShouldProductInsert() { + console.log('> itShouldPproductInsert'); + productInsert(); +} + +/** + * Tests productList function of shoppingContent.gs + */ +function itShouldProductList() { + console.log('> itShouldProductList'); + productList(); +} + +/** + * Tests custombatch function of shoppingContent.gs + */ +function itShouldCustombatch() { + console.log('> itShouldCustombatch'); + custombatch(productResource1, productResource2, productResource3); +} + +/** + * Tests updateAccountTax function of shoppingContent.gs + */ +function itShouldUpdateAccountTax() { + console.log('> itShouldUpdateAccountTax'); + updateAccountTax(); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldProductInsert(); + itShouldProductList(); + itShouldCustombatch(); + itShouldUpdateAccountTax(); +} diff --git a/advanced/test_slides.gs b/advanced/test_slides.gs new file mode 100644 index 000000000..156221503 --- /dev/null +++ b/advanced/test_slides.gs @@ -0,0 +1,170 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A simple existance assertion. Logs if the value is falsy. + * @param {object} value The value we expect to exist. + */ +function expectToExist(value) { + if (!value) { + console.log('DNE'); + return; + } + console.log('TEST: Exists'); +} + +/** + * A simple equality assertion. Logs if there is a mismatch. + * @param {object} expected The expected value. + * @param {object} actual The actual value. + */ +function expectToEqual(expected, actual) { + if (actual !== expected) { + console.log('TEST: actual: %s = expected: %s', actual, expected); + return; + } + console.log('TEST: actual: %s = expected: %s', actual, expected); +} +/** + * Creates a presentation. + * @param {string} presentationId The presentation ID. + * @param {string} pageId The page ID. + * @return {string} objectId + */ +function addShape(presentationId, pageId) { + // Create a new square textbox, using the supplied element ID. + const elementId = 'MyTextBox_01'; + const pt350 = { + magnitude: 350, + unit: 'PT' + }; + const requests = [{ + createShape: { + objectId: elementId, + shapeType: 'ELLIPSE', + elementProperties: { + pageObjectId: pageId, + size: { + height: pt350, + width: pt350 + }, + transform: { + scaleX: 1, + scaleY: 1, + translateX: 350, + translateY: 100, + unit: 'PT' + } + } + } + }, + + // Insert text into the box, using the supplied element ID. + { + insertText: { + objectId: elementId, + insertionIndex: 0, + text: 'Text Formatted!' + } + }]; + + // Execute the request. + const createTextboxWithTextResponse = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + const createShapeResponse = createTextboxWithTextResponse.replies[0].createShape; + console.log('Created textbox with ID: %s', createShapeResponse.objectId); + // [END slides_create_textbox_with_text] + return createShapeResponse.objectId; +} + + +/** + * Runs all tests. + */ +function RUN_ALL_TESTS() { + itShouldCreateAPresentation(); + itShouldCreateASlide(); + itShouldCreateATextboxWithText(); + itShouldFormatShapes(); + itShouldReadPage(); +} + +/** + * Creates a presentation. + */ +function itShouldCreateAPresentation() { + const presentationId = createPresentation(); + expectToExist(presentationId); + deleteFileOnCleanup(presentationId); +} + + +/** + * Creates a new slide. + */ +function itShouldCreateASlide() { + console.log('> itShouldCreateASlide'); + const presentationId = createPresentation(); + const slideId=createSlide(presentationId); + expectToExist(slideId); + deleteFileOnCleanup(presentationId); +} + +/** + * Creates a slide with text. + */ +function itShouldCreateATextboxWithText() { + const presentationId = createPresentation(); + const slide=createSlide(presentationId); + const pageId = slide.replies[0].createSlide.objectId; + const response = addTextBox(presentationId, pageId); + expectToEqual(2, response.replies.length); + const boxId = response.replies[0].createShape.objectId; + expectToExist(boxId); + deleteFileOnCleanup(presentationId); +} + +/** + * Test for Read Page. + */ +function itShouldReadPage() { + const presentationId = createPresentation(); + const slide=createSlide(presentationId); + const pageId = slide.replies[0].createSlide.objectId; + const response = readPageElementIds(presentationId, pageId); + expectToEqual(3, response.pageElements.length); + deleteFileOnCleanup(presentationId); +} +/** + * Test for format shapes + */ +function itShouldFormatShapes() { + const presentationId = createPresentation(); + const slide=createSlide(presentationId); + const pageId = slide.replies[0].createSlide.objectId; + const shapeId=addShape(presentationId, pageId); + const replies=formatShapeText(presentationId, shapeId); + expectToExist(replies); + deleteFileOnCleanup(presentationId); +} +/** + * Delete the file + * @param {string} id presentationId + */ +function deleteFileOnCleanup(id) { + Drive.Files.remove(id); +} diff --git a/advanced/test_tagManager.gs b/advanced/test_tagManager.gs new file mode 100644 index 000000000..42053dd27 --- /dev/null +++ b/advanced/test_tagManager.gs @@ -0,0 +1,65 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Before running tagManager tests create a test tagMAnager account +// and replace the value below with its account path +const path = 'accounts/6007387289'; + +/** + * Tests createContainerVersion function of tagManager.gs + * @param {string} accountPath Tag manager account's path + * @return {object} version The container version + */ +function itShouldCreateContainerVersion(accountPath) { + console.log('> itShouldCreateContainerVersion'); + const version = createContainerVersion(accountPath); + return version; +} + +/** + * Tests publishVersionAndQuickPreviewDraft function of tagManager.gs + * @param {object} version tag managers container version + */ +function itShouldPublishVersionAndQuickPreviewDraft(version) { + console.log('> itShouldPublishVersionAndQuickPreviewDraft'); + publishVersionAndQuickPreviewDraft(version); +} + +/** + * Tests createAndReauthorizeUserEnvironment function of tagManager.gs + * @param {object} version tag managers container version + */ +function itShouldCreateAndReauthorizeUserEnvironment(version) { + console.log('> itShouldCreateAndReauthorizeUserEnvironment'); + createAndReauthorizeUserEnvironment(version); +} + +/** + * Tests logAllAccountUserPermissionsWithContainerAccess function of tagManager.gs + * @param {string} accountPath Tag manager account's path + */ +function itShouldLogAllAccountUserPermissionsWithContainerAccess(accountPath) { + console.log('> itShouldLogAllAccountUserPermissionsWithContainerAccess'); + logAllAccountUserPermissionsWithContainerAccess(accountPath); +} +/** + * Runs all tests + */ +function RUN_ALL_TESTS() { + const version = itShouldCreateContainerVersion(path); + itShouldPublishVersionAndQuickPreviewDraft(version); + itShouldCreateAndReauthorizeUserEnvironment(version); + itShouldLogAllAccountUserPermissionsWithContainerAccess(path); +} diff --git a/advanced/test_tasks.gs b/advanced/test_tasks.gs new file mode 100644 index 000000000..b38668fcd --- /dev/null +++ b/advanced/test_tasks.gs @@ -0,0 +1,57 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Test functions for tasks.gs + * + * Add task API services to test + */ + +/** + * tests listTaskLists of tasks.gs + */ +function itShouldListTaskLists() { + console.log('> itShouldListTaskLists'); + listTaskLists(); +} + +/** + * tests listTasks of tasks.gs + */ +function itShouldListTasks() { + console.log('> itShouldListTasks'); + const taskId = Tasks.Tasklists.list().items[0].id; + listTasks(taskId); +} + +/** + * tests addTask of tasks.gs + */ +function itShouldAddTask() { + console.log('> itShouldAddTask'); + const taskId = Tasks.Tasklists.list().items[0].id; + addTask(taskId); +} + +/** + * run all tests + */ +function RUN_ALL_TESTS() { + itShouldListTaskLists(); + itShouldListTasks(); + itShouldAddTask(); + itShouldListTasks(); +} diff --git a/advanced/test_youtube.gs b/advanced/test_youtube.gs new file mode 100644 index 000000000..6afd7454d --- /dev/null +++ b/advanced/test_youtube.gs @@ -0,0 +1,29 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + console.log('> itShouldSearchByKeyword'); + searchByKeyword(); + console.log('> itShouldRetrieveMyUploads'); + retrieveMyUploads(); + console.log('> itShouldAddSubscription'); + addSubscription(); + console.log('> itShouldCreateSlides'); + createSlides(); +} diff --git a/advanced/test_youtubeAnalytics.gs b/advanced/test_youtubeAnalytics.gs new file mode 100644 index 000000000..5211bfb83 --- /dev/null +++ b/advanced/test_youtubeAnalytics.gs @@ -0,0 +1,30 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests createReport function of youtubeAnalytics.gs + */ +function itShouldCreateReport() { + console.log('> itShouldCreateReport'); + createReport(); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldCreateReport(); +} diff --git a/advanced/test_youtubeContentId.gs b/advanced/test_youtubeContentId.gs new file mode 100644 index 000000000..350c88de7 --- /dev/null +++ b/advanced/test_youtubeContentId.gs @@ -0,0 +1,48 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Tests claimYourVideoWithMonetizePolicy function of youtubeContentId.gs + */ +function itShouldClaimVideoWithMonetizePolicy() { + console.log('> itShouldClaimVideoWithMonetizePolicy'); + claimYourVideoWithMonetizePolicy(); +} + +/** + * Tests updateAssetOwnership function of youtubeContentId.gs + */ +function itShouldUpdateAssetOwnership() { + console.log('> itShouldUpdateAssetOwnership'); + updateAssetOwnership(); +} + +/** + * Tests releaseClaim function of youtubeContentId.gs + */ +function itShouldReleaseClaim() { + console.log('> itShouldReleaseClaim'); + releaseClaim(); +} + +/** + * Run all tests + */ +function RUN_ALL_TESTS() { + itShouldClaimVideoWithMonetizePolicy(); + itShouldUpdateAssetOwnership(); + itShouldReleaseClaim(); +} diff --git a/advanced/urlShortener.gs b/advanced/urlShortener.gs deleted file mode 100644 index e39c85361..000000000 --- a/advanced/urlShortener.gs +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_url_shortener_shorten] -/** - * Shortens a long URL. Logs this URL. - */ -function shortenUrl() { - var url = UrlShortener.Url.insert({ - longUrl: 'http://www.example.com' - }); - Logger.log('Shortened URL is "%s".', url.id); -} -// [END apps_script_url_shortener_shorten] - -// [START apps_script_url_shortener_get_clicks] -/** - * Logs the number of clicks to a short URL over the last week. - * @param {string} shortUrl The short URL. - */ -function getClicks(shortUrl) { - var url = UrlShortener.Url.get(shortUrl, { - projection: 'ANALYTICS_CLICKS' - }); - Logger.log('The URL received %s clicks this week.', url.analytics.week.shortUrlClicks); -} -// [END apps_script_url_shortener_get_clicks] diff --git a/advanced/youtube.gs b/advanced/youtube.gs index 45cf2ceda..87a2f86d8 100644 --- a/advanced/youtube.gs +++ b/advanced/youtube.gs @@ -21,14 +21,22 @@ * @see https://developers.google.com/youtube/v3/docs/search/list */ function searchByKeyword() { - var results = YouTube.Search.list('id,snippet', { - q: 'dogs', - maxResults: 25 - }); - - results.items.forEach(function(item) { - Logger.log('[%s] Title: %s', item.id.videoId, item.snippet.title); - }); + try { + const results = YouTube.Search.list('id,snippet', { + q: 'dogs', + maxResults: 25 + }); + if (results === null) { + console.log('Unable to search videos'); + return; + } + results.items.forEach((item)=> { + console.log('[%s] Title: %s', item.id.videoId, item.snippet.title); + }); + } catch (err) { + // TODO (developer) - Handle exceptions from Youtube API + console.log('Failed with an error %s', err.message); + } } // [END apps_script_youtube_search] @@ -41,31 +49,45 @@ function searchByKeyword() { * 4. If there is a next page of resuts, fetching it and returns to step 3. */ function retrieveMyUploads() { - var results = YouTube.Channels.list('contentDetails', { - mine: true - }); - - for (var i = 0; i < results.items.length; i++) { - var item = results.items[i]; - // Get the channel ID - it's nested in contentDetails, as described in the - // Channel resource: https://developers.google.com/youtube/v3/docs/channels - var playlistId = item.contentDetails.relatedPlaylists.uploads; - var nextPageToken; - while (nextPageToken !== null) { - var playlistResponse = YouTube.PlaylistItems.list('snippet', { - playlistId: playlistId, - maxResults: 25, - pageToken: nextPageToken - }); - - for (var j = 0; j < playlistResponse.items.length; j++) { - var playlistItem = playlistResponse.items[j]; - Logger.log('[%s] Title: %s', - playlistItem.snippet.resourceId.videoId, - playlistItem.snippet.title); - } - nextPageToken = playlistResponse.nextPageToken; + try { + // @see https://developers.google.com/youtube/v3/docs/channels/list + const results = YouTube.Channels.list('contentDetails', { + mine: true + }); + if (!results || results.items.length === 0) { + console.log('No Channels found.'); + return; + } + for (let i = 0; i < results.items.length; i++) { + const item = results.items[i]; + /** Get the channel ID - it's nested in contentDetails, as described in the + * Channel resource: https://developers.google.com/youtube/v3/docs/channels. + */ + const playlistId = item.contentDetails.relatedPlaylists.uploads; + let nextPageToken = null; + do { + // @see: https://developers.google.com/youtube/v3/docs/playlistItems/list + const playlistResponse = YouTube.PlaylistItems.list('snippet', { + playlistId: playlistId, + maxResults: 25, + pageToken: nextPageToken + }); + if (!playlistResponse || playlistResponse.items.length === 0) { + console.log('No Playlist found.'); + break; + } + for (let j = 0; j < playlistResponse.items.length; j++) { + const playlistItem = playlistResponse.items[j]; + console.log('[%s] Title: %s', + playlistItem.snippet.resourceId.videoId, + playlistItem.snippet.title); + } + nextPageToken = playlistResponse.nextPageToken; + } while (nextPageToken); } + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with err %s', err.message); } } // [END apps_script_youtube_uploads] @@ -73,11 +95,12 @@ function retrieveMyUploads() { // [START apps_script_youtube_subscription] /** * This sample subscribes the user to the Google Developers channel on YouTube. + * @see https://developers.google.com/youtube/v3/docs/subscriptions/insert */ function addSubscription() { // Replace this channel ID with the channel ID you want to subscribe to - var channelId = 'UC9gFih9rw0zNCK3ZtoKQQyA'; - var resource = { + const channelId = 'UC_x5XG1OV2P6uZZ5FSM9Ttw'; + const resource = { snippet: { resourceId: { kind: 'youtube#channel', @@ -87,14 +110,15 @@ function addSubscription() { }; try { - var response = YouTube.Subscriptions.insert(resource, 'snippet'); - Logger.log(response); + const response = YouTube.Subscriptions.insert(resource, 'snippet'); + console.log('Added subscription for channel title : %s', response.snippet.title); } catch (e) { if (e.message.match('subscriptionDuplicate')) { - Logger.log('Cannot subscribe; already subscribed to channel: ' + - channelId); + console.log('Cannot subscribe; already subscribed to channel: ' + + channelId); } else { - Logger.log('Error adding subscription: ' + e.message); + // TODO (developer) - Handle exception + console.log('Error adding subscription: ' + e.message); } } } @@ -105,23 +129,23 @@ function addSubscription() { * Creates a slide presentation with 10 videos from the YouTube search `YOUTUBE_QUERY`. * The YouTube Advanced Service must be enabled before using this sample. */ -var PRESENTATION_TITLE = 'San Francisco, CA'; -var YOUTUBE_QUERY = 'San Francisco, CA'; +const PRESENTATION_TITLE = 'San Francisco, CA'; +const YOUTUBE_QUERY = 'San Francisco, CA'; /** * Gets a list of YouTube videos. * @param {String} query - The query term to search for. * @return {object[]} A list of objects with YouTube video data. - * @ref https://developers.google.com/youtube/v3/docs/search/list + * @see https://developers.google.com/youtube/v3/docs/search/list */ function getYouTubeVideosJSON(query) { - var youTubeResults = YouTube.Search.list('id,snippet', { + const youTubeResults = YouTube.Search.list('id,snippet', { q: query, type: 'video', maxResults: 10 }); - return youTubeResults.items.map(function(item) { + return youTubeResults.items.map((item)=> { return { url: 'https://youtu.be/' + item.id.videoId, title: item.snippet.title, @@ -135,17 +159,25 @@ function getYouTubeVideosJSON(query) { * Logs out the URL of the presentation. */ function createSlides() { - var youTubeVideos = getYouTubeVideosJSON(YOUTUBE_QUERY); - var presentation = SlidesApp.create(PRESENTATION_TITLE); - presentation.getSlides()[0].getPageElements()[0].asShape() - .getText().setText(PRESENTATION_TITLE); - - // Add slides with videos and log the presentation URL to the user. - youTubeVideos.forEach(function(video) { - var slide = presentation.appendSlide(); - slide.insertVideo(video.url, - 0, 0, presentation.getPageWidth(), presentation.getPageHeight()); - }); - Logger.log(presentation.getUrl()); + try { + const youTubeVideos = getYouTubeVideosJSON(YOUTUBE_QUERY); + const presentation = SlidesApp.create(PRESENTATION_TITLE); + presentation.getSlides()[0].getPageElements()[0].asShape() + .getText().setText(PRESENTATION_TITLE); + if (!presentation) { + console.log('Unable to create presentation'); + return; + } + // Add slides with videos and log the presentation URL to the user. + youTubeVideos.forEach((video)=> { + const slide = presentation.appendSlide(); + slide.insertVideo(video.url, + 0, 0, presentation.getPageWidth(), presentation.getPageHeight()); + }); + console.log(presentation.getUrl()); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } } // [END apps_script_youtube_slides] diff --git a/advanced/youtubeAnalytics.gs b/advanced/youtubeAnalytics.gs index a8a3b30c2..cfb0edda0 100644 --- a/advanced/youtubeAnalytics.gs +++ b/advanced/youtubeAnalytics.gs @@ -13,30 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - // [START apps_script_youtube_report] +// [START apps_script_youtube_report] /** * Creates a spreadsheet containing daily view counts, watch-time metrics, * and new-subscriber counts for a channel's videos. */ function createReport() { // Retrieve info about the user's YouTube channel. - var channels = YouTube.Channels.list('id,contentDetails', { + const channels = YouTube.Channels.list('id,contentDetails', { mine: true }); - var channelId = channels.items[0].id; + const channelId = channels.items[0].id; // Retrieve analytics report for the channel. - var oneMonthInMillis = 1000 * 60 * 60 * 24 * 30; - var today = new Date(); - var lastMonth = new Date(today.getTime() - oneMonthInMillis); + const oneMonthInMillis = 1000 * 60 * 60 * 24 * 30; + const today = new Date(); + const lastMonth = new Date(today.getTime() - oneMonthInMillis); - var metrics = [ + const metrics = [ 'views', 'estimatedMinutesWatched', 'averageViewDuration', 'subscribersGained' ]; - var result = YouTubeAnalytics.Reports.query({ + const result = YouTubeAnalytics.Reports.query({ ids: 'channel==' + channelId, startDate: formatDateString(lastMonth), endDate: formatDateString(today), @@ -45,25 +45,25 @@ function createReport() { sort: 'day' }); - if (result.rows) { - var spreadsheet = SpreadsheetApp.create('YouTube Analytics Report'); - var sheet = spreadsheet.getActiveSheet(); + if (!result.rows) { + console.log('No rows returned.'); + return; + } + const spreadsheet = SpreadsheetApp.create('YouTube Analytics Report'); + const sheet = spreadsheet.getActiveSheet(); - // Append the headers. - var headers = result.columnHeaders.map(function(columnHeader) { - return formatColumnName(columnHeader.name); - }); - sheet.appendRow(headers); + // Append the headers. + const headers = result.columnHeaders.map((columnHeader)=> { + return formatColumnName(columnHeader.name); + }); + sheet.appendRow(headers); - // Append the results. - sheet.getRange(2, 1, result.rows.length, headers.length) - .setValues(result.rows); + // Append the results. + sheet.getRange(2, 1, result.rows.length, headers.length) + .setValues(result.rows); - Logger.log('Report spreadsheet created: %s', - spreadsheet.getUrl()); - } else { - Logger.log('No rows returned.'); - } + console.log('Report spreadsheet created: %s', + spreadsheet.getUrl()); } /** @@ -82,7 +82,7 @@ function formatDateString(date) { * @example "averageViewPercentage" becomes "Average View Percentage". */ function formatColumnName(columnName) { - var name = columnName.replace(/([a-z])([A-Z])/g, '$1 $2'); + let name = columnName.replace(/([a-z])([A-Z])/g, '$1 $2'); name = name.slice(0, 1).toUpperCase() + name.slice(1); return name; } diff --git a/advanced/youtubeContentId.gs b/advanced/youtubeContentId.gs index 9b2b12596..22d37737c 100644 --- a/advanced/youtubeContentId.gs +++ b/advanced/youtubeContentId.gs @@ -17,15 +17,16 @@ /** * This function creates a partner-uploaded claim on a video with the specified * asset and policy rules. + * @see https://developers.google.com/youtube/partner/docs/v1/claims/insert */ function claimYourVideoWithMonetizePolicy() { // The ID of the content owner that you are acting on behalf of. - var onBehalfOfContentOwner = 'replaceWithYourContentOwnerID'; + const onBehalfOfContentOwner = 'replaceWithYourContentOwnerID'; // A YouTube video ID to claim. In this example, the video must be uploaded // to one of your onBehalfOfContentOwner's linked channels. - var videoId = 'replaceWithYourVideoID'; - var assetId = 'replaceWithYourAssetID'; - var claimToInsert = { + const videoId = 'replaceWithYourVideoID'; + const assetId = 'replaceWithYourAssetID'; + const claimToInsert = { 'videoId': videoId, 'assetId': assetId, 'contentType': 'audiovisual', @@ -37,12 +38,12 @@ function claimYourVideoWithMonetizePolicy() { 'policy': {'rules': [{'action': 'monetize'}]} }; try { - var claimInserted = YoutubeContentId.Claims.insert(claimToInsert, + const claimInserted = YouTubeContentId.Claims.insert(claimToInsert, {'onBehalfOfContentOwner': onBehalfOfContentOwner}); - Logger.log('Claim created on video %s: %s', videoId, claimInserted); + console.log('Claim created on video %s: %s', videoId, claimInserted); } catch (e) { - Logger.log('Failed to create claim on video %s, error: %s', - videoId, e.message); + console.log('Failed to create claim on video %s, error: %s', + videoId, e.message); } } // [END apps_script_youtube_claim] @@ -51,12 +52,15 @@ function claimYourVideoWithMonetizePolicy() { /** * This function updates your onBehalfOfContentOwner's ownership on an existing * asset. + * @see https://developers.google.com/youtube/partner/docs/v1/ownership/update */ function updateAssetOwnership() { - var onBehalfOfContentOwner = 'replaceWithYourContentOwnerID'; - var assetId = 'replaceWithYourAssetID'; + // The ID of the content owner that you are acting on behalf of. + const onBehalfOfContentOwner = 'replaceWithYourContentOwnerID'; + // Replace values with your asset id + const assetId = 'replaceWithYourAssetID'; // The new ownership here would replace your existing ownership on the asset. - var myAssetOwnership = { + const myAssetOwnership = { 'general': [ { 'ratio': 100, @@ -70,12 +74,12 @@ function updateAssetOwnership() { ] }; try { - var updatedOwnership = YoutubeContentId.Ownership.update(myAssetOwnership, + const updatedOwnership = YouTubeContentId.Ownership.update(myAssetOwnership, assetId, {'onBehalfOfContentOwner': onBehalfOfContentOwner}); - Logger.log('Ownership updated on asset %s: %s', assetId, updatedOwnership); + console.log('Ownership updated on asset %s: %s', assetId, updatedOwnership); } catch (e) { - Logger.log('Ownership update failed on asset %s, error: %s', - assetId, e.message); + console.log('Ownership update failed on asset %s, error: %s', + assetId, e.message); } } // [END apps_script_youtube_update_asset_ownership] @@ -84,21 +88,23 @@ function updateAssetOwnership() { /** * This function releases an existing claim your onBehalfOfContentOwner has * on a video. + * @see https://developers.google.com/youtube/partner/docs/v1/claims/patch */ function releaseClaim() { - var onBehalfOfContentOwner = 'replaceWithYourContentOwnerID'; + // The ID of the content owner that you are acting on behalf of. + const onBehalfOfContentOwner = 'replaceWithYourContentOwnerID'; // The ID of the claim to be released. - var claimId = 'replaceWithYourClaimID'; + const claimId = 'replaceWithYourClaimID'; // To release the claim, change the resource's status to inactive. - var claimToBeReleased = { + const claimToBeReleased = { 'status': 'inactive' }; try { - var claimReleased = YoutubeContentId.Claims.patch(claimToBeReleased, + const claimReleased = YouTubeContentId.Claims.patch(claimToBeReleased, claimId, {'onBehalfOfContentOwner': onBehalfOfContentOwner}); - Logger.log('Claim %s was released: %s', claimId, claimReleased); + console.log('Claim %s was released: %s', claimId, claimReleased); } catch (e) { - Logger.log('Failed to release claim %s, error: %s', claimId, e.message); + console.log('Failed to release claim %s, error: %s', claimId, e.message); } } // [END apps_script_youtube_release_claim] diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml deleted file mode 100644 index 91c1a45dc..000000000 --- a/android/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/android/MyAddOnActivity.java b/android/MyAddOnActivity.java deleted file mode 100644 index 25207d78d..000000000 --- a/android/MyAddOnActivity.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// [START apps_script_android_activity] -import android.accounts.Account; -import android.app.Activity; - -public class MyAddOnActivity extends Activity { - // Your activity... - - String sessionState; - String docId; - Account account; - - @Override - protected void onCreate(Bundle state) { - super.onCreate(state); - docId = getIntent().getStringExtra( - "com.google.android.apps.docs.addons.DocumentId"); - sessionState = getIntent().getStringExtra( - "com.google.android.apps.docs.addons.SessionState"); - account = (Account) getIntent().getParcelableExtra( - "com.google.android.apps.docs.addons.Account"); - // Your activity’s initialization... - } - - // [START apps_script_android_execution] - protected void makeRequest() { - // Acquire the session state String from the calling Intent. - sessionState = getIntent().getStringExtra( - "com.google.android.apps.docs.addons.SessionState"); - // ... - // Construct the API request. - ExecutionRequest request = new ExecutionRequest() - .setFunction(functionName) - .setSessionState(sessionState) - .setParameters(params) // Only needed if the function requires parameters - .setDevMode(true); // Optional - } - // [END apps_script_android_execution] -} -// [END apps_script_android_activity] \ No newline at end of file diff --git a/android/README.md b/android/README.md deleted file mode 100644 index bb9ebeb4c..000000000 --- a/android/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Apps Scripts for Android - -## [Converting Android Apps into Android Add-ons](https://developers.google.com/apps-script/add-ons/mobile/android) - -This sample describes how to convert an existing Android app into an Android add-on. - -## Mobile Doc Translate Add-on - -A sample Google Apps Script mobile add-on for Google Docs. diff --git a/android/mobile-translate/Code.gs b/android/mobile-translate/Code.gs deleted file mode 100644 index 3f147683e..000000000 --- a/android/mobile-translate/Code.gs +++ /dev/null @@ -1,260 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * @OnlyCurrentDoc - * - * The above comment directs Apps Script to limit the scope of file - * access for this add-on. It specifies that this add-on will only - * attempt to read or modify the files in which the add-on is used, - * and not all of the user's files. The authorization request message - * presented to users will reflect this limited scope. - */ - -/** - * Creates a menu entry in the Google Docs UI when the document is opened. - * This method is only used by the regular add-on, and is never called by - * the mobile add-on version. - * - * @param {object} e The event parameter for a simple onOpen trigger. To - * determine which authorization mode (ScriptApp.AuthMode) the trigger is - * running in, inspect e.authMode. - */ -function onOpen(e) { - DocumentApp.getUi().createAddonMenu() - .addItem('Start', 'showSidebar') - .addToUi(); -} - -/** - * Runs when the add-on is installed. - * This method is only used by the regular add-on, and is never called by - * the mobile add-on version. - * - * @param {object} e The event parameter for a simple onInstall trigger. To - * determine which authorization mode (ScriptApp.AuthMode) the trigger is - * running in, inspect e.authMode. (In practice, onInstall triggers always - * run in AuthMode.FULL, but onOpen triggers may be AuthMode.LIMITED or - * AuthMode.NONE.) - */ -function onInstall(e) { - onOpen(e); -} - -/** - * Opens a sidebar in the document containing the add-on's user interface. - * This method is only used by the regular add-on, and is never called by - * the mobile add-on version. - */ -function showSidebar() { - var ui = HtmlService.createHtmlOutputFromFile('Sidebar') - .setTitle('Translate'); - DocumentApp.getUi().showSidebar(ui); -} - -/** - * Gets the text the user has selected. If there is no selection, - * this function displays an error message. - * - * @return {Array.} The selected text. - */ -function getSelectedText() { - var selection = DocumentApp.getActiveDocument().getSelection(); - if (selection) { - var text = []; - var elements = selection.getSelectedElements(); - for (var i = 0; i < elements.length; i++) { - if (elements[i].isPartial()) { - var element = elements[i].getElement().asText(); - var startIndex = elements[i].getStartOffset(); - var endIndex = elements[i].getEndOffsetInclusive(); - - text.push(element.getText().substring(startIndex, endIndex + 1)); - } else { - var element = elements[i].getElement(); - // Only translate elements that can be edited as text; skip images and - // other non-text elements. - if (element.editAsText) { - var elementText = element.asText().getText(); - // This check is necessary to exclude images, which return a blank - // text element. - if (elementText != '') { - text.push(elementText); - } - } - } - } - if (text.length) { - throw new Error('Please select some text.'); - } - return text; - } else { - throw new Error('Please select some text.'); - } -} - -/** - * Gets the stored user preferences for the origin and destination languages, - * if they exist. - * This method is only used by the regular add-on, and is never called by - * the mobile add-on version. - * - * @return {Object} The user's origin and destination language preferences, if - * they exist. - */ -function getPreferences() { - var userProperties = PropertiesService.getUserProperties(); - var languagePrefs = { - originLang: userProperties.getProperty('originLang'), - destLang: userProperties.getProperty('destLang') - }; - return languagePrefs; -} - -/** - * Gets the user-selected text and translates it from the origin language to the - * destination language. The languages are notated by their two-letter short - * form. For example, English is 'en', and Spanish is 'es'. The origin language - * may be specified as an empty string to indicate that Google Translate should - * auto-detect the language. - * - * @param {string} origin The two-letter short form for the origin language. - * @param {string} dest The two-letter short form for the destination language. - * @param {boolean} savePrefs Whether to save the origin and destination - * language preferences. - * @return {Object} Object containing the original text and the result of the - * translation. - */ -function getTextAndTranslation(origin, dest, savePrefs) { - var result = {}; - var text = getSelectedText(); - result['text'] = text.join('\n'); - - if (savePrefs == true) { - var userProperties = PropertiesService.getUserProperties(); - userProperties.setProperty('originLang', origin); - userProperties.setProperty('destLang', dest); - } - - result['translation'] = translateText(result['text'], origin, dest); - - return result; -} - -/** - * Replaces the text of the current selection with the provided text, or - * inserts text at the current cursor location. (There will always be either - * a selection or a cursor.) If multiple elements are selected, only inserts the - * translated text in the first element that can contain text and removes the - * other elements. - * - * @param {string} newText The text with which to replace the current selection. - */ -function insertText(newText) { - var selection = DocumentApp.getActiveDocument().getSelection(); - if (selection) { - var replaced = false; - var elements = selection.getSelectedElements(); - if (elements.length == 1 && - elements[0].getElement().getType() == - DocumentApp.ElementType.INLINE_IMAGE) { - throw new Error('Can\'t insert text into an image.'); - } - for (var i = 0; i < elements.length; i++) { - if (elements[i].isPartial()) { - var element = elements[i].getElement().asText(); - var startIndex = elements[i].getStartOffset(); - var endIndex = elements[i].getEndOffsetInclusive(); - - var remainingText = element.getText().substring(endIndex + 1); - element.deleteText(startIndex, endIndex); - if (!replaced) { - element.insertText(startIndex, newText); - replaced = true; - } else { - // This block handles a selection that ends with a partial element. We - // want to copy this partial text to the previous element so we don't - // have a line-break before the last partial. - var parent = element.getParent(); - parent.getPreviousSibling().asText().appendText(remainingText); - // We cannot remove the last paragraph of a doc. If this is the case, - // just remove the text within the last paragraph instead. - if (parent.getNextSibling()) { - parent.removeFromParent(); - } else { - element.removeFromParent(); - } - } - } else { - var element = elements[i].getElement(); - if (!replaced && element.editAsText) { - // Only translate elements that can be edited as text, removing other - // elements. - element.clear(); - element.asText().setText(newText); - replaced = true; - } else { - // We cannot remove the last paragraph of a doc. If this is the case, - // just clear the element. - if (element.getNextSibling()) { - element.removeFromParent(); - } else { - element.clear(); - } - } - } - } - } else { - var cursor = DocumentApp.getActiveDocument().getCursor(); - var surroundingText = cursor.getSurroundingText().getText(); - var surroundingTextOffset = cursor.getSurroundingTextOffset(); - - // If the cursor follows or preceds a non-space character, insert a space - // between the character and the translation. Otherwise, just insert the - // translation. - if (surroundingTextOffset > 0) { - if (surroundingText.charAt(surroundingTextOffset - 1) != ' ') { - newText = ' ' + newText; - } - } - if (surroundingTextOffset < surroundingText.length) { - if (surroundingText.charAt(surroundingTextOffset) != ' ') { - newText += ' '; - } - } - cursor.insertText(newText); - } -} - - -/** - * Given text, translate it from the origin language to the destination - * language. The languages are notated by their two-letter short form. For - * example, English is 'en', and Spanish is 'es'. The origin language may be - * specified as an empty string to indicate that Google Translate should - * auto-detect the language. - * - * @param {string} text text to translate. - * @param {string} origin The two-letter short form for the origin language. - * @param {string} dest The two-letter short form for the destination language. - * @return {string} The result of the translation, or the original text if - * origin and dest languages are the same. - */ -function translateText(text, origin, dest) { - if (origin === dest) { - return text; - } - return LanguageApp.translate(text, origin, dest); -} diff --git a/android/mobile-translate/README.md b/android/mobile-translate/README.md deleted file mode 100644 index 83c245640..000000000 --- a/android/mobile-translate/README.md +++ /dev/null @@ -1,118 +0,0 @@ -Mobile Doc Translate Add-on -=========================== - -A sample Google Apps Script mobile add-on for Google Docs. This add-on is -essentially a mobile version of the Docs -[Translate Add-on Quickstart](https://developers.google.com/apps-script/quickstart/docs). - -Introduction ------------- - -Google Apps Script now allows developers to construct Mobile Add-ons -- Android -applications which extend and support Google Docs and Sheets. - -This sample shows how to construct a mobile add-on called -**Mobile Doc Translate**. This add-on allows users to select text in a -Google Doc on their mobile device and see a translation of that text in one -of several languages. The user can then edit the translation as needed and -replace the original selected text in the Doc with the translation. - - -Getting Started ---------------- - -The add-on will need to call an Apps Script project to get Doc text, make -translations, and insert text into the Doc. Users can access this add-on from -the Google Docs Android app by highlighting text and selecting the add-on in the -text context menu. - -The Apps Script code file for this project is `Code.gs`. This is the same code -used in the [Translate Add-on Quickstart](https://developers.google.com/apps-script/quickstart/docs), -but does not include the HTML code that defines the quickstart's sidebar. - -The mobile add-on will make use of the -[Apps Script Execution API](https://developers.google.com/apps-script/guides/rest/) -to call the `Code.gs` functions. The -[Execution API quickstart for Android](https://developers.google.com/apps-script/guides/rest/quickstart/android) -describes how to call Apps Script functions from Android applications. - -To build this sample: - -1. The `app/` folder in this repository contains all the required Android files - for this add-on. These can be manually copied or imported into a new Android - Studio project. -1. Create a new Apps Script project. -1. Replace the code in the new project's `Code.gs` file with the code from this - repo. -1. Save the project. -1. In the code editor, select **Publish > Deploy as API** executable. -1. In the dialog that opens, leave the **Version** as "New" and enter - "Target-v1" into the text box. Click **Deploy**. -1. Follow the - [Keytool SHA1 Fingerprint](https://developers.google.com/apps-script/guides/rest/quickstart/android#step_1_acquire_a_sha1_fingerprint) - instructions to acquire a SHA1 fingerprint for your project. -1. Using that SHA code, follow the - [Turn on the Execution API](https://developers.google.com/apps-script/guides/rest/quickstart/android#step_2_turn_on_the_api_name) - instructions to enable the API for your script project and create OAuth - credentials. Be sure to match the same package name used in your Android - code. -1. Edit the `MainActivity.java` file so that the `SCRIPT_ID` constant is set to - your Apps Script project ID (in the script editor, select - **File > Project properties**, and use the **Project key**). - - These steps should allow you to build the Android app and have it successfully - call the Apps Script code. You can test it by: - - 1. Install the app on a test Android device. - 1. Set the app as the debug app on the device by running this - [ADB](https://developer.android.com/studio/command-line/adb.html) - command: - `$ adb shell am set-debug-app --persistent ` - 1. Open a docucment using the Google Docs app on the device. - 1. Highlight some text in the doc and select the three-dot icon to open the - context menu, and then select **Mobile Doc Translate**. - -Learn more ----------- - -To continue learning about mobile add-ons for Google Docs and Sheets, -take a look at the following resources: - -* [Mobile Add-ons](https://developers.google.com/apps-script/add-ons/mobile) -* [Apps Script Execution API](https://developers.google.com/apps-script/guides/) - -Support -------- - -For general Apps Script support, check the following: - -- Stack Overflow Tag: [google-apps-script](http://stackoverflow.com/questions/tagged/google-apps-script) -- Issue Tracker: [google-apps-script-issues](https://code.google.com/p/google-apps-script-issues/issues/list) - -If you've found an error in this sample, please file an issue: -https://github.com/googlesamples/apps-script-mobile-addons - -Patches are encouraged, and may be submitted by forking this project and -submitting a pull request through GitHub. - -License -------- - -Copyright 2016 Google, Inc. - -Licensed to the Apache Software Foundation (ASF) under one -or more contributor license agreements. See the NOTICE file -distributed with this work for additional information -regarding copyright ownership. The ASF licenses this file -to you 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. \ No newline at end of file diff --git a/android/mobile-translate/app/build.gradle b/android/mobile-translate/app/build.gradle deleted file mode 100644 index 5a3e94242..000000000 --- a/android/mobile-translate/app/build.gradle +++ /dev/null @@ -1,48 +0,0 @@ -apply plugin: 'com.android.application' - -def keystorePropertiesFile = rootProject.file("keystore.properties") -def keystoreProperties = new Properties() -keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - -android { - signingConfigs { - mainRelease { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - } - } - compileSdkVersion 23 - buildToolsVersion "24.0.0 rc4" - defaultConfig { - applicationId "com.google.samples.mobiledoctranslate" - minSdkVersion 17 - targetSdkVersion 23 - versionCode 2 - versionName "1.0.1" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), - 'proguard-rules.pro' - signingConfig signingConfigs.mainRelease - } - } -} - -dependencies { - compile fileTree(include: ['*.jar'], dir: 'libs') - testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:23.4.0' - compile 'com.google.android.gms:play-services-auth:9.0.2' - compile 'com.android.support:cardview-v7:23.4.0' - compile 'pub.devrel:easypermissions:0.1.5' - compile('com.google.api-client:google-api-client-android:1.20.0') { - exclude group: 'org.apache.httpcomponents' - } - compile('com.google.apis:google-api-services-script:v1-rev1-1.20.0') { - exclude group: 'org.apache.httpcomponents' - } -} diff --git a/android/mobile-translate/app/src/main/AndroidManifest.xml b/android/mobile-translate/app/src/main/AndroidManifest.xml deleted file mode 100644 index 6bffeaec7..000000000 --- a/android/mobile-translate/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/DefaultLaunchActivity.java b/android/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/DefaultLaunchActivity.java deleted file mode 100644 index e5dd3bfb9..000000000 --- a/android/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/DefaultLaunchActivity.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.samples.mobiledoctranslate; - -import android.app.Activity; -import android.os.Bundle; -import android.view.View; - -/** - * Since this add-on needs context from the Docs editor app, it should only be - * launched from that app (via context menus). - * - * This activity handles the edge case where the app is (erroneously) launched - * from the home screen or a notification. This activity simply presents a - * message to the user and provides an Exit button. - */ -public class DefaultLaunchActivity extends Activity { - - /** - * Create the default launch activity. - * @param savedInstanceState previously saved instance data - */ - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.launch_default); - } - - /** - * Cancel the add-on and return without action. - * @param v The button's View context - */ - public void cancel(View v) { - setResult(Activity.RESULT_CANCELED); - finish(); - } -} diff --git a/android/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/MainActivity.java b/android/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/MainActivity.java deleted file mode 100644 index 1995695aa..000000000 --- a/android/mobile-translate/app/src/main/java/com/google/samples/mobiledoctranslate/MainActivity.java +++ /dev/null @@ -1,848 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.samples.mobiledoctranslate; - -import com.google.android.gms.auth.GoogleAuthException; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.api.client.extensions.android.http.AndroidHttp; -import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential; -import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException; -import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestInitializer; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.jackson2.JacksonFactory; -import com.google.api.client.util.ExponentialBackOff; -import com.google.api.services.script.model.*; -import com.google.api.services.script.Script; - -import android.Manifest; -import android.accounts.Account; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.AsyncTask; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.text.method.ScrollingMovementMethod; -import android.view.View; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import java.io.IOException; -import java.lang.StringBuilder; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import pub.devrel.easypermissions.AfterPermissionGranted; -import pub.devrel.easypermissions.EasyPermissions; - -/** - * This is the main (and only) activity of the add-on. It shows the user what - * text or cells were selected, the results of translation and provides some - * UI controls. - */ -public class MainActivity extends Activity - implements EasyPermissions.PermissionCallbacks { - - /** - * The script ID for the Apps Script the add-on will call - */ - private static final String SCRIPT_ID = "ENTER_YOUR_SCRIPT_ID_HERE"; - - // Constants - private static final String FUNCTION_GET_TEXT = "getTextAndTranslation"; - private static final String FUNCTION_TRANSLATE_TEXT = "translateText"; - private static final String FUNCTION_INSERT_TEXT = "insertText"; - static final int REQUEST_AUTHORIZATION = 1001; - static final int REQUEST_GOOGLE_PLAY_SERVICES = 1002; - static final int REQUEST_PERMISSION_GET_ACCOUNTS = 1003; - private static final String[] SCOPES = { - "https://www.googleapis.com/auth/documents.currentonly", - "https://www.googleapis.com/auth/script.scriptapp", - "https://www.googleapis.com/auth/script.storage" - }; - static final String SAVED_ORIG_LANG = "origLangPosition"; - static final String SAVED_DEST_LANG = "destLangPosition"; - private static final int CALL_GET_TEXT = 0; - private static final int CALL_TRANSLATE_TEXT = 1; - private static final int CALL_REPLACE_TEXT = 2; - - /** - * An Apps Script API service object used to access the API, and related - * objects - */ - Script mService = null; - GoogleAccountCredential mCredential = null; - final HttpTransport mTransport = AndroidHttp.newCompatibleTransport(); - final JsonFactory mJsonFactory = JacksonFactory.getDefaultInstance(); - - // Layout components - private TextView mSelectedText; - private EditText mTranslationText; - private Button mReplaceButton; - private ProgressDialog mProgress; - - // Translation language controls - private String mOrigLang; - private String mDestLang; - private int mPrevOrigSpinnerPos; - private int mPrevDestSpinnerPos; - - // Other variables - private NetworkReceiver mReceiver; - private boolean mConnectionAvailable; - private int mLastFunctionCalled; - private String mState; - private Account mAccount; - - /** - * Create the main activity. - * @param savedInstanceState previously saved instance data - */ - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - // Verify the add-on was called from the Docs editor. - if (! "com.google.android.apps.docs.editors.docs".equals( - getCallingPackage())) { - showErrorDialog(getString(R.string.unexpected_app) - + getCallingPackage()); - } - - // Acquire the doc/sheet state from the incoming intent. - // It's also possible to acquire the docId from the intent; - // that is not used in this example, however. - mState = getIntent().getStringExtra( - "com.google.android.apps.docs.addons.SessionState"); - mAccount = getIntent().getParcelableExtra( - "com.google.android.apps.docs.addons.Account"); - - // Load previously chosen language selections, if any - SharedPreferences settings = getPreferences(Context.MODE_PRIVATE); - mPrevOrigSpinnerPos = settings.getInt(SAVED_ORIG_LANG, 0); - mOrigLang = getLangIdFromSpinnerPosition(mPrevOrigSpinnerPos, false); - mPrevDestSpinnerPos = settings.getInt(SAVED_DEST_LANG, 0); - mDestLang = getLangIdFromSpinnerPosition(mPrevDestSpinnerPos, true); - - // Initialize layout objects - mProgress = new ProgressDialog(MainActivity.this); - - mSelectedText = (TextView) findViewById(R.id.selected_text); - mSelectedText.setVerticalScrollBarEnabled(true); - mSelectedText.setMovementMethod(new ScrollingMovementMethod()); - - mTranslationText = (EditText) findViewById(R.id.translated_text); - mTranslationText.setVerticalScrollBarEnabled(true); - mTranslationText.setMovementMethod(new ScrollingMovementMethod()); - - mReplaceButton = (Button) findViewById(R.id.replace_button); - - Spinner origLangSpinner = (Spinner) findViewById(R.id.origin_lang); - Spinner destLangSpinner = (Spinner) findViewById(R.id.dest_lang); - origLangSpinner.setSelection(mPrevOrigSpinnerPos); - destLangSpinner.setSelection(mPrevDestSpinnerPos); - origLangSpinner.setOnItemSelectedListener( - new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected( - AdapterView parent, View view, int pos, long id) { - if (pos != mPrevOrigSpinnerPos) { - mPrevOrigSpinnerPos = pos; - mOrigLang = getLangIdFromSpinnerPosition(pos, false); - SharedPreferences settings = - getPreferences(Context.MODE_PRIVATE); - SharedPreferences.Editor editor = settings.edit(); - editor.putInt(SAVED_ORIG_LANG, pos); - editor.apply(); - translate(); - } - } - - @Override - public void onNothingSelected(AdapterView parent) {} - }); - destLangSpinner.setOnItemSelectedListener( - new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected( - AdapterView parent, View view, int pos, long id) { - if (pos != mPrevDestSpinnerPos) { - mPrevDestSpinnerPos = pos; - mDestLang = getLangIdFromSpinnerPosition(pos, true); - SharedPreferences settings = - getPreferences(Context.MODE_PRIVATE); - SharedPreferences.Editor editor = settings.edit(); - editor.putInt(SAVED_DEST_LANG, pos); - editor.apply(); - translate(); - } - } - - @Override - public void onNothingSelected(AdapterView parent) {} - }); - - // Register BroadcastReceiver to track connection changes, and - // determine if a connection is initially available - IntentFilter filter = - new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); - mReceiver = new NetworkReceiver(); - MainActivity.this.registerReceiver(mReceiver, filter); - updateButtonEnableStatus(); - - // Start the add-on by attempting to retrieve the selected text from - // the Doc that fired the add-on - callAppsScriptTask(CALL_GET_TEXT); - } - - /** - * Extend the given HttpRequestInitializer (usually a Credentials object) - * with additional initialize() instructions. - * - * @param requestInitializer the initializer to copy and adjust; typically - * a Credential object - * @return an initializer with an extended read timeout - */ - private static HttpRequestInitializer setHttpTimeout( - final HttpRequestInitializer requestInitializer) { - return new HttpRequestInitializer() { - @Override - public void initialize(HttpRequest httpRequest) - throws java.io.IOException { - requestInitializer.initialize(httpRequest); - // This allows the API to call (and avoid timing out on) - // functions that take up to 30 seconds to complete. Note that - // the maximum allowed script run time is 6 minutes. - httpRequest.setReadTimeout(30000); - } - }; - } - - /** - * Clean up and destroy the main activity. - */ - @Override - public void onDestroy() { - super.onDestroy(); - // Unregister the connectivity broadcast receiver. - if (mReceiver != null) { - MainActivity.this.unregisterReceiver(mReceiver); - } - } - - /** - * Called when an activity launched here (specifically, AccountPicker - * and authorization) exits, giving you the requestCode you started it with, - * the resultCode it returned, and any additional data from it. - * @param requestCode code indicating which activity result is incoming - * @param resultCode code indicating the result of the incoming - * activity result - * @param data Intent (containing result data) returned by incoming - * activity result - */ - @Override - protected void onActivityResult( - int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - switch(requestCode) { - case REQUEST_GOOGLE_PLAY_SERVICES: - if (resultCode == RESULT_OK) { - callAppsScriptTask(mLastFunctionCalled); - } else { - showErrorDialog(getString(R.string.gps_required)); - } - break; - case REQUEST_AUTHORIZATION: - if (resultCode == RESULT_OK) { - callAppsScriptTask(mLastFunctionCalled); - } else { - showErrorDialog(getString(R.string.no_auth_provided)); - } - break; - } - } - - /** - * Call the API to execute an Apps Script function, after verifying - * all the preconditions are satisfied. The preconditions are: Google - * Play Services is installed, the device has a network connection, and - * the Execution API service and credentials have been created. - * @param functionToCall code indicating which function to call using - * the API - */ - private void callAppsScriptTask(int functionToCall) { - mLastFunctionCalled = functionToCall; - if (! isGooglePlayServicesAvailable()) { - toast(getString(R.string.gps_required)); - acquireGooglePlayServices(); - } else if (! mConnectionAvailable) { - toast(getString(R.string.no_network)); - } else if (! hasValidCredentials()) { - createCredentialsAndService(); - } else { - switch (functionToCall) { - case CALL_GET_TEXT: - new GetTextTask().execute(mOrigLang, mDestLang, false); - break; - case CALL_TRANSLATE_TEXT: - String originalText = mSelectedText.getText().toString(); - new TranslateTextTask().execute( - originalText, mOrigLang, mDestLang); - break; - case CALL_REPLACE_TEXT: - String translation = mTranslationText.getText().toString(); - new ReplaceTextTask().execute(translation); - break; - } - } - } - - /** - * Attempts to initialize credentials and service object (prior to a call - * to the API); uses the account provided by the calling app. This - * requires the GET_ACCOUNTS permission to be explicitly granted by the - * user; this will be requested here if it is not already granted. The - * AfterPermissionGranted annotation indicates that this function will be - * rerun automatically whenever the GET_ACCOUNTS permission is granted. - */ - @AfterPermissionGranted(REQUEST_PERMISSION_GET_ACCOUNTS) - private void createCredentialsAndService() { - if (EasyPermissions.hasPermissions( - MainActivity.this, Manifest.permission.GET_ACCOUNTS)) { - mCredential = GoogleAccountCredential.usingOAuth2( - getApplicationContext(), Arrays.asList(SCOPES)) - .setBackOff(new ExponentialBackOff()) - .setSelectedAccountName(mAccount.name); - mService = new com.google.api.services.script.Script.Builder( - mTransport, mJsonFactory, setHttpTimeout(mCredential)) - .setApplicationName(getString(R.string.app_name)) - .build(); - updateButtonEnableStatus(); - - // Callback to retry the API call with valid service/credentials - callAppsScriptTask(mLastFunctionCalled); - } else { - // Request the GET_ACCOUNTS permission via a user dialog - EasyPermissions.requestPermissions( - MainActivity.this, - getString(R.string.get_accounts_rationale), - REQUEST_PERMISSION_GET_ACCOUNTS, - Manifest.permission.GET_ACCOUNTS); - } - } - - /** - * Returns true if a valid service object has been created and instantiated - * with valid OAuth credentials; returns false otherwise. - * @return true if the service and credentials are valid; false otherwise. - */ - private boolean hasValidCredentials() { - return mService != null - && mCredential != null - && mCredential.getSelectedAccountName() != null; - } - - /** - * Respond to requests for permissions at runtime for SDK 23 and above. - * @param requestCode The request code passed in - * requestPermissions(android.app.Activity, String, int, String[]) - * @param permissions The requested permissions. Never null. - * @param grantResults The grant results for the corresponding permissions - * which is either PERMISSION_GRANTED or PERMISSION_DENIED. Never null. - */ - @Override - public void onRequestPermissionsResult(int requestCode, - @NonNull String[] permissions, - @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - EasyPermissions.onRequestPermissionsResult( - requestCode, permissions, grantResults, MainActivity.this); - } - - /** - * Callback for when a permission is granted using the EasyPermissions - * library. - * @param requestCode The request code associated with the requested - * permission - * @param list The requested permission list. Never null. - */ - @Override - public void onPermissionsGranted(int requestCode, List list) { - // Do nothing. - } - - /** - * Callback for when a permission is denied using the EasyPermissions - * library. Displays status message and disables functionality that - * would require that permission. - * @param requestCode The request code associated with the requested - * permission - * @param list The requested permission list. Never null. - */ - @Override - public void onPermissionsDenied(int requestCode, List list) { - toast(getString(R.string.get_accounts_denied_message)); - updateButtonEnableStatus(); - } - - /** - * Given the position of one of the language spinners, return the language - * id corresponding to that position. - * @param pos spinner position - * @param omitAutoDetect true if the spinner does not include 'Auto-detect' - * as the first option - * @return String two-letter language id - */ - private String getLangIdFromSpinnerPosition(int pos, boolean omitAutoDetect) { - String id; - if (omitAutoDetect) { - pos++; - } - switch (pos) { - case 0: id = ""; break; // Auto-detect (input language only) - case 1: id = "ar"; break; // Arabic - case 2: id = "zh-CN"; break; // Chinese (Simplified) - case 3: id = "en"; break; // English - case 4: id = "fr"; break; // French - case 5: id = "de"; break; // German - case 6: id = "hi"; break; // Hindi - case 7: id = "ja"; break; // Japanese - case 8: id = "pt"; break; // Portuguese - case 9: id = "es"; break; // Spanish - default: id = "en"; break; - } - return id; - } - - /** - * Call the API to translate the selected text. - */ - private void translate() { - String originalText = mSelectedText.getText().toString(); - if (originalText.length() != 0) { - callAppsScriptTask(CALL_TRANSLATE_TEXT); - } - } - - /** - * Call the API to replace the translated text back to the original - * document. - * @param v The button's View context - */ - public void replace(View v) { - String translation = mTranslationText.getText().toString(); - if (translation.length() != 0) { - callAppsScriptTask(CALL_REPLACE_TEXT); - } - } - - /** - * Cancel the add-on and return without action to the calling app. - * @param v The button's View context - */ - public void cancel(View v) { - finishWithState(Activity.RESULT_CANCELED); - } - - /** - * End the add-on and return to the calling application. - * @param state result code for add-on: one of Activity.RESULT_CANCELED or - * Activity.RESULT_OK - */ - private void finishWithState(int state) { - dismissProgressDialog(); - setResult(state); - finish(); - } - - /** - * Display a short toast message. - * @param message text to display - */ - private void toast(String message) { - Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show(); - } - - /** - * Checks whether the device currently has a network connection. - * @return true if the device has a network connection, false otherwise - */ - private boolean isDeviceOnline() { - ConnectivityManager connMgr = - (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); - return (networkInfo != null && networkInfo.isConnected()); - } - - /** - * Check that Google Play services APK is installed and up to date. - * @return true if Google Play Services is available and up to - * date on this device; false otherwise. - */ - private boolean isGooglePlayServicesAvailable() { - GoogleApiAvailability apiAvailability = - GoogleApiAvailability.getInstance(); - final int connectionStatusCode = - apiAvailability.isGooglePlayServicesAvailable(MainActivity.this); - return connectionStatusCode == ConnectionResult.SUCCESS; - } - - /** - * Attempt to resolve a missing, out-of-date, invalid or disabled Google - * Play Services installation via a user dialog, if possible. - */ - private void acquireGooglePlayServices() { - GoogleApiAvailability apiAvailability = - GoogleApiAvailability.getInstance(); - final int connectionStatusCode = - apiAvailability.isGooglePlayServicesAvailable(MainActivity.this); - if (apiAvailability.isUserResolvableError(connectionStatusCode)) { - showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode); - } - } - /** - * Display an error dialog showing that Google Play Services is missing - * or out of date. - * @param connectionStatusCode code describing the presence (or lack of) - * Google Play Services on this device - */ - private void showGooglePlayServicesAvailabilityErrorDialog( - final int connectionStatusCode) { - Dialog dialog = - GoogleApiAvailability.getInstance().getErrorDialog( - MainActivity.this, - connectionStatusCode, - REQUEST_GOOGLE_PLAY_SERVICES); - dialog.show(); - } - - /** - * Check the current connectivity status of the device and enable/disable - * the highlight buttons if the device is online/offline, respectively. - */ - private void updateButtonEnableStatus() { - mConnectionAvailable = isDeviceOnline(); - boolean enable = mConnectionAvailable && hasValidCredentials(); - mReplaceButton.setEnabled(enable); - } - - /** - * Show a dialog with an error message, with a button to cancel out of - * the add-on. - * @param errorMessage Error message to display - */ - protected void showErrorDialog(String errorMessage) { - AlertDialog.Builder alertDialogBuilder = - new AlertDialog.Builder(MainActivity.this); - alertDialogBuilder.setTitle(getString(R.string.error_occurred)); - alertDialogBuilder - .setMessage(errorMessage) - .setCancelable(false) - .setNegativeButton( - getString(R.string.exit_button), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - finishWithState(Activity.RESULT_CANCELED); - } - }); - dismissProgressDialog(); - AlertDialog alertDialog = alertDialogBuilder.create(); - alertDialog.show(); - } - - /** - * Dismiss the ProgressDialog, if it is visible. - */ - public void dismissProgressDialog() { - if (mProgress != null && mProgress.isShowing()) { - Context context = - ((ContextWrapper) mProgress.getContext()).getBaseContext(); - // Dismiss only if launching activity hasn't been finished or - // destroyed - if(! (context instanceof Activity && - ((Activity)context).isFinishing() || - ((Activity)context).isDestroyed())) { - mProgress.dismiss(); - } - } - } - - /** - * This BroadcastReceiver intercepts the - * android.net.ConnectivityManager.CONNECTIVITY_ACTION, which indicates a - * connection change. This is used to determine if the API can be called. - */ - public class NetworkReceiver extends BroadcastReceiver { - /** - * Responds to a connection change, recording whether a connection is - * available. - * @param context The Context in which the receiver is running - * @param intent The Intent being received - */ - @Override - public void onReceive(Context context, Intent intent) { - // Checks the network connection. Based on the - // result, enables/disables flag to allow API calls and - // enables/disables buttons. - updateButtonEnableStatus(); - if (!mConnectionAvailable) { - toast(getString(R.string.no_network)); - } - } - } - - /** - * Abstract class for handling Execution API calls. Typically a subclass of - * this is created for each Apps Script function that will be called. - * Placing the API calls in their own task ensures the UI stays responsive. - */ - public abstract class CallApiTask extends AsyncTask { - - private Exception mLastError = null; - protected String mFunctionName; - - /** - * Background task to call Apps Script API. - * @param params Object parameters; used as parameters for that - * function, in the given order - * @return an object returned by the API; may be null - */ - @Override - protected Object doInBackground(Object... params) { - try { - return executeCall(mFunctionName, Arrays.asList(params)); - } catch (Exception e) { - mLastError = e; - cancel(true); - return null; - } - } - - /** - * Handle cancel requests -- specifically, those caused by exceptions - * raised when attempting to call the API. - */ - @Override - protected void onCancelled() { - mProgress.hide(); - if (mLastError != null) { - if (mLastError instanceof GooglePlayServicesAvailabilityIOException) { - showGooglePlayServicesAvailabilityErrorDialog( - ((GooglePlayServicesAvailabilityIOException) mLastError).getConnectionStatusCode()); - } else if (mLastError instanceof UserRecoverableAuthIOException) { - startActivityForResult( - ((UserRecoverableAuthIOException) mLastError).getIntent(), - MainActivity.REQUEST_AUTHORIZATION); - } else { - showErrorDialog(mLastError.toString()); - } - } - } - - /** - * Interpret an error response returned by the API and return a String - * summary. The summary will include the general error message and - * (in most cases) a stack trace. - * @param op the Operation returning an error response - * @return summary of error response, or null if Operation returned no - * error - */ - protected String getScriptError(Operation op) { - if (op.getError() == null) { - return null; - } - - // Extract the first (and only) set of error details and cast as a - // Map. The values of this map are the script's 'errorMessage' and - // 'errorType', and an array of stack trace elements (which also - // need to be cast as Maps). - Map detail = op.getError().getDetails().get(0); - List> stacktrace = - (List>)detail.get("scriptStackTraceElements"); - - StringBuilder sb = - new StringBuilder(getString(R.string.script_error)); - sb.append(detail.get("errorMessage")); - - if (stacktrace != null) { - // There may not be a stacktrace if the script didn't start - // executing. - sb.append(getString(R.string.script_error_trace)); - for (Map elem : stacktrace) { - sb.append("\n "); - sb.append(elem.get("function")); - sb.append(":"); - sb.append(elem.get("lineNumber")); - } - } - sb.append("\n"); - return sb.toString(); - } - - /** - * Given a script function and a list of parameter objects, create a - * request and run it with the API. - * @param functionName script function name to call - * @param params parameters needed by that function; may be null - * @return Object returned from a successful execution; may be null - * @throws IOException - * @throws GoogleAuthException - */ - protected Object executeCall(String functionName, List params) - throws IOException, GoogleAuthException { - // Create execution request. - ExecutionRequest request = new ExecutionRequest() - .setFunction(functionName) - .setSessionState(mState); - if (params != null) { - request.setParameters(params); - } - - // Call the API and return the results (as an Operation object). - Operation op = mService.scripts().run(SCRIPT_ID, request).execute(); - - // If the response from the API contains an error, throw an - // exception to display it. - if (op.getError() != null) { - throw new IOException(getScriptError(op)); - } - - // Return null if the API didn't yield a result. - if (op.getResponse() == null || - op.getResponse().get("result") == null) { - return null; - } - - return op.getResponse().get("result"); - } - } - - /** - * An asynchronous task that handles the Apps Script API call to get - * selected text data, and the translation of it to the previously chosen - * destination language (or the default destination language). - */ - public class GetTextTask extends CallApiTask { - /** - * Clear the display and show the progress bar prior to calling the - * API. - */ - @Override - protected void onPreExecute() { - mFunctionName = FUNCTION_GET_TEXT; - mProgress.setMessage(getString(R.string.retrieve_status)); - mProgress.show(); - } - - /** - * Take the text output and display it. If no data is returned, update - * the status bar accordingly. - * @param scriptResult object returned by the script function; may be - * null - */ - @Override - protected void onPostExecute(Object scriptResult) { - mProgress.hide(); - if (scriptResult != null) { - // Place the original text and default translation into the UI. - Map data = (Map) scriptResult; - mSelectedText.setText(data.get("text")); - mTranslationText.setText(data.get("translation")); - } - } - } - - /** - * An asynchronous task that handles the Apps Script API call to - * translate text data. - */ - public class TranslateTextTask extends CallApiTask { - /** - * Clear the display and show the progress bar prior to calling the - * API. - */ - @Override - protected void onPreExecute() { - mFunctionName = FUNCTION_TRANSLATE_TEXT; - mProgress.setMessage(getString(R.string.translate_status)); - mProgress.show(); - } - - /** - * Take the text output and display it. If no data is returned, update - * the status bar accordingly. - * @param scriptResult object returned by the script function; may be - * null - */ - @Override - protected void onPostExecute(Object scriptResult) { - mProgress.hide(); - if (scriptResult != null) { - String data = (String) scriptResult; - mTranslationText.setText(data); - } - } - } - - /** - * An asynchronous task that handles the Apps Script API call to send - * (translated) text back into the document, replacing the previous - * selection. - */ - public class ReplaceTextTask extends CallApiTask { - /** - * Show the progress bar prior to calling the API. - */ - @Override - protected void onPreExecute() { - mFunctionName = FUNCTION_INSERT_TEXT; - mProgress.setMessage(getString(R.string.replace_status)); - mProgress.show(); - } - - /** - * After finishing the API call, clear the progress bar and close - * the add-on. - * @param scriptResult object returned by the script function; should - * always be null for this script function - */ - @Override - protected void onPostExecute(Object scriptResult) { - mProgress.hide(); - finishWithState(Activity.RESULT_OK); - } - } -} diff --git a/android/mobile-translate/app/src/main/res/layout-land/activity_main.xml b/android/mobile-translate/app/src/main/res/layout-land/activity_main.xml deleted file mode 100644 index 80647ff0d..000000000 --- a/android/mobile-translate/app/src/main/res/layout-land/activity_main.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/mobile-translate/app/src/main/res/layout/activity_main.xml b/android/mobile-translate/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index caf9e4548..000000000 --- a/android/mobile-translate/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/mobile-translate/app/src/main/res/layout/buttons.xml b/android/mobile-translate/app/src/main/res/layout/buttons.xml deleted file mode 100644 index 6b336e14f..000000000 --- a/android/mobile-translate/app/src/main/res/layout/buttons.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - +
+ +
- +
+ + +
+
+ + +
+
+ + +
+ - - + + - - + - + /** + * Inserts a div that contains an error message after a given element. + * + * @param {string} msg The error message to display. + * @param {DOMElement} element The element after which to display the error. + */ + function showError(msg, element) { + const div = $('
' + msg + '
'); + $(element).after(div); + } + + diff --git a/docs/translate/translate.gs b/docs/translate/translate.gs index bec068f1a..38f7fc5b9 100644 --- a/docs/translate/translate.gs +++ b/docs/translate/translate.gs @@ -60,7 +60,7 @@ function onInstall(e) { * the mobile add-on version. */ function showSidebar() { - var ui = HtmlService.createHtmlOutputFromFile('sidebar') + const ui = HtmlService.createHtmlOutputFromFile('sidebar') .setTitle('Translate'); DocumentApp.getUi().showSidebar(ui); } @@ -72,23 +72,23 @@ function showSidebar() { * @return {Array.} The selected text. */ function getSelectedText() { - var selection = DocumentApp.getActiveDocument().getSelection(); - var text = []; + const selection = DocumentApp.getActiveDocument().getSelection(); + const text = []; if (selection) { - var elements = selection.getSelectedElements(); - for (var i = 0; i < elements.length; ++i) { + const elements = selection.getSelectedElements(); + for (let i = 0; i < elements.length; ++i) { if (elements[i].isPartial()) { - var element = elements[i].getElement().asText(); - var startIndex = elements[i].getStartOffset(); - var endIndex = elements[i].getEndOffsetInclusive(); + const element = elements[i].getElement().asText(); + const startIndex = elements[i].getStartOffset(); + const endIndex = elements[i].getEndOffsetInclusive(); text.push(element.getText().substring(startIndex, endIndex + 1)); } else { - var element = elements[i].getElement(); + const element = elements[i].getElement(); // Only translate elements that can be edited as text; skip images and // other non-text elements. if (element.editAsText) { - var elementText = element.asText().getText(); + const elementText = element.asText().getText(); // This check is necessary to exclude images, which return a blank // text element. if (elementText) { @@ -112,7 +112,7 @@ function getSelectedText() { * they exist. */ function getPreferences() { - var userProperties = PropertiesService.getUserProperties(); + const userProperties = PropertiesService.getUserProperties(); return { originLang: userProperties.getProperty('originLang'), destLang: userProperties.getProperty('destLang') @@ -139,7 +139,7 @@ function getTextAndTranslation(origin, dest, savePrefs) { .setProperty('originLang', origin) .setProperty('destLang', dest); } - var text = getSelectedText().join('\n'); + const text = getSelectedText().join('\n'); return { text: text, translation: translateText(text, origin, dest) @@ -156,19 +156,19 @@ function getTextAndTranslation(origin, dest, savePrefs) { * @param {string} newText The text with which to replace the current selection. */ function insertText(newText) { - var selection = DocumentApp.getActiveDocument().getSelection(); + const selection = DocumentApp.getActiveDocument().getSelection(); if (selection) { - var replaced = false; - var elements = selection.getSelectedElements(); + let replaced = false; + const elements = selection.getSelectedElements(); if (elements.length === 1 && elements[0].getElement().getType() === - DocumentApp.ElementType.INLINE_IMAGE) { + DocumentApp.ElementType.INLINE_IMAGE) { throw new Error('Can\'t insert text into an image.'); } - for (var i = 0; i < elements.length; ++i) { + for (let i = 0; i < elements.length; ++i) { if (elements[i].isPartial()) { - var element = elements[i].getElement().asText(); - var startIndex = elements[i].getStartOffset(); - var endIndex = elements[i].getEndOffsetInclusive(); + const element = elements[i].getElement().asText(); + const startIndex = elements[i].getStartOffset(); + const endIndex = elements[i].getEndOffsetInclusive(); element.deleteText(startIndex, endIndex); if (!replaced) { element.insertText(startIndex, newText); @@ -177,8 +177,8 @@ function insertText(newText) { // This block handles a selection that ends with a partial element. We // want to copy this partial text to the previous element so we don't // have a line-break before the last partial. - var parent = element.getParent(); - var remainingText = element.getText().substring(endIndex + 1); + const parent = element.getParent(); + const remainingText = element.getText().substring(endIndex + 1); parent.getPreviousSibling().asText().appendText(remainingText); // We cannot remove the last paragraph of a doc. If this is the case, // just remove the text within the last paragraph instead. @@ -189,7 +189,7 @@ function insertText(newText) { } } } else { - var element = elements[i].getElement(); + const element = elements[i].getElement(); if (!replaced && element.editAsText) { // Only translate elements that can be edited as text, removing other // elements. @@ -208,20 +208,20 @@ function insertText(newText) { } } } else { - var cursor = DocumentApp.getActiveDocument().getCursor(); - var surroundingText = cursor.getSurroundingText().getText(); - var surroundingTextOffset = cursor.getSurroundingTextOffset(); + const cursor = DocumentApp.getActiveDocument().getCursor(); + const surroundingText = cursor.getSurroundingText().getText(); + const surroundingTextOffset = cursor.getSurroundingTextOffset(); // If the cursor follows or preceds a non-space character, insert a space // between the character and the translation. Otherwise, just insert the // translation. if (surroundingTextOffset > 0) { - if (surroundingText.charAt(surroundingTextOffset - 1) != ' ') { + if (surroundingText.charAt(surroundingTextOffset - 1) !== ' ') { newText = ' ' + newText; } } if (surroundingTextOffset < surroundingText.length) { - if (surroundingText.charAt(surroundingTextOffset) != ' ') { + if (surroundingText.charAt(surroundingTextOffset) !== ' ') { newText += ' '; } } diff --git a/drive/activity-v2/quickstart.gs b/drive/activity-v2/quickstart.gs index a1b722b5c..662d2060b 100644 --- a/drive/activity-v2/quickstart.gs +++ b/drive/activity-v2/quickstart.gs @@ -15,46 +15,56 @@ */ // [START drive_activity_v2_quickstart] /** - * Lists activity for a Drive user. + * Lists 10 activity for a Drive user. + * @see https://developers.google.com/drive/activity/v2/reference/rest/v2/activity/query */ function listDriveActivity() { - var request = {pageSize: 10}; - var response = DriveActivity.Activity.query(request); - var activities = response.activities; - if (activities && activities.length > 0) { - Logger.log('Recent activity:'); - for (var i = 0; i < activities.length; i++) { - var activity = activities[i]; - var time = getTimeInfo(activity); - var action = getActionInfo(activity.primaryActionDetail); - var actors = activity.actors.map(getActorInfo); - var targets = activity.targets.map(getTargetInfo); - Logger.log( - '%s: %s, %s, %s', time, truncated(actors), action, - truncated(targets)); + const request = { + pageSize: 10 + // Use other parameter here if needed. + }; + try { + // Activity.query method is used Query past activity in Google Drive. + const response = DriveActivity.Activity.query(request); + const activities = response.activities; + if (!activities || activities.length === 0) { + console.log('No activity.'); + return; } - } else { - Logger.log('No activity.'); + console.log('Recent activity:'); + for (const activity of activities) { + // get time information of activity. + const time = getTimeInfo(activity); + // get the action details/information + const action = getActionInfo(activity.primaryActionDetail); + // get the actor's details of activity + const actors = activity.actors.map(getActorInfo); + // get target information of activity. + const targets = activity.targets.map(getTargetInfo); + // print the time,actor,action and targets of drive activity. + console.log('%s: %s, %s, %s', time, actors, action, targets); + } + } catch (err) { + // TODO (developer) - Handle error from drive activity API + console.log('Failed with an error %s', err.message); } } -/** Returns a string representation of the first elements in a list. */ -function truncated(array, opt_limit) { - var limit = opt_limit || 2; - var contents = array.slice(0, limit).join(', '); - var more = array.length > limit ? ', ...' : ''; - return '[' + contents + more + ']'; -} - -/** Returns the name of a set property in an object, or else "unknown". */ +/** + * @param {object} object + * @return {string} Returns the name of a set property in an object, or else "unknown". + */ function getOneOf(object) { - for (var key in object) { + for (const key in object) { return key; } return 'unknown'; } -/** Returns a time associated with an activity. */ +/** + * @param {object} activity Activity object. + * @return {string} Returns a time associated with an activity. + */ function getTimeInfo(activity) { if ('timestamp' in activity) { return activity.timestamp; @@ -65,42 +75,54 @@ function getTimeInfo(activity) { return 'unknown'; } -/** Returns the type of action. */ +/** + * @param {object} actionDetail The primary action details of the activity. + * @return {string} Returns the type of action. + */ function getActionInfo(actionDetail) { return getOneOf(actionDetail); } -/** Returns user information, or the type of user if not a known user. */ +/** + * @param {object} user The User object. + * @return {string} Returns user information, or the type of user if not a known user. + */ function getUserInfo(user) { if ('knownUser' in user) { - var knownUser = user.knownUser; - var isMe = knownUser.isCurrentUser || false; + const knownUser = user.knownUser; + const isMe = knownUser.isCurrentUser || false; return isMe ? 'people/me' : knownUser.personName; } return getOneOf(user); } -/** Returns actor information, or the type of actor if not a user. */ +/** + * @param {object} actor The Actor object. + * @return {string} Returns actor information, or the type of actor if not a user. + */ function getActorInfo(actor) { if ('user' in actor) { - return getUserInfo(actor.user) + return getUserInfo(actor.user); } return getOneOf(actor); } -/** Returns the type of a target and an associated title. */ +/** + * @param {object} target The Target object. + * @return {string} Returns the type of a target and an associated title. + */ function getTargetInfo(target) { if ('driveItem' in target) { - var title = target.driveItem.title || 'unknown'; + const title = target.driveItem.title || 'unknown'; return 'driveItem:"' + title + '"'; } if ('drive' in target) { - var title = target.drive.title || 'unknown'; + const title = target.drive.title || 'unknown'; return 'drive:"' + title + '"'; } if ('fileComment' in target) { - var parent = target.fileComment.parent || {}; - var title = parent.title || 'unknown'; + const parent = target.fileComment.parent || {}; + const title = parent.title || 'unknown'; return 'fileComment:"' + title + '"'; } return getOneOf(target) + ':unknown'; diff --git a/drive/activity/quickstart.gs b/drive/activity/quickstart.gs index a2ddd354a..c3fbf0fed 100644 --- a/drive/activity/quickstart.gs +++ b/drive/activity/quickstart.gs @@ -26,7 +26,7 @@ function listActivity() { var response = AppsActivity.Activities.list(optionalArgs); var activities = response.activities; if (activities && activities.length > 0) { - Logger.log('Recent activity:'); + console.log('Recent activity:'); for (i = 0; i < activities.length; i++) { var activity = activities[i]; var event = activity.combinedEvent; @@ -36,12 +36,12 @@ function listActivity() { continue; } else { var time = new Date(Number(event.eventTimeMillis)); - Logger.log('%s: %s, %s, %s (%s)', time, user.name, + console.log('%s: %s, %s, %s (%s)', time, user.name, event.primaryEventType, target.name, target.mimeType); } } } else { - Logger.log('No recent activity'); + console.log('No recent activity'); } } // [END drive_activity_quickstart] diff --git a/drive/quickstart/quickstart.gs b/drive/quickstart/quickstart.gs index 182ef16c8..47087ec83 100644 --- a/drive/quickstart/quickstart.gs +++ b/drive/quickstart/quickstart.gs @@ -18,13 +18,19 @@ * Lists the names and IDs of up to 10 files. */ function listFiles() { - var files = Drive.Files.list({ - fields: 'nextPageToken, items(id, title)', - maxResults: 10 - }).items; - for (var i = 0; i < files.length; i++) { - var file = files[i]; - Logger.log('%s (%s)', file.title, file.id); + try { + // Files.list method returns the list of files in drive. + const files = Drive.Files.list({ + fields: 'nextPageToken, items(id, title)', + maxResults: 10 + }).items; + // Print the title and id of files available in drive + for (const file of files) { + console.log('%s (%s)', file.title, file.id); + } + } catch (err) { + // TODO(developer)-Handle Files.list() exception + console.log('failed with error %s', err.message); } } // [END drive_quickstart] diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs b/forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs index 87c170caf..5be743b4e 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/FormsAPI.gs @@ -13,7 +13,7 @@ // limitations under the License. // Global constants. Customize as needed. -const formsAPIUrl = 'https://forms.googleapis.com/v1beta/forms/'; +const formsAPIUrl = 'https://forms.googleapis.com/v1/forms/'; const formId = ''; const topicName = 'projects/'; @@ -22,7 +22,7 @@ const topicName = 'projects/'; /** * Forms API Method: forms.create - * POST https://forms.googleapis.com/v1beta/forms + * POST https://forms.googleapis.com/v1/forms */ function create(title) { const accessToken = ScriptApp.getOAuthToken(); @@ -41,16 +41,16 @@ function create(title) { 'payload': jsonTitle }; - Logger.log('Forms API POST options was: ' + JSON.stringify(options)); + console.log('Forms API POST options was: ' + JSON.stringify(options)); let response = UrlFetchApp.fetch(formsAPIUrl, options); - Logger.log('Response from Forms API was: ' + JSON.stringify(response)); + console.log('Response from Forms API was: ' + JSON.stringify(response)); return ('' + response); } /** * Forms API Method: forms.get - * GET https://forms.googleapis.com/v1beta/forms/{formId}/responses/{responseId} + * GET https://forms.googleapis.com/v1/forms/{formId}/responses/{responseId} */ function get(formId) { const accessToken = ScriptApp.getOAuthToken(); @@ -65,10 +65,10 @@ function get(formId) { try { let response = UrlFetchApp.fetch(formsAPIUrl + formId, options); - Logger.log('Response from Forms API was: ' + response); + console.log('Response from Forms API was: ' + response); return ('' + response); } catch (e) { - Logger.log(JSON.stringify(e)); + console.log(JSON.stringify(e)); return ('Error:' + JSON.stringify(e) + '

Unable to find Form with formId:
' + formId); } @@ -76,7 +76,7 @@ function get(formId) { /** * Forms API Method: forms.batchUpdate - * POST https://forms.googleapis.com/v1beta/forms/{formId}:batchUpdate + * POST https://forms.googleapis.com/v1/forms/{formId}:batchUpdate */ function batchUpdate(formId) { const accessToken = ScriptApp.getOAuthToken(); @@ -105,13 +105,13 @@ function batchUpdate(formId) { let response = UrlFetchApp.fetch(formsAPIUrl + formId + ':batchUpdate', options); - Logger.log('Response code from API: ' + response.getResponseCode()); + console.log('Response code from API: ' + response.getResponseCode()); return (response.getResponseCode()); } /** * Forms API Method: forms.responses.get - * GET https://forms.googleapis.com/v1beta/forms/{formId}/responses/{responseId} + * GET https://forms.googleapis.com/v1/forms/{formId}/responses/{responseId} */ function responsesGet(formId, responseId) { const accessToken = ScriptApp.getOAuthToken(); @@ -127,17 +127,17 @@ function responsesGet(formId, responseId) { try { var response = UrlFetchApp.fetch(formsAPIUrl + formId + '/' + 'responses/' + responseId, options); - Logger.log('Response from Forms.responses.get was: ' + response); + console.log('Response from Forms.responses.get was: ' + response); return ('' + response); } catch (e) { - Logger.log(JSON.stringify(e)); + console.log(JSON.stringify(e)); return ('Error:' + JSON.stringify(e)) } } /** * Forms API Method: forms.responses.list - * GET https://forms.googleapis.com/v1beta/forms/{formId}/responses + * GET https://forms.googleapis.com/v1/forms/{formId}/responses */ function responsesList(formId) { const accessToken = ScriptApp.getOAuthToken(); @@ -153,17 +153,17 @@ function responsesList(formId) { try { var response = UrlFetchApp.fetch(formsAPIUrl + formId + '/' + 'responses', options); - Logger.log('Response from Forms.responses was: ' + response); + console.log('Response from Forms.responses was: ' + response); return ('' + response); } catch (e) { - Logger.log(JSON.stringify(e)); + console.log(JSON.stringify(e)); return ('Error:' + JSON.stringify(e)) } } /** * Forms API Method: forms.watches.create - * POST https://forms.googleapis.com/v1beta/forms/{formId}/watches + * POST https://forms.googleapis.com/v1/forms/{formId}/watches */ function createWatch(formId) { let accessToken = ScriptApp.getOAuthToken(); @@ -178,7 +178,7 @@ function createWatch(formId) { 'eventType': 'RESPONSES', } }; - Logger.log('myWatch is: ' + JSON.stringify(myWatch)); + console.log('myWatch is: ' + JSON.stringify(myWatch)); var options = { 'headers': { @@ -189,23 +189,23 @@ function createWatch(formId) { 'payload': JSON.stringify(myWatch), 'muteHttpExceptions': false, }; - Logger.log('options are: ' + JSON.stringify(options)); - Logger.log('formsAPIURL was: ' + formsAPIUrl); + console.log('options are: ' + JSON.stringify(options)); + console.log('formsAPIURL was: ' + formsAPIUrl); var response = UrlFetchApp.fetch(formsAPIUrl + formId + '/' + 'watches', options); - Logger.log(response); + console.log(response); return ('' + response); } /** * Forms API Method: forms.watches.delete - * DELETE https://forms.googleapis.com/v1beta/forms/{formId}/watches/{watchId} + * DELETE https://forms.googleapis.com/v1/forms/{formId}/watches/{watchId} */ function deleteWatch(formId, watchId) { let accessToken = ScriptApp.getOAuthToken(); - Logger.log('formsAPIUrl is: ' + formsAPIUrl); + console.log('formsAPIUrl is: ' + formsAPIUrl); var options = { 'headers': { @@ -219,10 +219,10 @@ function deleteWatch(formId, watchId) { try { var response = UrlFetchApp.fetch(formsAPIUrl + formId + '/' + 'watches/' + watchId, options); - Logger.log(response); + console.log(response); return ('' + response); } catch (e) { - Logger.log('API Error: ' + JSON.stringify(e)); + console.log('API Error: ' + JSON.stringify(e)); return (JSON.stringify(e)); } @@ -230,10 +230,10 @@ function deleteWatch(formId, watchId) { /** * Forms API Method: forms.watches.list - * GET https://forms.googleapis.com/v1beta/forms/{formId}/watches + * GET https://forms.googleapis.com/v1/forms/{formId}/watches */ function watchesList(formId) { - Logger.log('formId is: ' + formId); + console.log('formId is: ' + formId); let accessToken = ScriptApp.getOAuthToken(); var options = { 'headers': { @@ -245,17 +245,17 @@ function watchesList(formId) { try { var response = UrlFetchApp.fetch(formsAPIUrl + formId + '/' + 'watches', options); - Logger.log(response); + console.log(response); return ('' + response); } catch (e) { - Logger.log('API Error: ' + JSON.stringify(e)); + console.log('API Error: ' + JSON.stringify(e)); return (JSON.stringify(e)); } } /** * Forms API Method: forms.watches.renew - * POST https://forms.googleapis.com/v1beta/forms/{formId}/watches/{watchId}:renew + * POST https://forms.googleapis.com/v1/forms/{formId}/watches/{watchId}:renew */ function renewWatch(formId, watchId) { let accessToken = ScriptApp.getOAuthToken(); @@ -271,10 +271,10 @@ function renewWatch(formId, watchId) { try { var response = UrlFetchApp.fetch(formsAPIUrl + formId + '/' + 'watches/' + watchId + ':renew', options); - Logger.log(response); + console.log(response); return ('' + response); } catch (e) { - Logger.log('API Error: ' + JSON.stringify(e)); + console.log('API Error: ' + JSON.stringify(e)); return (JSON.stringify(e)); } -} \ No newline at end of file +} diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html b/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html index f53ff9a8a..8612e1b37 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/Main.html @@ -1,4 +1,20 @@ + + @@ -238,7 +254,7 @@ -

Forms API & Apps Script Testing +

Forms API logoForms API & Apps Script Testing Application - v.1

Form Id: @@ -257,7 +273,7 @@

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

(spec)
+ (spec)

@@ -275,7 +291,7 @@

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

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

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

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

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

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

(spec)
+ (spec)
Watch id:
diff --git a/forms-api/demos/AppsScriptFormsAPIWebApp/README.md b/forms-api/demos/AppsScriptFormsAPIWebApp/README.md index 1f0c7d6c5..71ae952cc 100644 --- a/forms-api/demos/AppsScriptFormsAPIWebApp/README.md +++ b/forms-api/demos/AppsScriptFormsAPIWebApp/README.md @@ -1,28 +1,32 @@ -## Google Forms API Apps Script Web app +# Google Forms API Apps Script web app -This example app demonstrates how to interact with the new Google Forms API directly from Apps Script using REST calls, and not the native Apps Script Forms Service service. +This solution demonstrates how to interact with the new Google Forms API directly from Apps Script using REST calls, not the native Apps Script Forms Service. -## General Setup +## General setup +* Enable the Forms API for your Google Cloud project -### Join the Forms API EAP! +## Web app setup -1. Required: Your account and GCP project must be allowlisted as per the EAP program in order to make requests to the API. -See: https://developers.google.com/forms/api +1. Create a new blank Apps Script project. +1. Click **Project Settings**, then: + * Check **Show "appsscript.json" manifest file in editor**. + * Enter the project number of the Google Cloud project that has the + Forms API enabled and click **Change project**. -Apps Script project customization +1. Copy the contents of the Apps Script, HTML and JSON files into your + Apps Script project. -1. After creating a new blank Apps Script project, click Project Settings and: - * Check 'Show "appsscript.json" manifest file in editor' - * Enter the Project Number of the project that was allowlisted for the Forms API and click 'Change project'. +1. Edit the `FormsAPI.gs` file to customize the constants. + * `formId`: Choose a `formId` from an existing form. + * `topicName`: Optional, if using watches (pub/sub). -1. Copy the contents of the Apps Script, HTML and JSON files into your Apps Script project. + Note: Further project setup is required to use the watch features. To + set up pub/sub topics, see + [Google Cloud Pubsub](https://cloud.google.com/pubsub/docs/building-pubsub-messaging-system) + for additional details. -1. Edit the FormsAPI.gs file and customize the constants. - * formId (Select one of your existing Forms id); - * topicName (Optional, if using Watches(pub/sub). Further project setup required) - * Note: To setup pub/sub topics, see: https://cloud.google.com/pubsub/docs/building-pubsub-messaging-system - -1. Deploy the project as a Web app, Authorize access and click on the deployment URL. +1. Deploy the project as a Web app, authorize access and click on the + deployment URL. diff --git a/forms-api/snippets/README.md b/forms-api/snippets/README.md index 17026ac9c..1a6f81ce5 100644 --- a/forms-api/snippets/README.md +++ b/forms-api/snippets/README.md @@ -1,4 +1,4 @@ -The Google Forms API is currently in Restricted Beta. To use the API and these -samples prior to General Availability, your Google Cloud project must be -allowlisted. To request that your project be allowlisted, complete the -[Early Adopter Program application](https://developers.google.com/forms/api/eap). +# Forms API + +To run, you must set up your GCP project to use the Forms API. +See: [Forms API](https://developers.google.com/forms/api/) diff --git a/forms-api/snippets/retrieve_all_responses.gs b/forms-api/snippets/retrieve_all_responses.gs index 688adecdd..78a55ceeb 100644 --- a/forms-api/snippets/retrieve_all_responses.gs +++ b/forms-api/snippets/retrieve_all_responses.gs @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ # [START forms_retrieve_all_responses] function callFormsAPI() { - Logger.log('Calling the Forms API!'); + console.log('Calling the Forms API!'); var formId = ''; // Get OAuth Token var OAuthToken = ScriptApp.getOAuthToken(); - Logger.log('OAuth token is: ' + OAuthToken); - var formsAPIUrl = 'https://forms.googleapis.com/v1beta/forms/' + formId + '/' + 'responses'; - Logger.log('formsAPIUrl is: ' + formsAPIUrl); + console.log('OAuth token is: ' + OAuthToken); + var formsAPIUrl = 'https://forms.googleapis.com/v1/forms/' + formId + '/' + 'responses'; + console.log('formsAPIUrl is: ' + formsAPIUrl); var options = { 'headers': { Authorization: 'Bearer ' + OAuthToken, @@ -30,6 +30,6 @@ 'method': 'get' }; var response = UrlFetchApp.fetch(formsAPIUrl, options); - Logger.log('Response from forms.responses was: ' + response); + console.log('Response from forms.responses was: ' + response); } -# [END forms_retrieve_all_responses] \ No newline at end of file +# [END forms_retrieve_all_responses] diff --git a/forms/notifications/about.html b/forms/notifications/about.html index b883c7b44..1c91e540f 100644 --- a/forms/notifications/about.html +++ b/forms/notifications/about.html @@ -1,3 +1,17 @@ + + @@ -23,4 +37,4 @@ - \ No newline at end of file + diff --git a/forms/notifications/authorizationEmail.html b/forms/notifications/authorizationEmail.html index 9bdb7a53c..a08efe05a 100644 --- a/forms/notifications/authorizationEmail.html +++ b/forms/notifications/authorizationEmail.html @@ -1,3 +1,17 @@ + +

The Google Forms add-on Form Notifications is set to run automatically whenever a form is submitted. The add-on was recently updated and it needs you @@ -18,4 +32,4 @@

This automatic message was sent to you via the Form Notifications add-on for Google Forms.

- \ No newline at end of file + diff --git a/forms/notifications/creatorNotification.html b/forms/notifications/creatorNotification.html index a1b9aa83b..2fbb8faee 100644 --- a/forms/notifications/creatorNotification.html +++ b/forms/notifications/creatorNotification.html @@ -1,3 +1,17 @@ + +

Form Notifications (a Google Forms add-on) has detected that the form titled has received diff --git a/forms/notifications/notification.gs b/forms/notifications/notification.gs index 233b6e309..8a8667e91 100644 --- a/forms/notifications/notification.gs +++ b/forms/notifications/notification.gs @@ -28,13 +28,14 @@ * A global constant String holding the title of the add-on. This is * used to identify the add-on in the notification emails. */ -var ADDON_TITLE = 'Form Notifications'; +const ADDON_TITLE = 'Form Notifications'; /** * A global constant 'notice' text to include with each email * notification. */ -var NOTICE = 'Form Notifications was created as an sample add-on, and is meant for' + +const NOTICE = 'Form Notifications was created as an sample add-on, and is' + + ' meant for' + 'demonstration purposes only. It should not be used for complex or important' + 'workflows. The number of notifications this add-on produces are limited by the' + 'owner\'s available email quota; it will not send email notifications if the' + @@ -50,11 +51,16 @@ var NOTICE = 'Form Notifications was created as an sample add-on, and is meant f * running in, inspect e.authMode. */ function onOpen(e) { - FormApp.getUi() - .createAddonMenu() - .addItem('Configure notifications', 'showSidebar') - .addItem('About', 'showAbout') - .addToUi(); + try { + FormApp.getUi() + .createAddonMenu() + .addItem('Configure notifications', 'showSidebar') + .addItem('About', 'showAbout') + .addToUi(); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } /** @@ -75,9 +81,14 @@ function onInstall(e) { * configuring the notifications this add-on will produce. */ function showSidebar() { - var ui = HtmlService.createHtmlOutputFromFile('sidebar') - .setTitle('Form Notifications'); - FormApp.getUi().showSidebar(ui); + try { + const ui = HtmlService.createHtmlOutputFromFile('sidebar') + .setTitle('Form Notifications'); + FormApp.getUi().showSidebar(ui); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } /** @@ -85,10 +96,15 @@ function showSidebar() { * this add-on. */ function showAbout() { - var ui = HtmlService.createHtmlOutputFromFile('about') - .setWidth(420) - .setHeight(270); - FormApp.getUi().showModalDialog(ui, 'About Form Notifications'); + try { + const ui = HtmlService.createHtmlOutputFromFile('about') + .setWidth(420) + .setHeight(270); + FormApp.getUi().showModalDialog(ui, 'About Form Notifications'); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } /** @@ -99,8 +115,13 @@ function showAbout() { * pairs to store. */ function saveSettings(settings) { - PropertiesService.getDocumentProperties().setProperties(settings); - adjustFormSubmitTrigger(); + try { + PropertiesService.getDocumentProperties().setProperties(settings); + adjustFormSubmitTrigger(); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } /** @@ -111,54 +132,65 @@ function saveSettings(settings) { * related data used to fill the configuration sidebar. */ function getSettings() { - var settings = PropertiesService.getDocumentProperties().getProperties(); + try { + const settings = PropertiesService.getDocumentProperties().getProperties(); - // Use a default email if the creator email hasn't been provided yet. - if (!settings.creatorEmail) { - settings.creatorEmail = Session.getEffectiveUser().getEmail(); - } + // Use a default email if the creator email hasn't been provided yet. + if (!settings.creatorEmail) { + settings.creatorEmail = Session.getEffectiveUser().getEmail(); + } - // Get text field items in the form and compile a list - // of their titles and IDs. - var form = FormApp.getActiveForm(); - var textItems = form.getItems(FormApp.ItemType.TEXT); - settings.textItems = []; - for (var i = 0; i < textItems.length; i++) { - settings.textItems.push({ - title: textItems[i].getTitle(), - id: textItems[i].getId() - }); + // Get text field items in the form and compile a list + // of their titles and IDs. + const form = FormApp.getActiveForm(); + const textItems = form.getItems(FormApp.ItemType.TEXT); + + settings.textItems = []; + for (let i = 0; i < textItems.length; i++) { + settings.textItems.push({ + title: textItems[i].getTitle(), + id: textItems[i].getId() + }); + } + return settings; + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } - return settings; } /** * Adjust the onFormSubmit trigger based on user's requests. */ function adjustFormSubmitTrigger() { - var form = FormApp.getActiveForm(); - var triggers = ScriptApp.getUserTriggers(form); - var settings = PropertiesService.getDocumentProperties(); - var triggerNeeded = - settings.getProperty('creatorNotify') == 'true' || - settings.getProperty('respondentNotify') == 'true'; + try { + const form = FormApp.getActiveForm(); + const triggers = ScriptApp.getUserTriggers(form); + const settings = PropertiesService.getDocumentProperties(); + const triggerNeeded = + settings.getProperty('creatorNotify') === 'true' || + settings.getProperty('respondentNotify') === 'true'; - // Create a new trigger if required; delete existing trigger - // if it is not needed. - var existingTrigger = null; - for (var i = 0; i < triggers.length; i++) { - if (triggers[i].getEventType() == ScriptApp.EventType.ON_FORM_SUBMIT) { - existingTrigger = triggers[i]; - break; + // Create a new trigger if required; delete existing trigger + // if it is not needed. + let existingTrigger = null; + for (let i = 0; i < triggers.length; i++) { + if (triggers[i].getEventType() === ScriptApp.EventType.ON_FORM_SUBMIT) { + existingTrigger = triggers[i]; + break; + } } - } - if (triggerNeeded && !existingTrigger) { - var trigger = ScriptApp.newTrigger('respondToFormSubmit') - .forForm(form) - .onFormSubmit() - .create(); - } else if (!triggerNeeded && existingTrigger) { - ScriptApp.deleteTrigger(existingTrigger); + if (triggerNeeded && !existingTrigger) { + const trigger = ScriptApp.newTrigger('respondToFormSubmit') + .forForm(form) + .onFormSubmit() + .create(); + } else if (!triggerNeeded && existingTrigger) { + ScriptApp.deleteTrigger(existingTrigger); + } + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } @@ -171,37 +203,42 @@ function adjustFormSubmitTrigger() { * https://developers.google.com/apps-script/understanding_events */ function respondToFormSubmit(e) { - var settings = PropertiesService.getDocumentProperties(); - var authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL); + try { + const settings = PropertiesService.getDocumentProperties(); + const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL); - // Check if the actions of the trigger require authorizations that have not - // been supplied yet -- if so, warn the active user via email (if possible). - // This check is required when using triggers with add-ons to maintain - // functional triggers. - if (authInfo.getAuthorizationStatus() == + // Check if the actions of the trigger require authorizations that have not + // been supplied yet -- if so, warn the active user via email (if possible). + // This check is required when using triggers with add-ons to maintain + // functional triggers. + if (authInfo.getAuthorizationStatus() === ScriptApp.AuthorizationStatus.REQUIRED) { - // Re-authorization is required. In this case, the user needs to be alerted - // that they need to reauthorize; the normal trigger action is not - // conducted, since authorization needs to be provided first. Send at - // most one 'Authorization Required' email a day, to avoid spamming users - // of the add-on. - sendReauthorizationRequest(); - } else { - // All required authorizations have been granted, so continue to respond to - // the trigger event. + // Re-authorization is required. In this case, the user needs to be alerted + // that they need to reauthorize; the normal trigger action is not + // conducted, since authorization needs to be provided first. Send at + // most one 'Authorization Required' email a day, to avoid spamming users + // of the add-on. + sendReauthorizationRequest(); + } else { + // All required authorizations have been granted, so continue to respond to + // the trigger event. - // Check if the form creator needs to be notified; if so, construct and - // send the notification. - if (settings.getProperty('creatorNotify') == 'true') { - sendCreatorNotification(); - } + // Check if the form creator needs to be notified; if so, construct and + // send the notification. + if (settings.getProperty('creatorNotify') === 'true') { + sendCreatorNotification(); + } - // Check if the form respondent needs to be notified; if so, construct and - // send the notification. Be sure to respect the remaining email quota. - if (settings.getProperty('respondentNotify') == 'true' && + // Check if the form respondent needs to be notified; if so, construct and + // send the notification. Be sure to respect the remaining email quota. + if (settings.getProperty('respondentNotify') === 'true' && MailApp.getRemainingDailyQuota() > 0) { - sendRespondentNotification(e.response); + sendRespondentNotification(e.response); + } } + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } @@ -213,25 +250,30 @@ function respondToFormSubmit(e) { * a day to prevent spamming the users of the add-on. */ function sendReauthorizationRequest() { - var settings = PropertiesService.getDocumentProperties(); - var authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL); - var lastAuthEmailDate = settings.getProperty('lastAuthEmailDate'); - var today = new Date().toDateString(); - if (lastAuthEmailDate != today) { - if (MailApp.getRemainingDailyQuota() > 0) { - var template = + try { + const settings = PropertiesService.getDocumentProperties(); + const authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL); + const lastAuthEmailDate = settings.getProperty('lastAuthEmailDate'); + const today = new Date().toDateString(); + if (lastAuthEmailDate !== today) { + if (MailApp.getRemainingDailyQuota() > 0) { + const template = HtmlService.createTemplateFromFile('authorizationEmail'); - template.url = authInfo.getAuthorizationUrl(); - template.notice = NOTICE; - var message = template.evaluate(); - MailApp.sendEmail(Session.getEffectiveUser().getEmail(), - 'Authorization Required', - message.getContent(), { - name: ADDON_TITLE, - htmlBody: message.getContent() - }); + template.url = authInfo.getAuthorizationUrl(); + template.notice = NOTICE; + const message = template.evaluate(); + MailApp.sendEmail(Session.getEffectiveUser().getEmail(), + 'Authorization Required', + message.getContent(), { + name: ADDON_TITLE, + htmlBody: message.getContent() + }); + } + settings.setProperty('lastAuthEmailDate', today); } - settings.setProperty('lastAuthEmailDate', today); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } @@ -241,35 +283,40 @@ function sendReauthorizationRequest() { * setting. */ function sendCreatorNotification() { - var form = FormApp.getActiveForm(); - var settings = PropertiesService.getDocumentProperties(); - var responseStep = settings.getProperty('responseStep'); - responseStep = responseStep ? parseInt(responseStep) : 10; + try { + const form = FormApp.getActiveForm(); + const settings = PropertiesService.getDocumentProperties(); + let responseStep = settings.getProperty('responseStep'); + responseStep = responseStep ? parseInt(responseStep) : 10; - // If the total number of form responses is an even multiple of the - // response step setting, send a notification email(s) to the form - // creator(s). For example, if the response step is 10, notifications - // will be sent when there are 10, 20, 30, etc. total form responses - // received. - if (form.getResponses().length % responseStep == 0) { - var addresses = settings.getProperty('creatorEmail').split(','); - if (MailApp.getRemainingDailyQuota() > addresses.length) { - var template = + // If the total number of form responses is an even multiple of the + // response step setting, send a notification email(s) to the form + // creator(s). For example, if the response step is 10, notifications + // will be sent when there are 10, 20, 30, etc. total form responses + // received. + if (form.getResponses().length % responseStep === 0) { + const addresses = settings.getProperty('creatorEmail').split(','); + if (MailApp.getRemainingDailyQuota() > addresses.length) { + const template = HtmlService.createTemplateFromFile('creatorNotification'); - template.summary = form.getSummaryUrl(); - template.responses = form.getResponses().length; - template.title = form.getTitle(); - template.responseStep = responseStep; - template.formUrl = form.getEditUrl(); - template.notice = NOTICE; - var message = template.evaluate(); - MailApp.sendEmail(settings.getProperty('creatorEmail'), - form.getTitle() + ': Form submissions detected', - message.getContent(), { - name: ADDON_TITLE, - htmlBody: message.getContent() - }); + template.summary = form.getSummaryUrl(); + template.responses = form.getResponses().length; + template.title = form.getTitle(); + template.responseStep = responseStep; + template.formUrl = form.getEditUrl(); + template.notice = NOTICE; + const message = template.evaluate(); + MailApp.sendEmail(settings.getProperty('creatorEmail'), + form.getTitle() + ': Form submissions detected', + message.getContent(), { + name: ADDON_TITLE, + htmlBody: message.getContent() + }); + } } + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } @@ -280,24 +327,29 @@ function sendCreatorNotification() { * that triggered this notification */ function sendRespondentNotification(response) { - var form = FormApp.getActiveForm(); - var settings = PropertiesService.getDocumentProperties(); - var emailId = settings.getProperty('respondentEmailItemId'); - var emailItem = form.getItemById(parseInt(emailId)); - var respondentEmail = response.getResponseForItem(emailItem) - .getResponse(); - if (respondentEmail) { - var template = + try { + const form = FormApp.getActiveForm(); + const settings = PropertiesService.getDocumentProperties(); + const emailId = settings.getProperty('respondentEmailItemId'); + const emailItem = form.getItemById(parseInt(emailId)); + const respondentEmail = response.getResponseForItem(emailItem) + .getResponse(); + if (respondentEmail) { + const template = HtmlService.createTemplateFromFile('respondentNotification'); - template.paragraphs = settings.getProperty('responseText').split('\n'); - template.notice = NOTICE; - var message = template.evaluate(); - MailApp.sendEmail(respondentEmail, - settings.getProperty('responseSubject'), - message.getContent(), { - name: form.getTitle(), + template.paragraphs = settings.getProperty('responseText').split('\n'); + template.notice = NOTICE; + const message = template.evaluate(); + MailApp.sendEmail(respondentEmail, + settings.getProperty('responseSubject'), + message.getContent(), { + name: form.getTitle(), htmlBody: message.getContent() - }); + }); + } + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); } } // [END apps_script_forms_notifications_quickstart] diff --git a/forms/notifications/respondentNotification.html b/forms/notifications/respondentNotification.html index 413849c36..0cb215589 100644 --- a/forms/notifications/respondentNotification.html +++ b/forms/notifications/respondentNotification.html @@ -1,3 +1,17 @@ + +

@@ -8,4 +22,4 @@

This automatic message was sent to you via the Form Notifications add-on for Google Forms.

- \ No newline at end of file + diff --git a/forms/notifications/sidebar.html b/forms/notifications/sidebar.html index 6b23afa26..78ae5acb4 100644 --- a/forms/notifications/sidebar.html +++ b/forms/notifications/sidebar.html @@ -1,3 +1,17 @@ + + @@ -257,4 +271,4 @@ - \ No newline at end of file + diff --git a/gmail/README.md b/gmail/README.md index 0bf3903d7..9c57176f0 100644 --- a/gmail/README.md +++ b/gmail/README.md @@ -9,3 +9,7 @@ This tutorial shows an easy way to collect information from different users in a ## [Sending Emails](https://developers.google.com/apps-script/articles/sending_emails) This tutorial shows how to use Spreadsheet data to send emails to different people. + +## [Inline Image](inlineimage/inlineimage.gs) + +This example shows how to send an HTML email that includes an inline image attachment. diff --git a/gmail/inlineimage/inlineimage.gs b/gmail/inlineimage/inlineimage.gs new file mode 100644 index 000000000..0ca113c6f --- /dev/null +++ b/gmail/inlineimage/inlineimage.gs @@ -0,0 +1,36 @@ + +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function sendEmailToMyself() { + // You can use this method to test the welcome email. + sendEmailWithInlineImage(Session.getActiveUser().getEmail()); +} + +function sendEmailWithInlineImage(toAddress) { + const options = {}; + const imageName = 'cat_emoji'; + // The URL "cid:cat_emoji" means that the inline attachment named "cat_emoji" would be used. + options['htmlBody'] = 'Welcome! Cat Emoji'; + options['inlineImages'] = {[imageName]: Utilities.newBlob(getImageBinary(), 'image/png', imageName)}; + GmailApp.sendEmail(toAddress, 'Welcome!', 'Welcome!', options); +} + +function getImageBinary() { + // Cat Face Emoji from https://github.com/googlefonts/noto-emoji/blob/main/png/32/emoji_u1f431.png, Base64 encoded. + const catPngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA+BJREFUeNrsV01ME1EQnpaltPK3iAT0oAsxMSYmlIOaGBO2etCDMTVq8CYl3jRBvehBI0YPehIPxhvFkxo1gHpQE9P15+KtROJFI6sxhEKwW6FY27o6s/vabpd9tPUn8eAkj8e+nTffzDez814B/kX5oXT4/7A9GceAk12Xg/IkThIOFUfIJb9XfgNYxCmMI8iWNLTXZNVx2zYEGTiwOUKe/wZ4xAJOIhIbXAdQuo2/dacB6i8gP7X0dA43hSsEJ+eJST9UtZv2fIdyr5d1wMyRsMkcBSd6y2WCRT5C0RrgZKN6K4C3x1FfcFw1QSFvYP4sWk4SE1F426gyRyVo/mbqzdUgiK6BoEcBkv35yAsBcEUoGRIZ8uwA+PYAQHeNgPsHzUv1MjYyfT0lwZ1S4Cz6DNNG8LoMX8+XLfz/9XZhXwUOaMUJTQJ8OYnRvSqs1VpAyCEaTu++T5p7aa7AgXGTzlfmRsq93cCKbHHE1qjt7FAAORvZidyqwm1E7BuNlORtoRoNou8iK0INi1DQ+emhWqBhpqQdm5HKK8JoWTVhB8o5wv02k+bA7moFX5ICfKmV7cQfErdDBys6MNTpLAzeS4AynirLoLagQ+jyLOw7G3PaI9lbsT0FQfuOwMkpwwmS8KkW6N1Vv6wDJ67NwfDjebPaxr9C/L5kV5GthWj/Cjrt2jlwkrGXiyUZUGPZIjYcWOgeGhrqxSHnGaAFKqVE5rq/sXqOa1ysK923pFahSF/u9Oaf3yS2wJsvm/2szhRrCuhBfjGzV6xyZ6Gr6Tm0eT8YLwYON8HAjbhhrH9/Y97Y+eE4KFEzOqlNgCvHmg2dK0ebjci1pI76DXn9d/OdkNa9sGdNOOrbOXGC1wciC1lRTus1sNIT40ZJwIHjU0VrkcE1IPu93D2f063wMbkB4ukWTU1uJAbUvr6+kAvpP44PhyllDdWfJcGVkbauepJngCehS7Mw/MgsNtnvg5GLrcumiBjwuFPgqUopq3dHAjwG6Mw/xzPStEeF8OkWCG6vNWhuP/TRmOMPJQM8x8zkrbVGWqzyNHYQ6oQELGbrFWTgKhGJDGh5LWLi5ofFbtEzC6sxej/WwZICQ6P7zsSMiNXpjAFO0nXkE/jX18DoyyTOniXgJDtb78B0ah281raNsV5DTU9xMXCR9QAl1HExbL82WT8rKr7ou7Tx3H+gASOvgqt3E8Y7azHyyge7baDUrbi8A+nXpAsdiC57IWHX8PN/ATxkB3dkoNyCrEA0Bj5a0ZUMN5ADAfsFokLgQXb+j3JxKrjnB9nvBpFTpLmjnM77ZzhG2fH+X/5t+SnAAE+HjvApIyOGAAAAAElFTkSuQmCC'; + return Utilities.base64Decode(catPngBase64); +} diff --git a/gmail/mailmerge/mailmerge.gs b/gmail/mailmerge/mailmerge.gs deleted file mode 100644 index 30cc15c0c..000000000 --- a/gmail/mailmerge/mailmerge.gs +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// [START apps_script_gmail_mail_merge] -/** - * Iterates row by row in the input range and returns an array of objects. - * Each object contains all the data for a given row, indexed by its normalized column name. - * @param {Sheet} sheet The sheet object that contains the data to be processed - * @param {Range} range The exact range of cells where the data is stored - * @param {number} columnHeadersRowIndex Specifies the row number where the column names are stored. - * This argument is optional and it defaults to the row immediately above range; - * @return {object[]} An array of objects. - */ -function getRowsData(sheet, range, columnHeadersRowIndex) { - columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1; - var numColumns = range.getEndColumn() - range.getColumn() + 1; - var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns); - var headers = headersRange.getValues()[0]; - return getObjects(range.getValues(), normalizeHeaders(headers)); -} - -/** - * For every row of data in data, generates an object that contains the data. Names of - * object fields are defined in keys. - * @param {object} data JavaScript 2d array - * @param {object} keys Array of Strings that define the property names for the objects to create - * @return {object[]} A list of objects. - */ -function getObjects(data, keys) { - var objects = []; - for (var i = 0; i < data.length; ++i) { - var object = {}; - var hasData = false; - for (var j = 0; j < data[i].length; ++j) { - var cellData = data[i][j]; - if (isCellEmpty(cellData)) { - continue; - } - object[keys[j]] = cellData; - hasData = true; - } - if (hasData) { - objects.push(object); - } - } - return objects; -} - -/** - * Returns an array of normalized Strings. - * @param {string[]} headers Array of strings to normalize - * @return {string[]} An array of normalized strings. - */ -function normalizeHeaders(headers) { - var keys = []; - for (var i = 0; i < headers.length; ++i) { - var key = normalizeHeader(headers[i]); - if (key.length > 0) { - keys.push(key); - } - } - return keys; -} - -/** - * Normalizes a string, by removing all alphanumeric characters and using mixed case - * to separate words. The output will always start with a lower case letter. - * This function is designed to produce JavaScript object property names. - * @param {string} header The header to normalize. - * @return {string} The normalized header. - * @example "First Name" -> "firstName" - * @example "Market Cap (millions) -> "marketCapMillions - * @example "1 number at the beginning is ignored" -> "numberAtTheBeginningIsIgnored" - */ -function normalizeHeader(header) { - var key = ''; - var upperCase = false; - for (var i = 0; i < header.length; ++i) { - var letter = header[i]; - if (letter == ' ' && key.length > 0) { - upperCase = true; - continue; - } - if (!isAlnum(letter)) { - continue; - } - if (key.length == 0 && isDigit(letter)) { - continue; // first character must be a letter - } - if (upperCase) { - upperCase = false; - key += letter.toUpperCase(); - } else { - key += letter.toLowerCase(); - } - } - return key; -} - -/** - * Returns true if the cell where cellData was read from is empty. - * @param {string} cellData Cell data - * @return {boolean} True if the cell is empty. - */ -function isCellEmpty(cellData) { - return typeof(cellData) == 'string' && cellData == ''; -} - -/** - * Returns true if the character char is alphabetical, false otherwise. - * @param {string} char The character. - * @return {boolean} True if the char is a number. - */ -function isAlnum(char) { - return char >= 'A' && char <= 'Z' || - char >= 'a' && char <= 'z' || - isDigit(char); -} - -/** - * Returns true if the character char is a digit, false otherwise. - * @param {string} char The character. - * @return {boolean} True if the char is a digit. - */ -function isDigit(char) { - return char >= '0' && char <= '9'; -} - -/** - * Sends emails from spreadsheet rows. - */ -function sendEmails() { - var ss = SpreadsheetApp.getActiveSpreadsheet(); - var dataSheet = ss.getSheets()[0]; - // [START apps_script_gmail_email_data_range] - var dataRange = dataSheet.getRange(2, 1, dataSheet.getMaxRows() - 1, 4); - // [END apps_script_gmail_email_data_range] - - // [START apps_script_gmail_email_template] - var templateSheet = ss.getSheets()[1]; - var emailTemplate = templateSheet.getRange('A1').getValue(); - // [END apps_script_gmail_email_template] - - // [START apps_script_gmail_email_objects] - // Create one JavaScript object per row of data. - var objects = getRowsData(dataSheet, dataRange); - // [END apps_script_gmail_email_objects] - - // For every row object, create a personalized email from a template and send - // it to the appropriate person. - for (var i = 0; i < objects.length; ++i) { - // Get a row object - var rowData = objects[i]; - - // [START apps_script_gmail_email_text] - // Generate a personalized email. - // Given a template string, replace markers (for instance ${"First Name"}) with - // the corresponding value in a row object (for instance rowData.firstName). - var emailText = fillInTemplateFromObject(emailTemplate, rowData); - // [END apps_script_gmail_email_text] - var emailSubject = 'Tutorial: Simple Mail Merge'; - - // [START apps_script_gmail_send_email] - MailApp.sendEmail(rowData.emailAddress, emailSubject, emailText); - // [END apps_script_gmail_send_email] - } -} - -/** - * Replaces markers in a template string with values define in a JavaScript data object. - * @param {string} template Contains markers, for instance ${"Column name"} - * @param {object} data values to that will replace markers. - * For instance data.columnName will replace marker ${"Column name"} - * @return {string} A string without markers. If no data is found to replace a marker, - * it is simply removed. - */ -function fillInTemplateFromObject(template, data) { - var email = template; - // [START apps_script_gmail_template_vars] - // Search for all the variables to be replaced, for instance ${"Column name"} - var templateVars = template.match(/\$\{\"[^\"]+\"\}/g); - // [END apps_script_gmail_template_vars] - - // Replace variables from the template with the actual values from the data object. - // If no value is available, replace with the empty string. - for (var i = 0; templateVars && i < templateVars.length; ++i) { - // normalizeHeader ignores ${"} so we can call it directly here. - // [START apps_script_gmail_template_variable_data] - var variableData = data[normalizeHeader(templateVars[i])]; - // [END apps_script_gmail_template_variable_data] - // [START apps_script_gmail_template_replace] - email = email.replace(templateVars[i], variableData || ''); - // [END apps_script_gmail_template_replace] - } - - return email; -} -// [END apps_script_gmail_mail_merge] diff --git a/gmail/markup/Code.gs b/gmail/markup/Code.gs index 61e2048d0..d4f65f6ed 100644 --- a/gmail/markup/Code.gs +++ b/gmail/markup/Code.gs @@ -1,14 +1,19 @@ -// [START apps_script_gmail_markup] +// [START gmail_send_email_with_markup] /** - * Tests the schema. + * Send an email with schemas in order to test email markup. */ function testSchemas() { - var htmlBody = HtmlService.createHtmlOutputFromFile('mail_template').getContent(); + try { + const htmlBody = HtmlService.createHtmlOutputFromFile('mail_template').getContent(); - MailApp.sendEmail({ - to: Session.getActiveUser().getEmail(), - subject: 'Test Email markup - ' + new Date(), - htmlBody: htmlBody, - }); + MailApp.sendEmail({ + to: Session.getActiveUser().getEmail(), + subject: 'Test Email markup - ' + new Date(), + htmlBody: htmlBody + }); + } catch (err) { + console.log(err.message); + } } -// [END apps_script_gmail_markup] +// [END gmail_send_email_with_markup] + diff --git a/gmail/markup/mail_template.html b/gmail/markup/mail_template.html index a0f9782f7..f339fbc31 100644 --- a/gmail/markup/mail_template.html +++ b/gmail/markup/mail_template.html @@ -1,13 +1,29 @@ + + @@ -17,4 +33,4 @@ This a test for a Go-To action in Gmail.

- \ No newline at end of file + diff --git a/gmail/quickstart/quickstart.gs b/gmail/quickstart/quickstart.gs index 97f28a1de..51ca6a28c 100644 --- a/gmail/quickstart/quickstart.gs +++ b/gmail/quickstart/quickstart.gs @@ -1,5 +1,5 @@ /** - * Copyright Google LLC + * Copyright Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + // [START gmail_quickstart] /** - * Lists the labels in the user's account. + * Lists all labels in the user's mailbox + * @see https://developers.google.com/gmail/api/reference/rest/v1/users.labels/list */ function listLabels() { - var response = Gmail.Users.Labels.list('me'); - if (response.labels.length == 0) { - Logger.log('No labels found.'); - } else { - Logger.log('Labels:'); - for (var i = 0; i < response.labels.length; i++) { - var label = response.labels[i]; - Logger.log('- %s', label.name); + try { + // Gmail.Users.Labels.list() API returns the list of all Labels in user's mailbox + const response = Gmail.Users.Labels.list('me'); + if (!response || response.labels.length === 0) { + // TODO (developer) - No labels are returned from the response + console.log('No labels found.'); + return; } + // Print the Labels that are available. + console.log('Labels:'); + for (const label of response.labels ) { + console.log('- %s', label.name); + } + } catch (err) { + // TODO (developer) - Handle exception on Labels.list() API + console.log('Labels.list() API failed with error %s', err.toString()); } } // [END gmail_quickstart] diff --git a/gmail/sendingEmails/sendingEmails.gs b/gmail/sendingEmails/sendingEmails.gs index 219e16ba8..f8e8e0067 100644 --- a/gmail/sendingEmails/sendingEmails.gs +++ b/gmail/sendingEmails/sendingEmails.gs @@ -14,56 +14,57 @@ * limitations under the License. */ -// [START apps_script_gmail_send_emails] +// [START gmail_send_emails] /** * Sends emails with data from the current spreadsheet. */ function sendEmails() { - var sheet = SpreadsheetApp.getActiveSheet(); - var startRow = 2; // First row of data to process - var numRows = 2; // Number of rows to process - // Fetch the range of cells A2:B3 - var dataRange = sheet.getRange(startRow, 1, numRows, 2); - // Fetch values for each row in the Range. - var data = dataRange.getValues(); - for (var i in data) { - var row = data[i]; - var emailAddress = row[0]; // First column - var message = row[1]; // Second column - var subject = 'Sending emails from a Spreadsheet'; - MailApp.sendEmail(emailAddress, subject, message); + try { + const sheet = SpreadsheetApp.getActiveSheet(); // Get the active sheet in spreadsheet + const startRow = 2; // First row of data to process + const numRows = 2; // Number of rows to process + const dataRange = sheet.getRange(startRow, 1, numRows, 2); // Fetch the range of cells A2:B3 + const data = dataRange.getValues(); // Fetch values for each row in the Range. + for (const row of data) { + const emailAddress = row[0]; // First column + const message = row[1]; // Second column + const subject = 'Sending emails from a Spreadsheet'; + MailApp.sendEmail(emailAddress, subject, message); // Send emails to emailAddresses which are presents in First column + } + } catch (err) { + console.log(err); } } -// [END apps_script_gmail_send_emails] - -// [START apps_script_gmail_send_emails_2] -// This constant is written in column C for rows for which an email -// has been sent successfully. -var EMAIL_SENT = 'EMAIL_SENT'; +// [END gmail_send_emails] +// [START gmail_send_non_duplicate_emails] /** * Sends non-duplicate emails with data from the current spreadsheet. */ -function sendEmails2() { - var sheet = SpreadsheetApp.getActiveSheet(); - var startRow = 2; // First row of data to process - var numRows = 2; // Number of rows to process - // Fetch the range of cells A2:B3 - var dataRange = sheet.getRange(startRow, 1, numRows, 3); - // Fetch values for each row in the Range. - var data = dataRange.getValues(); - for (var i = 0; i < data.length; ++i) { - var row = data[i]; - var emailAddress = row[0]; // First column - var message = row[1]; // Second column - var emailSent = row[2]; // Third column - if (emailSent !== EMAIL_SENT) { // Prevents sending duplicates - var subject = 'Sending emails from a Spreadsheet'; - MailApp.sendEmail(emailAddress, subject, message); +function sendNonDuplicateEmails() { + const EMAIL_SENT = 'email sent'; //This constant is used to write the message in Column C of Sheet + try { + const sheet = SpreadsheetApp.getActiveSheet(); // Get the active sheet in spreadsheet + const startRow = 2; // First row of data to process + const numRows = 2; // Number of rows to process + const dataRange = sheet.getRange(startRow, 1, numRows, 3); // Fetch the range of cells A2:B3 + const data = dataRange.getValues(); // Fetch values for each row in the Range. + for (let i = 0; i < data.length; ++i) { + const row = data[i]; + const emailAddress = row[0]; // First column + const message = row[1]; // Second column + const emailSent = row[2]; // Third column + if (emailSent === EMAIL_SENT) { + console.log('Email already sent'); + return; + } + const subject = 'Sending emails from a Spreadsheet'; + MailApp.sendEmail(emailAddress, subject, message);// Send emails to emailAddresses which are presents in First column sheet.getRange(startRow + i, 3).setValue(EMAIL_SENT); - // Make sure the cell is updated right away in case the script is interrupted - SpreadsheetApp.flush(); + SpreadsheetApp.flush(); // Make sure the cell is updated right away in case the script is interrupted } + } catch (err) { + console.log(err); } } -// [END apps_script_gmail_send_emails_2] +// [END gmail_send_non_duplicate_emails] diff --git a/mashups/sheets2chat.gs b/mashups/sheets2chat.gs index e18df38a6..bf888b3ea 100644 --- a/mashups/sheets2chat.gs +++ b/mashups/sheets2chat.gs @@ -20,7 +20,7 @@ function sendChatMessageOnEdit(e) { var changeMessage; if (oldValue && value) { changeMessage = Utilities.formatString('changed from "%s" to "%s"', - oldValue, value); + oldValue, value); } else if (value) { changeMessage = Utilities.formatString('set to "%s"', value); } else { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..7f3391426 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1642 @@ +{ + "name": "gsuite-apps-script-samples", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "gsuite-apps-script-samples", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "eslint": "8.18.0", + "eslint-config-google": "0.14.0", + "eslint-plugin-googleappsscript": "1.0.4" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz", + "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz", + "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-googleappsscript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz", + "integrity": "sha512-Z6w1EMw0z0VOUvI0PFquigNSkYTgB+pVb2O2KYedDHnllwrgjjNBOLxsqPmtwmjOtFqCu7TuL0hJQwxVnp5OzQ==", + "dev": true, + "dependencies": { + "requireindex": "~1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "dependencies": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI=", + "dev": true, + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + }, + "dependencies": { + "@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz", + "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz", + "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + } + }, + "eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true, + "requires": {} + }, + "eslint-plugin-googleappsscript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz", + "integrity": "sha512-Z6w1EMw0z0VOUvI0PFquigNSkYTgB+pVb2O2KYedDHnllwrgjjNBOLxsqPmtwmjOtFqCu7TuL0hJQwxVnp5OzQ==", + "dev": true, + "requires": { + "requireindex": "~1.1.0" + } + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "requires": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha1-5UBLgVV+91225JxacgBIk/4D4WI=", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json index bfb894a2d..e8a586000 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "author": "Grant Timmerman", "keywords": [ - "G Suite", + "Google Workspace", "Apps Script", "Calendar", "Drive", @@ -15,12 +15,13 @@ "API" ], "devDependencies": { - "eslint": "^4.17.0", - "eslint-config-google": "^0.9.1", - "eslint-plugin-async-await": "0.0.0" + "eslint": "8.18.0", + "eslint-config-google": "0.14.0", + "eslint-plugin-googleappsscript": "1.0.4" }, "scripts": { - "lint": "./node_modules/.bin/eslint **/*.gs --fix --quiet" + "lint": "./node_modules/.bin/eslint **/*.gs", + "fix": "./node_modules/.bin/eslint **/*.gs --fix --quiet" }, "eslintIgnore": [ "**/node_modules/**" diff --git a/people/quickstart/quickstart.gs b/people/quickstart/quickstart.gs index 4c5844f55..749ab63f6 100644 --- a/people/quickstart/quickstart.gs +++ b/people/quickstart/quickstart.gs @@ -18,16 +18,27 @@ * Print the display name if available for 10 connections. */ function listConnectionNames() { - var connections = People.People.Connections.list('people/me', { - pageSize: 10, - personFields: 'names,emailAddresses' - }); - connections.connections.forEach(function(person) { - if (person.names && person.names.length > 0) { - Logger.log(person.names[0].displayName); - } else { - Logger.log('No display name found for connection.'); - } - }); + try { + /** + * List the 10 connections/contacts of user + * @see https://developers.google.com/people/api/rest/v1/people.connections/list + */ + const connections = People.People.Connections.list('people/me', { + pageSize: 10, + personFields: 'names,emailAddresses' + // use other query parameter here if needed. + }); + connections.connections.forEach((person) => { + // if contacts/connections is available, print the name of person. + if (person.names && person.names.length === 0) { + console.log('No display name found for connection.'); + return; + } + console.log(person.names[0].displayName); + }); + } catch (err) { + // TODO (developer) - Handle exception from People API + console.log('Failed with error %s', err.message); + } } // [END people_quickstart] diff --git a/picker/README.md b/picker/README.md index edba6c4cd..d8dfd7b26 100644 --- a/picker/README.md +++ b/picker/README.md @@ -5,7 +5,7 @@ allows the user to select a file from their Drive. It does so by loading [Google Picker](https://developers.google.com/picker/), a standard G Suite client-side API for this purpose. More information is available in the Apps Script guide -[Dialogs and Sidebars in G Suite Documents](https://developers.google.com/apps-script/guides/dialogs#file-open_dialogs). +[Dialogs and Sidebars in Google Workspace Documents](https://developers.google.com/apps-script/guides/dialogs#file-open_dialogs). Note that this sample expects to be [bound](https://developers.google.com/apps-script/guides/bound) diff --git a/picker/code.gs b/picker/code.gs index 5e23b2c46..e373d62d9 100644 --- a/picker/code.gs +++ b/picker/code.gs @@ -19,9 +19,14 @@ * Creates a custom menu in Google Sheets when the spreadsheet opens. */ function onOpen() { - SpreadsheetApp.getUi().createMenu('Picker') - .addItem('Start', 'showPicker') - .addToUi(); + try { + SpreadsheetApp.getUi().createMenu('Picker') + .addItem('Start', 'showPicker') + .addToUi(); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } /** @@ -29,11 +34,16 @@ function onOpen() { * JavaScript code for the Google Picker API. */ function showPicker() { - var html = HtmlService.createHtmlOutputFromFile('dialog.html') - .setWidth(600) - .setHeight(425) - .setSandboxMode(HtmlService.SandboxMode.IFRAME); - SpreadsheetApp.getUi().showModalDialog(html, 'Select a file'); + try { + const html = HtmlService.createHtmlOutputFromFile('dialog.html') + .setWidth(600) + .setHeight(425) + .setSandboxMode(HtmlService.SandboxMode.IFRAME); + SpreadsheetApp.getUi().showModalDialog(html, 'Select a file'); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } /** @@ -47,7 +57,12 @@ function showPicker() { * @return {string} The user's OAuth 2.0 access token. */ function getOAuthToken() { - DriveApp.getRootFolder(); - return ScriptApp.getOAuthToken(); + try { + DriveApp.getRootFolder(); + return ScriptApp.getOAuthToken(); + } catch (e) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', e.error); + } } // [END picker_code] diff --git a/picker/dialog.html b/picker/dialog.html index c2b023d5c..4ba2aaa7d 100644 --- a/picker/dialog.html +++ b/picker/dialog.html @@ -109,8 +109,8 @@
- -

+ +

diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..f45d8f110 --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} diff --git a/service/jdbc.gs b/service/jdbc.gs index 5ec908d5a..28587741c 100644 --- a/service/jdbc.gs +++ b/service/jdbc.gs @@ -1,136 +1,161 @@ -// [START apps_script_jdbc_create] -// Replace the variables in this block with real values. -// You can find the "Instance connection name" in the Google Cloud -// Platform Console, on the instance Overview page. -var connectionName = 'Instance_connection_name'; -var rootPwd = 'root_password'; -var user = 'user_name'; -var userPwd = 'user_password'; -var db = 'database_name'; - -var root = 'root'; -var instanceUrl = 'jdbc:google:mysql://' + connectionName; -var dbUrl = instanceUrl + '/' + db; +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Replace the variables in this block with real values. + * You can find the "Instance connection name" in the Google Cloud + * Platform Console, on the instance Overview page. + */ +const connectionName = 'Instance_connection_name'; +const rootPwd = 'root_password'; +const user = 'user_name'; +const userPwd = 'user_password'; +const db = 'database_name'; + +const root = 'root'; +const instanceUrl = 'jdbc:google:mysql://' + connectionName; +const dbUrl = instanceUrl + '/' + db; +// [START apps_script_jdbc_create] /** * Create a new database within a Cloud SQL instance. */ function createDatabase() { - var conn = Jdbc.getCloudSqlConnection(instanceUrl, root, rootPwd); - conn.createStatement().execute('CREATE DATABASE ' + db); + try { + const conn = Jdbc.getCloudSqlConnection(instanceUrl, root, rootPwd); + conn.createStatement().execute('CREATE DATABASE ' + db); + } catch (err) { + // TODO(developer) - Handle exception from the API + console.log('Failed with an error %s', err.message); + } } /** * Create a new user for your database with full privileges. */ function createUser() { - var conn = Jdbc.getCloudSqlConnection(dbUrl, root, rootPwd); - - var stmt = conn.prepareStatement('CREATE USER ? IDENTIFIED BY ?'); - stmt.setString(1, user); - stmt.setString(2, userPwd); - stmt.execute(); - - conn.createStatement().execute('GRANT ALL ON `%`.* TO ' + user); + try { + const conn = Jdbc.getCloudSqlConnection(dbUrl, root, rootPwd); + + const stmt = conn.prepareStatement('CREATE USER ? IDENTIFIED BY ?'); + stmt.setString(1, user); + stmt.setString(2, userPwd); + stmt.execute(); + + conn.createStatement().execute('GRANT ALL ON `%`.* TO ' + user); + } catch (err) { + // TODO(developer) - Handle exception from the API + console.log('Failed with an error %s', err.message); + } } /** * Create a new table in the database. */ function createTable() { - var conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd); - conn.createStatement().execute('CREATE TABLE entries ' - + '(guestName VARCHAR(255), content VARCHAR(255), ' - + 'entryID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(entryID));'); + try { + const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd); + conn.createStatement().execute('CREATE TABLE entries ' + + '(guestName VARCHAR(255), content VARCHAR(255), ' + + 'entryID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(entryID));'); + } catch (err) { + // TODO(developer) - Handle exception from the API + console.log('Failed with an error %s', err.message); + } } // [END apps_script_jdbc_create] // [START apps_script_jdbc_write] -// Replace the variables in this block with real values. -// You can find the "Instance connection name" in the Google Cloud -// Platform Console, on the instance Overview page. -var connectionName = 'Instance_connection_name'; -var user = 'user_name'; -var userPwd = 'user_password'; -var db = 'database_name'; - -var dbUrl = 'jdbc:google:mysql://' + connectionName + '/' + db; - /** * Write one row of data to a table. */ function writeOneRecord() { - var conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd); - - var stmt = conn.prepareStatement('INSERT INTO entries ' - + '(guestName, content) values (?, ?)'); - stmt.setString(1, 'First Guest'); - stmt.setString(2, 'Hello, world'); - stmt.execute(); + try { + const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd); + + const stmt = conn.prepareStatement('INSERT INTO entries ' + + '(guestName, content) values (?, ?)'); + stmt.setString(1, 'First Guest'); + stmt.setString(2, 'Hello, world'); + stmt.execute(); + } catch (err) { + // TODO(developer) - Handle exception from the API + console.log('Failed with an error %s', err.message); + } } /** * Write 500 rows of data to a table in a single batch. */ function writeManyRecords() { - var conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd); - conn.setAutoCommit(false); - - var start = new Date(); - var stmt = conn.prepareStatement('INSERT INTO entries ' - + '(guestName, content) values (?, ?)'); - for (var i = 0; i < 500; i++) { - stmt.setString(1, 'Name ' + i); - stmt.setString(2, 'Hello, world ' + i); - stmt.addBatch(); - } + try { + const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd); + conn.setAutoCommit(false); + + const start = new Date(); + const stmt = conn.prepareStatement('INSERT INTO entries ' + + '(guestName, content) values (?, ?)'); + for (let i = 0; i < 500; i++) { + stmt.setString(1, 'Name ' + i); + stmt.setString(2, 'Hello, world ' + i); + stmt.addBatch(); + } - var batch = stmt.executeBatch(); - conn.commit(); - conn.close(); + const batch = stmt.executeBatch(); + conn.commit(); + conn.close(); - var end = new Date(); - Logger.log('Time elapsed: %sms for %s rows.', end - start, batch.length); + const end = new Date(); + console.log('Time elapsed: %sms for %s rows.', end - start, batch.length); + } catch (err) { + // TODO(developer) - Handle exception from the API + console.log('Failed with an error %s', err.message); + } } // [END apps_script_jdbc_write] // [START apps_script_jdbc_read] -/** - * Replace the variables in this block with real values. - * You can find the "Instance connection name" in the Google Cloud - * Platform Console, on the instance Overview page. - */ -var connectionName = 'Instance_connection_name'; -var user = 'user_name'; -var userPwd = 'user_password'; -var db = 'database_name'; - -var dbUrl = 'jdbc:google:mysql://' + connectionName + '/' + db; - /** * Read up to 1000 rows of data from the table and log them. */ function readFromTable() { - var conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd); - - var start = new Date(); - var stmt = conn.createStatement(); - stmt.setMaxRows(1000); - var results = stmt.executeQuery('SELECT * FROM entries'); - var numCols = results.getMetaData().getColumnCount(); - - while (results.next()) { - var rowString = ''; - for (var col = 0; col < numCols; col++) { - rowString += results.getString(col + 1) + '\t'; + try { + const conn = Jdbc.getCloudSqlConnection(dbUrl, user, userPwd); + const start = new Date(); + const stmt = conn.createStatement(); + stmt.setMaxRows(1000); + const results = stmt.executeQuery('SELECT * FROM entries'); + const numCols = results.getMetaData().getColumnCount(); + + while (results.next()) { + let rowString = ''; + for (let col = 0; col < numCols; col++) { + rowString += results.getString(col + 1) + '\t'; + } + console.log(rowString); } - Logger.log(rowString); - } - results.close(); - stmt.close(); + results.close(); + stmt.close(); - var end = new Date(); - Logger.log('Time elapsed: %sms', end - start); + const end = new Date(); + console.log('Time elapsed: %sms', end - start); + } catch (err) { + // TODO(developer) - Handle exception from the API + console.log('Failed with an error %s', err.message); + } } // [END apps_script_jdbc_read] diff --git a/service/propertyService.gs b/service/propertyService.gs index 3a74cafda..593ab0dfb 100644 --- a/service/propertyService.gs +++ b/service/propertyService.gs @@ -1,55 +1,144 @@ -// [START apps_script_property_service_save_data_1] -// Set a property in each of the three property stores. -var scriptProperties = PropertiesService.getScriptProperties(); -var userProperties = PropertiesService.getUserProperties(); -var documentProperties = PropertiesService.getDocumentProperties(); +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @see- https://developers.google.com/apps-script/guides/properties +/** + * Save or set the property in each three property store. + */ +function saveSingleProperty() { + // [START apps_script_property_service_save_data_single_value] + try { + // Set a property in each of the three property stores. + const scriptProperties = PropertiesService.getScriptProperties(); + const userProperties = PropertiesService.getUserProperties(); + const documentProperties = PropertiesService.getDocumentProperties(); -scriptProperties.setProperty('SERVER_URL', 'http://www.example.com/'); -userProperties.setProperty('DISPLAY_UNITS', 'metric'); -documentProperties.setProperty('SOURCE_DATA_ID', '1234567890abcdefghijklmnopqrstuvwxyz'); -// [END apps_script_property_service_save_data_1] + scriptProperties.setProperty('SERVER_URL', 'http://www.example.com/'); + userProperties.setProperty('DISPLAY_UNITS', 'metric'); + documentProperties.setProperty('SOURCE_DATA_ID', + '1j3GgabZvXUF177W0Zs_2v--H6SPCQb4pmZ6HsTZYT5k'); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } + // [END apps_script_property_service_save_data_single_value] +} -// [START apps_script_property_service_save_data_2] -// Set multiple script properties in one call. -var scriptProperties = PropertiesService.getScriptProperties(); -scriptProperties.setProperties({ - 'cow': 'moo', - 'sheep': 'baa', - 'chicken': 'cluck' -}); -// [END apps_script_property_service_save_data_2] +/** + * Save the multiple script properties. + */ +function saveMultipleProperties() { + // [START apps_script_property_service_save_data_multiple_value] + try { + // Set multiple script properties in one call. + const scriptProperties = PropertiesService.getScriptProperties(); + scriptProperties.setProperties({ + 'cow': 'moo', + 'sheep': 'baa', + 'chicken': 'cluck' + }); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } + // [END apps_script_property_service_save_data_multiple_value] +} -// [START apps_script_property_service_read_data_1] -// Get the value for the user property 'DISPLAY_UNITS'. -var userProperties = PropertiesService.getUserProperties(); -var units = userProperties.getProperty('DISPLAY_UNITS'); -// [END apps_script_property_service_read_data_1] +/** + * Read single value for user property. + */ +function readSingleProperty() { + // [START apps_script_property_service_read_data_single_value] + try { + // Get the value for the user property 'DISPLAY_UNITS'. + const userProperties = PropertiesService.getUserProperties(); + const units = userProperties.getProperty('DISPLAY_UNITS'); + console.log('values of units %s', units); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } + // [END apps_script_property_service_read_data_single_value] +} -// [START apps_script_property_service_read_data_2] -// Get multiple script properties in one call, then log them all. -var scriptProperties = PropertiesService.getScriptProperties(); -var data = scriptProperties.getProperties(); -for (var key in data) { - Logger.log('Key: %s, Value: %s', key, data[key]); +/** + * Read the multiple script properties. + */ +function readAllProperties() { + // [START apps_script_property_service_read_multiple_data_value] + try { + // Get multiple script properties in one call, then log them all. + const scriptProperties = PropertiesService.getScriptProperties(); + const data = scriptProperties.getProperties(); + for (const key in data) { + console.log('Key: %s, Value: %s', key, data[key]); + } + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } + // [END apps_script_property_service_read_multiple_data_value] } -// [END apps_script_property_service_read_data_2] -// [START apps_script_property_service_modify_data] -// Change the unit type in the user property 'DISPLAY_UNITS'. -var userProperties = PropertiesService.getUserProperties(); -var units = userProperties.getProperty('DISPLAY_UNITS'); -units = 'imperial'; // Only changes local value, not stored value. -userProperties.setProperty('DISPLAY_UNITS', units); // Updates stored value. -// [END apps_script_property_service_modify_data] +/** + * Update the user property value. + */ +function updateProperty() { + // [START apps_script_property_service_modify_data] + try { + // Change the unit type in the user property 'DISPLAY_UNITS'. + const userProperties = PropertiesService.getUserProperties(); + let units = userProperties.getProperty('DISPLAY_UNITS'); + units = 'imperial'; // Only changes local value, not stored value. + userProperties.setProperty('DISPLAY_UNITS', units); // Updates stored value. + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } + // [END apps_script_property_service_modify_data] +} -// [START apps_script_property_service_delete_data_1] -// Delete the user property 'DISPLAY_UNITS'. -var userProperties = PropertiesService.getUserProperties(); -userProperties.deleteProperty('DISPLAY_UNITS'); -// [END apps_script_property_service_delete_data_1] +/** + * Delete the single user property. + */ +function deleteSingleProperty() { + // [START apps_script_property_service_delete_data_single_value] + try { + // Delete the user property 'DISPLAY_UNITS'. + const userProperties = PropertiesService.getUserProperties(); + userProperties.deleteProperty('DISPLAY_UNITS'); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } + // [END apps_script_property_service_delete_data_single_value] +} -// [START apps_script_property_service_delete_data_2] -// Delete all user properties in the current script. -var userProperties = PropertiesService.getUserProperties(); -userProperties.deleteAllProperties(); -// [END apps_script_property_service_delete_data_2] +/** + * Delete all user properties in the current script. + */ +function deleteAllUserProperties() { + // [START apps_script_property_service_delete_all_data] + try { + // Get user properties in the current script. + const userProperties = PropertiesService.getUserProperties(); + // Delete all user properties in the current script. + userProperties.deleteAllProperties(); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } + // [END apps_script_property_service_delete_all_data] +} diff --git a/service/test_jdbc.gs b/service/test_jdbc.gs new file mode 100644 index 000000000..e58e9f569 --- /dev/null +++ b/service/test_jdbc.gs @@ -0,0 +1,76 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * Tests createDatabase function of jdbc.gs + */ +function itShouldCreateDatabase() { + console.log('itShouldCreateDatabase'); + createDatabase(); +} + +/** + * Tests createUser function of jdbc.gs + */ +function itShouldCreateUser() { + console.log('itShouldCreateUser'); + createUser(); +} + +/** + * Tests createTable function of jdbc.gs + */ +function itShouldCreateTable() { + console.log('itShouldCreateTable'); + createTable(); +} + +/** + * Tests writeOneRecord function of jdbc.gs + */ +function itShouldWriteOneRecord() { + console.log('itShouldWriteOneRecord'); + writeOneRecord(); +} + +/** + * Tests writeManyRecords function of jdbc.gs + */ +function itShouldWriteManyRecords() { + console.log('itShouldWriteManyRecords'); + writeManyRecords(); +} + +/** + * Tests readFromTable function of jdbc.gs + */ +function itShouldReadFromTable() { + console.log('itShouldReadFromTable'); + readFromTable(); +} + +/** + * Runs all the tests + */ +function RUN_ALL_TESTS() { + itShouldCreateDatabase(); + itShouldCreateUser(); + itShouldCreateTable(); + itShouldWriteOneRecord(); + itShouldWriteManyRecords(); + itShouldReadFromTable(); +} diff --git a/service/test_propertyServices.gs b/service/test_propertyServices.gs new file mode 100644 index 000000000..a2fbbca6e --- /dev/null +++ b/service/test_propertyServices.gs @@ -0,0 +1,36 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Run all tests for propertyService.gs + */ +function RUN_ALL_TESTS() { + console.log('> itShouldSaveSingleProperty'); + saveSingleProperty(); + console.log('> itShouldSaveMultipleProperties'); + saveMultipleProperties(); + console.log('> itShouldReadSingleProperty'); + readSingleProperty(); + console.log('> itShouldReadAllProperties'); + readAllProperties(); + // The tests below are successful if they run without any extra output + console.log('> itShouldUpdateProperty'); + updateProperty(); + console.log('> itShouldDeleteSingleProperty'); + deleteSingleProperty(); + console.log('> itShouldDeleteAllUserProperties'); + deleteAllUserProperties(); +} diff --git a/sheets/README.md b/sheets/README.md index ae73b98d8..fe9863abf 100644 --- a/sheets/README.md +++ b/sheets/README.md @@ -24,4 +24,4 @@ This tutorial shows you how to use the Spreadsheets service to create Tournament ## [Removing Duplicates](https://developers.google.com/apps-script/articles/removing_duplicates) -This tutorial shows how to avoid duplicates when you want to automate the process of copying data in G Suite and specifically how to remove duplicate rows in spreadsheet data. +This tutorial shows how to avoid duplicates when you want to automate the process of copying data in Google Workspace and specifically how to remove duplicate rows in spreadsheet data. diff --git a/sheets/api/helpers.gs b/sheets/api/helpers.gs index 08a80f41d..68ad23f58 100644 --- a/sheets/api/helpers.gs +++ b/sheets/api/helpers.gs @@ -1,4 +1,4 @@ -var filesToDelete = []; +let filesToDelete = []; /** * Helper methods for Google Sheets tests. */ @@ -19,8 +19,8 @@ Helpers.prototype.cleanup = function() { }; Helpers.prototype.createTestSpreadsheet = function() { - var spreadsheet = SpreadsheetApp.create('Test Spreadsheet'); - for (var i = 0; i < 3; ++i) { + const spreadsheet = SpreadsheetApp.create('Test Spreadsheet'); + for (let i = 0; i < 3; ++i) { spreadsheet.appendRow([1, 2, 3]); } this.deleteFileOnCleanup(spreadsheet.getId()); @@ -28,17 +28,17 @@ Helpers.prototype.createTestSpreadsheet = function() { }; Helpers.prototype.populateValues = function(spreadsheetId) { - var batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest(); - var repeatCellRequest = Sheets.newRepeatCellRequest(); + const batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest(); + const repeatCellRequest = Sheets.newRepeatCellRequest(); - var values = []; - for (var i = 0; i < 10; ++i) { + let values = []; + for (let i = 0; i < 10; ++i) { values[i] = []; - for (var j = 0; j < 10; ++j) { + for (let j = 0; j < 10; ++j) { values[i].push('Hello'); } } - var range = 'A1:J10'; + let range = 'A1:J10'; SpreadsheetApp.openById(spreadsheetId).getRange(range).setValues(values); SpreadsheetApp.flush(); }; diff --git a/sheets/api/spreadsheet_snippets.gs b/sheets/api/spreadsheet_snippets.gs index bfb16753c..4d08b67b3 100644 --- a/sheets/api/spreadsheet_snippets.gs +++ b/sheets/api/spreadsheet_snippets.gs @@ -1,110 +1,164 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Google Sheets API Snippets. */ function Snippets() {}; -Snippets.prototype.create = function(title) { - // [START sheets_create] +// [START sheets_create] +/** + * Creates a new sheet using the sheets advanced services + * @param {string} title the name of the sheet to be created + * @returns {string} the spreadsheet ID + */ +Snippets.prototype.create = (title) => { // This code uses the Sheets Advanced Service, but for most use cases // the built-in method SpreadsheetApp.create() is more appropriate. - var sheet = Sheets.newSpreadsheet(); - sheet.properties = Sheets.newSpreadsheetProperties(); - sheet.properties.title = title; - var spreadsheet = Sheets.Spreadsheets.create(sheet); - // [END sheets_create] - return spreadsheet.spreadsheetId; + try { + let sheet = Sheets.newSpreadsheet(); + sheet.properties = Sheets.newSpreadsheetProperties(); + sheet.properties.title = title; + const spreadsheet = Sheets.Spreadsheets.create(sheet); + + return spreadsheet.spreadsheetId; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } }; +// [END sheets_create] -Snippets.prototype.batchUpdate = function(spreadsheetId, title, find, - replacement) { - // [START sheets_batch_update] +// [START sheets_batch_update] +/** + * Updates the specified sheet using advanced sheet services + * @param {string} spreadsheetId id of the spreadsheet to be updated + * @param {string} title name of the sheet in the spreadsheet to be updated + * @param {string} find string to be replaced + * @param {string} replacement the string to replace the old data + * @returns {*} the updated spreadsheet + */ +Snippets.prototype.batchUpdate = (spreadsheetId, title, + find, replacement) => { // This code uses the Sheets Advanced Service, but for most use cases // the built-in method SpreadsheetApp.getActiveSpreadsheet() // .getRange(range).setValues(values) is more appropriate. - // Change the spreadsheet's title. - var updateSpreadsheetPropertiesRequest = Sheets.newUpdateSpreadsheetPropertiesRequest(); - updateSpreadsheetPropertiesRequest.properties = Sheets.newSpreadsheetProperties(); - updateSpreadsheetPropertiesRequest.properties.title = title; - updateSpreadsheetPropertiesRequest.fields = 'title'; - - // Find and replace text. - var findReplaceRequest = Sheets.newFindReplaceRequest(); - findReplaceRequest.find = find; - findReplaceRequest.replacement = replacement; - findReplaceRequest.allSheets = true; - - var requests = [Sheets.newRequest(), Sheets.newRequest()]; - requests[0].updateSpreadsheetProperties = updateSpreadsheetPropertiesRequest; - requests[1].findReplace = findReplaceRequest; - - var batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest(); - batchUpdateRequest.requests = requests; - - // Add additional requests (operations) - var result = Sheets.Spreadsheets.batchUpdate(batchUpdateRequest, spreadsheetId); - // [END sheets_batch_update] - return result; + try { + // Change the spreadsheet's title. + let updateSpreadsheetPropertiesRequest = + Sheets.newUpdateSpreadsheetPropertiesRequest(); + updateSpreadsheetPropertiesRequest.properties = + Sheets.newSpreadsheetProperties(); + updateSpreadsheetPropertiesRequest.properties.title = title; + updateSpreadsheetPropertiesRequest.fields = 'title'; + + // Find and replace text. + let findReplaceRequest = Sheets.newFindReplaceRequest(); + findReplaceRequest.find = find; + findReplaceRequest.replacement = replacement; + findReplaceRequest.allSheets = true; + + let requests = [Sheets.newRequest(), Sheets.newRequest()]; + requests[0].updateSpreadsheetProperties = + updateSpreadsheetPropertiesRequest; + requests[1].findReplace = findReplaceRequest; + + let batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest(); + batchUpdateRequest.requests = requests; + + // Add additional requests (operations) + const result = + Sheets.Spreadsheets.batchUpdate(batchUpdateRequest, spreadsheetId); + return result; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } }; +// [END sheets_batch_update] +// [START sheets_get_values] +/** + * Gets the values of the cells in the specified range + * @param {string} spreadsheetId id of the spreadsheet + * @param {string} range specifying the start and end cells of the range + * @returns {*} Values in the range + */ Snippets.prototype.getValues = function(spreadsheetId, range) { - // [START sheets_get_values] // This code uses the Sheets Advanced Service, but for most use cases // the built-in method SpreadsheetApp.getActiveSpreadsheet() // .getRange(range).getValues(values) is more appropriate. - var result = Sheets.Spreadsheets.Values.get(spreadsheetId, range); - var numRows = result.values ? result.values.length : 0; - // [END sheets_get_values] - return result; + try { + const result = Sheets.Spreadsheets.Values.get(spreadsheetId, range); + const numRows = result.values ? result.values.length : 0; + return result; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } }; +// [END sheets_get_values] -Snippets.prototype.batchGetValues = function(spreadsheetId, _ranges) { - // [START sheets_batch_get_values] +// [START sheets_batch_get_values] +/** + * Get the values in the specified ranges + * @param {string} spreadsheetId spreadsheet's ID + * @param {list} _ranges The span of ranges + * @returns {*} spreadsheet information and values + */ +Snippets.prototype.batchGetValues = (spreadsheetId, + _ranges) => { // This code uses the Sheets Advanced Service, but for most use cases // the built-in method SpreadsheetApp.getActiveSpreadsheet() // .getRange(range).getValues(values) is more appropriate. - var ranges = [ - // Range names ... + let ranges = [ + //Range names ... ]; // [START_EXCLUDE silent] ranges = _ranges; // [END_EXCLUDE] - var result = Sheets.Spreadsheets.Values.batchGet(spreadsheetId, {ranges: ranges}); - // [END sheets_batch_get_values] - return result; + try { + const result = + Sheets.Spreadsheets.Values.batchGet(spreadsheetId, {ranges: ranges}); + return result; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } }; +// [END sheets_batch_get_values] -Snippets.prototype.updateValues = function(spreadsheetId, range, valueInputOption, - _values) { - // [START sheets_update_values] - // This code uses the Sheets Advanced Service, but for most use cases - // the built-in method SpreadsheetApp.getActiveSpreadsheet() - // .getRange(range).setValues(values) is more appropriate. - var values = [ - [ - // Cell values ... - ] - // Additional rows ... - ]; - // [START_EXCLUDE silent] - values = _values; - // [END_EXCLUDE] - var valueRange = Sheets.newValueRange(); - valueRange.values = values; - var result = Sheets.Spreadsheets.Values.update(valueRange, spreadsheetId, range, { - valueInputOption: valueInputOption - }); - // [END sheets_update_values] - return result; -}; - -Snippets.prototype.batchUpdateValues = function(spreadsheetId, range, - valueInputOption, _values) { - // [START sheets_batch_update_values] +// [START sheets_update_values] +/** + * Updates the values in the specified range + * @param {string} spreadsheetId spreadsheet's ID + * @param {string} range the range of cells in spreadsheet + * @param {string} valueInputOption determines how the input should be interpreted + * @see + * https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption + * @param {list>} _values list of string lists to input + * @returns {*} spreadsheet with updated values + */ +Snippets.prototype.updateValues = (spreadsheetId, range, + valueInputOption, _values) => { // This code uses the Sheets Advanced Service, but for most use cases // the built-in method SpreadsheetApp.getActiveSpreadsheet() // .getRange(range).setValues(values) is more appropriate. - var values = [ + let values = [ [ // Cell values ... ] @@ -113,23 +167,80 @@ Snippets.prototype.batchUpdateValues = function(spreadsheetId, range, // [START_EXCLUDE silent] values = _values; // [END_EXCLUDE] - var valueRange = Sheets.newValueRange(); - valueRange.range = range; - valueRange.values = values; - - var batchUpdateRequest = Sheets.newBatchUpdateValuesRequest(); - batchUpdateRequest.data = valueRange; - batchUpdateRequest.valueInputOption = valueInputOption; - var result = Sheets.Spreadsheets.Values.batchUpdate(batchUpdateRequest, spreadsheetId); - // [END sheets_batch_update_values] - return result; + try { + let valueRange = Sheets.newValueRange(); + valueRange.values = values; + const result = Sheets.Spreadsheets.Values.update(valueRange, + spreadsheetId, range, {valueInputOption: valueInputOption}); + return result; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } }; +// [END sheets_update_values] -Snippets.prototype.appendValues = function(spreadsheetId, range, - valueInputOption, _values) { - // [START sheets_append_values] - var values = [ +// [START sheets_batch_update_values] +/** + * Updates the values in the specified range + * @param {string} spreadsheetId spreadsheet's ID + * @param {string} range range of cells of the spreadsheet + * @param {string} valueInputOption determines how the input should be interpreted + * @see + * https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption + * @param {list>} _values list of string values to input + * @returns {*} spreadsheet with updated values + */ +Snippets.prototype.batchUpdateValues = + (spreadsheetId, range, valueInputOption, + _values) => { + // This code uses the Sheets Advanced Service, but for most use cases + // the built-in method SpreadsheetApp.getActiveSpreadsheet() + // .getRange(range).setValues(values) is more appropriate. + let values = [ + [ + // Cell values ... + ] + // Additional rows ... + ]; + // [START_EXCLUDE silent] + values = _values; + // [END_EXCLUDE] + + try { + let valueRange = Sheets.newValueRange(); + valueRange.range = range; + valueRange.values = values; + + let batchUpdateRequest = Sheets.newBatchUpdateValuesRequest(); + batchUpdateRequest.data = valueRange; + batchUpdateRequest.valueInputOption = valueInputOption; + + const result = Sheets.Spreadsheets.Values.batchUpdate(batchUpdateRequest, + spreadsheetId); + return result; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } + }; +// [END sheets_batch_update_values] + +// [START sheets_append_values] +/** + * Appends values to the specified range + * @param {string} spreadsheetId spreadsheet's ID + * @param {string} range range of cells in the spreadsheet + * @param valueInputOption determines how the input should be interpreted + * @see + * https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption + * @param {list} _values list of rows of values to input + * @returns {*} spreadsheet with appended values + */ +Snippets.prototype.appendValues = (spreadsheetId, range, + valueInputOption, _values) => { + let values = [ [ // Cell values ... ] @@ -138,155 +249,185 @@ Snippets.prototype.appendValues = function(spreadsheetId, range, // [START_EXCLUDE silent] values = _values; // [END_EXCLUDE] - var valueRange = Sheets.newRowData(); - valueRange.values = values; - - var appendRequest = Sheets.newAppendCellsRequest(); - appendRequest.sheetId = spreadsheetId; - appendRequest.rows = [valueRange]; - - var result = Sheets.Spreadsheets.Values.append(valueRange, spreadsheetId, range, { - valueInputOption: valueInputOption - }); - // [END sheets_append_values] - return result; + try { + let valueRange = Sheets.newRowData(); + valueRange.values = values; + + let appendRequest = Sheets.newAppendCellsRequest(); + appendRequest.sheetId = spreadsheetId; + appendRequest.rows = [valueRange]; + + const result = Sheets.Spreadsheets.Values.append(valueRange, spreadsheetId, + range, {valueInputOption: valueInputOption}); + return result; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } }; +// [END sheets_append_values] -Snippets.prototype.pivotTable = function(spreadsheetId) { - var spreadsheet = SpreadsheetApp.openById(spreadsheetId); - // [START sheets_pivot_tables] - // Create two sheets for our pivot table, assume we have one. - var sheet = spreadsheet.getSheets()[0]; - sheet.copyTo(spreadsheet); - - var sourceSheetId = spreadsheet.getSheets()[0].getSheetId(); - var targetSheetId = spreadsheet.getSheets()[1].getSheetId(); - - // Create pivot table - var pivotTable = Sheets.newPivotTable(); - - var gridRange = Sheets.newGridRange(); - gridRange.sheetId = sourceSheetId; - gridRange.startRowIndex = 0; - gridRange.startColumnIndex = 0; - gridRange.endRowIndex = 20; - gridRange.endColumnIndex = 7; - pivotTable.source = gridRange; - - var pivotRows = Sheets.newPivotGroup(); - pivotRows.sourceColumnOffset = 1; - pivotRows.showTotals = true; - pivotRows.sortOrder = 'ASCENDING'; - pivotTable.rows = pivotRows; - - var pivotColumns = Sheets.newPivotGroup(); - pivotColumns.sourceColumnOffset = 4; - pivotColumns.sortOrder = 'ASCENDING'; - pivotColumns.showTotals = true; - pivotTable.columns = pivotColumns; - - var pivotValue = Sheets.newPivotValue(); - pivotValue.summarizeFunction = 'COUNTA'; - pivotValue.sourceColumnOffset = 4; - pivotTable.values = [pivotValue]; - - // Create other metadata for the updateCellsRequest - var cellData = Sheets.newCellData(); - cellData.pivotTable = pivotTable; - - var rows = Sheets.newRowData(); - rows.values = cellData; - - var start = Sheets.newGridCoordinate(); - start.sheetId = targetSheetId; - start.rowIndex = 0; - start.columnIndex = 0; - - var updateCellsRequest = Sheets.newUpdateCellsRequest(); - updateCellsRequest.rows = rows; - updateCellsRequest.start = start; - updateCellsRequest.fields = 'pivotTable'; - - // Batch update our spreadsheet - var batchUpdate = Sheets.newBatchUpdateSpreadsheetRequest(); - var updateCellsRawRequest = Sheets.newRequest(); - updateCellsRawRequest.updateCells = updateCellsRequest; - batchUpdate.requests = [updateCellsRawRequest]; - var response = Sheets.Spreadsheets.batchUpdate(batchUpdate, spreadsheetId); - // [END sheets_pivot_tables] - return response; +// [START sheets_pivot_tables] +/** + * Create pivot table + * @param {string} spreadsheetId spreadsheet ID + * @returns {*} pivot table's spreadsheet + */ +Snippets.prototype.pivotTable = (spreadsheetId) => { + try { + const spreadsheet = SpreadsheetApp.openById(spreadsheetId); + + // Create two sheets for our pivot table, assume we have one. + let sheet = spreadsheet.getSheets()[0]; + sheet.copyTo(spreadsheet); + + const sourceSheetId = spreadsheet.getSheets()[0].getSheetId(); + const targetSheetId = spreadsheet.getSheets()[1].getSheetId(); + + // Create pivot table + const pivotTable = Sheets.newPivotTable(); + + let gridRange = Sheets.newGridRange(); + gridRange.sheetId = sourceSheetId; + gridRange.startRowIndex = 0; + gridRange.startColumnIndex = 0; + gridRange.endRowIndex = 20; + gridRange.endColumnIndex = 7; + pivotTable.source = gridRange; + + let pivotRows = Sheets.newPivotGroup(); + pivotRows.sourceColumnOffset = 1; + pivotRows.showTotals = true; + pivotRows.sortOrder = 'ASCENDING'; + pivotTable.rows = pivotRows; + + let pivotColumns = Sheets.newPivotGroup(); + pivotColumns.sourceColumnOffset = 4; + pivotColumns.sortOrder = 'ASCENDING'; + pivotColumns.showTotals = true; + pivotTable.columns = pivotColumns; + + let pivotValue = Sheets.newPivotValue(); + pivotValue.summarizeFunction = 'COUNTA'; + pivotValue.sourceColumnOffset = 4; + pivotTable.values = [pivotValue]; + + // Create other metadata for the updateCellsRequest + let cellData = Sheets.newCellData(); + cellData.pivotTable = pivotTable; + + let rows = Sheets.newRowData(); + rows.values = cellData; + + let start = Sheets.newGridCoordinate(); + start.sheetId = targetSheetId; + start.rowIndex = 0; + start.columnIndex = 0; + + let updateCellsRequest = Sheets.newUpdateCellsRequest(); + updateCellsRequest.rows = rows; + updateCellsRequest.start = start; + updateCellsRequest.fields = 'pivotTable'; + + // Batch update our spreadsheet + let batchUpdate = Sheets.newBatchUpdateSpreadsheetRequest(); + let updateCellsRawRequest = Sheets.newRequest(); + updateCellsRawRequest.updateCells = updateCellsRequest; + batchUpdate.requests = [updateCellsRawRequest]; + const response = Sheets.Spreadsheets.batchUpdate(batchUpdate, + spreadsheetId); + + return response; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } }; +// [END sheets_pivot_tables] -Snippets.prototype.conditionalFormatting = function(spreadsheetId) { - // [START sheets_conditional_formatting] - var myRange = Sheets.newGridRange(); - myRange.sheetId = 0; - myRange.startRowIndex = 0; - myRange.endRowIndex = 11; - myRange.startColumnIndex = 0; - myRange.endColumnIndex = 4; - - // Request 1 - var rule1ConditionalValue = Sheets.newConditionValue(); - rule1ConditionalValue.userEnteredValue = '=GT($D2,median($D$2:$D$11))'; - - var rule1ConditionFormat = Sheets.newCellFormat(); - rule1ConditionFormat.textFormat = Sheets.newTextFormat(); - rule1ConditionFormat.textFormat.foregroundColor = Sheets.newColor(); - rule1ConditionFormat.textFormat.foregroundColor.red = 0.8; - - var rule1Condition = Sheets.newBooleanCondition(); - rule1Condition.type = 'CUSTOM_FORMULA'; - rule1Condition.values = [rule1ConditionalValue]; - - var rule1BooleanRule = Sheets.newBooleanRule(); - rule1BooleanRule.condition = rule1Condition; - rule1BooleanRule.format = rule1ConditionFormat; - - var rule1 = Sheets.newConditionalFormatRule(); - rule1.ranges = [myRange]; - rule1.booleanRule = rule1BooleanRule; - - var request1 = Sheets.newRequest(); - var addConditionalFormatRuleRequest1 = Sheets.newAddConditionalFormatRuleRequest(); - addConditionalFormatRuleRequest1.rule = rule1; - addConditionalFormatRuleRequest1.index = 0; - request1.addConditionalFormatRule = addConditionalFormatRuleRequest1; - - // Request 2 - var rule2ConditionalValue = Sheets.newConditionValue(); - rule2ConditionalValue.userEnteredValue = '=LT($D2,median($D$2:$D$11))'; - - var rule2ConditionFormat = Sheets.newCellFormat(); - rule2ConditionFormat.textFormat = Sheets.newTextFormat(); - rule2ConditionFormat.textFormat.foregroundColor = Sheets.newColor(); - rule2ConditionFormat.textFormat.foregroundColor.red = 1; - rule2ConditionFormat.textFormat.foregroundColor.green = 0.4; - rule2ConditionFormat.textFormat.foregroundColor.blue = 0.4; - - var rule2Condition = Sheets.newBooleanCondition(); - rule2Condition.type = 'CUSTOM_FORMULA'; - rule2Condition.values = [rule1ConditionalValue]; - - var rule2BooleanRule = Sheets.newBooleanRule(); - rule2BooleanRule.condition = rule2Condition; - rule2BooleanRule.format = rule2ConditionFormat; - - var rule2 = Sheets.newConditionalFormatRule(); - rule2.ranges = [myRange]; - rule2.booleanRule = rule2BooleanRule; - - var request2 = Sheets.newRequest(); - var addConditionalFormatRuleRequest2 = Sheets.newAddConditionalFormatRuleRequest(); - addConditionalFormatRuleRequest2.rule = rule2; - addConditionalFormatRuleRequest2.index = 0; - request2.addConditionalFormatRule = addConditionalFormatRuleRequest2; - - // Batch send the requests - var requests = [request1, request2]; - var batchUpdate = Sheets.newBatchUpdateSpreadsheetRequest(); - batchUpdate.requests = requests; - var response = Sheets.Spreadsheets.batchUpdate(batchUpdate, spreadsheetId); - // [END sheets_conditional_formatting] - return response; +// [START sheets_conditional_formatting] +/** + * conditional formatting + * @param {string} spreadsheetId spreadsheet ID + * @returns {*} spreadsheet + */ +Snippets.prototype.conditionalFormatting = (spreadsheetId) => { + try { + let myRange = Sheets.newGridRange(); + myRange.sheetId = 0; + myRange.startRowIndex = 0; + myRange.endRowIndex = 11; + myRange.startColumnIndex = 0; + myRange.endColumnIndex = 4; + + // Request 1 + let rule1ConditionalValue = Sheets.newConditionValue(); + rule1ConditionalValue.userEnteredValue = '=GT($D2,median($D$2:$D$11))'; + + let rule1ConditionFormat = Sheets.newCellFormat(); + rule1ConditionFormat.textFormat = Sheets.newTextFormat(); + rule1ConditionFormat.textFormat.foregroundColor = Sheets.newColor(); + rule1ConditionFormat.textFormat.foregroundColor.red = 0.8; + + let rule1Condition = Sheets.newBooleanCondition(); + rule1Condition.type = 'CUSTOM_FORMULA'; + rule1Condition.values = [rule1ConditionalValue]; + + let rule1BooleanRule = Sheets.newBooleanRule(); + rule1BooleanRule.condition = rule1Condition; + rule1BooleanRule.format = rule1ConditionFormat; + + let rule1 = Sheets.newConditionalFormatRule(); + rule1.ranges = [myRange]; + rule1.booleanRule = rule1BooleanRule; + + let request1 = Sheets.newRequest(); + let addConditionalFormatRuleRequest1 = + Sheets.newAddConditionalFormatRuleRequest(); + addConditionalFormatRuleRequest1.rule = rule1; + addConditionalFormatRuleRequest1.index = 0; + request1.addConditionalFormatRule = addConditionalFormatRuleRequest1; + + // Request 2 + let rule2ConditionalValue = Sheets.newConditionValue(); + rule2ConditionalValue.userEnteredValue = '=LT($D2,median($D$2:$D$11))'; + + let rule2ConditionFormat = Sheets.newCellFormat(); + rule2ConditionFormat.textFormat = Sheets.newTextFormat(); + rule2ConditionFormat.textFormat.foregroundColor = Sheets.newColor(); + rule2ConditionFormat.textFormat.foregroundColor.red = 1; + rule2ConditionFormat.textFormat.foregroundColor.green = 0.4; + rule2ConditionFormat.textFormat.foregroundColor.blue = 0.4; + + let rule2Condition = Sheets.newBooleanCondition(); + rule2Condition.type = 'CUSTOM_FORMULA'; + rule2Condition.values = [rule2ConditionalValue]; + + let rule2BooleanRule = Sheets.newBooleanRule(); + rule2BooleanRule.condition = rule2Condition; + rule2BooleanRule.format = rule2ConditionFormat; + + let rule2 = Sheets.newConditionalFormatRule(); + rule2.ranges = [myRange]; + rule2.booleanRule = rule2BooleanRule; + + let request2 = Sheets.newRequest(); + let addConditionalFormatRuleRequest2 = + Sheets.newAddConditionalFormatRuleRequest(); + addConditionalFormatRuleRequest2.rule = rule2; + addConditionalFormatRuleRequest2.index = 0; + request2.addConditionalFormatRule = addConditionalFormatRuleRequest2; + + // Batch send the requests + const requests = [request1, request2]; + let batchUpdate = Sheets.newBatchUpdateSpreadsheetRequest(); + batchUpdate.requests = requests; + const response = + Sheets.Spreadsheets.batchUpdate(batchUpdate, spreadsheetId); + return response; + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } }; +// [END sheets_conditional_formatting] diff --git a/sheets/api/test_spreadsheet_snippets.gs b/sheets/api/test_spreadsheet_snippets.gs index 0a1a98fde..80f3796b8 100644 --- a/sheets/api/test_spreadsheet_snippets.gs +++ b/sheets/api/test_spreadsheet_snippets.gs @@ -1,5 +1,5 @@ -var snippets = new Snippets(); -var helpers = new Helpers(); +let snippets = new Snippets(); +let helpers = new Helpers(); /** * A simple exists assertion check. Expects a value to exist. Errors if DNE. @@ -45,7 +45,7 @@ function RUN_ALL_TESTS() { * Tests creating a spreadsheet. */ function itShouldCreateASpreadsheet() { - var spreadsheetId = snippets.create('Title'); + const spreadsheetId = snippets.create('Title'); expectToExist(spreadsheetId); helpers.deleteFileOnCleanup(spreadsheetId); } @@ -54,12 +54,13 @@ function itShouldCreateASpreadsheet() { * Tests updating a spreadsheet. */ function itShouldBatchUpdateASpreadsheet() { - var spreadsheetId = helpers.createTestSpreadsheet(); + const spreadsheetId = helpers.createTestSpreadsheet(); helpers.populateValues(spreadsheetId); - var result = snippets.batchUpdate(spreadsheetId, 'New Title', 'Hello', 'Goodbye'); - var replies = result.replies; + const result = snippets.batchUpdate(spreadsheetId, 'New Title', + 'Hello', 'Goodbye'); + const replies = result.replies; expectToEqual(replies.length, 2); - var findReplaceResponse = replies[1].findReplace; + const findReplaceResponse = replies[1].findReplace; expectToEqual(findReplaceResponse.occurrencesChanged, 100); } @@ -67,10 +68,10 @@ function itShouldBatchUpdateASpreadsheet() { * Tests getting a spreadsheet value. */ function itShouldGetSpreadsheetValues() { - var spreadsheetId = helpers.createTestSpreadsheet(); + const spreadsheetId = helpers.createTestSpreadsheet(); helpers.populateValues(spreadsheetId); - var result = snippets.getValues(spreadsheetId, 'A1:C2'); - var values = result.values; + const result = snippets.getValues(spreadsheetId, 'A1:C2'); + const values = result.values; expectToEqual(values.length, 2); expectToEqual(values[0].length, 3); } @@ -79,12 +80,12 @@ function itShouldGetSpreadsheetValues() { * Tests batch getting spreadsheet values. */ function itShouldBatchGetSpreadsheetValues() { - var spreadsheetId = helpers.createTestSpreadsheet(); + const spreadsheetId = helpers.createTestSpreadsheet(); helpers.populateValues(spreadsheetId); - var result = snippets.batchGetValues(spreadsheetId, ['A1:A3', 'B1:C1']); + const result = snippets.batchGetValues(spreadsheetId, + ['A1:A3', 'B1:C1']); expectToExist(result); - expectToEqual(result.length, 2); - var values = result[0]; + expectToEqual(result.valueRanges.length, 2); expectToEqual(result.valueRanges[0].values.length, 3); } @@ -92,11 +93,9 @@ function itShouldBatchGetSpreadsheetValues() { * Tests updating spreadsheet values. */ function itShouldUpdateSpreadsheetValues() { - var spreadsheetId = helpers.createTestSpreadsheet(); - var result = snippets.updateValues(spreadsheetId, 'A1:B2', 'USER_ENTERED', [ - ['A', 'B'], - ['C', 'D'] - ]); + const spreadsheetId = helpers.createTestSpreadsheet(); + const result = snippets.updateValues(spreadsheetId, 'A1:B2', + 'USER_ENTERED', [['A', 'B'], ['C', 'D']]); expectToEqual(result.updatedRows, 2); expectToEqual(result.updatedColumns, 2); expectToEqual(result.updatedCells, 4); @@ -106,11 +105,9 @@ function itShouldUpdateSpreadsheetValues() { * Test batch updating spreadsheet values. */ function itShouldBatchUpdateSpreadsheetValues() { - var spreadsheetId = helpers.createTestSpreadsheet(); - var result = snippets.batchUpdateValues(spreadsheetId, 'A1:B2', 'USER_ENTERED', [ - ['A', 'B'], - ['C', 'D'] - ]); + const spreadsheetId = helpers.createTestSpreadsheet(); + const result = snippets.batchUpdateValues(spreadsheetId, 'A1:B2', + 'USER_ENTERED', [['A', 'B'], ['C', 'D']]); expectToEqual(result.totalUpdatedRows, 2); expectToEqual(result.totalUpdatedColumns, 2); expectToEqual(result.totalUpdatedCells, 4); @@ -120,13 +117,11 @@ function itShouldBatchUpdateSpreadsheetValues() { * Test appending values to a spreadsheet. */ function itShouldAppendValuesToASpreadsheet() { - var spreadsheetId = helpers.createTestSpreadsheet(); + const spreadsheetId = helpers.createTestSpreadsheet(); helpers.populateValues(spreadsheetId); - var result = snippets.appendValues(spreadsheetId, 'Sheet1', 'USER_ENTERED', [ - ['A', 'B'], - ['C', 'D'] - ]); - var updates = result.updates; + const result = snippets.appendValues(spreadsheetId, 'Sheet1', + 'USER_ENTERED', [['A', 'B'], ['C', 'D']]); + const updates = result.updates; expectToEqual(updates.updatedRows, 2); expectToEqual(updates.updatedColumns, 2); expectToEqual(updates.updatedCells, 4); @@ -136,9 +131,9 @@ function itShouldAppendValuesToASpreadsheet() { * Test creating pivot tables. */ function itShouldCreatePivotTables() { - var spreadsheetId = helpers.createTestSpreadsheet(); + const spreadsheetId = helpers.createTestSpreadsheet(); helpers.populateValues(spreadsheetId); - var result = snippets.pivotTable(spreadsheetId); + const result = snippets.pivotTable(spreadsheetId); expectToExist(result); } @@ -146,9 +141,9 @@ function itShouldCreatePivotTables() { * Test conditionally formatting spreadsheets. */ function itShouldConditionallyFormat() { - var spreadsheetId = helpers.createTestSpreadsheet(); + const spreadsheetId = helpers.createTestSpreadsheet(); helpers.populateValues(spreadsheetId); - var result = snippets.conditionalFormatting(spreadsheetId); + const result = snippets.conditionalFormatting(spreadsheetId); expectToExist(spreadsheetId); expectToEqual(result.replies.length, 2); } diff --git a/sheets/bracketmaker/bracketmaker.gs b/sheets/bracketmaker/bracketmaker.gs deleted file mode 100644 index e5ebe1eb9..000000000 --- a/sheets/bracketmaker/bracketmaker.gs +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// [START apps_script_bracketmaker] -// This script works with the Brackets Test spreadsheet to create a tournament bracket -// given a list of players or teams. - -var RANGE_PLAYER1 = 'FirstPlayer'; -var SHEET_PLAYERS = 'Players'; -var SHEET_BRACKET = 'Bracket'; -var CONNECTOR_WIDTH = 15; - -/** - * This method adds a custom menu item to run the script - */ -function onOpen() { - var ss = SpreadsheetApp.getActiveSpreadsheet(); - ss.addMenu('Bracket Maker', - [{name: 'Create Bracket', functionName: 'createBracket'}]); -} - -/** - * This method creates the brackets based on the data provided on the players - */ -function createBracket() { - var ss = SpreadsheetApp.getActiveSpreadsheet(); - // [START apps_script_bracketmaker_range_players_1] - var rangePlayers = ss.getRangeByName(RANGE_PLAYER1); - // [END apps_script_bracketmaker_range_players_1] - var sheetControl = ss.getSheetByName(SHEET_PLAYERS); - var sheetResults = ss.getSheetByName(SHEET_BRACKET); - - // [START apps_script_bracketmaker_range_players_2] - // Get the players from column A. We assume the entire column is filled here. - rangePlayers = rangePlayers.offset(0, 0, sheetControl.getMaxRows() - - rangePlayers.getRowIndex() + 1, 1); - var players = rangePlayers.getValues(); - // [END apps_script_bracketmaker_range_players_2] - - // [START apps_script_bracketmaker_num_players_1] - // Now figure out how many players there are(ie don't count the empty cells) - var numPlayers = 0; - for (var i = 0; i < players.length; i++) { - if (!players[i][0] || players[i][0].length == 0) { - break; - } - numPlayers++; - } - players = players.slice(0, numPlayers); - // [END apps_script_bracketmaker_num_players_1] - - // Provide some error checking in case there are too many or too few players/teams. - if (numPlayers > 64) { - Browser.msgBox('Sorry, this script can only create brackets for 64 or fewer players.'); - return; // Early exit - } - - // [START apps_script_bracketmaker_num_players_2] - if (numPlayers < 3) { - Browser.msgBox('Sorry, you must have at least 3 players.'); - return; // Early exit - } - - // First clear the results sheet and all formatting - sheetResults.clear(); - // [END apps_script_bracketmaker_num_players_2] - - var upperPower = Math.ceil(Math.log(numPlayers) / Math.log(2)); - - // Find out what is the number that is a power of 2 and lower than numPlayers. - var countNodesUpperBound = Math.pow(2, upperPower); - - // Find out what is the number that is a power of 2 and higher than numPlayers. - var countNodesLowerBound = countNodesUpperBound / 2; - - // This is the number of nodes that will not show in the 1st level. - var countNodesHidden = numPlayers - countNodesLowerBound; - - // Enter the players for the 1st round - var currentPlayer = 0; - for (var i = 0; i < countNodesLowerBound; i++) { - if (i < countNodesHidden) { - // Must be on the first level - var rng = sheetResults.getRange(i * 4 + 1, 1); - setBracketItem_(rng, players); - setBracketItem_(rng.offset(2, 0, 1, 1), players); - setConnector_(sheetResults, rng.offset(0, 1, 3, 1)); - setBracketItem_(rng.offset(1, 2, 1, 1)); - } else { - // This player gets a bye - setBracketItem_(sheetResults.getRange(i * 4 + 2, 3), players); - } - } - - // Now fill in the rest of the bracket - upperPower--; - for (var i = 0; i < upperPower; i++) { - var pow1 = Math.pow(2, i + 1); - var pow2 = Math.pow(2, i + 2); - var pow3 = Math.pow(2, i + 3); - for (var j = 0; j < Math.pow(2, upperPower - i - 1); j++) { - setBracketItem_(sheetResults.getRange((j * pow3) + pow2, i * 2 + 5)); - setConnector_(sheetResults, sheetResults.getRange((j * pow3) + pow1, i * 2 + 4, pow2 + 1, 1)); - } - } -} - -// [START apps_script_bracketmaker_set_bracket_item] -/** - * Sets the value of an item in the bracket and the color. - * @param {Range} rng The Spreadsheet Range. - * @param {string[]} players The list of players. - */ -function setBracketItem_(rng, players) { - if (players) { - var rand = Math.ceil(Math.random() * players.length); - rng.setValue(players.splice(rand - 1, 1)[0][0]); - } - rng.setBackgroundColor('yellow'); -} -// [END apps_script_bracketmaker_set_bracket_item] - -/** - * Sets the color and width for connector cells. - * @param {Sheet} sheet The spreadsheet to setup. - * @param {Range} rng The spreadsheet range. - */ -function setConnector_(sheet, rng) { - sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH); - rng.setBackgroundColor('green'); -} -// [END apps_script_bracketmaker] diff --git a/sheets/customFunctions/customFunctions.gs b/sheets/customFunctions/customFunctions.gs index b241f2677..9fe44c0ac 100644 --- a/sheets/customFunctions/customFunctions.gs +++ b/sheets/customFunctions/customFunctions.gs @@ -23,12 +23,17 @@ * custom menu to the spreadsheet. */ function onOpen() { - var spreadsheet = SpreadsheetApp.getActive(); - var menuItems = [ - {name: 'Prepare sheet...', functionName: 'prepareSheet_'}, - {name: 'Generate step-by-step...', functionName: 'generateStepByStep_'} - ]; - spreadsheet.addMenu('Directions', menuItems); + 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); + } } /** @@ -38,7 +43,7 @@ function onOpen() { * @return {Number} The distance in miles. */ function metersToMiles(meters) { - if (typeof meters != 'number') { + if (typeof meters !== 'number') { return null; } return meters / 1000 * 0.621371; @@ -52,7 +57,7 @@ function metersToMiles(meters) { * @return {Number} The distance in meters. */ function drivingDistance(origin, destination) { - var directions = getDirections_(origin, destination); + const directions = getDirections_(origin, destination); return directions.routes[0].legs[0].distance.value; } @@ -60,19 +65,24 @@ function drivingDistance(origin, destination) { * A function that adds headers and some initial data to the spreadsheet. */ function prepareSheet_() { - var sheet = SpreadsheetApp.getActiveSheet().setName('Settings'); - var headers = [ - 'Start Address', - 'End Address', - 'Driving Distance (meters)', - 'Driving Distance (miles)']; - var 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); + 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); + } } /** @@ -80,85 +90,90 @@ function prepareSheet_() { * addresses on the "Settings" sheet that the user selected. */ function generateStepByStep_() { - var spreadsheet = SpreadsheetApp.getActive(); - var settingsSheet = spreadsheet.getSheetByName('Settings'); - settingsSheet.activate(); + try { + const spreadsheet = SpreadsheetApp.getActive(); + const settingsSheet = spreadsheet.getSheetByName('Settings'); + settingsSheet.activate(); - // Prompt the user for a row number. - var 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; - } - var rowNumber = Number(selectedRow); - if (isNaN(rowNumber) || rowNumber < 2 || + // 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; - } + Browser.msgBox('Error', + Utilities.formatString('Row "%s" is not valid.', selectedRow), + Browser.Buttons.OK); + return; + } - // Retrieve the addresses in that row. - var row = settingsSheet.getRange(rowNumber, 1, 1, 2); - var rowValues = row.getValues(); - var origin = rowValues[0][0]; - var 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. - var directions = getDirections_(origin, destination); + // 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; + } - // Create a new sheet and append the steps in the directions. - var sheetName = 'Driving Directions for Row ' + rowNumber; - var directionsSheet = spreadsheet.getSheetByName(sheetName); - if (directionsSheet) { - directionsSheet.clear(); - directionsSheet.activate(); - } else { - directionsSheet = + // 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()); - } - var sheetTitle = Utilities.formatString('Driving Directions from %s to %s', - origin, destination); - var headers = [ - [sheetTitle, '', ''], - ['Step', 'Distance (Meters)', 'Distance (Miles)'] - ]; - var newRows = []; - for (var i = 0; i < directions.routes[0].legs[0].steps.length; i++) { - var step = directions.routes[0].legs[0].steps[i]; - // Remove HTML tags from the instructions. - var 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])'); + } + 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'); - var stepsRange = directionsSheet.getDataRange() - .offset(2, 0, directionsSheet.getLastRow() - 2); - setAlternatingRowBackgroundColors_(stepsRange, '#ffffff', '#eeeeee'); - directionsSheet.setFrozenRows(2); - SpreadsheetApp.flush(); + // 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); + } } /** @@ -170,11 +185,11 @@ function generateStepByStep_() { * start of the range). */ function setAlternatingRowBackgroundColors_(range, oddColor, evenColor) { - var backgrounds = []; - for (var row = 1; row <= range.getNumRows(); row++) { - var rowBackgrounds = []; - for (var column = 1; column <= range.getNumColumns(); column++) { - if (row % 2 == 0) { + 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); @@ -194,13 +209,14 @@ function setAlternatingRowBackgroundColors_(range, oddColor, evenColor) { * @return {Object} The directions response object. */ function getDirections_(origin, destination) { - var directionFinder = Maps.newDirectionFinder(); + const directionFinder = Maps.newDirectionFinder(); directionFinder.setOrigin(origin); directionFinder.setDestination(destination); - var directions = directionFinder.getDirections(); + const directions = directionFinder.getDirections(); if (directions.status !== 'OK') { throw directions.error_message; } return directions; } // [END apps_script_sheets_custom_functions_quickstart] + diff --git a/sheets/forms/forms.gs b/sheets/forms/forms.gs index 455fb0196..1e803de5b 100644 --- a/sheets/forms/forms.gs +++ b/sheets/forms/forms.gs @@ -13,13 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + // [START apps_script_sheets_custom_form_responses_quickstart] /** * A special function that inserts a custom menu when the spreadsheet opens. */ function onOpen() { - var menu = [{name: 'Set up conference', functionName: 'setUpConference_'}]; - SpreadsheetApp.getActive().addMenu('Conference', menu); + const menu = [{name: 'Set up conference', functionName: 'setUpConference_'}]; + try { + SpreadsheetApp.getActive().addMenu('Conference', menu); + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); + } } /** @@ -31,15 +37,21 @@ function setUpConference_() { if (ScriptProperties.getProperty('calId')) { Browser.msgBox('Your conference is already set up. Look in Google Drive!'); } - var ss = SpreadsheetApp.getActive(); - var sheet = ss.getSheetByName('Conference Setup'); - var range = sheet.getDataRange(); - var values = range.getValues(); - setUpCalendar_(values, range); - setUpForm_(ss, values); - ScriptApp.newTrigger('onFormSubmit').forSpreadsheet(ss).onFormSubmit() - .create(); - ss.removeMenu('Conference'); + + try { + 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(); + ss.removeMenu('Conference'); + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); + } } /** @@ -49,21 +61,26 @@ function setUpConference_() { * @param {Range} range A spreadsheet range that contains conference data. */ function setUpCalendar_(values, range) { - var cal = CalendarApp.createCalendar('Conference Calendar'); - for (var i = 1; i < values.length; i++) { - var session = values[i]; - var title = session[0]; - var start = joinDateAndTime_(session[1], session[2]); - var end = joinDateAndTime_(session[1], session[3]); - var options = {location: session[4], sendInvites: true}; - var event = cal.createEvent(title, start, end, options) - .setGuestsCanSeeGuests(false); - session[5] = event.getId(); - } - range.setValues(values); + try { + const cal = CalendarApp.createCalendar('Conference Calendar'); + for (var 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); - // Store the ID for the Calendar, which is needed to retrieve events by ID. - ScriptProperties.setProperty('calId', cal.getId()); + // Store the ID for the Calendar, which is needed to retrieve events by ID. + ScriptProperties.setProperty('calId', cal.getId()); + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); + } } /** @@ -89,11 +106,11 @@ function joinDateAndTime_(date, time) { */ function setUpForm_(ss, values) { // Group the sessions by date and time so that they can be passed to the form. - var schedule = {}; - for (var i = 1; i < values.length; i++) { - var session = values[i]; - var day = session[1].toLocaleDateString(); - var time = session[2].toLocaleTimeString(); + const schedule = {}; + 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] = {}; } @@ -103,17 +120,23 @@ function setUpForm_(ss, values) { schedule[day][time].push(session[0]); } - // Create the form and add a multiple-choice question for each timeslot. - var 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 (var day in schedule) { - var header = form.addSectionHeaderItem().setTitle('Sessions for ' + day); - for (var time in schedule[day]) { - var item = form.addMultipleChoiceItem().setTitle(time + ' ' + day) - .setChoiceValues(schedule[day][time]); + try { + // Create the form and add 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 schedule) { + const header = form.addSectionHeaderItem().setTitle( + 'Sessions for ' + day); + for (const time of schedule[day]) { + const item = form.addMultipleChoiceItem().setTitle(time + ' ' + day) + .setChoiceValues(schedule[day][time]); + } } + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); } } @@ -125,27 +148,33 @@ function setUpForm_(ss, values) { * see https://developers.google.com/apps-script/understanding_events */ function onFormSubmit(e) { - var user = {name: e.namedValues['Name'][0], email: e.namedValues['Email'][0]}; + 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. - var response = []; - var values = SpreadsheetApp.getActive().getSheetByName('Conference Setup') - .getDataRange().getValues(); - for (var i = 1; i < values.length; i++) { - var session = values[i]; - var title = session[0]; - var day = session[1].toLocaleDateString(); - var time = session[2].toLocaleTimeString(); - var timeslot = time + ' ' + day; + const response = []; + try { + 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); + // 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); + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); } - sendInvites_(user, response); - sendDoc_(user, response); } /** @@ -154,10 +183,15 @@ function onFormSubmit(e) { * @param {Array} response An array of data for the user's session choices. */ function sendInvites_(user, response) { - var id = ScriptProperties.getProperty('calId'); - var cal = CalendarApp.getCalendarById(id); - for (var i = 0; i < response.length; i++) { - cal.getEventSeriesById(response[i][5]).addGuest(user.email); + try { + 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); + } + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); } } @@ -167,26 +201,32 @@ function sendInvites_(user, response) { * @param {Array} response An array of data for the user's session choices. */ function sendDoc_(user, response) { - var doc = DocumentApp.create('Conference Itinerary for ' + user.name) - .addEditor(user.email); - var body = doc.getBody(); - var table = [['Session', 'Date', 'Time', 'Location']]; - for (var i = 0; i < response.length; i++) { - table.push([response[i][0], response[i][1].toLocaleDateString(), + try { + 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(); + } + body.insertParagraph(0, doc.getName()) + .setHeading(DocumentApp.ParagraphHeading.HEADING1); + table = body.appendTable(table); + table.getRow(0).editAsText().setBold(true); + doc.saveAndClose(); - // Email 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) - }); + // Email 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) + }); + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); + } } // [END apps_script_sheets_custom_form_responses_quickstart] + diff --git a/sheets/next18/LinkDialog.html b/sheets/next18/LinkDialog.html index 19bc52706..669aa8980 100644 --- a/sheets/next18/LinkDialog.html +++ b/sheets/next18/LinkDialog.html @@ -1,4 +1,20 @@ + + diff --git a/sheets/next18/appsscript.json b/sheets/next18/appsscript.json index 5ea5d8feb..cccf3c15e 100644 --- a/sheets/next18/appsscript.json +++ b/sheets/next18/appsscript.json @@ -14,6 +14,6 @@ "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", + "https://www.googleapis.com/auth/presentations" ] } diff --git a/sheets/quickstart/quickstart.gs b/sheets/quickstart/quickstart.gs index 0519504e1..917be5ed9 100644 --- a/sheets/quickstart/quickstart.gs +++ b/sheets/quickstart/quickstart.gs @@ -18,19 +18,27 @@ * Creates a Sheets API service object and prints the names and majors of * students in a sample spreadsheet: * https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get */ function logNamesAndMajors() { - var spreadsheetId = '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'; - var rangeName = 'Class Data!A2:E'; - var values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values; - if (!values) { - Logger.log('No data found.'); - } else { - Logger.log('Name, Major:'); - for (var row = 0; row < values.length; row++) { + const spreadsheetId = '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'; + const rangeName = 'Class Data!A2:E'; + try { + // Get the values from the spreadsheet using spreadsheetId and range. + const values = Sheets.Spreadsheets.Values.get(spreadsheetId, rangeName).values; + // Print the values from spreadsheet if values are available. + if (!values) { + console.log('No data found.'); + return; + } + console.log('Name, Major:'); + for (const row in values) { // Print columns A and E, which correspond to indices 0 and 4. - Logger.log(' - %s, %s', values[row][0], values[row][4]); + console.log(' - %s, %s', values[row][0], values[row][4]); } + } catch (err) { + // TODO (developer) - Handle Values.get() exception from Sheet API + console.log(err.message); } } // [END sheets_quickstart] diff --git a/sheets/removingDuplicates/removingDuplicates.gs b/sheets/removingDuplicates/removingDuplicates.gs index b403c8f64..384ad55f7 100644 --- a/sheets/removingDuplicates/removingDuplicates.gs +++ b/sheets/removingDuplicates/removingDuplicates.gs @@ -20,28 +20,21 @@ */ function removeDuplicates() { // [START apps_script_sheets_sheet] - var sheet = SpreadsheetApp.getActiveSheet(); - var data = sheet.getDataRange().getValues(); + const sheet = SpreadsheetApp.getActiveSheet(); + const data = sheet.getDataRange().getValues(); // [END apps_script_sheets_sheet] - // [START apps_script_sheets_new_data] - var newData = []; - // [END apps_script_sheets_new_data] - for (var i in data) { - var row = data[i]; - var duplicate = false; - for (var j in newData) { - if (row.join() == newData[j].join()) { - duplicate = true; - } - } + const uniqueData = {}; + for (let row of data) { + const key = row.join(); // [START apps_script_sheets_duplicate] - if (!duplicate) { - newData.push(row); - } + uniqueData[key] = uniqueData[key] || row; // [END apps_script_sheets_duplicate] } // [START apps_script_sheets_clear] sheet.clearContents(); + // [START apps_script_sheets_new_data] + const newData = Object.values(uniqueData); + // [END apps_script_sheets_new_data] sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData); // [END apps_script_sheets_clear] } diff --git a/slides/SpeakerNotesScript/README.md b/slides/SpeakerNotesScript/README.md index 521bbf425..51e78a3b8 100644 --- a/slides/SpeakerNotesScript/README.md +++ b/slides/SpeakerNotesScript/README.md @@ -11,7 +11,7 @@ To run this add-on, first go to your Google slides with Speaker Notes. **Tools > Script editor**. If you are presented with a welcome screen, click **Blank Project**. 1. Delete any code in the script editor and rename `Code.gs` to `scriptGen.gs`. Copy and paste the contents of `scriptGen.gs` into this file. 1. Then select the menu item **View > Show manifest file** in your Script Editor screen. Copy and paste the contents of `appsscript.json` in here. You need 2 scopes to run this sample: - * To create and write a document: `https://www.googleapis.com/auth/documents` + * To create and write a document: `https://www.googleapis.com/auth/documents` * To read the current presentation: `https://www.googleapis.com/auth/presentations.currentonly` ## Try It Out diff --git a/slides/api/Helpers.gs b/slides/api/Helpers.gs index 7ae8dfe88..1c52172de 100644 --- a/slides/api/Helpers.gs +++ b/slides/api/Helpers.gs @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var filesToDelete = []; /** * Helper functions. @@ -31,11 +30,11 @@ Helpers.prototype.deleteFileOnCleanup = function(id) { }; Helpers.prototype.cleanup = function() { - filesToDelete.forEach(Drive.Files.remove); + this.filesToDelete.forEach(Drive.Files.remove); }; Helpers.prototype.createTestPresentation = function() { - var presentation = Slides.Presentations.create({ + const presentation = Slides.Presentations.create({ title: 'Test Preso' }); this.deleteFileOnCleanup(presentation.presentationId); @@ -43,9 +42,9 @@ Helpers.prototype.createTestPresentation = function() { }; Helpers.prototype.addSlides = function(presentationId, num, layout) { - var requests = []; - var slideIds = []; - for (var i = 0; i < num; ++i) { + let requests = []; + let slideIds = []; + for (let i = 0; i < num; ++i) { slideIds.push('slide_' + i); requests.push({ createSlide: { @@ -61,12 +60,12 @@ Helpers.prototype.addSlides = function(presentationId, num, layout) { }; Helpers.prototype.createTestTextbox = function(presentationId, pageId, callback) { - var boxId = 'MyTextBox_01'; - var pt350 = { + const boxId = 'MyTextBox_01'; + const pt350 = { magnitude: 350, unit: 'PT' }; - var requests = [{ + const requests = [{ createShape: { objectId: boxId, shapeType: 'TEXT_BOX', @@ -87,25 +86,25 @@ Helpers.prototype.createTestTextbox = function(presentationId, pageId, callback) } }, { insertText: { - objectId: boxId, - insertionIndex: 0, - text: 'New Box Text Inserted' + objectId: boxId, + insertionIndex: 0, + text: 'New Box Text Inserted' } }]; - var createTextboxResponse = Slides.Presentations.batchUpdate({ + const createTextboxResponse = Slides.Presentations.batchUpdate({ requests: requests }, presentationId); return createTextboxResponse.replies[0].createShape.objectId; }; Helpers.prototype.createTestSheetsChart = function(presentationId, pageId, - spreadsheetId, sheetChartId, callback) { - var chartId = 'MyChart_01'; - var emu4M = { + spreadsheetId, sheetChartId, callback) { + const chartId = 'MyChart_01'; + const emu4M = { magnitude: 4000000, unit: 'EMU' }; - var requests = [{ + const requests = [{ createSheetsChart: { objectId: chartId, spreadsheetId: spreadsheetId, @@ -127,8 +126,8 @@ Helpers.prototype.createTestSheetsChart = function(presentationId, pageId, } } }]; - var createSheetsChartResponse = Slides.Presentations.batchUpdate({ + const createSheetsChartResponse = Slides.Presentations.batchUpdate({ requests: requests }, presentationId); return createSheetsChartResponse.replies[0].createSheetsChart.objectId; -}; +}; \ No newline at end of file diff --git a/slides/api/Snippets.gs b/slides/api/Snippets.gs index 8ffe32459..0412a1486 100644 --- a/slides/api/Snippets.gs +++ b/slides/api/Snippets.gs @@ -13,42 +13,65 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var title = 'my title'; +const title = 'my title'; +// [START slides_create_presentation] /** - * Snippet class for the Google Slides Advanced Service. + * Creates a presentation + * @returns {*} the created presentation */ -function Snippets() {} +function createPresentation() { + try { + const presentation = Slides.Presentations.create({ + title: title + }); + console.log('Created presentation with ID: %s', presentation.presentationId); -Snippets.prototype.createPresentation = function() { - // [START slides_create_presentation] - var presentation = Slides.Presentations.create({ - title: title - }); - console.log('Created presentation with ID: %s', presentation.presentationId); - // [END slides_create_presentation] - return presentation; + return presentation; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_create_presentation] -Snippets.prototype.copyPresentation = function() { - var presentationId = this.createPresentation().presentationId; - var copyTitle = 'Copy Title'; - // [START slides_copy_presentation] - var copyFile = { +// [START slides_copy_presentation] +/** + * create a presentation and copy it + * @param {string} presentationId - ID of presentation to copy + * @returns {*} the copy's presentation id + */ +function copyPresentation(presentationId) { + const copyTitle = 'Copy Title'; + + let copyFile = { title: copyTitle, parents: [{id: 'root'}] }; - copyFile = Drive.Files.copy(copyFile, presentationId); - var presentationCopyId = copyFile.id; - // [END slides_copy_presentation] - return presentationCopyId; + try { + copyFile = Drive.Files.copy(copyFile, presentationId); + // (optional) copyFile.id can be returned directly + const presentationCopyId = copyFile.id; + + return presentationCopyId; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_copy_presentation] -Snippets.prototype.createSlide = function(presentationId, pageId) { - // [START slides_create_slide] +// [START slides_create_slide] +/** + * Creates a slide + * @param {string} presentationId + * @param {string} pageId + * @returns {*} + */ +function createSlide(presentationId, pageId) { // See Presentation.insertSlide(...) to learn how to add a slide using SlidesApp. // http://developers.google.com/apps-script/reference/slides/presentation#appendslidelayout - var requests = [{ + const requests = [{ createSlide: { objectId: pageId, insertionIndex: '1', @@ -62,70 +85,92 @@ Snippets.prototype.createSlide = function(presentationId, pageId) { // using the pageId. // Execute the request. - var createSlideResponse = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationId); - console.log('Created slide with ID: %s', createSlideResponse.replies[0].createSlide.objectId); - // [END slides_create_slide] - return createSlideResponse; + try { + const createSlideResponse = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + console.log('Created slide with ID: %s', createSlideResponse.replies[0].createSlide.objectId); + // [END slides_create_slide] + return createSlideResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; -Snippets.prototype.createTextboxWithText = function(presentationId, pageId) { - // [START slides_create_textbox_with_text] - // Create a new square textbox, using the supplied element ID. - var elementId = 'MyTextBox_01'; - var pt350 = { +// [START slides_create_textbox_with_text] +/** + * Create a new square textbox, using the supplied element ID. + * @param {string} presentationId + * @param {string} pageId + * @returns {*} + */ +function createTextboxWithText(presentationId, pageId) { + const elementId = 'MyTextBox_01'; + const pt350 = { magnitude: 350, unit: 'PT' }; - var requests = [{ - createShape: { - objectId: elementId, - shapeType: 'TEXT_BOX', - elementProperties: { - pageObjectId: pageId, - size: { - height: pt350, - width: pt350 - }, - transform: { - scaleX: 1, - scaleY: 1, - translateX: 350, - translateY: 100, - unit: 'PT' + const requests = [ + { + createShape: { + objectId: elementId, + shapeType: 'TEXT_BOX', + elementProperties: { + pageObjectId: pageId, + size: { + height: pt350, + width: pt350 + }, + transform: { + scaleX: 1, + scaleY: 1, + translateX: 350, + translateY: 100, + unit: 'PT' + } } } + }, + // Insert text into the box, using the supplied element ID. + { + insertText: { + objectId: elementId, + insertionIndex: 0, + text: 'New Box Text Inserted!' + } } - }, - - // Insert text into the box, using the supplied element ID. - { - insertText: { - objectId: elementId, - insertionIndex: 0, - text: 'New Box Text Inserted!' - } - }]; + ]; // Execute the request. - var createTextboxWithTextResponse = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationId); - var createShapeResponse = createTextboxWithTextResponse.replies[0].createShape; - console.log('Created textbox with ID: %s', createShapeResponse.objectId); - // [END slides_create_textbox_with_text] - return createTextboxWithTextResponse; + try { + const createTextboxWithTextResponse = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + const createShapeResponse = createTextboxWithTextResponse.replies[0].createShape; + console.log('Created textbox with ID: %s', createShapeResponse.objectId); + + return createTextboxWithTextResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_create_textbox_with_text] -Snippets.prototype.createImage = function(presentationId, pageId) { - // [START slides_create_image] - // Create a new image, using the supplied object ID, with content downloaded from imageUrl. - var requests = []; - var imageId = 'MyImage_01'; - var imageUrl = 'https://www.google.com/images/branding/googlelogo/2x/' + +// [START slides_create_image] +/** + * Create a new image, using the supplied object ID, with content downloaded from imageUrl. + * @param {string} presentationId + * @param {string} pageId + * @returns {*} + */ +function createImage(presentationId, pageId) { + let requests = []; + const imageId = 'MyImage_01'; + const imageUrl = 'https://www.google.com/images/branding/googlelogo/2x/' + 'googlelogo_color_272x92dp.png'; - var emu4M = { + const emu4M = { magnitude: 4000000, unit: 'EMU' }; @@ -151,140 +196,175 @@ Snippets.prototype.createImage = function(presentationId, pageId) { }); // Execute the request. - var response = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationId); - - var createImageResponse = response.replies; - console.log('Created image with ID: %s', createImageResponse[0].createImage.objectId); - // [END slides_create_image] - return createImageResponse; + try { + const response = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + + const createImageResponse = response.replies; + console.log('Created image with ID: %s', createImageResponse[0].createImage.objectId); + + return createImageResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_create_image] -Snippets.prototype.textMerging = function(templatePresentationId, dataSpreadsheetId) { - var responses = []; - // [START slides_text_merging] - // Use the Sheets API to load data, one record per row. - var dataRangeNotation = 'Customers!A2:M6'; - var values = SpreadsheetApp.openById(dataSpreadsheetId).getRange(dataRangeNotation).getValues(); - - // For each record, create a new merged presentation. - for (var i = 0; i < values.length; ++i) { - var row = values[i]; - var customerName = row[2]; // name in column 3 - var caseDescription = row[5]; // case description in column 6 - var totalPortfolio = row[11]; // total portfolio in column 12 - - // Duplicate the template presentation using the Drive API. - var copyTitle = customerName + ' presentation'; - var copyFile = { - title: copyTitle, - parents: [{id: 'root'}] - }; - copyFile = Drive.Files.copy(copyFile, templatePresentationId); - var presentationCopyId = copyFile.id; +// [START slides_text_merging] +/** + * Use the Sheets API to load data, one record per row. + * @param {string} templatePresentationId + * @param {string} dataSpreadsheetId + * @returns {*[]} + */ +function textMerging(templatePresentationId, dataSpreadsheetId) { + let responses = []; + const dataRangeNotation = 'Customers!A2:M6'; + try { + let values = SpreadsheetApp.openById(dataSpreadsheetId).getRange(dataRangeNotation).getValues(); - // Create the text merge (replaceAllText) requests for this presentation. - requests = [{ - replaceAllText: { - containsText: { - text: '{{customer-name}}', - matchCase: true - }, - replaceText: customerName + // For each record, create a new merged presentation. + for (let i = 0; i < values.length; ++i) { + const row = values[i]; + const customerName = row[2]; // name in column 3 + const caseDescription = row[5]; // case description in column 6 + const totalPortfolio = row[11]; // total portfolio in column 12 + + // Duplicate the template presentation using the Drive API. + const copyTitle = customerName + ' presentation'; + let copyFile = { + title: copyTitle, + parents: [{id: 'root'}] + }; + copyFile = Drive.Files.copy(copyFile, templatePresentationId); + const presentationCopyId = copyFile.id; + + // Create the text merge (replaceAllText) requests for this presentation. + const requests = [{ + replaceAllText: { + containsText: { + text: '{{customer-name}}', + matchCase: true + }, + replaceText: customerName + } + }, { + replaceAllText: { + containsText: { + text: '{{case-description}}', + matchCase: true + }, + replaceText: caseDescription + } + }, { + replaceAllText: { + containsText: { + text: '{{total-portfolio}}', + matchCase: true + }, + replaceText: totalPortfolio + '' + } + }]; + + // Execute the requests for this presentation. + const result = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationCopyId); + // Count the total number of replacements made. + let numReplacements = 0; + result.replies.forEach(function(reply) { + numReplacements += reply.replaceAllText.occurrencesChanged; + }); + console.log('Created presentation for %s with ID: %s', customerName, presentationCopyId); + console.log('Replaced %s text instances', numReplacements); + // [START_EXCLUDE silent] + responses.push(result.replies); + if (responses.length === values.length) { // return for the last value + return responses; } - }, { - replaceAllText: { + // [END_EXCLUDE] + } + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } +}; +// [END slides_text_merging] + +// [START slides_image_merging] +/** + * Duplicate the template presentation using the Drive API. + * @param {string} templatePresentationId + * @param {string} imageUrl + * @param {string} customerName + * @returns {*} + */ +function imageMerging(templatePresentationId, imageUrl, customerName) { + const logoUrl = imageUrl; + const customerGraphicUrl = imageUrl; + + const copyTitle = customerName + ' presentation'; + let copyFile = { + title: copyTitle, + parents: [{id: 'root'}] + }; + + try { + copyFile = Drive.Files.copy(copyFile, templatePresentationId); + const presentationCopyId = copyFile.id; + + // Create the image merge (replaceAllShapesWithImage) requests. + const requests = [{ + replaceAllShapesWithImage: { + imageUrl: logoUrl, + imageReplaceMethod: 'CENTER_INSIDE', containsText: { - text: '{{case-description}}', + text: '{{company-logo}}', matchCase: true - }, - replaceText: caseDescription + } } }, { - replaceAllText: { + replaceAllShapesWithImage: { + imageUrl: customerGraphicUrl, + imageReplaceMethod: 'CENTER_INSIDE', containsText: { - text: '{{total-portfolio}}', + text: '{{customer-graphic}}', matchCase: true - }, - replaceText: totalPortfolio + '' + } } }]; // Execute the requests for this presentation. - var result = Slides.Presentations.batchUpdate({ + let batchUpdateResponse = Slides.Presentations.batchUpdate({ requests: requests }, presentationCopyId); - // Count the total number of replacements made. - var numReplacements = 0; - result.replies.forEach(function(reply) { - numReplacements += reply.replaceAllText.occurrencesChanged; + let numReplacements = 0; + batchUpdateResponse.replies.forEach(function(reply) { + numReplacements += reply.replaceAllShapesWithImage.occurrencesChanged; }); - console.log('Created presentation for %s with ID: %s', customerName, presentationCopyId); - console.log('Replaced %s text instances', numReplacements); - // [START_EXCLUDE silent] - responses.push(result.replies); - if (responses.length === values.length) { // return for the last value - return responses; - } - // [END_EXCLUDE] - } - // [END slides_text_merging] -}; - -Snippets.prototype.imageMerging = function(templatePresentationId, imageUrl, customerName) { - var logoUrl = imageUrl; - var customerGraphicUrl = imageUrl; - - // [START slides_image_merging] - // Duplicate the template presentation using the Drive API. - var copyTitle = customerName + ' presentation'; - var copyFile = { - title: copyTitle, - parents: [{id: 'root'}] - }; - copyFile = Drive.Files.copy(copyFile, templatePresentationId); - var presentationCopyId = copyFile.id; - - // Create the image merge (replaceAllShapesWithImage) requests. - var requests = [{ - replaceAllShapesWithImage: { - imageUrl: logoUrl, - imageReplaceMethod: 'CENTER_INSIDE', - containsText: { - text: '{{company-logo}}', - matchCase: true - } - } - }, { - replaceAllShapesWithImage: { - imageUrl: customerGraphicUrl, - imageReplaceMethod: 'CENTER_INSIDE', - containsText: { - text: '{{customer-graphic}}', - matchCase: true - } - } - }]; + console.log('Created merged presentation with ID: %s', presentationCopyId); + console.log('Replaced %s shapes with images.', numReplacements); - // Execute the requests for this presentation. - var batchUpdateResponse = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationCopyId); - var numReplacements = 0; - batchUpdateResponse.replies.forEach(function(reply) { - numReplacements += reply.replaceAllShapesWithImage.occurrencesChanged; - }); - console.log('Created merged presentation with ID: %s', presentationCopyId); - console.log('Replaced %s shapes with images.', numReplacements); - // [END slides_image_merging] - return batchUpdateResponse; + return batchUpdateResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_image_merging] -Snippets.prototype.simpleTextReplace = function(presentationId, shapeId, replacementText) { - // [START slides_simple_text_replace] - // Remove existing text in the shape, then insert new text. - var requests = [{ +// [START slides_simple_text_replace] +/** + * Remove existing text in the shape, then insert new text. + * @param {string} presentationId + * @param {string?} shapeId + * @param {string} replacementText + * @returns {*} + */ +function simpleTextReplace(presentationId, shapeId, replacementText) { + const requests = [{ deleteText: { objectId: shapeId, textRange: { @@ -298,21 +378,33 @@ Snippets.prototype.simpleTextReplace = function(presentationId, shapeId, replace text: replacementText } }]; + // Execute the requests. - var batchUpdateResponse = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationId); - console.log('Replaced text in shape with ID: %s', shapeId); - // [END slides_simple_text_replace] - return batchUpdateResponse; + try { + const batchUpdateResponse = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + console.log('Replaced text in shape with ID: %s', shapeId); + + return batchUpdateResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_simple_text_replace] -Snippets.prototype.textStyleUpdate = function(presentationId, shapeId) { - // [START slides_text_style_update] - // Update the text style so that the first 5 characters are bolded - // and italicized, the next 5 are displayed in blue 14 pt Times - // New Roman font, and the next 5 are hyperlinked. - var requests = [{ +// [START slides_text_style_update] +/** + * Update the text style so that the first 5 characters are bolded + * and italicized, the next 5 are displayed in blue 14 pt Times + * New Roman font, and the next 5 are hyperlinked. + * @param {string} presentationId + * @param {string} shapeId + * @returns {*} + */ +function textStyleUpdate(presentationId, shapeId) { + const requests = [{ updateTextStyle: { objectId: shapeId, textRange: { @@ -370,18 +462,26 @@ Snippets.prototype.textStyleUpdate = function(presentationId, shapeId) { }]; // Execute the requests. - var batchUpdateResponse = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationId); - console.log('Updated the text style for shape with ID: %s', shapeId); - // [END slides_text_style_update] - return batchUpdateResponse; + try { + const batchUpdateResponse = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + console.log('Updated the text style for shape with ID: %s', shapeId); + + return batchUpdateResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_text_style_update] -Snippets.prototype.createBulletedText = function(presentationId, shapeId) { - // [START slides_create_bulleted_text] - // Add arrow-diamond-disc bullets to all text in the shape. - var requests = [{ +// [START slides_create_bulleted_text] +/** + * Add arrow-diamond-disc bullets to all text in the shape. + */ +function createBulletedText(presentationId, shapeId) { + const requests = [{ createParagraphBullets: { objectId: shapeId, textRange: { @@ -392,25 +492,38 @@ Snippets.prototype.createBulletedText = function(presentationId, shapeId) { }]; // Execute the requests. - var batchUpdateResponse = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationId); - console.log('Added bullets to text in shape with ID: %s', shapeId); - // [END slides_create_bulleted_text] - return batchUpdateResponse; + try { + const batchUpdateResponse = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + console.log('Added bullets to text in shape with ID: %s', shapeId); + + return batchUpdateResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_create_bulleted_text] -Snippets.prototype.createSheetsChart = function(presentationId, pageId, shapeId, sheetChartId) { - // [START slides_create_sheets_chart] - // Embed a Sheets chart (indicated by the spreadsheetId and sheetChartId) onto - // a page in the presentation. Setting the linking mode as 'LINKED' allows the - // chart to be refreshed if the Sheets version is updated. - var emu4M = { +// [START slides_create_sheets_chart] +/** + * Embed a Sheets chart (indicated by the spreadsheetId and sheetChartId) onto + * a page in the presentation. Setting the linking mode as 'LINKED' allows the + * chart to be refreshed if the Sheets version is updated. + * @param {string} presentationId + * @param {string} pageId + * @param {string} shapeId + * @param {string} sheetChartId + * @returns {*} + */ +function createSheetsChart(presentationId, pageId, shapeId, sheetChartId) { + const emu4M = { magnitude: 4000000, unit: 'EMU' }; - var presentationChartId = 'MyEmbeddedChart'; - var requests = [{ + const presentationChartId = 'MyEmbeddedChart'; + const requests = [{ createSheetsChart: { objectId: presentationChartId, spreadsheetId: shapeId, @@ -434,27 +547,45 @@ Snippets.prototype.createSheetsChart = function(presentationId, pageId, shapeId, }]; // Execute the request. - var batchUpdateResponse = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationId); - console.log('Added a linked Sheets chart with ID: %s', presentationChartId); - // [END slides_create_sheets_chart] - return batchUpdateResponse; + try { + const batchUpdateResponse = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + console.log('Added a linked Sheets chart with ID: %s', presentationChartId); + + return batchUpdateResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_create_sheets_chart] -Snippets.prototype.refreshSheetsChart = function(presentationId, presentationChartId) { - // [START slides_refresh_sheets_chart] - var requests = [{ +// [START slides_refresh_sheets_chart] +/** + * Refresh the sheets charts + * @param {string} presentationId + * @param {string} presentationChartId + * @returns {*} + */ +function refreshSheetsChart(presentationId, presentationChartId) { + const requests = [{ refreshSheetsChart: { objectId: presentationChartId } }]; // Execute the request. - var batchUpdateResponse = Slides.Presentations.batchUpdate({ - requests: requests - }, presentationId); - console.log('Refreshed a linked Sheets chart with ID: %s', presentationChartId); - // [END slides_refresh_sheets_chart] - return batchUpdateResponse; + try { + const batchUpdateResponse = Slides.Presentations.batchUpdate({ + requests: requests + }, presentationId); + console.log('Refreshed a linked Sheets chart with ID: %s', presentationChartId); + + return batchUpdateResponse; + } catch (err) { + // TODO (Developer) - Handle exception + console.log('Failed with error: %s', err.error); + } }; +// [END slides_refresh_sheets_chart] diff --git a/slides/api/Tests.gs b/slides/api/Tests.gs index 448c85510..de5046ba7 100644 --- a/slides/api/Tests.gs +++ b/slides/api/Tests.gs @@ -13,16 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -var snippets = new Snippets(); -var helpers = new Helpers(); +const helpers = new Helpers(); // Constants -var IMAGE_URL = +const IMAGE_URL = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'; -var TEMPLATE_PRESENTATION_ID = '1iwq83aR9SIQbqVY-3ozLkJjKhdXErfS_m3zD8VZhtFA'; -var DATA_SPREADSHEET_ID = '1Y3GVGdJHDzlyMB9aLDWV2o_e2RstzUHK1iLDaBgbMwc'; -var CHART_ID = 1107320627; -var CUSTOMER_NAME = 'Fake Customer'; +const TEMPLATE_PRESENTATION_ID = '1iwq83aR9SIQbqVY-3ozLkJjKhdXErfS_m3zD8VZhtFA'; +const DATA_SPREADSHEET_ID = '1Y3GVGdJHDzlyMB9aLDWV2o_e2RstzUHK1iLDaBgbMwc'; +const CHART_ID = 1107320627; +const CUSTOMER_NAME = 'Fake Customer'; /** * A simple existance assertion. Logs if the value is falsy. @@ -68,7 +67,7 @@ function RUN_ALL_TESTS() { */ function itShouldCreateAPresentation() { console.log('> itShouldCreateAPresentation'); - var presentation = snippets.createPresentation(); + const presentation = createPresentation(); expectToExist(presentation.presentationId); helpers.deleteFileOnCleanup(presentation.presentationId); } @@ -78,8 +77,8 @@ function itShouldCreateAPresentation() { */ function itShouldCopyAPresentation() { console.log('> itShouldCopyAPresentation'); - var presentationId = helpers.createTestPresentation(); - var copyId = snippets.copyPresentation(presentationId, 'My Duplicate, Presentation'); + const presentationId = helpers.createTestPresentation(); + const copyId = copyPresentation(presentationId, 'My Duplicate, Presentation'); expectToExist(copyId); helpers.deleteFileOnCleanup(copyId); } @@ -89,10 +88,10 @@ function itShouldCopyAPresentation() { */ function itShouldCreateASlide() { console.log('> itShouldCreateASlide'); - var presentationId = helpers.createTestPresentation(); + const presentationId = helpers.createTestPresentation(); helpers.addSlides(presentationId, 3, 'TITLE_AND_TWO_COLUMNS'); - var pageId = 'my_page_id'; - var response = snippets.createSlide(presentationId, pageId); + const pageId = 'my_page_id'; + const response = createSlide(presentationId, pageId); expectToExist(response.replies[0].createSlide.objectId); } @@ -101,12 +100,12 @@ function itShouldCreateASlide() { */ function itShouldCreateATextboxWithText() { console.log('> itShouldCreateATextboxWithText'); - var presentationId = helpers.createTestPresentation(); - var ids = helpers.addSlides(presentationId, 3, 'TITLE_AND_TWO_COLUMNS'); - var pageId = ids[0]; - var response = snippets.createTextboxWithText(presentationId, pageId); + const presentationId = helpers.createTestPresentation(); + const ids = helpers.addSlides(presentationId, 3, 'TITLE_AND_TWO_COLUMNS'); + const pageId = ids[0]; + const response = createTextboxWithText(presentationId, pageId); expectToEqual(2, response.replies.length); - var boxId = response.replies[0].createShape.objectId; + const boxId = response.replies[0].createShape.objectId; expectToExist(boxId); } @@ -115,12 +114,12 @@ function itShouldCreateATextboxWithText() { */ function itShouldCreateAnImage() { console.log('> itShouldCreateAnImage'); - var presentationId = helpers.createTestPresentation(); - var ids = helpers.addSlides(presentationId, 1, 'BLANK'); - var pageId = ids[0]; - var response = snippets.createImage(presentationId, pageId); + const presentationId = helpers.createTestPresentation(); + const ids = helpers.addSlides(presentationId, 1, 'BLANK'); + const pageId = ids[0]; + const response = createImage(presentationId, pageId); expectToEqual(1, response.length); - var imageId = response[0].createImage.objectId; + const imageId = response[0].createImage.objectId; expectToExist(imageId); } @@ -129,10 +128,10 @@ function itShouldCreateAnImage() { */ function itShouldMergeText() { console.log('> itShouldMergeText'); - var responses = snippets.textMerging(TEMPLATE_PRESENTATION_ID, DATA_SPREADSHEET_ID); + let responses = textMerging(TEMPLATE_PRESENTATION_ID, DATA_SPREADSHEET_ID); expectToEqual(5, responses.length); responses.forEach(function(response) { - var numReplacements = 0; + let numReplacements = 0; response.forEach(function(res) { numReplacements += res.replaceAllText.occurrencesChanged; }); @@ -145,9 +144,9 @@ function itShouldMergeText() { */ function itShouldImageMerge() { console.log('> itShouldImageMerge'); - var response = snippets.imageMerging(TEMPLATE_PRESENTATION_ID, IMAGE_URL, CUSTOMER_NAME); + let response = imageMerging(TEMPLATE_PRESENTATION_ID, IMAGE_URL, CUSTOMER_NAME); expectToEqual(2, response.replies.length); - var numReplacements = 0; + let numReplacements = 0; response.replies.forEach(function(reply) { numReplacements += reply.replaceAllShapesWithImage.occurrencesChanged; }); @@ -159,11 +158,11 @@ function itShouldImageMerge() { */ function itShouldSimpleTextReplace() { console.log('> itShouldSimpleTextReplace'); - var presentationId = helpers.createTestPresentation(); - var pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); - var pageId = pageIds[0]; - var boxId = helpers.createTestTextbox(presentationId, pageId); - var response = snippets.simpleTextReplace(presentationId, boxId, 'MY NEW TEXT'); + const presentationId = helpers.createTestPresentation(); + const pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); + const pageId = pageIds[0]; + const boxId = helpers.createTestTextbox(presentationId, pageId); + const response = simpleTextReplace(presentationId, boxId, 'MY NEW TEXT'); expectToEqual(2, response.replies.length); } @@ -172,11 +171,11 @@ function itShouldSimpleTextReplace() { */ function itShouldTextStyleUpdate() { console.log('> itShouldTextStyleUpdate'); - var presentationId = helpers.createTestPresentation(); - var pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); - var pageId = pageIds[0]; - var boxId = helpers.createTestTextbox(presentationId, pageId); - var response = snippets.textStyleUpdate(presentationId, boxId); + const presentationId = helpers.createTestPresentation(); + const pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); + const pageId = pageIds[0]; + const boxId = helpers.createTestTextbox(presentationId, pageId); + const response = textStyleUpdate(presentationId, boxId); expectToEqual(3, response.replies.length); } @@ -185,11 +184,11 @@ function itShouldTextStyleUpdate() { */ function itShouldCreateBulletedText() { console.log('> itShouldCreateBulletedText'); - var presentationId = helpers.createTestPresentation(); - var pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); - var pageId = pageIds[0]; - var boxId = helpers.createTestTextbox(presentationId, pageId); - var response = snippets.createBulletedText(presentationId, boxId); + const presentationId = helpers.createTestPresentation(); + const pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); + const pageId = pageIds[0]; + const boxId = helpers.createTestTextbox(presentationId, pageId); + const response = createBulletedText(presentationId, boxId); expectToEqual(1, response.replies.length); } @@ -198,12 +197,12 @@ function itShouldCreateBulletedText() { */ function itShouldCreateSheetsChart() { console.log('> itShouldCreateSheetsChart'); - var presentationId = helpers.createTestPresentation(); - var pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); - var pageId = pageIds[0]; - var response = snippets.createSheetsChart(presentationId, pageId, DATA_SPREADSHEET_ID, CHART_ID); + const presentationId = helpers.createTestPresentation(); + const pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); + const pageId = pageIds[0]; + const response = createSheetsChart(presentationId, pageId, DATA_SPREADSHEET_ID, CHART_ID); expectToEqual(1, response.replies.length); - var chartId = response.replies[0].createSheetsChart.objectId; + const chartId = response.replies[0].createSheetsChart.objectId; expectToExist(chartId); } @@ -212,11 +211,11 @@ function itShouldCreateSheetsChart() { */ function itShouldRefreshSheetsChart() { console.log('> itShouldRefreshSheetsChart'); - var presentationId = helpers.createTestPresentation(); - var pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); - var pageId = pageIds[0]; - var sheetChartId = helpers.createTestSheetsChart(presentationId, pageId, DATA_SPREADSHEET_ID, + const presentationId = helpers.createTestPresentation(); + const pageIds = helpers.addSlides(presentationId, 1, 'BLANK'); + const pageId = pageIds[0]; + const sheetChartId = helpers.createTestSheetsChart(presentationId, pageId, DATA_SPREADSHEET_ID, CHART_ID); - var response = snippets.refreshSheetsChart(presentationId, sheetChartId); + const response = refreshSheetsChart(presentationId, sheetChartId); expectToEqual(1, response.replies.length); -} +} \ No newline at end of file diff --git a/slides/progress/progress.gs b/slides/progress/progress.gs index 002da0289..f4fc1c9fd 100644 --- a/slides/progress/progress.gs +++ b/slides/progress/progress.gs @@ -17,8 +17,8 @@ /** * @OnlyCurrentDoc Adds progress bars to a presentation. */ -var BAR_ID = 'PROGRESS_BAR_ID'; -var BAR_HEIGHT = 10; // px +const BAR_ID = 'PROGRESS_BAR_ID'; +const BAR_HEIGHT = 10; // px /** * Runs when the add-on is installed. @@ -48,16 +48,16 @@ function onOpen(e) { */ function createBars() { deleteBars(); // Delete any existing progress bars - var presentation = SlidesApp.getActivePresentation(); - var slides = presentation.getSlides(); - for (var i = 0; i < slides.length; ++i) { - var ratioComplete = (i / (slides.length - 1)); - var x = 0; - var y = presentation.getPageHeight() - BAR_HEIGHT; - var barWidth = presentation.getPageWidth() * ratioComplete; + const presentation = SlidesApp.getActivePresentation(); + const slides = presentation.getSlides(); + for (let i = 0; i < slides.length; ++i) { + const ratioComplete = (i / (slides.length - 1)); + const x = 0; + const y = presentation.getPageHeight() - BAR_HEIGHT; + const barWidth = presentation.getPageWidth() * ratioComplete; if (barWidth > 0) { - var bar = slides[i].insertShape(SlidesApp.ShapeType.RECTANGLE, x, y, - barWidth, BAR_HEIGHT); + const bar = slides[i].insertShape(SlidesApp.ShapeType.RECTANGLE, x, y, + barWidth, BAR_HEIGHT); bar.getBorder().setTransparent(); bar.setLinkUrl(BAR_ID); } @@ -68,15 +68,14 @@ function createBars() { * Deletes all progress bar rectangles. */ function deleteBars() { - var presentation = SlidesApp.getActivePresentation(); - var slides = presentation.getSlides(); - for (var i = 0; i < slides.length; ++i) { - var elements = slides[i].getPageElements(); - for (var j = 0; j < elements.length; ++j) { - var el = elements[j]; + const presentation = SlidesApp.getActivePresentation(); + const slides = presentation.getSlides(); + for (let i = 0; i < slides.length; ++i) { + const elements = slides[i].getPageElements(); + for (const el of elements) { if (el.getPageElementType() === SlidesApp.PageElementType.SHAPE && - el.asShape().getLink() && - el.asShape().getLink().getUrl() === BAR_ID) { + el.asShape().getLink() && + el.asShape().getLink().getUrl() === BAR_ID) { el.remove(); } } diff --git a/slides/quickstart/quickstart.gs b/slides/quickstart/quickstart.gs index 35d8e958d..4093fe865 100644 --- a/slides/quickstart/quickstart.gs +++ b/slides/quickstart/quickstart.gs @@ -15,20 +15,24 @@ */ // [START slides_quickstart] /** - * Creates a Slides API service object and logs the number of slides and - * elements in a sample presentation: - * https://docs.google.com/presentation/d/1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc/edit - */ + * Creates a Slides API service object and logs the number of slides and + * elements in a sample presentation: + * https://docs.google.com/presentation/d/1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc/edit + */ function logSlidesAndElements() { - var presentationId = '1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc'; - var presentation = Slides.Presentations.get(presentationId); - var slides = presentation.slides; - Logger.log('The presentation contains %s slides:', slides.length); - for (i = 0; i < slides.length; i++) { - Logger.log( - '- Slide # %s contains %s elements.', - i + 1, - slides[i].pageElements.length); + const presentationId = '1EAYk18WDjIG-zp_0vLm3CsfQh_i8eXc67Jo2O9C6Vuc'; + try { + // Gets the specified presentation using presentationId + const presentation = Slides.Presentations.get(presentationId); + const slides = presentation.slides; + // Print the number of slides and elements in presentation + console.log('The presentation contains %s slides:', slides.length); + for ( let i = 0; i < slides.length; i++) { + console.log('- Slide # %s contains %s elements.', i + 1, slides[i].pageElements.length); + } + } catch (err) { + // TODO (developer) - Handle Presentation.get() exception from Slides API + console.log('Failed to found Presentation with error %s', err.message); } } // [END slides_quickstart] diff --git a/slides/selection/selection.gs b/slides/selection/selection.gs index ce055104e..c8acaf346 100644 --- a/slides/selection/selection.gs +++ b/slides/selection/selection.gs @@ -15,76 +15,88 @@ */ // [START apps_script_slides_get_selection] -var selection = SlidesApp.getActivePresentation().getSelection(); +const selection = SlidesApp.getActivePresentation().getSelection(); // [END apps_script_slides_get_selection] // [START apps_script_slides_get_current_page] -var currentPage = SlidesApp.getActivePresentation().getSelection().getCurrentPage(); +const currentPage = SlidesApp.getActivePresentation().getSelection().getCurrentPage(); // [END apps_script_slides_get_current_page] -// [START apps_script_slides_selection_type] -var selection = SlidesApp.getActivePresentation().getSelection(); -var selectionType = selection.getSelectionType(); -var currentPage; -switch (selectionType) { - case SlidesApp.SelectionType.NONE: - Logger.log('Nothing selected'); - break; - case SlidesApp.SelectionType.CURRENT_PAGE: - currentPage = selection.getCurrentPage(); - Logger.log('Selection is a page with ID: ' + currentPage.getObjectId()); - break; - case SlidesApp.SelectionType.PAGE_ELEMENT: - var pageElements = selection.getPageElementRange().getPageElements(); - Logger.log('There are ' + pageElements.length + ' page elements selected.'); - break; - case SlidesApp.SelectionType.TEXT: - var tableCellRange = selection.getTableCellRange(); - if (tableCellRange != null) { - var tableCell = tableCellRange.getTableCells()[0]; - Logger.log('Selected text is in a table at row ' + - tableCell.getRowIndex() + ', column ' + - tableCell.getColumnIndex()); - } - var textRange = selection.getTextRange(); - if (textRange.getStartIndex() == textRange.getEndIndex()) { - Logger.log('Text cursor position: ' + textRange.getStartIndex()); - } else { - Logger.log('Selection is a text range from: ' + textRange.getStartIndex() + ' to: ' + - textRange.getEndIndex() + ' is selected'); - } - break; - case SlidesApp.SelectionType.TABLE_CELL: - var tableCells = selection.getTableCellRange().getTableCells(); - var table = tableCells[0].getParentTable(); - Logger.log('There are ' + tableCells.length + ' table cells selected.'); - break; - case SlidesApp.SelectionType.PAGE: - var pages = selection.getPageRange().getPages(); - Logger.log('There are ' + pages.length + ' pages selected.'); - break; - default: - break; -} +/** + * Selection type to read the current selection in a type-appropriate way. + */ +function slidesSelectionTypes() { + // [START apps_script_slides_selection_type] + const selection = SlidesApp.getActivePresentation().getSelection(); + const selectionType = selection.getSelectionType(); + let currentPage; + switch (selectionType) { + case SlidesApp.SelectionType.NONE: + console.log('Nothing selected'); + break; + case SlidesApp.SelectionType.CURRENT_PAGE: + currentPage = selection.getCurrentPage(); + console.log('Selection is a page with ID: ' + currentPage.getObjectId()); + break; + case SlidesApp.SelectionType.PAGE_ELEMENT: + const pageElements = selection.getPageElementRange().getPageElements(); + console.log('There are ' + pageElements.length + ' page elements selected.'); + break; + case SlidesApp.SelectionType.TEXT: + const tableCellRange = selection.getTableCellRange(); + if (tableCellRange !== null) { + const tableCell = tableCellRange.getTableCells()[0]; + console.log('Selected text is in a table at row ' + + tableCell.getRowIndex() + ', column ' + + tableCell.getColumnIndex()); + } + const textRange = selection.getTextRange(); + if (textRange.getStartIndex() === textRange.getEndIndex()) { + console.log('Text cursor position: ' + textRange.getStartIndex()); + } else { + console.log('Selection is a text range from: ' + textRange.getStartIndex() + ' to: ' + + textRange.getEndIndex() + ' is selected'); + } + break; + case SlidesApp.SelectionType.TABLE_CELL: + const tableCells = selection.getTableCellRange().getTableCells(); + const table = tableCells[0].getParentTable(); + console.log('There are ' + tableCells.length + ' table cells selected.'); + break; + case SlidesApp.SelectionType.PAGE: + const pages = selection.getPageRange().getPages(); + console.log('There are ' + pages.length + ' pages selected.'); + break; + default: + break; + } // [END apps_script_slides_selection_type] - +} +/** + * Selecting the current page + */ +function slideSelect() { // [START apps_script_slides_select] // Select the first slide as the current page selection and remove any previous selection. -var selection = SlidesApp.getActivePresentation().getSelection(); -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -slide.selectAsCurrentPage(); + const selection = SlidesApp.getActivePresentation().getSelection(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + slide.selectAsCurrentPage(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.CURRENT_PAGE // selection.getCurrentPage() = slide // // [END apps_script_slides_select] - +} +/** + * Selecting a page element. + */ +function selectPageElement() { // [START apps_script_slides_select_page_element] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var pageElement = slide.getPageElements()[0]; -// Only select this page element and remove any previous selection. -pageElement.select(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const pageElement = slide.getPageElements()[0]; + // Only select this page element and remove any previous selection. + pageElement.select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT @@ -92,16 +104,20 @@ pageElement.select(); // selection.getPageElementRange().getPageElements()[0] = pageElement // // [END apps_script_slides_select_page_element] - -// [START apps_script_slides_select_multiple_page_elements] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -// First select the slide page, as the current page selection. -slide.selectAsCurrentPage(); -// Then select all the page elements in the selected slide page. -var pageElements = slide.getPageElements(); -for (var i = 0; i < pageElements.length; i++) { - pageElements[i].select(false); } +/** + * Selecting multiple page elements + */ +function selectMultiplePageElement() { +// [START apps_script_slides_select_multiple_page_elements] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + // First select the slide page, as the current page selection. + slide.selectAsCurrentPage(); + // Then select all the page elements in the selected slide page. + const pageElements = slide.getPageElements(); + for (let i = 0; i < pageElements.length; i++) { + pageElements[i].select(false); + } // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT @@ -109,22 +125,27 @@ for (var i = 0; i < pageElements.length; i++) { // selection.getPageElementRange().getPageElements() = pageElements // // [END apps_script_slides_select_multiple_page_elements] - +} +/** + *This shows how selection can be transformed by manipulating + * selected page elements. + */ +function slideTransformSelection() { // [START apps_script_slides_transform_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape1 = slide.getPageElements()[0].asShape(); -var shape2 = slide.getPageElements()[1].asShape(); -// Select both the shapes. -shape1.select(); -shape2.select(false); -// State of selection -// -// selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT -// selection.getCurrentPage() = slide -// selection.getPageElementRange().getPageElements() = [shape1, shape2] -// -// Remove one shape. -shape2.remove(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const shape1 = slide.getPageElements()[0].asShape(); + const shape2 = slide.getPageElements()[1].asShape(); + // Select both the shapes. + shape1.select(); + shape2.select(false); + // State of selection + // + // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT + // selection.getCurrentPage() = slide + // selection.getPageElementRange().getPageElements() = [shape1, shape2] + // + // Remove one shape. + shape2.remove(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.PAGE_ELEMENT @@ -132,13 +153,17 @@ shape2.remove(); // selection.getPageElementRange().getPageElements() = [shape1] // // [END apps_script_slides_transform_selection] - -// [START apps_script_slides_range_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.getPageElements()[0].asShape(); -shape.getText().setText('Hello'); -// Range selection: Select the text range 'He'. -shape.getText().getRange(0, 2).select(); +} +/** + * Range selection within text contained in a shape. + */ +function slidesRangeSelection() { +// [START apps_script_slides_range_selection_in_shape] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const shape = slide.getPageElements()[0].asShape(); + shape.getText().setText('Hello'); + // Range selection: Select the text range 'He'. + shape.getText().getRange(0, 2).select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.TEXT @@ -147,14 +172,18 @@ shape.getText().getRange(0, 2).select(); // selection.getTextRange().getStartIndex() = 0 // selection.getTextRange().getEndIndex() = 2 // -// [END apps_script_slides_range_selection] - -// [START apps_script_slides_cursor_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.getPageElements()[0].asShape(); -shape.getText().setText('Hello'); -// Cursor selection: Place the cursor after 'H' like 'H|ello'. -shape.getText().getRange(1, 1).select(); +// [END apps_script_slides_range_selection_in_shape] +} +/** + * Cursor selection within text contained in a shape. + */ +function slidesCursorSelection() { +// [START apps_script_slides_cursor_selection_in_shape] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const shape = slide.getPageElements()[0].asShape(); + shape.getText().setText('Hello'); + // Cursor selection: Place the cursor after 'H' like 'H|ello'. + shape.getText().getRange(1, 1).select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.TEXT @@ -163,15 +192,19 @@ shape.getText().getRange(1, 1).select(); // selection.getTextRange().getStartIndex() = 1 // selection.getTextRange().getEndIndex() = 1 // -// [END apps_script_slides_cursor_selection] - -// [START apps_script_slides_range_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var table = slide.getPageElements()[0].asTable(); -var tableCell = table.getCell(0, 1); -tableCell.getText().setText('Hello'); -// Range selection: Select the text range 'He'. -tableCell.getText().getRange(0, 2).select(); +// [END apps_script_slides_cursor_selection_in_shape] +} +/** + * Range selection in table cell. + */ +function slideRangeSelection() { +// [START apps_script_slides_range_selection_in_table] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const table = slide.getPageElements()[0].asTable(); + const tableCell = table.getCell(0, 1); + tableCell.getText().setText('Hello'); + // Range selection: Select the text range 'He'. + tableCell.getText().getRange(0, 2).select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.TEXT @@ -181,15 +214,19 @@ tableCell.getText().getRange(0, 2).select(); // selection.getTextRange().getStartIndex() = 0 // selection.getTextRange().getEndIndex() = 2 // -// [END apps_script_slides_range_selection] - -// [START apps_script_slides_cursor_selection] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var table = slide.getPageElements()[0].asTable(); -var tableCell = table.getCell(0, 1); -tableCell.getText().setText('Hello'); -// Cursor selection: Place the cursor after 'H' like 'H|ello'. -tableCell.getText().getRange(1, 1).select(); +// [END apps_script_slides_range_selection_in_table] +} +/** + * Cursor selection in table cell. + */ +function cursorSelection() { +// [START apps_script_slides_cursor_selection_in_table] + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const table = slide.getPageElements()[0].asTable(); + const tableCell = table.getCell(0, 1); + tableCell.getText().setText('Hello'); + // Cursor selection: Place the cursor after 'H' like 'H|ello'. + tableCell.getText().getRange(1, 1).select(); // State of selection // // selection.getSelectionType() = SlidesApp.SelectionType.TEXT @@ -199,25 +236,29 @@ tableCell.getText().getRange(1, 1).select(); // selection.getTextRange().getStartIndex() = 1 // selection.getTextRange().getEndIndex() = 1 // -// [END apps_script_slides_cursor_selection] - +// [END apps_script_slides_cursor_selection_in_table] +} +/** + * This shows how the selection can be transformed by editing the selected text. + */ +function selectTransformation() { // [START apps_script_slides_selection_transformation] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.getPageElements()[0].asShape(); -var textRange = shape.getText(); -textRange.setText('World'); -// Select all the text 'World'. -textRange.select(); -// State of selection -// -// selection.getSelectionType() = SlidesApp.SelectionType.TEXT -// selection.getCurrentPage() = slide -// selection.getPageElementRange().getPageElements()[0] = shape -// selection.getTextRange().getStartIndex() = 0 -// selection.getTextRange().getEndIndex() = 6 -// -// Add some text to the shape, and the selection will be transformed. -textRange.insertText(0, 'Hello '); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + const shape = slide.getPageElements()[0].asShape(); + const textRange = shape.getText(); + textRange.setText('World'); + // Select all the text 'World'. + textRange.select(); + // State of selection + // + // selection.getSelectionType() = SlidesApp.SelectionType.TEXT + // selection.getCurrentPage() = slide + // selection.getPageElementRange().getPageElements()[0] = shape + // selection.getTextRange().getStartIndex() = 0 + // selection.getTextRange().getEndIndex() = 6 + // + // Add some text to the shape, and the selection will be transformed. + textRange.insertText(0, 'Hello '); // State of selection // @@ -228,17 +269,27 @@ textRange.insertText(0, 'Hello '); // selection.getTextRange().getEndIndex() = 12 // // [END apps_script_slides_selection_transformation] - +} +/** + * The following example shows how to unselect any current selections on a page + * by setting that page as the current page. + */ +function slidesUnselectingCurrentPage() { // [START apps_script_slides_unselecting] // Unselect one or more page elements already selected. // // In case one or more page elements in the first slide are selected, setting the // same (or any other) slide page as the current page would do the unselect. // -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -slide.selectAsCurrentPage(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + slide.selectAsCurrentPage(); // [END apps_script_slides_unselecting] - +} +/** + * The following example shows how to unselect any current selections on a page + * by selecting one page element, thus removing all other items from the selection. + */ +function slideUnselectingPageElements() { // [START apps_script_slides_selecting] // Unselect one or more page elements already selected. // @@ -246,6 +297,7 @@ slide.selectAsCurrentPage(); // selecting any pageElement in the first slide (or any other pageElement) would // do the unselect and select that pageElement. // -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -slide.getPageElements()[0].select(); + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + slide.getPageElements()[0].select(); // [END apps_script_slides_selecting] +} diff --git a/slides/style/style.gs b/slides/style/style.gs index dbdc1ae21..deeb02f7b 100644 --- a/slides/style/style.gs +++ b/slides/style/style.gs @@ -13,70 +13,133 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +function setTextHelloWorld() { // [START apps_script_slides_hello] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); -var textRange = shape.getText(); -textRange.setText('Hello world!'); -Logger.log('Start: ' + textRange.getStartIndex() + '; End: ' + - textRange.getEndIndex() + '; Content: ' + textRange.asString()); -var subRange = textRange.getRange(0, 5); -Logger.log('Sub-range Start: ' + subRange.getStartIndex() + '; Sub-range End: ' + - subRange.getEndIndex() + '; Sub-range Content: ' + subRange.asString()); + try { + // Get the first slide of active presentation + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + // Insert shape in the slide with dimensions + const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); + const textRange = shape.getText(); + // Set text in TEXT_BOX + textRange.setText('Hello world!'); + console.log('Start: ' + textRange.getStartIndex() + '; End: ' + + textRange.getEndIndex() + '; Content: ' + textRange.asString()); + const subRange = textRange.getRange(0, 5); + console.log('Sub-range Start: ' + subRange.getStartIndex() + '; Sub-range End: ' + + subRange.getEndIndex() + '; Sub-range Content: ' + subRange.asString()); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with an error %s ', err.message); + } // [END apps_script_slides_hello] - +} +/** + * Insert Text in shape. + */ +function insertText() { // [START apps_script_slides_insert_text] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); -var textRange = shape.getText(); -textRange.setText('Hello world!'); -textRange.clear(6, 11); -textRange.insertText(6, 'galaxy'); -Logger.log('Start: ' + textRange.getStartIndex() + '; End: ' + - textRange.getEndIndex() + '; Content: ' + textRange.asString()); + try { + // Get the first slide of active presentation + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + // Insert shape in the slide with dimensions + const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); + const textRange = shape.getText(); + textRange.setText('Hello world!'); + textRange.clear(6, 11); + // Insert text in TEXT_BOX + textRange.insertText(6, 'galaxy'); + console.log('Start: ' + textRange.getStartIndex() + '; End: ' + + textRange.getEndIndex() + '; Content: ' + textRange.asString()); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with an error %s ', err.message); + } // [END apps_script_slides_insert_text] - +} +/** + * Style the text + */ +function styleText() { // [START apps_script_slides_style_text] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); -var textRange = shape.getText(); -textRange.setText('Hello '); -var insertedText = textRange.appendText('world!'); -insertedText.getTextStyle() - .setBold(true) - .setLinkUrl('www.example.com') - .setForegroundColor('#ff0000'); -var helloRange = textRange.getRange(0, 5); -Logger.log('Text: ' + helloRange.asString() + '; Bold: ' + helloRange.getTextStyle().isBold()); -Logger.log('Text: ' + insertedText.asString() + '; Bold: ' + insertedText.getTextStyle().isBold()); -Logger.log('Text: ' + textRange.asString() + '; Bold: ' + textRange.getTextStyle().isBold()); + try { + // Get the first slide of active presentation + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + // Insert shape in the slide with dimensions + const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 100, 200, 300, 60); + const textRange = shape.getText(); + // Set text in TEXT_BOX + textRange.setText('Hello '); + // Append text in TEXT_BOX + const insertedText = textRange.appendText('world!'); + // Style the text with url,bold + insertedText.getTextStyle() + .setBold(true) + .setLinkUrl('www.example.com') + .setForegroundColor('#ff0000'); + const helloRange = textRange.getRange(0, 5); + console.log('Text: ' + helloRange.asString() + '; Bold: ' + helloRange.getTextStyle().isBold()); + console.log('Text: ' + insertedText.asString() + '; Bold: ' + + insertedText.getTextStyle().isBold()); + console.log('Text: ' + textRange.asString() + '; Bold: ' + textRange.getTextStyle().isBold()); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with an error %s ', err.message); + } // [END apps_script_slides_style_text] +} +/** + * Style the paragraph + */ +function paragraphStyling() { // [START apps_script_slides_paragraph_styling] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 50, 50, 300, 300); -var textRange = shape.getText(); -textRange.setText('Paragraph 1\nParagraph2\nParagraph 3\nParagraph 4'); -var paragraphs = textRange.getParagraphs(); -for (var i = 0; i < 3; i++) { - var paragraphStyle = paragraphs[i].getRange().getParagraphStyle(); - paragraphStyle.setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER); -} + try { + // Get the first slide of active presentation + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + // Insert shape in the slide with dimensions + const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 50, 50, 300, 300); + const textRange = shape.getText(); + // Set the text in the shape/TEXT_BOX + textRange.setText('Paragraph 1\nParagraph2\nParagraph 3\nParagraph 4'); + const paragraphs = textRange.getParagraphs(); + // Style the paragraph alignment center. + for (let i = 0; i <= 3; i++) { + const paragraphStyle = paragraphs[i].getRange().getParagraphStyle(); + paragraphStyle.setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER); + } + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with an error %s ', err.message); + } // [END apps_script_slides_paragraph_styling] - -// [START apps_script_slides_list_styling] -var slide = SlidesApp.getActivePresentation().getSlides()[0]; -var shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 50, 50, 300, 300); -var textRange = shape.getText(); -textRange.appendText('Item 1\n') - .appendText('\tItem 2\n') - .appendText('\t\tItem 3\n') - .appendText('Item 4'); -textRange.getListStyle().applyListPreset(SlidesApp.ListPreset.DIGIT_ALPHA_ROMAN); -var paragraphs = textRange.getParagraphs(); -for (var i = 0; i < paragraphs.length; i++) { - var listStyle = paragraphs[i].getRange().getListStyle(); - Logger.log('Paragraph ' + (i + 1) + '\'s nesting level: ' + listStyle.getNestingLevel()); } +/** + * Style the list + */ +function listStyling() { +// [START apps_script_slides_list_styling] + try { + // Get the first slide of active presentation + const slide = SlidesApp.getActivePresentation().getSlides()[0]; + // Insert shape in the slide with dimensions + const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 50, 50, 300, 300); + // Add and style the list + const textRange = shape.getText(); + textRange.appendText('Item 1\n') + .appendText('\tItem 2\n') + .appendText('\t\tItem 3\n') + .appendText('Item 4'); + // Preset patterns of glyphs for lists in text. + textRange.getListStyle().applyListPreset(SlidesApp.ListPreset.DIGIT_ALPHA_ROMAN); + const paragraphs = textRange.getParagraphs(); + for (let i = 0; i < paragraphs.length; i++) { + const listStyle = paragraphs[i].getRange().getListStyle(); + console.log('Paragraph ' + (i + 1) + '\'s nesting level: ' + listStyle.getNestingLevel()); + } + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with an error %s ', err.message); + } // [END apps_script_slides_list_styling] +} diff --git a/slides/style/test_style.gs b/slides/style/test_style.gs new file mode 100644 index 000000000..c09cb62ff --- /dev/null +++ b/slides/style/test_style.gs @@ -0,0 +1,31 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * runs all the test + */ +function RUN_ALL_TESTS() { + console.log('> itShouldSetTextHelloWorld'); + setTextHelloWorld(); + console.log('> itShouldInsertText'); + insertText(); + console.log('> itShouldStyleText'); + styleText(); + console.log('> itShouldStyleParagraph'); + paragraphStyling(); + console.log('> itShouldListStyling'); + listStyling(); +} diff --git a/slides/translate/sidebar.html b/slides/translate/sidebar.html index e9734e7bb..77fb6076d 100644 --- a/slides/translate/sidebar.html +++ b/slides/translate/sidebar.html @@ -12,87 +12,87 @@ --> - - - - - - - + + + + + + + - - + - + } + + diff --git a/slides/translate/translate.gs b/slides/translate/translate.gs index 220606bd5..757e9266c 100644 --- a/slides/translate/translate.gs +++ b/slides/translate/translate.gs @@ -40,7 +40,7 @@ function onInstall(event) { * Opens a sidebar in the document containing the add-on's user interface. */ function showSidebar() { - var ui = HtmlService + const ui = HtmlService .createHtmlOutputFromFile('sidebar') .setTitle('Translate'); SlidesApp.getUi().showSidebar(ui); @@ -52,18 +52,18 @@ function showSidebar() { * @return {Text[]} An array of text elements. */ function getElementTexts(elements) { - var texts = []; - elements.forEach(function(element) { + let texts = []; + elements.forEach((element)=> { switch (element.getPageElementType()) { case SlidesApp.PageElementType.GROUP: - element.asGroup().getChildren().forEach(function(child) { + element.asGroup().getChildren().forEach((child)=> { texts = texts.concat(getElementTexts(child)); }); break; case SlidesApp.PageElementType.TABLE: - var table = element.asTable(); - for (var y = 0; y < table.getNumColumns(); ++y) { - for (var x = 0; x < table.getNumRows(); ++x) { + const table = element.asTable(); + for (let y = 0; y < table.getNumColumns(); ++y) { + for (let x = 0; x < table.getNumRows(); ++x) { texts.push(table.getCell(x, y).getText()); } } @@ -84,33 +84,33 @@ function getElementTexts(elements) { */ function translateSelectedElements(targetLanguage) { // Get selected elements. - var selection = SlidesApp.getActivePresentation().getSelection(); - var selectionType = selection.getSelectionType(); - var texts = []; + const selection = SlidesApp.getActivePresentation().getSelection(); + const selectionType = selection.getSelectionType(); + let texts = []; switch (selectionType) { case SlidesApp.SelectionType.PAGE: - var pages = selection.getPageRange().getPages().forEach(function(page) { + selection.getPageRange().getPages().forEach((page)=> { texts = texts.concat(getElementTexts(page.getPageElements())); }); - break; + break; case SlidesApp.SelectionType.PAGE_ELEMENT: - var pageElements = selection.getPageElementRange().getPageElements(); + const pageElements = selection.getPageElementRange().getPageElements(); texts = texts.concat(getElementTexts(pageElements)); - break; + break; case SlidesApp.SelectionType.TABLE_CELL: - var cells = selection.getTableCellRange().getTableCells().forEach(function(cell) { + selection.getTableCellRange().getTableCells().forEach((cell)=> { texts.push(cell.getText()); }); - break; + break; case SlidesApp.SelectionType.TEXT: - var elements = selection.getPageElementRange().getPageElements().forEach(function(element) { + selection.getPageElementRange().getPageElements().forEach((element) =>{ texts.push(element.asShape().getText()); }); - break; + break; } // Translate all elements in-place. - texts.forEach(function(text) { + texts.forEach((text)=> { text.setText(LanguageApp.translate(text.asRenderedString(), '', targetLanguage)); }); diff --git a/solutions/add-on/book-smartchip/.clasp.json b/solutions/add-on/book-smartchip/.clasp.json new file mode 100644 index 000000000..490e7e41d --- /dev/null +++ b/solutions/add-on/book-smartchip/.clasp.json @@ -0,0 +1 @@ +{"scriptId":"14tK6PD4C624ivRyGk-S6eYCbYJnDfA24xeP0Jhb1U8sPgAvZXeZm5gpb"} diff --git a/solutions/add-on/book-smartchip/Code.js b/solutions/add-on/book-smartchip/Code.js new file mode 100644 index 000000000..f054c4d18 --- /dev/null +++ b/solutions/add-on/book-smartchip/Code.js @@ -0,0 +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); +} + +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(); + } +} diff --git a/solutions/add-on/book-smartchip/appsscript.json b/solutions/add-on/book-smartchip/appsscript.json new file mode 100644 index 000000000..4e8538061 --- /dev/null +++ b/solutions/add-on/book-smartchip/appsscript.json @@ -0,0 +1,40 @@ +{ + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/workspace.linkpreview", + "https://www.googleapis.com/auth/script.external_request" + ], + "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" + } + } + ] + } + } +} \ No newline at end of file diff --git a/solutions/add-on/share-macro/.clasp.json b/solutions/add-on/share-macro/.clasp.json new file mode 100644 index 000000000..b92f05bc2 --- /dev/null +++ b/solutions/add-on/share-macro/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1BsbWOAbLADGoLtp5P9oqctZMiqT5EFh_R-CufxAV9y1hvVSAMO35Azu9"} diff --git a/solutions/add-on/share-macro/Code.js b/solutions/add-on/share-macro/Code.js new file mode 100644 index 000000000..3d5bb47e0 --- /dev/null +++ b/solutions/add-on/share-macro/Code.js @@ -0,0 +1,165 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.devsite.corp.google.com/apps-script/add-ons/share-macro + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Uses Apps Script API to copy source Apps Script project + * to destination Google Spreadsheet container. + * + * @param {string} sourceScriptId - Script ID of the source project. + * @param {string} targetSpreadsheetUrl - URL if the target spreadsheet. + * @return {Card[]} - Card indicating successful copy. + */ +function shareMacro_(sourceScriptId, targetSpreadsheetUrl) { + + // Gets the source project content using the Apps Script API. + const sourceProject = APPS_SCRIPT_API.get(sourceScriptId); + const sourceFiles = APPS_SCRIPT_API.getContent(sourceScriptId); + + // Opens the target spreadsheet and gets its ID. + const parentSSId = SpreadsheetApp.openByUrl(targetSpreadsheetUrl).getId(); + + // Creates an Apps Script project that's bound to the target spreadsheet. + const targetProjectObj = APPS_SCRIPT_API.create(sourceProject.title, parentSSId); + + // Updates the Apps Script project with the source project content. + APPS_SCRIPT_API.updateContent(targetProjectObj.scriptId, sourceFiles); + +} + +/** + * Function that encapsulates Apps Script API project manipulation. +*/ +const APPS_SCRIPT_API = { + accessToken: ScriptApp.getOAuthToken(), + + /* APPS_SCRIPT_API.get + * Gets Apps Script source project. + * @param {string} scriptId - Script ID of the source project. + * @return {Object} - JSON representation of source project. + */ + get: function (scriptId) { + const url = ('https://script.googleapis.com/v1/projects/' + scriptId); + const options = { + "method": 'get', + "headers": { + "Authorization": "Bearer " + this.accessToken + }, + "muteHttpExceptions": true, + }; + const res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() == 200) { + return JSON.parse(res); + } else { + console.log('An error occurred gettting the project details'); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + } + }, + + /* APPS_SCRIPT_API.create + * Creates new Apps Script project in the target spreadsheet. + * @param {string} title - Name of Apps Script project. + * @param {string} parentId - Internal ID of target spreadsheet. + * @return {Object} - JSON representation completed project creation. + */ + create: function (title, parentId) { + const url = 'https://script.googleapis.com/v1/projects'; + const options = { + "headers": { + "Authorization": "Bearer " + this.accessToken, + "Content-Type": "application/json" + }, + "muteHttpExceptions": true, + "method": "POST", + "payload": { "title": title } + } + if (parentId) { + options.payload.parentId = parentId; + } + options.payload = JSON.stringify(options.payload); + let res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() == 200) { + res = JSON.parse(res); + return res; + } else { + console.log("An error occurred while creating the project"); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + } + }, + /* APPS_SCRIPT_API.getContent + * Gets the content of the source Apps Script project. + * @param {string} scriptId - Script ID of the source project. + * @return {Object} - JSON representation of Apps Script project content. + */ + getContent: function (scriptId) { + const url = "https://script.googleapis.com/v1/projects/" + scriptId + "/content"; + const options = { + "method": 'get', + "headers": { + "Authorization": "Bearer " + this.accessToken + }, + "muteHttpExceptions": true, + }; + let res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() == 200) { + res = JSON.parse(res); + return res['files']; + } else { + console.log('An error occurred obtaining the content from the source script'); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + } + }, + + /* APPS_SCRIPT_API.updateContent + * Updates (copies) content from source to target Apps Script project. + * @param {string} scriptId - Script ID of the source project. + * @param {Object} files - JSON representation of Apps Script project content. + * @return {boolean} - Result status of the function. + */ + updateContent: function (scriptId, files) { + const url = "https://script.googleapis.com/v1/projects/" + scriptId + "/content"; + const options = { + "method": 'put', + "headers": { + "Authorization": "Bearer " + this.accessToken + }, + "contentType": "application/json", + "payload": JSON.stringify({ "files": files }), + "muteHttpExceptions": true, + }; + let res = UrlFetchApp.fetch(url, options); + if (res.getResponseCode() == 200) { + return true; + } else { + console.log(`An error occurred updating content of script ${scriptId}`); + console.log(res.getResponseCode()); + console.log(res.getContentText()); + console.log(res); + return false; + } + } +} \ No newline at end of file diff --git a/solutions/add-on/share-macro/README.md b/solutions/add-on/share-macro/README.md new file mode 100644 index 000000000..c070b8721 --- /dev/null +++ b/solutions/add-on/share-macro/README.md @@ -0,0 +1,4 @@ +# Copy macros to other spreadsheets + +See [developers.google.com](https://developers.google.com/apps-script/add-ons/share-macro) for additional details. + diff --git a/solutions/add-on/share-macro/UI.js b/solutions/add-on/share-macro/UI.js new file mode 100644 index 000000000..1aec692c2 --- /dev/null +++ b/solutions/add-on/share-macro/UI.js @@ -0,0 +1,233 @@ +/** + * 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. + */ + +// Change application logo here (and in manifest) as desired. +const ADDON_LOGO = 'https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png'; + +/** + * Callback function for rendering the main card. + * @return {CardService.Card} The card to show the user. + */ +function onHomepage(e) { + return createSelectionCard(e); +} + +/** + * Builds the primary card interface used to collect user inputs. + * + * @param {Object} e - Add-on event object. + * @param {string} sourceScriptId - Script ID of the source project. + * @param {string} targetSpreadsheetUrl - URL of the target spreadsheet. + * @param {string[]} errors - Array of error messages. + * + * @return {CardService.Card} The card to show to the user for inputs. + */ +function createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors) { + + // Configures card header. + let cardHeader = CardService.newCardHeader() + .setTitle('Share macros with other spreadheets!') + .setImageUrl(ADDON_LOGO) + .setImageStyle(CardService.ImageStyle.SQUARE); + + // If form errors exist, configures section with error messages. + let showErrors = false; + + if (errors && errors.length) { + showErrors = true; + let msg = errors.reduce((str, err) => `${str}• ${err}
`, ''); + msg = `Form submission errors:
${msg}`; + + // Builds error message section. + sectionErrors = CardService.newCardSection() + .addWidget(CardService.newDecoratedText() + .setText(msg) + .setWrapText(true)); + } + + // Configures source project section. + let sectionSource = CardService.newCardSection() + .addWidget(CardService.newDecoratedText() + .setText('Source macro
The Apps Script project to copy')) + + .addWidget(CardService.newTextInput() + .setFieldName('sourceScriptId') + .setValue(sourceScriptId || '') + .setTitle('Script ID of the source macro') + .setHint('You must have at least edit permission for the source spreadsheet to access its script project')) + + .addWidget(CardService.newTextButton() + .setText('Find the script ID') + .setOpenLink(CardService.newOpenLink() + .setUrl('https://developers.google.com/apps-script/api/samples/execute') + .setOpenAs(CardService.OpenAs.FULL_SIZE) + .setOnClose(CardService.OnClose.NOTHING))); + + // Configures target spreadsheet section. + let sectionTarget = CardService.newCardSection() + .addWidget(CardService.newDecoratedText() + .setText('Target spreadsheet')) + + .addWidget(CardService.newTextInput() + .setFieldName('targetSpreadsheetUrl') + .setValue(targetSpreadsheetUrl || '') + .setHint('You must have at least edit permission for the target spreadsheet') + .setTitle('Target spreadsheet URL')); + + // Configures help section. + let sectionHelp = CardService.newCardSection() + .addWidget(CardService.newDecoratedText() + .setText('NOTE: ' + + 'The Apps Script API must be turned on.') + .setWrapText(true)) + + .addWidget(CardService.newTextButton() + .setText('Turn on Apps Script API') + .setOpenLink(CardService.newOpenLink() + .setUrl('https://script.google.com/home/usersettings') + .setOpenAs(CardService.OpenAs.FULL_SIZE) + .setOnClose(CardService.OnClose.NOTHING))); + + // Configures card footer with action to copy the macro. + var cardFooter = CardService.newFixedFooter() + .setPrimaryButton(CardService.newTextButton() + .setText('Share macro') + .setOnClickAction(CardService.newAction() + .setFunctionName('onClickFunction_'))); + + // Begins building the card. + let builder = CardService.newCardBuilder() + .setHeader(cardHeader); + + // Adds error section if applicable. + if (showErrors) { + builder.addSection(sectionErrors) + } + + // Adds final sections & footer. + builder + .addSection(sectionSource) + .addSection(sectionTarget) + .addSection(sectionHelp) + .setFixedFooter(cardFooter); + + return builder.build(); +} + +/** + * Action handler that validates user inputs and calls shareMacro_ + * function to copy Apps Script project to target spreadsheet. + * + * @param {Object} e - Add-on event object. + * + * @return {CardService.Card} Responds with either a success or error card. + */ +function onClickFunction_(e) { + + const sourceScriptId = e.formInput.sourceScriptId; + const targetSpreadsheetUrl = e.formInput.targetSpreadsheetUrl; + + // Validates inputs for errors. + const errors = []; + + // Pushes an error message if the Script ID parameter is missing. + if (!sourceScriptId) { + errors.push('Missing script ID'); + } else { + + // Gets the Apps Script project if the Script ID parameter is valid. + const sourceProject = APPS_SCRIPT_API.get(sourceScriptId); + if (!sourceProject) { + // Pushes an error message if the Script ID parameter isn't valid. + errors.push('Invalid script ID'); + } + } + + // Pushes an error message if the spreadsheet URL is missing. + if (!targetSpreadsheetUrl) { + errors.push('Missing Spreadsheet URL'); + } else + try { + // Tests for valid spreadsheet URL to get the spreadsheet ID. + const ssId = SpreadsheetApp.openByUrl(targetSpreadsheetUrl).getId(); + } catch (err) { + // Pushes an error message if the spreadsheet URL parameter isn't valid. + errors.push('Invalid spreadsheet URL'); + } + + if (errors && errors.length) { + // Redisplays form if inputs are missing or invalid. + return createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors); + + } else { + // Calls shareMacro function to copy the project. + shareMacro_(sourceScriptId, targetSpreadsheetUrl); + + // Creates a success card to display to users. + return buildSuccessCard(e, targetSpreadsheetUrl); + } +} + +/** + * Builds success card to inform user & let them open the spreadsheet. + * + * @param {Object} e - Add-on event object. + * @param {string} targetSpreadsheetUrl - URL of the target spreadsheet. + * + * @return {CardService.Card} Returns success card. + */function buildSuccessCard(e, targetSpreadsheetUrl) { + + // Configures card header. + let cardHeader = CardService.newCardHeader() + .setTitle('Share macros with other spreadsheets!') + .setImageUrl(ADDON_LOGO) + .setImageStyle(CardService.ImageStyle.SQUARE); + + // Configures card body section with success message and open button. + let sectionBody1 = CardService.newCardSection() + .addWidget(CardService.newTextParagraph() + .setText('Sharing process is complete!')) + .addWidget(CardService.newTextButton() + .setText('Open spreadsheet') + .setOpenLink(CardService.newOpenLink() + .setUrl(targetSpreadsheetUrl) + .setOpenAs(CardService.OpenAs.FULL_SIZE) + .setOnClose(CardService.OnClose.RELOAD_ADD_ON))); + let sectionBody2 = CardService.newCardSection() + .addWidget(CardService.newTextParagraph() + .setText('If you don\'t see the copied project in your target spreadsheet,' + + ' make sure you turned on the Apps Script API in the Apps Script dashboard.')) + .addWidget(CardService.newTextButton() + .setText("Check API") + .setOpenLink(CardService.newOpenLink() + .setUrl('https://script.google.com/home/usersettings') + .setOpenAs(CardService.OpenAs.FULL_SIZE) + .setOnClose(CardService.OnClose.RELOAD_ADD_ON))); + + // Configures the card footer with action to start new process. + let cardFooter = CardService.newFixedFooter() + .setPrimaryButton(CardService.newTextButton() + .setText('Share another') + .setOnClickAction(CardService.newAction() + .setFunctionName('onHomepage'))); + + return builder = CardService.newCardBuilder() + .setHeader(cardHeader) + .addSection(sectionBody1) + .addSection(sectionBody2) + .setFixedFooter(cardFooter) + .build(); + } \ No newline at end of file diff --git a/solutions/add-on/share-macro/appsscript.json b/solutions/add-on/share-macro/appsscript.json new file mode 100644 index 000000000..0a9e3f14f --- /dev/null +++ b/solutions/add-on/share-macro/appsscript.json @@ -0,0 +1,28 @@ +{ + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/script.projects" + ], + "urlFetchWhitelist": [ + "https://script.googleapis.com/" + ], + "addOns": { + "common": { + "name": "Share Macro", + "logoUrl": "https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png", + "layoutProperties": { + "primaryColor": "#188038", + "secondaryColor": "#34a853" + }, + "homepageTrigger": { + "runFunction": "onHomepage" + } + }, + "sheets": {} + } +} \ No newline at end of file diff --git a/solutions/automations/agenda-maker/.clasp.json b/solutions/automations/agenda-maker/.clasp.json new file mode 100644 index 000000000..4b9783746 --- /dev/null +++ b/solutions/automations/agenda-maker/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "147xVWUWmw8b010zbiDMIa3eeKATo3P2q5rJCZmY3meirC-yA_XucdZlp"} diff --git a/solutions/automations/agenda-maker/Code.js b/solutions/automations/agenda-maker/Code.js new file mode 100644 index 000000000..fa2bb7a95 --- /dev/null +++ b/solutions/automations/agenda-maker/Code.js @@ -0,0 +1,192 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/agenda-maker + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Checks if the folder for Agenda docs exists, and creates it if it doesn't. + * + * @return {*} Drive folder ID for the app. + */ +function checkFolder() { + const folders = DriveApp.getFoldersByName('Agenda Maker - App'); + // Finds the folder if it exists + while (folders.hasNext()) { + let folder = folders.next(); + if ( + folder.getDescription() == + 'Apps Script App - Do not change this description' && + folder.getOwner().getEmail() == Session.getActiveUser().getEmail() + ) { + return folder.getId(); + } + } + // If the folder doesn't exist, creates one + let folder = DriveApp.createFolder('Agenda Maker - App'); + folder.setDescription('Apps Script App - Do not change this description'); + return folder.getId(); +} + +/** + * Finds the template agenda doc, or creates one if it doesn't exist. + */ +function getTemplateId(folderId) { + const folder = DriveApp.getFolderById(folderId); + const files = folder.getFilesByName('Agenda TEMPLATE##'); + + // If there is a file, returns the ID. + while (files.hasNext()) { + const file = files.next(); + return file.getId(); + } + + // Otherwise, creates the agenda template. + // You can adjust the default template here + const doc = DocumentApp.create('Agenda TEMPLATE##'); + const body = doc.getBody(); + + body + .appendParagraph('##Attendees##') + .setHeading(DocumentApp.ParagraphHeading.HEADING1) + .editAsText() + .setBold(true); + body.appendParagraph(' ').editAsText().setBold(false); + + body + .appendParagraph('Overview') + .setHeading(DocumentApp.ParagraphHeading.HEADING1) + .editAsText() + .setBold(true); + body.appendParagraph(' '); + body.appendParagraph('- Topic 1: ').editAsText().setBold(true); + body.appendParagraph(' ').editAsText().setBold(false); + body.appendParagraph('- Topic 2: ').editAsText().setBold(true); + body.appendParagraph(' ').editAsText().setBold(false); + body.appendParagraph('- Topic 3: ').editAsText().setBold(true); + body.appendParagraph(' ').editAsText().setBold(false); + + body + .appendParagraph('Next Steps') + .setHeading(DocumentApp.ParagraphHeading.HEADING1) + .editAsText() + .setBold(true); + body.appendParagraph('- Takeaway 1: ').editAsText().setBold(true); + body.appendParagraph('- Responsible: ').editAsText().setBold(false); + body.appendParagraph('- Accountable: '); + body.appendParagraph('- Consult: '); + body.appendParagraph('- Inform: '); + body.appendParagraph(' '); + body.appendParagraph('- Takeaway 2: ').editAsText().setBold(true); + body.appendParagraph('- Responsible: ').editAsText().setBold(false); + body.appendParagraph('- Accountable: '); + body.appendParagraph('- Consult: '); + body.appendParagraph('- Inform: '); + body.appendParagraph(' '); + body.appendParagraph('- Takeaway 3: ').editAsText().setBold(true); + body.appendParagraph('- Responsible: ').editAsText().setBold(false); + body.appendParagraph('- Accountable: '); + body.appendParagraph('- Consult: '); + body.appendParagraph('- Inform: '); + + doc.saveAndClose(); + + folder.addFile(DriveApp.getFileById(doc.getId())); + + return doc.getId(); +} + +/** + * When there is a change to the calendar, searches for events that include "#agenda" + * in the decrisption. + * + */ +function onCalendarChange() { + // Gets recent events with the #agenda tag + const now = new Date(); + const events = CalendarApp.getEvents( + now, + new Date(now.getTime() + 2 * 60 * 60 * 1000000), + {search: '#agenda'}, + ); + + const folderId = checkFolder(); + const templateId = getTemplateId(folderId); + + const folder = DriveApp.getFolderById(folderId); + + // Loops through any events found + for (i = 0; i < events.length; i++) { + const event = events[i]; + + // Confirms whether the event has the #agenda tag + let description = event.getDescription(); + if (description.search('#agenda') == -1) continue; + + // Only works with events created by the owner of this calendar + if (event.isOwnedByMe()) { + // Creates a new document from the template for an agenda for this event + const newDoc = DriveApp.getFileById(templateId).makeCopy(); + newDoc.setName('Agenda for ' + event.getTitle()); + + const file = DriveApp.getFileById(newDoc.getId()); + folder.addFile(file); + + const doc = DocumentApp.openById(newDoc.getId()); + const body = doc.getBody(); + + // Fills in the template with information about the attendees from the + // calendar event + const conf = body.findText('##Attendees##'); + if (conf) { + const ref = conf.getStartOffset(); + + for (let i in event.getGuestList()) { + let guest = event.getGuestList()[i]; + + body.insertParagraph(ref + 2, guest.getEmail()); + } + body.replaceText('##Attendees##', 'Attendees'); + } + + // Replaces the tag with a link to the agenda document + const agendaUrl = 'https://docs.google.com/document/d/' + newDoc.getId(); + description = description.replace( + '#agenda', + 'Agenda Doc', + ); + event.setDescription(description); + + // Invites attendees to the Google doc so they automatically receive access to the agenda + newDoc.addEditor(newDoc.getOwner()); + + for (let i in event.getGuestList()) { + let guest = event.getGuestList()[i]; + + newDoc.addEditor(guest.getEmail()); + } + } + } + return; +} + +/** + * Creates an event-driven trigger that fires whenever there's a change to the calendar. + */ +function setUp() { + let email = Session.getActiveUser().getEmail(); + ScriptApp.newTrigger("onCalendarChange").forUserCalendar(email).onEventUpdated().create(); +} diff --git a/solutions/automations/agenda-maker/README.md b/solutions/automations/agenda-maker/README.md new file mode 100644 index 000000000..e8100c3c9 --- /dev/null +++ b/solutions/automations/agenda-maker/README.md @@ -0,0 +1,3 @@ +# Make an agenda for meetings + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/agenda-maker) for additional details. diff --git a/solutions/automations/agenda-maker/appsscript.json b/solutions/automations/agenda-maker/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/agenda-maker/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/aggregate-document-content/.clasp.json b/solutions/automations/aggregate-document-content/.clasp.json new file mode 100644 index 000000000..c264f85c1 --- /dev/null +++ b/solutions/automations/aggregate-document-content/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1YGstQLxmTcAQlSHfm0yke12Y2UgT8eVfCxrG_jGpG1dHDmFdOaHQfQZJ"} diff --git a/solutions/automations/aggregate-document-content/Code.js b/solutions/automations/aggregate-document-content/Code.js new file mode 100644 index 000000000..c042b83f1 --- /dev/null +++ b/solutions/automations/aggregate-document-content/Code.js @@ -0,0 +1,176 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/aggregate-document-content + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * This file containts the main application functions that import data from + * summary documents into the body of the main document. + */ + +// Application constants +const APP_TITLE = 'Document summary importer'; // Application name +const PROJECT_FOLDER_NAME = 'Project statuses'; // Drive folder for the source files. + +// Below are the parameters used to identify which content to import from the source documents +// and which content has already been imported. +const FIND_TEXT_KEYWORDS = 'Summary'; // String that must be found in the heading above the table (case insensitive). +const APP_STYLE = DocumentApp.ParagraphHeading.HEADING3; // Style that must be applied to heading above the table. +const TEXT_COLOR = '#2e7d32'; // Color applied to heading after import to avoid duplication. + +/** + * Updates the main document, importing content from the source files. + * Uses the above parameters to locate content to be imported. + * + * Called from menu option. + */ +function performImport() { + // Gets the folder in Drive associated with this application. + const folder = getFolderByName_(PROJECT_FOLDER_NAME); + // Gets the Google Docs files found in the folder. + const files = getFiles(folder); + + // Warns the user if the folder is empty. + const ui = DocumentApp.getUi(); + if (files.length === 0) { + const msg = + `No files found in the folder '${PROJECT_FOLDER_NAME}'. + Run '${MENU.SETUP}' | '${MENU.SAMPLES}' from the menu + if you'd like to create samples files.` + ui.alert(APP_TITLE, msg, ui.ButtonSet.OK); + return; + } + + /** Processes main document */ + // Gets the active document and body section. + const docTarget = DocumentApp.getActiveDocument(); + const docTargetBody = docTarget.getBody(); + + // Appends import summary section to the end of the target document. + // Adds a horizontal line and a header with today's date and a title string. + docTargetBody.appendHorizontalRule(); + const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy'); + const headingText = `Imported: ${dateString}`; + docTargetBody.appendParagraph(headingText).setHeading(APP_STYLE); + // Appends a blank paragraph for spacing. + docTargetBody.appendParagraph(" "); + + /** Process source documents */ + // Iterates through each source document in the folder. + // Copies and pastes new updates to the main document. + let noContentList = []; + let numUpdates = 0; + for (let id of files) { + + // Opens source document; get info and body. + const docOpen = DocumentApp.openById(id); + const docName = docOpen.getName(); + const docHtml = docOpen.getUrl(); + const docBody = docOpen.getBody(); + + // Gets summary content from document and returns as object {content:content} + const content = getContent(docBody); + + // Logs if document doesn't contain content to be imported. + if (!content) { + noContentList.push(docName); + continue; + } + else { + numUpdates++ + // Inserts content into the main document. + // Appends a title/url reference link back to source document. + docTargetBody.appendParagraph('').appendText(`${docName}`).setLinkUrl(docHtml); + // Appends a single-cell table and pastes the content. + docTargetBody.appendTable(content); + } + docOpen.saveAndClose() + } + /** Provides an import summary */ + docTarget.saveAndClose(); + let msg = `Number of documents updated: ${numUpdates}` + if (noContentList.length != 0) { + msg += `\n\nThe following documents had no updates:` + for (let file of noContentList) { + msg += `\n ${file}`; + } + } + ui.alert(APP_TITLE, msg, ui.ButtonSet.OK); +} + +/** + * Updates the main document drawing content from source files. + * Uses the parameters at the top of this file to locate content to import. + * + * Called from performImport(). + */ +function getContent(body) { + + // Finds the heading paragraph with matching style, keywords and !color. + var parValidHeading; + const searchType = DocumentApp.ElementType.PARAGRAPH; + const searchHeading = APP_STYLE; + let searchResult = null; + + // Gets and loops through all paragraphs that match the style of APP_STYLE. + while (searchResult = body.findElement(searchType, searchResult)) { + let par = searchResult.getElement().asParagraph(); + if (par.getHeading() == searchHeading) { + // If heading style matches, searches for text string (case insensitive). + let findPos = par.findText('(?i)' + FIND_TEXT_KEYWORDS); + if (findPos !== null) { + + // If text color is green, then the paragraph isn't a new summary to copy. + if (par.editAsText().getForegroundColor() != TEXT_COLOR) { + parValidHeading = par; + } + } + } + } + + if (!parValidHeading) { + return; + } else { + // Updates the heading color to indicate that the summary has been imported. + let style = {}; + style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR; + parValidHeading.setAttributes(style); + parValidHeading.appendText(" [Exported]"); + + // Gets the content from the table following the valid heading. + let elemObj = parValidHeading.getNextSibling().asTable(); + let content = elemObj.copy(); + + return content; + } +} + +/** + * Gets the IDs of the Docs files within the folder that contains source files. + * + * Called from function performImport(). + */ +function getFiles(folder) { + // Only gets Docs files. + const files = folder.getFilesByType(MimeType.GOOGLE_DOCS); + let docIDs = []; + while (files.hasNext()) { + let file = files.next(); + docIDs.push(file.getId()); + } + return docIDs; +} \ No newline at end of file diff --git a/solutions/automations/aggregate-document-content/Menu.js b/solutions/automations/aggregate-document-content/Menu.js new file mode 100644 index 000000000..58a6b59fb --- /dev/null +++ b/solutions/automations/aggregate-document-content/Menu.js @@ -0,0 +1,59 @@ +/** + * 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. + */ + +/** + * This file contains the functions that build the custom menu. + */ +// Menu constants for easy access to update. +const MENU = { + NAME: 'Import summaries', + IMPORT: 'Import summaries', + SETUP: 'Configure', + NEW_INSTANCE: 'Setup new instance', + TEMPLATE: 'Create starter template', + SAMPLES: 'Run demo setup with sample documents' +} + +/** + * Creates custom menu when the document is opened. + */ +function onOpen() { + const ui = DocumentApp.getUi(); + ui.createMenu(MENU.NAME) + .addItem(MENU.IMPORT, 'performImport') + .addSeparator() + .addSubMenu(ui.createMenu(MENU.SETUP) + .addItem(MENU.NEW_INSTANCE, 'setupConfig') + .addItem(MENU.TEMPLATE, 'createSampleFile') + .addSeparator() + .addItem(MENU.SAMPLES, 'setupWithSamples')) + .addItem('About', 'aboutApp') + .addToUi() +} + +/** + * About box for context and contact. + * TODO: Personalize + */ +function aboutApp() { + const msg = ` + ${APP_TITLE} + Version: 1.0 + Contact: ` + + const ui = DocumentApp.getUi(); + ui.alert("About this application", msg, ui.ButtonSet.OK); +} diff --git a/solutions/automations/aggregate-document-content/README.md b/solutions/automations/aggregate-document-content/README.md new file mode 100644 index 000000000..6e8623ded --- /dev/null +++ b/solutions/automations/aggregate-document-content/README.md @@ -0,0 +1,3 @@ +# Aggregate content from multiple documents + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/aggregate-document-content) for additional details. diff --git a/solutions/automations/aggregate-document-content/Setup.js b/solutions/automations/aggregate-document-content/Setup.js new file mode 100644 index 000000000..ef13ba0cb --- /dev/null +++ b/solutions/automations/aggregate-document-content/Setup.js @@ -0,0 +1,165 @@ +/** + * 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. + */ + +/** + * This file contains functions that create the template and sample documents. + */ + +/** + * Runs full setup configuration, with option to include samples. + * + * Called from menu & setupWithSamples() + * + * @param {boolean} includeSamples - Optional, if true creates samples files. * + */ +function setupConfig(includeSamples) { + + // Gets folder to store documents in. + const folder = getFolderByName_(PROJECT_FOLDER_NAME) + + let msg = + `\nDrive Folder for Documents: '${PROJECT_FOLDER_NAME}' + \nURL: \n${folder.getUrl()}` + + // Creates sample documents for testing. + // Remove sample document creation and add your own process as needed. + if (includeSamples) { + let filesCreated = 0; + for (let doc of samples.documents) { + filesCreated += createGoogleDoc(doc, folder, true); + } + msg += `\n\nFiles Created: ${filesCreated}` + } + const ui = DocumentApp.getUi(); + ui.alert(`${APP_TITLE} [Setup]`, msg, ui.ButtonSet.OK); + +} + +/** + * Creates a single document instance in the application folder. + * Includes import settings already created [Heading | Keywords | Table] + * + * Called from menu. + */ +function createSampleFile() { + + // Creates a new Google Docs document. + const templateName = `[Template] ${APP_TITLE}`; + const doc = DocumentApp.create(templateName); + const docId = doc.getId(); + + const msg = `\nDocument created: '${templateName}' + \nURL: \n${doc.getUrl()}` + + // Adds template content to the body. + const body = doc.getBody(); + + body.setText(templateName); + body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE); + body.appendParagraph('Description').setHeading(DocumentApp.ParagraphHeading.HEADING1); + body.appendParagraph(''); + + const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy'); + body.appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`).setHeading(APP_STYLE); + body.appendTable().appendTableRow().appendTableCell('TL;DR'); + body.appendParagraph(""); + + // Gets folder to store documents in. + const folder = getFolderByName_(PROJECT_FOLDER_NAME) + + // Moves document to application folder. + DriveApp.getFileById(docId).moveTo(folder); + + const ui = DocumentApp.getUi(); + ui.alert(`${APP_TITLE} [Template]`, msg, ui.ButtonSet.OK); +} + +/** + * Configures application for demonstration by setting it up with sample documents. + * + * Called from menu | Calls setupConfig with option set to true. + */ +function setupWithSamples() { + setupConfig(true) +} + +/** + * Sample document names and demo content. + * {object} samples[] +*/ +const samples = { + 'documents': [ + { + 'name': 'Project GHI', + 'description': 'Google Workspace Add-on inventory review.', + 'content': 'Reviewed all of the currently in-use and proposed Google Workspace Add-ons. Will perform an assessment on how we can reduce overlap, reduce licensing costs, and limit security exposures. \n\nNext week\'s goal is to report findings back to the Corp Ops team.' + }, + { + 'name': 'Project DEF', + 'description': 'Improve IT networks within the main corporate building.', + 'content': 'Primarily focused on 2nd thru 5th floors in the main corporate building evaluating the network infrastructure. Benchmarking tests were performed and results are being analyzed. \n\nWill submit all findings, analysis, and recommendations next week for committee review.' + }, + { + 'name': 'Project ABC', + 'description': 'Assess existing Google Chromebook inventory and recommend upgrades where necessary.', + 'content': 'Concluded a pilot program with the Customer Service department to perform inventory and update inventory records with Chromebook hardware, Chrome OS versions, and installed apps. \n\nScheduling a work plan and seeking necessary go-forward approvals for next week.' + }, + ], + 'common': 'This sample document is configured to work with the Import summaries custom menu. For the import to work, the source documents used must contain a specific keyword (currently set to "Summary"). The keyword must reside in a paragraph with a set style (currently set to "Heading 3") that is directly followed by a single-cell table. The table contains the contents to be imported into the primary document.\n\nWhile those rules might seem precise, it\'s how the application programmatically determines what content is meant to be imported and what can be ignored. Once a summary has been imported, the script updates the heading font to a new color (currently set to Green, hex \'#2e7d32\') to ensure the app ignores it in future imports. You can change these settings in the Apps Script code.' +} + +/** + * Creates a sample document in application folder. + * Includes import settings already created [Heading | Keywords | Table]. + * Inserts demo data from samples[]. + * + * Called from menu. + */ +function createGoogleDoc(document, folder, duplicate) { + + // Checks for duplicates. + if (!duplicate) { + // Doesn't create file of same name if one already exists. + if (folder.getFilesByName(document.name).hasNext()) { + return 0 // File not created. + } + } + + // Creates a new Google Docs document. + const doc = DocumentApp.create(document.name).setName(document.name); + const docId = doc.getId(); + + // Adds boilerplate content to the body. + const body = doc.getBody(); + + body.setText(document.name); + body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE); + body.appendParagraph("Description").setHeading(DocumentApp.ParagraphHeading.HEADING1); + body.appendParagraph(document.description); + body.appendParagraph("Usage Instructions").setHeading(DocumentApp.ParagraphHeading.HEADING1); + body.appendParagraph(samples.common); + + const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy'); + body.appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`).setHeading(APP_STYLE); + body.appendTable().appendTableRow().appendTableCell(document.content); + body.appendParagraph(""); + + // Moves document to application folder. + DriveApp.getFileById(docId).moveTo(folder); + + // Returns if successfully created. + return 1 +} \ No newline at end of file diff --git a/solutions/automations/aggregate-document-content/Utilities.js b/solutions/automations/aggregate-document-content/Utilities.js new file mode 100644 index 000000000..721260de6 --- /dev/null +++ b/solutions/automations/aggregate-document-content/Utilities.js @@ -0,0 +1,60 @@ +/** + * 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. + */ + +/** + * This file contains common utility functions. + */ + +/** + * Returns a Drive folder located in same folder that the application document is located. + * Checks if the folder exists and returns that folder, or creates new one if not found. + * + * @param {string} folderName - Name of the Drive folder. + * @return {object} Google Drive folder + */ +function getFolderByName_(folderName) { + // Gets the Drive folder where the current document is located. + const docId = DocumentApp.getActiveDocument().getId(); + const parentFolder = DriveApp.getFileById(docId).getParents().next(); + + // Iterates subfolders to check if folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === folderName) { + return folder; + } + } + // Creates a new folder if one doesn't already exist. + return parentFolder.createFolder(folderName) + .setDescription(`Created by ${APP_TITLE} application to store documents to process`); +} + +/** + * Test function to run getFolderByName_. + * @logs details of created Google Drive folder. + */ +function test_getFolderByName() { + + // Gets the folder in Drive associated with this application. + const folder = getFolderByName_(PROJECT_FOLDER_NAME); + + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`) + // Uncomment the following to automatically delete the test folder. + // folder.setTrashed(true); +} \ No newline at end of file diff --git a/solutions/automations/aggregate-document-content/appsscript.json b/solutions/automations/aggregate-document-content/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/aggregate-document-content/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/bracket-maker/.clasp.json b/solutions/automations/bracket-maker/.clasp.json new file mode 100644 index 000000000..92fd0ae7f --- /dev/null +++ b/solutions/automations/bracket-maker/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1LkY5nKFdBg2Q9-oIUcZsxRuESvgIcFHGobveNeQ5CpTgV6GgpTUQeOIB"} diff --git a/solutions/automations/bracket-maker/Code.js b/solutions/automations/bracket-maker/Code.js new file mode 100644 index 000000000..fccb6a7f7 --- /dev/null +++ b/solutions/automations/bracket-maker/Code.js @@ -0,0 +1,133 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/bracket-maker + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const RANGE_PLAYER1 = 'FirstPlayer'; +const SHEET_PLAYERS = 'Players'; +const SHEET_BRACKET = 'Bracket'; +const CONNECTOR_WIDTH = 15; + +/** + * Adds a custom menu item to run the script. + */ +function onOpen() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + ss.addMenu('Bracket maker', + [{name: 'Create bracket', functionName: 'createBracket'}]); +} + +/** + * Creates the brackets based on the data provided on the players. + */ +function createBracket() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let rangePlayers = ss.getRangeByName(RANGE_PLAYER1); + let sheetControl = ss.getSheetByName(SHEET_PLAYERS); + let sheetResults = ss.getSheetByName(SHEET_BRACKET); + + // Gets the players from column A. Assumes the entire column is filled. + rangePlayers = rangePlayers.offset(0, 0, sheetControl.getMaxRows() - + rangePlayers.getRowIndex() + 1, 1); + let players = rangePlayers.getValues(); + + // Figures out how many players there are by skipping the empty cells. + let numPlayers = 0; + for (let i = 0; i < players.length; i++) { + if (!players[i][0] || players[i][0].length == 0) { + break; + } + numPlayers++; + } + players = players.slice(0, numPlayers); + + // Provides some error checking in case there are too many or too few players/teams. + if (numPlayers > 64) { + Browser.msgBox('Sorry, this script can only create brackets for 64 or fewer players.'); + return; // Early exit + } + + if (numPlayers < 3) { + Browser.msgBox('Sorry, you must have at least 3 players.'); + return; // Early exit + } + + // Clears the 'Bracket' sheet and all formatting. + sheetResults.clear(); + + let upperPower = Math.ceil(Math.log(numPlayers) / Math.log(2)); + + // Calculates the number that is a power of 2 and lower than numPlayers. + let countNodesUpperBound = Math.pow(2, upperPower); + + // Calculates the number that is a power of 2 and higher than numPlayers. + let countNodesLowerBound = countNodesUpperBound / 2; + + // Determines the number of nodes that will not show in the 1st level. + let countNodesHidden = numPlayers - countNodesLowerBound; + + // Enters the players for the 1st round. + let currentPlayer = 0; + for (let i = 0; i < countNodesLowerBound; i++) { + if (i < countNodesHidden) { + // Must be on the first level + let rng = sheetResults.getRange(i * 4 + 1, 1); + setBracketItem_(rng, players); + setBracketItem_(rng.offset(2, 0, 1, 1), players); + setConnector_(sheetResults, rng.offset(0, 1, 3, 1)); + setBracketItem_(rng.offset(1, 2, 1, 1)); + } else { + // This player gets a bye. + setBracketItem_(sheetResults.getRange(i * 4 + 2, 3), players); + } + } + + // Fills in the rest of the bracket. + upperPower--; + for (let i = 0; i < upperPower; i++) { + let pow1 = Math.pow(2, i + 1); + let pow2 = Math.pow(2, i + 2); + let pow3 = Math.pow(2, i + 3); + for (let j = 0; j < Math.pow(2, upperPower - i - 1); j++) { + setBracketItem_(sheetResults.getRange((j * pow3) + pow2, i * 2 + 5)); + setConnector_(sheetResults, sheetResults.getRange((j * pow3) + pow1, i * 2 + 4, pow2 + 1, 1)); + } + } +} + +/** + * Sets the value of an item in the bracket and the color. + * @param {Range} rng The Spreadsheet Range. + * @param {string[]} players The list of players. + */ +function setBracketItem_(rng, players) { + if (players) { + let rand = Math.ceil(Math.random() * players.length); + rng.setValue(players.splice(rand - 1, 1)[0][0]); + } + rng.setBackgroundColor('yellow'); +} + +/** + * Sets the color and width for connector cells. + * @param {Sheet} sheet The spreadsheet to setup. + * @param {Range} rng The spreadsheet range. + */ +function setConnector_(sheet, rng) { + sheet.setColumnWidth(rng.getColumnIndex(), CONNECTOR_WIDTH); + rng.setBackgroundColor('green'); +} \ No newline at end of file diff --git a/solutions/automations/bracket-maker/README.md b/solutions/automations/bracket-maker/README.md new file mode 100644 index 000000000..920c6b3c2 --- /dev/null +++ b/solutions/automations/bracket-maker/README.md @@ -0,0 +1,3 @@ +# Create a tournament bracket + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/bracket-maker) for additional details. diff --git a/solutions/automations/bracket-maker/appsscript.json b/solutions/automations/bracket-maker/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/bracket-maker/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/calendar-timesheet/.clasp.json b/solutions/automations/calendar-timesheet/.clasp.json new file mode 100644 index 000000000..d8c75e26a --- /dev/null +++ b/solutions/automations/calendar-timesheet/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1WL3-mzC219UHqy_vqI1gEeoFy5Y8eeiKCZjiiPsWmVmQfVVedN5Vt7rK"} diff --git a/solutions/automations/calendar-timesheet/Code.js b/solutions/automations/calendar-timesheet/Code.js new file mode 100644 index 000000000..adda729b9 --- /dev/null +++ b/solutions/automations/calendar-timesheet/Code.js @@ -0,0 +1,363 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/calendar-timesheet + +/* +Copyright 2022 Jasper Duizendstra + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Runs when the spreadsheet is opened and adds the menu options + * to the spreadsheet menu + */ +const onOpen = () => { + SpreadsheetApp.getUi() + .createMenu('myTime') + .addItem('Sync calendar events', 'run') + .addItem('Settings', 'settings') + .addToUi(); +}; + +/** + * Opens the sidebar + */ +const settings = () => { + const html = HtmlService.createHtmlOutputFromFile('Page') + .setTitle('Settings'); + + SpreadsheetApp.getUi().showSidebar(html); +}; + +/** +* returns the settings from the script properties +*/ +const getSettings = () => { + const settings = {}; + + // get the current settings + const savedCalendarSettings = JSON.parse(PropertiesService.getScriptProperties().getProperty('calendar') || '[]'); + + // get the primary calendar + const primaryCalendar = CalendarApp.getAllCalendars() + .filter((cal) => cal.isMyPrimaryCalendar()) + .map((cal) => ({ + name: 'Primary calendar', + id: cal.getId() + })); + + // get the secondary calendars + const secundaryCalendars = CalendarApp.getAllCalendars() + .filter((cal) => cal.isOwnedByMe() && !cal.isMyPrimaryCalendar()) + .map((cal) => ({ + name: cal.getName(), + id: cal.getId() + })); + + // the current available calendars + const availableCalendars = primaryCalendar.concat(secundaryCalendars); + + // find any calendars that were removed + const unavailebleCalendars = []; + savedCalendarSettings.forEach((savedCalendarSetting) => { + if (!availableCalendars.find((availableCalendar) => availableCalendar.id === savedCalendarSetting.id)) { + unavailebleCalendars.push(savedCalendarSetting); + } + }); + + // map the current settings to the available calendars + const calendarSettings = availableCalendars.map((availableCalendar) => { + if (savedCalendarSettings.find((savedCalendar) => savedCalendar.id === availableCalendar.id)) { + availableCalendar.sync = true; + + } + return availableCalendar; + }); + + // add the calendar settings to the settings + settings.calendarSettings = calendarSettings; + + const savedFrom = PropertiesService.getScriptProperties().getProperty('syncFrom'); + settings.syncFrom = savedFrom; + + const savedTo = PropertiesService.getScriptProperties().getProperty('syncTo'); + settings.syncTo = savedTo; + + const savedIsUpdateTitle = PropertiesService.getScriptProperties().getProperty('isUpdateTitle') === 'true'; + settings.isUpdateCalendarItemTitle = savedIsUpdateTitle; + + const savedIsUseCategoriesAsCalendarItemTitle = PropertiesService.getScriptProperties().getProperty('isUseCategoriesAsCalendarItemTitle') === 'true'; + settings.isUseCategoriesAsCalendarItemTitle = savedIsUseCategoriesAsCalendarItemTitle; + + const savedIsUpdateDescription = PropertiesService.getScriptProperties().getProperty('isUpdateDescription') === 'true'; + settings.isUpdateCalendarItemDescription = savedIsUpdateDescription; + + return settings; +}; + +/** +* Saves the settings from the sidebar +*/ +const saveSettings = (settings) => { + PropertiesService.getScriptProperties().setProperty('calendar', JSON.stringify(settings.calendarSettings)); + PropertiesService.getScriptProperties().setProperty('syncFrom', settings.syncFrom); + PropertiesService.getScriptProperties().setProperty('syncTo', settings.syncTo); + PropertiesService.getScriptProperties().setProperty('isUpdateTitle', settings.isUpdateCalendarItemTitle); + PropertiesService.getScriptProperties().setProperty('isUseCategoriesAsCalendarItemTitle', settings.isUseCategoriesAsCalendarItemTitle); + PropertiesService.getScriptProperties().setProperty('isUpdateDescription', settings.isUpdateCalendarItemDescription); + return 'Settings saved'; +}; + +/** + * Builds the myTime object and runs the synchronisation + */ +const run = () => { + 'use strict'; + myTime({ + mainSpreadsheetId: SpreadsheetApp.getActiveSpreadsheet().getId(), + }).run(); +}; + +/** + * The main function used for the synchronisation + * @param {Object} par The main parameter object. + * @return {Object} The myTime Object. + */ +const myTime = (par) => { + 'use strict'; + + /** + * Format the sheet + */ + const formatSheet = () => { + // sort decending on start date + hourSheet.sort(3, false); + + // hide the technical columns + hourSheet.hideColumns(1, 2); + + // remove any extra rows + if (hourSheet.getLastRow() > 1 && hourSheet.getLastRow() < hourSheet.getMaxRows()) { + hourSheet.deleteRows(hourSheet.getLastRow() + 1, hourSheet.getMaxRows() - hourSheet.getLastRow()); + } + + // set the validation for the customers + let rule = SpreadsheetApp.newDataValidation() + .requireValueInRange(categoriesSheet.getRange('A2:A'), true) + .setAllowInvalid(true) + .build(); + hourSheet.getRange('I2:I').setDataValidation(rule); + + // set the validation for the projects + rule = SpreadsheetApp.newDataValidation() + .requireValueInRange(categoriesSheet.getRange('B2:B'), true) + .setAllowInvalid(true) + .build(); + hourSheet.getRange('J2:J').setDataValidation(rule); + + // set the validation for the tsaks + rule = SpreadsheetApp.newDataValidation() + .requireValueInRange(categoriesSheet.getRange('C2:C'), true) + .setAllowInvalid(true) + .build(); + hourSheet.getRange('K2:K').setDataValidation(rule); + + if(isUseCategoriesAsCalendarItemTitle) { + hourSheet.getRange('L2:L').setFormulaR1C1('IF(OR(R[0]C[-3]="tbd";R[0]C[-2]="tbd";R[0]C[-1]="tbd");""; CONCATENATE(R[0]C[-3];"|";R[0]C[-2];"|";R[0]C[-1];"|"))'); + } + // set the hours, month, week and number collumns + hourSheet.getRange('P2:P').setFormulaR1C1('=IF(R[0]C[-12]="";"";R[0]C[-12]-R[0]C[-13])'); + hourSheet.getRange('Q2:Q').setFormulaR1C1('=IF(R[0]C[-13]="";"";month(R[0]C[-13]))'); + hourSheet.getRange('R2:R').setFormulaR1C1('=IF(R[0]C[-14]="";"";WEEKNUM(R[0]C[-14];2))'); + hourSheet.getRange('S2:S').setFormulaR1C1('=R[0]C[-3]'); + }; + + /** + * Activate the synchronisation + */ + function run() { + console.log('Started processing hours.'); + + const processCalendar = (setting) => { + SpreadsheetApp.flush(); + + // current calendar info + const calendarName = setting.name; + const calendarId = setting.id; + + console.log(`processing ${calendarName} with the id ${calendarId} from ${syncStartDate} to ${syncEndDate}`); + + // get the calendar + const calendar = CalendarApp.getCalendarById(calendarId); + + // get the calendar events and create lookups + const events = calendar.getEvents(syncStartDate, syncEndDate); + const eventsLookup = events.reduce((jsn, event) => { + jsn[event.getId()] = event; + return jsn; + }, {}); + + // get the sheet events and create lookups + const existingEvents = hourSheet.getDataRange().getValues().slice(1); + const existingEventsLookUp = existingEvents.reduce((jsn, row, index) => { + if (row[0] !== calendarId) { + return jsn; + } + jsn[row[1]] = { + event: row, + row: index + 2 + }; + return jsn; + }, {}); + + // handle a calendar event + const handleEvent = (event) => { + const eventId = event.getId(); + + // new event + if (!existingEventsLookUp[eventId]) { + hourSheet.appendRow([ + calendarId, + eventId, + event.getStartTime(), + event.getEndTime(), + calendarName, + event.getCreators().join(','), + event.getTitle(), + event.getDescription(), + event.getTag('Client') || 'tbd', + event.getTag('Project') || 'tbd', + event.getTag('Task') || 'tbd', + (isUpdateCalendarItemTitle) ? '' : event.getTitle(), + (isUpdateCalendarItemDescription) ? '' : event.getDescription(), + event.getGuestList().map((guest) => guest.getEmail()).join(','), + event.getLocation(), + undefined, + undefined, + undefined, + undefined + ]); + return true; + } + + // existing event + const exisitingEvent = existingEventsLookUp[eventId].event; + const exisitingEventRow = existingEventsLookUp[eventId].row; + + if (event.getStartTime() - exisitingEvent[startTimeColumn - 1] !== 0) { + hourSheet.getRange(exisitingEventRow, startTimeColumn).setValue(event.getStartTime()); + } + + if (event.getEndTime() - exisitingEvent[endTimeColumn - 1] !== 0) { + hourSheet.getRange(exisitingEventRow, endTimeColumn).setValue(event.getEndTime()); + } + + if (event.getCreators().join(',') !== exisitingEvent[creatorsColumn - 1]) { + hourSheet.getRange(exisitingEventRow, creatorsColumn).setValue(event.getCreators()[0]); + } + + if (event.getGuestList().map((guest) => guest.getEmail()).join(',') !== exisitingEvent[guestListColumn - 1]) { + hourSheet.getRange(exisitingEventRow, guestListColumn).setValue(event.getGuestList().map((guest) => guest.getEmail()).join(',')); + } + + if (event.getLocation() !== exisitingEvent[locationColumn - 1]) { + hourSheet.getRange(exisitingEventRow, locationColumn).setValue(event.getLocation()); + } + + if(event.getTitle() !== exisitingEvent[titleColumn - 1]) { + if(!isUpdateCalendarItemTitle) { + hourSheet.getRange(exisitingEventRow, titleColumn).setValue(event.getTitle()); + } + if(isUpdateCalendarItemTitle) { + event.setTitle(exisitingEvent[titleColumn - 1]); + } + } + + if(event.getDescription() !== exisitingEvent[descriptionColumn - 1]) { + if(!isUpdateCalendarItemDescription) { + hourSheet.getRange(exisitingEventRow, descriptionColumn).setValue(event.getDescription()); + } + if(isUpdateCalendarItemDescription) { + event.setDescription(exisitingEvent[descriptionColumn - 1]); + } + } + + return true; + }; + + // process each event for the calendar + events.every(handleEvent); + + // remove any events in the sheet that are not in de calendar + existingEvents.every((event, index) => { + if (event[0] !== calendarId) { + return true; + }; + + if (eventsLookup[event[1]]) { + return true; + } + + if (event[3] < syncStartDate) { + return true; + } + + hourSheet.getRange(index + 2, 1, 1, 20).clear(); + return true; + }); + + return true; + }; + + // process the calendars + settings.calendarSettings.filter((calenderSetting) => calenderSetting.sync === true).every(processCalendar); + + formatSheet(); + SpreadsheetApp.setActiveSheet(hourSheet); + + console.log('Finished processing hours.'); + } + + const mainSpreadSheetId = par.mainSpreadsheetId; + const mainSpreadsheet = SpreadsheetApp.openById(mainSpreadSheetId); + const hourSheet = mainSpreadsheet.getSheetByName('Hours'); + const categoriesSheet = mainSpreadsheet.getSheetByName('Categories'); + const settings = getSettings(); + + const syncStartDate = new Date(); + syncStartDate.setDate(syncStartDate.getDate() - Number(settings.syncFrom)); + + const syncEndDate = new Date(); + syncEndDate.setDate(syncEndDate.getDate() + Number(settings.syncTo)); + + const isUpdateCalendarItemTitle = settings.isUpdateCalendarItemTitle; + const isUseCategoriesAsCalendarItemTitle = settings.isUseCategoriesAsCalendarItemTitle; + const isUpdateCalendarItemDescription = settings.isUpdateCalendarItemDescription; + + const startTimeColumn = 3; + const endTimeColumn = 4; + const creatorsColumn = 6; + const originalTitleColumn = 7; + const originalDescriptionColumn = 8; + const clientColumn = 9; + const projectColumn = 10; + const taskColumn = 11; + const titleColumn = 12; + const descriptionColumn = 13; + const guestListColumn = 14; + const locationColumn = 15; + + return Object.freeze({ + run: run, + }); +}; \ No newline at end of file diff --git a/solutions/automations/calendar-timesheet/Page.html b/solutions/automations/calendar-timesheet/Page.html new file mode 100644 index 000000000..c9bec2173 --- /dev/null +++ b/solutions/automations/calendar-timesheet/Page.html @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/solutions/automations/calendar-timesheet/README.md b/solutions/automations/calendar-timesheet/README.md new file mode 100644 index 000000000..0c90af2c2 --- /dev/null +++ b/solutions/automations/calendar-timesheet/README.md @@ -0,0 +1,3 @@ +# Record time and activities in Calendar and Sheets + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/calendar-timesheet) for additional details. diff --git a/solutions/automations/calendar-timesheet/appsscript.json b/solutions/automations/calendar-timesheet/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/calendar-timesheet/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/content-signup/.clasp.json b/solutions/automations/content-signup/.clasp.json new file mode 100644 index 000000000..0543fbf7b --- /dev/null +++ b/solutions/automations/content-signup/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1G8TfU6Rfcl76Uo4gKig7jFMYKai-V_fiUNbO12pAb25pA4_uyxN5PSvd"} diff --git a/solutions/automations/content-signup/Code.js b/solutions/automations/content-signup/Code.js new file mode 100644 index 000000000..e48e64cd7 --- /dev/null +++ b/solutions/automations/content-signup/Code.js @@ -0,0 +1,129 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/content-signup + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// To use your own template doc, update the below variable with the URL of your own Google Doc template. +// Make sure you update the sharing settings so that 'anyone' or 'anyone in your organization' can view. +const EMAIL_TEMPLATE_DOC_URL = 'https://docs.google.com/document/d/1enes74gWsMG3dkK3SFO08apXkr0rcYBd3JHKOb2Nksk/edit?usp=sharing'; +// Update this variable to customize the email subject. +const EMAIL_SUBJECT = 'Hello, here is the content you requested'; + +// Update this variable to the content titles and URLs you want to offer. Make sure you update the form so that the content titles listed here match the content titles you list in the form. +const topicUrls = { + 'Google Calendar how-to videos': 'https://www.youtube.com/playlist?list=PLU8ezI8GYqs7IPb_UdmUNKyUCqjzGO9PJ', + 'Google Drive how-to videos': 'https://www.youtube.com/playlist?list=PLU8ezI8GYqs7Y5d1cgZm2Obq7leVtLkT4', + 'Google Docs how-to videos': 'https://www.youtube.com/playlist?list=PLU8ezI8GYqs4JKwZ-fpBP-zSoWPL8Sit7', + 'Google Sheets how-to videos': 'https://www.youtube.com/playlist?list=PLU8ezI8GYqs61ciKpXf_KkV7ZRbRHVG38', +}; + +/** + * Installs a trigger on the spreadsheet for when someone submits a form. + */ +function installTrigger() { + ScriptApp.newTrigger('onFormSubmit') + .forSpreadsheet(SpreadsheetApp.getActive()) + .onFormSubmit() + .create(); +} + +/** + * Sends a customized email for every form response. + * + * @param {Object} event - Form submit event + */ +function onFormSubmit(e) { + let responses = e.namedValues; + + // If the question title is a label, it can be accessed as an object field. + // If it has spaces or other characters, it can be accessed as a dictionary. + let timestamp = responses.Timestamp[0]; + let email = responses['Email address'][0].trim(); + let name = responses.Name[0].trim(); + let topicsString = responses.Topics[0].toLowerCase(); + + // Parse topics of interest into a list (since there are multiple items + // that are saved in the row as blob of text). + let topics = Object.keys(topicUrls).filter(function(topic) { + // indexOf searches for the topic in topicsString and returns a non-negative + // index if the topic is found, or it will return -1 if it's not found. + return topicsString.indexOf(topic.toLowerCase()) != -1; + }); + + // If there is at least one topic selected, send an email to the recipient. + let status = ''; + if (topics.length > 0) { + MailApp.sendEmail({ + to: email, + subject: EMAIL_SUBJECT, + htmlBody: createEmailBody(name, topics), + }); + status = 'Sent'; + } + else { + status = 'No topics selected'; + } + + // Append the status on the spreadsheet to the responses' row. + let sheet = SpreadsheetApp.getActiveSheet(); + let row = sheet.getActiveRange().getRow(); + let column = e.values.length + 1; + sheet.getRange(row, column).setValue(status); + + console.log("status=" + status + "; responses=" + JSON.stringify(responses)); +} + +/** + * Creates email body and includes the links based on topic. + * + * @param {string} recipient - The recipient's email address. + * @param {string[]} topics - List of topics to include in the email body. + * @return {string} - The email body as an HTML string. + */ +function createEmailBody(name, topics) { + let topicsHtml = topics.map(function(topic) { + let url = topicUrls[topic]; + return '
  • ' + topic + '
  • '; + }).join(''); + topicsHtml = '
      ' + topicsHtml + '
    '; + + // Make sure to update the emailTemplateDocId at the top. + let docId = DocumentApp.openByUrl(EMAIL_TEMPLATE_DOC_URL).getId(); + let emailBody = docToHtml(docId); + emailBody = emailBody.replace(/{{NAME}}/g, name); + emailBody = emailBody.replace(/{{TOPICS}}/g, topicsHtml); + return emailBody; +} + +/** + * Downloads a Google Doc as an HTML string. + * + * @param {string} docId - The ID of a Google Doc to fetch content from. + * @return {string} The Google Doc rendered as an HTML string. + */ +function docToHtml(docId) { + + // Downloads a Google Doc as an HTML string. + let url = "https://docs.google.com/feeds/download/documents/export/Export?id=" + + docId + "&exportFormat=html"; + let param = { + method: "get", + headers: {"Authorization": "Bearer " + ScriptApp.getOAuthToken()}, + muteHttpExceptions: true, + }; + return UrlFetchApp.fetch(url, param).getContentText(); +} diff --git a/solutions/automations/content-signup/README.md b/solutions/automations/content-signup/README.md new file mode 100644 index 000000000..ac50839e6 --- /dev/null +++ b/solutions/automations/content-signup/README.md @@ -0,0 +1,3 @@ +# Send curated content + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/content-signup) for additional details. diff --git a/solutions/automations/content-signup/appsscript.json b/solutions/automations/content-signup/appsscript.json new file mode 100644 index 000000000..ba13ec1c1 --- /dev/null +++ b/solutions/automations/content-signup/appsscript.json @@ -0,0 +1,14 @@ +{ + "timeZone": "America/Los_Angeles", + "dependencies": {}, + "exceptionLogging": "STACKDRIVER", + "oauthScopes": [ + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/drive.readonly", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/script.scriptapp", + "https://www.googleapis.com/auth/script.send_mail", + "https://www.googleapis.com/auth/spreadsheets.currentonly" + ], + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/course-feedback-response/.clasp.json b/solutions/automations/course-feedback-response/.clasp.json new file mode 100644 index 000000000..ae51645f3 --- /dev/null +++ b/solutions/automations/course-feedback-response/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1k75E4EdC3TcJEGGIupBANjm5duvs35ORAU1Mg2_6DNXENo827dFzmFeC"} diff --git a/solutions/automations/course-feedback-response/Code.js b/solutions/automations/course-feedback-response/Code.js new file mode 100644 index 000000000..99dbad61f --- /dev/null +++ b/solutions/automations/course-feedback-response/Code.js @@ -0,0 +1,119 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/course-feedback-response + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Creates custom menu for user to run scripts. + */ +function onOpen() { + let ui = SpreadsheetApp.getUi(); + ui.createMenu('Form Reply Tool') + .addItem('Enable auto draft replies', 'installTrigger') + .addToUi(); +} + +/** + * Installs a trigger on the Spreadsheet for when a Form response is submitted. + */ +function installTrigger() { + ScriptApp.newTrigger('onFormSubmit') + .forSpreadsheet(SpreadsheetApp.getActive()) + .onFormSubmit() + .create(); +} + +/** + * Creates a draft email for every response on a form + * + * @param {Object} event - Form submit event + */ +function onFormSubmit(e) { + let responses = e.namedValues; + + // parse form response data + let timestamp = responses.Timestamp[0]; + let email = responses['Email address'][0].trim(); + + // create email body + let emailBody = createEmailBody(responses); + + // create draft email + createDraft(timestamp, email, emailBody); +} + +/** + * Creates email body and includes feedback from Google Form. + * + * @param {string} responses - The form response data + * @return {string} - The email body as an HTML string + */ +function createEmailBody(responses) { + // parse form response data + let name = responses.Name[0].trim(); + let industry = responses['What industry do you work in?'][0]; + let source = responses['How did you find out about this course?'][0]; + let rating = responses['On a scale of 1 - 5 how would you rate this course?'][0]; + let productFeedback = responses['What could be different to make it a 5 rating?'][0]; + let otherFeedback = responses['Any other feedback?'][0]; + + // create email body + let htmlBody = 'Hi ' + name + ',

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

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

    ' + + 'Have a great day!

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

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

    ' + + 'Your feedback:

    ' + + 'What industry do you work in?

    ' + + industry + '

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

    ' + + source + '

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

    ' + + rating + '

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

    ' + + productFeedback + '

    ' + + 'Any other feedback?

    ' + + otherFeedback + '

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

    + A new equipment request has been made by . +

    + +

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

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

    + An equipment request has been completed. +

    + +

    + Employee name:
    + Desk location name:
    +

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

    ` + + `Files successfully updated: ${processedCount}
    ` + + `${processedList}
    ` + + `Files not updated: ${unProcessedCount}
    ` + + `${unProcessedList}
    ` + + `
    View all updates in the Google Sheets spreadsheet ` + + `${sheetName}.
    ` + + `
    *************
    ` + + `
    This email was generated by Google Apps Script. ` + + `To learn more about this application or make changes, open the script project below:
    ` + + `${scriptName}` + + MailApp.sendEmail({ + to: emailTo, + subject: `Automated email from ${APP_TITLE}`, + htmlBody: eMailBody + }); + console.log(`Email sent to ${emailTo}`); +} + +/** + * Parses CSV data into an array and appends it after the last row in the destination spreadsheet. + * + * @return {boolean} true if the update is successful, false if unexpected errors occur. + */ +function processCsv_(objSpreadSheet, csvFile) { + + try { + // Gets the first sheet of the destination spreadsheet. + let sheet = objSpreadSheet.getSheets()[0]; + + // Parses CSV file into data array. + let data = Utilities.parseCsv(csvFile.getBlob().getDataAsString()); + + // Omits header row if application variable CSV_HEADER_EXIST is set to 'true'. + if (CSV_HEADER_EXIST) { + data.splice(0, 1); + } + // Gets the row and column coordinates for next available range in the spreadsheet. + let startRow = sheet.getLastRow() + 1; + let startCol = 1; + // Determines the incoming data size. + let numRows = data.length; + let numColumns = data[0].length; + + // Appends data into the sheet. + sheet.getRange(startRow, startCol, numRows, numColumns).setValues(data); + return true; // Success. + + } catch { + return false; // Failure. Checks for CSV data file error. + } +} diff --git a/solutions/automations/import-csv-sheets/README.md b/solutions/automations/import-csv-sheets/README.md new file mode 100644 index 000000000..1334e63f2 --- /dev/null +++ b/solutions/automations/import-csv-sheets/README.md @@ -0,0 +1,3 @@ +# Import CSV data to a spreadsheet + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/import-csv-sheets) for additional details. diff --git a/solutions/automations/import-csv-sheets/SampleData.js b/solutions/automations/import-csv-sheets/SampleData.js new file mode 100644 index 000000000..4fb3afec1 --- /dev/null +++ b/solutions/automations/import-csv-sheets/SampleData.js @@ -0,0 +1,190 @@ +/** + * 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. + */ + +/** + * This file contains functions to access headings and data for sample files. + * + * Sample data is stored in the variable SAMPLE_DATA. + */ + +// Fictitious sample data. +const SAMPLE_DATA = { + "headings": [ + "PropertyName", + "LeaseID", + "LeaseLocation", + "OwnerName", + "SquareFootage", + "RenewDate", + "LastAmount", + "LastPaymentDate", + "Revenue" + ], + "csvFiles": [ + { + "name": "Sample One.CSV", + "rows": [ + { + "PropertyName": "The Modern Building", + "LeaseID": "271312", + "LeaseLocation": "Mountain View CA 94045", + "OwnerName": "Yuri", + "SquareFootage": "17500", + "RenewDate": "12/15/2022", + "LastAmount": "100000", + "LastPaymentDate": "3/01/2022", + "Revenue": "12000" + }, + { + "PropertyName": "Garage @ 45", + "LeaseID": "271320", + "LeaseLocation": "Mountain View CA 94045", + "OwnerName": "Luka", + "SquareFootage": "1000", + "RenewDate": "6/2/2022", + "LastAmount": "50000", + "LastPaymentDate": "4/01/2022", + "Revenue": "20000" + }, + { + "PropertyName": "Office Park Deluxe", + "LeaseID": "271301", + "LeaseLocation": "Mountain View CA 94045", + "OwnerName": "Sasha", + "SquareFootage": "5000", + "RenewDate": "6/2/2022", + "LastAmount": "25000", + "LastPaymentDate": "4/01/2022", + "Revenue": "1200" + } + ] + }, + { + "name": "Sample Two.CSV", + "rows": [ + { + "PropertyName": "Tours Jumelles Minuscules", + "LeaseID": "271260", + "LeaseLocation": "8 Rue du Nom Fictif 341 Paris", + "OwnerName": "Lucian", + "SquareFootage": "1000000", + "RenewDate": "7/14/2022", + "LastAmount": "1250000", + "LastPaymentDate": "5/01/2022", + "Revenue": "77777" + }, + { + "PropertyName": "Barraca da Praia", + "LeaseID": "271281", + "LeaseLocation": "Avenida da Pastelaria 1903 Lisbon 1229-076", + "OwnerName": "Raha", + "SquareFootage": "1000", + "RenewDate": "6/2/2022", + "LastAmount": "50000", + "LastPaymentDate": "4/01/2022", + "Revenue": "20000" + } + ] + }, + { + "name": "Sample Three.CSV", + "rows": [ + { + "PropertyName": "Round Building in the Square", + "LeaseID": "371260", + "LeaseLocation": "8 Rue du Nom Fictif 341 Paris", + "OwnerName": "Charlie", + "SquareFootage": "75000", + "RenewDate": "8/1/2022", + "LastAmount": "250000", + "LastPaymentDate": "6/01/2022", + "Revenue": "22222" + }, + { + "PropertyName": "Square Building in the Round", + "LeaseID": "371281", + "LeaseLocation": "Avenida da Pastelaria 1903 Lisbon 1229-076", + "OwnerName": "Lee", + "SquareFootage": "10000", + "RenewDate": "6/2/2022", + "LastAmount": "5000", + "LastPaymentDate": "4/01/2022", + "Revenue": "1800" + } + ] + } + ] +} + + +/** + * Returns headings for use in destination spreadsheet and CSV files. + * @return {string[][]} array of each column heading as string. + */ +function getHeadings() { + let headings = [[]]; + for (let i in SAMPLE_DATA.headings) + headings[0].push(SAMPLE_DATA.headings[i]); + return (headings) +} + +/** + * Returns CSV file names and content to create sample CSV files. + * @return {object[]} {"file": ["name","csv"]} + */ +function getCSVFilesData() { + + let files = []; + + // Gets headings once - same for all files/rows. + let csvHeadings = ""; + for (let i in SAMPLE_DATA.headings) + csvHeadings += (SAMPLE_DATA.headings[i] + ','); + + // Gets data for each file by rows. + for (let i in SAMPLE_DATA.csvFiles) { + let sampleCSV = ""; + sampleCSV += csvHeadings; + let fileName = SAMPLE_DATA.csvFiles[i].name + for (let j in SAMPLE_DATA.csvFiles[i].rows) { + sampleCSV += '\n' + for (let k in SAMPLE_DATA.csvFiles[i].rows[j]) { + sampleCSV += SAMPLE_DATA.csvFiles[i].rows[j][k] + ',' + } + } + files.push({ name: fileName, csv: sampleCSV }) + } + return (files) +} + +/* + * Checks data functions are working as necessary. + */ +function test_getHeadings() { + let h = getHeadings() + console.log(h); + console.log(h[0].length); +} + +function test_getCSVFilesData() { + const csvFiles = getCSVFilesData(); + console.log(csvFiles) + + for (const file of csvFiles) { + console.log(file.name) + console.log(file.csv) + } +} \ No newline at end of file diff --git a/solutions/automations/import-csv-sheets/SetupSample.js b/solutions/automations/import-csv-sheets/SetupSample.js new file mode 100644 index 000000000..f7670293e --- /dev/null +++ b/solutions/automations/import-csv-sheets/SetupSample.js @@ -0,0 +1,111 @@ +/** + * 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. + */ + +/** + * This file contains functions that set up the folders and sample files used to demo the application. + * + * Sample data for the application is stored in the SampleData.gs file. + */ + +// Global variables for sample setup. +const INCLUDE_SAMPLE_DATA_FILES = true; // Set to true to create sample data files, false to skip. + +/** + * Runs the setup for the sample. + * 1) Creates the application folder and subfolders for unprocessed/processed CSV files. + * from global variables APP_FOLDER | SOURCE_FOLDER | PROCESSED_FOLDER + * 2) Creates the sample Sheets spreadsheet in the application folder. + * from global variable SHEET_REPORT_NAME + * 3) Creates CSV files from sample data in the unprocessed files folder. + * from variable SAMPLE_DATA in SampleData.gs. + * 4) Creates an installable trigger to run process automatically at a specified time interval. + */ +function setupSample() { + + console.log(`Application setup for: ${APP_TITLE}`) + + // Creates application folder. + const folderAppPrimary = getApplicationFolder_(APP_FOLDER); + // Creates supporting folders. + const folderSource = getFolder_(SOURCE_FOLDER); + const folderProcessed = getFolder_(PROCESSED_FOLDER); + + console.log(`Application folders: ${folderAppPrimary.getName()}, ${folderSource.getName()}, ${folderProcessed.getName()}`) + + if (INCLUDE_SAMPLE_DATA_FILES) { + + // Sets up primary destination spreadsheet + const sheet = setupPrimarySpreadsheet_(folderAppPrimary); + + // Gets the CSV files data - refer to the SampleData.gs file to view. + const csvFiles = getCSVFilesData(); + + // Processes each CSV file. + for (const file of csvFiles) { + // Creates CSV file in source folder if it doesn't exist. + if (!fileExists_(file.name, folderSource)) { + let csvFileId = DriveApp.createFile(file.name, file.csv, MimeType.CSV); + console.log(`Created Sample CSV: ${file.name}`) + csvFileId.moveTo(folderSource); + } + } + } + // Installs (or recreates) project trigger + installTrigger() + + console.log(`Setup completed for: ${APP_TITLE}`) +} + +/** + * + */ +function setupPrimarySpreadsheet_(folderAppPrimary) { + + // Creates the report destination spreadsheet if doesn't exist. + if (!fileExists_(SHEET_REPORT_NAME, folderAppPrimary)) { + + // Creates new destination spreadsheet (report) with cell size of 20 x 10. + const sheet = SpreadsheetApp.create(SHEET_REPORT_NAME, 20, 10); + + // Adds the sample data headings. + let sheetHeadings = getHeadings(); + sheet.getSheets()[0].getRange(1, 1, 1, sheetHeadings[0].length).setValues(sheetHeadings); + SpreadsheetApp.flush(); + // Moves to primary application root folder. + DriveApp.getFileById(sheet.getId()).moveTo(folderAppPrimary) + + console.log(`Created file: ${SHEET_REPORT_NAME} In folder: ${folderAppPrimary.getName()}.`) + return sheet; + } +} + +/** + * Moves sample content to Drive trash & uninstalls trigger. + * This function removes all folders and content related to this application. + */ +function removeSample() { + getApplicationFolder_(APP_FOLDER).setTrashed(true); + console.log(`'${APP_FOLDER}' contents have been moved to Drive Trash folder.`) + + // Removes existing trigger if found. + const projectTriggers = ScriptApp.getProjectTriggers(); + for (var i = 0; i < projectTriggers.length; i++) { + if (projectTriggers[i].getHandlerFunction() == HANDLER_FUNCTION) { + console.log(`Existing trigger with handler function of '${HANDLER_FUNCTION}' removed.`); + ScriptApp.deleteTrigger(projectTriggers[i]); + } + } +} \ No newline at end of file diff --git a/solutions/automations/import-csv-sheets/Utilities.js b/solutions/automations/import-csv-sheets/Utilities.js new file mode 100644 index 000000000..f37b81a20 --- /dev/null +++ b/solutions/automations/import-csv-sheets/Utilities.js @@ -0,0 +1,142 @@ +/** + * 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. + */ + +/** + * This file contains utility functions that work with application's folder and files. + */ + +/** + * Gets application destination spreadsheet from a given folder + * Returns new sample version if orignal is not found. + * + * @param {string} fileName - Name of the file to test for. + * @param {object} objFolder - Folder object in which to search. + * @return {object} Spreadsheet object. + */ +function getSpreadSheet_(fileName, objFolder) { + + let files = objFolder.getFilesByName(fileName); + + while (files.hasNext()) { + let file = files.next(); + let fileId = file.getId(); + + const existingSpreadsheet = SpreadsheetApp.openById(fileId); + return existingSpreadsheet; + } + + // If application destination spreadsheet is missing, creates a new sample version. + const folderAppPrimary = getApplicationFolder_(APP_FOLDER); + const sampleSheet = setupPrimarySpreadsheet_(folderAppPrimary); + return sampleSheet; +} + +/** + * Tests if a file exists within a given folder. + * + * @param {string} fileName - Name of the file to test for. + * @param {object} objFolder - Folder object in which to search. + * @return {boolean} true if found in folder, false if not. + */ +function fileExists_(fileName, objFolder) { + + let files = objFolder.getFilesByName(fileName); + + while (files.hasNext()) { + let file = files.next(); + console.log(`${file.getName()} already exists.`) + return true; + } + return false; +} + +/** + * Returns folder named in folderName parameter. + * Checks if folder already exists, creates it if it doesn't. + * + * @param {string} folderName - Name of the Drive folder. + * @return {object} Google Drive Folder + */ +function getFolder_(folderName) { + + // Gets the primary folder for the application. + const parentFolder = getApplicationFolder_(); + + // Iterates subfolders to check if folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === folderName) { + return folder; + } + } + // Creates a new folder if one doesn't already exist. + return parentFolder.createFolder(folderName) + .setDescription(`Supporting folder created by ${APP_TITLE}.`); +} + +/** + * Returns the primary folder as named by the APP_FOLDER variable in the Code.gs file. + * Checks if folder already exists to avoid duplication. + * Creates new instance if existing folder not found. + * + * @return {object} Google Drive Folder + */ +function getApplicationFolder_() { + + // Gets root folder, currently set to 'My Drive' + const parentFolder = DriveApp.getRootFolder(); + + // Iterates through the subfolders to check if folder already exists. + const subFolders = parentFolder.getFolders(); + while (subFolders.hasNext()) { + let folder = subFolders.next(); + + // Returns the existing folder if found. + if (folder.getName() === APP_FOLDER) { + return folder; + } + } + // Creates a new folder if one doesn't already exist. + return parentFolder.createFolder(APP_FOLDER) + .setDescription(`Main application folder created by ${APP_TITLE}.`); +} + +/** + * Tests getApplicationFolder_ and getFolder_ + * @logs details of created Google Drive folder. + */ +function test_getFolderByName() { + + let folder = getApplicationFolder_() + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`) + // Uncomment the following to automatically delete test folder. + // folder.setTrashed(true); + + folder = getFolder_(SOURCE_FOLDER); + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`) + // Uncomment the following to automatically delete test folder. + // folder.setTrashed(true); + + folder = getFolder_(PROCESSED_FOLDER); + console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`) + // Uncomment the following to automatically delete test folder. + // folder.setTrashed(true); + + +} \ No newline at end of file diff --git a/solutions/automations/import-csv-sheets/appsscript.json b/solutions/automations/import-csv-sheets/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/import-csv-sheets/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/mail-merge/.clasp.json b/solutions/automations/mail-merge/.clasp.json new file mode 100644 index 000000000..7f25c6014 --- /dev/null +++ b/solutions/automations/mail-merge/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1evL25lW9fLN43j6gGBJWtLq4GncLkdgoxxSVCawc8dWNoLoravNebAih"} diff --git a/solutions/automations/mail-merge/Code.js b/solutions/automations/mail-merge/Code.js new file mode 100644 index 000000000..7784dd31f --- /dev/null +++ b/solutions/automations/mail-merge/Code.js @@ -0,0 +1,210 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/mail-merge + +/* +Copyright 2022 Martin Hawksey + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @OnlyCurrentDoc +*/ + +/** + * Change these to match the column names you are using for email + * recipient addresses and email sent column. +*/ +const RECIPIENT_COL = "Recipient"; +const EMAIL_SENT_COL = "Email Sent"; + +/** + * Creates the menu item "Mail Merge" for user to run scripts on drop-down. + */ +function onOpen() { + const ui = SpreadsheetApp.getUi(); + ui.createMenu('Mail Merge') + .addItem('Send Emails', 'sendEmails') + .addToUi(); +} + +/** + * Sends emails from sheet data. + * @param {string} subjectLine (optional) for the email draft message + * @param {Sheet} sheet to read data from +*/ +function sendEmails(subjectLine, sheet=SpreadsheetApp.getActiveSheet()) { + // option to skip browser prompt if you want to use this code in other projects + if (!subjectLine){ + subjectLine = Browser.inputBox("Mail Merge", + "Type or copy/paste the subject line of the Gmail " + + "draft message you would like to mail merge with:", + Browser.Buttons.OK_CANCEL); + + if (subjectLine === "cancel" || subjectLine == ""){ + // If no subject line, finishes up + return; + } + } + + // Gets the draft Gmail message to use as a template + const emailTemplate = getGmailTemplateFromDrafts_(subjectLine); + + // Gets the data from the passed sheet + const dataRange = sheet.getDataRange(); + // Fetches displayed values for each row in the Range HT Andrew Roberts + // https://mashe.hawksey.info/2020/04/a-bulk-email-mail-merge-with-gmail-and-google-sheets-solution-evolution-using-v8/#comment-187490 + // @see https://developers.google.com/apps-script/reference/spreadsheet/range#getdisplayvalues + const data = dataRange.getDisplayValues(); + + // Assumes row 1 contains our column headings + const heads = data.shift(); + + // Gets the index of the column named 'Email Status' (Assumes header names are unique) + // @see http://ramblings.mcpher.com/Home/excelquirks/gooscript/arrayfunctions + const emailSentColIdx = heads.indexOf(EMAIL_SENT_COL); + + // Converts 2d array into an object array + // See https://stackoverflow.com/a/22917499/1027723 + // For a pretty version, see https://mashe.hawksey.info/?p=17869/#comment-184945 + const obj = data.map(r => (heads.reduce((o, k, i) => (o[k] = r[i] || '', o), {}))); + + // Creates an array to record sent emails + const out = []; + + // Loops through all the rows of data + obj.forEach(function(row, rowIdx){ + // Only sends emails if email_sent cell is blank and not hidden by a filter + if (row[EMAIL_SENT_COL] == ''){ + try { + const msgObj = fillInTemplateFromObject_(emailTemplate.message, row); + + // See https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object) + // If you need to send emails with unicode/emoji characters change GmailApp for MailApp + // Uncomment advanced parameters as needed (see docs for limitations) + GmailApp.sendEmail(row[RECIPIENT_COL], msgObj.subject, msgObj.text, { + htmlBody: msgObj.html, + // bcc: 'a.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'); + }; +} diff --git a/solutions/automations/mail-merge/README.md b/solutions/automations/mail-merge/README.md new file mode 100644 index 000000000..82e2b8f98 --- /dev/null +++ b/solutions/automations/mail-merge/README.md @@ -0,0 +1,3 @@ +# Create a mail merge with Gmail & Google Sheets + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/mail-merge) for additional details. diff --git a/solutions/automations/mail-merge/appsscript.json b/solutions/automations/mail-merge/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/mail-merge/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/news-sentiment/.clasp.json b/solutions/automations/news-sentiment/.clasp.json new file mode 100644 index 000000000..80b47c971 --- /dev/null +++ b/solutions/automations/news-sentiment/.clasp.json @@ -0,0 +1 @@ +{"scriptId":"1KHPvTOwE2pd2myZmvX0mbsp8SPlhJBFotNCwflZiP01xmTasNfibG4zl"} \ No newline at end of file diff --git a/solutions/automations/news-sentiment/Code.js b/solutions/automations/news-sentiment/Code.js new file mode 100644 index 000000000..7d084f907 --- /dev/null +++ b/solutions/automations/news-sentiment/Code.js @@ -0,0 +1,256 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/news-sentiment + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Global variables +const googleAPIKey = 'YOUR_GOOGLE_API_KEY'; +const newsApiKey = 'YOUR_NEWS_API_KEY'; +const apiEndPointHdr = 'https://newsapi.org/v2/everything?q='; +const happyFace = + '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/24/smiley-1635449_1280.png\")'; +const mehFace = + '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/24/smiley-1635450_1280.png\")'; +const sadFace = + '=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/25/smiley-1635454_1280.png\")'; +const happyColor = '#44f83d'; +const mehColor = '#f7f6cc'; +const sadColor = '#ff3c3d'; +const fullsheet = 'A2:D25'; +const sentimentCols = 'B2:D25'; +const articleMax = 20; +const threshold = 0.3; + +let headlines = []; +let rows = null; +let rowValues = null; +let topic = null; +let bottomRow = 0; +let ds = null; +let ss = null; +let headerRow = null; +let sentimentCol = null; +let headlineCol = null; +let scoreCol = null; + +/** + * Creates menu in the Google Sheets spreadsheet when the spreadsheet is opened. + * + */ +function onOpen() { + let ui = SpreadsheetApp.getUi(); + ui.createMenu('News Headlines Sentiments') + .addItem('Analyze News Headlines...', 'showNewsPrompt') + .addToUi(); +} + +/** + * Prompts user to enter a new headline topic. + * Calls main function AnalyzeHeadlines with entered topic. + */ +function showNewsPrompt() { + //Initializes global variables + ss = SpreadsheetApp.getActiveSpreadsheet(); + ds = ss.getSheetByName('Sheet1'); + headerRow = ds.getDataRange().getValues()[0]; + sentimentCol = headerRow.indexOf('Sentiment'); + headlineCol = headerRow.indexOf('Headlines'); + scoreCol = headerRow.indexOf('Score'); + + // Builds Menu + let ui = SpreadsheetApp.getUi(); + let result = ui.prompt( + 'Enter news topic:', + ui.ButtonSet.OK_CANCEL); + + // Processes the user's response. + let button = result.getSelectedButton(); + topic = result.getResponseText(); + if (button == ui.Button.OK) { + analyzeNewsHeadlines(); + } else if (button == ui.Button.CANCEL) { + // Shows alert if user clicked "Cancel." + ui.alert('News topic not selected!'); + } +} + +/** + * For each headline cell, calls the Natural Language API to get general sentiment and then updates + * the sentiment response column. + */ +function analyzeNewsHeadlines() { + // Clears and reformats the sheet + reformatSheet(); + + // Gets the headlines array + headlines = getHeadlinesArray(); + + // Syncs the headlines array to the sheet using a single setValues call + if (headlines.length > 0){ + ds.getRange(2, 1, headlines.length, headlineCol+1).setValues(headlines); + // Set global rowValues + rows = ds.getDataRange(); + rowValues = rows.getValues(); + getSentiments(); + } else { + ss.toast("No headlines returned for topic: " + topic + '!'); + } +} + +/** + * Fetches current headlines from the Free News API + */ +function getHeadlinesArray() { + // Fetches headlines for a given topic + let hdlnsResp = []; + let encodedtopic = encodeURIComponent(topic); + ss.toast("Getting headlines for: " + topic); + let response = UrlFetchApp.fetch(apiEndPointHdr + encodedtopic + '&apiKey=' + + newsApiKey); + let results = JSON.parse(response); + let articles = results["articles"]; + + for (let i = 0; i < articles.length && i < articleMax; i++) { + let newsStory = articles[i]['title']; + if (articles[i]['description'] !== null) { + newsStory += ': ' + articles[i]['description']; + } + // Scrubs newsStory of invalid characters + newsStory = scrub(newsStory); + + // Constructs hdlnsResp as a 2d array. This simplifies syncing to the sheet. + hdlnsResp.push(new Array(newsStory)); + } + + return hdlnsResp; +} + +/** + * For each article cell, calls the Natural Language API to get general sentiment and then updates + * the sentiment response columns. + */ +function getSentiments() { + ss.toast('Analyzing the headline sentiments...'); + + let articleCount = rows.getNumRows() - 1; + let avg = 0; + + // Gets sentiment for each row + for (let i = 1; i <= articleCount; i++) { + let headlineCell = rowValues[i][headlineCol]; + if (headlineCell) { + let sentimentData = retrieveSentiment(headlineCell); + let result = sentimentData['documentSentiment']['score']; + avg += result; + ds.getRange(i + 1, sentimentCol + 1).setBackgroundColor(getColor(result)); + ds.getRange(i + 1, sentimentCol + 1).setValue(getFace(result)); + ds.getRange(i + 1, scoreCol + 1).setValue(result); + } + } + let avgDecimal = (avg / articleCount).toFixed(2); + + // Shows news topic and average face, color and sentiment value. + bottomRow = articleCount + 3; + ds.getRange(bottomRow, 1, headlines.length, scoreCol+1).setFontWeight('bold'); + ds.getRange(bottomRow, headlineCol + 1).setValue('Topic: \"' + topic + '\"'); + ds.getRange(bottomRow, headlineCol + 2).setValue('Avg:'); + ds.getRange(bottomRow, sentimentCol + 1).setValue(getFace(avgDecimal)); + ds.getRange(bottomRow, sentimentCol + 1).setBackgroundColor(getColor(avgDecimal)); + ds.getRange(bottomRow, scoreCol + 1).setValue(avgDecimal); + ss.toast("Done!!"); +} + +/** + * Calls the Natureal Language API to get sentiment response for headline. + * + * Important note: Not all languages are supported by Google document + * sentiment analysis. + * Unsupported languages generate a "400" response: "INVALID_ARGUMENT". + */ +function retrieveSentiment(text) { + // Sets REST call options + let apiEndPoint = + 'https://language.googleapis.com/v1/documents:analyzeSentiment?key=' + + googleAPIKey; + let jsonReq = JSON.stringify({ + document: { + type: "PLAIN_TEXT", + content: text + }, + encodingType: "UTF8" + }); + + let options = { + 'method': 'post', + 'contentType': 'application/json', + 'payload': jsonReq + } + + // Makes the REST call + let response = UrlFetchApp.fetch(apiEndPoint, options); + let responseData = JSON.parse(response); + return responseData; +} + +// Helper Functions + +/** + * Removes old headlines, sentiments and reset formatting + */ +function reformatSheet() { + let range = ds.getRange(fullsheet); + range.clearContent(); + range.clearFormat(); + range.setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP); + + range = ds.getRange(sentimentCols); // Center the sentiment cols only + range.setHorizontalAlignment("center"); +} + +/** + * Returns a corresponding face based on numeric value. + */ +function getFace(value){ + if (value >= threshold) { + return happyFace; + } else if (value < threshold && value > -threshold){ + return mehFace; + } else if (value <= -threshold) { + return sadFace; + } +} + +/** + * Returns a corresponding color based on numeric value. + */ +function getColor(value){ + if (value >= threshold) { + return happyColor; + } else if (value < threshold && value > -threshold){ + return mehColor; + } else if (value <= -threshold) { + return sadColor; + } +} + +/** + * Scrubs invalid characters out of headline text. + * Can be expanded if needed. + */ +function scrub(text) { + return text.replace(/[\‘\,\“\”\"\'\’\-\n\â\€]/g, ' '); +} \ No newline at end of file diff --git a/solutions/automations/news-sentiment/README.md b/solutions/automations/news-sentiment/README.md new file mode 100644 index 000000000..14e247a6c --- /dev/null +++ b/solutions/automations/news-sentiment/README.md @@ -0,0 +1,3 @@ +# Connect to an external API: Analyze news headlines + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/news-sentiment) for additional details. \ No newline at end of file diff --git a/solutions/automations/news-sentiment/appsscript.json b/solutions/automations/news-sentiment/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/news-sentiment/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/offsite-activity-signup/.clasp.json b/solutions/automations/offsite-activity-signup/.clasp.json new file mode 100644 index 000000000..55715ed13 --- /dev/null +++ b/solutions/automations/offsite-activity-signup/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "10clpAH4ojSXvTlZaE74rhJ6dDwwkfvi24L_AilGROca5Nds2Jy2oZmvY"} diff --git a/solutions/automations/offsite-activity-signup/Code.js b/solutions/automations/offsite-activity-signup/Code.js new file mode 100644 index 000000000..5cac3f583 --- /dev/null +++ b/solutions/automations/offsite-activity-signup/Code.js @@ -0,0 +1,458 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/offsite-activity-signup + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const NUM_ITEMS_TO_RANK = 5; +const ACTIVITIES_PER_PERSON = 2; +const NUM_TEST_USERS = 150; + +/** + * Adds custom menu items when opening the sheet. + */ +function onOpen() { + let menu = SpreadsheetApp.getUi().createMenu('Activities') + .addItem('Create form', 'buildForm_') + .addItem('Generate test data', 'generateTestData_') + .addItem('Assign activities', 'assignActivities_') + .addToUi(); +} + +/** + * Builds a form based on the "Activity Schedule" sheet. The form asks attendees to rank their top + * N choices of activities, where N is defined by NUM_ITEMS_TO_RANK. + */ +function buildForm_() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + if (ss.getFormUrl()) { + let msg = 'Form already exists. Unlink the form and try again.'; + SpreadsheetApp.getUi().alert(msg); + return; + } + let form = FormApp.create('Activity Signup') + .setDestination(FormApp.DestinationType.SPREADSHEET, ss.getId()) + .setAllowResponseEdits(true) + .setLimitOneResponsePerUser(true) + .setCollectEmail(true); + let sectionHelpText = Utilities.formatString('Please choose your top %d activities', + NUM_ITEMS_TO_RANK); + form.addSectionHeaderItem() + .setTitle('Activity choices') + .setHelpText(sectionHelpText); + + // Presents activity ranking as a form grid with each activity as a row and rank as a column. + let rows = loadActivitySchedule_(ss).map(function(activity) { + return activity.description; + }); + let columns = range_(1, NUM_ITEMS_TO_RANK).map(function(value) { + return Utilities.formatString('%s', toOrdinal_(value)); + }); + let gridValidation = FormApp.createGridValidation() + .setHelpText('Select one item per column.') + .requireLimitOneResponsePerColumn() + .build(); + form.addGridItem() + .setColumns(columns) + .setRows(rows) + .setValidation(gridValidation); + + form.addListItem() + .setTitle('Assign other activities if choices are not available?') + .setChoiceValues(['Yes', 'No']); +} + +/** + * Assigns activities using a random priority/random serial dictatorship approach. The results + * are then populated into two new sheets, one listing activities per person, the other listing + * the rosters for each activity. + * + * See https://en.wikipedia.org/wiki/Random_serial_dictatorship for additional information. + */ +function assignActivities_() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let activities = loadActivitySchedule_(ss); + let activityIds = activities.map(function(activity) { + return activity.id; + }); + let attendees = loadAttendeeResponses_(ss, activityIds); + assignWithRandomPriority_(attendees, activities, 2); + writeAttendeeAssignments_(ss, attendees); + writeActivityRosters_(ss, activities); +} + +/** + * Selects activities via random priority. + * + * @param {object[]} attendees - Array of attendees to assign activities to + * @param {object[]} activities - Array of all available activities + * @param {number} numActivitiesPerPerson - Maximum number of activities to assign + */ +function assignWithRandomPriority_(attendees, activities, numActivitiesPerPerson) { + let activitiesById = activities.reduce(function(obj, activity) { + obj[activity.id] = activity; + return obj; + }, {}); + for (let i = 0; i < numActivitiesPerPerson; ++i) { + let randomizedAttendees = shuffleArray_(attendees); + randomizedAttendees.forEach(function(attendee) { + makeChoice_(attendee, activitiesById); + }); + } +} + +/** + * Attempts to assign an activity for an attendee based on their preferences and current schedule. + * + * @param {object} attendee - Attendee looking to join an activity + * @param {object} activitiesById - Map of all available activities + */ +function makeChoice_(attendee, activitiesById) { + for (let i = 0; i < attendee.preferences.length; ++i) { + let activity = activitiesById[attendee.preferences[i]]; + if (!activity) { + continue; + } + let canJoin = checkAvailability_(attendee, activity); + if (canJoin) { + attendee.assigned.push(activity); + activity.roster.push(attendee); + break; + } + } +} + +/** + * Checks that an activity has capacity and doesn't conflict with previously assigned + * activities. + * + * @param {object} attendee - Attendee looking to join the activity + * @param {object} activity - Proposed activity + * @return {boolean} - True if attendee can join the activity + */ +function checkAvailability_(attendee, activity) { + if (activity.capacity <= activity.roster.length) { + return false; + } + let timesConflict = attendee.assigned.some(function(assignedActivity) { + return !(assignedActivity.startAt.getTime() > activity.endAt.getTime() || + activity.startAt.getTime() > assignedActivity.endAt.getTime()); + }); + return !timesConflict; +}; + +/** + * Populates a sheet with the assigned activities for each attendee. + * + * @param {Spreadsheet} ss - Spreadsheet to write to. + * @param {object[]} attendees - Array of attendees with their activity assignments + */ +function writeAttendeeAssignments_(ss, attendees) { + let sheet = findOrCreateSheetByName_(ss, 'Activities by person'); + sheet.clear(); + sheet.appendRow(['Email address', 'Activities']); + sheet.getRange('B1:1').merge(); + let rows = attendees.map(function(attendee) { + // Prefill row to ensure consistent length otherwise + // can't bulk update the sheet with range.setValues() + let row = fillArray_([], ACTIVITIES_PER_PERSON + 1, ''); + row[0] = attendee.email; + attendee.assigned.forEach(function(activity, index) { + row[index + 1] = activity.description; + }); + return row; + }); + bulkAppendRows_(sheet, rows); + sheet.setFrozenRows(1); + sheet.getRange('1:1').setFontWeight('bold'); + sheet.autoResizeColumns(1, sheet.getLastColumn()); +} + +/** + * Populates a sheet with the rosters for each activity. + * + * @param {Spreadsheet} ss - Spreadsheet to write to. + * @param {object[]} activities - Array of activities with their rosters + */ +function writeActivityRosters_(ss, activities) { + let sheet = findOrCreateSheetByName_(ss, 'Activity rosters'); + sheet.clear(); + var rows = []; + var rows = activities.map(function(activity) { + let roster = activity.roster.map(function(attendee) { + return attendee.email; + }); + return [activity.description].concat(roster); + }); + // Transpose the data so each activity is a column + rows = transpose_(rows, ''); + bulkAppendRows_(sheet, rows); + sheet.setFrozenRows(1); + sheet.getRange('1:1').setFontWeight('bold'); + sheet.autoResizeColumns(1, sheet.getLastColumn()); +} + +/** + * Loads the activity schedule. + * + * @param {Spreadsheet} ss - Spreadsheet to load from + * @return {object[]} Array of available activities. + */ +function loadActivitySchedule_(ss) { + let timeZone = ss.getSpreadsheetTimeZone(); + let sheet = ss.getSheetByName('Activity Schedule'); + let rows = sheet.getSheetValues( + sheet.getFrozenRows() + 1, 1, + sheet.getLastRow() - 1, sheet.getLastRow()); + let activities = rows.map(function(row, index) { + let name = row[0]; + let startAt = new Date(row[1]); + let endAt = new Date(row[2]); + let capacity = parseInt(row[3]); + let formattedStartAt= Utilities.formatDate(startAt, timeZone, 'EEE hh:mm a'); + let formattedEndAt = Utilities.formatDate(endAt, timeZone, 'hh:mm a'); + let description = Utilities.formatString('%s (%s-%s)', name, formattedStartAt, formattedEndAt); + return { + id: index, + name: name, + description: description, + capacity: capacity, + startAt: startAt, + endAt: endAt, + roster: [], + }; + }); + return activities; +} + +/** + * Loads the attendeee response data. + * + * @param {Spreadsheet} ss - Spreadsheet to load from + * @param {number[]} allActivityIds - Full set of available activity IDs + * @return {object[]} Array of parsed attendee respones. + */ +function loadAttendeeResponses_(ss, allActivityIds) { + let sheet = findResponseSheetForForm_(ss); + + if (!sheet || sheet.getLastRow() == 1) { + return undefined; + } + + let rows = sheet.getSheetValues( + sheet.getFrozenRows() + 1, 1, + sheet.getLastRow() - 1, sheet.getLastRow()); + let attendees = rows.map(function(row) { + let _ = row.shift(); // Ignore timestamp + let email = row.shift(); + let autoAssign = row.pop(); + // Find ranked items in the response data. + let preferences = row.reduce(function(prefs, value, index) { + let match = value.match(/(\d+).*/); + if (!match) { + return prefs; + } + let rank = parseInt(match[1]) - 1; // Convert ordinal to array index + prefs[rank] = index; + return prefs; + }, []); + if (autoAssign == 'Yes') { + // If auto assigning additional activites, append a randomized list of all the activities. + // These will then be considered as if the attendee ranked them. + let additionalChoices = shuffleArray_(allActivityIds); + preferences = preferences.concat(additionalChoices); + } + return { + email: email, + preferences: preferences, + assigned: [], + }; + }); + return attendees; +} + +/** + * Simulates a large number of users responding to the form. This enables users to quickly + * experience the full solution without having to collect sufficient form responses + * through other means. + */ +function generateTestData_() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let sheet = findResponseSheetForForm_(ss); + if (!sheet) { + let msg = 'No response sheet found. Create the form and try again.'; + SpreadsheetApp.getUi().alert(msg); + } + if (sheet.getLastRow() > 1) { + let msg = 'Response sheet is not empty, can not generate test data. ' + + 'Remove responses and try again.'; + SpreadsheetApp.getUi().alert(msg); + return; + } + + let activities = loadActivitySchedule_(ss); + let choices = fillArray_([], activities.length, ''); + range_(1, 5).forEach(function(value) { + choices[value] = toOrdinal_(value); + }); + + let rows = range_(1, NUM_TEST_USERS).map(function(value) { + let randomizedChoices = shuffleArray_(choices); + let email = Utilities.formatString('person%d@example.com', value); + return [new Date(), email].concat(randomizedChoices).concat(['Yes']); + }); + bulkAppendRows_(sheet, rows); +} + +/** + * Retrieves a sheet by name, creating it if it doesn't yet exist. + * + * @param {Spreadsheet} ss - Containing spreadsheet + * @Param {string} name - Name of sheet to return + * @return {Sheet} Sheet instance + */ +function findOrCreateSheetByName_(ss, name) { + let sheet = ss.getSheetByName(name); + if (sheet) { + return sheet; + } + return ss.insertSheet(name); +} + +/** + * Faster version of appending multiple rows via ranges. Requires all rows are equal length. + * + * @param {Sheet} sheet - Sheet to append to + * @param {Array>} rows - Rows to append + */ +function bulkAppendRows_(sheet, rows) { + let startRow = sheet.getLastRow() + 1; + let startColumn = 1; + let numRows = rows.length; + let numColumns = rows[0].length; + sheet.getRange(startRow, startColumn, numRows, numColumns).setValues(rows); +} + +/** + * Copies and randomizes an array. + * + * @param {object[]} array - Array to shuffle + * @return {object[]} randomized copy of the array + */ +function shuffleArray_(array) { + let clone = array.slice(0); + for (let i = clone.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)); + let temp = clone[i]; + clone[i] = clone[j]; + clone[j] = temp; + } + return clone; +} + +/** + * Formats an number as an ordinal. + * + * See: https://stackoverflow.com/questions/13627308/add-st-nd-rd-and-th-ordinal-suffix-to-a-number/13627586 + * + * @param {number} i - Number to format + * @return {string} Formatted string + */ +function toOrdinal_(i) { + let j = i % 10; + let k = i % 100; + if (j == 1 && k != 11) { + return i + 'st'; + } + if (j == 2 && k != 12) { + return i + 'nd'; + } + if (j == 3 && k != 13) { + return i + 'rd'; + } + return i + 'th'; +} + +/** + * Locates the sheet containing the form responses. + * + * @param {Spreadsheet} ss - Spreadsheet instance to search + * @return {Sheet} Sheet with form responses, undefined if not found. + */ +function findResponseSheetForForm_(ss) { + let formUrl = ss.getFormUrl(); + if (!ss || !formUrl) { + return undefined; + } + let sheets = ss.getSheets(); + for (let i in sheets) { + if (sheets[i].getFormUrl() === formUrl) { + return sheets[i]; + } + } + return undefined; +} + +/** + * Fills an array with a value ([].fill() not supported in Apps Script). + * + * @param {object[]} arr - Array to fill + * @param {number} length - Number of items to fill. + * @param {object} value - Value to place at each index. + * @return {object[]} the array, for chaining purposes + */ +function fillArray_(arr, length, value) { + for (let i = 0; i < length; ++i) { + arr[i] = value; + } + return arr; +} + +/** + * Creates and fills an array with numbers in the range [start, end]. + * + * @param {number} start - First value in the range, inclusive + * @param {number} end - Last value in the range, inclusive + * @return {number[]} Array of values representing the range + */ +function range_(start, end) { + let arr = [start]; + let i = start; + while (i < end) { + arr.push(i += 1); + } + return arr; +} + +/** + * Transposes a matrix/2d array. For cases where the rows are not the same length, + * `fillValue` is used where no other value would otherwise be present. + * + * @param {Array>} arr - 2D array to transpose + * @param {object} fillValue - Placeholder for undefined values created as a result + * of the transpose. Only required if rows aren't all of equal length. + * @return {Array>} New transposed array + */ +function transpose_(arr, fillValue) { + let transposed = []; + arr.forEach(function(row, rowIndex) { + row.forEach(function(col, colIndex) { + transposed[colIndex] = transposed[colIndex] || fillArray_([], arr.length, fillValue); + transposed[colIndex][rowIndex] = row[colIndex]; + }); + }); + return transposed; +} diff --git a/solutions/automations/offsite-activity-signup/README.md b/solutions/automations/offsite-activity-signup/README.md new file mode 100644 index 000000000..06e8debc0 --- /dev/null +++ b/solutions/automations/offsite-activity-signup/README.md @@ -0,0 +1,3 @@ +# Create a sign-up for an offsite + +See [developers.google.com](https://developers.google.com/apps-script/samples/automations/offsite-activity-signup) for additional details. diff --git a/solutions/automations/offsite-activity-signup/appsscript.json b/solutions/automations/offsite-activity-signup/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/automations/offsite-activity-signup/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/automations/tax-loss-harvest-alerts/.clasp.json b/solutions/automations/tax-loss-harvest-alerts/.clasp.json new file mode 100644 index 000000000..b9dffc2b6 --- /dev/null +++ b/solutions/automations/tax-loss-harvest-alerts/.clasp.json @@ -0,0 +1 @@ +{"scriptId":"1SVf_XAGJiwksNTMnAwtlIvkKaDou4RLsmwGTa9ipVHKgwITgwXWqMixB"} \ No newline at end of file diff --git a/solutions/automations/tax-loss-harvest-alerts/Code.js b/solutions/automations/tax-loss-harvest-alerts/Code.js new file mode 100644 index 000000000..e25098b87 --- /dev/null +++ b/solutions/automations/tax-loss-harvest-alerts/Code.js @@ -0,0 +1,72 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/automations/tax-loss-harvest-alerts + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** +* Checks for losses in the sheet. +*/ +function checkLosses() { + // Pulls data from the spreadsheet + let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName( + "Calculations" + ); + let source = sheet.getRange("A:G"); + let data = source.getValues(); + + //Prepares the email alert content + let message = "Stocks:

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

    You have new comments and/or replies on videos:

    + + + + + + + + + + + + + +
    Video TitleLinkNumber of new replies and comments
    + + + diff --git a/solutions/chat-bots/schedule-meetings/.clasp.json b/solutions/chat-bots/schedule-meetings/.clasp.json new file mode 100644 index 000000000..34c4cfc7f --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1NdhQ_nXfEUUhWcWKiY6WJjeunY70a1W9vnFdS7BCLPMFreSaHaOS3ucM"} diff --git a/solutions/chat-bots/schedule-meetings/Code.js b/solutions/chat-bots/schedule-meetings/Code.js new file mode 100644 index 000000000..42b7f3f45 --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/Code.js @@ -0,0 +1,190 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/chat-bots/schedule-meetings + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Application constants +const BOTNAME = 'Chat Meeting Scheduler'; +const SLASHCOMMAND = { + HELP: 1, // /help + DIALOG: 2, // /schedule_Meeting +}; + +/** + * Responds to an ADDED_TO_SPACE event in Google Chat. + * Called when the bot is added to a space. The bot can either be directly added to the space + * or added by a @mention. If the bot is added by a @mention, the event object includes a message property. + * Returns a Message object, which is usually a welcome message informing users about the bot. + * + * @param {Object} event The event object from Google Chat + */ +function onAddToSpace(event) { + let message = ''; + + // Personalizes the message depending on how the bot is called. + if (event.space.singleUserBotDm) { + message = `Hi ${event.user.displayName}!`; + } else { + const spaceName = event.space.displayName ? event.space.displayName : "this chat"; + message = `Hi! Thank you for adding me to ${spaceName}`; + } + + // Lets users know what they can do and how they can get help. + message = message + '/nI can quickly schedule a meeting for you with just a few clicks.' + + 'Try me out by typing */schedule_Meeting*. ' + + '/nTo learn what else I can do, type */help*.' + + return { "text": message }; +} + +/** + * Responds to a MESSAGE event triggered in Chat. + * Called when the bot is already in the space and the user invokes it via @mention or / command. + * Returns a message object containing the bot's response. For this bot, the response is either the + * help text or the dialog to schedule a meeting. + * + * @param {object} event The event object from Google Chat + * @return {object} JSON-formatted response as text or Card message + */ +function onMessage(event) { + + // Handles regular onMessage logic. + // Evaluates if and handles for all slash commands. + if (event.message.slashCommand) { + switch (event.message.slashCommand.commandId) { + + case SLASHCOMMAND.DIALOG: // Displays meeting dialog for /schedule_Meeting. + + // TODO update this with your own logic to set meeting recipients, subjects, etc (e.g. a group email). + return getInputFormAsDialog_({ + invitee: '', + startTime: getTopOfHourDateString_(), + duration: 30, + subject: 'Status Stand-up', + body: 'Scheduling a quick status stand-up meeting.' + }); + + case SLASHCOMMAND.HELP: // Responds with help text for /help. + return getHelpTextResponse_(); + + /* TODO Add other use cases here. E.g: + case SLASHCOMMAND.NEW_FEATURE: // Your Feature Here + getDialogForAddContact(message); + */ + + } + } + else { + // Returns text if users didn't invoke a slash command. + return { text: 'No action taken - use Slash Commands.' } + } +} + +/** + * Responds to a CARD_CLICKED event triggered in Chat. + * @param {object} event the event object from Chat + * @return {object} JSON-formatted response + * @see https://developers.google.com/chat/api/guides/message-formats/events + */ +function onCardClick(event) { + if (event.action.actionMethodName === 'handleFormSubmit') { + const recipients = getFieldValue_(event.common.formInputs, 'email'); + const subject = getFieldValue_(event.common.formInputs, 'subject'); + const body = getFieldValue_(event.common.formInputs, 'body'); + + // Assumes dialog card inputs for date and times are in the correct format. mm/dd/yyy HH:MM + const dateTimeInput = getFieldValue_(event.common.formInputs, 'date'); + const startTime = getStartTimeAsDateObject_(dateTimeInput); + const duration = Number(getFieldValue_(event.common.formInputs, 'duration')); + + // Handles instances of missing or invalid input parameters. + const errors = []; + + if (!recipients) { + errors.push('Missing or invalid recipient email address.'); + } + if (!subject) { + errors.push('Missing subject line.'); + } + if (!body) { + errors.push('Missing event description.'); + } + if (!startTime) { + errors.push('Missing or invalid start time.'); + } + if (!duration || isNaN(duration)) { + errors.push('Missing or invalid duration'); + } + if (errors.length) { + // Redisplays the form if missing or invalid inputs exist. + return getInputFormAsDialog_({ + errors, + invitee: recipients, + startTime: dateTimeInput, + duration, + subject, + body + }); + } + + // Calculates the end time via duration. + const endTime = new Date(startTime.valueOf()); + endTime.setMinutes(endTime.getMinutes() + duration); + + // Creates calendar event with notification. + const calendar = CalendarApp.getDefaultCalendar() + const scheduledEvent = calendar.createEvent(subject, + startTime, + endTime, + { + guests: recipients, + sendInvites: true, + description: body + '\nThis meeting scheduled by a Google Chat App!' + }); + + // Gets a link to the Calendar event. + const url = getCalendarEventURL_(scheduledEvent, calendar) + + return getConfirmationDialog_(url); + + } else if (event.action.actionMethodName === 'closeDialog') { + + // Returns this dialog as success. + return { + actionResponse: { + type: 'DIALOG', + dialog_action: { + actionStatus: 'OK' + } + } + } + } +} + +/** + * Responds with help text about this chat bot. + * @return {string} The help text as seen below + */ +function getHelpTextResponse_() { + const help = `*${BOTNAME}* lets you quickly create meetings from Google Chat. Here\'s a list of all its commands: + \`/schedule_Meeting\` Opens a dialog with editable, preset parameters to create a meeting event + \`/help\` Displays this help message + + Learn more about creating Google Chat bots at https://developers.google.com/chat.` + + return { 'text': help } +} diff --git a/solutions/chat-bots/schedule-meetings/Dialog.js b/solutions/chat-bots/schedule-meetings/Dialog.js new file mode 100644 index 000000000..c7731683e --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/Dialog.js @@ -0,0 +1,210 @@ +/** + * 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. + */ + +/** +* Form input dialog as JSON. +* @return {object} JSON-formatted cards for the dialog. +*/ +function getInputFormAsDialog_(options) { + const form = getForm_(options); + return { + 'actionResponse': { + 'type': 'DIALOG', + 'dialogAction': { + 'dialog': { + 'body': form + } + } + } + }; +} + +/** +* Form JSON to collect inputs regarding the meeting. +* @return {object} JSON-formatted cards. +*/ +function getForm_(options) { + const sections = []; + + // If errors present, display additional section with validation messages. + if (options.errors && options.errors.length) { + let errors = options.errors.reduce((str, err) => `${str}• ${err}
    `, ''); + errors = `Errors:
    ${errors}`; + const errorSection = { + 'widgets': [ + { + textParagraph: { + text: errors + } + } + ] + } + sections.push(errorSection); + } + let formSection = { + 'header': 'Schedule meeting and send email to invited participants', + 'widgets': [ + { + 'textInput': { + 'label': 'Event Title', + 'type': 'SINGLE_LINE', + 'name': 'subject', + 'value': options.subject + } + }, + { + 'textInput': { + 'label': 'Invitee Email Address', + 'type': 'SINGLE_LINE', + 'name': 'email', + 'value': options.invitee, + 'hintText': 'Add team group email' + } + }, + { + 'textInput': { + 'label': 'Description', + 'type': 'MULTIPLE_LINE', + 'name': 'body', + 'value': options.body + } + }, + { + 'textInput': { + 'label': 'Meeting start date & time', + 'type': 'SINGLE_LINE', + 'name': 'date', + 'value': options.startTime, + 'hintText': 'mm/dd/yyyy H:MM' + } + }, + { + 'selectionInput': { + 'type': 'DROPDOWN', + 'label': 'Meeting Duration', + 'name': 'duration', + 'items': [ + { + 'text': '15 minutes', + 'value': '15', + 'selected': options.duration === 15 + }, + { + 'text': '30 minutes', + 'value': '30', + 'selected': options.duration === 30 + }, + { + 'text': '45 minutes', + 'value': '45', + 'selected': options.duration === 45 + }, + { + 'text': '1 Hour', + 'value': '60', + 'selected': options.duration === 60 + }, + { + 'text': '1.5 Hours', + 'value': '90', + 'selected': options.duration === 90 + }, + { + 'text': '2 Hours', + 'value': '120', + 'selected': options.duration === 120 + } + ] + } + } + ], + 'collapsible': false + }; + sections.push(formSection); + const card = { + 'sections': sections, + 'name': 'Google Chat Scheduled Meeting', + 'fixedFooter': { + 'primaryButton': { + 'text': 'Submit', + 'onClick': { + 'action': { + 'function': 'handleFormSubmit' + } + }, + 'altText': 'Submit' + } + } + }; + return card; +} + +/** +* Confirmation dialog after a calendar event is created successfully. +* @param {string} url The Google Calendar Event url for link button +* @return {object} JSON-formatted cards for the dialog +*/ +function getConfirmationDialog_(url) { + return { + 'actionResponse': { + 'type': 'DIALOG', + 'dialogAction': { + 'dialog': { + 'body': { + 'sections': [ + { + 'widgets': [ + { + 'textParagraph': { + 'text': 'Meeting created successfully!' + }, + 'horizontalAlignment': 'CENTER' + }, + { + 'buttonList': { + 'buttons': [ + { + 'text': 'Open Calendar Event', + 'onClick': { + 'openLink': { + 'url': url + } + } + } + + ] + }, + 'horizontalAlignment': 'CENTER' + } + ] + } + ], + 'fixedFooter': { + 'primaryButton': { + 'text': 'OK', + 'onClick': { + 'action': { + 'function': 'closeDialog' + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/solutions/chat-bots/schedule-meetings/README.md b/solutions/chat-bots/schedule-meetings/README.md new file mode 100644 index 000000000..000aed7af --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/README.md @@ -0,0 +1,4 @@ +# Schedule meetings from Google Chat + +See [developers.google.com](https://developers.google.com/apps-script/samples/chat-apps/schedule-meetings) for additional details. + diff --git a/solutions/chat-bots/schedule-meetings/Utilities.js b/solutions/chat-bots/schedule-meetings/Utilities.js new file mode 100644 index 000000000..dd08ea31b --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/Utilities.js @@ -0,0 +1,74 @@ +/** + * 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. + */ + +/** +* Helper function that gets the field value from the given form input. +* @return {string} +*/ +function getFieldValue_(formInputs, fieldName) { + return formInputs[fieldName][''].stringInputs.value[0]; +} + +// Regular expression to validate the date/time input. +const DATE_TIME_PATTERN = /\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d\d/; + +/** +* Casts date and time from string to Date object. +* @return {date} +*/ +function getStartTimeAsDateObject_(dateTimeStr) { + if (!dateTimeStr || !dateTimeStr.match(DATE_TIME_PATTERN)) { + return null; + } + + const parts = dateTimeStr.split(' '); + const [month, day, year] = parts[0].split('/').map(Number); + const [hour, minute] = parts[1].split(':').map(Number); + + + Session.getScriptTimeZone() + + return new Date(year, month - 1, day, hour, minute) +} + +/** +* Gets the current date and time for the upcoming top of the hour (e.g. 01/25/2022 18:00). +* @return {string} date/time in mm/dd/yyy HH:MM format needed for use by Calendar +*/ +function getTopOfHourDateString_() { + const date = new Date(); + date.setHours(date.getHours() + 1); + date.setMinutes(0, 0, 0); + // Adding the date as string might lead to an incorrect response due to time zone adjustments. + return Utilities.formatDate(date, Session.getScriptTimeZone(), 'MM/dd/yyyy H:mm'); +} + + +/** +* Creates the URL for the Google Calendar event. +* +* @param {object} event The Google Calendar Event instance +* @param {object} cal The associated Google Calendar +* @return {string} URL in the form of 'https://www.google.com/calendar/event?eid={event-id}' +*/ +function getCalendarEventURL_(event, cal) { + const baseCalUrl = 'https://www.google.com/calendar'; + // Joins Calendar Event Id with Calendar Id, then base64 encode to derive the event URL. + let encodedId = Utilities.base64Encode(event.getId().split('@')[0] + " " + cal.getId()).replace(/\=/g, ''); + encodedId = `/event?eid=${encodedId}`; + return (baseCalUrl + encodedId); + +} \ No newline at end of file diff --git a/solutions/chat-bots/schedule-meetings/appsscript.json b/solutions/chat-bots/schedule-meetings/appsscript.json new file mode 100644 index 000000000..40869f567 --- /dev/null +++ b/solutions/chat-bots/schedule-meetings/appsscript.json @@ -0,0 +1,8 @@ +{ + "timeZone": "America/Los_Angeles", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "chat": { + "addToSpaceFallbackMessage": "Thank you for adding this Chat Bot!" + } +} \ No newline at end of file diff --git a/solutions/custom-functions/calculate-driving-distance/.clasp.json b/solutions/custom-functions/calculate-driving-distance/.clasp.json new file mode 100644 index 000000000..6fb6a8c9b --- /dev/null +++ b/solutions/custom-functions/calculate-driving-distance/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1_cfhZv-VJBekzu1V4mFD1C5ggRaUumWw9rUz0NaLED6XD4_yHB-eJ01a"} diff --git a/solutions/custom-functions/calculate-driving-distance/Code.js b/solutions/custom-functions/calculate-driving-distance/Code.js new file mode 100644 index 000000000..aaf9ceebd --- /dev/null +++ b/solutions/custom-functions/calculate-driving-distance/Code.js @@ -0,0 +1,223 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/custom-functions/calculate-driving-distance + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @OnlyCurrentDoc Limits the script to only accessing the current sheet. + */ + +/** + * A special function that runs when the spreadsheet is open, used to add a + * custom menu to the spreadsheet. + */ +function onOpen() { + try { + const spreadsheet = SpreadsheetApp.getActive(); + const menuItems = [ + {name: 'Prepare sheet...', functionName: 'prepareSheet_'}, + {name: 'Generate step-by-step...', functionName: 'generateStepByStep_'} + ]; + spreadsheet.addMenu('Directions', menuItems); + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); + } +} + +/** + * A custom function that converts meters to miles. + * + * @param {Number} meters The distance in meters. + * @return {Number} The distance in miles. + */ +function metersToMiles(meters) { + if (typeof meters !== 'number') { + return null; + } + return meters / 1000 * 0.621371; +} + +/** + * A custom function that gets the driving distance between two addresses. + * + * @param {String} origin The starting address. + * @param {String} destination The ending address. + * @return {Number} The distance in meters. + */ +function drivingDistance(origin, destination) { + const directions = getDirections_(origin, destination); + return directions.routes[0].legs[0].distance.value; +} + +/** + * A function that adds headers and some initial data to the spreadsheet. + */ +function prepareSheet_() { + try { + const sheet = SpreadsheetApp.getActiveSheet().setName('Settings'); + const headers = [ + 'Start Address', + 'End Address', + 'Driving Distance (meters)', + 'Driving Distance (miles)']; + const initialData = [ + '350 5th Ave, New York, NY 10118', + '405 Lexington Ave, New York, NY 10174']; + sheet.getRange('A1:D1').setValues([headers]).setFontWeight('bold'); + sheet.getRange('A2:B2').setValues([initialData]); + sheet.setFrozenRows(1); + sheet.autoResizeColumns(1, 4); + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); + } +} + +/** + * Creates a new sheet containing step-by-step directions between the two + * addresses on the "Settings" sheet that the user selected. + */ +function generateStepByStep_() { + try { + const spreadsheet = SpreadsheetApp.getActive(); + const settingsSheet = spreadsheet.getSheetByName('Settings'); + settingsSheet.activate(); + + // Prompt the user for a row number. + const selectedRow = Browser + .inputBox('Generate step-by-step', 'Please enter the row number of' + + ' the' + ' addresses to use' + ' (for example, "2"):', + Browser.Buttons.OK_CANCEL); + if (selectedRow === 'cancel') { + return; + } + const rowNumber = Number(selectedRow); + if (isNaN(rowNumber) || rowNumber < 2 || + rowNumber > settingsSheet.getLastRow()) { + Browser.msgBox('Error', + Utilities.formatString('Row "%s" is not valid.', selectedRow), + Browser.Buttons.OK); + return; + } + + + // Retrieve the addresses in that row. + const row = settingsSheet.getRange(rowNumber, 1, 1, 2); + const rowValues = row.getValues(); + const origin = rowValues[0][0]; + const destination = rowValues[0][1]; + if (!origin || !destination) { + Browser.msgBox('Error', 'Row does not contain two addresses.', + Browser.Buttons.OK); + return; + } + + // Get the raw directions information. + const directions = getDirections_(origin, destination); + + // Create a new sheet and append the steps in the directions. + const sheetName = 'Driving Directions for Row ' + rowNumber; + let directionsSheet = spreadsheet.getSheetByName(sheetName); + if (directionsSheet) { + directionsSheet.clear(); + directionsSheet.activate(); + } else { + directionsSheet = + spreadsheet.insertSheet(sheetName, spreadsheet.getNumSheets()); + } + const sheetTitle = Utilities + .formatString('Driving Directions from %s to %s', origin, destination); + const headers = [ + [sheetTitle, '', ''], + ['Step', 'Distance (Meters)', 'Distance (Miles)'] + ]; + const newRows = []; + for (const step of directions.routes[0].legs[0].steps) { + // Remove HTML tags from the instructions. + const instructions = step.html_instructions + .replace(/
    |/g, '\n').replace(/<.*?>/g, ''); + newRows.push([ + instructions, + step.distance.value + ]); + } + directionsSheet.getRange(1, 1, headers.length, 3).setValues(headers); + directionsSheet.getRange(headers.length + 1, 1, newRows.length, 2) + .setValues(newRows); + directionsSheet.getRange(headers.length + 1, 3, newRows.length, 1) + .setFormulaR1C1('=METERSTOMILES(R[0]C[-1])'); + + // Format the new sheet. + directionsSheet.getRange('A1:C1').merge().setBackground('#ddddee'); + directionsSheet.getRange('A1:2').setFontWeight('bold'); + directionsSheet.setColumnWidth(1, 500); + directionsSheet.getRange('B2:C').setVerticalAlignment('top'); + directionsSheet.getRange('C2:C').setNumberFormat('0.00'); + const stepsRange = directionsSheet.getDataRange() + .offset(2, 0, directionsSheet.getLastRow() - 2); + setAlternatingRowBackgroundColors_(stepsRange, '#ffffff', '#eeeeee'); + directionsSheet.setFrozenRows(2); + SpreadsheetApp.flush(); + } catch (e) { + // TODO (Developer) - Handle Exception + console.log('Failed with error: %s' + e.error); + } +} + +/** + * Sets the background colors for alternating rows within the range. + * @param {Range} range The range to change the background colors of. + * @param {string} oddColor The color to apply to odd rows (relative to the + * start of the range). + * @param {string} evenColor The color to apply to even rows (relative to the + * start of the range). + */ +function setAlternatingRowBackgroundColors_(range, oddColor, evenColor) { + const backgrounds = []; + for (let row = 1; row <= range.getNumRows(); row++) { + const rowBackgrounds = []; + for (let column = 1; column <= range.getNumColumns(); column++) { + if (row % 2 === 0) { + rowBackgrounds.push(evenColor); + } else { + rowBackgrounds.push(oddColor); + } + } + backgrounds.push(rowBackgrounds); + } + range.setBackgrounds(backgrounds); +} + +/** + * A shared helper function used to obtain the full set of directions + * information between two addresses. Uses the Apps Script Maps Service. + * + * @param {String} origin The starting address. + * @param {String} destination The ending address. + * @return {Object} The directions response object. + */ +function getDirections_(origin, destination) { + const directionFinder = Maps.newDirectionFinder(); + directionFinder.setOrigin(origin); + directionFinder.setDestination(destination); + const directions = directionFinder.getDirections(); + if (directions.status !== 'OK') { + throw directions.error_message; + } + return directions; +} diff --git a/solutions/custom-functions/calculate-driving-distance/README.md b/solutions/custom-functions/calculate-driving-distance/README.md new file mode 100644 index 000000000..3648623a0 --- /dev/null +++ b/solutions/custom-functions/calculate-driving-distance/README.md @@ -0,0 +1,4 @@ +# Calculate driving distance & convert meters to miles + +See [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/calculate-driving-distance) for additional details. + diff --git a/solutions/custom-functions/calculate-driving-distance/appsscript.json b/solutions/custom-functions/calculate-driving-distance/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/custom-functions/calculate-driving-distance/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/custom-functions/summarize-sheets-data/.clasp.json b/solutions/custom-functions/summarize-sheets-data/.clasp.json new file mode 100644 index 000000000..a53755e8a --- /dev/null +++ b/solutions/custom-functions/summarize-sheets-data/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1NN-ROSZO3ZsfiVUlCdmNqggpCQuGNtgO_r0nehV0s5mkZJN2bcMTri-7"} diff --git a/solutions/custom-functions/summarize-sheets-data/Code.js b/solutions/custom-functions/summarize-sheets-data/Code.js new file mode 100644 index 000000000..70ac671bc --- /dev/null +++ b/solutions/custom-functions/summarize-sheets-data/Code.js @@ -0,0 +1,83 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/custom-functions/summarize-sheets-data + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Gets summary data from other sheets. The sheets you want to summarize must have columns with headers that match the names of the columns this function summarizes data from. + * + * @return {string} Summary data from other sheets. + * @customfunction + */ + +// The following sheets are ignored. Add additional constants for other sheets that should be ignored. +const READ_ME_SHEET_NAME = "ReadMe"; +const PM_SHEET_NAME = "Summary"; + +/** + * Reads data ranges for each sheet. Filters and counts based on 'Status' columns. To improve performance, the script uses arrays + * until all summary data is gathered. Then the script writes the summary array starting at the cell of the custom function. + */ +function getSheetsData() { + let ss = SpreadsheetApp.getActiveSpreadsheet(); + let sheets = ss.getSheets(); + let outputArr = []; + + // For each sheet, summarizes the data and pushes to a temporary array. + for (let s in sheets) { + // Gets sheet name. + let sheetNm = sheets[s].getName(); + // Skips ReadMe and Summary sheets. + if (sheetNm === READ_ME_SHEET_NAME || sheetNm === PM_SHEET_NAME) { continue; } + // Gets sheets data. + let values = sheets[s].getDataRange().getValues(); + // Gets the first row of the sheet which is the header row. + let headerRowValues = values[0]; + // Finds the columns with the heading names 'Owner Name' and 'Status' and gets the index value of each. + // Using 'indexOf()' to get the position of each column prevents the script from breaking if the columns change positions in a sheet. + let columnOwner = headerRowValues.indexOf("Owner Name"); + let columnStatus = headerRowValues.indexOf("Status"); + // Removes header row. + values.splice(0,1); + // Gets the 'Owner Name' column value by retrieving the first data row in the array. + let owner = values[0][columnOwner]; + // Counts the total number of tasks. + let taskCnt = values.length; + // Counts the number of tasks that have the 'Complete' status. + // If the options you want to count in your spreadsheet differ, update the strings below to match the text of each option. + // To add more options, copy the line below and update the string to the new text. + let completeCnt = filterByPosition(values,'Complete', columnStatus).length; + // Counts the number of tasks that have the 'In-Progress' status. + let inProgressCnt = filterByPosition(values,'In-Progress', columnStatus).length; + // Counts the number of tasks that have the 'Scheduled' status. + let scheduledCnt = filterByPosition(values,'Scheduled', columnStatus).length; + // Counts the number of tasks that have the 'Overdue' status. + let overdueCnt = filterByPosition(values,'Overdue', columnStatus).length; + // Builds the output array. + outputArr.push([owner,taskCnt,completeCnt,inProgressCnt,scheduledCnt,overdueCnt,sheetNm]); + } + // Writes the output array. + return outputArr; +} + +/** + * Below is a helper function that filters a 2-dimenstional array. + */ +function filterByPosition(array, find, position) { + return array.filter(innerArray => innerArray[position] === find); +} + diff --git a/solutions/custom-functions/summarize-sheets-data/README.md b/solutions/custom-functions/summarize-sheets-data/README.md new file mode 100644 index 000000000..4ae6b97ac --- /dev/null +++ b/solutions/custom-functions/summarize-sheets-data/README.md @@ -0,0 +1,4 @@ +# Summarize data from multiple sheets + +See [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/summarize-sheets-data) for additional details. + diff --git a/solutions/custom-functions/summarize-sheets-data/appsscript.json b/solutions/custom-functions/summarize-sheets-data/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/custom-functions/summarize-sheets-data/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/custom-functions/tier-pricing/.clasp.json b/solutions/custom-functions/tier-pricing/.clasp.json new file mode 100644 index 000000000..c8264b479 --- /dev/null +++ b/solutions/custom-functions/tier-pricing/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "1-ql7ECe91XZgWu-hW_UZBx8mhuTtQQj0yNITYh8yQCOuHxLEjxtTngGB"} diff --git a/solutions/custom-functions/tier-pricing/Code.js b/solutions/custom-functions/tier-pricing/Code.js new file mode 100644 index 000000000..9fca30cf0 --- /dev/null +++ b/solutions/custom-functions/tier-pricing/Code.js @@ -0,0 +1,51 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/samples/custom-functions/tier-pricing + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Calculates the tiered pricing discount. + * + * You must provide a value to calculate its discount. The value can be a string or a reference + * to a cell that contains a string. + * You must provide a data table range, for example, $B$4:$D$7, that includes the + * tier start, end, and percent columns. If your table has headers, don't include + * the headers in the range. + * + * @param {string} value The value to calculate the discount for, which can be a string or a + * reference to a cell that contains a string. + * @param {string} table The tier table data range using A1 notation. + * @return number The total discount amount for the value. + * @customfunction + * + */ +function tierPrice(value, table) { + let total = 0; + // Creates an array for each row of the table and loops through each array. + for (let [start, end, percent] of table) { + // Checks if the value is less than the starting value of the tier. If it is less, the loop stops. + if (value < start) { + break; + } + // Calculates the portion of the value to be multiplied by the tier's percent value. + let amount = Math.min(value, end) - start; + // Multiplies the amount by the tier's percent value and adds the product to the total. + total += amount * percent; + } + return total; +} + \ No newline at end of file diff --git a/solutions/custom-functions/tier-pricing/README.md b/solutions/custom-functions/tier-pricing/README.md new file mode 100644 index 000000000..edbadc063 --- /dev/null +++ b/solutions/custom-functions/tier-pricing/README.md @@ -0,0 +1,4 @@ +# Calculate a tiered pricing discount + +See [developers.google.com](https://developers.google.com/apps-script/samples/custom-functions/tier-pricing) for additional details. + diff --git a/solutions/custom-functions/tier-pricing/appsscript.json b/solutions/custom-functions/tier-pricing/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/custom-functions/tier-pricing/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/editor-add-on/clean-sheet/.clasp.json b/solutions/editor-add-on/clean-sheet/.clasp.json new file mode 100644 index 000000000..2cae3aa10 --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/.clasp.json @@ -0,0 +1 @@ +{"scriptId": "10bxhn6eGypm20dgRcTbUCbzP4Bz0dyYR6IZTNEA2gIXXxwoy8Zqs06yr"} diff --git a/solutions/editor-add-on/clean-sheet/Code.js b/solutions/editor-add-on/clean-sheet/Code.js new file mode 100644 index 000000000..deb3136c3 --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/Code.js @@ -0,0 +1,246 @@ +// To learn how to use this script, refer to the documentation: +// https://developers.google.com/apps-script/add-ons/clean-sheet + +/* +Copyright 2022 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Application Constants +const APP_TITLE = 'Clean sheet'; + +/** + * Identifies and deletes empty rows in selected range of active sheet. + * + * Cells that contain space characters are treated as non-empty. + * The entire row, including the cells outside of the selected range, + * must be empty to be deleted. + * + * Called from menu option. + */ +function deleteEmptyRows() { + + const sheet = SpreadsheetApp.getActiveSheet(); + + // Gets active selection and dimensions. + let activeRange = sheet.getActiveRange(); + const rowCount = activeRange.getHeight(); + const firstActiveRow = activeRange.getRow(); + const columnCount = sheet.getMaxColumns(); + + // Tests that the selection is a valid range. + if (rowCount < 1) { + showMessage('Select a valid range.'); + return; + } + // Tests active range isn't too large to process. Enforces limit set to 10k. + if (rowCount > 10000) { + showMessage("Selected range too large. Select up to 10,000 rows at one time."); + return; + } + + // Utilizes an array of values for efficient processing to determine blank rows. + const activeRangeValues = sheet.getRange(firstActiveRow, 1, rowCount, columnCount).getValues(); + + // Checks if array is all empty values. + const valueFilter = value => value !== ''; + const isRowEmpty = (row) => { + return row.filter(valueFilter).length === 0; + } + + // Maps the range values as an object with value (to test) and corresponding row index (with offset from selection). + const rowsToDelete = activeRangeValues.map((row, index) => ({ row, offset: index + activeRange.getRowIndex() })) + .filter(item => isRowEmpty(item.row)) // Test to filter out non-empty rows. + .map(item => item.offset); //Remap to include just the row indexes that will be removed. + + // Combines a sorted, ascending list of indexes into a set of ranges capturing consecutive values as start/end ranges. + // Combines sequential empty rows for faster processing. + const rangesToDelete = rowsToDelete.reduce((ranges, index) => { + const currentRange = ranges[ranges.length - 1]; + if (currentRange && index === currentRange[1] + 1) { + currentRange[1] = index; + return ranges; + } + ranges.push([index, index]); + return ranges; + }, []); + + // Sends a list of row indexes to be deleted to the console. + console.log(rangesToDelete); + + // Deletes the rows using REVERSE order to ensure proper indexing is used. + rangesToDelete.reverse().forEach(([start, end]) => sheet.deleteRows(start, end - start + 1)); + SpreadsheetApp.flush(); +} + +/** + * Removes blank columns in a selected range. + * + * Cells containing Space characters are treated as non-empty. + * The entire column, including cells outside of the selected range, + * must be empty to be deleted. + * + * Called from menu option. + */ +function deleteEmptyColumns() { + + const sheet = SpreadsheetApp.getActiveSheet(); + + // Gets active selection and dimensions. + let activeRange = sheet.getActiveRange(); + const rowCountMax = sheet.getMaxRows(); + const columnWidth = activeRange.getWidth(); + const firstActiveColumn = activeRange.getColumn(); + + // Tests that the selection is a valid range. + if (columnWidth < 1) { + showMessage('Select a valid range.'); + return; + } + // Tests active range is not too large to process. Enforces limit set to 1k. + if (columnWidth > 1000) { + showMessage("Selected range too large. Select up to 10,000 rows at one time."); + return; + } + + // Utilizes an array of values for efficient processing to determine blank columns. + const activeRangeValues = sheet.getRange(1, firstActiveColumn, rowCountMax, columnWidth).getValues(); + + // Transposes the array of range values so it can be processed in order of columns. + const activeRangeValuesTransposed = activeRangeValues[0].map((_, colIndex) => activeRangeValues.map(row => row[colIndex])); + + // Checks if array is all empty values. + const valueFilter = value => value !== ''; + const isColumnEmpty = (column) => { + return column.filter(valueFilter).length === 0; + } + + // Maps the range values as an object with value (to test) and corresponding column index (with offset from selection). + const columnsToDelete = activeRangeValuesTransposed.map((column, index) => ({ column, offset: index + firstActiveColumn})) + .filter(item => isColumnEmpty(item.column)) // Test to filter out non-empty rows. + .map(item => item.offset); //Remap to include just the column indexes that will be removed. + + // Combines a sorted, ascending list of indexes into a set of ranges capturing consecutive values as start/end ranges. + // Combines sequential empty columns for faster processing. + const rangesToDelete = columnsToDelete.reduce((ranges, index) => { + const currentRange = ranges[ranges.length - 1]; + if (currentRange && index === currentRange[1] + 1) { + currentRange[1] = index; + return ranges; + } + ranges.push([index, index]); + return ranges; + }, []); + + // Sends a list of column indexes to be deleted to the console. + console.log(rangesToDelete); + + // Deletes the columns using REVERSE order to ensure proper indexing is used. + rangesToDelete.reverse().forEach(([start, end]) => sheet.deleteColumns(start, end - start + 1)); + SpreadsheetApp.flush(); +} + +/** + * Trims all of the unused rows and columns outside of selected data range. + * + * Called from menu option. + */ +function cropSheet() { + const dataRange = SpreadsheetApp.getActiveSheet().getDataRange(); + const sheet = dataRange.getSheet(); + + let numRows = dataRange.getNumRows(); + let numColumns = dataRange.getNumColumns(); + + const maxRows = sheet.getMaxRows(); + const maxColumns = sheet.getMaxColumns(); + + const numFrozenRows = sheet.getFrozenRows(); + const numFrozenColumns = sheet.getFrozenColumns(); + + // If last data row is less than maximium row, then deletes rows after the last data row. + if (numRows < maxRows) { + numRows = Math.max(numRows, numFrozenRows + 1); // Don't crop empty frozen rows. + sheet.deleteRows(numRows + 1, maxRows - numRows); + } + + // If last data column is less than maximium column, then deletes columns after the last data column. + if (numColumns < maxColumns) { + numColumns = Math.max(numColumns, numFrozenColumns + 1); // Don't crop empty frozen columns. + sheet.deleteColumns(numColumns + 1, maxColumns - numColumns); + } +} + +/** + * Copies value of active cell to the blank cells beneath it. + * Stops at last row of the sheet's data range if only blank cells are encountered. + * + * Called from menu option. + */ +function fillDownData() { + + const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); + + // Gets sheet's active cell and confirms it's not empty. + const activeCell = sheet.getActiveCell(); + const activeCellValue = activeCell.getValue(); + + if (!activeCellValue) { + showMessage("The active cell is empty. Nothing to fill."); + return; + } + + // Gets coordinates of active cell. + const column = activeCell.getColumn(); + const row = activeCell.getRow(); + + // Gets entire data range of the sheet. + const dataRange = sheet.getDataRange(); + const dataRangeRows = dataRange.getNumRows(); + + // Gets trimmed range starting from active cell to the end of sheet data range. + const searchRange = dataRange.offset(row - 1, column - 1, dataRangeRows - row + 1, 1) + const searchValues = searchRange.getDisplayValues(); + + // Find the number of empty rows below the active cell. + let i = 1; // Start at 1 to skip the ActiveCell. + while (searchValues[i] && searchValues[i][0] == "") { i++; } + + // If blanks exist, fill the range with values. + if (i > 1) { + const fillRange = searchRange.offset(0, 0, i, 1).setValue(activeCellValue) + //sheet.setActiveRange(fillRange) // Uncomment to test affected range. + } + else { + showMessage("There are no empty cells below the Active Cell to fill."); + } +} + +/** + * A helper function to display messages to user. + * + * @param {string} message - Message to be displayed. + * @param {string} caller - {Optional} text to append to title. + */ +function showMessage(message, caller) { + + // Sets the title using the APP_TITLE variable; adds optional caller string. + const title = APP_TITLE + if (caller != null) { + title += ` : ${caller}` + }; + + const ui = SpreadsheetApp.getUi(); + ui.alert(title, message, ui.ButtonSet.OK); +} diff --git a/solutions/editor-add-on/clean-sheet/Menu.js b/solutions/editor-add-on/clean-sheet/Menu.js new file mode 100644 index 000000000..fb6feca40 --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/Menu.js @@ -0,0 +1,60 @@ +/** + * 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. + */ + +/** + * Creates a menu entry in the Google Sheets Extensions menu when the document is opened. + * + * @param {object} e The event parameter for a simple onOpen trigger. + */ +function onOpen(e) { + // Builds a menu that displays under the Extensions menu in Sheets. + let menu = SpreadsheetApp.getUi().createAddonMenu() + + menu + .addItem('Delete blank rows (from selected rows only)', 'deleteEmptyRows') + .addItem('Delete blank columns (from selected columns only)', 'deleteEmptyColumns') + .addItem('Crop sheet to data range', 'cropSheet') + .addSeparator() + .addItem('Fill in blank rows below', 'fillDownData') + .addSeparator() + .addItem('About', 'aboutApp') + .addToUi(); +} + +/** + * Runs when the add-on is installed; calls onOpen() to ensure menu creation and + * any other initializion work is done immediately. This method is only used by + * the desktop add-on and is never called by the mobile version. + * + * @param {object} e The event parameter for a simple onInstall trigger. + */ +function onInstall(e) { + onOpen(e); +} + +/** + * About box for context and developer contact information. + * TODO: Personalize + */ +function aboutApp() { + const msg = ` + Name: ${APP_TITLE} + Version: 1.0 + Contact: ` + + const ui = SpreadsheetApp.getUi(); + ui.alert("About this application", msg, ui.ButtonSet.OK); +} \ No newline at end of file diff --git a/solutions/editor-add-on/clean-sheet/README.md b/solutions/editor-add-on/clean-sheet/README.md new file mode 100644 index 000000000..11a3932ca --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/README.md @@ -0,0 +1,4 @@ +# Clean up data in a spreadsheet + +See [developers.google.com](https://developers.google.com/apps-script/add-ons/clean-sheet) for additional details. + diff --git a/solutions/editor-add-on/clean-sheet/appsscript.json b/solutions/editor-add-on/clean-sheet/appsscript.json new file mode 100644 index 000000000..3cf1d247d --- /dev/null +++ b/solutions/editor-add-on/clean-sheet/appsscript.json @@ -0,0 +1,7 @@ +{ + "timeZone": "America/New_York", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8" +} \ No newline at end of file diff --git a/solutions/ooo-chat-app/Code.js b/solutions/ooo-chat-app/Code.js new file mode 100644 index 000000000..1ff83e3a0 --- /dev/null +++ b/solutions/ooo-chat-app/Code.js @@ -0,0 +1,210 @@ +/** + * Responds to an ADDED_TO_SPACE event in Chat. + * @param {object} event the event object from Chat + * @return {object} JSON-formatted response + * @see https://developers.google.com/hangouts/chat/reference/message-formats/events + */ + function onAddToSpace(event) { + let message = 'Thank you for adding me to '; + if (event.space.type === 'DM') { + message += 'a DM, ' + event.user.displayName + '!'; + } else { + message += event.space.displayName; + } + return { text: message }; + } + + /** + * Responds to a REMOVED_FROM_SPACE event in Chat. + * @param {object} event the event object from Chat + * @param {object} event the event object from Chat + * @see https://developers.google.com/hangouts/chat/reference/message-formats/events + */ + function onRemoveFromSpace(event) { + console.log('App removed from ', event.space.name); + } + + + /** + * Responds to a MESSAGE event triggered in Chat. + * @param {object} event the event object from Chat + * @return {function} call the respective function + */ + function onMessage(event) { + const message = event.message; + + if (message.slashCommand) { + switch (message.slashCommand.commandId) { + case 1: // Help command + return createHelpCard(); + case 2: // Block out day command + return blockDayOut(); + case 3: // Cancel all meetings command + return cancelAllMeetings(); + case 4: // Set auto reply command + return setAutoReply(); + } + } + } + + function createHelpCard() { + return { + "cardsV2": [ + { + "cardId": "2", + "card": { + "sections": [ + { + "header": "", + "widgets": [ + { + "decoratedText": { + "topLabel": "", + "text": "Hi! 👋 I'm here to help you with your out of office tasks.

    Here's a list of commands I understand.", + "wrapText": true + } + } + ] + }, + { + "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" + } + } + } + ] + } + } + + /** + * Adds an all day event to the users Google Calendar. + * @return {object} JSON-formatted response + */ + function blockDayOut() { + blockOutCalendar(); + return createResponseCard('Your calendar has been blocked out for you.') + } + + /** + * Cancels all of the users meeting for the current day. + * @return {object} JSON-formatted response + */ + function cancelAllMeetings() { + cancelMeetings(); + return createResponseCard('All your meetings have been canceled.') + } + + /** + * Sets an out of office auto reply in the users Gmail account. + * @return {object} JSON-formatted response + */ + function setAutoReply() { + turnOnAutoResponder(); + return createResponseCard('The out of office auto reply has been turned on.') + } + + + const ONE_DAY_MILLIS = 24 * 60 * 60 * 1000; + + /** + * Places an all-day meeting on the user's Calendar. + */ + function blockOutCalendar() { + CalendarApp.createAllDayEvent('I am out of office today', new Date(), new Date(Date.now() + ONE_DAY_MILLIS)); + } + + /** + * Declines all meetings for the day. + */ + + function cancelMeetings() { + const events = CalendarApp.getEventsForDay(new Date()); + + events.forEach(function(event) { + if (event.getGuestList().length > 0) { + event.setMyStatus(CalendarApp.GuestStatus.NO); + } + }); + } + + /** + * Turns on the user's vacation response for today in Gmail. + */ + function turnOnAutoResponder() { + const currentTime = (new Date()).getTime(); + Gmail.Users.Settings.updateVacation({ + enableAutoReply: true, + responseSubject: 'I am out of the office today', + responseBodyHtml: 'I am out of the office today; will be back on the next business day.

    Created by OOO Chat app!', + restrictToContacts: true, + restrictToDomain: true, + startTime: currentTime, + endTime: currentTime + ONE_DAY_MILLIS + }, 'me'); + } + + function 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" + } + } + } + ] + } + } + + \ No newline at end of file diff --git a/solutions/ooo-chat-app/README.md b/solutions/ooo-chat-app/README.md new file mode 100644 index 000000000..2982befa1 --- /dev/null +++ b/solutions/ooo-chat-app/README.md @@ -0,0 +1,3 @@ +Sample code for a custom Google Chat app that manages your out of office tasks. + +Learn more about Chat apps: https://developers.google.com/chat \ No newline at end of file diff --git a/solutions/ooo-chat-app/appsscript.json b/solutions/ooo-chat-app/appsscript.json new file mode 100644 index 000000000..ba4ac17c5 --- /dev/null +++ b/solutions/ooo-chat-app/appsscript.json @@ -0,0 +1,15 @@ +{ + "timeZone": "Europe/Madrid", + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "dependencies": { + "enabledAdvancedServices": [ + { + "userSymbol": "Gmail", + "version": "v1", + "serviceId": "gmail" + } + ] + }, + "chat": {} + } \ No newline at end of file diff --git a/tasks/quickstart/quickstart.gs b/tasks/quickstart/quickstart.gs index 9434e0b22..fae31d265 100644 --- a/tasks/quickstart/quickstart.gs +++ b/tasks/quickstart/quickstart.gs @@ -16,21 +16,27 @@ // [START tasks_quickstart] /** * Lists the user's tasks. + * @see https://developers.google.com/tasks/reference/rest/v1/tasklists/list */ function listTaskLists() { - var optionalArgs = { + const optionalArgs = { maxResults: 10 }; - var response = Tasks.Tasklists.list(optionalArgs); - var taskLists = response.items; - if (taskLists && taskLists.length > 0) { - Logger.log('Task lists:'); - for (var i = 0; i < taskLists.length; i++) { - var taskList = taskLists[i]; - Logger.log('%s (%s)', taskList.title, taskList.id); + try { + // Returns all the authenticated user's task lists. + const response = Tasks.Tasklists.list(optionalArgs); + const taskLists = response.items; + // Print task list of user if available. + if (!taskLists || taskLists.length === 0) { + console.log('No task lists found.'); + return; } - } else { - Logger.log('No task lists found.'); + for (const taskList of taskLists) { + console.log('%s (%s)', taskList.title, taskList.id); + } + } catch (err) { + // TODO (developer) - Handle exception from Task API + console.log('Failed with error %s', err.message); } } // [END tasks_quickstart] diff --git a/tasks/simpleTasks/README.md b/tasks/simpleTasks/README.md index 16c1ed4ab..2e6cce063 100644 --- a/tasks/simpleTasks/README.md +++ b/tasks/simpleTasks/README.md @@ -3,7 +3,7 @@ Simple Tasks is a sample web app built using Apps Script that provides limited read and write access to your data in [Google Tasks](https://mail.google.com/tasks/canvas). It was created using the -[Html Service](https://developers.google.com/apps-script/guides/html-service) +[HTML Service](https://developers.google.com/apps-script/guides/html-service) and demonstrates some common patterns and best practices to use when developing user interfaces. @@ -30,8 +30,8 @@ scratch, follow the steps below. and add in each of the files in this directory. You should already have a file named Code.gs in your project, and you can replace its contents with the new code. For the remaining files, ensure you select - **File > New > Html file** when creating the files, and when entering the - file name omit the .html suffix as it will be added automatically. + **File > New > HTML file** when creating the files, and when entering the + filename omit the `.html` suffix as it will be added automatically. 2. Enabled the Google Tasks API on the script ([instructions available here](https://developers.google.com/apps-script/built_in_services#advanced_google_services)). diff --git a/tasks/simpleTasks/javascript.html b/tasks/simpleTasks/javascript.html index 7f057bacc..5331918ce 100644 --- a/tasks/simpleTasks/javascript.html +++ b/tasks/simpleTasks/javascript.html @@ -1,3 +1,19 @@ + + diff --git a/tasks/simpleTasks/page.html b/tasks/simpleTasks/page.html index 35ae82369..f5f9f677a 100644 --- a/tasks/simpleTasks/page.html +++ b/tasks/simpleTasks/page.html @@ -1,4 +1,20 @@ + + @@ -30,7 +46,7 @@

    Simple Tasks

    - + diff --git a/tasks/simpleTasks/stylesheet.html b/tasks/simpleTasks/stylesheet.html index 76f0567ab..02c35c0d0 100644 --- a/tasks/simpleTasks/stylesheet.html +++ b/tasks/simpleTasks/stylesheet.html @@ -1,3 +1,19 @@ + + diff --git a/templates/docs-addon/README.md b/templates/docs-addon/README.md index c918cb16e..98e9a6f3d 100644 --- a/templates/docs-addon/README.md +++ b/templates/docs-addon/README.md @@ -19,5 +19,5 @@ and manipulate the (sometimes complex) Finally, note that this template must be added to a container-bound script attached to a Google Doc in order to function. Developed add-ons must go through a -[publishing process](https://developers.google.com/apps-script/add-ons/publish) +[publishing process](https://developers.google.com/apps-script/add-ons/publish) before they can be made available publicly. diff --git a/templates/sheets-import/README.md b/templates/sheets-import/README.md index 7795340fc..6bbb6b92a 100644 --- a/templates/sheets-import/README.md +++ b/templates/sheets-import/README.md @@ -1,11 +1,10 @@ -Template: Importing Data to Sheets -================================== +# Template: Importing Data to Sheets This template provides a framework for creating a Sheets [add-on](https://developers.google.com/apps-script/add-ons/) -that imports data from a 3rd-party source (such as an API). +that imports data from a third-party source (such as an API). It shows the basic structure needed to define a UI and how to coordinate -communication between the client, server, and 3rd-party source. +communication between the client, server, and third-party source. This template also demonstrates some useful aspects of Apps Script, including: * Writing data to a Google Sheet @@ -16,44 +15,45 @@ This template also demonstrates some useful aspects of Apps Script, including: **Note**: The purpose of this template is to show a general add-on structure. It will not run as an add-on in it's current state. To make use of this template, you will need to fill in the sections marked **TODO** to customize -the template to a specific 3rd-party data source. +the template to a specific third-party data source. ## Project manifest The following project files are included in this template: -* **APICode.gs** - This file contains all the API-specific code for handling +* `**APICode.gs**` - This file contains all the API-specific code for handling authorization, callbacks, and API calls. It will need to be modified to handle a specific API. -* **Auth.gs** - This file contains code that assists with constructing a +* `**Auth.gs**` - This file contains code that assists with constructing a OAuth2 service object using the [Apps Script OAuth2 library](https://github.com/googlesamples/apps-script-oauth2). -* **AuthCallbackView.html** - This file is the page that is presented to the +* `**AuthCallbackView.html**` - This file is the page that is presented to the user after an authorization attempt, and shows whether the authorization was successful. -* **AuthorizationEmail.html** - This file contains the html template of an email +* `**AuthorizationEmail.html**` - This file contains the HTML template of an email that would be sent to the user in the event that a trigger attempts to fire without all the required authorizations. -* **Configurations.gs** - This file contains code that controls the creation, +* `**Configurations.gs**` - This file contains code that controls the creation, updating and deletion of report configurations that describe what to import - to Sheets from the 3rd party source. By default report configurations are + to Sheets from the third-party source. By default report configurations are saved to Apps Script's PropertyService, but it would be possible to adapt the code here to store that data elsewhere (for example, in an external database). -* **JavaScript.html** - This file contains the bulk of the control code for the +* `**JavaScript.html**` - This file contains the bulk of the control code for the sidebar UI. -* **Server.gs** - This file contains server-side code that responds to user +* `**Server.gs**` - This file contains server-side code that responds to user interactions in the sidebar UI. It also sets up the add-on menu. -* **Sidebar.html** - This file contains the HTML structure for that defines +* `**Sidebar.html**` - This file contains the HTML structure for that defines the sidebar UI. -* **Stylesheet.html** - This file contains all the CSS properties defined for +* *`*Stylesheet.html**` - This file contains all the CSS properties defined for the template. -* **Utilities.gs** - This file contains some generic functionalities to support +* `**Utilities.gs**` - This file contains some generic functionalities to support the rest of the code. The functions here are not specific to this template and could be taken for use in other projects without modification. -* **intercom.js.html** - This file contains a copy of +* `**intercom.js.html**` - This file contains a copy of [intercom.js](https://github.com/diy/intercom.js), a cross-window message broadcast interface (intercom.js is released under an Apache V2.0 license). ## Setup: Libraries + This template makes use of the following libraries, which much the added to the Apps Script project before the template can be used: @@ -75,6 +75,7 @@ to your project. Repeat the above steps with the project key "MGwgKN2Th03tJ5OdmlzB8KPxhMjh3Sh48" to add Underscore to the project as well. ## Setup: API configuration + This template requires app-specific configuration before it can used. Specifically, the template will need to be informed of the authorization details, and certain adjustments made to ensure the correct data can be diff --git a/templates/sheets-import/intercom.js.html b/templates/sheets-import/intercom.js.html index 1154c4048..c8fcde5cb 100644 --- a/templates/sheets-import/intercom.js.html +++ b/templates/sheets-import/intercom.js.html @@ -1,3 +1,19 @@ + + -
    +
    diff --git a/ui/html/printing_scriptlet.html b/ui/html/printing_scriptlet.html index 41330449f..bbcaa7dd2 100644 --- a/ui/html/printing_scriptlet.html +++ b/ui/html/printing_scriptlet.html @@ -1,4 +1,20 @@ + + diff --git a/ui/html/scriptlet.html b/ui/html/scriptlet.html index 5c24ac8da..375d6015e 100644 --- a/ui/html/scriptlet.html +++ b/ui/html/scriptlet.html @@ -1,4 +1,20 @@ + + diff --git a/ui/html/standard_scriptlet.html b/ui/html/standard_scriptlet.html index dca1e0e1b..17fab1171 100644 --- a/ui/html/standard_scriptlet.html +++ b/ui/html/standard_scriptlet.html @@ -1,4 +1,20 @@ + + diff --git a/ui/sidebar/index.html b/ui/sidebar/index.html index f843bb290..c10e72907 100644 --- a/ui/sidebar/index.html +++ b/ui/sidebar/index.html @@ -1,4 +1,20 @@ + + diff --git a/ui/user/index.html b/ui/user/index.html index ea04a8256..59f26702e 100644 --- a/ui/user/index.html +++ b/ui/user/index.html @@ -1,4 +1,20 @@ + + diff --git a/ui/webapp/index.html b/ui/webapp/index.html index 7758abe48..86cb24e9a 100644 --- a/ui/webapp/index.html +++ b/ui/webapp/index.html @@ -1,4 +1,20 @@ + + diff --git a/utils/logging.gs b/utils/logging.gs index a3dc27ebb..cced7b3c4 100644 --- a/utils/logging.gs +++ b/utils/logging.gs @@ -1,4 +1,20 @@ -// [START apps_script_logging] +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START apps_script_logging_execution_time] /** * Logs the time taken to execute 'myFunction'. */ @@ -9,14 +25,13 @@ function measuringExecutionTime() { // Log a JSON object at a DEBUG level. The log is labeled // with the message string in the log viewer, and the JSON content // is displayed in the expanded log structure under "jsonPayload". - var parameters = { - isValid: true, - content: 'some string', - timestamp: new Date() + const parameters = { + isValid: true, + content: 'some string', + timestamp: new Date() }; console.log({message: 'Function Input', initialData: parameters}); - - var label = 'myFunction() time'; // Labels the timing log entry. + const label = 'myFunction() time'; // Labels the timing log entry. console.time(label); // Starts the timer. try { myFunction(parameters); // Function to time. @@ -26,9 +41,9 @@ function measuringExecutionTime() { } console.timeEnd(label); // Stops the timer, logs execution duration. } -// [END apps_script_logging] +// [END apps_script_logging_execution_time] -// [START apps_script_logging_2] +// [START apps_script_logging_sheet_information] /** * Logs Google Sheet information. * @param {number} rowNumber The spreadsheet row number. @@ -36,12 +51,15 @@ function measuringExecutionTime() { */ function emailDataRow(rowNumber, email) { console.log('Emailing data row ' + rowNumber + ' to ' + email); - var sheet = SpreadsheetApp.getActiveSheet(); - var data = sheet.getDataRange().getValues(); - var rowData = data[rowNumber-1].join(" "); - console.log('Row ' + rowNumber + ' data: ' + rowData); - MailApp.sendEmail(email, - 'Data in row ' + rowNumber, - rowData); + try { + const sheet = SpreadsheetApp.getActiveSheet(); + const data = sheet.getDataRange().getValues(); + const rowData = data[rowNumber - 1].join(' '); + console.log('Row ' + rowNumber + ' data: ' + rowData); + MailApp.sendEmail(email, 'Data in row ' + rowNumber, rowData); + } catch (err) { + // TODO (developer) - Handle exception + console.log('Failed with error %s', err.message); + } } -// [END apps_script_logging_2] +// [END apps_script_logging_sheet_information] diff --git a/utils/test_logging.gs b/utils/test_logging.gs new file mode 100644 index 000000000..4204bc181 --- /dev/null +++ b/utils/test_logging.gs @@ -0,0 +1,68 @@ +/** + * Copyright Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * create test spreadsheets + * @return {string} spreadsheet + */ +function createTestSpreadsheet() { + const spreadsheet = SpreadsheetApp.create('Test Spreadsheet'); + for (let i = 0; i < 3; ++i) { + spreadsheet.appendRow([1, 2, 3]); + } + return spreadsheet.getId(); +} + +/** + * populate the created spreadsheet with values + * @param {string} spreadsheetId + */ +function populateValues(spreadsheetId) { + const batchUpdateRequest = Sheets.newBatchUpdateSpreadsheetRequest(); + const repeatCellRequest = Sheets.newRepeatCellRequest(); + + const values = []; + for (let i = 0; i < 10; ++i) { + values[i] = []; + for (let j = 0; j < 10; ++j) { + values[i].push('Hello'); + } + } + const range = 'A1:J10'; + SpreadsheetApp.openById(spreadsheetId).getRange(range).setValues(values); + SpreadsheetApp.flush(); +} + +/** + * Test emailDataRow of logging.gs + */ +function itShouldEmailDataRow() { + console.log('> itShouldEmailDataRow'); + const email = Session.getActiveUser().getEmail(); + const spreadsheetId = createTestSpreadsheet(); + populateValues(spreadsheetId); + const data = Spreadsheet.openById(); + emailDataRow(1, email); +} + +/** + * runs all the functions of logging.gs + */ +function RUN_ALL_TESTS() { + console.log('> itShouldMeasureExecutionTime'); + measuringExecutionTime(); + itShouldEmailDataRow(); +} diff --git a/yarn.lock b/yarn.lock index 7e3016150..9bfb90d40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,361 +2,315 @@ # yarn lockfile v1 -acorn-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" - dependencies: - acorn "^3.0.4" +"@eslint/eslintrc@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz" + integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.2" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.9.2": + version "0.9.2" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz" + integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" -acorn@^3.0.4: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -acorn@^5.5.0: - version "5.5.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -ajv-keywords@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" +acorn@^8.7.1: + version "8.7.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== -ajv@^5.2.3, ajv@^5.3.0: - version "5.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" + fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -ansi-escapes@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - dependencies: - color-convert "^1.9.0" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - dependencies: - sprintf-js "~1.0.2" +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + color-convert "^2.0.1" -babel-code-frame@^6.22.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" -buffer-from@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" - -caller-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" - dependencies: - callsites "^0.2.0" - -callsites@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" + ansi-styles "^4.1.0" + supports-color "^7.1.0" -chalk@^2.0.0, chalk@^2.1.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" + color-name "~1.1.4" -chardet@^0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -circular-json@^0.3.1: - version "0.3.3" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - restore-cursor "^2.0.0" - -cli-width@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - -color-convert@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" +debug@^4.1.1, debug@^4.3.2: + version "4.3.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: - color-name "^1.1.1" - -color-name@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + ms "2.1.2" -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -concat-stream@^1.6.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" + esutils "^2.0.2" -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -cross-spawn@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" +eslint-config-google@0.14.0: + version "0.14.0" + resolved "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz" + integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== -debug@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" +eslint-plugin-googleappsscript@1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/eslint-plugin-googleappsscript/-/eslint-plugin-googleappsscript-1.0.4.tgz" + integrity sha512-Z6w1EMw0z0VOUvI0PFquigNSkYTgB+pVb2O2KYedDHnllwrgjjNBOLxsqPmtwmjOtFqCu7TuL0hJQwxVnp5OzQ== dependencies: - ms "2.0.0" - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + requireindex "~1.1.0" -del@^2.0.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== dependencies: - globby "^5.0.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - rimraf "^2.2.8" + esrecurse "^4.3.0" + estraverse "^5.2.0" -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== dependencies: - esutils "^2.0.2" - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - -eslint-config-google@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.9.1.tgz#83353c3dba05f72bb123169a4094f4ff120391eb" + eslint-visitor-keys "^2.0.0" -eslint-plugin-async-await@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-async-await/-/eslint-plugin-async-await-0.0.0.tgz#0f2ae17a3814780635d48f2409df9e37898ca09f" - -eslint-scope@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" - -eslint@^4.17.0: - version "4.19.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" - dependencies: - ajv "^5.3.0" - babel-code-frame "^6.22.0" - chalk "^2.1.0" - concat-stream "^1.6.0" - cross-spawn "^5.1.0" - debug "^3.1.0" - doctrine "^2.1.0" - eslint-scope "^3.7.1" - eslint-visitor-keys "^1.0.0" - espree "^3.5.4" - esquery "^1.0.0" +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@8.18.0: + version "8.18.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz" + integrity sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA== + dependencies: + "@eslint/eslintrc" "^1.3.0" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.2" + esquery "^1.4.0" esutils "^2.0.2" - file-entry-cache "^2.0.0" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" - glob "^7.1.2" - globals "^11.0.1" - ignore "^3.3.3" + glob-parent "^6.0.1" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^3.0.6" - is-resolvable "^1.0.0" - js-yaml "^3.9.1" + is-glob "^4.0.0" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.4" - minimatch "^3.0.2" - mkdirp "^0.5.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.2" - pluralize "^7.0.0" - progress "^2.0.0" - regexpp "^1.0.1" - require-uncached "^1.0.3" - semver "^5.3.0" - strip-ansi "^4.0.0" - strip-json-comments "~2.0.1" - table "4.0.2" - text-table "~0.2.0" - -espree@^3.5.4: - version "3.5.4" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" - dependencies: - acorn "^5.5.0" - acorn-jsx "^3.0.0" - -esprima@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" - -esquery@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^9.3.2: + version "9.3.2" + resolved "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz" + integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA== + dependencies: + acorn "^8.7.1" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: - estraverse "^4.0.0" + estraverse "^5.1.0" -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - -external-editor@^2.0.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" - dependencies: - chardet "^0.4.0" - iconv-lite "^0.4.17" - tmp "^0.0.33" + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -fast-deep-equal@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.4: +fast-levenshtein@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: - escape-string-regexp "^1.0.5" + flat-cache "^3.0.4" -file-entry-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: - flat-cache "^1.2.1" - object-assign "^4.0.1" + flatted "^3.1.0" + rimraf "^3.0.2" -flat-cache@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" - dependencies: - circular-json "^0.3.1" - del "^2.0.2" - graceful-fs "^4.1.2" - write "^0.2.1" +flatted@^3.1.0: + version "3.2.4" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz" + integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= functional-red-black-tree@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" -glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" +glob@^7.1.3: + version "7.2.0" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -365,447 +319,254 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^11.0.1: - version "11.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.4.0.tgz#b85c793349561c16076a3c13549238a27945f1bc" - -globby@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" +globals@^13.15.0: + version "13.15.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz" + integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -graceful-fs@^4.1.2: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + type-fest "^0.20.2" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - dependencies: - ansi-regex "^2.0.0" +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== -iconv-lite@^0.4.17: - version "0.4.21" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.21.tgz#c47f8733d02171189ebc4a400f3218d348094798" +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: - safer-buffer "^2.1.0" - -ignore@^3.3.3: - version "3.3.7" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + parent-module "^1.0.0" + resolve-from "^4.0.0" imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - -inquirer@^3.0.6: - version "3.3.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.0" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^2.0.4" - figures "^2.0.0" - lodash "^4.3.0" - mute-stream "0.0.7" - run-async "^2.2.0" - rx-lite "^4.0.8" - rx-lite-aggregates "^4.0.8" - string-width "^2.1.0" - strip-ansi "^4.0.0" - through "^2.3.6" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" +inherits@2: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-path-in-cwd@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" - dependencies: - is-path-inside "^1.0.0" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" +is-glob@^4.0.0, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: - path-is-inside "^1.0.1" - -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - -is-resolvable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + is-extglob "^2.1.1" isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -js-yaml@^3.9.1: - version "3.11.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: - argparse "^1.0.7" - esprima "^4.0.0" + argparse "^2.0.1" -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -lodash@^4.17.4, lodash@^4.3.0: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - -lru-cache@^4.0.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" + prelude-ls "^1.2.1" + type-check "~0.4.0" -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -minimatch@^3.0.2, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" +minimatch@^3.0.4, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - -mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - dependencies: - minimist "0.0.8" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - -object-assign@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== dependencies: - mimic-fn "^1.0.0" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" -optionator@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + callsites "^3.0.0" path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-is-inside@^1.0.1, path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -pluralize@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - -progress@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -readable-stream@^2.2.2: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -regexpp@^1.0.1: +requireindex@~1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" - -require-uncached@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" - dependencies: - caller-path "^0.1.0" - resolve-from "^1.0.0" - -resolve-from@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + resolved "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz" + integrity sha1-5UBLgVV+91225JxacgBIk/4D4WI= -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" - -rimraf@^2.2.8: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - dependencies: - glob "^7.0.5" - -run-async@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" - dependencies: - is-promise "^2.1.0" - -rx-lite-aggregates@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" - dependencies: - rx-lite "*" - -rx-lite@*, rx-lite@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" - -safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - -semver@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -signal-exit@^3.0.2: +rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - -slice-ansi@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: - is-fullwidth-code-point "^2.0.0" + glob "^7.1.3" -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - -string-width@^2.1.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: - safe-buffer "~5.1.0" + shebang-regex "^3.0.0" -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - dependencies: - ansi-regex "^2.0.0" +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^3.0.0" - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + ansi-regex "^5.0.1" -supports-color@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0" - dependencies: - has-flag "^3.0.0" +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -table@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: - ajv "^5.2.3" - ajv-keywords "^2.1.0" - chalk "^2.1.0" - lodash "^4.17.4" - slice-ansi "1.0.0" - string-width "^2.1.1" + has-flag "^4.0.0" -text-table@~0.2.0: +text-table@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - -through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: - os-tmpdir "~1.0.2" + prelude-ls "^1.2.1" -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - dependencies: - prelude-ls "~1.1.2" +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" -util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -which@^1.2.9: - version "1.3.0" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - -write@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" - dependencies: - mkdirp "^0.5.1" - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=