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