Last active
March 27, 2021 01:34
-
-
Save jesseditson/0a40b3f4058e608ab39b7ba82d74ab95 to your computer and use it in GitHub Desktop.
Live Apps Workshop 03/29 code samples
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module.exports = { | |
tabWidth: 4, | |
jsxBracketSameLine: true, | |
trailingComma: "es5", | |
bracketSpacing: false, | |
quoteProps: "preserve", | |
parensSameLine: true, | |
arrowParens: "avoid", | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | |
}, | |
], | |
}, | |
]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}, | |
}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}, | |
}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}, | |
}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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