feat: Support embed configuration (#3980)
* wip * stash * fix: make authenticationId nullable fk * fix: apply generics to resolve compile time type errors * fix: loosen integration settings * chore: refactor into functional component * feat: pass integrations all the way to embeds * perf: avoid re-fetching integrations * fix: change attr name to avoid type overlap * feat: use hostname from embed settings in matcher * Revert "feat: use hostname from embed settings in matcher" This reverts commit e7485d9cda4dcf45104e460465ca104a56c67ddc. * feat: refactor into a class * chore: refactor url regex formation as a util * fix: escape regex special chars * fix: remove in-house escapeRegExp in favor of lodash's * fix: sanitize url * perf: memoize embeds * fix: rename hostname to url and allow spreading entire settings instead of just url * fix: replace diagrams with drawio * fix: rename * fix: support self-hosted and saas both * fix: assert on settings url * fix: move embed integrations loading to hook * fix: address review comments * fix: use observer in favor of explicit state setters * fix: refactor useEmbedIntegrations into useEmbeds * fix: use translations for toasts Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { EditorState } from "prosemirror-state";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { EmbedDescriptor } from "@shared/editor/types";
|
||||
import { IntegrationType } from "../../types";
|
||||
import type { IntegrationSettings } from "../../types";
|
||||
import { urlRegex } from "../../utils/urls";
|
||||
import Image from "../components/Image";
|
||||
import Abstract from "./Abstract";
|
||||
import Airtable from "./Airtable";
|
||||
@@ -55,10 +58,57 @@ export type EmbedProps = {
|
||||
};
|
||||
};
|
||||
|
||||
function matcher(Component: React.ComponentType<EmbedProps>) {
|
||||
return (url: string): boolean | [] | RegExpMatchArray => {
|
||||
const Img = styled(Image)`
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
margin: 4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
`;
|
||||
|
||||
export class EmbedDescriptor {
|
||||
icon: React.FC<any>;
|
||||
name?: string;
|
||||
title?: string;
|
||||
shortcut?: string;
|
||||
keywords?: string;
|
||||
tooltip?: string;
|
||||
defaultHidden?: boolean;
|
||||
attrs?: Record<string, any>;
|
||||
visible?: boolean;
|
||||
active?: (state: EditorState) => boolean;
|
||||
component: typeof React.Component | React.FC<any>;
|
||||
settings?: IntegrationSettings<IntegrationType.Embed>;
|
||||
|
||||
constructor(options: Omit<EmbedDescriptor, "matcher">) {
|
||||
this.icon = options.icon;
|
||||
this.name = options.name;
|
||||
this.title = options.title;
|
||||
this.shortcut = options.shortcut;
|
||||
this.keywords = options.keywords;
|
||||
this.tooltip = options.tooltip;
|
||||
this.defaultHidden = options.defaultHidden;
|
||||
this.attrs = options.attrs;
|
||||
this.visible = options.visible;
|
||||
this.active = options.active;
|
||||
this.component = options.component;
|
||||
this.settings = options.settings;
|
||||
}
|
||||
|
||||
matcher(url: string): boolean | [] | RegExpMatchArray {
|
||||
const regex = urlRegex(this.settings?.url);
|
||||
|
||||
// @ts-expect-error not aware of static
|
||||
const regexes = Component.ENABLED;
|
||||
const regexes = this.component.ENABLED;
|
||||
|
||||
regex &&
|
||||
regexes.unshift(
|
||||
new RegExp(
|
||||
// @ts-expect-error not aware of static
|
||||
`^${regex.source}${this.component.URL_PATH_REGEX.source}$`
|
||||
)
|
||||
);
|
||||
|
||||
for (const regex of regexes) {
|
||||
const result = url.match(regex);
|
||||
@@ -69,324 +119,273 @@ function matcher(Component: React.ComponentType<EmbedProps>) {
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const Img = styled(Image)`
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
margin: 4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
`;
|
||||
|
||||
const embeds: EmbedDescriptor[] = [
|
||||
{
|
||||
new EmbedDescriptor({
|
||||
title: "Abstract",
|
||||
keywords: "design",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/abstract.png" alt="Abstract" />,
|
||||
component: Abstract,
|
||||
matcher: matcher(Abstract),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Airtable",
|
||||
keywords: "spreadsheet",
|
||||
icon: () => <Img src="/images/airtable.png" alt="Airtable" />,
|
||||
component: Airtable,
|
||||
matcher: matcher(Airtable),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Berrycast",
|
||||
keywords: "video",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/berrycast.png" alt="Berrycast" />,
|
||||
component: Berrycast,
|
||||
matcher: matcher(Berrycast),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Bilibili",
|
||||
keywords: "video",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/bilibili.png" alt="Bilibili" />,
|
||||
component: Bilibili,
|
||||
matcher: matcher(Bilibili),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Cawemo",
|
||||
keywords: "bpmn process",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/cawemo.png" alt="Cawemo" />,
|
||||
component: Cawemo,
|
||||
matcher: matcher(Cawemo),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "ClickUp",
|
||||
keywords: "project",
|
||||
icon: () => <Img src="/images/clickup.png" alt="ClickUp" />,
|
||||
component: ClickUp,
|
||||
matcher: matcher(ClickUp),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Codepen",
|
||||
keywords: "code editor",
|
||||
icon: () => <Img src="/images/codepen.png" alt="Codepen" />,
|
||||
component: Codepen,
|
||||
matcher: matcher(Codepen),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "DBDiagram",
|
||||
keywords: "diagrams database",
|
||||
icon: () => <Img src="/images/dbdiagram.png" alt="DBDiagram" />,
|
||||
component: DBDiagram,
|
||||
matcher: matcher(DBDiagram),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Descript",
|
||||
keywords: "audio",
|
||||
icon: () => <Img src="/images/descript.png" alt="Descript" />,
|
||||
component: Descript,
|
||||
matcher: matcher(Descript),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Figma",
|
||||
keywords: "design svg vector",
|
||||
icon: () => <Img src="/images/figma.png" alt="Figma" />,
|
||||
component: Figma,
|
||||
matcher: matcher(Figma),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Framer",
|
||||
keywords: "design prototyping",
|
||||
icon: () => <Img src="/images/framer.png" alt="Framer" />,
|
||||
component: Framer,
|
||||
matcher: matcher(Framer),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "GitHub Gist",
|
||||
keywords: "code",
|
||||
icon: () => <Img src="/images/github-gist.png" alt="GitHub" />,
|
||||
component: Gist,
|
||||
matcher: matcher(Gist),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Gliffy",
|
||||
keywords: "diagram",
|
||||
icon: () => <Img src="/images/gliffy.png" alt="Gliffy" />,
|
||||
component: Gliffy,
|
||||
matcher: matcher(Gliffy),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Diagrams.net",
|
||||
keywords: "diagrams drawio",
|
||||
icon: () => <Img src="/images/diagrams.png" alt="Diagrams.net" />,
|
||||
component: Diagrams,
|
||||
matcher: matcher(Diagrams),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Google Drawings",
|
||||
keywords: "drawings",
|
||||
icon: () => <Img src="/images/google-drawings.png" alt="Google Drawings" />,
|
||||
component: GoogleDrawings,
|
||||
matcher: matcher(GoogleDrawings),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Google Drive",
|
||||
keywords: "drive",
|
||||
icon: () => <Img src="/images/google-drive.png" alt="Google Drive" />,
|
||||
component: GoogleDrive,
|
||||
matcher: matcher(GoogleDrive),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Google Docs",
|
||||
keywords: "documents word",
|
||||
icon: () => <Img src="/images/google-docs.png" alt="Google Docs" />,
|
||||
component: GoogleDocs,
|
||||
matcher: matcher(GoogleDocs),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Google Sheets",
|
||||
keywords: "excel spreadsheet",
|
||||
icon: () => <Img src="/images/google-sheets.png" alt="Google Sheets" />,
|
||||
component: GoogleSheets,
|
||||
matcher: matcher(GoogleSheets),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Google Slides",
|
||||
keywords: "presentation slideshow",
|
||||
icon: () => <Img src="/images/google-slides.png" alt="Google Slides" />,
|
||||
component: GoogleSlides,
|
||||
matcher: matcher(GoogleSlides),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Google Calendar",
|
||||
keywords: "calendar",
|
||||
icon: () => <Img src="/images/google-calendar.png" alt="Google Calendar" />,
|
||||
component: GoogleCalendar,
|
||||
matcher: matcher(GoogleCalendar),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Google Data Studio",
|
||||
keywords: "bi business intelligence",
|
||||
icon: () => (
|
||||
<Img src="/images/google-datastudio.png" alt="Google Data Studio" />
|
||||
),
|
||||
component: GoogleDataStudio,
|
||||
matcher: matcher(GoogleDataStudio),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Google Forms",
|
||||
keywords: "form survey",
|
||||
icon: () => <Img src="/images/google-forms.png" alt="Google Forms" />,
|
||||
component: GoogleForms,
|
||||
matcher: matcher(GoogleForms),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Grist",
|
||||
keywords: "spreadsheet",
|
||||
icon: () => <Img src="/images/grist.png" alt="Grist" />,
|
||||
component: Grist,
|
||||
matcher: matcher(Grist),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "InVision",
|
||||
keywords: "design prototype",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/invision.png" alt="InVision" />,
|
||||
component: InVision,
|
||||
matcher: matcher(InVision),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "JSFiddle",
|
||||
keywords: "code",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/jsfiddle.png" alt="JSFiddle" />,
|
||||
component: JSFiddle,
|
||||
matcher: matcher(JSFiddle),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Loom",
|
||||
keywords: "video screencast",
|
||||
icon: () => <Img src="/images/loom.png" alt="Loom" />,
|
||||
component: Loom,
|
||||
matcher: matcher(Loom),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Lucidchart",
|
||||
keywords: "chart",
|
||||
icon: () => <Img src="/images/lucidchart.png" alt="Lucidchart" />,
|
||||
component: Lucidchart,
|
||||
matcher: matcher(Lucidchart),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Marvel",
|
||||
keywords: "design prototype",
|
||||
icon: () => <Img src="/images/marvel.png" alt="Marvel" />,
|
||||
component: Marvel,
|
||||
matcher: matcher(Marvel),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Mindmeister",
|
||||
keywords: "mindmap",
|
||||
icon: () => <Img src="/images/mindmeister.png" alt="Mindmeister" />,
|
||||
component: Mindmeister,
|
||||
matcher: matcher(Mindmeister),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Miro",
|
||||
keywords: "whiteboard",
|
||||
icon: () => <Img src="/images/miro.png" alt="Miro" />,
|
||||
component: Miro,
|
||||
matcher: matcher(Miro),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Mode",
|
||||
keywords: "analytics",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/mode-analytics.png" alt="Mode" />,
|
||||
component: ModeAnalytics,
|
||||
matcher: matcher(ModeAnalytics),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Otter.ai",
|
||||
keywords: "audio transcription meeting notes",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/otter.png" alt="Otter.ai" />,
|
||||
component: Otter,
|
||||
matcher: matcher(Otter),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Pitch",
|
||||
keywords: "presentation",
|
||||
defaultHidden: true,
|
||||
icon: () => <Img src="/images/pitch.png" alt="Pitch" />,
|
||||
component: Pitch,
|
||||
matcher: matcher(Pitch),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Prezi",
|
||||
keywords: "presentation",
|
||||
icon: () => <Img src="/images/prezi.png" alt="Prezi" />,
|
||||
component: Prezi,
|
||||
matcher: matcher(Prezi),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Scribe",
|
||||
keywords: "screencast",
|
||||
icon: () => <Img src="/images/scribe.png" alt="Scribe" />,
|
||||
component: Scribe,
|
||||
matcher: matcher(Scribe),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Spotify",
|
||||
keywords: "music",
|
||||
icon: () => <Img src="/images/spotify.png" alt="Spotify" />,
|
||||
component: Spotify,
|
||||
matcher: matcher(Spotify),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Tldraw",
|
||||
keywords: "draw schematics diagrams",
|
||||
icon: () => <Img src="/images/tldraw.png" alt="Tldraw" />,
|
||||
component: Tldraw,
|
||||
matcher: matcher(Tldraw),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Trello",
|
||||
keywords: "kanban",
|
||||
icon: () => <Img src="/images/trello.png" alt="Trello" />,
|
||||
component: Trello,
|
||||
matcher: matcher(Trello),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Typeform",
|
||||
keywords: "form survey",
|
||||
icon: () => <Img src="/images/typeform.png" alt="Typeform" />,
|
||||
component: Typeform,
|
||||
matcher: matcher(Typeform),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Vimeo",
|
||||
keywords: "video",
|
||||
icon: () => <Img src="/images/vimeo.png" alt="Vimeo" />,
|
||||
component: Vimeo,
|
||||
matcher: matcher(Vimeo),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "Whimsical",
|
||||
keywords: "whiteboard",
|
||||
icon: () => <Img src="/images/whimsical.png" alt="Whimsical" />,
|
||||
component: Whimsical,
|
||||
matcher: matcher(Whimsical),
|
||||
},
|
||||
{
|
||||
}),
|
||||
new EmbedDescriptor({
|
||||
title: "YouTube",
|
||||
keywords: "google video",
|
||||
icon: () => <Img src="/images/youtube.png" alt="YouTube" />,
|
||||
component: YouTube,
|
||||
matcher: matcher(YouTube),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export default embeds;
|
||||
|
||||
Reference in New Issue
Block a user