mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 11:33:41 +03:00
link fe: implement members & settings pages
This commit is contained in:
parent
3e7f0dd9d8
commit
c71e5315e9
@ -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) {
|
||||
|
74
pkg/interface/link/src/js/components/lib/invite-element.js
Normal file
74
pkg/interface/link/src/js/components/lib/invite-element.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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"/>
|
||||
|
52
pkg/interface/link/src/js/components/lib/member-element.js
Normal file
52
pkg/interface/link/src/js/components/lib/member-element.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
15
pkg/interface/link/src/js/components/loading.js
Normal file
15
pkg/interface/link/src/js/components/loading.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
106
pkg/interface/link/src/js/components/member.js
Normal file
106
pkg/interface/link/src/js/components/member.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
301
pkg/interface/link/src/js/components/settings.js
Normal file
301
pkg/interface/link/src/js/components/settings.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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 || {
|
||||
|
63
pkg/interface/link/src/js/reducers/group-update.js
Normal file
63
pkg/interface/link/src/js/reducers/group-update.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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'],
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user