mirror of
https://github.com/outline/outline.git
synced 2026-06-14 03:45:00 +03:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e92500dd85 | |||
| 1acec2502e | |||
| c42ce87309 | |||
| cab5dcef6a | |||
| f5c659f902 | |||
| 722d10e7de | |||
| ce001547b5 | |||
| 8d05e2b095 | |||
| 19e40cf814 | |||
| 2bb9b50637 | |||
| 4885612661 | |||
| e2dd6221f8 | |||
| 7f513a6950 | |||
| 6440d78b6f | |||
| 7e05fc1017 | |||
| 2bc47cfcef | |||
| e8e46a438c | |||
| 3156f62e94 | |||
| 9274f56ef6 | |||
| 4bb9ac40c7 | |||
| 36772f1444 | |||
| e503225f04 | |||
| 762140e493 | |||
| 21e756c357 | |||
| 2cc5846f1b | |||
| de6c1735d9 | |||
| b7c13f092b | |||
| 298298223b | |||
| 21f37c0d14 | |||
| 18bc93c9c2 |
@@ -127,6 +127,10 @@ GITHUB_APP_NAME=
|
||||
GITHUB_APP_ID=
|
||||
GITHUB_APP_PRIVATE_KEY=
|
||||
|
||||
# Linear
|
||||
LINEAR_CLIENT_ID=
|
||||
LINEAR_CLIENT_SECRET=
|
||||
|
||||
# To configure Discord auth, you'll need to create a Discord Application at
|
||||
# => https://discord.com/developers/applications/
|
||||
#
|
||||
|
||||
@@ -18,6 +18,13 @@ type Props = {
|
||||
children?: React.ReactNode;
|
||||
document: Document;
|
||||
onlyText?: boolean;
|
||||
reverse?: boolean;
|
||||
/**
|
||||
* Maximum number of items to show in the breadcrumb.
|
||||
* If value is less than or equals to 0, no items will be shown.
|
||||
* If value is undefined, all items will be shown.
|
||||
*/
|
||||
maxDepth?: number;
|
||||
};
|
||||
|
||||
function useCategory(document: Document): MenuInternalLink | null {
|
||||
@@ -54,7 +61,7 @@ function useCategory(document: Document): MenuInternalLink | null {
|
||||
}
|
||||
|
||||
function DocumentBreadcrumb(
|
||||
{ document, children, onlyText }: Props,
|
||||
{ document, children, onlyText, reverse = false, maxDepth }: Props,
|
||||
ref: React.RefObject<HTMLDivElement> | null
|
||||
) {
|
||||
const { collections } = useStores();
|
||||
@@ -65,6 +72,7 @@ function DocumentBreadcrumb(
|
||||
? collections.get(document.collectionId)
|
||||
: undefined;
|
||||
const can = usePolicy(collection);
|
||||
const depth = maxDepth === undefined ? undefined : Math.max(0, maxDepth);
|
||||
|
||||
React.useEffect(() => {
|
||||
void document.loadRelations({ withoutPolicies: true });
|
||||
@@ -91,20 +99,23 @@ function DocumentBreadcrumb(
|
||||
};
|
||||
}
|
||||
|
||||
const path = document.pathTo;
|
||||
const path = document.pathTo.slice(0, -1);
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
const output = [];
|
||||
const output: MenuInternalLink[] = [];
|
||||
|
||||
if (depth === 0) {
|
||||
return output;
|
||||
}
|
||||
|
||||
if (category) {
|
||||
output.push(category);
|
||||
}
|
||||
|
||||
if (collectionNode) {
|
||||
output.push(collectionNode);
|
||||
}
|
||||
|
||||
path.slice(0, -1).forEach((node: NavigationNode) => {
|
||||
path.forEach((node: NavigationNode) => {
|
||||
const title = node.title || t("Untitled");
|
||||
output.push({
|
||||
type: "route",
|
||||
@@ -121,21 +132,43 @@ function DocumentBreadcrumb(
|
||||
},
|
||||
});
|
||||
});
|
||||
return output;
|
||||
}, [t, path, category, sidebarContext, collectionNode]);
|
||||
|
||||
return reverse
|
||||
? depth !== undefined
|
||||
? output.slice(-depth)
|
||||
: output
|
||||
: depth !== undefined
|
||||
? output.slice(0, depth)
|
||||
: output;
|
||||
}, [t, path, category, sidebarContext, collectionNode, reverse, depth]);
|
||||
|
||||
if (!collections.isLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (onlyText === true) {
|
||||
if (onlyText) {
|
||||
if (depth === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const slicedPath = reverse
|
||||
? path.slice(depth && -depth)
|
||||
: path.slice(0, depth);
|
||||
|
||||
const showCollection =
|
||||
collection &&
|
||||
(!reverse || depth === undefined || slicedPath.length < depth);
|
||||
|
||||
return (
|
||||
<>
|
||||
{collection?.name}
|
||||
{path.slice(0, -1).map((node: NavigationNode) => (
|
||||
{showCollection && collection.name}
|
||||
{slicedPath.map((node: NavigationNode, index: number) => (
|
||||
<React.Fragment key={node.id}>
|
||||
<SmallSlash />
|
||||
{showCollection && <SmallSlash />}
|
||||
{node.title || t("Untitled")}
|
||||
{!showCollection && index !== slicedPath.length - 1 && (
|
||||
<SmallSlash />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -46,10 +46,10 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<PaginatedList
|
||||
<PaginatedList<User>
|
||||
aria-label={t("Viewers")}
|
||||
items={users}
|
||||
renderItem={(model: User) => {
|
||||
renderItem={(model) => {
|
||||
const view = documentViews.find((v) => v.userId === model.id);
|
||||
const isPresent = presentIds.includes(model.id);
|
||||
const isEditing = editingIds.includes(model.id);
|
||||
|
||||
@@ -56,7 +56,7 @@ const FilterOptions = ({
|
||||
: "";
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
(option: TFilterOption) => (
|
||||
(option) => (
|
||||
<MenuItem
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
@@ -174,7 +174,7 @@ const FilterOptions = ({
|
||||
)}
|
||||
</MenuButton>
|
||||
<ContextMenu aria-label={defaultLabel} minHeight={66} {...menu}>
|
||||
<PaginatedList
|
||||
<PaginatedList<TFilterOption>
|
||||
listRef={listRef}
|
||||
options={{ query, ...fetchQueryOptions }}
|
||||
items={filteredOptions}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { transparentize } from "polished";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled, { css } from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import { getTextColor } from "@shared/utils/color";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
export const CARD_MARGIN = 10;
|
||||
@@ -33,7 +32,7 @@ export const Title = styled(Text).attrs({ as: "h2", size: "large" })`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
export const Info = styled(StyledText).attrs(() => ({
|
||||
@@ -60,15 +59,27 @@ export const Thumbnail = styled.img`
|
||||
export const Label = styled(Text).attrs({ size: "xsmall", weight: "bold" })<{
|
||||
color?: string;
|
||||
}>`
|
||||
background-color: ${(props) =>
|
||||
props.color ?? props.theme.backgroundSecondary};
|
||||
color: ${(props) =>
|
||||
props.color ? getTextColor(props.color) : props.theme.text};
|
||||
border: 1px solid ${(props) => props.theme.divider};
|
||||
width: fit-content;
|
||||
border-radius: 2em;
|
||||
padding: 0 8px;
|
||||
padding: 1px 8px 1px 20px;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: ${(props) =>
|
||||
props.color || props.theme.backgroundSecondary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const CardContent = styled.div`
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import * as React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { Backticks } from "@shared/components/Backticks";
|
||||
import { IssueStatusIcon } from "@shared/components/IssueStatusIcon";
|
||||
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
|
||||
import {
|
||||
IntegrationService,
|
||||
UnfurlResourceType,
|
||||
UnfurlResponse,
|
||||
} from "@shared/types";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import Flex from "~/components/Flex";
|
||||
import Text from "../Text";
|
||||
@@ -23,6 +29,11 @@ const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
const authorName = author.name;
|
||||
const urlObj = new URL(url);
|
||||
const service =
|
||||
urlObj.hostname === "github.com"
|
||||
? IntegrationService.GitHub
|
||||
: IntegrationService.Linear;
|
||||
|
||||
return (
|
||||
<Preview as="a" href={url} target="_blank" rel="noopener noreferrer">
|
||||
@@ -31,13 +42,18 @@ const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
|
||||
<CardContent>
|
||||
<Flex gap={2} column>
|
||||
<Title>
|
||||
<IssueStatusIcon status={state.name} color={state.color} />
|
||||
<StyledIssueStatusIcon
|
||||
service={service}
|
||||
state={state}
|
||||
size={18}
|
||||
/>
|
||||
<span>
|
||||
{title} <Text type="tertiary">{id}</Text>
|
||||
<Backticks content={title} />
|
||||
<Text type="tertiary">{id}</Text>
|
||||
</span>
|
||||
</Title>
|
||||
<Flex align="center" gap={4}>
|
||||
<Avatar src={author.avatarUrl} />
|
||||
<Flex align="center" gap={6}>
|
||||
<Avatar src={author.avatarUrl} size={18} />
|
||||
<Info>
|
||||
<Trans>
|
||||
{{ authorName }} created{" "}
|
||||
@@ -62,4 +78,8 @@ const HoverPreviewIssue = React.forwardRef(function _HoverPreviewIssue(
|
||||
);
|
||||
});
|
||||
|
||||
const StyledIssueStatusIcon = styled(IssueStatusIcon)`
|
||||
margin-top: 2px;
|
||||
`;
|
||||
|
||||
export default HoverPreviewIssue;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { Backticks } from "@shared/components/Backticks";
|
||||
import { PullRequestIcon } from "@shared/components/PullRequestIcon";
|
||||
import { UnfurlResourceType, UnfurlResponse } from "@shared/types";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
@@ -31,13 +33,14 @@ const HoverPreviewPullRequest = React.forwardRef(
|
||||
<CardContent>
|
||||
<Flex gap={2} column>
|
||||
<Title>
|
||||
<PullRequestIcon status={state.name} color={state.color} />
|
||||
<StyledPullRequestIcon size={18} state={state} />
|
||||
<span>
|
||||
{title} <Text type="tertiary">{id}</Text>
|
||||
<Backticks content={title} />
|
||||
<Text type="tertiary">{id}</Text>
|
||||
</span>
|
||||
</Title>
|
||||
<Flex align="center" gap={4}>
|
||||
<Avatar src={author.avatarUrl} />
|
||||
<Flex align="center" gap={6}>
|
||||
<Avatar src={author.avatarUrl} size={18} />
|
||||
<Info>
|
||||
<Trans>
|
||||
{{ authorName }} opened{" "}
|
||||
@@ -55,4 +58,8 @@ const HoverPreviewPullRequest = React.forwardRef(
|
||||
}
|
||||
);
|
||||
|
||||
const StyledPullRequestIcon = styled(PullRequestIcon)`
|
||||
margin-top: 2px;
|
||||
`;
|
||||
|
||||
export default HoverPreviewPullRequest;
|
||||
|
||||
@@ -79,11 +79,11 @@ function Notifications(
|
||||
</Header>
|
||||
<React.Suspense fallback={null}>
|
||||
<Scrollable ref={ref} flex topShadow>
|
||||
<PaginatedList
|
||||
<PaginatedList<Notification>
|
||||
fetch={notifications.fetchPage}
|
||||
options={{ archived: false }}
|
||||
items={isOpen ? notifications.orderedData : undefined}
|
||||
renderItem={(item: Notification) => (
|
||||
renderItem={(item) => (
|
||||
<NotificationListItem
|
||||
key={item.id}
|
||||
notification={item}
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
fetch: (options: any) => Promise<Document[] | undefined>;
|
||||
options?: Record<string, any>;
|
||||
heading?: React.ReactNode;
|
||||
empty?: React.ReactNode;
|
||||
empty?: JSX.Element;
|
||||
showParentDocuments?: boolean;
|
||||
showCollection?: boolean;
|
||||
showPublished?: boolean;
|
||||
@@ -34,7 +34,7 @@ const PaginatedDocumentList = React.memo<Props>(function PaginatedDocumentList({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<PaginatedList
|
||||
<PaginatedList<Document>
|
||||
aria-label={t("Documents")}
|
||||
items={documents}
|
||||
empty={empty}
|
||||
@@ -42,7 +42,7 @@ const PaginatedDocumentList = React.memo<Props>(function PaginatedDocumentList({
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderError={(props) => <Error {...props} />}
|
||||
renderItem={(item: Document, _index) => (
|
||||
renderItem={(item, _index) => (
|
||||
<DocumentListItem
|
||||
key={item.id}
|
||||
document={item}
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
fetch: (options: Record<string, any> | undefined) => Promise<Event[]>;
|
||||
options?: Record<string, any>;
|
||||
heading?: React.ReactNode;
|
||||
empty?: React.ReactNode;
|
||||
empty?: JSX.Element;
|
||||
};
|
||||
|
||||
const PaginatedEventList = React.memo<Props>(function PaginatedEventList({
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import "../stores";
|
||||
import { render } from "@testing-library/react";
|
||||
import { TFunction } from "i18next";
|
||||
import { Provider } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { getI18n } from "react-i18next";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import { Component as PaginatedList } from "./PaginatedList";
|
||||
import PaginatedList from "./PaginatedList";
|
||||
|
||||
describe("PaginatedList", () => {
|
||||
const i18n = getI18n();
|
||||
const authStore = {};
|
||||
|
||||
const props = {
|
||||
i18n,
|
||||
@@ -17,19 +19,23 @@ describe("PaginatedList", () => {
|
||||
|
||||
it("with no items renders nothing", () => {
|
||||
const result = render(
|
||||
<PaginatedList items={[]} renderItem={render} {...props} />
|
||||
<Provider auth={authStore}>
|
||||
<PaginatedList items={[]} renderItem={render} {...props} />
|
||||
</Provider>
|
||||
);
|
||||
expect(result.container.innerHTML).toEqual("");
|
||||
});
|
||||
|
||||
it("with no items renders empty prop", async () => {
|
||||
const result = render(
|
||||
<PaginatedList
|
||||
items={[]}
|
||||
empty={<p>Sorry, no results</p>}
|
||||
renderItem={render}
|
||||
{...props}
|
||||
/>
|
||||
<Provider auth={authStore}>
|
||||
<PaginatedList
|
||||
items={[]}
|
||||
empty={<p>Sorry, no results</p>}
|
||||
renderItem={render}
|
||||
{...props}
|
||||
/>{" "}
|
||||
</Provider>
|
||||
);
|
||||
await expect(
|
||||
result.findAllByText("Sorry, no results")
|
||||
@@ -42,13 +48,15 @@ describe("PaginatedList", () => {
|
||||
id: "one",
|
||||
};
|
||||
render(
|
||||
<PaginatedList
|
||||
items={[]}
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderItem={render}
|
||||
{...props}
|
||||
/>
|
||||
<Provider auth={authStore}>
|
||||
<PaginatedList
|
||||
items={[]}
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderItem={render}
|
||||
{...props}
|
||||
/>{" "}
|
||||
</Provider>
|
||||
);
|
||||
expect(fetch).toHaveBeenCalledWith({
|
||||
...options,
|
||||
|
||||
+244
-194
@@ -1,265 +1,315 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observable, action, computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, WithTranslation } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
|
||||
import DelayedMount from "~/components/DelayedMount";
|
||||
import PlaceholderList from "~/components/List/Placeholder";
|
||||
import withStores from "~/components/withStores";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import { dateToHeading } from "~/utils/date";
|
||||
|
||||
/**
|
||||
* Base interface for items that can be paginated
|
||||
* @interface PaginatedItem
|
||||
*/
|
||||
export interface PaginatedItem {
|
||||
/** Unique identifier for the item */
|
||||
id?: string;
|
||||
/** Last update timestamp of the item */
|
||||
updatedAt?: string;
|
||||
/** Creation timestamp of the item */
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
type Props<T> = WithTranslation &
|
||||
RootStore &
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
fetch?: (
|
||||
options: Record<string, any> | undefined
|
||||
) => Promise<T[] | undefined> | undefined;
|
||||
options?: Record<string, any>;
|
||||
heading?: React.ReactNode;
|
||||
empty?: React.ReactNode;
|
||||
loading?: React.ReactElement;
|
||||
items?: T[];
|
||||
className?: string;
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
renderError?: (options: {
|
||||
error: Error;
|
||||
retry: () => void;
|
||||
}) => React.ReactNode;
|
||||
renderHeading?: (name: React.ReactElement<any> | string) => React.ReactNode;
|
||||
onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
listRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
/**
|
||||
* Props for the PaginatedList component
|
||||
* @template T Type of items in the list, must extend PaginatedItem
|
||||
*/
|
||||
interface Props<T extends PaginatedItem>
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Function to fetch paginated data. Should return a promise resolving to an array of items
|
||||
* @param options Pagination and other query options
|
||||
*/
|
||||
fetch?: (
|
||||
options: Record<string, any> | undefined
|
||||
) => Promise<unknown[] | undefined> | undefined;
|
||||
|
||||
@observer
|
||||
class PaginatedList<T extends PaginatedItem> extends React.PureComponent<
|
||||
Props<T>
|
||||
> {
|
||||
@observable
|
||||
error?: Error;
|
||||
/** Additional options to pass to the fetch function */
|
||||
options?: Record<string, any>;
|
||||
|
||||
@observable
|
||||
isFetchingMore = false;
|
||||
/** Optional header content to display above the list */
|
||||
heading?: React.ReactNode;
|
||||
|
||||
@observable
|
||||
isFetching = false;
|
||||
/** Content to display when the list is empty */
|
||||
empty?: JSX.Element | null;
|
||||
|
||||
@observable
|
||||
isFetchingInitial = !this.props.items?.length;
|
||||
/** Optional loading state content */
|
||||
loading?: JSX.Element | null;
|
||||
|
||||
@observable
|
||||
fetchCounter = 0;
|
||||
/** Array of items to display in the list */
|
||||
items?: T[];
|
||||
|
||||
@observable
|
||||
renderCount = Pagination.defaultLimit;
|
||||
/** CSS class name to apply to the list container */
|
||||
className?: string;
|
||||
|
||||
@observable
|
||||
offset = 0;
|
||||
/**
|
||||
* Function to render each individual item in the list
|
||||
* @param item The item to render
|
||||
* @param index The index of the item in the list
|
||||
*/
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
|
||||
@observable
|
||||
allowLoadMore = true;
|
||||
/**
|
||||
* Function to render error state
|
||||
* @param options Object containing error details and retry function
|
||||
*/
|
||||
renderError?: (options: {
|
||||
/** Details of the error */
|
||||
error: Error;
|
||||
/** Function to retry the fetch operation */
|
||||
retry: () => void;
|
||||
}) => JSX.Element;
|
||||
|
||||
componentDidMount() {
|
||||
void this.fetchResults();
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
componentDidUpdate(prevProps: Props<T>) {
|
||||
if (
|
||||
prevProps.fetch !== this.props.fetch ||
|
||||
!isEqual(prevProps.options, this.props.options)
|
||||
) {
|
||||
this.reset();
|
||||
void this.fetchResults();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handler for escape key press
|
||||
* @param ev Keyboard event object
|
||||
*/
|
||||
onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
|
||||
reset = () => {
|
||||
this.offset = 0;
|
||||
this.allowLoadMore = true;
|
||||
this.renderCount = Pagination.defaultLimit;
|
||||
this.isFetching = false;
|
||||
this.isFetchingInitial = false;
|
||||
this.isFetchingMore = false;
|
||||
};
|
||||
/** Reference to the list container element */
|
||||
listRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
@action
|
||||
fetchResults = async () => {
|
||||
if (!this.props.fetch) {
|
||||
/**
|
||||
* A reusable component that renders a paginated list with infinite scrolling
|
||||
* and optional date-based section headings.
|
||||
*
|
||||
* @template T Type of the list items, must extend PaginatedItem
|
||||
*/
|
||||
const PaginatedList = <T extends PaginatedItem>({
|
||||
fetch,
|
||||
options,
|
||||
heading,
|
||||
empty = null,
|
||||
loading = null,
|
||||
items = [],
|
||||
className,
|
||||
renderItem,
|
||||
renderError,
|
||||
renderHeading,
|
||||
onEscape,
|
||||
listRef,
|
||||
...rest
|
||||
}: Props<T>): JSX.Element | null => {
|
||||
const user = useCurrentUser({ rejectOnEmpty: false });
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [error, setError] = React.useState<Error | undefined>();
|
||||
const [isFetchingMore, setIsFetchingMore] = React.useState(false);
|
||||
const [isFetching, setIsFetching] = React.useState(false);
|
||||
const [isFetchingInitial, setIsFetchingInitial] = React.useState(
|
||||
!items?.length
|
||||
);
|
||||
const [fetchCounter, setFetchCounter] = React.useState(0);
|
||||
const [renderCount, setRenderCount] = React.useState(Pagination.defaultLimit);
|
||||
const [offset, setOffset] = React.useState(0);
|
||||
const [allowLoadMore, setAllowLoadMore] = React.useState(true);
|
||||
|
||||
const reset = React.useCallback(() => {
|
||||
setOffset(0);
|
||||
setAllowLoadMore(true);
|
||||
setRenderCount(Pagination.defaultLimit);
|
||||
setIsFetching(false);
|
||||
setIsFetchingInitial(false);
|
||||
setIsFetchingMore(false);
|
||||
}, []);
|
||||
|
||||
const fetchResults = React.useCallback(async () => {
|
||||
if (!fetch) {
|
||||
return;
|
||||
}
|
||||
this.isFetching = true;
|
||||
const counter = ++this.fetchCounter;
|
||||
const limit = this.props.options?.limit ?? Pagination.defaultLimit;
|
||||
this.error = undefined;
|
||||
|
||||
setIsFetching(true);
|
||||
const counter = fetchCounter + 1;
|
||||
setFetchCounter(counter);
|
||||
const limit = options?.limit ?? Pagination.defaultLimit;
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
const results = await this.props.fetch({
|
||||
const results = await fetch({
|
||||
limit,
|
||||
offset: this.offset,
|
||||
...this.props.options,
|
||||
offset,
|
||||
...options,
|
||||
});
|
||||
|
||||
if (this.offset !== 0) {
|
||||
this.renderCount += limit;
|
||||
if (offset !== 0) {
|
||||
setRenderCount((prevCount) => prevCount + limit);
|
||||
}
|
||||
|
||||
if (results && (results.length === 0 || results.length < limit)) {
|
||||
this.allowLoadMore = false;
|
||||
setAllowLoadMore(false);
|
||||
} else {
|
||||
this.offset += limit;
|
||||
setOffset((prevOffset) => prevOffset + limit);
|
||||
}
|
||||
|
||||
this.isFetchingInitial = false;
|
||||
setIsFetchingInitial(false);
|
||||
} catch (err) {
|
||||
this.error = err;
|
||||
setError(err);
|
||||
} finally {
|
||||
// only the most recent fetch should end the loading state
|
||||
if (counter >= this.fetchCounter) {
|
||||
this.isFetching = false;
|
||||
this.isFetchingMore = false;
|
||||
if (counter >= fetchCounter) {
|
||||
setIsFetching(false);
|
||||
setIsFetchingMore(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [fetch, fetchCounter, offset, options]);
|
||||
|
||||
@action
|
||||
loadMoreResults = async () => {
|
||||
// Don't paginate if there aren't more results or we’re currently fetching
|
||||
if (!this.allowLoadMore || this.isFetching) {
|
||||
const loadMoreResults = React.useCallback(async () => {
|
||||
// Don't paginate if there aren't more results or we're currently fetching
|
||||
if (!allowLoadMore || isFetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are already cached results that we haven't yet rendered because
|
||||
// of lazy rendering then show another page.
|
||||
const leftToRender = (this.props.items?.length ?? 0) - this.renderCount;
|
||||
const leftToRender = (items?.length ?? 0) - renderCount;
|
||||
|
||||
if (leftToRender > 0) {
|
||||
this.renderCount += Pagination.defaultLimit;
|
||||
setRenderCount((prevCount) => prevCount + Pagination.defaultLimit);
|
||||
}
|
||||
|
||||
// If there are less than a pages results in the cache go ahead and fetch
|
||||
// another page from the server
|
||||
if (leftToRender <= Pagination.defaultLimit) {
|
||||
this.isFetchingMore = true;
|
||||
await this.fetchResults();
|
||||
setIsFetchingMore(true);
|
||||
await fetchResults();
|
||||
}
|
||||
};
|
||||
}, [allowLoadMore, isFetching, items?.length, renderCount, fetchResults]);
|
||||
|
||||
@computed
|
||||
get itemsToRender() {
|
||||
return this.props.items?.slice(0, this.renderCount) ?? [];
|
||||
}
|
||||
const prevFetch = usePrevious(fetch);
|
||||
const prevOptions = usePrevious(options);
|
||||
|
||||
render() {
|
||||
const {
|
||||
items = [],
|
||||
heading,
|
||||
auth,
|
||||
empty = null,
|
||||
renderHeading,
|
||||
renderError,
|
||||
onEscape,
|
||||
} = this.props;
|
||||
// Initial fetch on mount
|
||||
React.useEffect(() => {
|
||||
if (fetch) {
|
||||
void fetchResults();
|
||||
}
|
||||
}, [fetch]);
|
||||
|
||||
const showLoading =
|
||||
this.isFetching &&
|
||||
!this.isFetchingMore &&
|
||||
(!items?.length || (this.fetchCounter <= 1 && this.isFetchingInitial));
|
||||
|
||||
if (showLoading) {
|
||||
return (
|
||||
this.props.loading || (
|
||||
<DelayedMount>
|
||||
<div className={this.props.className}>
|
||||
<PlaceholderList count={5} />
|
||||
</div>
|
||||
</DelayedMount>
|
||||
)
|
||||
);
|
||||
// Handle updates to fetch or options
|
||||
React.useEffect(() => {
|
||||
if (!prevFetch || !prevOptions) {
|
||||
return; // Skip on initial mount since it's handled by the above effect
|
||||
}
|
||||
|
||||
if (items?.length === 0) {
|
||||
if (this.error && renderError) {
|
||||
return renderError({ error: this.error, retry: this.fetchResults });
|
||||
}
|
||||
|
||||
return empty;
|
||||
if (prevFetch !== fetch || !isEqual(prevOptions, options)) {
|
||||
reset();
|
||||
void fetchResults();
|
||||
}
|
||||
}, [fetch, options, reset, fetchResults, prevFetch, prevOptions]);
|
||||
|
||||
// Computed property equivalent
|
||||
const itemsToRender = React.useMemo(
|
||||
() => items?.slice(0, renderCount) ?? [],
|
||||
[items, renderCount]
|
||||
);
|
||||
|
||||
const showLoading =
|
||||
isFetching &&
|
||||
!isFetchingMore &&
|
||||
(!items?.length || (fetchCounter <= 1 && isFetchingInitial));
|
||||
|
||||
if (showLoading) {
|
||||
return (
|
||||
<>
|
||||
{heading}
|
||||
<ArrowKeyNavigation
|
||||
aria-label={this.props["aria-label"]}
|
||||
onEscape={onEscape}
|
||||
className={this.props.className}
|
||||
items={this.itemsToRender}
|
||||
ref={this.props.listRef}
|
||||
>
|
||||
{() => {
|
||||
let previousHeading = "";
|
||||
return this.itemsToRender.map((item, index) => {
|
||||
const children = this.props.renderItem(item, index);
|
||||
|
||||
// If there is no renderHeading method passed then no date
|
||||
// headings are rendered
|
||||
if (!renderHeading) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// Our models have standard date fields, updatedAt > createdAt.
|
||||
// Get what a heading would look like for this item
|
||||
const currentDate =
|
||||
"updatedAt" in item && item.updatedAt
|
||||
? item.updatedAt
|
||||
: "createdAt" in item && item.createdAt
|
||||
? item.createdAt
|
||||
: previousHeading;
|
||||
const currentHeading = dateToHeading(
|
||||
currentDate,
|
||||
this.props.t,
|
||||
auth.user?.language
|
||||
);
|
||||
|
||||
// If the heading is different to any previous heading then we
|
||||
// should render it, otherwise the item can go under the previous
|
||||
// heading
|
||||
if (
|
||||
children &&
|
||||
(!previousHeading || currentHeading !== previousHeading)
|
||||
) {
|
||||
previousHeading = currentHeading;
|
||||
return (
|
||||
<React.Fragment
|
||||
key={"id" in item && item.id ? item.id : index}
|
||||
>
|
||||
{renderHeading(currentHeading)}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
}}
|
||||
</ArrowKeyNavigation>
|
||||
{this.allowLoadMore && (
|
||||
<div style={{ height: "1px" }}>
|
||||
<Waypoint key={this.renderCount} onEnter={this.loadMoreResults} />
|
||||
loading || (
|
||||
<DelayedMount>
|
||||
<div className={className}>
|
||||
<PlaceholderList count={5} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</DelayedMount>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Component = PaginatedList;
|
||||
if (items?.length === 0) {
|
||||
if (error && renderError) {
|
||||
return renderError({ error, retry: fetchResults });
|
||||
}
|
||||
|
||||
export default withTranslation()(withStores(PaginatedList));
|
||||
return empty;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{heading}
|
||||
<ArrowKeyNavigation
|
||||
aria-label={rest["aria-label"]}
|
||||
onEscape={onEscape}
|
||||
className={className}
|
||||
items={itemsToRender}
|
||||
ref={listRef}
|
||||
>
|
||||
{() => {
|
||||
let previousHeading = "";
|
||||
return itemsToRender.map((item, index) => {
|
||||
const children = renderItem(item, index);
|
||||
|
||||
// If there is no renderHeading method passed then no date
|
||||
// headings are rendered
|
||||
if (!renderHeading) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// Our models have standard date fields, updatedAt > createdAt.
|
||||
// Get what a heading would look like for this item
|
||||
const currentDate =
|
||||
"updatedAt" in item && item.updatedAt
|
||||
? item.updatedAt
|
||||
: "createdAt" in item && item.createdAt
|
||||
? item.createdAt
|
||||
: previousHeading;
|
||||
const currentHeading = dateToHeading(
|
||||
currentDate,
|
||||
t,
|
||||
user?.language
|
||||
);
|
||||
|
||||
// If the heading is different to any previous heading then we
|
||||
// should render it, otherwise the item can go under the previous
|
||||
// heading
|
||||
if (
|
||||
children &&
|
||||
(!previousHeading || currentHeading !== previousHeading)
|
||||
) {
|
||||
previousHeading = currentHeading;
|
||||
return (
|
||||
<React.Fragment key={"id" in item && item.id ? item.id : index}>
|
||||
{renderHeading(currentHeading)}
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
});
|
||||
}}
|
||||
</ArrowKeyNavigation>
|
||||
{allowLoadMore && (
|
||||
<div style={{ height: "1px" }}>
|
||||
<Waypoint key={renderCount} onEnter={loadMoreResults} />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginatedList;
|
||||
|
||||
@@ -200,7 +200,7 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
style={{ zIndex: depths.sidebar + 1 }}
|
||||
shrink
|
||||
>
|
||||
<PaginatedList
|
||||
<PaginatedList<SearchResult>
|
||||
options={{ query, snippetMinWords: 10, snippetMaxWords: 11 }}
|
||||
items={cachedSearchResults}
|
||||
fetch={performSearch}
|
||||
@@ -209,7 +209,7 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
<NoResults>{t("No results for {{query}}", { query })}</NoResults>
|
||||
}
|
||||
loading={<PlaceholderList count={3} header={{ height: 20 }} />}
|
||||
renderItem={(item: SearchResult, index) => (
|
||||
renderItem={(item, index) => (
|
||||
<SearchListItem
|
||||
key={item.document.id}
|
||||
shareId={shareId}
|
||||
|
||||
@@ -82,12 +82,12 @@ function ArchiveLink() {
|
||||
</div>
|
||||
{expanded === true ? (
|
||||
<Relative>
|
||||
<PaginatedList
|
||||
<PaginatedList<Collection>
|
||||
aria-label={t("Archived collections")}
|
||||
items={collections.archived}
|
||||
loading={<PlaceholderCollections />}
|
||||
renderError={(props) => <StyledError {...props} />}
|
||||
renderItem={(item: Collection) => (
|
||||
renderItem={(item) => (
|
||||
<ArchivedCollectionLink
|
||||
key={item.id}
|
||||
depth={1}
|
||||
|
||||
@@ -54,7 +54,7 @@ function Collections() {
|
||||
<Flex column>
|
||||
<Header id="collections" title={t("Collections")}>
|
||||
<Relative>
|
||||
<PaginatedList
|
||||
<PaginatedList<Collection>
|
||||
options={params}
|
||||
aria-label={t("Collections")}
|
||||
items={collections.allActive}
|
||||
@@ -69,7 +69,7 @@ function Collections() {
|
||||
) : undefined
|
||||
}
|
||||
renderError={(props) => <StyledError {...props} />}
|
||||
renderItem={(item: Collection, index) => (
|
||||
renderItem={(item, index) => (
|
||||
<DraggableCollectionLink
|
||||
key={item.id}
|
||||
collection={item}
|
||||
|
||||
@@ -10,6 +10,7 @@ import styled from "styled-components";
|
||||
import Icon from "@shared/components/Icon";
|
||||
import { hideScrollbars, s } from "@shared/styles";
|
||||
import { isInternalUrl, sanitizeUrl } from "@shared/utils/urls";
|
||||
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
|
||||
import Flex from "~/components/Flex";
|
||||
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
@@ -253,7 +254,14 @@ const LinkEditor: React.FC<Props> = ({
|
||||
onPointerMove={() => setSelectedIndex(index)}
|
||||
selected={index === selectedIndex}
|
||||
key={doc.id}
|
||||
subtitle={doc.collection?.name}
|
||||
subtitle={
|
||||
<DocumentBreadcrumb
|
||||
document={doc}
|
||||
onlyText
|
||||
reverse
|
||||
maxDepth={2}
|
||||
/>
|
||||
}
|
||||
title={doc.title}
|
||||
icon={
|
||||
doc.icon ? (
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MenuItem } from "@shared/editor/types";
|
||||
import { MentionType } from "@shared/types";
|
||||
import parseDocumentSlug from "@shared/utils/parseDocumentSlug";
|
||||
import { Avatar, AvatarSize } from "~/components/Avatar";
|
||||
import DocumentBreadcrumb from "~/components/DocumentBreadcrumb";
|
||||
import Flex from "~/components/Flex";
|
||||
import {
|
||||
DocumentsSection,
|
||||
@@ -57,7 +58,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
|
||||
res.data.documents.map(documents.add);
|
||||
res.data.users.map(users.add);
|
||||
res.data.collections.map(collections.add);
|
||||
}, [search, documents, users])
|
||||
}, [search, documents, users, collections])
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -68,7 +69,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (actorId && !loading) {
|
||||
const items = users
|
||||
const items: MentionItem[] = users
|
||||
.findByQuery(search, { maxResults: maxResultsInSection })
|
||||
.map(
|
||||
(user) =>
|
||||
@@ -112,7 +113,14 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
|
||||
<DocumentIcon />
|
||||
),
|
||||
title: doc.title,
|
||||
subtitle: doc.collection?.name,
|
||||
subtitle: (
|
||||
<DocumentBreadcrumb
|
||||
document={doc}
|
||||
onlyText
|
||||
reverse
|
||||
maxDepth={2}
|
||||
/>
|
||||
),
|
||||
section: DocumentsSection,
|
||||
appendSpace: true,
|
||||
attrs: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { v4 } from "uuid";
|
||||
import { EmbedDescriptor } from "@shared/editor/embeds";
|
||||
import { MenuItem } from "@shared/editor/types";
|
||||
import { MentionType } from "@shared/types";
|
||||
import { isUrl } from "@shared/utils/urls";
|
||||
import Integration from "~/models/Integration";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
@@ -29,9 +30,9 @@ export const PasteMenu = observer(({ pastedText, embeds, ...props }: Props) => {
|
||||
const user = useCurrentUser({ rejectOnEmpty: false });
|
||||
|
||||
let mentionType: MentionType | undefined;
|
||||
const url = pastedText ? new URL(pastedText) : undefined;
|
||||
|
||||
if (url) {
|
||||
if (pastedText && isUrl(pastedText)) {
|
||||
const url = new URL(pastedText);
|
||||
const integration = integrations.find((intg: Integration) =>
|
||||
isURLMentionable({ url, integration: intg })
|
||||
);
|
||||
|
||||
@@ -144,10 +144,10 @@ function Insights() {
|
||||
small
|
||||
/>
|
||||
)}
|
||||
<PaginatedList
|
||||
<PaginatedList<User>
|
||||
aria-label={t("Contributors")}
|
||||
items={document.collaborators}
|
||||
renderItem={(model: User) => (
|
||||
renderItem={(model) => (
|
||||
<ListItem
|
||||
key={model.id}
|
||||
title={model.name}
|
||||
|
||||
@@ -58,11 +58,11 @@ function ApiKeys() {
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<PaginatedList
|
||||
<PaginatedList<ApiKey>
|
||||
fetch={apiKeys.fetchPage}
|
||||
items={apiKeys.orderedData}
|
||||
heading={<h2>{t("All")}</h2>}
|
||||
renderItem={(apiKey: ApiKey) => (
|
||||
renderItem={(apiKey) => (
|
||||
<ApiKeyListItem key={apiKey.id} apiKey={apiKey} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -48,7 +48,7 @@ function Export() {
|
||||
{t("Export data")}…
|
||||
</Button>
|
||||
<br />
|
||||
<PaginatedList
|
||||
<PaginatedList<FileOperation>
|
||||
items={fileOperations.exports}
|
||||
fetch={fileOperations.fetchPage}
|
||||
options={{
|
||||
@@ -59,7 +59,7 @@ function Export() {
|
||||
<Trans>Recent exports</Trans>
|
||||
</h2>
|
||||
}
|
||||
renderItem={(item: FileOperation) => (
|
||||
renderItem={(item) => (
|
||||
<FileOperationListItem key={item.id} fileOperation={item} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -183,7 +183,7 @@ function Import() {
|
||||
))}
|
||||
</div>
|
||||
<br />
|
||||
<PaginatedList
|
||||
<PaginatedList<ImportModel | FileOperation>
|
||||
items={allImports}
|
||||
fetch={fetchImports}
|
||||
heading={
|
||||
@@ -191,7 +191,7 @@ function Import() {
|
||||
<Trans>Recent imports</Trans>
|
||||
</h2>
|
||||
}
|
||||
renderItem={(item: ImportModel | FileOperation) =>
|
||||
renderItem={(item) =>
|
||||
item instanceof ImportModel ? (
|
||||
<ImportListItem key={item.id} importModel={item} />
|
||||
) : (
|
||||
|
||||
@@ -61,12 +61,12 @@ function PersonalApiKeys() {
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<PaginatedList
|
||||
<PaginatedList<ApiKey>
|
||||
fetch={apiKeys.fetchPage}
|
||||
items={apiKeys.personalApiKeys}
|
||||
options={{ userId: user.id }}
|
||||
heading={<h2>{t("Personal keys")}</h2>}
|
||||
renderItem={(apiKey: ApiKey) => (
|
||||
renderItem={(apiKey) => (
|
||||
<ApiKeyListItem key={apiKey.id} apiKey={apiKey} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -268,14 +268,14 @@ export const ViewGroupMembersDialog = observer(function ({
|
||||
<Subheading>
|
||||
<Trans>Members</Trans>
|
||||
</Subheading>
|
||||
<PaginatedList
|
||||
<PaginatedList<User>
|
||||
items={users.inGroup(group.id)}
|
||||
fetch={groupUsers.fetchPage}
|
||||
options={{
|
||||
id: group.id,
|
||||
}}
|
||||
empty={<Empty>{t("This group has no members.")}</Empty>}
|
||||
renderItem={(user: User) => (
|
||||
renderItem={(user) => (
|
||||
<GroupMemberListItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
@@ -382,7 +382,7 @@ const AddPeopleToGroupDialog = observer(function ({
|
||||
<PlaceholderList count={5} />
|
||||
</DelayedMount>
|
||||
) : (
|
||||
<PaginatedList
|
||||
<PaginatedList<User>
|
||||
empty={
|
||||
query ? (
|
||||
<Empty>{t("No people matching your search")}</Empty>
|
||||
@@ -392,7 +392,7 @@ const AddPeopleToGroupDialog = observer(function ({
|
||||
}
|
||||
items={users.notInGroup(group.id, query)}
|
||||
fetch={query ? undefined : users.fetchPage}
|
||||
renderItem={(item: User) => (
|
||||
renderItem={(item) => (
|
||||
<GroupMemberListItem
|
||||
key={item.id}
|
||||
user={item}
|
||||
|
||||
@@ -21,6 +21,13 @@ class IntegrationsStore extends Store<Integration> {
|
||||
(integration) => integration.service === IntegrationService.GitHub
|
||||
);
|
||||
}
|
||||
|
||||
@computed
|
||||
get linear(): Integration<IntegrationType.Embed>[] {
|
||||
return this.orderedData.filter(
|
||||
(integration) => integration.service === IntegrationService.Linear
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IntegrationsStore;
|
||||
|
||||
@@ -27,6 +27,16 @@ export const isURLMentionable = ({
|
||||
);
|
||||
}
|
||||
|
||||
case IntegrationService.Linear: {
|
||||
const settings =
|
||||
integration.settings as IntegrationSettings<IntegrationType.Embed>;
|
||||
|
||||
return (
|
||||
hostname === "linear.app" &&
|
||||
settings.linear?.workspace.key === pathParts[1] // ensure installed workspace key matches with the provided url.
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -52,6 +62,11 @@ export const determineMentionType = ({
|
||||
: undefined;
|
||||
}
|
||||
|
||||
case IntegrationService.Linear: {
|
||||
const type = pathParts[2];
|
||||
return type === "issue" ? MentionType.Issue : undefined;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
+6
-6
@@ -79,6 +79,7 @@
|
||||
"@hocuspocus/server": "1.1.2",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.49",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@linear/sdk": "^39.0.0",
|
||||
"@notionhq/client": "^2.3.0",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
"@outlinewiki/koa-passport": "^4.2.1",
|
||||
@@ -173,7 +174,7 @@
|
||||
"passport-oauth2": "^1.8.0",
|
||||
"passport-slack-oauth2": "^1.2.0",
|
||||
"patch-package": "^7.0.2",
|
||||
"pg": "^8.14.1",
|
||||
"pg": "^8.15.6",
|
||||
"pg-tsquery": "^8.4.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
@@ -208,7 +209,7 @@
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-medium-image-zoom": "5.2.13",
|
||||
"react-merge-refs": "^2.1.1",
|
||||
"react-portal": "^4.2.2",
|
||||
"react-portal": "^4.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-waypoint": "^10.3.0",
|
||||
@@ -248,7 +249,7 @@
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "13.12.0",
|
||||
"vaul": "^1.1.2",
|
||||
"vite": "^5.4.18",
|
||||
"vite": "^6.3.3",
|
||||
"vite-plugin-pwa": "^0.21.2",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^7.5.10",
|
||||
@@ -355,12 +356,12 @@
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"lint-staged": "^13.3.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"nodemon": "^3.1.10",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"react-refresh": "^0.14.2",
|
||||
"rimraf": "^2.5.4",
|
||||
"rollup-plugin-webpack-stats": "^2.0.3",
|
||||
"rollup-plugin-webpack-stats": "^2.0.5",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-static-copy": "^0.17.0",
|
||||
@@ -374,7 +375,6 @@
|
||||
"node-fetch": "^2.7.0",
|
||||
"js-yaml": "^3.14.1",
|
||||
"qs": "6.9.7",
|
||||
"rollup": "^4.5.1",
|
||||
"prismjs": "1.30.0"
|
||||
},
|
||||
"version": "0.83.0",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import Router from "koa-router";
|
||||
import find from "lodash/find";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import { parseDomain } from "@shared/utils/domains";
|
||||
import { createContext } from "@server/context";
|
||||
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, Team } from "@server/models";
|
||||
import { IntegrationAuthentication, Integration } from "@server/models";
|
||||
import { APIContext } from "@server/types";
|
||||
import { GitHubUtils } from "../../shared/GitHubUtils";
|
||||
import { GitHub } from "../github";
|
||||
@@ -17,10 +16,17 @@ const router = new Router();
|
||||
|
||||
router.get(
|
||||
"github.callback",
|
||||
auth({
|
||||
optional: true,
|
||||
}),
|
||||
auth({ optional: true }),
|
||||
validate(T.GitHubCallbackSchema),
|
||||
apexAuthRedirect<T.GitHubCallbackReq>({
|
||||
getTeamId: (ctx) => ctx.input.query.state,
|
||||
getRedirectPath: (ctx, team) =>
|
||||
GitHubUtils.callbackUrl({
|
||||
baseUrl: team.url,
|
||||
params: ctx.request.querystring,
|
||||
}),
|
||||
getErrorPath: () => GitHubUtils.errorUrl("unauthenticated"),
|
||||
}),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.GitHubCallbackReq>) => {
|
||||
const {
|
||||
@@ -43,33 +49,6 @@ router.get(
|
||||
return;
|
||||
}
|
||||
|
||||
// this code block accounts for the root domain being unable to
|
||||
// access authentication for subdomains. We must forward to the appropriate
|
||||
// subdomain to complete the oauth flow
|
||||
if (!user) {
|
||||
if (teamId) {
|
||||
try {
|
||||
const team = await Team.findByPk(teamId, {
|
||||
rejectOnEmpty: true,
|
||||
transaction,
|
||||
});
|
||||
return parseDomain(ctx.host).teamSubdomain === team.subdomain
|
||||
? ctx.redirect("/")
|
||||
: ctx.redirectOnClient(
|
||||
GitHubUtils.callbackUrl({
|
||||
baseUrl: team.url,
|
||||
params: ctx.request.querystring,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
Logger.error(`Error fetching team for teamId: ${teamId}!`, err);
|
||||
return ctx.redirect(GitHubUtils.errorUrl("unauthenticated"));
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(GitHubUtils.errorUrl("unauthenticated"));
|
||||
}
|
||||
}
|
||||
|
||||
const client = await GitHub.authenticateAsUser(code!, teamId);
|
||||
const installationsByUser = await client.requestAppInstallations();
|
||||
const installation = find(
|
||||
|
||||
@@ -279,7 +279,8 @@ export class GitHub {
|
||||
},
|
||||
state: {
|
||||
name: prState,
|
||||
color: GitHubUtils.getColorForStatus(prState),
|
||||
color: GitHubUtils.getColorForStatus(prState, !!pr.draft),
|
||||
draft: pr.draft,
|
||||
},
|
||||
createdAt: pr.created_at,
|
||||
transformed_unfurl: true,
|
||||
|
||||
@@ -45,10 +45,10 @@ export class GitHubUtils {
|
||||
return `${this.url}?install_request=true`;
|
||||
}
|
||||
|
||||
public static getColorForStatus(status: string) {
|
||||
public static getColorForStatus(status: string, isDraftPR: boolean = false) {
|
||||
switch (status) {
|
||||
case "open":
|
||||
return "#238636";
|
||||
return isDraftPR ? "#848d97" : "#238636";
|
||||
case "done":
|
||||
return "#a371f7";
|
||||
case "closed":
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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="M3.93091 12.8481C4.11753 14.6298 4.89358 16.3615 6.25902 17.727C7.62446 19.0923 9.35612 19.8684 11.1378 20.0551L3.93091 12.8481Z"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M3.89929 11.5437L12.4422 20.0865C13.1671 20.0459 13.8876 19.9084 14.5827 19.6738L4.31194 9.4032C4.07743 10.0982 3.93988 10.8187 3.89929 11.5437Z"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M4.67981 8.49828L15.4875 19.306C16.0482 19.0374 16.5845 18.7005 17.0837 18.2953L5.6905 6.90222C5.28537 7.40142 4.94847 7.93759 4.67981 8.49828Z"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M6.29602 6.23494C9.46213 3.10852 14.5632 3.12079 17.7141 6.27173C20.865 9.42266 20.8774 14.5237 17.7509 17.6898L6.29602 6.23494Z"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
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 LinearIcon from "./Icon";
|
||||
import { LinearConnectButton } from "./components/LinearButton";
|
||||
|
||||
function Linear() {
|
||||
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.Linear,
|
||||
withRelations: true,
|
||||
});
|
||||
}, [integrations]);
|
||||
|
||||
return (
|
||||
<Scene title="Linear" icon={<LinearIcon />}>
|
||||
<Heading>Linear</Heading>
|
||||
|
||||
{error === "access_denied" && (
|
||||
<Notice>
|
||||
<Trans>
|
||||
Whoops, you need to accept the permissions in Linear to connect{" "}
|
||||
{{ appName }} to your workspace. Try again?
|
||||
</Trans>
|
||||
</Notice>
|
||||
)}
|
||||
{error === "unauthenticated" && (
|
||||
<Notice>
|
||||
<Trans>
|
||||
Something went wrong while authenticating your request. Please try
|
||||
logging in again.
|
||||
</Trans>
|
||||
</Notice>
|
||||
)}
|
||||
{env.LINEAR_CLIENT_ID ? (
|
||||
<>
|
||||
<Text as="p">
|
||||
<Trans>
|
||||
Enable previews of Linear issues in documents by connecting a
|
||||
Linear workspace to {appName}.
|
||||
</Trans>
|
||||
</Text>
|
||||
{integrations.linear.length ? (
|
||||
<>
|
||||
<Heading as="h2">
|
||||
<Flex justify="space-between" auto>
|
||||
{t("Connected")}
|
||||
<LinearConnectButton icon={<PlusIcon />} />
|
||||
</Flex>
|
||||
</Heading>
|
||||
<List>
|
||||
{integrations.linear.map((integration) => {
|
||||
const linearWorkspace =
|
||||
integration.settings?.linear?.workspace;
|
||||
const integrationCreatedBy = integration.user
|
||||
? integration.user.name
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={linearWorkspace?.id}
|
||||
small
|
||||
title={linearWorkspace?.name}
|
||||
subtitle={
|
||||
integrationCreatedBy ? (
|
||||
<>
|
||||
<Trans>Enabled by {{ integrationCreatedBy }}</Trans>{" "}
|
||||
·{" "}
|
||||
<Time
|
||||
dateTime={integration.createdAt}
|
||||
relative={false}
|
||||
format={{ en_US: "MMMM d, y" }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<PlaceholderText />
|
||||
)
|
||||
}
|
||||
image={
|
||||
<TeamLogo
|
||||
src={linearWorkspace?.logoUrl}
|
||||
size={AvatarSize.Large}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<ConnectedButton
|
||||
onClick={integration.delete}
|
||||
confirmationMessage={t(
|
||||
"Disconnecting will prevent previewing Linear links from this workspace in documents. Are you sure?"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
<LinearConnectButton icon={<LinearIcon />} />
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Notice>
|
||||
<Trans>
|
||||
The Linear integration is currently disabled. Please set the
|
||||
associated environment variables and restart the server to enable
|
||||
the integration.
|
||||
</Trans>
|
||||
</Notice>
|
||||
)}
|
||||
</Scene>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Linear);
|
||||
@@ -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 { LinearUtils } from "../../shared/LinearUtils";
|
||||
|
||||
export function LinearConnectButton(props: Props<HTMLButtonElement>) {
|
||||
const { t } = useTranslation();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
redirectTo(LinearUtils.authUrl({ state: { teamId: team.id } }))
|
||||
}
|
||||
neutral
|
||||
{...props}
|
||||
>
|
||||
{t("Connect")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -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")),
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "linear",
|
||||
"name": "Linear",
|
||||
"priority": 15,
|
||||
"description": "Adds a Linear integration for link unfurling and converting links to mentions.",
|
||||
"after": "github"
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import Router from "koa-router";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
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 { Linear } from "../linear";
|
||||
import UploadLinearWorkspaceLogoTask from "../tasks/UploadLinearWorkspaceLogoTask";
|
||||
import * as T from "./schema";
|
||||
import { LinearUtils } from "plugins/linear/shared/LinearUtils";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get(
|
||||
"linear.callback",
|
||||
auth({
|
||||
optional: true,
|
||||
}),
|
||||
validate(T.LinearCallbackSchema),
|
||||
apexAuthRedirect<T.LinearCallbackReq>({
|
||||
getTeamId: (ctx) => LinearUtils.parseState(ctx.input.query.state)?.teamId,
|
||||
getRedirectPath: (ctx, team) =>
|
||||
LinearUtils.callbackUrl({
|
||||
baseUrl: team.url,
|
||||
params: ctx.request.querystring,
|
||||
}),
|
||||
getErrorPath: () => LinearUtils.errorUrl("unauthenticated"),
|
||||
}),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.LinearCallbackReq>) => {
|
||||
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(LinearUtils.errorUrl(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// validation middleware ensures that code is non-null at this point.
|
||||
const oauth = await Linear.oauthAccess(code!);
|
||||
const workspace = await Linear.getInstalledWorkspace(oauth.access_token);
|
||||
|
||||
const authentication = await IntegrationAuthentication.create(
|
||||
{
|
||||
service: IntegrationService.Linear,
|
||||
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.Linear,
|
||||
type: IntegrationType.Embed,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
settings: {
|
||||
linear: {
|
||||
workspace: {
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
key: workspace.urlKey,
|
||||
logoUrl: workspace.logoUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ transaction }
|
||||
);
|
||||
|
||||
transaction.afterCommit(async () => {
|
||||
if (workspace.logoUrl) {
|
||||
await new UploadLinearWorkspaceLogoTask().schedule({
|
||||
integrationId: integration.id,
|
||||
logoUrl: workspace.logoUrl,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ctx.redirect(LinearUtils.successUrl());
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,17 @@
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { z } from "zod";
|
||||
import { BaseSchema } from "@server/routes/api/schema";
|
||||
|
||||
export const LinearCallbackSchema = 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 LinearCallbackReq = z.infer<typeof LinearCallbackSchema>;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IsOptional } from "class-validator";
|
||||
import { Environment } from "@server/env";
|
||||
import { Public } from "@server/utils/decorators/Public";
|
||||
import environment from "@server/utils/environment";
|
||||
import { CannotUseWithout } from "@server/utils/validators";
|
||||
|
||||
class LinearPluginEnvironment extends Environment {
|
||||
/**
|
||||
* Linear OAuth2 app client id. To enable integration with Linear.
|
||||
*/
|
||||
@Public
|
||||
@IsOptional()
|
||||
public LINEAR_CLIENT_ID = this.toOptionalString(environment.LINEAR_CLIENT_ID);
|
||||
|
||||
/**
|
||||
* Linear OAuth2 app client secret. To enable integration with Linear.
|
||||
*/
|
||||
@IsOptional()
|
||||
@CannotUseWithout("LINEAR_CLIENT_ID")
|
||||
public LINEAR_CLIENT_SECRET = this.toOptionalString(
|
||||
environment.LINEAR_CLIENT_SECRET
|
||||
);
|
||||
}
|
||||
|
||||
export default new LinearPluginEnvironment();
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Minute } from "@shared/utils/time";
|
||||
import { Hook, PluginManager } from "@server/utils/PluginManager";
|
||||
import config from "../plugin.json";
|
||||
import router from "./api/linear";
|
||||
import env from "./env";
|
||||
import { Linear } from "./linear";
|
||||
import UploadLinearWorkspaceLogoTask from "./tasks/UploadLinearWorkspaceLogoTask";
|
||||
import { uninstall } from "./uninstall";
|
||||
|
||||
const enabled = !!env.LINEAR_CLIENT_ID && !!env.LINEAR_CLIENT_SECRET;
|
||||
|
||||
if (enabled) {
|
||||
PluginManager.add([
|
||||
{
|
||||
...config,
|
||||
type: Hook.API,
|
||||
value: router,
|
||||
},
|
||||
{
|
||||
type: Hook.Task,
|
||||
value: UploadLinearWorkspaceLogoTask,
|
||||
},
|
||||
{
|
||||
type: Hook.UnfurlProvider,
|
||||
value: { unfurl: Linear.unfurl, cacheExpiry: Minute.seconds },
|
||||
},
|
||||
{
|
||||
type: Hook.Uninstall,
|
||||
value: uninstall,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { Issue, LinearClient, WorkflowState } from "@linear/sdk";
|
||||
import sortBy from "lodash/sortBy";
|
||||
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 { UnfurlIssueAndPR, UnfurlSignature } from "@server/types";
|
||||
import { LinearUtils } from "../shared/LinearUtils";
|
||||
import env from "./env";
|
||||
|
||||
const AccessTokenResponseSchema = z.object({
|
||||
access_token: z.string(),
|
||||
token_type: z.string(),
|
||||
expires_in: z.number(),
|
||||
scope: z.string(),
|
||||
});
|
||||
|
||||
export class Linear {
|
||||
private static supportedUnfurls = [UnfurlResourceType.Issue];
|
||||
|
||||
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.LINEAR_CLIENT_ID!);
|
||||
body.set("client_secret", env.LINEAR_CLIENT_SECRET!);
|
||||
body.set("redirect_uri", LinearUtils.callbackUrl());
|
||||
body.set("grant_type", "authorization_code");
|
||||
|
||||
const res = await fetch(LinearUtils.tokenUrl, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
return AccessTokenResponseSchema.parse(await res.json());
|
||||
}
|
||||
|
||||
static async revokeAccess(accessToken: string) {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
};
|
||||
|
||||
await fetch(LinearUtils.revokeUrl, {
|
||||
method: "POST",
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
static async getInstalledWorkspace(accessToken: string) {
|
||||
const client = new LinearClient({ accessToken });
|
||||
return client.organization;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url Linear resource url
|
||||
* @param actor User attempting to unfurl resource url
|
||||
* @returns An object containing resource details e.g, a Linear issue details
|
||||
*/
|
||||
static unfurl: UnfurlSignature = async (url: string, actor: User) => {
|
||||
const resource = Linear.parseUrl(url);
|
||||
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const integration = (await Integration.scope("withAuthentication").findOne({
|
||||
where: {
|
||||
service: IntegrationService.Linear,
|
||||
teamId: actor.teamId,
|
||||
"settings.linear.workspace.key": resource.workspaceKey,
|
||||
},
|
||||
})) as Integration<IntegrationType.Embed>;
|
||||
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new LinearClient({
|
||||
accessToken: integration.authentication.token,
|
||||
});
|
||||
const issue = await client.issue(resource.id);
|
||||
|
||||
if (!issue) {
|
||||
return { error: "Resource not found" };
|
||||
}
|
||||
|
||||
const [author, state, labels] = await Promise.all([
|
||||
issue.creator,
|
||||
issue.state,
|
||||
issue.paginate(issue.labels, {}),
|
||||
]);
|
||||
|
||||
if (!author || !state || !labels) {
|
||||
return { error: "Failed to fetch auxiliary data from Linear" };
|
||||
}
|
||||
|
||||
const completionPercentage = await Linear.completionPercentage(
|
||||
client,
|
||||
issue,
|
||||
state
|
||||
);
|
||||
|
||||
return {
|
||||
type: UnfurlResourceType.Issue,
|
||||
url: issue.url,
|
||||
id: issue.identifier,
|
||||
title: issue.title,
|
||||
description: issue.description ?? null,
|
||||
author: {
|
||||
name: author.name,
|
||||
avatarUrl: author.avatarUrl ?? "",
|
||||
},
|
||||
labels: labels.map((label) => ({
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
})),
|
||||
state: {
|
||||
type: state.type,
|
||||
name: state.name,
|
||||
color: state.color,
|
||||
completionPercentage,
|
||||
},
|
||||
createdAt: issue.createdAt.toISOString(),
|
||||
transformed_unfurl: true,
|
||||
} satisfies UnfurlIssueAndPR;
|
||||
} catch (err) {
|
||||
Logger.warn("Failed to fetch resource from Linear", err);
|
||||
return { error: err.message || "Unknown error" };
|
||||
}
|
||||
};
|
||||
|
||||
private static async completionPercentage(
|
||||
client: LinearClient,
|
||||
issue: Issue,
|
||||
state: WorkflowState
|
||||
) {
|
||||
const defaultCompletionPercentage = 0.5; // fallback when we cannot determine actual value.
|
||||
|
||||
if (state.type !== "started") {
|
||||
return defaultCompletionPercentage;
|
||||
}
|
||||
|
||||
const team = await issue.team;
|
||||
if (!team) {
|
||||
return defaultCompletionPercentage;
|
||||
}
|
||||
|
||||
const allStates = await client.paginate(client.workflowStates, {
|
||||
filter: {
|
||||
team: { id: { eq: team.id } },
|
||||
type: { eq: "started" },
|
||||
},
|
||||
});
|
||||
const states = sortBy(
|
||||
allStates.map((s) => ({
|
||||
name: s.name,
|
||||
position: s.position,
|
||||
})),
|
||||
(s) => s.position
|
||||
);
|
||||
|
||||
const idx = states.findIndex((s) => s.name === state.name);
|
||||
|
||||
if (idx === -1) {
|
||||
return defaultCompletionPercentage;
|
||||
} else if (states.length === 1) {
|
||||
return 0.5;
|
||||
} else if (states.length === 2) {
|
||||
return idx === 0 ? 0.5 : 0.75;
|
||||
} else {
|
||||
return (idx + 1) / (states.length + 1); // add 1 to states for the "done" state.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a given URL and returns resource identifiers for Linear specific URLs
|
||||
*
|
||||
* @param url URL to parse
|
||||
* @returns {object} Containing resource identifiers - `workspaceKey`, `type`, `id` and `name`.
|
||||
*/
|
||||
private static parseUrl(url: string) {
|
||||
const { hostname, pathname } = new URL(url);
|
||||
if (hostname !== "linear.app") {
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = pathname.split("/");
|
||||
const workspaceKey = parts[1];
|
||||
const type = parts[2] ? (parts[2] as UnfurlResourceType) : undefined;
|
||||
const id = parts[3];
|
||||
const name = parts[4];
|
||||
|
||||
if (!type || !Linear.supportedUnfurls.includes(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { workspaceKey, type, id, name };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import { Integration } from "@server/models";
|
||||
import { Buckets } from "@server/models/helpers/AttachmentHelper";
|
||||
import BaseTask, { TaskPriority } from "@server/queues/tasks/BaseTask";
|
||||
import FileStorage from "@server/storage/files";
|
||||
|
||||
type Props = {
|
||||
/** The integrationId to operate on */
|
||||
integrationId: string;
|
||||
/** The original logoUrl from Linear */
|
||||
logoUrl: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A task that uploads the provided logoUrl to storage and updates the
|
||||
* Linear integration record with the new url.
|
||||
*/
|
||||
export default class UploadLinearWorkspaceLogoTask extends BaseTask<Props> {
|
||||
public async perform(props: Props) {
|
||||
const integration = await Integration.scope("withAuthentication").findByPk<
|
||||
Integration<IntegrationType.Embed>
|
||||
>(props.integrationId);
|
||||
if (!integration || integration.service !== IntegrationService.Linear) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await FileStorage.storeFromUrl(
|
||||
props.logoUrl,
|
||||
`${Buckets.avatars}/${integration.teamId}/${uuidv4()}`,
|
||||
"public-read",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${integration.authentication.token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (res?.url) {
|
||||
integration.settings.linear!.workspace.logoUrl = res.url;
|
||||
integration.changed("settings", true);
|
||||
await integration.save();
|
||||
}
|
||||
}
|
||||
|
||||
public get options() {
|
||||
return {
|
||||
attempts: 3,
|
||||
priority: TaskPriority.Normal,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { Integration } from "@server/models";
|
||||
import { Linear } from "./linear";
|
||||
|
||||
export async function uninstall(
|
||||
integration: Integration<IntegrationType.Embed>
|
||||
) {
|
||||
if (integration.service !== IntegrationService.Linear) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authentication = await integration.$get("authentication");
|
||||
|
||||
if (!authentication) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Linear.revokeAccess(authentication.token);
|
||||
} catch (err) {
|
||||
// suppress error since this is a best-effort operation.
|
||||
Logger.error("Failed to revoke Linear access token", err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import queryString from "query-string";
|
||||
import env from "@shared/env";
|
||||
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
|
||||
|
||||
export type OAuthState = {
|
||||
teamId: string;
|
||||
};
|
||||
|
||||
export class LinearUtils {
|
||||
private static oauthScopes = "read,issues:create";
|
||||
|
||||
public static tokenUrl = "https://api.linear.app/oauth/token";
|
||||
public static revokeUrl = "https://api.linear.app/oauth/revoke";
|
||||
private static authBaseUrl = "https://linear.app/oauth/authorize";
|
||||
|
||||
private static settingsUrl = integrationSettingsPath("linear");
|
||||
|
||||
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/linear.callback?${params}`
|
||||
: `${baseUrl}/api/linear.callback`;
|
||||
}
|
||||
|
||||
static authUrl({ state }: { state: OAuthState }) {
|
||||
const params = {
|
||||
client_id: env.LINEAR_CLIENT_ID,
|
||||
redirect_uri: this.callbackUrl(),
|
||||
state: JSON.stringify(state),
|
||||
scope: this.oauthScopes,
|
||||
response_type: "code",
|
||||
prompt: "consent",
|
||||
actor: "app",
|
||||
};
|
||||
return `${this.authBaseUrl}?${queryString.stringify(params)}`;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import Router from "koa-router";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import { parseDomain } from "@shared/utils/domains";
|
||||
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 { Integration, IntegrationAuthentication, Team } from "@server/models";
|
||||
import { Integration, IntegrationAuthentication } from "@server/models";
|
||||
import { APIContext } from "@server/types";
|
||||
import { NotionClient } from "../notion";
|
||||
import * as T from "./schema";
|
||||
@@ -17,49 +16,21 @@ router.get(
|
||||
"notion.callback",
|
||||
auth({ optional: true }),
|
||||
validate(T.NotionCallbackSchema),
|
||||
apexAuthRedirect<T.NotionCallbackReq>({
|
||||
getTeamId: (ctx) => NotionUtils.parseState(ctx.input.query.state)?.teamId,
|
||||
getRedirectPath: (ctx, team) =>
|
||||
NotionUtils.callbackUrl({
|
||||
baseUrl: team.url,
|
||||
params: ctx.request.querystring,
|
||||
}),
|
||||
getErrorPath: () => NotionUtils.errorUrl("unauthenticated"),
|
||||
}),
|
||||
transaction(),
|
||||
async (ctx: APIContext<T.NotionCallbackReq>) => {
|
||||
const { code, state, error } = ctx.input.query;
|
||||
const { code, error } = ctx.input.query;
|
||||
const { user } = ctx.state.auth;
|
||||
const { transaction } = ctx.state;
|
||||
|
||||
let parsedState;
|
||||
try {
|
||||
parsedState = NotionUtils.parseState(state);
|
||||
} catch {
|
||||
ctx.redirect(NotionUtils.errorUrl("invalid_state"));
|
||||
return;
|
||||
}
|
||||
|
||||
const { teamId } = parsedState;
|
||||
|
||||
// This code block accounts for the root domain being unable to access authentication for subdomains.
|
||||
// We must forward to the appropriate subdomain to complete the oauth flow.
|
||||
if (!user) {
|
||||
if (teamId) {
|
||||
try {
|
||||
const team = await Team.findByPk(teamId, {
|
||||
rejectOnEmpty: true,
|
||||
transaction,
|
||||
});
|
||||
|
||||
return parseDomain(ctx.host).teamSubdomain === team.subdomain
|
||||
? ctx.redirect("/")
|
||||
: ctx.redirectOnClient(
|
||||
NotionUtils.callbackUrl({
|
||||
baseUrl: team.url,
|
||||
params: ctx.request.querystring,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
Logger.error(`Error fetching team for teamId: ${teamId}!`, err);
|
||||
return ctx.redirect(NotionUtils.errorUrl("unauthenticated"));
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(NotionUtils.errorUrl("unauthenticated"));
|
||||
}
|
||||
}
|
||||
|
||||
// Check error after any sub-domain redirection. Otherwise, the user will be redirected to the root domain.
|
||||
if (error) {
|
||||
ctx.redirect(NotionUtils.errorUrl(error));
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
import { RateLimit } from "async-sema";
|
||||
import emojiRegex from "emoji-regex";
|
||||
import compact from "lodash/compact";
|
||||
import truncate from "lodash/truncate";
|
||||
import { z } from "zod";
|
||||
import { Second } from "@shared/utils/time";
|
||||
import { isUrl } from "@shared/utils/urls";
|
||||
import { CollectionValidation, DocumentValidation } from "@shared/validations";
|
||||
import { NotionUtils } from "../shared/NotionUtils";
|
||||
import { Block, Page, PageType } from "../shared/types";
|
||||
import env from "./env";
|
||||
@@ -116,7 +118,9 @@ export class NotionClient {
|
||||
pages.push({
|
||||
type: item.object === "page" ? PageType.Page : PageType.Database,
|
||||
id: item.id,
|
||||
name: this.parseTitle(item),
|
||||
name: this.parseTitle(item, {
|
||||
maxLength: CollectionValidation.maxNameLength,
|
||||
}),
|
||||
emoji: this.parseEmoji(item),
|
||||
});
|
||||
}
|
||||
@@ -129,14 +133,22 @@ export class NotionClient {
|
||||
return pages;
|
||||
}
|
||||
|
||||
async fetchPage(pageId: string) {
|
||||
const pageInfo = await this.fetchPageInfo(pageId);
|
||||
async fetchPage(
|
||||
pageId: string,
|
||||
{ titleMaxLength }: { titleMaxLength: number }
|
||||
) {
|
||||
const pageInfo = await this.fetchPageInfo(pageId, { titleMaxLength });
|
||||
const blocks = await this.fetchBlockChildren(pageId);
|
||||
return { ...pageInfo, blocks };
|
||||
}
|
||||
|
||||
async fetchDatabase(databaseId: string) {
|
||||
const databaseInfo = await this.fetchDatabaseInfo(databaseId);
|
||||
async fetchDatabase(
|
||||
databaseId: string,
|
||||
{ titleMaxLength }: { titleMaxLength: number }
|
||||
) {
|
||||
const databaseInfo = await this.fetchDatabaseInfo(databaseId, {
|
||||
titleMaxLength,
|
||||
});
|
||||
const pages = await this.queryDatabase(databaseId);
|
||||
return { ...databaseInfo, pages };
|
||||
}
|
||||
@@ -162,7 +174,6 @@ export class NotionClient {
|
||||
cursor = response.next_cursor ?? undefined;
|
||||
}
|
||||
|
||||
// Recursive fetch when direct children have their own children.
|
||||
await Promise.all(
|
||||
blocks.map(async (block) => {
|
||||
if (
|
||||
@@ -203,7 +214,9 @@ export class NotionClient {
|
||||
return {
|
||||
type: PageType.Page,
|
||||
id: item.id,
|
||||
name: this.parseTitle(item),
|
||||
name: this.parseTitle(item, {
|
||||
maxLength: DocumentValidation.maxTitleLength,
|
||||
}),
|
||||
emoji: this.parseEmoji(item),
|
||||
};
|
||||
})
|
||||
@@ -218,7 +231,10 @@ export class NotionClient {
|
||||
return pages;
|
||||
}
|
||||
|
||||
private async fetchPageInfo(pageId: string): Promise<PageInfo> {
|
||||
private async fetchPageInfo(
|
||||
pageId: string,
|
||||
{ titleMaxLength }: { titleMaxLength: number }
|
||||
): Promise<PageInfo> {
|
||||
await this.limiter();
|
||||
const page = (await this.client.pages.retrieve({
|
||||
page_id: pageId,
|
||||
@@ -227,7 +243,9 @@ export class NotionClient {
|
||||
const author = await this.fetchUsername(page.created_by.id);
|
||||
|
||||
return {
|
||||
title: this.parseTitle(page),
|
||||
title: this.parseTitle(page, {
|
||||
maxLength: titleMaxLength,
|
||||
}),
|
||||
emoji: this.parseEmoji(page),
|
||||
author: author ?? undefined,
|
||||
createdAt: !page.created_time ? undefined : new Date(page.created_time),
|
||||
@@ -237,7 +255,10 @@ export class NotionClient {
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchDatabaseInfo(databaseId: string): Promise<PageInfo> {
|
||||
private async fetchDatabaseInfo(
|
||||
databaseId: string,
|
||||
{ titleMaxLength }: { titleMaxLength: number }
|
||||
): Promise<PageInfo> {
|
||||
await this.limiter();
|
||||
const database = (await this.client.databases.retrieve({
|
||||
database_id: databaseId,
|
||||
@@ -246,7 +267,9 @@ export class NotionClient {
|
||||
const author = await this.fetchUsername(database.created_by.id);
|
||||
|
||||
return {
|
||||
title: this.parseTitle(database),
|
||||
title: this.parseTitle(database, {
|
||||
maxLength: titleMaxLength,
|
||||
}),
|
||||
emoji: this.parseEmoji(database),
|
||||
author: author ?? undefined,
|
||||
createdAt: !database.created_time
|
||||
@@ -267,12 +290,12 @@ export class NotionClient {
|
||||
return user.name;
|
||||
}
|
||||
|
||||
// bot belongs to a user, get the user's name.
|
||||
// bot belongs to a user, get the user's name
|
||||
if (user.bot.owner.type === "user" && isFullUser(user.bot.owner.user)) {
|
||||
return user.bot.owner.user.name;
|
||||
}
|
||||
|
||||
// bot belongs to a workspace, fallback to bot's name.
|
||||
// bot belongs to a workspace, fallback to bot's name
|
||||
return user.name;
|
||||
} catch (error) {
|
||||
// Handle the case where a user can't be found
|
||||
@@ -286,7 +309,12 @@ export class NotionClient {
|
||||
}
|
||||
}
|
||||
|
||||
private parseTitle(item: PageObjectResponse | DatabaseObjectResponse) {
|
||||
private parseTitle(
|
||||
item: PageObjectResponse | DatabaseObjectResponse,
|
||||
{
|
||||
maxLength = DocumentValidation.maxTitleLength,
|
||||
}: { maxLength?: number } = {}
|
||||
) {
|
||||
let richTexts: RichTextItemResponse[];
|
||||
|
||||
if (item.object === "page") {
|
||||
@@ -298,7 +326,10 @@ export class NotionClient {
|
||||
richTexts = item.title;
|
||||
}
|
||||
|
||||
return richTexts.map((richText) => richText.plain_text).join("");
|
||||
const title = richTexts.map((richText) => richText.plain_text).join("");
|
||||
|
||||
// Truncate title to fit within validation limits
|
||||
return truncate(title, { length: maxLength });
|
||||
}
|
||||
|
||||
private parseEmoji(item: PageObjectResponse | DatabaseObjectResponse) {
|
||||
|
||||
@@ -66,6 +66,6 @@ export class NotionImportsProcessor extends ImportsProcessor<IntegrationService.
|
||||
protected async scheduleTask(
|
||||
importTask: ImportTask<IntegrationService.Notion>
|
||||
): Promise<void> {
|
||||
await NotionAPIImportTask.schedule({ importTaskId: importTask.id });
|
||||
await new NotionAPIImportTask().schedule({ importTaskId: importTask.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { APIResponseError, APIErrorCode } from "@notionhq/client";
|
||||
import { ImportTaskInput, ImportTaskOutput } from "@shared/schema";
|
||||
import { IntegrationService, ProsemirrorDoc } from "@shared/types";
|
||||
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
|
||||
import { CollectionValidation, DocumentValidation } from "@shared/validations";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { Integration } from "@server/models";
|
||||
import ImportTask from "@server/models/ImportTask";
|
||||
@@ -76,7 +77,7 @@ export default class NotionAPIImportTask extends APIImportTask<IntegrationServic
|
||||
protected async scheduleNextTask(
|
||||
importTask: ImportTask<IntegrationService.Notion>
|
||||
) {
|
||||
await NotionAPIImportTask.schedule({ importTaskId: importTask.id });
|
||||
await new NotionAPIImportTask().schedule({ importTaskId: importTask.id });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,12 +96,17 @@ export default class NotionAPIImportTask extends APIImportTask<IntegrationServic
|
||||
client: NotionClient;
|
||||
}): Promise<ParsePageOutput | null> {
|
||||
const collectionExternalId = item.collectionExternalId ?? item.externalId;
|
||||
const titleMaxLength =
|
||||
item.externalId === collectionExternalId // This means it's a root page which will be imported as a collection
|
||||
? CollectionValidation.maxNameLength
|
||||
: DocumentValidation.maxTitleLength;
|
||||
|
||||
try {
|
||||
// Convert Notion database to an empty page with "pages in database" as its children.
|
||||
if (item.type === PageType.Database) {
|
||||
const { pages, ...databaseInfo } = await client.fetchDatabase(
|
||||
item.externalId
|
||||
item.externalId,
|
||||
{ titleMaxLength }
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -115,7 +121,9 @@ export default class NotionAPIImportTask extends APIImportTask<IntegrationServic
|
||||
};
|
||||
}
|
||||
|
||||
const { blocks, ...pageInfo } = await client.fetchPage(item.externalId);
|
||||
const { blocks, ...pageInfo } = await client.fetchPage(item.externalId, {
|
||||
titleMaxLength,
|
||||
});
|
||||
|
||||
return {
|
||||
...pageInfo,
|
||||
|
||||
@@ -4,16 +4,15 @@ import Router from "koa-router";
|
||||
import { Profile } from "passport";
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import { parseDomain } from "@shared/utils/domains";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import { ValidationError } from "@server/errors";
|
||||
import apexAuthRedirect from "@server/middlewares/apexAuthRedirect";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import validate from "@server/middlewares/validate";
|
||||
import {
|
||||
IntegrationAuthentication,
|
||||
Integration,
|
||||
Team,
|
||||
User,
|
||||
Collection,
|
||||
} from "@server/models";
|
||||
@@ -126,6 +125,15 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
"slack.post",
|
||||
auth({ optional: true }),
|
||||
validate(T.SlackPostSchema),
|
||||
apexAuthRedirect<T.SlackPostReq>({
|
||||
getTeamId: (ctx) => SlackUtils.parseState(ctx.input.query.state)?.teamId,
|
||||
getRedirectPath: (ctx, team) =>
|
||||
SlackUtils.connectUrl({
|
||||
baseUrl: team.url,
|
||||
params: ctx.request.querystring,
|
||||
}),
|
||||
getErrorPath: () => SlackUtils.errorUrl("unauthenticated"),
|
||||
}),
|
||||
async (ctx: APIContext<T.SlackPostReq>) => {
|
||||
const { code, error, state } = ctx.input.query;
|
||||
const { user } = ctx.state.auth;
|
||||
@@ -144,31 +152,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
throw ValidationError("Invalid state");
|
||||
}
|
||||
|
||||
const { teamId, collectionId, type } = parsedState;
|
||||
|
||||
// This code block accounts for the root domain being unable to access authentication for
|
||||
// subdomains. We must forward to the appropriate subdomain to complete the OAuth flow.
|
||||
if (!user) {
|
||||
if (teamId) {
|
||||
try {
|
||||
const team = await Team.findByPk(teamId, {
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
return parseDomain(ctx.host).teamSubdomain === team.subdomain
|
||||
? ctx.redirect("/")
|
||||
: ctx.redirectOnClient(
|
||||
SlackUtils.connectUrl({
|
||||
baseUrl: team.url,
|
||||
params: ctx.request.querystring,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
return ctx.redirect(SlackUtils.errorUrl("unauthenticated"));
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(SlackUtils.errorUrl("unauthenticated"));
|
||||
}
|
||||
}
|
||||
const { collectionId, type } = parsedState;
|
||||
|
||||
switch (type) {
|
||||
case IntegrationType.Post: {
|
||||
|
||||
@@ -96,6 +96,7 @@ router.get(
|
||||
ctx.set("Accept-Ranges", "bytes");
|
||||
ctx.set("Cache-Control", cacheHeader);
|
||||
ctx.set("Content-Type", contentType);
|
||||
ctx.set("Content-Security-Policy", "sandbox");
|
||||
ctx.attachment(fileName, {
|
||||
type: forceDownload
|
||||
? "attachment"
|
||||
|
||||
@@ -52,18 +52,18 @@ function Webhooks() {
|
||||
in near real-time.
|
||||
</Trans>
|
||||
</Text>
|
||||
<PaginatedList
|
||||
<PaginatedList<WebhookSubscription>
|
||||
fetch={webhookSubscriptions.fetchPage}
|
||||
items={webhookSubscriptions.enabled}
|
||||
heading={<h2>{t("Active")}</h2>}
|
||||
renderItem={(webhook: WebhookSubscription) => (
|
||||
renderItem={(webhook) => (
|
||||
<WebhookSubscriptionListItem key={webhook.id} webhook={webhook} />
|
||||
)}
|
||||
/>
|
||||
<PaginatedList
|
||||
<PaginatedList<WebhookSubscription>
|
||||
items={webhookSubscriptions.disabled}
|
||||
heading={<h2>{t("Inactive")}</h2>}
|
||||
renderItem={(webhook: WebhookSubscription) => (
|
||||
renderItem={(webhook) => (
|
||||
<WebhookSubscriptionListItem key={webhook.id} webhook={webhook} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -29,8 +29,12 @@ describe("WebhookProcessor", () => {
|
||||
|
||||
await processor.perform(event);
|
||||
|
||||
expect(DeliverWebhookTask.schedule).toHaveBeenCalled();
|
||||
expect(DeliverWebhookTask.schedule).toHaveBeenCalledWith({
|
||||
expect(
|
||||
jest.mocked(DeliverWebhookTask.prototype.schedule)
|
||||
).toHaveBeenCalled();
|
||||
expect(
|
||||
jest.mocked(DeliverWebhookTask.prototype.schedule)
|
||||
).toHaveBeenCalledWith({
|
||||
event,
|
||||
subscriptionId: subscription.id,
|
||||
});
|
||||
@@ -53,7 +57,9 @@ describe("WebhookProcessor", () => {
|
||||
|
||||
await processor.perform(event);
|
||||
|
||||
expect(DeliverWebhookTask.schedule).toHaveBeenCalledTimes(0);
|
||||
expect(
|
||||
jest.mocked(DeliverWebhookTask.prototype.schedule)
|
||||
).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("it schedules a delivery for the event for each subscription", async () => {
|
||||
@@ -79,13 +85,21 @@ describe("WebhookProcessor", () => {
|
||||
|
||||
await processor.perform(event);
|
||||
|
||||
expect(DeliverWebhookTask.schedule).toHaveBeenCalled();
|
||||
expect(DeliverWebhookTask.schedule).toHaveBeenCalledTimes(2);
|
||||
expect(DeliverWebhookTask.schedule).toHaveBeenCalledWith({
|
||||
expect(
|
||||
jest.mocked(DeliverWebhookTask.prototype.schedule)
|
||||
).toHaveBeenCalled();
|
||||
expect(
|
||||
jest.mocked(DeliverWebhookTask.prototype.schedule)
|
||||
).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
jest.mocked(DeliverWebhookTask.prototype.schedule)
|
||||
).toHaveBeenCalledWith({
|
||||
event,
|
||||
subscriptionId: subscription.id,
|
||||
});
|
||||
expect(DeliverWebhookTask.schedule).toHaveBeenCalledWith({
|
||||
expect(
|
||||
jest.mocked(DeliverWebhookTask.prototype.schedule)
|
||||
).toHaveBeenCalledWith({
|
||||
event,
|
||||
subscriptionId: subscriptionTwo.id,
|
||||
});
|
||||
|
||||
@@ -24,7 +24,10 @@ export default class WebhookProcessor extends BaseProcessor {
|
||||
|
||||
await Promise.all(
|
||||
applicableSubscriptions.map((subscription) =>
|
||||
DeliverWebhookTask.schedule({ event, subscriptionId: subscription.id })
|
||||
new DeliverWebhookTask().schedule({
|
||||
event,
|
||||
subscriptionId: subscription.id,
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type Props = {
|
||||
/** Position of moved document within document structure */
|
||||
index?: number;
|
||||
/** The IP address of the user moving the document */
|
||||
ip: string;
|
||||
ip: string | null;
|
||||
/** The database transaction to run within */
|
||||
transaction?: Transaction;
|
||||
};
|
||||
|
||||
@@ -4,9 +4,11 @@ import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
|
||||
import { buildAttachment, buildDocument } from "@server/test/factories";
|
||||
import documentPermanentDeleter from "./documentPermanentDeleter";
|
||||
|
||||
jest.mock("@server/queues/tasks/DeleteAttachmentTask", () => ({
|
||||
schedule: jest.fn(),
|
||||
}));
|
||||
jest.mock("@server/queues/tasks/DeleteAttachmentTask");
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("documentPermanentDeleter", () => {
|
||||
it("should destroy documents", async () => {
|
||||
@@ -60,7 +62,9 @@ describe("documentPermanentDeleter", () => {
|
||||
await document.save();
|
||||
const countDeletedDoc = await documentPermanentDeleter([document]);
|
||||
expect(countDeletedDoc).toEqual(1);
|
||||
expect(DeleteAttachmentTask.schedule).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
jest.mocked(DeleteAttachmentTask.prototype.schedule)
|
||||
).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
await Document.unscoped().count({
|
||||
where: {
|
||||
|
||||
@@ -67,7 +67,7 @@ export default async function documentPermanentDeleter(documents: Document[]) {
|
||||
"commands",
|
||||
`Attachment ${attachmentId} scheduled for deletion`
|
||||
);
|
||||
await DeleteAttachmentTask.schedule({
|
||||
await new DeleteAttachmentTask().schedule({
|
||||
attachmentId,
|
||||
teamId: document.teamId,
|
||||
});
|
||||
|
||||
@@ -56,5 +56,5 @@ export default async function userSuspender({
|
||||
}
|
||||
);
|
||||
|
||||
await CleanupDemotedUserTask.schedule({ userId: user.id });
|
||||
await new CleanupDemotedUserTask().schedule({ userId: user.id });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Next } from "koa";
|
||||
import { parseDomain } from "@shared/utils/domains";
|
||||
import { Team } from "@server/models";
|
||||
import { APIContext } from "@server/types";
|
||||
|
||||
/**
|
||||
* An authentication middleware that should be used on routes that return from external auth flows
|
||||
* to the apex domain. In these cases the user will be redirected to the correct subdomain where
|
||||
* they are authenticated.
|
||||
*
|
||||
* @param options Options for the middleware
|
||||
* @returns Koa middleware function
|
||||
*/
|
||||
export default function apexAuthRedirect<T>({
|
||||
getTeamId,
|
||||
getRedirectPath,
|
||||
getErrorPath,
|
||||
}: {
|
||||
/** Get the team ID for the current request */
|
||||
getTeamId: (ctx: APIContext<T>) => string | null | undefined;
|
||||
/** Get the redirect URL for the given team ID */
|
||||
getRedirectPath: (ctx: APIContext<T>, team: Team) => string;
|
||||
/** Get the error URL for the current request */
|
||||
getErrorPath: (ctx: APIContext<T>) => string;
|
||||
}) {
|
||||
return async function apexAuthRedirectMiddleware(
|
||||
ctx: APIContext<T>,
|
||||
next: Next
|
||||
) {
|
||||
const { user } = ctx.state.auth;
|
||||
if (user) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const teamId = getTeamId(ctx);
|
||||
|
||||
if (teamId) {
|
||||
try {
|
||||
const team = await Team.findByPk(teamId, {
|
||||
attributes: ["id", "subdomain"],
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
|
||||
return parseDomain(ctx.host).teamSubdomain === team.subdomain
|
||||
? ctx.redirect("/")
|
||||
: ctx.redirectOnClient(getRedirectPath(ctx, team));
|
||||
} catch (err) {
|
||||
return ctx.redirect(getErrorPath(ctx));
|
||||
}
|
||||
} else {
|
||||
return ctx.redirect(getErrorPath(ctx));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -408,7 +408,7 @@ class Team extends ParanoidModel<
|
||||
});
|
||||
|
||||
if (attachment) {
|
||||
await DeleteAttachmentTask.schedule({
|
||||
await new DeleteAttachmentTask().schedule({
|
||||
attachmentId: attachment.id,
|
||||
teamId: model.id,
|
||||
});
|
||||
|
||||
@@ -717,7 +717,7 @@ class User extends ParanoidModel<
|
||||
});
|
||||
|
||||
if (attachment) {
|
||||
await DeleteAttachmentTask.schedule({
|
||||
await new DeleteAttachmentTask().schedule({
|
||||
attachmentId: attachment.id,
|
||||
teamId: model.teamId,
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("policies/team", () => {
|
||||
const permissions = new Map<UserRole, boolean>([
|
||||
[UserRole.Admin, true],
|
||||
[UserRole.Member, true],
|
||||
[UserRole.Viewer, false],
|
||||
[UserRole.Viewer, true],
|
||||
[UserRole.Guest, true],
|
||||
]);
|
||||
for (const [role, permission] of permissions.entries()) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
or,
|
||||
} from "./utils";
|
||||
|
||||
allow(User, "read", Team, isTeamModel);
|
||||
allow(User, ["read", "readTemplate"], Team, isTeamModel);
|
||||
|
||||
allow(User, "share", Team, (actor, team) =>
|
||||
and(
|
||||
@@ -50,10 +50,6 @@ allow(User, "createTemplate", Team, (actor, team) =>
|
||||
)
|
||||
);
|
||||
|
||||
allow(User, "readTemplate", Team, (actor, team) =>
|
||||
and(!actor.isViewer, isTeamModel(actor, team))
|
||||
);
|
||||
|
||||
allow(User, "updateTemplate", Team, (actor, team) =>
|
||||
and(
|
||||
//
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class AvatarProcessor extends BaseProcessor {
|
||||
});
|
||||
|
||||
if (user.avatarUrl) {
|
||||
await UploadUserAvatarTask.schedule({
|
||||
await new UploadUserAvatarTask().schedule({
|
||||
userId: event.userId,
|
||||
avatarUrl: user.avatarUrl,
|
||||
});
|
||||
@@ -30,7 +30,7 @@ export default class AvatarProcessor extends BaseProcessor {
|
||||
});
|
||||
|
||||
if (team.avatarUrl) {
|
||||
await UploadTeamAvatarTask.schedule({
|
||||
await new UploadTeamAvatarTask().schedule({
|
||||
teamId: event.teamId,
|
||||
avatarUrl: team.avatarUrl,
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ export default class CollectionsProcessor extends BaseProcessor {
|
||||
];
|
||||
|
||||
async perform(event: CollectionEvent) {
|
||||
await DetachDraftsFromCollectionTask.schedule({
|
||||
await new DetachDraftsFromCollectionTask().schedule({
|
||||
collectionId: event.collectionId,
|
||||
actorId: event.actorId,
|
||||
ip: event.ip,
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class DocumentSubscriptionProcessor extends BaseProcessor {
|
||||
async perform(event: ReceivedEvent) {
|
||||
switch (event.name) {
|
||||
case "collections.remove_user": {
|
||||
await CollectionSubscriptionRemoveUserTask.schedule(event);
|
||||
await new CollectionSubscriptionRemoveUserTask().schedule(event);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default class DocumentSubscriptionProcessor extends BaseProcessor {
|
||||
return this.handleRemoveGroupFromCollection(event);
|
||||
|
||||
case "documents.remove_user": {
|
||||
await DocumentSubscriptionRemoveUserTask.schedule(event);
|
||||
await new DocumentSubscriptionRemoveUserTask().schedule(event);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ export default class DocumentSubscriptionProcessor extends BaseProcessor {
|
||||
async (groupUsers) => {
|
||||
await Promise.all(
|
||||
groupUsers.map((groupUser) =>
|
||||
CollectionSubscriptionRemoveUserTask.schedule({
|
||||
new CollectionSubscriptionRemoveUserTask().schedule({
|
||||
...event,
|
||||
name: "collections.remove_user",
|
||||
userId: groupUser.userId,
|
||||
})
|
||||
} as CollectionUserEvent)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -79,11 +79,11 @@ export default class DocumentSubscriptionProcessor extends BaseProcessor {
|
||||
async (groupUsers) => {
|
||||
await Promise.all(
|
||||
groupUsers.map((groupUser) =>
|
||||
DocumentSubscriptionRemoveUserTask.schedule({
|
||||
new DocumentSubscriptionRemoveUserTask().schedule({
|
||||
...event,
|
||||
name: "documents.remove_user",
|
||||
userId: groupUser.userId,
|
||||
})
|
||||
} as DocumentUserEvent)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ export default class FileOperationCreatedProcessor extends BaseProcessor {
|
||||
if (fileOperation.type === FileOperationType.Import) {
|
||||
switch (fileOperation.format) {
|
||||
case FileOperationFormat.MarkdownZip:
|
||||
await ImportMarkdownZipTask.schedule({
|
||||
await new ImportMarkdownZipTask().schedule({
|
||||
fileOperationId: event.modelId,
|
||||
});
|
||||
break;
|
||||
case FileOperationFormat.JSON:
|
||||
await ImportJSONTask.schedule({
|
||||
await new ImportJSONTask().schedule({
|
||||
fileOperationId: event.modelId,
|
||||
});
|
||||
break;
|
||||
@@ -36,17 +36,17 @@ export default class FileOperationCreatedProcessor extends BaseProcessor {
|
||||
if (fileOperation.type === FileOperationType.Export) {
|
||||
switch (fileOperation.format) {
|
||||
case FileOperationFormat.HTMLZip:
|
||||
await ExportHTMLZipTask.schedule({
|
||||
await new ExportHTMLZipTask().schedule({
|
||||
fileOperationId: event.modelId,
|
||||
});
|
||||
break;
|
||||
case FileOperationFormat.MarkdownZip:
|
||||
await ExportMarkdownZipTask.schedule({
|
||||
await new ExportMarkdownZipTask().schedule({
|
||||
fileOperationId: event.modelId,
|
||||
});
|
||||
break;
|
||||
case FileOperationFormat.JSON:
|
||||
await ExportJSONTask.schedule({
|
||||
await new ExportJSONTask().schedule({
|
||||
fileOperationId: event.modelId,
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -20,7 +20,7 @@ export default class IntegrationCreatedProcessor extends BaseProcessor {
|
||||
}
|
||||
|
||||
// Store the available issue sources in the integration record.
|
||||
await CacheIssueSourcesTask.schedule({
|
||||
await new CacheIssueSourcesTask().schedule({
|
||||
integrationId: integration.id,
|
||||
});
|
||||
|
||||
|
||||
@@ -62,25 +62,25 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
await DocumentPublishedNotificationsTask.schedule(event);
|
||||
await new DocumentPublishedNotificationsTask().schedule(event);
|
||||
}
|
||||
|
||||
async documentAddUser(event: DocumentUserEvent) {
|
||||
if (!event.data.isNew || event.userId === event.actorId) {
|
||||
return;
|
||||
}
|
||||
await DocumentAddUserNotificationsTask.schedule(event);
|
||||
await new DocumentAddUserNotificationsTask().schedule(event);
|
||||
}
|
||||
|
||||
async documentAddGroup(event: DocumentGroupEvent) {
|
||||
if (!event.data.isNew) {
|
||||
return;
|
||||
}
|
||||
await DocumentAddGroupNotificationsTask.schedule(event);
|
||||
await new DocumentAddGroupNotificationsTask().schedule(event);
|
||||
}
|
||||
|
||||
async revisionCreated(event: RevisionEvent) {
|
||||
await RevisionCreatedNotificationsTask.schedule(event);
|
||||
await new RevisionCreatedNotificationsTask().schedule(event);
|
||||
}
|
||||
|
||||
async collectionCreated(event: CollectionEvent) {
|
||||
@@ -93,7 +93,7 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
await CollectionCreatedNotificationsTask.schedule(event);
|
||||
await new CollectionCreatedNotificationsTask().schedule(event);
|
||||
}
|
||||
|
||||
async collectionAddUser(event: CollectionUserEvent) {
|
||||
@@ -101,14 +101,14 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
await CollectionAddUserNotificationsTask.schedule(event);
|
||||
await new CollectionAddUserNotificationsTask().schedule(event);
|
||||
}
|
||||
|
||||
async commentCreated(event: CommentEvent) {
|
||||
await CommentCreatedNotificationsTask.schedule(event);
|
||||
await new CommentCreatedNotificationsTask().schedule(event);
|
||||
}
|
||||
|
||||
async commentUpdated(event: CommentEvent) {
|
||||
await CommentUpdatedNotificationsTask.schedule(event);
|
||||
await new CommentUpdatedNotificationsTask().schedule(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class RevisionsProcessor extends BaseProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
await DocumentUpdateTextTask.schedule(event);
|
||||
await new DocumentUpdateTextTask().schedule(event);
|
||||
|
||||
const user = await User.findByPk(event.actorId, {
|
||||
paranoid: false,
|
||||
|
||||
@@ -6,6 +6,6 @@ export default class UserDemotedProcessor extends BaseProcessor {
|
||||
static applicableEvents: TEvent["name"][] = ["users.demote"];
|
||||
|
||||
async perform(event: UserEvent) {
|
||||
await CleanupDemotedUserTask.schedule({ userId: event.userId });
|
||||
await new CleanupDemotedUserTask().schedule({ userId: event.userId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +325,9 @@ export default abstract class APIImportTask<
|
||||
([url, attachment]) => ({ attachmentId: attachment.id, url })
|
||||
);
|
||||
// publish task after attachments are persisted in DB.
|
||||
const job = await UploadAttachmentsForImportTask.schedule(uploadItems);
|
||||
const job = await new UploadAttachmentsForImportTask().schedule(
|
||||
uploadItems
|
||||
);
|
||||
await job.finished();
|
||||
} catch (err) {
|
||||
// upload attachments failure is not critical enough to fail the whole import.
|
||||
|
||||
@@ -21,7 +21,7 @@ export default abstract class BaseTask<T extends Record<string, any>> {
|
||||
static cron: TaskSchedule | undefined;
|
||||
|
||||
/**
|
||||
* Schedule this task type to be processed asyncronously by a worker.
|
||||
* Schedule this task type to be processed asynchronously by a worker.
|
||||
*
|
||||
* @param props Properties to be used by the task
|
||||
* @returns A promise that resolves once the job is placed on the task queue
|
||||
@@ -39,6 +39,23 @@ export default abstract class BaseTask<T extends Record<string, any>> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule this task type to be processed asynchronously by a worker.
|
||||
*
|
||||
* @param props Properties to be used by the task
|
||||
* @param options Job options such as priority and retry strategy, as defined by Bull.
|
||||
* @returns A promise that resolves once the job is placed on the task queue
|
||||
*/
|
||||
public schedule(props: T, options?: JobOptions): Promise<Job> {
|
||||
return taskQueue.add(
|
||||
{
|
||||
name: this.constructor.name,
|
||||
props,
|
||||
},
|
||||
{ ...options, ...this.options }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the task.
|
||||
*
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class CleanupDeletedTeamsTask extends BaseTask<Props> {
|
||||
});
|
||||
|
||||
for (const team of teams) {
|
||||
await CleanupDeletedTeamTask.schedule({
|
||||
await new CleanupDeletedTeamTask().schedule({
|
||||
teamId: team.id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import BaseTask from "./BaseTask";
|
||||
type Props = {
|
||||
collectionId: string;
|
||||
actorId: string;
|
||||
ip: string;
|
||||
ip: string | null;
|
||||
};
|
||||
|
||||
export default class DetachDraftsFromCollectionTask extends BaseTask<Props> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Op } from "sequelize";
|
||||
import { GroupUser } from "@server/models";
|
||||
import { DocumentGroupEvent } from "@server/types";
|
||||
import { DocumentGroupEvent, DocumentUserEvent } from "@server/types";
|
||||
import BaseTask, { TaskPriority } from "./BaseTask";
|
||||
import DocumentAddUserNotificationsTask from "./DocumentAddUserNotificationsTask";
|
||||
|
||||
@@ -19,11 +19,12 @@ export default class DocumentAddGroupNotificationsTask extends BaseTask<Document
|
||||
async (groupUsers) => {
|
||||
await Promise.all(
|
||||
groupUsers.map(async (groupUser) => {
|
||||
await DocumentAddUserNotificationsTask.schedule({
|
||||
await new DocumentAddUserNotificationsTask().schedule({
|
||||
...event,
|
||||
name: "documents.add_user",
|
||||
modelId: event.data.membershipId,
|
||||
userId: groupUser.userId,
|
||||
});
|
||||
} as DocumentUserEvent);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = {
|
||||
sourceMetadata: Pick<Required<SourceMetadata>, "fileName" | "mimeType">;
|
||||
publish?: boolean;
|
||||
collectionId?: string;
|
||||
parentDocumentId?: string;
|
||||
parentDocumentId?: string | null;
|
||||
ip: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class UpdateTeamsAttachmentsSizeTask extends BaseTask<Props> {
|
||||
const teamIds = rows.map((row) => row.teamId);
|
||||
|
||||
for (const teamId of teamIds) {
|
||||
await UpdateTeamAttachmentsSizeTask.schedule({ teamId });
|
||||
await new UpdateTeamAttachmentsSizeTask().schedule({ teamId });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -166,7 +166,7 @@ router.post(
|
||||
)
|
||||
);
|
||||
|
||||
const job = await UploadAttachmentFromUrlTask.schedule({
|
||||
const job = await new UploadAttachmentFromUrlTask().schedule({
|
||||
attachmentId: attachment.id,
|
||||
url,
|
||||
});
|
||||
|
||||
@@ -135,7 +135,7 @@ router.post("auth.info", auth(), async (ctx: APIContext<T.AuthInfoReq>) => {
|
||||
// If the user did not _just_ sign in then we need to check if they continue
|
||||
// to have access to the workspace they are signed into.
|
||||
if (user.lastSignedInAt && user.lastSignedInAt < subHours(new Date(), 1)) {
|
||||
await ValidateSSOAccessTask.schedule({ userId: user.id });
|
||||
await new ValidateSSOAccessTask().schedule({ userId: user.id });
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
|
||||
@@ -29,7 +29,8 @@ const cronHandler = async (ctx: APIContext<T.CronSchemaReq>) => {
|
||||
for (const name in tasks) {
|
||||
const TaskClass = tasks[name];
|
||||
if (TaskClass.cron === period) {
|
||||
await TaskClass.schedule({ limit });
|
||||
// @ts-expect-error We won't instantiate an abstract class
|
||||
await new TaskClass().schedule({ limit });
|
||||
|
||||
// Backwards compatibility for installations that have not set up
|
||||
// cron jobs periods other than daily.
|
||||
@@ -38,13 +39,15 @@ const cronHandler = async (ctx: APIContext<T.CronSchemaReq>) => {
|
||||
!receivedPeriods.has(TaskSchedule.Minute) &&
|
||||
(period === TaskSchedule.Hour || period === TaskSchedule.Day)
|
||||
) {
|
||||
await TaskClass.schedule({ limit });
|
||||
// @ts-expect-error We won't instantiate an abstract class
|
||||
await new TaskClass().schedule({ limit });
|
||||
} else if (
|
||||
TaskClass.cron === TaskSchedule.Hour &&
|
||||
!receivedPeriods.has(TaskSchedule.Hour) &&
|
||||
period === TaskSchedule.Day
|
||||
) {
|
||||
await TaskClass.schedule({ limit });
|
||||
// @ts-expect-error We won't instantiate an abstract class
|
||||
await new TaskClass().schedule({ limit });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1539,7 +1539,7 @@ router.post(
|
||||
acl,
|
||||
});
|
||||
|
||||
const job = await DocumentImportTask.schedule({
|
||||
const job = await new DocumentImportTask().schedule({
|
||||
key,
|
||||
sourceMetadata: {
|
||||
fileName,
|
||||
@@ -1549,6 +1549,7 @@ router.post(
|
||||
collectionId,
|
||||
parentDocumentId,
|
||||
publish,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
const response: DocumentImportTaskResponse = await job.finished();
|
||||
if ("error" in response) {
|
||||
@@ -2062,7 +2063,7 @@ router.post(
|
||||
});
|
||||
|
||||
if (documents.length) {
|
||||
await EmptyTrashTask.schedule({
|
||||
await new EmptyTrashTask().schedule({
|
||||
documentIds: documents.map((doc) => doc.id),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ export default function init() {
|
||||
for (const name in tasks) {
|
||||
const TaskClass = tasks[name];
|
||||
if (TaskClass.cron === schedule) {
|
||||
await TaskClass.schedule({ limit: 10000 });
|
||||
// @ts-expect-error We won't instantiate an abstract class
|
||||
await new TaskClass().schedule({ limit: 10000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import cookie from "cookie";
|
||||
import Koa from "koa";
|
||||
import IO from "socket.io";
|
||||
import { createAdapter } from "socket.io-redis";
|
||||
import env from "@server/env";
|
||||
import { AuthenticationError } from "@server/errors";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import Metrics from "@server/logging/Metrics";
|
||||
@@ -38,7 +39,8 @@ export default function init(
|
||||
pingInterval: 15000,
|
||||
pingTimeout: 30000,
|
||||
cors: {
|
||||
origin: "*",
|
||||
// Included for completeness, though CORS does not apply to websocket transport.
|
||||
origin: env.isCloudHosted ? "*" : env.URL,
|
||||
methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
@@ -60,6 +62,16 @@ export default function init(
|
||||
"upgrade",
|
||||
function (req: IncomingMessage, socket: Duplex, head: Buffer) {
|
||||
if (req.url?.startsWith(path) && ioHandleUpgrade) {
|
||||
// For on-premise deployments, ensure the websocket origin matches the deployed URL.
|
||||
// In cloud-hosted we support any origin for custom domains.
|
||||
if (
|
||||
!env.isCloudHosted &&
|
||||
(!req.headers.origin || !env.URL.startsWith(req.headers.origin))
|
||||
) {
|
||||
socket.end(`HTTP/1.1 400 Bad Request\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
ioHandleUpgrade(req, socket, head);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Blob } from "buffer";
|
||||
import { Readable } from "stream";
|
||||
import { PresignedPost } from "@aws-sdk/s3-presigned-post";
|
||||
import omit from "lodash/omit";
|
||||
import FileHelper from "@shared/editor/lib/FileHelper";
|
||||
import { isBase64Url, isInternalUrl } from "@shared/utils/urls";
|
||||
import env from "@server/env";
|
||||
@@ -162,6 +163,12 @@ export default abstract class BaseStorage {
|
||||
buffer = Buffer.from(match[2], "base64");
|
||||
} else {
|
||||
try {
|
||||
const headers = {
|
||||
"User-Agent": chromeUserAgent,
|
||||
...init?.headers,
|
||||
};
|
||||
const initWithoutHeaders = omit(init, ["headers"]);
|
||||
|
||||
const res = await fetch(url, {
|
||||
follow: 3,
|
||||
redirect: "follow",
|
||||
@@ -169,11 +176,9 @@ export default abstract class BaseStorage {
|
||||
options?.maxUploadSize ?? Infinity,
|
||||
env.FILE_STORAGE_UPLOAD_MAX_SIZE
|
||||
),
|
||||
headers: {
|
||||
"User-Agent": chromeUserAgent,
|
||||
},
|
||||
headers,
|
||||
timeout: 10000,
|
||||
...init,
|
||||
...initWithoutHeaders,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { s } from "../styles";
|
||||
|
||||
const BacktickSpan = styled.span`
|
||||
font-family: ${s("fontFamilyMono")};
|
||||
background: ${s("codeBackground")};
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
font-size: 90%;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
/** The content to be rendered that may contain backticks. */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to render backticked content with styling.
|
||||
* @param props - Props object containing the content to be rendered.
|
||||
* @returns JSX.Element - The rendered component.
|
||||
*/
|
||||
export const Backticks: React.FC<Props> = ({ content }) => {
|
||||
// Regex to match text between backticks
|
||||
const regex = /`([^`]+)`/g;
|
||||
const parts = content.split(regex);
|
||||
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) => {
|
||||
// Even indices are normal text, odd indices are backticked content
|
||||
if (i % 2 === 0) {
|
||||
return part;
|
||||
}
|
||||
return <BacktickSpan key={i}>{part}</BacktickSpan>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
status: string;
|
||||
color: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Issue status icon based on GitHub issue status, but can be used for any git-style integration.
|
||||
*/
|
||||
export function IssueStatusIcon({ size, ...rest }: Props) {
|
||||
return (
|
||||
<Icon size={size}>
|
||||
<BaseIcon {...rest} />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = styled.span<{ size?: number }>`
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
width: ${(props) => props.size ?? 24}px;
|
||||
height: ${(props) => props.size ?? 24}px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
function BaseIcon(props: Props) {
|
||||
switch (props.status) {
|
||||
case "open":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill={props.color}
|
||||
className={props.className}
|
||||
>
|
||||
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z" />
|
||||
</svg>
|
||||
);
|
||||
case "closed":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill={props.color}
|
||||
className={props.className}
|
||||
>
|
||||
<path d="M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z" />
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z" />
|
||||
</svg>
|
||||
);
|
||||
case "canceled":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill={props.color}
|
||||
className={props.className}
|
||||
>
|
||||
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { BaseIconProps } from ".";
|
||||
|
||||
export function GitHubIssueStatusIcon(props: BaseIconProps) {
|
||||
const { state, className, size = 16 } = props;
|
||||
|
||||
switch (state.name) {
|
||||
case "open":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={state.color}
|
||||
className={className}
|
||||
>
|
||||
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z" />
|
||||
</svg>
|
||||
);
|
||||
case "closed":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={state.color}
|
||||
className={className}
|
||||
>
|
||||
<path d="M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z" />
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z" />
|
||||
</svg>
|
||||
);
|
||||
case "canceled":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
height={size}
|
||||
fill={state.color}
|
||||
className={className}
|
||||
>
|
||||
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as React from "react";
|
||||
import { useTheme } from "styled-components";
|
||||
import { isSafari } from "../../utils/browser";
|
||||
import { BaseIconProps } from ".";
|
||||
|
||||
enum StateType {
|
||||
Triage = "triage",
|
||||
Backlog = "backlog",
|
||||
Unstarted = "unstarted",
|
||||
Started = "started",
|
||||
Completed = "completed",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
export function LinearIssueStatusIcon(props: BaseIconProps) {
|
||||
const theme = useTheme();
|
||||
const { state, size = 16 } = props;
|
||||
const percentage =
|
||||
state.type === StateType.Triage ||
|
||||
state.type === StateType.Backlog ||
|
||||
state.type === StateType.Unstarted
|
||||
? 0
|
||||
: state.type === StateType.Started
|
||||
? state.completionPercentage ?? 0.5
|
||||
: 1;
|
||||
const isTriage = state.type === StateType.Triage;
|
||||
const isBacklog = state.type === StateType.Backlog;
|
||||
const isCompleted = state.type === StateType.Completed;
|
||||
// Due to some rendering issues and differences between browsers, the logical constant 4 in the rendering below
|
||||
// needs to be a bit less to make 50% look like half a circle.
|
||||
const magicFour = isSafari() ? 3.895 : 3.98;
|
||||
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 14 14" fill="none">
|
||||
<circle
|
||||
cx={7}
|
||||
cy={7}
|
||||
r={isTriage ? 3.5 : 6}
|
||||
fill="none"
|
||||
stroke={state.color}
|
||||
strokeWidth={isTriage ? 7 : 1.5}
|
||||
strokeDasharray={isTriage ? "2 0" : isBacklog ? "1.4 1.74" : "3.14 0"}
|
||||
strokeDashoffset={isTriage ? 3.2 : isBacklog ? 0.65 : -0.7}
|
||||
/>
|
||||
<circle
|
||||
cx={7}
|
||||
cy={7}
|
||||
r={percentage === 1 ? 3 : 2}
|
||||
fill="none"
|
||||
stroke={state.color}
|
||||
strokeWidth={percentage === 1 ? 6 : 4}
|
||||
strokeDasharray={`${
|
||||
percentage * Math.PI * (percentage === 1 ? 6 : magicFour)
|
||||
} 100`}
|
||||
strokeDashoffset={0}
|
||||
transform={`rotate(-90 7 7)`}
|
||||
/>
|
||||
{(isTriage || percentage === 1) && (
|
||||
<path
|
||||
className="icon"
|
||||
stroke="none"
|
||||
d={isTriage ? triageIcon : isCompleted ? checkMarkIcon : closeIcon}
|
||||
fill={theme.background}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const checkMarkIcon =
|
||||
"M10.951 4.24896C11.283 4.58091 11.283 5.11909 10.951 5.45104L5.95104 10.451C5.61909 10.783 5.0809 10.783 4.74896 10.451L2.74896 8.45104C2.41701 8.11909 2.41701 7.5809 2.74896 7.24896C3.0809 6.91701 3.61909 6.91701 3.95104 7.24896L5.35 8.64792L9.74896 4.24896C10.0809 3.91701 10.6191 3.91701 10.951 4.24896Z";
|
||||
const triageIcon =
|
||||
"M8.0126 7.98223V9.50781C8.0126 9.92901 8.52329 10.1548 8.85102 9.87854L11.8258 7.37066C12.0581 7.17486 12.0581 6.82507 11.8258 6.62927L8.85102 4.12139C8.52329 3.84509 8.0126 4.07092 8.0126 4.49212V6.01763H5.98739V4.49218C5.98739 4.07098 5.4767 3.84515 5.14897 4.12146L2.17419 6.62933C1.94194 6.82513 1.94194 7.17492 2.17419 7.37072L5.14897 9.8786C5.4767 10.1549 5.98739 9.92907 5.98739 9.50787V7.98223H8.0126Z";
|
||||
const closeIcon =
|
||||
"M3.73657 3.73657C4.05199 3.42114 4.56339 3.42114 4.87881 3.73657L5.93941 4.79716L7 5.85775L9.12117 3.73657C9.4366 3.42114 9.94801 3.42114 10.2634 3.73657C10.5789 4.05199 10.5789 4.56339 10.2634 4.87881L8.14225 7L10.2634 9.12118C10.5789 9.4366 10.5789 9.94801 10.2634 10.2634C9.94801 10.5789 9.4366 10.5789 9.12117 10.2634L7 8.14225L4.87881 10.2634C4.56339 10.5789 4.05199 10.5789 3.73657 10.2634C3.42114 9.94801 3.42114 9.4366 3.73657 9.12118L4.79716 8.06059L5.85775 7L3.73657 4.87881C3.42114 4.56339 3.42114 4.05199 3.73657 3.73657Z";
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
IntegrationService,
|
||||
IssueTrackerIntegrationService,
|
||||
UnfurlResourceType,
|
||||
UnfurlResponse,
|
||||
} from "../../types";
|
||||
import { GitHubIssueStatusIcon } from "./GitHubIssueStatusIcon";
|
||||
import { LinearIssueStatusIcon } from "./LinearIssueStatusIcon";
|
||||
|
||||
export type BaseIconProps = {
|
||||
state: UnfurlResponse[UnfurlResourceType.Issue]["state"];
|
||||
className?: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
type Props = BaseIconProps & {
|
||||
service: IssueTrackerIntegrationService;
|
||||
};
|
||||
|
||||
export function IssueStatusIcon(props: Props) {
|
||||
return (
|
||||
<Icon size={props.size} className={props.className}>
|
||||
{getIcon(props)}
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
|
||||
function getIcon(props: Props) {
|
||||
switch (props.service) {
|
||||
case IntegrationService.GitHub:
|
||||
return <GitHubIssueStatusIcon {...props} />;
|
||||
case IntegrationService.Linear:
|
||||
return <LinearIssueStatusIcon {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const Icon = styled.span<{ size?: number }>`
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
width: ${(props) => props.size ?? 24}px;
|
||||
height: ${(props) => props.size ?? 24}px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { UnfurlResourceType, UnfurlResponse } from "../types";
|
||||
|
||||
type Props = {
|
||||
status: string;
|
||||
color: string;
|
||||
state: UnfurlResponse[UnfurlResourceType.PR]["state"];
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
@@ -11,10 +11,10 @@ type Props = {
|
||||
/**
|
||||
* Issue status icon based on GitHub pull requests, but can be used for any git-style integration.
|
||||
*/
|
||||
export function PullRequestIcon({ size, ...rest }: Props) {
|
||||
export function PullRequestIcon({ size, className, state }: Props) {
|
||||
return (
|
||||
<Icon size={size}>
|
||||
<BaseIcon {...rest} />
|
||||
<Icon size={size} className={className}>
|
||||
<BaseIcon state={state} />
|
||||
</Icon>
|
||||
);
|
||||
}
|
||||
@@ -28,42 +28,32 @@ const Icon = styled.span<{ size?: number }>`
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
function BaseIcon(props: Props) {
|
||||
switch (props.status) {
|
||||
function BaseIcon({ state }: Pick<Props, "state">) {
|
||||
switch (state.name) {
|
||||
case "open":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill={props.color}
|
||||
className={props.className}
|
||||
>
|
||||
<path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z" />
|
||||
<svg viewBox="0 0 16 16" fill={state.color}>
|
||||
{state.draft ? (
|
||||
<path d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 14a2.25 2.25 0 1 1 0-4.5 2.25 2.25 0 0 1 0 4.5ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM14 7.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm0-4.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z" />
|
||||
) : (
|
||||
<path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z" />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
case "merged":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill={props.color}
|
||||
className={props.className}
|
||||
>
|
||||
<svg viewBox="0 0 16 16" fill={state.color}>
|
||||
<path d="M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z" />
|
||||
</svg>
|
||||
);
|
||||
case "closed":
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
height="16"
|
||||
fill={props.color}
|
||||
className={props.className}
|
||||
>
|
||||
<path d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" />
|
||||
<svg viewBox="0 0 16 16" fill={state.color}>
|
||||
{state.draft ? (
|
||||
<path d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" />
|
||||
) : (
|
||||
<path d="M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { Backticks } from "../../components/Backticks";
|
||||
import Flex from "../../components/Flex";
|
||||
import Icon from "../../components/Icon";
|
||||
import { IssueStatusIcon } from "../../components/IssueStatusIcon";
|
||||
@@ -19,10 +20,11 @@ import Text from "../../components/Text";
|
||||
import useIsMounted from "../../hooks/useIsMounted";
|
||||
import useStores from "../../hooks/useStores";
|
||||
import theme from "../../styles/theme";
|
||||
import type {
|
||||
JSONValue,
|
||||
UnfurlResourceType,
|
||||
UnfurlResponse,
|
||||
import {
|
||||
IntegrationService,
|
||||
type JSONValue,
|
||||
type UnfurlResourceType,
|
||||
type UnfurlResponse,
|
||||
} from "../../types";
|
||||
import { cn } from "../styles/utils";
|
||||
import { ComponentProps } from "../types";
|
||||
@@ -187,6 +189,12 @@ export const MentionIssue = observer((props: IssuePrProps) => {
|
||||
|
||||
const issue = unfurl as UnfurlResponse[UnfurlResourceType.Issue];
|
||||
|
||||
const url = new URL(issue.url);
|
||||
const service =
|
||||
url.hostname === "github.com"
|
||||
? IntegrationService.GitHub
|
||||
: IntegrationService.Linear;
|
||||
|
||||
return (
|
||||
<a
|
||||
{...attrs}
|
||||
@@ -198,13 +206,11 @@ export const MentionIssue = observer((props: IssuePrProps) => {
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<Flex align="center" gap={6}>
|
||||
<IssueStatusIcon
|
||||
size={14}
|
||||
status={issue.state.name}
|
||||
color={issue.state.color}
|
||||
/>
|
||||
<IssueStatusIcon size={14} service={service} state={issue.state} />
|
||||
<Flex align="center" gap={4}>
|
||||
<Text>{issue.title}</Text>
|
||||
<Text>
|
||||
<Backticks content={issue.title} />
|
||||
</Text>
|
||||
<Text type="tertiary">{issue.id}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -273,13 +279,11 @@ export const MentionPullRequest = observer((props: IssuePrProps) => {
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<Flex align="center" gap={6}>
|
||||
<PullRequestIcon
|
||||
size={14}
|
||||
status={pullRequest.state.name}
|
||||
color={pullRequest.state.color}
|
||||
/>
|
||||
<PullRequestIcon size={14} state={pullRequest.state} />
|
||||
<Flex align="center" gap={4}>
|
||||
<Text>{pullRequest.title}</Text>
|
||||
<Text>
|
||||
<Backticks content={pullRequest.title} />
|
||||
</Text>
|
||||
<Text type="tertiary">{pullRequest.id}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -1091,6 +1091,10 @@ p a {
|
||||
}
|
||||
}
|
||||
|
||||
.heading-content a {
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: ${props.theme.link};
|
||||
cursor: pointer;
|
||||
|
||||
@@ -24,7 +24,7 @@ export type MenuItem = {
|
||||
name?: string;
|
||||
title?: string;
|
||||
section?: Section;
|
||||
subtitle?: string;
|
||||
subtitle?: React.ReactNode;
|
||||
shortcut?: string;
|
||||
keywords?: string;
|
||||
tooltip?: string;
|
||||
|
||||
@@ -1089,6 +1089,10 @@
|
||||
"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",
|
||||
"Create a \"Web\" stream in your Google Analytics admin dashboard and copy the measurement ID from the generated code snippet to install.": "Create a \"Web\" stream in your Google Analytics admin dashboard and copy the measurement ID from the generated code snippet to install.",
|
||||
"Whoops, you need to accept the permissions in Linear to connect {{appName}} to your workspace. Try again?": "Whoops, you need to accept the permissions in Linear to connect {{appName}} to your workspace. Try again?",
|
||||
"Enable previews of Linear issues in documents by connecting a Linear workspace to {appName}.": "Enable previews of Linear issues in documents by connecting a Linear workspace to {appName}.",
|
||||
"Disconnecting will prevent previewing Linear links from this workspace in documents. Are you sure?": "Disconnecting will prevent previewing Linear links from this workspace in documents. Are you sure?",
|
||||
"The Linear integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.": "The Linear integration is currently disabled. Please set the associated environment variables and restart the server to enable the integration.",
|
||||
"Configure a Matomo installation to send views and analytics from the workspace to your own Matomo instance.": "Configure a Matomo installation to send views and analytics from the workspace to your own Matomo instance.",
|
||||
"Instance URL": "Instance URL",
|
||||
"The URL of your Matomo instance. If you are using Matomo Cloud it will end in matomo.cloud/": "The URL of your Matomo instance. If you are using Matomo Cloud it will end in matomo.cloud/",
|
||||
|
||||
+14
-4
@@ -117,6 +117,7 @@ export enum IntegrationService {
|
||||
Matomo = "matomo",
|
||||
Umami = "umami",
|
||||
GitHub = "github",
|
||||
Linear = "linear",
|
||||
Notion = "notion",
|
||||
}
|
||||
|
||||
@@ -131,11 +132,12 @@ export const ImportableIntegrationService = {
|
||||
|
||||
export type IssueTrackerIntegrationService = Extract<
|
||||
IntegrationService,
|
||||
IntegrationService.GitHub
|
||||
IntegrationService.GitHub | IntegrationService.Linear
|
||||
>;
|
||||
|
||||
export const IssueTrackerIntegrationService = {
|
||||
GitHub: IntegrationService.GitHub,
|
||||
Linear: IntegrationService.Linear,
|
||||
} as const;
|
||||
|
||||
export type UserCreatableIntegrationService = Extract<
|
||||
@@ -169,13 +171,16 @@ export enum DocumentPermission {
|
||||
|
||||
export type IntegrationSettings<T> = T extends IntegrationType.Embed
|
||||
? {
|
||||
url: string;
|
||||
url?: string;
|
||||
github?: {
|
||||
installation: {
|
||||
id: number;
|
||||
account: { id: number; name: string; avatarUrl: string };
|
||||
};
|
||||
};
|
||||
linear?: {
|
||||
workspace: { id: string; name: string; key: string; logoUrl?: string };
|
||||
};
|
||||
}
|
||||
: T extends IntegrationType.Analytics
|
||||
? { measurementId: string; instanceUrl?: string; scriptName?: string }
|
||||
@@ -433,7 +438,12 @@ export type UnfurlResponse = {
|
||||
/** Issue's labels */
|
||||
labels: Array<{ name: string; color: string }>;
|
||||
/** Issue's status */
|
||||
state: { name: string; color: string };
|
||||
state: {
|
||||
type?: string;
|
||||
name: string;
|
||||
color: string;
|
||||
completionPercentage?: number;
|
||||
};
|
||||
/** Issue's creation time */
|
||||
createdAt: string;
|
||||
};
|
||||
@@ -451,7 +461,7 @@ export type UnfurlResponse = {
|
||||
/** Pull Request author */
|
||||
author: { name: string; avatarUrl: string };
|
||||
/** Pull Request status */
|
||||
state: { name: string; color: string };
|
||||
state: { name: string; color: string; draft?: boolean };
|
||||
/** Pull Request creation time */
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
@@ -33,6 +33,18 @@ export function isWindows(): boolean {
|
||||
return window.navigator.platform === "Win32";
|
||||
}
|
||||
|
||||
export function isSafari(): boolean {
|
||||
if (!isBrowser) {
|
||||
return false;
|
||||
}
|
||||
const userAgent = window.navigator.userAgent;
|
||||
return (
|
||||
userAgent.includes("Safari") &&
|
||||
!userAgent.includes("Chrome") &&
|
||||
!userAgent.includes("Chromium")
|
||||
);
|
||||
}
|
||||
|
||||
let supportsPassive = false;
|
||||
|
||||
try {
|
||||
|
||||
@@ -117,6 +117,7 @@ export const RESERVED_SUBDOMAINS = [
|
||||
"localhost",
|
||||
"mail",
|
||||
"marketing",
|
||||
"mcp",
|
||||
"mobile",
|
||||
"multiplayer",
|
||||
"new",
|
||||
|
||||
+10
-2
@@ -3,12 +3,12 @@ import path from "path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import browserslistToEsbuild from "browserslist-to-esbuild";
|
||||
import webpackStats from "rollup-plugin-webpack-stats";
|
||||
import { CommonServerOptions, defineConfig } from "vite";
|
||||
import { ServerOptions, defineConfig } from "vite";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||
import environment from "./server/utils/environment";
|
||||
|
||||
let httpsConfig: CommonServerOptions["https"] | undefined;
|
||||
let httpsConfig: ServerOptions["https"] | undefined;
|
||||
let host: string | undefined;
|
||||
|
||||
if (environment.NODE_ENV === "development") {
|
||||
@@ -145,6 +145,7 @@ export default () =>
|
||||
},
|
||||
}),
|
||||
// Generate a stats.json file for webpack that will be consumed by RelativeCI
|
||||
// @ts-expect-error Type mismatch with latest versions but Plugin runs without issue
|
||||
webpackStats(),
|
||||
],
|
||||
optimizeDeps: {
|
||||
@@ -175,6 +176,13 @@ export default () =>
|
||||
keep_fnames: true,
|
||||
},
|
||||
rollupOptions: {
|
||||
onwarn(warning, warn) {
|
||||
// Suppress noisy warnings about module-level directives, e.g. "use client"
|
||||
if (warning.code === "MODULE_LEVEL_DIRECTIVE") {
|
||||
return;
|
||||
}
|
||||
warn(warning);
|
||||
},
|
||||
input: {
|
||||
index: "./app/index.tsx",
|
||||
},
|
||||
|
||||
@@ -1978,230 +1978,240 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
|
||||
integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
|
||||
|
||||
"@esbuild/aix-ppc64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
|
||||
integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
|
||||
"@esbuild/aix-ppc64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz#014180d9a149cffd95aaeead37179433f5ea5437"
|
||||
integrity sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==
|
||||
|
||||
"@esbuild/android-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
|
||||
integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
|
||||
|
||||
"@esbuild/android-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
|
||||
integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
|
||||
"@esbuild/android-arm64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz#649e47e04ddb24a27dc05c395724bc5f4c55cbfe"
|
||||
integrity sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==
|
||||
|
||||
"@esbuild/android-arm@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
|
||||
integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
|
||||
|
||||
"@esbuild/android-arm@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
|
||||
integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
|
||||
"@esbuild/android-arm@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.3.tgz#8a0f719c8dc28a4a6567ef7328c36ea85f568ff4"
|
||||
integrity sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==
|
||||
|
||||
"@esbuild/android-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
|
||||
integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
|
||||
|
||||
"@esbuild/android-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
|
||||
integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
|
||||
"@esbuild/android-x64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.3.tgz#e2ab182d1fd06da9bef0784a13c28a7602d78009"
|
||||
integrity sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==
|
||||
|
||||
"@esbuild/darwin-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb"
|
||||
integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
|
||||
|
||||
"@esbuild/darwin-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
|
||||
integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
|
||||
"@esbuild/darwin-arm64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz#c7f3166fcece4d158a73dcfe71b2672ca0b1668b"
|
||||
integrity sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==
|
||||
|
||||
"@esbuild/darwin-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
|
||||
integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
|
||||
|
||||
"@esbuild/darwin-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
|
||||
integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
|
||||
"@esbuild/darwin-x64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz#d8c5342ec1a4bf4b1915643dfe031ba4b173a87a"
|
||||
integrity sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
|
||||
integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
|
||||
integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
|
||||
"@esbuild/freebsd-arm64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz#9f7d789e2eb7747d4868817417cc968ffa84f35b"
|
||||
integrity sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==
|
||||
|
||||
"@esbuild/freebsd-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
|
||||
integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
|
||||
|
||||
"@esbuild/freebsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
|
||||
integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
|
||||
"@esbuild/freebsd-x64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz#8ad35c51d084184a8e9e76bb4356e95350a64709"
|
||||
integrity sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==
|
||||
|
||||
"@esbuild/linux-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
|
||||
integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
|
||||
|
||||
"@esbuild/linux-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
|
||||
integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
|
||||
"@esbuild/linux-arm64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz#3af0da3d9186092a9edd4e28fa342f57d9e3cd30"
|
||||
integrity sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==
|
||||
|
||||
"@esbuild/linux-arm@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
|
||||
integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
|
||||
|
||||
"@esbuild/linux-arm@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
|
||||
integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
|
||||
"@esbuild/linux-arm@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz#e91cafa95e4474b3ae3d54da12e006b782e57225"
|
||||
integrity sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==
|
||||
|
||||
"@esbuild/linux-ia32@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
|
||||
integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
|
||||
|
||||
"@esbuild/linux-ia32@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
|
||||
integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
|
||||
"@esbuild/linux-ia32@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz#81025732d85b68ee510161b94acdf7e3007ea177"
|
||||
integrity sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==
|
||||
|
||||
"@esbuild/linux-loong64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
|
||||
integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
|
||||
|
||||
"@esbuild/linux-loong64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
|
||||
integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
|
||||
"@esbuild/linux-loong64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz#3c744e4c8d5e1148cbe60a71a11b58ed8ee5deb8"
|
||||
integrity sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==
|
||||
|
||||
"@esbuild/linux-mips64el@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
|
||||
integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
|
||||
|
||||
"@esbuild/linux-mips64el@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
|
||||
integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
|
||||
"@esbuild/linux-mips64el@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz#1dfe2a5d63702db9034cc6b10b3087cc0424ec26"
|
||||
integrity sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==
|
||||
|
||||
"@esbuild/linux-ppc64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
|
||||
integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
|
||||
|
||||
"@esbuild/linux-ppc64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
|
||||
integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
|
||||
"@esbuild/linux-ppc64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz#2e85d9764c04a1ebb346dc0813ea05952c9a5c56"
|
||||
integrity sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==
|
||||
|
||||
"@esbuild/linux-riscv64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
|
||||
integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
|
||||
|
||||
"@esbuild/linux-riscv64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
|
||||
integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
|
||||
"@esbuild/linux-riscv64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz#a9ea3334556b09f85ccbfead58c803d305092415"
|
||||
integrity sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==
|
||||
|
||||
"@esbuild/linux-s390x@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
|
||||
integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
|
||||
|
||||
"@esbuild/linux-s390x@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
|
||||
integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
|
||||
"@esbuild/linux-s390x@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz#f6a7cb67969222b200974de58f105dfe8e99448d"
|
||||
integrity sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==
|
||||
|
||||
"@esbuild/linux-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
|
||||
integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
|
||||
|
||||
"@esbuild/linux-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
|
||||
integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
|
||||
"@esbuild/linux-x64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz#a237d3578ecdd184a3066b1f425e314ade0f8033"
|
||||
integrity sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz#4c15c68d8149614ddb6a56f9c85ae62ccca08259"
|
||||
integrity sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==
|
||||
|
||||
"@esbuild/netbsd-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
|
||||
integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
|
||||
|
||||
"@esbuild/netbsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
|
||||
integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
|
||||
"@esbuild/netbsd-x64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz#12f6856f8c54c2d7d0a8a64a9711c01a743878d5"
|
||||
integrity sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz#ca078dad4a34df192c60233b058db2ca3d94bc5c"
|
||||
integrity sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
|
||||
integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
|
||||
|
||||
"@esbuild/openbsd-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
|
||||
integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
|
||||
"@esbuild/openbsd-x64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz#c9178adb60e140e03a881d0791248489c79f95b2"
|
||||
integrity sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==
|
||||
|
||||
"@esbuild/sunos-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
|
||||
integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
|
||||
|
||||
"@esbuild/sunos-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
|
||||
integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
|
||||
"@esbuild/sunos-x64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz#03765eb6d4214ff27e5230af779e80790d1ee09f"
|
||||
integrity sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==
|
||||
|
||||
"@esbuild/win32-arm64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
|
||||
integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
|
||||
|
||||
"@esbuild/win32-arm64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
|
||||
integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
|
||||
"@esbuild/win32-arm64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz#f1c867bd1730a9b8dfc461785ec6462e349411ea"
|
||||
integrity sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
|
||||
integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
|
||||
integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
|
||||
"@esbuild/win32-ia32@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz#77491f59ef6c9ddf41df70670d5678beb3acc322"
|
||||
integrity sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==
|
||||
|
||||
"@esbuild/win32-x64@0.20.2":
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
|
||||
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
|
||||
|
||||
"@esbuild/win32-x64@0.21.5":
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
|
||||
integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
|
||||
"@esbuild/win32-x64@0.25.3":
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz#b17a2171f9074df9e91bfb07ef99a892ac06412a"
|
||||
integrity sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.4.0"
|
||||
@@ -2358,6 +2368,11 @@
|
||||
dependencies:
|
||||
warning "^4.0.3"
|
||||
|
||||
"@graphql-typed-document-node/core@^3.1.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||
|
||||
"@gulpjs/to-absolute-glob@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz#1fc2460d3953e1d9b9f2dfdb4bcc99da4710c021"
|
||||
@@ -2796,6 +2811,15 @@
|
||||
resolved "https://registry.yarnpkg.com/@lifeomic/attempt/-/attempt-3.0.3.tgz#e742a5b85eb673e2f1746b0f39cb932cbc6145bb"
|
||||
integrity "sha1-50KluF62c+LxdGsPOcuTLLxhRbs= sha512-GlM2AbzrErd/TmLL3E8hAHmb5Q7VhDJp35vIbyPVA5Rz55LZuRr8pwL3qrwwkVNo05gMX1J44gURKb4MHQZo7w=="
|
||||
|
||||
"@linear/sdk@^39.0.0":
|
||||
version "39.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@linear/sdk/-/sdk-39.0.0.tgz#6238a04e4123386d5d873014afe5cafc40e6493d"
|
||||
integrity sha512-GFZakeWsLFXyJjhrjt/VOW/lxMX9obOVw7ItypYWDh8IEzIui6gVN3EBof5A7zKqpdc6fsZEPYCvrI5FVet9+Q==
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core" "^3.1.0"
|
||||
graphql "^15.4.0"
|
||||
isomorphic-unfetch "^3.1.0"
|
||||
|
||||
"@mermaid-js/parser@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-0.3.0.tgz#7a28714599f692f93df130b299fa1aadc9f9c8ab"
|
||||
@@ -3804,65 +3828,105 @@
|
||||
estree-walker "^2.0.2"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.5.1.tgz#11bea66c013e5a88a0f53f315b2d49cfd663584e"
|
||||
integrity "sha1-Eb6mbAE+Woig9T8xWy1Jz9ZjWE4= sha512-YaN43wTyEBaMqLDYeze+gQ4ZrW5RbTEGtT5o1GVDkhpdNcsLTnLRcLccvwy3E9wiDKWg9RIhuoy3JQKDRBfaZA=="
|
||||
"@rollup/rollup-android-arm-eabi@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz#d964ee8ce4d18acf9358f96adc408689b6e27fe3"
|
||||
integrity sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.5.1.tgz#cae505492204c018d1c6335f3b845319b15dc669"
|
||||
integrity "sha1-yuUFSSIEwBjRxjNfO4RTGbFdxmk= sha512-n1bX+LCGlQVuPlCofO0zOKe1b2XkFozAVRoczT+yxWZPGnkEAKTTYVOGZz8N4sKuBnKMxDbfhUsB1uwYdup/sw=="
|
||||
"@rollup/rollup-android-arm64@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz#9b5e130ecc32a5fc1e96c09ff371743ee71a62d3"
|
||||
integrity sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.5.1.tgz#5442ca442fca1a166e41e03b983b2f3e3235c17c"
|
||||
integrity "sha1-VELKRC/KGhZuQeA7mDsvPjI1wXw= sha512-QqJBumdvfBqBBmyGHlKxje+iowZwrHna7pokj/Go3dV1PJekSKfmjKrjKQ/e6ESTGhkfPNLq3VXdYLAc+UtAQw=="
|
||||
"@rollup/rollup-darwin-arm64@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz#ef439182c739b20b3c4398cfc03e3c1249ac8903"
|
||||
integrity sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.5.1.tgz#e5140b0aaab0ea1424a4c8a1e76442105866290c"
|
||||
integrity "sha1-5RQLCqqw6hQkpMih52RCEFhmKQw= sha512-RrkDNkR/P5AEQSPkxQPmd2ri8WTjSl0RYmuFOiEABkEY/FSg0a4riihWQGKDJ4LnV9gigWZlTMx2DtFGzUrYQw=="
|
||||
"@rollup/rollup-darwin-x64@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz#d7380c1531ab0420ca3be16f17018ef72dd3d504"
|
||||
integrity sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.5.1.tgz#501a336b1dc4cb350a1b8b4e24bba4d049902d74"
|
||||
integrity "sha1-UBozax3EyzUKG4tOJLuk0EmQLXQ= sha512-ZFPxvUZmE+fkB/8D9y/SWl/XaDzNSaxd1TJUSE27XAKlRpQ2VNce/86bGd9mEUgL3qrvjJ9XTGwoX0BrJkYK/A=="
|
||||
"@rollup/rollup-freebsd-arm64@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz#cbcbd7248823c6b430ce543c59906dd3c6df0936"
|
||||
integrity sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.5.1.tgz#bdb0c8552d167477d2624a4a6df0f71f128dc546"
|
||||
integrity "sha1-vbDIVS0WdHfSYkpKbfD3HxKNxUY= sha512-FEuAjzVIld5WVhu+M2OewLmjmbXWd3q7Zcx+Rwy4QObQCqfblriDMMS7p7+pwgjZoo9BLkP3wa9uglQXzsB9ww=="
|
||||
"@rollup/rollup-freebsd-x64@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz#96bf6ff875bab5219c3472c95fa6eb992586a93b"
|
||||
integrity sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.5.1.tgz#f7e8036c2f771bb366ca0d8c79d2132cffb1d295"
|
||||
integrity "sha1-9+gDbC93G7Nmyg2MedITLP+x0pU= sha512-f5Gs8WQixqGRtI0Iq/cMqvFYmgFzMinuJO24KRfnv7Ohi/HQclwrBCYkzQu1XfLEEt3DZyvveq9HWo4bLJf1Lw=="
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz#d80cd62ce6d40f8e611008d8dbf03b5e6bbf009c"
|
||||
integrity sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.5.1.tgz#079ca543a649b1dcf9832a34dff94ebb46c96745"
|
||||
integrity "sha1-B5ylQ6ZJsdz5gyo03/lOu0bJZ0U= sha512-CWPkPGrFfN2vj3mw+S7A/4ZaU3rTV7AkXUr08W9lNP+UzOvKLVf34tWCqrKrfwQ0NTk5GFqUr2XGpeR2p6R4gw=="
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz#75440cfc1e8d0f87a239b4c31dfeaf4719b656b7"
|
||||
integrity sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.5.1.tgz#9cc8c0ea1c0e0d3b18888d5b2fd51ef6c9b42481"
|
||||
integrity "sha1-nMjA6hwODTsYiI1bL9Ue9sm0JIE= sha512-ZRETMFA0uVukUC9u31Ed1nx++29073goCxZtmZARwk5aF/ltuENaeTtRVsSQzFlzdd4J6L3qUm+EW8cbGt0CKQ=="
|
||||
"@rollup/rollup-linux-arm64-gnu@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz#ac527485ecbb619247fb08253ec8c551a0712e7c"
|
||||
integrity sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.5.1.tgz#df70597f6639549e79f0801004525d6a7a0075e4"
|
||||
integrity "sha1-33BZf2Y5VJ558IAQBFJdanoAdeQ= sha512-ihqfNJNb2XtoZMSCPeoo0cYMgU04ksyFIoOw5S0JUVbOhafLot+KD82vpKXOurE2+9o/awrqIxku9MRR9hozHQ=="
|
||||
"@rollup/rollup-linux-arm64-musl@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz#74d2b5cb11cf714cd7d1682e7c8b39140e908552"
|
||||
integrity sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.5.1.tgz#6f9e8b30a4d6b5c564bfe55cdf44a5b493139838"
|
||||
integrity "sha1-b56LMKTWtcVkv+Vc30SltJMTmDg= sha512-zK9MRpC8946lQ9ypFn4gLpdwr5a01aQ/odiIJeL9EbgZDMgbZjjT/XzTqJvDfTmnE1kHdbG20sAeNlpc91/wbg=="
|
||||
"@rollup/rollup-linux-loongarch64-gnu@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz#a0a310e51da0b5fea0e944b0abd4be899819aef6"
|
||||
integrity sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.5.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.5.1.tgz#0e79117bacb5817ff9a88ab19cb59df839638d6d"
|
||||
integrity "sha1-DnkRe6y1gX/5qIqxnLWd+DljjW0= sha512-5I3Nz4Sb9TYOtkRwlH0ow+BhMH2vnh38tZ4J4mggE48M/YyJyp/0sPSxhw1UeS1+oBgQ8q7maFtSeKpeRJu41Q=="
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz#4077e2862b0ac9f61916d6b474d988171bd43b83"
|
||||
integrity sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz#5812a1a7a2f9581cbe12597307cc7ba3321cf2f3"
|
||||
integrity sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz#973aaaf4adef4531375c36616de4e01647f90039"
|
||||
integrity sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz#9bad59e907ba5bfcf3e9dbd0247dfe583112f70b"
|
||||
integrity sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz#68b045a720bd9b4d905f462b997590c2190a6de0"
|
||||
integrity sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz#8e703e2c2ad19ba7b2cb3d8c3a4ad11d4ee3a282"
|
||||
integrity sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz#c5bee19fa670ff5da5f066be6a58b4568e9c650b"
|
||||
integrity sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz#846e02c17044bd922f6f483a3b4d36aac6e2b921"
|
||||
integrity sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.40.0":
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz#fd92d31a2931483c25677b9c6698106490cbbc76"
|
||||
integrity sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==
|
||||
|
||||
"@rtsao/scc@^1.1.0":
|
||||
version "1.1.0"
|
||||
@@ -4969,10 +5033,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.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
"@types/estree@1.0.7", "@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/express-serve-static-core@^4.17.18":
|
||||
version "4.17.24"
|
||||
@@ -8536,34 +8600,36 @@ esbuild@^0.20.1:
|
||||
"@esbuild/win32-ia32" "0.20.2"
|
||||
"@esbuild/win32-x64" "0.20.2"
|
||||
|
||||
esbuild@^0.21.3:
|
||||
version "0.21.5"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
|
||||
integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
|
||||
esbuild@^0.25.0:
|
||||
version "0.25.3"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.3.tgz#371f7cb41283e5b2191a96047a7a89562965a285"
|
||||
integrity sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.21.5"
|
||||
"@esbuild/android-arm" "0.21.5"
|
||||
"@esbuild/android-arm64" "0.21.5"
|
||||
"@esbuild/android-x64" "0.21.5"
|
||||
"@esbuild/darwin-arm64" "0.21.5"
|
||||
"@esbuild/darwin-x64" "0.21.5"
|
||||
"@esbuild/freebsd-arm64" "0.21.5"
|
||||
"@esbuild/freebsd-x64" "0.21.5"
|
||||
"@esbuild/linux-arm" "0.21.5"
|
||||
"@esbuild/linux-arm64" "0.21.5"
|
||||
"@esbuild/linux-ia32" "0.21.5"
|
||||
"@esbuild/linux-loong64" "0.21.5"
|
||||
"@esbuild/linux-mips64el" "0.21.5"
|
||||
"@esbuild/linux-ppc64" "0.21.5"
|
||||
"@esbuild/linux-riscv64" "0.21.5"
|
||||
"@esbuild/linux-s390x" "0.21.5"
|
||||
"@esbuild/linux-x64" "0.21.5"
|
||||
"@esbuild/netbsd-x64" "0.21.5"
|
||||
"@esbuild/openbsd-x64" "0.21.5"
|
||||
"@esbuild/sunos-x64" "0.21.5"
|
||||
"@esbuild/win32-arm64" "0.21.5"
|
||||
"@esbuild/win32-ia32" "0.21.5"
|
||||
"@esbuild/win32-x64" "0.21.5"
|
||||
"@esbuild/aix-ppc64" "0.25.3"
|
||||
"@esbuild/android-arm" "0.25.3"
|
||||
"@esbuild/android-arm64" "0.25.3"
|
||||
"@esbuild/android-x64" "0.25.3"
|
||||
"@esbuild/darwin-arm64" "0.25.3"
|
||||
"@esbuild/darwin-x64" "0.25.3"
|
||||
"@esbuild/freebsd-arm64" "0.25.3"
|
||||
"@esbuild/freebsd-x64" "0.25.3"
|
||||
"@esbuild/linux-arm" "0.25.3"
|
||||
"@esbuild/linux-arm64" "0.25.3"
|
||||
"@esbuild/linux-ia32" "0.25.3"
|
||||
"@esbuild/linux-loong64" "0.25.3"
|
||||
"@esbuild/linux-mips64el" "0.25.3"
|
||||
"@esbuild/linux-ppc64" "0.25.3"
|
||||
"@esbuild/linux-riscv64" "0.25.3"
|
||||
"@esbuild/linux-s390x" "0.25.3"
|
||||
"@esbuild/linux-x64" "0.25.3"
|
||||
"@esbuild/netbsd-arm64" "0.25.3"
|
||||
"@esbuild/netbsd-x64" "0.25.3"
|
||||
"@esbuild/openbsd-arm64" "0.25.3"
|
||||
"@esbuild/openbsd-x64" "0.25.3"
|
||||
"@esbuild/sunos-x64" "0.25.3"
|
||||
"@esbuild/win32-arm64" "0.25.3"
|
||||
"@esbuild/win32-ia32" "0.25.3"
|
||||
"@esbuild/win32-x64" "0.25.3"
|
||||
|
||||
escalade@^3.1.1, escalade@^3.2.0:
|
||||
version "3.2.0"
|
||||
@@ -9032,10 +9098,10 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fdir@^6.4.2:
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72"
|
||||
integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==
|
||||
fdir@^6.4.4:
|
||||
version "6.4.4"
|
||||
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9"
|
||||
integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==
|
||||
|
||||
fecha@^4.2.0:
|
||||
version "4.2.1"
|
||||
@@ -9520,6 +9586,11 @@ graphemer@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
integrity "sha1-+y8dVeDjoYSa7/yQxPoN1ToOZsY= sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
|
||||
|
||||
graphql@^15.4.0:
|
||||
version "15.10.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.10.1.tgz#e9ff3bb928749275477f748b14aa5c30dcad6f2f"
|
||||
integrity sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==
|
||||
|
||||
gulp-sort@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/gulp-sort/-/gulp-sort-2.0.0.tgz#c6762a2f1f0de0a3fc595a21599d3fac8dba1aca"
|
||||
@@ -10381,6 +10452,14 @@ isexe@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
|
||||
isomorphic-unfetch@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f"
|
||||
integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==
|
||||
dependencies:
|
||||
node-fetch "^2.6.1"
|
||||
unfetch "^4.2.0"
|
||||
|
||||
isomorphic-ws@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
|
||||
@@ -12085,10 +12164,10 @@ mz@^2.7.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nanoid@^3.3.7:
|
||||
version "3.3.8"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||
nanoid@^3.3.8:
|
||||
version "3.3.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -12167,10 +12246,10 @@ nodemailer@^6.10.0:
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.0.tgz#1f24c9de94ad79c6206f66d132776b6503003912"
|
||||
integrity sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==
|
||||
|
||||
nodemon@^3.1.9:
|
||||
version "3.1.9"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.9.tgz#df502cdc3b120e1c3c0c6e4152349019efa7387b"
|
||||
integrity sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==
|
||||
nodemon@^3.1.10:
|
||||
version "3.1.10"
|
||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1"
|
||||
integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==
|
||||
dependencies:
|
||||
chokidar "^3.5.2"
|
||||
debug "^4"
|
||||
@@ -12751,30 +12830,30 @@ performance-now@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
integrity "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
|
||||
|
||||
pg-cloudflare@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98"
|
||||
integrity "sha1-5tWDMBWxcOI66Bnoxdfq7bRyypg= sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="
|
||||
pg-cloudflare@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz#2e3649c38a7a9c74a7e5327c8098a2fd9af595bd"
|
||||
integrity sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==
|
||||
|
||||
pg-connection-string@^2.6.1, pg-connection-string@^2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37"
|
||||
integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==
|
||||
pg-connection-string@^2.6.1, pg-connection-string@^2.8.5:
|
||||
version "2.8.5"
|
||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.8.5.tgz#82cefd0269cb64a09603342d9b69e8392e6eb6cd"
|
||||
integrity sha512-Ni8FuZ8yAF+sWZzojvtLE2b03cqjO5jNULcHFfM9ZZ0/JXrgom5pBREbtnAw7oxsxJqHw9Nz/XWORUEL3/IFow==
|
||||
|
||||
pg-int8@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
|
||||
integrity "sha1-lDvUY79bcbQXARX4D478mgwOt4w= sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
|
||||
|
||||
pg-pool@^3.8.0:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.8.0.tgz#e6bce7fc4506a8d6106551363fc5283e5445b776"
|
||||
integrity sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==
|
||||
pg-pool@^3.9.6:
|
||||
version "3.9.6"
|
||||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.9.6.tgz#c6fde89dee615d6c262724e68a3a37e9593157fc"
|
||||
integrity sha512-rFen0G7adh1YmgvrmE5IPIqbb+IgEzENUm+tzm6MLLDSlPRoZVhzU1WdML9PV2W5GOdRA9qBKURlbt1OsXOsPw==
|
||||
|
||||
pg-protocol@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.8.0.tgz#c707101dd07813868035a44571488e4b98639d48"
|
||||
integrity sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==
|
||||
pg-protocol@^1.9.5:
|
||||
version "1.9.5"
|
||||
resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.9.5.tgz#e544eff37d6ab79c26281d7c0b59ac9be4862686"
|
||||
integrity sha512-DYTWtWpfd5FOro3UnAfwvhD8jh59r2ig8bPtc9H8Ds7MscE/9NYruUQWFAOuraRl29jwcT2kyMFQ3MxeaVjUhg==
|
||||
|
||||
pg-tsquery@^8.4.2:
|
||||
version "8.4.2"
|
||||
@@ -12792,18 +12871,18 @@ pg-types@^2.1.0:
|
||||
postgres-date "~1.0.4"
|
||||
postgres-interval "^1.1.0"
|
||||
|
||||
pg@^8.14.1:
|
||||
version "8.14.1"
|
||||
resolved "https://registry.yarnpkg.com/pg/-/pg-8.14.1.tgz#2e3d1f287b64797cdfc8d1ba000f61a7ff8d66ed"
|
||||
integrity sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==
|
||||
pg@^8.15.6:
|
||||
version "8.15.6"
|
||||
resolved "https://registry.yarnpkg.com/pg/-/pg-8.15.6.tgz#2a28e98fb6cab18b886ce58b2c184d712a94880a"
|
||||
integrity sha512-yvao7YI3GdmmrslNVsZgx9PfntfWrnXwtR+K/DjI0I/sTKif4Z623um+sjVZ1hk5670B+ODjvHDAckKdjmPTsg==
|
||||
dependencies:
|
||||
pg-connection-string "^2.7.0"
|
||||
pg-pool "^3.8.0"
|
||||
pg-protocol "^1.8.0"
|
||||
pg-connection-string "^2.8.5"
|
||||
pg-pool "^3.9.6"
|
||||
pg-protocol "^1.9.5"
|
||||
pg-types "^2.1.0"
|
||||
pgpass "1.x"
|
||||
optionalDependencies:
|
||||
pg-cloudflare "^1.1.1"
|
||||
pg-cloudflare "^1.2.5"
|
||||
|
||||
pgpass@1.x:
|
||||
version "1.0.4"
|
||||
@@ -12812,7 +12891,7 @@ pgpass@1.x:
|
||||
dependencies:
|
||||
split2 "^3.1.1"
|
||||
|
||||
picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1:
|
||||
picocolors@^1.0.0, picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
@@ -12922,13 +13001,13 @@ postcss-value-parser@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity "sha1-cjwJkgg2um0+WvAZ+SvAlxwC5RQ= sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
|
||||
postcss@^8.4.43:
|
||||
version "8.4.47"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
|
||||
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
|
||||
postcss@^8.5.3:
|
||||
version "8.5.3"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb"
|
||||
integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.1.0"
|
||||
nanoid "^3.3.8"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
postgres-array@~2.0.0:
|
||||
@@ -13442,10 +13521,10 @@ react-merge-refs@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-2.1.1.tgz#e46763f8f1b881c0226ee54a1a2a10ffefba0233"
|
||||
integrity sha512-jLQXJ/URln51zskhgppGJ2ub7b2WFKGq3cl3NYKtlHoTG+dN2q7EzWrn3hN3EgPsTMvpR9tpq5ijdp7YwFZkag==
|
||||
|
||||
react-portal@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.2.tgz#bff1e024147d6041ba8c530ffc99d4c8248f49fa"
|
||||
integrity "sha1-v/HgJBR9YEG6jFMP/JnUyCSPSfo= sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q=="
|
||||
react-portal@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.3.0.tgz#92ca3492b1309883134f317a6aa88004534c860f"
|
||||
integrity sha512-qs/2uKq1ifB3J1+K8ExfgUvCDZqlqCkfOEhqTELEDTfosloKiuzOzc7hl7IQ/7nohiFZD41BUYU0boAsIsGYHw==
|
||||
dependencies:
|
||||
prop-types "^15.5.8"
|
||||
|
||||
@@ -13935,35 +14014,59 @@ rollup-plugin-node-resolve@^3.0.0:
|
||||
is-module "^1.0.0"
|
||||
resolve "^1.1.6"
|
||||
|
||||
rollup-plugin-stats@1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-stats/-/rollup-plugin-stats-1.3.4.tgz#772f147bb195f7bd77877b95a7e0baf801aeaf94"
|
||||
integrity sha512-N6PCNucOrRJxqzeAvpEyK9Wf4bxQlCCuIj5+n0XUB0plFU0u1VwjY0GDM6Z3lvs/KDhzJ2anYwxXUZVfmFw8LA==
|
||||
rollup-plugin-stats@1.3.6:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-stats/-/rollup-plugin-stats-1.3.6.tgz#aa1d3f29c0f443b4e39e380f0d23c5eceb9909eb"
|
||||
integrity sha512-wpsRsRtp52kN0gv3yBPB1KEKU99Rbsj+aqcHdfH1YVrSLnRuFWJj7auOcp3siF+xpnkd+85XOBZzKCbysD06gw==
|
||||
|
||||
rollup-plugin-webpack-stats@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-webpack-stats/-/rollup-plugin-webpack-stats-2.0.3.tgz#918dc46aa652a731bb62239c894e487bf8e5365a"
|
||||
integrity sha512-tJJaYkP3yQctWRoAn7zh47DszOpwXVaDDmxriu48cohi7Nv/87l8Ry0vHnUd8e9MbpXuKZwEGQjqk1BjYLbYww==
|
||||
rollup-plugin-webpack-stats@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-webpack-stats/-/rollup-plugin-webpack-stats-2.0.5.tgz#dee3ef2fb34adea2df0bef1711e253a360994741"
|
||||
integrity sha512-okto2hDUh7TU1/9JHpLBS5BZwMYTsQUPiiiY1LF3aNy+61PzyzF6jugz5TWj0c04/8N/7DDnX/YDNqmpzGLX7Q==
|
||||
dependencies:
|
||||
rollup-plugin-stats "1.3.4"
|
||||
rollup-plugin-stats "1.3.6"
|
||||
|
||||
rollup@^0.41.4, rollup@^2.43.1, rollup@^4.20.0, rollup@^4.5.1:
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.5.1.tgz#95661ead5373d46e1c91b38fc1549bc59fc72aa4"
|
||||
integrity "sha1-lWYerVNz1G4ckbOPwVSbxZ/HKqQ= sha512-0EQribZoPKpb5z1NW/QYm3XSR//Xr8BeEXU49Lc/mQmpmVVG5jPUVrpc2iptup/0WMrY9mzas0fxH+TjYvG2CA=="
|
||||
rollup@^0.41.4:
|
||||
version "0.41.6"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.41.6.tgz#e0d05497877a398c104d816d2733a718a7a94e2a"
|
||||
integrity sha512-rq3eIm5O0oWo2LswknhTxP9FQj+EsyRk8KN6DaRG+OVtQ1Ce1wA71+7eY9mjtqLfNVphcmBdlutn0bcHwHREng==
|
||||
dependencies:
|
||||
source-map-support "^0.4.0"
|
||||
|
||||
rollup@^2.43.1:
|
||||
version "2.79.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090"
|
||||
integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.5.1"
|
||||
"@rollup/rollup-android-arm64" "4.5.1"
|
||||
"@rollup/rollup-darwin-arm64" "4.5.1"
|
||||
"@rollup/rollup-darwin-x64" "4.5.1"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.5.1"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.5.1"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.5.1"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.5.1"
|
||||
"@rollup/rollup-linux-x64-musl" "4.5.1"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.5.1"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.5.1"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.5.1"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rollup@^4.34.9:
|
||||
version "4.40.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.0.tgz#13742a615f423ccba457554f006873d5a4de1920"
|
||||
integrity sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.7"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.40.0"
|
||||
"@rollup/rollup-android-arm64" "4.40.0"
|
||||
"@rollup/rollup-darwin-arm64" "4.40.0"
|
||||
"@rollup/rollup-darwin-x64" "4.40.0"
|
||||
"@rollup/rollup-freebsd-arm64" "4.40.0"
|
||||
"@rollup/rollup-freebsd-x64" "4.40.0"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.40.0"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.40.0"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.40.0"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.40.0"
|
||||
"@rollup/rollup-linux-loongarch64-gnu" "4.40.0"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.40.0"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.40.0"
|
||||
"@rollup/rollup-linux-riscv64-musl" "4.40.0"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.40.0"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.40.0"
|
||||
"@rollup/rollup-linux-x64-musl" "4.40.0"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.40.0"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.40.0"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.40.0"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rope-sequence@^1.3.0:
|
||||
@@ -14433,6 +14536,13 @@ source-map-support@0.5.13:
|
||||
buffer-from "^1.0.0"
|
||||
source-map "^0.6.0"
|
||||
|
||||
source-map-support@^0.4.0:
|
||||
version "0.4.18"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
|
||||
integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==
|
||||
dependencies:
|
||||
source-map "^0.5.6"
|
||||
|
||||
source-map-support@~0.5.20:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||
@@ -14979,12 +15089,12 @@ tinyexec@^0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.1.tgz#0ab0daf93b43e2c211212396bdb836b468c97c98"
|
||||
integrity sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==
|
||||
|
||||
tinyglobby@^0.2.10:
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.10.tgz#e712cf2dc9b95a1f5c5bbd159720e15833977a0f"
|
||||
integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==
|
||||
tinyglobby@^0.2.10, tinyglobby@^0.2.13:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e"
|
||||
integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==
|
||||
dependencies:
|
||||
fdir "^6.4.2"
|
||||
fdir "^6.4.4"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
tippy.js@^6.3.1:
|
||||
@@ -15369,6 +15479,11 @@ undici-types@~6.19.2:
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
|
||||
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
|
||||
|
||||
unfetch@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
|
||||
integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
|
||||
@@ -15630,14 +15745,17 @@ vite-plugin-static-copy@^0.17.0:
|
||||
fs-extra "^11.1.0"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
vite@^5.4.18:
|
||||
version "5.4.18"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.18.tgz#b5af357f9d5ebb2e0c085779b7a37a77f09168a4"
|
||||
integrity sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==
|
||||
vite@^6.3.3:
|
||||
version "6.3.3"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.3.tgz#497392c3f2243194e4dbf09ea83e9a3dddf49b88"
|
||||
integrity sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==
|
||||
dependencies:
|
||||
esbuild "^0.21.3"
|
||||
postcss "^8.4.43"
|
||||
rollup "^4.20.0"
|
||||
esbuild "^0.25.0"
|
||||
fdir "^6.4.4"
|
||||
picomatch "^4.0.2"
|
||||
postcss "^8.5.3"
|
||||
rollup "^4.34.9"
|
||||
tinyglobby "^0.2.13"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user