Compare commits
10 Commits
50fc0de433
...
8e0b928a80
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e0b928a80 | |||
| 248a90023c | |||
| 85223ca42a | |||
| a976451c78 | |||
| 31befe1d68 | |||
| 5f2012ce56 | |||
| 7a26c690e1 | |||
| 8265933d4b | |||
| f9a1d02588 | |||
| 7fd979aed8 |
@@ -1,19 +1,37 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@babel/eslint-parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"extends": ["airbnb-base", "plugin:prettier/recommended"],
|
"extends": [
|
||||||
"plugins": ["prettier"],
|
"airbnb-base",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"plugins": ["prettier", "googleappsscript"],
|
||||||
|
"env": {
|
||||||
|
"googleappsscript/googleappsscript": true
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"camelcase": "warn",
|
"camelcase": "warn",
|
||||||
"import/prefer-default-export": "warn",
|
"import/prefer-default-export": "warn",
|
||||||
"import/no-extraneous-dependencies": "warn",
|
"import/no-extraneous-dependencies": "warn",
|
||||||
"prefer-object-spread": "warn",
|
"prefer-object-spread": "warn",
|
||||||
"spaced-comment":"off"
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"ignorePackages",
|
||||||
|
{
|
||||||
|
"js": "never",
|
||||||
|
"ts": "never"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"ignorePatterns": ["dist", ".eslintrc.json"],
|
||||||
"babelOptions": {
|
"settings": {
|
||||||
"configFile": "./dev/.babelrc"
|
"import/resolver": {
|
||||||
|
"node": {
|
||||||
|
"extensions": [".js", ".ts"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,35 +9,35 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-10.15, macos-latest, windows-latest]
|
os: [macos-13, windows-2022]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
node-version: [12, 14, 16]
|
node-version: [20]
|
||||||
timeout-minutes: 8
|
timeout-minutes: 8
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- name: Install packages [npm ci]
|
- name: Install packages
|
||||||
run: npm ci
|
run: yarn install
|
||||||
- name: Allow installing cert in Mac OS Big Sur
|
- name: Allow running mkcert on Mac
|
||||||
run: sudo security authorizationdb write com.apple.trust-settings.admin allow
|
run: sudo security authorizationdb write com.apple.trust-settings.admin allow
|
||||||
if: matrix.os == 'macOS-latest'
|
if: runner.os == 'MacOS'
|
||||||
- name: Install mkcert
|
- name: Install mkcert
|
||||||
run: brew install mkcert
|
run: brew install mkcert
|
||||||
if: runner.os == 'MacOS'
|
if: runner.os == 'MacOS'
|
||||||
- name: Run mkcert setup [mkcert -install]
|
- name: Run mkcert setup [mkcert -install]
|
||||||
run: mkcert -install
|
run: mkcert -install
|
||||||
if: runner.os == 'MacOS'
|
if: runner.os == 'MacOS'
|
||||||
- name: Install https cert [npm setup:https]
|
- name: Install https cert [yarn setup:https]
|
||||||
run: npm run setup:https
|
run: yarn setup:https
|
||||||
if: runner.os == 'MacOS'
|
if: runner.os == 'MacOS'
|
||||||
- run: |
|
- run: |
|
||||||
mkdir certs
|
mkdir certs
|
||||||
.\test\generate-cert.ps1
|
.\scripts\generate-cert.ps1
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
run: npm run test:integration
|
run: yarn test:integration
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -1,40 +1,41 @@
|
|||||||
name: Local integration tests - Extended Version
|
name: Local integration tests - Extended Version
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
extended-integration-test:
|
extended-integration-test:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-10.15, macos-latest, windows-latest]
|
os: [macos-13, windows-2022]
|
||||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
node-version: [12, 14, 16]
|
node-version: [20]
|
||||||
timeout-minutes: 11
|
timeout-minutes: 11
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- name: Install packages [npm ci]
|
- name: Install packages
|
||||||
run: npm ci
|
run: yarn install
|
||||||
- name: Allow installing cert in Mac OS Big Sur
|
- name: Allow running mkcert on Mac
|
||||||
run: sudo security authorizationdb write com.apple.trust-settings.admin allow
|
run: sudo security authorizationdb write com.apple.trust-settings.admin allow
|
||||||
if: matrix.os == 'macOS-latest'
|
if: runner.os == 'MacOS'
|
||||||
- name: Install mkcert
|
- name: Install mkcert
|
||||||
run: brew install mkcert
|
run: brew install mkcert
|
||||||
if: runner.os == 'MacOS'
|
if: runner.os == 'MacOS'
|
||||||
- name: Run mkcert setup [mkcert -install]
|
- name: Run mkcert setup [mkcert -install]
|
||||||
run: mkcert -install
|
run: mkcert -install
|
||||||
if: runner.os == 'MacOS'
|
if: runner.os == 'MacOS'
|
||||||
- name: Install https cert [npm setup:https]
|
- name: Install https cert [yarn setup:https]
|
||||||
run: npm run setup:https
|
run: yarn setup:https
|
||||||
if: runner.os == 'MacOS'
|
if: runner.os == 'MacOS'
|
||||||
- run: |
|
- run: |
|
||||||
mkdir certs
|
mkdir certs
|
||||||
.\test\generate-cert.ps1
|
.\scripts\generate-cert.ps1
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
- name: Add .clasprc.json to home folder
|
- name: Add .clasprc.json to home folder
|
||||||
@@ -65,8 +66,12 @@ jobs:
|
|||||||
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
|
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
- name: Build and deploy dev setup [npm run deploy:dev]
|
- name: Build and deploy dev setup [yarn deploy:dev]
|
||||||
run: npm run deploy:dev
|
run: yarn deploy:dev
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
run: npm run test:integration:extended
|
# use ci-reporter to publish failing diff images to s3 bucket
|
||||||
|
# run: yarn test:integration:extended:ci-reporter
|
||||||
|
run: yarn test:integration:extended
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: macos-13
|
||||||
|
timeout-minutes: 8
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Install packages
|
||||||
|
run: yarn install
|
||||||
|
- name: Run lint
|
||||||
|
run: yarn lint
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
# npm
|
# npm
|
||||||
node_modules
|
node_modules
|
||||||
|
package-lock.json # repo pakai yarn.lock, jangan commit keduanya
|
||||||
|
|
||||||
# clasp files
|
# clasp files
|
||||||
creds.json
|
creds.json
|
||||||
|
# multi-account: token OAuth + scriptId per env, JANGAN commit
|
||||||
|
.clasprc*.json
|
||||||
|
.clasp.json
|
||||||
|
.clasp.*.json
|
||||||
|
|
||||||
# certs
|
# certs
|
||||||
*.pem
|
*.pem
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch spreadsheet with debugger",
|
||||||
|
"trace": true,
|
||||||
|
"sourceMaps": true,
|
||||||
|
"pauseForSourceMap": false,
|
||||||
|
"skipFiles": ["**/node_modules/**", "!${workspaceFolder}/**"],
|
||||||
|
"webRoot": "${workspaceFolder}/src/client",
|
||||||
|
// Need random open port for logging into spreadsheets:
|
||||||
|
// https://github.com/microsoft/vscode-js-debug/issues/918
|
||||||
|
"port": 12345,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2020 Elisha Nuchi
|
Copyright (c) 2024 Elisha Nuchi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<img width="400" src="https://i.imgur.com/83Y7bWN.png" alt="React & Google Apps Script logos"></a>
|
<img width="400" src="https://i.imgur.com/83Y7bWN.png" alt="React & Google Apps Script logos"></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center"><i>
|
<p align="center"><i>
|
||||||
Update 2022: Now with support for React v17 and React Fast Refresh
|
Built with React v18 and Vite for best-in-class frontend development.
|
||||||
</i></p>
|
</i></p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center"> 🚀 This is your boilerplate project for developing React apps inside Google Sheets, Docs, Forms and Slides projects. It's perfect for personal projects and for publishing complex add-ons in the G Suite Marketplace.
|
<p align="center"> 🚀 This is your boilerplate project for developing React apps inside Google Sheets, Docs, Forms and Slides projects. It's perfect for personal projects and for publishing complex add-ons in the Google Workspace Marketplace.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Getting started](#getting-started)
|
- [Getting started](#getting-started)
|
||||||
- [Deploy](#deploy)
|
- [Deploy](#deploy)
|
||||||
- [[New!] Local Development](#local-development)
|
- [Local Development](#local-development)
|
||||||
- [Using React DevTools](#dev-tools)
|
- [Using React DevTools](#dev-tools)
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [The included sample app](#the-included-sample-app)
|
- [The included sample app](#the-included-sample-app)
|
||||||
- [[New!] Typescript](#new-typescript)
|
- [Typescript](#new-typescript)
|
||||||
- [Adding packages](#adding-packages)
|
- [Adding packages](#adding-packages)
|
||||||
- [Styles](#styles)
|
- [Styles](#styles)
|
||||||
- [Modifying scopes](#modifying-scopes)
|
- [Modifying scopes](#modifying-scopes)
|
||||||
@@ -46,62 +46,58 @@
|
|||||||
|
|
||||||
[Google Apps Script](https://developers.google.com/apps-script/overview) is Google's Javascript-based development platform for building applications and add-ons for Google Sheets, Docs, Forms and other Google Apps.
|
[Google Apps Script](https://developers.google.com/apps-script/overview) is Google's Javascript-based development platform for building applications and add-ons for Google Sheets, Docs, Forms and other Google Apps.
|
||||||
|
|
||||||
You can add custom [user interfaces inside dialog windows](https://developers.google.com/apps-script/guides/html), but the platform is designed for simple HTML pages built with [templates](https://developers.google.com/apps-script/guides/html/templates) and [jQuery](https://developers.google.com/apps-script/guides/html/best-practices#take_advantage_of_jquery).
|
Google Apps Scripts lets you add custom [user interfaces inside dialog windows](https://developers.google.com/apps-script/guides/html). Using this template, it's easy to run [React](https://reactjs.org/) apps inside these dialogs, and build everything from small projects to advanced add-ons that can be published in the Google Workspace Marketplace.
|
||||||
|
|
||||||
However, using this repo, it's easy to run [React](https://reactjs.org/) apps inside these dialogs, and build everything from small projects to advanced add-ons that can be published on the G Suite Marketplace.
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img width="75%" src="https://i.imgur.com/BZvQ5ua.png" alt="React & Google Apps Script">
|
<img width="75%" src="https://i.imgur.com/BZvQ5ua.png" alt="React & Google Apps Script">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
This repo is a boilerplate project that uses React and the same development tools that you use for building traditional websites, all inside Google Apps Script projects.
|
This repo is a boilerplate project for developing React apps with Google Apps Script projects. You can use this starter template to build your own React apps and deploy them inside Google Sheets, Docs, Forms and Slides for use in dialogs and sidebars. Sample code is provided showing how your React app can interact with the underlying Google Apps Script server-side code.
|
||||||
|
|
||||||
See below how to get started!
|
|
||||||
|
|
||||||
|
Read on to get started with your own project!
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
## 🚜 Install <a name = "install"></a>
|
## 🚜 Install <a name = "install"></a>
|
||||||
|
|
||||||
These instructions will get you set up with a copy of the React project code on your local machine. It will also get you logged in to `clasp` so you can manage script projects from the command line.
|
These instructions will get you set up with a copy of the React project code on your local machine. It will also get you logged in to `clasp`, which lets you manage script projects from the command line.
|
||||||
|
|
||||||
See [deploy](#deploy) for notes on how to deploy the project and see it live in a Google Spreadsheet.
|
See [deploy](#deploy) for notes on how to deploy the project and see it live in a Google Spreadsheet.
|
||||||
|
|
||||||
### Prerequisites <a name = "prerequisites"></a>
|
### Prerequisites <a name = "prerequisites"></a>
|
||||||
|
|
||||||
- Make sure you're running at least [Node.js](https://nodejs.org/en/download/) v10 and `npm` v6.
|
- Make sure you're running at least [Node.js](https://nodejs.org/en/download/) v18 and [yarn (classic)](https://classic.yarnpkg.com/lang/en/docs/install/).
|
||||||
|
|
||||||
- You'll need to enable the Google Apps Script API. You can do that by visiting [script.google.com/home/usersettings](https://script.google.com/home/usersettings).
|
- You'll need to enable the Google Apps Script API. You can do that by visiting [script.google.com/home/usersettings](https://script.google.com/home/usersettings).
|
||||||
|
|
||||||
- [New!] To use live reload while developing, you'll need to serve your files locally using HTTPS. See [local development](#local-development) below for how to set up your local environment.
|
- To use live reload while developing, you'll need to serve your files locally using HTTPS. See [local development](#local-development) below for instructions on setting up your local environment.
|
||||||
|
|
||||||
### 🏁 Getting started <a name = "getting-started"></a>
|
### 🏁 Getting started <a name = "getting-started"></a>
|
||||||
|
|
||||||
**1.** First, let's clone the repo and install the dependencies.
|
Full steps to getting your local environment set up, deploying your app, and also running your app locally for local development are shown in the video below:
|
||||||
|
|
||||||
|
https://github.com/enuchi/React-Google-Apps-Script/assets/31550519/83622b83-0d0e-43de-a589-36f96d51c9c4
|
||||||
|
|
||||||
|
|
||||||
|
**1.** First, let's clone the repo and install the dependencies. This project is published as a public template, so you can also fork the repo or select "Use this template" in GitHub.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/enuchi/React-Google-Apps-Script.git
|
git clone https://github.com/enuchi/React-Google-Apps-Script.git
|
||||||
cd React-Google-Apps-Script
|
cd React-Google-Apps-Script
|
||||||
npm install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
<img width="100%" src="https://i.imgur.com/EGSsCqO.gif">
|
|
||||||
|
|
||||||
**2.** Next, we'll need to log in to [clasp](https://github.com/google/clasp), which lets us manage our Google Apps Script projects locally.
|
**2.** Next, we'll need to log in to [clasp](https://github.com/google/clasp), which lets us manage our Google Apps Script projects locally.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run login
|
yarn run login
|
||||||
```
|
```
|
||||||
|
|
||||||
<img width="100%" src="https://i.imgur.com/zKCgkMl.gif">
|
|
||||||
|
|
||||||
**3.** Now let's run the setup script to create a New spreadsheet and script project from the command line.
|
**3.** Now let's run the setup script to create a New spreadsheet and script project from the command line.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run setup
|
yarn run setup
|
||||||
```
|
```
|
||||||
|
|
||||||
<img width="100%" src="https://imgur.com/Zk2eHFV.gif">
|
|
||||||
|
|
||||||
Alternatively, you can use an existing Google Spreadsheet and Script file instead of creating a new one.
|
Alternatively, you can use an existing Google Spreadsheet and Script file instead of creating a new one.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -127,6 +123,8 @@ You will need to update the `.clasp.json` file in the root of this project with
|
|||||||
|
|
||||||
Next, let's deploy the app so we can see it live in Google Spreadsheets.
|
Next, let's deploy the app so we can see it live in Google Spreadsheets.
|
||||||
|
|
||||||
|
https://github.com/enuchi/React-Google-Apps-Script/assets/31550519/0c67c4b8-e3f5-4345-8460-470e9211aeb9
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
## 🚀 Deploy <a name = "deploy"></a>
|
## 🚀 Deploy <a name = "deploy"></a>
|
||||||
@@ -134,23 +132,19 @@ Next, let's deploy the app so we can see it live in Google Spreadsheets.
|
|||||||
Run the deploy command. You may be prompted to update your manifest file. Type 'yes'.
|
Run the deploy command. You may be prompted to update your manifest file. Type 'yes'.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run deploy
|
yarn run deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
The deploy command will build all necessary files using production settings, including all server code (Google Apps Script code), client code (React bundle), and config files. All bundled files will be outputted to the `dist/` folder, then pushed to the Google Apps Script project.
|
The deploy command will build all necessary files using production settings, including all server code (Google Apps Script code), client code (React bundle), and config files. All bundled files will be outputted to the `dist/` folder, then pushed to the Google Apps Script project.
|
||||||
|
|
||||||
Now open Google Sheets and navigate to your new spreadsheet (e.g. the file "My React Project"). You can also run `npm run open`. Make sure to refresh the page if you already had it open. You will now see a new menu item appear containing your app!
|
Now open Google Sheets and navigate to your new spreadsheet (e.g. the file "My React Project"). You can also run `yarn run open`. Make sure to refresh the page if you already had it open. You will now see a new menu item appear containing your app!
|
||||||
|
|
||||||
<img width="100%" src="https://i.imgur.com/W7UkEpv.gif">
|
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
## 🎈 [NEW!] Local Development <a name="local-development"></a>
|
## 🎈 Local Development <a name="local-development"></a>
|
||||||
|
|
||||||
We can develop our client-side React apps locally, and see our changes directly inside our Google Spreadsheet dialog window.
|
We can develop our client-side React apps locally, and see our changes directly inside our Google Spreadsheet dialog window.
|
||||||
|
|
||||||
<img width="100%" src="https://i.imgur.com/EsnOEHP.gif">
|
|
||||||
|
|
||||||
There are two steps to getting started: installing a certificate (first time only), and running the start command.
|
There are two steps to getting started: installing a certificate (first time only), and running the start command.
|
||||||
|
|
||||||
1. Generating a certificate for local development <a name = "generatingcerts"></a>
|
1. Generating a certificate for local development <a name = "generatingcerts"></a>
|
||||||
@@ -176,23 +170,19 @@ There are two steps to getting started: installing a certificate (first time onl
|
|||||||
Create the certs in your repo:
|
Create the certs in your repo:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run setup:https
|
yarn run setup:https
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Now you're ready to start:
|
2. Now you're ready to start:
|
||||||
```bash
|
```bash
|
||||||
npm run start
|
yarn run start
|
||||||
```
|
```
|
||||||
|
|
||||||
The start command will create and deploy a development build, and serve your local files.
|
The start command will create and deploy a development build, and serve your local files.
|
||||||
|
|
||||||
<img width="100%" src="https://imgur.com/uD4uZZK.gif">
|
|
||||||
|
|
||||||
After running the start command, navigate to your spreadsheet and open one of the menu items. It should now be serving your local files. When you make and save changes to your React app, your app will reload instantly within the Google Spreadsheet, and have access to any server-side functions!
|
After running the start command, navigate to your spreadsheet and open one of the menu items. It should now be serving your local files. When you make and save changes to your React app, your app will reload instantly within the Google Spreadsheet, and have access to any server-side functions!
|
||||||
|
|
||||||
<img width="100%" src="https://i.imgur.com/EsnOEHP.gif">
|
https://github.com/enuchi/React-Google-Apps-Script/assets/31550519/981604ac-bdea-489d-97fa-72e6d24ba6dd
|
||||||
|
|
||||||
Support for [Fast Refresh](https://github.com/pmmmwh/react-refresh-webpack-plugin) now means that only modified components are refreshed when files are changed, and state is not lost.
|
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
@@ -210,14 +200,14 @@ You will need to use the "standalone" version of React DevTools since our React
|
|||||||
1. In your repo install the React DevTools package as a dev dependency:
|
1. In your repo install the React DevTools package as a dev dependency:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -D react-devtools
|
yarn add -D react-devtools
|
||||||
```
|
```
|
||||||
|
|
||||||
2. In a new terminal window run `npx react-devtools` to launch the DevTools standalone app.
|
2. In a new terminal window run `npx react-devtools` to launch the DevTools standalone app.
|
||||||
|
|
||||||
3. Add `<script src="http://localhost:8097"></script>` to the top of your `<head>` in your React app, e.g. in the [index.html](https://github.com/enuchi/React-Google-Apps-Script/blob/e73e51e56e99903885ef8dd5525986f99038d8bf/src/client/dialog-demo-bootstrap/index.html) file in the sample Bootstrap app.
|
3. Add `<script src="http://localhost:8097"></script>` to the top of your `<head>` in your React app, e.g. in the [index.html](https://github.com/enuchi/React-Google-Apps-Script/blob/e73e51e56e99903885ef8dd5525986f99038d8bf/src/client/dialog-demo-bootstrap/index.html) file in the sample Bootstrap app.
|
||||||
|
|
||||||
4. Deploy your app (`npm run deploy:dev`) and you should see DevTools tool running and displaying your app hierarchy.
|
4. Deploy your app (`yarn run deploy:dev`) and you should see DevTools tool running and displaying your app hierarchy.
|
||||||
|
|
||||||
<img width="100%" src="https://user-images.githubusercontent.com/31550519/110273600-ee9eae80-7f9a-11eb-9796-31353b47dfa8.gif">
|
<img width="100%" src="https://user-images.githubusercontent.com/31550519/110273600-ee9eae80-7f9a-11eb-9796-31353b47dfa8.gif">
|
||||||
|
|
||||||
@@ -233,37 +223,63 @@ You will need to use the "standalone" version of React DevTools since our React
|
|||||||
|
|
||||||
The included sample app allows inserting/activating/deleting sheets through a simple HTML dialog, built with React. This simple app demonstrates how a React app can interact with the underlying Spreadsheet using Google Apps Script functions.
|
The included sample app allows inserting/activating/deleting sheets through a simple HTML dialog, built with React. This simple app demonstrates how a React app can interact with the underlying Spreadsheet using Google Apps Script functions.
|
||||||
|
|
||||||
The included sample app has three menu items for loading pages in various dialogs and sidebars.
|
The included sample app has five menu items that demonstrate how to load pages in various dialogs and sidebars. Sample implementations using different component libraries are included.
|
||||||
|
|
||||||
Two versions of the same app are provided with different styling: the first version uses vanilla React, and the second uses the popular bootstrap library (in this case, it uses [`react-bootstrap`](https://react-bootstrap.github.io/)). The bootstrap example also contains an example of a page built with typescript (see below)
|
- `Sheet Editor` - This is a basic app that opens in a dialog window that demonstrates how to select, create and delete sheets in a Google Sheets documents through server calls. It uses vanilla React with no component library.
|
||||||
|
- `Sheet Editor (Boostrap)` - The same basic app is included but styled with the Bootstrap library using [`react-bootstrap`](https://react-bootstrap.github.io/). The bootstrap example also contains an example of a page built with typescript (see below).
|
||||||
A third app just demonstrates how to load a sidebar dialog.
|
- `Sheet Editor (MUI)` - A similar example using [`Material UI`](https://mui.com/).
|
||||||
|
- `Sheet Editor (Tailwind CSS)` - Another example, using [`Tailwind CSS`](https://tailwindcss.com/)
|
||||||
|
- `About me` - This is just a simple page that demonstrates the use of a sidebar dialog.
|
||||||
|
|
||||||
Access the dialogs through the new menu item that appears. You may need to refresh the spreadsheet and approve the app's permissions the first time you use it.
|
Access the dialogs through the new menu item that appears. You may need to refresh the spreadsheet and approve the app's permissions the first time you use it.
|
||||||
|
|
||||||
### [New!] Typescript
|
Note that if you are choosing to use one framework, for example `Tailwind`, for your project, it is advisable to remove the dependencies for the other component libraries.
|
||||||
|
|
||||||
This project now supports typescript!
|
<details>
|
||||||
|
<summary>Here are some steps to take to clean up the repo if you are only using a single library</summary>
|
||||||
|
|
||||||
To use, simply use a typescript extension in either the client code (.ts/.tsx) or the server code (.ts), and your typescript file will compile to the proper format.
|
1. Uninstall unneeded dependencies (`yarn remove react-bootstrap ...` etc.)
|
||||||
|
|
||||||
|
2. Remove the unneeded menu bar items from the server code.
|
||||||
|
|
||||||
|
3. Remove the unneeded client code.
|
||||||
|
|
||||||
|
4. Update the `clientEntrypoints` in the [vite config file](./vite.config.ts) to only target the relevant apps.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</br>
|
||||||
|
|
||||||
|
### Typescript
|
||||||
|
|
||||||
|
This project is built mainly with typescript but also supports Javascript, and examples of both are included here, both in server-side and client-side (React) code. The included sample app has a typescript example using the Bootstrap component library.
|
||||||
|
|
||||||
|
To use typescript, simply use a typescript extension in either the client code (.ts/.tsx) or the server code (.ts), and your typescript file will compile to the proper format.
|
||||||
|
|
||||||
For client-side code, see [FormInput.tsx in the Bootstrap demo](./src/client/dialog-demo-bootstrap/components/FormInput.tsx) for an example file. Note that it is okay to have a mix of javascript and typescript, as seen in the Bootstrap demo.
|
For client-side code, see [FormInput.tsx in the Bootstrap demo](./src/client/dialog-demo-bootstrap/components/FormInput.tsx) for an example file. Note that it is okay to have a mix of javascript and typescript, as seen in the Bootstrap demo.
|
||||||
|
|
||||||
To use typescript in server code, just change the file extension to .ts. The server-side code already utilizes type definitions for Google Apps Script APIs.
|
To use typescript in server code, just change the file extension to .ts. The server-side code already utilizes type definitions for Google Apps Script APIs.
|
||||||
|
|
||||||
A basic typescript configuration is used here, because after code is transpiled from typescript to javascript it is once again transpiled to code that is compatible with Google Apps Script. However, if you want more control over your setup you can modify the included [tsconfig.json file](./tsconfig.json).
|
A basic typescript configuration is used here that correctly transpiles to code that is compatible with Google Apps Script. However, if you want more control over your setup you can modify the included [tsconfig.json file](./tsconfig.json).
|
||||||
|
|
||||||
### Adding packages
|
### Adding packages
|
||||||
|
|
||||||
You can add packages to your client-side React app.
|
You can add packages to your client-side React app.
|
||||||
|
|
||||||
For instance, install `react-transition-group` from npm:
|
For instance, install `react-transition-group`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install react-transition-group
|
yarn add react-transition-group
|
||||||
```
|
```
|
||||||
|
|
||||||
Important: Since Google Apps Scripts projects don't let you easily reference external files, this project will bundle an entire app into one HTML file. This can result in large files if you are importing large packages. To help split up the files, you can grab a CDN url for your package and declare it in the [webpack file, here](./webpack.config.js#L157). If set up properly, this will add a script tag that will load packages from a CDN, reducing your bundle size.
|
Important: Since Google Apps Scripts projects don't let you easily reference external files, this project will bundle an entire app into one HTML file. If you are importing large libraries this can result in a large file. To help reduce the size of these large HTML files, you can try to externalize packages by using a CDN to load packages. For packages that can be loaded through a CDN (usually they will have a UMD build), you can configure the externals and globals details in the [vite config file](./vite.config.ts). You will also need to include a script element in the head of the `index.html` file, loading the library from a CDN, and making sure it supports a UMD build, e.g.
|
||||||
|
`<script crossorigin src="https://unpkg.com/react-transition-group@4.4.2/dist/react-transition-group.min.js"></script>`.
|
||||||
|
|
||||||
|
If set up properly, this will load packages from the CDN in production and will reduce your overall bundle size.
|
||||||
|
|
||||||
|
Make sure that you update the script tag with the same version of the package you are installing with yarn, so that you are using the same version in development and production.
|
||||||
|
|
||||||
### Styles
|
### Styles
|
||||||
|
|
||||||
@@ -275,8 +291,6 @@ import './styles.css';
|
|||||||
|
|
||||||
Many external component libraries require a css stylesheet in order to work properly. You can import stylesheets in the HTML template, [as shown here with the Bootstrap stylesheet](./src/client/dialog-demo-bootstrap/index.html).
|
Many external component libraries require a css stylesheet in order to work properly. You can import stylesheets in the HTML template, [as shown here with the Bootstrap stylesheet](./src/client/dialog-demo-bootstrap/index.html).
|
||||||
|
|
||||||
The webpack.config.js file can also be modified to support scss and other style libraries.
|
|
||||||
|
|
||||||
### Modifying scopes
|
### Modifying scopes
|
||||||
|
|
||||||
The included app only requires access to Google Spreadsheets and to loading dialog windows. If you make changes to the app's requirements, for instance, if you modify this project to work with Google Forms or Docs, make sure to edit the oauthScopes in the [appscript.json file](./appsscript.json).
|
The included app only requires access to Google Spreadsheets and to loading dialog windows. If you make changes to the app's requirements, for instance, if you modify this project to work with Google Forms or Docs, make sure to edit the oauthScopes in the [appscript.json file](./appsscript.json).
|
||||||
@@ -288,23 +302,23 @@ See https://developers.google.com/apps-script/manifest for information on the `a
|
|||||||
This project uses the [gas-client](https://github.com/enuchi/gas-client) package to more easily call server-side functions using promises.
|
This project uses the [gas-client](https://github.com/enuchi/gas-client) package to more easily call server-side functions using promises.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Google's documentation wants you to do this. Boo.
|
// Google's client-side google.script.run utility requires calling server-side functions like this:
|
||||||
google.script.run
|
google.script.run
|
||||||
.withSuccessHandler(response => doSomething(response))
|
.withSuccessHandler((response) => doSomething(response))
|
||||||
.withFailureHandler(err => handleError(err))
|
.withFailureHandler((err) => handleError(err))
|
||||||
.addSheet(sheetTitle);
|
.addSheet(sheetTitle);
|
||||||
|
|
||||||
// Poof! With a little magic we can now do this:
|
// Using gas-client we can use more familiar promises style like this:
|
||||||
import Server from 'gas-client';
|
import { GASClient } from 'gas-client';
|
||||||
const { serverFunctions } = new Server();
|
const { serverFunctions, scriptHostFunctions } = new GASClient({});
|
||||||
|
|
||||||
// We now have access to all our server functions, which return promises!
|
// We now have access to all our server functions, which return promises!
|
||||||
serverFunctions
|
serverFunctions
|
||||||
.addSheet(sheetTitle)
|
.addSheet(sheetTitle)
|
||||||
.then(response => doSomething(response))
|
.then((response) => doSomething(response))
|
||||||
.catch(err => handleError(err));
|
.catch((err) => handleError(err));
|
||||||
|
|
||||||
// Or we can equally use async/await style:
|
// Or with async/await:
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const response = await serverFunctions.addSheet(sheetTitle);
|
const response = await serverFunctions.addSheet(sheetTitle);
|
||||||
@@ -313,9 +327,15 @@ async () => {
|
|||||||
handleError(err);
|
handleError(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Use scriptHostFunctions to control dialogs
|
||||||
|
scriptHostFunctions.close(); // close a dialog or sidebar
|
||||||
|
scriptHostFunctions.setWidth(400); // set dialog width to 400px
|
||||||
|
scriptHostFunctions.setHeight(800); // set dialog height to 800px
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In development, `gas-client` will interact with [the custom Webpack Dev Server package](https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server) which allows us to run our app within the dialog window and still interact with Google Apps Script functions.
|
In development, `gas-client` will allow you to call server-side functions from your local environment. In production, it will use Google's underlying `google.script.run` utility.
|
||||||
|
|
||||||
### Autocomplete
|
### Autocomplete
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Aktifkan salah satu akun clasp (dev / prod / bdn) dengan swap file kredensial global.
|
||||||
|
.DESCRIPTION
|
||||||
|
clasp hanya mengenal SATU login global (%USERPROFILE%\.clasprc.json) dan tidak
|
||||||
|
punya flag --local (terverifikasi di clasp 2.5.0). Skrip ini meng-copy profil
|
||||||
|
tersimpan (.clasp-accounts\<env>.clasprc.json) menjadi login aktif, lalu
|
||||||
|
verifikasi dengan `clasp login --status`.
|
||||||
|
.EXAMPLE
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\Use-Clasp.ps1 dev
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[ValidateSet("dev", "prod", "bdn")]
|
||||||
|
[string]$Account = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$profileDir = Join-Path $env:USERPROFILE ".clasp-accounts"
|
||||||
|
$src = Join-Path $profileDir ("$Account.clasprc.json")
|
||||||
|
$dst = Join-Path $env:USERPROFILE ".clasprc.json"
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $src)) {
|
||||||
|
Write-Output "Profil '$Account' belum ada: $src"
|
||||||
|
Write-Output "Cara buat (sekali per akun):"
|
||||||
|
Write-Output " 1. clasp login # login sebagai akun-$Account di browser"
|
||||||
|
Write-Output " 2. New-Item -ItemType Directory -Force '$profileDir'"
|
||||||
|
Write-Output " 3. Copy-Item '$dst' '$src'"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||||
|
Write-Output "Akun clasp aktif: $Account"
|
||||||
|
|
||||||
|
$claspCmd = Get-Command clasp -ErrorAction SilentlyContinue
|
||||||
|
if (-not $claspCmd) {
|
||||||
|
$localClasp = Join-Path $PSScriptRoot "node_modules/.bin/clasp.ps1"
|
||||||
|
if (Test-Path -LiteralPath $localClasp) { $claspCmd = $localClasp }
|
||||||
|
else { $claspCmd = "clasp" }
|
||||||
|
}
|
||||||
|
|
||||||
|
& $claspCmd login --status
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Output "Token profil '$Account' tidak valid/kadaluarsa."
|
||||||
|
Write-Output "Perbaiki: clasp login (sebagai akun-$Account), lalu Copy-Item '$dst' '$src' -Force"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/react"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-proposal-object-rest-spread",
|
|
||||||
"@babel/plugin-proposal-class-properties"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"extends": [
|
|
||||||
"airbnb-base",
|
|
||||||
"plugin:prettier/recommended",
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"plugins": ["babel", "react", "prettier"],
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"google": false,
|
|
||||||
"alert": false,
|
|
||||||
"css": true
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 9,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"react/prop-types": "warn",
|
|
||||||
"camelcase": "warn",
|
|
||||||
"import/prefer-default-export": "warn",
|
|
||||||
"import/no-extraneous-dependencies": "warn",
|
|
||||||
"prefer-object-spread": "warn"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
},
|
|
||||||
"import/resolver": {
|
|
||||||
"node": {
|
|
||||||
"extensions": [".js", ".jsx"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Development App Wrapper
|
|
||||||
|
|
||||||
This directory contains the app needed to run development mode.
|
|
||||||
|
|
||||||
It utilizes special Webpack configurations as well as two packages, [gas-client](https://github.com/enuchi/gas-client) and [Webpack Dev Server for Google Apps Script](https://github.com/enuchi/Google-Apps-Script-Webpack-Dev-Server), in order to achieve hot reloading inside of a dialog window.
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
Running `npm run start` will build and deploy the development app, and then serve files locally.
|
|
||||||
|
|
||||||
The simple React app in this directory, found at [index.js](./index.js), is designed to only be used with development builds. It loads an iframe with the source pointing to `https://localhost:${PORT}/gas/${FILENAME}-impl.html`. During the build step, we will replace `FILENAME` with the appropriate name of the HTML file to load.
|
|
||||||
|
|
||||||
As an example, during the development build, the file `dialog-demo-bootstrap.html` will be generated from the app in this directory, using `dialog-demo-bootstrap` as the `FILENAME` for the iframe source of this page. Opening the menu items in the Google Spreadsheet will load this app, and webpack's devServer settings will serve `https://localhost:3000/gas/dialog-demo-bootstrap-impl.html` within the iframe, using the customized Webpack Dev Server build.
|
|
||||||
|
|
||||||
The customized Google Apps Script Webpack Dev Server acts very similarly to Webpack Dev Server's iframe mode, but is able to pass requests to Google Apps Script server functions back and forth between all the iframes being used in development.
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<!--
|
||||||
|
This is a development server page that serves as a wrapper for Google Apps Script (GAS) client-side development.
|
||||||
|
|
||||||
|
It is meant to be run inside a Google Sheets/Docs/Forms dialog window during local development.
|
||||||
|
|
||||||
|
It loads the gas-client library (as an external), sets up an iframe that points to a local development
|
||||||
|
server (such as running with vite), and establishes a communication bridge between the GAS server functions and the local development server.
|
||||||
|
|
||||||
|
This allows for local development and testing of client-side code while still being able to interact with
|
||||||
|
the GAS server-side functions.
|
||||||
|
|
||||||
|
Two placeholders are used in this file that will need to be replaced in a build step:
|
||||||
|
- _ _PORT_ _: The port number of the local development server. (e.g. 3000)
|
||||||
|
- _ _FILE_NAME_ _: The name of the file being loaded. (e.g. dialog-demo-bootstrap/index.html)
|
||||||
|
|
||||||
|
-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base target="_top" />
|
||||||
|
<title>Dev Server</title>
|
||||||
|
<!-- Load gas-client as external. Exposed global variable is GASClient. -->
|
||||||
|
<script src="https://unpkg.com/gas-client@1.2.0/dist/index.js"></script>
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// These values need to be replaced during the build process
|
||||||
|
const PORT = '__PORT__';
|
||||||
|
const FILE_NAME = '__FILE_NAME__';
|
||||||
|
|
||||||
|
const iframe = document.getElementById('iframe');
|
||||||
|
iframe.src = 'https://localhost:' + PORT + '/' + FILE_NAME;
|
||||||
|
|
||||||
|
const { serverFunctions, scriptHostFunctions } =
|
||||||
|
new window.GASClient.GASClient({
|
||||||
|
allowedDevelopmentDomains: (origin) =>
|
||||||
|
/https:\/\/.*\.googleusercontent\.com$/.test(origin),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRequest = (event) => {
|
||||||
|
const request = event.data;
|
||||||
|
const { type, functionName, id, args } = request;
|
||||||
|
|
||||||
|
if (type === 'SCRIPT_HOST_FUNCTION_REQUEST') {
|
||||||
|
scriptHostFunctions[functionName](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type !== 'REQUEST') return;
|
||||||
|
|
||||||
|
serverFunctions[functionName](...args)
|
||||||
|
.then((response) => {
|
||||||
|
iframe.contentWindow.postMessage(
|
||||||
|
{ type: 'RESPONSE', id, status: 'SUCCESS', response },
|
||||||
|
'https://localhost:' + PORT
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
iframe.contentWindow.postMessage(
|
||||||
|
{
|
||||||
|
type: 'RESPONSE',
|
||||||
|
id,
|
||||||
|
status: 'ERROR',
|
||||||
|
response: err,
|
||||||
|
},
|
||||||
|
'https://localhost:' + PORT
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.addEventListener('message', handleRequest, false);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="width: 100%; height: 100%">
|
||||||
|
<iframe
|
||||||
|
id="iframe"
|
||||||
|
style="width: 100%; height: 100%; border: 0; position: absolute"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<base target="_top" />
|
|
||||||
<!-- Add any external scripts and stylesheets here -->
|
|
||||||
<style>
|
|
||||||
body,
|
|
||||||
html {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section id="index" />
|
|
||||||
<!-- bundled js and css will get inlined here during build -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import { serverFunctions } from '../src/client/utils/serverFunctions.ts';
|
|
||||||
|
|
||||||
const { FILENAME, PORT } = process.env;
|
|
||||||
|
|
||||||
const DevServer = () => {
|
|
||||||
const iframe = React.useRef(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const handleRequest = (event) => {
|
|
||||||
const request = event.data;
|
|
||||||
const { type, functionName, id, args } = request;
|
|
||||||
|
|
||||||
if (type !== 'REQUEST') return;
|
|
||||||
|
|
||||||
serverFunctions[functionName](...args)
|
|
||||||
.then((response) => {
|
|
||||||
iframe.current.contentWindow.postMessage(
|
|
||||||
{ type: 'RESPONSE', id, status: 'SUCCESS', response },
|
|
||||||
`https://localhost:${PORT}`
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
iframe.current.contentWindow.postMessage(
|
|
||||||
{
|
|
||||||
type: 'RESPONSE',
|
|
||||||
id,
|
|
||||||
status: 'ERROR',
|
|
||||||
response: err,
|
|
||||||
},
|
|
||||||
`https://localhost:${PORT}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('message', handleRequest, false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
// we want our dev environment to fill the dialog window
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
border: '0',
|
|
||||||
position: 'absolute',
|
|
||||||
}}
|
|
||||||
ref={iframe}
|
|
||||||
src={`https://localhost:${PORT}/${FILENAME}-impl.html`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactDOM.render(<DevServer />, document.getElementById('index'));
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
module.exports = {
|
export default {
|
||||||
globalSetup: './test/global-setup.js',
|
globalSetup: './test/global-setup.js',
|
||||||
globalTeardown: './test/global-teardown.js',
|
globalTeardown: './test/global-teardown.js',
|
||||||
testEnvironment: './test/puppeteer-environment.js',
|
testEnvironment: './test/puppeteer-environment.js',
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "react-google-apps-script",
|
"name": "react-google-apps-script",
|
||||||
"version": "2.1.1",
|
"version": "3.1.0",
|
||||||
|
"type": "module",
|
||||||
"description": "Starter project for using React with Google Apps Script",
|
"description": "Starter project for using React with Google Apps Script",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/enuchi/React-Google-Apps-Script.git"
|
"url": "https://github.com/enuchi/React-Google-Apps-Script.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"lint": "eslint .",
|
||||||
"test:integration": "jest --forceExit test/local-development.test",
|
"test:integration": "jest --forceExit test/local-development.test",
|
||||||
"test:integration:extended": "cross-env IS_EXTENDED=true jest --forceExit test/local-development.test",
|
"test:integration:extended": "cross-env IS_EXTENDED=true jest --forceExit test/local-development.test",
|
||||||
|
"test:integration:extended:ci-reporter": "cross-env IS_EXTENDED=true jest --forceExit || node test/utils/image-reporter-standalone.js",
|
||||||
"login": "clasp login",
|
"login": "clasp login",
|
||||||
"setup": "rimraf .clasp.json && mkdirp dist && clasp create --type sheets --title \"My React Project\" --rootDir ./dist && mv ./dist/.clasp.json ./.clasp.json && rimraf dist",
|
"setup": "rimraf .clasp.json && mkdirp dist && clasp create --type sheets --title \"My React Project\" --rootDir ./dist && mv ./dist/.clasp.json ./.clasp.json && rimraf dist",
|
||||||
"open": "clasp open --addon",
|
"open": "clasp open --addon",
|
||||||
|
"push": "clasp push",
|
||||||
|
"use:dev": "powershell -NoProfile -ExecutionPolicy Bypass -File ./Use-Clasp.ps1 dev",
|
||||||
|
"use:prod": "powershell -NoProfile -ExecutionPolicy Bypass -File ./Use-Clasp.ps1 prod",
|
||||||
|
"use:bdn": "powershell -NoProfile -ExecutionPolicy Bypass -File ./Use-Clasp.ps1 bdn",
|
||||||
"setup:https": "mkdirp certs && mkcert -key-file ./certs/key.pem -cert-file ./certs/cert.pem localhost 127.0.0.1",
|
"setup:https": "mkdirp certs && mkcert -key-file ./certs/key.pem -cert-file ./certs/cert.pem localhost 127.0.0.1",
|
||||||
"build:dev": "cross-env NODE_ENV=development webpack",
|
"build:dev": "tsc && vite build --mode development",
|
||||||
"build": "cross-env NODE_ENV=production webpack",
|
"build": "tsc && vite build --mode production",
|
||||||
"deploy:dev": "rimraf dist && npm run build:dev && npx clasp push",
|
"deploy:dev": "yarn build:dev && yarn push",
|
||||||
"deploy": "rimraf dist && npm run build && npx clasp push",
|
"deploy": "yarn build && yarn push",
|
||||||
"serve": "cross-env NODE_ENV=development webpack serve",
|
"start": "yarn deploy:dev && yarn dev"
|
||||||
"start": "npm run deploy:dev && npm run serve"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
@@ -34,79 +41,56 @@
|
|||||||
"npm": ">=6.0.0"
|
"npm": ">=6.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.10.6",
|
||||||
|
"@emotion/styled": "^11.10.6",
|
||||||
|
"@mui/material": "^5.11.11",
|
||||||
|
"gas-client": "^1.2.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^2.4.0",
|
"react-bootstrap": "^2.4.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-transition-group": "^4.4.2",
|
"react-transition-group": "^4.4.2"
|
||||||
"tailwindcss": "^3.1.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.17.6",
|
"@babel/preset-env": "^7.24.6",
|
||||||
"@babel/core": "^7.17.8",
|
"@google/clasp": "^2.4.2",
|
||||||
"@babel/eslint-parser": "^7.17.0",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.17.12",
|
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.18.0",
|
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.17.12",
|
|
||||||
"@babel/plugin-transform-object-assign": "^7.16.7",
|
|
||||||
"@babel/polyfill": "^7.12.1",
|
|
||||||
"@babel/preset-env": "^7.18.2",
|
|
||||||
"@babel/preset-react": "^7.17.12",
|
|
||||||
"@effortlessmotion/dynamic-cdn-webpack-plugin": "^5.0.1",
|
|
||||||
"@effortlessmotion/html-webpack-inline-source-plugin": "^1.0.3",
|
|
||||||
"@google/clasp": "^2.4.1",
|
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
|
||||||
"@types/expect-puppeteer": "^5.0.0",
|
"@types/expect-puppeteer": "^5.0.0",
|
||||||
"@types/jest-environment-puppeteer": "^5.0.2",
|
"@types/jest-environment-puppeteer": "^5.0.2",
|
||||||
|
"@types/node": "^20.11.30",
|
||||||
"@types/puppeteer": "^5.4.6",
|
"@types/puppeteer": "^5.4.6",
|
||||||
"@types/react": "^18.0.14",
|
"@types/react": "^18.2.66",
|
||||||
"autoprefixer": "10.4.5",
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
"aws-sdk": "^2.1106.0",
|
"aws-sdk": "^2.1106.0",
|
||||||
"babel-loader": "^8.2.5",
|
|
||||||
"babel-plugin-add-module-exports": "^1.0.4",
|
|
||||||
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
|
||||||
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "^6.7.1",
|
"dotenv": "^16.4.5",
|
||||||
"dotenv": "^16.0.1",
|
"eslint": "^8.57.0",
|
||||||
"eslint": "^8.17.0",
|
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-config-standard": "^17.0.0",
|
|
||||||
"eslint-plugin-babel": "^5.3.1",
|
|
||||||
"eslint-plugin-googleappsscript": "^1.0.4",
|
"eslint-plugin-googleappsscript": "^1.0.4",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-jest": "^26.5.3",
|
"eslint-plugin-jest": "^26.5.3",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react": "^7.30.0",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
"eslint-plugin-standard": "^5.0.0",
|
"gas-types-detailed": "^1.1.2",
|
||||||
"gas-client": "^1.1.1",
|
|
||||||
"gas-lib": "^2.0.4",
|
|
||||||
"gas-types-detailed": "^1.1.0",
|
|
||||||
"gas-webpack-plugin": "^2.2.2",
|
|
||||||
"html-webpack-plugin": "^5.5.0",
|
|
||||||
"jest": "^28.1.1",
|
"jest": "^28.1.1",
|
||||||
"jest-environment-node": "^28.1.1",
|
"jest-environment-node": "^28.1.1",
|
||||||
"jest-image-snapshot": "^5.1.0",
|
"jest-image-snapshot": "^5.1.0",
|
||||||
"mkdirp": "^1.0.4",
|
"postcss": "^8.4.38",
|
||||||
"module-to-cdn": "^3.1.5",
|
"postcss-preset-env": "^9.5.4",
|
||||||
"postcss-loader": "^7.0.1",
|
|
||||||
"postcss-preset-env": "^7.7.2",
|
|
||||||
"prettier": "^2.7.0",
|
"prettier": "^2.7.0",
|
||||||
"puppeteer": "^14.3.0",
|
"puppeteer": "^14.3.0",
|
||||||
"puppeteer-extra": "^3.2.3",
|
"puppeteer-extra": "^3.2.3",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.9.0",
|
"puppeteer-extra-plugin-stealth": "^2.9.0",
|
||||||
"react-refresh": "^0.14.0",
|
"rollup": "^4.18.0",
|
||||||
"rimraf": "^3.0.2",
|
"tailwindcss": "^3.4.3",
|
||||||
"style-loader": "^3.3.1",
|
"typescript": "^5.2.2",
|
||||||
"terser-webpack-plugin": "^5.3.3",
|
"vite": "^5.2.0",
|
||||||
"ts-loader": "^9.3.0",
|
"vite-plugin-singlefile": "^2.0.1",
|
||||||
"webpack": "^5.73.0",
|
"vite-plugin-static-copy": "^1.0.1"
|
||||||
"webpack-cli": "^4.10.0",
|
|
||||||
"webpack-dev-server": "^4.9.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const tailwindcss = require('tailwindcss');
|
|
||||||
const autoprefixer = require('autoprefixer');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
plugins: ['postcss-preset-env', tailwindcss, autoprefixer],
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Wrapper satu pintu untuk clasp tiga akun + tiga project (dev / prod / bdn).
|
||||||
|
.DESCRIPTION
|
||||||
|
Urutan kerja: swap akun (Use-Clasp.ps1) -> salin .clasp.<env>.json menjadi
|
||||||
|
.clasp.json -> guard khusus prod -> jalankan clasp. Jangan panggil
|
||||||
|
`clasp push` langsung; selalu lewat wrapper ini / npm script.
|
||||||
|
.EXAMPLE
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\clasp-env.ps1 -Env dev -Cmd use
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\clasp-env.ps1 -Env dev -Cmd push
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\clasp-env.ps1 -Env prod -Cmd push
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[ValidateSet("dev", "prod", "bdn")]
|
||||||
|
[string]$Env = "dev",
|
||||||
|
[ValidateSet("use", "push")]
|
||||||
|
[string]$Cmd = "use"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$repoRoot = Split-Path $PSScriptRoot -Parent
|
||||||
|
|
||||||
|
# 1. Aktifkan akun yang sesuai (berhenti bila profil/token bermasalah).
|
||||||
|
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repoRoot "Use-Clasp.ps1") $Env
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||||
|
|
||||||
|
# 2. Arahkan ke project script yang sesuai.
|
||||||
|
$projectFile = Join-Path $repoRoot ".clasp.$Env.json"
|
||||||
|
$activeFile = Join-Path $repoRoot ".clasp.json"
|
||||||
|
if (-not (Test-Path -LiteralPath $projectFile)) {
|
||||||
|
Write-Output "File project belum ada: $projectFile"
|
||||||
|
Write-Output "Cara buat: clasp create --type sheets --title ... --rootDir ./dist"
|
||||||
|
Write-Output "lalu isi scriptId project-$Env, atau copy dari .clasp.json.SAMPLE"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Copy-Item -LiteralPath $projectFile -Destination $activeFile -Force
|
||||||
|
|
||||||
|
$scriptId = (Get-Content -LiteralPath $activeFile -Raw | ConvertFrom-Json).scriptId
|
||||||
|
Write-Output "Project aktif [$Env]: $scriptId"
|
||||||
|
|
||||||
|
if ($Cmd -eq "use") {
|
||||||
|
Write-Output "Siap. Perintah clasp berikutnya memakai akun-$Env + project-$Env."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Guard prod/bdn: wajib konfirmasi eksplisit.
|
||||||
|
if ($Env -eq "prod" -or $Env -eq "bdn") {
|
||||||
|
$need = $Env.ToUpper()
|
||||||
|
$answer = Read-Host "Ketik $need untuk push ke PROYEK $need ($scriptId)"
|
||||||
|
if ($answer -cne $need) {
|
||||||
|
Write-Output "Dibatalkan. Tidak ada yang di-push."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Push (build dulu agar dist/ selalu segar dari src/).
|
||||||
|
Push-Location $repoRoot
|
||||||
|
try {
|
||||||
|
& npm run build
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||||
|
& clasp push
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["@babel/react"],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-proposal-object-rest-spread",
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
"@babel/plugin-proposal-optional-chaining"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,24 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"extends": [
|
"extends": [
|
||||||
"airbnb-base",
|
"airbnb-base",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
"plugin:react/recommended"
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
],
|
],
|
||||||
"plugins": ["babel", "react", "prettier"],
|
"plugins": ["react-refresh", "prettier"],
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"globals": {
|
|
||||||
"google": false,
|
|
||||||
"alert": false,
|
|
||||||
"css": true
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 9,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"react/prop-types": "warn",
|
|
||||||
"camelcase": "warn",
|
"camelcase": "warn",
|
||||||
"import/prefer-default-export": "warn",
|
"import/prefer-default-export": "warn",
|
||||||
"import/no-extraneous-dependencies": "warn",
|
"import/no-extraneous-dependencies": "warn",
|
||||||
"prefer-object-spread": "warn",
|
"prefer-object-spread": "warn",
|
||||||
|
"spaced-comment": "off",
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ "allowConstantExport": true }
|
||||||
|
],
|
||||||
"import/extensions": [
|
"import/extensions": [
|
||||||
"error",
|
"error",
|
||||||
"ignorePackages",
|
"ignorePackages",
|
||||||
@@ -50,5 +39,7 @@
|
|||||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"env": { "browser": true, "es2020": true },
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, ChangeEvent, FormEvent } from 'react';
|
import { useState, ChangeEvent, FormEvent } from 'react';
|
||||||
import { Form, Button, Col, Row } from 'react-bootstrap';
|
import { Form, Button, Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
interface FormInputProps {
|
interface FormInputProps {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||||
import { Button, ListGroup } from 'react-bootstrap';
|
import { Button, ListGroup } from 'react-bootstrap';
|
||||||
import FormInput from './FormInput';
|
import FormInput from './FormInput';
|
||||||
|
|
||||||
// This is a wrapper for google.script.run that lets us use promises.
|
// This is a wrapper for google.script.run that lets us use promises.
|
||||||
import { serverFunctions } from '../../utils/serverFunctions';
|
import {
|
||||||
|
serverFunctions,
|
||||||
|
scriptHostFunctions,
|
||||||
|
} from '../../utils/serverFunctions';
|
||||||
|
|
||||||
const SheetEditor = () => {
|
const SheetEditor = () => {
|
||||||
const [names, setNames] = useState([]);
|
const [names, setNames] = useState([]);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
serverFunctions.getSheetsData().then(setNames).catch(alert);
|
serverFunctions.getSheetsData().then(setNames).catch(alert);
|
||||||
@@ -77,6 +81,29 @@ const SheetEditor = () => {
|
|||||||
))}
|
))}
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
{names.length > 0 && (
|
||||||
|
<div className="d-flex justify-content-end py-3">
|
||||||
|
{!isExpanded ? (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
className="mr-2"
|
||||||
|
onClick={() => {
|
||||||
|
scriptHostFunctions.setHeight(1000);
|
||||||
|
scriptHostFunctions.setWidth(1000);
|
||||||
|
setIsExpanded(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Expand Dialog
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
variant="outline-dark"
|
||||||
|
onClick={() => scriptHostFunctions.close()}
|
||||||
|
>
|
||||||
|
Close Dialog Window
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,36 @@
|
|||||||
<head>
|
<head>
|
||||||
<base target="_top" />
|
<base target="_top" />
|
||||||
<!-- Add any external scripts and stylesheets here -->
|
<!-- Add any external scripts and stylesheets here -->
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-transition-group@4.4.2/dist/react-transition-group.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-bootstrap@2.4.0/dist/react-bootstrap.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/gas-client@1.2.0/dist/index.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/@types/react@18.2.66/index.d.ts"
|
||||||
|
></script>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
|
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
|
||||||
@@ -12,6 +42,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="index">
|
<section id="index">
|
||||||
|
<script type="module" src="./index.jsx"></script>
|
||||||
<!-- bundled js and css will get inlined here during build-->
|
<!-- bundled js and css will get inlined here during build-->
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import SheetEditor from './components/SheetEditor';
|
||||||
|
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
const container = document.getElementById('index');
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<SheetEditor />);
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
/* needed to make consistent test snapshots across OSs */
|
/* needed to make consistent test snapshots across OSs */
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: Arial !important;
|
font-family: Roboto !important;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useState, ChangeEvent, FormEvent } from 'react';
|
||||||
|
import { Button, Grid, TextField, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
interface FormInputProps {
|
||||||
|
submitNewSheet: (sheetName: string) => {
|
||||||
|
name: string;
|
||||||
|
index: number;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormInput = ({ submitNewSheet }: FormInputProps) => {
|
||||||
|
const [newSheetName, setNewSheetName] = useState('');
|
||||||
|
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setNewSheetName(event.target.value);
|
||||||
|
|
||||||
|
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (newSheetName.length === 0) return;
|
||||||
|
submitNewSheet(newSheetName);
|
||||||
|
setNewSheetName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={10}>
|
||||||
|
<TextField
|
||||||
|
id="sheet-name"
|
||||||
|
label="New Sheet Name"
|
||||||
|
variant="outlined"
|
||||||
|
value={newSheetName}
|
||||||
|
onChange={handleChange}
|
||||||
|
name="newSheetName"
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={2}
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Button variant="contained" type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Typography variant="caption" gutterBottom>
|
||||||
|
Enter the name for your new sheet.
|
||||||
|
</Typography>
|
||||||
|
<br />
|
||||||
|
<Typography variant="caption" gutterBottom sx={{ fontStyle: 'italic' }}>
|
||||||
|
This component is written in typescript!
|
||||||
|
</Typography>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormInput;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Button, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
import FormInput from './FormInput';
|
||||||
|
import SheetTable from './SheetTable';
|
||||||
|
|
||||||
|
// This is a wrapper for google.script.run that lets us use promises.
|
||||||
|
import { serverFunctions } from '../../utils/serverFunctions';
|
||||||
|
|
||||||
|
const SheetEditor = () => {
|
||||||
|
const [names, setNames] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
serverFunctions.getSheetsData().then(setNames).catch(alert);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deleteSheet = (sheetIndex) => {
|
||||||
|
serverFunctions.deleteSheet(sheetIndex).then(setNames).catch(alert);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActiveSheet = (sheetName) => {
|
||||||
|
serverFunctions.setActiveSheet(sheetName).then(setNames).catch(alert);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitNewSheet = async (newSheetName) => {
|
||||||
|
try {
|
||||||
|
const response = await serverFunctions.addSheet(newSheetName);
|
||||||
|
setNames(response);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
alert(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '3px', overflowX: 'hidden' }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
☀️ MUI demo! ☀️
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body1" gutterBottom sx={{ marginBottom: '30px' }}>
|
||||||
|
This is a sample app that uses the <code>mui</code> library to help us
|
||||||
|
build a simple React app. Enter a name for a new sheet, hit enter and
|
||||||
|
the new sheet will be created. Click the red button next to the sheet
|
||||||
|
name to delete it.
|
||||||
|
</Typography>
|
||||||
|
<FormInput submitNewSheet={submitNewSheet} />
|
||||||
|
|
||||||
|
{names.length > 0 && (
|
||||||
|
<SheetTable
|
||||||
|
rows={names.map((name) => {
|
||||||
|
return {
|
||||||
|
sheetName: name.name,
|
||||||
|
goToButton: (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
disabled={name?.isActive}
|
||||||
|
onClick={() => setActiveSheet(name.name)}
|
||||||
|
>
|
||||||
|
Go To Sheet
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
deleteButton: (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={() => deleteSheet(name.index)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SheetEditor;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
type sheetRow = {
|
||||||
|
sheetName: string;
|
||||||
|
goToButton: React.ReactNode;
|
||||||
|
deleteButton: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SheetTable({ rows }: { rows: Array<sheetRow> }) {
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table aria-label="simple table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Sheet Name</TableCell>
|
||||||
|
<TableCell align="center">Go To</TableCell>
|
||||||
|
<TableCell align="center">Delete</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{rows.map((row: sheetRow) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.sheetName}
|
||||||
|
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||||
|
>
|
||||||
|
<TableCell component="th" scope="row">
|
||||||
|
{row.sheetName}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">{row.goToButton}</TableCell>
|
||||||
|
<TableCell align="center">{row.deleteButton}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base target="_top" />
|
||||||
|
<!-- Add any external scripts and stylesheets here -->
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/@mui/material@5.11.11/umd/material-ui.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/gas-client@1.2.0/dist/index.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/@types/react@18.2.66/index.d.ts"
|
||||||
|
></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="index">
|
||||||
|
<script type="module" src="./index.jsx"></script>
|
||||||
|
<!-- bundled js and css will get inlined here during build-->
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import SheetEditor from './components/SheetEditor';
|
import SheetEditor from './components/SheetEditor';
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/* needed to make consistent test snapshots across OSs */
|
||||||
|
body {
|
||||||
|
font-family: Arial !important;
|
||||||
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState, ChangeEvent, FormEvent } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const FormInput = ({ submitNewSheet }) => {
|
const FormInput = ({
|
||||||
|
submitNewSheet,
|
||||||
|
}: {
|
||||||
|
submitNewSheet: (arg: string) => void;
|
||||||
|
}) => {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
const handleChange = (event) => setInputValue(event.target.value);
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setInputValue(event.target.value);
|
||||||
|
|
||||||
const handleSubmit = (event) => {
|
const handleSubmit = (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (inputValue.length === 0) return;
|
if (inputValue.length === 0) return;
|
||||||
|
|
||||||
@@ -18,16 +23,18 @@ const FormInput = ({ submitNewSheet }) => {
|
|||||||
<form className="flex w-full mx-auto items-center" onSubmit={handleSubmit}>
|
<form className="flex w-full mx-auto items-center" onSubmit={handleSubmit}>
|
||||||
<div className="grow pr-2 py-1">
|
<div className="grow pr-2 py-1">
|
||||||
<input
|
<input
|
||||||
className="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-indigo-500 focus:bg-transparent focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 transition-colors duration-200 ease-in-out"
|
className="w-full bg-gray-100 bg-opacity-50 rounded border border-gray-300 focus:border-indigo-500 focus:bg-transparent focus:ring-2 focus:ring-indigo-200 text-base outline-none text-gray-700 py-1 transition-colors duration-200 ease-in-out"
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
placeholder="New sheet name"
|
placeholder="New sheet name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="text-white bg-indigo-500 border-0 py-2 px-8 focus:outline-none hover:bg-indigo-600 rounded"
|
className="text-white bg-indigo-500 border-0 py-2 px-8 focus:outline-none hover:bg-indigo-600 rounded"
|
||||||
type="submit"
|
type="submit"
|
||||||
>Add Sheet</button>
|
>
|
||||||
|
Add Sheet
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const SheetButton = ({ sheetDetails, deleteSheet, setActiveSheet }) => {
|
const SheetButton = ({ sheetDetails, deleteSheet, setActiveSheet }) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||||
import FormInput from './FormInput';
|
import FormInput from './FormInput';
|
||||||
import SheetButton from './SheetButton';
|
import SheetButton from './SheetButton';
|
||||||
|
|||||||
@@ -3,6 +3,26 @@
|
|||||||
<head>
|
<head>
|
||||||
<base target="_top" />
|
<base target="_top" />
|
||||||
<!-- Add any external scripts and stylesheets here -->
|
<!-- Add any external scripts and stylesheets here -->
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-transition-group@4.4.2/dist/react-transition-group.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/gas-client@1.2.0/dist/index.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/@types/react@18.2.66/index.d.ts"
|
||||||
|
></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Nunito"
|
href="https://fonts.googleapis.com/css2?family=Nunito"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -10,6 +30,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="index">
|
<section id="index">
|
||||||
|
<script type="module" src="./index.jsx"></script>
|
||||||
<!-- bundled js and css will get inlined here during build -->
|
<!-- bundled js and css will get inlined here during build -->
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import SheetEditor from './components/SheetEditor';
|
import SheetEditor from './components/SheetEditor';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const FormInput = ({ submitNewSheet }) => {
|
const FormInput = ({ submitNewSheet }) => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const SheetButton = ({ sheetDetails, deleteSheet, setActiveSheet }) => {
|
const SheetButton = ({ sheetDetails, deleteSheet, setActiveSheet }) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||||
import FormInput from './FormInput';
|
import FormInput from './FormInput';
|
||||||
import SheetButton from './SheetButton';
|
import SheetButton from './SheetButton';
|
||||||
|
|||||||
@@ -3,6 +3,26 @@
|
|||||||
<head>
|
<head>
|
||||||
<base target="_top" />
|
<base target="_top" />
|
||||||
<!-- Add any external scripts and stylesheets here -->
|
<!-- Add any external scripts and stylesheets here -->
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-transition-group@4.4.2/dist/react-transition-group.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/gas-client@1.2.0/dist/index.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/@types/react@18.2.66/index.d.ts"
|
||||||
|
></script>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Nunito"
|
href="https://fonts.googleapis.com/css2?family=Nunito"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
@@ -10,6 +30,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="index">
|
<section id="index">
|
||||||
|
<script type="module" src="./index.jsx"></script>
|
||||||
<!-- bundled js and css will get inlined here during build -->
|
<!-- bundled js and css will get inlined here during build -->
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import SheetEditor from './components/SheetEditor';
|
|
||||||
|
|
||||||
import './styles.css';
|
|
||||||
|
|
||||||
ReactDOM.render(<SheetEditor />, document.getElementById('index'));
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import SheetEditor from './components/SheetEditor';
|
||||||
|
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
const container = document.getElementById('index');
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<SheetEditor />);
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const About = () => (
|
const About = () => (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -3,10 +3,18 @@
|
|||||||
<head>
|
<head>
|
||||||
<base target="_top" />
|
<base target="_top" />
|
||||||
<!-- Add any external scripts and stylesheets here -->
|
<!-- Add any external scripts and stylesheets here -->
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"
|
||||||
|
></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section id="index">
|
<section id="index">
|
||||||
<!-- bundled js and css will get inlined here during build -->
|
<script type="module" src="./index.jsx"></script>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import About from './components/About';
|
|
||||||
|
|
||||||
ReactDOM.render(<About />, document.getElementById('index'));
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import About from './components/About';
|
||||||
|
|
||||||
|
const container = document.getElementById('index');
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(<About />);
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { GASClient } from 'gas-client';
|
import { GASClient } from 'gas-client';
|
||||||
import * as publicServerFunctions from '../../server';
|
import * as publicServerFunctions from '../../server';
|
||||||
|
|
||||||
const { serverFunctions } = new GASClient<typeof publicServerFunctions>({
|
const { serverFunctions, scriptHostFunctions } = new GASClient<
|
||||||
|
typeof publicServerFunctions
|
||||||
|
>({
|
||||||
// this is necessary for local development but will be ignored in production
|
// this is necessary for local development but will be ignored in production
|
||||||
allowedDevelopmentDomains: (origin) =>
|
allowedDevelopmentDomains: (origin) =>
|
||||||
/https:\/\/.*\.googleusercontent\.com$/.test(origin),
|
/https:\/\/.*\.googleusercontent\.com$/.test(origin),
|
||||||
});
|
});
|
||||||
|
|
||||||
export { serverFunctions };
|
export { serverFunctions, scriptHostFunctions };
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"@babel/env",
|
|
||||||
{
|
|
||||||
"modules": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"transform-es3-property-literals",
|
|
||||||
"transform-es3-member-expression-literals",
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
"@babel/plugin-proposal-object-rest-spread",
|
|
||||||
"@babel/plugin-transform-object-assign",
|
|
||||||
"@babel/plugin-proposal-optional-chaining"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@babel/eslint-parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"extends": ["airbnb-base", "plugin:prettier/recommended"],
|
"extends": [
|
||||||
|
"airbnb-base",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
"plugins": ["prettier", "googleappsscript"],
|
"plugins": ["prettier", "googleappsscript"],
|
||||||
"env": {
|
"env": {
|
||||||
"googleappsscript/googleappsscript": true
|
"googleappsscript/googleappsscript": true
|
||||||
@@ -11,7 +16,15 @@
|
|||||||
"camelcase": "warn",
|
"camelcase": "warn",
|
||||||
"import/prefer-default-export": "warn",
|
"import/prefer-default-export": "warn",
|
||||||
"import/no-extraneous-dependencies": "warn",
|
"import/no-extraneous-dependencies": "warn",
|
||||||
"prefer-object-spread": "warn"
|
"prefer-object-spread": "warn",
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"ignorePackages",
|
||||||
|
{
|
||||||
|
"js": "never",
|
||||||
|
"ts": "never"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
onOpen,
|
onOpen,
|
||||||
openDialog,
|
openDialog,
|
||||||
openDialogBootstrap,
|
openDialogBootstrap,
|
||||||
|
openDialogMUI,
|
||||||
openDialogTailwindCSS,
|
openDialogTailwindCSS,
|
||||||
openAboutSidebar,
|
openAboutSidebar,
|
||||||
} from './ui';
|
} from './ui';
|
||||||
@@ -13,6 +14,7 @@ export {
|
|||||||
onOpen,
|
onOpen,
|
||||||
openDialog,
|
openDialog,
|
||||||
openDialogBootstrap,
|
openDialogBootstrap,
|
||||||
|
openDialogMUI,
|
||||||
openDialogTailwindCSS,
|
openDialogTailwindCSS,
|
||||||
openAboutSidebar,
|
openAboutSidebar,
|
||||||
getSheetsData,
|
getSheetsData,
|
||||||
|
|||||||
@@ -14,18 +14,18 @@ export const getSheetsData = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addSheet = (sheetTitle) => {
|
export const addSheet = (sheetTitle: string) => {
|
||||||
SpreadsheetApp.getActive().insertSheet(sheetTitle);
|
SpreadsheetApp.getActive().insertSheet(sheetTitle);
|
||||||
return getSheetsData();
|
return getSheetsData();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteSheet = (sheetIndex) => {
|
export const deleteSheet = (sheetIndex: number) => {
|
||||||
const sheets = getSheets();
|
const sheets = getSheets();
|
||||||
SpreadsheetApp.getActive().deleteSheet(sheets[sheetIndex]);
|
SpreadsheetApp.getActive().deleteSheet(sheets[sheetIndex]);
|
||||||
return getSheetsData();
|
return getSheetsData();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setActiveSheet = (sheetName) => {
|
export const setActiveSheet = (sheetName: string) => {
|
||||||
SpreadsheetApp.getActive().getSheetByName(sheetName).activate();
|
SpreadsheetApp.getActive().getSheetByName(sheetName).activate();
|
||||||
return getSheetsData();
|
return getSheetsData();
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,7 @@ export const onOpen = () => {
|
|||||||
.createMenu('My Sample React Project') // edit me!
|
.createMenu('My Sample React Project') // edit me!
|
||||||
.addItem('Sheet Editor', 'openDialog')
|
.addItem('Sheet Editor', 'openDialog')
|
||||||
.addItem('Sheet Editor (Bootstrap)', 'openDialogBootstrap')
|
.addItem('Sheet Editor (Bootstrap)', 'openDialogBootstrap')
|
||||||
|
.addItem('Sheet Editor (MUI)', 'openDialogMUI')
|
||||||
.addItem('Sheet Editor (Tailwind CSS)', 'openDialogTailwindCSS')
|
.addItem('Sheet Editor (Tailwind CSS)', 'openDialogTailwindCSS')
|
||||||
.addItem('About me', 'openAboutSidebar');
|
.addItem('About me', 'openAboutSidebar');
|
||||||
|
|
||||||
@@ -23,6 +24,13 @@ export const openDialogBootstrap = () => {
|
|||||||
SpreadsheetApp.getUi().showModalDialog(html, 'Sheet Editor (Bootstrap)');
|
SpreadsheetApp.getUi().showModalDialog(html, 'Sheet Editor (Bootstrap)');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openDialogMUI = () => {
|
||||||
|
const html = HtmlService.createHtmlOutputFromFile('dialog-demo-mui')
|
||||||
|
.setWidth(600)
|
||||||
|
.setHeight(600);
|
||||||
|
SpreadsheetApp.getUi().showModalDialog(html, 'Sheet Editor (MUI)');
|
||||||
|
};
|
||||||
|
|
||||||
export const openDialogTailwindCSS = () => {
|
export const openDialogTailwindCSS = () => {
|
||||||
const html = HtmlService.createHtmlOutputFromFile('dialog-demo-tailwindcss')
|
const html = HtmlService.createHtmlOutputFromFile('dialog-demo-tailwindcss')
|
||||||
.setWidth(600)
|
.setWidth(600)
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"plugins": [
|
"presets": ["@babel/preset-env"]
|
||||||
"@babel/plugin-proposal-object-rest-spread",
|
|
||||||
"@babel/plugin-proposal-class-properties"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"parser": "@babel/eslint-parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"extends": [
|
"extends": [
|
||||||
"airbnb-base",
|
"airbnb-base",
|
||||||
"plugin:prettier/recommended",
|
"plugin:prettier/recommended",
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:jest/recommended"
|
"plugin:jest/recommended"
|
||||||
],
|
],
|
||||||
"plugins": ["babel", "prettier"],
|
"plugins": ["prettier"],
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
@@ -27,8 +29,9 @@
|
|||||||
"jest/no-done-callback": "off",
|
"jest/no-done-callback": "off",
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"camelcase": "warn",
|
"camelcase": "warn",
|
||||||
"import/prefer-default-export": "warn",
|
"import/prefer-default-export": "off",
|
||||||
"import/no-extraneous-dependencies": "warn",
|
"import/no-extraneous-dependencies": "warn",
|
||||||
|
"import/extensions": "off",
|
||||||
"jest/expect-expect": "off",
|
"jest/expect-expect": "off",
|
||||||
"no-new": "off",
|
"no-new": "off",
|
||||||
"no-underscore-dangle": "off"
|
"no-underscore-dangle": "off"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@@ -2,18 +2,20 @@
|
|||||||
// jestjs.io/docs/puppeteer#custom-example-without-jest-puppeteer-preset
|
// jestjs.io/docs/puppeteer#custom-example-without-jest-puppeteer-preset
|
||||||
// This allows using stealth mode.
|
// This allows using stealth mode.
|
||||||
|
|
||||||
const { mkdir, writeFile } = require('fs').promises;
|
import fs from 'fs';
|
||||||
const os = require('os');
|
import os from 'os';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const puppeteer = require('puppeteer-extra');
|
import puppeteer from 'puppeteer-extra';
|
||||||
|
|
||||||
// add stealth plugin and use defaults (all evasion techniques)
|
// add stealth plugin and use defaults (all evasion techniques)
|
||||||
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||||
|
|
||||||
|
import jestPuppeteerConfig from './jest-puppeteer.config.js';
|
||||||
|
|
||||||
|
const fsPromises = fs.promises;
|
||||||
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
||||||
const jestPuppeteerConfig = require('./jest-puppeteer.config');
|
|
||||||
|
|
||||||
module.exports = async function globalSetup() {
|
export default async function globalSetup() {
|
||||||
puppeteer.use(StealthPlugin());
|
puppeteer.use(StealthPlugin());
|
||||||
const browser = await puppeteer.launch(jestPuppeteerConfig.launch);
|
const browser = await puppeteer.launch(jestPuppeteerConfig.launch);
|
||||||
// store the browser instance so we can teardown it later
|
// store the browser instance so we can teardown it later
|
||||||
@@ -21,6 +23,9 @@ module.exports = async function globalSetup() {
|
|||||||
global.__BROWSER_GLOBAL__ = browser;
|
global.__BROWSER_GLOBAL__ = browser;
|
||||||
|
|
||||||
// use the file system to expose the wsEndpoint for TestEnvironments
|
// use the file system to expose the wsEndpoint for TestEnvironments
|
||||||
await mkdir(DIR, { recursive: true });
|
await fsPromises.mkdir(DIR, { recursive: true });
|
||||||
await writeFile(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint());
|
await fsPromises.writeFile(
|
||||||
};
|
path.join(DIR, 'wsEndpoint'),
|
||||||
|
browser.wsEndpoint()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,15 +2,17 @@
|
|||||||
// jestjs.io/docs/puppeteer#custom-example-without-jest-puppeteer-preset
|
// jestjs.io/docs/puppeteer#custom-example-without-jest-puppeteer-preset
|
||||||
// This allows using stealth mode.
|
// This allows using stealth mode.
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
import fs from 'fs';
|
||||||
const os = require('os');
|
import os from 'os';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
|
|
||||||
|
const fsPromises = fs.promises;
|
||||||
|
|
||||||
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
||||||
module.exports = async function globalTeardown() {
|
export default async function globalTeardown() {
|
||||||
// close the browser instance
|
// close the browser instance
|
||||||
await global.__BROWSER_GLOBAL__.close();
|
await global.__BROWSER_GLOBAL__.close();
|
||||||
|
|
||||||
// clean-up the wsEndpoint file
|
// clean-up the wsEndpoint file
|
||||||
await fs.rmdir(DIR, { recursive: true, force: true });
|
await fsPromises.rmdir(DIR, { recursive: true, force: true });
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
module.exports = {
|
export default {
|
||||||
launch: {
|
launch: {
|
||||||
headless: false,
|
headless: false,
|
||||||
product: 'chrome',
|
product: 'chrome',
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const { exec } = require('child_process');
|
import { exec } from 'child_process';
|
||||||
const { configureToMatchImageSnapshot } = require('jest-image-snapshot');
|
import { configureToMatchImageSnapshot } from 'jest-image-snapshot';
|
||||||
const { openAddon } = require('./utils/open-addon');
|
import dotenv from 'dotenv';
|
||||||
|
import { openAddon } from './utils/open-addon';
|
||||||
|
|
||||||
require('dotenv').config();
|
dotenv.config();
|
||||||
|
|
||||||
const isExtended = `${process.env.IS_EXTENDED}` === 'true';
|
const isExtended = `${process.env.IS_EXTENDED}` === 'true';
|
||||||
|
|
||||||
@@ -25,11 +26,11 @@ const srcTestFile = path.join(
|
|||||||
'../src/client/dialog-demo-bootstrap/components/SheetEditor.jsx'
|
'../src/client/dialog-demo-bootstrap/components/SheetEditor.jsx'
|
||||||
);
|
);
|
||||||
|
|
||||||
const webpackDevServerReady = async (process) => {
|
const viteDevServerReady = async (process) => {
|
||||||
console.log('Waiting for Webpack Dev Server to finish loading...');
|
console.log('Waiting for vite to serve...');
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
process.stdout.on('data', (data) => {
|
process.stdout.on('data', (data) => {
|
||||||
if (data.includes('DEVELOPMENT: CLIENT - Dialog Demo Bootstrap')) {
|
if (data.includes('ready in')) {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -39,10 +40,10 @@ const webpackDevServerReady = async (process) => {
|
|||||||
describe(`Local setup ${isExtended ? '*extended*' : ''}`, () => {
|
describe(`Local setup ${isExtended ? '*extended*' : ''}`, () => {
|
||||||
let page;
|
let page;
|
||||||
let process;
|
let process;
|
||||||
const containerSelector = isExtended ? '.script-app-dialog' : 'body';
|
const containerSelector = isExtended ? 'div[role="dialog"]' : 'body';
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
process = exec('npm run serve');
|
process = exec('yarn dev');
|
||||||
page = await global.__BROWSER_GLOBAL__.newPage();
|
page = await global.__BROWSER_GLOBAL__.newPage();
|
||||||
|
|
||||||
await page.setViewport({
|
await page.setViewport({
|
||||||
@@ -51,17 +52,20 @@ describe(`Local setup ${isExtended ? '*extended*' : ''}`, () => {
|
|||||||
deviceScaleFactor: 1,
|
deviceScaleFactor: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
await webpackDevServerReady(process);
|
await viteDevServerReady(process);
|
||||||
|
|
||||||
if (isExtended) {
|
if (isExtended) {
|
||||||
await openAddon(page);
|
await openAddon(page);
|
||||||
} else {
|
} else {
|
||||||
await page.goto('https://localhost:3000/dialog-demo-bootstrap.html');
|
await page.goto(
|
||||||
|
'https://localhost:3000/dialog-demo-bootstrap/index.html'
|
||||||
|
);
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(3000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
console.log('Closing process.');
|
||||||
process.kill();
|
process.kill();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,15 @@
|
|||||||
// jestjs.io/docs/puppeteer#custom-example-without-jest-puppeteer-preset
|
// jestjs.io/docs/puppeteer#custom-example-without-jest-puppeteer-preset
|
||||||
// This allows using stealth mode.
|
// This allows using stealth mode.
|
||||||
|
|
||||||
const { readFile } = require('fs').promises;
|
import { readFile } from 'fs/promises';
|
||||||
const os = require('os');
|
import os from 'os';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const puppeteer = require('puppeteer');
|
import puppeteer from 'puppeteer';
|
||||||
const NodeEnvironment = require('jest-environment-node').default;
|
import NodeEnvironment from 'jest-environment-node';
|
||||||
|
|
||||||
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
||||||
|
|
||||||
class PuppeteerEnvironment extends NodeEnvironment {
|
export default class PuppeteerEnvironment extends NodeEnvironment.default {
|
||||||
constructor(config) {
|
|
||||||
super(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setup() {
|
async setup() {
|
||||||
await super.setup();
|
await super.setup();
|
||||||
// get the wsEndpoint
|
// get the wsEndpoint
|
||||||
@@ -37,5 +33,3 @@ class PuppeteerEnvironment extends NodeEnvironment {
|
|||||||
return super.getVmContext();
|
return super.getVmContext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PuppeteerEnvironment;
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference: https://github.com/americanexpress/jest-image-snapshot/blob/main/examples/image-reporter.js
|
||||||
|
*
|
||||||
|
* To enable this image reporter, add it to your `jest.config.js` "reporters" definition:
|
||||||
|
* "reporters": [ "default", "<rootDir>/image-reporter.js" ]
|
||||||
|
*
|
||||||
|
* Note: Image Reporter may not work with jest's --forceExit flag
|
||||||
|
*
|
||||||
|
* Note: If image reporter doesn't work in pipeline can try running as standalone script
|
||||||
|
* by creating a separate script file and running like this:
|
||||||
|
* "test:integration:extended:report": "cross-env IS_EXTENDED=true jest --forceExit test/local-development.test || node test/utils/image-reporter-standalone.js",
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import AWS from 'aws-sdk/global.js';
|
||||||
|
import S3 from 'aws-sdk/clients/s3.js'; // this is needed
|
||||||
|
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const UPLOAD_BUCKET = process.env.S3_BUCKET_NAME;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!process.env.S3_BUCKET_NAME ||
|
||||||
|
!process.env.AWS_SECRET_ACCESS_KEY ||
|
||||||
|
!process.env.AWS_ACCESS_KEY_ID
|
||||||
|
) {
|
||||||
|
console.log('Missing env variables. Skipping upload of image diff files.');
|
||||||
|
}
|
||||||
|
|
||||||
|
AWS.config.update({
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const s3 = new AWS.S3({ apiVersion: '2006-03-01' });
|
||||||
|
|
||||||
|
const targetDirectories = [
|
||||||
|
'./test/__image_snapshots__/',
|
||||||
|
'./test/__image_snapshots__/__diff_output__/',
|
||||||
|
];
|
||||||
|
targetDirectories.forEach((targetDirectory) => {
|
||||||
|
fs.readdirSync(targetDirectory, { withFileTypes: true }).forEach((dirent) => {
|
||||||
|
if (!dirent.isFile()) return;
|
||||||
|
const path = `images/${dirent.name}`;
|
||||||
|
const params = {
|
||||||
|
Body: fs.readFileSync(`${targetDirectory}/${dirent.name}`),
|
||||||
|
Bucket: UPLOAD_BUCKET,
|
||||||
|
Key: path,
|
||||||
|
ContentType: 'image/png',
|
||||||
|
};
|
||||||
|
s3.putObject(params, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err, err.stack);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Uploaded file to https://${UPLOAD_BUCKET}.s3.amazonaws.com/${path}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,16 +13,23 @@
|
|||||||
* "test:integration": "jest test/local-development.test || node test/utils/image-reporter-standalone.js"
|
* "test:integration": "jest test/local-development.test || node test/utils/image-reporter-standalone.js"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
import fs from 'fs';
|
||||||
const AWS = require('aws-sdk/global');
|
import AWS from 'aws-sdk/global.js';
|
||||||
const S3 = require('aws-sdk/clients/s3'); // this is needed
|
import S3 from 'aws-sdk/clients/s3.js'; // this is needed
|
||||||
require('dotenv').config();
|
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
const UPLOAD_BUCKET = process.env.S3_BUCKET_NAME;
|
const UPLOAD_BUCKET = process.env.S3_BUCKET_NAME;
|
||||||
|
|
||||||
|
AWS.config.update({
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
const s3 = new AWS.S3({ apiVersion: '2006-03-01' });
|
const s3 = new AWS.S3({ apiVersion: '2006-03-01' });
|
||||||
|
|
||||||
class ImageReporter {
|
export default class ImageReporter {
|
||||||
constructor(globalConfig, options) {
|
constructor(globalConfig, options) {
|
||||||
this._globalConfig = globalConfig;
|
this._globalConfig = globalConfig;
|
||||||
this._options = options;
|
this._options = options;
|
||||||
@@ -74,5 +81,3 @@ class ImageReporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ImageReporter;
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const openAddon = async (page) => {
|
export const openAddon = async (page) => {
|
||||||
await page.goto(process.env.SHEET_URL);
|
await page.goto(process.env.SHEET_URL);
|
||||||
|
|
||||||
|
await page.waitForTimeout(5000); // pause for 3 seconds
|
||||||
await page.click('a:nth-child(2)'); // click on signin button
|
await page.click('a:nth-child(2)'); // click on signin button
|
||||||
|
|
||||||
await page.waitForSelector('input[name="identifier"]', { visible: true });
|
await page.waitForSelector('input[name="identifier"]', { visible: true });
|
||||||
@@ -34,19 +35,39 @@ const openAddon = async (page) => {
|
|||||||
process.env.TEST_RECOVERY_EMAIL
|
process.env.TEST_RECOVERY_EMAIL
|
||||||
); // type recovery email
|
); // type recovery email
|
||||||
await page.waitForTimeout(6000);
|
await page.waitForTimeout(6000);
|
||||||
await page.click('button'); // click "next" button
|
await page.click('div[data-primary-action-label] button'); // click "next" button
|
||||||
await page.waitForTimeout(5000);
|
await page.waitForTimeout(5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
await page.evaluate(
|
||||||
|
() =>
|
||||||
|
document.querySelector('h1#headingText') &&
|
||||||
|
document
|
||||||
|
.querySelector('h1#headingText')
|
||||||
|
.innerText.includes('implify your sign')
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await page.click(
|
||||||
|
'div[data-secondary-action-label] > div > div:nth-child(2) button'
|
||||||
|
);
|
||||||
|
await page.waitForTimeout(6000);
|
||||||
|
} catch {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('The "Simplify your sign-in" page isn\'t shown');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
'div.menu-button.goog-control.goog-inline-block:nth-child(11)',
|
'div.menu-button.goog-control.goog-inline-block:nth-child(10)',
|
||||||
{ visible: true }
|
{ visible: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// open new addon menubar item
|
// open new addon menubar item
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const addOnMenuButton = document.querySelector(
|
const addOnMenuButton = document.querySelector(
|
||||||
'div.menu-button.goog-control.goog-inline-block:nth-child(11)'
|
'div.menu-button.goog-control.goog-inline-block:nth-child(10)'
|
||||||
);
|
);
|
||||||
addOnMenuButton.dispatchEvent(
|
addOnMenuButton.dispatchEvent(
|
||||||
new MouseEvent('mousedown', { bubbles: true })
|
new MouseEvent('mousedown', { bubbles: true })
|
||||||
@@ -77,12 +98,10 @@ const openAddon = async (page) => {
|
|||||||
new MouseEvent('mouseup', { bubbles: true })
|
new MouseEvent('mouseup', { bubbles: true })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
await page.waitForSelector('.script-app-dialog', {
|
await page.waitForSelector('div[role="dialog"]', {
|
||||||
visible: true,
|
visible: true,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.waitForTimeout(3000);
|
await page.waitForTimeout(15000);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { openAddon };
|
|
||||||
|
|||||||
@@ -1,7 +1,27 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "ES2019",
|
||||||
"types": ["gas-types-detailed"],
|
"types": ["gas-types-detailed"],
|
||||||
"jsx": "react",
|
"useDefineForClassFields": true,
|
||||||
"esModuleInterop": true
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
}
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.vite.json" }]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { BuildOptions, ServerOptions, build, defineConfig } from 'vite';
|
||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
||||||
|
import { writeFile } from 'fs/promises';
|
||||||
|
|
||||||
|
const PORT = 3000;
|
||||||
|
const clientRoot = './src/client';
|
||||||
|
const outDir = './dist';
|
||||||
|
const serverEntry = 'src/server/index.ts';
|
||||||
|
const copyAppscriptEntry = './appsscript.json';
|
||||||
|
const devServerWrapper = './dev/dev-server-wrapper.html';
|
||||||
|
|
||||||
|
const clientEntrypoints = [
|
||||||
|
{
|
||||||
|
name: 'CLIENT - Dialog Demo',
|
||||||
|
filename: 'dialog-demo', // we'll add the .html suffix to these
|
||||||
|
template: 'dialog-demo/index.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CLIENT - Dialog Demo Bootstrap',
|
||||||
|
filename: 'dialog-demo-bootstrap',
|
||||||
|
template: 'dialog-demo-bootstrap/index.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CLIENT - Dialog Demo MUI',
|
||||||
|
filename: 'dialog-demo-mui',
|
||||||
|
template: 'dialog-demo-mui/index.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CLIENT - Dialog Demo Tailwind CSS',
|
||||||
|
filename: 'dialog-demo-tailwindcss',
|
||||||
|
template: 'dialog-demo-tailwindcss/index.html',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CLIENT - Sidebar About Page',
|
||||||
|
filename: 'sidebar-about-page',
|
||||||
|
template: 'sidebar-about-page/index.html',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const keyPath = resolve(__dirname, './certs/key.pem');
|
||||||
|
const certPath = resolve(__dirname, './certs/cert.pem');
|
||||||
|
const pfxPath = resolve(__dirname, './certs/cert.pfx'); // if needed for Windows
|
||||||
|
|
||||||
|
const devServerOptions: ServerOptions = {
|
||||||
|
port: PORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// use key and cert settings only if they are found
|
||||||
|
if (existsSync(keyPath) && existsSync(certPath)) {
|
||||||
|
devServerOptions.https = {
|
||||||
|
key: readFileSync(resolve(__dirname, './certs/key.pem')),
|
||||||
|
cert: readFileSync(resolve(__dirname, './certs/cert.pem')),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If mkcert -install cannot be used on Windows machines (in pipeline, for example), the
|
||||||
|
// script at scripts/generate-cert.ps1 can be used to create a .pfx cert
|
||||||
|
if (existsSync(pfxPath)) {
|
||||||
|
// use pfx file if it's found
|
||||||
|
devServerOptions.https = {
|
||||||
|
pfx: readFileSync(pfxPath),
|
||||||
|
passphrase: 'abc123',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientServeConfig = () =>
|
||||||
|
defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: devServerOptions,
|
||||||
|
root: clientRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientBuildConfig = ({
|
||||||
|
clientEntrypointRoot,
|
||||||
|
template,
|
||||||
|
}: {
|
||||||
|
clientEntrypointRoot: string;
|
||||||
|
template: string;
|
||||||
|
}) =>
|
||||||
|
defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile({ useRecommendedBuildConfig: true })],
|
||||||
|
root: resolve(__dirname, clientRoot, clientEntrypointRoot),
|
||||||
|
build: {
|
||||||
|
sourcemap: false,
|
||||||
|
write: false, // don't write to disk
|
||||||
|
outDir,
|
||||||
|
emptyOutDir: true,
|
||||||
|
minify: true,
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
'react',
|
||||||
|
'react-dom',
|
||||||
|
'react-transition-group',
|
||||||
|
'react-bootstrap',
|
||||||
|
'@mui/material',
|
||||||
|
'@emotion/react',
|
||||||
|
'@emotion/styled',
|
||||||
|
'gas-client',
|
||||||
|
'@types/react',
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
format: 'iife', // needed to use globals from UMD builds
|
||||||
|
dir: outDir,
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react-dom': 'ReactDOM',
|
||||||
|
'react-transition-group': 'ReactTransitionGroup',
|
||||||
|
'react-bootstrap': 'ReactBootstrap',
|
||||||
|
'@mui/material': 'MaterialUI',
|
||||||
|
'@emotion/react': 'emotionReact',
|
||||||
|
'@emotion/styled': 'emotionStyled',
|
||||||
|
'gas-client': 'GASClient',
|
||||||
|
'@types/react': '@types/react',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
input: resolve(__dirname, clientRoot, template),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverBuildConfig: BuildOptions = {
|
||||||
|
emptyOutDir: true,
|
||||||
|
minify: false, // needed to work with footer
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, serverEntry),
|
||||||
|
fileName: 'code',
|
||||||
|
name: 'globalThis',
|
||||||
|
formats: ['iife'],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'code.js',
|
||||||
|
extend: true,
|
||||||
|
footer: (chunk) =>
|
||||||
|
chunk.exports
|
||||||
|
.map((exportedFunction) => `function ${exportedFunction}() {};`)
|
||||||
|
.join('\n'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildConfig = ({ mode }: { mode: string }) => {
|
||||||
|
const targets = [{ src: copyAppscriptEntry, dest: './' }];
|
||||||
|
if (mode === 'development') {
|
||||||
|
targets.push(
|
||||||
|
...clientEntrypoints.map((entrypoint) => ({
|
||||||
|
src: devServerWrapper,
|
||||||
|
dest: './',
|
||||||
|
rename: `${entrypoint.filename}.html`,
|
||||||
|
transform: (contents: string) =>
|
||||||
|
contents
|
||||||
|
.toString()
|
||||||
|
.replace(/__PORT__/g, String(PORT))
|
||||||
|
.replace(/__FILE_NAME__/g, entrypoint.template),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return defineConfig({
|
||||||
|
plugins: [
|
||||||
|
viteStaticCopy({
|
||||||
|
targets,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* This builds the client react app bundles for production, and writes them to disk.
|
||||||
|
* Because multiple client entrypoints (dialogs) are built, we need to loop through
|
||||||
|
* each entrypoint and build the client bundle for each. Vite doesn't have great tooling for
|
||||||
|
* building multiple single-page apps in one project, so we have to do this manually with a
|
||||||
|
* post-build closeBundle hook (https://rollupjs.org/guide/en/#closebundle).
|
||||||
|
*/
|
||||||
|
mode === 'production' && {
|
||||||
|
name: 'build-client-production-bundles',
|
||||||
|
closeBundle: async () => {
|
||||||
|
console.log('Building client production bundles...');
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const clientEntrypoint of clientEntrypoints) {
|
||||||
|
console.log('Building client bundle for', clientEntrypoint.name);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const buildOutput = await build(
|
||||||
|
clientBuildConfig({
|
||||||
|
clientEntrypointRoot: clientEntrypoint.filename,
|
||||||
|
template: clientEntrypoint.template,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await writeFile(
|
||||||
|
resolve(__dirname, outDir, `${clientEntrypoint.filename}.html`),
|
||||||
|
// @ts-expect-error - output is an array of RollupOutput
|
||||||
|
buildOutput.output[0].source
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log('Finished building client bundles!');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].filter(Boolean),
|
||||||
|
build: serverBuildConfig,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default async ({ command, mode }: { command: string; mode: string }) => {
|
||||||
|
if (command === 'serve') {
|
||||||
|
// for 'serve' mode, we only want to serve the client bundle locally
|
||||||
|
return clientServeConfig();
|
||||||
|
}
|
||||||
|
if (command === 'build') {
|
||||||
|
// for 'build' mode, we have two paths: build assets for local development, and build for production
|
||||||
|
return buildConfig({ mode });
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
/*********************************
|
|
||||||
* import webpack plugins
|
|
||||||
********************************/
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
const GasPlugin = require('gas-webpack-plugin');
|
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const HtmlWebpackInlineSourcePlugin = require('@effortlessmotion/html-webpack-inline-source-plugin');
|
|
||||||
const DynamicCdnWebpackPlugin = require('@effortlessmotion/dynamic-cdn-webpack-plugin');
|
|
||||||
const moduleToCdn = require('module-to-cdn');
|
|
||||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
|
||||||
|
|
||||||
/*********************************
|
|
||||||
* set up environment variables
|
|
||||||
********************************/
|
|
||||||
const dotenv = require('dotenv').config();
|
|
||||||
|
|
||||||
const parsed = dotenv.error ? {} : dotenv.parsed;
|
|
||||||
const envVars = parsed || {};
|
|
||||||
const PORT = envVars.PORT || 3000;
|
|
||||||
envVars.NODE_ENV = process.env.NODE_ENV;
|
|
||||||
envVars.PORT = PORT;
|
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
|
||||||
const publicPath = process.env.ASSET_PATH || '/';
|
|
||||||
|
|
||||||
/*********************************
|
|
||||||
* define entrypoints
|
|
||||||
********************************/
|
|
||||||
// our destination directory
|
|
||||||
const destination = path.resolve(__dirname, 'dist');
|
|
||||||
|
|
||||||
// define server paths
|
|
||||||
const serverEntry = './src/server/index.ts';
|
|
||||||
|
|
||||||
// define appsscript.json file path
|
|
||||||
const copyAppscriptEntry = './appsscript.json';
|
|
||||||
|
|
||||||
// define live development dialog paths
|
|
||||||
const devDialogEntry = './dev/index.js';
|
|
||||||
|
|
||||||
// define client entry points and output names
|
|
||||||
const clientEntrypoints = [
|
|
||||||
{
|
|
||||||
name: 'CLIENT - Dialog Demo',
|
|
||||||
entry: './src/client/dialog-demo/index.js',
|
|
||||||
filename: 'dialog-demo', // we'll add the .html suffix to these
|
|
||||||
template: './src/client/dialog-demo/index.html',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CLIENT - Dialog Demo Bootstrap',
|
|
||||||
entry: './src/client/dialog-demo-bootstrap/index.js',
|
|
||||||
filename: 'dialog-demo-bootstrap',
|
|
||||||
template: './src/client/dialog-demo-bootstrap/index.html',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CLIENT - Dialog Demo Tailwind CSS',
|
|
||||||
entry: './src/client/dialog-demo-tailwindcss/index.js',
|
|
||||||
filename: 'dialog-demo-tailwindcss',
|
|
||||||
template: './src/client/dialog-demo-tailwindcss/index.html',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CLIENT - Sidebar About Page',
|
|
||||||
entry: './src/client/sidebar-about-page/index.js',
|
|
||||||
filename: 'sidebar-about-page',
|
|
||||||
template: './src/client/sidebar-about-page/index.html',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// define certificate locations
|
|
||||||
// see "npm run setup:https" script in package.json
|
|
||||||
const keyPath = path.resolve(__dirname, './certs/key.pem');
|
|
||||||
const certPath = path.resolve(__dirname, './certs/cert.pem');
|
|
||||||
const pfxPath = path.resolve(__dirname, './certs/cert.pfx'); // if needed for Windows
|
|
||||||
|
|
||||||
/*********************************
|
|
||||||
* Declare settings
|
|
||||||
********************************/
|
|
||||||
|
|
||||||
// webpack settings for copying files to the destination folder
|
|
||||||
const copyFilesConfig = {
|
|
||||||
name: 'COPY FILES - appsscript.json',
|
|
||||||
mode: 'production', // unnecessary for this config, but removes console warning
|
|
||||||
entry: copyAppscriptEntry,
|
|
||||||
output: {
|
|
||||||
path: destination,
|
|
||||||
publicPath,
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new CopyWebpackPlugin({
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
from: copyAppscriptEntry,
|
|
||||||
to: destination,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// webpack settings used by both client and server
|
|
||||||
const sharedClientAndServerConfig = {
|
|
||||||
context: __dirname,
|
|
||||||
};
|
|
||||||
|
|
||||||
// webpack settings used by all client entrypoints
|
|
||||||
const clientConfig = ({ isDevClientWrapper }) => ({
|
|
||||||
...sharedClientAndServerConfig,
|
|
||||||
mode: isProd ? 'production' : 'development',
|
|
||||||
output: {
|
|
||||||
path: destination,
|
|
||||||
// this file will get added to the html template inline
|
|
||||||
// and should be put in .claspignore so it is not pushed
|
|
||||||
filename: 'main.js',
|
|
||||||
publicPath,
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.m?js/,
|
|
||||||
resolve: {
|
|
||||||
fullySpecified: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// typescript config
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'babel-loader',
|
|
||||||
// only enable react-refresh for dev builds, and not when building the dev client "wrapper"
|
|
||||||
options: {
|
|
||||||
plugins: [
|
|
||||||
!isProd &&
|
|
||||||
!isDevClientWrapper &&
|
|
||||||
require.resolve('react-refresh/babel'),
|
|
||||||
].filter(Boolean),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'ts-loader',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
// only enable react-refresh for dev builds, and not when building the dev client "wrapper"
|
|
||||||
options: {
|
|
||||||
plugins: [
|
|
||||||
!isProd &&
|
|
||||||
!isDevClientWrapper &&
|
|
||||||
require.resolve('react-refresh/babel'),
|
|
||||||
].filter(Boolean),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// we could add support for scss here
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: ['style-loader', 'css-loader', 'postcss-loader'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// DynamicCdnWebpackPlugin settings
|
|
||||||
// these settings help us load 'react', 'react-dom' and the packages defined below from a CDN
|
|
||||||
// see https://github.com/enuchi/React-Google-Apps-Script#adding-new-libraries-and-packages
|
|
||||||
const DynamicCdnWebpackPluginConfig = {
|
|
||||||
// set "verbose" to true to print console logs on CDN usage while webpack builds
|
|
||||||
verbose: false,
|
|
||||||
resolver: (packageName, packageVersion, options) => {
|
|
||||||
const packageSuffix = isProd ? '.min.js' : '.js';
|
|
||||||
const moduleDetails = moduleToCdn(packageName, packageVersion, options);
|
|
||||||
|
|
||||||
// don't externalize react during development due to issue with react-refresh
|
|
||||||
// https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/334
|
|
||||||
if (!isProd && packageName === 'react') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// define custom CDN configuration for new packages
|
|
||||||
// "name" should match the package being imported
|
|
||||||
// "var" is important to get right -- this should be the exposed global. Look up "webpack externals" for info.
|
|
||||||
switch (packageName) {
|
|
||||||
case 'react-transition-group':
|
|
||||||
return {
|
|
||||||
name: packageName,
|
|
||||||
var: 'ReactTransitionGroup',
|
|
||||||
version: packageVersion,
|
|
||||||
url: `https://unpkg.com/react-transition-group@${packageVersion}/dist/react-transition-group${packageSuffix}`,
|
|
||||||
};
|
|
||||||
case 'react-bootstrap':
|
|
||||||
return {
|
|
||||||
name: packageName,
|
|
||||||
var: 'ReactBootstrap',
|
|
||||||
version: packageVersion,
|
|
||||||
url: `https://unpkg.com/react-bootstrap@${packageVersion}/dist/react-bootstrap${packageSuffix}`,
|
|
||||||
};
|
|
||||||
// externalize gas-client to keep bundle size even smaller
|
|
||||||
case 'gas-client':
|
|
||||||
return {
|
|
||||||
name: packageName,
|
|
||||||
var: 'GASClient',
|
|
||||||
version: packageVersion,
|
|
||||||
url: `https://unpkg.com/gas-client@${packageVersion}/dist/index.js`,
|
|
||||||
};
|
|
||||||
// must include peer dependencies for any custom imports
|
|
||||||
case '@types/react':
|
|
||||||
return {
|
|
||||||
name: packageName,
|
|
||||||
var: '@types/react',
|
|
||||||
version: packageVersion,
|
|
||||||
url: `https://unpkg.com/@types/react@${packageVersion}/index.d.ts`,
|
|
||||||
};
|
|
||||||
// return defaults/null depending if Dynamic CDN plugin finds package
|
|
||||||
default:
|
|
||||||
return moduleDetails;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// webpack settings used by each client entrypoint defined at top
|
|
||||||
const clientConfigs = clientEntrypoints.map((clientEntrypoint) => {
|
|
||||||
const isDevClientWrapper = false;
|
|
||||||
return {
|
|
||||||
...clientConfig({ isDevClientWrapper }),
|
|
||||||
name: clientEntrypoint.name,
|
|
||||||
entry: clientEntrypoint.entry,
|
|
||||||
plugins: [
|
|
||||||
!isProd && new ReactRefreshWebpackPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': JSON.stringify(envVars),
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: clientEntrypoint.template,
|
|
||||||
filename: `${clientEntrypoint.filename}${isProd ? '' : '-impl'}.html`,
|
|
||||||
inlineSource: '^/.*(js|css)$', // embed all js and css inline, exclude packages from dynamic cdn insertion
|
|
||||||
scriptLoading: 'blocking',
|
|
||||||
inject: 'body',
|
|
||||||
}),
|
|
||||||
// add the generated js code to the html file inline
|
|
||||||
new HtmlWebpackInlineSourcePlugin(),
|
|
||||||
// this plugin allows us to add dynamically load packages from a CDN
|
|
||||||
new DynamicCdnWebpackPlugin(DynamicCdnWebpackPluginConfig),
|
|
||||||
].filter(Boolean),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// webpack settings for devServer https://webpack.js.org/configuration/dev-server/
|
|
||||||
const devServer = {
|
|
||||||
hot: true,
|
|
||||||
port: PORT,
|
|
||||||
server: 'https',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
||||||
// use key and cert settings only if they are found
|
|
||||||
devServer.server = {
|
|
||||||
type: 'https',
|
|
||||||
options: { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If mkcert -install cannot be used on Windows machines (in pipeline, for example), the
|
|
||||||
// script at test/generate-cert.ps1 can be used to create a .pfx cert
|
|
||||||
if (fs.existsSync(pfxPath)) {
|
|
||||||
// use pfx file if it's found
|
|
||||||
devServer.server = {
|
|
||||||
type: 'https',
|
|
||||||
options: { pfx: fs.readFileSync(pfxPath), passphrase: 'abc123' },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// webpack settings for the development client wrapper
|
|
||||||
const devClientConfigs = clientEntrypoints.map((clientEntrypoint) => {
|
|
||||||
envVars.FILENAME = clientEntrypoint.filename;
|
|
||||||
const isDevClientWrapper = true;
|
|
||||||
return {
|
|
||||||
...clientConfig({ isDevClientWrapper }),
|
|
||||||
name: `DEVELOPMENT: ${clientEntrypoint.name}`,
|
|
||||||
entry: devDialogEntry,
|
|
||||||
plugins: [
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': JSON.stringify(envVars),
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: './dev/index.html',
|
|
||||||
// this should match the html files we load in src/server/ui.js
|
|
||||||
filename: `${clientEntrypoint.filename}.html`,
|
|
||||||
inlineSource: '^/.*(js|css)$', // embed all js and css inline, exclude packages from dynamic cdn insertion
|
|
||||||
scriptLoading: 'blocking',
|
|
||||||
inject: 'body',
|
|
||||||
}),
|
|
||||||
new HtmlWebpackInlineSourcePlugin(),
|
|
||||||
new DynamicCdnWebpackPlugin({}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// webpack settings used by the server-side code
|
|
||||||
const serverConfig = {
|
|
||||||
...sharedClientAndServerConfig,
|
|
||||||
name: 'SERVER',
|
|
||||||
// server config can't use 'development' mode
|
|
||||||
// https://github.com/fossamagna/gas-webpack-plugin/issues/135
|
|
||||||
mode: isProd ? 'production' : 'none',
|
|
||||||
entry: serverEntry,
|
|
||||||
output: {
|
|
||||||
filename: 'code.js',
|
|
||||||
path: destination,
|
|
||||||
libraryTarget: 'this',
|
|
||||||
publicPath,
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.js', '.json'],
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
// typescript config
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'babel-loader',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'ts-loader',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
minimize: true,
|
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin({
|
|
||||||
terserOptions: {
|
|
||||||
// ecma 5 is needed to support Rhino "DEPRECATED_ES5" runtime
|
|
||||||
// can use ecma 6 if V8 runtime is used
|
|
||||||
ecma: 5,
|
|
||||||
warnings: false,
|
|
||||||
parse: {},
|
|
||||||
compress: {
|
|
||||||
properties: false,
|
|
||||||
},
|
|
||||||
mangle: false,
|
|
||||||
module: false,
|
|
||||||
output: {
|
|
||||||
beautify: true,
|
|
||||||
// support custom function autocompletion
|
|
||||||
// https://developers.google.com/apps-script/guides/sheets/functions
|
|
||||||
comments: /@customfunction/,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new GasPlugin({
|
|
||||||
// removes need for assigning public server functions to "global"
|
|
||||||
autoGlobalExportsFiles: [serverEntry],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
// 1. Copy appsscript.json to destination,
|
|
||||||
// 2. Set up webpack dev server during development
|
|
||||||
// Note: devServer settings are only read in the first element when module.exports is an array
|
|
||||||
{ ...copyFilesConfig, ...(isProd ? {} : { devServer }) },
|
|
||||||
// 3. Create the server bundle
|
|
||||||
serverConfig,
|
|
||||||
// 4. Create one client bundle for each client entrypoint.
|
|
||||||
...clientConfigs,
|
|
||||||
// 5. Create a development dialog bundle for each client entrypoint during development.
|
|
||||||
...(isProd ? [] : devClientConfigs),
|
|
||||||
];
|
|
||||||