mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-13 08:38:43 +03:00
groups: update to new designs
This commit is contained in:
parent
94050f150e
commit
971d38290a
5
pkg/interface/package-lock.json
generated
5
pkg/interface/package-lock.json
generated
@ -1709,9 +1709,8 @@
|
||||
"integrity": "sha512-3OPSdf9cejP/TSzWXuBaYbzLtAfBzQnc75SlPLkoPfwpxnv1Bvy9hiWngLY0WnKRR6lMOldnkYQCCuNWeDibYQ=="
|
||||
},
|
||||
"@tlon/indigo-react": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tlon/indigo-react/-/indigo-react-1.1.15.tgz",
|
||||
"integrity": "sha512-Ao+1hAJjN5y1gDyT7GIUgXORPXTIpZKVVtrS++ZGYBemYMSq3oJFMIZertsSZbDHuh/TsVPenJrMUZBpV60law=="
|
||||
"version": "github:liam-fitzgerald/indigo-react#e88a94b1556c029300fc08b33190060aac559f20",
|
||||
"from": "github:liam-fitzgerald/indigo-react#lf/1.1.17"
|
||||
},
|
||||
"@types/anymatch": {
|
||||
"version": "1.3.1",
|
||||
|
@ -9,7 +9,7 @@
|
||||
"@reach/menu-button": "^0.10.5",
|
||||
"@reach/tabs": "^0.10.5",
|
||||
"@tlon/indigo-light": "^1.0.3",
|
||||
"@tlon/indigo-react": "^1.1.15",
|
||||
"@tlon/indigo-react": "github:liam-fitzgerald/indigo-react#lf/1.1.17",
|
||||
"aws-sdk": "^2.726.0",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^5.55.0",
|
||||
|
@ -1,22 +1,36 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
|
||||
export function useLocalStorageState<T>(key: string, initial: T) {
|
||||
const [state, _setState] = useState(() => {
|
||||
const s = localStorage.getItem(key);
|
||||
if(s) {
|
||||
function retrieve<T>(key: string, initial: T): T {
|
||||
const s = localStorage.getItem(key);
|
||||
if (s) {
|
||||
try {
|
||||
return JSON.parse(s) as T;
|
||||
} catch (e) {
|
||||
return initial;
|
||||
}
|
||||
return initial;
|
||||
}
|
||||
return initial;
|
||||
}
|
||||
|
||||
});
|
||||
interface SetStateFunc<T> {
|
||||
(t: T): T;
|
||||
}
|
||||
type SetState<T> = T | SetStateFunc<T>;
|
||||
export function useLocalStorageState<T>(key: string, initial: T) {
|
||||
const [state, _setState] = useState(() => retrieve(key, initial));
|
||||
|
||||
const setState = useCallback((s: T) => {
|
||||
_setState(s);
|
||||
localStorage.setItem(key, JSON.stringify(s));
|
||||
useEffect(() => {
|
||||
_setState(retrieve(key, initial));
|
||||
}, [key]);
|
||||
|
||||
}, [_setState]);
|
||||
const setState = useCallback(
|
||||
(s: SetState<T>) => {
|
||||
const updated = typeof s === "function" ? s(state) : s;
|
||||
_setState(updated);
|
||||
localStorage.setItem(key, JSON.stringify(s));
|
||||
},
|
||||
[_setState]
|
||||
);
|
||||
|
||||
return [state, setState] as const;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,76 +1,145 @@
|
||||
import React from 'react';
|
||||
import { Box, Col, Row, Text, IconButton, Button, Icon } from '@tlon/indigo-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React from "react";
|
||||
import {
|
||||
Center,
|
||||
Box,
|
||||
Col,
|
||||
Row,
|
||||
Text,
|
||||
IconButton,
|
||||
Button,
|
||||
Icon,
|
||||
} from "@tlon/indigo-react";
|
||||
import { uxToHex } from "~/logic/lib/util";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Association, } from "~/types/metadata-update";
|
||||
import { Association, Associations } from "~/types/metadata-update";
|
||||
import { Dropdown } from "~/views/components/Dropdown";
|
||||
|
||||
|
||||
const GroupSwitcherItem = ({ to, children, ...rest }) => (
|
||||
const GroupSwitcherItem = ({ to, children, bottom = false, ...rest }) => (
|
||||
<Link to={to}>
|
||||
<Row {...rest} px={1} mb={2} alignItems="center">
|
||||
{children}
|
||||
</Row>
|
||||
<Box
|
||||
py={1}
|
||||
{...rest}
|
||||
borderBottom={bottom ? 0 : 1}
|
||||
borderBottomColor="lightGray"
|
||||
>
|
||||
<Row p={2} alignItems="center">
|
||||
{children}
|
||||
</Row>
|
||||
</Box>
|
||||
</Link>
|
||||
);
|
||||
|
||||
export function GroupSwitcher(props: { association: Association; baseUrl: string }) {
|
||||
function RecentGroups(props: { recent: string[]; associations: Associations }) {
|
||||
const { associations, recent } = props;
|
||||
if (recent.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Col borderBottom={1} borderBottomColor="lightGray" p={1}>
|
||||
<Box px={1} py={2} color="gray">
|
||||
Recent Groups
|
||||
</Box>
|
||||
{props.recent.slice(1, 5).map((g) => {
|
||||
const assoc = associations.contacts[g];
|
||||
const color = uxToHex(assoc?.metadata?.color);
|
||||
return (
|
||||
<Row key={g} px={1} pb={2} alignItems="center">
|
||||
<Box
|
||||
borderRadius={1}
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
height="16px"
|
||||
width="16px"
|
||||
bg={color}
|
||||
mr={2}
|
||||
display="block"
|
||||
/>
|
||||
<Link to={`/~groups${g}`}>{assoc?.metadata?.title}</Link>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupSwitcher(props: {
|
||||
association: Association;
|
||||
associations: Associations;
|
||||
baseUrl: string;
|
||||
recentGroups: string[];
|
||||
}) {
|
||||
const { title } = props.association.metadata;
|
||||
const navTo = (to: string) => `${props.baseUrl}${to}`;
|
||||
return (
|
||||
<Box position="sticky" top="0px" p={2}>
|
||||
<Col bg="white" borderRadius={1} border={1} borderColor="washedGray">
|
||||
<Row justifyContent="space-between">
|
||||
<Col
|
||||
justifyContent="center"
|
||||
bg="white"
|
||||
borderRadius={1}
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
>
|
||||
<Row alignItems="center" justifyContent="space-between">
|
||||
<Dropdown
|
||||
width="220px"
|
||||
width="200px"
|
||||
options={
|
||||
<Col width="100%" alignItems="flex-start">
|
||||
<Row
|
||||
alignItems="center"
|
||||
px={2}
|
||||
pb={2}
|
||||
my={2}
|
||||
borderBottom={1}
|
||||
borderBottomColor="washedGray"
|
||||
>
|
||||
<img
|
||||
src="/~landscape/img/groups.png"
|
||||
height="12px"
|
||||
width="12px"
|
||||
<Col bg="white" width="100%" alignItems="stretch">
|
||||
<GroupSwitcherItem to="">
|
||||
<Icon
|
||||
mr={2}
|
||||
stroke="gray"
|
||||
fill="rgba(0,0,0,0)"
|
||||
display="block"
|
||||
icon="Groups"
|
||||
/>
|
||||
<Text ml={2}>Switch Groups</Text>
|
||||
</Row>
|
||||
<Text>All Groups</Text>
|
||||
</GroupSwitcherItem>
|
||||
<RecentGroups
|
||||
recent={props.recentGroups}
|
||||
associations={props.associations}
|
||||
/>
|
||||
<GroupSwitcherItem to={navTo("/popover/participants")}>
|
||||
<Icon mr={2} icon="Circle" />
|
||||
<Icon mr={2} fill="none" stroke="gray" icon="CircleDot" />
|
||||
<Text> Participants</Text>
|
||||
</GroupSwitcherItem>
|
||||
<GroupSwitcherItem to={navTo("/popover/settings")}>
|
||||
<Icon mr={2} icon="Circle" />
|
||||
<Icon mr={2} fill="none" stroke="gray" icon="Gear" />
|
||||
<Text> Settings</Text>
|
||||
</GroupSwitcherItem>
|
||||
<GroupSwitcherItem to={navTo("/popover/settings")}>
|
||||
<Icon mr={2} icon="Circle" />
|
||||
<Text>Group Settings</Text>
|
||||
</GroupSwitcherItem>
|
||||
<GroupSwitcherItem to={navTo("/invites")}>
|
||||
<Icon mr={2} fill="blue" icon="Circle" />
|
||||
<GroupSwitcherItem bottom to={navTo("/invites")}>
|
||||
<Icon
|
||||
mr={2}
|
||||
fill="rgba(0,0,0,0)"
|
||||
stroke="blue"
|
||||
icon="CreateGroup"
|
||||
/>
|
||||
<Text color="blue">Invite to group</Text>
|
||||
</GroupSwitcherItem>
|
||||
</Col>
|
||||
}
|
||||
>
|
||||
<Button width="max-content">
|
||||
<Box display="flex">
|
||||
<Box width="max-content">
|
||||
<Text>{title}</Text>
|
||||
</Box>
|
||||
<Icon icon="ChevronSouth" />
|
||||
<Box p={2} alignItems="center" display="flex">
|
||||
<Box mr={1}>
|
||||
<Text>{title}</Text>
|
||||
</Box>
|
||||
</Button>
|
||||
<Icon mt="2px" display="block" icon="ChevronSouth" />
|
||||
</Box>
|
||||
</Dropdown>
|
||||
<Link to={navTo("/popover/settings")}>
|
||||
<IconButton icon="MagnifyingGlass" />
|
||||
</Link>
|
||||
<Row collapse pr={1} justifyContent="flex-end" alignItems="center">
|
||||
<Link to={navTo("/invites")}>
|
||||
<Icon
|
||||
display="block"
|
||||
fill="rgba(0,0,0,0)"
|
||||
stroke="blue"
|
||||
icon="CreateGroup"
|
||||
/>
|
||||
</Link>
|
||||
<Link to={navTo("/popover/settings")}>
|
||||
<Icon display="block" ml={2} icon="Gear" />
|
||||
</Link>
|
||||
</Row>
|
||||
</Row>
|
||||
</Col>
|
||||
</Box>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { StoreState } from "~/logic/store/type";
|
||||
@ -20,19 +21,23 @@ export function PublishResource(props: PublishResourceProps) {
|
||||
const notebookContacts = props.contacts[association["group-path"]];
|
||||
|
||||
return (
|
||||
<NotebookRoutes
|
||||
api={api}
|
||||
ship={ship}
|
||||
book={book}
|
||||
contacts={props.contacts}
|
||||
groups={props.groups}
|
||||
notebook={notebook}
|
||||
notebookContacts={notebookContacts}
|
||||
rootUrl={baseUrl}
|
||||
baseUrl={`${baseUrl}/resource/publish/${ship}/${book}`}
|
||||
history={props.history}
|
||||
match={props.match}
|
||||
location={props.location}
|
||||
/>
|
||||
<Box height="100%" width="100%" overflowY="auto">
|
||||
<NotebookRoutes
|
||||
api={api}
|
||||
ship={ship}
|
||||
book={book}
|
||||
contacts={props.contacts}
|
||||
groups={props.groups}
|
||||
notebook={notebook}
|
||||
notebookContacts={notebookContacts}
|
||||
rootUrl={baseUrl}
|
||||
baseUrl={`${baseUrl}/resource/publish/${ship}/${book}`}
|
||||
history={props.history}
|
||||
match={props.match}
|
||||
location={props.location}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -15,9 +15,10 @@ const ClickBox = styled(Box)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const DropdownOptions = styled(Box)`
|
||||
const DropdownOptions = styled(Box)<{ pos: string }>`
|
||||
z-index: 20;
|
||||
position: absolute;
|
||||
${(p) => p.pos}: -1px;
|
||||
`;
|
||||
|
||||
export function Dropdown(props: DropdownProps) {
|
||||
@ -35,20 +36,18 @@ export function Dropdown(props: DropdownProps) {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const position = { [props.position]: "0px" };
|
||||
|
||||
const align = props.position === "right" ? "flex-end" : "flex-start";
|
||||
|
||||
return (
|
||||
<Box width="min-content" position="relative">
|
||||
<Box position={open ? "relative" : "static"}>
|
||||
<ClickBox onClick={() => setOpen((o) => !o)}> {children}</ClickBox>
|
||||
{open && (
|
||||
<DropdownOptions {...position} ref={dropdownRef}>
|
||||
<DropdownOptions pos={props.position} ref={dropdownRef}>
|
||||
<Col
|
||||
alignItems={align}
|
||||
width={props.width || "max-content"}
|
||||
border={1}
|
||||
borderColor="black"
|
||||
borderColor="lightGray"
|
||||
bg="white"
|
||||
borderRadius={2}
|
||||
>
|
||||
|
33
pkg/interface/src/views/components/FormikOnBlur.tsx
Normal file
33
pkg/interface/src/views/components/FormikOnBlur.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { useImperativeHandle, useEffect } from "react";
|
||||
import { FormikValues, useFormik, FormikProvider, FormikConfig } from "formik";
|
||||
|
||||
export function FormikOnBlur<
|
||||
Values extends FormikValues = FormikValues,
|
||||
ExtraProps = {}
|
||||
>(props: FormikConfig<Values> & ExtraProps) {
|
||||
const formikBag = useFormik<Values>({ ...props, validateOnBlur: true });
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
Object.keys(formikBag.errors || {}).length === 0 &&
|
||||
Object.keys(formikBag.touched || {}).length !== 0 &&
|
||||
!formikBag.isSubmitting
|
||||
) {
|
||||
const { values } = formikBag;
|
||||
formikBag.submitForm().then(() => {
|
||||
formikBag.resetForm({ values });
|
||||
});
|
||||
}
|
||||
}, [
|
||||
formikBag.errors,
|
||||
formikBag.touched,
|
||||
formikBag.submitForm,
|
||||
formikBag.values,
|
||||
]);
|
||||
|
||||
const { children, innerRef } = props;
|
||||
|
||||
useImperativeHandle(innerRef, () => formikBag);
|
||||
|
||||
return <FormikProvider value={formikBag}>{children}</FormikProvider>;
|
||||
}
|
@ -1,6 +1,12 @@
|
||||
import React from "react";
|
||||
import { Switch, Route, useLocation } from "react-router-dom";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Switch,
|
||||
Route,
|
||||
useLocation,
|
||||
RouteComponentProps,
|
||||
} from "react-router-dom";
|
||||
import { Center } from "@tlon/indigo-react";
|
||||
import _ from "lodash";
|
||||
|
||||
import { Resource } from "~/views/components/Resource";
|
||||
import { PopoverRoutes } from "~/views/apps/groups/components/PopoverRoutes";
|
||||
@ -15,6 +21,7 @@ import GlobalApi from "~/logic/api/global";
|
||||
import { StoreState } from "~/logic/store/type";
|
||||
import { UnjoinedResource } from "./UnjoinedResource";
|
||||
import { InvitePopover } from "../apps/groups/components/InvitePopover";
|
||||
import { useLocalStorageState } from "~/logic/lib/useLocalStorageState";
|
||||
|
||||
type GroupsPaneProps = StoreState & {
|
||||
baseUrl: string;
|
||||
@ -29,15 +36,48 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
const groupContacts = contacts[groupPath];
|
||||
const groupAssociation = associations.contacts[groupPath];
|
||||
const group = groups[groupPath];
|
||||
const location = useLocation();
|
||||
const mobileHide = location.pathname === baseUrl;
|
||||
|
||||
const [recentGroups, setRecentGroups] = useLocalStorageState<string[]>(
|
||||
"recent-groups",
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!groupPath) {
|
||||
return;
|
||||
}
|
||||
setRecentGroups((gs) => _.uniq([groupPath, ...gs]));
|
||||
}, [groupPath]);
|
||||
|
||||
if(!groupAssociation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const popovers = (routeProps: RouteComponentProps, baseUrl: string) => (
|
||||
<>
|
||||
<PopoverRoutes
|
||||
contacts={groupContacts}
|
||||
association={groupAssociation}
|
||||
group={group}
|
||||
api={api}
|
||||
s3={props.s3}
|
||||
{...routeProps}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<InvitePopover
|
||||
api={api}
|
||||
association={groupAssociation}
|
||||
baseUrl={baseUrl}
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
path={[
|
||||
relativePath("/resource/:app/:host/:name")
|
||||
]}
|
||||
path={[relativePath("/resource/:app/:host/:name")]}
|
||||
render={(routeProps) => {
|
||||
const { app, host, name } = routeProps.match.params as Record<
|
||||
string,
|
||||
@ -46,7 +86,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
const resource = `/${host}/${name}`;
|
||||
const appName = app as AppName;
|
||||
const association = associations[appName][resource];
|
||||
const resourceUrl = `${baseUrl}/resource/${app}${resource}`
|
||||
const resourceUrl = `${baseUrl}/resource/${app}${resource}`;
|
||||
|
||||
if (!association) {
|
||||
return null;
|
||||
@ -55,6 +95,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
return (
|
||||
<Skeleton
|
||||
mobileHide
|
||||
recentGroups={recentGroups}
|
||||
selected={resource}
|
||||
selectedApp={appName}
|
||||
selectedGroup={groupPath}
|
||||
@ -67,22 +108,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
association={association}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<PopoverRoutes
|
||||
contacts={groupContacts}
|
||||
association={groupAssociation}
|
||||
group={group}
|
||||
api={api}
|
||||
s3={props.s3}
|
||||
{...routeProps}
|
||||
baseUrl={resourceUrl}
|
||||
/>
|
||||
<InvitePopover
|
||||
api={api}
|
||||
association={groupAssociation}
|
||||
baseUrl={resourceUrl}
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
@ -94,17 +120,18 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
const appName = app as AppName;
|
||||
const appPath = `/${host}/${name}`;
|
||||
const association = associations[appName][appPath];
|
||||
const resourceUrl = `${baseUrl}/join/${app}/${host}/${name}`;
|
||||
return (
|
||||
<Skeleton mobileHide selectedGroup={groupPath} {...props} baseUrl={baseUrl}>
|
||||
<Skeleton
|
||||
recentGroups={recentGroups}
|
||||
mobileHide
|
||||
selected={appPath}
|
||||
selectedGroup={groupPath}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
<UnjoinedResource association={association} />
|
||||
<PopoverRoutes
|
||||
contacts={groupContacts}
|
||||
association={groupAssociation}
|
||||
group={group}
|
||||
api={api}
|
||||
{...routeProps}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
@ -113,18 +140,16 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
path={relativePath("")}
|
||||
render={(routeProps) => {
|
||||
return (
|
||||
<Skeleton selectedGroup={groupPath} {...props} baseUrl={baseUrl}>
|
||||
<Skeleton
|
||||
recentGroups={recentGroups}
|
||||
selectedGroup={groupPath}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
<Center display={["none", "auto"]}>
|
||||
Open something to get started
|
||||
</Center>
|
||||
<PopoverRoutes
|
||||
contacts={groupContacts}
|
||||
association={groupAssociation}
|
||||
group={group}
|
||||
api={api}
|
||||
{...routeProps}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Text,
|
||||
Icon,
|
||||
MenuItem as _MenuItem,
|
||||
IconButton,
|
||||
} from "@tlon/indigo-react";
|
||||
import { capitalize } from "lodash";
|
||||
|
||||
@ -15,6 +16,12 @@ import { alphabeticalOrder } from "~/logic/lib/util";
|
||||
import { GroupSwitcher } from "~/views/apps/groups/components/GroupSwitcher";
|
||||
import { AppInvites, Associations, AppAssociations } from "~/types";
|
||||
import { SidebarItem } from "./SidebarItem";
|
||||
import {
|
||||
SidebarListHeader,
|
||||
SidebarListConfig,
|
||||
SidebarSort,
|
||||
} from "./SidebarListHeader";
|
||||
import { useLocalStorageState } from "~/logic/lib/useLocalStorageState";
|
||||
|
||||
interface SidebarAppConfig {
|
||||
name: string;
|
||||
@ -31,81 +38,51 @@ export type SidebarItemStatus =
|
||||
| "disconnected"
|
||||
| "loading";
|
||||
|
||||
function ItemGroup(props: {
|
||||
app: string;
|
||||
apps: SidebarAppConfigs;
|
||||
associations: Associations;
|
||||
selected?: string;
|
||||
group: string;
|
||||
}) {
|
||||
const { selected, apps, associations } = props;
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const toggleOpen = () => setOpen((o) => !o);
|
||||
|
||||
const assoc = associations[props.app as AppName];
|
||||
|
||||
const items = _.pickBy(
|
||||
assoc,
|
||||
(value) =>
|
||||
value["group-path"] === props.group && value["app-name"] === props.app
|
||||
);
|
||||
|
||||
if (Object.keys(items).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box mb={3}>
|
||||
<Row alignItems="center" onClick={toggleOpen} pl={2} mb={1}>
|
||||
<Icon
|
||||
mb="1px"
|
||||
fill="lightGray"
|
||||
icon={open ? "TriangleSouth" : "TriangleEast"}
|
||||
/>
|
||||
<Text pl={1} color="lightGray">
|
||||
{capitalize(props.app)}
|
||||
</Text>
|
||||
</Row>
|
||||
{open && <SidebarItems selected={selected} items={items} apps={apps} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const apps = ["chat", "publish", "link"];
|
||||
const GroupItems = (props: {
|
||||
associations: Associations;
|
||||
group: string;
|
||||
apps: SidebarAppConfigs;
|
||||
selected?: string;
|
||||
}) => (
|
||||
<>
|
||||
{apps.map((app) => (
|
||||
<ItemGroup app={app} {...props} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
function SidebarItems(props: {
|
||||
apps: SidebarAppConfigs;
|
||||
items: AppAssociations;
|
||||
selected?: string;
|
||||
}) {
|
||||
const { items, associations, selected } = props;
|
||||
|
||||
const ordered = Object.keys(items).sort((a, b) => {
|
||||
const aAssoc = items[a];
|
||||
const bAssoc = items[b];
|
||||
function sidebarSort(
|
||||
associations: AppAssociations
|
||||
): Record<SidebarSort, (a: string, b: string) => number> {
|
||||
const alphabetical = (a: string, b: string) => {
|
||||
const aAssoc = associations[a];
|
||||
const bAssoc = associations[b];
|
||||
const aTitle = aAssoc?.metadata?.title || b;
|
||||
const bTitle = bAssoc?.metadata?.title || b;
|
||||
|
||||
return alphabeticalOrder(aTitle, bTitle);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
asc: alphabetical,
|
||||
desc: (a, b) => alphabetical(b, a),
|
||||
};
|
||||
}
|
||||
|
||||
const apps = ["chat", "publish", "link"];
|
||||
|
||||
function SidebarItems(props: {
|
||||
apps: SidebarAppConfigs;
|
||||
config: SidebarListConfig;
|
||||
associations: Associations;
|
||||
group: string;
|
||||
selected?: string;
|
||||
}) {
|
||||
const { selected, group, config } = props;
|
||||
const associations = {
|
||||
...props.associations.chat,
|
||||
...props.associations.publish,
|
||||
...props.associations.link,
|
||||
};
|
||||
|
||||
const ordered = Object.keys(associations)
|
||||
.filter((a) => {
|
||||
const assoc = associations[a];
|
||||
return assoc["group-path"] === group;
|
||||
})
|
||||
.sort(sidebarSort(associations)[config.sortBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ordered.map((path) => {
|
||||
const assoc = items[path];
|
||||
const assoc = associations[path];
|
||||
return (
|
||||
<SidebarItem
|
||||
key={path}
|
||||
@ -113,6 +90,7 @@ function SidebarItems(props: {
|
||||
selected={path === selected}
|
||||
association={assoc}
|
||||
apps={props.apps}
|
||||
hideUnjoined={config.hideUnjoined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -122,6 +100,7 @@ function SidebarItems(props: {
|
||||
|
||||
interface SidebarProps {
|
||||
children: ReactNode;
|
||||
recentGroups: string[];
|
||||
invites: AppInvites;
|
||||
api: GlobalApi;
|
||||
associations: Associations;
|
||||
@ -139,6 +118,17 @@ export function Sidebar(props: SidebarProps) {
|
||||
if (!groupAsssociation) {
|
||||
return null;
|
||||
}
|
||||
if (!associations) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [config, setConfig] = useLocalStorageState<SidebarListConfig>(
|
||||
`group-config:${props.selectedGroup}`,
|
||||
{
|
||||
sortBy: "asc",
|
||||
hideUnjoined: false,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
display={display}
|
||||
@ -153,7 +143,12 @@ export function Sidebar(props: SidebarProps) {
|
||||
bg="white"
|
||||
position="relative"
|
||||
>
|
||||
<GroupSwitcher baseUrl={props.baseUrl} association={groupAsssociation} />
|
||||
<GroupSwitcher
|
||||
associations={associations}
|
||||
recentGroups={props.recentGroups}
|
||||
baseUrl={props.baseUrl}
|
||||
association={groupAsssociation}
|
||||
/>
|
||||
{Object.keys(invites).map((appPath) =>
|
||||
Object.keys(invites[appPath]).map((uid) => (
|
||||
<SidebarInvite
|
||||
@ -164,11 +159,13 @@ export function Sidebar(props: SidebarProps) {
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<GroupItems
|
||||
group={props.selectedGroup}
|
||||
apps={apps}
|
||||
<SidebarListHeader initialValues={config} handleSubmit={setConfig} />
|
||||
<SidebarItems
|
||||
config={config}
|
||||
associations={associations}
|
||||
selected={selected}
|
||||
associations={associations || {}}
|
||||
group={props.selectedGroup}
|
||||
apps={props.apps}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@ -22,7 +22,15 @@ function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
|
||||
}
|
||||
}
|
||||
|
||||
const getAppIcon = (app: string) => {
|
||||
if (app === "link") {
|
||||
return "Links";
|
||||
}
|
||||
return _.capitalize(app);
|
||||
};
|
||||
|
||||
export function SidebarItem(props: {
|
||||
hideUnjoined: boolean;
|
||||
association: Association;
|
||||
path: string;
|
||||
selected: boolean;
|
||||
@ -43,6 +51,12 @@ export function SidebarItem(props: {
|
||||
? `/~groups${groupPath}/resource/${appName}${appPath}`
|
||||
: `/~groups${groupPath}/join/${appName}${appPath}`;
|
||||
|
||||
const color = selected ? 'black' : isSynced ? 'gray' : 'lightGray';
|
||||
|
||||
if(props.hideUnjoined && !isSynced) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HoverBoxLink
|
||||
to={to}
|
||||
@ -58,11 +72,13 @@ export function SidebarItem(props: {
|
||||
selected={selected}
|
||||
>
|
||||
<Row alignItems="center">
|
||||
<Box ml={2} lineHeight="1.33" fontWeight={hasUnread ? "600" : "400"}>
|
||||
<Text color={isSynced ? "black" : "lightGray"}>{title}</Text>
|
||||
<Icon display="block" fill="rgba(0,0,0,0)" stroke={color} icon={getAppIcon(appName)} />
|
||||
<Box flexShrink={2} ml={2} lineHeight="1.33" fontWeight={hasUnread ? "600" : "400"}>
|
||||
<Text color={selected || isSynced ? 'black' : 'lightGray'}>
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
</Row>
|
||||
<SidebarItemIndicator status={status} />
|
||||
</HoverBoxLink>
|
||||
);
|
||||
}
|
||||
|
60
pkg/interface/src/views/components/SidebarListHeader.tsx
Normal file
60
pkg/interface/src/views/components/SidebarListHeader.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useCallback } from "react";
|
||||
import * as Yup from "yup";
|
||||
import { Row, Box, Icon, Radio, Col, Checkbox } from "@tlon/indigo-react";
|
||||
import { FormikOnBlur } from "./FormikOnBlur";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import { FormikHelpers } from "formik";
|
||||
|
||||
export type SidebarSort = "asc" | "desc";
|
||||
|
||||
export interface SidebarListConfig {
|
||||
sortBy: SidebarSort;
|
||||
hideUnjoined: boolean;
|
||||
}
|
||||
|
||||
export function SidebarListHeader(props: {
|
||||
initialValues: SidebarListConfig;
|
||||
handleSubmit: (c: SidebarListConfig) => void;
|
||||
}) {
|
||||
const onSubmit = useCallback(
|
||||
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
|
||||
props.handleSubmit(values);
|
||||
actions.setSubmitting(false);
|
||||
},
|
||||
[props.handleSubmit]
|
||||
);
|
||||
|
||||
return (
|
||||
<Row alignItems="center" justifyContent="space-between" py={2} px={3}>
|
||||
<Box>
|
||||
{props.initialValues.hideUnjoined ? "Joined Channels" : "All Channels"}
|
||||
</Box>
|
||||
<Dropdown
|
||||
width="200px"
|
||||
position="right"
|
||||
options={
|
||||
<FormikOnBlur initialValues={props.initialValues} onSubmit={onSubmit}>
|
||||
<Col>
|
||||
<Col borderBottom={1} borderBottomColor="washedGray" p={2}>
|
||||
<Box color="gray" mt={2} mb={4}>
|
||||
Sort Order
|
||||
</Box>
|
||||
<Radio label="A -> Z" id="asc" name="sortBy" />
|
||||
<Radio label="Z -> A" id="desc" name="sortBy" />
|
||||
</Col>
|
||||
<Col px={2}>
|
||||
<Checkbox
|
||||
mt={4}
|
||||
id="hideUnjoined"
|
||||
label="Hide Unsubscribed Channels"
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
</FormikOnBlur>
|
||||
}
|
||||
>
|
||||
<Icon stroke="gray" icon="Menu" />
|
||||
</Dropdown>
|
||||
</Row>
|
||||
);
|
||||
}
|
@ -10,8 +10,10 @@ import { Notebooks } from "~/types/publish-update";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Path, AppName } from "~/types/noun";
|
||||
import { LinkCollections } from "~/types/link-update";
|
||||
import styled from "styled-components";
|
||||
interface SkeletonProps {
|
||||
children: ReactNode;
|
||||
recentGroups: string[];
|
||||
associations: Associations;
|
||||
chatSynced: ChatHookUpdate | null;
|
||||
linkListening: Set<Path>;
|
||||
@ -31,6 +33,12 @@ const buntAppConfig = (name: string) => ({
|
||||
getStatus: (s: string) => undefined,
|
||||
});
|
||||
|
||||
const TruncatedBox = styled(Box)`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export function Skeleton(props: SkeletonProps) {
|
||||
const chatConfig = {
|
||||
name: "chat",
|
||||
@ -108,6 +116,7 @@ export function Skeleton(props: SkeletonProps) {
|
||||
gridTemplateRows="32px 1fr"
|
||||
>
|
||||
<Sidebar
|
||||
recentGroups={props.recentGroups}
|
||||
selected={props.selected}
|
||||
selectedGroup={props.selectedGroup}
|
||||
selectedApp={props.selectedApp}
|
||||
@ -134,7 +143,14 @@ export function Skeleton(props: SkeletonProps) {
|
||||
>
|
||||
<Link to={`/~groups${props.selectedGroup}`}> {"<- Back"}</Link>
|
||||
</Box>
|
||||
<Box>{association?.metadata?.title}</Box>
|
||||
<Box mr={2}>{association?.metadata?.title}</Box>
|
||||
<TruncatedBox
|
||||
maxWidth="50%"
|
||||
flexShrink={1}
|
||||
color="gray"
|
||||
>
|
||||
{association?.metadata?.description}
|
||||
</TruncatedBox>
|
||||
</Box>
|
||||
{props.children}
|
||||
</Box>
|
||||
|
Loading…
Reference in New Issue
Block a user