Merge pull request #2286 from urbit/ixv/publish-members-settings

OS1: added members and settings page to publish
This commit is contained in:
ixv 2020-02-14 14:34:43 -08:00 committed by GitHub
commit d49987bcc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 485 additions and 171 deletions

View File

@ -1,6 +1,7 @@
::
/- *publish,
*group-store,
*group-hook,
*permission-hook,
*permission-group-hook,
*permission-store,
@ -295,7 +296,9 @@
[~ this]
=/ who=@p (slav %p i.t.wir)
=/ book=@tas i.t.t.wir
[~ this(subs (~(del by subs) who book))]
=/ del [%del-book who book]
:_ this(subs (~(del by subs) who book))
[%give %fact [/primary]~ %publish-primary-delta !>(del)]~
:: Resubscribe to any subscription we get kicked from. The case of actually
:: getting banned from a notebook is handled by %watch-ack
::
@ -703,49 +706,31 @@
++ handle-permission-update
|= upd=permission-update
^- (quip card _state)
?+ -.upd
?. ?=(?(%remove %add) -.upd)
[~ state]
::
%remove
=/ book=(unit @tas)
%+ roll ~(tap by books)
|= [[nom=@tas book=notebook] out=(unit @tas)]
?: =(path.upd subscribers.book)
`nom
out
?~ book
[~ state]
:_ state
%- zing
%+ turn ~(tap in who.upd)
|= who=@p
?: (allowed who %read u.book)
~
=/ book=(unit @tas)
%+ roll ~(tap by books)
|= [[nom=@tas book=notebook] out=(unit @tas)]
?: =(path.upd subscribers.book)
`nom
out
?~ book
[~ state]
:_ state
%- zing
%+ turn ~(tap in who.upd)
|= who=@p
?. (allowed who %read u.book)
[%give %kick [/notebook/[u.book]]~ `who]~
::
%add
=/ book=(unit @tas)
%+ roll ~(tap by books)
|= [[nom=@tas book=notebook] out=(unit @tas)]
?: =(path.upd subscribers.book)
`nom
out
?~ book
[~ state]
:_ state
%- zing
%+ turn ~(tap in who.upd)
|= who=@p
?. (allowed who %read u.book)
~
=/ uid (sham %publish who u.book eny.bol)
=/ inv=invite
:* our.bol %publish /notebook/[u.book] who
(crip "invite for notebook {<our.bol>}/{(trip u.book)}")
==
=/ act=invite-action [%invite /publish uid inv]
[%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)]~
==
?: ?=(%remove -.upd)
~
=/ uid (sham %publish who u.book eny.bol)
=/ inv=invite
:* our.bol %publish /notebook/[u.book] who
(crip "invite for notebook {<our.bol>}/{(trip u.book)}")
==
=/ act=invite-action [%invite /publish uid inv]
[%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)]~
::
++ handle-invite-update
|= upd=invite-update
@ -870,6 +855,11 @@
^- card
[%pass / %agent [our.bol %group-store] %poke %group-action !>(act)]
::
++ group-hook-poke
|= act=group-hook-action
^- card
[%pass / %agent [our.bol %group-hook] %poke %group-hook-action !>(act)]
::
++ contact-view-create
|= act=[%create path (set ship)]
^- card
@ -989,8 +979,9 @@
%- zing
:~ [(group-poke [%bundle write-path])]~
[(group-poke [%bundle read-path])]~
[(group-hook-poke [%add our.bol write-path])]~
[(group-hook-poke [%add our.bol read-path])]~
[(group-poke [%add (sy our.bol ~) write-path])]~
[(group-poke [%add (sy our.bol ~) read-path])]~
(create-security read-path write-path %journal)
[(perm-hook-poke [%add-owned write-path write-path])]~
[(perm-hook-poke [%add-owned read-path read-path])]~
@ -1148,11 +1139,22 @@
%del-book
?. (team:title our.bol src.bol)
~|("action not permitted" !!)
?. (~(has by books) book.act)
=/ book=(unit notebook) (~(get by books) book.act)
?~ book
~|("nonexistent notebook {<book.act>}" !!)
=/ pax=path /app/publish/notebooks/[book.act]
:_ state
[(delete-dir pax)]~
?> ?=(^ writers.u.book)
?> ?=(^ subscribers.u.book)
=/ cards=(list card)
:~ (delete-dir pax)
(perm-hook-poke [%remove writers.u.book])
(perm-hook-poke [%remove subscribers.u.book])
==
=? cards =('~' i.writers.u.book)
[(group-poke [%unbundle writers.u.book]) cards]
=? cards =('~' i.subscribers.u.book)
[(group-poke [%unbundle subscribers.u.book]) cards]
[cards state]
::
%del-note
?: &(=(src.bol our.bol) !=(our.bol who.act))
@ -1279,7 +1281,17 @@
?- -.del
%add-book
=. tile-num (add tile-num (get-unread data.del))
(emit-updates-and-state host.del book.del data.del del sty)
?: =(our.bol host.del)
(emit-updates-and-state host.del book.del data.del del sty)
=/ write-pax writers.data.del
=/ read-pax subscribers.data.del
=^ cards state
(emit-updates-and-state host.del book.del data.del del sty)
:_ state
:* (group-hook-poke [%add host.del write-pax])
(group-hook-poke [%add host.del read-pax])
cards
==
::
%add-note
=/ book=(unit notebook)
@ -1347,14 +1359,14 @@
(emit-updates-and-state host.del book.del u.book del sty)
::
%del-book
=. tile-num
%+ sub tile-num
(get-unread (~(got by books) book.del))
?: =(our.bol host.del)
:_ sty(books (~(del by books.sty) book.del))
:~ [%give %fact [/notebook/[book.del]]~ %publish-notebook-delta !>(del)]
[%give %fact [/primary]~ %publish-primary-delta !>(del)]
==
=. tile-num
%+ sub tile-num
(get-unread (~(got by subs) host.del book.del))
=/ jon=json
(frond:enjs:format %notifications (numb:enjs:format tile-num.sty))
:_ sty(subs (~(del by subs.sty) host.del book.del))

