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) {
store.handleEvent({
data: {

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { InviteSearch } from './invite-search';
import { Spinner } from './icons/icon-spinner';
export class AddScreen extends Component {
@ -11,7 +12,8 @@ export class AddScreen extends Component {
invites: {
groups: [],
ships: []
}
},
awaiting: false
};
this.invChange = this.invChange.bind(this);
@ -38,12 +40,12 @@ export class AddScreen extends Component {
invites: {
groups: [],
ships: []
}
},
awaiting: true
}, () => {
props.api.setSpinner(true);
let submit = props.api.group.add(props.path, aud);
submit.then(() => {
props.api.setSpinner(false);
this.setState({awaiting: false});
props.history.push("/~groups" + props.path);
})
});
@ -51,14 +53,6 @@ export class AddScreen extends Component {
render() {
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 (
<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">
<button className="f8 ml4 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button>
</Link>
<Spinner awaiting={this.state.awaiting} classes="mt4 pl4" text="Inviting to group..." />
</div>
</div>
)

View File

@ -4,6 +4,7 @@ import { Sigil } from './icons/sigil';
import { api } from '/api';
import { Route, Link } from 'react-router-dom';
import { EditElement } from '/components/lib/edit-element';
import { Spinner } from './icons/icon-spinner';
import { uxToHex } from '/lib/util';
export class ContactCard extends Component {
@ -16,7 +17,9 @@ export class ContactCard extends Component {
emailToSet: null,
phoneToSet: null,
websiteToSet: null,
notesToSet: null
notesToSet: null,
awaiting: false,
type: "Saving to group"
};
this.editToggle = this.editToggle.bind(this);
this.sigilColorSet = this.sigilColorSet.bind(this);
@ -44,16 +47,6 @@ export class ContactCard extends Component {
});
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() {
@ -123,8 +116,11 @@ export class ContactCard extends Component {
let hexTest = hexExp.exec(this.state.colorToSet);
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(() => {
this.setState({ awaiting: false });
});
}))
}
break;
}
@ -137,7 +133,11 @@ export class ContactCard extends Component {
}
let emailTestResult = emailTest.exec(state.emailToSet);
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;
}
@ -148,7 +148,12 @@ export class ContactCard extends Component {
) {
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;
}
case "notes": {
@ -158,7 +163,11 @@ export class ContactCard extends Component {
) {
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;
}
case "phone": {
@ -170,7 +179,11 @@ export class ContactCard extends Component {
}
let phoneTestResult = phoneTest.exec(state.phoneToSet);
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;
}
@ -183,37 +196,60 @@ export class ContactCard extends Component {
}
let websiteTestResult = websiteTest.exec(state.websiteToSet);
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;
}
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;
}
case "removeEmail": {
this.setState({ emailToSet: "" });
api.contactEdit(props.path, ship, { email: "" });
this.setState({ emailToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { email: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
case "removeNickname": {
this.setState({ nicknameToSet: "" });
api.contactEdit(props.path, ship, { nickname: "" });
this.setState({ nicknameToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { nickname: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
case "removePhone": {
this.setState({ phoneToSet: "" });
api.contactEdit(props.path, ship, { phone: "" });
this.setState({ phoneToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { phone: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
case "removeWebsite": {
this.setState({ websiteToSet: "" });
api.contactEdit(props.path, ship, { website: "" });
this.setState({ websiteToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { website: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
case "removeNotes": {
this.setState({ notesToSet: "" });
api.contactEdit(props.path, ship, { notes: "" });
this.setState({ notesToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { notes: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
}
@ -253,13 +289,13 @@ export class ContactCard extends Component {
color: this.pickFunction(state.colorToSet, defaultVal.color),
avatar: null
};
api.setSpinner(true);
api.contactView.share(
`~${props.ship}`, props.path, `~${window.ship}`, contact
).then(() => {
api.setSpinner(false);
props.history.push(`/~groups/view${props.path}/${window.ship}`)
});
this.setState({awaiting: true, type: "Sharing with group"}, (() => {
api.contactView.share(
`~${props.ship}`, props.path, `~${window.ship}`, contact
).then(() => {
props.history.push(`/~groups/view${props.path}/${window.ship}`)
});
}))
}
removeFromGroup() {
@ -280,13 +316,14 @@ export class ContactCard extends Component {
`~${props.ship}`, props.path, `~${window.ship}`, contact
);
api.setSpinner(true);
api.contactHook.remove(props.path, `~${props.ship}`).then(() => {
api.setSpinner(false);
let destination = (props.ship === window.ship)
? "" : props.path;
props.history.push(`/~groups${destination}`);
});
this.setState({awaiting: true, type: "Removing from group"}, (() => {
api.contactHook.remove(props.path, `~${props.ship}`).then(() => {
let destination = (props.ship === window.ship)
? "" : props.path;
this.setState({awaiting: false});
props.history.push(`/~groups${destination}`);
});
}))
}
renderEditCard() {
@ -548,6 +585,7 @@ export class ContactCard extends Component {
</button>
</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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ export class Skeleton extends Component {
return (
<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">
<GroupSidebar
contacts={props.contacts}
@ -26,7 +26,7 @@ export class Skeleton extends Component {
associations={props.associations}
/>
<div
className={"h-100 w-100 " + rightPanelClasses}
className={"h-100 w-100 relative " + rightPanelClasses}
style={{ flexGrow: 1 }}>
{props.children}
</div>

View File

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

View File

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