diff --git a/.claspignore b/.claspignore index 866740f..82df346 100644 --- a/.claspignore +++ b/.claspignore @@ -1 +1 @@ -dist/main.js \ No newline at end of file +main.js \ No newline at end of file diff --git a/README.md b/README.md index 3b11ee9..b57d182 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ _The included demo React app for Google Sheets shows insertion, deletion and sel > cd React-Google-Apps-Script > npm install ``` -2. Enable the Google Apps Script API for your account by visiting[(script.google.com/home/usersettings)](https://script.google.com/home/usersettings): +2. Enable the Google Apps Script API for your account by visiting [script.google.com/home/usersettings](https://script.google.com/home/usersettings): @@ -68,12 +68,12 @@ Modify the server-side and client-side source code in the `src` folder. [Add any ## 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. +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. ## Features - Includes popular `eslint` and `prettier` configs for clean, standardized code. -- `import` CSS from another file: +- Supports importing CSS from another file: ```js import './styles.css'; ``` @@ -89,15 +89,14 @@ Insert/activate/delete sheets through a simple HTML dialog, built with React. Ac import server from '../server'; // We now have access to all our server functions, which return promises! - const { addSheet } = server; - addSheet(sheetTitle) - .then(response => doSomething(response)) - .catch(handleError(err)); + server.addSheet(sheetTitle) + .then(response => doSomething(response)) + .catch(err => handleError(err)); - // Or we can use async/await: + // Or we can use async/await. This is the same thing as above: async () => { try { - const response = await addSheet(sheetTitle); + const response = await server.addSheet(sheetTitle); doSomething(response); } catch (err) { handleError(err) @@ -122,7 +121,7 @@ To add new client-side libraries for your React app: 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. +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: diff --git a/dist/code.js b/dist/code.js index ec11250..46d2867 100644 --- a/dist/code.js +++ b/dist/code.js @@ -2,6 +2,8 @@ function onOpen() { } function openDialog() { } +function openDialogBootstrap() { +} function openAboutSidebar() { } function getSheetsData() { @@ -59,6 +61,29 @@ function setActiveSheet() { 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; @@ -74,11 +99,11 @@ function setActiveSheet() { }, getSheetsData = function() { var activeSheetName = SpreadsheetApp.getActive().getSheetName(); return getSheets().map((function(sheet, index) { - var sheetName = sheet.getName(); + var name = sheet.getName(); return { - text: sheetName, - sheetIndex: index, - isActive: sheetName === activeSheetName + name: name, + index: index, + isActive: name === activeSheetName }; })); }, addSheet = function(sheetTitle) { @@ -89,32 +114,14 @@ 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 _ui__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1), _sheets__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(0); + 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.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"]; + 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; diff --git a/dist/dialog-demo-bootstrap.html b/dist/dialog-demo-bootstrap.html new file mode 100644 index 0000000..554dd07 --- /dev/null +++ b/dist/dialog-demo-bootstrap.html @@ -0,0 +1,18 @@ + + + + + + + + +
+ +
+ + diff --git a/dist/dialog-demo.html b/dist/dialog-demo.html new file mode 100644 index 0000000..e187d36 --- /dev/null +++ b/dist/dialog-demo.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ +
+ + diff --git a/dist/main.html b/dist/main.html deleted file mode 100644 index a475bcc..0000000 --- a/dist/main.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - -
- -
- - \ No newline at end of file diff --git a/dist/about.html b/dist/sidebar-about-page.html similarity index 68% rename from dist/about.html rename to dist/sidebar-about-page.html index 7bcef36..a02ec85 100644 --- a/dist/about.html +++ b/dist/sidebar-about-page.html @@ -1,11 +1,12 @@ - + + -
- -
- - \ No newline at end of file +
+ +
+ + diff --git a/package-lock.json b/package-lock.json index 3d263ba..978c3c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1093,6 +1093,25 @@ } } }, + "@popperjs/core": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.2.tgz", + "integrity": "sha512-JlGTGRYHC2QK+DDbePyXdBdooxFq2+noLfWpRqJtkxcb/oYWzOF0kcbfvvbWrwevCC1l6hLUg1wHYT+ona5BWQ==" + }, + "@restart/context": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", + "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==" + }, + "@restart/hooks": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.25.tgz", + "integrity": "sha512-m2v3N5pxTsIiSH74/sb1yW8D9RxkJidGW+5Mfwn/lHb2QzhZNlaU1su7abSyT9EGf0xS/0waLjrf7/XxQHUk7w==", + "requires": { + "lodash": "^4.17.15", + "lodash-es": "^4.17.15" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -1120,6 +1139,25 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, + "@types/react": { + "version": "16.9.41", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.41.tgz", + "integrity": "sha512-6cFei7F7L4wwuM+IND/Q2cV1koQUvJ8iSV+Gwn0c3kvABZ691g7sp3hfEQHOUBJtccl1gPi+EyNjMIl9nGA0ug==", + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, + "@types/warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -1353,9 +1391,9 @@ "dev": true }, "agent-base": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", - "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", "dev": true, "requires": { "debug": "4" @@ -1799,9 +1837,9 @@ "dev": true }, "bignumber.js": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", "dev": true }, "binary-extensions": { @@ -2100,11 +2138,6 @@ "integrity": "sha512-8joOm7BwcpEN4BfVHtfh0hBXSAPVYk+eUIcNntGtMkUWy/6AKRCDZINCLe3kB1vHhT2vBxBF85Hh9VlPXi/qjA==", "dev": true }, - "chain-function": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.1.tgz", - "integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==" - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2190,6 +2223,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -2632,6 +2670,11 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "csstype": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.10.tgz", + "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==" + }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -2788,9 +2831,9 @@ } }, "dns-socket": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.0.tgz", - "integrity": "sha512-4XuD3z28jht3jvHbiom6fAipgG5LkjYeDLrX5OH8cbl0AtzTyUUAxGckcW8T7z0pLfBBV5qOiuC4wUEohk6FrQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.1.tgz", + "integrity": "sha512-fNvDq86lS522+zMbh31X8cQzYQd6xumCNlxsuZF5TKxQThF/e+rJbVM6K8mmlsdcSm6yNjKJQq3Sf38viAJj8g==", "dev": true, "requires": { "dns-packet": "^5.1.2" @@ -2815,11 +2858,12 @@ } }, "dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz", + "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", "requires": { - "@babel/runtime": "^7.1.2" + "@babel/runtime": "^7.8.7", + "csstype": "^2.6.7" } }, "dom-serializer": { @@ -2872,13 +2916,21 @@ } }, "dotf": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/dotf/-/dotf-1.5.0.tgz", - "integrity": "sha512-t0RzzkjSrc1jDxUde8cbD0q0mB9xk0Hdgt1joeMUwXcWiP9V+Ydw7gHjoeeuBN8aiJbnXAaCiM5/U1C2aN1F1g==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dotf/-/dotf-1.5.1.tgz", + "integrity": "sha512-z/VJm6Z5TfFPahHNPw1B/XrKaGQmy2Z4xrsAvzu9xr4saFXrCvQh8Y4DWcdllp9ciZW0VkkjkJPIixZ1un5oJw==", "dev": true, "requires": { - "graceful-fs": "^4.2.3", - "jsonfile": "^6.0.0" + "graceful-fs": "^4.2.4", + "jsonfile": "^6.0.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + } } }, "duplexer3": { @@ -3938,9 +3990,9 @@ "dev": true }, "fast-text-encoding": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.1.tgz", - "integrity": "sha512-x4FEgaz3zNRtJfLFqJmHWxkMDDvXVtaznj2V9jiP8ACUJrUgist4bP9FmDL2Vew2Y9mEQI/tG4GqabaitYp9CQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", "dev": true }, "figgy-pudding": { @@ -4777,9 +4829,9 @@ } }, "gaxios": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.2.tgz", - "integrity": "sha512-K/+py7UvKRDaEwEKlLiRKrFr+wjGjsMz5qH7Vs549QJS7cpSCOT/BbWL7pzqECflc46FcNPipjSfB+V1m8PAhw==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.4.tgz", + "integrity": "sha512-US8UMj8C5pRnao3Zykc4AAVr+cffoNKRTg9Rsf2GiuZCW69vgJj38VK2PzlPuQU73FZ/nTk9/Av6/JGcE1N9vA==", "dev": true, "requires": { "abort-controller": "^3.0.0", @@ -5505,7 +5557,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -5677,15 +5728,15 @@ } }, "is-online": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/is-online/-/is-online-8.2.1.tgz", - "integrity": "sha512-853p45I2b//EDV7n1Rbk60f/fy1KQlp1IkTjV/K2EyAD/h8qXrIAxwIbZ8c4K5p5yDsQlTWUrxocgW/aBtNfYQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/is-online/-/is-online-8.4.0.tgz", + "integrity": "sha512-i0qGRbtUaQEU5Z7O3LmOnH3yorhG1lnygqY2cv3InlQKKm3nx6XiGXZk49lATR3N7hyxoiuHMR0pKwRuB+s5lg==", "dev": true, "requires": { "got": "^9.6.0", "p-any": "^2.0.0", "p-timeout": "^3.0.0", - "public-ip": "^3.0.0" + "public-ip": "^4.0.1" } }, "is-plain-object": { @@ -5818,12 +5869,12 @@ "dev": true }, "json-bigint": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", - "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz", + "integrity": "sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ==", "dev": true, "requires": { - "bignumber.js": "^7.0.0" + "bignumber.js": "^9.0.0" } }, "json-buffer": { @@ -6066,8 +6117,12 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash-es": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", + "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" }, "loose-envify": { "version": "1.4.0", @@ -6214,9 +6269,9 @@ } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", "dev": true }, "mimic-fn": { @@ -7255,6 +7310,25 @@ "react-is": "^16.8.1" } }, + "prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "requires": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "dependencies": { + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -7276,12 +7350,12 @@ } }, "public-ip": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/public-ip/-/public-ip-3.2.0.tgz", - "integrity": "sha512-DBq4o955zhrhESG4z6GkLN9mtY9NT/JOjEV8pvnYy3bjVQOQF0J5lJNwWLbEWwNstyNFJlY7JxCPFq4bdXSabw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/public-ip/-/public-ip-4.0.2.tgz", + "integrity": "sha512-ZHqUjaYT/+FuSiy5/o2gBxvj0PF7M3MXGnaLJBsJNMCyXI4jzuXXHJKrk0gDxx1apiF/jYsBwjTQOM9V8G6oCQ==", "dev": true, "requires": { - "dns-socket": "^4.2.0", + "dns-socket": "^4.2.1", "got": "^9.6.0", "is-ip": "^3.1.0" } @@ -7326,9 +7400,9 @@ "dev": true }, "qs": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz", - "integrity": "sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==", + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", "dev": true }, "querystring": { @@ -7372,6 +7446,56 @@ "prop-types": "^15.6.2" } }, + "react-bootstrap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.0.1.tgz", + "integrity": "sha512-xMHwsvDN7sIv26P9wWiosWjITZije2dRCjEJHVfV2KFoSJY+8uv2zttEw0XMB7xviQcW3zuIGLJXuj8vf6lYEg==", + "requires": { + "@babel/runtime": "^7.4.2", + "@restart/context": "^2.1.4", + "@restart/hooks": "^0.3.21", + "@types/react": "^16.9.23", + "classnames": "^2.2.6", + "dom-helpers": "^5.1.2", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "prop-types-extra": "^1.1.0", + "react-overlays": "^3.1.2", + "react-transition-group": "^4.0.0", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + }, + "dependencies": { + "dom-helpers": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz", + "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^2.6.7" + } + }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, "react-dom": { "version": "16.13.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.0.tgz", @@ -7388,16 +7512,54 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.0.tgz", "integrity": "sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA==" }, - "react-transition-group": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-1.2.1.tgz", - "integrity": "sha512-CWaL3laCmgAFdxdKbhhps+c0HRGF4c+hdM4H23+FI1QBNUyx/AMeIJGWorehPNSaKnQNOAxL7PQmqMu78CDj3Q==", + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-overlays": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-3.2.0.tgz", + "integrity": "sha512-YTgCmw6l4uBOYylSnc3V8WLX+A0EoGnzDrqkYz0K7MUKbMBZFpaxLXH4EF9eZbspd+syZHQ5XAABI7n/zak1EA==", "requires": { - "chain-function": "^1.0.0", - "dom-helpers": "^3.2.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.5.6", - "warning": "^3.0.0" + "@babel/runtime": "^7.4.5", + "@popperjs/core": "^2.0.0", + "@restart/hooks": "^0.3.12", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.1.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + }, + "dependencies": { + "dom-helpers": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz", + "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^2.6.7" + } + }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + } + } + }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" } }, "read-pkg": { @@ -8676,12 +8838,12 @@ "dev": true }, "ts2gas": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/ts2gas/-/ts2gas-3.6.1.tgz", - "integrity": "sha512-B/4OZJKslQytpAtRE0Q0MoZ0h8fyXzJDmMu1CIPdP6q3iEAQJHxCFTxUsrZWOCNQMIuFlxsEX/PHn41g8TVSvg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/ts2gas/-/ts2gas-3.6.2.tgz", + "integrity": "sha512-vKp6IuH59NQmmd93S+unEPCHZbGTpnXtC8VxRyTf87VCIMMo/qs9zNBU5oxVvVIOyFyqb20KDf94NYN7cbtL5A==", "dev": true, "requires": { - "typescript": "^3.8.3" + "typescript": "^3.9.2" } }, "tslib": { @@ -8718,9 +8880,9 @@ "dev": true }, "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz", + "integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==", "dev": true }, "uglify-js": { @@ -8747,6 +8909,17 @@ } } }, + "uncontrollable": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.1.1.tgz", + "integrity": "sha512-EcPYhot3uWTS3w00R32R2+vS8Vr53tttrvMj/yA1uYRhf8hbTG2GyugGqWDY0qIskxn0uTTojVd6wPYW9ZEf8Q==", + "requires": { + "@babel/runtime": "^7.6.3", + "@types/react": "^16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + } + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -8963,9 +9136,9 @@ "dev": true }, "uuid": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz", - "integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", "dev": true }, "v8-compile-cache": { @@ -8990,14 +9163,6 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "requires": { - "loose-envify": "^1.0.0" - } - }, "watch": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz", diff --git a/package.json b/package.json index 8cedc9d..9652a29 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,14 @@ "name": "react-google-apps-script", "version": "1.0.0", "description": "Starter project for using React with Google Apps Script", + "repository": { + "type": "git", + "url": "https://github.com/enuchi/React-Google-Apps-Script.git" + }, "scripts": { "test": "test", "login": "clasp login", - "setup": "rm .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", "build": "webpack --mode production", "deploy": "npm run build && npx clasp push" @@ -26,8 +30,9 @@ "dependencies": { "prop-types": "^15.7.2", "react": "^16.13.0", + "react-bootstrap": "^1.0.1", "react-dom": "^16.13.0", - "react-transition-group": "^1.2.1" + "react-transition-group": "^4.4.1" }, "devDependencies": { "@babel/cli": "^7.8.4", diff --git a/src/client/components/FormInput.jsx b/src/client/components/FormInput.jsx deleted file mode 100644 index a66e16a..0000000 --- a/src/client/components/FormInput.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; - -const FormInput = ({ newSheetFormHandler }) => { - const [text, setText] = useState(''); - - const handleChange = event => setText(event.target.value); - - const handleSubmit = event => { - event.preventDefault(); - if (text.length === 0) return; - - newSheetFormHandler(event, text); - setText(''); - }; - - return ( -
- Add a sheet: -
- -
-
- ); -}; - -export default FormInput; - -FormInput.propTypes = { - newSheetFormHandler: PropTypes.func, -}; diff --git a/src/client/components/SheetButton.jsx b/src/client/components/SheetButton.jsx deleted file mode 100644 index 406ad8e..0000000 --- a/src/client/components/SheetButton.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const SheetButton = ({ name, deleteButtonHandler, clickSheetNameHandler }) => { - const { sheetIndex, text, isActive } = name; - - return ( -
- - clickSheetNameHandler(e, text)} - className={`sheetNameText ${isActive ? 'active-sheet' : ''}`} - > - {text} - -
- ); -}; - -export default SheetButton; - -SheetButton.propTypes = { - name: PropTypes.shape({ - sheetIndex: PropTypes.number, - text: PropTypes.string, - isActive: PropTypes.bool, - }), - deleteButtonHandler: PropTypes.func, - clickSheetNameHandler: PropTypes.func, -}; diff --git a/src/client/components/SheetEditor.jsx b/src/client/components/SheetEditor.jsx deleted file mode 100644 index 8c85a08..0000000 --- a/src/client/components/SheetEditor.jsx +++ /dev/null @@ -1,67 +0,0 @@ -/* 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 ( -
- - - {names.length && - names.map(name => ( - - - - ))} - -
- ); -}; - -export default SheetEditor; diff --git a/src/client/dialog-demo-bootstrap/components/SheetEditor.jsx b/src/client/dialog-demo-bootstrap/components/SheetEditor.jsx new file mode 100644 index 0000000..c525417 --- /dev/null +++ b/src/client/dialog-demo-bootstrap/components/SheetEditor.jsx @@ -0,0 +1,106 @@ +import React, { useState, useEffect } from 'react'; +import { TransitionGroup, CSSTransition } from 'react-transition-group'; +import { Form, Button, ListGroup, Col, Row } from 'react-bootstrap'; + +// This is a wrapper for google.script.run that lets us use promises. +import server from '../../utils/server'; + +const SheetEditor = () => { + const [newSheetName, setNewSheetName] = useState(''); + const [names, setNames] = useState([]); + + useEffect(() => { + server + .getSheetsData() + .then(setNames) + .catch(alert); + }, []); + + const deleteSheet = sheetIndex => { + server + .deleteSheet(sheetIndex) + .then(setNames) + .catch(alert); + }; + + const setActiveSheet = sheetName => { + server + .setActiveSheet(sheetName) + .then(setNames) + .catch(alert); + }; + + const submitNewSheet = async () => { + try { + const response = await server.addSheet(newSheetName); + setNames(response); + } catch (error) { + alert(error); + } + }; + + return ( +
+
+ + Add a new sheet + + + { + setNewSheetName(e.target.value); + }} + /> + + + + + + + Enter the name for your new sheet. + + +
+ + + {names.length > 0 && + names.map(name => ( + + + + + + + ))} + + +
+ ); +}; + +export default SheetEditor; diff --git a/src/client/dialog-demo-bootstrap/index.html b/src/client/dialog-demo-bootstrap/index.html new file mode 100644 index 0000000..2b7fdfe --- /dev/null +++ b/src/client/dialog-demo-bootstrap/index.html @@ -0,0 +1,18 @@ + + + + + + + + +
+ +
+ + diff --git a/src/client/MainDialog.jsx b/src/client/dialog-demo-bootstrap/index.js similarity index 100% rename from src/client/MainDialog.jsx rename to src/client/dialog-demo-bootstrap/index.js diff --git a/src/client/dialog-demo-bootstrap/styles.css b/src/client/dialog-demo-bootstrap/styles.css new file mode 100644 index 0000000..201cdac --- /dev/null +++ b/src/client/dialog-demo-bootstrap/styles.css @@ -0,0 +1,29 @@ +/* +CSSTransitionGroup styling + */ +.sheetNames-enter { + opacity: 0; +} + +.sheetNames-enter.sheetNames-enter-active { + opacity: 1; + transition: opacity 300ms ease-in; +} + +.sheetNames-exit { + opacity: 1; +} + +.sheetNames-exit.sheetNames-exit-active { + opacity: 0; + transition: opacity 300ms ease-in; +} + +.sheetNames-appear { + opacity: 0; +} + +.sheetNames-appear.sheetNames-appear-active { + opacity: 1; + transition: opacity 0.5ms ease-in; +} diff --git a/src/client/dialog-demo/components/FormInput.jsx b/src/client/dialog-demo/components/FormInput.jsx new file mode 100644 index 0000000..afac1dd --- /dev/null +++ b/src/client/dialog-demo/components/FormInput.jsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +const FormInput = ({ submitNewSheet }) => { + const [inputValue, setInputValue] = useState(''); + + const handleChange = event => setInputValue(event.target.value); + + const handleSubmit = event => { + event.preventDefault(); + if (inputValue.length === 0) return; + + submitNewSheet(inputValue); + setInputValue(''); + }; + + return ( +
+
+
+ Add a sheet +
+
+ + +
+
+
+ ); +}; + +export default FormInput; + +FormInput.propTypes = { + submitNewSheet: PropTypes.func, +}; diff --git a/src/client/dialog-demo/components/SheetButton.jsx b/src/client/dialog-demo/components/SheetButton.jsx new file mode 100644 index 0000000..ea95b36 --- /dev/null +++ b/src/client/dialog-demo/components/SheetButton.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const SheetButton = ({ sheetDetails, deleteSheet, setActiveSheet }) => { + const { index, name, isActive } = sheetDetails; + + return ( +
+ + +
+ ); +}; + +export default SheetButton; + +SheetButton.propTypes = { + sheetDetails: PropTypes.shape({ + index: PropTypes.number, + name: PropTypes.string, + isActive: PropTypes.bool, + }), + deleteSheet: PropTypes.func, + setActiveSheet: PropTypes.func, +}; diff --git a/src/client/dialog-demo/components/SheetEditor.jsx b/src/client/dialog-demo/components/SheetEditor.jsx new file mode 100644 index 0000000..f35f9e9 --- /dev/null +++ b/src/client/dialog-demo/components/SheetEditor.jsx @@ -0,0 +1,68 @@ +import React, { useState, useEffect } from 'react'; +import { TransitionGroup, CSSTransition } from 'react-transition-group'; +import FormInput from './FormInput'; +import SheetButton from './SheetButton'; + +// This is a wrapper for google.script.run that lets us use promises. +import server from '../../utils/server'; + +const SheetEditor = () => { + const [names, setNames] = useState([]); + + useEffect(() => { + // Call a server global function here and handle the response with .then() and .catch() + server + .getSheetsData() + .then(setNames) + .catch(alert); + }, []); + + const deleteSheet = sheetIndex => { + server + .deleteSheet(sheetIndex) + .then(setNames) + .catch(alert); + }; + + const setActiveSheet = sheetName => { + server + .setActiveSheet(sheetName) + .then(setNames) + .catch(alert); + }; + + // You can also use async/await notation for server calls with our server wrapper. + // (This does the same thing as .then().catch() in the above handlers.) + const submitNewSheet = async newSheetName => { + try { + const response = await server.addSheet(newSheetName); + setNames(response); + } catch (error) { + alert(error); + } + }; + + return ( +
+ + + {names.length > 0 && + names.map(name => ( + + + + ))} + +
+ ); +}; + +export default SheetEditor; diff --git a/src/client/dialog-demo/index.html b/src/client/dialog-demo/index.html new file mode 100644 index 0000000..10a7274 --- /dev/null +++ b/src/client/dialog-demo/index.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ +
+ + diff --git a/src/client/dialog-demo/index.js b/src/client/dialog-demo/index.js new file mode 100644 index 0000000..846e94a --- /dev/null +++ b/src/client/dialog-demo/index.js @@ -0,0 +1,7 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import SheetEditor from './components/SheetEditor'; + +import './styles.css'; + +ReactDOM.render(, document.getElementById('index')); diff --git a/src/client/dialog-demo/styles.css b/src/client/dialog-demo/styles.css new file mode 100644 index 0000000..5f67693 --- /dev/null +++ b/src/client/dialog-demo/styles.css @@ -0,0 +1,137 @@ +input { + font-family: 'Nunito', sans-serif; + width: 150px; + height: 28px; + border: 1px solid #6b6b6b; + border-radius: 4px; +} + +input:focus { + outline: 0 none; +} + +button { + cursor: pointer; + font-family: 'Nunito', sans-serif; +} + +button.submit { + background: rgb(234, 234, 255); + margin: 0px 10px; + padding-top: 6px; + padding-bottom: 6px; + border: 1px solid #6b6b6b; + border-radius: 4px; + transition: background 300ms ease-in; +} + +button.submit:hover { + background: rgb(234, 234, 255, 0.4); +} + +button.delete { + width: 17px; + -webkit-transition-duration: 0.4s; /* Safari */ + background-color: #ffffff; + border: 1px solid #f44336; + color: #676767; + display: inline-block; + font-size: 11px; + height: 17px; + line-height: 15px; + margin-left: 3px; + margin-right: 11px; + padding: 1px; + text-transform: uppercase; + transition-duration: 0.6s; + vertical-align: middle; +} + +button.delete:hover { + background-color: #f44336; + color: #ffffff; +} + +button.delete:active { + background-color: #f44336; + + transform: translateY(1px); + transition-duration: 0.3s; +} + +button.delete:focus { + outline: none; +} + +.formBlock { + display: -webkit-box; + font-family: 'Nunito', sans-serif; + font-weight: 700; + padding-bottom: 30px; +} + +.sheetLine { + padding: 10px 0px; + border-bottom: 1px solid #eaeaea; +} + +button.basicButton { + padding: 0; + border: none; + outline: none; + font: inherit; + color: inherit; + background: none; +} + +span.sheetNameText { + cursor: pointer; + font-size: 14px; + font-family: 'Nunito', sans-serif; + border-bottom-color: rgba(51, 130, 54, 0); + border-bottom-width: 3px solid none; + border-bottom-style: solid; + transition: border-bottom-color 300ms ease-in; +} + +span.sheetNameText:hover { + border-bottom-color: rgba(51, 130, 54, 0.2); +} + +span.sheetNameText:active { + border-bottom-color: rgba(51, 130, 54, 0.2); +} + +span.sheetNameText.active-sheet { + border-bottom-color: rgb(51, 130, 54); +} + +/* +CSSTransitionGroup styling + */ +.sheetNames-enter { + opacity: 0; +} + +.sheetNames-enter.sheetNames-enter-active { + opacity: 1; + transition: opacity 300ms ease-in; +} + +.sheetNames-exit { + opacity: 1; +} + +.sheetNames-exit.sheetNames-exit-active { + opacity: 0; + transition: opacity 300ms ease-in; +} + +.sheetNames-appear { + opacity: 0; +} + +.sheetNames-appear.sheetNames-appear-active { + opacity: 1; + transition: opacity 0.5ms ease-in; +} diff --git a/src/client/dialog-template.html b/src/client/dialog-template.html deleted file mode 100644 index 8ca1f3f..0000000 --- a/src/client/dialog-template.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - -
- -
- - \ No newline at end of file diff --git a/src/client/components/About.jsx b/src/client/sidebar-about-page/components/About.jsx similarity index 90% rename from src/client/components/About.jsx rename to src/client/sidebar-about-page/components/About.jsx index d960ebf..e33960e 100644 --- a/src/client/components/About.jsx +++ b/src/client/sidebar-about-page/components/About.jsx @@ -1,7 +1,7 @@ import React from 'react'; const About = () => ( -
+
Github repo:
+ + + + + + +
+ +
+ + diff --git a/src/client/AboutDialog.jsx b/src/client/sidebar-about-page/index.js similarity index 100% rename from src/client/AboutDialog.jsx rename to src/client/sidebar-about-page/index.js diff --git a/src/client/styles.css b/src/client/styles.css deleted file mode 100644 index b208d6d..0000000 --- a/src/client/styles.css +++ /dev/null @@ -1,91 +0,0 @@ -@import url('https://fonts.googleapis.com/css?family=Lora|Mukta'); - -input { - width: 130px; - margin-left: 8px; -} - -button { - -webkit-transition-duration: 0.4s; /* Safari */ - background-color: #f44336; - border: 1px solid #f44336; - color: white; - display: inline-block; - font-family: 'Mukta', sans-serif; - font-size: 11px; - height: 20px; - line-height: 15px; - margin-left: 3px; - margin-right: 11px; - padding: 3px; - text-transform: uppercase; - transition-duration: 0.6s; - vertical-align: middle; - width: 20px; -} - -button:hover { - background-color: white; - color: black; -} - -button:active { - background-color: #fafafa; - transform: translateY(1px); - transition-duration: 0.3s; -} - -button:focus { - outline: none; -} - -.formBlock { - display: -webkit-box; - font-family: 'Lora', serif; - font-weight: 700; -} - -.sheetLine { - cursor: pointer; - line-height: 3; - height: 30px; -} - -.sheetNameText { - font-size: 14px; - font-family: 'Lora', serif; -} - -.sheetNameText.active-sheet { - border-bottom: 3px solid #338236; -} - -/* -ReactCSSTransitionGroup styling - */ -.sheetNames-enter { - opacity: 0.01; -} - -.sheetNames-enter.sheetNames-enter-active { - opacity: 1; - transition: opacity 800ms ease-in; -} - -.sheetNames-leave { - opacity: 1; -} - -.sheetNames-leave.sheetNames-leave-active { - opacity: 0.01; - transition: opacity 100ms ease-in; -} - -.sheetNames-appear { - opacity: 0.01; -} - -.sheetNames-appear.sheetNames-appear-active { - opacity: 1; - transition: opacity .5s ease-in; -} \ No newline at end of file diff --git a/src/client/server.js b/src/client/utils/server.js similarity index 100% rename from src/client/server.js rename to src/client/utils/server.js diff --git a/src/server/index.js b/src/server/index.js index 9d03438..52414cc 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -4,6 +4,7 @@ import * as publicSheetFunctions from './sheets'; // Expose public functions by attaching to `global` global.onOpen = publicUiFunctions.onOpen; global.openDialog = publicUiFunctions.openDialog; +global.openDialogBootstrap = publicUiFunctions.openDialogBootstrap; global.openAboutSidebar = publicUiFunctions.openAboutSidebar; global.getSheetsData = publicSheetFunctions.getSheetsData; global.addSheet = publicSheetFunctions.addSheet; diff --git a/src/server/sheets.js b/src/server/sheets.js index 7bf069c..3550abd 100644 --- a/src/server/sheets.js +++ b/src/server/sheets.js @@ -5,11 +5,11 @@ const getActiveSheetName = () => SpreadsheetApp.getActive().getSheetName(); export const getSheetsData = () => { const activeSheetName = getActiveSheetName(); return getSheets().map((sheet, index) => { - const sheetName = sheet.getName(); + const name = sheet.getName(); return { - text: sheetName, - sheetIndex: index, - isActive: sheetName === activeSheetName, + name, + index, + isActive: name === activeSheetName, }; }); }; diff --git a/src/server/ui.js b/src/server/ui.js index 4c65cbc..1512ebf 100644 --- a/src/server/ui.js +++ b/src/server/ui.js @@ -1,19 +1,27 @@ export const onOpen = () => { SpreadsheetApp.getUi() .createMenu('My Sample React Project') // edit me! - .addItem('Sheet Name Editor', 'openDialog') + .addItem('Sheet Editor', 'openDialog') + .addItem('Sheet Editor (Bootstrap)', 'openDialogBootstrap') .addItem('About me', 'openAboutSidebar') .addToUi(); }; export const openDialog = () => { - const html = HtmlService.createHtmlOutputFromFile('main') - .setWidth(400) + const html = HtmlService.createHtmlOutputFromFile('dialog-demo') + .setWidth(600) .setHeight(600); SpreadsheetApp.getUi().showModalDialog(html, 'Sheet Editor'); }; +export const openDialogBootstrap = () => { + const html = HtmlService.createHtmlOutputFromFile('dialog-demo-bootstrap') + .setWidth(600) + .setHeight(600); + SpreadsheetApp.getUi().showModalDialog(html, 'Sheet Editor (Bootstrap)'); +}; + export const openAboutSidebar = () => { - const html = HtmlService.createHtmlOutputFromFile('about'); + const html = HtmlService.createHtmlOutputFromFile('sidebar-about-page'); SpreadsheetApp.getUi().showSidebar(html); }; diff --git a/webpack.config.js b/webpack.config.js index de2b745..3f4bd97 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,21 +15,28 @@ const moduleToCdn = require('module-to-cdn'); * define file paths ********************************/ const destination = 'dist'; -const htmlTemplate = './src/client/dialog-template.html'; /********************************* * client entry point paths ********************************/ const clientEntrypoints = [ { - name: 'CLIENT - main dialog', - entry: './src/client/MainDialog.jsx', - filename: 'main.html', + name: 'CLIENT - Dialog Demo', + entry: './src/client/dialog-demo/index.js', + filename: 'dialog-demo.html', + template: './src/client/dialog-demo/index.html', }, { - name: 'CLIENT - about sidebar', - entry: './src/client/AboutDialog.jsx', - filename: 'about.html', + name: 'CLIENT - Dialog Demo Bootstrap', + entry: './src/client/dialog-demo-bootstrap/index.js', + filename: 'dialog-demo-bootstrap.html', + template: './src/client/dialog-demo-bootstrap/index.html', + }, + { + name: 'CLIENT - Sidebar About Page', + entry: './src/client/sidebar-about-page/index.js', + filename: 'sidebar-about-page.html', + template: './src/client/sidebar-about-page/index.html', }, ]; @@ -46,19 +53,6 @@ const sharedConfigSettings = { }, }; -// eslint settings, to check during build if desired -const eslintConfig = { - enforce: 'pre', - test: /\.jsx?$/, - exclude: /node_modules/, - loader: 'eslint-loader', - options: { - cache: false, - failOnError: false, - fix: true, - }, -}; - // appscript copy settings, to copy this file over to the destination directory const appsscriptConfig = { name: 'COPY APPSSCRIPT.JSON', @@ -83,8 +77,6 @@ const clientConfig = { }, module: { rules: [ - // turned off by default, but uncomment below to run linting during build - // eslintConfig, { test: /\.jsx?$/, exclude: /node_modules/, @@ -108,16 +100,20 @@ const clientConfigs = clientEntrypoints.map(clientEntrypoint => { entry: clientEntrypoint.entry, plugins: [ new HtmlWebpackPlugin({ - template: htmlTemplate, + template: clientEntrypoint.template, filename: clientEntrypoint.filename, inlineSource: '^[^(//)]+.(js|css)$', // embed all js and css inline, exclude packages with '//' for dynamic cdn insertion }), new HtmlWebpackInlineSourcePlugin(), - new WebpackCleanPlugin([path.join(destination, 'main.js')]), + new WebpackCleanPlugin(['main.js'], { + basePath: path.join(__dirname, destination), + }), + // 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, @@ -132,7 +128,14 @@ const clientConfigs = clientEntrypoints.map(clientEntrypoint => { name: packageName, var: 'ReactTransitionGroup', version: packageVersion, - url: `https://unpkg.com/react-transition-group/dist/react-transition-group.min.js`, + 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; @@ -154,8 +157,6 @@ const serverConfig = { }, module: { rules: [ - // turned off by default, but uncomment below to run linting during build - // eslintConfig, { test: /\.js$/, exclude: /node_modules/,