View File

@ -195,6 +195,15 @@ a {
color: #7F7F7F;
}
.options::after {
content: "⌃";
transform: rotate(180deg);
position: absolute;
right: 4px;
top: 6px;
color: #7f7f7f;
}
[contenteditable]:focus {
outline: 0px solid transparent;
}

View File

@ -32,6 +32,9 @@ export class Comments extends Component {
}
render() {
if (!this.props.enabled) {
return null;
}
let commentArray = this.props.comments.map((com, i) => {
return (
<CommentItem

View File

@ -0,0 +1,73 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export class Dropdown extends Component {
constructor(props) {
super(props);
this.toggleDropdown = this.toggleDropdown.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
this.collapseAndDispatch = this.collapseAndDispatch.bind(this);
this.state = {
open: false
}
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
handleClickOutside(evt) {
if (this.optsList && !this.optsList.contains(evt.target) &&
this.optsButton && !this.optsButton.contains(evt.target)) {
this.setState({open: false});
}
}
toggleDropdown() {
this.setState({open: !this.state.open});
}
collapseAndDispatch(action){
this.setState({open: false}, action);
}
render() {
let display = (this.state.open)
? "block" : "none";
let optionsColor = (this.state.open)
? '#e6e6e6' : 'white';
let optionsList = this.props.options.map((val, i) => {
return (
<button key={i} className={val.cls}
onClick={() => this.collapseAndDispatch(val.action)}>
{val.txt}
</button>
);
});
return (
<div className="options relative dib"
ref={(el) => {this.optsButton = el}}>
<button className="pr3 mb1 pointer br2 pa2 pr4"
style={{backgroundColor: optionsColor}}
onClick={this.toggleDropdown}>
{this.props.buttonText}
</button>
<div className="absolute flex flex-column pa4 ba b--gray4 br2 z-1 bg-white"
ref={(el) => {this.optsList = el}}
style={{right:0, width:this.props.width, display: display}}>
{optionsList}
</div>
</div>
)
}
}
export default Dropdown

View File

@ -98,7 +98,7 @@ export class NewPost extends Component {
popout={props.popout}
/>
<button
className="v-mid w-100 mw7 tl pl4 h1"
className={"v-mid w-100 mw7 tl h1 pl4"}
disabled={!state.submit}
style={submitStyle}
onClick={this.postSubmit}>

View File

@ -15,7 +15,8 @@ export class NewScreen extends Component {
groups: [],
ships: []
},
createGroup: false
createGroup: false,
awaiting: false,
};
this.idChange = this.idChange.bind(this);
@ -27,9 +28,8 @@ export class NewScreen extends Component {
componentDidUpdate(prevProps) {
const { props, state } = this;
if (props.notebooks && (("~" + window.ship) in props.notebooks)) {
let notebookId = stringToSymbol(state.idName)
if (notebookId in props.notebooks["~" + window.ship]) {
let notebook = `/~${window.ship}/${notebookId}`;
if (state.awaiting in props.notebooks["~" + window.ship]) {
let notebook = `/~${window.ship}/${state.awaiting}`;
props.history.push("/~publish/notebook" + notebook);
}
}
@ -92,7 +92,9 @@ export class NewScreen extends Component {
}
}
props.api.action("publish", "publish-action", action);
this.setState({awaiting: bookId}, () => {
props.api.action("publish", "publish-action", action);
});
}
render() {

View File

@ -5,10 +5,7 @@ import { Comments } from './comments';
import { NoteNavigation } from './note-navigation';
import moment from 'moment';
import ReactMarkdown from 'react-markdown';
//TODO ask for note if we don't have it
//TODO initialise note if no state
//TODO if comments are disabled on the notebook, don't render comments
export class Note extends Component {
constructor(props){
super(props);
@ -38,6 +35,14 @@ export class Note extends Component {
}
componentWillMount() {
let readAction = {
read: {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note,
}
}
window.api.action("publish", "publish-action", readAction);
window.api.fetchNote(this.props.ship, this.props.book, this.props.note);
}
@ -189,7 +194,7 @@ export class Note extends Component {
ship={props.ship}
book={props.book}
/>
<Comments
<Comments enabled={notebook.comments}
ship={props.ship}
book={props.book}
note={props.note}

View File

@ -6,8 +6,6 @@ import { Subscribers } from './subscribers';
import { Settings } from './settings';
import Sidebar from './sidebar';
//TODO subcomponent logic for subscribers, settings
export class Notebook extends Component {
constructor(props){
super(props);
@ -46,13 +44,15 @@ export class Notebook extends Component {
}
componentDidUpdate(prevProps) {
if (!this.props.notebooks[this.props.ship][this.props.book].notes) {
let notebook = this.props.notebooks[this.props.ship][this.props.book];
if (!notebook.subscribers) {
window.api.fetchNotebook(this.props.ship, this.props.book);
}
}
componentDidMount() {
if (this.props.notebooks[this.props.ship][this.props.book].notes) {
let notebook = this.props.notebooks[this.props.ship][this.props.book];
if (notebook.notes) {
this.onScroll();
}
}
@ -82,9 +82,9 @@ export class Notebook extends Component {
let tabStyles = {
posts: "bb b--gray4 gray2 pv4 ph2",
about: "bb b--gray4 gray2 pv4 ph2"
// subscribers: "bb b--gray4 gray2 pv4 ph2",
// settings: "bb b--gray4 pr2 gray2 pv4 ph2",
about: "bb b--gray4 gray2 pv4 ph2",
subscribers: "bb b--gray4 gray2 pv4 ph2",
settings: "bb b--gray4 pr2 gray2 pv4 ph2",
};
tabStyles[props.view] = "bb b--black black pv4 ph2";
@ -104,12 +104,22 @@ export class Notebook extends Component {
case "about":
inner = <p className="f8 lh-solid">{notebook.about}</p>
break;
// case "subscribers":
// inner = <Subscribers/>
// break;
// case "settings":
// inner = <Settings/>
// break;
case "subscribers":
inner = <Subscribers
host={this.props.ship}
book={this.props.book}
notebook={notebook}
permissions={this.props.permissions}
groups={this.props.groups}/>
break;
case "settings":
inner = <Settings
host={this.props.ship}
book={this.props.book}
notebook={notebook}
groups={this.props.groups}
history={this.props.history}/>
break;
default:
break;
}
@ -148,6 +158,18 @@ export class Notebook extends Component {
Unsubscribe
</button>
let subsComponent = (this.props.ship.slice(1) !== window.ship)
? null
: <Link to={subs} className={tabStyles.subscribers}>
Subscribers
</Link>;
let settingsComponent = (this.props.ship.slice(1) !== window.ship)
? null
: <Link to={settings} className={tabStyles.settings}>
Settings
</Link>;
return (
<div
className="center mw6 f9 h-100"
@ -200,8 +222,9 @@ export class Notebook extends Component {
<Link to={about} className={tabStyles.about}>
About
</Link>
<div
className="bb b--gray4 gray2 pv4 ph2"
{subsComponent}
{settingsComponent}
<div className="bb b--gray4 gray2 pv4 ph2"
style={{ flexGrow: 1 }}></div>
</div>

View File

@ -1,41 +1,39 @@
import React, { Component } from 'react';
//TODO Settings for owned notebooks
export class Settings extends Component {
render() {
return (
<div>
<div className="flex flex-column mb8">
<label for="name" className="f9">Share</label>
<small id="name-desc" className="f9 mb2 gray3">Share a link to this notebook</small>
<div className="flex">
<input style={{flex: "1"}} id="name" placeholder="dopzod.arvo.network/4f5hsS" className="input-reset bt bl bb pa3 gray4" type="text" aria-describedby="name-desc"/>
<button className="bt br bb pa3 b--gray4">Copy</button>
</div>
</div>
constructor(props){
super(props)
this.deleteNotebook = this.deleteNotebook.bind(this);
}
<div className="flex flex-column mb8">
<label for="name" className="f9">Rename</label>
<small id="name-desc" className="f9 mb2 gray3">Change the name of this notebook</small>
<div className="flex">
<input style={{flex: "1"}} id="name" placeholder="Notebook Name" className="input-reset ba pa3 gray4" type="text" aria-describedby="name-desc"/>
</div>
</div>
<div className="flex flex-column">
<label for="name" className="f9">Export</label>
<small id="name-desc" className="f9 mb2 gray3">Change the name of this notebook</small>
<button className="bg-black white pa3">
<div className="flex justify-between">
<div>Export Notebook</div>
<div></div>
</div>
deleteNotebook(){
let action = {
"del-book": {
book: this.props.book
}
}
window.api.action("publish", "publish-action", action);
this.props.history.push('/~publish');
}
render() {
if (this.props.host.slice(1) === window.ship) {
return (
<div className="flex-column">
<p className="f9 mt3 lh-copy db">Delete Notebook</p>
<p className="f9 gray2 db mb4">
Permanently delete this notebook. (All current members will no longer see this notebook)
</p>
<button className="b--red2 red2 pointer dib f9 ba pa2"
onClick={this.deleteNotebook}>
Delete this notebook
</button>
</div>
</div>
)
)
} else {
return null;
}
}
}

View File

@ -1,15 +0,0 @@
import React, { Component } from 'react'
//TODO fill sigil/avatar + name from props
export class SubscriberItem extends Component {
render() {
return (
<div>
</div>
)
}
}
export default SubscriberItem

View File

@ -1,53 +1,184 @@
import React, { Component } from 'react';
import { SubscriberItem } from './subscriber-item';
//TODO map list of subscriber-items from props
import { Dropdown } from './dropdown';
export class Subscribers extends Component {
constructor(props){
super(props);
this.redirect = this.redirect.bind(this);
this.addUser = this.addUser.bind(this);
this.removeUser = this.removeUser.bind(this);
}
addUser(who, path) {
let action = {
add: {
members: [who],
path: path,
}
}
window.api.action("group-store", "group-action", action);
}
removeUser(who, path) {
let action = {
remove: {
members: [who],
path: path,
}
}
window.api.action("group-store", "group-action", action);
}
redirect(url) {
window.location.href = url;
}
render() {
let readPath = this.props.notebook["subscribers-group-path"]
let readPerms = (readPath)
? this.props.permissions[readPath]
: null;
let writePath = this.props.notebook["writers-group-path"]
let writePerms = (writePath)
? this.props.permissions[writePath]
: null;
let writers = [];
if (writePerms && writePerms.kind === 'white') {
let withoutUs = new Set(writePerms.who)
withoutUs.delete(window.ship);
writers = Array.from(withoutUs).map((who, i) => {
let width = 0;
let options = [];
if (readPath === writePath) {
width = 258;
let url = `/~contacts${writePath}`;
options = [{
cls: "tl pointer",
txt: "Manage this group in the contacts view",
action: () => {this.redirect(url)}
}];
} else {
width = 157;
options = [{
cls: "tl pointer",
txt: "Demote to subscriber",
action: () => {this.removeUser(`~${who}`, writePath)}
}];
}
return (
<div className="flex justify-between" key={i}>
<div className="f9 mono mr2">{`~${who}`}</div>
<Dropdown
options={options}
width={width}
buttonText={"Options"}
/>
</div>
)
});
}
if (writers.length === 0) {
writers =
<div className="f9">
There are no participants on this notebook.
</div>
}
let subscribers = null;
if (readPath !== writePath) {
if (this.props.notebook.subscribers){
let width = 162;
subscribers = this.props.notebook.subscribers.map((who, i) => {
let options = [
{ cls: "tl mb2 pointer",
txt: "Promote to participant",
action: () => {this.addUser(who, writePath)}
},
{ cls: "tl red2 pointer",
txt: "Ban",
action: () => {this.addUser(who, readPath)}
},
];
return (
<div className="flex justify-between" key={i}>
<div className="f9 mono mr2">{who}</div>
<Dropdown
options={options}
width={width}
buttonText={"Options"}
/>
</div>
)
});
}
if (subscribers.length === 0) {
subscribers =
<div className="f9">
There are no subscribers to this notebook.
</div>
}
}
let subsContainer = (readPath === writePath)
? null
: <div className="flex flex-column">
<div className="f9 gray2 mt6 mb3">Subscribers (read access only)</div>
{subscribers}
</div>;
let bannedContainer = null;
if (readPerms && readPerms.kind === 'black') {
let width = 72;
let banned = Array.from(readPerms.who).map((who, i) => {
let options = [{
cls: "tl red2 pointer",
txt: "Unban",
action: () => {this.removeUser(`~${who}`, readPath)}
}];
return (
<div className="flex justify-between" key={i}>
<div className="f9 mono mr2">{`~${who}`}</div>
<Dropdown
options={options}
width={width}
buttonText={"Options"}
/>
</div>
)
});
if (banned.length === 0) {
banned =
<div className="f9">
There are no users banned from this notebook.
</div>
}
bannedContainer =
<div className="flex flex-column">
<div className="f9 gray2 mt6 mb3">Banned</div>
{banned}
</div>;
}
return (
<div>
<div className="flex flex-column">
<div className="f9 gray2">Host</div>
<div className="flex justify-between mt3">
<div className="flex">
<div className="f9 mono mr2">~fabled-faster</div>
<div className="f9 gray2">Last active</div>
<div className="flex flex-column">
<div className="f9 gray2">Host</div>
<div className="flex justify-between mt3">
<div className="f9 mono mr2">{this.props.host}</div>
</div>
<div className="f9">Options </div>
</div>
</div>
<div className="flex flex-column">
<div className="f9 gray2 mt6">Participants (read and write access)</div>
<div className="f9 mt3">There are no paticipants in this notebook.</div>
<div className="flex justify-between mt3">
<div className="flex">
<div className="f9 mono mr2">~fabled-faster</div>
<div className="f9 gray2">Last active</div>
<div className="flex flex-column">
<div className="f9 gray2 mt6 mb3">
Participants (read and write access)
</div>
<div className="f9">Options </div>
{writers}
</div>
</div>
<div className="flex flex-column">
<div className="f9 gray2 mt6 mb3">Subscribers (read access only)</div>
<div className="flex justify-between">
<div className="flex">
<div className="f9 mono mr2">~fabled-faster</div>
<div className="f9 gray2">Last active</div>
</div>
<div className="f9">Options </div>
</div>
</div>
<div className="flex flex-column">
<div className="f9 gray2 mt6 mb3">Banned</div>
<div className="flex justify-between">
<div className="flex">
<div className="f9 mono mr2">~fabled-faster</div>
<div className="f9 gray2">Last active</div>
</div>
<div className="f9">Options </div>
</div>
</div>
{subsContainer}
{bannedContainer}
</div>
)
}

View File

@ -143,6 +143,7 @@ export class Root extends Component {
contacts={notebookContacts}
sidebarShown={state.sidebarShown}
popout={popout}
permissions={state.permissions}
{...props}
/>
</Skeleton>

View File

@ -0,0 +1,58 @@
import _ from 'lodash';
export class PermissionReducer {
reduce(json, state) {
let data = _.get(json, 'permission-initial', false);
if (data) {
for (let perm in data) {
state.permissions[perm] = {
who: new Set(data[perm].who),
kind: data[perm].kind
}
}
}
data = _.get(json, 'permission-update', false);
if (data) {
this.create(data, state);
this.delete(data, state);
this.add(data, state);
this.remove(data, state);
}
}
create(json, state) {
let data = _.get(json, 'create', false);
if (data) {
state.permissions[data.path] = {
kind: data.kind,
who: new Set(data.who)
};
}
}
delete(json, state) {
let data = _.get(json, 'delete', false);
if (data) {
delete state.permissions[data.path];
}
}
add(json, state) {
let data = _.get(json, 'add', false);
if (data) {
for (let member of data.who) {
state.permissions[data.path].who.add(member);
}
}
}
remove(json, state) {
let data = _.get(json, 'remove', false);
if (data) {
for (let member of data.who) {
state.permissions[data.path].who.delete(member);
}
}
}
}

View File

@ -64,6 +64,14 @@ export class ResponseReducer {
if (state.notebooks[json.host][json.notebook]) {
state.notebooks[json.host][json.notebook]["notes-by-date"] =
json.data.notebook["notes-by-date"];
state.notebooks[json.host][json.notebook].subscribers =
json.data.notebook.subscribers;
state.notebooks[json.host][json.notebook].comments =
json.data.notebook.comments;
state.notebooks[json.host][json.notebook]["subscribers-group-path"] =
json.data.notebook["subscribers-group-path"];
state.notebooks[json.host][json.notebook]["writers-group-path"] =
json.data.notebook["writers-group-path"];
if (state.notebooks[json.host][json.notebook].notes) {
for (var key in json.data.notebook.notes) {
let oldNote = state.notebooks[json.host][json.notebook].notes[key];

View File

@ -1,8 +1,9 @@
import { InitialReducer } from '/reducers/initial';
import { PrimaryReducer } from '/reducers/primary';
import { ResponseReducer } from '/reducers/response';
import { GroupReducer } from '/reducers/group';
import { InviteReducer } from '/reducers/invite';
import { InitialReducer } from '/reducers/initial';
import { PrimaryReducer } from '/reducers/primary';
import { ResponseReducer } from '/reducers/response';
import { GroupReducer } from '/reducers/group';
import { InviteReducer } from '/reducers/invite';
import { PermissionReducer } from '/reducers/permission';
class Store {
constructor() {
@ -16,11 +17,12 @@ class Store {
sidebarShown: true
}
this.initialReducer = new InitialReducer();
this.primaryReducer = new PrimaryReducer();
this.responseReducer = new ResponseReducer();
this.groupReducer = new GroupReducer();
this.inviteReducer = new InviteReducer();
this.initialReducer = new InitialReducer();
this.primaryReducer = new PrimaryReducer();
this.responseReducer = new ResponseReducer();
this.groupReducer = new GroupReducer();
this.inviteReducer = new InviteReducer();
this.permissionReducer = new PermissionReducer();
this.setState = () => {};
this.initialReducer.reduce(window.injectedState, this.state);
@ -33,6 +35,7 @@ class Store {
handleEvent(evt) {
if (evt.from && evt.from.path === '/all') {
this.groupReducer.reduce(evt.data, this.state);
this.permissionReducer.reduce(evt.data, this.state);
}
else if (evt.from && evt.from.path === '/primary'){
this.primaryReducer.reduce(evt.data, this.state);

View File

@ -24,6 +24,9 @@ export class Subscription {
api.bind('/primary', 'PUT', api.authTokens.ship, 'invite-view',
this.handleEvent.bind(this),
this.handleError.bind(this));
api.bind('/all', 'PUT', api.authTokens.ship, 'permission-store',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
handleEvent(diff) {