groups: update to new designs

This commit is contained in:
Liam Fitzgerald 2020-09-18 15:48:26 +10:00
parent 94050f150e
commit 971d38290a
12 changed files with 434 additions and 201 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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}
>

View 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>;
}

View File

@ -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>
);
}}

View File

@ -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>
);

View File

@ -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>
);
}

View 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>
);
}

View File

@ -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>