Plugin architecture (#4861)

* wip

* Refactor, tasks, processors, routes loading

* Move Slack settings config to plugin

* Fix translations in plugins

* Move Slack auth to plugin

* test

* Move other slack-related files into plugin

* Forgot to save

* refactor
This commit is contained in:
Tom Moor
2023-02-12 13:11:30 -05:00
committed by GitHub
parent 492beedf00
commit 33afa2f029
30 changed files with 273 additions and 117 deletions

View File

@@ -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,
[]
),

View File

@@ -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 */}
<Redirect from="/settings/import-export" to="/settings/export" />
<Redirect from="/settings/people" to="/settings/members" />
<Redirect from="/settings/profile" to="/settings" />
<Route component={Error404} />
</Switch>
);

View File

@@ -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;

37
app/utils/plugins.ts Normal file
View File

@@ -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;
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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 (
<svg
fill={color}

View File

@@ -9,7 +9,6 @@ import Integration from "~/models/Integration";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import SlackIcon from "~/components/Icons/SlackIcon";
import List from "~/components/List";
import ListItem from "~/components/List/Item";
import Notice from "~/components/Notice";
@@ -19,6 +18,7 @@ import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useQuery from "~/hooks/useQuery";
import useStores from "~/hooks/useStores";
import SlackIcon from "./Icon";
import SlackButton from "./components/SlackButton";
import SlackListItem from "./components/SlackListItem";

View File

@@ -0,0 +1,5 @@
{
"name": "Slack",
"description": "Adds a Slack authentication provider, support for the /outline slash command, and link unfurling.",
"requiredEnvVars": ["SLACK_CLIENT_ID", "SLACK_CLIENT_SECRET"]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../../../server/.babelrc"
}

View File

@@ -3,7 +3,7 @@ import env from "@server/env";
import { IntegrationAuthentication, SearchQuery } from "@server/models";
import { buildDocument, buildIntegration } from "@server/test/factories";
import { seed, getTestServer } from "@server/test/support";
import * as Slack from "@server/utils/slack";
import * as Slack from "../slack";
jest.mock("@server/utils/slack", () => ({
post: jest.fn(),

View File

@@ -17,11 +17,11 @@ import {
IntegrationAuthentication,
} from "@server/models";
import SearchHelper from "@server/models/helpers/SearchHelper";
import { presentSlackAttachment } from "@server/presenters";
import { APIContext } from "@server/types";
import { opts } from "@server/utils/i18n";
import * as Slack from "@server/utils/slack";
import { assertPresent } from "@server/validation";
import presentMessageAttachment from "../presenters/messageAttachment";
import * as Slack from "../slack";
const router = new Router();
@@ -140,7 +140,7 @@ router.post("hooks.interactive", async (ctx: APIContext) => {
response_type: "in_channel",
replace_original: false,
attachments: [
presentSlackAttachment(
presentMessageAttachment(
document,
team,
document.collection,
@@ -329,7 +329,7 @@ router.post("hooks.slack", async (ctx: APIContext) => {
.toLowerCase()
.match(escapeRegExp(text.toLowerCase()));
attachments.push(
presentSlackAttachment(
presentMessageAttachment(
result.document,
team,
result.document.collection,

View File

@@ -4,6 +4,7 @@ import Router from "koa-router";
import { Profile } from "passport";
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
import { IntegrationService, IntegrationType } from "@shared/types";
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
import accountProvisioner from "@server/commands/accountProvisioner";
import env from "@server/env";
import auth from "@server/middlewares/authentication";
@@ -21,8 +22,8 @@ import {
getTeamFromContext,
StateStore,
} from "@server/utils/passport";
import * as Slack from "@server/utils/slack";
import { assertPresent, assertUuid } from "@server/validation";
import * as Slack from "../slack";
type SlackProfile = Profile & {
team: {
@@ -59,11 +60,6 @@ function redirectOnClient(ctx: Context, url: string) {
</head>`;
}
export const config = {
name: "Slack",
enabled: !!env.SLACK_CLIENT_ID,
};
if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
const strategy = new SlackStrategy(
{
@@ -142,7 +138,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
assertPresent(code || error, "code is required");
if (error) {
ctx.redirect(`/settings/integrations/slack?error=${error}`);
ctx.redirect(integrationSettingsPath(`slack?error=${error}`));
return;
}
@@ -161,12 +157,12 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
);
} catch (err) {
return ctx.redirect(
`/settings/integrations/slack?error=unauthenticated`
integrationSettingsPath(`slack?error=unauthenticated`)
);
}
} else {
return ctx.redirect(
`/settings/integrations/slack?error=unauthenticated`
integrationSettingsPath(`slack?error=unauthenticated`)
);
}
}
@@ -190,7 +186,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
serviceTeamId: data.team_id,
},
});
ctx.redirect("/settings/integrations/slack");
ctx.redirect(integrationSettingsPath("slack"));
}
);
@@ -208,7 +204,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
assertUuid(collectionId, "collectionId must be an uuid");
if (error) {
ctx.redirect(`/settings/integrations/slack?error=${error}`);
ctx.redirect(integrationSettingsPath(`slack?error=${error}`));
return;
}
@@ -232,7 +228,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
);
} catch (err) {
return ctx.redirect(
`/settings/integrations/slack?error=unauthenticated`
integrationSettingsPath(`slack?error=unauthenticated`)
);
}
}
@@ -261,7 +257,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
channelId: data.incoming_webhook.channel_id,
},
});
ctx.redirect("/settings/integrations/slack");
ctx.redirect(integrationSettingsPath("slack"));
}
);
}

View File

@@ -8,7 +8,7 @@ type Action = {
value: string;
};
function presentSlackAttachment(
function presentMessageAttachment(
document: Document,
team: Team,
collection?: Collection | null,
@@ -35,4 +35,4 @@ function presentSlackAttachment(
export default traceFunction({
spanName: "presenters",
})(presentSlackAttachment);
})(presentMessageAttachment);

View File

@@ -3,14 +3,14 @@ import { Op } from "sequelize";
import { IntegrationService, IntegrationType } from "@shared/types";
import env from "@server/env";
import { Document, Integration, Collection, Team } from "@server/models";
import { presentSlackAttachment } from "@server/presenters";
import BaseProcessor from "@server/queues/processors/BaseProcessor";
import {
DocumentEvent,
IntegrationEvent,
RevisionEvent,
Event,
} from "@server/types";
import BaseProcessor from "./BaseProcessor";
import presentMessageAttachment from "../presenters/messageAttachment";
export default class SlackProcessor extends BaseProcessor {
static applicableEvents: Event["name"][] = [
@@ -131,7 +131,7 @@ export default class SlackProcessor extends BaseProcessor {
body: JSON.stringify({
text,
attachments: [
presentSlackAttachment(document, team, document.collection),
presentMessageAttachment(document, team, document.collection),
],
}),
});

View File

@@ -1,7 +1,7 @@
import querystring from "querystring";
import fetch from "fetch-with-proxy";
import env from "@server/env";
import { InvalidRequestError } from "../errors";
import { InvalidRequestError } from "@server/errors";
const SLACK_API_URL = "https://slack.com/api";

View File

@@ -18,7 +18,6 @@ import presentProviderConfig from "./providerConfig";
import presentRevision from "./revision";
import presentSearchQuery from "./searchQuery";
import presentShare from "./share";
import presentSlackAttachment from "./slackAttachment";
import presentStar from "./star";
import presentSubscription from "./subscription";
import presentTeam from "./team";
@@ -48,7 +47,6 @@ export {
presentRevision,
presentSearchQuery,
presentShare,
presentSlackAttachment,
presentStar,
presentSubscription,
presentTeam,

View File

@@ -1,4 +1,4 @@
import { signin } from "@shared/utils/urlHelpers";
import { signin } from "@shared/utils/routeHelpers";
import { AuthenticationProviderConfig } from "@server/routes/auth/providers";
export default function presentProviderConfig(

View File

@@ -1,16 +1,28 @@
import path from "path";
import { glob } from "glob";
import Logger from "@server/logging/Logger";
import { requireDirectory } from "@server/utils/fs";
import BaseProcessor from "./BaseProcessor";
const processors = {};
requireDirectory(__dirname).forEach(([module, id]) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'default' does not exist on type 'unknown'
const { default: Processor } = module;
if (id === "index") {
return;
requireDirectory<{ default: BaseProcessor }>(__dirname).forEach(
([module, id]) => {
if (id === "index") {
return;
}
processors[id] = module.default;
}
);
processors[id] = Processor;
});
glob
.sync("build/plugins/*/server/processors/!(*.test).js")
.forEach((filePath: string) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const processor = require(path.join(process.cwd(), filePath)).default;
const name = path.basename(filePath, ".js");
processors[name] = processor;
Logger.debug("processor", `Registered processor ${name}`);
});
export default processors;

View File

@@ -1,16 +1,28 @@
import path from "path";
import { glob } from "glob";
import Logger from "@server/logging/Logger";
import { requireDirectory } from "@server/utils/fs";
import BaseTask from "./BaseTask";
const tasks = {};
requireDirectory(__dirname).forEach(([module, id]) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'default' does not exist on type 'unknown'
const { default: Task } = module;
if (id === "index") {
return;
requireDirectory<{ default: BaseTask<any> }>(__dirname).forEach(
([module, id]) => {
if (id === "index") {
return;
}
tasks[id] = module.default;
}
);
tasks[id] = Task;
});
glob
.sync("build/plugins/*/server/tasks/!(*.test).js")
.forEach((filePath: string) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const task = require(path.join(process.cwd(), filePath)).default;
const name = path.basename(filePath, ".js");
tasks[name] = task;
Logger.debug("task", `Registered task ${name}`);
});
export default tasks;

View File

@@ -1,9 +1,12 @@
import path from "path";
import glob from "glob";
import Koa, { BaseContext } from "koa";
import bodyParser from "koa-body";
import Router from "koa-router";
import userAgent, { UserAgentContext } from "koa-useragent";
import env from "@server/env";
import { NotFoundError } from "@server/errors";
import Logger from "@server/logging/Logger";
import { AppState, AppContext } from "@server/types";
import apiKeys from "./apiKeys";
import attachments from "./attachments";
@@ -16,7 +19,6 @@ import documents from "./documents";
import events from "./events";
import fileOperationsRoute from "./fileOperations";
import groups from "./groups";
import hooks from "./hooks";
import integrations from "./integrations";
import apiWrapper from "./middlewares/apiWrapper";
import editor from "./middlewares/editor";
@@ -48,6 +50,16 @@ api.use<BaseContext, UserAgentContext>(userAgent);
api.use(apiWrapper());
api.use(editor());
// register package API routes before others to allow for overrides
glob
.sync("build/plugins/*/server/api/!(*.test).js")
.forEach((filePath: string) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg: Router = require(path.join(process.cwd(), filePath)).default;
router.use("/", pkg.routes());
Logger.debug("lifecycle", `Registered API routes for ${filePath}`);
});
// routes
router.use("/", auth.routes());
router.use("/", authenticationProviders.routes());
@@ -58,7 +70,6 @@ router.use("/", documents.routes());
router.use("/", pins.routes());
router.use("/", revisions.routes());
router.use("/", views.routes());
router.use("/", hooks.routes());
router.use("/", apiKeys.routes());
router.use("/", searches.routes());
router.use("/", shares.routes());

View File

@@ -1,5 +1,9 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import path from "path";
import { glob } from "glob";
import Router from "koa-router";
import { sortBy } from "lodash";
import env from "@server/env";
import { requireDirectory } from "@server/utils/fs";
export type AuthenticationProviderConfig = {
@@ -11,8 +15,10 @@ export type AuthenticationProviderConfig = {
const authenticationProviderConfigs: AuthenticationProviderConfig[] = [];
requireDirectory(__dirname).forEach(([module, id]) => {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'config' does not exist on type 'unknown'... Remove this comment to see the full error message
requireDirectory<{
default: Router;
config: { name: string; enabled: boolean };
}>(__dirname).forEach(([module, id]) => {
const { config, default: router } = module;
if (id === "index") {
@@ -41,4 +47,34 @@ requireDirectory(__dirname).forEach(([module, id]) => {
}
});
// Temporarily also include plugins here until all auth methods are moved over.
glob
.sync(
(env.ENVIRONMENT === "test" ? "" : "build/") +
"plugins/*/server/auth/!(*.test).[jt]s"
)
.forEach((filePath: string) => {
const authProvider = require(path.join(process.cwd(), filePath)).default;
const id = filePath.replace("build/", "").split("/")[1];
const config = require(path.join(
process.cwd(),
env.ENVIRONMENT === "test" ? "" : "build",
"plugins",
id,
"plugin.json"
));
// Test the all required env vars are set for the auth provider
const enabled = (config.requiredEnvVars ?? []).every(
(name: string) => !!env[name]
);
authenticationProviderConfigs.push({
id,
name: config.name,
enabled,
router: authProvider,
});
});
export default sortBy(authenticationProviderConfigs, "id");

View File

@@ -9,7 +9,7 @@ export function deserializeFilename(text: string): string {
return text.replace(/%2F/g, "/").replace(/%5C/g, "\\");
}
export function requireDirectory<T>(dirName: string): [T, string][] {
export function getFilenamesInDirectory(dirName: string): string[] {
return fs
.readdirSync(dirName)
.filter(
@@ -18,10 +18,13 @@ export function requireDirectory<T>(dirName: string): [T, string][] {
file.match(/\.[jt]s$/) &&
file !== path.basename(__filename) &&
!file.includes(".test")
)
.map((fileName) => {
const filePath = path.join(dirName, fileName);
const name = path.basename(filePath.replace(/\.[jt]s$/, ""));
return [require(filePath), name];
});
);
}
export function requireDirectory<T>(dirName: string): [T, string][] {
return getFilenamesInDirectory(dirName).map((fileName) => {
const filePath = path.join(dirName, fileName);
const name = path.basename(filePath.replace(/\.[jt]s$/, ""));
return [require(filePath), name];
});
}

View File

@@ -312,8 +312,8 @@
"Groups": "Groups",
"Shared Links": "Shared Links",
"Import": "Import",
"Webhooks": "Webhooks",
"Integrations": "Integrations",
"Webhooks": "Webhooks",
"Self Hosted": "Self Hosted",
"Google Analytics": "Google Analytics",
"Show path to document": "Show path to document",
@@ -662,14 +662,6 @@
"Last accessed": "Last accessed",
"Date shared": "Date shared",
"Shared nested": "Shared nested",
"Add to Slack": "Add to Slack",
"Settings saved": "Settings saved",
"document published": "document published",
"document updated": "document updated",
"Posting to the <em>{{ channelName }}</em> channel on": "Posting to the <em>{{ channelName }}</em> channel on",
"These events should be posted to Slack": "These events should be posted to Slack",
"Document updated": "Document updated",
"Disconnect": "Disconnect",
"API token copied to clipboard": "API token copied to clipboard",
"Revoke token": "Revoke token",
"Copied": "Copied",
@@ -695,6 +687,7 @@
"Subscribed events": "Subscribed events",
"Edit webhook": "Edit webhook",
"Webhook created": "Webhook created",
"Settings saved": "Settings saved",
"Logo updated": "Logo updated",
"Unable to upload new logo": "Unable to upload new logo",
"These settings affect the way that your knowledge base appears to everyone on the team.": "These settings affect the way that your knowledge base appears to everyone on the team.",
@@ -732,6 +725,7 @@
"Everyone that has signed into {{appName}} is listed here. Its possible that there are other users who have access through {team.signinMethods} but havent signed in yet.": "Everyone that has signed into {{appName}} is listed here. Its possible that there are other users who have access through {team.signinMethods} but havent signed in yet.",
"Filter": "Filter",
"Receive a notification whenever a new document is published": "Receive a notification whenever a new document is published",
"Document updated": "Document updated",
"Receive a notification when a document you created is edited": "Receive a notification when a document you created is edited",
"Collection created": "Collection created",
"Receive a notification whenever a new collection is created": "Receive a notification whenever a new collection is created",
@@ -795,12 +789,6 @@
"Sharing is currently disabled.": "Sharing is currently disabled.",
"You can globally enable and disable public document sharing in the <em>security settings</em>.": "You can globally enable and disable public document sharing in the <em>security settings</em>.",
"Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.": "Documents that have been shared are listed below. Anyone that has the public link can access a read-only version of the document until the link has been revoked.",
"Whoops, you need to accept the permissions in Slack to connect{{appName}} to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect{{appName}} to your team. Try again?",
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
"Get rich previews of {{ appName }} links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of {{ appName }} links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
"Connect {{appName}} collections to Slack channels. Messages will be automatically posted to Slack when documents are published or updated.": "Connect {{appName}} collections to Slack channels. Messages will be automatically posted to Slack when documents are published or updated.",
"Connect": "Connect",
"The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
"New token": "New token",
"You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Tokens": "Tokens",
@@ -832,6 +820,18 @@
"This month": "This month",
"Last month": "Last month",
"This year": "This year",
"Add to Slack": "Add to Slack",
"document published": "document published",
"document updated": "document updated",
"Posting to the <em>{{ channelName }}</em> channel on": "Posting to the <em>{{ channelName }}</em> channel on",
"These events should be posted to Slack": "These events should be posted to Slack",
"Disconnect": "Disconnect",
"Whoops, you need to accept the permissions in Slack to connect{{appName}} to your team. Try again?": "Whoops, you need to accept the permissions in Slack to connect{{appName}} to your team. Try again?",
"Something went wrong while authenticating your request. Please try logging in again?": "Something went wrong while authenticating your request. Please try logging in again?",
"Get rich previews of {{ appName }} links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.": "Get rich previews of {{ appName }} links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat.",
"Connect {{appName}} collections to Slack channels. Messages will be automatically posted to Slack when documents are published or updated.": "Connect {{appName}} collections to Slack channels. Messages will be automatically posted to Slack when documents are published or updated.",
"Connect": "Connect",
"The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Slack integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
"To search your knowledgebase use {{ command }}. \nYouve already learned how to get help with {{ command2 }}.": "To search your knowledgebase use {{ command }}. \nYouve already learned how to get help with {{ command2 }}.",
"Sorry, we couldnt find an integration for your team. Head to your {{ appName }} settings to set one up.": "Sorry, we couldnt find an integration for your team. Head to your {{ appName }} settings to set one up.",
"It looks like you havent signed in to {{ appName }} yet, so results may be limited": "It looks like you havent signed in to {{ appName }} yet, so results may be limited",

View File

@@ -0,0 +1,7 @@
export function signin(service = "slack"): string {
return `/auth/${service}`;
}
export function integrationSettingsPath(id: string): string {
return `/settings/integrations/${id}`;
}

View File

@@ -48,10 +48,6 @@ export function changelogUrl(): string {
return "https://www.getoutline.com/changelog";
}
export function signin(service = "slack"): string {
return `/auth/${service}`;
}
export const SLUG_URL_REGEX = /^(?:[0-9a-zA-Z-_~]*-)?([a-zA-Z0-9]{10,15})$/;
export const SHARE_URL_SLUG_REGEX = /^[0-9a-z-]+$/;

View File

@@ -25,6 +25,7 @@ module.exports = {
include: [
path.join(__dirname, 'app'),
path.join(__dirname, 'shared'),
path.join(__dirname, 'plugins'),
],
options: {
cacheDirectory: true

View File

@@ -2985,6 +2985,14 @@
resolved "https://registry.yarnpkg.com/@types/fuzzy-search/-/fuzzy-search-2.1.2.tgz#d57b2af8fb723baa1792d40d511f431c4c8f75af"
integrity sha512-YOqA50Z3xcycm4Br5+MBUpSumfdOAcv34A8A8yFn62zBQPTzJSXQk11qYE5w8BWQ0KrVThXUgEQh7ZLrYI1NaQ==
"@types/glob@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.0.1.tgz#6e3041640148b7764adf21ce5c7138ad454725b0"
integrity sha512-8bVUjXZvJacUFkJXHdyZ9iH1Eaj5V7I8c4NdH5sQJsdXkqT4CA5Dhb4yb4VE/3asyx4L9ayZr1NIhTsWHczmMw==
dependencies:
"@types/minimatch" "^5.1.2"
"@types/node" "*"
"@types/google.analytics@^0.0.42":
version "0.0.42"
resolved "https://registry.yarnpkg.com/@types/google.analytics/-/google.analytics-0.0.42.tgz#efe6ef9251a22ec8208dbb09f221a48a1863d720"
@@ -3250,6 +3258,11 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
"@types/minimatch@^5.1.2":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==
"@types/ms@*":
version "0.7.31"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
@@ -4868,6 +4881,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^2.3.1, braces@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
@@ -8374,6 +8394,17 @@ glob@7.2.0, glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^5.0.1"
once "^1.3.0"
global@~4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
@@ -11209,6 +11240,13 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"