groups: add componentised spinner and behaviours

This commit is contained in:
Matilde Park 2020-04-01 17:28:24 -04:00 committed by Matilde Park
parent 48e65499e2
commit afec74cb2e
14 changed files with 162 additions and 145 deletions

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

@ -181,16 +181,6 @@ class UrbitApi {
}) })
} }
setSpinner(boolean) {
store.handleEvent({
data: {
local: {
spinner: boolean
}
}
})
}
setSelected(selected) { setSelected(selected) {
store.handleEvent({ store.handleEvent({
data: { data: {

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom'; import { Route, Link } from 'react-router-dom';
import { InviteSearch } from './invite-search'; import { InviteSearch } from './invite-search';
import { Spinner } from './icons/icon-spinner';
export class AddScreen extends Component { export class AddScreen extends Component {
@ -11,7 +12,8 @@ export class AddScreen extends Component {
invites: { invites: {
groups: [], groups: [],
ships: [] ships: []
} },
awaiting: false
}; };
this.invChange = this.invChange.bind(this); this.invChange = this.invChange.bind(this);
@ -38,12 +40,12 @@ export class AddScreen extends Component {
invites: { invites: {
groups: [], groups: [],
ships: [] ships: []
} },
awaiting: true
}, () => { }, () => {
props.api.setSpinner(true);
let submit = props.api.group.add(props.path, aud); let submit = props.api.group.add(props.path, aud);
submit.then(() => { submit.then(() => {
props.api.setSpinner(false); this.setState({awaiting: false});
props.history.push("/~groups" + props.path); props.history.push("/~groups" + props.path);
}) })
}); });
@ -51,14 +53,6 @@ export class AddScreen extends Component {
render() { render() {
const { props } = this; const { props } = this;
let invErrElem = (<span />);
if (this.state.inviteError) {
invErrElem = (
<span className="f9 inter red2 ml3 mb5 db">
Invites must be validly formatted ship names.
</span>
);
}
return ( return (
<div className="h-100 w-100 flex flex-column overflow-y-scroll white-d"> <div className="h-100 w-100 flex flex-column overflow-y-scroll white-d">
@ -86,6 +80,7 @@ export class AddScreen extends Component {
<Link to="/~groups"> <Link to="/~groups">
<button className="f8 ml4 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button> <button className="f8 ml4 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button>
</Link> </Link>
<Spinner awaiting={this.state.awaiting} classes="mt4 pl4" text="Inviting to group..." />
</div> </div>
</div> </div>
) )

View File

@ -4,6 +4,7 @@ import { Sigil } from './icons/sigil';
import { api } from '/api'; import { api } from '/api';
import { Route, Link } from 'react-router-dom'; import { Route, Link } from 'react-router-dom';
import { EditElement } from '/components/lib/edit-element'; import { EditElement } from '/components/lib/edit-element';
import { Spinner } from './icons/icon-spinner';
import { uxToHex } from '/lib/util'; import { uxToHex } from '/lib/util';
export class ContactCard extends Component { export class ContactCard extends Component {
@ -16,7 +17,9 @@ export class ContactCard extends Component {
emailToSet: null, emailToSet: null,
phoneToSet: null, phoneToSet: null,
websiteToSet: null, websiteToSet: null,
notesToSet: null notesToSet: null,
awaiting: false,
type: "Saving to group"
}; };
this.editToggle = this.editToggle.bind(this); this.editToggle = this.editToggle.bind(this);
this.sigilColorSet = this.sigilColorSet.bind(this); this.sigilColorSet = this.sigilColorSet.bind(this);
@ -44,16 +47,6 @@ export class ContactCard extends Component {
}); });
return; return;
} }
// sigil color updates are done by keystroke parsing on update
// other field edits are exclusively handled by setField()
let currentColor = (props.contact.color) ? props.contact.color : "000000";
currentColor = uxToHex(currentColor);
let hexExp = /([0-9A-Fa-f]{6})/
let hexTest = hexExp.exec(this.state.colorToSet);
if (hexTest && (hexTest[1] !== currentColor) && !props.share) {
api.contactEdit(props.path, `~${props.ship}`, {color: hexTest[1]});
}
} }
editToggle() { editToggle() {
@ -123,8 +116,11 @@ export class ContactCard extends Component {
let hexTest = hexExp.exec(this.state.colorToSet); let hexTest = hexExp.exec(this.state.colorToSet);
if (hexTest && (hexTest[1] !== currentColor) && !props.share) { if (hexTest && (hexTest[1] !== currentColor) && !props.share) {
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, `~${props.ship}`, { color: hexTest[1] }).then(() => { api.contactEdit(props.path, `~${props.ship}`, { color: hexTest[1] }).then(() => {
this.setState({ awaiting: false });
}); });
}))
} }
break; break;
} }
@ -137,7 +133,11 @@ export class ContactCard extends Component {
} }
let emailTestResult = emailTest.exec(state.emailToSet); let emailTestResult = emailTest.exec(state.emailToSet);
if (emailTestResult) { if (emailTestResult) {
api.contactEdit(props.path, ship, { email: state.emailToSet }); this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { email: state.emailToSet }).then(() => {
this.setState({awaiting: false});
});
}))
} }
break; break;
} }
@ -148,7 +148,12 @@ export class ContactCard extends Component {
) { ) {
return false; return false;
} }
api.contactEdit(props.path, ship, { nickname: state.nickNameToSet }); this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { nickname: state.nickNameToSet }).then(() => {
this.setState({ awaiting: false });
});
}))
break; break;
} }
case "notes": { case "notes": {
@ -158,7 +163,11 @@ export class ContactCard extends Component {
) { ) {
return false; return false;
} }
api.contactEdit(props.path, ship, { notes: state.notesToSet }); this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { notes: state.notesToSet }).then(() => {
this.setState({ awaiting: false });
});
}))
break; break;
} }
case "phone": { case "phone": {
@ -170,7 +179,11 @@ export class ContactCard extends Component {
} }
let phoneTestResult = phoneTest.exec(state.phoneToSet); let phoneTestResult = phoneTest.exec(state.phoneToSet);
if (phoneTestResult) { if (phoneTestResult) {
api.contactEdit(props.path, ship, { phone: state.phoneToSet }); this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { phone: state.phoneToSet }).then(() => {
this.setState({ awaiting: false });
});
}))
} }
break; break;
} }
@ -183,37 +196,60 @@ export class ContactCard extends Component {
} }
let websiteTestResult = websiteTest.exec(state.websiteToSet); let websiteTestResult = websiteTest.exec(state.websiteToSet);
if (websiteTestResult) { if (websiteTestResult) {
api.contactEdit(props.path, ship, { website: state.websiteToSet }); this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { website: state.websiteToSet }).then(() => {
this.setState({ awaiting: false });
});
}))
} }
break; break;
} }
case "removeAvatar": { case "removeAvatar": {
api.contactEdit(props.path, ship, { avatar: null }); this.setState({ awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { avatar: null }).then(() => {
this.setState({ awaiting: false });
});
}))
break; break;
} }
case "removeEmail": { case "removeEmail": {
this.setState({ emailToSet: "" }); this.setState({ emailToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { email: "" }); api.contactEdit(props.path, ship, { email: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break; break;
} }
case "removeNickname": { case "removeNickname": {
this.setState({ nicknameToSet: "" }); this.setState({ nicknameToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { nickname: "" }); api.contactEdit(props.path, ship, { nickname: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break; break;
} }
case "removePhone": { case "removePhone": {
this.setState({ phoneToSet: "" }); this.setState({ phoneToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { phone: "" }); api.contactEdit(props.path, ship, { phone: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break; break;
} }
case "removeWebsite": { case "removeWebsite": {
this.setState({ websiteToSet: "" }); this.setState({ websiteToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { website: "" }); api.contactEdit(props.path, ship, { website: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break; break;
} }
case "removeNotes": { case "removeNotes": {
this.setState({ notesToSet: "" }); this.setState({ notesToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { notes: "" }); api.contactEdit(props.path, ship, { notes: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break; break;
} }
} }
@ -253,13 +289,13 @@ export class ContactCard extends Component {
color: this.pickFunction(state.colorToSet, defaultVal.color), color: this.pickFunction(state.colorToSet, defaultVal.color),
avatar: null avatar: null
}; };
api.setSpinner(true); this.setState({awaiting: true, type: "Sharing with group"}, (() => {
api.contactView.share( api.contactView.share(
`~${props.ship}`, props.path, `~${window.ship}`, contact `~${props.ship}`, props.path, `~${window.ship}`, contact
).then(() => { ).then(() => {
api.setSpinner(false); props.history.push(`/~groups/view${props.path}/${window.ship}`)
props.history.push(`/~groups/view${props.path}/${window.ship}`) });
}); }))
} }
removeFromGroup() { removeFromGroup() {
@ -280,13 +316,14 @@ export class ContactCard extends Component {
`~${props.ship}`, props.path, `~${window.ship}`, contact `~${props.ship}`, props.path, `~${window.ship}`, contact
); );
api.setSpinner(true); this.setState({awaiting: true, type: "Removing from group"}, (() => {
api.contactHook.remove(props.path, `~${props.ship}`).then(() => { api.contactHook.remove(props.path, `~${props.ship}`).then(() => {
api.setSpinner(false); let destination = (props.ship === window.ship)
let destination = (props.ship === window.ship) ? "" : props.path;
? "" : props.path; this.setState({awaiting: false});
props.history.push(`/~groups${destination}`); props.history.push(`/~groups${destination}`);
}); });
}))
} }
renderEditCard() { renderEditCard() {
@ -548,6 +585,7 @@ export class ContactCard extends Component {
</button> </button>
</div> </div>
<div className="h-100 w-100 overflow-x-hidden pb8 white-d">{card}</div> <div className="h-100 w-100 overflow-x-hidden pb8 white-d">{card}</div>
<Spinner awaiting={this.state.awaiting} text={`${this.state.type}...`} classes="absolute right-1 bottom-1 ba pa2 b--gray1-d" />
</div> </div>
); );
} }

View File

@ -3,9 +3,16 @@ import { Route, Link } from 'react-router-dom';
import { ContactItem } from '/components/lib/contact-item'; import { ContactItem } from '/components/lib/contact-item';
import { ShareSheet } from '/components/lib/share-sheet'; import { ShareSheet } from '/components/lib/share-sheet';
import { Sigil } from '../lib/icons/sigil'; import { Sigil } from '../lib/icons/sigil';
import { Spinner } from '../lib/icons/icon-spinner';
import { cite } from '../../lib/util'; import { cite } from '../../lib/util';
export class ContactSidebar extends Component { export class ContactSidebar extends Component {
constructor(props) {
super(props);
this.state = {
awaiting: false
}
}
render() { render() {
const { props } = this; const { props } = this;
@ -71,11 +78,12 @@ export class ContactSidebar extends Component {
<p className={"v-mid f9 mh3 red2 pointer " + adminOpt} <p className={"v-mid f9 mh3 red2 pointer " + adminOpt}
style={{paddingTop: 6}} style={{paddingTop: 6}}
onClick={() => { onClick={() => {
props.api.setSpinner(true); this.setState({awaiting: true}, (() => {
props.api.groupRemove(props.path, [`~${member}`]) props.api.groupRemove(props.path, [`~${member}`])
.then(() => { .then(() => {
props.api.setSpinner(false); this.setState({awaiting: false})
}) })
}))
}}> }}>
Remove Remove
</p> </p>
@ -107,6 +115,7 @@ export class ContactSidebar extends Component {
{contactItems} {contactItems}
{groupItems} {groupItems}
</div> </div>
<Spinner awaiting={this.state.awaiting} text="Removing from group..." classes="pa2 ba absolute right-1 bottom-1 b--gray1-d" />
</div> </div>
); );
} }

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom'; import { Route, Link } from 'react-router-dom';
import { Spinner } from './icons/icon-spinner';
import { deSig, uxToHex } from '/lib/util.js'; import { deSig, uxToHex } from '/lib/util.js';
export class GroupDetail extends Component { export class GroupDetail extends Component {
@ -8,6 +9,8 @@ export class GroupDetail extends Component {
this.state = { this.state = {
title: "", title: "",
description: "", description: "",
awaiting: false,
type: "Editing"
} }
this.changeTitle = this.changeTitle.bind(this); this.changeTitle = this.changeTitle.bind(this);
this.changeDescription = this.changeDescription.bind(this); this.changeDescription = this.changeDescription.bind(this);
@ -199,17 +202,18 @@ export class GroupDetail extends Component {
onChange={this.changeTitle} onChange={this.changeTitle}
onBlur={() => { onBlur={() => {
if (groupOwner) { if (groupOwner) {
props.api.setSpinner(true); this.setState({awaiting: true}, (() => {
props.api.metadataAdd( props.api.metadataAdd(
association['app-path'], association['app-path'],
association['group-path'], association['group-path'],
this.state.title, this.state.title,
association.metadata.description, association.metadata.description,
association.metadata['date-created'], association.metadata['date-created'],
uxToHex(association.metadata.color) uxToHex(association.metadata.color)
).then(() => { ).then(() => {
props.api.setSpinner(false); this.setState({awaiting: false})
}) })
}))
} }
}} }}
/> />
@ -226,17 +230,18 @@ export class GroupDetail extends Component {
onChange={this.changeDescription} onChange={this.changeDescription}
onBlur={() => { onBlur={() => {
if (groupOwner) { if (groupOwner) {
props.api.setSpinner(true); this.setState({awaiting: true}, (() => {
props.api.metadataAdd( props.api.metadataAdd(
association['app-path'], association['app-path'],
association['group-path'], association['group-path'],
association.metadata.title, association.metadata.title,
this.state.description, this.state.description,
association.metadata['date-created'], association.metadata['date-created'],
uxToHex(association.metadata.color) uxToHex(association.metadata.color)
).then(() => { ).then(() => {
props.api.setSpinner(false); this.setState({awaiting: false})
}) })
}))
} }
}} }}
/> />
@ -248,14 +253,15 @@ export class GroupDetail extends Component {
<a className={"dib f9 ba pa2 " + deleteButtonClasses} <a className={"dib f9 ba pa2 " + deleteButtonClasses}
onClick={() => { onClick={() => {
if (groupOwner) { if (groupOwner) {
props.api.setSpinner(true); this.setState({awaiting: true, type: "Deleting"}, (() => {
props.api.contactView.delete(props.path).then(() => { props.api.contactView.delete(props.path).then(() => {
props.api.setSpinner(false); props.history.push("/~groups");
props.history.push("/~groups"); })
}) }))
} }
}}>Delete this group</a> }}>Delete this group</a>
</div> </div>
<Spinner awaiting={this.state.awaiting} text={`${this.state.type} group...`} classes="pa2 ba absolute right-1 bottom-1 b--gray1-d"/>
</div> </div>
) )
} }

View File

@ -7,15 +7,6 @@ export class HeaderBar extends Component {
let popout = window.location.href.includes("popout/") let popout = window.location.href.includes("popout/")
? "dn" : "dn db-m db-l db-xl"; ? "dn" : "dn db-m db-l db-xl";
// let spinner = !!this.props.spinner
// ? this.props.spinner : false;
// let spinnerClasses = "";
// if (spinner === true) {
// spinnerClasses = "spin-active";
// }
let invites = (this.props.invites && this.props.invites.contacts) let invites = (this.props.invites && this.props.invites.contacts)
? this.props.invites.contacts ? this.props.invites.contacts
: {}; : {};

View File

@ -1,16 +0,0 @@
import React, { Component } from "react";
export class IconHome extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
return (
<img
className={"invert-d " + classes}
src="/~groups/img/Home.png"
width={16}
height={16}
/>
);
}
}

View File

@ -1,9 +1,25 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
export class IconSpinner extends Component { export class Spinner extends Component {
render() { render() {
return (
<div className="spinner-pending"></div> let classes = !!this.props.classes ? this.props.classes : "";
); let text = !!this.props.text ? this.props.text : "";
let awaiting = !!this.props.awaiting ? this.props.awaiting : false;
if (awaiting) {
return (
<div className={classes + " z-2 bg-white bg-gray0-d white-d"}>
<img className="invert-d spin-active v-mid"
src="/~groups/img/Spinner.png"
width={16}
height={16} />
<p className="dib f9 ml2 v-mid inter">{text}</p>
</div>
);
}
else {
return null;
}
} }
} }

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import { Route, Link } from 'react-router-dom'; import { Route, Link } from 'react-router-dom';
import { InviteSearch } from './lib/invite-search'; import { InviteSearch } from './lib/invite-search';
import { Spinner } from './lib/icons/icon-spinner';
import { deSig } from '/lib/util'; import { deSig } from '/lib/util';
import urbitOb from 'urbit-ob'; import urbitOb from 'urbit-ob';
@ -19,6 +20,7 @@ export class NewScreen extends Component {
}, },
// color: '', // color: '',
groupNameError: false, groupNameError: false,
awaiting: false
}; };
this.groupNameChange = this.groupNameChange.bind(this); this.groupNameChange = this.groupNameChange.bind(this);
@ -64,16 +66,16 @@ export class NewScreen extends Component {
this.setState({ this.setState({
error: false, error: false,
success: true, success: true,
invites: '' invites: '',
awaiting: true
}, () => { }, () => {
props.api.setSpinner(true);
props.api.contactView.create( props.api.contactView.create(
group, group,
aud, aud,
this.state.title, this.state.title,
this.state.description this.state.description
).then(() => { ).then(() => {
props.api.setSpinner(false); this.setState({awaiting: false});
props.history.push(`/~groups${group}`); props.history.push(`/~groups${group}`);
}) })
}); });
@ -147,6 +149,7 @@ export class NewScreen extends Component {
<Link to="/~groups"> <Link to="/~groups">
<button className="f9 ml3 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button> <button className="f9 ml3 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button>
</Link> </Link>
<Spinner awaiting={this.state.awaiting} classes="mt4" text="Creating group..." />
</div> </div>
</div> </div>
); );

View File

@ -45,7 +45,6 @@ export class Root extends Component {
return ( return (
<Skeleton <Skeleton
activeDrawer="groups" activeDrawer="groups"
spinner={state.spinner}
selectedGroups={selectedGroups} selectedGroups={selectedGroups}
history={props.history} history={props.history}
api={api} api={api}
@ -67,7 +66,6 @@ export class Root extends Component {
render={ (props) => { render={ (props) => {
return ( return (
<Skeleton <Skeleton
spinner={state.spinner}
history={props.history} history={props.history}
selectedGroups={selectedGroups} selectedGroups={selectedGroups}
api={api} api={api}
@ -100,7 +98,6 @@ export class Root extends Component {
return ( return (
<Skeleton <Skeleton
spinner={state.spinner}
history={props.history} history={props.history}
selectedGroups={selectedGroups} selectedGroups={selectedGroups}
api={api} api={api}
@ -141,7 +138,6 @@ export class Root extends Component {
return ( return (
<Skeleton <Skeleton
spinner={state.spinner}
history={props.history} history={props.history}
selectedGroups={selectedGroups} selectedGroups={selectedGroups}
api={api} api={api}
@ -185,7 +181,6 @@ export class Root extends Component {
return ( return (
<Skeleton <Skeleton
spinner={state.spinner}
history={props.history} history={props.history}
api={api} api={api}
selectedGroups={selectedGroups} selectedGroups={selectedGroups}
@ -235,7 +230,6 @@ export class Root extends Component {
return ( return (
<Skeleton <Skeleton
spinner={state.spinner}
history={props.history} history={props.history}
api={api} api={api}
selectedGroups={selectedGroups} selectedGroups={selectedGroups}
@ -271,7 +265,6 @@ export class Root extends Component {
return ( return (
<Skeleton <Skeleton
spinner={state.spinner}
history={props.history} history={props.history}
api={api} api={api}
selectedGroups={selectedGroups} selectedGroups={selectedGroups}

View File

@ -12,7 +12,7 @@ export class Skeleton extends Component {
return ( return (
<div className="h-100 w-100 ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl"> <div className="h-100 w-100 ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl">
<HeaderBar spinner={props.spinner} invites={props.invites} associations={props.associations} /> <HeaderBar invites={props.invites} associations={props.associations} />
<div className="cf w-100 h-100 h-100-m-40-ns flex ba-m ba-l ba-xl b--gray4 b--gray1-d br1"> <div className="cf w-100 h-100 h-100-m-40-ns flex ba-m ba-l ba-xl b--gray4 b--gray1-d br1">
<GroupSidebar <GroupSidebar
contacts={props.contacts} contacts={props.contacts}
@ -26,7 +26,7 @@ export class Skeleton extends Component {
associations={props.associations} associations={props.associations}
/> />
<div <div
className={"h-100 w-100 " + rightPanelClasses} className={"h-100 w-100 relative " + rightPanelClasses}
style={{ flexGrow: 1 }}> style={{ flexGrow: 1 }}>
{props.children} {props.children}
</div> </div>

View File

@ -4,17 +4,10 @@ export class LocalReducer {
reduce(json, state) { reduce(json, state) {
let data = _.get(json, 'local', false); let data = _.get(json, 'local', false);
if (data) { if (data) {
this.setSpinner(data, state);
this.setSelected(data, state); this.setSelected(data, state);
} }
} }
setSpinner(json, state) {
let data = _.has(json, 'spinner', false);
if (data) {
state.spinner = json.spinner;
}
}
setSelected(json, state) { setSelected(json, state) {
let data = _.has(json, 'selected', false); let data = _.has(json, 'selected', false);
if (data) { if (data) {

View File

@ -15,8 +15,7 @@ class Store {
associations: {}, associations: {},
permissions: {}, permissions: {},
invites: {}, invites: {},
selectedGroups: [], selectedGroups: []
spinner: false
}; };
this.initialReducer = new InitialReducer(); this.initialReducer = new InitialReducer();