mirror of
https://github.com/urbit/shrub.git
synced 2024-12-25 04:52:06 +03:00
launch: add group filter dropdown, header redesign
This commit is contained in:
parent
09eec6ea65
commit
891018159a
BIN
pkg/arvo/app/launch/img/Chevron.png
Normal file
BIN
pkg/arvo/app/launch/img/Chevron.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 453 B |
@ -19,6 +19,10 @@ a:-webkit-any-link {
|
||||
|
||||
textarea, select, input, button { outline: none; }
|
||||
|
||||
.c-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: "Source Code Pro", monospace;
|
||||
}
|
||||
@ -35,6 +39,12 @@ textarea, select, input, button { outline: none; }
|
||||
.bg-gray0-d {
|
||||
background-color: #333;
|
||||
}
|
||||
.bg-gray1-d {
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
.bg-gray2-d {
|
||||
background-color: #7f7f7f;
|
||||
}
|
||||
.b--gray1-d {
|
||||
border-color: #4d4d4d;
|
||||
}
|
||||
@ -44,4 +54,7 @@ textarea, select, input, button { outline: none; }
|
||||
.invert-d {
|
||||
filter: invert(1);
|
||||
}
|
||||
.hover-bg-gray1-d:hover {
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
}
|
228
pkg/interface/launch/src/js/components/group-filter.js
Normal file
228
pkg/interface/launch/src/js/components/group-filter.js
Normal file
@ -0,0 +1,228 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
export class GroupFilter extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
selected: [],
|
||||
groups: [],
|
||||
searchTerm: "",
|
||||
results: []
|
||||
}
|
||||
this.toggleOpen = this.toggleOpen.bind(this);
|
||||
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||
this.groupIndex = this.groupIndex.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
this.addGroup = this.addGroup.bind(this);
|
||||
this.deleteGroup = this.deleteGroup.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mousedown', this.handleClickOutside);
|
||||
this.groupIndex();
|
||||
let selected = localStorage.getItem("urbit-selectedGroups");
|
||||
if (selected) {
|
||||
this.setState({selected: JSON.parse(selected)})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mousedown', this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps !== this.props) {
|
||||
this.groupIndex();
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOutside(evt) {
|
||||
if ((this.dropdown && !this.dropdown.contains(evt.target))
|
||||
&& (this.toggleButton && !this.toggleButton.contains(evt.target))) {
|
||||
this.setState({ open: false });
|
||||
}
|
||||
}
|
||||
|
||||
toggleOpen() {
|
||||
this.setState({open: !this.state.open});
|
||||
}
|
||||
|
||||
groupIndex() {
|
||||
const { props, state } = this;
|
||||
let index = [];
|
||||
let associations = !!props.associations ? props.associations.contacts : {};
|
||||
index = Object.keys(associations).map((each) => {
|
||||
let eachGroup = [];
|
||||
eachGroup.push(each);
|
||||
let name = each;
|
||||
if (associations[each].metadata) {
|
||||
name = (associations[each].metadata.title !== "")
|
||||
? associations[each].metadata.title : name;
|
||||
}
|
||||
eachGroup.push(name);
|
||||
return eachGroup;
|
||||
});
|
||||
this.setState({groups: index})
|
||||
}
|
||||
|
||||
search(evt) {
|
||||
this.setState({searchTerm: evt.target.value});
|
||||
let term = evt.target.value.toLowerCase();
|
||||
|
||||
if (term.length < 3) {
|
||||
return this.setState({results: []})
|
||||
}
|
||||
|
||||
let groupMatches = [];
|
||||
groupMatches = this.state.groups.filter(e => {
|
||||
return (e[0].includes(term) || e[1].includes(term));
|
||||
});
|
||||
this.setState({results: groupMatches});
|
||||
}
|
||||
|
||||
addGroup(group) {
|
||||
let selected = this.state.selected;
|
||||
if (!(group in selected)) {
|
||||
selected.push(group);
|
||||
}
|
||||
this.setState({
|
||||
searchTerm: "",
|
||||
selected: selected,
|
||||
results: []
|
||||
}, (() => {
|
||||
localStorage.setItem("urbit-selectedGroups", JSON.stringify(this.state.selected));
|
||||
}))
|
||||
}
|
||||
|
||||
deleteGroup(group) {
|
||||
let selected = this.state.selected;
|
||||
selected = selected.filter(e => {
|
||||
return e !== group;
|
||||
});
|
||||
this.setState({selected: selected}, (() => {
|
||||
localStorage.setItem("urbit-selectedGroups", JSON.stringify(this.state.selected));
|
||||
}))
|
||||
//TODO localstorage
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
let currentGroup = "All Groups";
|
||||
|
||||
if (state.selected.length > 0) {
|
||||
let titles = state.selected.map((each) => {
|
||||
return each[1];
|
||||
})
|
||||
currentGroup = titles.join(" + ");
|
||||
}
|
||||
|
||||
let buttonOpened = (state.open)
|
||||
? "bg-gray5 bg-gray1-d white-d" : "hover-bg-gray5 hover-bg-gray1-d white-d";
|
||||
|
||||
let dropdownClass = (state.open)
|
||||
? "absolute db z-2 bg-white bg-gray0-d white-d ba b--gray3 b--gray1-d"
|
||||
: "dn";
|
||||
|
||||
let inviteCount = (props.invites && props.invites.length > 0)
|
||||
? <template className="dib fr">
|
||||
<p className="dib bg-green2 bg-gray2-d white fw6 ph1 br1 v-mid" style={{ marginBottom: 2 }}>
|
||||
{props.invites.length}
|
||||
</p>
|
||||
<span className="dib v-mid ml1">
|
||||
<img
|
||||
className="v-mid"
|
||||
src="/~launch/img/Chevron.png"
|
||||
style={{ height: 16, width: 16, paddingBottom: 1 }}
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
: <template className="dib fr" style={{paddingTop: 1}}>
|
||||
<span className="dib v-mid ml1">
|
||||
<img className="v-mid"
|
||||
src="/~launch/img/Chevron.png"
|
||||
style={{ height: 16, width: 16, paddingBottom: 1 }}
|
||||
/>
|
||||
</span>
|
||||
</template>;
|
||||
|
||||
let selectedGroups = <div/>
|
||||
let searchResults = <div/>
|
||||
|
||||
if (state.results.length > 0) {
|
||||
let groupResults = state.results.map((group => {
|
||||
return(
|
||||
<li
|
||||
key={group[0]}
|
||||
className="tl list white-d f9 pv2 ph3 pointer hover-bg-gray4 hover-bg-gray1-d inter" onClick={() => this.addGroup(group)}>
|
||||
<span className="mix-blend-diff white">{(group[1]) ? group[1] : group[0]}</span>
|
||||
</li>
|
||||
)
|
||||
}))
|
||||
searchResults = (
|
||||
<div className={"tl absolute bg-white bg-gray0-d white-d pv3 z-1 w-100 ba b--gray4 b--white-d overflow-y-scroll"} style={{maxWidth: "15.67rem", maxHeight: "8rem"}}>
|
||||
<p className="f9 tl gray2 ph3 pb2">Groups</p>
|
||||
{groupResults}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (state.selected.length > 0) {
|
||||
let allSelected = this.state.selected.map((each) => {
|
||||
let name = each[1];
|
||||
return(
|
||||
<span
|
||||
key={each[0]}
|
||||
className={"f9 inter black pa2 bg-gray5 bg-gray1-d " +
|
||||
"ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default"}
|
||||
>
|
||||
{name}
|
||||
<span
|
||||
className="white-d ml3 mono pointer"
|
||||
onClick={e => this.deleteGroup(each)}>
|
||||
x
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
selectedGroups = (
|
||||
<div className={
|
||||
"f9 gray2 bb bl br b--gray3 b--gray2-d bg-gray0-d " +
|
||||
"white-d pa3 db w-100 inter bg-gray5 lh-solid"
|
||||
}>
|
||||
{allSelected}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ml1 dib">
|
||||
<div className={buttonOpened}
|
||||
onClick={() => this.toggleOpen()}
|
||||
ref={(el) => this.toggleButton = el}>
|
||||
<p className="dib f9 pointer pa1 mw5 truncate v-mid">{currentGroup}</p>
|
||||
</div>
|
||||
<div className={dropdownClass}
|
||||
style={{ maxHeight: "24rem", width: 285 }}
|
||||
ref={(el) => { this.dropdown = el }}>
|
||||
<p className="tc bb b--gray3 b--gray1-d gray3 pv4 f9">Group Select and Filter</p>
|
||||
<a href="/~groups" className="ma4 bg-gray5 bg-gray1-d f9 tl pa1 br1 db no-underline" style={{paddingLeft: "6.5px", paddingRight: "6.5px"}}>Manage all Groups
|
||||
{inviteCount}
|
||||
</a>
|
||||
<p className="pt4 gray3 f9 tl mh4">Filter Groups</p>
|
||||
<div className="relative pb6 w-100 ph4 pt2">
|
||||
<input className="ba b--gray3 white-d bg-gray0-d inter w-100 f9 pa2" placeholder="Group name..."
|
||||
onChange={this.search}
|
||||
value={state.searchTerm}
|
||||
/>
|
||||
{searchResults}
|
||||
{selectedGroups}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupFilter;
|
@ -1,75 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Sigil } from './sigil';
|
||||
import { cite } from '../lib/util';
|
||||
import { GroupFilter } from './group-filter';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class Header extends Component {
|
||||
render() {
|
||||
|
||||
let data = _.get(this.props.data, "invites", false);
|
||||
let inviteNum = 0;
|
||||
|
||||
if (data && "/contacts" in data) {
|
||||
inviteNum = Object.keys(data["/contacts"]).length;
|
||||
}
|
||||
|
||||
let numNotificationsElem =
|
||||
inviteNum > 0 ? (
|
||||
<a href="/~groups" className="absolute bn no-underline"
|
||||
style={{left: "-32"}}>
|
||||
<p
|
||||
className="ph1 br2 ba b--gray2 green2 white-d f9 lh-solid"
|
||||
title={"Invitations to new groups"}
|
||||
style={{
|
||||
bottom: "-2",
|
||||
fontWeight: 600,
|
||||
fontSize: "8pt",
|
||||
lineHeight: "1.25",
|
||||
padding: "0.2rem 0.4rem"
|
||||
}}>
|
||||
{inviteNum > 99 ? "99+" : inviteNum}
|
||||
</p>
|
||||
</a>
|
||||
) : (
|
||||
<a href="/~groups" className="absolute bn no-underline"
|
||||
style={{left: "-32"}}>
|
||||
<p
|
||||
className="ph1 br2 ba b--gray2 gray2 white-d f9 lh-solid"
|
||||
title={"No new invitations to new groups"}
|
||||
style={{
|
||||
bottom: "-2",
|
||||
fontWeight: 600,
|
||||
fontSize: "8pt",
|
||||
lineHeight: "1.25",
|
||||
padding: "0.2rem 0.4rem"
|
||||
}}>
|
||||
0
|
||||
</p>
|
||||
</a>
|
||||
);
|
||||
let invites = (this.props.invites && this.props.invites.contacts)
|
||||
? this.props.invites.contacts
|
||||
: {};
|
||||
|
||||
return (
|
||||
<header
|
||||
className={"bg-white bg-gray0-d w-100 justify-between relative " +
|
||||
"tl tc-m tc-l tc-xl pt3"}
|
||||
"tl pt3"}
|
||||
style={{ height: 40 }}>
|
||||
<span
|
||||
className="f9 white-d inter dib ml4 ml0-m ml0-l ml0-xl"
|
||||
style={{
|
||||
verticalAlign: "text-top",
|
||||
paddingTop: 3
|
||||
}}>
|
||||
Home
|
||||
</span>
|
||||
<div className="absolute relative right-1 lh-copy" style={{ top: 12 }}>
|
||||
{numNotificationsElem}
|
||||
<Sigil
|
||||
<div className="fl lh-copy absolute left-1" style={{top: 12}}>
|
||||
<a href="/~groups/me"><Sigil
|
||||
ship={"~" + window.ship}
|
||||
size={16} color={"#000000"}
|
||||
classes="mix-blend-diff v-mid" />
|
||||
<span className="mono white-d f9 ml2">
|
||||
{cite(window.ship)}
|
||||
</span>
|
||||
</a>
|
||||
<GroupFilter invites={invites} associations={this.props.associations}/>
|
||||
<span
|
||||
className="f9 white-d inter dib ml1 c-default">
|
||||
<span className="inter gray2 f9 dib mr1">/</span> Home
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
@ -25,7 +25,7 @@ export default class Home extends Component {
|
||||
// tileData["invites"] = ("invites" in this.props.data)
|
||||
// ? this.props.data["invites"] : {};
|
||||
}
|
||||
if (tile !== "invites") {
|
||||
if ((tile !== "invites") && (tile !== "associations")) {
|
||||
return <Tile key={tile} type={tile} data={tileData} />;
|
||||
}
|
||||
});
|
||||
@ -37,7 +37,7 @@ export default class Home extends Component {
|
||||
|
||||
return (
|
||||
<div className="fl w-100 h-100 bg-white bg-gray0-d center">
|
||||
<Header data={headerData}/>
|
||||
<Header data={headerData} associations={this.props.data.associations} invites={this.props.data.invites}/>
|
||||
<div className={"v-mid pa2 dtc-m dtc-l dtc-xl " +
|
||||
"flex justify-between flex-wrap"}
|
||||
style={{maxWidth: "40rem"}}>
|
||||
|
65
pkg/interface/launch/src/js/reducers/metadata.js
Normal file
65
pkg/interface/launch/src/js/reducers/metadata.js
Normal file
@ -0,0 +1,65 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class MetadataReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'metadata-update', false);
|
||||
if (data) {
|
||||
this.associations(data, state);
|
||||
this.add(data, state);
|
||||
this.update(data, state);
|
||||
this.remove(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
associations(json, state) {
|
||||
let data = _.get(json, 'associations', false);
|
||||
if (data) {
|
||||
let metadata = state.associations;
|
||||
Object.keys(data).map((channel) => {
|
||||
let channelObj = data[channel];
|
||||
let app = data[channel]["app-name"];
|
||||
if (!(app in metadata)) {
|
||||
metadata[app] = {};
|
||||
}
|
||||
metadata[app][channelObj["app-path"]] = channelObj;
|
||||
})
|
||||
state.associations = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
add(json, state) {
|
||||
let data = _.get(json, 'add', false);
|
||||
if (data) {
|
||||
let metadata = state.associations;
|
||||
let app = data["app-name"];
|
||||
if (!(app in metadata)) {
|
||||
metadata[app] = {};
|
||||
}
|
||||
metadata[app][data["app-path"]] = data;
|
||||
state.associations = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
update(json, state) {
|
||||
let data = _.get(json, 'update-metadata', false);
|
||||
if (data) {
|
||||
let metadata = state.associations;
|
||||
let app = data["app-name"];
|
||||
metadata[app][data["app-path"]] = data;
|
||||
state.associations = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
remove(json, state) {
|
||||
let data = _.get(json, 'remove', false);
|
||||
if (data) {
|
||||
let metadata = state.associations;
|
||||
let app = data["app-name"];
|
||||
if (!(app in metadata)) {
|
||||
return false;
|
||||
}
|
||||
delete metadata[app][data["app-path"]];
|
||||
state.associations = metadata;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,17 @@
|
||||
import { InviteReducer } from '/reducers/invite.js';
|
||||
import { MetadataReducer } from '/reducers/metadata.js';
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.state = {
|
||||
invites: {}
|
||||
invites: {},
|
||||
associations: {
|
||||
contacts: {}
|
||||
}
|
||||
};
|
||||
this.setState = () => {};
|
||||
this.inviteReducer = new InviteReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
}
|
||||
|
||||
setStateHandler(setState) {
|
||||
@ -14,8 +19,9 @@ class Store {
|
||||
}
|
||||
|
||||
handleEvent(data) {
|
||||
if (("from" in data) && data.from.app === "invite-view") {
|
||||
this.inviteReducer.reduce(data.data, this.state)
|
||||
if (("from" in data) && ((data.from.app === "invite-view") || data.from.app === "metadata-store")) {
|
||||
this.inviteReducer.reduce(data.data, this.state);
|
||||
this.metadataReducer.reduce(data.data, this.state);
|
||||
}
|
||||
else {
|
||||
let json = data.data;
|
||||
|
@ -30,6 +30,10 @@ export class Subscription {
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
ship);
|
||||
api.bind("metadata-store", "/app-name/contacts",
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
ship);
|
||||
}
|
||||
|
||||
handleEvent(diff) {
|
||||
|
Loading…
Reference in New Issue
Block a user