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:
Elisha Nuchi
2020-08-15 13:09:04 -04:00
committed by GitHub
parent 5030a2f81e
commit 35a641d0f7
30 changed files with 5392 additions and 1742 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
},
"import/resolver": {
"node": {
"extensions": [".js", ".jsx"]
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
}
+31
View File
@@ -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">&times;</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 &times; 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>
);
+6 -28
View File
@@ -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;