mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
feat: Configurable slash embeds (#11612)
* wip * Use id instead of title Settings UI tweaks * test * Add toggle for all providers * Remove 'Abstract' embed, no longer available
This commit is contained in:
@@ -67,7 +67,10 @@ function useItems({
|
||||
|
||||
const singleUrl =
|
||||
typeof pastedText === "string" && isUrl(pastedText) ? pastedText : null;
|
||||
const embed = singleUrl ? getMatchingEmbed(embeds, singleUrl)?.embed : null;
|
||||
const matchedEmbed = singleUrl
|
||||
? getMatchingEmbed(embeds, singleUrl)?.embed
|
||||
: null;
|
||||
const embed = matchedEmbed?.disabled ? null : matchedEmbed;
|
||||
|
||||
// Check embeddability for single URL
|
||||
useEffect(() => {
|
||||
|
||||
@@ -461,7 +461,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
|
||||
const embedItems: EmbedDescriptor[] = [];
|
||||
|
||||
for (const embed of embeds) {
|
||||
if (embed.title && embed.visible !== false) {
|
||||
if (embed.title && embed.visible !== false && !embed.disabled) {
|
||||
embedItems.push(
|
||||
new EmbedDescriptor({
|
||||
...embed,
|
||||
|
||||
+10
-2
@@ -1,9 +1,10 @@
|
||||
import find from "lodash/find";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import embeds from "@shared/editor/embeds";
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import { IntegrationType, TeamPreference } from "@shared/types";
|
||||
import type Integration from "~/models/Integration";
|
||||
import Logger from "~/utils/Logger";
|
||||
import useCurrentTeam from "./useCurrentTeam";
|
||||
import useStores from "./useStores";
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,7 @@ import useStores from "./useStores";
|
||||
*/
|
||||
export default function useEmbeds(loadIfMissing = false) {
|
||||
const { integrations } = useStores();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchEmbedIntegrations() {
|
||||
@@ -31,6 +33,9 @@ export default function useEmbeds(loadIfMissing = false) {
|
||||
}
|
||||
}, [integrations, loadIfMissing]);
|
||||
|
||||
const disabledEmbeds =
|
||||
(team.getPreference(TeamPreference.DisabledEmbeds) as string[]) || [];
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
embeds.map((e) => {
|
||||
@@ -42,8 +47,11 @@ export default function useEmbeds(loadIfMissing = false) {
|
||||
e.settings = integration.settings;
|
||||
}
|
||||
|
||||
e.disabled = disabledEmbeds.includes(e.id);
|
||||
|
||||
return e;
|
||||
}),
|
||||
[integrations.orderedData]
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[integrations.orderedData, team.preferences]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
InternetIcon,
|
||||
SmileyIcon,
|
||||
BuildingBlocksIcon,
|
||||
BrowserIcon,
|
||||
} from "outline-icons";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -48,6 +49,7 @@ const Security = lazy(() => import("~/scenes/Settings/Security"));
|
||||
const Shares = lazy(() => import("~/scenes/Settings/Shares"));
|
||||
const Templates = lazy(() => import("~/scenes/Settings/Templates"));
|
||||
const CustomEmojis = lazy(() => import("~/scenes/Settings/CustomEmojis"));
|
||||
const Embeds = lazy(() => import("~/scenes/Settings/Embeds"));
|
||||
|
||||
export type ConfigItem = {
|
||||
name: string;
|
||||
@@ -234,6 +236,18 @@ const useSettingsConfig = () => {
|
||||
icon: ExportIcon,
|
||||
},
|
||||
// Integrations
|
||||
{
|
||||
name: t("Embeds"),
|
||||
path: integrationSettingsPath("embeds"),
|
||||
component: Embeds.Component,
|
||||
preload: Embeds.preload,
|
||||
description: t(
|
||||
"Configure which embed providers are available in the editor."
|
||||
),
|
||||
enabled: can.update,
|
||||
group: t("Integrations"),
|
||||
icon: BrowserIcon,
|
||||
},
|
||||
{
|
||||
name: `${t("Install")}…`,
|
||||
path: settingsPath("integrations"),
|
||||
|
||||
+3
-3
@@ -114,10 +114,10 @@ class Team extends Model {
|
||||
/**
|
||||
* Set the value for a specific preference key.
|
||||
*
|
||||
* @param key The TeamPreference key to retrieve
|
||||
* @param value The value to set
|
||||
* @param key The TeamPreference key to set.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
setPreference(key: TeamPreference, value: boolean) {
|
||||
setPreference<T extends TeamPreference>(key: T, value: TeamPreferences[T]) {
|
||||
this.preferences = {
|
||||
...this.preferences,
|
||||
[key]: value,
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import debounce from "lodash/debounce";
|
||||
import { observer } from "mobx-react";
|
||||
import { BrowserIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import embeds from "@shared/editor/embeds";
|
||||
import { TeamPreference } from "@shared/types";
|
||||
import Heading from "~/components/Heading";
|
||||
import Switch from "~/components/Switch";
|
||||
import Text from "~/components/Text";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import { IntegrationScene } from "./components/IntegrationScene";
|
||||
import SettingRow from "./components/SettingRow";
|
||||
import { HStack } from "~/components/primitives/HStack";
|
||||
|
||||
/** List of embed providers available for configuration. */
|
||||
const providers = embeds.filter((e) => e.id !== "embed");
|
||||
|
||||
function Embeds() {
|
||||
const team = useCurrentTeam();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showSuccessMessage = React.useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
toast.success(t("Settings saved"));
|
||||
}, 250),
|
||||
[t]
|
||||
);
|
||||
|
||||
const saveData = React.useCallback(
|
||||
async (newData: Record<string, unknown>) => {
|
||||
try {
|
||||
await team.save(newData);
|
||||
showSuccessMessage();
|
||||
} catch (err) {
|
||||
toast.error((err as Error).message);
|
||||
}
|
||||
},
|
||||
[team, showSuccessMessage]
|
||||
);
|
||||
|
||||
const handleDocumentEmbedsChange = React.useCallback(
|
||||
async (checked: boolean) => {
|
||||
await saveData({ documentEmbeds: checked });
|
||||
},
|
||||
[saveData]
|
||||
);
|
||||
|
||||
const handleToggleEmbed = React.useCallback(
|
||||
async (id: string, enabled: boolean) => {
|
||||
const disabledEmbeds =
|
||||
(team.getPreference(TeamPreference.DisabledEmbeds) as string[]) || [];
|
||||
|
||||
const updated = enabled
|
||||
? disabledEmbeds.filter((t) => t !== id)
|
||||
: [...disabledEmbeds, id];
|
||||
|
||||
team.setPreference(TeamPreference.DisabledEmbeds, updated);
|
||||
await saveData({
|
||||
preferences: { ...team.preferences },
|
||||
});
|
||||
},
|
||||
[team, saveData]
|
||||
);
|
||||
|
||||
const handleToggleAllEmbeds = React.useCallback(
|
||||
async (enabled: boolean) => {
|
||||
const updated = enabled ? [] : providers.map((e) => e.id);
|
||||
|
||||
team.setPreference(TeamPreference.DisabledEmbeds, updated);
|
||||
await saveData({
|
||||
preferences: { ...team.preferences },
|
||||
});
|
||||
},
|
||||
[team, saveData]
|
||||
);
|
||||
|
||||
const disabledEmbeds =
|
||||
(team.getPreference(TeamPreference.DisabledEmbeds) as string[]) || [];
|
||||
|
||||
return (
|
||||
<IntegrationScene title={t("Embeds")} icon={<BrowserIcon />}>
|
||||
<Heading>{t("Embeds")}</Heading>
|
||||
|
||||
<SettingRow
|
||||
label={t("Enabled")}
|
||||
name="documentEmbeds"
|
||||
description={t(
|
||||
"Allow supported providers to be inserted as interactive embeds in documents."
|
||||
)}
|
||||
>
|
||||
<Switch
|
||||
id="documentEmbeds"
|
||||
checked={team.documentEmbeds}
|
||||
onChange={handleDocumentEmbedsChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
{team.documentEmbeds && (
|
||||
<>
|
||||
<Heading as="h2">{t("Providers")}</Heading>
|
||||
<Text as="p" type="secondary">
|
||||
<Trans>
|
||||
Enabled providers will appear in the editor slash menu and embed
|
||||
automatically when a compatible link is pasted. Existing embeds in
|
||||
documents will continue to display regardless of these settings.
|
||||
</Trans>
|
||||
</Text>
|
||||
<SettingRow
|
||||
name="allEmbeds"
|
||||
label={t("All providers")}
|
||||
compact
|
||||
border={false}
|
||||
>
|
||||
<Switch
|
||||
id="allEmbeds"
|
||||
checked={disabledEmbeds.length === 0}
|
||||
onChange={handleToggleAllEmbeds}
|
||||
/>
|
||||
</SettingRow>
|
||||
{providers.map((embed) => {
|
||||
const enabled = !disabledEmbeds.includes(embed.id);
|
||||
return (
|
||||
<SettingRow
|
||||
key={embed.id}
|
||||
name={embed.title}
|
||||
label={
|
||||
<HStack
|
||||
style={{ filter: enabled ? "none" : "grayscale(100%)" }}
|
||||
>
|
||||
{embed.icon}
|
||||
<Text type={enabled ? undefined : "tertiary"}>
|
||||
{embed.title}
|
||||
</Text>
|
||||
</HStack>
|
||||
}
|
||||
compact
|
||||
>
|
||||
<Switch
|
||||
id={embed.id}
|
||||
checked={enabled}
|
||||
onChange={(checked: boolean) =>
|
||||
handleToggleEmbed(embed.id, checked)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</IntegrationScene>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Embeds);
|
||||
@@ -10,7 +10,7 @@ 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 IntegrationCard, { Card } from "./components/IntegrationCard";
|
||||
import { StickyFilters } from "./components/StickyFilters";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
@@ -62,6 +62,9 @@ function Integrations() {
|
||||
{groupedItems.available?.map((item) => (
|
||||
<IntegrationCard key={item.path} integration={item} />
|
||||
))}
|
||||
{groupedItems.available?.length % 2 === 1 && (
|
||||
<Card style={{ visibility: "hidden" }} />
|
||||
)}
|
||||
</Cards>
|
||||
</Scene>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,6 @@ function Security() {
|
||||
|
||||
const [data, setData] = useState({
|
||||
sharing: team.sharing,
|
||||
documentEmbeds: team.documentEmbeds,
|
||||
defaultUserRole: team.defaultUserRole,
|
||||
memberCollectionCreate: team.memberCollectionCreate,
|
||||
memberTeamCreate: team.memberTeamCreate,
|
||||
@@ -107,13 +106,6 @@ function Security() {
|
||||
[saveData]
|
||||
);
|
||||
|
||||
const handleDocumentEmbedsChange = React.useCallback(
|
||||
async (checked: boolean) => {
|
||||
await saveData({ documentEmbeds: checked });
|
||||
},
|
||||
[saveData]
|
||||
);
|
||||
|
||||
const handlePasskeysEnabledChange = React.useCallback(
|
||||
async (checked: boolean) => {
|
||||
await saveData({ passkeysEnabled: checked });
|
||||
@@ -327,19 +319,6 @@ function Security() {
|
||||
onChange={handleMembersCanDeleteAccountChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("Rich service embeds")}
|
||||
name="documentEmbeds"
|
||||
description={t(
|
||||
"Links to supported services are shown as rich embeds within your documents"
|
||||
)}
|
||||
>
|
||||
<Switch
|
||||
id="documentEmbeds"
|
||||
checked={data.documentEmbeds}
|
||||
onChange={handleDocumentEmbedsChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("Email address visibility")}
|
||||
name={TeamPreference.EmailDisplay}
|
||||
|
||||
@@ -28,7 +28,7 @@ function IntegrationCard({ integration, isConnected }: Props) {
|
||||
</VStack>
|
||||
</VStack>
|
||||
<Button as="span" neutral>
|
||||
{isConnected ? t("Configure") : t("Connect")}
|
||||
{t("Configure")}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -39,7 +39,7 @@ function IntegrationCard({ integration, isConnected }: Props) {
|
||||
|
||||
export default IntegrationCard;
|
||||
|
||||
const Card = styled.div`
|
||||
export const Card = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -12,11 +12,13 @@ type Props = {
|
||||
name: string;
|
||||
visible?: boolean;
|
||||
border?: boolean;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
const Row = styled(Flex)<{ $border?: boolean }>`
|
||||
const Row = styled(Flex)<{ $border?: boolean; $compact?: boolean }>`
|
||||
display: block;
|
||||
padding: 22px 0;
|
||||
padding: ${(props) => (props.$compact ? "12px 0" : "22px 0")};
|
||||
align-items: ${(props) => (props.$compact ? "center" : "initial")};
|
||||
border-bottom: 1px solid
|
||||
${(props) =>
|
||||
props.$border === false
|
||||
@@ -60,6 +62,7 @@ const Label = styled(Text)`
|
||||
const SettingRow: React.FC<Props> = ({
|
||||
visible,
|
||||
description,
|
||||
compact,
|
||||
name,
|
||||
label,
|
||||
border,
|
||||
@@ -69,7 +72,7 @@ const SettingRow: React.FC<Props> = ({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Row gap={32} $border={border}>
|
||||
<Row gap={32} $border={border} $compact={compact}>
|
||||
<Column>
|
||||
<Label as="h3">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
|
||||
@@ -65,6 +65,8 @@ export const TeamsUpdateSchema = BaseSchema.extend({
|
||||
preventDocumentEmbedding: z.boolean().optional(),
|
||||
/** Whether external MCP clients can connect to the workspace. */
|
||||
mcp: z.boolean().optional(),
|
||||
/** List of disabled embed provider titles. */
|
||||
disabledEmbeds: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
|
||||
@@ -36,6 +36,7 @@ export const TeamPreferenceDefaults: TeamPreferences = {
|
||||
[TeamPreference.PreventDocumentEmbedding]: false,
|
||||
[TeamPreference.EmailDisplay]: EmailDisplay.Members,
|
||||
[TeamPreference.MCP]: true,
|
||||
[TeamPreference.DisabledEmbeds]: [],
|
||||
};
|
||||
|
||||
export const UserPreferenceDefaults: UserPreferences = {
|
||||
|
||||
@@ -47,6 +47,8 @@ const Img = styled(Image)<{ $invertable?: boolean }>`
|
||||
`;
|
||||
|
||||
export class EmbedDescriptor {
|
||||
/** A unique identifier for the embed */
|
||||
id: string;
|
||||
/** An icon that will be used to represent the embed in menus */
|
||||
icon?: React.ReactNode;
|
||||
/** The name of the embed. If this embed has a matching integration it should match IntegrationService */
|
||||
@@ -88,8 +90,11 @@ export class EmbedDescriptor {
|
||||
component?: React.FunctionComponent<EmbedProps>;
|
||||
/** The integration settings, if any */
|
||||
settings?: IntegrationSettings<IntegrationType.Embed>;
|
||||
/** Whether this embed has been disabled by the team admin */
|
||||
disabled?: boolean;
|
||||
|
||||
constructor(options: Omit<EmbedDescriptor, "matcher">) {
|
||||
this.id = options.id;
|
||||
this.icon = options.icon;
|
||||
this.name = options.name;
|
||||
this.title = options.title;
|
||||
@@ -130,18 +135,7 @@ export class EmbedDescriptor {
|
||||
|
||||
const embeds: EmbedDescriptor[] = [
|
||||
new EmbedDescriptor({
|
||||
title: "Abstract",
|
||||
keywords: "design",
|
||||
defaultHidden: true,
|
||||
icon: <Img src="/images/abstract.png" alt="Abstract" />,
|
||||
regexMatch: [
|
||||
new RegExp("^https?://share\\.(?:go)?abstract\\.com/(.*)$"),
|
||||
new RegExp("^https?://app\\.(?:go)?abstract\\.com/(?:share|embed)/(.*)$"),
|
||||
],
|
||||
transformMatch: (matches: RegExpMatchArray) =>
|
||||
`https://app.goabstract.com/embed/${matches[1]}`,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "airtable",
|
||||
title: "Airtable",
|
||||
keywords: "spreadsheet",
|
||||
icon: <Img src="/images/airtable.png" alt="Airtable" />,
|
||||
@@ -153,6 +147,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
`https://airtable.com/embed/${matches[1] ?? ""}${matches[2]}`,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "berrycast",
|
||||
title: "Berrycast",
|
||||
keywords: "video",
|
||||
defaultHidden: true,
|
||||
@@ -161,6 +156,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: Berrycast,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "bilibili",
|
||||
title: "Bilibili",
|
||||
keywords: "video",
|
||||
defaultHidden: true,
|
||||
@@ -172,6 +168,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/bilibili.png" alt="Bilibili" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "camunda",
|
||||
title: "Camunda Modeler",
|
||||
keywords: "bpmn process cawemo",
|
||||
defaultHidden: true,
|
||||
@@ -183,6 +180,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/camunda.png" alt="Camunda" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "canva",
|
||||
title: "Canva",
|
||||
keywords: "design",
|
||||
regexMatch: [
|
||||
@@ -205,6 +203,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/canva.png" alt="Canva" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "cawemo",
|
||||
title: "Cawemo",
|
||||
keywords: "bpmn process",
|
||||
defaultHidden: true,
|
||||
@@ -214,6 +213,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/cawemo.png" alt="Cawemo" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "clickup",
|
||||
title: "ClickUp",
|
||||
keywords: "project",
|
||||
regexMatch: [
|
||||
@@ -226,6 +226,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/clickup.png" alt="ClickUp" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "codepen",
|
||||
title: "Codepen",
|
||||
keywords: "code editor",
|
||||
regexMatch: [new RegExp("^https://codepen.io/(.*?)/(pen|embed)/(.*)$")],
|
||||
@@ -234,6 +235,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/codepen.png" alt="Codepen" $invertable />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "dbdiagram",
|
||||
title: "DBDiagram",
|
||||
keywords: "diagrams database",
|
||||
regexMatch: [new RegExp("^https://dbdiagram.io/(embed|e|d)/(\\w+)(/.*)?$")],
|
||||
@@ -241,6 +243,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/dbdiagram.png" alt="DBDiagram" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "diagrams",
|
||||
title: "Diagrams.net",
|
||||
name: IntegrationService.Diagrams,
|
||||
keywords: "diagrams drawio",
|
||||
@@ -250,6 +253,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
visible: false,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "descript",
|
||||
title: "Descript",
|
||||
keywords: "audio",
|
||||
regexMatch: [new RegExp("^https?://share\\.descript\\.com/view/(\\w+)$")],
|
||||
@@ -260,6 +264,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
...(env.DROPBOX_APP_KEY
|
||||
? [
|
||||
new EmbedDescriptor({
|
||||
id: "dropbox",
|
||||
title: "Dropbox",
|
||||
keywords: "file document",
|
||||
regexMatch: [
|
||||
@@ -271,6 +276,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
]
|
||||
: []),
|
||||
new EmbedDescriptor({
|
||||
id: "figma",
|
||||
title: "Figma",
|
||||
keywords: "design svg vector",
|
||||
regexMatch: [
|
||||
@@ -291,6 +297,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/figma.png" alt="Figma" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "framer",
|
||||
title: "Framer",
|
||||
keywords: "design prototyping",
|
||||
regexMatch: [new RegExp("^https://framer.cloud/(.*)$")],
|
||||
@@ -298,6 +305,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/framer.png" alt="Framer" $invertable />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "github-gist",
|
||||
title: "GitHub Gist",
|
||||
keywords: "code",
|
||||
regexMatch: [
|
||||
@@ -309,6 +317,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: Gist,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "gitlab-snippet",
|
||||
title: "GitLab Snippet",
|
||||
keywords: "code",
|
||||
regexMatch: [
|
||||
@@ -318,6 +327,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: GitLabSnippet,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "gliffy",
|
||||
title: "Gliffy",
|
||||
keywords: "diagram",
|
||||
regexMatch: [new RegExp("https?://go\\.gliffy\\.com/go/share/(.*)$")],
|
||||
@@ -325,6 +335,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/gliffy.png" alt="Gliffy" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-maps",
|
||||
title: "Google Maps",
|
||||
keywords: "maps",
|
||||
regexMatch: [new RegExp("^https?://www\\.google\\.com/maps/embed\\?(.*)$")],
|
||||
@@ -332,6 +343,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/google-maps.png" alt="Google Maps" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-drawings",
|
||||
title: "Google Drawings",
|
||||
keywords: "drawings",
|
||||
transformMatch: (matches: RegExpMatchArray) =>
|
||||
@@ -344,6 +356,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/google-drawings.png" alt="Google Drawings" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-drive",
|
||||
title: "Google Drive",
|
||||
keywords: "drive",
|
||||
regexMatch: [new RegExp("^https?://drive\\.google\\.com/file/d/(.*)$")],
|
||||
@@ -352,6 +365,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/google-drive.png" alt="Google Drive" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-docs",
|
||||
title: "Google Docs",
|
||||
keywords: "documents word",
|
||||
regexMatch: [new RegExp("^https?://docs\\.google\\.com/document/(.*)$")],
|
||||
@@ -360,6 +374,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/google-docs.png" alt="Google Docs" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-sheets",
|
||||
title: "Google Sheets",
|
||||
keywords: "excel spreadsheet",
|
||||
regexMatch: [
|
||||
@@ -370,6 +385,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/google-sheets.png" alt="Google Sheets" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-slides",
|
||||
title: "Google Slides",
|
||||
keywords: "presentation slideshow",
|
||||
regexMatch: [
|
||||
@@ -380,6 +396,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/google-slides.png" alt="Google Slides" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-calendar",
|
||||
title: "Google Calendar",
|
||||
keywords: "calendar",
|
||||
regexMatch: [
|
||||
@@ -391,6 +408,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/google-calendar.png" alt="Google Calendar" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-forms",
|
||||
title: "Google Forms",
|
||||
keywords: "form survey",
|
||||
regexMatch: [new RegExp("^https?://docs\\.google\\.com/forms/d/(.+)$")],
|
||||
@@ -402,6 +420,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/google-forms.png" alt="Google Forms" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "google-looker-studio",
|
||||
title: "Google Looker Studio",
|
||||
keywords: "bi business intelligence",
|
||||
regexMatch: [
|
||||
@@ -416,6 +435,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
),
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "grist",
|
||||
title: "Grist",
|
||||
name: IntegrationService.Grist,
|
||||
keywords: "spreadsheet",
|
||||
@@ -441,6 +461,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/grist.png" alt="Grist" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "instagram",
|
||||
title: "Instagram",
|
||||
keywords: "post",
|
||||
regexMatch: [
|
||||
@@ -450,6 +471,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/instagram.png" alt="Instagram" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "invision",
|
||||
title: "InVision",
|
||||
keywords: "design prototype",
|
||||
defaultHidden: true,
|
||||
@@ -462,6 +484,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: InVision,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "jsfiddle",
|
||||
title: "JSFiddle",
|
||||
keywords: "code",
|
||||
defaultHidden: true,
|
||||
@@ -470,6 +493,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: JSFiddle,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "linkedin",
|
||||
title: "LinkedIn",
|
||||
keywords: "post",
|
||||
defaultHidden: true,
|
||||
@@ -480,6 +504,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: Linkedin,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "loom",
|
||||
title: "Loom",
|
||||
keywords: "video screencast",
|
||||
regexMatch: [/^https:\/\/(www\.)?(use)?loom\.com\/(embed|share)\/(.*)$/],
|
||||
@@ -488,6 +513,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/loom.png" alt="Loom" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "lucidchart",
|
||||
title: "Lucidchart",
|
||||
keywords: "chart",
|
||||
regexMatch: [
|
||||
@@ -499,6 +525,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/lucidchart.png" alt="Lucidchart" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "marvel",
|
||||
title: "Marvel",
|
||||
keywords: "design prototype",
|
||||
regexMatch: [new RegExp("^https://marvelapp\\.com/([A-Za-z0-9-]{6})/?$")],
|
||||
@@ -506,6 +533,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/marvel.png" alt="Marvel" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "mindmeister",
|
||||
title: "Mindmeister",
|
||||
keywords: "mindmap",
|
||||
regexMatch: [
|
||||
@@ -520,6 +548,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/mindmeister.png" alt="Mindmeister" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "miro",
|
||||
title: "Miro",
|
||||
keywords: "whiteboard",
|
||||
regexMatch: [/^https:\/\/(realtimeboard|miro)\.com\/app\/board\/(.*)$/],
|
||||
@@ -528,6 +557,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/miro.png" alt="Miro" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "mode",
|
||||
title: "Mode",
|
||||
keywords: "analytics",
|
||||
defaultHidden: true,
|
||||
@@ -539,6 +569,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/mode-analytics.png" alt="Mode" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "otter",
|
||||
title: "Otter.ai",
|
||||
keywords: "audio transcription meeting notes",
|
||||
defaultHidden: true,
|
||||
@@ -547,6 +578,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/otter.png" alt="Otter.ai" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "pitch",
|
||||
title: "Pitch",
|
||||
keywords: "presentation",
|
||||
defaultHidden: true,
|
||||
@@ -561,6 +593,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/pitch.png" alt="Pitch" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "prezi",
|
||||
title: "Prezi",
|
||||
keywords: "presentation",
|
||||
regexMatch: [new RegExp("^https://prezi\\.com/view/(.*)$")],
|
||||
@@ -569,6 +602,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/prezi.png" alt="Prezi" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "scribe",
|
||||
title: "Scribe",
|
||||
keywords: "screencast",
|
||||
regexMatch: [/^https?:\/\/scribehow\.com\/shared\/(.*)$/],
|
||||
@@ -577,6 +611,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/scribe.png" alt="Scribe" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "smartsuite",
|
||||
title: "SmartSuite",
|
||||
regexMatch: [
|
||||
new RegExp("^https?://app\\.smartsuite\\.com/shared/(.*)(?:\\?)?(?:.*)$"),
|
||||
@@ -588,6 +623,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
`https://app.smartsuite.com/shared/${matches[1]}?embed=true&header=false&toolbar=true`,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "spotify",
|
||||
title: "Spotify",
|
||||
keywords: "music",
|
||||
regexMatch: [new RegExp("^https?://open\\.spotify\\.com/(.*)$")],
|
||||
@@ -595,6 +631,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: Spotify,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "tella",
|
||||
title: "Tella",
|
||||
keywords: "video",
|
||||
regexMatch: [/^https?:\/\/(?:www\.)?tella\.tv\/video\/([^\/]+)(?:.*)?$/],
|
||||
@@ -605,6 +642,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
hideToolbar: true,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "tldraw",
|
||||
title: "Tldraw",
|
||||
keywords: "draw schematics diagrams",
|
||||
regexMatch: [
|
||||
@@ -614,6 +652,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/tldraw.png" alt="Tldraw" $invertable />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "trello",
|
||||
title: "Trello",
|
||||
keywords: "kanban",
|
||||
regexMatch: [/^https:\/\/trello\.com\/(c|b)\/([^/]*)(.*)?$/],
|
||||
@@ -621,6 +660,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: Trello,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "typeform",
|
||||
title: "Typeform",
|
||||
keywords: "form survey",
|
||||
regexMatch: [
|
||||
@@ -632,6 +672,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/typeform.png" alt="Typeform" $invertable />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "valtown",
|
||||
title: "Valtown",
|
||||
keywords: "code",
|
||||
regexMatch: [/^https?:\/\/(?:www.)?val\.town\/(?:v|embed)\/(.*)$/],
|
||||
@@ -640,6 +681,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/valtown.png" alt="Valtown" $invertable />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "vimeo",
|
||||
title: "Vimeo",
|
||||
keywords: "video",
|
||||
regexMatch: [
|
||||
@@ -649,6 +691,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: Vimeo,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "pinterest",
|
||||
title: "Pinterest",
|
||||
keywords: "board moodboard pins",
|
||||
regexMatch: [
|
||||
@@ -661,6 +704,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: Pinterest,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "whimsical",
|
||||
title: "Whimsical",
|
||||
keywords: "whiteboard",
|
||||
regexMatch: [
|
||||
@@ -671,6 +715,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
icon: <Img src="/images/whimsical.png" alt="Whimsical" />,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "youtube",
|
||||
title: "YouTube",
|
||||
keywords: "google video",
|
||||
regexMatch: [
|
||||
@@ -680,6 +725,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
component: YouTube,
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
id: "plant-uml",
|
||||
title: "Plant UML",
|
||||
keywords: "plant plantuml uml",
|
||||
regexMatch: [
|
||||
@@ -690,6 +736,7 @@ const embeds: EmbedDescriptor[] = [
|
||||
}),
|
||||
/* The generic iframe embed should always be the last one */
|
||||
new EmbedDescriptor({
|
||||
id: "embed",
|
||||
title: "Embed",
|
||||
keywords: "iframe webpage",
|
||||
placeholder: "Paste a URL to embed",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EmbedDescriptor } from "../embeds";
|
||||
import filterExcessSeparators from "./filterExcessSeparators";
|
||||
|
||||
const embedDescriptor = new EmbedDescriptor({
|
||||
id: "test",
|
||||
title: "Test",
|
||||
icon: () => null,
|
||||
component: () => null,
|
||||
|
||||
@@ -671,8 +671,10 @@
|
||||
"Applications": "Applications",
|
||||
"Shared Links": "Shared Links",
|
||||
"Import": "Import",
|
||||
"Install": "Install",
|
||||
"Embeds": "Embeds",
|
||||
"Configure which embed providers are available in the editor.": "Configure which embed providers are available in the editor.",
|
||||
"Integrations": "Integrations",
|
||||
"Install": "Install",
|
||||
"Change name": "Change name",
|
||||
"Change email": "Change email",
|
||||
"Suspend user": "Suspend user",
|
||||
@@ -1207,6 +1209,11 @@
|
||||
"When enabled team members can add comments to documents.": "When enabled team members can add comments to documents.",
|
||||
"Danger": "Danger",
|
||||
"You can delete this entire workspace including collections, documents, and users.": "You can delete this entire workspace including collections, documents, and users.",
|
||||
"Enabled": "Enabled",
|
||||
"Allow supported providers to be inserted as interactive embeds in documents.": "Allow supported providers to be inserted as interactive embeds in documents.",
|
||||
"Providers": "Providers",
|
||||
"Enabled providers will appear in the editor slash menu and embed automatically when a compatible link is pasted. Existing embeds in documents will continue to display regardless of these settings.": "Enabled providers will appear in the editor slash menu and embed automatically when a compatible link is pasted. Existing embeds in documents will continue to display regardless of these settings.",
|
||||
"All providers": "All providers",
|
||||
"Export data": "Export data",
|
||||
"A full export might take some time, consider exporting a single document or collection. You may leave this page once the export has started – if you have notifications enabled, we will email a link to <em>{{ userEmail }}</em> when it’s complete.": "A full export might take some time, consider exporting a single document or collection. You may leave this page once the export has started – if you have notifications enabled, we will email a link to <em>{{ userEmail }}</em> when it’s complete.",
|
||||
"Recent exports": "Recent exports",
|
||||
@@ -1308,8 +1315,6 @@
|
||||
"When enabled, viewers can see download options for documents": "When enabled, viewers can see download options for documents",
|
||||
"Users can delete account": "Users can delete account",
|
||||
"When enabled, users can delete their own account from the workspace": "When enabled, users can delete their own account from the workspace",
|
||||
"Rich service embeds": "Rich service embeds",
|
||||
"Links to supported services are shown as rich embeds within your documents": "Links to supported services are shown as rich embeds within your documents",
|
||||
"Email address visibility": "Email address visibility",
|
||||
"Controls who can see user email addresses in the workspace": "Controls who can see user email addresses in the workspace",
|
||||
"Collection creation": "Collection creation",
|
||||
|
||||
@@ -386,6 +386,8 @@ export enum TeamPreference {
|
||||
EmailDisplay = "emailDisplay",
|
||||
/** Whether external MCP clients can connect to the workspace. */
|
||||
MCP = "mcp",
|
||||
/** List of disabled embed provider titles. */
|
||||
DisabledEmbeds = "disabledEmbeds",
|
||||
}
|
||||
|
||||
export type TeamPreferences = {
|
||||
@@ -402,6 +404,7 @@ export type TeamPreferences = {
|
||||
[TeamPreference.PreventDocumentEmbedding]?: boolean;
|
||||
[TeamPreference.EmailDisplay]?: EmailDisplay;
|
||||
[TeamPreference.MCP]?: boolean;
|
||||
[TeamPreference.DisabledEmbeds]?: string[];
|
||||
};
|
||||
|
||||
export enum NavigationNodeType {
|
||||
|
||||
Reference in New Issue
Block a user