Compare commits

..

1 Commits

Author SHA1 Message Date
Tom Moor fa7f8d3592 fix: ExportDocumentTreeTask needs documentStructure 2025-05-07 19:38:30 -04:00
394 changed files with 2802 additions and 6413 deletions
+2 -7
View File
@@ -1,11 +1,6 @@
{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic"
}
],
"@babel/preset-react",
"@babel/preset-env",
"@babel/preset-typescript"
],
@@ -65,4 +60,4 @@
]
}
}
}
}
+140 -165
View File
@@ -1,80 +1,48 @@
# –––––––––––––––– REQUIRED ––––––––––––––––
NODE_ENV=production
# This URL should point to the fully qualified, publicly accessible, URL. If using a
# proxy this will be the proxy's URL.
URL=
# Generate a hex-encoded 32-byte random key. You should use `openssl rand -hex 32`
# in your terminal to generate a random value.
SECRET_KEY=generate_a_new_key
# The port to expose the Outline server on, this should match what is configured
# in your docker-compose.yml
# Generate a unique random key. The format is not important but you could still use
# `openssl rand -hex 32` in your terminal to produce this.
UTILS_SECRET=generate_a_new_key
# For production point these at your databases, in development the default
# should work out of the box.
DATABASE_URL=postgres://user:pass@postgres:5432/outline
DATABASE_CONNECTION_POOL_MIN=
DATABASE_CONNECTION_POOL_MAX=
# Uncomment this to disable SSL for connecting to Postgres
# PGSSLMODE=disable
# For redis you can either specify an ioredis compatible url like this
REDIS_URL=redis://redis:6379
# or alternatively, if you would like to provide additional connection options,
# use a base64 encoded JSON connection option object. Refer to the ioredis documentation
# for a list of available options.
# Example: Use Redis Sentinel for high availability
# {"sentinels":[{"host":"sentinel-0","port":26379},{"host":"sentinel-1","port":26379}],"name":"mymaster"}
# REDIS_URL=ioredis://eyJzZW50aW5lbHMiOlt7Imhvc3QiOiJzZW50aW5lbC0wIiwicG9ydCI6MjYzNzl9LHsiaG9zdCI6InNlbnRpbmVsLTEiLCJwb3J0IjoyNjM3OX1dLCJuYW1lIjoibXltYXN0ZXIifQ==
# URL should point to the fully qualified, publicly accessible URL. If using a
# proxy the port in URL and PORT may be different.
URL=
PORT=3000
# See [documentation](docs/SERVICES.md) on running a separate collaboration
# server, for normal operation this does not need to be set.
COLLABORATION_URL=
# If using a Cloudfront/Cloudflare distribution or similar it can be set below.
# This will cause paths to javascript, stylesheets, and images to be updated to
# the hostname defined in CDN_URL. In your CDN configuration the origin server
# should be set to the same as URL.
CDN_URL=
# How many processes should be spawned. As a reasonable rule divide your servers
# available memory by 512 for a rough estimate
WEB_CONCURRENCY=1
# Generate a hex-encoded 32-byte random key. Use `openssl rand -hex 32` in your
# terminal to generate a random value.
SECRET_KEY=generate_a_new_key
# Generate a unique random key. The format is not important but you could still use
# `openssl rand -hex 32` in your terminal to generate a random value.
UTILS_SECRET=generate_a_new_key
# The default interface language. See translate.getoutline.com for a list of
# available language codes and their rough percentage translated.
DEFAULT_LANGUAGE=en_US
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––––– DATABASE –––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# The database URL for your production database, including username, password, and database name.
DATABASE_URL=postgres://user:pass@postgres:5432/outline
# The in-memory database pool per-process settings. Ensure that the pool size that will not exceed
# the maximum number of connections allowed by your database. Defaults to 0 and 5.
DATABASE_CONNECTION_POOL_MIN=
DATABASE_CONNECTION_POOL_MAX=
# Uncomment this line if you will not use SSL for connecting to Postgres. This is acceptable
# if the database and the application are on the same machine.
# PGSSLMODE=disable
# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––– REDIS –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# The Redis URL for your environment you can either specify an ioredis compatible url or a Base64
# encoded configuration object.
# DOCS: https://docs.getoutline.com/s/hosting/doc/redis-LGM4BFXYp4
REDIS_URL=redis://redis:6379
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––– FILE STORAGE –––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Specify what storage system to use. Possible value is one of "s3" or "local".
# For "local" images and document attachments will be saved on local disk, for "s3" they
# will be stored in an S3-compatible network store.
# DOCS: https://docs.getoutline.com/s/hosting/doc/file-storage-N4M0T6Ypu7
# For "local", the avatar images and document attachments will be saved on local disk.
FILE_STORAGE=local
# If "local" is configured for FILE_STORAGE above, then this sets the parent directory under
# which all attachments/images are stored. Make sure that the process has permissions to
# create this path and also to write files to it.
# which all attachments/images go. Make sure that the process has permissions to create
# this path and also to write files to it.
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
# Maximum allowed size for the uploaded attachment.
@@ -88,8 +56,8 @@ FILE_STORAGE_IMPORT_MAX_SIZE=
# and the files are temporary being automatically deleted after a period of time.
FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE=
# To support uploading of images for avatars and document attachments in a distributed
# architecture, an s3-compatible storage can be configured if FILE_STORAGE=s3 above.
# To support uploading of images for avatars and document attachments in a distributed
# architecture an s3-compatible storage can be configured if FILE_STORAGE=s3 above.
AWS_ACCESS_KEY_ID=get_a_key_from_aws
AWS_SECRET_ACCESS_KEY=get_the_secret_of_above_key
AWS_REGION=xx-xxxx-x
@@ -99,55 +67,38 @@ AWS_S3_UPLOAD_BUCKET_NAME=bucket_name_here
AWS_S3_FORCE_PATH_STYLE=true
AWS_S3_ACL=private
# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––––– SSL –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Base64 encoded private key and certificate for HTTPS termination. This is one
# of three ways to configure SSL and can be left empty.
# DOCS: https://docs.getoutline.com/s/hosting/doc/ssl-pzk7WO8d1n
SSL_KEY=
SSL_CERT=
# Auto-redirect to https in production. The default is true but you may set to
# false if you can be sure that SSL is terminated at an external loadbalancer.
FORCE_HTTPS=true
# ––––––––––––––––––––––––––––––––––––––
# –––––––––– AUTHENTICATION ––––––––––
# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––– AUTHENTICATION ––––––––––––––
# Third party signin credentials, at least ONE OF EITHER Google, Slack,
# Discord, or Microsoft is required for a working installation or you'll
# have no sign-in options.
# or Microsoft is required for a working installation or you'll have no sign-in
# options.
# Slack sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-sgMujR8J9J
# To configure Slack auth, you'll need to create an Application at
# => https://api.slack.com/apps
#
# When configuring the Client ID, add a redirect URL under "OAuth & Permissions":
# https://<URL>/auth/slack.callback
SLACK_CLIENT_ID=get_a_key_from_slack
SLACK_CLIENT_SECRET=get_the_secret_of_above_key
# Google sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/google-hOuvtCmTqQ
# To configure Google auth, you'll need to create an OAuth Client ID at
# => https://console.cloud.google.com/apis/credentials
#
# When configuring the Client ID, add an Authorized redirect URI:
# https://<URL>/auth/google.callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Microsoft Entra / Azure AD sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/microsoft-entra-UVz6jsIOcv
# To configure Microsoft/Azure auth, you'll need to create an OAuth Client. See
# the guide for details on setting up your Azure App:
# => https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=
AZURE_RESOURCE_APP_ID=
# Discord sign-in provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/discord-g4JdWFFub6
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_SERVER_ID=
DISCORD_SERVER_ROLES=
# Generic OIDC provider
# DOCS: https://docs.getoutline.com/s/hosting/doc/oidc-8CPBm6uC0I
# To configure generic OIDC auth, you'll need some kind of identity provider.
# See documentation for whichever IdP you use to acquire the following info:
# Redirect URI is https://<URL>/auth/oidc.callback
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_AUTH_URI=
@@ -165,54 +116,83 @@ OIDC_DISPLAY_NAME=OpenID Connect
# Space separated auth scopes.
OIDC_SCOPES=openid profile email
# ––––––––––––––––––––––––––––––––––––––
# –––––––––––––– EMAIL –––––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# To support sending outgoing transactional emails such as "document updated" or
# email sign-in you'll need to connect an SMTP server. Service can be configured
# with any service from this list: https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/
# DOCS: https://docs.getoutline.com/s/hosting/doc/smtp-cqCJyZGMIB
SMTP_SERVICE=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
# ––––––––––––––––––––––––––––––––––––––
# –––––––––– RATE LIMITER ––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Whether the rate limiter is enabled or not
RATE_LIMITER_ENABLED=true
# Individual endpoints have hardcoded rate limits that are enabled
# with the above setting, however this is a global rate limiter
# across all requests
RATE_LIMITER_REQUESTS=1000
RATE_LIMITER_DURATION_WINDOW=60
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––– INTEGRATIONS –––––––––––
# ––––––––––––––––––––––––––––––––––––––
# The GitHub integration allows previewing issue and pull request links
# DOCS: https://docs.getoutline.com/s/hosting/doc/github-GchT3NNxI9
# To configure the GitHub integration, you'll need to create a GitHub App at
# => https://github.com/settings/apps
#
# When configuring the Client ID, add a redirect URL under "Permissions & events":
# https://<URL>/api/github.callback
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_APP_NAME=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
# The Linear integration allows previewing issue links as rich mentions
# Linear
LINEAR_CLIENT_ID=
LINEAR_CLIENT_SECRET=
# To configure Discord auth, you'll need to create a Discord Application at
# => https://discord.com/developers/applications/
#
# When configuring the Client ID, add a redirect URL under "OAuth2":
# https://<URL>/auth/discord.callback
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
# DISCORD_SERVER_ID should be the ID of the Discord server that Outline is
# integrated with.
# Used to verify that the user is a member of the server as well as server
# metadata such as nicknames, server icon and name.
DISCORD_SERVER_ID=
# DISCORD_SERVER_ROLES should be a comma separated list of role IDs that are
# allowed to access Outline. If this is not set, all members of the server
# will be allowed to access Outline.
# DISCORD_SERVER_ID and DISCORD_SERVER_ROLES must be set together.
DISCORD_SERVER_ROLES=
# –––––––––––––– IMPORTS ––––––––––––––
NOTION_CLIENT_ID=
NOTION_CLIENT_SECRET=
# –––––––––––––––– OPTIONAL ––––––––––––––––
# Base64 encoded private key and certificate for HTTPS termination. This is only
# required if you do not use an external reverse proxy. See documentation:
# https://wiki.generaloutline.com/share/1c922644-40d8-41fe-98f9-df2b67239d45
SSL_KEY=
SSL_CERT=
# If using a Cloudfront/Cloudflare distribution or similar it can be set below.
# This will cause paths to javascript, stylesheets, and images to be updated to
# the hostname defined in CDN_URL. In your CDN configuration the origin server
# should be set to the same as URL.
CDN_URL=
# Auto-redirect to https in production. The default is true but you may set to
# false if you can be sure that SSL is terminated at an external loadbalancer.
FORCE_HTTPS=true
# Have the installation check for updates by sending anonymized statistics to
# the maintainers
ENABLE_UPDATES=true
# How many processes should be spawned. As a reasonable rule divide your servers
# available memory by 512 for a rough estimate
WEB_CONCURRENCY=1
# You can remove this line if your reverse proxy already logs incoming http
# requests and this ends up being duplicative
DEBUG=http
# Configure lowest severity level for server logs. Should be one of
# error, warn, info, http, verbose, debug and silly
LOG_LEVEL=info
# For a complete Slack integration with search and posting to channels the
# following configs are also needed in addition to Slack authentication:
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-G2mc8DOJHk
# following configs are also needed, some more details
# => https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a
#
SLACK_VERIFICATION_TOKEN=your_token
SLACK_APP_ID=A0XXXXXXX
SLACK_MESSAGE_ACTIONS=true
@@ -222,34 +202,29 @@ SLACK_MESSAGE_ACTIONS=true
DROPBOX_APP_KEY=
# Optionally enable Sentry (sentry.io) to track errors and performance,
# DOCS: https://docs.getoutline.com/s/hosting/doc/sentry-jxcFttcDl5
# and optionally add a Sentry proxy tunnel for bypassing ad blockers in the UI:
# https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option)
SENTRY_DSN=
SENTRY_TUNNEL=
# Enable importing pages from a Notion workspace
# DOCS: https://docs.getoutline.com/s/hosting/doc/notion-2v6g7WY3l3
NOTION_CLIENT_ID=
NOTION_CLIENT_SECRET=
# To support sending outgoing transactional emails such as "document updated" or
# "you've been invited" you'll need to provide authentication for an SMTP server
SMTP_SERVICE=
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=
# The Iframely integration allows previews of third-party content within Outline.
# For example, hovering over an external link will show a preview.
# DOCS: https://docs.getoutline.com/s/hosting/doc/iframely-HwLF1EZ9mo
# The default interface language. See translate.getoutline.com for a list of
# available language codes and their rough percentage translated.
DEFAULT_LANGUAGE=en_US
# Optionally enable rate limiter at application web server
RATE_LIMITER_ENABLED=true
# Configure default throttling parameters for rate limiter
RATE_LIMITER_REQUESTS=1000
RATE_LIMITER_DURATION_WINDOW=60
# Iframely API config
IFRAMELY_URL=
IFRAMELY_API_KEY=
# ––––––––––––––––––––––––––––––––––––––
# ––––––––––––– DEBUGGING ––––––––––––
# ––––––––––––––––––––––––––––––––––––––
# Have the installation check for updates by sending anonymized statistics to
# the maintainers
ENABLE_UPDATES=true
# Debugging categories to enable you can remove the default "http" value if
# your proxy already logs incoming http requests and this ends up being duplicative
DEBUG=http
# Configure lowest severity level for server logs. Should be one of
# error, warn, info, http, verbose, debug, or silly
LOG_LEVEL=info
+12 -43
View File
@@ -2,9 +2,7 @@
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"extraFileExtensions": [
".json"
],
"extraFileExtensions": [".json"],
"project": "./tsconfig.json",
"ecmaFeatures": {
"jsx": true
@@ -19,7 +17,6 @@
],
"plugins": [
"es",
"react",
"@typescript-eslint",
"eslint-plugin-import",
"eslint-plugin-node",
@@ -30,30 +27,21 @@
"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/react-in-jsx-scope": "off",
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"react/self-closing-comp": ["error", {
"component": true,
"html": true
}],
"@typescript-eslint/no-shadow": [
"warn",
{
"allow": [
"transaction"
],
"allow": ["transaction"],
"hoist": "all",
"ignoreTypeValueShadow": true
}
@@ -75,25 +63,9 @@
"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,
@@ -162,13 +134,10 @@
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"typescript": {}
}
}
}
}
-59
View File
@@ -1,59 +0,0 @@
name: Auto Close Unsigned PRs
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
jobs:
close-unsigned-prs:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Close unsigned PRs
uses: actions/github-script@v6
with:
script: |
const now = new Date();
const TWO_WEEKS = 14 * 24 * 60 * 60 * 1000; // 14 days in milliseconds
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open'
});
for (const pr of prs.data) {
const prCreatedAt = new Date(pr.created_at);
const prAge = now - prCreatedAt;
if (prAge < TWO_WEEKS) continue;
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});
const hasNotSignedComment = comments.data.some(comment =>
comment.body.toLowerCase().includes('https://cla-assistant.io/pull/badge/not_signed')
);
if (hasNotSignedComment) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: 'This PR has been automatically closed because it has been open for more than 14 days and has not accepted the CLA.'
});
}
}
+2 -2
View File
@@ -3,7 +3,7 @@ Business Source License 1.1
Parameters
Licensor: General Outline, Inc.
Licensed Work: Outline 0.84.0
Licensed Work: Outline 0.83.0
The Licensed Work is (c) 2025 General Outline, Inc.
Additional Use Grant: You may make use of the Licensed Work, provided that
you may not use the Licensed Work for a Document
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work, provided that
Licensed Work by creating teams and documents
controlled by such third parties.
Change Date: 2029-05-11
Change Date: 2029-04-11
Change License: Apache License, Version 2.0
-3
View File
@@ -7,9 +7,6 @@
"plugins": [
"eslint-plugin-react-hooks"
],
"rules": {
"react/react-in-jsx-scope": "off"
},
"env": {
"jest": true,
"browser": true
+1
View File
@@ -1,4 +1,5 @@
import { PlusIcon } from "outline-icons";
import * as React from "react";
import stores from "~/stores";
import ApiKeyNew from "~/scenes/ApiKeyNew";
import { createAction } from "..";
+1
View File
@@ -13,6 +13,7 @@ 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
View File
@@ -1,4 +1,5 @@
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";
+1
View File
@@ -7,6 +7,7 @@ import {
TrashIcon,
UserIcon,
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import { createAction } from "~/actions";
import { DeveloperSection } from "~/actions/sections";
+1
View File
@@ -31,6 +31,7 @@ import {
LogoutIcon,
CaseSensitiveIcon,
} from "outline-icons";
import * as React from "react";
import { toast } from "sonner";
import Icon from "@shared/components/Icon";
import {
+1
View File
@@ -13,6 +13,7 @@ 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,4 +1,5 @@
import { ArchiveIcon, MarkAsReadIcon } from "outline-icons";
import * as React from "react";
import { createAction } from "..";
import { NotificationSection } from "../sections";
+1
View File
@@ -1,4 +1,5 @@
import { PlusIcon } from "outline-icons";
import * as React from "react";
import stores from "~/stores";
import { OAuthClientNew } from "~/components/OAuthClient/OAuthClientNew";
import { createAction } from "..";
+1
View File
@@ -1,5 +1,6 @@
import copy from "copy-to-clipboard";
import { LinkIcon, RestoreIcon } from "outline-icons";
import * as React from "react";
import { matchPath } from "react-router-dom";
import { toast } from "sonner";
import stores from "~/stores";
+1
View File
@@ -1,4 +1,5 @@
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
View File
@@ -1,4 +1,5 @@
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";
+1
View File
@@ -1,4 +1,5 @@
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
View File
@@ -1,4 +1,5 @@
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";
+2
View File
@@ -1,3 +1,5 @@
import * as React from "react";
export default function Arrow() {
return (
<svg
+2 -2
View File
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import { useEffect } from "react";
import * as React 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
useEffect(() => {
React.useEffect(() => {
void changeLanguage(language, i18n);
}, [i18n, language]);
+2 -15
View File
@@ -45,23 +45,12 @@ type Props = {
};
function Avatar(props: Props) {
const {
model,
style,
variant = AvatarVariant.Round,
className,
...rest
} = props;
const { model, style, variant = AvatarVariant.Round, ...rest } = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
return (
<Relative
style={style}
$variant={variant}
$size={props.size}
className={className}
>
<Relative style={style} $variant={variant} $size={props.size}>
{src && !error ? (
<Image onError={handleError} src={src} {...rest} />
) : model ? (
@@ -86,8 +75,6 @@ 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
View File
@@ -1,4 +1,5 @@
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";
+2 -2
View File
@@ -1,7 +1,7 @@
import Avatar, { IAvatar, AvatarSize, AvatarVariant } from "./Avatar";
import Avatar, { IAvatar, AvatarSize } from "./Avatar";
import AvatarWithPresence from "./AvatarWithPresence";
import { GroupAvatar } from "./GroupAvatar";
export { Avatar, GroupAvatar, AvatarSize, AvatarVariant, AvatarWithPresence };
export { Avatar, GroupAvatar, AvatarSize, AvatarWithPresence };
export type { IAvatar };
+1
View File
@@ -1,3 +1,4 @@
import * as React from "react";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { depths, s } from "@shared/styles";
+2 -2
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import * as React 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();
useEffect(() => {
React.useEffect(() => {
void changeLanguage(locale, i18n);
}, [locale, i18n]);
+1
View File
@@ -1,3 +1,4 @@
import React from "react";
import styled, { useTheme } from "styled-components";
const cleanPercentage = (percentage: number) => {
+5 -5
View File
@@ -3,7 +3,7 @@ import isEqual from "lodash/isEqual";
import orderBy from "lodash/orderBy";
import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import { useState, useMemo, useEffect, useCallback } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import Document from "~/models/Document";
@@ -31,7 +31,7 @@ function Collaborators(props: Props) {
const { t } = useTranslation();
const user = useCurrentUser();
const currentUserId = user?.id;
const [requestedUserIds, setRequestedUserIds] = useState<string[]>([]);
const [requestedUserIds, setRequestedUserIds] = React.useState<string[]>([]);
const { users, presence, ui } = useStores();
const { document } = props;
const documentPresence = presence.get(document.id);
@@ -45,7 +45,7 @@ function Collaborators(props: Props) {
.map((p) => p.userId);
// ensure currently present via websocket are always ordered first
const collaborators = useMemo(
const collaborators = React.useMemo(
() =>
orderBy(
filter(
@@ -62,7 +62,7 @@ function Collaborators(props: Props) {
);
// load any users we don't yet have in memory
useEffect(() => {
React.useEffect(() => {
const ids = uniq([...document.collaboratorIds, ...presentIds])
.filter((userId) => !users.get(userId))
.sort();
@@ -78,7 +78,7 @@ function Collaborators(props: Props) {
placement: "bottom-end",
});
const renderAvatar = useCallback(
const renderAvatar = React.useCallback(
({ model: collaborator, ...rest }) => {
const isPresent = presentIds.includes(collaborator.id);
const isEditing = editingIds.includes(collaborator.id);
+2 -2
View File
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import { useCallback } from "react";
import * as React 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 = useCallback(
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
await collection?.save(data);
+9 -15
View File
@@ -1,6 +1,6 @@
import uniq from "lodash/uniq";
import { observer } from "mobx-react";
import { useMemo, useEffect, useCallback, Suspense } from "react";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
@@ -15,7 +15,6 @@ import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission";
import { createLazyComponent } from "~/components/LazyLoad";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
@@ -23,7 +22,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import { EmptySelectValue } from "~/types";
const IconPicker = createLazyComponent(() => import("~/components/IconPicker"));
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
export interface FormData {
name: string;
@@ -40,7 +39,7 @@ const useIconColor = (collection?: Collection) => {
collections.orderedData.map((c) => c.color).filter(Boolean)
) as string[];
const iconColor = useMemo(
const iconColor = React.useMemo(
() =>
collection?.color ??
// If all the existing collections have the same color, use that color,
@@ -89,12 +88,7 @@ export const CollectionForm = observer(function CollectionForm_({
const values = watch();
// Preload the IconPicker component on mount
useEffect(() => {
void IconPicker.preload();
}, []);
useEffect(() => {
React.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,11 +101,11 @@ export const CollectionForm = observer(function CollectionForm_({
}
}, [collection, hasOpenedIconPicker, setValue, values.name, values.icon]);
useEffect(() => {
React.useEffect(() => {
setTimeout(() => setFocus("name", { shouldSelect: true }), 100);
}, [setFocus]);
const handleIconChange = useCallback(
const handleIconChange = React.useCallback(
(icon: string, color: string | null) => {
if (icon !== values.icon) {
setFocus("name");
@@ -140,7 +134,7 @@ export const CollectionForm = observer(function CollectionForm_({
maxLength: CollectionValidation.maxNameLength,
})}
prefix={
<Suspense fallback={fallbackIcon}>
<React.Suspense fallback={fallbackIcon}>
<StyledIconPicker
icon={values.icon}
color={values.color ?? iconColor}
@@ -149,7 +143,7 @@ export const CollectionForm = observer(function CollectionForm_({
onOpen={setHasOpenedIconPicker}
onChange={handleIconChange}
/>
</Suspense>
</React.Suspense>
}
autoComplete="off"
autoFocus
@@ -208,7 +202,7 @@ export const CollectionForm = observer(function CollectionForm_({
);
});
const StyledIconPicker = styled(IconPicker.Component)`
const StyledIconPicker = styled(IconPicker)`
margin-left: 4px;
margin-right: 4px;
`;
+2 -2
View File
@@ -1,6 +1,6 @@
import { runInAction } from "mobx";
import { observer } from "mobx-react";
import { useCallback } from "react";
import * as React 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 = useCallback(
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
const collection = await collections.save(data);
@@ -1,4 +1,5 @@
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,4 +1,5 @@
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 { useMemo } from "react";
import * as React 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 useMemo(
return React.useMemo(
() =>
documents.recentlyViewed
.filter((document) => document.id !== ui.activeDocumentId)
@@ -1,5 +1,5 @@
import { SettingsIcon } from "outline-icons";
import { useMemo } from "react";
import * as React 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 = useMemo(
const actions = React.useMemo(
() =>
config.map((item) => {
const Icon = item.icon;
@@ -22,7 +22,7 @@ const useSettingsAction = () => {
[config]
);
const navigateToSettings = useMemo(
const navigateToSettings = React.useMemo(
() =>
createAction({
id: "settings",
@@ -1,5 +1,5 @@
import { NewDocumentIcon, ShapesIcon } from "outline-icons";
import { useEffect, useMemo } from "react";
import * as React 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();
useEffect(() => {
React.useEffect(() => {
void documents.fetchAllTemplates();
}, [documents]);
const actions = useMemo(
const actions = React.useMemo(
() =>
documents.templatesAlphabetical.map((template) =>
createAction({
@@ -61,7 +61,7 @@ const useTemplatesAction = () => {
[documents.templatesAlphabetical]
);
const newFromTemplate = useMemo(
const newFromTemplate = React.useMemo(
() =>
createAction({
id: "templates",
+1
View File
@@ -1,4 +1,5 @@
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
View File
@@ -1,4 +1,5 @@
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";
+1
View File
@@ -1,5 +1,6 @@
import { observer } from "mobx-react";
import { DisconnectedIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
+10 -19
View File
@@ -138,7 +138,7 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
as={Link}
id={`${item.title}-${index}`}
to={item.to}
key={`${item.type}-${item.title}-${index}`}
key={index}
disabled={item.disabled}
selected={item.selected}
icon={showIcons !== false ? item.icon : undefined}
@@ -154,7 +154,7 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
<MenuItem
id={`${item.title}-${index}`}
href={item.href}
key={`${item.type}-${item.title}-${index}`}
key={index}
disabled={item.disabled}
selected={item.selected}
level={item.level}
@@ -176,7 +176,7 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
disabled={item.disabled}
selected={item.selected}
dangerous={item.dangerous}
key={`${item.type}-${item.title}-${index}`}
key={index}
icon={showIcons !== false ? item.icon : undefined}
{...menu}
>
@@ -185,25 +185,18 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
);
return item.tooltip ? (
<Tooltip
content={item.tooltip}
placement={"bottom"}
key={`tooltip-${item.title}-${index}`}
>
<Tooltip content={item.tooltip} placement={"bottom"}>
<div>{menuItem}</div>
</Tooltip>
) : (
<React.Fragment key={`${item.type}-${item.title}-${index}`}>
{menuItem}
</React.Fragment>
<>{menuItem}</>
);
}
if (item.type === "submenu") {
// Skip rendering empty submenus
return item.items.length > 0 ? (
return (
<BaseMenuItem
key={`${item.type}-${item.title}-${index}`}
key={index}
as={SubMenu}
id={`${item.title}-${index}`}
templateItems={item.items}
@@ -216,17 +209,15 @@ function Template({ items, actions, context, showIcons, ...menu }: Props) {
}
{...menu}
/>
) : null;
);
}
if (item.type === "separator") {
return <Separator key={`separator-${index}`} />;
return <Separator key={index} />;
}
if (item.type === "heading") {
return (
<Header key={`heading-${item.title}-${index}`}>{item.title}</Header>
);
return <Header key={index}>{item.title}</Header>;
}
const _exhaustiveCheck: never = item;
+3 -3
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import * as React from "react";
type Props = {
delay?: number;
@@ -6,9 +6,9 @@ type Props = {
};
export default function DelayedMount({ delay = 250, children }: Props) {
const [isShowing, setShowing] = useState(false);
const [isShowing, setShowing] = React.useState(false);
useEffect(() => {
React.useEffect(() => {
const timeout = setTimeout(() => setShowing(true), delay);
return () => {
clearTimeout(timeout);
+3 -3
View File
@@ -1,4 +1,4 @@
import { useRef, useEffect } from "react";
import * as React 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 = useRef(false);
const hasDisabledUpdateMessage = React.useRef(false);
useEffect(() => {
React.useEffect(() => {
Desktop.bridge?.redirect((path: string, replace = false) => {
if (replace) {
history.replace(path);
+1
View File
@@ -1,4 +1,5 @@
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 -4
View File
@@ -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 { useRef, useCallback, useMemo } from "react";
import * as React 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 = useRef(!pin?.collectionId).current;
const pinnedToHome = React.useRef(!pin?.collectionId).current;
const collection = document.collectionId
? collections.get(document.collectionId)
: undefined;
@@ -63,7 +63,7 @@ function DocumentCard(props: Props) {
transition,
};
const handleUnpin = useCallback(
const handleUnpin = React.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 = useMemo(() => document.toMarkdown(), [document]);
const markdown = React.useMemo(() => document.toMarkdown(), [document]);
const stats = useTextStats(markdown);
return (
+4 -4
View File
@@ -1,5 +1,5 @@
import { action, computed, observable } from "mobx";
import { createContext, useContext, useMemo, PropsWithChildren } from "react";
import React, { 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 = createContext<DocumentContext | null>(null);
const Context = React.createContext<DocumentContext | null>(null);
export const useDocumentContext = () => {
const ctx = useContext(Context);
const ctx = React.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 = useMemo(() => new DocumentContext(), []);
const context = React.useMemo(() => new DocumentContext(), []);
return <Context.Provider value={context}>{children}</Context.Provider>;
};
+1
View File
@@ -1,6 +1,7 @@
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";
+2 -2
View File
@@ -1,7 +1,7 @@
import compact from "lodash/compact";
import sortBy from "lodash/sortBy";
import { observer } from "mobx-react";
import { useMemo } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { dateLocale, dateToRelative } from "@shared/utils/date";
import Document from "~/models/Document";
@@ -38,7 +38,7 @@ function DocumentViews({ document, isOpen }: Props) {
documentViews,
(view) => !presentIds.includes(view.userId)
);
const users = useMemo(
const users = React.useMemo(
() => compact(sortedViews.map((v) => v.user)),
[sortedViews]
);
+1 -4
View File
@@ -64,12 +64,11 @@ function EditableTitle(
async (ev) => {
ev.preventDefault();
ev.stopPropagation();
setIsEditing(false);
const trimmedValue = value.trim();
if (trimmedValue === originalValue || trimmedValue.length === 0) {
setValue(originalValue);
setIsEditing(false);
onCancel?.();
return;
}
@@ -81,8 +80,6 @@ function EditableTitle(
setValue(originalValue);
toast.error(error.message);
throw error;
} finally {
setIsEditing(false);
}
},
[originalValue, value, onCancel, onSubmit]
+3 -3
View File
@@ -11,7 +11,7 @@ import {
UserIcon,
CrossIcon,
} from "outline-icons";
import { useRef } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import styled, { css } from "styled-components";
@@ -65,7 +65,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 = useRef(false);
const revisionLoadedRef = React.useRef(false);
const opts = {
userName: actor?.name,
};
@@ -74,7 +74,7 @@ const EventListItem = ({ event, document, ...rest }: Props) => {
event.id === RevisionHelper.latestId(document.id);
let meta, icon, to: LocationDescriptor | undefined;
const ref = useRef<HTMLAnchorElement>(null);
const ref = React.useRef<HTMLAnchorElement>(null);
// the time component tends to steal focus when clicked
// ...so forward the focus back to the parent item
const handleTimeClick = () => {
+2 -2
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import React 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] = useState(animate);
const [isAnimated] = React.useState(animate);
return isAnimated ? <Fade>{children}</Fade> : <>{children}</>;
};
+1
View File
@@ -1,3 +1,4 @@
import * as React from "react";
import styled from "styled-components";
import Empty from "~/components/Empty";
import Fade from "~/components/Fade";
@@ -1,5 +1,5 @@
import { BackIcon } from "outline-icons";
import * as React from "react";
import React from "react";
import styled from "styled-components";
import { breakpoints, s, hover } from "@shared/styles";
import { colorPalette } from "@shared/utils/collections";
@@ -1,5 +1,5 @@
import concat from "lodash/concat";
import * as React from "react";
import 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 * as React from "react";
import React from "react";
import styled from "styled-components";
import { IconType } from "@shared/types";
import { IconLibrary } from "@shared/utils/IconLibrary";
@@ -1,4 +1,4 @@
import * as React from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { IconType } from "@shared/types";
@@ -1,4 +1,4 @@
import { useMemo, useCallback } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { Menu, MenuButton, MenuItem, useMenuState } from "reakit";
import styled from "styled-components";
@@ -19,13 +19,16 @@ const SkinTonePicker = ({
}) => {
const { t } = useTranslation();
const handEmojiVariants = useMemo(() => getEmojiVariants({ id: "hand" }), []);
const handEmojiVariants = React.useMemo(
() => getEmojiVariants({ id: "hand" }),
[]
);
const menu = useMenuState({
placement: "bottom-end",
});
const handleSkinClick = useCallback(
const handleSkinClick = React.useCallback(
(emojiSkin) => {
menu.hide();
onChange(emojiSkin);
@@ -33,7 +36,7 @@ const SkinTonePicker = ({
[menu, onChange]
);
const menuItems = useMemo(
const menuItems = React.useMemo(
() =>
Object.entries(handEmojiVariants).map(([eskin, emoji]) => (
<MenuItem {...menu} key={emoji.value}>
+1 -5
View File
@@ -45,7 +45,6 @@ type Props = {
onChange: (icon: string | null, color: string | null) => void;
onOpen?: () => void;
onClose?: () => void;
children?: React.ReactNode;
};
const IconPicker = ({
@@ -60,7 +59,6 @@ const IconPicker = ({
onOpen,
onClose,
borderOnHover,
children,
}: Props) => {
const { t } = useTranslation();
@@ -176,9 +174,7 @@ const IconPicker = ({
onClick={handlePopoverButtonClick}
$borderOnHover={borderOnHover}
>
{children ? (
children
) : iconType && icon ? (
{iconType && icon ? (
<Icon value={icon} color={color} size={size} initial={initial} />
) : (
<StyledSmileyIcon color={theme.placeholder} size={size} />
+2
View File
@@ -1,3 +1,5 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
+1
View File
@@ -1,6 +1,7 @@
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";
+2
View File
@@ -1,3 +1,5 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
+2
View File
@@ -1,3 +1,5 @@
import * as React from "react";
export function LanguageIcon({ className }: { className?: string }) {
return (
<svg
+2
View File
@@ -1,3 +1,5 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
+2
View File
@@ -1,3 +1,5 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
+1 -1
View File
@@ -1,6 +1,6 @@
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
import { transparentize } from "polished";
import * as React from "react";
import React from "react";
import styled from "styled-components";
import Text from "~/components/Text";
import useMobile from "~/hooks/useMobile";
+1
View File
@@ -1,5 +1,6 @@
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";
-47
View File
@@ -1,47 +0,0 @@
import * as React from "react";
import lazyWithRetry from "~/utils/lazyWithRetry";
export interface LazyComponent<T extends React.ComponentType<any>> {
Component: React.LazyExoticComponent<T>;
preload: () => Promise<{ default: T }>;
}
interface LazyLoadOptions {
retries?: number;
interval?: number;
}
/**
* Creates a lazy-loaded component with preloading capability and automatic retries on failure.
*
* @param factory A function that returns a promise of a component (eg: () => import('./MyComponent'))
* @param options Optional configuration for retry behavior
* @returns An object containing the lazy Component and a preload function
*
* @example
* ```typescript
* const MyComponent = createLazyComponent(() => import('./MyComponent'));
*
* function App() {
* return (
* <Suspense fallback={<div>Loading...</div>}>
* <MyComponent.Component />
* </Suspense>
* );
* }
*
* // Preload when needed:
* MyComponent.preload();
* ```
*/
export function createLazyComponent<T extends React.ComponentType<any>>(
factory: () => Promise<{ default: T }>,
options: LazyLoadOptions = {}
): LazyComponent<T> {
const { retries, interval } = options;
return {
Component: lazyWithRetry(factory, retries, interval),
preload: factory,
};
}
+1
View File
@@ -1,4 +1,5 @@
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";
+1
View File
@@ -1,4 +1,5 @@
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 { useEffect } from "react";
import * as React from "react";
import useStores from "~/hooks/useStores";
function LoadingIndicator() {
const { ui } = useStores();
useEffect(() => {
React.useEffect(() => {
ui.enableProgressBar();
return () => ui.disableProgressBar();
}, [ui]);
@@ -1,3 +1,4 @@
import * as React from "react";
import styled, { keyframes } from "styled-components";
import { depths, s } from "@shared/styles";
+1 -1
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import React from "react";
import styled from "styled-components";
import { s } from "@shared/styles";
import Flex from "./Flex";
@@ -1,5 +1,6 @@
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, AvatarVariant } from "../Avatar";
import { Avatar, AvatarSize } 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} />
<StyledAvatar model={notification.actor} size={AvatarSize.Large} />
<Flex column>
<Text as="div" size="small">
<Text weight="bold">
@@ -79,10 +79,7 @@ const StyledCommentEditor = styled(CommentEditor)`
${truncateMultiline(3)}
`;
const StyledAvatar = styled(Avatar).attrs({
variant: AvatarVariant.Round,
size: AvatarSize.Medium,
})`
const StyledAvatar = styled(Avatar)`
margin-top: 4px;
`;
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import { useEffect } from "react";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { OAuthClientValidation } from "@shared/validations";
@@ -49,7 +49,7 @@ export const OAuthClientForm = observer(function OAuthClientForm_({
},
});
useEffect(() => {
React.useEffect(() => {
setTimeout(() => setFocus("name", { shouldSelect: true }), 100);
}, [setFocus]);
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import { useCallback } from "react";
import * as React 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 = useCallback(
const handleSubmit = React.useCallback(
async (data: FormData) => {
try {
const oauthClient = await oauthClients.save(data);
+2 -2
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import * as React 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();
useEffect(() => {
React.useEffect(() => {
// wider page background beyond the React root
if (document.body) {
document.body.style.background = theme.background;
+1
View File
@@ -2,6 +2,7 @@ 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";
+5 -5
View File
@@ -17,7 +17,7 @@ import {
import fractionalIndex from "fractional-index";
import { AnimatePresence } from "framer-motion";
import { observer } from "mobx-react";
import { useState, useRef, useEffect, useCallback } from "react";
import * as React 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] = useState(pins.map((pin) => pin.documentId));
const showPlaceholderRef = useRef(true);
const [items, setItems] = React.useState(pins.map((pin) => pin.documentId));
const showPlaceholderRef = React.useRef(true);
const showPlaceholder =
placeholderCount && !items.length && showPlaceholderRef.current;
useEffect(() => {
React.useEffect(() => {
setItems(pins.map((pin) => pin.documentId));
}, [pins]);
@@ -65,7 +65,7 @@ function PinnedDocuments({
})
);
const handleDragEnd = useCallback(
const handleDragEnd = React.useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
+1
View File
@@ -1,3 +1,4 @@
import * as React from "react";
import styled from "styled-components";
import DelayedMount from "~/components/DelayedMount";
import Fade from "~/components/Fade";
+1
View File
@@ -1,4 +1,5 @@
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 -1
View File
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import { transparentize } from "polished";
import * as React from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import styled, { css } from "styled-components";
import { s, hover } from "@shared/styles";
+1 -1
View File
@@ -1,6 +1,6 @@
import compact from "lodash/compact";
import { observer } from "mobx-react";
import * as React from "react";
import React from "react";
import Comment from "~/models/Comment";
import useHover from "~/hooks/useHover";
import useStores from "~/hooks/useStores";
+3 -5
View File
@@ -1,10 +1,9 @@
import { ReactionIcon } from "outline-icons";
import * as React from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { PopoverDisclosure, usePopoverState } from "reakit";
import EventBoundary from "@shared/components/EventBoundary";
import Flex from "~/components/Flex";
import { createLazyComponent } from "~/components/LazyLoad";
import NudeButton from "~/components/NudeButton";
import PlaceholderText from "~/components/PlaceholderText";
import Popover from "~/components/Popover";
@@ -13,7 +12,7 @@ import useOnClickOutside from "~/hooks/useOnClickOutside";
import useWindowSize from "~/hooks/useWindowSize";
import Tooltip from "../Tooltip";
const EmojiPanel = createLazyComponent(
const EmojiPanel = React.lazy(
() => import("~/components/IconPicker/components/EmojiPanel")
);
@@ -105,7 +104,6 @@ const ReactionPicker: React.FC<Props> = ({
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
onMouseEnter={() => EmojiPanel.preload()}
size={size}
>
<ReactionIcon size={22} />
@@ -125,7 +123,7 @@ const ReactionPicker: React.FC<Props> = ({
{popover.visible && (
<React.Suspense fallback={<Placeholder />}>
<EventBoundary>
<EmojiPanel.Component
<EmojiPanel
height={300}
panelWidth={panelWidth}
query={query}
@@ -1,6 +1,6 @@
import compact from "lodash/compact";
import { observer } from "mobx-react";
import * as React from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { Tab, TabPanel, useTabState } from "reakit";
import { toast } from "sonner";
+3 -3
View File
@@ -1,15 +1,15 @@
import { createContext, useContext } from "react";
import * as React from "react";
/**
* Context to provide a reference to the scrollable container
*/
const ScrollContext = createContext<
const ScrollContext = React.createContext<
React.RefObject<HTMLDivElement> | undefined
>(undefined);
/**
* Hook to get the scrollable container reference
*/
export const useScrollContext = () => useContext(ScrollContext);
export const useScrollContext = () => React.useContext(ScrollContext);
export default ScrollContext;
+2 -2
View File
@@ -1,5 +1,5 @@
// based on: https://reacttraining.com/react-router/web/guides/scroll-restoration
import { useEffect } from "react";
import * as React 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();
useEffect(() => {
React.useEffect(() => {
if (
location.pathname === previousLocationPathname ||
location.state?.retainScrollPosition
+2 -2
View File
@@ -1,5 +1,5 @@
import { useKBar } from "kbar";
import { useEffect } from "react";
import * as React 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();
useEffect(() => {
React.useEffect(() => {
if (!searches.isLoaded && !searches.isFetching) {
void searches.fetchPage({
source: "app",
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import { useCallback, Fragment } from "react";
import * as React 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 = useCallback(
const handleChange = React.useCallback(
(permission: DocumentPermission | typeof EmptySelectValue) => {
if (permission === EmptySelectValue) {
onRemove?.();
@@ -68,7 +68,7 @@ const DocumentMemberListItem = ({
if (!currentPermission) {
return null;
}
const MaybeLink = membership?.source ? StyledLink : Fragment;
const MaybeLink = membership?.source ? StyledLink : React.Fragment;
return (
<ListItem
@@ -1,5 +1,5 @@
import { LinkIcon } from "outline-icons";
import { useRef, useCallback } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import CopyToClipboard from "~/components/CopyToClipboard";
@@ -14,9 +14,9 @@ export function CopyLinkButton({
onCopy: () => void;
}) {
const { t } = useTranslation();
const timeout = useRef<ReturnType<typeof setTimeout>>();
const timeout = React.useRef<ReturnType<typeof setTimeout>>();
const handleCopied = useCallback(() => {
const handleCopied = React.useCallback(() => {
onCopy();
timeout.current = setTimeout(() => {
@@ -1,3 +1,4 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "@shared/components/Flex";
@@ -1,4 +1,5 @@
import times from "lodash/times";
import * as React from "react";
import { AvatarSize } from "~/components/Avatar";
import Fade from "~/components/Fade";
import PlaceholderText from "~/components/PlaceholderText";
@@ -93,13 +93,11 @@ export const Suggestions = observer(
const suggestions = React.useMemo(() => {
const filtered: Suggestion[] = (
document
? users
.notInDocument(document.id, query)
.filter((u) => u.id !== user.id)
? users.notInDocument(document.id, query)
: collection
? users.notInCollection(collection.id, query)
: users.activeOrInvited
).filter((u) => !u.isSuspended);
).filter((u) => !u.isSuspended && u.id !== user.id);
if (isEmail(query)) {
filtered.push(getSuggestionForEmail(query));
+5 -5
View File
@@ -1,6 +1,6 @@
import { observer } from "mobx-react";
import { SearchIcon, HomeIcon, SidebarIcon } from "outline-icons";
import { useEffect, useState, useCallback, useMemo } from "react";
import * as React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
@@ -38,7 +38,7 @@ function AppSidebar() {
const user = useCurrentUser();
const can = usePolicy(team);
useEffect(() => {
React.useEffect(() => {
void collections.fetchAll();
if (!user.isViewer) {
@@ -46,9 +46,9 @@ function AppSidebar() {
}
}, [documents, collections, user.isViewer]);
const [dndArea, setDndArea] = useState();
const handleSidebarRef = useCallback((node) => setDndArea(node), []);
const html5Options = useMemo(
const [dndArea, setDndArea] = React.useState();
const handleSidebarRef = React.useCallback((node) => setDndArea(node), []);
const html5Options = React.useMemo(
() => ({
rootElement: dndArea,
}),
+5 -14
View File
@@ -1,7 +1,7 @@
import groupBy from "lodash/groupBy";
import { observer } from "mobx-react";
import { BackIcon, SidebarIcon } from "outline-icons";
import { useCallback } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
import styled from "styled-components";
@@ -23,22 +23,14 @@ import ToggleButton from "./components/ToggleButton";
import Version from "./components/Version";
function SettingsSidebar() {
const { ui, integrations } = useStores();
const { ui } = useStores();
const { t } = useTranslation();
const history = useHistory();
const location = useLocation();
const configs = useSettingsConfig();
const groupedConfig = groupBy(configs, "group");
const groupedConfig = groupBy(
configs.filter((item) =>
item.group === "Integrations" && item.pluginId
? integrations.findByService(item.pluginId)
: true
),
"group"
);
const returnToApp = useCallback(() => {
const returnToApp = React.useCallback(() => {
history.push("/home");
}, [history]);
@@ -71,9 +63,8 @@ function SettingsSidebar() {
<SidebarLink
key={item.path}
to={item.path}
onClickIntent={item.preload}
active={
item.path.startsWith(settingsPath("templates"))
item.path !== settingsPath()
? location.pathname.startsWith(item.path)
: undefined
}
+1
View File
@@ -1,5 +1,6 @@
import { observer } from "mobx-react";
import { SidebarIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { hover } from "@shared/styles";
@@ -1,7 +1,7 @@
import isUndefined from "lodash/isUndefined";
import { observer } from "mobx-react";
import { ArchiveIcon } from "outline-icons";
import { useState, useEffect, useCallback } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import Flex from "@shared/components/Flex";
import Collection from "~/models/Collection";
@@ -21,43 +21,43 @@ function ArchiveLink() {
const { collections } = useStores();
const { t } = useTranslation();
const [disclosure, setDisclosure] = useState<boolean>(false);
const [expanded, setExpanded] = useState<boolean | undefined>();
const [disclosure, setDisclosure] = React.useState<boolean>(false);
const [expanded, setExpanded] = React.useState<boolean | undefined>();
const { request, data, loading, error } = useRequest(
collections.fetchArchived,
true
);
useEffect(() => {
React.useEffect(() => {
if (!isUndefined(data) && !loading && isUndefined(error)) {
setDisclosure(data.length > 0);
}
}, [data, loading, error]);
useEffect(() => {
React.useEffect(() => {
setDisclosure(collections.archived.length > 0);
}, [collections.archived]);
useEffect(() => {
React.useEffect(() => {
if (disclosure && isUndefined(expanded)) {
setExpanded(false);
}
}, [disclosure]);
useEffect(() => {
React.useEffect(() => {
if (expanded) {
void request();
}
}, [expanded, request]);
const handleDisclosureClick = useCallback((ev) => {
const handleDisclosureClick = React.useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((e) => !e);
}, []);
const handleClick = useCallback(() => {
const handleClick = React.useCallback(() => {
setExpanded(true);
}, []);
@@ -1,4 +1,4 @@
import { useState, useCallback } from "react";
import * as React from "react";
import Collection from "~/models/Collection";
import useStores from "~/hooks/useStores";
import CollectionLink from "./CollectionLink";
@@ -13,15 +13,15 @@ type Props = {
export function ArchivedCollectionLink({ collection, depth }: Props) {
const { documents } = useStores();
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = React.useState(false);
const handleDisclosureClick = useCallback((ev) => {
const handleDisclosureClick = React.useCallback((ev) => {
ev.preventDefault();
ev.stopPropagation();
setExpanded((e) => !e);
}, []);
const handleClick = useCallback(() => {
const handleClick = React.useCallback(() => {
setExpanded(true);
}, []);
@@ -1,6 +1,6 @@
import noop from "lodash/noop";
import { observer } from "mobx-react";
import { useState, useRef, useEffect, useCallback } from "react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Waypoint } from "react-waypoint";
import styled from "styled-components";
@@ -37,8 +37,8 @@ function CollectionLinkChildren({
const { documents } = useStores();
const { t } = useTranslation();
const childDocuments = useCollectionDocuments(collection, documents.active);
const [showing, setShowing] = useState(pageSize);
const dummyRef = useRef<HTMLDivElement>(null);
const [showing, setShowing] = React.useState(pageSize);
const dummyRef = React.useRef<HTMLDivElement>(null);
const [{ isOver, canDrop }, dropRef] = useDropToChangeCollection(
collection,
@@ -46,13 +46,13 @@ function CollectionLinkChildren({
dummyRef
);
useEffect(() => {
React.useEffect(() => {
if (!expanded) {
setShowing(pageSize);
}
}, [expanded]);
const showMore = useCallback(() => {
const showMore = React.useCallback(() => {
if (childDocuments && childDocuments.length > showing) {
setShowing((value) => value + pageSize);
}
@@ -1,6 +1,6 @@
import fractionalIndex from "fractional-index";
import { observer } from "mobx-react";
import { useMemo } from "react";
import * as React from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
@@ -22,9 +22,9 @@ import SidebarContext from "./SidebarContext";
function Collections() {
const { documents, collections } = useStores();
const { t } = useTranslation();
const orderedCollections = collections.allActive;
const orderedCollections = collections.orderedData;
const params = useMemo(
const params = React.useMemo(
() => ({
limit: 100,
}),

Some files were not shown because too many files have changed in this diff Show More