Compare commits

...

20 Commits

Author SHA1 Message Date
codegen-sh[bot] b378af2294 Fix linting issues in OIDCStrategy 2025-08-19 00:15:29 +00:00
codegen-sh[bot] 5fec2681da Fix TypeScript errors in OIDCStrategy 2025-08-19 00:11:01 +00:00
codegen-sh[bot] 64f51ee40d Fix linting issues: replace any with unknown types and add missing React keys 2025-08-19 00:09:05 +00:00
codegen-sh[bot] fec0f193a8 Fix linting issues 2025-08-19 00:02:14 +00:00
codegen-sh[bot] b689ebd8ca fix: Update GitLabIssueStatusIcon to match style of other icon components 2025-08-18 23:49:32 +00:00
codegen-sh[bot] 8e74bb7d01 fix: add type guard for draft property in GitLabIssueStatusIcon 2025-08-18 23:20:56 +00:00
codegen-sh[bot] c02bc22cce Fix code formatting issues to pass CI checks 2025-08-18 23:16:18 +00:00
codegen-sh[bot] 7ea308a52c Fix linting issues in React Hook dependencies 2025-08-18 23:15:39 +00:00
codegen-sh[bot] e4d1a38367 fix: Fix TypeScript errors in OIDCStrategy
- Change Record<string, unknown> to any to match the Strategy interface
- Add null check for options parameter
2025-08-18 23:15:22 +00:00
codegen-sh[bot] 98d54da0de fix: Remove trailing whitespace in HoverPreviewIssue.tsx 2025-08-18 23:03:21 +00:00
codegen-sh[bot] 4b638ae346 fix: fix remaining linting errors 2025-08-18 22:38:22 +00:00
codegen-sh[bot] abb849e1f6 fix: replace any with unknown in types and add proper type definitions 2025-08-18 22:30:48 +00:00
codegen-sh[bot] 2ec65e3dfc fix: replace any with unknown in MutexLock 2025-08-18 22:27:16 +00:00
codegen-sh[bot] 4e493972e5 fix: fix remaining linting errors 2025-08-18 22:24:07 +00:00
codegen-sh[bot] 4a8b8d5fa7 fix: replace any types with unknown to fix linting errors 2025-08-18 22:19:11 +00:00
codegen-sh[bot] 391fc5fdee fix: replace any types in MultiplayerEditor and SlackUtils 2025-08-18 22:15:50 +00:00
codegen-sh[bot] cbcf7d6a8e fix: replace more any types with more specific types to fix linting errors 2025-08-18 22:15:05 +00:00
codegen-sh[bot] 94eb1aa07d fix: replace any types with more specific types to fix linting errors 2025-08-18 22:13:36 +00:00
codegen-sh[bot] ca66a6b2fa fix: Fix linting issues in GitLab integration and OIDC strategy
- Remove unused IntegrationService import in UploadGitLabProjectAvatarTask
- Replace 'any' type with proper types in gitlab.ts and OIDCStrategy.ts
2025-08-18 21:59:03 +00:00
codegen-sh[bot] 404a5991b3 feat: Add GitLab integration matching Linear integration patterns 2025-08-18 21:47:16 +00:00
52 changed files with 1294 additions and 110 deletions
+4
View File
@@ -211,6 +211,10 @@ GITHUB_APP_PRIVATE_KEY=
LINEAR_CLIENT_ID=
LINEAR_CLIENT_SECRET=
# The GitLab integration allows previewing issue and merge request links as rich mentions
GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=
# For a complete Slack integration with search and posting to channels the
# following configs are also needed in addition to Slack authentication:
# DOCS: https://docs.getoutline.com/s/hosting/doc/slack-G2mc8DOJHk
+1 -1
View File
@@ -125,8 +125,8 @@ function Collaborators(props: Props) {
return (
<AvatarWithPresence
{...rest}
key={collaborator.id}
{...rest}
user={collaborator}
isPresent={isPresent}
isEditing={isEditing}
@@ -30,10 +30,15 @@ const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
) {
const authorName = author.name;
const urlObj = new URL(url);
const service =
urlObj.hostname === "github.com"
? IntegrationService.GitHub
: IntegrationService.Linear;
let service;
if (urlObj.hostname === "github.com") {
service = IntegrationService.GitHub;
} else if (urlObj.hostname === "gitlab.com") {
service = IntegrationService.GitLab;
} else {
service = IntegrationService.Linear;
}
return (
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from "react";
import lazyWithRetry from "~/utils/lazyWithRetry";
export interface LazyComponent<T extends React.ComponentType<any>> {
export interface LazyComponent<T extends React.ComponentType<unknown>> {
Component: React.LazyExoticComponent<T>;
preload: () => Promise<{ default: T }>;
}
@@ -34,7 +34,7 @@ interface LazyLoadOptions {
* MyComponent.preload();
* ```
*/
export function createLazyComponent<T extends React.ComponentType<any>>(
export function createLazyComponent<T extends React.ComponentType<unknown>>(
factory: () => Promise<{ default: T }>,
options: LazyLoadOptions = {}
): LazyComponent<T> {
+1 -1
View File
@@ -14,7 +14,7 @@ describe("PaginatedList", () => {
i18n,
tReady: true,
t: ((key: string) => key) as TFunction,
} as any;
} as unknown;
it("with no items renders nothing", () => {
const result = render(
+6 -4
View File
@@ -34,11 +34,11 @@ interface Props<T extends PaginatedItem>
* @param options Pagination and other query options
*/
fetch?: (
options: Record<string, any> | undefined
options: Record<string, unknown> | undefined
) => Promise<unknown[] | undefined> | undefined;
/** Additional options to pass to the fetch function */
options?: Record<string, any>;
options?: Record<string, unknown>;
/** Optional header content to display above the list */
heading?: React.ReactNode;
@@ -77,7 +77,9 @@ interface Props<T extends PaginatedItem>
* Function to render section headings (typically date-based)
* @param name The heading text or element to render
*/
renderHeading?: (name: React.ReactElement<any> | string) => React.ReactNode;
renderHeading?: (
name: React.ReactElement<unknown> | string
) => React.ReactNode;
/**
* Handler for escape key press
@@ -206,7 +208,7 @@ const PaginatedList = <T extends PaginatedItem>({
if (fetch) {
void fetchResults();
}
}, [fetch]);
}, [fetch, fetchResults]);
// Handle updates to fetch or options
React.useEffect(() => {
+1 -1
View File
@@ -9,7 +9,7 @@ function Toasts() {
return (
<StyledToaster
theme={ui.resolvedTheme as any}
theme={ui.resolvedTheme as unknown}
closeButton
toastOptions={{
duration: 5000,
+24 -18
View File
@@ -70,7 +70,7 @@ const LinkEditor: React.FC<Props> = ({
React.useCallback(async () => {
const res = await client.post("/suggestions.mention", { query });
res.data.documents.map(documents.add);
}, [query])
}, [query, documents.add])
);
useEffect(() => {
@@ -79,6 +79,22 @@ const LinkEditor: React.FC<Props> = ({
}
}, [trimmedQuery, request]);
const save = React.useCallback(
(href: string, title?: string) => {
href = href.trim();
if (href.length === 0) {
return;
}
discardRef.current = true;
href = sanitizeUrl(href) ?? "";
onSelectLink({ href, title, from, to });
},
[onSelectLink, from, to]
);
useEffect(() => {
const handleGlobalKeyDown = (event: KeyboardEvent) => {
if (event.key === "k" && event.metaKey) {
@@ -107,20 +123,7 @@ const LinkEditor: React.FC<Props> = ({
save(trimmedQuery, trimmedQuery);
};
}, [trimmedQuery, initialValue]);
const save = (href: string, title?: string) => {
href = href.trim();
if (href.length === 0) {
return;
}
discardRef.current = true;
href = sanitizeUrl(href) ?? "";
onSelectLink({ href, title, from, to });
};
}, [trimmedQuery, initialValue, handleRemoveLink, save]);
const moveSelectionToEnd = () => {
const { state, dispatch } = view;
@@ -195,7 +198,7 @@ const LinkEditor: React.FC<Props> = ({
}
};
const handleRemoveLink = () => {
const handleRemoveLink = React.useCallback(() => {
discardRef.current = true;
const { state, dispatch } = view;
@@ -203,9 +206,12 @@ const LinkEditor: React.FC<Props> = ({
dispatch(state.tr.removeMark(from, to, mark));
}
onRemoveLink?.();
if (onRemoveLink) {
onRemoveLink();
}
view.focus();
};
}, [view, mark, from, to, onRemoveLink]);
const isInternal = isInternalUrl(query);
const hasResults = !!results.length;
+10 -1
View File
@@ -184,7 +184,16 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
setItems(items);
setLoaded(true);
}
}, [t, actorId, loading, search, users, documents, maxResultsInSection]);
}, [
t,
actorId,
loading,
search,
users,
documents,
maxResultsInSection,
collections,
]);
const handleSelect = useCallback(
async (item: MentionItem) => {
+1 -1
View File
@@ -87,7 +87,7 @@ function useIsActive(state: EditorState) {
const slice = selection.content();
const fragment = slice.content;
const nodes = (fragment as any).content;
const nodes = (fragment as unknown).content;
return some(nodes, (n) => n.content.size);
}
+1 -1
View File
@@ -55,7 +55,7 @@ type Props = Omit<EditorProps, "editorStyle"> & {
* The main document editor includes an editable title with metadata below it,
* and support for commenting.
*/
function DocumentEditor(props: Props, ref: React.RefObject<any>) {
function DocumentEditor(props: Props, ref: React.RefObject<unknown>) {
const titleRef = React.useRef<RefHandle>(null);
const { t } = useTranslation();
const match = useRouteMatch();
@@ -51,7 +51,10 @@ type MessageEvent = {
};
};
function MultiplayerEditor({ onSynced, ...props }: Props, ref: any) {
function MultiplayerEditor(
{ onSynced, ...props }: Props,
ref: React.Ref<unknown>
) {
const documentId = props.id;
const history = useHistory();
const { t } = useTranslation();
+3 -2
View File
@@ -1,3 +1,4 @@
import * as React from "react";
import { useState, useRef, useEffect } from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
@@ -132,10 +133,10 @@ function Authorize() {
{t("Required OAuth parameters are missing")}
<Pre>
{missingParams.map((param) => (
<>
<React.Fragment key={param}>
{param}
<br />
</>
</React.Fragment>
))}
</Pre>
</Text>
@@ -1,5 +1,6 @@
import { observer } from "mobx-react";
import { CopyIcon } from "outline-icons";
import * as React from "react";
import { useState, useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -50,10 +51,10 @@ const ApiKeyListItem = ({ apiKey }: Props) => {
{apiKey.scope && (
<Tooltip
content={apiKey.scope.map((s) => (
<>
<React.Fragment key={s}>
{s}
<br />
</>
</React.Fragment>
))}
>
<Text type="tertiary">{t("Restricted scope")}</Text>
+7
View File
@@ -34,6 +34,13 @@ class IntegrationsStore extends Store<Integration> {
(integration) => integration.service === IntegrationService.Linear
);
}
@computed
get gitlab(): Integration<IntegrationType.Embed>[] {
return this.orderedData.filter(
(integration) => integration.service === IntegrationService.GitLab
);
}
}
export default IntegrationsStore;
+2 -2
View File
@@ -125,7 +125,7 @@ export type Action = {
* Perform the action note this should generally not be called directly, use `performAction`
* instead. Errors will be caught and displayed to the user as a toast message.
*/
perform?: (context: ActionContext) => any;
perform?: (context: ActionContext) => unknown;
to?: string | { url: string; target?: string };
children?: ((context: ActionContext) => Action[]) | Action[];
};
@@ -154,7 +154,7 @@ export type ActionV2 = BaseActionV2 & {
tooltip?:
| ((context: ActionContext) => React.ReactChild | undefined)
| React.ReactChild;
perform: (context: ActionContext) => any;
perform: (context: ActionContext) => unknown;
};
export type InternalLinkActionV2 = BaseActionV2 & {
+4 -4
View File
@@ -47,7 +47,7 @@ class ApiClient {
this.shareId = shareId;
};
fetch = async <T = any>(
fetch = async <T = unknown>(
path: string,
method: string,
data: JSONObject | FormData | undefined,
@@ -180,7 +180,7 @@ class ApiClient {
const error: {
message?: string;
error?: string;
data?: Record<string, any>;
data?: Record<string, unknown>;
} = {};
try {
@@ -244,13 +244,13 @@ class ApiClient {
throw err;
};
get = <T = any>(
get = <T = unknown>(
path: string,
data: JSONObject | undefined,
options?: FetchOptions
) => this.fetch<T>(path, "GET", data, options);
post = <T = any>(
post = <T = unknown>(
path: string,
data?: JSONObject | FormData | undefined,
options?: FetchOptions
+1 -1
View File
@@ -13,7 +13,7 @@ type LogCategory =
| "plugins"
| "policies";
type Extra = Record<string, any>;
type Extra = Record<string, unknown>;
class Logger {
/**
+3 -3
View File
@@ -1,6 +1,6 @@
import * as React from "react";
type ComponentPromise<T extends React.ComponentType<any>> = Promise<{
type ComponentPromise<T extends React.ComponentType<unknown>> = Promise<{
default: T;
}>;
@@ -12,7 +12,7 @@ type ComponentPromise<T extends React.ComponentType<any>> = Promise<{
* @param interval The interval between retries in milliseconds, defaults to 1000.
* @returns A lazy component.
*/
export default function lazyWithRetry<T extends React.ComponentType<any>>(
export default function lazyWithRetry<T extends React.ComponentType<unknown>>(
component: () => ComponentPromise<T>,
retries?: number,
interval?: number
@@ -20,7 +20,7 @@ export default function lazyWithRetry<T extends React.ComponentType<any>>(
return React.lazy(() => retry(component, retries, interval));
}
function retry<T extends React.ComponentType<any>>(
function retry<T extends React.ComponentType<unknown>>(
fn: () => ComponentPromise<T>,
retriesLeft = 3,
interval = 1000
+21
View File
@@ -37,6 +37,17 @@ export const isURLMentionable = ({
);
}
case IntegrationService.GitLab: {
const settings =
integration.settings as IntegrationSettings<IntegrationType.Embed>;
return (
hostname === "gitlab.com" &&
settings.gitlab?.project.path_with_namespace ===
pathParts.slice(1, -2).join("/") // ensure installed project path matches with the provided url.
);
}
default:
return false;
}
@@ -67,6 +78,16 @@ export const determineMentionType = ({
return type === "issue" ? MentionType.Issue : undefined;
}
case IntegrationService.GitLab: {
const type = pathParts[pathParts.length - 2];
if (type === "issues") {
return MentionType.Issue;
} else if (type === "merge_requests") {
return MentionType.PullRequest;
}
return undefined;
}
default:
return;
}
+1
View File
@@ -352,6 +352,7 @@
"browserslist-to-esbuild": "^1.2.0",
"concurrently": "^8.2.2",
"discord-api-types": "^0.37.119",
"eslint": "^9.33.0",
"husky": "^8.0.3",
"i18next-parser": "^8.13.0",
"ioredis-mock": "^8.9.0",
+50
View File
@@ -0,0 +1,50 @@
import * as React from "react";
type Props = {
/** The size of the icon, 24px is default to match standard icons */
size?: number;
/** The color of the icon, defaults to the current text color */
fill?: string;
};
export default function Icon({ size = 24, fill = "currentColor" }: Props) {
return (
<svg
fill={fill}
width={size}
height={size}
viewBox="0 0 24 24"
version="1.1"
>
<path
d="M12 20.8L4.6 13.4L6.3 7.8L12 13.4L17.7 7.8L19.4 13.4L12 20.8Z"
fillRule="evenodd"
clipRule="evenodd"
/>
<path
d="M12 20.8L4.6 13.4L6.3 7.8L12 13.4L12 20.8Z"
fillRule="evenodd"
clipRule="evenodd"
fillOpacity="0.3"
/>
<path
d="M4.6 13.4L2.5 7.8L6.3 7.8L4.6 13.4Z"
fillRule="evenodd"
clipRule="evenodd"
fillOpacity="0.5"
/>
<path
d="M19.4 13.4L21.5 7.8L17.7 7.8L19.4 13.4Z"
fillRule="evenodd"
clipRule="evenodd"
fillOpacity="0.5"
/>
<path
d="M6.3 7.8L8.7 2.2L15.3 2.2L17.7 7.8L6.3 7.8Z"
fillRule="evenodd"
clipRule="evenodd"
fillOpacity="0.7"
/>
</svg>
);
}
+139
View File
@@ -0,0 +1,139 @@
import { observer } from "mobx-react";
import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { IntegrationService } from "@shared/types";
import { ConnectedButton } from "~/scenes/Settings/components/ConnectedButton";
import { AvatarSize } from "~/components/Avatar";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
import List from "~/components/List";
import ListItem from "~/components/List/Item";
import Notice from "~/components/Notice";
import PlaceholderText from "~/components/PlaceholderText";
import Scene from "~/components/Scene";
import TeamLogo from "~/components/TeamLogo";
import Text from "~/components/Text";
import Time from "~/components/Time";
import env from "~/env";
import useQuery from "~/hooks/useQuery";
import useStores from "~/hooks/useStores";
import GitLabIcon from "./Icon";
import { GitLabConnectButton } from "./components/GitLabButton";
function GitLab() {
const { integrations } = useStores();
const { t } = useTranslation();
const query = useQuery();
const error = query.get("error");
const appName = env.APP_NAME;
React.useEffect(() => {
void integrations.fetchAll({
service: IntegrationService.GitLab,
withRelations: true,
});
}, [integrations]);
return (
<Scene title="GitLab" icon={<GitLabIcon />}>
<Heading>GitLab</Heading>
{error === "access_denied" && (
<Notice>
<Trans>
Whoops, you need to accept the permissions in GitLab to connect{" "}
{{ appName }} to your project. Try again?
</Trans>
</Notice>
)}
{error === "unauthenticated" && (
<Notice>
<Trans>
Something went wrong while authenticating your request. Please try
logging in again.
</Trans>
</Notice>
)}
{env.GITLAB_CLIENT_ID ? (
<>
<Text as="p">
<Trans>
Enable previews of GitLab issues and merge requests in documents
by connecting a GitLab project to {appName}.
</Trans>
</Text>
{integrations.gitlab.length ? (
<>
<Heading as="h2">
<Flex justify="space-between" auto>
{t("Connected")}
<GitLabConnectButton icon={<PlusIcon />} />
</Flex>
</Heading>
<List>
{integrations.gitlab.map((integration) => {
const gitlabProject = integration.settings?.gitlab?.project;
const integrationCreatedBy = integration.user
? integration.user.name
: undefined;
return (
<ListItem
key={gitlabProject?.id}
small
title={gitlabProject?.name}
subtitle={
integrationCreatedBy ? (
<>
<Trans>Enabled by {{ integrationCreatedBy }}</Trans>{" "}
&middot;{" "}
<Time
dateTime={integration.createdAt}
relative={false}
format={{ en_US: "MMMM d, y" }}
/>
</>
) : (
<PlaceholderText />
)
}
image={
<TeamLogo
src={gitlabProject?.avatar_url}
size={AvatarSize.Large}
/>
}
actions={
<ConnectedButton
onClick={integration.delete}
confirmationMessage={t(
"Disconnecting will prevent previewing GitLab links from this project in documents. Are you sure?"
)}
/>
}
/>
);
})}
</List>
</>
) : (
<p>
<GitLabConnectButton icon={<GitLabIcon />} />
</p>
)}
</>
) : (
<Notice>
<Trans>
The GitLab integration is currently disabled. Please set the
associated environment variables and restart the server to enable
the integration.
</Trans>
</Notice>
)}
</Scene>
);
}
export default observer(GitLab);
@@ -0,0 +1,23 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import Button, { type Props } from "~/components/Button";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import { redirectTo } from "~/utils/urls";
import { GitLabUtils } from "../../shared/GitLabUtils";
export function GitLabConnectButton(props: Props<HTMLButtonElement>) {
const { t } = useTranslation();
const team = useCurrentTeam();
return (
<Button
onClick={() =>
redirectTo(GitLabUtils.authUrl({ state: { teamId: team.id } }))
}
neutral
{...props}
>
{t("Connect")}
</Button>
);
}
+16
View File
@@ -0,0 +1,16 @@
import * as React from "react";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
PluginManager.add([
{
...config,
type: Hook.Settings,
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
},
},
]);
+7
View File
@@ -0,0 +1,7 @@
{
"id": "gitlab",
"name": "GitLab",
"priority": 16,
"description": "Adds a GitLab integration for link unfurling and converting links to mentions.",
"after": "linear"
}
+99
View File
@@ -0,0 +1,99 @@
import Router from "koa-router";
import { IntegrationService, IntegrationType } from "@shared/types";
import Logger from "@server/logging/Logger";
import apexAuthRedirect from "@server/middlewares/apexAuthRedirect";
import auth from "@server/middlewares/authentication";
import { transaction } from "@server/middlewares/transaction";
import validate from "@server/middlewares/validate";
import { IntegrationAuthentication, Integration } from "@server/models";
import { APIContext } from "@server/types";
import { GitLab } from "../gitlab";
import UploadGitLabProjectAvatarTask from "../tasks/UploadGitLabProjectAvatarTask";
import * as T from "./schema";
import { GitLabUtils } from "plugins/gitlab/shared/GitLabUtils";
const router = new Router();
router.get(
"gitlab.callback",
auth({
optional: true,
}),
validate(T.GitLabCallbackSchema),
apexAuthRedirect<T.GitLabCallbackReq>({
getTeamId: (ctx) => GitLabUtils.parseState(ctx.input.query.state)?.teamId,
getRedirectPath: (ctx, team) =>
GitLabUtils.callbackUrl({
baseUrl: team.url,
params: ctx.request.querystring,
}),
getErrorPath: () => GitLabUtils.errorUrl("unauthenticated"),
}),
transaction(),
async (ctx: APIContext<T.GitLabCallbackReq>) => {
const { code, error } = ctx.input.query;
const { user } = ctx.state.auth;
const { transaction } = ctx.state;
// Check error after any sub-domain redirection. Otherwise, the user will be redirected to the root domain.
if (error) {
ctx.redirect(GitLabUtils.errorUrl(error));
return;
}
try {
// validation middleware ensures that code is non-null at this point.
const oauth = await GitLab.oauthAccess(code!);
const project = await GitLab.getInstalledProject(oauth.access_token);
const authentication = await IntegrationAuthentication.create(
{
service: IntegrationService.GitLab,
userId: user.id,
teamId: user.teamId,
token: oauth.access_token,
scopes: oauth.scope.split(" "),
},
{ transaction }
);
const integration = await Integration.create<
Integration<IntegrationType.Embed>
>(
{
service: IntegrationService.GitLab,
type: IntegrationType.Embed,
userId: user.id,
teamId: user.teamId,
authenticationId: authentication.id,
settings: {
gitlab: {
project: {
id: project.id,
name: project.name,
path_with_namespace: project.path_with_namespace,
avatar_url: project.avatar_url,
},
},
},
},
{ transaction }
);
transaction.afterCommit(async () => {
if (project.avatar_url) {
await new UploadGitLabProjectAvatarTask().schedule({
integrationId: integration.id,
avatarUrl: project.avatar_url,
});
}
});
ctx.redirect(GitLabUtils.successUrl());
} catch (err) {
Logger.error("Encountered error during GitLab OAuth callback", err);
ctx.redirect(GitLabUtils.errorUrl("unknown"));
}
}
);
export default router;
+17
View File
@@ -0,0 +1,17 @@
import isEmpty from "lodash/isEmpty";
import { z } from "zod";
import { BaseSchema } from "@server/routes/api/schema";
export const GitLabCallbackSchema = BaseSchema.extend({
query: z
.object({
code: z.string().nullish(),
state: z.string(),
error: z.string().nullish(),
})
.refine((req) => !(isEmpty(req.code) && isEmpty(req.error)), {
message: "one of code or error is required",
}),
});
export type GitLabCallbackReq = z.infer<typeof GitLabCallbackSchema>;
+6
View File
@@ -0,0 +1,6 @@
import env from "@server/env";
export default {
GITLAB_CLIENT_ID: env.GITLAB_CLIENT_ID,
GITLAB_CLIENT_SECRET: env.GITLAB_CLIENT_SECRET,
};
+273
View File
@@ -0,0 +1,273 @@
import { z } from "zod";
import {
IntegrationService,
IntegrationType,
UnfurlResourceType,
} from "@shared/types";
import Logger from "@server/logging/Logger";
import { Integration } from "@server/models";
import User from "@server/models/User";
import { UnfurlIssueOrPR, UnfurlSignature } from "@server/types";
import { GitLabUtils } from "../shared/GitLabUtils";
import env from "./env";
const AccessTokenResponseSchema = z.object({
access_token: z.string(),
token_type: z.string(),
expires_in: z.number(),
refresh_token: z.string(),
scope: z.string(),
created_at: z.number(),
});
const GitLabProjectSchema = z.object({
id: z.string(),
name: z.string(),
path_with_namespace: z.string(),
avatar_url: z.string().optional(),
});
const GitLabIssueSchema = z.object({
id: z.number(),
iid: z.number(),
title: z.string(),
description: z.string().nullable(),
state: z.string(),
created_at: z.string(),
author: z.object({
id: z.number(),
name: z.string(),
avatar_url: z.string().nullable(),
}),
labels: z.array(z.string()).optional(),
});
const GitLabMergeRequestSchema = z.object({
id: z.number(),
iid: z.number(),
title: z.string(),
description: z.string().nullable(),
state: z.string(),
created_at: z.string(),
author: z.object({
id: z.number(),
name: z.string(),
avatar_url: z.string().nullable(),
}),
labels: z.array(z.string()).optional(),
draft: z.boolean().optional(),
});
export class GitLab {
private static supportedUnfurls = [
UnfurlResourceType.Issue,
UnfurlResourceType.PR,
];
static async oauthAccess(code: string) {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
};
const body = new URLSearchParams();
body.set("code", code);
body.set("client_id", env.GITLAB_CLIENT_ID!);
body.set("client_secret", env.GITLAB_CLIENT_SECRET!);
body.set("redirect_uri", GitLabUtils.callbackUrl());
body.set("grant_type", "authorization_code");
const res = await fetch(GitLabUtils.tokenUrl, {
method: "POST",
headers,
body,
});
if (res.status !== 200) {
throw new Error(
`Error while exchanging oauth code from GitLab; status: ${res.status}`
);
}
return AccessTokenResponseSchema.parse(await res.json());
}
static async revokeAccess(accessToken: string) {
const headers = {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
};
const body = new URLSearchParams();
body.set("client_id", env.GITLAB_CLIENT_ID!);
body.set("client_secret", env.GITLAB_CLIENT_SECRET!);
body.set("token", accessToken);
await fetch(GitLabUtils.revokeUrl, {
method: "POST",
headers,
body,
});
}
static async getInstalledProject(accessToken: string) {
const headers = {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
};
// Get the first project the user has access to
// In a real implementation, we would want to let the user select which project to connect
const res = await fetch(
"https://gitlab.com/api/v4/projects?membership=true&per_page=1",
{
headers,
}
);
if (res.status !== 200) {
throw new Error(
`Error while fetching GitLab projects; status: ${res.status}`
);
}
const projects = await res.json();
if (!projects.length) {
throw new Error("No GitLab projects found");
}
return GitLabProjectSchema.parse(projects[0]);
}
/**
*
* @param url GitLab resource url
* @param actor User attempting to unfurl resource url
* @returns An object containing resource details e.g, a GitLab issue or merge request details
*/
static unfurl: UnfurlSignature = async (url: string, actor: User) => {
const resource = GitLab.parseUrl(url);
if (!resource) {
return;
}
const integration = (await Integration.scope("withAuthentication").findOne({
where: {
service: IntegrationService.GitLab,
teamId: actor.teamId,
"settings.gitlab.project.path_with_namespace": resource.projectPath,
},
})) as Integration<IntegrationType.Embed>;
if (!integration) {
return;
}
try {
const headers = {
Authorization: `Bearer ${integration.authentication.token}`,
Accept: "application/json",
};
let apiUrl: string;
let resourceSchema: z.ZodObject<z.ZodRawShape>;
let resourceType: UnfurlResourceType;
if (resource.type === "issues") {
apiUrl = `https://gitlab.com/api/v4/projects/${encodeURIComponent(resource.projectPath)}/issues/${resource.id}`;
resourceSchema = GitLabIssueSchema;
resourceType = UnfurlResourceType.Issue;
} else if (resource.type === "merge_requests") {
apiUrl = `https://gitlab.com/api/v4/projects/${encodeURIComponent(resource.projectPath)}/merge_requests/${resource.id}`;
resourceSchema = GitLabMergeRequestSchema;
resourceType = UnfurlResourceType.PR;
} else {
return;
}
const res = await fetch(apiUrl, { headers });
if (res.status !== 200) {
return { error: `Resource not found (${res.status})` };
}
const data = resourceSchema.parse(await res.json());
// Fetch labels if they exist
let labels = [];
if (data.labels && data.labels.length > 0) {
labels = data.labels.map((label) => ({
name: label,
color: "#428BCA", // Default GitLab blue
}));
}
return {
type: resourceType,
url,
id: `#${data.iid}`,
title: data.title,
description: data.description,
author: {
name: data.author.name,
avatarUrl: data.author.avatar_url || "",
},
labels,
state: {
name: data.state,
color: data.state === "opened" ? "#1aaa55" : "#db3b21", // Green for open, red for closed
draft:
resourceType === UnfurlResourceType.PR ? data.draft : undefined,
},
createdAt: data.created_at,
} satisfies UnfurlIssueOrPR;
} catch (err) {
Logger.warn("Failed to fetch resource from GitLab", err);
return { error: err.message || "Unknown error" };
}
};
/**
* Parses a given URL and returns resource identifiers for GitLab specific URLs
*
* @param url URL to parse
* @returns {object} Containing resource identifiers - `projectPath`, `type`, and `id`.
*/
private static parseUrl(url: string) {
const { hostname, pathname } = new URL(url);
if (hostname !== "gitlab.com") {
return;
}
const parts = pathname.split("/");
// Remove empty first element
parts.shift();
// GitLab URLs are in the format: /namespace/project/-/issues/1 or /namespace/project/-/merge_requests/1
// The namespace can have multiple levels (e.g., /group/subgroup/project/-/issues/1)
if (parts.length < 4) {
return;
}
// Find the index of "-" which separates project path from resource type
const separatorIndex = parts.indexOf("-");
if (separatorIndex === -1 || separatorIndex === parts.length - 1) {
return;
}
const projectPath = parts.slice(0, separatorIndex).join("/");
const type = parts[separatorIndex + 1];
const id = parts[separatorIndex + 2];
if (
!type ||
!id ||
!GitLab.supportedUnfurls.includes(type as UnfurlResourceType)
) {
return;
}
return { projectPath, type, id };
}
}
@@ -0,0 +1,56 @@
import { IntegrationType } from "@shared/types";
import BaseTask from "@server/queues/tasks/BaseTask";
import { Integration } from "@server/models";
import { FileOperation } from "@server/models";
import fetch from "node-fetch";
type Props = {
integrationId: string;
avatarUrl: string;
};
export default class UploadGitLabProjectAvatarTask extends BaseTask<Props> {
public async perform({ integrationId, avatarUrl }: Props) {
const integration = await Integration.findByPk(integrationId, {
rejectOnEmpty: true,
});
try {
const res = await fetch(avatarUrl);
const buffer = await res.buffer();
const name = avatarUrl.split("/").pop() || "avatar";
const contentType = res.headers.get("content-type") || "image/png";
const operation = await FileOperation.createFromBuffer({
buffer,
contentType,
name,
userId: integration.userId,
teamId: integration.teamId,
source: "gitlab",
});
await integration.update({
settings: {
...integration.settings,
gitlab: {
...(integration.settings as Integration<IntegrationType.Embed>)
.gitlab,
project: {
...(integration.settings as Integration<IntegrationType.Embed>)
.gitlab?.project,
avatar_url: operation.url,
},
},
},
});
} catch (err) {
// If the avatar upload fails, we don't need to fail the entire task
// as it's not critical to the integration's functionality.
// Just log the error and continue.
this.logger.error(
`Failed to upload GitLab project avatar: ${err.message}`
);
}
}
}
+51
View File
@@ -0,0 +1,51 @@
import queryString from "query-string";
import env from "@shared/env";
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
export type OAuthState = {
teamId: string;
};
export class GitLabUtils {
private static oauthScopes = "api read_api read_user read_repository";
public static tokenUrl = "https://gitlab.com/oauth/token";
public static revokeUrl = "https://gitlab.com/oauth/revoke";
private static authBaseUrl = "https://gitlab.com/oauth/authorize";
private static settingsUrl = integrationSettingsPath("gitlab");
static parseState(state: string): OAuthState {
return JSON.parse(state);
}
static successUrl() {
return this.settingsUrl;
}
static errorUrl(error: string) {
return `${this.settingsUrl}?error=${error}`;
}
static callbackUrl(
{ baseUrl, params }: { baseUrl: string; params?: string } = {
baseUrl: env.URL,
params: undefined,
}
) {
return params
? `${baseUrl}/api/gitlab.callback?${params}`
: `${baseUrl}/api/gitlab.callback`;
}
static authUrl({ state }: { state: OAuthState }) {
const params = {
client_id: env.GITLAB_CLIENT_ID,
redirect_uri: this.callbackUrl(),
state: JSON.stringify(state),
scope: this.oauthScopes,
response_type: "code",
};
return `${this.authBaseUrl}?${queryString.stringify(params)}`;
}
}
+9 -1
View File
@@ -55,7 +55,15 @@ export const Notion = observer(() => {
onClose: clearQueryParams,
});
}
}, [t, dialogs, oauthSuccess, service, clearQueryParams]);
}, [
t,
dialogs,
oauthSuccess,
service,
clearQueryParams,
handleSubmit,
integrationId,
]);
React.useEffect(() => {
if (!oauthError) {
@@ -52,7 +52,15 @@ export function ImportDialog({ integrationId, onSubmit }: Props) {
toast.error(err.message);
resetSubmitting();
}
}, [permission, onSubmit]);
}, [
permission,
onSubmit,
integrationId,
t,
imports,
resetSubmitting,
setSubmitting,
]);
return (
<Flex column gap={12}>
+17 -8
View File
@@ -1,10 +1,17 @@
import { HttpsProxyAgent } from "https-proxy-agent";
import OAuth2Strategy, { Strategy } from "passport-oauth2";
import * as OAuth2StrategyModule from "passport-oauth2";
import { Request } from "express";
const { Strategy } = OAuth2StrategyModule;
type OIDCOptions = Record<string, unknown> & {
originalQuery?: Record<string, unknown>;
};
export class OIDCStrategy extends Strategy {
constructor(
options: OAuth2Strategy.StrategyOptionsWithRequest,
verify: OAuth2Strategy.VerifyFunctionWithRequest
options: OAuth2StrategyModule.StrategyOptionsWithRequest,
verify: OAuth2StrategyModule.VerifyFunctionWithRequest
) {
super(options, verify);
@@ -14,14 +21,16 @@ export class OIDCStrategy extends Strategy {
}
}
authenticate(req: any, options: any) {
options.originalQuery = req.query;
super.authenticate(req, options);
authenticate(req: Request, options?: unknown) {
const opts = (options || {}) as OIDCOptions;
opts.originalQuery = req.query as Record<string, unknown>;
super.authenticate(req, opts);
}
authorizationParams(options: any) {
authorizationParams(options: unknown) {
const opts = options as OIDCOptions;
return {
...options.originalQuery,
...(opts.originalQuery ?? {}),
...super.authorizationParams?.(options),
};
}
+2 -2
View File
@@ -6,7 +6,7 @@ import env from "./env";
const SLACK_API_URL = "https://slack.com/api";
export async function post(endpoint: string, body: Record<string, any>) {
export async function post(endpoint: string, body: Record<string, unknown>) {
let data;
const token = body.token;
@@ -30,7 +30,7 @@ export async function post(endpoint: string, body: Record<string, any>) {
return data;
}
export async function request(endpoint: string, body: Record<string, any>) {
export async function request(endpoint: string, body: Record<string, unknown>) {
let data;
try {
+1 -1
View File
@@ -16,7 +16,7 @@ export class SlackUtils {
static createState(
teamId: string,
type: IntegrationType,
data?: Record<string, any>
data?: Record<string, unknown>
) {
return JSON.stringify({ type, teamId, ...data });
}
+2 -2
View File
@@ -14,7 +14,7 @@ export enum TaskSchedule {
Minute = "minute",
}
export default abstract class BaseTask<T extends Record<string, any>> {
export default abstract class BaseTask<T extends Record<string, unknown>> {
/**
* An optional schedule for this task to be run automatically.
*/
@@ -43,7 +43,7 @@ export default abstract class BaseTask<T extends Record<string, any>> {
* @param props Properties to be used by the task
* @returns A promise that resolves once the task has completed.
*/
public abstract perform(props: T): Promise<any>;
public abstract perform(props: T): Promise<unknown>;
/**
* Handle failure when all attempts are exhausted for the task.
+1 -1
View File
@@ -3,7 +3,7 @@ import BaseTask from "./BaseTask";
type Props = {
templateName: string;
props: Record<string, any>;
props: Record<string, unknown>;
};
export default class EmailTask extends BaseTask<Props> {
@@ -825,7 +825,7 @@ describe("#documents.list", () => {
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data).toHaveLength(2);
const docIds = body.data.map((doc: any) => doc.id);
const docIds = body.data.map((doc: { id: string }) => doc.id);
expect(docIds).toContain(docs[0].id);
expect(docIds).toContain(docs[1].id);
expect(docIds).not.toContain(docs[2].id);
@@ -5361,7 +5361,7 @@ describe("#documents.documents", () => {
expect(res.status).toBe(200);
expect(body.data.id).toBe(parent.id);
const childIds = body.data.children.map((node: any) => node.id);
const childIds = body.data.children.map((node: { id: string }) => node.id);
expect(childIds).toContain(child1.id);
expect(childIds).toContain(child2.id);
});
+1 -1
View File
@@ -573,7 +573,7 @@ router.post(
});
let document: Document | null;
let serializedDocument: Record<string, any> | undefined;
let serializedDocument: Record<string, unknown> | undefined;
let isPublic = false;
if (shareId) {
+9 -5
View File
@@ -227,20 +227,22 @@ describe("#groups.list", () => {
},
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.groups.length).toEqual(2);
expect(body.data.groups[0].id).toEqual(anotherGroup.id);
expect(body.data.groups[1].id).toEqual(group.id);
expect(body.data.groupMemberships.length).toEqual(2);
expect(body.data.groupMemberships[0].groupId).toEqual(group.id);
expect(body.data.groupMemberships[1].groupId).toEqual(group.id);
expect(
body.data.groupMemberships.map((u: any) => u.user.id).includes(user.id)
body.data.groupMemberships
.map((u: { user: { id: string }; groupId: string }) => u.user.id)
.includes(user.id)
).toBe(true);
expect(
body.data.groupMemberships
.map((u: any) => u.user.id)
.map((u: { user: { id: string }; groupId: string }) => u.user.id)
.includes(anotherUser.id)
).toBe(true);
expect(body.policies.length).toEqual(2);
@@ -259,11 +261,13 @@ describe("#groups.list", () => {
expect(anotherBody.data.groupMemberships[0].groupId).toEqual(group.id);
expect(anotherBody.data.groupMemberships[1].groupId).toEqual(group.id);
expect(
body.data.groupMemberships.map((u: any) => u.user.id).includes(user.id)
body.data.groupMemberships
.map((u: { user: { id: string }; groupId: string }) => u.user.id)
.includes(user.id)
).toBe(true);
expect(
body.data.groupMemberships
.map((u: any) => u.user.id)
.map((u: { user: { id: string }; groupId: string }) => u.user.id)
.includes(anotherUser.id)
).toBe(true);
});
@@ -153,7 +153,9 @@ describe("#relationships.info", () => {
expect(body.data.relationship).toBeTruthy();
expect(body.data.documents).toHaveLength(2);
// User can read their own document but admin document should also be included
const documentIds = body.data.documents.map((doc: any) => doc.id);
const documentIds = body.data.documents.map(
(doc: { id: string }) => doc.id
);
expect(documentIds).toContain(userDocument.id);
});
@@ -170,7 +172,9 @@ describe("#relationships.info", () => {
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data.documents).toHaveLength(2);
const documentIds = body.data.documents.map((doc: any) => doc.id);
const documentIds = body.data.documents.map(
(doc: { id: string }) => doc.id
);
expect(documentIds).toContain(document.id);
expect(documentIds).toContain(reverseDocument.id);
});
@@ -265,7 +269,7 @@ describe("#relationships.list", () => {
expect(body.data.relationships).toBeTruthy();
// All returned relationships should be backlinks
body.data.relationships.forEach((rel: any) => {
body.data.relationships.forEach((rel: { type: string }) => {
expect(rel.type).toEqual(RelationshipType.Backlink);
});
});
@@ -283,7 +287,7 @@ describe("#relationships.list", () => {
expect(body.data.relationships).toBeTruthy();
// All returned relationships should have the specified documentId
body.data.relationships.forEach((rel: any) => {
body.data.relationships.forEach((rel: { documentId: string }) => {
expect(rel.documentId).toEqual(documents[0].id);
});
});
@@ -301,7 +305,7 @@ describe("#relationships.list", () => {
expect(body.data.relationships).toBeTruthy();
// All returned relationships should have the specified reverseDocumentId
body.data.relationships.forEach((rel: any) => {
body.data.relationships.forEach((rel: { reverseDocumentId: string }) => {
expect(rel.reverseDocumentId).toEqual(documents[1].id);
});
});
@@ -320,10 +324,12 @@ describe("#relationships.list", () => {
expect(body.data.relationships).toBeTruthy();
// All returned relationships should match both filters
body.data.relationships.forEach((rel: any) => {
expect(rel.type).toEqual(RelationshipType.Backlink);
expect(rel.documentId).toEqual(documents[0].id);
});
body.data.relationships.forEach(
(rel: { type: string; documentId: string }) => {
expect(rel.type).toEqual(RelationshipType.Backlink);
expect(rel.documentId).toEqual(documents[0].id);
}
);
});
it("should fail with status 400 bad request when documentId is invalid", async () => {
+1 -1
View File
@@ -38,7 +38,7 @@ describe("#searches.list", () => {
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.data).toHaveLength(3);
const queries = body.data.map((d: any) => d.query);
const queries = body.data.map((d: { query: string }) => d.query);
expect(queries).toContain("query");
expect(queries).toContain("foo");
expect(queries).toContain("bar");
+1 -1
View File
@@ -14,5 +14,5 @@ export class MutexLock {
};
}
private static redlock: any;
private static redlock: unknown;
}
@@ -0,0 +1,31 @@
import React from "react";
import { BaseIconProps } from ".";
export function GitLabIssueStatusIcon(props: BaseIconProps) {
const { state, className, size = 16 } = props;
const isOpen = state.name === "opened";
const color = state.color || (isOpen ? "#1aaa55" : "#db3b21"); // Green for open, red for closed
return (
<svg
viewBox="0 0 16 16"
width={size}
height={size}
fill="none"
className={className}
>
<circle cx="8" cy="8" r="7" stroke={color} strokeWidth="2" fill="none" />
{!isOpen && (
<path
d="M4.5 4.5L11.5 11.5M4.5 11.5L11.5 4.5"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
/>
)}
{"draft" in state && state.draft && (
<rect x="4" y="7" width="8" height="2" rx="1" fill={color} />
)}
</svg>
);
}
@@ -7,6 +7,7 @@ import {
UnfurlResponse,
} from "../../types";
import { GitHubIssueStatusIcon } from "./GitHubIssueStatusIcon";
import { GitLabIssueStatusIcon } from "./GitLabIssueStatusIcon";
import { LinearIssueStatusIcon } from "./LinearIssueStatusIcon";
export type BaseIconProps = {
@@ -33,6 +34,8 @@ function getIcon(props: Props) {
return <GitHubIssueStatusIcon {...props} />;
case IntegrationService.Linear:
return <LinearIssueStatusIcon {...props} />;
case IntegrationService.GitLab:
return <GitLabIssueStatusIcon {...props} />;
}
}
+2 -2
View File
@@ -10,10 +10,10 @@ export type CommandFactory = (attrs?: Record<string, Primitive>) => Command;
export type WidgetProps = { rtl: boolean; readOnly: boolean | undefined };
export default class Extension {
options: any;
options: Record<string, unknown>;
editor: Editor;
constructor(options: Record<string, any> = {}) {
constructor(options: Record<string, unknown> = {}) {
this.options = {
...this.defaultOptions,
...options,
@@ -1182,6 +1182,10 @@
"Enabled by {{integrationCreatedBy}}": "Enabled by {{integrationCreatedBy}}",
"Disconnecting will prevent previewing GitHub links from this organization in documents. Are you sure?": "Disconnecting will prevent previewing GitHub links from this organization in documents. Are you sure?",
"The GitHub integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The GitHub integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
"Whoops, you need to accept the permissions in GitLab to connect {{appName}} to your project. Try again?": "Whoops, you need to accept the permissions in GitLab to connect {{appName}} to your project. Try again?",
"Enable previews of GitLab issues and merge requests in documents by connecting a GitLab project to {appName}.": "Enable previews of GitLab issues and merge requests in documents by connecting a GitLab project to {appName}.",
"Disconnecting will prevent previewing GitLab links from this project in documents. Are you sure?": "Disconnecting will prevent previewing GitLab links from this project in documents. Are you sure?",
"The GitLab integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The GitLab integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
"Google Analytics": "Google Analytics",
"Add a Google Analytics 4 measurement ID to send document views and analytics from the workspace to your own Google Analytics account.": "Add a Google Analytics 4 measurement ID to send document views and analytics from the workspace to your own Google Analytics account.",
"Measurement ID": "Measurement ID",
+13 -1
View File
@@ -126,6 +126,7 @@ export enum IntegrationService {
Umami = "umami",
GitHub = "github",
Linear = "linear",
GitLab = "gitlab",
Notion = "notion",
}
@@ -140,12 +141,15 @@ export const ImportableIntegrationService = {
export type IssueTrackerIntegrationService = Extract<
IntegrationService,
IntegrationService.GitHub | IntegrationService.Linear
| IntegrationService.GitHub
| IntegrationService.Linear
| IntegrationService.GitLab
>;
export const IssueTrackerIntegrationService = {
GitHub: IntegrationService.GitHub,
Linear: IntegrationService.Linear,
GitLab: IntegrationService.GitLab,
} as const;
export type UserCreatableIntegrationService = Extract<
@@ -189,6 +193,14 @@ export type IntegrationSettings<T> = T extends IntegrationType.Embed
linear?: {
workspace: { id: string; name: string; key: string; logoUrl?: string };
};
gitlab?: {
project: {
id: string;
name: string;
path_with_namespace: string;
avatar_url?: string;
};
};
}
: T extends IntegrationType.Analytics
? { measurementId: string; instanceUrl?: string; scriptName?: string }
+2 -2
View File
@@ -14,7 +14,7 @@ const stripEmojis = (value: string) => value.replace(regex, "");
const cleanValue = (value: string) => stripEmojis(deburr(value));
function getSortByField<T extends Record<string, any>>(
function getSortByField<T extends Record<string, unknown>>(
item: T,
keyOrCallback: string | ((item: T) => string)
) {
@@ -25,7 +25,7 @@ function getSortByField<T extends Record<string, any>>(
return cleanValue(field);
}
function naturalSortBy<T extends Record<string, any>>(
function naturalSortBy<T extends Record<string, unknown>>(
items: T[],
key: string | ((item: T) => string),
sortOptions?: NaturalSortOptions
+323 -21
View File
@@ -2102,6 +2102,72 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
"@eslint-community/eslint-utils@^4.2.0":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.12.1":
version "4.12.1"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
"@eslint/config-array@^0.21.0":
version "0.21.0"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.0.tgz#abdbcbd16b124c638081766392a4d6b509f72636"
integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==
dependencies:
"@eslint/object-schema" "^2.1.6"
debug "^4.3.1"
minimatch "^3.1.2"
"@eslint/config-helpers@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.3.1.tgz#d316e47905bd0a1a931fa50e669b9af4104d1617"
integrity sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==
"@eslint/core@^0.15.2":
version "0.15.2"
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.2.tgz#59386327d7862cc3603ebc7c78159d2dcc4a868f"
integrity sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964"
integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^10.0.1"
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@9.33.0":
version "9.33.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.33.0.tgz#475c92fdddab59b8b8cab960e3de2564a44bf368"
integrity sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
"@eslint/plugin-kit@^0.3.5":
version "0.3.5"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz#fd8764f0ee79c8ddab4da65460c641cefee017c5"
integrity sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==
dependencies:
"@eslint/core" "^0.15.2"
levn "^0.4.1"
"@faker-js/faker@^8.4.1":
version "8.4.1"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.4.1.tgz#5d5e8aee8fce48f5e189bf730ebd1f758f491451"
@@ -2262,6 +2328,34 @@
ws "*"
xtend "^4.0.0"
"@humanfs/core@^0.19.1":
version "0.19.1"
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
"@humanfs/node@^0.16.6":
version "0.16.6"
resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e"
integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==
dependencies:
"@humanfs/core" "^0.19.1"
"@humanwhocodes/retry" "^0.3.0"
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/retry@^0.3.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
"@humanwhocodes/retry@^0.4.2":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
"@iconify/types@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57"
@@ -4830,10 +4924,10 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity "sha1-4Xfmme4bjCLSMXTKqnQiZEOJUJ8= sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
"@types/estree@^1.0.0":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8"
integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==
"@types/estree@^1.0.0", "@types/estree@^1.0.6":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/express-serve-static-core@^4.17.18":
version "4.17.24"
@@ -4989,6 +5083,11 @@
"@types/tough-cookie" "*"
parse5 "^7.0.0"
"@types/json-schema@^7.0.15":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -5577,12 +5676,17 @@ acorn-import-attributes@^1.9.5:
resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef"
integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-walk@^8.0.2:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity "sha1-dBIQ8uJCZFRQiFOi9E0KuDt/acE= sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="
acorn@^8.1.0, acorn@^8.14.0, acorn@^8.8.1:
acorn@^8.1.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.1:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -5612,6 +5716,16 @@ aggregate-error@^3.1.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.6.0:
version "8.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
@@ -7453,7 +7567,7 @@ de-indent@^1.0.2:
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="
debug@4, debug@4.3.4, debug@4.4.0, debug@^2.2.0, debug@^2.6.8, debug@^3.1.0, debug@^4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.6, debug@^4.4.0, debug@^4.4.1, debug@~4.3.1, debug@~4.3.2:
debug@4, debug@4.3.4, debug@4.4.0, debug@^2.2.0, debug@^2.6.8, debug@^3.1.0, debug@^4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.4.0, debug@^4.4.1, debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity "sha1-Exn2V5NX8jONMzfSzdSRS7XcyGU= sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="
@@ -7504,7 +7618,7 @@ deep-equal@~1.0.1:
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
integrity "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw=="
deep-is@~0.1.3:
deep-is@^0.1.3, deep-is@~0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE= sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
@@ -8080,6 +8194,11 @@ escape-string-regexp@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity "sha1-owME6Z2qMuI7L9IPUbq9B8/8o0Q= sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
escodegen@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd"
@@ -8092,12 +8211,94 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
eslint-scope@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint-visitor-keys@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@^9.33.0:
version "9.33.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.33.0.tgz#cc186b3d9eb0e914539953d6a178a5b413997b73"
integrity sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.12.1"
"@eslint/config-array" "^0.21.0"
"@eslint/config-helpers" "^0.3.1"
"@eslint/core" "^0.15.2"
"@eslint/eslintrc" "^3.3.1"
"@eslint/js" "9.33.0"
"@eslint/plugin-kit" "^0.3.5"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@humanwhocodes/retry" "^0.4.2"
"@types/estree" "^1.0.6"
"@types/json-schema" "^7.0.15"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.6"
debug "^4.3.2"
escape-string-regexp "^4.0.0"
eslint-scope "^8.4.0"
eslint-visitor-keys "^4.2.1"
espree "^10.4.0"
esquery "^1.5.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^8.0.0"
find-up "^5.0.0"
glob-parent "^6.0.2"
ignore "^5.2.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
json-stable-stringify-without-jsonify "^1.0.1"
lodash.merge "^4.6.2"
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.3"
espree@^10.0.1, espree@^10.4.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
dependencies:
acorn "^8.15.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^4.2.1"
esprima@^4.0.0, esprima@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE= sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
estraverse@^5.2.0:
esquery@^1.5.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
dependencies:
estraverse "^5.1.0"
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
dependencies:
estraverse "^5.2.0"
estraverse@^5.1.0, estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity "sha1-LupSkHAvJquP5TcDcP+GyWXSESM= sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
@@ -8216,12 +8417,12 @@ fast-glob@^3.3.2:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@^2.1.0:
fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM= sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
fast-levenshtein@~2.0.6:
fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
@@ -8276,6 +8477,13 @@ fetch-retry@^5.0.6:
resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-5.0.6.tgz#17d0bc90423405b7a88b74355bf364acd2a7fa56"
integrity "sha1-F9C8kEI0Bbeoi3Q1W/NkrNKn+lY= sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ=="
file-entry-cache@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
dependencies:
flat-cache "^4.0.0"
file-selector@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17"
@@ -8325,6 +8533,14 @@ find-up@^4.0.0, find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
dependencies:
locate-path "^6.0.0"
path-exists "^4.0.0"
find-yarn-workspace-root@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
@@ -8332,6 +8548,19 @@ find-yarn-workspace-root@^2.0.0:
dependencies:
micromatch "^4.0.2"
flat-cache@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
dependencies:
flatted "^3.2.9"
keyv "^4.5.4"
flatted@^3.2.9:
version "3.3.3"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
fn.name@1.x.x:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
@@ -8686,6 +8915,11 @@ glob@^8.1.0:
minimatch "^5.0.1"
once "^1.3.0"
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@^15.14.0:
version "15.15.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8"
@@ -9084,7 +9318,7 @@ ignore-by-default@^1.0.1:
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
integrity "sha1-SMptcvbGo68Aqa1K5odr44ieKwk= sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
ignore@^5.3.0:
ignore@^5.2.0, ignore@^5.3.0:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
@@ -9099,10 +9333,10 @@ immediate@~3.0.5:
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
import-fresh@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity "sha1-NxYsJfy566oublPVtNiM4X2eDCs= sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="
import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
@@ -9406,7 +9640,7 @@ is-generator-function@^1.0.10:
dependencies:
has-tostringtag "^1.0.0"
is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ= sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="
@@ -10164,11 +10398,21 @@ jsesc@^3.0.2, jsesc@~3.0.2:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==
json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity "sha1-fEeAWpQxmSjgV3dAXcEuH3pO4C0= sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-schema-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
@@ -10179,6 +10423,11 @@ json-schema@^0.4.0:
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
integrity "sha1-995M9u+rg4666zI2R0y7paGTCrU= sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json-stable-stringify@^1.0.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70"
@@ -10317,6 +10566,13 @@ keygrip@~1.1.0:
dependencies:
tsscmp "1.0.6"
keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
dependencies:
json-buffer "3.0.1"
khroma@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1"
@@ -10540,6 +10796,14 @@ leven@^3.1.0:
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
integrity "sha1-d4kd6DQGTMy6gq54QrtrFKE+1/I= sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
dependencies:
prelude-ls "^1.2.1"
type-check "~0.4.0"
levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -10748,6 +11012,13 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
dependencies:
p-locate "^5.0.0"
lodash-es@4.17.21, lodash-es@^4.17.15, lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
@@ -11610,6 +11881,18 @@ optionator@^0.8.1:
type-check "~0.3.2"
word-wrap "~1.2.3"
optionator@^0.9.3:
version "0.9.4"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734"
integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
dependencies:
deep-is "^0.1.3"
fast-levenshtein "^2.0.6"
levn "^0.4.1"
prelude-ls "^1.2.1"
type-check "^0.4.0"
word-wrap "^1.2.5"
orderedmap@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
@@ -11664,7 +11947,7 @@ p-limit@^2.0.0, p-limit@^2.2.0:
dependencies:
p-try "^2.0.0"
p-limit@^3.1.0:
p-limit@^3.0.2, p-limit@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
integrity "sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs= sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="
@@ -11685,6 +11968,13 @@ p-locate@^4.1.0:
dependencies:
p-limit "^2.2.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
dependencies:
p-limit "^3.0.2"
p-map@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
@@ -12169,6 +12459,11 @@ pprof-format@^2.1.0:
resolved "https://registry.yarnpkg.com/pprof-format/-/pprof-format-2.1.0.tgz#acc8d7773bcf4faf0a3d3df11bceefba7ac06664"
integrity sha512-0+G5bHH0RNr8E5hoZo/zJYsL92MhkZjwrHp3O2IxmY8RJL9ooKeuZ8Tm0ZNBw5sGZ9TiM71sthTjWoR2Vf5/xw==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -14332,6 +14627,13 @@ turndown@^7.2.0:
dependencies:
"@mixmark-io/domino" "^2.2.0"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
dependencies:
prelude-ls "^1.2.1"
type-check@~0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@@ -15039,10 +15341,10 @@ wkx@^0.5.0:
dependencies:
"@types/node" "*"
word-wrap@~1.2.3:
version "1.2.4"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f"
integrity "sha1-y0tQ7JrKVwq9H1LzPNRbbGFzmp8= sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA=="
word-wrap@^1.2.5, word-wrap@~1.2.3:
version "1.2.5"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
workbox-background-sync@7.3.0:
version "7.3.0"