Compare commits

...

24 Commits

Author SHA1 Message Date
Tom Moor 12f2a21bda Add hover state, tweak descriptions 2025-05-10 09:49:54 -04:00
Tom Moor 20769ea041 Merge public/main 2025-05-10 09:23:29 -04:00
Tom Moor fd984774d0 Add smart preloading of settings screens to reduce flicker (#9165) 2025-05-10 09:17:43 -04:00
Tom Moor e216c68f6d fix: CMD+F with in-app find interface open should open native find interface (#9153) 2025-05-08 21:40:01 -04:00
Tom Moor 14102de673 refactor 2025-05-08 21:35:18 -04:00
Tom Moor d876bf99fb Add filtering 2025-05-08 21:20:06 -04:00
Tom Moor d29bae9a14 Add breadcrumbs 2025-05-08 21:05:05 -04:00
Tom Moor d0c5ec2f39 fix: Google analytics never shows as installed
fix: Styling tweaks
Move webhooks out of integrations
2025-05-08 19:06:48 -04:00
Tom Moor 2e2a8bcc94 fix: Allow searching for current user in collection permissions (#9154) 2025-05-08 22:15:16 +00:00
Tom Moor 245d14f905 fix: Upgrade KaTeX (#9151) 2025-05-08 00:40:50 +00:00
Tom Moor 8717d160ce fix: Backlinks are limited at 25 (#9150) 2025-05-07 20:36:56 -04:00
Tom Moor 587ba85cc9 fix: LaTeX blocks show vertical scrollbar (#9149) 2025-05-08 00:17:47 +00:00
Tom Moor 80bb1ce977 fix: ExportDocumentTreeTask needs documentStructure (#9148) 2025-05-07 23:42:59 +00:00
Tess99854 cffe5c0ec6 update card status 2025-05-01 20:48:37 +01:00
Mahmoud Mohammed Ali 4d294c8cbe update IntegrationCard to use the Text component 2025-04-26 22:42:47 +01:00
Mahmoud Mohammed Ali f52634139c Update all integrations menu item 2025-04-26 22:33:47 +01:00
Mahmoud Ali 0790358fb3 Update integration icon size
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2025-04-27 00:11:13 +03:00
Mahmoud Ali 760b1041bc update integration card
Co-authored-by: Tom Moor <tom.moor@gmail.com>
2025-04-27 00:10:46 +03:00
Tess99854 2f0f966665 clean up 2025-04-24 23:53:28 +01:00
Mahmoud Mohammed Ali 62a894ca66 Integration page style update 2025-04-24 23:39:30 +01:00
Tess99854 29ec5f5a46 update design 2025-04-24 22:49:57 +01:00
Tess99854 11f2b8a53f add descriptions 2025-04-24 20:10:04 +01:00
Mahmoud Mohammed Ali a5f593d68d Integration page skeleton 2025-04-24 19:58:28 +01:00
Tess99854 b48498d07f update useSettings 2025-04-23 18:52:44 +01:00
31 changed files with 393 additions and 83 deletions
+8 -2
View File
@@ -15,6 +15,7 @@ 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";
@@ -22,7 +23,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import { EmptySelectValue } from "~/types";
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
const IconPicker = createLazyComponent(() => import("~/components/IconPicker"));
export interface FormData {
name: string;
@@ -88,6 +89,11 @@ export const CollectionForm = observer(function CollectionForm_({
const values = watch();
// Preload the IconPicker component on mount
React.useEffect(() => {
void IconPicker.preload();
}, []);
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.
@@ -202,7 +208,7 @@ export const CollectionForm = observer(function CollectionForm_({
);
});
const StyledIconPicker = styled(IconPicker)`
const StyledIconPicker = styled(IconPicker.Component)`
margin-left: 4px;
margin-right: 4px;
`;
+47
View File
@@ -0,0 +1,47 @@
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,
};
}
+4 -2
View File
@@ -4,6 +4,7 @@ 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";
@@ -12,7 +13,7 @@ import useOnClickOutside from "~/hooks/useOnClickOutside";
import useWindowSize from "~/hooks/useWindowSize";
import Tooltip from "../Tooltip";
const EmojiPanel = React.lazy(
const EmojiPanel = createLazyComponent(
() => import("~/components/IconPicker/components/EmojiPanel")
);
@@ -104,6 +105,7 @@ const ReactionPicker: React.FC<Props> = ({
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
onMouseEnter={() => EmojiPanel.preload()}
size={size}
>
<ReactionIcon size={22} />
@@ -123,7 +125,7 @@ const ReactionPicker: React.FC<Props> = ({
{popover.visible && (
<React.Suspense fallback={<Placeholder />}>
<EventBoundary>
<EmojiPanel
<EmojiPanel.Component
height={300}
panelWidth={panelWidth}
query={query}
@@ -93,11 +93,13 @@ export const Suggestions = observer(
const suggestions = React.useMemo(() => {
const filtered: Suggestion[] = (
document
? users.notInDocument(document.id, query)
? users
.notInDocument(document.id, query)
.filter((u) => u.id !== user.id)
: collection
? users.notInCollection(collection.id, query)
: users.activeOrInvited
).filter((u) => !u.isSuspended && u.id !== user.id);
).filter((u) => !u.isSuspended);
if (isEmail(query)) {
filtered.push(getSuggestionForEmail(query));
+8 -2
View File
@@ -27,7 +27,12 @@ function SettingsSidebar() {
const { t } = useTranslation();
const history = useHistory();
const location = useLocation();
const configs = useSettingsConfig();
let configs = useSettingsConfig();
configs = configs.filter((item) =>
"isActive" in item ? item.isActive : true
);
const groupedConfig = groupBy(configs, "group");
const returnToApp = React.useCallback(() => {
@@ -63,8 +68,9 @@ function SettingsSidebar() {
<SidebarLink
key={item.path}
to={item.path}
onClickIntent={item.preload}
active={
item.path !== settingsPath()
item.path.startsWith(settingsPath("templates"))
? location.pathname.startsWith(item.path)
: undefined
}
+1
View File
@@ -41,6 +41,7 @@ function useKeyboardShortcuts({
useKeyDown(
(ev) =>
isModKey(ev) &&
!popover.visible &&
ev.code === "KeyF" &&
// Keyboard handler is through the AppMenu on Desktop v1.2.0+
!(Desktop.bridge && "onFindInPage" in Desktop.bridge),
+59 -18
View File
@@ -13,20 +13,23 @@ import {
ImportIcon,
ShapesIcon,
Icon,
PlusIcon,
InternetIcon,
} from "outline-icons";
import React, { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
import { Integrations } from "~/scenes/Settings/Integrations";
import ZapierIcon from "~/components/Icons/ZapierIcon";
import { createLazyComponent as lazy } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import isCloudHosted from "~/utils/isCloudHosted";
import lazy from "~/utils/lazyWithRetry";
import { settingsPath } from "~/utils/routeHelpers";
import { useComputed } from "./useComputed";
import useCurrentTeam from "./useCurrentTeam";
import useCurrentUser from "./useCurrentUser";
import usePolicy from "./usePolicy";
import useStores from "./useStores";
const ApiKeys = lazy(() => import("~/scenes/Settings/ApiKeys"));
const Applications = lazy(() => import("~/scenes/Settings/Applications"));
@@ -50,23 +53,32 @@ export type ConfigItem = {
path: string;
icon: React.FC<ComponentProps<typeof Icon>>;
component: React.ComponentType;
description?: string;
preload?: () => void;
enabled: boolean;
group: string;
isActive?: boolean;
};
const useSettingsConfig = () => {
const { integrations } = useStores();
const user = useCurrentUser();
const team = useCurrentTeam();
const can = usePolicy(team);
const { t } = useTranslation();
React.useEffect(() => {
void integrations.fetchAll();
}, [integrations]);
const config = useComputed(() => {
const items: ConfigItem[] = [
// Account
{
name: t("Profile"),
path: settingsPath(),
component: Profile,
component: Profile.Component,
preload: Profile.preload,
enabled: true,
group: t("Account"),
icon: ProfileIcon,
@@ -74,7 +86,8 @@ const useSettingsConfig = () => {
{
name: t("Preferences"),
path: settingsPath("preferences"),
component: Preferences,
component: Preferences.Component,
preload: Preferences.preload,
enabled: true,
group: t("Account"),
icon: SettingsIcon,
@@ -82,7 +95,8 @@ const useSettingsConfig = () => {
{
name: t("Notifications"),
path: settingsPath("notifications"),
component: Notifications,
component: Notifications.Component,
preload: Notifications.preload,
enabled: true,
group: t("Account"),
icon: EmailIcon,
@@ -90,7 +104,8 @@ const useSettingsConfig = () => {
{
name: t("API & Apps"),
path: settingsPath("api-and-apps"),
component: APIAndApps,
component: APIAndApps.Component,
preload: APIAndApps.preload,
enabled: true,
group: t("Account"),
icon: PadlockIcon,
@@ -99,7 +114,8 @@ const useSettingsConfig = () => {
{
name: t("Details"),
path: settingsPath("details"),
component: Details,
component: Details.Component,
preload: Details.preload,
enabled: can.update,
group: t("Workspace"),
icon: TeamIcon,
@@ -107,7 +123,8 @@ const useSettingsConfig = () => {
{
name: t("Security"),
path: settingsPath("security"),
component: Security,
component: Security.Component,
preload: Security.preload,
enabled: can.update,
group: t("Workspace"),
icon: PadlockIcon,
@@ -115,7 +132,8 @@ const useSettingsConfig = () => {
{
name: t("Features"),
path: settingsPath("features"),
component: Features,
component: Features.Component,
preload: Features.preload,
enabled: can.update,
group: t("Workspace"),
icon: BeakerIcon,
@@ -123,7 +141,8 @@ const useSettingsConfig = () => {
{
name: t("Members"),
path: settingsPath("members"),
component: Members,
component: Members.Component,
preload: Members.preload,
enabled: can.listUsers,
group: t("Workspace"),
icon: UserIcon,
@@ -131,7 +150,8 @@ const useSettingsConfig = () => {
{
name: t("Groups"),
path: settingsPath("groups"),
component: Groups,
component: Groups.Component,
preload: Groups.preload,
enabled: can.listGroups,
group: t("Workspace"),
icon: GroupIcon,
@@ -139,7 +159,8 @@ const useSettingsConfig = () => {
{
name: t("Templates"),
path: settingsPath("templates"),
component: Templates,
component: Templates.Component,
preload: Templates.preload,
enabled: can.readTemplate,
group: t("Workspace"),
icon: ShapesIcon,
@@ -147,7 +168,8 @@ const useSettingsConfig = () => {
{
name: t("API Keys"),
path: settingsPath("api-keys"),
component: ApiKeys,
component: ApiKeys.Component,
preload: ApiKeys.preload,
enabled: can.listApiKeys,
group: t("Workspace"),
icon: CodeIcon,
@@ -155,7 +177,8 @@ const useSettingsConfig = () => {
{
name: t("Applications"),
path: settingsPath("applications"),
component: Applications,
component: Applications.Component,
preload: Applications.preload,
enabled: can.listOAuthClients,
group: t("Workspace"),
icon: InternetIcon,
@@ -163,7 +186,8 @@ const useSettingsConfig = () => {
{
name: t("Shared Links"),
path: settingsPath("shares"),
component: Shares,
component: Shares.Component,
preload: Shares.preload,
enabled: can.listShares,
group: t("Workspace"),
icon: GlobeIcon,
@@ -171,7 +195,8 @@ const useSettingsConfig = () => {
{
name: t("Import"),
path: settingsPath("import"),
component: Import,
component: Import.Component,
preload: Import.preload,
enabled: can.createImport,
group: t("Workspace"),
icon: ImportIcon,
@@ -179,7 +204,8 @@ const useSettingsConfig = () => {
{
name: t("Export"),
path: settingsPath("export"),
component: Export,
component: Export.Component,
preload: Export.preload,
enabled: can.createExport,
group: t("Workspace"),
icon: ExportIcon,
@@ -188,11 +214,20 @@ const useSettingsConfig = () => {
{
name: "Zapier",
path: integrationSettingsPath("zapier"),
component: Zapier,
component: Zapier.Component,
preload: Zapier.preload,
enabled: can.update && isCloudHosted,
group: t("Integrations"),
icon: ZapierIcon,
},
{
name: `${t("Install")}`,
path: settingsPath("integrations"),
component: Integrations,
enabled: true,
group: t("Integrations"),
icon: PlusIcon,
},
];
// Plugins
@@ -208,11 +243,17 @@ const useSettingsConfig = () => {
? integrationSettingsPath(plugin.id)
: settingsPath(plugin.id),
group: t(group),
component: plugin.value.component,
description: plugin.value.description,
component: plugin.value.component.Component,
preload: plugin.value.component.preload,
enabled: plugin.value.enabled
? plugin.value.enabled(team, user)
: can.update,
icon: plugin.value.icon,
isActive: integrations.orderedData.some(
(integration) =>
integration.service.toLowerCase() === plugin.id.toLowerCase()
),
} as ConfigItem);
});
+62
View File
@@ -0,0 +1,62 @@
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import Flex from "@shared/components/Flex";
import Heading from "~/components/Heading";
import InputSearch from "~/components/InputSearch";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import useSettingsConfig from "~/hooks/useSettingsConfig";
import { settingsPath } from "~/utils/routeHelpers";
import IntegrationCard from "./components/IntegrationCard";
import { StickyFilters } from "./components/StickyFilters";
export function Integrations() {
const { t } = useTranslation();
let items = useSettingsConfig();
const [query, setQuery] = React.useState("");
const handleQuery = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
};
items = items
.filter(
(item) =>
item.group === "Integrations" &&
item.enabled &&
item.path !== settingsPath("integrations") &&
item.name.toLowerCase().includes(query.toLowerCase())
)
.sort((item) => (item.isActive ? -1 : 1));
return (
<Scene title={t("Integrations")}>
<Heading>{t("Integrations")}</Heading>
<Text as="p" type="secondary">
<Trans>
Configure a variety of integrations with third-party services.
</Trans>
</Text>
<StickyFilters gap={8}>
<InputSearch
short
value={query}
placeholder={`${t("Filter")}`}
onChange={handleQuery}
/>
</StickyFilters>
<CardsFlex gap={30} wrap>
{items.map((item) => (
<IntegrationCard key={item.path} integration={item} />
))}
</CardsFlex>
</Scene>
);
}
const CardsFlex = styled(Flex)`
margin-top: 20px;
width: "100%";
`;
@@ -0,0 +1,91 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { s, ellipsis } from "@shared/styles";
import { ConfigItem } from "~/hooks/useSettingsConfig";
import Button from "../../../components/Button";
import Flex from "../../../components/Flex";
import Text from "../../../components/Text";
type Props = {
integration: ConfigItem;
};
function IntegrationCard({ integration }: Props) {
const { t } = useTranslation();
return (
<Card as={Link} to={integration.path}>
<Flex align="center" gap={8}>
<integration.icon size={48} />
<Flex auto column>
<Name>{integration.name}</Name>
{integration.isActive && <Status>{t("Connected")}</Status>}
</Flex>
<Button as={Link} to={integration.path} neutral>
{integration.isActive ? t("Configure") : t("Connect")}
</Button>
</Flex>
<Description>{integration.description}</Description>
</Card>
);
}
export default IntegrationCard;
const Card = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 20px;
width: 300px;
background: ${s("background")};
border: 1px solid ${s("inputBorder")};
color: ${s("text")};
border-radius: 8px;
transition: box-shadow 200ms ease;
cursor: var(--pointer);
&:hover {
box-shadow: rgba(0, 0, 0, 0.08) 0px 2px 4px, rgba(0, 0, 0, 0.06) 0px 4px 8px;
}
`;
const Name = styled(Text)`
margin: 0;
font-size: 16px;
font-weight: 600;
color: ${s("text")};
${ellipsis()}
`;
const Description = styled(Text)`
margin: 8px 0 0;
font-size: 15px;
max-width: 100%;
color: ${s("textTertiary")};
`;
const Status = styled(Text).attrs({
type: "secondary",
size: "small",
as: "span",
})`
display: inline-flex;
align-items: center;
&::after {
content: "";
display: inline-block;
width: 17px;
height: 17px;
background: radial-gradient(
circle at center,
${s("accent")} 0 33%,
transparent 33%
);
border-radius: 50%;
}
`;
@@ -0,0 +1,33 @@
import { SettingsIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import Breadcrumb from "~/components/Breadcrumb";
import Scene from "~/components/Scene";
import { settingsPath } from "~/utils/routeHelpers";
export function IntegrationScene({
children,
...rest
}: React.ComponentProps<typeof Scene>) {
const { t } = useTranslation();
return (
<Scene
left={
<Breadcrumb
items={[
{
type: "route",
title: t("Integrations"),
icon: <SettingsIcon />,
to: settingsPath("integrations"),
},
]}
/>
}
{...rest}
>
{children}
</Scene>
);
}
+2 -7
View File
@@ -279,19 +279,14 @@ export default class DocumentsStore extends Store<Document> {
@action
fetchBacklinks = async (documentId: string): Promise<void> => {
const res = await client.post(`/documents.list`, {
const documents = await this.fetchAll({
backlinkDocumentId: documentId,
});
invariant(res?.data, "Document list not available");
const { data } = res;
runInAction("DocumentsStore#fetchBacklinks", () => {
data.forEach(this.add);
this.addPolicies(res.policies);
this.backlinks.set(
documentId,
data.map((doc: Partial<Document>) => doc.id)
documents.map((doc) => doc.id)
);
});
};
+5 -2
View File
@@ -3,6 +3,7 @@ import sortBy from "lodash/sortBy";
import { action, observable } from "mobx";
import Team from "~/models/Team";
import User from "~/models/User";
import { LazyComponent } from "~/components/LazyLoad";
import { useComputed } from "~/hooks/useComputed";
import Logger from "./Logger";
import isCloudHosted from "./isCloudHosted";
@@ -27,8 +28,10 @@ type PluginValueMap = {
after?: string;
/** The displayed icon of the plugin. */
icon: React.ElementType;
/** The settings screen somponent, should be lazy loaded. */
component: React.LazyExoticComponent<React.ComponentType>;
/** The lazy loaded settings screen component. */
component: LazyComponent<React.ComponentType>;
/** The description that will show on the plugins card. */
description?: string;
/** Whether the plugin is enabled in the current context. */
enabled?: (team: Team, user: User) => boolean;
};
+1 -1
View File
@@ -141,7 +141,7 @@
"jsdom": "^22.1.0",
"jsonwebtoken": "^9.0.0",
"jszip": "^3.10.1",
"katex": "^0.16.21",
"katex": "^0.16.22",
"kbar": "0.1.0-beta.41",
"koa": "^2.16.1",
"koa-body": "^6.0.1",
+3 -3
View File
@@ -4,6 +4,7 @@ import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { IntegrationService } from "@shared/types";
import { ConnectedButton } from "~/scenes/Settings/components/ConnectedButton";
import { IntegrationScene } from "~/scenes/Settings/components/IntegrationScene";
import { AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
@@ -11,7 +12,6 @@ import List from "~/components/List";
import ListItem from "~/components/List/Item";
import Notice from "~/components/Notice";
import PlaceholderText from "~/components/PlaceholderText";
import Scene from "~/components/Scene";
import TeamLogo from "~/components/TeamLogo";
import Text from "~/components/Text";
import Time from "~/components/Time";
@@ -38,7 +38,7 @@ function GitHub() {
}, [integrations]);
return (
<Scene title="GitHub" icon={<GitHubIcon />}>
<IntegrationScene title="GitHub" icon={<GitHubIcon />}>
<Heading>GitHub</Heading>
{error === "access_denied" && (
@@ -146,7 +146,7 @@ function GitHub() {
</Trans>
</Notice>
)}
</Scene>
</IntegrationScene>
);
}
+4 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -10,7 +10,9 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
description:
"Connect your GitHub account to Outline to enable rich, realtime, issue and pull request previews inside documents.",
component: createLazyComponent(() => import("./Settings")),
},
},
]);
+3 -3
View File
@@ -6,12 +6,12 @@ import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import { IntegrationType, IntegrationService } from "@shared/types";
import Integration from "~/models/Integration";
import { IntegrationScene } from "~/scenes/Settings/components/IntegrationScene";
import SettingRow from "~/scenes/Settings/components/SettingRow";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import GoogleIcon from "~/components/Icons/GoogleIcon";
import Input from "~/components/Input";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
@@ -75,7 +75,7 @@ function GoogleAnalytics() {
);
return (
<Scene title={t("Google Analytics")} icon={<GoogleIcon />}>
<IntegrationScene title={t("Google Analytics")} icon={<GoogleIcon />}>
<Heading>{t("Google Analytics")}</Heading>
<Text as="p" type="secondary">
@@ -100,7 +100,7 @@ function GoogleAnalytics() {
{formState.isSubmitting ? `${t("Saving")}` : t("Save")}
</Button>
</form>
</Scene>
</IntegrationScene>
);
}
+4 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -10,7 +10,9 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
description:
"Measure adoption and engagement by sending view and event analytics directly to your GA4 dashboard.",
component: createLazyComponent(() => import("./Settings")),
},
},
]);
+1 -1
View File
@@ -1,5 +1,5 @@
{
"id": "googleanalytics",
"id": "google-analytics",
"name": "Google Analytics",
"priority": 30,
"description": "Adds support for reporting analytics to a Google."
+4 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -10,7 +10,9 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
description:
"Connect your Linear account to Outline to enable rich, realtime, issue previews inside documents.",
component: createLazyComponent(() => import("./Settings")),
},
},
]);
+3 -3
View File
@@ -6,11 +6,11 @@ import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import { IntegrationType, IntegrationService } from "@shared/types";
import Integration from "~/models/Integration";
import { IntegrationScene } from "~/scenes/Settings/components/IntegrationScene";
import SettingRow from "~/scenes/Settings/components/SettingRow";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import Input from "~/components/Input";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import Icon from "./Icon";
@@ -82,7 +82,7 @@ function Matomo() {
);
return (
<Scene title="Matomo" icon={<Icon />}>
<IntegrationScene title="Matomo" icon={<Icon />}>
<Heading>Matomo</Heading>
<Text as="p" type="secondary">
@@ -121,7 +121,7 @@ function Matomo() {
{formState.isSubmitting ? `${t("Saving")}` : t("Save")}
</Button>
</form>
</Scene>
</IntegrationScene>
);
}
+4 -2
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { UserRole } from "@shared/types";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -11,7 +11,9 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
description:
"Track your docs with a self-hosted, open-source analytics platform, link Outline to Matomo for 100% data ownership, GDPR compliance, and deep usage insights on your own servers.",
component: createLazyComponent(() => import("./Settings")),
enabled: (_, user) => user.role === UserRole.Admin,
},
},
+3 -3
View File
@@ -6,6 +6,7 @@ import { IntegrationService, IntegrationType } from "@shared/types";
import Collection from "~/models/Collection";
import Integration from "~/models/Integration";
import { ConnectedButton } from "~/scenes/Settings/components/ConnectedButton";
import { IntegrationScene } from "~/scenes/Settings/components/IntegrationScene";
import SettingRow from "~/scenes/Settings/components/SettingRow";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
@@ -13,7 +14,6 @@ import CollectionIcon from "~/components/Icons/CollectionIcon";
import List from "~/components/List";
import ListItem from "~/components/List/Item";
import Notice from "~/components/Notice";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
@@ -67,7 +67,7 @@ function Slack() {
const appName = env.APP_NAME;
return (
<Scene title="Slack" icon={<SlackIcon />}>
<IntegrationScene title="Slack" icon={<SlackIcon />}>
<Heading>Slack</Heading>
{error === "access_denied" && (
@@ -205,7 +205,7 @@ function Slack() {
</List>
</>
)}
</Scene>
</IntegrationScene>
);
}
+4 -2
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { UserRole } from "@shared/types";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -11,7 +11,9 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
description:
"Search your knowledge base directly in Slack, get /outline search, rich link previews, and notifications on new or updated docs.",
component: createLazyComponent(() => import("./Settings")),
enabled: (_, user) =>
[UserRole.Member, UserRole.Admin].includes(user.role),
},
+3 -3
View File
@@ -6,11 +6,11 @@ import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import { IntegrationType, IntegrationService } from "@shared/types";
import Integration from "~/models/Integration";
import { IntegrationScene } from "~/scenes/Settings/components/IntegrationScene";
import SettingRow from "~/scenes/Settings/components/SettingRow";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import Input from "~/components/Input";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";
import Icon from "./Icon";
@@ -85,7 +85,7 @@ function Umami() {
);
return (
<Scene title="Umami" icon={<Icon />}>
<IntegrationScene title="Umami" icon={<Icon />}>
<Heading>Umami</Heading>
<Text as="p" type="secondary">
@@ -145,7 +145,7 @@ function Umami() {
{formState.isSubmitting ? `${t("Saving")}` : t("Save")}
</Button>
</form>
</Scene>
</IntegrationScene>
);
}
+4 -2
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { UserRole } from "@shared/types";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -11,7 +11,9 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
description:
"Gain privacy-first insights into how your team consumes docs, inject your self-hosted Umami script across Outline pages to track views and engagement while retaining full control of your data.",
component: createLazyComponent(() => import("./Settings")),
enabled: (_, user) => user.role === UserRole.Admin,
},
},
+6 -3
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -8,9 +8,12 @@ PluginManager.add([
...config,
type: Hook.Settings,
value: {
group: "Integrations",
group: "Workspace",
after: "Shared Links",
icon: Icon,
component: React.lazy(() => import("./Settings")),
description:
"Automate downstream workflows with real-time JSON POSTs, subscribe to events in Outline so external systems can react instantly.",
component: createLazyComponent(() => import("./Settings")),
},
},
]);
@@ -171,7 +171,8 @@ export default abstract class ExportDocumentTreeTask extends ExportTask {
/**
* Generates a map of document urls to their path in the zip file.
*
* @param collections
* @param collections The collections to generate the path map for.
* @param format The format of the exported documents.
*/
private createPathMap(
collections: Collection[],
+7 -5
View File
@@ -44,11 +44,13 @@ export default abstract class ExportTask extends BaseTask<Props> {
? [fileOperation.collectionId]
: await user.collectionIds();
const collections = await Collection.findAll({
where: {
id: collectionIds,
},
});
const collections = await Collection.scope("withDocumentStructure").findAll(
{
where: {
id: collectionIds,
},
}
);
let filePath: string | undefined;
+1 -1
View File
@@ -55,7 +55,7 @@ const mathStyle = (props: Props) => css`
cursor: auto;
white-space: pre-wrap;
overflow-x: auto;
overflow-y: none;
overflow-y: hidden;
}
.math-node.empty-math .math-render::before {
+4 -1
View File
@@ -529,6 +529,7 @@
"Shared Links": "Shared Links",
"Import": "Import",
"Integrations": "Integrations",
"Install": "Install",
"Revoke token": "Revoke token",
"Revoke": "Revoke",
"Show path to document": "Show path to document",
@@ -974,6 +975,8 @@
"{{ count }} document imported_plural": "{{ count }} documents imported",
"You can import a zip file that was previously exported from an Outline installation collections, documents, and images will be imported. In Outline, open <em>Export</em> in the Settings sidebar and click on <em>Export Data</em>.": "You can import a zip file that was previously exported from an Outline installation collections, documents, and images will be imported. In Outline, open <em>Export</em> in the Settings sidebar and click on <em>Export Data</em>.",
"Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload": "Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload",
"Configure": "Configure",
"Connect": "Connect",
"Last active": "Last active",
"Guest": "Guest",
"Never used": "Never used",
@@ -1034,6 +1037,7 @@
"Enterprise": "Enterprise",
"Quickly transfer your existing documents, pages, and files from other tools and services into {{appName}}. You can also drag and drop any HTML, Markdown, and text documents directly into Collections in the app.": "Quickly transfer your existing documents, pages, and files from other tools and services into {{appName}}. You can also drag and drop any HTML, Markdown, and text documents directly into Collections in the app.",
"Recent imports": "Recent imports",
"Configure a variety of integrations with third-party services.": "Configure a variety of integrations with third-party services.",
"Could not load members": "Could not load members",
"Everyone that has signed into {{appName}} is listed here. Its possible that there are other users who have access through {{signinMethods}} but havent signed in yet.": "Everyone that has signed into {{appName}} is listed here. Its possible that there are other users who have access through {{signinMethods}} but havent signed in yet.",
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
@@ -1144,7 +1148,6 @@
"Expires today": "Expires today",
"Expires tomorrow": "Expires tomorrow",
"Expires {{ date }}": "Expires {{ date }}",
"Connect": "Connect",
"Whoops, you need to accept the permissions in GitHub to connect {{appName}} to your workspace. Try again?": "Whoops, you need to accept the permissions in GitHub to connect {{appName}} to your workspace. Try again?",
"Something went wrong while authenticating your request. Please try logging in again.": "Something went wrong while authenticating your request. Please try logging in again.",
"The owner of GitHub account has been requested to install the {{githubAppName}} GitHub app. Once approved, previews will be shown for respective links.": "The owner of GitHub account has been requested to install the {{githubAppName}} GitHub app. Once approved, previews will be shown for respective links.",
+8 -8
View File
@@ -737,7 +737,7 @@
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.22.5", "@babel/helper-annotate-as-pure@^7.25.9", "@babel/helper-annotate-as-pure@^7.27.1":
"@babel/helper-annotate-as-pure@^7.22.5", "@babel/helper-annotate-as-pure@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz#4345d81a9a46a6486e24d069469f13e60445c05d"
integrity sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==
@@ -834,7 +834,7 @@
dependencies:
"@babel/types" "^7.27.1"
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0":
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c"
integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==
@@ -1663,7 +1663,7 @@
"@babel/parser" "^7.27.1"
"@babel/types" "^7.27.1"
"@babel/traverse@^7.13.0", "@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0":
"@babel/traverse@^7.13.0", "@babel/traverse@^7.27.1", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.1.tgz#4db772902b133bbddd1c4f7a7ee47761c1b9f291"
integrity sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==
@@ -1676,7 +1676,7 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.25.9", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
"@babel/types@^7.0.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560"
integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==
@@ -11162,10 +11162,10 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
katex@^0.16.21, katex@^0.16.9:
version "0.16.21"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.21.tgz#8f63c659e931b210139691f2cc7bb35166b792a3"
integrity sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==
katex@^0.16.22, katex@^0.16.9:
version "0.16.22"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.22.tgz#d2b3d66464b1e6d69e6463b28a86ced5a02c5ccd"
integrity sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==
dependencies:
commander "^8.3.0"