Merge branch 'main' of github.com:outline/outline

This commit is contained in:
Tom Moor
2024-06-13 18:19:46 -04:00
19 changed files with 219 additions and 329 deletions

View File

@@ -1,13 +1,9 @@
import { RovingTabIndexProvider } from "@getoutline/react-roving-tabindex";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import {
useCompositeState,
Composite,
CompositeStateReturn,
} from "reakit/Composite";
type Props = React.HTMLAttributes<HTMLDivElement> & { type Props = React.HTMLAttributes<HTMLDivElement> & {
children: (composite: CompositeStateReturn) => React.ReactNode; children: () => React.ReactNode;
onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void; onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void;
}; };
@@ -15,40 +11,36 @@ function ArrowKeyNavigation(
{ children, onEscape, ...rest }: Props, { children, onEscape, ...rest }: Props,
ref: React.RefObject<HTMLDivElement> ref: React.RefObject<HTMLDivElement>
) { ) {
const composite = useCompositeState();
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(ev) => { (ev: React.KeyboardEvent<HTMLDivElement>) => {
if (onEscape) { if (onEscape) {
if (ev.nativeEvent.isComposing) { if (ev.nativeEvent.isComposing) {
return; return;
} }
if (ev.key === "Escape") { if (ev.key === "Escape") {
ev.preventDefault();
onEscape(ev); onEscape(ev);
} }
if ( if (
ev.key === "ArrowUp" && ev.key === "ArrowUp" &&
composite.currentId === composite.items[0].id // If the first item is focused and the user presses ArrowUp
ev.currentTarget.firstElementChild === document.activeElement
) { ) {
onEscape(ev); onEscape(ev);
} }
} }
}, },
[composite.currentId, composite.items, onEscape] [onEscape]
); );
return ( return (
<Composite <RovingTabIndexProvider options={{ focusOnClick: true, direction: "both" }}>
{...rest} <div {...rest} onKeyDown={handleKeyDown} ref={ref}>
{...composite} {children()}
onKeyDown={handleKeyDown} </div>
role="menu" </RovingTabIndexProvider>
ref={ref}
>
{children(composite)}
</Composite>
); );
} }

View File

@@ -1,8 +1,11 @@
import {
useFocusEffect,
useRovingTabIndex,
} from "@getoutline/react-roving-tabindex";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { CompositeStateReturn, CompositeItem } from "reakit/Composite";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import { s } from "@shared/styles"; import { s } from "@shared/styles";
@@ -32,7 +35,7 @@ type Props = {
showPin?: boolean; showPin?: boolean;
showDraft?: boolean; showDraft?: boolean;
showTemplate?: boolean; showTemplate?: boolean;
} & CompositeStateReturn; };
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi; const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
@@ -49,6 +52,15 @@ function DocumentListItem(
const user = useCurrentUser(); const user = useCurrentUser();
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean(); const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
let itemRef: React.Ref<HTMLAnchorElement> =
React.useRef<HTMLAnchorElement>(null);
if (ref) {
itemRef = ref;
}
const { focused, ...rovingTabIndex } = useRovingTabIndex(itemRef, false);
useFocusEffect(focused, itemRef);
const { const {
document, document,
showParentDocuments, showParentDocuments,
@@ -68,9 +80,8 @@ function DocumentListItem(
!document.isDraft && !document.isArchived && !document.isTemplate; !document.isDraft && !document.isArchived && !document.isTemplate;
return ( return (
<CompositeItem <DocumentLink
as={DocumentLink} ref={itemRef}
ref={ref}
dir={document.dir} dir={document.dir}
role="menuitem" role="menuitem"
$isStarred={document.isStarred} $isStarred={document.isStarred}
@@ -82,6 +93,7 @@ function DocumentListItem(
}, },
}} }}
{...rest} {...rest}
{...rovingTabIndex}
> >
<Content> <Content>
<Heading dir={document.dir}> <Heading dir={document.dir}>
@@ -142,7 +154,7 @@ function DocumentListItem(
modal={false} modal={false}
/> />
</Actions> </Actions>
</CompositeItem> </DocumentLink>
); );
} }

