mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dac8843934 | |||
| 30108ebded | |||
| d0bd2baa9f | |||
| fd984774d0 | |||
| e216c68f6d | |||
| 2e2a8bcc94 | |||
| 245d14f905 | |||
| 8717d160ce | |||
| 587ba85cc9 | |||
| 80bb1ce977 |
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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,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));
|
||||
|
||||
@@ -23,12 +23,20 @@ import ToggleButton from "./components/ToggleButton";
|
||||
import Version from "./components/Version";
|
||||
|
||||
function SettingsSidebar() {
|
||||
const { ui } = useStores();
|
||||
const { ui, integrations } = 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 = React.useCallback(() => {
|
||||
history.push("/home");
|
||||
@@ -63,8 +71,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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -18,8 +18,7 @@ export default function useEmbeds(loadIfMissing = false) {
|
||||
React.useEffect(() => {
|
||||
async function fetchEmbedIntegrations() {
|
||||
try {
|
||||
await integrations.fetchPage({
|
||||
limit: 100,
|
||||
await integrations.fetchAll({
|
||||
type: IntegrationType.Embed,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -13,20 +13,21 @@ 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 ZapierIcon from "~/components/Icons/ZapierIcon";
|
||||
import { Integrations } from "~/scenes/Settings/Integrations";
|
||||
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"));
|
||||
@@ -43,30 +44,38 @@ const Profile = lazy(() => import("~/scenes/Settings/Profile"));
|
||||
const Security = lazy(() => import("~/scenes/Settings/Security"));
|
||||
const Shares = lazy(() => import("~/scenes/Settings/Shares"));
|
||||
const Templates = lazy(() => import("~/scenes/Settings/Templates"));
|
||||
const Zapier = lazy(() => import("~/scenes/Settings/Zapier"));
|
||||
|
||||
export type ConfigItem = {
|
||||
name: string;
|
||||
path: string;
|
||||
icon: React.FC<ComponentProps<typeof Icon>>;
|
||||
component: React.ComponentType;
|
||||
description?: string;
|
||||
preload?: () => void;
|
||||
enabled: boolean;
|
||||
group: string;
|
||||
pluginId?: string;
|
||||
};
|
||||
|
||||
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 +83,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 +92,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 +101,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 +111,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 +120,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 +129,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 +138,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 +147,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 +156,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 +165,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 +174,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 +183,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 +192,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,19 +201,20 @@ const useSettingsConfig = () => {
|
||||
{
|
||||
name: t("Export"),
|
||||
path: settingsPath("export"),
|
||||
component: Export,
|
||||
component: Export.Component,
|
||||
preload: Export.preload,
|
||||
enabled: can.createExport,
|
||||
group: t("Workspace"),
|
||||
icon: ExportIcon,
|
||||
},
|
||||
// Integrations
|
||||
{
|
||||
name: "Zapier",
|
||||
path: integrationSettingsPath("zapier"),
|
||||
component: Zapier,
|
||||
enabled: can.update && isCloudHosted,
|
||||
name: `${t("Install")}…`,
|
||||
path: settingsPath("integrations"),
|
||||
component: Integrations,
|
||||
enabled: true,
|
||||
group: t("Integrations"),
|
||||
icon: ZapierIcon,
|
||||
icon: PlusIcon,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -208,7 +231,10 @@ const useSettingsConfig = () => {
|
||||
? integrationSettingsPath(plugin.id)
|
||||
: settingsPath(plugin.id),
|
||||
group: t(group),
|
||||
component: plugin.value.component,
|
||||
pluginId: plugin.id,
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import groupBy from "lodash/groupBy";
|
||||
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 useStores from "~/hooks/useStores";
|
||||
import { settingsPath } from "~/utils/routeHelpers";
|
||||
import IntegrationCard from "./components/IntegrationCard";
|
||||
import { StickyFilters } from "./components/StickyFilters";
|
||||
|
||||
export function Integrations() {
|
||||
const { t } = useTranslation();
|
||||
const { integrations } = useStores();
|
||||
const items = useSettingsConfig();
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
const handleQuery = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setQuery(event.target.value);
|
||||
};
|
||||
|
||||
const groupedItems = groupBy(
|
||||
items.filter(
|
||||
(item) =>
|
||||
item.group === "Integrations" &&
|
||||
item.enabled &&
|
||||
item.path !== settingsPath("integrations") &&
|
||||
item.name.toLowerCase().includes(query.toLowerCase())
|
||||
),
|
||||
(item) =>
|
||||
item.pluginId && integrations.findByService(item.pluginId)
|
||||
? "connected"
|
||||
: "available"
|
||||
);
|
||||
|
||||
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>
|
||||
|
||||
<Cards gap={30} wrap>
|
||||
{groupedItems.connected.map((item) => (
|
||||
<IntegrationCard key={item.path} integration={item} isConnected />
|
||||
))}
|
||||
{groupedItems.available.map((item) => (
|
||||
<IntegrationCard key={item.path} integration={item} />
|
||||
))}
|
||||
</Cards>
|
||||
</Scene>
|
||||
);
|
||||
}
|
||||
|
||||
const Cards = styled(Flex)`
|
||||
margin-top: 20px;
|
||||
width: "100%";
|
||||
`;
|
||||
@@ -0,0 +1,93 @@
|
||||
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;
|
||||
isConnected?: boolean;
|
||||
};
|
||||
|
||||
function IntegrationCard({ integration, isConnected }: 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>
|
||||
{isConnected && <Status>{t("Connected")}</Status>}
|
||||
</Flex>
|
||||
<Button as="span" neutral>
|
||||
{isConnected ? 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>
|
||||
);
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -10,6 +10,12 @@ class IntegrationsStore extends Store<Integration> {
|
||||
super(rootStore, Integration);
|
||||
}
|
||||
|
||||
findByService(service: string) {
|
||||
return this.orderedData.find(
|
||||
(integration) => integration.service === service
|
||||
);
|
||||
}
|
||||
|
||||
@computed
|
||||
get orderedData(): Integration[] {
|
||||
return naturalSort(Array.from(this.data.values()), "name");
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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")),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -40,12 +40,6 @@ function GoogleAnalytics() {
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
void integrations.fetchPage({
|
||||
type: IntegrationType.Analytics,
|
||||
});
|
||||
}, [integrations]);
|
||||
|
||||
React.useEffect(() => {
|
||||
reset({ measurementId: integration?.settings.measurementId });
|
||||
}, [integration, reset]);
|
||||
@@ -75,7 +69,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 +94,7 @@ function GoogleAnalytics() {
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
</Scene>
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"id": "googleanalytics",
|
||||
"id": "google-analytics",
|
||||
"name": "Google Analytics",
|
||||
"priority": 30,
|
||||
"description": "Adds support for reporting analytics to a Google."
|
||||
|
||||
@@ -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")),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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";
|
||||
@@ -42,12 +42,6 @@ function Matomo() {
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
void integrations.fetchPage({
|
||||
type: IntegrationType.Analytics,
|
||||
});
|
||||
}, [integrations]);
|
||||
|
||||
React.useEffect(() => {
|
||||
reset({
|
||||
measurementId: integration?.settings.measurementId,
|
||||
@@ -82,7 +76,7 @@ function Matomo() {
|
||||
);
|
||||
|
||||
return (
|
||||
<Scene title="Matomo" icon={<Icon />}>
|
||||
<IntegrationScene title="Matomo" icon={<Icon />}>
|
||||
<Heading>Matomo</Heading>
|
||||
|
||||
<Text as="p" type="secondary">
|
||||
@@ -121,7 +115,7 @@ function Matomo() {
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
</Scene>
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
@@ -34,13 +34,7 @@ function Slack() {
|
||||
const error = query.get("error");
|
||||
|
||||
React.useEffect(() => {
|
||||
void collections.fetchPage({
|
||||
limit: 100,
|
||||
});
|
||||
void integrations.fetchPage({
|
||||
service: IntegrationService.Slack,
|
||||
limit: 100,
|
||||
});
|
||||
void collections.fetchAll();
|
||||
}, [collections, integrations]);
|
||||
|
||||
const commandIntegration = integrations.find({
|
||||
@@ -67,7 +61,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 +199,7 @@ function Slack() {
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
</Scene>
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
@@ -44,12 +44,6 @@ function Umami() {
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
void integrations.fetchPage({
|
||||
type: IntegrationType.Analytics,
|
||||
});
|
||||
}, [integrations]);
|
||||
|
||||
React.useEffect(() => {
|
||||
reset({
|
||||
umamiWebsiteId: integration?.settings.measurementId,
|
||||
@@ -85,7 +79,7 @@ function Umami() {
|
||||
);
|
||||
|
||||
return (
|
||||
<Scene title="Umami" icon={<Icon />}>
|
||||
<IntegrationScene title="Umami" icon={<Icon />}>
|
||||
<Heading>Umami</Heading>
|
||||
|
||||
<Text as="p" type="secondary">
|
||||
@@ -145,7 +139,7 @@ function Umami() {
|
||||
{formState.isSubmitting ? `${t("Saving")}…` : t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
</Scene>
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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")),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Trans } from "react-i18next";
|
||||
import { IntegrationScene } from "~/scenes/Settings/components/IntegrationScene";
|
||||
import Heading from "~/components/Heading";
|
||||
import ZapierIcon from "~/components/Icons/ZapierIcon";
|
||||
import Scene from "~/components/Scene";
|
||||
import Text from "~/components/Text";
|
||||
import env from "~/env";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import ZapierIcon from "./Icon";
|
||||
|
||||
function Zapier() {
|
||||
const { ui } = useStores();
|
||||
const user = useCurrentUser();
|
||||
const { resolvedTheme } = ui;
|
||||
const appName = env.APP_NAME;
|
||||
|
||||
return (
|
||||
<Scene title="Zapier" icon={<ZapierIcon />}>
|
||||
<IntegrationScene title="Zapier" icon={<ZapierIcon />}>
|
||||
<Heading>Zapier</Heading>
|
||||
<Helmet>
|
||||
<script
|
||||
@@ -39,14 +41,15 @@ function Zapier() {
|
||||
<zapier-app-directory
|
||||
app="outline"
|
||||
link-target="new-tab"
|
||||
theme={resolvedTheme}
|
||||
sign-up-email={user.email}
|
||||
theme={resolvedTheme === "system" ? undefined : resolvedTheme}
|
||||
hide="notion,confluence-cloud,confluence,google-docs,slack"
|
||||
applimit={6}
|
||||
introcopy="hide"
|
||||
create-without-template="show"
|
||||
use-this-zap="show"
|
||||
/>
|
||||
</Scene>
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createLazyComponent } from "~/components/LazyLoad";
|
||||
import { Hook, PluginManager } from "~/utils/PluginManager";
|
||||
import config from "../plugin.json";
|
||||
import Icon from "./Icon";
|
||||
|
||||
PluginManager.add([
|
||||
{
|
||||
...config,
|
||||
type: Hook.Settings,
|
||||
value: {
|
||||
group: "Integrations",
|
||||
icon: Icon,
|
||||
description:
|
||||
"Connect your Outline workspace to Zapier to automate workflows and integrate with thousands of other tools.",
|
||||
component: createLazyComponent(() => import("./Settings")),
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "zapier",
|
||||
"name": "Zapier",
|
||||
"description": "Adds a settings screen for connecting to Zapier.",
|
||||
"deployments": ["cloud"]
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -528,6 +528,7 @@
|
||||
"Applications": "Applications",
|
||||
"Shared Links": "Shared Links",
|
||||
"Import": "Import",
|
||||
"Install": "Install",
|
||||
"Integrations": "Integrations",
|
||||
"Revoke token": "Revoke token",
|
||||
"Revoke": "Revoke",
|
||||
@@ -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. It’s possible that there are other users who have access through {{signinMethods}} but haven’t signed in yet.": "Everyone that has signed into {{appName}} is listed here. It’s possible that there are other users who have access through {{signinMethods}} but haven’t signed in yet.",
|
||||
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
|
||||
@@ -1118,7 +1122,6 @@
|
||||
"You can create templates to help your team create consistent and accurate documentation.": "You can create templates to help your team create consistent and accurate documentation.",
|
||||
"Alphabetical": "Alphabetical",
|
||||
"There are no templates just yet.": "There are no templates just yet.",
|
||||
"Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.": "Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.",
|
||||
"A confirmation code has been sent to your email address, please enter the code below to permanently destroy this workspace.": "A confirmation code has been sent to your email address, please enter the code below to permanently destroy this workspace.",
|
||||
"Confirmation code": "Confirmation code",
|
||||
"Deleting the <1>{{workspaceName}}</1> workspace will destroy all collections, documents, users, and associated data. You will be immediately logged out of {{appName}}.": "Deleting the <1>{{workspaceName}}</1> workspace will destroy all collections, documents, users, and associated data. You will be immediately logged out of {{appName}}.",
|
||||
@@ -1144,7 +1147,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.",
|
||||
@@ -1215,6 +1217,7 @@
|
||||
"Webhooks can be used to notify your application when events happen in {{appName}}. Events are sent as a https request with a JSON payload in near real-time.": "Webhooks can be used to notify your application when events happen in {{appName}}. Events are sent as a https request with a JSON payload in near real-time.",
|
||||
"Inactive": "Inactive",
|
||||
"Create a webhook": "Create a webhook",
|
||||
"Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.": "Zapier is a platform that allows {{appName}} to easily integrate with thousands of other business tools. Automate your workflows, sync data, and more.",
|
||||
"Never logged in": "Never logged in",
|
||||
"Online now": "Online now",
|
||||
"Online {{ timeAgo }}": "Online {{ timeAgo }}",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user