Merge branch 'main' of github.com:outline/outline
This commit is contained in:
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
@@ -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>
|
||||||
|
|||||||
92
app/scenes/Search/components/RecentSearchListItem.tsx
Normal file
92
app/scenes/Search/components/RecentSearchListItem.tsx
Normal 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;
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -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 && (
|
|
||||||
<>
|
|
||||||
{" "}
|
|
||||||
· {t("Last accessed")}{" "}
|
|
||||||
<Time dateTime={lastAccessedAt} addSuffix />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
actions={<ShareMenu share={share} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShareListItem;
|
|
||||||
@@ -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);
|
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 you’re looking for.": "We were unable to find the page you’re looking for.",
|
"We were unable to find the page you’re looking for.": "We were unable to find the page you’re 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",
|
||||||
|
|||||||
14
yarn.lock
14
yarn.lock
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user