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:
Tom Moor
2026-03-01 17:47:29 -05:00
committed by GitHub
parent 5a14944d0c
commit 8619b219e7
16 changed files with 275 additions and 49 deletions
+4 -1
View File
@@ -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(() => {
+1 -1
View File
@@ -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
View File
@@ -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]
);
}
+14
View File
@@ -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
View File
@@ -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,
+157
View File
@@ -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);
+4 -1
View File
@@ -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>
);
-21
View File
@@ -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>
+2
View File
@@ -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(),
}),
+1
View File
@@ -36,6 +36,7 @@ export const TeamPreferenceDefaults: TeamPreferences = {
[TeamPreference.PreventDocumentEmbedding]: false,
[TeamPreference.EmailDisplay]: EmailDisplay.Members,
[TeamPreference.MCP]: true,
[TeamPreference.DisabledEmbeds]: [],
};
export const UserPreferenceDefaults: UserPreferences = {
+59 -12
View File
@@ -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,
+8 -3
View File
@@ -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 its 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 its 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",
+3
View File
@@ -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 {