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:
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"import/resolver": {
|
||||
"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 { 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.
|
||||
import server from '../../utils/server';
|
||||
|
||||
const { serverFunctions } = server;
|
||||
|
||||
const SheetEditor = () => {
|
||||
const [newSheetName, setNewSheetName] = useState('');
|
||||
const [names, setNames] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
server
|
||||
serverFunctions
|
||||
.getSheetsData()
|
||||
.then(setNames)
|
||||
.catch(alert);
|
||||
}, []);
|
||||
|
||||
const deleteSheet = sheetIndex => {
|
||||
server
|
||||
serverFunctions
|
||||
.deleteSheet(sheetIndex)
|
||||
.then(setNames)
|
||||
.catch(alert);
|
||||
};
|
||||
|
||||
const setActiveSheet = sheetName => {
|
||||
server
|
||||
serverFunctions
|
||||
.setActiveSheet(sheetName)
|
||||
.then(setNames)
|
||||
.catch(alert);
|
||||
};
|
||||
|
||||
const submitNewSheet = async () => {
|
||||
const submitNewSheet = async newSheetName => {
|
||||
try {
|
||||
const response = await server.addSheet(newSheetName);
|
||||
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' }}>
|
||||
<Form onSubmit={submitNewSheet}>
|
||||
<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={e => {
|
||||
setNewSheetName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<p>
|
||||
<b>☀️ Bootstrap demo! ☀️</b>
|
||||
</p>
|
||||
<p>
|
||||
This is a sample app that uses the <code>react-bootstrap</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{' '}
|
||||
<span className="text-danger">×</span> next to the sheet name to
|
||||
delete it.
|
||||
</p>
|
||||
<FormInput submitNewSheet={submitNewSheet} />
|
||||
<ListGroup>
|
||||
<TransitionGroup className="sheet-list">
|
||||
{names.length > 0 &&
|
||||
|
||||
@@ -6,26 +6,28 @@ import SheetButton from './SheetButton';
|
||||
// This is a wrapper for google.script.run that lets us use promises.
|
||||
import server from '../../utils/server';
|
||||
|
||||
const { serverFunctions } = server;
|
||||
|
||||
const SheetEditor = () => {
|
||||
const [names, setNames] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Call a server global function here and handle the response with .then() and .catch()
|
||||
server
|
||||
serverFunctions
|
||||
.getSheetsData()
|
||||
.then(setNames)
|
||||
.catch(alert);
|
||||
}, []);
|
||||
|
||||
const deleteSheet = sheetIndex => {
|
||||
server
|
||||
serverFunctions
|
||||
.deleteSheet(sheetIndex)
|
||||
.then(setNames)
|
||||
.catch(alert);
|
||||
};
|
||||
|
||||
const setActiveSheet = sheetName => {
|
||||
server
|
||||
serverFunctions
|
||||
.setActiveSheet(sheetName)
|
||||
.then(setNames)
|
||||
.catch(alert);
|
||||
@@ -35,15 +37,24 @@ const SheetEditor = () => {
|
||||
// (This does the same thing as .then().catch() in the above handlers.)
|
||||
const submitNewSheet = async newSheetName => {
|
||||
try {
|
||||
const response = await server.addSheet(newSheetName);
|
||||
const response = await serverFunctions.addSheet(newSheetName);
|
||||
setNames(response);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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} />
|
||||
<TransitionGroup className="sheet-list">
|
||||
{names.length > 0 &&
|
||||
|
||||
@@ -2,7 +2,17 @@ import React from 'react';
|
||||
|
||||
const About = () => (
|
||||
<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
|
||||
href="https://www.github.com/enuchi/React-Google-Apps-Script"
|
||||
target="_blank"
|
||||
@@ -10,7 +20,6 @@ const About = () => (
|
||||
>
|
||||
React + Google Apps Script
|
||||
</a>
|
||||
<div>-Elisha Nuchi</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 ignoredMethods = new Set([
|
||||
'withFailureHandler',
|
||||
'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);
|
||||
});
|
||||
const server = new Server({
|
||||
// this is necessary for local development but will be ignored in production
|
||||
allowedDevelopmentDomains: `https://localhost:${PORT}`,
|
||||
});
|
||||
|
||||
export default myServerFunctions;
|
||||
export default server;
|
||||
|
||||
@@ -12,5 +12,10 @@
|
||||
"import/prefer-default-export": "warn",
|
||||
"import/no-extraneous-dependencies": "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 = () => {
|
||||
SpreadsheetApp.getUi()
|
||||
const menu = SpreadsheetApp.getUi()
|
||||
.createMenu('My Sample React Project') // edit me!
|
||||
.addItem('Sheet Editor', 'openDialog')
|
||||
.addItem('Sheet Editor (Bootstrap)', 'openDialogBootstrap')
|
||||
.addItem('About me', 'openAboutSidebar')
|
||||
.addToUi();
|
||||
.addItem('About me', 'openAboutSidebar');
|
||||
|
||||
menu.addToUi();
|
||||
};
|
||||
|
||||
export const openDialog = () => {
|
||||
|
||||
Reference in New Issue
Block a user