mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c802a0434 | |||
| 6d76379821 | |||
| cd84eb7228 | |||
| f686edbcf4 | |||
| cc849be25e | |||
| 26f16939ca | |||
| e4917cc4bd | |||
| 0f4c1d7db5 | |||
| 554c7a8111 | |||
| 6cf230963e | |||
| 39f9bfbbcd | |||
| 987dceed28 | |||
| 047239ae16 | |||
| 1704a40045 | |||
| 39c2aca883 | |||
| dc3952212f | |||
| f3b4640c7a | |||
| 4ab2b22f7b | |||
| 9b973c64e9 | |||
| a85fec57cc | |||
| c2069db882 | |||
| 5ace3363ac | |||
| ee285bc4d5 | |||
| b23596600b | |||
| 384186c318 | |||
| 6660cd6746 | |||
| 6a16dc07c1 | |||
| 16038896b4 | |||
| c7f7c43aaf | |||
| b1fd8878f4 | |||
| 44eabf4b8d | |||
| 301a6f1177 | |||
| 277d9fb0d9 | |||
| fde507f34f | |||
| 34bdd59f35 | |||
| 62a388fc3b | |||
| 758d4edbb9 | |||
| 1836d2ef63 | |||
| d73d0523f4 | |||
| bba94faf00 | |||
| 82dc24040c | |||
| 66209c4ee8 | |||
| 665b19d933 | |||
| f6d9d00947 | |||
| 76bd503581 | |||
| e7b7eb7818 | |||
| e51d2f643e | |||
| cd0acc40bb | |||
| 7a5480f12f | |||
| 7c087b125e | |||
| 933dde935d | |||
| 6efcf1beee | |||
| caaff1c3d6 | |||
| 2686e059a0 | |||
| dae1bce48c | |||
| aa8e077649 | |||
| 878f2d2e76 | |||
| d538497fe2 | |||
| 11cff77162 | |||
| f284a27941 | |||
| 022d8fca94 | |||
| ee125e6235 | |||
| 3cc4030221 | |||
| c599b689ab | |||
| c8b121a3bb | |||
| 0198b80b5d | |||
| 6c1df04721 | |||
| e85befb41f | |||
| d0c7409de8 | |||
| d559afe2ce | |||
| c02a33a74c | |||
| 5b7a5d751c | |||
| 73fea094a8 | |||
| 82b5b87440 | |||
| 0170b98974 | |||
| f7f1f07716 | |||
| b7e80036eb | |||
| 3ffee1239b | |||
| 22c0f18b6b | |||
| 76d9a02fee | |||
| 1752c04c06 | |||
| 084490ba6b | |||
| eaf2fd102e | |||
| 3bc566915e | |||
| 19627f4d07 |
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-react",
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
"runtime": "automatic"
|
||||
}
|
||||
],
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
@@ -60,4 +65,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"extraFileExtensions": [".json"],
|
||||
"extraFileExtensions": [
|
||||
".json"
|
||||
],
|
||||
"project": "./tsconfig.json",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
@@ -17,6 +19,7 @@
|
||||
],
|
||||
"plugins": [
|
||||
"es",
|
||||
"react",
|
||||
"@typescript-eslint",
|
||||
"eslint-plugin-import",
|
||||
"eslint-plugin-node",
|
||||
@@ -24,28 +27,52 @@
|
||||
"eslint-plugin-lodash"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "reakit/Menu",
|
||||
"importNames": [
|
||||
"useMenuState"
|
||||
],
|
||||
"message": "Do not use useMenuState from reakit/Menu. Use useMenuState instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"eqeqeq": 2,
|
||||
"curly": 2,
|
||||
"no-console": "error",
|
||||
"arrow-body-style": ["error", "as-needed"],
|
||||
"arrow-body-style": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"spaced-comment": "error",
|
||||
"object-shorthand": "error",
|
||||
"no-mixed-operators": "off",
|
||||
"no-useless-escape": "off",
|
||||
"no-shadow": "off",
|
||||
"es/no-regexp-lookbehind-assertions": "error",
|
||||
"react/self-closing-comp": ["error", {
|
||||
"component": true,
|
||||
"html": true
|
||||
}],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/self-closing-comp": [
|
||||
"error",
|
||||
{
|
||||
"component": true,
|
||||
"html": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-shadow": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["transaction"],
|
||||
"allow": [
|
||||
"transaction"
|
||||
],
|
||||
"hoist": "all",
|
||||
"ignoreTypeValueShadow": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
@@ -59,13 +86,30 @@
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"padding-line-between-statements": ["error", { "blankLine": "always", "prev": "*", "next": "export" }],
|
||||
"lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
|
||||
"lodash/import-scope": ["error", "method"],
|
||||
"padding-line-between-statements": [
|
||||
"error",
|
||||
{
|
||||
"blankLine": "always",
|
||||
"prev": "*",
|
||||
"next": "export"
|
||||
}
|
||||
],
|
||||
"lines-between-class-members": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"exceptAfterSingleLine": true
|
||||
}
|
||||
],
|
||||
"lodash/import-scope": [
|
||||
"error",
|
||||
"method"
|
||||
],
|
||||
"import/no-named-as-default": "off",
|
||||
"import/no-named-as-default-member": "off",
|
||||
"import/newline-after-import": 2,
|
||||
@@ -134,7 +178,10 @@
|
||||
"version": "detect"
|
||||
},
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"]
|
||||
"@typescript-eslint/parser": [
|
||||
".ts",
|
||||
".tsx"
|
||||
]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"plugins": [
|
||||
"eslint-plugin-react-hooks"
|
||||
],
|
||||
"rules": {
|
||||
"react/react-in-jsx-scope": "off"
|
||||
},
|
||||
"env": {
|
||||
"jest": true,
|
||||
"browser": true
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "~/stores";
|
||||
import ApiKeyNew from "~/scenes/ApiKeyNew";
|
||||
import { createAction } from "..";
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
UnstarredIcon,
|
||||
UnsubscribeIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import Collection from "~/models/Collection";
|
||||
import { CollectionEdit } from "~/components/Collection/CollectionEdit";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DoneIcon, SmileyIcon, TrashIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import stores from "~/stores";
|
||||
import Comment from "~/models/Comment";
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
TrashIcon,
|
||||
UserIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { createAction } from "~/actions";
|
||||
import { DeveloperSection } from "~/actions/sections";
|
||||
@@ -107,8 +106,8 @@ export const startTyping = createAction({
|
||||
}, 250);
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
intervalId && clearInterval(intervalId);
|
||||
if (event.key === "Escape" && intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
LogoutIcon,
|
||||
CaseSensitiveIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import {
|
||||
@@ -751,7 +750,7 @@ export const importDocument = createAction({
|
||||
|
||||
return false;
|
||||
},
|
||||
perform: ({ activeCollectionId, activeDocumentId, stores }) => {
|
||||
perform: ({ activeDocumentId, activeCollectionId, stores }) => {
|
||||
const { documents } = stores;
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
@@ -1082,12 +1081,17 @@ export const openDocumentComments = createAction({
|
||||
analyticsName: "Open comments",
|
||||
section: ActiveDocumentSection,
|
||||
icon: <CommentIcon />,
|
||||
visible: ({ activeDocumentId, stores }) => {
|
||||
visible: ({ activeCollectionId, activeDocumentId, stores }) => {
|
||||
const can = stores.policies.abilities(activeDocumentId ?? "");
|
||||
const collection = activeCollectionId
|
||||
? stores.collections.get(activeCollectionId)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
!!activeDocumentId &&
|
||||
can.comment &&
|
||||
!!stores.auth.team?.getPreference(TeamPreference.Commenting)
|
||||
(collection?.canCreateComment ??
|
||||
!!stores.auth.team?.getPreference(TeamPreference.Commenting))
|
||||
);
|
||||
},
|
||||
perform: ({ activeDocumentId, stores }) => {
|
||||
@@ -1211,7 +1215,7 @@ export const leaveDocument = createAction({
|
||||
} as UserMembership);
|
||||
|
||||
toast.success(t("You have left the shared document"));
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
toast.error(t("Could not leave document"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ShapesIcon,
|
||||
DraftsIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { UrlHelper } from "@shared/utils/UrlHelper";
|
||||
import { isMac } from "@shared/utils/browser";
|
||||
import stores from "~/stores";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ArchiveIcon, MarkAsReadIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { createAction } from "..";
|
||||
import { NotificationSection } from "../sections";
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "~/stores";
|
||||
import { OAuthClientNew } from "~/components/OAuthClient/OAuthClientNew";
|
||||
import { createAction } from "..";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import copy from "copy-to-clipboard";
|
||||
import { LinkIcon, RestoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { LinkIcon, RestoreIcon, TrashIcon } from "outline-icons";
|
||||
import { matchPath } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import stores from "~/stores";
|
||||
@@ -13,7 +12,7 @@ import {
|
||||
} from "~/utils/routeHelpers";
|
||||
|
||||
export const restoreRevision = createAction({
|
||||
name: ({ t }) => t("Restore revision"),
|
||||
name: ({ t }) => t("Restore"),
|
||||
analyticsName: "Restore revision",
|
||||
icon: <RestoreIcon />,
|
||||
section: RevisionSection,
|
||||
@@ -42,6 +41,38 @@ export const restoreRevision = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteRevision = createAction({
|
||||
name: ({ t }) => t("Delete"),
|
||||
analyticsName: "Delete revision",
|
||||
icon: <TrashIcon />,
|
||||
section: RevisionSection,
|
||||
dangerous: true,
|
||||
visible: ({ activeDocumentId }) =>
|
||||
!!activeDocumentId && stores.policies.abilities(activeDocumentId).update,
|
||||
perform: async ({ t, event, location, activeDocumentId }) => {
|
||||
event?.preventDefault();
|
||||
if (!activeDocumentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const document = stores.documents.get(activeDocumentId);
|
||||
if (!document) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = matchPath<{ revisionId: string }>(location.pathname, {
|
||||
path: matchDocumentHistory,
|
||||
});
|
||||
const revisionId = match?.params.revisionId;
|
||||
if (revisionId) {
|
||||
const revision = stores.revisions.get(revisionId);
|
||||
await revision?.delete();
|
||||
toast.success(t("This version of the document was deleted"));
|
||||
history.push(documentHistoryPath(document));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const copyLinkToRevision = createAction({
|
||||
name: ({ t }) => t("Copy link"),
|
||||
analyticsName: "Copy link to revision",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SunIcon, MoonIcon, BrowserIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import stores from "~/stores";
|
||||
import { Theme } from "~/stores/UiStore";
|
||||
import { createAction } from "~/actions";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ArrowIcon, PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { stringToColor } from "@shared/utils/color";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
@@ -58,13 +57,15 @@ export const createTeam = createAction({
|
||||
perform: ({ t, event, stores }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
const { user } = stores.auth;
|
||||
user &&
|
||||
if (user) {
|
||||
stores.dialogs.openModal({
|
||||
title: t("Create a workspace"),
|
||||
fullscreen: true,
|
||||
content: <TeamNew user={user} />,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { UserRole } from "@shared/types";
|
||||
import { UserRoleHelper } from "@shared/utils/UserRoleHelper";
|
||||
import stores from "~/stores";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import flattenDeep from "lodash/flattenDeep";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Optional } from "utility-types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from "react";
|
||||
|
||||
export default function Arrow() {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
@@ -19,7 +19,7 @@ const Authenticated = ({ children }: Props) => {
|
||||
|
||||
// Watching for language changes here as this is the earliest point we might have the user
|
||||
// available and means we can start loading translations faster
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
void changeLanguage(language, i18n);
|
||||
}, [i18n, language]);
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
||||
const { ui, auth } = useStores();
|
||||
const { ui, auth, collections } = useStores();
|
||||
const location = useLocation();
|
||||
const layoutRef = React.useRef<HTMLDivElement>(null);
|
||||
const can = usePolicy(ui.activeDocumentId);
|
||||
@@ -108,7 +108,9 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => {
|
||||
can.comment &&
|
||||
ui.activeDocumentId &&
|
||||
ui.commentsExpanded &&
|
||||
team.getPreference(TeamPreference.Commenting);
|
||||
(ui.activeCollectionId
|
||||
? collections.get(ui.activeCollectionId)?.canCreateComment
|
||||
: !!team.getPreference(TeamPreference.Commenting));
|
||||
|
||||
const sidebarRight = (
|
||||
<AnimatePresence
|
||||
|
||||
@@ -45,12 +45,23 @@ type Props = {
|
||||
};
|
||||
|
||||
function Avatar(props: Props) {
|
||||
const { model, style, variant = AvatarVariant.Round, ...rest } = props;
|
||||
const {
|
||||
model,
|
||||
style,
|
||||
variant = AvatarVariant.Round,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
const src = props.src || model?.avatarUrl;
|
||||
const [error, handleError] = useBoolean(false);
|
||||
|
||||
return (
|
||||
<Relative style={style} $variant={variant} $size={props.size}>
|
||||
<Relative
|
||||
style={style}
|
||||
$variant={variant}
|
||||
$size={props.size}
|
||||
className={className}
|
||||
>
|
||||
{src && !error ? (
|
||||
<Image onError={handleError} src={src} {...rest} />
|
||||
) : model ? (
|
||||
@@ -75,6 +86,8 @@ const Relative = styled.div<{ $variant: AvatarVariant; $size: AvatarSize }>`
|
||||
border-radius: ${(props) =>
|
||||
props.$variant === AvatarVariant.Round ? "50%" : `${props.$size / 8}px`};
|
||||
overflow: hidden;
|
||||
width: ${(props) => props.$size}px;
|
||||
height: ${(props) => props.$size}px;
|
||||
`;
|
||||
|
||||
const Image = styled.img<{ size: number }>`
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GroupIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTheme } from "styled-components";
|
||||
import Squircle from "@shared/components/Squircle";
|
||||
import Group from "~/models/Group";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Avatar, { IAvatar, AvatarSize } from "./Avatar";
|
||||
import Avatar, { IAvatar, AvatarSize, AvatarVariant } from "./Avatar";
|
||||
import AvatarWithPresence from "./AvatarWithPresence";
|
||||
import { GroupAvatar } from "./GroupAvatar";
|
||||
|
||||
export { Avatar, GroupAvatar, AvatarSize, AvatarWithPresence };
|
||||
export { Avatar, GroupAvatar, AvatarSize, AvatarVariant, AvatarWithPresence };
|
||||
|
||||
export type { IAvatar };
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { depths, s } from "@shared/styles";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { changeLanguage } from "~/utils/language";
|
||||
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
export default function ChangeLanguage({ locale }: Props) {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
void changeLanguage(locale, i18n);
|
||||
}, [locale, i18n]);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
|
||||
const cleanPercentage = (percentage: number) => {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import filter from "lodash/filter";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import uniq from "lodash/uniq";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useState, useMemo, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
||||
import styled from "styled-components";
|
||||
import { depths, s } from "@shared/styles";
|
||||
import Document from "~/models/Document";
|
||||
import { AvatarSize, AvatarWithPresence } from "~/components/Avatar";
|
||||
import DocumentViews from "~/components/DocumentViews";
|
||||
import Facepile from "~/components/Facepile";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Popover from "~/components/Popover";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { fadeAndScaleIn } from "~/styles/animations";
|
||||
|
||||
type Props = {
|
||||
/** The document to display live collaborators for */
|
||||
@@ -22,6 +24,21 @@ type Props = {
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
// Styled components to match the original Popover styling
|
||||
const StyledPopoverContent = styled(Popover.Content)`
|
||||
animation: ${fadeAndScaleIn} 200ms ease;
|
||||
transform-origin: 75% 0;
|
||||
background: ${s("menuBackground")};
|
||||
border-radius: 6px;
|
||||
padding: 12px 24px;
|
||||
max-height: 75vh;
|
||||
box-shadow: ${s("menuShadow")};
|
||||
z-index: ${depths.modal};
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
outline: none;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Displays a list of live collaborators for a document, including their avatars
|
||||
* and presence status.
|
||||
@@ -31,58 +48,93 @@ function Collaborators(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const user = useCurrentUser();
|
||||
const currentUserId = user?.id;
|
||||
const [requestedUserIds, setRequestedUserIds] = React.useState<string[]>([]);
|
||||
const [requestedUserIds, setRequestedUserIds] = useState<string[]>([]);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const { users, presence, ui } = useStores();
|
||||
const { document } = props;
|
||||
const { observingUserId } = ui;
|
||||
const documentPresence = presence.get(document.id);
|
||||
const documentPresenceArray = documentPresence
|
||||
? Array.from(documentPresence.values())
|
||||
: [];
|
||||
|
||||
const presentIds = documentPresenceArray.map((p) => p.userId);
|
||||
const editingIds = documentPresenceArray
|
||||
.filter((p) => p.isEditing)
|
||||
.map((p) => p.userId);
|
||||
// Use Set for O(1) lookups and stable references
|
||||
const presentIds = useMemo(
|
||||
() => new Set(documentPresenceArray.map((p) => p.userId)),
|
||||
[documentPresenceArray]
|
||||
);
|
||||
const editingIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
documentPresenceArray.filter((p) => p.isEditing).map((p) => p.userId)
|
||||
),
|
||||
[documentPresenceArray]
|
||||
);
|
||||
|
||||
// ensure currently present via websocket are always ordered first
|
||||
const collaborators = React.useMemo(
|
||||
// Memoize collaboratorIds as a Set for efficient lookup
|
||||
const collaboratorIdsSet = useMemo(
|
||||
() => new Set(document.collaboratorIds),
|
||||
[document.collaboratorIds]
|
||||
);
|
||||
const collaborators = useMemo(
|
||||
() =>
|
||||
orderBy(
|
||||
filter(
|
||||
users.all,
|
||||
(u) =>
|
||||
(presentIds.includes(u.id) ||
|
||||
document.collaboratorIds.includes(u.id)) &&
|
||||
(presentIds.has(u.id) || collaboratorIdsSet.has(u.id)) &&
|
||||
!u.isSuspended
|
||||
),
|
||||
[(u) => presentIds.includes(u.id), "id"],
|
||||
[(u) => presentIds.has(u.id), "id"],
|
||||
["asc", "asc"]
|
||||
),
|
||||
[document.collaboratorIds, users.all, presentIds]
|
||||
[collaboratorIdsSet, users.all, presentIds]
|
||||
);
|
||||
|
||||
// load any users we don't yet have in memory
|
||||
React.useEffect(() => {
|
||||
const ids = uniq([...document.collaboratorIds, ...presentIds])
|
||||
.filter((userId) => !users.get(userId))
|
||||
.sort();
|
||||
// Memoize ids to avoid unnecessary effect executions
|
||||
const missingUserIds = useMemo(
|
||||
() =>
|
||||
uniq([...document.collaboratorIds, ...Array.from(presentIds)])
|
||||
.filter((userId) => !users.get(userId))
|
||||
.sort(),
|
||||
[document.collaboratorIds, presentIds, users]
|
||||
);
|
||||
|
||||
if (!isEqual(requestedUserIds, ids) && ids.length > 0) {
|
||||
setRequestedUserIds(ids);
|
||||
void users.fetchPage({ ids, limit: 100 });
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isEqual(requestedUserIds, missingUserIds) &&
|
||||
missingUserIds.length > 0
|
||||
) {
|
||||
setRequestedUserIds(missingUserIds);
|
||||
void users.fetchPage({ ids: missingUserIds, limit: 100 });
|
||||
}
|
||||
}, [document, users, presentIds, document.collaboratorIds, requestedUserIds]);
|
||||
}, [missingUserIds, requestedUserIds, users]);
|
||||
|
||||
const popover = usePopoverState({
|
||||
gutter: 0,
|
||||
placement: "bottom-end",
|
||||
});
|
||||
// Memoize onClick handler to avoid inline function creation
|
||||
const handleAvatarClick = useCallback(
|
||||
(
|
||||
collaboratorId: string,
|
||||
isPresent: boolean,
|
||||
isObserving: boolean,
|
||||
isObservable: boolean
|
||||
) =>
|
||||
(ev: React.MouseEvent) => {
|
||||
if (isObservable && isPresent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
ui.setObservingUser(isObserving ? undefined : collaboratorId);
|
||||
}
|
||||
},
|
||||
[ui]
|
||||
);
|
||||
|
||||
const renderAvatar = React.useCallback(
|
||||
const renderAvatar = useCallback(
|
||||
({ model: collaborator, ...rest }) => {
|
||||
const isPresent = presentIds.includes(collaborator.id);
|
||||
const isEditing = editingIds.includes(collaborator.id);
|
||||
const isObserving = ui.observingUserId === collaborator.id;
|
||||
const isPresent = presentIds.has(collaborator.id);
|
||||
const isEditing = editingIds.has(collaborator.id);
|
||||
const isObserving = observingUserId === collaborator.id;
|
||||
const isObservable = collaborator.id !== currentUserId;
|
||||
|
||||
return (
|
||||
@@ -96,46 +148,48 @@ function Collaborators(props: Props) {
|
||||
isCurrentUser={currentUserId === collaborator.id}
|
||||
onClick={
|
||||
isObservable
|
||||
? (ev) => {
|
||||
if (isPresent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
ui.setObservingUser(
|
||||
isObserving ? undefined : collaborator.id
|
||||
);
|
||||
}
|
||||
}
|
||||
? handleAvatarClick(
|
||||
collaborator.id,
|
||||
isPresent,
|
||||
isObserving,
|
||||
isObservable
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[presentIds, ui, currentUserId, editingIds]
|
||||
[presentIds, editingIds, observingUserId, currentUserId, handleAvatarClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopoverDisclosure {...popover}>
|
||||
{(popoverProps) => (
|
||||
<NudeButton
|
||||
width={Math.min(collaborators.length, limit) * AvatarSize.Large}
|
||||
height={AvatarSize.Large}
|
||||
{...popoverProps}
|
||||
>
|
||||
<Facepile
|
||||
size={AvatarSize.Large}
|
||||
limit={limit}
|
||||
overflow={Math.max(0, collaborators.length - limit)}
|
||||
users={collaborators}
|
||||
renderAvatar={renderAvatar}
|
||||
/>
|
||||
</NudeButton>
|
||||
)}
|
||||
</PopoverDisclosure>
|
||||
<Popover {...popover} width={300} aria-label={t("Viewers")} tabIndex={0}>
|
||||
<DocumentViews document={document} isOpen={popover.visible} />
|
||||
</Popover>
|
||||
</>
|
||||
<Popover.Root open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<NudeButton
|
||||
width={Math.min(collaborators.length, limit) * AvatarSize.Large}
|
||||
height={AvatarSize.Large}
|
||||
>
|
||||
<Facepile
|
||||
size={AvatarSize.Large}
|
||||
limit={limit}
|
||||
overflow={Math.max(0, collaborators.length - limit)}
|
||||
users={collaborators}
|
||||
renderAvatar={renderAvatar}
|
||||
/>
|
||||
</NudeButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<StyledPopoverContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={0}
|
||||
aria-label={t("Viewers")}
|
||||
style={{ width: 300 }}
|
||||
>
|
||||
<DocumentViews document={document} />
|
||||
</StyledPopoverContent>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { CollectionForm, FormData } from "./CollectionForm";
|
||||
@@ -16,7 +16,7 @@ export const CollectionEdit = observer(function CollectionEdit_({
|
||||
const { collections } = useStores();
|
||||
const collection = collections.get(collectionId);
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
const handleSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
await collection?.save(data);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import uniq from "lodash/uniq";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useEffect, useCallback, Suspense } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { randomElement } from "@shared/random";
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { CollectionPermission, TeamPreference } from "@shared/types";
|
||||
import { IconLibrary } from "@shared/utils/IconLibrary";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import { CollectionValidation } from "@shared/validations";
|
||||
@@ -22,6 +22,7 @@ import useBoolean from "~/hooks/useBoolean";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { EmptySelectValue } from "~/types";
|
||||
import { createSwitchRegister } from "~/utils/forms";
|
||||
|
||||
const IconPicker = createLazyComponent(() => import("~/components/IconPicker"));
|
||||
|
||||
@@ -31,6 +32,7 @@ export interface FormData {
|
||||
color: string | null;
|
||||
sharing: boolean;
|
||||
permission: CollectionPermission | undefined;
|
||||
commenting?: boolean | null;
|
||||
}
|
||||
|
||||
const useIconColor = (collection?: Collection) => {
|
||||
@@ -40,7 +42,7 @@ const useIconColor = (collection?: Collection) => {
|
||||
collections.orderedData.map((c) => c.color).filter(Boolean)
|
||||
) as string[];
|
||||
|
||||
const iconColor = React.useMemo(
|
||||
const iconColor = useMemo(
|
||||
() =>
|
||||
collection?.color ??
|
||||
// If all the existing collections have the same color, use that color,
|
||||
@@ -83,6 +85,7 @@ export const CollectionForm = observer(function CollectionForm_({
|
||||
icon: collection?.icon,
|
||||
sharing: collection?.sharing ?? true,
|
||||
permission: collection?.permission,
|
||||
commenting: collection?.commenting ?? true,
|
||||
color: iconColor,
|
||||
},
|
||||
});
|
||||
@@ -90,11 +93,11 @@ export const CollectionForm = observer(function CollectionForm_({
|
||||
const values = watch();
|
||||
|
||||
// Preload the IconPicker component on mount
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
void IconPicker.preload();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
// If the user hasn't picked an icon yet, go ahead and suggest one based on
|
||||
// the name of the collection. It's the little things sometimes.
|
||||
if (!hasOpenedIconPicker && !collection) {
|
||||
@@ -107,12 +110,12 @@ export const CollectionForm = observer(function CollectionForm_({
|
||||
}
|
||||
}, [collection, hasOpenedIconPicker, setValue, values.name, values.icon]);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setTimeout(() => setFocus("name", { shouldSelect: true }), 100);
|
||||
}, [setFocus]);
|
||||
|
||||
const handleIconChange = React.useCallback(
|
||||
(icon: string, color: string | null) => {
|
||||
const handleIconChange = useCallback(
|
||||
(icon: string, color: string) => {
|
||||
if (icon !== values.icon) {
|
||||
setFocus("name");
|
||||
}
|
||||
@@ -129,7 +132,6 @@ export const CollectionForm = observer(function CollectionForm_({
|
||||
<Trans>
|
||||
Collections are used to group documents and choose permissions
|
||||
</Trans>
|
||||
.
|
||||
</Text>
|
||||
<Flex gap={8}>
|
||||
<Input
|
||||
@@ -140,7 +142,7 @@ export const CollectionForm = observer(function CollectionForm_({
|
||||
maxLength: CollectionValidation.maxNameLength,
|
||||
})}
|
||||
prefix={
|
||||
<React.Suspense fallback={fallbackIcon}>
|
||||
<Suspense fallback={fallbackIcon}>
|
||||
<StyledIconPicker
|
||||
icon={values.icon}
|
||||
color={values.color ?? iconColor}
|
||||
@@ -149,7 +151,7 @@ export const CollectionForm = observer(function CollectionForm_({
|
||||
onOpen={setHasOpenedIconPicker}
|
||||
onChange={handleIconChange}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</Suspense>
|
||||
}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
@@ -186,7 +188,16 @@ export const CollectionForm = observer(function CollectionForm_({
|
||||
note={t(
|
||||
"Allow documents within this collection to be shared publicly on the internet."
|
||||
)}
|
||||
{...register("sharing")}
|
||||
{...createSwitchRegister(register, "sharing")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{team.getPreference(TeamPreference.Commenting) && (
|
||||
<Switch
|
||||
id="commenting"
|
||||
label={t("Commenting")}
|
||||
note={t("Allow commenting on documents within this collection.")}
|
||||
{...createSwitchRegister(register, "commenting")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { runInAction } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import history from "~/utils/history";
|
||||
@@ -14,7 +14,7 @@ export const CollectionNew = observer(function CollectionNew_({
|
||||
onSubmit,
|
||||
}: Props) {
|
||||
const { collections } = useStores();
|
||||
const handleSubmit = React.useCallback(
|
||||
const handleSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
const collection = await collections.save(data);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMatches, KBarResults } from "kbar";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Text from "~/components/Text";
|
||||
import CommandBarItem from "./CommandBarItem";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DocumentIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { createAction } from "~/actions";
|
||||
import { RecentSection } from "~/actions/sections";
|
||||
@@ -10,7 +10,7 @@ import { documentPath } from "~/utils/routeHelpers";
|
||||
const useRecentDocumentActions = (count = 6) => {
|
||||
const { documents, ui } = useStores();
|
||||
|
||||
return React.useMemo(
|
||||
return useMemo(
|
||||
() =>
|
||||
documents.recentlyViewed
|
||||
.filter((document) => document.id !== ui.activeDocumentId)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SettingsIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import { createAction } from "~/actions";
|
||||
import { NavigationSection } from "~/actions/sections";
|
||||
import useSettingsConfig from "~/hooks/useSettingsConfig";
|
||||
@@ -7,7 +7,7 @@ import history from "~/utils/history";
|
||||
|
||||
const useSettingsAction = () => {
|
||||
const config = useSettingsConfig();
|
||||
const actions = React.useMemo(
|
||||
const actions = useMemo(
|
||||
() =>
|
||||
config.map((item) => {
|
||||
const Icon = item.icon;
|
||||
@@ -22,7 +22,7 @@ const useSettingsAction = () => {
|
||||
[config]
|
||||
);
|
||||
|
||||
const navigateToSettings = React.useMemo(
|
||||
const navigateToSettings = useMemo(
|
||||
() =>
|
||||
createAction({
|
||||
id: "settings",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NewDocumentIcon, ShapesIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { createAction } from "~/actions";
|
||||
import {
|
||||
@@ -14,11 +14,11 @@ import { newDocumentPath } from "~/utils/routeHelpers";
|
||||
const useTemplatesAction = () => {
|
||||
const { documents } = useStores();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
void documents.fetchAllTemplates();
|
||||
}, [documents]);
|
||||
|
||||
const actions = React.useMemo(
|
||||
const actions = useMemo(
|
||||
() =>
|
||||
documents.templatesAlphabetical.map((template) =>
|
||||
createAction({
|
||||
@@ -61,7 +61,7 @@ const useTemplatesAction = () => {
|
||||
[documents.templatesAlphabetical]
|
||||
);
|
||||
|
||||
const newFromTemplate = React.useMemo(
|
||||
const newFromTemplate = useMemo(
|
||||
() =>
|
||||
createAction({
|
||||
id: "templates",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import Comment from "~/models/Comment";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { CollectionPermission, NavigationNode } from "@shared/types";
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
useMenuState,
|
||||
MenuButton,
|
||||
MenuItem as BaseMenuItem,
|
||||
MenuStateReturn,
|
||||
@@ -13,6 +12,7 @@ import MenuIconWrapper from "~/components/ContextMenu/MenuIconWrapper";
|
||||
import Flex from "~/components/Flex";
|
||||
import { actionToMenuItem } from "~/actions";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import {
|
||||
Action,
|
||||
ActionContext,
|
||||
@@ -52,7 +52,9 @@ const SubMenu = React.forwardRef(function _Template(
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const menu = useMenuState();
|
||||
const menu = useMenuState({
|
||||
parentId: parentMenuState.baseId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -171,7 +171,9 @@ const InnerContextMenu = (props: InnerContextMenuProps) => {
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
scrollElement && !props.isSubMenu && enableBodyScroll(scrollElement);
|
||||
if (scrollElement && !props.isSubMenu) {
|
||||
enableBodyScroll(scrollElement);
|
||||
}
|
||||
};
|
||||
}, [props.isSubMenu, props.visible]);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ function CopyToClipboard(props: Props, ref: React.Ref<HTMLElement>) {
|
||||
|
||||
const onClick = React.useCallback(
|
||||
(ev: React.MouseEvent<HTMLElement>) => {
|
||||
const elem = React.Children.only(children);
|
||||
const childElem = React.Children.only(children);
|
||||
|
||||
copy(text, {
|
||||
debug: env.ENVIRONMENT !== "production",
|
||||
@@ -24,8 +24,12 @@ function CopyToClipboard(props: Props, ref: React.Ref<HTMLElement>) {
|
||||
|
||||
onCopy?.();
|
||||
|
||||
if (elem && elem.props && typeof elem.props.onClick === "function") {
|
||||
elem.props.onClick(ev);
|
||||
if (
|
||||
childElem &&
|
||||
childElem.props &&
|
||||
typeof childElem.props.onClick === "function"
|
||||
) {
|
||||
childElem.props.onClick(ev);
|
||||
} else {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type Props = {
|
||||
delay?: number;
|
||||
@@ -6,9 +6,9 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function DelayedMount({ delay = 250, children }: Props) {
|
||||
const [isShowing, setShowing] = React.useState(false);
|
||||
const [isShowing, setShowing] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => setShowing(true), delay);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
@@ -12,9 +12,9 @@ export default function DesktopEventHandler() {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const { dialogs } = useStores();
|
||||
const hasDisabledUpdateMessage = React.useRef(false);
|
||||
const hasDisabledUpdateMessage = useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
Desktop.bridge?.redirect((path: string, replace = false) => {
|
||||
if (replace) {
|
||||
history.replace(path);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import Guide from "~/components/Guide";
|
||||
import Modal from "~/components/Modal";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { subDays } from "date-fns";
|
||||
import { m } from "framer-motion";
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseIcon, DocumentIcon, ClockIcon, EyeIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useRef, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
@@ -40,7 +40,7 @@ function DocumentCard(props: Props) {
|
||||
const { collections } = useStores();
|
||||
const theme = useTheme();
|
||||
const { document, pin, canUpdatePin, isDraggable } = props;
|
||||
const pinnedToHome = React.useRef(!pin?.collectionId).current;
|
||||
const pinnedToHome = useRef(!pin?.collectionId).current;
|
||||
const collection = document.collectionId
|
||||
? collections.get(document.collectionId)
|
||||
: undefined;
|
||||
@@ -63,7 +63,7 @@ function DocumentCard(props: Props) {
|
||||
transition,
|
||||
};
|
||||
|
||||
const handleUnpin = React.useCallback(
|
||||
const handleUnpin = useCallback(
|
||||
async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -178,7 +178,7 @@ function DocumentCard(props: Props) {
|
||||
|
||||
const ReadingTime = ({ document }: { document: Document }) => {
|
||||
const { t } = useTranslation();
|
||||
const markdown = React.useMemo(() => document.toMarkdown(), [document]);
|
||||
const markdown = useMemo(() => document.toMarkdown(), [document]);
|
||||
const stats = useTextStats(markdown);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { action, computed, observable } from "mobx";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { createContext, useContext, useMemo, PropsWithChildren } from "react";
|
||||
import { Heading } from "@shared/utils/ProsemirrorHelper";
|
||||
import Document from "~/models/Document";
|
||||
import { Editor } from "~/editor";
|
||||
@@ -64,10 +64,10 @@ class DocumentContext {
|
||||
}
|
||||
}
|
||||
|
||||
const Context = React.createContext<DocumentContext | null>(null);
|
||||
const Context = createContext<DocumentContext | null>(null);
|
||||
|
||||
export const useDocumentContext = () => {
|
||||
const ctx = React.useContext(Context);
|
||||
const ctx = useContext(Context);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
"useDocumentContext must be used within DocumentContextProvider"
|
||||
@@ -79,6 +79,6 @@ export const useDocumentContext = () => {
|
||||
export const DocumentContextProvider = ({
|
||||
children,
|
||||
}: PropsWithChildren<unknown>) => {
|
||||
const context = React.useMemo(() => new DocumentContext(), []);
|
||||
const context = useMemo(() => new DocumentContext(), []);
|
||||
return <Context.Provider value={context}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
@@ -46,20 +46,6 @@ function DocumentCopy({ document, onSubmit }: Props) {
|
||||
return nodes;
|
||||
}, [policies, collectionTrees, document.isTemplate]);
|
||||
|
||||
const handlePublishChange = React.useCallback(
|
||||
(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPublish(ev.target.checked);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleRecursiveChange = React.useCallback(
|
||||
(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRecursive(ev.target.checked);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const copy = async () => {
|
||||
if (!selectedPath) {
|
||||
toast.message(t("Select a location to copy"));
|
||||
@@ -79,7 +65,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
|
||||
|
||||
toast.success(t("Document copied"));
|
||||
onSubmit(result);
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
toast.error(t("Couldn’t copy the document, try again?"));
|
||||
}
|
||||
};
|
||||
@@ -102,7 +88,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
|
||||
label={t("Publish")}
|
||||
labelPosition="right"
|
||||
checked={publish}
|
||||
onChange={handlePublishChange}
|
||||
onChange={setPublish}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
@@ -113,7 +99,7 @@ function DocumentCopy({ document, onSubmit }: Props) {
|
||||
label={t("Include nested documents")}
|
||||
labelPosition="right"
|
||||
checked={recursive}
|
||||
onChange={handleRecursiveChange}
|
||||
onChange={setRecursive}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -60,7 +60,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
|
||||
if (defaultValue) {
|
||||
const node = items.find((item) => item.id === defaultValue);
|
||||
if (node) {
|
||||
return ancestors(node).map((node) => node.id);
|
||||
return ancestors(node).map((ancestorNode) => ancestorNode.id);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
@@ -99,10 +99,10 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
|
||||
}, [searchTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setItemRefs((itemRefs) =>
|
||||
setItemRefs((existingItemRefs) =>
|
||||
map(
|
||||
fill(Array(items.length), 0),
|
||||
(_, i) => itemRefs[i] || React.createRef()
|
||||
(_, i) => existingItemRefs[i] || React.createRef()
|
||||
)
|
||||
);
|
||||
}, [items.length]);
|
||||
@@ -180,7 +180,7 @@ function DocumentExplorer({ onSubmit, onSelect, items, defaultValue }: Props) {
|
||||
);
|
||||
|
||||
// remove children
|
||||
const newNodes = filter(nodes, (node) => !includes(descendantIds, node.id));
|
||||
const newNodes = filter(nodes, (n) => !includes(descendantIds, n.id));
|
||||
const scrollOffset = calculateInitialScrollOffset(newNodes.length);
|
||||
setInitialScrollOffset(scrollOffset);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { TFunction } from "i18next";
|
||||
import { observer } from "mobx-react";
|
||||
import { DoneIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import Document from "~/models/Document";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import compact from "lodash/compact";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { dateLocale, dateToRelative } from "@shared/utils/date";
|
||||
import Document from "~/models/Document";
|
||||
@@ -14,78 +14,86 @@ import useStores from "~/hooks/useStores";
|
||||
|
||||
type Props = {
|
||||
document: Document;
|
||||
isOpen?: boolean;
|
||||
};
|
||||
|
||||
function DocumentViews({ document, isOpen }: Props) {
|
||||
function DocumentViews({ document }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { views, presence } = useStores();
|
||||
const user = useCurrentUser();
|
||||
const locale = dateLocale(user.language);
|
||||
|
||||
const documentPresence = presence.get(document.id);
|
||||
const documentPresenceArray = documentPresence
|
||||
? Array.from(documentPresence.values())
|
||||
: [];
|
||||
const presentIds = documentPresenceArray.map((p) => p.userId);
|
||||
const editingIds = documentPresenceArray
|
||||
.filter((p) => p.isEditing)
|
||||
.map((p) => p.userId);
|
||||
|
||||
// Use Set for O(1) lookups and stable references
|
||||
const presentIds = useMemo(
|
||||
() => new Set(documentPresenceArray.map((p) => p.userId)),
|
||||
[documentPresenceArray]
|
||||
);
|
||||
const editingIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
documentPresenceArray.filter((p) => p.isEditing).map((p) => p.userId)
|
||||
),
|
||||
[documentPresenceArray]
|
||||
);
|
||||
|
||||
// ensure currently present via websocket are always ordered first
|
||||
const documentViews = views.inDocument(document.id);
|
||||
const sortedViews = sortBy(
|
||||
documentViews,
|
||||
(view) => !presentIds.includes(view.userId)
|
||||
const documentViews = useMemo(
|
||||
() => views.inDocument(document.id),
|
||||
[views, document.id]
|
||||
);
|
||||
const users = React.useMemo(
|
||||
const sortedViews = useMemo(
|
||||
() => sortBy(documentViews, (view) => !presentIds.has(view.userId)),
|
||||
[documentViews, presentIds]
|
||||
);
|
||||
const users = useMemo(
|
||||
() => compact(sortedViews.map((v) => v.user)),
|
||||
[sortedViews]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<PaginatedList<User>
|
||||
aria-label={t("Viewers")}
|
||||
items={users}
|
||||
renderItem={(model) => {
|
||||
const view = documentViews.find((v) => v.userId === model.id);
|
||||
const isPresent = presentIds.includes(model.id);
|
||||
const isEditing = editingIds.includes(model.id);
|
||||
const subtitle = isPresent
|
||||
? isEditing
|
||||
? t("Currently editing")
|
||||
: t("Currently viewing")
|
||||
: t("Viewed {{ timeAgo }}", {
|
||||
timeAgo: dateToRelative(
|
||||
view ? Date.parse(view.lastViewedAt) : new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
locale,
|
||||
}
|
||||
),
|
||||
});
|
||||
return (
|
||||
<ListItem
|
||||
key={model.id}
|
||||
title={model.name}
|
||||
subtitle={subtitle}
|
||||
image={
|
||||
<Avatar
|
||||
key={model.id}
|
||||
model={model}
|
||||
size={AvatarSize.Large}
|
||||
/>
|
||||
}
|
||||
border={false}
|
||||
small
|
||||
/>
|
||||
);
|
||||
}}
|
||||
// Memoize renderItem for PaginatedList
|
||||
const renderItem = useCallback(
|
||||
(model: User) => {
|
||||
const view = documentViews.find((v) => v.userId === model.id);
|
||||
const isPresent = presentIds.has(model.id);
|
||||
const isEditing = editingIds.has(model.id);
|
||||
const subtitle = isPresent
|
||||
? isEditing
|
||||
? t("Currently editing")
|
||||
: t("Currently viewing")
|
||||
: t("Viewed {{ timeAgo }}", {
|
||||
timeAgo: dateToRelative(
|
||||
view ? Date.parse(view.lastViewedAt) : new Date(),
|
||||
{
|
||||
addSuffix: true,
|
||||
locale,
|
||||
}
|
||||
),
|
||||
});
|
||||
return (
|
||||
<ListItem
|
||||
key={model.id}
|
||||
title={model.name}
|
||||
subtitle={subtitle}
|
||||
image={
|
||||
<Avatar key={model.id} model={model} size={AvatarSize.Large} />
|
||||
}
|
||||
border={false}
|
||||
small
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
[documentViews, presentIds, editingIds, t, locale]
|
||||
);
|
||||
|
||||
return (
|
||||
<PaginatedList<User>
|
||||
aria-label={t("Viewers")}
|
||||
items={users}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
UserIcon,
|
||||
CrossIcon,
|
||||
} from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import styled, { css } from "styled-components";
|
||||
@@ -48,10 +48,12 @@ export type DocumentEvent = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type Event = { id: string; actorId: string; createdAt: string } & (
|
||||
| RevisionEvent
|
||||
| DocumentEvent
|
||||
);
|
||||
export type Event = {
|
||||
id: string;
|
||||
actorId: string;
|
||||
createdAt: string;
|
||||
deletedAt?: string;
|
||||
} & (RevisionEvent | DocumentEvent);
|
||||
|
||||
type Props = {
|
||||
document: Document;
|
||||
@@ -65,7 +67,7 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
|
||||
const user = "userId" in event ? users.get(event.userId) : undefined;
|
||||
const location = useLocation();
|
||||
const sidebarContext = useLocationSidebarContext();
|
||||
const revisionLoadedRef = React.useRef(false);
|
||||
const revisionLoadedRef = useRef(false);
|
||||
const opts = {
|
||||
userName: actor?.name,
|
||||
};
|
||||
@@ -74,7 +76,7 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
|
||||
event.id === RevisionHelper.latestId(document.id);
|
||||
let meta, icon, to: LocationDescriptor | undefined;
|
||||
|
||||
const ref = React.useRef<HTMLAnchorElement>(null);
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
// the time component tends to steal focus when clicked
|
||||
// ...so forward the focus back to the parent item
|
||||
const handleTimeClick = () => {
|
||||
@@ -85,6 +87,7 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
|
||||
if (
|
||||
!document.isDeleted &&
|
||||
event.name === "revisions.create" &&
|
||||
!event.deletedAt &&
|
||||
!isDerivedFromDocument &&
|
||||
!revisionLoadedRef.current
|
||||
) {
|
||||
@@ -95,24 +98,31 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
|
||||
|
||||
switch (event.name) {
|
||||
case "revisions.create":
|
||||
icon = <EditIcon size={16} />;
|
||||
meta = event.latest ? (
|
||||
<>
|
||||
{t("Current version")} · {actor?.name}
|
||||
</>
|
||||
) : (
|
||||
t("{{userName}} edited", opts)
|
||||
);
|
||||
to = {
|
||||
pathname: documentHistoryPath(
|
||||
document,
|
||||
isDerivedFromDocument ? "latest" : event.id
|
||||
),
|
||||
state: {
|
||||
sidebarContext,
|
||||
retainScrollPosition: true,
|
||||
},
|
||||
};
|
||||
{
|
||||
if (event.deletedAt) {
|
||||
icon = <TrashIcon />;
|
||||
meta = t("Revision deleted");
|
||||
} else {
|
||||
icon = <EditIcon size={16} />;
|
||||
meta = event.latest ? (
|
||||
<>
|
||||
{t("Current version")} · {actor?.name}
|
||||
</>
|
||||
) : (
|
||||
t("{{userName}} edited", opts)
|
||||
);
|
||||
to = {
|
||||
pathname: documentHistoryPath(
|
||||
document,
|
||||
isDerivedFromDocument ? "latest" : event.id
|
||||
),
|
||||
state: {
|
||||
sidebarContext,
|
||||
retainScrollPosition: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "documents.archive":
|
||||
@@ -181,7 +191,7 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
|
||||
to = undefined;
|
||||
}
|
||||
|
||||
return event.name === "revisions.create" ? (
|
||||
return event.name === "revisions.create" && !event.deletedAt ? (
|
||||
<RevisionItem
|
||||
small
|
||||
exact
|
||||
@@ -218,7 +228,12 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
|
||||
</IconWrapper>
|
||||
<Text size="xsmall" type="secondary">
|
||||
{meta} ·{" "}
|
||||
<Time dateTime={event.createdAt} relative shorten addSuffix />
|
||||
<Time
|
||||
dateTime={event.deletedAt ?? event.createdAt}
|
||||
relative
|
||||
shorten
|
||||
addSuffix
|
||||
/>
|
||||
</Text>
|
||||
</EventItem>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { fadeIn } from "~/styles/animations";
|
||||
|
||||
@@ -21,7 +21,7 @@ type Props = {
|
||||
* Wraps children in a <Fade> if loading is true on mount.
|
||||
*/
|
||||
export const ConditionalFade = ({ animate, children }: Props) => {
|
||||
const [isAnimated] = React.useState(animate);
|
||||
const [isAnimated] = useState(animate);
|
||||
return isAnimated ? <Fade>{children}</Fade> : <>{children}</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import deburr from "lodash/deburr";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMenuState, MenuButton } from "reakit/Menu";
|
||||
import { MenuButton } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import type { FetchPageParams } from "~/stores/base/Store";
|
||||
@@ -9,6 +9,7 @@ import Button, { Inner } from "~/components/Button";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import MenuItem from "~/components/ContextMenu/MenuItem";
|
||||
import Text from "~/components/Text";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import Input, { NativeInput, Outline } from "./Input";
|
||||
import PaginatedList, { PaginatedItem } from "./PaginatedList";
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Empty from "~/components/Empty";
|
||||
import Fade from "~/components/Fade";
|
||||
|
||||
@@ -36,8 +36,8 @@ const Guide: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<DialogBackdrop {...dialog}>
|
||||
{(props) => (
|
||||
<Backdrop {...props}>
|
||||
{(backdropProps) => (
|
||||
<Backdrop {...backdropProps}>
|
||||
<Dialog
|
||||
{...dialog}
|
||||
aria-label={title}
|
||||
@@ -45,8 +45,8 @@ const Guide: React.FC<Props> = ({
|
||||
hideOnEsc
|
||||
hide={onRequestClose}
|
||||
>
|
||||
{(props) => (
|
||||
<Scene {...props} {...rest}>
|
||||
{(dialogProps) => (
|
||||
<Scene {...dialogProps} {...rest}>
|
||||
<Content>
|
||||
{title && <Header>{title}</Header>}
|
||||
{children}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BackIcon } from "outline-icons";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { breakpoints, s, hover } from "@shared/styles";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
@@ -193,7 +193,7 @@ const SwitcherButton = styled(NudeButton)<{ panel: Panel }>`
|
||||
`;
|
||||
|
||||
const LargeMobileBuiltinColors = styled(BuiltinColors)`
|
||||
max-width: 380px;
|
||||
max-width: 400px;
|
||||
padding-right: 8px;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import concat from "lodash/concat";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { EmojiCategory, EmojiSkinTone, IconType } from "@shared/types";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chunk from "lodash/chunk";
|
||||
import compact from "lodash/compact";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { IconType } from "@shared/types";
|
||||
import { IconLibrary } from "@shared/utils/IconLibrary";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { IconType } from "@shared/types";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useMemo, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Menu, MenuButton, MenuItem, useMenuState } from "reakit";
|
||||
import { Menu, MenuButton, MenuItem } from "reakit";
|
||||
import styled from "styled-components";
|
||||
import { depths, s, hover } from "@shared/styles";
|
||||
import { EmojiSkinTone } from "@shared/types";
|
||||
@@ -8,6 +8,7 @@ import { getEmojiVariants } from "@shared/utils/emoji";
|
||||
import { Emoji } from "~/components/Emoji";
|
||||
import Flex from "~/components/Flex";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import { IconButton } from "./IconButton";
|
||||
|
||||
const SkinTonePicker = ({
|
||||
@@ -19,16 +20,13 @@ const SkinTonePicker = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handEmojiVariants = React.useMemo(
|
||||
() => getEmojiVariants({ id: "hand" }),
|
||||
[]
|
||||
);
|
||||
const handEmojiVariants = useMemo(() => getEmojiVariants({ id: "hand" }), []);
|
||||
|
||||
const menu = useMenuState({
|
||||
placement: "bottom-end",
|
||||
});
|
||||
|
||||
const handleSkinClick = React.useCallback(
|
||||
const handleSkinClick = useCallback(
|
||||
(emojiSkin) => {
|
||||
menu.hide();
|
||||
onChange(emojiSkin);
|
||||
@@ -36,7 +34,7 @@ const SkinTonePicker = ({
|
||||
[menu, onChange]
|
||||
);
|
||||
|
||||
const menuItems = React.useMemo(
|
||||
const menuItems = useMemo(
|
||||
() =>
|
||||
Object.entries(handEmojiVariants).map(([eskin, emoji]) => (
|
||||
<MenuItem {...menu} key={emoji.value}>
|
||||
|
||||
+196
-138
@@ -1,27 +1,20 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { SmileyIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
PopoverDisclosure,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
usePopoverState,
|
||||
useTabState,
|
||||
} from "reakit";
|
||||
import styled, { css } from "styled-components";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { s, hover } from "@shared/styles";
|
||||
import { s, hover, depths } from "@shared/styles";
|
||||
import theme from "@shared/styles/theme";
|
||||
import { IconType } from "@shared/types";
|
||||
import { determineIconType } from "@shared/utils/icon";
|
||||
import Flex from "~/components/Flex";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Popover from "~/components/Popover";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
import useOnClickOutside from "~/hooks/useOnClickOutside";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useWindowSize from "~/hooks/useWindowSize";
|
||||
import { fadeAndScaleIn } from "~/styles/animations";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../primitives/Drawer";
|
||||
import EmojiPanel from "./components/EmojiPanel";
|
||||
import IconPanel from "./components/IconPanel";
|
||||
import { PopoverButton } from "./components/PopoverButton";
|
||||
@@ -31,6 +24,8 @@ const TAB_NAMES = {
|
||||
Emoji: "emoji",
|
||||
} as const;
|
||||
|
||||
type TabName = (typeof TAB_NAMES)[keyof typeof TAB_NAMES];
|
||||
|
||||
const POPOVER_WIDTH = 408;
|
||||
|
||||
type Props = {
|
||||
@@ -67,9 +62,9 @@ const IconPicker = ({
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const isMobile = useMobile();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [chosenColor, setChosenColor] = React.useState(color);
|
||||
const contentRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const iconType = determineIconType(icon);
|
||||
const defaultTab = React.useMemo(
|
||||
@@ -78,32 +73,40 @@ const IconPicker = ({
|
||||
[iconType]
|
||||
);
|
||||
|
||||
const popover = usePopoverState({
|
||||
placement: popoverPosition,
|
||||
modal: true,
|
||||
unstable_offset: [0, 0],
|
||||
});
|
||||
const { hide, show, visible } = popover;
|
||||
const tab = useTabState({ selectedId: defaultTab });
|
||||
const previouslyVisible = usePrevious(popover.visible);
|
||||
const [activeTab, setActiveTab] = React.useState<TabName>(defaultTab);
|
||||
|
||||
const popoverWidth = isMobile ? windowWidth : POPOVER_WIDTH;
|
||||
// In mobile, popover is absolutely positioned to leave 8px on both sides.
|
||||
const panelWidth = isMobile ? windowWidth - 16 : popoverWidth;
|
||||
|
||||
const handleTabChange = React.useCallback((value: string) => {
|
||||
setActiveTab(value as TabName);
|
||||
}, []);
|
||||
|
||||
const resetDefaultTab = React.useCallback(() => {
|
||||
tab.select(defaultTab);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
setActiveTab(defaultTab);
|
||||
}, [defaultTab]);
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(isOpen: boolean) => {
|
||||
setOpen(isOpen);
|
||||
if (isOpen) {
|
||||
onOpen?.();
|
||||
} else {
|
||||
onClose?.();
|
||||
setQuery("");
|
||||
resetDefaultTab();
|
||||
}
|
||||
},
|
||||
[onOpen, onClose, resetDefaultTab]
|
||||
);
|
||||
|
||||
const handleIconChange = React.useCallback(
|
||||
(ic: string) => {
|
||||
hide();
|
||||
setOpen(false);
|
||||
const icType = determineIconType(ic);
|
||||
const finalColor = icType === IconType.SVG ? chosenColor : null;
|
||||
onChange(ic, finalColor);
|
||||
},
|
||||
[hide, onChange, chosenColor]
|
||||
[onChange, chosenColor]
|
||||
);
|
||||
|
||||
const handleIconColorChange = React.useCallback(
|
||||
@@ -111,7 +114,6 @@ const IconPicker = ({
|
||||
setChosenColor(c);
|
||||
|
||||
const icType = determineIconType(icon);
|
||||
// Outline icon set; propagate color change
|
||||
if (icType === IconType.SVG) {
|
||||
onChange(icon, c);
|
||||
}
|
||||
@@ -120,60 +122,40 @@ const IconPicker = ({
|
||||
);
|
||||
|
||||
const handleIconRemove = React.useCallback(() => {
|
||||
hide();
|
||||
setOpen(false);
|
||||
onChange(null, null);
|
||||
}, [hide, onChange]);
|
||||
}, [setOpen, onChange]);
|
||||
|
||||
const handlePopoverButtonClick = React.useCallback(
|
||||
(ev: React.MouseEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (visible) {
|
||||
hide();
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
},
|
||||
[hide, show, visible]
|
||||
const PickerContent = (
|
||||
<Content
|
||||
open={open}
|
||||
activeTab={activeTab}
|
||||
iconColor={chosenColor}
|
||||
iconInitial={initial ?? ""}
|
||||
query={query}
|
||||
panelWidth={popoverWidth}
|
||||
allowDelete={!!(allowDelete && icon)}
|
||||
onTabChange={handleTabChange}
|
||||
onQueryChange={setQuery}
|
||||
onIconChange={handleIconChange}
|
||||
onIconColorChange={handleIconColorChange}
|
||||
onIconRemove={handleIconRemove}
|
||||
/>
|
||||
);
|
||||
|
||||
// Popover open effect
|
||||
// Update selected tab when default tab changes
|
||||
React.useEffect(() => {
|
||||
if (visible && !previouslyVisible) {
|
||||
onOpen?.();
|
||||
} else if (!visible && previouslyVisible) {
|
||||
onClose?.();
|
||||
setQuery("");
|
||||
resetDefaultTab();
|
||||
}
|
||||
}, [visible, previouslyVisible, onOpen, onClose, resetDefaultTab]);
|
||||
setActiveTab(defaultTab);
|
||||
}, [defaultTab]);
|
||||
|
||||
// Custom click outside handling rather than using `hideOnClickOutside` from reakit so that we can
|
||||
// prevent event bubbling.
|
||||
useOnClickOutside(
|
||||
contentRef,
|
||||
(event) => {
|
||||
if (
|
||||
popover.visible &&
|
||||
!popover.unstable_disclosureRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
popover.hide();
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopoverDisclosure {...popover}>
|
||||
{(props) => (
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild>
|
||||
<PopoverButton
|
||||
{...props}
|
||||
aria-label={t("Show menu")}
|
||||
className={className}
|
||||
size={size}
|
||||
onClick={handlePopoverButtonClick}
|
||||
$borderOnHover={borderOnHover}
|
||||
>
|
||||
{children ? (
|
||||
@@ -184,71 +166,124 @@ const IconPicker = ({
|
||||
<StyledSmileyIcon color={theme.placeholder} size={size} />
|
||||
)}
|
||||
</PopoverButton>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent aria-label={t("Icon Picker")}>
|
||||
{PickerContent}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover.Root open={open} onOpenChange={handleOpenChange} modal={true}>
|
||||
<Popover.Trigger asChild>
|
||||
<PopoverButton
|
||||
aria-label={t("Show menu")}
|
||||
className={className}
|
||||
size={size}
|
||||
$borderOnHover={borderOnHover}
|
||||
>
|
||||
{children ? (
|
||||
children
|
||||
) : iconType && icon ? (
|
||||
<Icon value={icon} color={color} size={size} initial={initial} />
|
||||
) : (
|
||||
<StyledSmileyIcon color={theme.placeholder} size={size} />
|
||||
)}
|
||||
</PopoverButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<StyledPopoverContent
|
||||
side={popoverPosition === "right" ? "right" : "bottom"}
|
||||
align={popoverPosition === "bottom-start" ? "start" : "center"}
|
||||
sideOffset={0}
|
||||
width={popoverWidth}
|
||||
aria-label={t("Icon Picker")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{PickerContent}
|
||||
</StyledPopoverContent>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
open: boolean;
|
||||
activeTab: TabName;
|
||||
query: string;
|
||||
iconColor: string;
|
||||
iconInitial: string;
|
||||
panelWidth: number;
|
||||
allowDelete: boolean;
|
||||
onTabChange: (tab: string) => void;
|
||||
onQueryChange: (query: string) => void;
|
||||
onIconChange: (icon: string) => void;
|
||||
onIconColorChange: (color: string) => void;
|
||||
onIconRemove: () => void;
|
||||
};
|
||||
|
||||
const Content = ({
|
||||
open,
|
||||
activeTab,
|
||||
iconColor,
|
||||
iconInitial,
|
||||
query,
|
||||
panelWidth,
|
||||
allowDelete,
|
||||
onTabChange,
|
||||
onQueryChange,
|
||||
onIconChange,
|
||||
onIconColorChange,
|
||||
onIconRemove,
|
||||
}: ContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tabs.Root value={activeTab} onValueChange={onTabChange}>
|
||||
<TabActionsWrapper justify="space-between" align="center">
|
||||
<Tabs.List>
|
||||
<StyledTab
|
||||
value={TAB_NAMES["Icon"]}
|
||||
aria-label={t("Icons")}
|
||||
$active={activeTab === TAB_NAMES["Icon"]}
|
||||
>
|
||||
{t("Icons")}
|
||||
</StyledTab>
|
||||
<StyledTab
|
||||
value={TAB_NAMES["Emoji"]}
|
||||
aria-label={t("Emojis")}
|
||||
$active={activeTab === TAB_NAMES["Emoji"]}
|
||||
>
|
||||
{t("Emojis")}
|
||||
</StyledTab>
|
||||
</Tabs.List>
|
||||
{allowDelete && (
|
||||
<RemoveButton onClick={onIconRemove}>{t("Remove")}</RemoveButton>
|
||||
)}
|
||||
</PopoverDisclosure>
|
||||
<Popover
|
||||
{...popover}
|
||||
ref={contentRef}
|
||||
width={popoverWidth}
|
||||
shrink
|
||||
aria-label={t("Icon Picker")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
hideOnClickOutside={false}
|
||||
>
|
||||
<>
|
||||
<TabActionsWrapper justify="space-between" align="center">
|
||||
<TabList {...tab}>
|
||||
<StyledTab
|
||||
{...tab}
|
||||
id={TAB_NAMES["Icon"]}
|
||||
aria-label={t("Icons")}
|
||||
$active={tab.selectedId === TAB_NAMES["Icon"]}
|
||||
>
|
||||
{t("Icons")}
|
||||
</StyledTab>
|
||||
<StyledTab
|
||||
{...tab}
|
||||
id={TAB_NAMES["Emoji"]}
|
||||
aria-label={t("Emojis")}
|
||||
$active={tab.selectedId === TAB_NAMES["Emoji"]}
|
||||
>
|
||||
{t("Emojis")}
|
||||
</StyledTab>
|
||||
</TabList>
|
||||
{allowDelete && icon && (
|
||||
<RemoveButton onClick={handleIconRemove}>
|
||||
{t("Remove")}
|
||||
</RemoveButton>
|
||||
)}
|
||||
</TabActionsWrapper>
|
||||
<StyledTabPanel {...tab}>
|
||||
<IconPanel
|
||||
panelWidth={panelWidth}
|
||||
initial={initial ?? "?"}
|
||||
color={chosenColor}
|
||||
query={query}
|
||||
panelActive={
|
||||
popover.visible && tab.selectedId === TAB_NAMES["Icon"]
|
||||
}
|
||||
onIconChange={handleIconChange}
|
||||
onColorChange={handleIconColorChange}
|
||||
onQueryChange={setQuery}
|
||||
/>
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel {...tab}>
|
||||
<EmojiPanel
|
||||
panelWidth={panelWidth}
|
||||
query={query}
|
||||
panelActive={
|
||||
popover.visible && tab.selectedId === TAB_NAMES["Emoji"]
|
||||
}
|
||||
onEmojiChange={handleIconChange}
|
||||
onQueryChange={setQuery}
|
||||
/>
|
||||
</StyledTabPanel>
|
||||
</>
|
||||
</Popover>
|
||||
</>
|
||||
</TabActionsWrapper>
|
||||
<StyledTabContent value={TAB_NAMES["Icon"]}>
|
||||
<IconPanel
|
||||
panelWidth={panelWidth}
|
||||
initial={iconInitial}
|
||||
color={iconColor}
|
||||
query={query}
|
||||
panelActive={open && activeTab === TAB_NAMES["Icon"]}
|
||||
onIconChange={onIconChange}
|
||||
onColorChange={onIconColorChange}
|
||||
onQueryChange={onQueryChange}
|
||||
/>
|
||||
</StyledTabContent>
|
||||
<StyledTabContent value={TAB_NAMES["Emoji"]}>
|
||||
<EmojiPanel
|
||||
panelWidth={panelWidth}
|
||||
query={query}
|
||||
panelActive={open && activeTab === TAB_NAMES["Emoji"]}
|
||||
onEmojiChange={onIconChange}
|
||||
onQueryChange={onQueryChange}
|
||||
/>
|
||||
</StyledTabContent>
|
||||
</Tabs.Root>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -277,7 +312,7 @@ const TabActionsWrapper = styled(Flex)`
|
||||
border-bottom: 1px solid ${s("inputBorder")};
|
||||
`;
|
||||
|
||||
const StyledTab = styled(Tab)<{ $active: boolean }>`
|
||||
const StyledTab = styled(Tabs.Trigger)<{ $active: boolean }>`
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
@@ -308,9 +343,32 @@ const StyledTab = styled(Tab)<{ $active: boolean }>`
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledTabPanel = styled(TabPanel)`
|
||||
const StyledTabContent = styled(Tabs.Content)`
|
||||
height: 410px;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const StyledPopoverContent = styled(Popover.Content)<{ width: number }>`
|
||||
animation: ${fadeAndScaleIn} 200ms ease;
|
||||
transform-origin: var(--radix-popover-content-transform-origin);
|
||||
background: ${s("menuBackground")};
|
||||
border-radius: 6px;
|
||||
padding: 6px 0;
|
||||
max-height: 75vh;
|
||||
box-shadow: ${s("menuShadow")};
|
||||
z-index: ${depths.modal};
|
||||
width: ${(props) => props.width}px;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
position: fixed;
|
||||
z-index: ${depths.menu};
|
||||
top: 50px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
width: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(IconPicker);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
/** The size of the icon, 24px is default to match standard icons */
|
||||
size?: number;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { CollectionIcon, PrivateCollectionIcon } from "outline-icons";
|
||||
import { getLuminance } from "polished";
|
||||
import * as React from "react";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { colorPalette } from "@shared/utils/collections";
|
||||
import Collection from "~/models/Collection";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
/** The size of the icon, 24px is default to match standard icons */
|
||||
size?: number;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from "react";
|
||||
|
||||
export function LanguageIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
/** The size of the icon, 24px is default to match standard icons */
|
||||
size?: number;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
/** The size of the icon, 24px is default to match standard icons */
|
||||
size?: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
|
||||
import * as React from "react";
|
||||
import { mergeRefs } from "react-merge-refs";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { s, ellipsis } from "@shared/styles";
|
||||
@@ -221,7 +221,7 @@ function Input(
|
||||
<label>
|
||||
{label &&
|
||||
(labelHidden ? (
|
||||
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
|
||||
<VisuallyHidden.Root>{wrappedLabel}</VisuallyHidden.Root>
|
||||
) : (
|
||||
wrappedLabel
|
||||
))}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import { MenuButton } from "reakit/Menu";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { useMenuState } from "~/hooks/useMenuState";
|
||||
import lazyWithRetry from "~/utils/lazyWithRetry";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import DelayedMount from "./DelayedMount";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
} from "@renderlesskit/react";
|
||||
import { CheckmarkIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { VisuallyHidden } from "reakit/VisuallyHidden";
|
||||
import styled, { css } from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import Button, { Props as ButtonProps, Inner } from "~/components/Button";
|
||||
@@ -213,7 +213,7 @@ const InputSelect = (props: Props, ref: React.RefObject<InputSelectRef>) => {
|
||||
<Wrapper short={short}>
|
||||
{label &&
|
||||
(labelHidden ? (
|
||||
<VisuallyHidden>{wrappedLabel}</VisuallyHidden>
|
||||
<VisuallyHidden.Root>{wrappedLabel}</VisuallyHidden.Root>
|
||||
) : (
|
||||
wrappedLabel
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
|
||||
import { transparentize } from "polished";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Text from "~/components/Text";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { m } from "framer-motion";
|
||||
import find from "lodash/find";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { languages, languageOptions } from "@shared/i18n";
|
||||
|
||||
+29
-27
@@ -38,39 +38,41 @@ const Layout = React.forwardRef(function Layout_(
|
||||
});
|
||||
|
||||
return (
|
||||
<Container column auto ref={ref}>
|
||||
<Helmet>
|
||||
<title>{title ? title : env.APP_NAME}</title>
|
||||
</Helmet>
|
||||
<MenuProvider>
|
||||
<Container column auto ref={ref}>
|
||||
<Helmet>
|
||||
<title>{title ? title : env.APP_NAME}</title>
|
||||
</Helmet>
|
||||
|
||||
<SkipNavLink />
|
||||
<SkipNavLink />
|
||||
|
||||
{ui.progressBarVisible && <LoadingIndicatorBar />}
|
||||
{ui.progressBarVisible && <LoadingIndicatorBar />}
|
||||
|
||||
<Container auto>
|
||||
<MenuProvider>{sidebar}</MenuProvider>
|
||||
<Container auto>
|
||||
<MenuProvider>{sidebar}</MenuProvider>
|
||||
|
||||
<SkipNavContent />
|
||||
<Content
|
||||
auto
|
||||
justify="center"
|
||||
$isResizing={ui.sidebarIsResizing}
|
||||
$sidebarCollapsed={sidebarCollapsed}
|
||||
$hasSidebar={!!sidebar}
|
||||
style={
|
||||
sidebarCollapsed
|
||||
? undefined
|
||||
: {
|
||||
marginLeft: `${ui.sidebarWidth}px`,
|
||||
}
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Content>
|
||||
<SkipNavContent />
|
||||
<Content
|
||||
auto
|
||||
justify="center"
|
||||
$isResizing={ui.sidebarIsResizing}
|
||||
$sidebarCollapsed={sidebarCollapsed}
|
||||
$hasSidebar={!!sidebar}
|
||||
style={
|
||||
sidebarCollapsed
|
||||
? undefined
|
||||
: {
|
||||
marginLeft: `${ui.sidebarWidth}px`,
|
||||
}
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Content>
|
||||
|
||||
{sidebarRight}
|
||||
{sidebarRight}
|
||||
</Container>
|
||||
</Container>
|
||||
</Container>
|
||||
</MenuProvider>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DisconnectedIcon, WarningIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
|
||||
@@ -33,7 +33,7 @@ export type Props = Omit<React.HTMLAttributes<HTMLAnchorElement>, "title"> & {
|
||||
small?: boolean;
|
||||
/** Whether to enable keyboard navigation */
|
||||
keyboardNavigation?: boolean;
|
||||
ellipsis?: boolean;
|
||||
enableEllipsis?: boolean;
|
||||
};
|
||||
|
||||
const ListItem = (
|
||||
@@ -46,7 +46,7 @@ const ListItem = (
|
||||
border,
|
||||
to,
|
||||
keyboardNavigation,
|
||||
ellipsis,
|
||||
enableEllipsis,
|
||||
...rest
|
||||
}: Props,
|
||||
ref: React.RefObject<HTMLAnchorElement>
|
||||
@@ -85,7 +85,7 @@ const ListItem = (
|
||||
column={!compact}
|
||||
$selected={selected}
|
||||
>
|
||||
<Heading $small={small} $ellipsis={ellipsis}>
|
||||
<Heading $small={small} $ellipsis={enableEllipsis}>
|
||||
{title}
|
||||
</Heading>
|
||||
{subtitle && (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import times from "lodash/times";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Fade from "~/components/Fade";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
function LoadingIndicator() {
|
||||
const { ui } = useStores();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
ui.enableProgressBar();
|
||||
return () => ui.disableProgressBar();
|
||||
}, [ui]);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import { depths, s } from "@shared/styles";
|
||||
|
||||
|
||||
+80
-75
@@ -1,9 +1,9 @@
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseIcon, BackIcon } from "outline-icons";
|
||||
import { transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialog, DialogBackdrop, useDialogState } from "reakit/Dialog";
|
||||
import styled, { DefaultTheme } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { depths, s } from "@shared/styles";
|
||||
@@ -37,9 +37,6 @@ const Modal: React.FC<Props> = ({
|
||||
style,
|
||||
onRequestClose,
|
||||
}: Props) => {
|
||||
const dialog = useDialogState({
|
||||
animated: 250,
|
||||
});
|
||||
const [depth, setDepth] = React.useState(0);
|
||||
const wasOpen = usePrevious(isOpen);
|
||||
const isMobile = useMobile();
|
||||
@@ -48,14 +45,12 @@ const Modal: React.FC<Props> = ({
|
||||
React.useEffect(() => {
|
||||
if (!wasOpen && isOpen) {
|
||||
setDepth(openModals++);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
if (wasOpen && !isOpen) {
|
||||
setDepth(openModals--);
|
||||
dialog.hide();
|
||||
}
|
||||
}, [dialog, wasOpen, isOpen]);
|
||||
}, [wasOpen, isOpen]);
|
||||
|
||||
useUnmount(() => {
|
||||
if (isOpen) {
|
||||
@@ -68,78 +63,75 @@ const Modal: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogBackdrop {...dialog}>
|
||||
{(props) => (
|
||||
<Backdrop $fullscreen={fullscreen} {...props}>
|
||||
<Dialog
|
||||
{...dialog}
|
||||
aria-label={typeof title === "string" ? title : undefined}
|
||||
preventBodyScroll
|
||||
hideOnEsc
|
||||
hideOnClickOutside={!fullscreen}
|
||||
hide={onRequestClose}
|
||||
<Dialog.Root
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => !open && onRequestClose()}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<StyledOverlay $fullscreen={fullscreen}>
|
||||
<StyledContent
|
||||
onEscapeKeyDown={onRequestClose}
|
||||
onPointerDownOutside={fullscreen ? undefined : onRequestClose}
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
{(props) =>
|
||||
fullscreen || isMobile ? (
|
||||
<Fullscreen
|
||||
$nested={!!depth}
|
||||
style={
|
||||
isMobile
|
||||
? undefined
|
||||
: {
|
||||
marginLeft: `${depth * 12}px`,
|
||||
}
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<Content>
|
||||
<Centered onClick={(ev) => ev.stopPropagation()} column>
|
||||
{title && (
|
||||
<Text size="xlarge" weight="bold">
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</Centered>
|
||||
</Content>
|
||||
<Close onClick={onRequestClose}>
|
||||
<CloseIcon size={32} />
|
||||
</Close>
|
||||
<Back onClick={onRequestClose}>
|
||||
<BackIcon size={32} />
|
||||
<Text>{t("Back")} </Text>
|
||||
</Back>
|
||||
</Fullscreen>
|
||||
) : (
|
||||
<Small {...props}>
|
||||
<Centered
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
// maxHeight needed for proper overflow behavior in Safari
|
||||
style={{ maxHeight: "65vh" }}
|
||||
column
|
||||
reverse
|
||||
>
|
||||
<SmallContent style={style} shadow>
|
||||
<ErrorBoundary component="div">{children}</ErrorBoundary>
|
||||
</SmallContent>
|
||||
<Header>
|
||||
{title && <Text size="large">{title}</Text>}
|
||||
<NudeButton onClick={onRequestClose}>
|
||||
<CloseIcon />
|
||||
</NudeButton>
|
||||
</Header>
|
||||
{fullscreen || isMobile ? (
|
||||
<Fullscreen
|
||||
$nested={!!depth}
|
||||
style={
|
||||
isMobile
|
||||
? undefined
|
||||
: {
|
||||
marginLeft: `${depth * 12}px`,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Content>
|
||||
<Centered onClick={(ev) => ev.stopPropagation()} column>
|
||||
{title && (
|
||||
<Text size="xlarge" weight="bold">
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</Centered>
|
||||
</Small>
|
||||
)
|
||||
}
|
||||
</Dialog>
|
||||
</Backdrop>
|
||||
)}
|
||||
</DialogBackdrop>
|
||||
</Content>
|
||||
<Close onClick={onRequestClose}>
|
||||
<CloseIcon size={32} />
|
||||
</Close>
|
||||
<Back onClick={onRequestClose}>
|
||||
<BackIcon size={32} />
|
||||
<Text>{t("Back")} </Text>
|
||||
</Back>
|
||||
</Fullscreen>
|
||||
) : (
|
||||
<Small>
|
||||
<Centered
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
// maxHeight needed for proper overflow behavior in Safari
|
||||
style={{ maxHeight: "65vh" }}
|
||||
column
|
||||
reverse
|
||||
>
|
||||
<SmallContent style={style} shadow>
|
||||
<ErrorBoundary component="div">{children}</ErrorBoundary>
|
||||
</SmallContent>
|
||||
<Header>
|
||||
{title && <Text size="large">{title}</Text>}
|
||||
<NudeButton onClick={onRequestClose}>
|
||||
<CloseIcon />
|
||||
</NudeButton>
|
||||
</Header>
|
||||
</Centered>
|
||||
</Small>
|
||||
)}
|
||||
</StyledContent>
|
||||
</StyledOverlay>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Backdrop = styled(Flex)<{ $fullscreen?: boolean }>`
|
||||
const StyledOverlay = styled(Dialog.Overlay)<{ $fullscreen?: boolean }>`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -153,11 +145,24 @@ const Backdrop = styled(Flex)<{ $fullscreen?: boolean }>`
|
||||
transition: opacity 50ms ease-in-out;
|
||||
opacity: 0;
|
||||
|
||||
&[data-enter] {
|
||||
&[data-state="open"] {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContent = styled(Dialog.Content)`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: ${depths.modal};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
outline: none;
|
||||
`;
|
||||
|
||||
type FullscreenProps = {
|
||||
$nested: boolean;
|
||||
theme: DefaultTheme;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import Flex from "./Flex";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { SubscribeIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { s, hover, truncateMultiline } from "@shared/styles";
|
||||
import Notification from "~/models/Notification";
|
||||
import CommentEditor from "~/scenes/Document/components/CommentEditor";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { Avatar, AvatarSize } from "../Avatar";
|
||||
import { Avatar, AvatarSize, AvatarVariant } from "../Avatar";
|
||||
import Flex from "../Flex";
|
||||
import Text from "../Text";
|
||||
import Time from "../Time";
|
||||
@@ -41,7 +41,7 @@ function NotificationListItem({ notification, onNavigate }: Props) {
|
||||
return (
|
||||
<StyledLink to={notification.path ?? ""} onClick={handleClick}>
|
||||
<Container gap={8} $unread={!notification.viewedAt}>
|
||||
<StyledAvatar model={notification.actor} size={AvatarSize.Large} />
|
||||
<StyledAvatar model={notification.actor} />
|
||||
<Flex column>
|
||||
<Text as="div" size="small">
|
||||
<Text weight="bold">
|
||||
@@ -79,7 +79,10 @@ const StyledCommentEditor = styled(CommentEditor)`
|
||||
${truncateMultiline(3)}
|
||||
`;
|
||||
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
const StyledAvatar = styled(Avatar).attrs({
|
||||
variant: AvatarVariant.Round,
|
||||
size: AvatarSize.Medium,
|
||||
})`
|
||||
margin-top: 4px;
|
||||
`;
|
||||
|
||||
|
||||
@@ -23,15 +23,13 @@ import NotificationListItem from "./NotificationListItem";
|
||||
type Props = {
|
||||
/** Callback when the notification panel wants to close. */
|
||||
onRequestClose: () => void;
|
||||
/** Whether the panel is open or not. */
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A panel containing a list of notifications and controls to manage them.
|
||||
*/
|
||||
function Notifications(
|
||||
{ onRequestClose, isOpen }: Props,
|
||||
{ onRequestClose }: Props,
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
const context = useActionContext();
|
||||
@@ -82,7 +80,7 @@ function Notifications(
|
||||
<PaginatedList<Notification>
|
||||
fetch={notifications.fetchPage}
|
||||
options={{ archived: false }}
|
||||
items={isOpen ? notifications.orderedData : undefined}
|
||||
items={notifications.orderedData}
|
||||
renderItem={(item) => (
|
||||
<NotificationListItem
|
||||
key={item.id}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
||||
import styled from "styled-components";
|
||||
import { depths } from "@shared/styles";
|
||||
import Popover from "~/components/Popover";
|
||||
import { depths, s } from "@shared/styles";
|
||||
import { fadeAndSlideUp } from "~/styles/animations";
|
||||
import Notifications from "./Notifications";
|
||||
|
||||
type Props = {
|
||||
@@ -14,44 +15,71 @@ type Props = {
|
||||
const NotificationsPopover: React.FC = ({ children }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const scrollableRef = React.useRef<HTMLDivElement>(null);
|
||||
const closeRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
const popover = usePopoverState({
|
||||
gutter: 0,
|
||||
placement: "top-start",
|
||||
unstable_fixed: true,
|
||||
});
|
||||
|
||||
// Reset scroll position to the top when popover is opened
|
||||
React.useEffect(() => {
|
||||
if (popover.visible && scrollableRef.current) {
|
||||
scrollableRef.current.scrollTop = 0;
|
||||
const handleRequestClose = React.useCallback(() => {
|
||||
if (closeRef.current) {
|
||||
closeRef.current.click();
|
||||
}
|
||||
}, [popover.visible]);
|
||||
}, []);
|
||||
|
||||
const handleAutoFocus = React.useCallback((event: Event) => {
|
||||
// Prevent focus from moving to the popover content
|
||||
event.preventDefault();
|
||||
|
||||
// Reset scroll position to the top when popover is opened
|
||||
if (scrollableRef.current) {
|
||||
scrollableRef.current.scrollTop = 0;
|
||||
scrollableRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopoverDisclosure {...popover}>{children}</PopoverDisclosure>
|
||||
<StyledPopover
|
||||
{...popover}
|
||||
scrollable={false}
|
||||
mobilePosition="bottom"
|
||||
aria-label={t("Notifications")}
|
||||
unstable_initialFocusRef={scrollableRef}
|
||||
shrink
|
||||
flex
|
||||
>
|
||||
<Notifications
|
||||
onRequestClose={popover.hide}
|
||||
isOpen={popover.visible}
|
||||
ref={scrollableRef}
|
||||
/>
|
||||
</StyledPopover>
|
||||
</>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>{children}</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<StyledContent
|
||||
side="top"
|
||||
align="start"
|
||||
sideOffset={0}
|
||||
avoidCollisions={true}
|
||||
aria-label={t("Notifications")}
|
||||
onOpenAutoFocus={handleAutoFocus}
|
||||
>
|
||||
<Notifications
|
||||
onRequestClose={handleRequestClose}
|
||||
ref={scrollableRef}
|
||||
/>
|
||||
<VisuallyHidden>
|
||||
<Popover.Close ref={closeRef} />
|
||||
</VisuallyHidden>
|
||||
</StyledContent>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledPopover = styled(Popover)`
|
||||
const StyledContent = styled(Popover.Content)`
|
||||
z-index: ${depths.menu};
|
||||
display: flex;
|
||||
animation: ${fadeAndSlideUp} 200ms ease;
|
||||
transform-origin: 75% 0;
|
||||
background: ${s("menuBackground")};
|
||||
border-radius: 6px;
|
||||
padding: 6px 0;
|
||||
max-height: 75vh;
|
||||
box-shadow: ${s("menuShadow")};
|
||||
width: 380px;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
position: fixed;
|
||||
z-index: ${depths.menu};
|
||||
top: 50px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
width: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(NotificationsPopover);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { OAuthClientValidation } from "@shared/validations";
|
||||
@@ -8,6 +8,7 @@ import ImageInput from "~/scenes/Settings/components/ImageInput";
|
||||
import Button from "~/components/Button";
|
||||
import Flex from "~/components/Flex";
|
||||
import Input, { LabelText } from "~/components/Input";
|
||||
import { createSwitchRegister } from "~/utils/forms";
|
||||
import isCloudHosted from "~/utils/isCloudHosted";
|
||||
import Switch from "../Switch";
|
||||
|
||||
@@ -49,7 +50,7 @@ export const OAuthClientForm = observer(function OAuthClientForm_({
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setTimeout(() => setFocus("name", { shouldSelect: true }), 100);
|
||||
}, [setFocus]);
|
||||
|
||||
@@ -116,7 +117,7 @@ export const OAuthClientForm = observer(function OAuthClientForm_({
|
||||
/>
|
||||
{isCloudHosted && (
|
||||
<Switch
|
||||
{...register("published")}
|
||||
{...createSwitchRegister(register, "published")}
|
||||
label={t("Published")}
|
||||
note={t("Allow this app to be installed by other workspaces")}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import useStores from "~/hooks/useStores";
|
||||
@@ -16,7 +16,7 @@ export const OAuthClientNew = observer(function OAuthClientNew_({
|
||||
const { oauthClients } = useStores();
|
||||
const history = useHistory();
|
||||
|
||||
const handleSubmit = React.useCallback(
|
||||
const handleSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
const oauthClient = await oauthClients.save(data);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "styled-components";
|
||||
import useStores from "~/hooks/useStores";
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function PageTheme() {
|
||||
const { ui } = useStores();
|
||||
const theme = useTheme();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
// wider page background beyond the React root
|
||||
if (document.body) {
|
||||
document.body.style.background = theme.background;
|
||||
|
||||
@@ -2,7 +2,6 @@ import "../stores";
|
||||
import { render } from "@testing-library/react";
|
||||
import { TFunction } from "i18next";
|
||||
import { Provider } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { getI18n } from "react-i18next";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import PaginatedList from "./PaginatedList";
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import fractionalIndex from "fractional-index";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import Pin from "~/models/Pin";
|
||||
@@ -44,12 +44,12 @@ function PinnedDocuments({
|
||||
...rest
|
||||
}: Props) {
|
||||
const { documents } = useStores();
|
||||
const [items, setItems] = React.useState(pins.map((pin) => pin.documentId));
|
||||
const showPlaceholderRef = React.useRef(true);
|
||||
const [items, setItems] = useState(pins.map((pin) => pin.documentId));
|
||||
const showPlaceholderRef = useRef(true);
|
||||
const showPlaceholder =
|
||||
placeholderCount && !items.length && showPlaceholderRef.current;
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
setItems(pins.map((pin) => pin.documentId));
|
||||
}, [pins]);
|
||||
|
||||
@@ -65,7 +65,7 @@ function PinnedDocuments({
|
||||
})
|
||||
);
|
||||
|
||||
const handleDragEnd = React.useCallback(
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import DelayedMount from "~/components/DelayedMount";
|
||||
import Fade from "~/components/Fade";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Logger from "~/utils/Logger";
|
||||
import { Hook, usePluginValue } from "~/utils/PluginManager";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { transparentize } from "polished";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled, { css } from "styled-components";
|
||||
import { s, hover } from "@shared/styles";
|
||||
@@ -107,9 +107,11 @@ const Reaction: React.FC<Props> = ({
|
||||
const handleClick = React.useCallback(
|
||||
(event: React.SyntheticEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
active
|
||||
? void onRemoveReaction(reaction.emoji)
|
||||
: void onAddReaction(reaction.emoji);
|
||||
if (active) {
|
||||
void onRemoveReaction(reaction.emoji);
|
||||
} else {
|
||||
void onAddReaction(reaction.emoji);
|
||||
}
|
||||
},
|
||||
[reaction, active, onAddReaction, onRemoveReaction]
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import compact from "lodash/compact";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import Comment from "~/models/Comment";
|
||||
import useHover from "~/hooks/useHover";
|
||||
import useStores from "~/hooks/useStores";
|
||||
@@ -41,7 +41,7 @@ const ReactionList: React.FC<Props> = ({
|
||||
const loadReactedUsersData = async () => {
|
||||
try {
|
||||
await model.loadReactedUsersData();
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
Logger.warn("Could not prefetch reaction data");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactionIcon } from "outline-icons";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PopoverDisclosure, usePopoverState } from "reakit";
|
||||
import EventBoundary from "@shared/components/EventBoundary";
|
||||
@@ -99,7 +99,7 @@ const ReactionPicker: React.FC<Props> = ({
|
||||
<>
|
||||
<PopoverDisclosure {...popover}>
|
||||
{(props) => (
|
||||
<Tooltip content={t("Add reaction")} placement="top" hideOnClick>
|
||||
<Tooltip content={t("Add reaction")} placement="top">
|
||||
<NudeButton
|
||||
{...props}
|
||||
aria-label={t("Reaction picker")}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import compact from "lodash/compact";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tab, TabPanel, useTabState } from "reakit";
|
||||
import { toast } from "sonner";
|
||||
@@ -29,7 +29,7 @@ const ViewReactionsDialog: React.FC<Props> = ({ model }) => {
|
||||
const loadReactedUsersData = async () => {
|
||||
try {
|
||||
await model.loadReactedUsersData();
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
toast.error(t("Could not load reactions"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as React from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
/**
|
||||
* Context to provide a reference to the scrollable container
|
||||
*/
|
||||
const ScrollContext = React.createContext<
|
||||
const ScrollContext = createContext<
|
||||
React.RefObject<HTMLDivElement> | undefined
|
||||
>(undefined);
|
||||
|
||||
/**
|
||||
* Hook to get the scrollable container reference
|
||||
*/
|
||||
export const useScrollContext = () => React.useContext(ScrollContext);
|
||||
export const useScrollContext = () => useContext(ScrollContext);
|
||||
|
||||
export default ScrollContext;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// based on: https://reacttraining.com/react-router/web/guides/scroll-restoration
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import { useScrollContext } from "./ScrollContext";
|
||||
@@ -13,7 +13,7 @@ export default function ScrollToTop({ children }: Props) {
|
||||
const previousLocationPathname = usePrevious(location.pathname);
|
||||
const scrollContainerRef = useScrollContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (
|
||||
location.pathname === previousLocationPathname ||
|
||||
location.state?.retainScrollPosition
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useKBar } from "kbar";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { searchDocumentsForQuery } from "~/actions/definitions/documents";
|
||||
import { navigateToRecentSearchQuery } from "~/actions/definitions/navigation";
|
||||
|
||||
@@ -9,7 +9,7 @@ import useStores from "~/hooks/useStores";
|
||||
export default function SearchActions() {
|
||||
const { searches } = useStores();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!searches.isLoaded && !searches.isFetching) {
|
||||
void searches.fetchPage({
|
||||
source: "app",
|
||||
|
||||
@@ -53,10 +53,10 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
}, [searchResults, query, show]);
|
||||
|
||||
const performSearch = React.useCallback(
|
||||
async ({ query, ...options }) => {
|
||||
if (query?.length > 0) {
|
||||
async ({ query: searchQuery, ...options }) => {
|
||||
if (searchQuery?.length > 0) {
|
||||
const response = await documents.search({
|
||||
query,
|
||||
query: searchQuery,
|
||||
shareId,
|
||||
...options,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import type Collection from "~/models/Collection";
|
||||
import type Document from "~/models/Document";
|
||||
import Share from "~/models/Share";
|
||||
import Flex from "~/components/Flex";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import Text from "~/components/Text";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
@@ -230,7 +231,9 @@ const AccessTooltip = ({
|
||||
{children}
|
||||
</Text>
|
||||
<Tooltip content={content ?? t("Access inherited from collection")}>
|
||||
<QuestionMarkIcon size={18} />
|
||||
<NudeButton size={18}>
|
||||
<QuestionMarkIcon size={18} />
|
||||
</NudeButton>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
toast.error(t("Could not remove user"));
|
||||
}
|
||||
},
|
||||
@@ -62,19 +62,19 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
|
||||
);
|
||||
|
||||
const handleUpdateUser = React.useCallback(
|
||||
async (user, permission) => {
|
||||
async (userToUpdate, permission) => {
|
||||
try {
|
||||
await userMemberships.create({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
userId: userToUpdate.id,
|
||||
permission,
|
||||
});
|
||||
toast.success(
|
||||
t(`Permissions for {{ userName }} updated`, {
|
||||
userName: user.name,
|
||||
userName: userToUpdate.name,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
toast.error(t("Could not update user"));
|
||||
}
|
||||
},
|
||||
@@ -87,9 +87,9 @@ function DocumentMembersList({ document, invitedInSession }: Props) {
|
||||
() =>
|
||||
orderBy(
|
||||
document.members,
|
||||
(user) =>
|
||||
(invitedInSession.includes(user.id) ? "_" : "") +
|
||||
user.name.toLocaleLowerCase(),
|
||||
(memberUser) =>
|
||||
(invitedInSession.includes(memberUser.id) ? "_" : "") +
|
||||
memberUser.name.toLocaleLowerCase(),
|
||||
"asc"
|
||||
),
|
||||
[document.members, invitedInSession]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useCallback, Fragment } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
@@ -31,7 +31,7 @@ const DocumentMemberListItem = ({
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
const handleChange = useCallback(
|
||||
(permission: DocumentPermission | typeof EmptySelectValue) => {
|
||||
if (permission === EmptySelectValue) {
|
||||
onRemove?.();
|
||||
@@ -68,7 +68,7 @@ const DocumentMemberListItem = ({
|
||||
if (!currentPermission) {
|
||||
return null;
|
||||
}
|
||||
const MaybeLink = membership?.source ? StyledLink : React.Fragment;
|
||||
const MaybeLink = membership?.source ? StyledLink : Fragment;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user