add Bootstrap working example

• re-organize file structure with one directory per dialog
• add index.html file per dialog
• add bootstrap styling and dynamic cdn loading
• clean up react code and stylesheet

npm setup script update

adds force flag
"setup": "rm -f .clasp.json ..."

Update README.md

Move clasp to devDependencies

update dynamic cdn urls

Add repository field
This commit is contained in:
Elisha Nuchi
2020-06-29 02:08:18 -04:00
parent f4351f1a49
commit f2a259ce66
32 changed files with 845 additions and 399 deletions
-31
View File
@@ -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 (
<div className="formBlock">
<span>Add a sheet: </span>
<form onSubmit={handleSubmit}>
<input onChange={handleChange} value={text} />
</form>
</div>
);
};
export default FormInput;
FormInput.propTypes = {
newSheetFormHandler: PropTypes.func,
};
-30
View File
@@ -1,30 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const SheetButton = ({ name, deleteButtonHandler, clickSheetNameHandler }) => {
const { sheetIndex, text, isActive } = name;
return (
<div className="sheetLine">
<button onClick={e => deleteButtonHandler(e, sheetIndex)}>X</button>
<span
onClick={e => clickSheetNameHandler(e, text)}
className={`sheetNameText ${isActive ? 'active-sheet' : ''}`}
>
{text}
</span>
</div>
);
};
export default SheetButton;
SheetButton.propTypes = {
name: PropTypes.shape({
sheetIndex: PropTypes.number,
text: PropTypes.string,
isActive: PropTypes.bool,
}),
deleteButtonHandler: PropTypes.func,
clickSheetNameHandler: PropTypes.func,
};
-67
View File
@@ -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 (
<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;
@@ -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 (
<div style={{ padding: '3px', overflowX: 'hidden' }}>
<Form onSubmit={submitNewSheet}>
<Form.Group controlId="formNewSheet">
<Form.Label>Add a new sheet</Form.Label>
<Row>
<Col xs={10}>
<Form.Control
type="text"
placeholder="Sheet name"
value={newSheetName}
onChange={e => {
setNewSheetName(e.target.value);
}}
/>
</Col>
<Col xs={2}>
<Button variant="primary" type="submit">
Submit
</Button>
</Col>
</Row>
<Form.Text className="text-muted">
Enter the name for your new sheet.
</Form.Text>
</Form.Group>
</Form>
<ListGroup>
<TransitionGroup className="sheet-list">
{names.length > 0 &&
names.map(name => (
<CSSTransition
classNames="sheetNames"
timeout={500}
key={name.name}
>
<ListGroup.Item
className="d-flex"
key={`${name.index}-${name.name}`}
>
<Button
className="border-0"
variant="outline-danger"
size="sm"
onClick={() => deleteSheet(name.index)}
>
&times;
</Button>
<Button
className="border-0 mx-2"
variant={name.isActive ? 'success' : 'outline-success'}
onClick={() => setActiveSheet(name.name)}
>
{name.name}
</Button>
</ListGroup.Item>
</CSSTransition>
))}
</TransitionGroup>
</ListGroup>
</div>
);
};
export default SheetEditor;
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<base target="_top" />
<!-- Add any external scripts and stylesheets here -->
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
crossorigin="anonymous"
/>
</head>
<body>
<section id="index">
<!-- bundled js and css will get inlined here during build-->
</section>
</body>
</html>
@@ -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;
}
@@ -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 (
<div className="formBlock">
<form onSubmit={handleSubmit}>
<div>
<span>Add a sheet</span>
</div>
<div>
<input
onChange={handleChange}
value={inputValue}
placeholder="New sheet name"
/>
<button className="submit" type="submit">
Add
</button>
</div>
</form>
</div>
);
};
export default FormInput;
FormInput.propTypes = {
submitNewSheet: PropTypes.func,
};
@@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
const SheetButton = ({ sheetDetails, deleteSheet, setActiveSheet }) => {
const { index, name, isActive } = sheetDetails;
return (
<div className="sheetLine">
<button className="delete" onClick={() => deleteSheet(index)}>
&times;
</button>
<button className="basicButton" onClick={() => setActiveSheet(name)}>
<span className={`sheetNameText ${isActive ? 'active-sheet' : ''}`}>
{name}
</span>
</button>
</div>
);
};
export default SheetButton;
SheetButton.propTypes = {
sheetDetails: PropTypes.shape({
index: PropTypes.number,
name: PropTypes.string,
isActive: PropTypes.bool,
}),
deleteSheet: PropTypes.func,
setActiveSheet: PropTypes.func,
};
@@ -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 (
<div>
<FormInput submitNewSheet={submitNewSheet} />
<TransitionGroup className="sheet-list">
{names.length > 0 &&
names.map(name => (
<CSSTransition
classNames="sheetNames"
timeout={500}
key={name.name}
>
<SheetButton
sheetDetails={name}
deleteSheet={deleteSheet}
setActiveSheet={setActiveSheet}
/>
</CSSTransition>
))}
</TransitionGroup>
</div>
);
};
export default SheetEditor;
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<base target="_top" />
<!-- Add any external scripts and stylesheets here -->
<link
href="https://fonts.googleapis.com/css2?family=Nunito"
rel="stylesheet"
/>
</head>
<body>
<section id="index">
<!-- bundled js and css will get inlined here during build -->
</section>
</body>
</html>
+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'));
+137
View File
@@ -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;
}
-11
View File
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<section id="index">
<!-- bundled js and css will get inlined here -->
</section>
</body>
</html>
@@ -1,7 +1,7 @@
import React from 'react';
const About = () => (
<div className="sheetNameText">
<div>
<div>Github repo:</div>
<a
href="https://www.github.com/enuchi/React-Google-Apps-Script"
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<base target="_top" />
<!-- Add any external scripts and stylesheets here -->
</head>
<body>
<section id="index">
<!-- bundled js and css will get inlined here during build -->
</section>
</body>
</html>
-91
View File
@@ -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;
}
+1
View File
@@ -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;
+4 -4
View File
@@ -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,
};
});
};
+12 -4
View File
@@ -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);
};