Skip to content

Instantly share code, notes, and snippets.

@jesseditson
Last active March 27, 2021 01:34
Show Gist options
  • Save jesseditson/0a40b3f4058e608ab39b7ba82d74ab95 to your computer and use it in GitHub Desktop.
Save jesseditson/0a40b3f4058e608ab39b7ba82d74ab95 to your computer and use it in GitHub Desktop.
Live Apps Workshop 03/29 code samples
module.exports = {
tabWidth: 4,
jsxBracketSameLine: true,
trailingComma: "es5",
bracketSpacing: false,
quoteProps: "preserve",
parensSameLine: true,
arrowParens: "avoid",
};
.root {
position: relative;
display: flex;
justify-content: center;
flex-direction: column;
}
ul,
li {
list-style-type: none;
padding: 0;
}
.flows {
display: grid;
grid-template-columns: 1fr 1fr;
}
.flow-cell {
border-radius: 16px;
box-shadow: 2px 2px 10px rgb(var(--quipUiColorGray6));
padding: 40px;
border: 4px solid rgb(var(--quipBackgroundInverse));
cursor: pointer;
margin: 10px;
}
.flow-cell .title {
font-size: 1.5em;
}
.flow .name {
font-size: 1.5em;
margin-bottom: 10px;
}
.flow .progress {
display: flex;
align-items: center;
position: relative;
color: rgb(var(--quipUiColorYellow1));
font-weight: bold;
background-color: rgba(var(--quipPurple), 0.2);
height: 30px;
}
.flow .progress .fill {
position: absolute;
background-color: rgba(var(--quipPurple), 0.4);
height: 100%;
}
.flow .progress .count {
position: absolute;
}
.step .header,
.step .body {
display: flex;
align-items: center;
}
.step .comments-trigger {
margin-right: 10px;
width: 20px;
height: 20px;
}
.step .title {
font-size: 1em;
margin-right: 20px;
}
.step .header input,
.step .header select {
font-size: 1em;
height: 20px;
cursor: pointer;
}
export interface FlowStep {
title: string;
description: string;
options?: string[];
substeps?: {
[key: string]: FlowStep[];
};
}
export interface Flow {
name: string;
steps: FlowStep[];
}
export const FLOWS: Flow[] = [
{
name: "Live App not loading",
steps: [
{
title: "Look at the Console",
description: "See if there are any errors",
},
{
title: "Isolate Extensions",
description: "Try reproducing in a private session",
},
{
title: "Check for known issues",
description: "Search github issues for similar reports",
},
{
title: "Check if first party",
description:
"See if this app is developed by Quip or a third party",
options: ["first party", "third party"],
substeps: {
"first party": [
{
title: "Gather information",
description: `What was the user doing when they see the bug?
How often does it appear?
Quip web or desktop?
What operating system and browser are they using?
Which doc did it appear in?`,
},
{
title: "Ask for access",
description:
"Can the customer share the doc or a copy of the doc with us?",
},
],
},
},
{
title: "Report to app developer",
description:
"Either use the feedback button or Quip issues to report a bug",
},
],
},
];
import quip from "quip-apps-api";
import {FLOWS, Flow, FlowStep} from "./flows";
interface StepState {
isDone: boolean;
selectedOption?: string;
}
export interface Step extends FlowStep {
key: string;
}
export interface AppData {
flows: Flow[];
selectedFlow?: Flow;
stepState: StepState[];
}
export class RootEntity extends quip.apps.RootRecord {
static ID = "flows";
static getProperties() {
return {};
}
static getDefaultProperties(): {[property: string]: any} {
return {};
}
getData(): AppData {
return {};
}
getActions() {
return {};
}
}
import quip from "quip-apps-api";
import {FLOWS, Flow, FlowStep} from "./flows";
interface StepState {
isDone: boolean;
selectedOption?: string;
}
export interface Step extends FlowStep {
key: string;
}
export interface AppData {
flows: Flow[];
selectedFlow?: Flow;
stepState: StepState[];
}
export class RootEntity extends quip.apps.RootRecord {
static ID = "flows";
static getProperties() {
return {
selectedFlow: "object",
stepState: "object",
};
}
static getDefaultProperties(): {[property: string]: any} {
return {
stepState: {},
};
}
private selectedFlow = () => this.get("selectedFlow") as Flow;
getData(): AppData {
const flows = FLOWS;
const selectedFlow = this.selectedFlow();
const stepState: StepState[] = [];
return {
flows,
selectedFlow,
stepState,
};
}
getActions() {
return {};
}
}
import quip from "quip-apps-api";
import {FLOWS, Flow, FlowStep} from "./flows";
interface StepState {
isDone: boolean;
selectedOption?: string;
}
export interface Step extends FlowStep {
key: string;
}
export interface AppData {
flows: Flow[];
selectedFlow?: Flow;
stepState: StepState[];
}
export class RootEntity extends quip.apps.RootRecord {
static ID = "flows";
static getProperties() {
return {
selectedFlow: "object",
stepState: "object",
};
}
static getDefaultProperties(): {[property: string]: any} {
return {
stepState: {},
};
}
private selectedFlow = () => this.get("selectedFlow") as Flow;
getData(): AppData {
const flows = FLOWS;
const selectedFlow = this.selectedFlow();
const stepState: StepState[] = [];
return {
flows,
selectedFlow,
stepState,
};
}
getActions() {
return {
onSelectFlow: (flow: Flow) => {
this.set("selectedFlow", flow);
this.notifyListeners();
},
onUpdateStepState: (
key: string,
isDone: boolean,
selectedOption?: string
) => {
const steps = this.get("stepState");
steps[key] = {
isDone,
selectedOption,
};
this.set("stepState", steps);
this.notifyListeners();
},
};
}
}
import React, {Component} from "react";
import quip from "quip-apps-api";
import {menuActions, Menu} from "../menus";
import {Flow} from "../model/flows";
import {Step, AppData, RootEntity} from "../model/root";
interface MainProps {
rootRecord: RootEntity;
menu: Menu;
isCreation: boolean;
creationUrl?: string;
}
interface MainState {
data: AppData;
}
export default class Main extends Component<MainProps, MainState> {
setupMenuActions_(rootRecord: RootEntity) {}
constructor(props: MainProps) {
super(props);
const {rootRecord} = props;
this.setupMenuActions_(rootRecord);
const data = rootRecord.getData();
this.state = {data};
}
componentDidMount() {
// Set up the listener on the rootRecord (RootEntity). The listener
// will propogate changes to the render() method in this component
// using setState
const {rootRecord} = this.props;
rootRecord.listen(this.refreshData_);
this.refreshData_();
}
componentWillUnmount() {
const {rootRecord} = this.props;
rootRecord.unlisten(this.refreshData_);
}
/**
* Update the app state using the RootEntity's AppData.
* This component will render based on the values of `this.state.data`.
* This function will set `this.state.data` using the RootEntity's AppData.
*/
private refreshData_ = () => {
const {rootRecord, menu} = this.props;
const data = rootRecord.getData();
// Update the app menu to reflect most recent app data
menu.updateToolbar(data);
this.setState({data: rootRecord.getData()});
};
private renderFlowListItem = (flow: Flow) => {
return (
<li className="flow-cell" key={flow.name}>
<h2 className="title">{flow.name}</h2>
</li>
);
};
render() {
const {flows} = this.state.data;
return (
<div className="root">
<ul className="flows">{flows.map(this.renderFlowListItem)}</ul>
</div>
);
}
}
import React, {Component} from "react";
import quip from "quip-apps-api";
import {menuActions, Menu} from "../menus";
import {Flow} from "../model/flows";
import {Step, AppData, RootEntity} from "../model/root";
interface MainProps {
rootRecord: RootEntity;
menu: Menu;
isCreation: boolean;
creationUrl?: string;
}
interface MainState {
data: AppData;
}
export default class Main extends Component<MainProps, MainState> {
setupMenuActions_(rootRecord: RootEntity) {}
constructor(props: MainProps) {
super(props);
const {rootRecord} = props;
this.setupMenuActions_(rootRecord);
const data = rootRecord.getData();
this.state = {data};
}
componentDidMount() {
// Set up the listener on the rootRecord (RootEntity). The listener
// will propogate changes to the render() method in this component
// using setState
const {rootRecord} = this.props;
rootRecord.listen(this.refreshData_);
this.refreshData_();
}
componentWillUnmount() {
const {rootRecord} = this.props;
rootRecord.unlisten(this.refreshData_);
}
/**
* Update the app state using the RootEntity's AppData.
* This component will render based on the values of `this.state.data`.
* This function will set `this.state.data` using the RootEntity's AppData.
*/
private refreshData_ = () => {
const {rootRecord, menu} = this.props;
const data = rootRecord.getData();
// Update the app menu to reflect most recent app data
menu.updateToolbar(data);
this.setState({data: rootRecord.getData()});
};
private renderFlowListItem = (flow: Flow) => {
const {onSelectFlow} = this.props.rootRecord.getActions();
const {selectedFlow} = this.state.data;
return (
<li
className="flow-cell"
key={flow.name}
onClick={() => onSelectFlow(flow)}>
{selectedFlow === flow ? "SELECTED" : null}
<h2 className="title">{flow.name}</h2>
</li>
);
};
render() {
const {flows} = this.state.data;
return (
<div className="root">
<ul className="flows">{flows.map(this.renderFlowListItem)}</ul>
</div>
);
}
}
import React, {Component} from "react";
import quip from "quip-apps-api";
import {menuActions, Menu} from "../menus";
import {Flow} from "../model/flows";
import {Step, AppData, RootEntity} from "../model/root";
interface MainProps {
rootRecord: RootEntity;
menu: Menu;
isCreation: boolean;
creationUrl?: string;
}
interface MainState {
data: AppData;
}
export default class Main extends Component<MainProps, MainState> {
setupMenuActions_(rootRecord: RootEntity) {}
constructor(props: MainProps) {
super(props);
const {rootRecord} = props;
this.setupMenuActions_(rootRecord);
const data = rootRecord.getData();
this.state = {data};
}
componentDidMount() {
// Set up the listener on the rootRecord (RootEntity). The listener
// will propogate changes to the render() method in this component
// using setState
const {rootRecord} = this.props;
rootRecord.listen(this.refreshData_);
this.refreshData_();
}
componentWillUnmount() {
const {rootRecord} = this.props;
rootRecord.unlisten(this.refreshData_);
}
/**
* Update the app state using the RootEntity's AppData.
* This component will render based on the values of `this.state.data`.
* This function will set `this.state.data` using the RootEntity's AppData.
*/
private refreshData_ = () => {
const {rootRecord, menu} = this.props;
const data = rootRecord.getData();
// Update the app menu to reflect most recent app data
menu.updateToolbar(data);
this.setState({data: rootRecord.getData()});
};
private renderFlowListItem = (flow: Flow) => {
const {onSelectFlow} = this.props.rootRecord.getActions();
return (
<li
className="flow-cell"
key={flow.name}
onClick={() => onSelectFlow(flow)}>
<h2 className="title">{flow.name}</h2>
</li>
);
};
private renderFlowList = (flows: Flow[]) => {
return <ul className="flows">{flows.map(this.renderFlowListItem)}</ul>;
};
private renderFlowStep = (step: Step, index: number) => {
return (
<li className="step" key={step.key}>
{step.title}
{step.description}
</li>
);
};
private renderFlow = (flow: Flow) => {
const {stepState} = this.state.data;
let finishedSteps = 0;
stepState.forEach(state => {
if (state && state.isDone) {
finishedSteps++;
}
});
const percent = (finishedSteps / flow.steps.length) * 100;
return (
<div className="flow">
<h1 className="name">{flow.name}</h1>
<div className="progress">
<span
className="fill"
style={{width: `${percent}%`}}></span>
<span
className="count"
style={{left: `calc(${percent}% - 30px)`}}>
{finishedSteps}/{flow.steps.length}
</span>
</div>
<ul className="steps">
{flow.steps.map(this.renderFlowStep)}
</ul>
</div>
);
};
render() {
const {flows, selectedFlow} = this.state.data;
return (
<div className="root">
{selectedFlow
? this.renderFlow(selectedFlow)
: this.renderFlowList(flows)}
</div>
);
}
}
import quip from "quip-apps-api";
import {FLOWS, Flow, FlowStep} from "./flows";
interface StepState {
isDone: boolean;
selectedOption?: string;
}
export interface Step extends FlowStep {
key: string;
}
export interface AppData {
flows: Flow[];
selectedFlow?: Flow;
visibleSteps: Step[];
stepState: StepState[];
}
export class RootEntity extends quip.apps.RootRecord {
static ID = "flows";
static getProperties() {
return {
selectedFlow: "object",
stepState: "object",
};
}
static getDefaultProperties(): {[property: string]: any} {
return {
stepState: {},
};
}
private selectedFlow = () => this.get("selectedFlow") as Flow;
private stepStateMap = () => this.get("stepState") as {[key: string]: StepState};
getData(): AppData {
const flows = FLOWS;
const selectedFlow = this.selectedFlow();
const stepStateMap = this.stepStateMap();
const stepState: StepState[] = [];
const visibleSteps: Step[] = [];
if (selectedFlow) {
// Flatten our nested steps into a list of selected steps
const addVisibleStep = (
step: FlowStep,
index: number,
parent: string
) => {
const keyName = `${parent}:${index}`;
visibleSteps.push({key: keyName, ...step});
const state = stepStateMap[keyName] || {};
stepState.push(state);
if (state.selectedOption && step.substeps) {
const substeps = step.substeps[state.selectedOption];
if (substeps) {
substeps.forEach((step, index) => {
addVisibleStep(step, index, keyName);
});
}
}
};
selectedFlow.steps.forEach((step, index) => {
addVisibleStep(step, index, selectedFlow.name);
});
}
return {
flows,
selectedFlow,
visibleSteps,
stepState,
};
}
getActions() {
return {
onSelectFlow: (flow: Flow) => {
this.set("selectedFlow", flow);
this.notifyListeners();
},
onUpdateStepState: (
key: string,
isDone: boolean,
selectedOption?: string
) => {
const steps = this.get("stepState");
steps[key] = {
isDone,
selectedOption,
};
this.set("stepState", steps);
this.notifyListeners();
},
};
}
}
import React, {Component} from "react";
import quip from "quip-apps-api";
import {menuActions, Menu} from "../menus";
import {Flow} from "../model/flows";
import {Step, AppData, RootEntity} from "../model/root";
interface MainProps {
rootRecord: RootEntity;
menu: Menu;
isCreation: boolean;
creationUrl?: string;
}
interface MainState {
data: AppData;
}
export default class Main extends Component<MainProps, MainState> {
setupMenuActions_(rootRecord: RootEntity) {}
constructor(props: MainProps) {
super(props);
const {rootRecord} = props;
this.setupMenuActions_(rootRecord);
const data = rootRecord.getData();
this.state = {data};
}
componentDidMount() {
// Set up the listener on the rootRecord (RootEntity). The listener
// will propogate changes to the render() method in this component
// using setState
const {rootRecord} = this.props;
rootRecord.listen(this.refreshData_);
this.refreshData_();
}
componentWillUnmount() {
const {rootRecord} = this.props;
rootRecord.unlisten(this.refreshData_);
}
/**
* Update the app state using the RootEntity's AppData.
* This component will render based on the values of `this.state.data`.
* This function will set `this.state.data` using the RootEntity's AppData.
*/
private refreshData_ = () => {
const {rootRecord, menu} = this.props;
const data = rootRecord.getData();
// Update the app menu to reflect most recent app data
menu.updateToolbar(data);
this.setState({data: rootRecord.getData()});
};
private renderFlowListItem = (flow: Flow) => {
const {onSelectFlow} = this.props.rootRecord.getActions();
return (
<li
className="flow-cell"
key={flow.name}
onClick={() => onSelectFlow(flow)}>
<h2 className="title">{flow.name}</h2>
</li>
);
};
private renderFlowList = (flows: Flow[]) => {
return <ul className="flows">{flows.map(this.renderFlowListItem)}</ul>;
};
private renderFlowStep = (step: Step, index: number) => {
const {stepState} = this.state.data;
const {onUpdateStepState} = this.props.rootRecord.getActions();
const state = stepState[index]!;
return (
<li
className="step"
key={step.key}>
<div
className="header"
onClick={() => onUpdateStepState(step.key, !state.isDone)}>
<h4 className="title">{step.title}</h4>
<input type="checkbox" checked={state.isDone} />
</div>
<div className="body">
<pre className="description">{step.description}</pre>
</div>
</li>
);
};
private renderFlow = (flow: Flow) => {
const {stepState, visibleSteps} = this.state.data;
let finishedSteps = 0;
stepState.forEach(state => {
if (state && state.isDone) {
finishedSteps++;
}
});
const percent = (finishedSteps / visibleSteps.length) * 100;
return (
<div className="flow">
<h1 className="name">{flow.name}</h1>
<div className="progress">
<span
className="fill"
style={{width: `${percent}%`}}></span>
<span
className="count"
style={{left: `calc(${percent}% - 30px)`}}>
{finishedSteps}/{visibleSteps.length}
</span>
</div>
<ul className="steps">
{visibleSteps.map(this.renderFlowStep)}
</ul>
</div>
);
};
render() {
const {flows, selectedFlow} = this.state.data;
return (
<div className="root">
{selectedFlow
? this.renderFlow(selectedFlow)
: this.renderFlowList(flows)}
</div>
);
}
}
import React, {Component} from "react";
import quip from "quip-apps-api";
import {menuActions, Menu} from "../menus";
import {Flow} from "../model/flows";
import {Step, AppData, RootEntity} from "../model/root";
interface MainProps {
rootRecord: RootEntity;
menu: Menu;
isCreation: boolean;
creationUrl?: string;
}
interface MainState {
data: AppData;
}
export default class Main extends Component<MainProps, MainState> {
setupMenuActions_(rootRecord: RootEntity) {}
constructor(props: MainProps) {
super(props);
const {rootRecord} = props;
this.setupMenuActions_(rootRecord);
const data = rootRecord.getData();
this.state = {data};
}
componentDidMount() {
// Set up the listener on the rootRecord (RootEntity). The listener
// will propogate changes to the render() method in this component
// using setState
const {rootRecord} = this.props;
rootRecord.listen(this.refreshData_);
this.refreshData_();
}
componentWillUnmount() {
const {rootRecord} = this.props;
rootRecord.unlisten(this.refreshData_);
}
/**
* Update the app state using the RootEntity's AppData.
* This component will render based on the values of `this.state.data`.
* This function will set `this.state.data` using the RootEntity's AppData.
*/
private refreshData_ = () => {
const {rootRecord, menu} = this.props;
const data = rootRecord.getData();
// Update the app menu to reflect most recent app data
menu.updateToolbar(data);
this.setState({data: rootRecord.getData()});
};
private renderFlowListItem = (flow: Flow) => {
const {onSelectFlow} = this.props.rootRecord.getActions();
return (
<li
className="flow-cell"
key={flow.name}
onClick={() => onSelectFlow(flow)}>
<h2 className="title">{flow.name}</h2>
</li>
);
};
private renderFlowList = (flows: Flow[]) => {
return <ul className="flows">{flows.map(this.renderFlowListItem)}</ul>;
};
private renderFlowStep = (step: Step, index: number) => {
const {stepState} = this.state.data;
const {onUpdateStepState} = this.props.rootRecord.getActions();
const state = stepState[index]!;
let select = <input type="checkbox" checked={state.isDone} />;
if (step.options) {
const updateStep = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
onUpdateStepState(step.key, value !== "none", value);
};
select = (
<select onChange={updateStep} value={state.selectedOption}>
<option value="none">Choose an option</option>
{step.options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
}
return (
<li
className="step"
key={step.key}>
<div
className="header"
onClick={
step.options
? undefined
: () => onUpdateStepState(step.key, !state.isDone)
}>
<h4 className="title">{step.title}</h4>
{select}
</div>
<div className="body">
<pre className="description">{step.description}</pre>
</div>
</li>
);
};
private renderFlow = (flow: Flow) => {
const {stepState, visibleSteps} = this.state.data;
let finishedSteps = 0;
stepState.forEach(state => {
if (state && state.isDone) {
finishedSteps++;
}
});
const percent = (finishedSteps / visibleSteps.length) * 100;
return (
<div className="flow">
<h1 className="name">{flow.name}</h1>
<div className="progress">
<span
className="fill"
style={{width: `${percent}%`}}></span>
<span
className="count"
style={{left: `calc(${percent}% - 30px)`}}>
{finishedSteps}/{visibleSteps.length}
</span>
</div>
<ul className="steps">
{visibleSteps.map(this.renderFlowStep)}
</ul>
</div>
);
};
render() {
const {flows, selectedFlow} = this.state.data;
return (
<div className="root">
{selectedFlow
? this.renderFlow(selectedFlow)
: this.renderFlowList(flows)}
</div>
);
}
}
import quip from "quip-apps-api";
export class Comment extends quip.apps.Record {
static ID = "comment";
static getProperties() {
return {
key: "string"
};
}
static getDefaultProperties(): {[property: string]: any} {
return {};
}
supportsComments() {
return true;
}
setKey(key: string) {
this.set("key", key);
}
key() {
return this.get("key");
}
private dom: HTMLElement | null = null;
setDom(dom: HTMLElement | null) {
this.dom = dom;
}
getDom(): HTMLElement {
return this.dom!;
}
}
import quip from "quip-apps-api";
import {FLOWS, Flow, FlowStep} from "./flows";
import {Comment} from "./comment";
interface StepState {
isDone: boolean;
selectedOption?: string;
}
export interface Step extends FlowStep {
key: string;
}
export interface AppData {
flows: Flow[];
selectedFlow?: Flow;
visibleSteps: Step[];
stepState: StepState[];
commentRecords: Comment[];
}
export class RootEntity extends quip.apps.RootRecord {
static ID = "flows";
static getProperties() {
return {
selectedFlow: "object",
stepState: "object",
comments: quip.apps.RecordList.Type(Comment),
};
}
static getDefaultProperties(): {[property: string]: any} {
return {
stepState: {},
comments: [],
};
}
private selectedFlow = () => this.get("selectedFlow") as Flow;
private stepStateMap = () => this.get("stepState") as {[key: string]: StepState};
private commentsMap = () => {
const comments = this.get("comments").getRecords() as Comment[];
const commentsMap: {[key: string]: Comment} = {};
comments.forEach(comment => {
commentsMap[comment.key()] = comment;
});
return commentsMap;
};
private backfillComments(flow: Flow) {
const commentsMap = this.commentsMap();
const fillComments = (
step: FlowStep,
parentName: string,
index: number
) => {
const keyName = `${parentName}:${index}`;
if (!commentsMap[keyName]) {
this.get("comments").add({key: keyName});
}
Object.keys(step.substeps || {}).forEach(subName => {
step.substeps![subName]!.forEach((step, index) => {
fillComments(step, keyName, index);
});
});
};
flow.steps.forEach((step, index) => {
fillComments(step, flow.name, index);
});
}
getData(): AppData {
const flows = FLOWS;
const selectedFlow = this.selectedFlow();
const stepStateMap = this.stepStateMap();
const commentsMap = this.commentsMap();
const stepState: StepState[] = [];
const commentRecords: Comment[] = [];
const visibleSteps: Step[] = [];
if (selectedFlow) {
this.backfillComments(selectedFlow);
const commentsMap = this.commentsMap();
// Flatten our nested steps into a list of selected steps
const addVisibleStep = (
step: FlowStep,
index: number,
parent: string
) => {
const keyName = `${parent}:${index}`;
visibleSteps.push({key: keyName, ...step});
const state: StepState = stepStateMap[keyName] || { isDone: false };
stepState.push(state);
commentRecords.push(commentsMap[keyName]);
if (state.selectedOption && step.substeps) {
const substeps = step.substeps[state.selectedOption];
if (substeps) {
substeps.forEach((step, index) => {
addVisibleStep(step, index, keyName);
});
}
}
};
selectedFlow.steps.forEach((step, index) => {
addVisibleStep(step, index, selectedFlow.name);
});
}
return {
flows,
selectedFlow,
visibleSteps,
stepState,
commentRecords,
};
}
getActions() {
return {
onSelectFlow: (flow: Flow) => {
this.set("selectedFlow", flow);
this.notifyListeners();
},
onUpdateStepState: (
key: string,
isDone: boolean,
selectedOption?: string
) => {
const steps = this.get("stepState");
steps[key] = {
isDone,
selectedOption,
};
this.set("stepState", steps);
this.notifyListeners();
},
};
}
}
import React, {Component} from "react";
import quip from "quip-apps-api";
import {menuActions, Menu} from "../menus";
import {Flow} from "../model/flows";
import {Step, AppData, RootEntity} from "../model/root";
interface MainProps {
rootRecord: RootEntity;
menu: Menu;
isCreation: boolean;
creationUrl?: string;
}
interface MainState {
data: AppData;
}
export default class Main extends Component<MainProps, MainState> {
setupMenuActions_(rootRecord: RootEntity) {}
constructor(props: MainProps) {
super(props);
const {rootRecord} = props;
this.setupMenuActions_(rootRecord);
const data = rootRecord.getData();
this.state = {data};
}
componentDidMount() {
// Set up the listener on the rootRecord (RootEntity). The listener
// will propogate changes to the render() method in this component
// using setState
const {rootRecord} = this.props;
rootRecord.listen(this.refreshData_);
this.refreshData_();
}
componentWillUnmount() {
const {rootRecord} = this.props;
rootRecord.unlisten(this.refreshData_);
}
/**
* Update the app state using the RootEntity's AppData.
* This component will render based on the values of `this.state.data`.
* This function will set `this.state.data` using the RootEntity's AppData.
*/
private refreshData_ = () => {
const {rootRecord, menu} = this.props;
const data = rootRecord.getData();
// Update the app menu to reflect most recent app data
menu.updateToolbar(data);
this.setState({data: rootRecord.getData()});
};
private renderFlowListItem = (flow: Flow) => {
const {onSelectFlow} = this.props.rootRecord.getActions();
return (
<li
className="flow-cell"
key={flow.name}
onClick={() => onSelectFlow(flow)}>
<h2 className="title">{flow.name}</h2>
</li>
);
};
private renderFlowList = (flows: Flow[]) => {
return <ul className="flows">{flows.map(this.renderFlowListItem)}</ul>;
};
private renderFlowStep = (step: Step, index: number) => {
const {stepState, commentRecords} = this.state.data;
const {onUpdateStepState} = this.props.rootRecord.getActions();
const state = stepState[index]!;
const commentRecord = commentRecords[index]!;
let select = <input type="checkbox" checked={state.isDone} />;
if (step.options) {
const updateStep = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
onUpdateStepState(step.key, value !== "none", value);
};
select = (
<select onChange={updateStep} value={state.selectedOption}>
<option value="none">Choose an option</option>
{step.options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
}
return (
<li
className="step"
key={step.key}
ref={dom => commentRecord.setDom(dom)}>
<div
className="header"
onClick={
step.options
? undefined
: () => onUpdateStepState(step.key, !state.isDone)
}>
<h4 className="title">{step.title}</h4>
{select}
</div>
<div className="body">
<quip.apps.ui.CommentsTrigger
record={commentRecord}
showEmpty={true}
/>
<pre className="description">{step.description}</pre>
</div>
</li>
);
};
private renderFlow = (flow: Flow) => {
const {stepState, visibleSteps} = this.state.data;
let finishedSteps = 0;
stepState.forEach(state => {
if (state && state.isDone) {
finishedSteps++;
}
});
const percent = (finishedSteps / visibleSteps.length) * 100;
return (
<div className="flow">
<h1 className="name">{flow.name}</h1>
<div className="progress">
<span
className="fill"
style={{width: `${percent}%`}}></span>
<span
className="count"
style={{left: `calc(${percent}% - 30px)`}}>
{finishedSteps}/{visibleSteps.length}
</span>
</div>
<ul className="steps">
{visibleSteps.map(this.renderFlowStep)}
</ul>
</div>
);
};
render() {
const {flows, selectedFlow} = this.state.data;
return (
<div className="root">
{selectedFlow
? this.renderFlow(selectedFlow)
: this.renderFlowList(flows)}
</div>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment