Flow Card
Some flows allow the user to fill out sections independently from one another. Other flows allow sections to be optional. Unordered Flow Cards allow for both of these scenarios.
const UnorderedPreview = () => {const [active, setActive] = React.useState(undefined);const [saved, setSaved] = React.useState([]);const saveValue = (value) => setSaved(prevState => [...prevState, value]);const card0 = {default: {title: "Goal",content: <BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>,headerAction: <Button fill="outline" small primary onClick={() => setActive(0)}>Add Goal</Button>},active: {title: "Conversion Goal",content: (<Stack direction="column" spacing={4}>{/* First Section */}<StackItem fill><Stack direction="column" spacing={2}><BodyText bold>Primary Goal Metric</BodyText><div className="m-t-2 w-100" style={{ maxWidth: 400 }}><AnvilSelectoptions={[{text: "Estimate is sold", value: 1}]}value={{text: "Estimate is sold", value: 1}}/></div><BodyText size="small" subdued italic className="m-t-2">Hint: What factor will determine if the campaign is successful?</BodyText></Stack></StackItem>{/* Second Section */}<StackItem fill><Stack direction="column" spacing={3}><BodyText bold>Tracking Number</BodyText><BodyText size="small" subdued className="m-t-2">A unique Tracking Number will be used to attribute revenue to your campaign. It’s important to not use Tracking Numbers across multiple campaigns, or your reporting metrics will be inaccurate.</BodyText><ButtonGroup className="m-t-2"><Button primary fill="outline">Add Tracking Number</Button><Button fill="subtle">Enter Number Manually</Button></ButtonGroup></Stack></StackItem></Stack>),headerAction: <Button fill="outline" small primary>Add Goal</Button>,footerAction: (<ButtonGroup className="flex-row-reverse"><ButtonprimaryclassName="m-l-1"onClick={() => {setActive(undefined);saveValue("card0");}}>Save</Button><Button onClick={() => setActive(undefined)}>Cancel</Button></ButtonGroup>)},saved: {content: (<Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Primary goal metric</Eyebrow><BodyText>Estimate is sold</BodyText></Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Tracking number</Eyebrow><BodyText>(999) 999-9999</BodyText></Stack></Stack>),headerAction: <Button fill="outline" small onClick={() => setActive(0)}>Edit Goal</Button>}}const card1 = {default: {title: "Delivery",content: <BodyText subdued italic>Do you want to send this one-time or automate it?</BodyText>,headerAction: <Button fill="outline" small primary onClick={() => setActive(1)}>Edit Delivery</Button>},active: {content: (<Form><Stack direction="column" spacing={4}><div style={{ maxWidth: 295 }}><Form.ButtonTogglelabel="Delivery Logic"options={[{text: 'Automated',value: 'Automated',selected: true},{text: 'One-Time',value: 'One-Time'}]}/></div><div style={{ maxWidth: 295 }}><Form.AnvilSelectoptions={[{text: "Called", value: 1}]}value={{text: "Called", value: 1}}label="Send Emails Until..."/></div><Form.Input className="m-b-0" label="Sender Name" value="John at Plumb Gurus" /><FormGroup><Form.Input label="Sender Email (domain added automatically)" value="john.smith" /><BodyText className="align-self-end p-b-1" subdued>@plumbguys.servicetitanmail.io</BodyText></FormGroup></Stack></Form>),headerAction: <Button fill="outline" small primary>Add Goal</Button>,footerAction: (<ButtonGroup className="flex-row-reverse"><ButtonprimaryclassName="m-l-1"onClick={() => {setActive(undefined);saveValue("card1");}}>Save</Button><Button onClick={() => setActive(undefined)}>Cancel</Button></ButtonGroup>)},saved: {content: (<Stack direction="column" spacing={3}><Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Delivery logic</Eyebrow><BodyText>Automated</BodyText></Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Send until</Eyebrow><BodyText>Called</BodyText></Stack></Stack><Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Sender name</Eyebrow><BodyText>John at Plumb Gurus</BodyText></Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Sender Email</Eyebrow><BodyText>john.smith@plumbguys.servicetitanmail.io</BodyText></Stack></Stack></Stack>),headerAction: <Button fill="outline" small onClick={() => setActive(1)}>Edit Delivery</Button>}}return (<FlowCard currentIndex={active}><FlowCard.Steptitle={card0.default.title}content={active === 0 ? card0.active.content : saved.includes("card0") ? card0.saved.content : card0.default.content}headerAction={active !== 0 ? saved.includes("card0") ? card0.saved.headerAction : card0.default.headerAction : null}footerAction={active === 0 && card0.active.footerAction}saved={saved.includes("card0")}disabled={active !== undefined && active !== 0}/><FlowCard.Steptitle={card1.default.title}content={active === 1 ? card1.active.content : saved.includes("card1") ? card1.saved.content : card1.default.content}headerAction={active !== 1 && saved.includes("card1") ? card1.saved.headerAction : card1.default.headerAction}footerAction={active === 1 && card1.active.footerAction}saved={saved.includes("card1")}disabled={active !== undefined && active !== 1}/></FlowCard>)}render (UnorderedPreview);
Commonly Flow Cards are used so that a user fills out each step sequentially.
const OrderedPreview = () => {const [active, setActive] = React.useState(0);const [saved, setSaved] = React.useState([]);const [value, setValue] = React.useState([]);const [dRevenue, setDRevenue] = React.useState('Yes');const saveValue = (value) => setSaved(prevState => [...prevState, value]);const onClick = (value, checked) => {if (checked) {setValue(prevState => [...prevState].filter(item => item !== value))} else {setValue(prevState => [...prevState, value])}};// Made separate data object so it is easier to see how component worksconst card0 = {default: {title: "Basic Info",content: <BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>,headerAction: <Button fill="outline" small onClick={() => setActive(0)}>Edit</Button>,},active: {content: (<Stack direction="column" spacing={4}>{/* First Section */}<StackItem fill><Stack direction="column" spacing={2}><BodyText bold>Select the trade(s) you want to include in your membership package.</BodyText><Toggleboxvalue="HVAC"onClick={onClick}checked={value.includes("HVAC")}label={"HVAC"}/><Toggleboxvalue="Plumbing"onClick={onClick}checked={value.includes("Plumbing")}label={"Plumbing"}/><Toggleboxvalue="Electrical"onClick={onClick}checked={value.includes("Electrical")}label={"Electrical"}/></Stack></StackItem></Stack>),footerAction: (<ButtonGroup className="flex-row-reverse"><Buttonprimarydisabled={value.length === 0}onClick={() => {setActive(1);saveValue("card0");}}>Next</Button><Button onClick={() => setActive(undefined)}>Cancel</Button></ButtonGroup>)},saved: {content: (<Stack direction="column" spacing={3}><Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>MEMBERSHIP TRADE(S)</Eyebrow><BodyText>{value.map((item, index) => {if(index + 1 < value.length) return `${item} and `return item})}</BodyText></Stack></Stack></Stack>),headerAction: <Button fill="outline" small onClick={() => setActive(0)}>Edit</Button>}}const card1 = {default: {title: "Billing",content: <BodyText subdued italic>Importing data into ServiceTitan brings over your customers, equipment, and jobs so you can go live faster.</BodyText>,headerAction: <Button fill="outline" small onClick={() => setActive(1)}>Edit</Button>,},active: {content: (<Form><Stack direction="column" spacing={4}><div style={{ maxWidth: 295 }}><Form.ButtonTogglelabel="Do you use deferred revenue?"onChange={(value) => {console.log(value); setDRevenue(value)}}value={dRevenue}options={[{text: 'Yes',value: 'Yes',selected: true},{text: 'No',value: 'No'}]}/></div><Stack direction="column"><BodyText size="large" className="m-b-2">Residential Memberships</BodyText><Form.Input label="How much do you charge for 1 year paid up front?" value="150.00" shortLabel='$' /><Form.Input label="How much do you charge for monthly ongoing billing?" value="15.00" shortLabel='$' /></Stack></Stack></Form>),footerAction: (<ButtonGroup className="flex-row-reverse"><ButtonprimaryonClick={() => {setActive(undefined);saveValue("card1");}}>Complete</Button><Button onClick={() => setActive(undefined)}>Cancel</Button></ButtonGroup>)},saved: {content: (<Stack direction="column" spacing={3}><Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Deferred revenue</Eyebrow><BodyText>{dRevenue}</BodyText></Stack></Stack><Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Yearly Charge</Eyebrow><BodyText>$150.00</BodyText></Stack><Stack direction="column" className="w-100" style={{maxWidth: 240}}><Eyebrow>Monthly Charge</Eyebrow><BodyText>$15.00</BodyText></Stack></Stack></Stack>),}}return (<FlowCard ordered currentIndex={active}><FlowCard.Steptitle={card0.default.title}content={active === 0 ? card0.active.content : saved.includes("card0") ? card0.saved.content : card0.default.content}headerAction={active !== 0 ? saved.includes("card0") ? card0.saved.headerAction : card0.default.headerAction : null}footerAction={active === 0 && card0.active.footerAction}saved={saved.includes("card0")}disabled={active !== undefined && active < 0}/><FlowCard.Steptitle={card1.default.title}content={active === 1 ? card1.active.content : saved.includes("card1") ? card1.saved.content : card1.default.content}headerAction={active === 1 ? null : saved.includes("card0") ? card0.default.headerAction : null}footerAction={active === 1 && card1.active.footerAction}saved={saved.includes("card1")}disabled={active !== undefined && active < 1}/></FlowCard>)}render (OrderedPreview);
<FlowCard.Steptitle="Goal"content={<Stack direction="column" spacing={2}><BodyText bold>Primary Goal Metric</BodyText><div className="m-t-2 w-100" style={{ maxWidth: 400 }}><AnvilSelectoptions={[{text: "Estimate is sold", value: 1}]}value={{text: "Estimate is sold", value: 1}}/></div><BodyText size="small" subdued italic className="m-t-2">Hint: What factor will determine if the campaign is successful?</BodyText></Stack>}footerAction={<ButtonGroup className="flex-row-reverse"><Buttonprimary>Save</Button><Button>Cancel</Button></ButtonGroup>}active/>
const BasicExample = () => {const [active, setActive] = React.useState(0);const [saved, setSaved] = React.useState({item00: false,item01: false,item02: false,});const cancelStep = () => setActive(undefined);const editStep = (target) => setActive(target);const saveStep = (current) => {if (active === current && active !== undefined) {setSaved((prevState) => ({...prevState,[`item0${current}`]: true,}));setActive(current + 1);}};return (<FlowCard currentIndex={active}><FlowCard.Steptitle="Step #1"saved={saved.item00}headerAction={active !== 0 && (<Button small onClick={() => editStep(0)}>Edit</Button>)}content={active === 0 && <div>This is FlowCard Content</div>}footerAction={active === 0 && (<ButtonGroup className="flex-row-reverse"><ButtonprimaryonClick={() => saveStep(0)}>Save</Button><Button onClick={cancelStep}>Cancel</Button></ButtonGroup>)}/><FlowCard.Steptitle="Step #2"saved={saved.item01}headerAction={active !== 1 && (<Button small onClick={() => editStep(1)}>Edit</Button>)}content={active === 1 && <div>This is FlowCard Content</div>}footerAction={active === 1 && (<ButtonGroup className="flex-row-reverse"><ButtonprimaryonClick={() => saveStep(1)}>Save</Button><Button onClick={cancelStep}>Cancel</Button></ButtonGroup>)}/><FlowCard.Steptitle="Step #3"saved={saved.item02}headerAction={active !== 2 && (<Button small onClick={() => editStep(2)}>Edit</Button>)}content={active === 2 && <div>This is FlowCard Content</div>}footerAction={active === 2 && (<ButtonGroup className="flex-row-reverse"><ButtonprimaryonClick={() => saveStep(2)}>Save</Button><Button onClick={cancelStep}>Cancel</Button></ButtonGroup>)}/></FlowCard>);};render (BasicExample);
const CustomOrderedNumber = () => {const [active, setActive] = React.useState(undefined);const [saved, setSaved] = React.useState({item00: false,item01: false,item02: false,});const cancelStep = () => setActive(undefined);const editStep = (target) => setActive(target);const saveStep = (current) => {if (active === current && active !== undefined) {setSaved((prevState) => ({...prevState,[`item0${current}`]: true,}));setActive(current + 1);}};return (<FlowCard ordered currentIndex={active}><FlowCard.Steptitle="Step #1"saved={saved.item00}headerAction={active !== 0 && (<Button small onClick={() => editStep(0)}>Edit</Button>)}content={active === 0 && <div>This is FlowCard Content</div>}footerAction={active === 0 && (<ButtonGroup className="flex-row-reverse"><ButtonprimaryonClick={() => saveStep(0)}>Save</Button><Button onClick={cancelStep}>Cancel</Button></ButtonGroup>)}/><FlowCard.Steptitle="Step #2.2"orderNumber={<span style={{ fontSize: 12 }}>2.2</span>}saved={saved.item01}headerAction={active !== 1 && (<Button small onClick={() => editStep(1)}>Edit</Button>)}content={active === 1 && <div>This is FlowCard Content</div>}footerAction={active === 1 && (<ButtonGroup className="flex-row-reverse"><ButtonprimaryonClick={() => saveStep(1)}>Save</Button><Button onClick={cancelStep}>Cancel</Button></ButtonGroup>)}/><FlowCard.Steptitle="Step #3"saved={saved.item02}headerAction={active !== 2 && (<Button small onClick={() => editStep(2)}>Edit</Button>)}content={active === 2 && <div>This is FlowCard Content</div>}footerAction={active === 2 && (<ButtonGroup className="flex-row-reverse"><ButtonprimaryonClick={() => saveStep(2)}>Save</Button><Button onClick={cancelStep}>Cancel</Button></ButtonGroup>)}/></FlowCard>);};render (CustomOrderedNumber);
When an edit state has significant amount of content, a Modal or Takeover can be used, and opening a new page should be considered.
const CustomContent = () => {const [modalOpen, setModalOpen] = React.useState(false);const [takeoverOpen, setTakeoverOpen] = React.useState(false);const toggleModal = () => setModalOpen(!modalOpen);const toggleTakeover = () => setTakeoverOpen(!takeoverOpen);const longContent = () => (<Form><Form.Group widths="equal"><Form.Input placeholder="First data goes here..." label="Data #1" /><Form.Input placeholder="Second data goes here..." label="Data #2" /></Form.Group><Form.Input placeholder="Third data goes here..." label="Data #3" /><Form.ButtonTogglelabel="Toggle"options={[{text: 'Yes',value: 'Yes',selected: true},{text: 'No',value: 'No'}]}/><Form.Group widths="equal"><Form.Input placeholder="Fourth data goes here..." label="Data #4" /><Form.Input placeholder="Fifth data goes here..." label="Data #5" /></Form.Group><Form.Input placeholder="Sixth data goes here..." label="Data #6" /><Form.ButtonTogglelabel="Toggle"options={[{text: 'Yes',value: 'Yes',selected: true},{text: 'No',value: 'No'}]}/><Form.Group widths="equal"><Form.Input placeholder="Seventh data goes here..." label="Data #7" /><Form.Input placeholder="Eighth data goes here..." label="Data #8" /></Form.Group><Form.Input placeholder="Ninth data goes here..." label="Data #9" /><Form.ButtonTogglelabel="Toggle"options={[{text: 'Yes',value: 'Yes',selected: true},{text: 'No',value: 'No'}]}/></Form>)return (<><FlowCard><FlowCard.Steptitle="Modal Example"content={<BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>}headerAction={<Button fill="outline" small primary onClick={ () => toggleModal() }>Edit</Button>}/><FlowCard.Steptitle="Takeover Example"content={<BodyText subdued italic>How do you want to track if this campaign is successful?</BodyText>}headerAction={<Button fill="outline" small primary onClick={ () => toggleTakeover() }>Edit</Button>}/></FlowCard><Modalopen={modalOpen}size={Modal.Sizes.S}title="Form to fill out"footer={<Button primary onClick={() => toggleModal()}>Close</Button>}>{longContent()}</Modal>{takeoverOpen && (<Takeovertitle="Takeover for long content"onClose={() => toggleTakeover()}footer={<Button primary onClick={() => toggleTakeover()}>Close</Button>}portal>{longContent()}</Takeover>)}</>)}render (CustomContent);
import { FlowCard, FlowCardStep } from '@servicetitan/design-system';