v2 Enable local development (#52)
- Create local dev environment - Add gas-client and google-apps-script-webpack-dev-server packages for development - Add dev/ wrapper app - Update webpack config to support development environment - Redesigned READMEs - Support typescript - Update npm scripts - Organize and update .gitignore file - Add .vscode settings for clasp files - Remove tracking for files in dist/
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
main.js
|
main.js
|
||||||
|
|||||||
+12
-1
@@ -1,3 +1,14 @@
|
|||||||
{
|
{
|
||||||
"parser": "babel-eslint"
|
"root": true,
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"extends": ["airbnb-base", "plugin:prettier/recommended"],
|
||||||
|
"plugins": ["prettier"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"camelcase": "warn",
|
||||||
|
"import/prefer-default-export": "warn",
|
||||||
|
"import/no-extraneous-dependencies": "warn",
|
||||||
|
"prefer-object-spread": "warn",
|
||||||
|
"spaced-comment":"off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -1,3 +1,16 @@
|
|||||||
|
# npm
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# clasp files
|
||||||
.clasp*
|
.clasp*
|
||||||
creds.json
|
creds.json
|
||||||
|
|
||||||
|
# certs
|
||||||
|
*.pem
|
||||||
|
certs/
|
||||||
|
|
||||||
|
# build
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# mac
|
||||||
|
.DS_Store
|
||||||
|
|||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"json.schemas": [{
|
||||||
|
"fileMatch": [
|
||||||
|
"appsscript.json"
|
||||||
|
],
|
||||||
|
"url": "http://json.schemastore.org/appsscript"
|
||||||
|
}, {
|
||||||
|
"fileMatch": [
|
||||||
|
".clasp.json"
|
||||||
|
],
|
||||||
|
"url": "http://json.schemastore.org/clasp"
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -1,6 +1,30 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 Elisha Nuchi
|
Copyright (c) 2020 Elisha Nuchi
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
Sections adapted from github.com/labnol/apps-script-starter
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Amit Agarwal
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,189 +1,299 @@
|
|||||||
<img width="75" height="39" src="https://i.imgur.com/yg6skMo.png">
|
<p align="center">
|
||||||
|
<a href="" rel="noopener">
|
||||||
|
<img width="400" src="https://i.imgur.com/83Y7bWN.png" alt="React & Google Apps Script logos"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
# React & Google Apps Script
|
[]()
|
||||||
_Use this project as your boilerplate for React apps inside Google Sheets, Docs and Forms dialogs._
|
[](https://github.com/enuchi/React-Google-Apps-Script/issues)
|
||||||
|
[](https://github.com/enuchi/React-Google-Apps-Script/pulls)
|
||||||
|
[](/LICENSE)
|
||||||
|
|
||||||
This project uses labnol's excellent [apps-script-starter](https://github.com/labnol/apps-script-starter) as a starting point for developing server-side projects, and adds support for building React apps inside Google Sheets, Forms, and Docs dialogs. Simply clone this project and modify the source code to get started developing React apps with Google Apps Script.
|
</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.
|
||||||
_The included demo React app for Google Sheets shows insertion, deletion and selection of sheets through the dialog window._
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Installation
|
## 📝 Table of Contents
|
||||||
|
|
||||||
1. Clone the sample project and install dependencies:
|
- [About](#about)
|
||||||
```bash
|
- [Install](#install)
|
||||||
> git clone https://github.com/enuchi/React-Google-Apps-Script.git
|
- [Prerequisites](#prerequisites)
|
||||||
> cd React-Google-Apps-Script
|
- [Getting started](#getting-started)
|
||||||
> npm install
|
- [Deploy](#deploy)
|
||||||
```
|
- [[New!] Local Development](#local-development)
|
||||||
2. Enable the Google Apps Script API for your account by visiting [script.google.com/home/usersettings](https://script.google.com/home/usersettings):
|
- [Usage](#usage)
|
||||||
|
- [The included sample app](#the-included-sample-app)
|
||||||
|
- [[New!] Typescript](#new-typescript)
|
||||||
|
- [Adding packages](#adding-packages)
|
||||||
|
- [Styles](#styles)
|
||||||
|
- [Modifying scopes](#modifying-scopes)
|
||||||
|
- [Calling server-side Google Apps Script functions](#calling-server-side-google-apps-script-functions)
|
||||||
|
- [Autocomplete](#Autocomplete)
|
||||||
|
- [Authors](#authors)
|
||||||
|
- [Acknowledgments](#acknowledgement)
|
||||||
|
|
||||||
<img width="215" height="82" src="https://i.imgur.com/vuwkzMU.png">
|
<br/>
|
||||||
|
|
||||||
3. Log in to `clasp`:
|
## 🔎 About <a name = "about"></a>
|
||||||
```bash
|
|
||||||
> npm run login
|
|
||||||
```
|
|
||||||
This will use `clasp` to manage your login credentials. `clasp` is [Google's tool](https://github.com/google/clasp) that helps you develop and manage Apps Script projects locally.
|
|
||||||
4. Run the setup command to generate a new Google Sheets spreadsheet and bound Google Scripts project for your React project:
|
|
||||||
|
|
||||||
```bash
|
[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.
|
||||||
> npm run setup
|
|
||||||
|
|
||||||
Created new Google Sheet: https://drive.google.com/open?id=1lVQUPZ*************************************
|
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).
|
||||||
Created new Google Sheets Add-on script: https://script.google.com/d/1K7MPtCH*************************************-**/edit
|
|
||||||
```
|
|
||||||
|
|
||||||
This will use `clasp` to create the new files, and save the project's ID to the `.clasp.json` file in your root directory. If you don't want to create a new spreadsheet and script, and instead want to use this React project with an existing project, [see the section below](#Using-an-existing-Sheet).
|
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.
|
||||||
|
|
||||||
Okay, now you've created a new sheet and bound script project! (But they're still empty for now.)
|
<p align="center">
|
||||||
|
<img width="75%" src="https://i.imgur.com/BZvQ5ua.png" alt="React & Google Apps Script">
|
||||||
|
</p>
|
||||||
|
|
||||||
5. Deploy the app with the command below:
|
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.
|
||||||
```bash
|
|
||||||
> npm run deploy
|
|
||||||
```
|
|
||||||
This will build and push your code to your script project. Open the new spreadsheet in Google Sheets and your new React app will be available from the menu bar!
|
|
||||||
|
|
||||||
### Using an existing Sheet
|
See below how to get started!
|
||||||
|
|
||||||
If you want to use an existing google script for your project:
|
<br/>
|
||||||
|
|
||||||
|
## 🚜 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.
|
||||||
|
|
||||||
|
See [deploy](#deploy) for notes on how to deploy the project and see it live in a Google Spreadsheet.
|
||||||
|
|
||||||
|
### Prerequisites <a name = "prerequisites"></a>
|
||||||
|
|
||||||
|
- Make sure you're running at least [Node.js](https://nodejs.org/en/download/) v10 and `npm` v6.
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### 🏁 Getting started <a name = "getting-started"></a>
|
||||||
|
|
||||||
|
**1.** First, let's clone the repo and install the dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/enuchi/React-Google-Apps-Script.git
|
||||||
|
cd React-Google-Apps-Script
|
||||||
|
npm 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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm 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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm 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.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>See instructions here for using an existing project.</summary>
|
||||||
|
|
||||||
1. Copy your existing script project's `scriptId`. You can find it by opening your spreadsheet, selecting **Tools > Script Editor** from the menubar, then **File > Project properties**.
|
1. Copy your existing script project's `scriptId`. You can find it by opening your spreadsheet, selecting **Tools > Script Editor** from the menubar, then **File > Project properties**.
|
||||||
|
|
||||||
2. Run the command below using your project's `scriptId`:
|
2. Run the command below using your project's `scriptId`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If your scriptId is 1K7MPtCHkjasdf93238234asdKFDF3sa9 then run
|
npm run setup:use-id your_script_id_here
|
||||||
> npm run setup:use-id 1K7MPtCHkjasdf93238234asdKFDF3sa9
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will add the existing project's `scriptId` to your`.clasp.json` file. (You can also just edit the `.clasp.json` file directly. Make sure not to remove `"rootDir": "dist"` from the `.clasp.json` file.)
|
This command will add the existing project's `scriptId` to your`.clasp.json` file. See [here](https://github.com/google/clasp#setting) for working with `clasp`.
|
||||||
|
|
||||||
### Making changes to the code
|
<img width="100%" src="https://i.imgur.com/VYl3JHx.gif">
|
||||||
|
|
||||||
Modify the server-side and client-side source code in the `src` folder. [Add any additional scopes](https://developers.google.com/apps-script/concepts/scopes) to `appsscript.json` as needed. When you're ready, build the app and deploy! You can run `npm run deploy` to build and deploy, or `npm run build` just to build the bundled files in the `./dist` directory.
|
</details>
|
||||||
|
|
||||||
## The sample app
|
Next, let's deploy the app so we can see it live in Google Spreadsheets.
|
||||||
|
|
||||||
The included sample app allows inserting/activating/deleting sheets through a simple HTML dialog, built with React. 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/)). 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.
|
<br/>
|
||||||
|
|
||||||
## Features
|
## 🚀 Deploy <a name = "deploy"></a>
|
||||||
|
|
||||||
- Includes popular `eslint` and `prettier` configs for clean, standardized code.
|
Run the deploy command. You may be prompted to update your manifest file. Type 'yes'.
|
||||||
- Supports importing CSS from another file:
|
|
||||||
```js
|
|
||||||
import './styles.css';
|
|
||||||
```
|
|
||||||
- This project uses promises to call and handle responses from the server, instead of using `google.script.run`:
|
|
||||||
```js
|
|
||||||
// Google's documentation wants you to do this. Boo.
|
|
||||||
google.script.run
|
|
||||||
.withSuccessHandler(response => doSomething(response))
|
|
||||||
.withFailureHandler(err => handleError(err))
|
|
||||||
.addSheet(sheetTitle);
|
|
||||||
|
|
||||||
// Poof! With a little magic we can now do this:
|
```bash
|
||||||
import server from '../server';
|
npm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
// We now have access to all our server functions, which return promises!
|
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.
|
||||||
server.addSheet(sheetTitle)
|
|
||||||
.then(response => doSomething(response))
|
|
||||||
.catch(err => handleError(err));
|
|
||||||
|
|
||||||
// Or we can use async/await. This is the same thing as above:
|
Now open Google Sheets and navigate to your new spreadsheet (e.g. the file "My React Project"). Make sure to refresh the page if you already had it open. You will now see a new menu item appear containing your app!
|
||||||
async () => {
|
|
||||||
try {
|
<img width="100%" src="https://i.imgur.com/W7UkEpv.gif">
|
||||||
const response = await server.addSheet(sheetTitle);
|
|
||||||
doSomething(response);
|
<br/>
|
||||||
} catch (err) {
|
|
||||||
handleError(err)
|
## 🎈 [NEW!] 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.
|
||||||
|
|
||||||
|
<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.
|
||||||
|
|
||||||
|
1. Generating a certificate for local development <a name = "generatingcerts"></a>
|
||||||
|
|
||||||
|
Install the mkcert package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# mac:
|
||||||
|
$ brew install mkcert
|
||||||
|
|
||||||
|
# windows:
|
||||||
|
$ choco install mkcert
|
||||||
|
```
|
||||||
|
|
||||||
|
[More install options here.](https://github.com/FiloSottile/mkcert#installation)
|
||||||
|
|
||||||
|
Then run the mkcert install script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mkcert -install
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the certs in your repo:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run setup:https
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Now you're ready to start:
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
<img width="100%" src="https://i.imgur.com/EsnOEHP.gif">
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## ⛏️ Usage <a name = "Usage"></a>
|
||||||
|
|
||||||
|
### The included sample app
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
A third app just demonstrates how to load 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.
|
||||||
|
|
||||||
|
### [New!] Typescript
|
||||||
|
|
||||||
|
This project now supports typescript!
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
### Adding packages
|
||||||
|
|
||||||
|
You can add packages to your client-side React app.
|
||||||
|
|
||||||
|
For instance, install `react-transition-group` from npm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install 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#L129). If set up properly, this will add a script tag that will load packages from a CDN, reducing your bundle size.
|
||||||
|
|
||||||
|
### Styles
|
||||||
|
|
||||||
|
By default this project supports global CSS stylesheets. Make sure to import your stylesheet in your entrypoint file [index.js](./src/client/dialog-demo/index.js):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
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).
|
||||||
|
|
||||||
|
The webpack.config.js file can also be modified to support scss and other style libraries.
|
||||||
|
|
||||||
|
### 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).
|
||||||
|
|
||||||
|
See https://developers.google.com/apps-script/manifest for information on the `appsscript.json` structure.
|
||||||
|
|
||||||
|
### Calling server-side Google Apps Script functions
|
||||||
|
|
||||||
|
This project uses the [gas-client](https://github.com/enuchi/gas-client) package to more easily call server-side functions using promises.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Google's documentation wants you to do this. Boo.
|
||||||
|
google.script.run
|
||||||
|
.withSuccessHandler(response => doSomething(response))
|
||||||
|
.withFailureHandler(err => handleError(err))
|
||||||
|
.addSheet(sheetTitle);
|
||||||
|
|
||||||
|
// Poof! With a little magic we can now do this:
|
||||||
|
import Server from 'gas-client';
|
||||||
|
const { serverFunctions } = new Server();
|
||||||
|
|
||||||
|
// We now have access to all our server functions, which return promises!
|
||||||
|
serverFunctions
|
||||||
|
.addSheet(sheetTitle)
|
||||||
|
.then(response => doSomething(response))
|
||||||
|
.catch(err => handleError(err));
|
||||||
|
|
||||||
|
// Or we can equally use async/await style:
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const response = await serverFunctions.addSheet(sheetTitle);
|
||||||
|
doSomething(response);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
|
||||||
Now we can use familiar Promises in our client-side code and have easy access to all server functions. See [the code](./src/client/utils/server.js) for the implementation details.
|
|
||||||
|
|
||||||
- This project includes support for autocompletion and complete type definitions for all Google Apps Script methods.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- All available methods from the Google Apps Script API are shown with full definitions and links to the official documentation, plus information on argument and return type
|
|
||||||
|
|
||||||
|
|
||||||
## Extending this app
|
|
||||||
### Adding new libraries and packages
|
|
||||||
To add new client-side libraries for your React app:
|
|
||||||
1. Install from npm, e.g. `npm install react-transition-group`
|
|
||||||
2. Grap a CDN url and declare it in the [webpack file, here](./webpack.config.js#L129).
|
|
||||||
|
|
||||||
Longer explanation:
|
|
||||||
|
|
||||||
Google Apps Script requires all HTML to be in a single file that is loaded into the dialog. Therefore, webpack has been configured to inline all generated client JS code into a single HTML file (in this case [main.html](./dist/main.html) and [about.html](./dist/about.html)). Inlining all code into a single file can result in large output files, which take longer to load, and can also sometimes cause the Google Apps Script editor to crash when you open it. So to reduce bundle size this project takes advantage of [dynamic-cdn-webpack-plugin](https://github.com/mastilver/dynamic-cdn-webpack-plugin) to automatically load popular libraries found in your app, such as `react` and `react-dom`, from a CDN. It doesn't know about all libraries (only [these](https://github.com/mastilver/module-to-cdn/blob/master/modules.json)), so if you've installed new packages, especially large packages, you should add a CDN url to the [webpack file](./webpack.config.js#L129) to reduce bundle size. You will need to know your package's global variable. See the examples provided, and also see [here](https://webpack.js.org/configuration/externals/#externals) for more info on how this works.
|
|
||||||
|
|
||||||
### Expose all public functions
|
|
||||||
Make sure to expose all public functions, including `onOpen` and any functions you are calling from the client. Example below shows assignment to `global` object:
|
|
||||||
```js
|
|
||||||
const onOpen = () => {
|
|
||||||
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
|
|
||||||
.createMenu('Dialog')
|
|
||||||
.addItem('Add sheets', 'openDialog')
|
|
||||||
.addToUi();
|
|
||||||
}
|
|
||||||
|
|
||||||
global.onOpen = onOpen
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multiple dialogs
|
|
||||||
|
|
||||||
This project now supports multiple dialogs and sidebars. See the `server` code at [src/server/ui.js](./src/server/ui.js) for a 'main.html' dialog and an 'about.html' sidebar:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// ./src/server/ui.js
|
|
||||||
|
|
||||||
export const onOpen = () => {
|
|
||||||
SpreadsheetApp.getUi()
|
|
||||||
.createMenu('My Sample React Project') // edit me!
|
|
||||||
.addItem('Sheet Name Editor', 'openDialog')
|
|
||||||
.addItem('About me', 'openAboutSidebar')
|
|
||||||
.addToUi();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const openDialog = () => {
|
|
||||||
const html = HtmlService.createHtmlOutputFromFile('main')
|
|
||||||
.setWidth(400)
|
|
||||||
.setHeight(600);
|
|
||||||
SpreadsheetApp.getUi().showModalDialog(html, 'Sheet Editor');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const openAboutSidebar = () => {
|
|
||||||
const html = HtmlService.createHtmlOutputFromFile('about');
|
|
||||||
SpreadsheetApp.getUi().showSidebar(html);
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
And here is the configuration in webpack that creates multiple html files. You will need to edit this if you want to add more dialog html files:
|
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.
|
||||||
|
|
||||||
```js
|
### Autocomplete
|
||||||
// ./webpack.config.js
|
|
||||||
|
|
||||||
// Client entrypoints:
|
This project includes support for autocompletion and complete type definitions for Google Apps Script methods.
|
||||||
const clientEntrypoints = [
|
|
||||||
{
|
|
||||||
name: 'CLIENT - main dialog',
|
|
||||||
entry: './src/client/MainDialog.jsx',
|
|
||||||
filename: 'main.html',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CLIENT - about sidebar',
|
|
||||||
entry: './src/client/AboutDialog.jsx',
|
|
||||||
filename: 'about.html',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
## Suggestions
|

|
||||||
|
|
||||||
Pull requests welcome!
|
All available methods from the Google Apps Script API are shown with full definitions and links to the official documentation, plus information on argument, return type and sample code.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## ✍️ Authors <a name = "authors"></a>
|
||||||
|
|
||||||
|
- [@enuchi](https://github.com/enuchi) - Creator and maintainer
|
||||||
|
|
||||||
|
See the list of [contributors](https://github.com/enuchi/React-Google-Apps-Script/contributors) who participated in this project.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## 🎉 Acknowledgements <a name = "acknowledgement"></a>
|
||||||
|
|
||||||
|
Part of this project has been adapted from [apps-script-starter](https://github.com/labnol/apps-script-starter), a great starter project for server-side projects.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-proposal-object-rest-spread",
|
||||||
|
"@babel/plugin-proposal-class-properties"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 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,19 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import server from '../src/client/utils/server';
|
||||||
|
|
||||||
|
const { serverFunctions } = server;
|
||||||
|
|
||||||
|
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}
|
||||||
|
// The "/gas/" path here must match the path where the custom dev server is being loaded.
|
||||||
|
// See webpack.config.js "devServer" "before" settings.
|
||||||
|
// Filename extension "-impl" must match webpack.config.js clientConfigs filename settings for development.
|
||||||
|
src={`https://localhost:${PORT}/gas/${FILENAME}-impl.html`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ReactDOM.render(<DevServer />, document.getElementById('index'));
|
||||||
Vendored
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"timeZone": "America/New_York",
|
|
||||||
"dependencies": {},
|
|
||||||
"exceptionLogging": "STACKDRIVER",
|
|
||||||
"oauthScopes": [
|
|
||||||
"https://www.googleapis.com/auth/script.container.ui",
|
|
||||||
"https://www.googleapis.com/auth/spreadsheets"
|
|
||||||
],
|
|
||||||
"runtimeVersion": "V8"
|
|
||||||
}
|
|
||||||
Vendored
-137
@@ -1,137 +0,0 @@
|
|||||||
function onOpen() {
|
|
||||||
}
|
|
||||||
function openDialog() {
|
|
||||||
}
|
|
||||||
function openDialogBootstrap() {
|
|
||||||
}
|
|
||||||
function openAboutSidebar() {
|
|
||||||
}
|
|
||||||
function getSheetsData() {
|
|
||||||
}
|
|
||||||
function addSheet() {
|
|
||||||
}
|
|
||||||
function deleteSheet() {
|
|
||||||
}
|
|
||||||
function setActiveSheet() {
|
|
||||||
}!function(e, a) {
|
|
||||||
for (var i in a) e[i] = a[i];
|
|
||||||
}(this, function(modules) {
|
|
||||||
var installedModules = {};
|
|
||||||
function __webpack_require__(moduleId) {
|
|
||||||
if (installedModules[moduleId]) return installedModules[moduleId].exports;
|
|
||||||
var module = installedModules[moduleId] = {
|
|
||||||
i: moduleId,
|
|
||||||
l: !1,
|
|
||||||
exports: {}
|
|
||||||
};
|
|
||||||
return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__),
|
|
||||||
module.l = !0, module.exports;
|
|
||||||
}
|
|
||||||
return __webpack_require__.m = modules, __webpack_require__.c = installedModules,
|
|
||||||
__webpack_require__.d = function(exports, name, getter) {
|
|
||||||
__webpack_require__.o(exports, name) || Object.defineProperty(exports, name, {
|
|
||||||
enumerable: !0,
|
|
||||||
get: getter
|
|
||||||
});
|
|
||||||
}, __webpack_require__.r = function(exports) {
|
|
||||||
"undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(exports, Symbol.toStringTag, {
|
|
||||||
value: "Module"
|
|
||||||
}), Object.defineProperty(exports, "__esModule", {
|
|
||||||
value: !0
|
|
||||||
});
|
|
||||||
}, __webpack_require__.t = function(value, mode) {
|
|
||||||
if (1 & mode && (value = __webpack_require__(value)), 8 & mode) return value;
|
|
||||||
if (4 & mode && "object" == typeof value && value && value.__esModule) return value;
|
|
||||||
var ns = Object.create(null);
|
|
||||||
if (__webpack_require__.r(ns), Object.defineProperty(ns, "default", {
|
|
||||||
enumerable: !0,
|
|
||||||
value: value
|
|
||||||
}), 2 & mode && "string" != typeof value) for (var key in value) __webpack_require__.d(ns, key, function(key) {
|
|
||||||
return value[key];
|
|
||||||
}.bind(null, key));
|
|
||||||
return ns;
|
|
||||||
}, __webpack_require__.n = function(module) {
|
|
||||||
var getter = module && module.__esModule ? function() {
|
|
||||||
return module["default"];
|
|
||||||
} : function() {
|
|
||||||
return module;
|
|
||||||
};
|
|
||||||
return __webpack_require__.d(getter, "a", getter), getter;
|
|
||||||
}, __webpack_require__.o = function(object, property) {
|
|
||||||
return Object.prototype.hasOwnProperty.call(object, property);
|
|
||||||
}, __webpack_require__.p = "", __webpack_require__(__webpack_require__.s = 2);
|
|
||||||
}([ function(module, __webpack_exports__, __webpack_require__) {
|
|
||||||
"use strict";
|
|
||||||
__webpack_require__.d(__webpack_exports__, "a", (function() {
|
|
||||||
return onOpen;
|
|
||||||
})), __webpack_require__.d(__webpack_exports__, "c", (function() {
|
|
||||||
return openDialog;
|
|
||||||
})), __webpack_require__.d(__webpack_exports__, "d", (function() {
|
|
||||||
return openDialogBootstrap;
|
|
||||||
})), __webpack_require__.d(__webpack_exports__, "b", (function() {
|
|
||||||
return openAboutSidebar;
|
|
||||||
}));
|
|
||||||
var onOpen = function() {
|
|
||||||
SpreadsheetApp.getUi().createMenu("My Sample React Project").addItem("Sheet Editor", "openDialog").addItem("Sheet Editor (Bootstrap)", "openDialogBootstrap").addItem("About me", "openAboutSidebar").addToUi();
|
|
||||||
}, openDialog = function() {
|
|
||||||
var html = HtmlService.createHtmlOutputFromFile("dialog-demo").setWidth(600).setHeight(600);
|
|
||||||
SpreadsheetApp.getUi().showModalDialog(html, "Sheet Editor");
|
|
||||||
}, openDialogBootstrap = function() {
|
|
||||||
var html = HtmlService.createHtmlOutputFromFile("dialog-demo-bootstrap").setWidth(600).setHeight(600);
|
|
||||||
SpreadsheetApp.getUi().showModalDialog(html, "Sheet Editor (Bootstrap)");
|
|
||||||
}, openAboutSidebar = function() {
|
|
||||||
var html = HtmlService.createHtmlOutputFromFile("sidebar-about-page");
|
|
||||||
SpreadsheetApp.getUi().showSidebar(html);
|
|
||||||
};
|
|
||||||
}, function(module, __webpack_exports__, __webpack_require__) {
|
|
||||||
"use strict";
|
|
||||||
__webpack_require__.d(__webpack_exports__, "c", (function() {
|
|
||||||
return getSheetsData;
|
|
||||||
})), __webpack_require__.d(__webpack_exports__, "a", (function() {
|
|
||||||
return addSheet;
|
|
||||||
})), __webpack_require__.d(__webpack_exports__, "b", (function() {
|
|
||||||
return deleteSheet;
|
|
||||||
})), __webpack_require__.d(__webpack_exports__, "d", (function() {
|
|
||||||
return setActiveSheet;
|
|
||||||
}));
|
|
||||||
var getSheets = function() {
|
|
||||||
return SpreadsheetApp.getActive().getSheets();
|
|
||||||
}, getSheetsData = function() {
|
|
||||||
var activeSheetName = SpreadsheetApp.getActive().getSheetName();
|
|
||||||
return getSheets().map((function(sheet, index) {
|
|
||||||
var name = sheet.getName();
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
index: index,
|
|
||||||
isActive: name === activeSheetName
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
}, addSheet = function(sheetTitle) {
|
|
||||||
return SpreadsheetApp.getActive().insertSheet(sheetTitle), getSheetsData();
|
|
||||||
}, deleteSheet = function(sheetIndex) {
|
|
||||||
var sheets = getSheets();
|
|
||||||
return SpreadsheetApp.getActive().deleteSheet(sheets[sheetIndex]), getSheetsData();
|
|
||||||
}, setActiveSheet = function(sheetName) {
|
|
||||||
return SpreadsheetApp.getActive().getSheetByName(sheetName).activate(), getSheetsData();
|
|
||||||
};
|
|
||||||
}, function(module, __webpack_exports__, __webpack_require__) {
|
|
||||||
"use strict";
|
|
||||||
__webpack_require__.r(__webpack_exports__), function(global) {
|
|
||||||
var _ui__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0), _sheets__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1);
|
|
||||||
global.onOpen = _ui__WEBPACK_IMPORTED_MODULE_0__["a"], global.openDialog = _ui__WEBPACK_IMPORTED_MODULE_0__["c"],
|
|
||||||
global.openDialogBootstrap = _ui__WEBPACK_IMPORTED_MODULE_0__["d"], global.openAboutSidebar = _ui__WEBPACK_IMPORTED_MODULE_0__["b"],
|
|
||||||
global.getSheetsData = _sheets__WEBPACK_IMPORTED_MODULE_1__["c"], global.addSheet = _sheets__WEBPACK_IMPORTED_MODULE_1__["a"],
|
|
||||||
global.deleteSheet = _sheets__WEBPACK_IMPORTED_MODULE_1__["b"], global.setActiveSheet = _sheets__WEBPACK_IMPORTED_MODULE_1__["d"];
|
|
||||||
}.call(this, __webpack_require__(3));
|
|
||||||
}, function(module, exports) {
|
|
||||||
var g;
|
|
||||||
g = function() {
|
|
||||||
return this;
|
|
||||||
}();
|
|
||||||
try {
|
|
||||||
g = g || new Function("return this")();
|
|
||||||
} catch (e) {
|
|
||||||
"object" == typeof window && (g = window);
|
|
||||||
}
|
|
||||||
module.exports = g;
|
|
||||||
} ]));
|
|
||||||
Vendored
-18
File diff suppressed because one or more lines are too long
Vendored
-16
File diff suppressed because one or more lines are too long
Vendored
-12
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<base target="_top" />
|
|
||||||
<!-- Add any external scripts and stylesheets here -->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<section id="index">
|
|
||||||
<!-- bundled js and css will get inlined here during build -->
|
|
||||||
</section>
|
|
||||||
<script type="text/javascript" src="https://unpkg.com/react@16.13.0/umd/react.production.min.js"></script><script type="text/javascript" src="https://unpkg.com/react-dom@16.13.0/umd/react-dom.production.min.js"></script><script type="text/javascript">!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=2)}([function(e,t){e.exports=React},function(e,t){e.exports=ReactDOM},function(e,t,r){"use strict";r.r(t);var n=r(0),o=r.n(n),u=r(1);var l=()=>o.a.createElement("div",null,o.a.createElement("div",null,"Github repo:"),o.a.createElement("a",{href:"https://www.github.com/enuchi/React-Google-Apps-Script",target:"_blank",rel:"noopener noreferrer"},"React + Google Apps Script"),o.a.createElement("div",null,"-Elisha Nuchi"));r.n(u).a.render(o.a.createElement(l,null),document.getElementById("index"))}]);</script></body>
|
|
||||||
</html>
|
|
||||||
Generated
+4488
-1213
File diff suppressed because it is too large
Load Diff
+42
-33
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-google-apps-script",
|
"name": "react-google-apps-script",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"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",
|
||||||
@@ -10,9 +10,14 @@
|
|||||||
"test": "test",
|
"test": "test",
|
||||||
"login": "clasp login",
|
"login": "clasp login",
|
||||||
"setup": "rm -f .clasp.json && clasp create --type sheets --title 'My React Project' --rootDir ./dist",
|
"setup": "rm -f .clasp.json && clasp create --type sheets --title 'My React Project' --rootDir ./dist",
|
||||||
"setup:use-id": "clasp setting scriptId",
|
"setup:use-id": "clasp setting rootDir dist && clasp setting scriptId",
|
||||||
"build": "webpack --mode production",
|
"setup:https": "mkdir certs && mkcert -key-file ./certs/key.pem -cert-file ./certs/cert.pem localhost 127.0.0.1",
|
||||||
"deploy": "npm run build && npx clasp push"
|
"build:dev": "NODE_ENV=development webpack",
|
||||||
|
"build": "NODE_ENV=production webpack",
|
||||||
|
"deploy:dev": "rm -rf dist && npm run build:dev && npx clasp push",
|
||||||
|
"deploy": "rm -rf dist && npm run build && npx clasp push",
|
||||||
|
"serve": "webpack-dev-server",
|
||||||
|
"start": "npm run deploy:dev && npm run serve"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
@@ -28,56 +33,60 @@
|
|||||||
"npm": ">=6.0.0"
|
"npm": ">=6.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"gas-client": "^0.1.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.13.0",
|
"react": "^16.13.1",
|
||||||
"react-bootstrap": "^1.0.1",
|
"react-bootstrap": "^1.3.0",
|
||||||
"react-dom": "^16.13.0",
|
"react-dom": "^16.13.1",
|
||||||
"react-transition-group": "^4.4.1"
|
"react-transition-group": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.8.4",
|
"@babel/cli": "^7.10.5",
|
||||||
"@babel/core": "^7.8.7",
|
"@babel/core": "^7.11.1",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
|
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||||
"@babel/plugin-transform-object-assign": "^7.8.3",
|
"@babel/plugin-transform-object-assign": "^7.10.4",
|
||||||
"@babel/polyfill": "^7.8.7",
|
"@babel/polyfill": "^7.10.4",
|
||||||
"@babel/preset-env": "^7.8.7",
|
"@babel/preset-env": "^7.11.0",
|
||||||
"@babel/preset-react": "^7.8.3",
|
"@babel/preset-react": "^7.10.4",
|
||||||
"@google/clasp": "^2.3.0",
|
"@google/clasp": "^2.3.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.1.0",
|
||||||
"babel-plugin-add-module-exports": "^1.0.2",
|
"babel-plugin-add-module-exports": "^1.0.2",
|
||||||
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
|
||||||
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
"babel-plugin-transform-es3-property-literals": "^6.22.0",
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^3.6.0",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"dynamic-cdn-webpack-plugin": "^5.0.0",
|
"dynamic-cdn-webpack-plugin": "^5.0.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"eslint-config-airbnb-base": "^14.0.0",
|
"eslint-config-airbnb-base": "^14.2.0",
|
||||||
"eslint-config-prettier": "^6.10.0",
|
"eslint-config-prettier": "^6.11.0",
|
||||||
"eslint-config-standard": "^14.1.0",
|
"eslint-config-standard": "^14.1.1",
|
||||||
"eslint-loader": "^3.0.3",
|
"eslint-loader": "^3.0.4",
|
||||||
"eslint-plugin-babel": "^5.3.0",
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
"eslint-plugin-googleappsscript": "^1.0.3",
|
"eslint-plugin-googleappsscript": "^1.0.3",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
"eslint-plugin-import": "^2.22.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||||
"eslint-plugin-node": "^11.0.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-prettier": "^3.1.2",
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.19.0",
|
"eslint-plugin-react": "^7.20.5",
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
"eslint-plugin-standard": "^4.0.1",
|
||||||
"gas-lib": "^2.0.3",
|
"gas-lib": "^2.0.3",
|
||||||
"gas-types-detailed": "^1.0.0",
|
"gas-types-detailed": "^1.0.0",
|
||||||
"gas-webpack-plugin": "^1.0.2",
|
"gas-webpack-plugin": "^1.0.4",
|
||||||
|
"google-apps-script-webpack-dev-server": "^1.0.0",
|
||||||
"html-webpack-inline-source-plugin": "0.0.10",
|
"html-webpack-inline-source-plugin": "0.0.10",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"module-to-cdn": "^3.1.5",
|
"module-to-cdn": "^3.1.5",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"style-loader": "^1.1.3",
|
"style-loader": "^1.2.1",
|
||||||
"tern": "^0.24.3",
|
"tern": "^0.24.3",
|
||||||
"terser-webpack-plugin": "^2.3.5",
|
"terser-webpack-plugin": "^2.3.7",
|
||||||
"webpack": "^4.42.0",
|
"ts-loader": "^8.0.2",
|
||||||
"webpack-clean": "^1.2.3",
|
"webpack": "^4.44.1",
|
||||||
"webpack-cli": "^3.3.11"
|
"webpack-cli": "^3.3.12",
|
||||||
|
"webpack-dev-server": "^3.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"import/resolver": {
|
"import/resolver": {
|
||||||
"node": {
|
"node": {
|
||||||
"extensions": [".js", ".jsx"]
|
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Client (our React code)
|
||||||
|
|
||||||
|
This directory is where we store the source code for our client-side React apps.
|
||||||
|
|
||||||
|
We have multiple directories in here because our app creates menu items that open multiple dialog windows. Each dialog opens a separate app, so each directory here represents its own distinct React app. Our webpack configuration will generate a separate bundle for each React app.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Each React app will need:
|
||||||
|
- an entrypoint, usually a file named `index.js`, that loads the app
|
||||||
|
- an HTML file that acts as a template, in which the bundled React app is loaded
|
||||||
|
|
||||||
|
You'll need to declare the following in [webpack.config.js](../../webpack.config.js):
|
||||||
|
- **name**: just a name to print in the webpack console, e.g. 'CLIENT - Dialog Demo'
|
||||||
|
- **entry**: the path to the entry point for the app, e.g. './src/client/dialog-demo/index.js'
|
||||||
|
- **filename**: the name of the html file that is generated. The server code will reference this filename to load the app into a dialog window. E.g. 'dialog-demo'
|
||||||
|
- **template**: the path to the HTML template for the app, e.g. './src/client/dialog-demo/index.html'
|
||||||
|
|
||||||
|
|
||||||
|
### Adding or removing an entrypoint
|
||||||
|
Your app or use case may only require a single dialog or sidebar, or you may want to add more than are included in the sample app.
|
||||||
|
|
||||||
|
To edit the entrypoints, you will need to:
|
||||||
|
|
||||||
|
1. Create or remove the entrypoint directories in the client source code. For instance, you can remove `./src/client/sidebar-about-page` altogether, or copy it and modify the source code. See above [requirements](#requirements).
|
||||||
|
|
||||||
|
2. Modify the server-side code to load the correct menu items and expose the correct public functions:
|
||||||
|
- [ui file](../server/ui.js)
|
||||||
|
- [index file](../server/index.js)
|
||||||
|
|
||||||
|
3. Modify the `clientEntrypoints` config in the [webpack config file](../../webpack.config.js).
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState, ChangeEvent, FormEvent } from 'react';
|
||||||
|
import { Form, Button, Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
|
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}>
|
||||||
|
<Form.Group controlId="formNewSheet">
|
||||||
|
<Form.Label>Add a new sheet</Form.Label>
|
||||||
|
<Row>
|
||||||
|
<Col xs={10}>
|
||||||
|
<Form.Control
|
||||||
|
type="text"
|
||||||
|
placeholder="Sheet name"
|
||||||
|
value={newSheetName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={2}>
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Enter the name for your new sheet.
|
||||||
|
</Form.Text>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
<i>This component is written in typescript!</i>
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormInput;
|
||||||
@@ -1,71 +1,60 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||||
import { Form, Button, ListGroup, Col, Row } from 'react-bootstrap';
|
import { Button, ListGroup } from 'react-bootstrap';
|
||||||
|
import FormInput from './FormInput.tsx';
|
||||||
|
|
||||||
// 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 server from '../../utils/server';
|
import server from '../../utils/server';
|
||||||
|
|
||||||
|
const { serverFunctions } = server;
|
||||||
|
|
||||||
const SheetEditor = () => {
|
const SheetEditor = () => {
|
||||||
const [newSheetName, setNewSheetName] = useState('');
|
|
||||||
const [names, setNames] = useState([]);
|
const [names, setNames] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
server
|
serverFunctions
|
||||||
.getSheetsData()
|
.getSheetsData()
|
||||||
.then(setNames)
|
.then(setNames)
|
||||||
.catch(alert);
|
.catch(alert);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const deleteSheet = sheetIndex => {
|
const deleteSheet = sheetIndex => {
|
||||||
server
|
serverFunctions
|
||||||
.deleteSheet(sheetIndex)
|
.deleteSheet(sheetIndex)
|
||||||
.then(setNames)
|
.then(setNames)
|
||||||
.catch(alert);
|
.catch(alert);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setActiveSheet = sheetName => {
|
const setActiveSheet = sheetName => {
|
||||||
server
|
serverFunctions
|
||||||
.setActiveSheet(sheetName)
|
.setActiveSheet(sheetName)
|
||||||
.then(setNames)
|
.then(setNames)
|
||||||
.catch(alert);
|
.catch(alert);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitNewSheet = async () => {
|
const submitNewSheet = async newSheetName => {
|
||||||
try {
|
try {
|
||||||
const response = await server.addSheet(newSheetName);
|
const response = await serverFunctions.addSheet(newSheetName);
|
||||||
setNames(response);
|
setNames(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
alert(error);
|
alert(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '3px', overflowX: 'hidden' }}>
|
<div style={{ padding: '3px', overflowX: 'hidden' }}>
|
||||||
<Form onSubmit={submitNewSheet}>
|
<p>
|
||||||
<Form.Group controlId="formNewSheet">
|
<b>☀️ Bootstrap demo! ☀️</b>
|
||||||
<Form.Label>Add a new sheet</Form.Label>
|
</p>
|
||||||
<Row>
|
<p>
|
||||||
<Col xs={10}>
|
This is a sample app that uses the <code>react-bootstrap</code> library
|
||||||
<Form.Control
|
to help us build a simple React app. Enter a name for a new sheet, hit
|
||||||
type="text"
|
enter and the new sheet will be created. Click the red{' '}
|
||||||
placeholder="Sheet name"
|
<span className="text-danger">×</span> next to the sheet name to
|
||||||
value={newSheetName}
|
delete it.
|
||||||
onChange={e => {
|
</p>
|
||||||
setNewSheetName(e.target.value);
|
<FormInput submitNewSheet={submitNewSheet} />
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col xs={2}>
|
|
||||||
<Button variant="primary" type="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
Enter the name for your new sheet.
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
</Form>
|
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
<TransitionGroup className="sheet-list">
|
<TransitionGroup className="sheet-list">
|
||||||
{names.length > 0 &&
|
{names.length > 0 &&
|
||||||
|
|||||||
@@ -6,26 +6,28 @@ import SheetButton from './SheetButton';
|
|||||||
// 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 server from '../../utils/server';
|
import server from '../../utils/server';
|
||||||
|
|
||||||
|
const { serverFunctions } = server;
|
||||||
|
|
||||||
const SheetEditor = () => {
|
const SheetEditor = () => {
|
||||||
const [names, setNames] = useState([]);
|
const [names, setNames] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Call a server global function here and handle the response with .then() and .catch()
|
// Call a server global function here and handle the response with .then() and .catch()
|
||||||
server
|
serverFunctions
|
||||||
.getSheetsData()
|
.getSheetsData()
|
||||||
.then(setNames)
|
.then(setNames)
|
||||||
.catch(alert);
|
.catch(alert);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const deleteSheet = sheetIndex => {
|
const deleteSheet = sheetIndex => {
|
||||||
server
|
serverFunctions
|
||||||
.deleteSheet(sheetIndex)
|
.deleteSheet(sheetIndex)
|
||||||
.then(setNames)
|
.then(setNames)
|
||||||
.catch(alert);
|
.catch(alert);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setActiveSheet = sheetName => {
|
const setActiveSheet = sheetName => {
|
||||||
server
|
serverFunctions
|
||||||
.setActiveSheet(sheetName)
|
.setActiveSheet(sheetName)
|
||||||
.then(setNames)
|
.then(setNames)
|
||||||
.catch(alert);
|
.catch(alert);
|
||||||
@@ -35,15 +37,24 @@ const SheetEditor = () => {
|
|||||||
// (This does the same thing as .then().catch() in the above handlers.)
|
// (This does the same thing as .then().catch() in the above handlers.)
|
||||||
const submitNewSheet = async newSheetName => {
|
const submitNewSheet = async newSheetName => {
|
||||||
try {
|
try {
|
||||||
const response = await server.addSheet(newSheetName);
|
const response = await serverFunctions.addSheet(newSheetName);
|
||||||
setNames(response);
|
setNames(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
alert(error);
|
alert(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<p>
|
||||||
|
<b>☀️ React demo! ☀️</b>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is a sample page that demonstrates a simple React app. Enter a name
|
||||||
|
for a new sheet, hit enter and the new sheet will be created. Click the
|
||||||
|
red × next to the sheet name to delete it.
|
||||||
|
</p>
|
||||||
<FormInput submitNewSheet={submitNewSheet} />
|
<FormInput submitNewSheet={submitNewSheet} />
|
||||||
<TransitionGroup className="sheet-list">
|
<TransitionGroup className="sheet-list">
|
||||||
{names.length > 0 &&
|
{names.length > 0 &&
|
||||||
|
|||||||
@@ -2,7 +2,17 @@ import React from 'react';
|
|||||||
|
|
||||||
const About = () => (
|
const About = () => (
|
||||||
<div>
|
<div>
|
||||||
<div>Github repo:</div>
|
<p>
|
||||||
|
<b>☀️ React app inside a sidebar! ☀️</b>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This is a very simple page demonstrating how to build a React app inside a
|
||||||
|
sidebar.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Visit the Github repo for more information on how to use this project.
|
||||||
|
</p>
|
||||||
|
<p>- Elisha Nuchi</p>
|
||||||
<a
|
<a
|
||||||
href="https://www.github.com/enuchi/React-Google-Apps-Script"
|
href="https://www.github.com/enuchi/React-Google-Apps-Script"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -10,7 +20,6 @@ const About = () => (
|
|||||||
>
|
>
|
||||||
React + Google Apps Script
|
React + Google Apps Script
|
||||||
</a>
|
</a>
|
||||||
<div>-Elisha Nuchi</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,10 @@
|
|||||||
// Convert google script server calls to more familiar promise-based functions
|
import Server from 'gas-client';
|
||||||
|
|
||||||
const myServerFunctions = {};
|
const { PORT } = process.env;
|
||||||
|
|
||||||
// identify the reserved functions
|
const server = new Server({
|
||||||
const ignoredMethods = new Set([
|
// this is necessary for local development but will be ignored in production
|
||||||
'withFailureHandler',
|
allowedDevelopmentDomains: `https://localhost:${PORT}`,
|
||||||
'withLogger',
|
|
||||||
'withSuccessHandler',
|
|
||||||
'withUserObject',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// get all the public/global function names from the server
|
|
||||||
const serverFunctionNames = Object.keys(google.script.run);
|
|
||||||
|
|
||||||
// filter out the reserved names
|
|
||||||
const myServerFunctionNames = serverFunctionNames.filter(
|
|
||||||
serverFunction => !ignoredMethods.has(serverFunction)
|
|
||||||
);
|
|
||||||
|
|
||||||
// save each function to our new server object using promises
|
|
||||||
myServerFunctionNames.forEach(serverFunctionName => {
|
|
||||||
myServerFunctions[serverFunctionName] = (...args) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
google.script.run
|
|
||||||
.withSuccessHandler(resolve)
|
|
||||||
.withFailureHandler(reject)
|
|
||||||
[serverFunctionName](...args);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default myServerFunctions;
|
export default server;
|
||||||
|
|||||||
@@ -12,5 +12,10 @@
|
|||||||
"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/resolver": {
|
||||||
|
"node": {
|
||||||
|
"extensions": [".js", ".ts"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Server (our Google Apps Script code)
|
||||||
|
|
||||||
|
This directory is where we store the source code for our Google Apps Script code, which runs on Google's servers.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
The server code will need:
|
||||||
|
- an entrypoint, usually a file named `index.js`, that loads the app.
|
||||||
|
- the entry point will need to declare any public functions by attaching them to the "`global`" object, like this:
|
||||||
|
```javascript
|
||||||
|
global.onOpen = someFunction;
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [ui.js](./ui.js) file for how to open menu items and set up the development settings properly.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Server-side code here will be compiled using settings that are compatible with the V8 or Rhino runtime (https://developers.google.com/apps-script/guides/v8-runtime). Update the [appsscript.json](../../appsscript.json) file as needed to switch runtimes.
|
||||||
|
|
||||||
+4
-3
@@ -1,10 +1,11 @@
|
|||||||
export const onOpen = () => {
|
export const onOpen = () => {
|
||||||
SpreadsheetApp.getUi()
|
const menu = SpreadsheetApp.getUi()
|
||||||
.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('About me', 'openAboutSidebar')
|
.addItem('About me', 'openAboutSidebar');
|
||||||
.addToUi();
|
|
||||||
|
menu.addToUi();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openDialog = () => {
|
export const openDialog = () => {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["gas-types-detailed"],
|
||||||
|
"jsx": "react",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+209
-81
@@ -1,82 +1,127 @@
|
|||||||
/*********************************
|
/*********************************
|
||||||
* import webpack plugins
|
* import webpack plugins
|
||||||
********************************/
|
********************************/
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const webpack = require('webpack');
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
const GasPlugin = require('gas-webpack-plugin');
|
const GasPlugin = require('gas-webpack-plugin');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
const WebpackCleanPlugin = require('webpack-clean');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
|
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
|
||||||
const DynamicCdnWebpackPlugin = require('dynamic-cdn-webpack-plugin');
|
const DynamicCdnWebpackPlugin = require('dynamic-cdn-webpack-plugin');
|
||||||
const moduleToCdn = require('module-to-cdn');
|
const moduleToCdn = require('module-to-cdn');
|
||||||
|
|
||||||
/*********************************
|
/*********************************
|
||||||
* define file paths
|
* set up environment variables
|
||||||
********************************/
|
********************************/
|
||||||
const destination = 'dist';
|
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';
|
||||||
|
|
||||||
/*********************************
|
/*********************************
|
||||||
* client entry point paths
|
* define entrypoints
|
||||||
********************************/
|
********************************/
|
||||||
|
// our destination directory
|
||||||
|
const destination = path.resolve(__dirname, 'dist');
|
||||||
|
|
||||||
|
// define server paths
|
||||||
|
const serverEntry = './src/server/index.js';
|
||||||
|
|
||||||
|
// 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 = [
|
const clientEntrypoints = [
|
||||||
{
|
{
|
||||||
name: 'CLIENT - Dialog Demo',
|
name: 'CLIENT - Dialog Demo',
|
||||||
entry: './src/client/dialog-demo/index.js',
|
entry: './src/client/dialog-demo/index.js',
|
||||||
filename: 'dialog-demo.html',
|
filename: 'dialog-demo', // we'll add the .html suffix to these
|
||||||
template: './src/client/dialog-demo/index.html',
|
template: './src/client/dialog-demo/index.html',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'CLIENT - Dialog Demo Bootstrap',
|
name: 'CLIENT - Dialog Demo Bootstrap',
|
||||||
entry: './src/client/dialog-demo-bootstrap/index.js',
|
entry: './src/client/dialog-demo-bootstrap/index.js',
|
||||||
filename: 'dialog-demo-bootstrap.html',
|
filename: 'dialog-demo-bootstrap',
|
||||||
template: './src/client/dialog-demo-bootstrap/index.html',
|
template: './src/client/dialog-demo-bootstrap/index.html',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'CLIENT - Sidebar About Page',
|
name: 'CLIENT - Sidebar About Page',
|
||||||
entry: './src/client/sidebar-about-page/index.js',
|
entry: './src/client/sidebar-about-page/index.js',
|
||||||
filename: 'sidebar-about-page.html',
|
filename: 'sidebar-about-page',
|
||||||
template: './src/client/sidebar-about-page/index.html',
|
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');
|
||||||
|
|
||||||
/*********************************
|
/*********************************
|
||||||
* Declare settings
|
* Declare settings
|
||||||
********************************/
|
********************************/
|
||||||
|
|
||||||
// any shared client & server settings
|
// webpack settings for copying files to the destination folder
|
||||||
const sharedConfigSettings = {
|
const copyFilesConfig = {
|
||||||
performance: {
|
name: 'COPY FILES - appsscript.json',
|
||||||
hints: 'warning',
|
mode: 'production', // unnecessary for this config, but removes console warning
|
||||||
maxEntrypointSize: 512000,
|
entry: copyAppscriptEntry,
|
||||||
maxAssetSize: 512000,
|
output: {
|
||||||
|
path: destination,
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
|
||||||
// appscript copy settings, to copy this file over to the destination directory
|
|
||||||
const appsscriptConfig = {
|
|
||||||
name: 'COPY APPSSCRIPT.JSON',
|
|
||||||
entry: './appsscript.json',
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{
|
{
|
||||||
from: './appsscript.json',
|
from: copyAppscriptEntry,
|
||||||
|
to: destination,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// config shared for all client settings
|
// webpack settings used by both client and server
|
||||||
|
const sharedClientAndServerConfig = {
|
||||||
|
context: __dirname,
|
||||||
|
};
|
||||||
|
|
||||||
|
// webpack settings used by all client entrypoints
|
||||||
const clientConfig = {
|
const clientConfig = {
|
||||||
...sharedConfigSettings,
|
...sharedClientAndServerConfig,
|
||||||
|
mode: isProd ? 'production' : 'development',
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, destination),
|
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',
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.jsx', '.json'],
|
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
// typescript config
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.jsx?$/,
|
test: /\.jsx?$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
@@ -84,6 +129,7 @@ const clientConfig = {
|
|||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// we could add support for scss here
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: ['style-loader', 'css-loader'],
|
use: ['style-loader', 'css-loader'],
|
||||||
@@ -92,71 +138,145 @@ const clientConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// config for EACH client entrypoint
|
// 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);
|
||||||
|
if (moduleDetails) {
|
||||||
|
return moduleDetails;
|
||||||
|
}
|
||||||
|
// "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}`,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// webpack settings used by each client entrypoint defined at top
|
||||||
const clientConfigs = clientEntrypoints.map(clientEntrypoint => {
|
const clientConfigs = clientEntrypoints.map(clientEntrypoint => {
|
||||||
return {
|
return {
|
||||||
...clientConfig,
|
...clientConfig,
|
||||||
name: clientEntrypoint.name,
|
name: clientEntrypoint.name,
|
||||||
entry: clientEntrypoint.entry,
|
entry: clientEntrypoint.entry,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': JSON.stringify(envVars),
|
||||||
|
}),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: clientEntrypoint.template,
|
template: clientEntrypoint.template,
|
||||||
filename: clientEntrypoint.filename,
|
filename: `${clientEntrypoint.filename}${isProd ? '' : '-impl'}.html`,
|
||||||
inlineSource: '^[^(//)]+.(js|css)$', // embed all js and css inline, exclude packages with '//' for dynamic cdn insertion
|
inlineSource: '^[^(//)]+.(js|css)$', // embed all js and css inline, exclude packages with '//' for dynamic cdn insertion
|
||||||
}),
|
}),
|
||||||
|
// add the generated js code to the html file inline
|
||||||
new HtmlWebpackInlineSourcePlugin(),
|
new HtmlWebpackInlineSourcePlugin(),
|
||||||
new WebpackCleanPlugin(['main.js'], {
|
// this plugin allows us to add dynamically load packages from a CDN
|
||||||
basePath: path.join(__dirname, destination),
|
new DynamicCdnWebpackPlugin(DynamicCdnWebpackPluginConfig),
|
||||||
}),
|
|
||||||
// this plugin allows us to add dynamically load packages from cdn
|
|
||||||
new DynamicCdnWebpackPlugin({
|
|
||||||
// set "verbose" to true to print console logs on CDN usage while webpack builds
|
|
||||||
verbose: false,
|
|
||||||
resolver: (packageName, packageVersion, options) => {
|
|
||||||
const packageSuffix = options.env === 'production' ? '.min.js' : '.js'
|
|
||||||
const moduleDetails = moduleToCdn(
|
|
||||||
packageName,
|
|
||||||
packageVersion,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
if (moduleDetails) {
|
|
||||||
return moduleDetails;
|
|
||||||
}
|
|
||||||
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}`,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const gasWebpackDevServerPath = require.resolve(
|
||||||
|
'google-apps-script-webpack-dev-server'
|
||||||
|
);
|
||||||
|
|
||||||
|
// webpack settings for devServer https://webpack.js.org/configuration/dev-server/
|
||||||
|
const devServer = {
|
||||||
|
port: PORT,
|
||||||
|
https: true,
|
||||||
|
// run our own route to serve the package google-apps-script-webpack-dev-server
|
||||||
|
before: app => {
|
||||||
|
// this '/gas/' path needs to match the path loaded in the iframe in dev/index.js
|
||||||
|
app.get('/gas/*', (req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'text/html');
|
||||||
|
fs.createReadStream(gasWebpackDevServerPath).pipe(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||||||
|
// use key and cert settings only if they are found
|
||||||
|
devServer.https = {
|
||||||
|
key: fs.readFileSync(keyPath),
|
||||||
|
cert: fs.readFileSync(certPath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// webpack settings for the development client wrapper
|
||||||
|
const devClientConfigs = clientEntrypoints.map(clientEntrypoint => {
|
||||||
|
envVars.FILENAME = clientEntrypoint.filename;
|
||||||
|
return {
|
||||||
|
...clientConfig,
|
||||||
|
name: `DEVELOPMENT: ${clientEntrypoint.name}`,
|
||||||
|
entry: devDialogEntry,
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': JSON.stringify(envVars),
|
||||||
|
}),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: './dev/index.html',
|
||||||
|
// we name the development wrapper with a '-development' suffix
|
||||||
|
// 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 with '//' for dynamic cdn insertion
|
||||||
|
}),
|
||||||
|
new HtmlWebpackInlineSourcePlugin(),
|
||||||
|
new DynamicCdnWebpackPlugin({}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// webpack settings used by the server-side code
|
||||||
const serverConfig = {
|
const serverConfig = {
|
||||||
...sharedConfigSettings,
|
...sharedClientAndServerConfig,
|
||||||
name: 'SERVER',
|
name: 'SERVER',
|
||||||
entry: './src/server/index.js',
|
// server config can't use 'development' mode
|
||||||
|
// https://github.com/fossamagna/gas-webpack-plugin/issues/135
|
||||||
|
mode: isProd ? 'production' : 'none',
|
||||||
|
entry: serverEntry,
|
||||||
output: {
|
output: {
|
||||||
filename: 'code.js',
|
filename: 'code.js',
|
||||||
path: path.resolve(__dirname, destination),
|
path: destination,
|
||||||
libraryTarget: 'this',
|
libraryTarget: 'this',
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js', '.json'],
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
// typescript config
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
@@ -172,7 +292,9 @@ const serverConfig = {
|
|||||||
new TerserPlugin({
|
new TerserPlugin({
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
terserOptions: {
|
terserOptions: {
|
||||||
ecma: undefined,
|
// ecma 5 is needed to support Rhino "DEPRECATED_ES5" runtime
|
||||||
|
// can use ecma 6 if V8 runtime is used
|
||||||
|
ecma: 5,
|
||||||
warnings: false,
|
warnings: false,
|
||||||
parse: {},
|
parse: {},
|
||||||
compress: {
|
compress: {
|
||||||
@@ -183,24 +305,30 @@ const serverConfig = {
|
|||||||
output: {
|
output: {
|
||||||
beautify: true,
|
beautify: true,
|
||||||
},
|
},
|
||||||
toplevel: false,
|
|
||||||
nameCache: null,
|
|
||||||
ie8: true,
|
|
||||||
keep_classnames: undefined,
|
|
||||||
keep_fnames: false,
|
|
||||||
safari10: false,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [new GasPlugin()],
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': JSON.stringify(envVars),
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(
|
||||||
|
isProd ? 'production' : 'development'
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new GasPlugin(),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
// 1. Copy the appscript file.
|
// 1. Copy appsscript.json to destination,
|
||||||
appsscriptConfig,
|
// 2. Set up webpack dev server during development
|
||||||
// 2. One client bundle for each client entrypoint.
|
// Note: devServer settings are only read in the first element when module.exports is an array
|
||||||
...clientConfigs,
|
{ ...copyFilesConfig, ...(isProd ? {} : { devServer }) },
|
||||||
// 3. Bundle the server
|
// 3. Create the server bundle
|
||||||
serverConfig,
|
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),
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user