link fe: implement members & settings pages

This commit is contained in:
Fang 2020-03-05 15:52:44 +01:00
parent 3e7f0dd9d8
commit c71e5315e9
No known key found for this signature in database
GPG Key ID: EB035760C1BBA972
13 changed files with 812 additions and 29 deletions

View File

@ -16,6 +16,10 @@ class UrbitApi {
decline: this.inviteDecline.bind(this)
};
this.groups = {
remove: this.groupRemove.bind(this)
}
this.bind = this.bind.bind(this);
this.bindLinkView = this.bindLinkView.bind(this);
}
@ -60,6 +64,18 @@ class UrbitApi {
});
}
groupsAction(data) {
this.action("group-store", "group-action", data);
}
groupRemove(path, members) {
this.groupsAction({
remove: {
path, members
}
});
}
inviteAction(data) {
this.action("invite-store", "json", data);
}
@ -125,12 +141,28 @@ class UrbitApi {
);
}
linkViewAction(data) {
return this.action("link-view", "link-view-action", data);
}
createCollection(path, title, description, members, realGroup) {
// members is either {group:'/group-path'} or {'ships':[~zod]},
// with realGroup signifying if ships should become a managed group or not.
return this.action("link-view", "link-view-action", {
return this.linkViewAction({
create: {path, title, description, members, realGroup}
})
});
}
deleteCollection(path) {
return this.linkViewAction({
'delete': {path}
});
}
inviteToCollection(path, ships) {
return this.linkViewAction({
'invite': {path, ships}
});
}
linkAction(data) {
@ -156,6 +188,29 @@ class UrbitApi {
});
}
metadataAction(data) {
return this.action("metadata-hook", "metadata-action", data);
}
metadataAdd(appPath, groupPath, title, description, dateCreated, color) {
return this.metadataAction({
add: {
'group-path': groupPath,
resource: {
'app-path': appPath,
'app-name': 'link'
},
metadata: {
title,
description,
color,
'date-created': dateCreated,
creator: `~${window.ship}`
}
}
});
}
sidebarToggle() {
let sidebarBoolean = true;
if (store.state.sidebarShown === true) {

View File

@ -0,0 +1,74 @@
import React, { Component } from 'react';
import { InviteSearch } from './invite-search';
export class InviteElement extends Component {
constructor(props) {
super(props);
this.state = {
members: [],
error: false,
success: false
};
this.setInvite = this.setInvite.bind(this);
}
modifyMembers() {
const { props, state } = this;
let aud = state.members.map(mem => `~${mem}`);
if (state.members.length === 0) {
this.setState({
error: true,
success: false
});
return;
}
api.setSpinner(true);
this.setState({
error: false,
success: true,
members: []
}, () => {
api.inviteToCollection(props.resourcePath, aud).then(() => {
api.setSpinner(false);
});
});
}
setInvite(invite) {
this.setState({members: invite.ships});
}
render() {
const { props, state } = this;
let modifyButtonClasses = "mt4 db f9 ba pa2 white-d bg-gray0-d b--black b--gray2-d pointer";
if (state.error) {
modifyButtonClasses = modifyButtonClasses + ' gray3';
}
return (
<div>
<InviteSearch
groups={{}}
contacts={props.contacts}
groupResults={false}
invites={{
groups: [],
ships: this.state.members
}}
setInvite={this.setInvite}
/>
<button
onClick={this.modifyMembers.bind(this)}
className={modifyButtonClasses}>
Invite
</button>
</div>
);
}
}

View File

@ -1,27 +1,33 @@
import React, { Component } from 'react'
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { makeRoutePath } from '../../lib/util';
export class LinksTabBar extends Component {
render() {
let props = this.props;
let memColor = '';
let memColor = '',
setColor = '';
if (props.location.pathname.includes('/members')) {
memColor = 'black';
if (props.location.pathname.includes('/settings')) {
memColor = 'gray3';
setColor = 'black white-d';
} else if (props.location.pathname.includes('/members')) {
memColor = 'black white-d';
setColor = 'gray3';
} else {
memColor = 'gray3';
memColor = 'gray3';
setColor = 'gray3';
}
let hidePopoutIcon = (props.popout)
? "dn-m dn-l dn-xl"
: "dib-m dib-l dib-xl";
return (
<div className="dib pt2 flex-shrink-0 flex-grow-1">
{!!props.isOwner ? (
<div className={"dib f8 pl6"}>
<div className="dib flex-shrink-0 flex-grow-1">
{!!props.amOwner ? (
<div className={"dib pt2 f9 pl6 lh-solid"}>
<Link
className={"no-underline " + memColor}
to={makeRoutePath(props.resourcePath, props.popout) + '/members'}>
@ -31,10 +37,18 @@ export class LinksTabBar extends Component {
) : (
<div className="dib" style={{ width: 0 }}></div>
)}
<a href={makeRoutePath(props.resourcePath, true, props.page)} target="_blank"
className="dib fr">
<div className={"dib pt2 f9 pl6 pr6 lh-solid"}>
<Link
className={"no-underline " + setColor}
to={makeRoutePath(props.resourcePath, props.popout) + '/settings'}>
Settings
</Link>
</div>
<a href={makeRoutePath(props.resourcePath, true, props.page)}
target="_blank"
className="dib fr pt2 pr1">
<img
className={`flex-shrink-0 pr4 dn` + hidePopoutIcon}
className={`flex-shrink-0 pr3 dn ` + hidePopoutIcon}
src="/~link/img/popout.png"
height="16"
width="16"/>

View File

@ -0,0 +1,52 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
import { uxToHex } from '/lib/util';
export class MemberElement extends Component {
onRemove() {
const { props } = this;
//TODO don't really need to use link-view here, but should we anyway?
api.groups.remove(props.groupPath, [`~${props.ship}`]);
}
render() {
const { props } = this;
let actionElem;
if (props.ship === props.owner) {
actionElem = (
<p className="w-20 dib list-ship black white-d f8 c-default">
Host
</p>
);
} else if (props.amOwner && window.ship !== props.ship) {
actionElem = (
<a onClick={this.onRemove.bind(this)}
className="w-20 dib list-ship black white-d f8 pointer">
Ban
</a>
);
} else {
actionElem = (
<span></span>
);
}
let name = !!props.contact
? `${props.contact.nickname} (~${props.ship})` : `~${props.ship}`;
let color = !!props.contact ? uxToHex(props.contact.color) : '000000';
return (
<div className="flex mb2">
<Sigil ship={props.ship} size={32} color={`#${color}`} />
<p className={
"w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8"
}>{name}</p>
{actionElem}
</div>
);
}
}

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react'
import { LoadingScreen } from './loading';
import { LinksTabBar } from './lib/links-tabbar';
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
import { Route, Link } from "react-router-dom";
@ -37,15 +38,7 @@ export class Links extends Component {
let props = this.props;
if (!props.resource.title) {
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Loading...
</p>
</div>
</div>
);
return <LoadingScreen/>;
}
let linkPage = props.page;

View File

@ -0,0 +1,15 @@
import React, { Component } from 'react';
export class LoadingScreen extends Component {
render() {
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Loading...
</p>
</div>
</div>
);
}
}

View File

@ -0,0 +1,106 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { store } from '/store';
import urbitOb from 'urbit-ob';
import { LoadingScreen } from './loading';
import { LinksTabBar } from '/components/lib/links-tabbar';
import { MemberElement } from '/components/lib/member-element';
import { InviteElement } from '/components/lib/invite-element';
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
import { makeRoutePath } from '/lib/util';
export class MemberScreen extends Component {
render() {
const { props, state } = this;
if (!props.groupPath) {
return <LoadingScreen/>;
}
const isManaged = ('/~/' !== props.groupPath.slice(0,3));
let members = Array.from(props.group).map((mem) => {
let contact = (mem in props.contactDetails)
? props.contactDetails[mem] : false;
return (
<MemberElement
key={mem}
amOwner={props.amOwner}
contact={contact}
ship={mem}
groupPath={props.groupPath}
resourcePath={props.resourcePath}
/>
);
});
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}>
<Link to="/~link">{"⟵ All Collections"}</Link>
</div>
<div
className={`pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative
overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0`}
style={{ height: 48 }}>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
/>
<Link to={makeRoutePath(props.resourcePath, props.popout)}
className="pt2 white-d">
<h2
className="mono dib f9 fw4 v-top"
style={{ width: "max-content" }}>
{props.resource.title}
</h2>
</Link>
<LinksTabBar
{...props}
groupPath={props.groupPath}
resourcePath={props.resourcePath}
amOwner={props.amOwner}
popout={props.popout}
/>
</div>
<div className="w-100 pl3 mt0 mt4-m mt4-l mt4-xl cf pr6">
<div className="w-100 w-50-l w-50-xl fl pa2 pr3 pt3 pt0-l pt0-xl">
<p className="f8 pb2">Members</p>
<p className="f9 gray2 mb3">
{ 'Everyone with permission to use this collection.' +
((isManaged && props.amOwner)
? ' Removing someone removes them from the group.'
: '')
}
</p>
{members}
</div>
{ !props.amOwner ? null : (
<div className="w-100 w-50-l w-50-xl fl pa2 pr3 pt3 pt0-l pt0-xl">
<p className="f8 pb2">Modify Permissions</p>
<p className="f9 gray2 mb3">
{ 'Invite someone to this collection.' +
(isManaged
? ' Adding someone adds them to the group.'
: '')
}
</p>
<InviteElement
groupPath={props.groupPath}
resourcePath={props.resourcePath}
permissions={props.permission}
contacts={props.contacts}
/>
</div>
)}
</div>
</div>
);
}
}

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { BrowserRouter, Route, Link } from "react-router-dom";
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
import classnames from 'classnames';
import _ from 'lodash';
@ -8,9 +8,11 @@ import { subscription } from '/subscription';
import { store } from '/store';
import { Skeleton } from '/components/skeleton';
import { NewScreen } from '/components/new';
import { MemberScreen } from '/components/member';
import { SettingsScreen } from '/components/settings';
import { Links } from '/components/links-list';
import { LinkDetail } from '/components/link';
import { makeRoutePath, base64urlDecode } from '../lib/util';
import { makeRoutePath, amOwnerOfGroup, base64urlDecode } from '../lib/util';
//NOTE route paths make the assumption that a resource identifier is always
// just a single /path element. technically, backend supports /longer/paths
@ -40,7 +42,7 @@ export class Root extends Component {
state.invites['/link'] : {};
return (
<BrowserRouter>
<BrowserRouter><Switch>
<Route exact path="/~link"
render={ (props) => {
return (
@ -88,12 +90,85 @@ export class Root extends Component {
const resourcePath = '/' + props.match.params.resource;
props.history.push(makeRoutePath(resourcePath));
}}
/>
<Route exact path="/~link/(popout)?/:resource/members"
render={(props) => {
const popout = props.match.url.includes("/popout/");
const resourcePath = '/' + props.match.params.resource;
const resource = resources[resourcePath] || {};
const contactDetails = contacts[resource.group] || {};
const group = groups[resource.group] || new Set([]);
const amOwner = amOwnerOfGroup(resource.group);
return (
<Skeleton
spinner={state.spinner}
resources={resources}
invites={invites}
groups={groups}
selected={resourcePath}
rightPanelHide={true}
sidebarShown={state.sidebarShown}
links={links}>
<MemberScreen
resource={resource}
contacts={contacts}
contactDetails={contactDetails}
groupPath={resource.group}
group={group}
amOwner={amOwner}
resourcePath={resourcePath}
popout={popout}
{...props}
/>
</Skeleton>
);
}}
/>
<Route exact path="/~link/(popout)?/:resource/settings"
render={ (props) => {
const popout = props.match.url.includes("/popout/");
const resourcePath = '/' + props.match.params.resource;
const resource = resources[resourcePath] || false;
const contactDetails = contacts[resource.group] || {};
const group = groups[resource.group] || new Set([]);
const amOwner = amOwnerOfGroup(resource.group);
return (
<Skeleton
spinner={state.spinner}
resources={resources}
invites={invites}
groups={groups}
selected={resourcePath}
rightPanelHide={true}
sidebarShown={state.sidebarShown}
links={links}>
<SettingsScreen
sidebarShown={state.sidebarShown}
resource={resource}
contacts={contacts}
contactDetails={contactDetails}
groupPath={resource.group}
group={group}
amOwner={amOwner}
resourcePath={resourcePath}
popout={popout}
{...props}
/>
</Skeleton>
);
}}
/>
<Route exact path="/~link/(popout)?/:resource/:page?"
render={ (props) => {
const resourcePath = '/' + props.match.params.resource;
const resource = resources[resourcePath] || {};
const amOwner = amOwnerOfGroup(resource.group);
let contactDetails = contacts[resource.group] || {};
let page = props.match.params.page || 0;
@ -132,6 +207,7 @@ export class Root extends Component {
page={page}
resourcePath={resourcePath}
resource={resource}
amOwner={amOwner}
popout={popout}
sidebarShown={state.sidebarShown}
/>
@ -144,6 +220,8 @@ export class Root extends Component {
const resourcePath = '/' + props.match.params.resource;
const resource = resources[resourcePath] || {};
const amOwner = amOwnerOfGroup(resource.group);
let popout = props.match.url.includes("/popout/");
let contactDetails = contacts[resource.group] || {};
@ -182,6 +260,7 @@ export class Root extends Component {
contacts={contactDetails}
resourcePath={resourcePath}
groupPath={resource.group}
amOwner={amOwner}
popout={popout}
sidebarShown={state.sidebarShown}
data={data}
@ -192,7 +271,7 @@ export class Root extends Component {
)
}}
/>
</BrowserRouter>
</Switch></BrowserRouter>
)
}
}

View File

@ -0,0 +1,301 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { deSig, uxToHex } from '/lib/util';
import { Route, Link } from "react-router-dom";
import { LoadingScreen } from './loading';
import { LinksTabBar } from '/components/lib/links-tabbar';
import SidebarSwitcher from './lib/icons/icon-sidebar-switch';
import { makeRoutePath } from '../lib/util';
export class SettingsScreen extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
title: "",
description: "",
color: ""
};
this.changeTitle = this.changeTitle.bind(this);
this.changeDescription = this.changeDescription.bind(this);
this.changeColor = this.changeColor.bind(this);
this.renderDelete = this.renderDelete.bind(this);
this.renderMetadataSettings = this.renderMetadataSettings.bind(this);
}
componentDidMount() {
if (this.props.resource) {
this.setState({
title: this.props.resource.title,
description: this.props.resource.description,
color: uxToHex(this.props.resource.color || '0x0')
});
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
if (!!state.isLoading && !props.resource) {
this.setState({
isLoading: false
}, () => {
api.setSpinner(false);
props.history.push('/~link');
});
}
if (props.resource && (prevProps !== props)) {
this.setState({
title: props.resource.title,
description: props.resource.description,
color: uxToHex(props.resource.color || '0x0')
});
}
}
changeTitle() {
this.setState({title: event.target.value})
}
changeDescription() {
this.setState({description: event.target.value});
}
changeColor() {
this.setState({color: event.target.value});
}
deleteCollection() {
const { props, state } = this;
api.deleteCollection(props.resourcePath);
api.setSpinner(true);
this.setState({
isLoading: true
});
}
renderDelete() {
const { props, state } = this;
const isManaged = ('/~/' !== props.groupPath.slice(0,3));
let deleteButtonClasses = (props.amOwner) ? 'b--red2 red2 pointer bg-gray0-d' : 'b--grey3 grey3 bg-gray0-d c-default';
let leaveButtonClasses = (!props.amOwner) ? "pointer" : "c-default";
let deleteClasses = 'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray0-d pointer';
let deleteText = 'Remove this collection from your collection list.';
let deleteAction = 'Remove';
if (props.amOwner && isManaged) {
deleteText = 'Delete this collection from the group. This deletes it for everyone!';
deleteAction = 'Delete';
deleteClasses = 'dib f9 ba pa2 b--red2 red2 pointer bg-gray0-d';
} else if (!isManaged) {
deleteText = deleteText + " You won't be able to manage access to this collection anymore!"
}
return (
<div className="w-100 fl mt3">
<p className="f8 mt3 lh-copy db">Delete Collection</p>
<p className="f9 gray2 db mb4">{deleteText}</p>
<a onClick={this.deleteCollection.bind(this)}
className={deleteClasses}>{deleteAction + ' collection'}</a>
</div>
);
}
renderMetadataSettings() {
const { props, state } = this;
const { resource } = props;
return(
<div>
<div className={"w-100 pb6 fl mt3 " + ((props.amOwner) ? '' : 'o-30')}>
<p className="f8 mt3 lh-copy">Rename</p>
<p className="f9 gray2 db mb4">Change the name of this collection</p>
<div className="relative w-100 flex"
style={{maxWidth: "29rem"}}>
<input
className={"f8 ba b--gray3 b--gray2-d bg-gray0-d white-d " +
"focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3"}
value={this.state.title}
disabled={!props.amOwner}
onChange={this.changeTitle}
/>
<span className={"f8 absolute pa3 inter " +
((props.amOwner) ? "pointer" : "")}
style={{ right: 12, top: 1 }}
ref="rename"
onClick={() => {
if (props.amOwner) {
api.setSpinner(true);
api.metadataAdd(
props.resourcePath,
props.groupPath,
state.title,
props.resource.description,
props.resource['date-created'],
uxToHex(props.resource.color)
).then(() => {
api.setSpinner(false);
this.refs.rename.innerText = "Saved";
});
}
}}>
Save
</span>
</div>
<p className="f8 mt3 lh-copy">Change description</p>
<p className="f9 gray2 db mb4">
Change the description of this collection
</p>
<div className="relative w-100 flex"
style={{ maxWidth: "29rem" }}>
<input
className={"f8 ba b--gray3 b--gray2-d bg-gray0-d white-d " +
"focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3"}
value={this.state.description}
disabled={!props.amOwner}
onChange={this.changeDescription}
/>
<span className={"f8 absolute pa3 inter " +
((props.amOwner) ? "pointer" : "")}
style={{ right: 12, top: 1 }}
ref="description"
onClick={() => {
if (props.amOwner) {
api.setSpinner(true);
api.metadataAdd(
props.resourcePath,
props.groupPath,
props.resource.title,
state.description,
props.resource['date-created'],
uxToHex(props.resource.color)
).then(() => {
api.setSpinner(false);
this.refs.description.innerText = "Saved";
});
}
}}>
Save
</span>
</div>
<p className="f8 mt3 lh-copy">Change color</p>
<p className="f9 gray2 db mb4">Give this collection a color when viewing group channels</p>
<div className="relative w-100 flex"
style={{ maxWidth: "20rem" }}>
<input
className={"f8 ba b--gray3 b--gray2-d bg-gray0-d white-d " +
"focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3"}
value={this.state.color}
disabled={!props.amOwner}
onChange={this.changeColor}
/>
<span className={"f8 absolute pa3 inter " +
((props.amOwner) ? "pointer" : "")}
style={{ right: 12, top: 1 }}
ref="color"
onClick={() => {
if (props.amOwner && state.color.match(/[0-9A-F]{6}/i)) {
api.setSpinner(true);
api.metadataAdd(
props.resourcePath,
props.groupPath,
props.resource.title,
props.resource.description,
props.resource['date-created'],
state.color
).then(() => {
api.setSpinner(false);
this.refs.color.innerText = "Saved";
});
}
}}>
Save
</span>
</div>
</div>
</div>
)
}
render() {
const { props, state } = this;
const isinPopout = this.props.popout ? "popout/" : "";
let writeGroup = Array.from(props.group.values());
if (props.groupPath === undefined) {
return <LoadingScreen/>;
}
if (!!state.isLoading) {
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}>
<Link to="/~link">{"⟵ All Collections"}</Link>
</div>
<div
className="pl4 pt2 bb b--gray4 b--gray2-d bg-gray0-d flex relative overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0"
style={{ height: 48 }}>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
/>
<Link to={makeRoutePath(props.resourcePath, props.popout)}
className="pt2 white-d">
<h2
className="mono dib f9 fw4 v-top"
style={{ width: "max-content" }}>
{props.resourcePath.substr(1)}
</h2>
</Link>
<LinksTabBar {...props}/>
</div>
<div className="w-100 pl3 mt4 cf">
<h2 className="f8 pb2">Removing...</h2>
</div>
</div>
);
}
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}>
<Link to="/~chat/">{"⟵ All Chats"}</Link>
</div>
<div
className="pl4 pt2 bb b--gray4 b--gray1-d flex relative overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0"
style={{ height: 48 }}>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
/>
<Link to={makeRoutePath(props.resourcePath, props.popout)}
className="pt2">
<h2
className="mono dib f9 fw4 v-top"
style={{ width: "max-content" }}>
{props.resource.title}
</h2>
</Link>
<LinksTabBar {...props}/>
</div>
<div className="w-100 pl3 mt4 cf">
<h2 className="f8 pb2">Collection Settings</h2>
{this.renderDelete()}
{this.renderMetadataSettings()}
</div>
</div>
);
}
}

View File

@ -18,6 +18,12 @@ export function makeRoutePath(
return route;
}
export function amOwnerOfGroup(groupPath) {
if (!groupPath) return false;
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)[2];
return (window.ship === groupOwner);
}
export function getContactDetails(contact) {
const member = !contact;
contact = contact || {

View File

@ -0,0 +1,63 @@
import _ from 'lodash';
export class GroupUpdateReducer {
reduce(json, state) {
let data = _.get(json, 'group-update', false);
if (data) {
this.add(data, state);
this.remove(data, state);
this.bundle(data, state);
this.unbundle(data, state);
this.keys(data, state);
this.path(data, state);
}
}
add(json, state) {
let data = _.get(json, 'add', false);
if (data) {
for (let member of data.members) {
state.groups[data.path].add(member);
}
}
}
remove(json, state) {
let data = _.get(json, 'remove', false);
if (data) {
for (let member of data.members) {
state.groups[data.path].delete(member);
}
}
}
bundle(json, state) {
let data = _.get(json, 'bundle', false);
if (data) {
state.groups[data.path] = new Set();
}
}
unbundle(json, state) {
let data = _.get(json, 'unbundle', false);
if (data) {
delete state.groups[data.path];
}
}
keys(json, state) {
let data = _.get(json, 'keys', false);
if (data) {
state.groupKeys = new Set(data.keys);
}
}
path(json, state) {
let data = _.get(json, 'path', false);
if (data) {
state.groups[data.path] = new Set([data.members]);
}
}
}

View File

@ -6,6 +6,8 @@ export class MetadataReducer {
if (data) {
this.associations(data, state);
this.add(data, state);
this.remove(data, state);
this.update(data, state);
}
}
@ -31,12 +33,32 @@ export class MetadataReducer {
add(json, state) {
let data = _.get(json, 'add', false);
if (data) {
if (state.resources[data['app-path']]) {
console.error('beware! overwriting previous data', data['app-path']);
}
this.update({'update-metadata': data}, state);
}
}
remove(json, state) {
let data = _.get(json, 'remove', false);
if (data) {
if (data['app-name'] !== 'link') {
return;
}
if (state.resources[data['app-path']]) {
console.error('beware! overwriting previous data', data['app-path']);
const have = state.resources[data['app-path']];
if (have && have.group === data['group-path']) {
delete state.resources[data['app-path']];
}
}
}
update(json, state) {
let data = _.get(json, 'update-metadata', false);
if (data) {
if (data['app-name'] !== 'link') {
return;
}
state.resources[data['app-path']] = {
group: data['group-path'],

View File

@ -1,4 +1,5 @@
import { InitialReducer } from '/reducers/initial';
import { GroupUpdateReducer } from '/reducers/group-update';
import { ContactUpdateReducer } from '/reducers/contact-update.js';
import { PermissionUpdateReducer } from '/reducers/permission-update';
import { MetadataReducer } from '/reducers/metadata-update.js';
@ -24,6 +25,7 @@ class Store {
};
this.initialReducer = new InitialReducer();
this.groupUpdateReducer = new GroupUpdateReducer();
this.contactUpdateReducer = new ContactUpdateReducer();
this.permissionUpdateReducer = new PermissionUpdateReducer();
this.metadataReducer = new MetadataReducer();
@ -47,6 +49,7 @@ class Store {
console.log('event', json);
this.initialReducer.reduce(json, this.state);
this.groupUpdateReducer.reduce(json, this.state);
this.contactUpdateReducer.reduce(json, this.state);
this.permissionUpdateReducer.reduce(json, this.state);
this.metadataReducer.reduce(json, this.state);