mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c7b84ff0e |
@@ -53,10 +53,9 @@ const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
}
|
||||
|
||||
const label =
|
||||
rest["aria-label"] ??
|
||||
(typeof action.name === "function"
|
||||
typeof action.name === "function"
|
||||
? action.name(actionContext)
|
||||
: action.name);
|
||||
: action.name;
|
||||
|
||||
const button = (
|
||||
<button
|
||||
|
||||
@@ -25,8 +25,6 @@ type Props = {
|
||||
onClick?: React.MouseEventHandler<HTMLImageElement>;
|
||||
/** Size of the avatar, defaults to AvatarSize.Large */
|
||||
size?: AvatarSize;
|
||||
/** Optional alt text for the avatar image */
|
||||
alt?: string;
|
||||
/** Optional inline styles to apply to the avatar wrapper */
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
@@ -55,7 +53,6 @@ function AvatarWithPresence({
|
||||
isCurrentUser,
|
||||
size = AvatarSize.Large,
|
||||
style,
|
||||
alt,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const status = isPresent
|
||||
@@ -86,7 +83,7 @@ function AvatarWithPresence({
|
||||
$color={user.color}
|
||||
style={style}
|
||||
>
|
||||
<Avatar model={user} onClick={onClick} size={size} alt={alt} />
|
||||
<Avatar model={user} onClick={onClick} size={size} />
|
||||
</AvatarPresence>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -132,7 +132,6 @@ function Collaborators(props: Props) {
|
||||
isEditing={isEditing}
|
||||
isObserving={isObserving}
|
||||
isCurrentUser={currentUserId === collaborator.id}
|
||||
alt={t("Avatar of {{ name }}", { name: collaborator.name })}
|
||||
onClick={
|
||||
isObservable
|
||||
? handleAvatarClick(
|
||||
|
||||
@@ -143,14 +143,13 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
|
||||
},
|
||||
[]
|
||||
);
|
||||
const contentEditable = !disabled && !readOnly;
|
||||
|
||||
return (
|
||||
<div className={className} dir={dir} onClick={onClick} tabIndex={-1}>
|
||||
{children}
|
||||
<Content
|
||||
ref={contentRef}
|
||||
contentEditable={contentEditable}
|
||||
contentEditable={!disabled && !readOnly}
|
||||
onInput={wrappedEvent(onInput)}
|
||||
onFocus={wrappedEvent(onFocus)}
|
||||
onBlur={wrappedEvent(onBlur)}
|
||||
@@ -158,7 +157,7 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
|
||||
onPaste={handlePaste}
|
||||
data-placeholder={placeholder}
|
||||
suppressContentEditableWarning
|
||||
role={contentEditable ? "textbox" : undefined}
|
||||
role="textbox"
|
||||
{...rest}
|
||||
>
|
||||
{innerValue}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton } from "reakit/Menu";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
|
||||
@@ -9,16 +8,10 @@ type Props = React.ComponentProps<typeof MenuButton> & {
|
||||
};
|
||||
|
||||
export default function OverflowMenuButton({ className, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<MenuButton {...rest}>
|
||||
{(props) => (
|
||||
<NudeButton
|
||||
className={className}
|
||||
aria-label={t("More options")}
|
||||
{...props}
|
||||
>
|
||||
<NudeButton className={className} {...props}>
|
||||
<MoreIcon />
|
||||
</NudeButton>
|
||||
)}
|
||||
|
||||
@@ -114,6 +114,7 @@ function DocumentListItem(
|
||||
<DocumentLink
|
||||
ref={itemRef}
|
||||
dir={document.dir}
|
||||
role="menuitem"
|
||||
$isStarred={document.isStarred}
|
||||
$menuOpen={menuOpen}
|
||||
to={{
|
||||
@@ -278,7 +279,7 @@ const DocumentLink = styled(Link)<{
|
||||
`}
|
||||
`;
|
||||
|
||||
const Heading = styled.span<{ rtl?: boolean }>`
|
||||
const Heading = styled.h3<{ rtl?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
|
||||
align-items: center;
|
||||
@@ -288,8 +289,6 @@ const Heading = styled.span<{ rtl?: boolean }>`
|
||||
color: ${s("text")};
|
||||
font-family: ${s("fontFamily")};
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
`;
|
||||
|
||||
const StarPositioner = styled(Flex)`
|
||||
|
||||
@@ -168,7 +168,13 @@ const DocumentMeta: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Container align="center" rtl={document.dir === "rtl"} {...rest} dir="ltr">
|
||||
<Container
|
||||
align="center"
|
||||
rtl={document.dir === "rtl"}
|
||||
{...rest}
|
||||
dir="ltr"
|
||||
lang=""
|
||||
>
|
||||
{to ? (
|
||||
<Link to={to} replace={replace}>
|
||||
{content}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import styled from "styled-components";
|
||||
import { s, truncateMultiline } from "@shared/styles";
|
||||
import { s } from "@shared/styles";
|
||||
|
||||
type Props = Omit<React.HTMLAttributes<HTMLInputElement>, "onSubmit"> & {
|
||||
/** A callback when the title is submitted. */
|
||||
@@ -128,21 +128,17 @@ function EditableTitle(
|
||||
/>
|
||||
</form>
|
||||
) : (
|
||||
<Text
|
||||
<span
|
||||
onDoubleClick={canUpdate ? handleDoubleClick : undefined}
|
||||
className={rest.className}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Text = styled.span`
|
||||
${truncateMultiline(3)}
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
color: ${s("text")};
|
||||
background: ${s("background")};
|
||||
|
||||
@@ -67,11 +67,7 @@ function Notifications(
|
||||
<Flex gap={8}>
|
||||
{notifications.approximateUnreadCount > 0 && (
|
||||
<Tooltip content={t("Mark all as read")}>
|
||||
<Button
|
||||
action={markNotificationsAsRead}
|
||||
context={context}
|
||||
aria-label={t("Mark all as read")}
|
||||
>
|
||||
<Button action={markNotificationsAsRead} context={context}>
|
||||
<MarkAsReadIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -63,7 +63,6 @@ export const OAuthClientForm = observer(function OAuthClientForm_({
|
||||
name="avatarUrl"
|
||||
render={({ field }) => (
|
||||
<ImageInput
|
||||
alt={t("OAuth client icon")}
|
||||
onSuccess={(url) => field.onChange(url)}
|
||||
onError={(err) => setError("avatarUrl", { message: err })}
|
||||
model={{
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import "../stores";
|
||||
import { render } from "@testing-library/react";
|
||||
import { TFunction } from "i18next";
|
||||
import { Provider } from "mobx-react";
|
||||
import { getI18n } from "react-i18next";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import PaginatedList from "./PaginatedList";
|
||||
|
||||
describe("PaginatedList", () => {
|
||||
const i18n = getI18n();
|
||||
const authStore = {};
|
||||
|
||||
const props = {
|
||||
i18n,
|
||||
tReady: true,
|
||||
t: ((key: string) => key) as TFunction,
|
||||
} as any;
|
||||
|
||||
it("with no items renders nothing", () => {
|
||||
const result = render(
|
||||
<Provider auth={authStore}>
|
||||
<PaginatedList items={[]} renderItem={render} {...props} />
|
||||
</Provider>
|
||||
);
|
||||
expect(result.container.innerHTML).toEqual("");
|
||||
});
|
||||
|
||||
it("with no items renders empty prop", async () => {
|
||||
const result = render(
|
||||
<Provider auth={authStore}>
|
||||
<PaginatedList
|
||||
items={[]}
|
||||
empty={<p>Sorry, no results</p>}
|
||||
renderItem={render}
|
||||
{...props}
|
||||
/>{" "}
|
||||
</Provider>
|
||||
);
|
||||
await expect(
|
||||
result.findAllByText("Sorry, no results")
|
||||
).resolves.toHaveLength(1);
|
||||
});
|
||||
|
||||
it("calls fetch with options + pagination on mount", () => {
|
||||
const fetch = jest.fn();
|
||||
const options = {
|
||||
id: "one",
|
||||
};
|
||||
render(
|
||||
<Provider auth={authStore}>
|
||||
<PaginatedList
|
||||
items={[]}
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderItem={render}
|
||||
{...props}
|
||||
/>{" "}
|
||||
</Provider>
|
||||
);
|
||||
expect(fetch).toHaveBeenCalledWith({
|
||||
...options,
|
||||
limit: Pagination.defaultLimit,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -255,7 +255,6 @@ const PaginatedList = <T extends PaginatedItem>({
|
||||
<React.Fragment>
|
||||
{heading}
|
||||
<ArrowKeyNavigation
|
||||
role={rest.role}
|
||||
aria-label={rest["aria-label"]}
|
||||
onEscape={onEscape}
|
||||
className={className}
|
||||
|
||||
@@ -168,7 +168,6 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
||||
<PopoverAnchor>
|
||||
<StyledInputSearch
|
||||
role="combobox"
|
||||
aria-controls="search-results"
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
@@ -177,8 +176,6 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
onFocus={handleSearchInputFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={className}
|
||||
label={t("Search")}
|
||||
labelHidden
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
@@ -197,7 +194,6 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
}}
|
||||
>
|
||||
<PaginatedList<SearchResult>
|
||||
role="listbox"
|
||||
options={{ query, snippetMinWords: 10, snippetMaxWords: 11 }}
|
||||
items={cachedSearchResults}
|
||||
fetch={performSearch}
|
||||
|
||||
@@ -25,11 +25,6 @@ export const AppearanceAction = observer(() => {
|
||||
onClick={() =>
|
||||
ui.setTheme(resolvedTheme === "light" ? Theme.Dark : Theme.Light)
|
||||
}
|
||||
aria-label={
|
||||
resolvedTheme === "light"
|
||||
? t("Switch to dark")
|
||||
: t("Switch to light")
|
||||
}
|
||||
neutral
|
||||
borderOnHover
|
||||
/>
|
||||
|
||||
@@ -81,11 +81,6 @@ function AppSidebar() {
|
||||
<ToggleButton
|
||||
position="bottom"
|
||||
image={<SidebarIcon />}
|
||||
aria-label={
|
||||
ui.sidebarCollapsed
|
||||
? t("Expand sidebar")
|
||||
: t("Collapse sidebar")
|
||||
}
|
||||
onClick={() => {
|
||||
ui.toggleCollapsedSidebar();
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
|
||||
@@ -52,9 +52,6 @@ function SettingsSidebar() {
|
||||
>
|
||||
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
|
||||
<ToggleButton
|
||||
aria-label={
|
||||
ui.sidebarCollapsed ? t("Expand sidebar") : t("Collapse sidebar")
|
||||
}
|
||||
position="bottom"
|
||||
image={<SidebarIcon />}
|
||||
onClick={() => {
|
||||
|
||||
@@ -96,9 +96,6 @@ const ToggleSidebar = () => {
|
||||
<ToggleButton
|
||||
position="bottom"
|
||||
image={<SidebarIcon />}
|
||||
aria-label={
|
||||
ui.sidebarCollapsed ? t("Expand sidebar") : t("Collapse sidebar")
|
||||
}
|
||||
onClick={() => {
|
||||
ui.toggleCollapsedSidebar();
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
|
||||
@@ -21,7 +21,6 @@ import { TooltipProvider } from "../TooltipContext";
|
||||
import ResizeBorder from "./components/ResizeBorder";
|
||||
import SidebarButton from "./components/SidebarButton";
|
||||
import ToggleButton from "./components/ToggleButton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const ANIMATION_MS = 250;
|
||||
|
||||
@@ -36,7 +35,6 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
const [isCollapsing, setCollapsing] = React.useState(false);
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { ui } = useStores();
|
||||
const location = useLocation();
|
||||
@@ -239,7 +237,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
|
||||
position="bottom"
|
||||
image={
|
||||
<Avatar
|
||||
alt={t("Avatar of {{ name }}", { name: user.name })}
|
||||
alt={user.name}
|
||||
model={user}
|
||||
size={24}
|
||||
style={{ marginLeft: 4 }}
|
||||
@@ -247,11 +245,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
|
||||
}
|
||||
>
|
||||
<NotificationsPopover>
|
||||
<SidebarButton
|
||||
position="bottom"
|
||||
image={<NotificationIcon />}
|
||||
aria-label={t("Notifications")}
|
||||
/>
|
||||
<SidebarButton position="bottom" image={<NotificationIcon />} />
|
||||
</NotificationsPopover>
|
||||
</SidebarButton>
|
||||
</AccountMenu>
|
||||
|
||||
@@ -150,7 +150,6 @@ const CollectionLink: React.FC<Props> = ({
|
||||
{can.createDocument && (
|
||||
<NudeButton
|
||||
tooltip={{ content: t("New doc"), delay: 500 }}
|
||||
aria-label={t("New nested document")}
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
setIsAddingNewChild();
|
||||
|
||||
@@ -364,6 +364,7 @@ function InnerDocumentLink(
|
||||
{can.createChildDocument && (
|
||||
<Tooltip content={t("New doc")}>
|
||||
<NudeButton
|
||||
type={undefined}
|
||||
aria-label={t("New nested document")}
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import invariant from "invariant";
|
||||
import { observer } from "mobx-react";
|
||||
import { useCallback } from "react";
|
||||
@@ -62,12 +61,7 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) {
|
||||
$isDragActive={isDragActive}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<label>
|
||||
{t("Import files")}
|
||||
<input {...getInputProps()} />
|
||||
</label>
|
||||
</VisuallyHidden>
|
||||
<input {...getInputProps()} />
|
||||
{isImporting && <LoadingIndicator />}
|
||||
{children}
|
||||
</DropzoneContainer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CollapsedIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import { extraArea, s } from "@shared/styles";
|
||||
import { s } from "@shared/styles";
|
||||
import usePersistedState from "~/hooks/usePersistedState";
|
||||
import { undraggableOnDesktop } from "~/styles";
|
||||
|
||||
@@ -71,18 +71,17 @@ const Button = styled.button`
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
color: ${s("sidebarText")};
|
||||
position: relative;
|
||||
color: ${s("textTertiary")};
|
||||
letter-spacing: 0.03em;
|
||||
margin: 0;
|
||||
padding: 4px 2px 4px 12px;
|
||||
height: 22px;
|
||||
border: 0;
|
||||
background: none;
|
||||
border-radius: 4px;
|
||||
-webkit-appearance: none;
|
||||
transition: all 100ms ease;
|
||||
${undraggableOnDesktop()}
|
||||
${extraArea(4)}
|
||||
|
||||
&:not(:disabled):hover,
|
||||
&:not(:disabled):active {
|
||||
@@ -103,8 +102,7 @@ const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>`
|
||||
const H3 = styled.h3`
|
||||
margin: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
&:hover {
|
||||
${Disclosure} {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import styled, { useTheme, css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import EventBoundary from "@shared/components/EventBoundary";
|
||||
import { s } from "@shared/styles";
|
||||
import { s, truncateMultiline } from "@shared/styles";
|
||||
import { isMobile } from "@shared/utils/browser";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { UnreadBadge } from "~/components/UnreadBadge";
|
||||
@@ -273,6 +273,7 @@ const Label = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
line-height: 24px;
|
||||
${truncateMultiline(3)}
|
||||
|
||||
* {
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
@@ -347,7 +347,6 @@ export default function FindAndReplace({
|
||||
<ButtonLarge
|
||||
disabled={disabled}
|
||||
onClick={() => editor.commands.prevSearchMatch()}
|
||||
aria-label={t("Previous match")}
|
||||
>
|
||||
<CaretUpIcon />
|
||||
</ButtonLarge>
|
||||
@@ -356,7 +355,6 @@ export default function FindAndReplace({
|
||||
<ButtonLarge
|
||||
disabled={disabled}
|
||||
onClick={() => editor.commands.nextSearchMatch()}
|
||||
aria-label={t("Next match")}
|
||||
>
|
||||
<CaretDownIcon />
|
||||
</ButtonLarge>
|
||||
@@ -392,10 +390,7 @@ export default function FindAndReplace({
|
||||
shortcut={`${altDisplay}+${metaDisplay}+c`}
|
||||
placement="bottom"
|
||||
>
|
||||
<ButtonSmall
|
||||
onClick={handleCaseSensitive}
|
||||
aria-label={t("Match case")}
|
||||
>
|
||||
<ButtonSmall onClick={handleCaseSensitive}>
|
||||
<CaseSensitiveIcon
|
||||
color={caseSensitive ? theme.accent : theme.textSecondary}
|
||||
/>
|
||||
@@ -406,10 +401,7 @@ export default function FindAndReplace({
|
||||
shortcut={`${altDisplay}+${metaDisplay}+r`}
|
||||
placement="bottom"
|
||||
>
|
||||
<ButtonSmall
|
||||
onClick={handleRegex}
|
||||
aria-label={t("Enable regex")}
|
||||
>
|
||||
<ButtonSmall onClick={handleRegex}>
|
||||
<RegexIcon
|
||||
color={regexEnabled ? theme.accent : theme.textSecondary}
|
||||
/>
|
||||
@@ -424,10 +416,7 @@ export default function FindAndReplace({
|
||||
shortcut={`${altDisplay}+${metaDisplay}+f`}
|
||||
placement="bottom"
|
||||
>
|
||||
<ButtonLarge
|
||||
onClick={handleMore}
|
||||
aria-label={t("Replace options")}
|
||||
>
|
||||
<ButtonLarge onClick={handleMore}>
|
||||
<ReplaceIcon color={theme.textSecondary} />
|
||||
</ButtonLarge>
|
||||
</Tooltip>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
|
||||
import { extraArea } from "@shared/styles";
|
||||
import Input, { NativeInput, Outline } from "~/components/Input";
|
||||
import { useEditor } from "./EditorContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Dimension = {
|
||||
width: string;
|
||||
@@ -21,7 +20,6 @@ export function MediaDimension() {
|
||||
width: { min: number; max: number };
|
||||
height: { min: number; max: number };
|
||||
}>();
|
||||
const { t } = useTranslation();
|
||||
const { view, commands } = useEditor();
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
@@ -207,8 +205,6 @@ export function MediaDimension() {
|
||||
return (
|
||||
<StyledFlex ref={ref} align="center">
|
||||
<StyledInput
|
||||
label={t("Image width")}
|
||||
labelHidden
|
||||
value={localDimension.width}
|
||||
onChange={handleChange("width")}
|
||||
onBlur={handleBlur}
|
||||
@@ -216,11 +212,9 @@ export function MediaDimension() {
|
||||
$error={error.width}
|
||||
/>
|
||||
<Text size="xsmall" type="tertiary">
|
||||
×
|
||||
x
|
||||
</Text>
|
||||
<StyledInput
|
||||
label={t("Image height")}
|
||||
labelHidden
|
||||
value={localDimension.height}
|
||||
onChange={handleChange("height")}
|
||||
onBlur={handleBlur}
|
||||
|
||||
@@ -64,11 +64,7 @@ function ToolbarDropdown(props: { active: boolean; item: MenuItem }) {
|
||||
<>
|
||||
<MenuButton {...menu}>
|
||||
{(buttonProps) => (
|
||||
<ToolbarButton
|
||||
{...buttonProps}
|
||||
hovering={menu.visible}
|
||||
aria-label={item.tooltip}
|
||||
>
|
||||
<ToolbarButton {...buttonProps} hovering={menu.visible}>
|
||||
{item.label && <Label>{item.label}</Label>}
|
||||
{item.icon}
|
||||
</ToolbarButton>
|
||||
@@ -122,7 +118,6 @@ function ToolbarMenu(props: Props) {
|
||||
<ToolbarButton
|
||||
onClick={handleClick(item)}
|
||||
active={isActive && !item.label}
|
||||
aria-label={item.label ? undefined : item.tooltip}
|
||||
>
|
||||
{item.label && <Label>{item.label}</Label>}
|
||||
{item.icon}
|
||||
|
||||
@@ -498,7 +498,6 @@ export class Editor extends React.PureComponent<
|
||||
|
||||
// Tell third-party libraries and screen-readers that this is an input
|
||||
view.dom.setAttribute("role", "textbox");
|
||||
view.dom.setAttribute("aria-label", "Editor content");
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function useDictionary() {
|
||||
deleteRow: t("Delete"),
|
||||
deleteTable: t("Delete table"),
|
||||
deleteAttachment: t("Delete file"),
|
||||
dimensions: `${t("Width")} × ${t("Height")}`,
|
||||
dimensions: t("Width x Height"),
|
||||
download: t("Download"),
|
||||
downloadAttachment: t("Download file"),
|
||||
replaceAttachment: t("Replace file"),
|
||||
|
||||
@@ -20,7 +20,7 @@ const NotificationMenu: React.FC = () => {
|
||||
|
||||
return (
|
||||
<DropdownMenu action={rootAction} ariaLabel={t("Notifications")}>
|
||||
<Button aria-label={t("Notifications")}>
|
||||
<Button>
|
||||
<MoreIcon />
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { computed, observable } from "mobx";
|
||||
import GroupMembership from "./GroupMembership";
|
||||
import Model from "./base/Model";
|
||||
import Field from "./decorators/Field";
|
||||
import { GroupPermission } from "@shared/types";
|
||||
|
||||
class Group extends Model {
|
||||
static modelName = "Group";
|
||||
@@ -26,18 +25,6 @@ class Group extends Model {
|
||||
return users.inGroup(this.id);
|
||||
}
|
||||
|
||||
@computed
|
||||
get admins() {
|
||||
const { groupUsers } = this.store.rootStore;
|
||||
return groupUsers.orderedData
|
||||
.filter(
|
||||
(groupUser) =>
|
||||
groupUser.groupId === this.id &&
|
||||
groupUser.permission === GroupPermission.Admin
|
||||
)
|
||||
.map((groupUser) => groupUser.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the direct memberships that this group has to documents. Documents that the current
|
||||
* user already has access to through a collection, archived, and trashed documents are not included.
|
||||
|
||||
@@ -23,7 +23,6 @@ import { useDocumentContext } from "~/components/DocumentContext";
|
||||
import { PopoverButton } from "~/components/IconPicker/components/PopoverButton";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
|
||||
|
||||
@@ -71,7 +70,6 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
|
||||
}: Props,
|
||||
externalRef: React.RefObject<RefHandle>
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const ref = React.useRef<RefHandle>(null);
|
||||
const [iconPickerIsOpen, handleOpen, setIconPickerClosed] = useBoolean();
|
||||
const { editor } = useDocumentContext();
|
||||
@@ -251,7 +249,6 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle(
|
||||
autoFocus={!title}
|
||||
maxLength={DocumentValidation.maxTitleLength}
|
||||
readOnly={readOnly}
|
||||
aria-label={t("Document title")}
|
||||
dir="auto"
|
||||
ref={mergeRefs([ref, externalRef])}
|
||||
>
|
||||
|
||||
@@ -134,7 +134,6 @@ function DocumentHeader({
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
aria-label={t("Show contents")}
|
||||
onClick={handleToggle}
|
||||
icon={<TableOfContentsIcon />}
|
||||
borderOnHover
|
||||
|
||||
@@ -23,11 +23,7 @@ function KeyboardShortcutsButton() {
|
||||
|
||||
return (
|
||||
<Tooltip content={t("Keyboard shortcuts")} shortcut="?">
|
||||
<Button
|
||||
onClick={handleOpenKeyboardShortcuts}
|
||||
$hidden={isEditingFocus}
|
||||
aria-label={t("Keyboard shortcuts")}
|
||||
>
|
||||
<Button onClick={handleOpenKeyboardShortcuts} $hidden={isEditingFocus}>
|
||||
<KeyboardIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -161,7 +161,6 @@ const Application = observer(function Application({ oauthClient }: Props) {
|
||||
name="avatarUrl"
|
||||
render={({ field }) => (
|
||||
<ImageInput
|
||||
alt={t("Application icon")}
|
||||
onSuccess={(url) => field.onChange(url)}
|
||||
onError={(err) => setError("avatarUrl", { message: err })}
|
||||
model={{
|
||||
|
||||
@@ -193,7 +193,6 @@ function Details() {
|
||||
)}
|
||||
>
|
||||
<ImageInput
|
||||
alt={t("Workspace logo")}
|
||||
onSuccess={handleAvatarChange}
|
||||
onError={handleAvatarError}
|
||||
model={team}
|
||||
|
||||
@@ -72,7 +72,6 @@ const Profile = () => {
|
||||
description={t("Choose a photo or image to represent yourself.")}
|
||||
>
|
||||
<ImageInput
|
||||
alt={t("Profile picture")}
|
||||
onSuccess={handleAvatarChange}
|
||||
onError={handleAvatarError}
|
||||
model={user}
|
||||
|
||||
@@ -430,7 +430,7 @@ const GroupMemberListItem = observer(function ({
|
||||
() =>
|
||||
[
|
||||
{
|
||||
label: t("Group admin"),
|
||||
label: t("Manage"),
|
||||
value: GroupPermission.Admin,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@ export function GroupsTable(props: Props) {
|
||||
<Title onClick={() => handleViewMembers(group)}>
|
||||
{group.name}
|
||||
</Title>
|
||||
<Text type="tertiary" size="small" weight="normal">
|
||||
<Text type="tertiary" size="small">
|
||||
<Trans
|
||||
defaults="{{ count }} member"
|
||||
values={{ count: group.memberCount }}
|
||||
@@ -97,30 +97,6 @@ export function GroupsTable(props: Props) {
|
||||
width: "1fr",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
type: "data",
|
||||
id: "admins",
|
||||
header: t("Admins"),
|
||||
accessor: (group) => `${group.memberCount} admins`,
|
||||
component: (group) => {
|
||||
const users = group.admins.slice(0, MAX_AVATAR_DISPLAY);
|
||||
|
||||
if (users.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupMembers
|
||||
onClick={() => handleViewMembers(group)}
|
||||
width={users.length * AvatarSize.Large}
|
||||
>
|
||||
<Facepile users={users} />
|
||||
</GroupMembers>
|
||||
);
|
||||
},
|
||||
width: "1fr",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
type: "data",
|
||||
id: "createdAt",
|
||||
|
||||
@@ -10,10 +10,9 @@ import ImageUpload, { Props as ImageUploadProps } from "./ImageUpload";
|
||||
|
||||
type Props = ImageUploadProps & {
|
||||
model: IAvatar;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
export default function ImageInput({ model, onSuccess, alt, ...rest }: Props) {
|
||||
export default function ImageInput({ model, onSuccess, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -28,7 +27,6 @@ export default function ImageInput({ model, onSuccess, alt, ...rest }: Props) {
|
||||
model={model}
|
||||
size={AvatarSize.Upload}
|
||||
variant={AvatarVariant.Square}
|
||||
alt={alt}
|
||||
/>
|
||||
<Flex auto align="center" justify="center" className="upload">
|
||||
<EditIcon />
|
||||
|
||||
@@ -242,8 +242,8 @@ export default class AuthStore extends Store<Team> {
|
||||
// Update the user's timezone if it has changed
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (data.user.timezone !== timezone) {
|
||||
const user = this.rootStore.users.get(data.user.id);
|
||||
void user?.save({ timezone });
|
||||
const user = this.rootStore.users.get(data.user.id)!;
|
||||
void user.save({ timezone });
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@relative-ci/agent": "^4.3.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@types/addressparser": "^1.0.3",
|
||||
"@types/body-scroll-lock": "^3.1.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
|
||||
@@ -28,7 +28,7 @@ const router = new Router();
|
||||
router.post(
|
||||
"files.create",
|
||||
rateLimiter(RateLimiterStrategy.TenPerMinute),
|
||||
auth(),
|
||||
auth({ allowMultipart: true }),
|
||||
validate(T.FilesCreateSchema),
|
||||
multipart({
|
||||
maximumFileSize: Math.max(
|
||||
|
||||
@@ -17,29 +17,185 @@ import {
|
||||
} from "../errors";
|
||||
|
||||
type AuthenticationOptions = {
|
||||
/** Role required to access the route. */
|
||||
/** Role requuired to access the route. */
|
||||
role?: UserRole;
|
||||
/** Type of authentication required to access the route. */
|
||||
type?: AuthenticationType | AuthenticationType[];
|
||||
/** Authentication is parsed, but optional. */
|
||||
optional?: boolean;
|
||||
};
|
||||
|
||||
type AuthTransport = "cookie" | "header" | "body" | "query";
|
||||
|
||||
type AuthInput = {
|
||||
/** The authentication token extracted from the request, if any. */
|
||||
token?: string;
|
||||
/** The method used to receive the authentication token. */
|
||||
transport?: AuthTransport;
|
||||
/**
|
||||
* Allow multipart requests with cookie authentication, otherwise
|
||||
* the request will fail if the content type is not application/json.
|
||||
* This is useful for file uploads where the cookie is used to authenticate.
|
||||
*/
|
||||
allowMultipart?: boolean;
|
||||
};
|
||||
|
||||
export default function auth(options: AuthenticationOptions = {}) {
|
||||
return async function authMiddleware(ctx: AppContext, next: Next) {
|
||||
try {
|
||||
const { type, token, user } = await validateAuthentication(ctx, options);
|
||||
let token;
|
||||
const authorizationHeader = ctx.request.get("authorization");
|
||||
|
||||
// We are not awaiting the promises here so that the request is not blocked
|
||||
if (authorizationHeader) {
|
||||
const parts = authorizationHeader.split(" ");
|
||||
|
||||
if (parts.length === 2) {
|
||||
const scheme = parts[0];
|
||||
const credentials = parts[1];
|
||||
|
||||
if (/^Bearer$/i.test(scheme)) {
|
||||
token = credentials;
|
||||
}
|
||||
} else {
|
||||
throw AuthenticationError(
|
||||
`Bad Authorization header format. Format is "Authorization: Bearer <token>"`
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
ctx.request.body &&
|
||||
typeof ctx.request.body === "object" &&
|
||||
"token" in ctx.request.body
|
||||
) {
|
||||
token = ctx.request.body.token;
|
||||
} else if (ctx.request.query?.token) {
|
||||
token = ctx.request.query.token;
|
||||
} else {
|
||||
token = ctx.cookies.get("accessToken");
|
||||
|
||||
// check if the request is application/json encoded
|
||||
// TODO: Enable once clients have updated
|
||||
// if (
|
||||
// token &&
|
||||
// !ctx.request.is("application/json") &&
|
||||
// !options.allowMultipart
|
||||
// ) {
|
||||
// throw AuthenticationError(
|
||||
// "Mismatched content type. Expected application/json"
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
throw AuthenticationError("Authentication required");
|
||||
}
|
||||
|
||||
let user: User | null;
|
||||
let type: AuthenticationType;
|
||||
|
||||
if (OAuthAuthentication.match(String(token))) {
|
||||
if (!authorizationHeader) {
|
||||
throw AuthenticationError(
|
||||
"OAuth access token must be passed in the Authorization header"
|
||||
);
|
||||
}
|
||||
|
||||
type = AuthenticationType.OAUTH;
|
||||
|
||||
let authentication;
|
||||
try {
|
||||
authentication = await OAuthAuthentication.findByAccessToken(token, {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
} catch (_err) {
|
||||
throw AuthenticationError("Invalid access token");
|
||||
}
|
||||
if (!authentication) {
|
||||
throw AuthenticationError("Invalid access token");
|
||||
}
|
||||
if (authentication.accessTokenExpiresAt < new Date()) {
|
||||
throw AuthenticationError("Access token is expired");
|
||||
}
|
||||
if (!authentication.canAccess(ctx.request.url)) {
|
||||
throw AuthenticationError(
|
||||
"Access token does not have access to this resource"
|
||||
);
|
||||
}
|
||||
|
||||
user = await User.findByPk(authentication.userId, {
|
||||
include: [
|
||||
{
|
||||
model: Team,
|
||||
as: "team",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!user) {
|
||||
throw AuthenticationError("Invalid access token");
|
||||
}
|
||||
|
||||
await authentication.updateActiveAt();
|
||||
} else if (ApiKey.match(String(token))) {
|
||||
type = AuthenticationType.API;
|
||||
let apiKey;
|
||||
|
||||
try {
|
||||
apiKey = await ApiKey.findByToken(token);
|
||||
} catch (_err) {
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
|
||||
throw AuthenticationError("API key is expired");
|
||||
}
|
||||
|
||||
if (!apiKey.canAccess(ctx.request.url)) {
|
||||
throw AuthenticationError(
|
||||
"API key does not have access to this resource"
|
||||
);
|
||||
}
|
||||
|
||||
user = await User.findByPk(apiKey.userId, {
|
||||
include: [
|
||||
{
|
||||
model: Team,
|
||||
as: "team",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
await apiKey.updateActiveAt();
|
||||
} else {
|
||||
type = AuthenticationType.APP;
|
||||
user = await getUserForJWT(String(token));
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
const suspendingAdmin = await User.findOne({
|
||||
where: {
|
||||
id: user.suspendedById!,
|
||||
},
|
||||
paranoid: false,
|
||||
});
|
||||
throw UserSuspendedError({
|
||||
adminEmail: suspendingAdmin?.email || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.role && UserRoleHelper.isRoleLower(user.role, options.role)) {
|
||||
throw AuthorizationError(`${capitalize(options.role)} role required`);
|
||||
}
|
||||
|
||||
if (
|
||||
options.type &&
|
||||
(Array.isArray(options.type)
|
||||
? !options.type.includes(type)
|
||||
: type !== options.type)
|
||||
) {
|
||||
throw AuthorizationError(`Invalid authentication type`);
|
||||
}
|
||||
|
||||
// not awaiting the promises here so that the request is not blocked
|
||||
user.updateActiveAt(ctx).catch((err) => {
|
||||
Logger.error("Failed to update user activeAt", err);
|
||||
});
|
||||
@@ -49,7 +205,7 @@ export default function auth(options: AuthenticationOptions = {}) {
|
||||
|
||||
ctx.state.auth = {
|
||||
user,
|
||||
token,
|
||||
token: String(token),
|
||||
type,
|
||||
};
|
||||
|
||||
@@ -84,196 +240,3 @@ export default function auth(options: AuthenticationOptions = {}) {
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the authentication token from the request context.
|
||||
*
|
||||
* @param ctx The application context containing the request information.
|
||||
* @returns An object containing the token and its transport method.
|
||||
*/
|
||||
export function parseAuthentication(ctx: AppContext): AuthInput {
|
||||
const authorizationHeader = ctx.request.get("authorization");
|
||||
|
||||
if (authorizationHeader) {
|
||||
const parts = authorizationHeader.split(" ");
|
||||
|
||||
if (parts.length === 2) {
|
||||
const scheme = parts[0];
|
||||
const credentials = parts[1];
|
||||
|
||||
if (/^Bearer$/i.test(scheme)) {
|
||||
return {
|
||||
token: credentials,
|
||||
transport: "header",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw AuthenticationError(
|
||||
`Bad Authorization header format. Format is "Authorization: Bearer <token>"`
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
ctx.request.body &&
|
||||
typeof ctx.request.body === "object" &&
|
||||
"token" in ctx.request.body
|
||||
) {
|
||||
return {
|
||||
token: String(ctx.request.body.token),
|
||||
transport: "body",
|
||||
};
|
||||
} else if (ctx.request.query?.token) {
|
||||
return {
|
||||
token: String(ctx.request.query.token),
|
||||
transport: "query",
|
||||
};
|
||||
} else {
|
||||
const accessToken = ctx.cookies.get("accessToken");
|
||||
if (accessToken) {
|
||||
return {
|
||||
token: accessToken,
|
||||
transport: "cookie",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
token: undefined,
|
||||
transport: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function validateAuthentication(
|
||||
ctx: AppContext,
|
||||
options: AuthenticationOptions
|
||||
): Promise<{ user: User; token: string; type: AuthenticationType }> {
|
||||
const { token, transport } = parseAuthentication(ctx);
|
||||
|
||||
if (!token) {
|
||||
throw AuthenticationError("Authentication required");
|
||||
}
|
||||
|
||||
let user: User | null;
|
||||
let type: AuthenticationType;
|
||||
|
||||
if (OAuthAuthentication.match(token)) {
|
||||
if (transport !== "header") {
|
||||
throw AuthenticationError(
|
||||
"OAuth access token must be passed in the Authorization header"
|
||||
);
|
||||
}
|
||||
|
||||
type = AuthenticationType.OAUTH;
|
||||
|
||||
let authentication;
|
||||
try {
|
||||
authentication = await OAuthAuthentication.findByAccessToken(token, {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
} catch (_err) {
|
||||
throw AuthenticationError("Invalid access token");
|
||||
}
|
||||
if (!authentication) {
|
||||
throw AuthenticationError("Invalid access token");
|
||||
}
|
||||
if (authentication.accessTokenExpiresAt < new Date()) {
|
||||
throw AuthenticationError("Access token is expired");
|
||||
}
|
||||
if (!authentication.canAccess(ctx.request.url)) {
|
||||
throw AuthenticationError(
|
||||
"Access token does not have access to this resource"
|
||||
);
|
||||
}
|
||||
|
||||
user = await User.findByPk(authentication.userId, {
|
||||
include: [
|
||||
{
|
||||
model: Team,
|
||||
as: "team",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!user) {
|
||||
throw AuthenticationError("Invalid access token");
|
||||
}
|
||||
|
||||
await authentication.updateActiveAt();
|
||||
} else if (ApiKey.match(token)) {
|
||||
if (transport === "cookie") {
|
||||
throw AuthenticationError("API key must not be passed in the cookie");
|
||||
}
|
||||
|
||||
type = AuthenticationType.API;
|
||||
let apiKey;
|
||||
|
||||
try {
|
||||
apiKey = await ApiKey.findByToken(token);
|
||||
} catch (_err) {
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
|
||||
throw AuthenticationError("API key is expired");
|
||||
}
|
||||
|
||||
if (!apiKey.canAccess(ctx.request.url)) {
|
||||
throw AuthenticationError(
|
||||
"API key does not have access to this resource"
|
||||
);
|
||||
}
|
||||
|
||||
user = await User.findByPk(apiKey.userId, {
|
||||
include: [
|
||||
{
|
||||
model: Team,
|
||||
as: "team",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw AuthenticationError("Invalid API key");
|
||||
}
|
||||
|
||||
await apiKey.updateActiveAt();
|
||||
} else {
|
||||
type = AuthenticationType.APP;
|
||||
user = await getUserForJWT(token);
|
||||
}
|
||||
|
||||
if (user.isSuspended) {
|
||||
const suspendingAdmin = await User.findOne({
|
||||
where: {
|
||||
id: user.suspendedById!,
|
||||
},
|
||||
paranoid: false,
|
||||
});
|
||||
throw UserSuspendedError({
|
||||
adminEmail: suspendingAdmin?.email || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.role && UserRoleHelper.isRoleLower(user.role, options.role)) {
|
||||
throw AuthorizationError(`${capitalize(options.role)} role required`);
|
||||
}
|
||||
|
||||
if (
|
||||
options.type &&
|
||||
(Array.isArray(options.type)
|
||||
? !options.type.includes(type)
|
||||
: type !== options.type)
|
||||
) {
|
||||
throw AuthorizationError(`Invalid authentication type`);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
type,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { getCookieDomain } from "@shared/utils/domains";
|
||||
import { CSRF } from "@shared/constants";
|
||||
import { CSRFError } from "@server/errors";
|
||||
import { parseAuthentication } from "./authentication";
|
||||
|
||||
/**
|
||||
* Middleware that generates and attaches CSRF tokens for safe methods
|
||||
@@ -49,8 +48,7 @@ export function verifyCSRFToken() {
|
||||
}
|
||||
|
||||
// If not using cookie-based auth, skip CSRF protection
|
||||
const { transport } = parseAuthentication(ctx);
|
||||
if (transport !== "cookie") {
|
||||
if (!ctx.cookies.get("accessToken")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
InferCreationAttributes,
|
||||
NonNullFindOptions,
|
||||
SaveOptions,
|
||||
ScopeOptions,
|
||||
} from "sequelize";
|
||||
import {
|
||||
Transaction,
|
||||
@@ -81,13 +80,8 @@ const stateIfContentEmpty = Sequelize.literal(
|
||||
);
|
||||
|
||||
type AdditionalFindOptions = {
|
||||
/** The user ID to load associated permissions for. */
|
||||
userId?: string;
|
||||
/** Whether to include the state column in the attributes. */
|
||||
includeState?: boolean;
|
||||
/** Whether to views (default: true). */
|
||||
includeViews?: boolean;
|
||||
/** Whether to reject the query if no document is found. */
|
||||
rejectOnEmpty?: boolean | Error;
|
||||
};
|
||||
|
||||
@@ -707,25 +701,16 @@ class Document extends ArchivableModel<
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
includeViews = true,
|
||||
includeState = false,
|
||||
userId,
|
||||
...rest
|
||||
} = options;
|
||||
const { includeState, userId, ...rest } = options;
|
||||
|
||||
// allow default preloading of collection membership if `userId` is passed in find options
|
||||
// almost every endpoint needs the collection membership to determine policy permissions.
|
||||
const scope = this.scope([
|
||||
"withDrafts",
|
||||
includeState ? "withState" : "withoutState",
|
||||
...((includeViews
|
||||
? [
|
||||
{
|
||||
method: ["withViews", userId],
|
||||
},
|
||||
]
|
||||
: []) as ScopeOptions[]),
|
||||
{
|
||||
method: ["withViews", userId],
|
||||
},
|
||||
{
|
||||
method: ["withMembership", userId, rest.paranoid],
|
||||
},
|
||||
@@ -780,19 +765,14 @@ class Document extends ArchivableModel<
|
||||
options: Omit<FindOptions<Document>, "where"> &
|
||||
Omit<AdditionalFindOptions, "rejectOnEmpty"> = {}
|
||||
): Promise<Document[]> {
|
||||
const { userId, includeViews = true, includeState, ...rest } = options;
|
||||
const { userId, ...rest } = options;
|
||||
|
||||
const user = userId ? await User.findByPk(userId) : null;
|
||||
const documents = await this.scope([
|
||||
"withDrafts",
|
||||
includeState ? "withState" : "withoutState",
|
||||
...((includeViews
|
||||
? [
|
||||
{
|
||||
method: ["withViews", userId],
|
||||
},
|
||||
]
|
||||
: []) as ScopeOptions[]),
|
||||
{
|
||||
method: ["withViews", userId],
|
||||
},
|
||||
{
|
||||
method: ["withMembership", userId],
|
||||
},
|
||||
|
||||
@@ -71,12 +71,7 @@ class Relationship extends IdModel<
|
||||
|
||||
const documents = await Document.findByIds(
|
||||
relationships.map((relationship) => relationship.reverseDocumentId),
|
||||
{
|
||||
attributes: ["id"],
|
||||
userId: user.id,
|
||||
includeState: false,
|
||||
includeViews: false,
|
||||
}
|
||||
{ userId: user.id }
|
||||
);
|
||||
|
||||
return documents.map((doc) => doc.id);
|
||||
|
||||
@@ -1575,7 +1575,7 @@ router.post(
|
||||
|
||||
router.post(
|
||||
"documents.import",
|
||||
auth(),
|
||||
auth({ allowMultipart: true }),
|
||||
rateLimiter(RateLimiterStrategy.TwentyFivePerMinute),
|
||||
validate(T.DocumentsImportSchema),
|
||||
multipart({ maximumFileSize: env.FILE_STORAGE_IMPORT_MAX_SIZE }),
|
||||
|
||||
@@ -102,7 +102,6 @@ router.post(
|
||||
where: {
|
||||
groupId: group.id,
|
||||
},
|
||||
order: [["permission", "ASC"]],
|
||||
limit: MAX_AVATAR_DISPLAY,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { s } from "../../styles";
|
||||
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
/** Callback triggered when the caption is blurred */
|
||||
@@ -24,7 +23,6 @@ type Props = {
|
||||
* A component that renders a caption for an image or video.
|
||||
*/
|
||||
function Caption({ placeholder, children, isSelected, width, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const handlePaste = (event: React.ClipboardEvent<HTMLParagraphElement>) => {
|
||||
event.preventDefault();
|
||||
const text = event.clipboardData.getData("text/plain");
|
||||
@@ -44,7 +42,6 @@ function Caption({ placeholder, children, isSelected, width, ...rest }: Props) {
|
||||
onPaste={handlePaste}
|
||||
className={EditorStyleHelper.imageCaption}
|
||||
tabIndex={-1}
|
||||
aria-label={t("Caption")}
|
||||
role="textbox"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
|
||||
@@ -9,7 +9,6 @@ import toggleCheckboxItem from "../commands/toggleCheckboxItem";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import checkboxRule from "../rules/checkboxes";
|
||||
import Node from "./Node";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export default class CheckboxItem extends Node {
|
||||
get name() {
|
||||
@@ -35,7 +34,6 @@ export default class CheckboxItem extends Node {
|
||||
},
|
||||
],
|
||||
toDOM: (node) => {
|
||||
const id = `checkbox-${v4()}`;
|
||||
const checked = node.attrs.checked.toString();
|
||||
let input;
|
||||
if (typeof document !== "undefined") {
|
||||
@@ -43,7 +41,6 @@ export default class CheckboxItem extends Node {
|
||||
input.tabIndex = -1;
|
||||
input.className = "checkbox";
|
||||
input.setAttribute("aria-checked", checked);
|
||||
input.setAttribute("aria-labelledby", id);
|
||||
input.setAttribute("role", "checkbox");
|
||||
input.addEventListener("click", this.handleClick);
|
||||
}
|
||||
@@ -63,7 +60,7 @@ export default class CheckboxItem extends Node {
|
||||
? [input]
|
||||
: [["span", { class: "checkbox", "aria-checked": checked }]]),
|
||||
],
|
||||
["div", { id }, 0],
|
||||
["div", 0],
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -175,7 +175,6 @@
|
||||
"currently viewing": "currently viewing",
|
||||
"previously edited": "previously edited",
|
||||
"You": "You",
|
||||
"Avatar of {{ name }}": "Avatar of {{ name }}",
|
||||
"Viewers": "Viewers",
|
||||
"Collections are used to group documents and choose permissions": "Collections are used to group documents and choose permissions",
|
||||
"Name": "Name",
|
||||
@@ -206,7 +205,6 @@
|
||||
"Move document": "Move document",
|
||||
"Moving": "Moving",
|
||||
"Moving the document <em>{{ title }}</em> to the {{ newCollectionName }} collection will change permission for all workspace members from <em>{{ prevPermission }}</em> to <em>{{ newPermission }}</em>.": "Moving the document <em>{{ title }}</em> to the {{ newCollectionName }} collection will change permission for all workspace members from <em>{{ prevPermission }}</em> to <em>{{ newPermission }}</em>.",
|
||||
"More options": "More options",
|
||||
"Submenu": "Submenu",
|
||||
"Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app",
|
||||
"Start view": "Start view",
|
||||
@@ -331,7 +329,6 @@
|
||||
"Mark all as read": "Mark all as read",
|
||||
"You're all caught up": "You're all caught up",
|
||||
"Icon": "Icon",
|
||||
"OAuth client icon": "OAuth client icon",
|
||||
"My App": "My App",
|
||||
"Tagline": "Tagline",
|
||||
"A short description": "A short description",
|
||||
@@ -408,15 +405,12 @@
|
||||
"{{ count }} groups added to the document": "{{ count }} groups added to the document",
|
||||
"{{ count }} groups added to the document_plural": "{{ count }} groups added to the document",
|
||||
"Logo": "Logo",
|
||||
"Expand sidebar": "Expand sidebar",
|
||||
"Collapse sidebar": "Collapse sidebar",
|
||||
"Archived collections": "Archived collections",
|
||||
"New doc": "New doc",
|
||||
"Empty": "Empty",
|
||||
"Collapse": "Collapse",
|
||||
"Expand": "Expand",
|
||||
"Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word",
|
||||
"Import files": "Import files",
|
||||
"Go back": "Go back",
|
||||
"Go forward": "Go forward",
|
||||
"Could not load shared documents": "Could not load shared documents",
|
||||
@@ -466,8 +460,6 @@
|
||||
"Replacement": "Replacement",
|
||||
"Replace": "Replace",
|
||||
"Replace all": "Replace all",
|
||||
"Image width": "Image width",
|
||||
"Image height": "Image height",
|
||||
"Profile picture": "Profile picture",
|
||||
"Create a new doc": "Create a new doc",
|
||||
"{{ userName }} won't be notified, as they do not have access to this document": "{{ userName }} won't be notified, as they do not have access to this document",
|
||||
@@ -494,8 +486,7 @@
|
||||
"Create a new child doc": "Create a new child doc",
|
||||
"Delete table": "Delete table",
|
||||
"Delete file": "Delete file",
|
||||
"Width": "Width",
|
||||
"Height": "Height",
|
||||
"Width x Height": "Width x Height",
|
||||
"Download file": "Download file",
|
||||
"Replace file": "Replace file",
|
||||
"Delete image": "Delete image",
|
||||
@@ -689,7 +680,6 @@
|
||||
"only you": "only you",
|
||||
"person": "person",
|
||||
"people": "people",
|
||||
"Document title": "Document title",
|
||||
"Last updated": "Last updated",
|
||||
"Type '/' to insert, or start writing…": "Type '/' to insert, or start writing…",
|
||||
"Hide contents": "Hide contents",
|
||||
@@ -934,7 +924,6 @@
|
||||
"Rotate secret": "Rotate secret",
|
||||
"Rotating the client secret will invalidate the current secret. Make sure to update any applications using these credentials.": "Rotating the client secret will invalidate the current secret. Make sure to update any applications using these credentials.",
|
||||
"Displayed to users when authorizing": "Displayed to users when authorizing",
|
||||
"Application icon": "Application icon",
|
||||
"Developer information shown to users when authorizing": "Developer information shown to users when authorizing",
|
||||
"Developer name": "Developer name",
|
||||
"Developer URL": "Developer URL",
|
||||
@@ -998,9 +987,7 @@
|
||||
"Search people": "Search people",
|
||||
"No people matching your search": "No people matching your search",
|
||||
"No people left to add": "No people left to add",
|
||||
"Group admin": "Group admin",
|
||||
"Member": "Member",
|
||||
"Admins": "Admins",
|
||||
"Date created": "Date created",
|
||||
"Crop Image": "Crop Image",
|
||||
"Crop image": "Crop image",
|
||||
@@ -1030,6 +1017,7 @@
|
||||
"Domain": "Domain",
|
||||
"Views": "Views",
|
||||
"All roles": "All roles",
|
||||
"Admins": "Admins",
|
||||
"Editors": "Editors",
|
||||
"All status": "All status",
|
||||
"Active": "Active",
|
||||
@@ -1042,7 +1030,6 @@
|
||||
"These settings affect the way that your workspace appears to everyone on the team.": "These settings affect the way that your workspace appears to everyone on the team.",
|
||||
"Display": "Display",
|
||||
"The logo is displayed at the top left of the application.": "The logo is displayed at the top left of the application.",
|
||||
"Workspace logo": "Workspace logo",
|
||||
"The workspace name, usually the same as your company name.": "The workspace name, usually the same as your company name.",
|
||||
"Description": "Description",
|
||||
"A short description of your workspace.": "A short description of your workspace.",
|
||||
@@ -1273,7 +1260,6 @@
|
||||
"{{ user }} updated {{ timeAgo }}": "{{ user }} updated {{ timeAgo }}",
|
||||
"You created {{ timeAgo }}": "You created {{ timeAgo }}",
|
||||
"{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}",
|
||||
"Caption": "Caption",
|
||||
"Open": "Open",
|
||||
"Error loading data": "Error loading data"
|
||||
}
|
||||
|
||||
@@ -694,7 +694,7 @@
|
||||
"@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3"
|
||||
chokidar "^3.6.0"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.27.1":
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
|
||||
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
|
||||
@@ -4412,6 +4412,29 @@
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578"
|
||||
integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==
|
||||
|
||||
"@testing-library/dom@^8.0.0":
|
||||
version "8.20.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f"
|
||||
integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/aria-query" "^5.0.1"
|
||||
aria-query "5.1.3"
|
||||
chalk "^4.1.0"
|
||||
dom-accessibility-api "^0.5.9"
|
||||
lz-string "^1.5.0"
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/react@^12.0.0":
|
||||
version "12.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b"
|
||||
integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@testing-library/dom" "^8.0.0"
|
||||
"@types/react-dom" "<18.0.0"
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
@@ -4443,6 +4466,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9"
|
||||
integrity "sha1-qB/YYG1IH4c6OADG665PHXaKVqk= sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="
|
||||
|
||||
"@types/aria-query@^5.0.1":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
|
||||
integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==
|
||||
|
||||
"@types/async-lock@^1.1.3":
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/async-lock/-/async-lock-1.1.5.tgz#a82f33e09aef451d6ded7bffae73f9d254723124"
|
||||
@@ -5270,7 +5298,7 @@
|
||||
dependencies:
|
||||
"@types/reactcss" "*"
|
||||
|
||||
"@types/react-dom@^17.0.11":
|
||||
"@types/react-dom@<18.0.0", "@types/react-dom@^17.0.11":
|
||||
version "17.0.25"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5"
|
||||
integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==
|
||||
@@ -5706,7 +5734,14 @@ aria-hidden@^1.2.4:
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2:
|
||||
aria-query@5.1.3:
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e"
|
||||
integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==
|
||||
dependencies:
|
||||
deep-equal "^2.0.5"
|
||||
|
||||
array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b"
|
||||
integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==
|
||||
@@ -6314,7 +6349,7 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
call-bind@^1.0.7, call-bind@^1.0.8:
|
||||
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7, call-bind@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c"
|
||||
integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==
|
||||
@@ -7457,6 +7492,30 @@ dedent@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff"
|
||||
integrity "sha1-Tz/JTItxHpuygA0YXNatIPKpCv8= sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg=="
|
||||
|
||||
deep-equal@^2.0.5:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1"
|
||||
integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==
|
||||
dependencies:
|
||||
array-buffer-byte-length "^1.0.0"
|
||||
call-bind "^1.0.5"
|
||||
es-get-iterator "^1.1.3"
|
||||
get-intrinsic "^1.2.2"
|
||||
is-arguments "^1.1.1"
|
||||
is-array-buffer "^3.0.2"
|
||||
is-date-object "^1.0.5"
|
||||
is-regex "^1.1.4"
|
||||
is-shared-array-buffer "^1.0.2"
|
||||
isarray "^2.0.5"
|
||||
object-is "^1.1.5"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.4"
|
||||
regexp.prototype.flags "^1.5.1"
|
||||
side-channel "^1.0.4"
|
||||
which-boxed-primitive "^1.0.2"
|
||||
which-collection "^1.0.1"
|
||||
which-typed-array "^1.1.13"
|
||||
|
||||
deep-equal@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||
@@ -7481,7 +7540,7 @@ define-data-property@^1.0.1, define-data-property@^1.1.4:
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.0.1"
|
||||
|
||||
define-properties@^1.2.1:
|
||||
define-properties@^1.1.3, define-properties@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
|
||||
integrity "sha1-EHgcxhbrlRqAoDS6/Kpzd/avK2w= sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="
|
||||
@@ -7594,6 +7653,11 @@ dnd-core@^16.0.1:
|
||||
"@react-dnd/invariant" "^4.0.1"
|
||||
redux "^4.2.0"
|
||||
|
||||
dom-accessibility-api@^0.5.9:
|
||||
version "0.5.16"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
||||
|
||||
dom-serializer@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
|
||||
@@ -7933,6 +7997,21 @@ es-errors@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-get-iterator@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"
|
||||
integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
get-intrinsic "^1.1.3"
|
||||
has-symbols "^1.0.3"
|
||||
is-arguments "^1.1.1"
|
||||
is-map "^2.0.2"
|
||||
is-set "^2.0.2"
|
||||
is-string "^1.0.7"
|
||||
isarray "^2.0.5"
|
||||
stop-iteration-iterator "^1.0.0"
|
||||
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||
@@ -8484,7 +8563,7 @@ get-caller-file@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34= sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
||||
|
||||
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0:
|
||||
get-intrinsic@^1.1.3, get-intrinsic@^1.2.2, get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
@@ -9106,7 +9185,7 @@ ini@^1.3.4:
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
|
||||
integrity "sha1-oJNj4ZEZcuoW16iFEAXYTPCamoQ= sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="
|
||||
|
||||
internal-slot@^1.1.0:
|
||||
internal-slot@^1.0.4, internal-slot@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961"
|
||||
integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==
|
||||
@@ -9193,7 +9272,15 @@ is-alphanumerical@^1.0.0:
|
||||
is-alphabetical "^1.0.0"
|
||||
is-decimal "^1.0.0"
|
||||
|
||||
is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
|
||||
is-arguments@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
|
||||
integrity "sha1-FbP4j9oB8ql/7ITKdhpWDxI++ps= sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA=="
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
has-tostringtag "^1.0.0"
|
||||
|
||||
is-array-buffer@^3.0.2, is-array-buffer@^3.0.4, is-array-buffer@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280"
|
||||
integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==
|
||||
@@ -9348,7 +9435,7 @@ is-hexadecimal@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
|
||||
integrity "sha1-zDXJdYjaS9Saju3WvECC1E3LI6c= sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
|
||||
|
||||
is-map@^2.0.3:
|
||||
is-map@^2.0.2, is-map@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e"
|
||||
integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==
|
||||
@@ -9396,7 +9483,7 @@ is-printable-key-event@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-printable-key-event/-/is-printable-key-event-1.0.0.tgz#1ea47b8abe1a2e53a1f5ea6aecbd6d24da707c66"
|
||||
integrity "sha1-HqR7ir4aLlOh9epq7L1tJNpwfGY= sha512-C/GJ8ApSdY6/RGQrSSkBzuWDtYI9/mOTRLCOu/5iYH46pI7Ki6y6B71kPL7OWRzqv9KkWSEmskKdq5IvgAGPHA=="
|
||||
|
||||
is-regex@^1.2.1:
|
||||
is-regex@^1.1.4, is-regex@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
|
||||
integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==
|
||||
@@ -9411,12 +9498,12 @@ is-regexp@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
|
||||
integrity "sha1-/S2INUXEa6xaYz57mgnof6LLUGk= sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="
|
||||
|
||||
is-set@^2.0.3:
|
||||
is-set@^2.0.2, is-set@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d"
|
||||
integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==
|
||||
|
||||
is-shared-array-buffer@^1.0.4:
|
||||
is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f"
|
||||
integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==
|
||||
@@ -9433,7 +9520,7 @@ is-stream@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac"
|
||||
integrity "sha1-5r/XqmvvafT0cs6btoHj5XtDGaw= sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="
|
||||
|
||||
is-string@^1.1.1:
|
||||
is-string@^1.0.7, is-string@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9"
|
||||
integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==
|
||||
@@ -10866,6 +10953,11 @@ luxon@^3.2.1:
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.6.1.tgz#d283ffc4c0076cb0db7885ec6da1c49ba97e47b0"
|
||||
integrity sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==
|
||||
|
||||
lz-string@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
|
||||
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
|
||||
|
||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||
version "0.25.9"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
||||
@@ -11423,6 +11515,14 @@ object-inspect@^1.13.3:
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.3.tgz#f14c183de51130243d6d18ae149375ff50ea488a"
|
||||
integrity sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==
|
||||
|
||||
object-is@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
|
||||
integrity "sha1-ud7qpfx/GEag+uzc7sE45XePU6w= sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw=="
|
||||
dependencies:
|
||||
call-bind "^1.0.2"
|
||||
define-properties "^1.1.3"
|
||||
|
||||
object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
@@ -11433,7 +11533,7 @@ object-treeify@1.1.33:
|
||||
resolved "https://registry.yarnpkg.com/object-treeify/-/object-treeify-1.1.33.tgz#f06fece986830a3cba78ddd32d4c11d1f76cdf40"
|
||||
integrity sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==
|
||||
|
||||
object.assign@^4.1.7:
|
||||
object.assign@^4.1.4, object.assign@^4.1.7:
|
||||
version "4.1.7"
|
||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d"
|
||||
integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==
|
||||
@@ -12106,6 +12206,15 @@ pretty-bytes@^6.1.1:
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b"
|
||||
integrity "sha1-OM1rtG9Hr79mfCAs/HVL/9IBajs= sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="
|
||||
|
||||
pretty-format@^27.0.2:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
pretty-format@^29.0.0, pretty-format@^29.7.0:
|
||||
version "29.7.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812"
|
||||
@@ -12515,6 +12624,11 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity "sha1-eJcppNw23imZ3BVt1sHZwYzqVqQ= sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
|
||||
react-is@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
"react-is@^17.0.1 || ^18.0.0", react-is@^18.0.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
@@ -12811,7 +12925,7 @@ regenerator-runtime@^0.14.0:
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
|
||||
integrity "sha1-XhnWjrEtSG95fhWjxqkY987F60U= sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
|
||||
regexp.prototype.flags@^1.5.3:
|
||||
regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz#b3ae40b1d2499b8350ab2c3fe6ef3845d3a96f42"
|
||||
integrity sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==
|
||||
@@ -13369,7 +13483,7 @@ side-channel-weakmap@^1.0.2:
|
||||
object-inspect "^1.13.3"
|
||||
side-channel-map "^1.0.1"
|
||||
|
||||
side-channel@^1.1.0:
|
||||
side-channel@^1.0.4, side-channel@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
|
||||
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
|
||||
@@ -13639,6 +13753,13 @@ statuses@2.0.1, statuses@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="
|
||||
|
||||
stop-iteration-iterator@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4"
|
||||
integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==
|
||||
dependencies:
|
||||
internal-slot "^1.0.4"
|
||||
|
||||
stoppable@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/stoppable/-/stoppable-1.1.0.tgz#32da568e83ea488b08e4d7ea2c3bcc9d75015d5b"
|
||||
@@ -14836,7 +14957,7 @@ whatwg-url@^7.0.0:
|
||||
tr46 "^1.0.1"
|
||||
webidl-conversions "^4.0.2"
|
||||
|
||||
which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
|
||||
which-boxed-primitive@^1.0.2, which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"
|
||||
integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==
|
||||
@@ -14866,7 +14987,7 @@ which-builtin-type@^1.2.1:
|
||||
which-collection "^1.0.2"
|
||||
which-typed-array "^1.1.16"
|
||||
|
||||
which-collection@^1.0.2:
|
||||
which-collection@^1.0.1, which-collection@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0"
|
||||
integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==
|
||||
@@ -14876,7 +14997,7 @@ which-collection@^1.0.2:
|
||||
is-weakmap "^2.0.2"
|
||||
is-weakset "^2.0.3"
|
||||
|
||||
which-typed-array@^1.1.16, which-typed-array@^1.1.18:
|
||||
which-typed-array@^1.1.13, which-typed-array@^1.1.16, which-typed-array@^1.1.18:
|
||||
version "1.1.18"
|
||||
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.18.tgz#df2389ebf3fbb246a71390e90730a9edb6ce17ad"
|
||||
integrity sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==
|
||||
|
||||
Reference in New Issue
Block a user