View File

@@ -11,16 +11,12 @@ import {
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { CompositeStateReturn } from "reakit/Composite";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { s } from "@shared/styles"; import { s } from "@shared/styles";
import Document from "~/models/Document"; import Document from "~/models/Document";
import Event from "~/models/Event"; import Event from "~/models/Event";
import Avatar from "~/components/Avatar"; import Avatar from "~/components/Avatar";
import CompositeItem, { import Item, { Actions, Props as ItemProps } from "~/components/List/Item";
Props as ItemProps,
} from "~/components/List/CompositeItem";
import Item, { Actions } from "~/components/List/Item";
import Time from "~/components/Time"; import Time from "~/components/Time";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import RevisionMenu from "~/menus/RevisionMenu"; import RevisionMenu from "~/menus/RevisionMenu";
@@ -32,7 +28,7 @@ type Props = {
document: Document; document: Document;
event: Event; event: Event;
latest?: boolean; latest?: boolean;
} & CompositeStateReturn; };
const EventListItem = ({ event, latest, document, ...rest }: Props) => { const EventListItem = ({ event, latest, document, ...rest }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -176,11 +172,7 @@ const BaseItem = React.forwardRef(function _BaseItem(
{ to, ...rest }: ItemProps, { to, ...rest }: ItemProps,
ref?: React.Ref<HTMLAnchorElement> ref?: React.Ref<HTMLAnchorElement>
) { ) {
if (to) { return <ListItem to={to} ref={ref} {...rest} />;
return <CompositeListItem to={to} ref={ref} {...rest} />;
}
return <ListItem ref={ref} {...rest} />;
}); });
const Subtitle = styled.span` const Subtitle = styled.span`
@@ -240,8 +232,4 @@ const ListItem = styled(Item)`
${ItemStyle} ${ItemStyle}
`; `;
const CompositeListItem = styled(CompositeItem)`
${ItemStyle}
`;
export default observer(EventListItem); export default observer(EventListItem);

View File

@@ -1,17 +0,0 @@
import * as React from "react";
import {
CompositeStateReturn,
CompositeItem as BaseCompositeItem,
} from "reakit/Composite";
import Item, { Props as ItemProps } from "./Item";
export type Props = ItemProps & CompositeStateReturn;
function CompositeItem(
{ to, ...rest }: Props,
ref?: React.Ref<HTMLAnchorElement>
) {
return <BaseCompositeItem as={Item} to={to} {...rest} ref={ref} />;
}
export default React.forwardRef(CompositeItem);

View File

@@ -1,3 +1,7 @@
import {
useFocusEffect,
useRovingTabIndex,
} from "@getoutline/react-roving-tabindex";
import { LocationDescriptor } from "history"; import { LocationDescriptor } from "history";
import * as React from "react"; import * as React from "react";
import styled, { useTheme } from "styled-components"; import styled, { useTheme } from "styled-components";
@@ -33,6 +37,18 @@ const ListItem = (
const theme = useTheme(); const theme = useTheme();
const compact = !subtitle; const compact = !subtitle;
let itemRef: React.Ref<HTMLAnchorElement> =
React.useRef<HTMLAnchorElement>(null);
if (ref) {
itemRef = ref;
}
const { focused, ...rovingTabIndex } = useRovingTabIndex(
itemRef as React.RefObject<HTMLAnchorElement>,
to ? false : true
);
useFocusEffect(focused, itemRef as React.RefObject<HTMLAnchorElement>);
const content = (selected: boolean) => ( const content = (selected: boolean) => (
<> <>
{image && <Image>{image}</Image>} {image && <Image>{image}</Image>}
@@ -59,13 +75,20 @@ const ListItem = (
if (to) { if (to) {
return ( return (
<Wrapper <Wrapper
ref={ref} ref={itemRef}
$border={border} $border={border}
$small={small} $small={small}
activeStyle={{ activeStyle={{
background: theme.accent, background: theme.accent,
}} }}
{...rest} {...rest}
{...rovingTabIndex}
onClick={(ev) => {
if (rest.onClick) {
rest.onClick(ev);
}
rovingTabIndex.onClick(ev);
}}
as={NavLink} as={NavLink}
to={to} to={to}
> >
@@ -75,7 +98,7 @@ const ListItem = (
} }
return ( return (
<Wrapper ref={ref} $border={border} $small={small} {...rest}> <Wrapper ref={itemRef} $border={border} $small={small} {...rest}>
{content(false)} {content(false)}
</Wrapper> </Wrapper>
); );

View File

@@ -42,7 +42,7 @@ const PaginatedDocumentList = React.memo<Props>(function PaginatedDocumentList({
fetch={fetch} fetch={fetch}
options={options} options={options}
renderError={(props) => <Error {...props} />} renderError={(props) => <Error {...props} />}
renderItem={(item: Document, _index, compositeProps) => ( renderItem={(item: Document, _index) => (
<DocumentListItem <DocumentListItem
key={item.id} key={item.id}
document={item} document={item}
@@ -52,7 +52,6 @@ const PaginatedDocumentList = React.memo<Props>(function PaginatedDocumentList({
showPublished={showPublished} showPublished={showPublished}
showTemplate={showTemplate} showTemplate={showTemplate}
showDraft={showDraft} showDraft={showDraft}
{...compositeProps}
/> />
)} )}
{...rest} {...rest}

View File

@@ -30,13 +30,12 @@ const PaginatedEventList = React.memo<Props>(function PaginatedEventList({
heading={heading} heading={heading}
fetch={fetch} fetch={fetch}
options={options} options={options}
renderItem={(item: Event, index, compositeProps) => ( renderItem={(item: Event, index) => (
<EventListItem <EventListItem
key={item.id} key={item.id}
event={item} event={item}
document={document} document={document}
latest={index === 0} latest={index === 0}
{...compositeProps}
/> />
)} )}
renderHeading={(name) => <Heading>{name}</Heading>} renderHeading={(name) => <Heading>{name}</Heading>}

View File

@@ -4,7 +4,6 @@ import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { withTranslation, WithTranslation } from "react-i18next"; import { withTranslation, WithTranslation } from "react-i18next";
import { Waypoint } from "react-waypoint"; import { Waypoint } from "react-waypoint";
import { CompositeStateReturn } from "reakit/Composite";
import { Pagination } from "@shared/constants"; import { Pagination } from "@shared/constants";
import RootStore from "~/stores/RootStore"; import RootStore from "~/stores/RootStore";
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation"; import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
@@ -30,11 +29,7 @@ type Props<T> = WithTranslation &
loading?: React.ReactElement; loading?: React.ReactElement;
items?: T[]; items?: T[];
className?: string; className?: string;
renderItem: ( renderItem: (item: T, index: number) => React.ReactNode;
item: T,
index: number,
compositeProps: CompositeStateReturn
) => React.ReactNode;
renderError?: (options: { renderError?: (options: {
error: Error; error: Error;
retry: () => void; retry: () => void;
@@ -194,10 +189,10 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
onEscape={onEscape} onEscape={onEscape}
className={this.props.className} className={this.props.className}
> >
{(composite: CompositeStateReturn) => { {() => {
let previousHeading = ""; let previousHeading = "";
return items.slice(0, this.renderCount).map((item, index) => { return items.slice(0, this.renderCount).map((item, index) => {
const children = this.props.renderItem(item, index, composite); const children = this.props.renderItem(item, index);
// If there is no renderHeading method passed then no date // If there is no renderHeading method passed then no date
// headings are rendered // headings are rendered

View File

@@ -1,7 +1,10 @@
import {
useFocusEffect,
useRovingTabIndex,
} from "@getoutline/react-roving-tabindex";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { CompositeItem } from "reakit/Composite";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import breakpoint from "styled-components-breakpoint"; import breakpoint from "styled-components-breakpoint";
import { s, ellipsis } from "@shared/styles"; import { s, ellipsis } from "@shared/styles";
@@ -34,10 +37,18 @@ function DocumentListItem(
) { ) {
const { document, highlight, context, shareId, ...rest } = props; const { document, highlight, context, shareId, ...rest } = props;
let itemRef: React.Ref<HTMLAnchorElement> =
React.useRef<HTMLAnchorElement>(null);
if (ref) {
itemRef = ref;
}
const { focused, ...rovingTabIndex } = useRovingTabIndex(itemRef, false);
useFocusEffect(focused, itemRef);
return ( return (
<CompositeItem <DocumentLink
as={DocumentLink} ref={itemRef}
ref={ref}
dir={document.dir} dir={document.dir}
to={{ to={{
pathname: shareId pathname: shareId
@@ -48,6 +59,13 @@ function DocumentListItem(
}, },
}} }}
{...rest} {...rest}
{...rovingTabIndex}
onClick={(ev) => {
if (rest.onClick) {
rest.onClick(ev);
}
rovingTabIndex.onClick(ev);
}}
> >
<Content> <Content>
<Heading dir={document.dir}> <Heading dir={document.dir}>
@@ -66,7 +84,7 @@ function DocumentListItem(
/> />
} }
</Content> </Content>
</CompositeItem> </DocumentLink>
); );
} }

View File

@@ -206,7 +206,7 @@ function SearchPopover({ shareId }: Props) {
<NoResults>{t("No results for {{query}}", { query })}</NoResults> <NoResults>{t("No results for {{query}}", { query })}</NoResults>
} }
loading={<PlaceholderList count={3} header={{ height: 20 }} />} loading={<PlaceholderList count={3} header={{ height: 20 }} />}
renderItem={(item: SearchResult, index, compositeProps) => ( renderItem={(item: SearchResult, index) => (
<SearchListItem <SearchListItem
key={item.document.id} key={item.document.id}
shareId={shareId} shareId={shareId}
@@ -215,7 +215,6 @@ function SearchPopover({ shareId }: Props) {
context={item.context} context={item.context}
highlight={cachedQuery} highlight={cachedQuery}
onClick={handleSearchItemClick} onClick={handleSearchItemClick}
{...compositeProps}
/> />
)} )}
/> />

View File

@@ -1,49 +0,0 @@
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import Button from "~/components/Button";
import ListItem from "~/components/List/Item";
import Time from "~/components/Time";
type Props = {
user: User;
canEdit: boolean;
onAdd: () => void;
};
const UserListItem = ({ user, onAdd, canEdit }: Props) => {
const { t } = useTranslation();
return (
<ListItem
title={user.name}
image={<Avatar model={user} size={32} />}
subtitle={
<>
{user.lastActiveAt ? (
<Trans>
Active <Time dateTime={user.lastActiveAt} /> ago
</Trans>
) : (
t("Never signed in")
)}
{user.isInvited && <Badge>{t("Invited")}</Badge>}
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</>
}
actions={
canEdit ? (
<Button type="button" onClick={onAdd} icon={<PlusIcon />} neutral>
{t("Add")}
</Button>
) : undefined
}
/>
);
};
export default observer(UserListItem);

View File

@@ -53,8 +53,8 @@ function Search(props: Props) {
// refs // refs
const searchInputRef = React.useRef<HTMLInputElement | null>(null); const searchInputRef = React.useRef<HTMLInputElement | null>(null);
const resultListCompositeRef = React.useRef<HTMLDivElement | null>(null); const resultListRef = React.useRef<HTMLDivElement | null>(null);
const recentSearchesCompositeRef = React.useRef<HTMLDivElement | null>(null); const recentSearchesRef = React.useRef<HTMLDivElement | null>(null);
// filters // filters
const query = decodeURIComponentSafe(routeMatch.params.term ?? ""); const query = decodeURIComponentSafe(routeMatch.params.term ?? "");
@@ -178,19 +178,9 @@ function Search(props: Props) {
} }
} }
const firstResultItem = ( const firstItem = (resultListRef.current?.firstElementChild ??
resultListCompositeRef.current?.querySelectorAll( recentSearchesRef.current?.firstElementChild) as HTMLAnchorElement;
"[href]"
) as NodeListOf<HTMLAnchorElement>
)?.[0];
const firstRecentSearchItem = (
recentSearchesCompositeRef.current?.querySelectorAll(
"li > [href]"
) as NodeListOf<HTMLAnchorElement>
)?.[0];
const firstItem = firstResultItem ?? firstRecentSearchItem;
firstItem?.focus(); firstItem?.focus();
} }
}; };
@@ -277,11 +267,11 @@ function Search(props: Props) {
)} )}
<ResultList column> <ResultList column>
<StyledArrowKeyNavigation <StyledArrowKeyNavigation
ref={resultListCompositeRef} ref={resultListRef}
onEscape={handleEscape} onEscape={handleEscape}
aria-label={t("Search Results")} aria-label={t("Search Results")}
> >
{(compositeProps) => {() =>
data?.length data?.length
? data.map((result) => ( ? data.map((result) => (
<DocumentListItem <DocumentListItem
@@ -291,7 +281,6 @@ function Search(props: Props) {
context={result.context} context={result.context}
showCollection showCollection
showTemplate showTemplate
{...compositeProps}
/> />
)) ))
: null : null
@@ -305,10 +294,7 @@ function Search(props: Props) {
</ResultList> </ResultList>
</> </>
) : documentId || collectionId ? null : ( ) : documentId || collectionId ? null : (
<RecentSearches <RecentSearches ref={recentSearchesRef} onEscape={handleEscape} />
ref={recentSearchesCompositeRef}
onEscape={handleEscape}
/>
)} )}
</ResultsWrapper> </ResultsWrapper>
</Scene> </Scene>

View File

@@ -0,0 +1,92 @@
import {
useFocusEffect,
useRovingTabIndex,
} from "@getoutline/react-roving-tabindex";
import { CloseIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { s } from "@shared/styles";
import type SearchQuery from "~/models/SearchQuery";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import { hover } from "~/styles";
import { searchPath } from "~/utils/routeHelpers";
type Props = {
searchQuery: SearchQuery;
};
function RecentSearchListItem({ searchQuery }: Props) {
const { t } = useTranslation();
const ref = React.useRef<HTMLAnchorElement>(null);
const { focused, ...rovingTabIndex } = useRovingTabIndex(ref, false);
useFocusEffect(focused, ref);
return (
<RecentSearch
to={searchPath(searchQuery.query)}
ref={ref}
{...rovingTabIndex}
>
{searchQuery.query}
<Tooltip content={t("Remove search")} delay={150}>
<RemoveButton
aria-label={t("Remove search")}
onClick={async (ev) => {
ev.preventDefault();
await searchQuery.delete();
}}
>
<CloseIcon />
</RemoveButton>
</Tooltip>
</RecentSearch>
);
}
const RemoveButton = styled(NudeButton)`
opacity: 0;
color: ${s("textTertiary")};
&:hover {
color: ${s("text")};
}
`;
const RecentSearch = styled(Link)`
display: flex;
justify-content: space-between;
color: ${s("textSecondary")};
cursor: var(--pointer);
padding: 1px 4px;
border-radius: 4px;
position: relative;
font-size: 14px;
&:before {
content: "·";
color: ${s("textTertiary")};
position: absolute;
left: -8px;
}
&:focus-visible {
outline: none;
}
&:focus,
&:${hover} {
color: ${s("text")};
background: ${s("secondaryBackground")};
${RemoveButton} {
opacity: 1;
}
}
`;
export default RecentSearchListItem;

View File

@@ -1,18 +1,12 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CloseIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { CompositeItem } from "reakit/Composite";
import styled from "styled-components"; import styled from "styled-components";
import { s } from "@shared/styles"; import { s } from "@shared/styles";
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation"; import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
import Fade from "~/components/Fade"; import Fade from "~/components/Fade";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import { hover } from "~/styles"; import RecentSearchListItem from "./RecentSearchListItem";
import { searchPath } from "~/utils/routeHelpers";
type Props = { type Props = {
/** Callback when the Escape key is pressed while navigating the list */ /** Callback when the Escape key is pressed while navigating the list */
@@ -36,39 +30,20 @@ function RecentSearches(
const content = searches.recent.length ? ( const content = searches.recent.length ? (
<> <>
<Heading>{t("Recent searches")}</Heading> <Heading>{t("Recent searches")}</Heading>
<List> <StyledArrowKeyNavigation
<ArrowKeyNavigation ref={ref}
ref={ref} onEscape={onEscape}
onEscape={onEscape} aria-label={t("Recent searches")}
aria-label={t("Search Results")} >
> {() =>
{(compositeProps) => searches.recent.map((searchQuery) => (
searches.recent.map((searchQuery) => ( <RecentSearchListItem
<ListItem key={searchQuery.id}> key={searchQuery.id}
<CompositeItem searchQuery={searchQuery}
as={RecentSearch} />
to={searchPath(searchQuery.query)} ))
role="menuitem" }
{...compositeProps} </StyledArrowKeyNavigation>
>
{searchQuery.query}
<Tooltip content={t("Remove search")} delay={150}>
<RemoveButton
aria-label={t("Remove search")}
onClick={async (ev) => {
ev.preventDefault();
await searchQuery.delete();
}}
>
<CloseIcon />
</RemoveButton>
</Tooltip>
</CompositeItem>
</ListItem>
))
}
</ArrowKeyNavigation>
</List>
</> </>
) : null; ) : null;
@@ -83,55 +58,9 @@ const Heading = styled.h2`
margin-bottom: 0; margin-bottom: 0;
`; `;
const List = styled.ol` const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
padding: 0; padding: 0;
margin-top: 8px; margin-top: 8px;
`; `;
const ListItem = styled.li`
font-size: 14px;
padding: 0;
list-style: none;
position: relative;
&:before {
content: "·";
color: ${s("textTertiary")};
position: absolute;
left: -8px;
}
`;
const RemoveButton = styled(NudeButton)`
opacity: 0;
color: ${s("textTertiary")};
&:hover {
color: ${s("text")};
}
`;
const RecentSearch = styled(Link)`
display: flex;
justify-content: space-between;
color: ${s("textSecondary")};
cursor: var(--pointer);
padding: 1px 4px;
border-radius: 4px;
&:focus-visible {
outline: none;
}
&:focus,
&: ${hover} {
color: ${s("text")};
background: ${s("secondaryBackground")};
${RemoveButton} {
opacity: 1;
}
}
`;
export default observer(React.forwardRef(RecentSearches)); export default observer(React.forwardRef(RecentSearches));

View File

@@ -1,39 +0,0 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import Share from "~/models/Share";
import ListItem from "~/components/List/Item";
import Time from "~/components/Time";
import ShareMenu from "~/menus/ShareMenu";
type Props = {
share: Share;
};
const ShareListItem = ({ share }: Props) => {
const { t } = useTranslation();
const { lastAccessedAt } = share;
return (
<ListItem
title={share.documentTitle}
subtitle={
<>
{t("Shared")} <Time dateTime={share.createdAt} addSuffix />{" "}
{t("by {{ name }}", {
name: share.createdBy.name,
})}{" "}
{lastAccessedAt && (
<>
{" "}
&middot; {t("Last accessed")}{" "}
<Time dateTime={lastAccessedAt} addSuffix />
</>
)}
</>
}
actions={<ShareMenu share={share} />}
/>
);
};
export default ShareListItem;

View File

@@ -1,50 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import ListItem from "~/components/List/Item";
import Time from "~/components/Time";
import UserMenu from "~/menus/UserMenu";
type Props = {
user: User;
showMenu: boolean;
};
const UserListItem = ({ user, showMenu }: Props) => {
const { t } = useTranslation();
return (
<ListItem
title={<Title>{user.name}</Title>}
image={<Avatar model={user} size={32} />}
subtitle={
<>
{user.email ? `${user.email} · ` : undefined}
{user.lastActiveAt ? (
<Trans>
Active <Time dateTime={user.lastActiveAt} /> ago
</Trans>
) : (
t("Invited")
)}
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
{user.isSuspended && <Badge>{t("Suspended")}</Badge>}
</>
}
actions={showMenu ? <UserMenu user={user} /> : undefined}
/>
);
};
const Title = styled.span`
&:hover {
text-decoration: underline;
cursor: var(--pointer);
}
`;
export default observer(UserListItem);

View File

@@ -70,6 +70,7 @@
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@getoutline/react-roving-tabindex": "^3.2.2",
"@getoutline/y-prosemirror": "^1.0.18", "@getoutline/y-prosemirror": "^1.0.18",
"@hocuspocus/extension-throttle": "1.1.2", "@hocuspocus/extension-throttle": "1.1.2",
"@hocuspocus/provider": "1.1.2", "@hocuspocus/provider": "1.1.2",

View File

@@ -749,13 +749,13 @@
"Past year": "Past year", "Past year": "Past year",
"Remove document filter": "Remove document filter", "Remove document filter": "Remove document filter",
"Any status": "Any status", "Any status": "Any status",
"Search Results": "Search Results",
"Remove search": "Remove search", "Remove search": "Remove search",
"Any author": "Any author", "Any author": "Any author",
"Author": "Author", "Author": "Author",
"We were unable to find the page youre looking for.": "We were unable to find the page youre looking for.", "We were unable to find the page youre looking for.": "We were unable to find the page youre looking for.",
"Search titles only": "Search titles only", "Search titles only": "Search titles only",
"No documents found for your search filters.": "No documents found for your search filters.", "No documents found for your search filters.": "No documents found for your search filters.",
"Search Results": "Search Results",
"API Keys": "API Keys", "API Keys": "API Keys",
"Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.", "Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Active": "Active", "Active": "Active",
@@ -799,11 +799,9 @@
"Drag and drop the zip file from Notion's HTML export option, or click to upload": "Drag and drop the zip file from Notion's HTML export option, or click to upload", "Drag and drop the zip file from Notion's HTML export option, or click to upload": "Drag and drop the zip file from Notion's HTML export option, or click to upload",
"Last active": "Last active", "Last active": "Last active",
"Guest": "Guest", "Guest": "Guest",
"Shared": "Shared",
"by {{ name }}": "by {{ name }}",
"Last accessed": "Last accessed",
"Shared by": "Shared by", "Shared by": "Shared by",
"Date shared": "Date shared", "Date shared": "Date shared",
"Last accessed": "Last accessed",
"Domain": "Domain", "Domain": "Domain",
"Views": "Views", "Views": "Views",
"All roles": "All roles", "All roles": "All roles",

View File

@@ -2776,6 +2776,13 @@
dependencies: dependencies:
prop-types "^15.8.1" prop-types "^15.8.1"
"@getoutline/react-roving-tabindex@^3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@getoutline/react-roving-tabindex/-/react-roving-tabindex-3.2.2.tgz#a3d957b0aa298d8c601a02a575cbfb263dcd940e"
integrity sha512-K8uk2BQpngOTrJUMvw/Ai3zOmKNkEo2wmjnkfgHrpW6eOgzTh3VCDOLdqCMNBNqFlNwJudpVRD6EuXNO77cQ0w==
dependencies:
warning "^4.0.3"
"@getoutline/y-prosemirror@^1.0.18": "@getoutline/y-prosemirror@^1.0.18":
version "1.0.18" version "1.0.18"
resolved "https://registry.yarnpkg.com/@getoutline/y-prosemirror/-/y-prosemirror-1.0.18.tgz#17245c0362d30adb85131c86fb9a59358884b234" resolved "https://registry.yarnpkg.com/@getoutline/y-prosemirror/-/y-prosemirror-1.0.18.tgz#17245c0362d30adb85131c86fb9a59358884b234"
@@ -15380,6 +15387,13 @@ walker@^1.0.8:
dependencies: dependencies:
makeerror "1.0.12" makeerror "1.0.12"
warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
webidl-conversions@^3.0.0: webidl-conversions@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"