diff --git a/app/hooks/useSettingsConfig.ts b/app/hooks/useSettingsConfig.ts
index 6fdbc739a..4801059ed 100644
--- a/app/hooks/useSettingsConfig.ts
+++ b/app/hooks/useSettingsConfig.ts
@@ -1,3 +1,4 @@
+import { mapValues } from "lodash";
import {
EmailIcon,
ProfileIcon,
@@ -16,6 +17,7 @@ import {
} from "outline-icons";
import React from "react";
import { useTranslation } from "react-i18next";
+import { integrationSettingsPath } from "@shared/utils/routeHelpers";
import Details from "~/scenes/Settings/Details";
import Export from "~/scenes/Settings/Export";
import Features from "~/scenes/Settings/Features";
@@ -29,36 +31,18 @@ import Profile from "~/scenes/Settings/Profile";
import Security from "~/scenes/Settings/Security";
import SelfHosted from "~/scenes/Settings/SelfHosted";
import Shares from "~/scenes/Settings/Shares";
-import Slack from "~/scenes/Settings/Slack";
import Tokens from "~/scenes/Settings/Tokens";
import Webhooks from "~/scenes/Settings/Webhooks";
import Zapier from "~/scenes/Settings/Zapier";
import GoogleIcon from "~/components/Icons/GoogleIcon";
-import SlackIcon from "~/components/Icons/SlackIcon";
import ZapierIcon from "~/components/Icons/ZapierIcon";
-import env from "~/env";
import isCloudHosted from "~/utils/isCloudHosted";
+import { loadPlugins } from "~/utils/plugins";
import { accountPreferencesPath } from "~/utils/routeHelpers";
import useCurrentTeam from "./useCurrentTeam";
import usePolicy from "./usePolicy";
type SettingsGroups = "Account" | "Team" | "Integrations";
-type SettingsPage =
- | "Profile"
- | "Notifications"
- | "Api"
- | "Details"
- | "Security"
- | "Features"
- | "Members"
- | "Groups"
- | "Shares"
- | "Import"
- | "Export"
- | "Webhooks"
- | "Slack"
- | "Zapier"
- | "GoogleAnalytics";
export type ConfigItem = {
name: string;
@@ -70,7 +54,7 @@ export type ConfigItem = {
};
type ConfigType = {
- [key in SettingsPage]: ConfigItem;
+ [key in string]: ConfigItem;
};
const useSettingsConfig = () => {
@@ -178,6 +162,16 @@ const useSettingsConfig = () => {
icon: ExportIcon,
},
// Integrations
+ ...mapValues(loadPlugins(), (plugin) => {
+ return {
+ name: plugin.config.name,
+ path: integrationSettingsPath(plugin.id),
+ group: t("Integrations"),
+ component: plugin.settings,
+ enabled: !!plugin.settings && can.update,
+ icon: plugin.icon,
+ } as ConfigItem;
+ }),
Webhooks: {
name: t("Webhooks"),
path: "/settings/webhooks",
@@ -188,23 +182,15 @@ const useSettingsConfig = () => {
},
SelfHosted: {
name: t("Self Hosted"),
- path: "/settings/integrations/self-hosted",
+ path: integrationSettingsPath("self-hosted"),
component: SelfHosted,
enabled: can.update,
group: t("Integrations"),
icon: BuildingBlocksIcon,
},
- Slack: {
- name: "Slack",
- path: "/settings/integrations/slack",
- component: Slack,
- enabled: can.update && (!!env.SLACK_CLIENT_ID || isCloudHosted),
- group: t("Integrations"),
- icon: SlackIcon,
- },
GoogleAnalytics: {
name: t("Google Analytics"),
- path: "/settings/integrations/google-analytics",
+ path: integrationSettingsPath("google-analytics"),
component: GoogleAnalytics,
enabled: can.update,
group: t("Integrations"),
@@ -212,7 +198,7 @@ const useSettingsConfig = () => {
},
Zapier: {
name: "Zapier",
- path: "/settings/integrations/zapier",
+ path: integrationSettingsPath("zapier"),
component: Zapier,
enabled: can.update && isCloudHosted,
group: t("Integrations"),
@@ -232,7 +218,7 @@ const useSettingsConfig = () => {
const enabledConfigs = React.useMemo(
() =>
Object.keys(config).reduce(
- (acc, key: SettingsPage) =>
+ (acc, key: string) =>
config[key].enabled ? [...acc, config[key]] : acc,
[]
),
diff --git a/app/routes/settings.tsx b/app/routes/settings.tsx
index af81f1b4c..1e1ac4e08 100644
--- a/app/routes/settings.tsx
+++ b/app/routes/settings.tsx
@@ -1,5 +1,5 @@
import * as React from "react";
-import { Switch, Redirect } from "react-router-dom";
+import { Switch } from "react-router-dom";
import Error404 from "~/scenes/Error404";
import Route from "~/components/ProfiledRoute";
import useSettingsConfig from "~/hooks/useSettingsConfig";
@@ -17,10 +17,6 @@ export default function SettingsRoutes() {
component={config.component}
/>
))}
- {/* old routes */}
-
-
-
);
diff --git a/app/typings/window.d.ts b/app/typings/window.d.ts
index 049fed18d..db4c1fe8d 100644
--- a/app/typings/window.d.ts
+++ b/app/typings/window.d.ts
@@ -1,4 +1,13 @@
declare global {
+ interface NodeRequire {
+ /** A special feature supported by webpack's compiler that allows you to get all matching modules starting from some base directory. */
+ context: (
+ directory: string,
+ useSubdirectories: boolean,
+ regExp: RegExp
+ ) => any;
+ }
+
interface Window {
dataLayer: any[];
gtag: (...args: any[]) => void;
diff --git a/app/utils/plugins.ts b/app/utils/plugins.ts
new file mode 100644
index 000000000..a4e73abcb
--- /dev/null
+++ b/app/utils/plugins.ts
@@ -0,0 +1,37 @@
+interface Plugin {
+ id: string;
+ config: {
+ name: string;
+ description: string;
+ requiredEnvVars?: string[];
+ };
+ settings: React.FC;
+ icon: React.FC;
+}
+
+export function loadPlugins(): { [id: string]: Plugin } {
+ const plugins = {};
+
+ function importAll(r: any, property: string) {
+ r.keys().forEach((key: string) => {
+ const id = key.split("/")[1];
+ plugins[id] = plugins[id] || {
+ id,
+ };
+
+ const plugin = r(key);
+ plugins[id][property] = "default" in plugin ? plugin.default : plugin;
+ });
+ }
+ importAll(
+ require.context("../../plugins", true, /client\/Settings\.[tj]sx?$/),
+ "settings"
+ );
+ importAll(
+ require.context("../../plugins", true, /client\/Icon\.[tj]sx?$/),
+ "icon"
+ );
+ importAll(require.context("../../plugins", true, /plugin\.json?$/), "config");
+
+ return plugins;
+}
diff --git a/build.sh b/build.sh
index dc33cd202..17a35c5bd 100755
--- a/build.sh
+++ b/build.sh
@@ -1,5 +1,16 @@
#!/bin/sh
-yarn concurrently "yarn babel --extensions .ts,.tsx --quiet -d ./build/server ./server" "yarn babel --extensions .ts,.tsx --quiet -d ./build/shared ./shared"
+
+# Compile server and shared
+yarn concurrently "yarn babel --extensions .ts,.tsx --quiet -d ./build/server ./server" \
+ "yarn babel --extensions .ts,.tsx --quiet -d ./build/shared ./shared"
+
+# Compile code in packages
+for d in ./plugins/*; do
+ # Get the name of the folder
+ package=$(basename "$d")
+ yarn babel --extensions .ts,.tsx --quiet -d "./build/plugins/$package/server" "./plugins/$package/server"
+ cp ./plugins/$package/plugin.json ./build/plugins/$package/plugin.json
+done
# Copy static files
cp ./server/collaboration/Procfile ./build/server/collaboration/Procfile
diff --git a/package.json b/package.json
index e0952b78b..d0c9a73e8 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"scripts": {
"clean": "rimraf build",
"copy:i18n": "mkdir -p ./build/shared/i18n && cp -R ./shared/i18n/locales ./build/shared/i18n",
- "build:i18n": "i18next --silent 'shared/**/*.tsx' 'shared/**/*.ts' 'app/**/*.tsx' 'app/**/*.ts' 'server/**/*.ts' 'server/**/*.tsx' && yarn copy:i18n",
+ "build:i18n": "i18next --silent '{shared,app,server,plugins}/**/*.{ts,tsx}' && yarn copy:i18n",
"build:server": "./build.sh",
"build:webpack": "webpack --config webpack.config.prod.js",
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
@@ -100,6 +100,7 @@
"fs-extra": "^4.0.2",
"fuzzy-search": "^3.2.1",
"gemoji": "6.x",
+ "glob": "^8.1.0",
"http-errors": "2.0.0",
"i18next": "^22.4.8",
"i18next-fs-backend": "^2.1.1",
@@ -240,6 +241,7 @@
"@types/formidable": "^2.0.5",
"@types/fs-extra": "^9.0.13",
"@types/fuzzy-search": "^2.1.2",
+ "@types/glob": "^8.0.1",
"@types/google.analytics": "^0.0.42",
"@types/inline-css": "^3.0.1",
"@types/invariant": "^2.2.35",
diff --git a/app/components/Icons/SlackIcon.tsx b/plugins/slack/client/Icon.tsx
similarity index 96%
rename from app/components/Icons/SlackIcon.tsx
rename to plugins/slack/client/Icon.tsx
index f27bff5fa..db79b9f3c 100644
--- a/app/components/Icons/SlackIcon.tsx
+++ b/plugins/slack/client/Icon.tsx
@@ -7,10 +7,7 @@ type Props = {
color?: string;
};
-export default function SlackIcon({
- size = 24,
- color = "currentColor",
-}: Props) {
+export default function Icon({ size = 24, color = "currentColor" }: Props) {
return (