mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
chore: resolve no-explicit-any lint warnings in plugins (#12237)
* chore: resolve no-explicit-any lint warnings in plugins Replaces uses of `any` in the plugins directory with concrete types, `unknown`, or structured type assertions, addressing the remaining typescript-eslint(no-explicit-any) warnings flagged by oxlint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: address review feedback in GitLabIssueProvider Drop trailing semicolon from log string and add early return in `destroyNamespace` when neither `user_id` nor `full_path` is present to avoid an unnecessary full-scan transaction. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { getCookie, removeCookie, setCookie } from "tiny-cookie";
|
import { getCookie, removeCookie, setCookie } from "tiny-cookie";
|
||||||
import usePersistedState, { setPersistedState } from "~/hooks/usePersistedState";
|
import usePersistedState, {
|
||||||
|
setPersistedState,
|
||||||
|
} from "~/hooks/usePersistedState";
|
||||||
import Logger from "~/utils/Logger";
|
import Logger from "~/utils/Logger";
|
||||||
import history from "~/utils/history";
|
import history from "~/utils/history";
|
||||||
import { isAllowedLoginRedirect } from "~/utils/urls";
|
import { isAllowedLoginRedirect } from "~/utils/urls";
|
||||||
@@ -39,9 +41,12 @@ export function useLastVisitedPath(): [string, (path: string) => void] {
|
|||||||
*/
|
*/
|
||||||
export function useTrackLastVisitedPath(currentPath: string): void {
|
export function useTrackLastVisitedPath(currentPath: string): void {
|
||||||
const prevPathRef = useRef<string>();
|
const prevPathRef = useRef<string>();
|
||||||
|
|
||||||
// Update localStorage directly if path has changed
|
// Update localStorage directly if path has changed
|
||||||
if (prevPathRef.current !== currentPath && isAllowedLoginRedirect(currentPath)) {
|
if (
|
||||||
|
prevPathRef.current !== currentPath &&
|
||||||
|
isAllowedLoginRedirect(currentPath)
|
||||||
|
) {
|
||||||
prevPathRef.current = currentPath;
|
prevPathRef.current = currentPath;
|
||||||
setPersistedState("lastVisitedPath", currentPath);
|
setPersistedState("lastVisitedPath", currentPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useMenuAction } from "~/hooks/useMenuAction";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that constructs the action menu for share management operations.
|
* Hook that constructs the action menu for share management operations.
|
||||||
*
|
*
|
||||||
* @param targetShare - the share to build actions for, or null to skip.
|
* @param targetShare - the share to build actions for, or null to skip.
|
||||||
* @returns action with children for use in menus, or undefined if share is null.
|
* @returns action with children for use in menus, or undefined if share is null.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that constructs the action menu for user management operations.
|
* Hook that constructs the action menu for user management operations.
|
||||||
*
|
*
|
||||||
* @param targetUser - the user to build actions for, or null to skip.
|
* @param targetUser - the user to build actions for, or null to skip.
|
||||||
* @returns action with children for use in menus, or undefined if user is null.
|
* @returns action with children for use in menus, or undefined if user is null.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,7 +5,26 @@ import { Integration, IntegrationAuthentication } from "@server/models";
|
|||||||
import { BaseIssueProvider } from "@server/utils/BaseIssueProvider";
|
import { BaseIssueProvider } from "@server/utils/BaseIssueProvider";
|
||||||
import { GitLab } from "./gitlab";
|
import { GitLab } from "./gitlab";
|
||||||
import { sequelize } from "@server/storage/database";
|
import { sequelize } from "@server/storage/database";
|
||||||
import { Op } from "sequelize";
|
import { Op, type WhereOptions } from "sequelize";
|
||||||
|
|
||||||
|
interface GitLabWebhookPayload {
|
||||||
|
event_name?: string;
|
||||||
|
old_full_path?: string;
|
||||||
|
old_username?: string;
|
||||||
|
full_path?: string;
|
||||||
|
username?: string;
|
||||||
|
group_id?: string;
|
||||||
|
user_id?: string;
|
||||||
|
project_id?: string | number;
|
||||||
|
project_namespace_id?: string | number;
|
||||||
|
name?: string;
|
||||||
|
path_with_namespace?: string;
|
||||||
|
changes?: { before: string }[];
|
||||||
|
project?: {
|
||||||
|
name: string;
|
||||||
|
path_with_namespace: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class GitLabIssueProvider extends BaseIssueProvider {
|
export class GitLabIssueProvider extends BaseIssueProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -64,7 +83,8 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
headers: Record<string, unknown>;
|
headers: Record<string, unknown>;
|
||||||
}) {
|
}) {
|
||||||
const hookId = headers["x-gitlab-webhook-uuid"] as string;
|
const hookId = headers["x-gitlab-webhook-uuid"] as string;
|
||||||
const eventName = payload.event_name as string;
|
const typedPayload = payload as GitLabWebhookPayload;
|
||||||
|
const eventName = typedPayload.event_name;
|
||||||
|
|
||||||
if (!eventName) {
|
if (!eventName) {
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
@@ -77,28 +97,28 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
case "project_update":
|
case "project_update":
|
||||||
case "project_transfer":
|
case "project_transfer":
|
||||||
case "project_rename":
|
case "project_rename":
|
||||||
await this.updateProject(payload);
|
await this.updateProject(typedPayload);
|
||||||
break;
|
break;
|
||||||
case "repository_update":
|
case "repository_update":
|
||||||
await this.createProject(payload);
|
await this.createProject(typedPayload);
|
||||||
break;
|
break;
|
||||||
case "project_destroy":
|
case "project_destroy":
|
||||||
await this.destroyProject(payload);
|
await this.destroyProject(typedPayload);
|
||||||
break;
|
break;
|
||||||
case "group_rename":
|
case "group_rename":
|
||||||
case "user_rename":
|
case "user_rename":
|
||||||
await this.updateNamespace(payload);
|
await this.updateNamespace(typedPayload);
|
||||||
break;
|
break;
|
||||||
case "user_destroy":
|
case "user_destroy":
|
||||||
case "group_destroy":
|
case "group_destroy":
|
||||||
await this.destroyNamespace(payload);
|
await this.destroyNamespace(typedPayload);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateNamespace(payload: Record<string, any>) {
|
private async updateNamespace(payload: GitLabWebhookPayload) {
|
||||||
const name = payload.old_full_path ?? payload.old_username;
|
const name = payload.old_full_path ?? payload.old_username;
|
||||||
const where = {
|
const where = {
|
||||||
service: IntegrationService.GitLab,
|
service: IntegrationService.GitLab,
|
||||||
@@ -119,6 +139,12 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newName = payload.full_path ?? payload.username;
|
||||||
|
if (!newName) {
|
||||||
|
Logger.warn(`GitLab namespace_update event without new name`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sources = integration.issueSources ?? [];
|
const sources = integration.issueSources ?? [];
|
||||||
const updatedSources = sources.map((source) => {
|
const updatedSources = sources.map((source) => {
|
||||||
if (source.owner.name === name) {
|
if (source.owner.name === name) {
|
||||||
@@ -126,7 +152,7 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
...source,
|
...source,
|
||||||
owner: {
|
owner: {
|
||||||
id: payload.group_id || source.owner.id,
|
id: payload.group_id || source.owner.id,
|
||||||
name: payload.full_path ?? payload.username,
|
name: newName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -139,19 +165,29 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async destroyNamespace(payload: Record<string, any>) {
|
private async destroyNamespace(payload: GitLabWebhookPayload) {
|
||||||
|
if (!payload.user_id && !payload.full_path) {
|
||||||
|
Logger.warn(
|
||||||
|
`GitLab namespace_destroy event without user_id or full_path`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let replacements = {};
|
let replacements = {};
|
||||||
const whereCondition: any = {
|
const whereCondition: WhereOptions = {
|
||||||
service: IntegrationService.GitLab,
|
service: IntegrationService.GitLab,
|
||||||
|
...(payload.user_id && {
|
||||||
|
"settings.gitlab.installation.account.id": payload.user_id,
|
||||||
|
}),
|
||||||
|
...(!payload.user_id &&
|
||||||
|
payload.full_path && {
|
||||||
|
[Op.and]: sequelize.literal(
|
||||||
|
`"issueSources"::jsonb @> :jsonCondition`
|
||||||
|
),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (payload.user_id) {
|
if (!payload.user_id && payload.full_path) {
|
||||||
whereCondition["settings.gitlab.installation.account.id"] =
|
|
||||||
payload.user_id;
|
|
||||||
} else if (payload.full_path) {
|
|
||||||
whereCondition[Op.and] = sequelize.literal(
|
|
||||||
`"issueSources"::jsonb @> :jsonCondition`
|
|
||||||
);
|
|
||||||
replacements = {
|
replacements = {
|
||||||
jsonCondition: JSON.stringify([{ owner: { name: payload.full_path } }]),
|
jsonCondition: JSON.stringify([{ owner: { name: payload.full_path } }]),
|
||||||
};
|
};
|
||||||
@@ -187,7 +223,7 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async destroyProject(payload: Record<string, any>) {
|
private async destroyProject(payload: GitLabWebhookPayload) {
|
||||||
await sequelize.transaction(async (transaction) => {
|
await sequelize.transaction(async (transaction) => {
|
||||||
const integrations = await Integration.findAll({
|
const integrations = await Integration.findAll({
|
||||||
where: {
|
where: {
|
||||||
@@ -223,14 +259,20 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createProject(payload: Record<string, any>) {
|
private async createProject(payload: GitLabWebhookPayload) {
|
||||||
const createEvent = payload.changes.some((p: { before: string }) =>
|
const createEvent = payload.changes?.some((p: { before: string }) =>
|
||||||
/^0{40}$/.test(p.before)
|
/^0{40}$/.test(p.before)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!createEvent) {
|
if (!createEvent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const project = payload.project;
|
||||||
|
if (!project || !payload.project_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await sequelize.transaction(async (transaction) => {
|
await sequelize.transaction(async (transaction) => {
|
||||||
const integration = (await Integration.findOne({
|
const integration = (await Integration.findOne({
|
||||||
where: {
|
where: {
|
||||||
@@ -245,7 +287,6 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = payload.project;
|
|
||||||
const owner = {
|
const owner = {
|
||||||
id: "", // namespace.id is not provided in this webhook payload
|
id: "", // namespace.id is not provided in this webhook payload
|
||||||
name: project.path_with_namespace.split("/").slice(0, -1).join("/"),
|
name: project.path_with_namespace.split("/").slice(0, -1).join("/"),
|
||||||
@@ -264,7 +305,13 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateProject(payload: Record<string, any>) {
|
private async updateProject(payload: GitLabWebhookPayload) {
|
||||||
|
if (!payload.name || !payload.path_with_namespace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newName = payload.name;
|
||||||
|
const pathWithNamespace = payload.path_with_namespace;
|
||||||
|
|
||||||
await sequelize.transaction(async (transaction) => {
|
await sequelize.transaction(async (transaction) => {
|
||||||
const integrations = await Integration.findAll({
|
const integrations = await Integration.findAll({
|
||||||
where: {
|
where: {
|
||||||
@@ -293,8 +340,8 @@ export class GitLabIssueProvider extends BaseIssueProvider {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (source) {
|
if (source) {
|
||||||
source.name = payload.name;
|
source.name = newName;
|
||||||
source.owner.name = payload.path_with_namespace
|
source.owner.name = pathWithNamespace
|
||||||
.split("/")
|
.split("/")
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.join("/");
|
.join("/");
|
||||||
|
|||||||
@@ -46,7 +46,14 @@ class Iframely {
|
|||||||
return { error: data.error } as UnfurlError; // In addition to our custom UnfurlError, sometimes iframely returns error in the response body.
|
return { error: data.error } as UnfurlError; // In addition to our custom UnfurlError, sometimes iframely returns error in the response body.
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedData = data as Record<string, any>;
|
const parsedData = data as {
|
||||||
|
url: string;
|
||||||
|
meta: { title: string; description: string; site: string };
|
||||||
|
links: {
|
||||||
|
thumbnail?: { href: string }[];
|
||||||
|
icon?: { href: string }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: UnfurlResourceType.URL,
|
type: UnfurlResourceType.URL,
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
import type { Request } from "express";
|
||||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||||
import type OAuth2Strategy from "passport-oauth2";
|
import type OAuth2Strategy from "passport-oauth2";
|
||||||
import { Strategy } from "passport-oauth2";
|
import { Strategy } from "passport-oauth2";
|
||||||
|
|
||||||
|
interface AuthenticateOptions {
|
||||||
|
originalQuery?: Request["query"];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export class OIDCStrategy extends Strategy {
|
export class OIDCStrategy extends Strategy {
|
||||||
constructor(
|
constructor(
|
||||||
options: OAuth2Strategy.StrategyOptionsWithRequest,
|
options: OAuth2Strategy.StrategyOptionsWithRequest,
|
||||||
@@ -15,12 +21,12 @@ export class OIDCStrategy extends Strategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate(req: any, options: any) {
|
authenticate(req: Request, options: AuthenticateOptions) {
|
||||||
options.originalQuery = req.query;
|
options.originalQuery = req.query;
|
||||||
super.authenticate(req, options);
|
super.authenticate(req, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizationParams(options: any) {
|
authorizationParams(options: AuthenticateOptions) {
|
||||||
return {
|
return {
|
||||||
...options.originalQuery,
|
...options.originalQuery,
|
||||||
...super.authorizationParams?.(options),
|
...super.authorizationParams?.(options),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { startRegistration } from "@simplewebauthn/browser";
|
import { startRegistration } from "@simplewebauthn/browser";
|
||||||
|
import type { JSONObject } from "@shared/types";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { KeyIcon, PlusIcon } from "outline-icons";
|
import { KeyIcon, PlusIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@@ -66,9 +67,13 @@ function PasskeysSettings() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
const attResp = await startRegistration(resp.data);
|
const attResp = await startRegistration(resp.data);
|
||||||
await client.post("/passkeys.verifyRegistration", attResp as any, {
|
await client.post(
|
||||||
baseUrl: "/auth",
|
"/passkeys.verifyRegistration",
|
||||||
});
|
attResp as unknown as JSONObject,
|
||||||
|
{
|
||||||
|
baseUrl: "/auth",
|
||||||
|
}
|
||||||
|
);
|
||||||
toast.success(t("Passkey added successfully"));
|
toast.success(t("Passkey added successfully"));
|
||||||
await loadPasskeys();
|
await loadPasskeys();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -8,19 +8,20 @@ describe("getExpectedOrigin", () => {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
host: string;
|
host: string;
|
||||||
forwardedPort?: string;
|
forwardedPort?: string;
|
||||||
}): APIContext => ({
|
}): APIContext =>
|
||||||
protocol: options.protocol,
|
({
|
||||||
request: {
|
protocol: options.protocol,
|
||||||
hostname: options.hostname,
|
request: {
|
||||||
host: options.host,
|
hostname: options.hostname,
|
||||||
get: (header: string) => {
|
host: options.host,
|
||||||
if (header === "X-Forwarded-Port" && options.forwardedPort) {
|
get: (header: string) => {
|
||||||
return options.forwardedPort;
|
if (header === "X-Forwarded-Port" && options.forwardedPort) {
|
||||||
}
|
return options.forwardedPort;
|
||||||
return undefined;
|
}
|
||||||
},
|
return undefined;
|
||||||
} as unknown,
|
},
|
||||||
}) as unknown as APIContext;
|
} as unknown,
|
||||||
|
}) as unknown as APIContext;
|
||||||
|
|
||||||
it("should construct origin with non-standard HTTPS port from X-Forwarded-Port", () => {
|
it("should construct origin with non-standard HTTPS port from X-Forwarded-Port", () => {
|
||||||
const ctx = createMockContext({
|
const ctx = createMockContext({
|
||||||
|
|||||||
@@ -236,13 +236,13 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
|
|||||||
where,
|
where,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
}) as any as Promise<RankedDocument[]>;
|
}) as unknown as Promise<RankedDocument[]>;
|
||||||
|
|
||||||
const countQuery = Document.unscoped().count({
|
const countQuery = Document.unscoped().count({
|
||||||
// @ts-expect-error Types are incorrect for count
|
// @ts-expect-error Types are incorrect for count
|
||||||
replacements: findOptions.replacements,
|
replacements: findOptions.replacements,
|
||||||
where,
|
where,
|
||||||
}) as any as Promise<number>;
|
}) as unknown as Promise<number>;
|
||||||
const [results, count] = await Promise.all([resultsQuery, countQuery]);
|
const [results, count] = await Promise.all([resultsQuery, countQuery]);
|
||||||
|
|
||||||
// Final query to get associated document data
|
// Final query to get associated document data
|
||||||
@@ -428,7 +428,7 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
|
|||||||
where,
|
where,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
})) as any as RankedDocument[];
|
})) as unknown as RankedDocument[];
|
||||||
|
|
||||||
const countQuery = Document.unscoped().count({
|
const countQuery = Document.unscoped().count({
|
||||||
// @ts-expect-error Types are incorrect for count
|
// @ts-expect-error Types are incorrect for count
|
||||||
@@ -436,7 +436,7 @@ export default class PostgresSearchProvider extends BaseSearchProvider {
|
|||||||
include,
|
include,
|
||||||
replacements: findOptions.replacements,
|
replacements: findOptions.replacements,
|
||||||
where,
|
where,
|
||||||
}) as any as Promise<number>;
|
}) as unknown as Promise<number>;
|
||||||
|
|
||||||
// Final query to get associated document data
|
// Final query to get associated document data
|
||||||
const [documents, count] = await Promise.all([
|
const [documents, count] = await Promise.all([
|
||||||
|
|||||||
@@ -238,7 +238,8 @@ router.post(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { results, total } = await SearchProviderManager.getProvider().searchForUser(user, options);
|
const { results, total } =
|
||||||
|
await SearchProviderManager.getProvider().searchForUser(user, options);
|
||||||
|
|
||||||
await SearchQuery.create({
|
await SearchQuery.create({
|
||||||
userId: user ? user.id : null,
|
userId: user ? user.id : null,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import querystring from "node:querystring";
|
import querystring, { type ParsedUrlQueryInput } from "node:querystring";
|
||||||
import { InvalidRequestError } from "@server/errors";
|
import { InvalidRequestError } from "@server/errors";
|
||||||
import fetch from "@server/utils/fetch";
|
import fetch from "@server/utils/fetch";
|
||||||
import { SlackUtils } from "../shared/SlackUtils";
|
import { SlackUtils } from "../shared/SlackUtils";
|
||||||
@@ -13,7 +13,10 @@ const SLACK_API_URL = "https://slack.com/api";
|
|||||||
* @param body - the request body containing token and other parameters.
|
* @param body - the request body containing token and other parameters.
|
||||||
* @returns the parsed JSON response from Slack.
|
* @returns the parsed JSON response from Slack.
|
||||||
*/
|
*/
|
||||||
export async function post(endpoint: string, body: Record<string, any>) {
|
export async function post(
|
||||||
|
endpoint: string,
|
||||||
|
body: { token: string } & Record<string, unknown>
|
||||||
|
) {
|
||||||
let data;
|
let data;
|
||||||
const { token, ...bodyWithoutToken } = body;
|
const { token, ...bodyWithoutToken } = body;
|
||||||
|
|
||||||
@@ -44,7 +47,10 @@ export async function post(endpoint: string, body: Record<string, any>) {
|
|||||||
* @param body - the request parameters.
|
* @param body - the request parameters.
|
||||||
* @returns the parsed JSON response from Slack.
|
* @returns the parsed JSON response from Slack.
|
||||||
*/
|
*/
|
||||||
export async function request(endpoint: string, body: Record<string, any>) {
|
export async function request(
|
||||||
|
endpoint: string,
|
||||||
|
body: { client_id?: string; client_secret?: string } & ParsedUrlQueryInput
|
||||||
|
) {
|
||||||
let data;
|
let data;
|
||||||
const { client_id, client_secret, ...params } = body;
|
const { client_id, client_secret, ...params } = body;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export class SlackUtils {
|
|||||||
static createState(
|
static createState(
|
||||||
teamId: string,
|
teamId: string,
|
||||||
type: IntegrationType,
|
type: IntegrationType,
|
||||||
data?: Record<string, any>
|
data?: Record<string, unknown>
|
||||||
) {
|
) {
|
||||||
return JSON.stringify({ type, teamId, ...data });
|
return JSON.stringify({ type, teamId, ...data });
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-6
@@ -16,7 +16,10 @@ describe("errors", () => {
|
|||||||
describe("User input errors", () => {
|
describe("User input errors", () => {
|
||||||
const userInputErrors = [
|
const userInputErrors = [
|
||||||
{ name: "AuthenticationError", fn: errors.AuthenticationError },
|
{ name: "AuthenticationError", fn: errors.AuthenticationError },
|
||||||
{ name: "InvalidAuthenticationError", fn: errors.InvalidAuthenticationError },
|
{
|
||||||
|
name: "InvalidAuthenticationError",
|
||||||
|
fn: errors.InvalidAuthenticationError,
|
||||||
|
},
|
||||||
{ name: "AuthorizationError", fn: errors.AuthorizationError },
|
{ name: "AuthorizationError", fn: errors.AuthorizationError },
|
||||||
{ name: "CSRFError", fn: errors.CSRFError },
|
{ name: "CSRFError", fn: errors.CSRFError },
|
||||||
{ name: "RateLimitExceededError", fn: errors.RateLimitExceededError },
|
{ name: "RateLimitExceededError", fn: errors.RateLimitExceededError },
|
||||||
@@ -33,12 +36,24 @@ describe("errors", () => {
|
|||||||
{ name: "FileImportError", fn: errors.FileImportError },
|
{ name: "FileImportError", fn: errors.FileImportError },
|
||||||
{ name: "OAuthStateMismatchError", fn: errors.OAuthStateMismatchError },
|
{ name: "OAuthStateMismatchError", fn: errors.OAuthStateMismatchError },
|
||||||
{ name: "TeamPendingDeletionError", fn: errors.TeamPendingDeletionError },
|
{ name: "TeamPendingDeletionError", fn: errors.TeamPendingDeletionError },
|
||||||
{ name: "EmailAuthenticationRequiredError", fn: errors.EmailAuthenticationRequiredError },
|
{
|
||||||
|
name: "EmailAuthenticationRequiredError",
|
||||||
|
fn: errors.EmailAuthenticationRequiredError,
|
||||||
|
},
|
||||||
{ name: "MicrosoftGraphError", fn: errors.MicrosoftGraphError },
|
{ name: "MicrosoftGraphError", fn: errors.MicrosoftGraphError },
|
||||||
{ name: "TeamDomainRequiredError", fn: errors.TeamDomainRequiredError },
|
{ name: "TeamDomainRequiredError", fn: errors.TeamDomainRequiredError },
|
||||||
{ name: "GmailAccountCreationError", fn: errors.GmailAccountCreationError },
|
{
|
||||||
{ name: "OIDCMalformedUserInfoError", fn: errors.OIDCMalformedUserInfoError },
|
name: "GmailAccountCreationError",
|
||||||
{ name: "AuthenticationProviderDisabledError", fn: errors.AuthenticationProviderDisabledError },
|
fn: errors.GmailAccountCreationError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OIDCMalformedUserInfoError",
|
||||||
|
fn: errors.OIDCMalformedUserInfoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AuthenticationProviderDisabledError",
|
||||||
|
fn: errors.AuthenticationProviderDisabledError,
|
||||||
|
},
|
||||||
{ name: "UnprocessableEntityError", fn: errors.UnprocessableEntityError },
|
{ name: "UnprocessableEntityError", fn: errors.UnprocessableEntityError },
|
||||||
{ name: "ClientClosedRequestError", fn: errors.ClientClosedRequestError },
|
{ name: "ClientClosedRequestError", fn: errors.ClientClosedRequestError },
|
||||||
];
|
];
|
||||||
@@ -53,7 +68,9 @@ describe("errors", () => {
|
|||||||
|
|
||||||
describe("UserSuspendedError", () => {
|
describe("UserSuspendedError", () => {
|
||||||
it("should not be marked for Sentry reporting", () => {
|
it("should not be marked for Sentry reporting", () => {
|
||||||
const error = errors.UserSuspendedError({ adminEmail: "test@example.com" });
|
const error = errors.UserSuspendedError({
|
||||||
|
adminEmail: "test@example.com",
|
||||||
|
});
|
||||||
expect(error.isReportable).toBe(false);
|
expect(error.isReportable).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export default class IntegrationCreatedProcessor extends BaseProcessor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Clear the cache of unfurled data for the team as it may be stale now.
|
// Clear the cache of unfurled data for the team as it may be stale now.
|
||||||
await CacheHelper.clearData(RedisPrefixHelper.getUnfurlKey(integration.teamId));
|
await CacheHelper.clearData(
|
||||||
|
RedisPrefixHelper.getUnfurlKey(integration.teamId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export default class IntegrationDeletedProcessor extends BaseProcessor {
|
|||||||
|
|
||||||
// Clear the cache of unfurled data for the team as it may be stale now.
|
// Clear the cache of unfurled data for the team as it may be stale now.
|
||||||
if (integration.type === IntegrationType.Embed) {
|
if (integration.type === IntegrationType.Embed) {
|
||||||
await CacheHelper.clearData(RedisPrefixHelper.getUnfurlKey(integration.teamId));
|
await CacheHelper.clearData(
|
||||||
|
RedisPrefixHelper.getUnfurlKey(integration.teamId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await integration.destroy({ force: true });
|
await integration.destroy({ force: true });
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ router.post(
|
|||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
}),
|
}),
|
||||||
SearchProviderManager.getProvider().searchCollectionsForUser(actor, { query, offset, limit }),
|
SearchProviderManager.getProvider().searchCollectionsForUser(actor, {
|
||||||
|
query,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ export const renderShare = async (ctx: Context, next: Next) => {
|
|||||||
content,
|
content,
|
||||||
shortcutIcon:
|
shortcutIcon:
|
||||||
publicBranding && team?.avatarUrl
|
publicBranding && team?.avatarUrl
|
||||||
? (await team.publicAvatarUrl()) ?? undefined
|
? ((await team.publicAvatarUrl()) ?? undefined)
|
||||||
: undefined,
|
: undefined,
|
||||||
analytics,
|
analytics,
|
||||||
isShare: true,
|
isShare: true,
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ describe("getVersionInfo", () => {
|
|||||||
it("should return version info when Docker Hub is accessible", async () => {
|
it("should return version info when Docker Hub is accessible", async () => {
|
||||||
fetchMock.mockResponseOnce(
|
fetchMock.mockResponseOnce(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
results: [
|
results: [{ name: "0.81.0" }, { name: "0.80.0" }, { name: "0.79.0" }],
|
||||||
{ name: "0.81.0" },
|
|
||||||
{ name: "0.80.0" },
|
|
||||||
{ name: "0.79.0" },
|
|
||||||
],
|
|
||||||
next: null,
|
next: null,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ describe("getDocumentHighlightColors", () => {
|
|||||||
it("returns empty array when no highlights exist", () => {
|
it("returns empty array when no highlights exist", () => {
|
||||||
const testDoc = doc([p("Plain text without highlights")]);
|
const testDoc = doc([p("Plain text without highlights")]);
|
||||||
const state = createEditorState(testDoc);
|
const state = createEditorState(testDoc);
|
||||||
|
|
||||||
const colors = getDocumentHighlightColors(state);
|
const colors = getDocumentHighlightColors(state);
|
||||||
|
|
||||||
expect(colors).toEqual([]);
|
expect(colors).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -15,18 +15,18 @@ describe("getDocumentHighlightColors", () => {
|
|||||||
// Create text with highlight marks
|
// Create text with highlight marks
|
||||||
const highlightMark1 = schema.marks.highlight.create({ color: "#FDEA9B" });
|
const highlightMark1 = schema.marks.highlight.create({ color: "#FDEA9B" });
|
||||||
const highlightMark2 = schema.marks.highlight.create({ color: "#FED46A" });
|
const highlightMark2 = schema.marks.highlight.create({ color: "#FED46A" });
|
||||||
|
|
||||||
const text1 = schema.text("Highlighted text 1", [highlightMark1]);
|
const text1 = schema.text("Highlighted text 1", [highlightMark1]);
|
||||||
const text2 = schema.text(" and more ", [highlightMark2]);
|
const text2 = schema.text(" and more ", [highlightMark2]);
|
||||||
const text3 = schema.text("and again", [highlightMark1]);
|
const text3 = schema.text("and again", [highlightMark1]);
|
||||||
|
|
||||||
const testDoc = doc([
|
const testDoc = doc([
|
||||||
schema.nodes.paragraph.create(null, [text1, text2, text3])
|
schema.nodes.paragraph.create(null, [text1, text2, text3]),
|
||||||
]);
|
]);
|
||||||
const state = createEditorState(testDoc);
|
const state = createEditorState(testDoc);
|
||||||
|
|
||||||
const colors = getDocumentHighlightColors(state);
|
const colors = getDocumentHighlightColors(state);
|
||||||
|
|
||||||
expect(colors).toHaveLength(2);
|
expect(colors).toHaveLength(2);
|
||||||
expect(colors).toContain("#FDEA9B");
|
expect(colors).toContain("#FDEA9B");
|
||||||
expect(colors).toContain("#FED46A");
|
expect(colors).toContain("#FED46A");
|
||||||
@@ -34,18 +34,18 @@ describe("getDocumentHighlightColors", () => {
|
|||||||
|
|
||||||
it("deduplicates colors used multiple times", () => {
|
it("deduplicates colors used multiple times", () => {
|
||||||
const highlightMark = schema.marks.highlight.create({ color: "#FDEA9B" });
|
const highlightMark = schema.marks.highlight.create({ color: "#FDEA9B" });
|
||||||
|
|
||||||
const text1 = schema.text("First highlight", [highlightMark]);
|
const text1 = schema.text("First highlight", [highlightMark]);
|
||||||
const text2 = schema.text("Second highlight", [highlightMark]);
|
const text2 = schema.text("Second highlight", [highlightMark]);
|
||||||
|
|
||||||
const testDoc = doc([
|
const testDoc = doc([
|
||||||
schema.nodes.paragraph.create(null, [text1]),
|
schema.nodes.paragraph.create(null, [text1]),
|
||||||
schema.nodes.paragraph.create(null, [text2])
|
schema.nodes.paragraph.create(null, [text2]),
|
||||||
]);
|
]);
|
||||||
const state = createEditorState(testDoc);
|
const state = createEditorState(testDoc);
|
||||||
|
|
||||||
const colors = getDocumentHighlightColors(state);
|
const colors = getDocumentHighlightColors(state);
|
||||||
|
|
||||||
expect(colors).toHaveLength(1);
|
expect(colors).toHaveLength(1);
|
||||||
expect(colors).toContain("#FDEA9B");
|
expect(colors).toContain("#FDEA9B");
|
||||||
});
|
});
|
||||||
@@ -54,20 +54,20 @@ describe("getDocumentHighlightColors", () => {
|
|||||||
const highlightMark1 = schema.marks.highlight.create({ color: "#FDEA9B" });
|
const highlightMark1 = schema.marks.highlight.create({ color: "#FDEA9B" });
|
||||||
const highlightMark2 = schema.marks.highlight.create({ color: "#FED46A" });
|
const highlightMark2 = schema.marks.highlight.create({ color: "#FED46A" });
|
||||||
const highlightMark3 = schema.marks.highlight.create({ color: "#FA551E" });
|
const highlightMark3 = schema.marks.highlight.create({ color: "#FA551E" });
|
||||||
|
|
||||||
const text1 = schema.text("First paragraph", [highlightMark1]);
|
const text1 = schema.text("First paragraph", [highlightMark1]);
|
||||||
const text2 = schema.text("Second paragraph", [highlightMark2]);
|
const text2 = schema.text("Second paragraph", [highlightMark2]);
|
||||||
const text3 = schema.text("Third paragraph", [highlightMark3]);
|
const text3 = schema.text("Third paragraph", [highlightMark3]);
|
||||||
|
|
||||||
const testDoc = doc([
|
const testDoc = doc([
|
||||||
schema.nodes.paragraph.create(null, [text1]),
|
schema.nodes.paragraph.create(null, [text1]),
|
||||||
schema.nodes.paragraph.create(null, [text2]),
|
schema.nodes.paragraph.create(null, [text2]),
|
||||||
schema.nodes.paragraph.create(null, [text3])
|
schema.nodes.paragraph.create(null, [text3]),
|
||||||
]);
|
]);
|
||||||
const state = createEditorState(testDoc);
|
const state = createEditorState(testDoc);
|
||||||
|
|
||||||
const colors = getDocumentHighlightColors(state);
|
const colors = getDocumentHighlightColors(state);
|
||||||
|
|
||||||
expect(colors).toHaveLength(3);
|
expect(colors).toHaveLength(3);
|
||||||
expect(colors).toContain("#FDEA9B");
|
expect(colors).toContain("#FDEA9B");
|
||||||
expect(colors).toContain("#FED46A");
|
expect(colors).toContain("#FED46A");
|
||||||
@@ -77,17 +77,17 @@ describe("getDocumentHighlightColors", () => {
|
|||||||
it("ignores text with other marks but no highlight", () => {
|
it("ignores text with other marks but no highlight", () => {
|
||||||
const boldMark = schema.marks.strong.create();
|
const boldMark = schema.marks.strong.create();
|
||||||
const highlightMark = schema.marks.highlight.create({ color: "#FDEA9B" });
|
const highlightMark = schema.marks.highlight.create({ color: "#FDEA9B" });
|
||||||
|
|
||||||
const boldText = schema.text("Bold text", [boldMark]);
|
const boldText = schema.text("Bold text", [boldMark]);
|
||||||
const highlightedText = schema.text("Highlighted text", [highlightMark]);
|
const highlightedText = schema.text("Highlighted text", [highlightMark]);
|
||||||
|
|
||||||
const testDoc = doc([
|
const testDoc = doc([
|
||||||
schema.nodes.paragraph.create(null, [boldText, highlightedText])
|
schema.nodes.paragraph.create(null, [boldText, highlightedText]),
|
||||||
]);
|
]);
|
||||||
const state = createEditorState(testDoc);
|
const state = createEditorState(testDoc);
|
||||||
|
|
||||||
const colors = getDocumentHighlightColors(state);
|
const colors = getDocumentHighlightColors(state);
|
||||||
|
|
||||||
expect(colors).toHaveLength(1);
|
expect(colors).toHaveLength(1);
|
||||||
expect(colors).toContain("#FDEA9B");
|
expect(colors).toContain("#FDEA9B");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export function getDocumentHighlightColors(state: EditorState): string[] {
|
|||||||
|
|
||||||
state.doc.descendants((node) => {
|
state.doc.descendants((node) => {
|
||||||
if (node.isText) {
|
if (node.isText) {
|
||||||
const highlightMark = node.marks.find((mark) => mark.type.name === "highlight");
|
const highlightMark = node.marks.find(
|
||||||
|
(mark) => mark.type.name === "highlight"
|
||||||
|
);
|
||||||
if (highlightMark?.attrs.color) {
|
if (highlightMark?.attrs.color) {
|
||||||
colors.add(highlightMark.attrs.color);
|
colors.add(highlightMark.attrs.color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,14 +54,17 @@ export default function markdownItAlphaLists(md: MarkdownIt): void {
|
|||||||
|
|
||||||
// Post-process tokens to add the listStyle attribute
|
// Post-process tokens to add the listStyle attribute
|
||||||
md.core.ruler.after("block", "alpha_lists_postprocess", (state) => {
|
md.core.ruler.after("block", "alpha_lists_postprocess", (state) => {
|
||||||
if (!state.env.alphaListMarkers || state.env.alphaListMarkers.length === 0) {
|
if (
|
||||||
|
!state.env.alphaListMarkers ||
|
||||||
|
state.env.alphaListMarkers.length === 0
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const markers = state.env.alphaListMarkers;
|
const markers = state.env.alphaListMarkers;
|
||||||
|
|
||||||
// Build a map of line numbers to markers for more reliable matching
|
// Build a map of line numbers to markers for more reliable matching
|
||||||
const lineToMarkerMap = new Map<number, typeof markers[0]>();
|
const lineToMarkerMap = new Map<number, (typeof markers)[0]>();
|
||||||
for (const marker of markers) {
|
for (const marker of markers) {
|
||||||
lineToMarkerMap.set(marker.lineIndex, marker);
|
lineToMarkerMap.set(marker.lineIndex, marker);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user