Merge pull request #33 from enuchi/update-clasp-create

update webpack, update packages, dev experience, readme
This commit is contained in:
Elisha Nuchi
2020-05-04 03:34:54 -04:00
committed by GitHub
30 changed files with 1985 additions and 2853 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
{
"rootDir": "dist",
"scriptId": "...paste scriptId here..."
"scriptId": "...add scriptId here..."
}
+3
View File
@@ -0,0 +1,3 @@
{
"parser": "babel-eslint"
}
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+1
View File
@@ -0,0 +1 @@
lts/*
View File
+109 -143
View File
@@ -1,164 +1,132 @@
<img width="75" height="39" src="https://i.imgur.com/yg6skMo.png">
## React + Google Apps Script
*Use this demo project as your boilerplate React app for HTML dialogs in Google Sheets, Docs and Forms.*
# React & Google Apps Script
_Use this project as your boilerplate for React apps inside Google Sheets, Docs and Forms dialogs._
This project uses labnol's excellent [apps-script-starter](https://github.com/labnol/apps-script-starter) as a starting point, adding support for React. It demonstrates how easy it is to build React apps that interact with Google Apps server-side scripts. Simply clone this project and modify the source code to get started developing with React for Google Apps Script client-side dialogs.
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.
![Google Apps Script / React development](https://i.imgur.com/0yYQoYj.jpg "Start a React project for Google Apps Script")
*The demo app for Google Sheets shows insertion/deletion/activation of sheets through React-built HTML dialog.*
![Google Apps Script / React development](https://i.imgur.com/z99uP89.png 'Start a React project for Google Apps Script')
_The included demo React app for Google Sheets shows insertion, deletion and selection of sheets through the dialog window._
&nbsp;
## Installation
1. Clone the sample project and install dependencies:
```
```bash
> git clone https://github.com/enuchi/React-Google-Apps-Script.git
> cd React-Google-Apps-Script
> npm install
```
2. Enable the Google Apps Script API [(script.google.com/home/usersettings)](https://script.google.com/home/usersettings):
![Enable Google Apps Script](https://i.imgur.com/PxuNbP3.png "enable the Google Apps Script API")
2. Enable the Google Apps Script API for your account by visiting[(script.google.com/home/usersettings)](https://script.google.com/home/usersettings):
3. Log in to `clasp` to manage your Google Apps Scripts from the command line:
```
<img width="215" height="82" src="https://i.imgur.com/vuwkzMU.png">
3. Log in to `clasp`:
```bash
> npm run login
```
4. Create a new Google Sheets file and a bound Google Scripts project for your React project:
```
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
> npm run setup
Created new Google Sheet: https://drive.google.com/open?id=1lVQUPZ*************************************
Created new Google Sheets Add-on script: https://script.google.com/d/1K7MPtCH*************************************-**/edit
```
You've created a new Sheet and attached Script file! (But they're still empty for now.) See the notes below if you want to use an existing Google Sheet and Script instead of creating a new one.
5. Build the app and deploy!
```
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).
Okay, now you've created a new sheet and bound script project! (But they're still empty for now.)
5. Deploy the app with the command below:
```bash
> npm run deploy
```
You can now visit your Google Sheet and see your new React app.
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
The above installation instructions create a new Google Sheets spreadsheet and 'bound' Apps Script, and saves the credentials to a `.clasp.json` file in the root directory. If you want to use an existing sheet's script file, then simply copy the scriptId into the `.clasp.json` file as below. You can find the script's scriptId by opening your sheet, selecting **Tools > Script Editor**, then **File > Project properties**.
Paste the `scriptId` into the `.clasp.json` file. *Make sure to include `"rootDir": "dist"` in this file:
```
// .clasp.json
{"rootDir": "dist", "scriptId":"...paste scriptId here..."}
If you want to use an existing google script for your project:
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`:
```bash
# If your scriptId is 1K7MPtCHkjasdf93238234asdKFDF3sa9 then run
> 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.)
### Making changes to the code
Modify the server-side and client-side source code in the `src` folder using ES6/7 and React. Change the scopes in `appsscript.json` if 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.
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.
## The sample app
Insert/activate/delete sheets through a simple HTML dialog, built with React. Access the dialog 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.
## How it works
"[Google Apps Script](https://en.wikipedia.org/wiki/Google_Apps_Script) is based on JavaScript 1.6 with some portions of 1.7 and 1.8 and provides subset of ECMAScript 5 API."
That means many JavaScript tools used today in modern web development will not work in the Google Apps Script environment, including `let`/`const` declarations, arrow functions, spread operator, etc.
This project circumvents those restrictions by transpiling newer code to older code that Google Apps Script understands using Babel, and also bundles separate files and modules using Webpack.
On the client-side, Google Apps Scripts has restrictions on the way HTML dialogs are used. While in normal web development you can simply reference a separate css file, e.g.
```
<link rel="stylesheet" href="styles.css">
```
in the Google Apps Script environment you need to use [HTML templates](https://developers.google.com/apps-script/guides/html/templates), which can be cumbersome to work with. With this project, all files are bundled together by inlining .css and .js files. Using a transpiler and bundling tool also allows us to use JSX syntax, and external libraries such as React.
## Features
- Support for JSX syntax:
```
return <div>Name: {person.firstName}</div>
```
- Support for external packages. Simply install with npm or from a file and `import`:
```
$ npm install react-addons-css-transition-group
```
```
// index.jsx
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
```
- Includes popular `eslint` and `prettier` configs for clean, standardized code.
- `import` CSS from another file:
```js
import './styles.css';
```
import "./styles.css";
```
- Use promises with e.g. `.then`/`.catch` instead of `google.script.run`:
```
// instead of this:
- 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(successHandler)
.withFailureHandler(failureHandler)
.getSheetsData()
.withSuccessHandler(response => doSomething(response))
.withFailureHandler(err => handleError(err))
.addSheet(sheetTitle);
// you can do this:
// Poof! With a little magic we can now do this:
import server from '../server';
// access all your server functions here:
const { getSheetsData } = server;
addSheet(newSheetTitle)
.then(successHandler)
.catch(failureHandler);
```
How does this work? We rewrite `google.script.run` to support Promises:
```
// ./src/client/server.js
const serverMethods = {};
// We now have access to all our server functions, which return promises!
const { addSheet } = server;
addSheet(sheetTitle)
.then(response => doSomething(response))
.catch(handleError(err));
// skip the reserved methods
const ignoredMethods = [
'withFailureHandler',
'withLogger',
'withSuccessHandler',
'withUserObject',
];
for (const method in google.script.run) {
if (!ignoredMethods.includes(method)) {
serverMethods[method] = (...args) => {
return new Promise((resolve, reject) => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)[method](...args);
});
};
// Or we can use async/await:
async () => {
try {
const response = await addSheet(sheetTitle);
doSomething(response);
} catch (err) {
handleError(err)
}
}
export default serverMethods;
```
Now we can use familiar Promises in our client-side code and have easy access to all server functions!
Now we can use familiar Promises in our client-side code and have easy access to all server functions. See [the code](./src/client/server.js) for the implementation details.
- Use newer ES6/7 code, including arrow functions, spread operators, `const`/`let`, and more:
```
const getSheetsData = () => {
let activeSheetName = getActiveSheetName();
return getSheets().map((sheet, index) => {
let sheetName = sheet.getName();
return {
text: sheetName,
sheetIndex: index,
isActive: sheetName === activeSheetName,
};
});
};
```
## Tern support
This project includes support for GAS definitions and autocomplete through a [Tern](http://ternjs.net/) plugin. Tern is a code-analysis engine for JavaScript, providing many useful tools for developing. See Tern's site for setup instructions for many popular code editors, such as Sublime, Vim and others.
- This project includes support for autocompletion and complete type definitions for all Google Apps Script methods.
Tern provides many indispensable tools for working with Google Apps Script, such as autocompletion on variables and properties, function argument hints and querying the type of an expression.
- Autocomplete example. Lists all available methods from the appropriate Google Apps Script API:
![tern support](https://i.imgur.com/s1OrQNr.png "autocomplete and intelligent type detection with Tern")
- Full definitions with links to official documentation, plus information on argument and return type:
![tern support](https://i.imgur.com/yg5VwAC.png "definitions with links to official documentation make developing with Google Apps Script")
![autocomplete support](https://i.imgur.com/W0Ks6Wj.gif "autocomplete")
- 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
- You can split up server-side code into multiple files and folders using `import` and `export` statements.
- 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:
```
### 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.
### 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')
@@ -168,57 +136,55 @@ const onOpen = () => {
global.onOpen = onOpen
```
- You may wish to remove automatic linting when running Webpack. You can do so by editing the Webpack config file and commenting out the eslintConfig line in client or server settings:
```
// webpack.config.js
const clientConfig = Object.assign({}, sharedConfigSettings, {
...
module: {
rules: [
// eslintConfig,
{
```
## Multiple dialogs
This project now supports multiple dialogs and sidebars. Here is an example of the `server` code for a 'main.html' dialog and an 'about.html' sidebar:
```
// ./src/server/sheets-utilities.js
const openDialog = () => {
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() // Or DocumentApp or FormApp.
.showModalDialog(html, 'Sheet Editor');
SpreadsheetApp.getUi().showModalDialog(html, 'Sheet Editor');
};
const openAboutSidebar = () => {
export const openAboutSidebar = () => {
const html = HtmlService.createHtmlOutputFromFile('about');
SpreadsheetApp
.getUi()
.showSidebar(html);
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:
```
```js
// ./webpack.config.js
// Client entrypoints:
const clientEntrypoints = [
{
name: "CLIENT - main dialog",
entry: "./src/client/main.jsx",
filename: "main.html"
name: 'CLIENT - main dialog',
entry: './src/client/MainDialog.jsx',
filename: 'main.html',
},
{
name: "CLIENT - about sidebar",
entry: "./src/client/about.jsx",
filename: "about.html"
name: 'CLIENT - about sidebar',
entry: './src/client/AboutDialog.jsx',
filename: 'about.html',
},
];
```
## Suggestions
Open a pull request!
Pull requests welcome!
+1 -1
View File
@@ -7,5 +7,5 @@
<section id="index">
<!-- bundled js and css will get inlined here -->
</section>
<script type="text/javascript" src="https://unpkg.com/react@16.12.0/umd/react.production.min.js"></script><script type="text/javascript" src="https://unpkg.com/react-dom@16.12.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);r.n(u).a.render(o.a.createElement(()=>o.a.createElement("div",{className:"sheetNameText"},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")),null),document.getElementById("index"))}]);</script></body>
<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 a=()=>o.a.createElement("div",{className:"sheetNameText"},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(a,null),document.getElementById("index"))}]);</script></body>
</html>
+28 -24
View File
@@ -57,33 +57,19 @@ function setActiveSheet() {
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 = 1);
}, __webpack_require__.p = "", __webpack_require__(__webpack_require__.s = 2);
}([ function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.d(__webpack_exports__, "d", (function() {
return onOpen;
})), __webpack_require__.d(__webpack_exports__, "f", (function() {
return openDialog;
})), __webpack_require__.d(__webpack_exports__, "e", (function() {
return openAboutSidebar;
})), __webpack_require__.d(__webpack_exports__, "c", (function() {
__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__, "g", (function() {
})), __webpack_require__.d(__webpack_exports__, "d", (function() {
return setActiveSheet;
}));
var onOpen = function() {
SpreadsheetApp.getUi().createMenu("Custom scripts").addItem("Edit sheets [sample React project]", "openDialog").addItem("About me", "openAboutSidebar").addToUi();
}, openDialog = function() {
var html = HtmlService.createHtmlOutputFromFile("main").setWidth(400).setHeight(600);
SpreadsheetApp.getUi().showModalDialog(html, "Sheet Editor");
}, openAboutSidebar = function() {
var html = HtmlService.createHtmlOutputFromFile("about");
SpreadsheetApp.getUi().showSidebar(html);
}, getSheets = function() {
var getSheets = function() {
return SpreadsheetApp.getActive().getSheets();
}, getSheetsData = function() {
var activeSheetName = SpreadsheetApp.getActive().getSheetName();
@@ -103,15 +89,33 @@ function setActiveSheet() {
}, setActiveSheet = function(sheetName) {
return SpreadsheetApp.getActive().getSheetByName(sheetName).activate(), getSheetsData();
};
}, 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__, "b", (function() {
return openAboutSidebar;
}));
var onOpen = function() {
SpreadsheetApp.getUi().createMenu("My Sample React Project").addItem("Sheet Name Editor", "openDialog").addItem("About me", "openAboutSidebar").addToUi();
}, openDialog = function() {
var html = HtmlService.createHtmlOutputFromFile("main").setWidth(400).setHeight(600);
SpreadsheetApp.getUi().showModalDialog(html, "Sheet Editor");
}, openAboutSidebar = function() {
var html = HtmlService.createHtmlOutputFromFile("about");
SpreadsheetApp.getUi().showSidebar(html);
};
}, function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__), function(global) {
var _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
global.onOpen = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__["d"], global.openDialog = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__["f"],
global.openAboutSidebar = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__["e"], global.getSheetsData = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__["c"],
global.addSheet = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__["a"], global.deleteSheet = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__["b"],
global.setActiveSheet = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__["g"];
}.call(this, __webpack_require__(2));
var _ui__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1), _sheets__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(0);
global.onOpen = _ui__WEBPACK_IMPORTED_MODULE_0__["a"], global.openDialog = _ui__WEBPACK_IMPORTED_MODULE_0__["c"],
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() {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1531 -2418
View File
File diff suppressed because it is too large Load Diff
+30 -28
View File
@@ -5,7 +5,8 @@
"scripts": {
"test": "test",
"login": "clasp login",
"setup": "clasp create --type sheets --title 'My React Project' --rootDir ./dist",
"setup": "rm .clasp.json && clasp create --type sheets --title 'My React Project' --rootDir ./dist",
"setup:use-id": "clasp setting scriptId",
"start": "webpack --mode development",
"build": "webpack --mode production",
"deploy": "npm run build && npx clasp push"
@@ -19,59 +20,60 @@
],
"author": "Elisha Nuchi",
"license": "MIT",
"engines": {
"node": ">=10.0.0",
"npm": ">=6.0.0"
},
"dependencies": {
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-addons-css-transition-group": "^15.6.2",
"react-dom": "^16.12.0",
"react-transition-group": "^4.3.0"
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-transition-group": "^1.2.1"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.4",
"@babel/core": "^7.8.7",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-transform-object-assign": "^7.8.3",
"@babel/polyfill": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/polyfill": "^7.8.7",
"@babel/preset-env": "^7.8.7",
"@babel/preset-react": "^7.8.3",
"@google/clasp": "^2.3.0",
"babel-eslint": "^10.0.3",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.0.6",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-add-module-exports": "^1.0.2",
"babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
"babel-plugin-transform-es3-property-literals": "^6.22.0",
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^1.0.1",
"css-loader": "^3.4.2",
"dynamic-cdn-webpack-plugin": "^5.0.0",
"eslint": "^5.16.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^2.10.0",
"eslint-config-standard": "^11.0.0",
"eslint-loader": "^2.1.2",
"eslint-config-prettier": "^6.10.0",
"eslint-config-standard": "^14.1.0",
"eslint-loader": "^3.0.3",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-googleappsscript": "^1.0.3",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-prettier": "^2.7.0",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-react": "^7.18.3",
"eslint-plugin-standard": "^3.1.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-standard": "^4.0.1",
"gas-lib": "^2.0.3",
"gas-types-detailed": "^1.0.0",
"gas-webpack-plugin": "^0.3.0",
"gas-webpack-plugin": "^1.0.2",
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-plugin": "^3.2.0",
"module-to-cdn": "^3.1.5",
"prettier": "^1.19.1",
"style-loader": "^0.22.1",
"style-loader": "^1.1.3",
"tern": "^0.24.3",
"tern-googleappsscript": "^1.0.2",
"terser-webpack-plugin": "^2.3.4",
"webpack": "^4.41.5",
"terser-webpack-plugin": "^2.3.5",
"webpack": "^4.42.0",
"webpack-clean": "^1.2.3",
"webpack-cli": "^3.3.10"
"webpack-cli": "^3.3.11"
}
}
+29 -7
View File
@@ -1,8 +1,12 @@
{
"root": true,
"parser": "babel-eslint",
"extends": ["airbnb-base", "plugin:react/recommended", "eslint:recommended"],
"plugins": ["babel", "react"],
"extends": [
"airbnb-base",
"plugin:prettier/recommended",
"plugin:react/recommended"
],
"plugins": ["babel", "react", "prettier"],
"env": {
"browser": true,
"es6": true
@@ -12,11 +16,29 @@
"alert": false,
"css": true
},
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"import/prefer-default-export": 0,
"no-console": 0,
"require-jsdoc": 0,
"no-invalid-this": 0,
"babel/no-invalid-this": 0
"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"]
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import About from './components/About';
ReactDOM.render(<About />, document.getElementById('index'));
+7
View File
@@ -0,0 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import SheetEditor from './components/SheetEditor';
import './styles.css';
ReactDOM.render(<SheetEditor />, document.getElementById('index'));
-8
View File
@@ -1,8 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {AboutPage} from './components/about-sidebar';
ReactDOM.render(
<AboutPage />,
document.getElementById('index')
);
@@ -1,6 +1,6 @@
import React from 'react';
export const AboutPage = () => (
const About = () => (
<div className="sheetNameText">
<div>Github repo:</div>
<a
@@ -13,3 +13,5 @@ export const AboutPage = () => (
<div>-Elisha Nuchi</div>
</div>
);
export default About;
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
export default function FormInput(props) {
const FormInput = ({ newSheetFormHandler }) => {
const [text, setText] = useState('');
const handleChange = event => setText(event.target.value);
@@ -10,7 +10,7 @@ export default function FormInput(props) {
event.preventDefault();
if (text.length === 0) return;
props.newSheetFormHandler(event, text);
newSheetFormHandler(event, text);
setText('');
};
@@ -22,7 +22,9 @@ export default function FormInput(props) {
</form>
</div>
);
}
};
export default FormInput;
FormInput.propTypes = {
newSheetFormHandler: PropTypes.func,
@@ -1,9 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
const SheetButton = props => {
const { name, deleteButtonHandler, clickSheetNameHandler } = props;
const SheetButton = ({ name, deleteButtonHandler, clickSheetNameHandler }) => {
const { sheetIndex, text, isActive } = name;
return (
@@ -11,7 +9,7 @@ const SheetButton = props => {
<button onClick={e => deleteButtonHandler(e, sheetIndex)}>X</button>
<span
onClick={e => clickSheetNameHandler(e, text)}
className={'sheetNameText ' + (isActive ? 'active-sheet' : '')}
className={`sheetNameText ${isActive ? 'active-sheet' : ''}`}
>
{text}
</span>
@@ -22,7 +20,11 @@ const SheetButton = props => {
export default SheetButton;
SheetButton.propTypes = {
name: PropTypes.object,
name: PropTypes.shape({
sheetIndex: PropTypes.number,
text: PropTypes.string,
isActive: PropTypes.bool,
}),
deleteButtonHandler: PropTypes.func,
clickSheetNameHandler: PropTypes.func,
};
+67
View File
@@ -0,0 +1,67 @@
/* eslint-disable no-alert */
import React, { useState, useEffect } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import FormInput from './FormInput';
import SheetButton from './SheetButton';
import server from '../server';
const SheetEditor = () => {
const { getSheetsData, addSheet, deleteSheet, setActiveSheet } = server;
const [names, setNames] = useState([]);
useEffect(() => {
getSheetsData()
.then(setNames)
.catch(alert);
}, []);
const deleteButtonHandler = (e, sheetIndex) => {
deleteSheet(sheetIndex)
.then(setNames)
.catch(alert);
};
const clickSheetNameHandler = (e, sheetName) => {
setActiveSheet(sheetName)
.then(setNames)
.catch(alert);
};
// just for fun, let's use async/await for this one
const newSheetFormHandler = async (e, newSheetTitle) => {
try {
const response = await addSheet(newSheetTitle);
setNames(response);
} catch (error) {
alert(error);
}
};
return (
<div>
<FormInput newSheetFormHandler={newSheetFormHandler} />
<TransitionGroup className="todo-list">
{names.length &&
names.map(name => (
<CSSTransition
transitionName="sheetNames"
transitionAppear={true}
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
key={name.sheetName}
>
<SheetButton
name={name}
deleteButtonHandler={deleteButtonHandler}
clickSheetNameHandler={clickSheetNameHandler}
/>
</CSSTransition>
))}
</TransitionGroup>
</div>
);
};
export default SheetEditor;
-61
View File
@@ -1,61 +0,0 @@
import React, { useState, useEffect } from 'react';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import FormInput from './form-input';
import SheetButton from './sheet-button';
import server from '../server';
export default function SheetEditor() {
const { getSheetsData, addSheet, deleteSheet, setActiveSheet } = server;
const [names, setNames] = useState([]);
useEffect(() => {
getSheetsData()
.then(setNames)
.catch(alert);
}, []);
const deleteButtonHandler = (e, sheetIndex) => {
deleteSheet(sheetIndex)
.then(setNames)
.catch(alert);
};
const clickSheetNameHandler = (e, sheetName) => {
setActiveSheet(sheetName)
.then(setNames)
.catch(alert);
};
const newSheetFormHandler = (e, newSheetTitle) => {
addSheet(newSheetTitle)
.then(setNames)
.catch(alert);
};
return (
<div>
<FormInput newSheetFormHandler={newSheetFormHandler} />
<ReactCSSTransitionGroup
transitionName="sheetNames"
transitionAppear={true}
transitionEnterTimeout={100}
transitionLeaveTimeout={100}
>
{names.length
? names.map(name => {
return (
<SheetButton
name={name}
deleteButtonHandler={deleteButtonHandler}
clickSheetNameHandler={clickSheetNameHandler}
key={name.sheetName}
/>
);
})
: null}
</ReactCSSTransitionGroup>
</div>
);
}
-10
View File
@@ -1,10 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import SheetEditor from './components/sheet-editor';
import './styles.css';
ReactDOM.render(
<SheetEditor />,
document.getElementById('index')
);
+21 -18
View File
@@ -1,29 +1,32 @@
/*
Convert google script server calls to more
familiar js/promise-based functions.
*/
// Convert google script server calls to more familiar promise-based functions
const serverMethods = {};
const myServerFunctions = {};
// skip the reserved methods
const ignoredMethods = [
// identify the reserved functions
const ignoredMethods = new Set([
'withFailureHandler',
'withLogger',
'withSuccessHandler',
'withUserObject',
];
]);
for (const method in google.script.run) {
if (!ignoredMethods.includes(method)) {
serverMethods[method] = (...args) => {
return new Promise((resolve, reject) => {
// 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)[method](...args);
.withFailureHandler(reject)
[serverFunctionName](...args);
});
});
};
}
}
export default serverMethods;
export default myServerFunctions;
+5 -2
View File
@@ -1,8 +1,11 @@
{
"presets": [
["@babel/env", {
[
"@babel/env",
{
"modules": false
}]
}
]
],
"plugins": [
"transform-es3-property-literals",
+7 -9
View File
@@ -1,18 +1,16 @@
{
"root": true,
"parser": "babel-eslint",
"extends": ["airbnb-base", "prettier"],
"plugins": ["googleappsscript", "prettier"],
"extends": ["airbnb-base", "plugin:prettier/recommended"],
"plugins": ["prettier", "googleappsscript"],
"env": {
"googleappsscript/googleappsscript": true
},
"globals": {
"SpreadsheetApp": true,
"HtmlService": true,
"global": true
},
"rules": {
"import/prefer-default-export": 0,
"prettier/prettier": "error"
"prettier/prettier": "error",
"camelcase": "warn",
"import/prefer-default-export": "warn",
"import/no-extraneous-dependencies": "warn",
"prefer-object-spread": "warn"
}
}
-10
View File
@@ -1,10 +0,0 @@
import * as publicFunctions from './sheets-utilities';
// Expose public functions
global.onOpen = publicFunctions.onOpen;
global.openDialog = publicFunctions.openDialog;
global.openAboutSidebar = publicFunctions.openAboutSidebar;
global.getSheetsData = publicFunctions.getSheetsData;
global.addSheet = publicFunctions.addSheet;
global.deleteSheet = publicFunctions.deleteSheet;
global.setActiveSheet = publicFunctions.setActiveSheet;
+11
View File
@@ -0,0 +1,11 @@
import * as publicUiFunctions from './ui';
import * as publicSheetFunctions from './sheets';
// Expose public functions by attaching to `global`
global.onOpen = publicUiFunctions.onOpen;
global.openDialog = publicUiFunctions.openDialog;
global.openAboutSidebar = publicUiFunctions.openAboutSidebar;
global.getSheetsData = publicSheetFunctions.getSheetsData;
global.addSheet = publicSheetFunctions.addSheet;
global.deleteSheet = publicSheetFunctions.deleteSheet;
global.setActiveSheet = publicSheetFunctions.setActiveSheet;
-65
View File
@@ -1,65 +0,0 @@
// Use ES6/7 code
const onOpen = () => {
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.createMenu('Custom scripts')
.addItem('Edit sheets [sample React project]', 'openDialog')
.addItem('About me', 'openAboutSidebar')
.addToUi();
};
const openDialog = () => {
const html = HtmlService.createHtmlOutputFromFile('main')
.setWidth(400)
.setHeight(600);
SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
.showModalDialog(html, 'Sheet Editor');
};
const openAboutSidebar = () => {
const html = HtmlService.createHtmlOutputFromFile('about');
SpreadsheetApp.getUi().showSidebar(html);
};
const getSheets = () => SpreadsheetApp.getActive().getSheets();
const getActiveSheetName = () => SpreadsheetApp.getActive().getSheetName();
const getSheetsData = () => {
const activeSheetName = getActiveSheetName();
return getSheets().map((sheet, index) => {
const sheetName = sheet.getName();
return {
text: sheetName,
sheetIndex: index,
isActive: sheetName === activeSheetName,
};
});
};
const addSheet = sheetTitle => {
SpreadsheetApp.getActive().insertSheet(sheetTitle);
return getSheetsData();
};
const deleteSheet = sheetIndex => {
const sheets = getSheets();
SpreadsheetApp.getActive().deleteSheet(sheets[sheetIndex]);
return getSheetsData();
};
const setActiveSheet = sheetName => {
SpreadsheetApp.getActive()
.getSheetByName(sheetName)
.activate();
return getSheetsData();
};
export {
onOpen,
openDialog,
openAboutSidebar,
getSheetsData,
addSheet,
deleteSheet,
setActiveSheet,
};
+33
View File
@@ -0,0 +1,33 @@
const getSheets = () => SpreadsheetApp.getActive().getSheets();
const getActiveSheetName = () => SpreadsheetApp.getActive().getSheetName();
export const getSheetsData = () => {
const activeSheetName = getActiveSheetName();
return getSheets().map((sheet, index) => {
const sheetName = sheet.getName();
return {
text: sheetName,
sheetIndex: index,
isActive: sheetName === activeSheetName,
};
});
};
export const addSheet = sheetTitle => {
SpreadsheetApp.getActive().insertSheet(sheetTitle);
return getSheetsData();
};
export const deleteSheet = sheetIndex => {
const sheets = getSheets();
SpreadsheetApp.getActive().deleteSheet(sheets[sheetIndex]);
return getSheetsData();
};
export const setActiveSheet = sheetName => {
SpreadsheetApp.getActive()
.getSheetByName(sheetName)
.activate();
return getSheetsData();
};
+19
View File
@@ -0,0 +1,19 @@
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);
};
+28 -6
View File
@@ -3,13 +3,13 @@
********************************/
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const GasPlugin = require('gas-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebpackCleanPlugin = require('webpack-clean');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
const DynamicCdnWebpackPlugin = require('dynamic-cdn-webpack-plugin');
const moduleToCdn = require('module-to-cdn');
/*********************************
* define file paths
@@ -23,12 +23,12 @@ const htmlTemplate = './src/client/dialog-template.html';
const clientEntrypoints = [
{
name: 'CLIENT - main dialog',
entry: './src/client/main.jsx',
entry: './src/client/MainDialog.jsx',
filename: 'main.html',
},
{
name: 'CLIENT - about sidebar',
entry: './src/client/about.jsx',
entry: './src/client/AboutDialog.jsx',
filename: 'about.html',
},
];
@@ -64,7 +64,6 @@ const appsscriptConfig = {
name: 'COPY APPSSCRIPT.JSON',
entry: './appsscript.json',
plugins: [
new CleanWebpackPlugin([destination]),
new CopyWebpackPlugin([
{
from: './appsscript.json',
@@ -115,7 +114,30 @@ const clientConfigs = clientEntrypoints.map(clientEntrypoint => {
}),
new HtmlWebpackInlineSourcePlugin(),
new WebpackCleanPlugin([path.join(destination, 'main.js')]),
new DynamicCdnWebpackPlugin(),
new DynamicCdnWebpackPlugin({
verbose: true,
resolver: (packageName, packageVersion, options) => {
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/dist/react-transition-group.min.js`,
};
default:
return null;
}
},
}),
],
};
});
@@ -123,7 +145,7 @@ const clientConfigs = clientEntrypoints.map(clientEntrypoint => {
const serverConfig = {
...sharedConfigSettings,
name: 'SERVER',
entry: './src/server/code.js',
entry: './src/server/index.js',
output: {
filename: 'code.js',
path: path.resolve(__dirname, destination),