Compare commits

..

1 Commits

Author SHA1 Message Date
Tom Moor 6ddafa54bb Remove maintainers from probot behavior 2025-03-26 16:33:57 -07:00
23 changed files with 98 additions and 450 deletions
-30
View File
@@ -1,30 +0,0 @@
name: Lint
on:
pull_request:
branches: [ main ]
jobs:
run-linters:
if: startsWith(github.actor, 'codegen-sh')
name: Run linters
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn lint --fix
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'Applied automatic fixes'
@@ -7,43 +7,17 @@ import User from "~/models/User";
import Tooltip from "~/components/Tooltip";
import Avatar, { AvatarSize } from "./Avatar";
/**
* Props for the AvatarWithPresence component
*/
type Props = {
/** The user to display the avatar for */
user: User;
/** Whether the user is currently present in the document */
isPresent: boolean;
/** Whether the user is currently editing the document */
isEditing: boolean;
/** Whether the user is currently observing the document */
isObserving: boolean;
/** Whether this avatar represents the current user */
isCurrentUser: boolean;
/** Optional click handler for the avatar */
onClick?: React.MouseEventHandler<HTMLImageElement>;
/** Size of the avatar, defaults to AvatarSize.Large */
size?: AvatarSize;
/** Optional inline styles to apply to the avatar wrapper */
style?: React.CSSProperties;
};
/**
* AvatarWithPresence component displays a user's avatar with visual indicators
* for their current status (present, editing, observing).
*
* The component shows different visual states:
* - Present users have full opacity
* - Non-present users have reduced opacity
* - Observing users have a colored border matching their user color
* - Hovering shows a colored border
*
* A tooltip displays the user's name and current status.
*
* @param props - Component properties
* @returns React component
*/
function AvatarWithPresence({
onClick,
user,
@@ -90,33 +64,16 @@ function AvatarWithPresence({
);
}
/**
* Centered container for tooltip content
*/
const Centered = styled.div`
text-align: center;
`;
/**
* Props for the AvatarPresence styled component
*/
type AvatarWrapperProps = {
/** Whether the user is currently present */
$isPresent: boolean;
/** Whether the user is currently observing */
$isObserving: boolean;
/** The user's color for border highlighting */
$color: string;
};
/**
* Styled component that wraps the Avatar and provides visual indicators
* for the user's presence status.
*
* - Adjusts opacity based on presence
* - Adds colored borders for observing users
* - Handles hover effects
*/
const AvatarPresence = styled.div<AvatarWrapperProps>`
opacity: ${(props) => (props.$isPresent ? 1 : 0.5)};
transition: opacity 250ms ease-in-out;
-5
View File
@@ -1,11 +1,6 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
/**
* Hook that provides a dictionary of translated UI strings.
*
* @returns An object containing all translated UI strings used throughout the application
*/
export default function useDictionary() {
const { t } = useTranslation();
-9
View File
@@ -1,15 +1,6 @@
import * as React from "react";
import useWindowSize from "./useWindowSize";
/**
* Hook to calculate the maximum height for an element based on its position and viewport size.
*
* @param options Configuration options
* @param options.elementRef A ref pointing to the element to calculate max height for
* @param options.maxViewportPercentage The maximum height of the element as a percentage of the viewport
* @param options.margin The margin to apply to the positioning
* @returns Object containing the calculated maxHeight and a function to recalculate it
*/
const useMaxHeight = ({
elementRef,
maxViewportPercentage = 90,
-6
View File
@@ -1,11 +1,5 @@
import { useState, useEffect } from "react";
/**
* Hook to check if a media query matches the current viewport.
*
* @param query The CSS media query to check against
* @returns boolean indicating whether the media query matches
*/
export default function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState<boolean>(false);
-5
View File
@@ -1,11 +1,6 @@
import { breakpoints } from "@shared/styles";
import useMediaQuery from "~/hooks/useMediaQuery";
/**
* Hook to detect if the current viewport is mobile-sized.
*
* @returns boolean indicating whether the current viewport is mobile-sized
*/
export default function useMobile(): boolean {
return useMediaQuery(`(max-width: ${breakpoints.tablet - 1}px)`);
}
-5
View File
@@ -1,11 +1,6 @@
import React from "react";
import { useLocation } from "react-router-dom";
/**
* Hook to access URL query parameters from the current location.
*
* @returns URLSearchParams object containing the current URL query parameters
*/
export default function useQuery() {
const location = useLocation();
-5
View File
@@ -2,11 +2,6 @@ import { MobXProviderContext } from "mobx-react";
import * as React from "react";
import RootStore from "~/stores";
/**
* Hook to access the MobX stores from the React context.
*
* @returns The root store containing all application stores
*/
export default function useStores() {
return React.useContext(MobXProviderContext) as typeof RootStore;
}
-5
View File
@@ -1,10 +1,5 @@
import * as React from "react";
/**
* Hook that executes a callback when the component unmounts.
*
* @param callback Function to be called on component unmount
*/
const useUnmount = (callback: (...args: Array<any>) => any) => {
const ref = React.useRef(callback);
ref.current = callback;
-6
View File
@@ -1,11 +1,5 @@
import { useLayoutEffect, useState } from "react";
/**
* Hook to get the current viewport height, accounting for mobile virtual keyboards.
* Uses the VisualViewport API when available, falling back to window.innerHeight.
*
* @returns The current viewport height in pixels
*/
export default function useViewportHeight(): number | void {
// https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport#browser_compatibility
// Note: No support in Firefox at time of writing, however this mainly exists
-7
View File
@@ -13,13 +13,6 @@ const defaultOptions = {
throttle: 100,
};
/**
* Hook to track the window's scroll position.
*
* @param options Configuration options
* @param options.throttle Time in milliseconds to throttle the scroll event
* @returns Object containing the current scroll position (x, y coordinates)
*/
export default function useWindowScrollPosition(options: {
throttle: number;
}): {
+2 -45
View File
@@ -1,5 +1,4 @@
/* eslint-disable lines-between-class-members */
import fractionalIndex from "fractional-index";
import find from "lodash/find";
import findIndex from "lodash/findIndex";
import remove from "lodash/remove";
@@ -12,8 +11,6 @@ import {
InferAttributes,
InferCreationAttributes,
EmptyResultError,
type CreateOptions,
type UpdateOptions,
} from "sequelize";
import {
Sequelize,
@@ -35,8 +32,6 @@ import {
BeforeDestroy,
IsDate,
AllowNull,
BeforeCreate,
BeforeUpdate,
} from "sequelize-typescript";
import isUUID from "validator/lib/isUUID";
import type { CollectionSort, ProsemirrorData } from "@shared/types";
@@ -46,9 +41,7 @@ import { sortNavigationNodes } from "@shared/utils/collections";
import slugify from "@shared/utils/slugify";
import { CollectionValidation } from "@shared/validations";
import { ValidationError } from "@server/errors";
import removeIndexCollision from "@server/utils/removeIndexCollision";
import { generateUrlId } from "@server/utils/url";
import { ValidateIndex } from "@server/validation";
import Document from "./Document";
import FileOperation from "./FileOperation";
import Group from "./Group";
@@ -224,8 +217,8 @@ class Collection extends ParanoidModel<
color: string | null;
@Length({
max: ValidateIndex.maxLength,
msg: `index must be ${ValidateIndex.maxLength} characters or less`,
max: 256,
msg: `index must be 256 characters or less`,
})
@Column
index: string | null;
@@ -331,30 +324,6 @@ class Collection extends ParanoidModel<
}
}
@BeforeCreate
static async setIndex(model: Collection, options: CreateOptions<Collection>) {
if (model.index) {
model.index = await removeIndexCollision(model.teamId, model.index, {
transaction: options.transaction,
});
return;
}
const firstCollectionForTeam = await this.findOne({
where: {
teamId: model.teamId,
},
order: [
// using LC_COLLATE:"C" because we need byte order to drive the sorting
Sequelize.literal('"collection"."index" collate "C"'),
["updatedAt", "DESC"],
],
...options,
});
model.index = fractionalIndex(null, firstCollectionForTeam?.index ?? null);
}
@AfterCreate
static async onAfterCreate(
model: Collection,
@@ -374,18 +343,6 @@ class Collection extends ParanoidModel<
});
}
@BeforeUpdate
static async checkIndex(
model: Collection,
options: UpdateOptions<Collection>
) {
if (model.index && model.changed("index")) {
model.index = await removeIndexCollision(model.teamId, model.index, {
transaction: options.transaction,
});
}
}
// associations
@BelongsTo(() => FileOperation, "importId")
+32 -100
View File
@@ -9,7 +9,7 @@ import NotificationHelper from "./NotificationHelper";
describe("NotificationHelper", () => {
describe("getCommentNotificationRecipients", () => {
it("should only return users who have notification enabled for comment creation and are subscribed to the document in case of new thread", async () => {
it("should return users who have notification enabled for comment creation and are subscribed to the document in case of parent comment", async () => {
const documentAuthor = await buildUser();
const document = await buildDocument({
userId: documentAuthor.id,
@@ -54,7 +54,7 @@ describe("NotificationHelper", () => {
expect(recipients[0].id).toEqual(notificationEnabledUser.id);
});
it("should only return users who have notification enabled for comment creation and are in the thread in case of child comment", async () => {
it("should return users who have notification enabled for comment creation and are in the thread in case of child comment", async () => {
const documentAuthor = await buildUser();
const document = await buildDocument({
userId: documentAuthor.id,
@@ -112,104 +112,32 @@ describe("NotificationHelper", () => {
expect(recipients.length).toEqual(1);
expect(recipients[0].id).toEqual(notificationEnabledUserInThread.id);
});
it("should not return users who have notification disabled for comment creation and are in the thread in case of child comment", async () => {
const documentAuthor = await buildUser();
const document = await buildDocument({
userId: documentAuthor.id,
teamId: documentAuthor.teamId,
});
const notificationEnabledUserInThread = await buildUser({
teamId: document.teamId,
notificationSettings: { [NotificationEventType.CreateComment]: false },
});
const notificationEnabledUserNotInThread = await buildUser({
teamId: document.teamId,
notificationSettings: { [NotificationEventType.CreateComment]: true },
});
const notificationDisabledUser = await buildUser({
teamId: document.teamId,
notificationSettings: {
[NotificationEventType.CreateComment]: false,
},
});
await Promise.all([
buildSubscription({
userId: documentAuthor.id,
documentId: document.id,
}),
buildSubscription({
userId: notificationEnabledUserInThread.id,
documentId: document.id,
}),
buildSubscription({
userId: notificationEnabledUserNotInThread.id,
documentId: document.id,
}),
buildSubscription({
userId: notificationDisabledUser.id,
documentId: document.id,
}),
]);
const parentComment = await buildComment({
documentId: document.id,
userId: notificationEnabledUserInThread.id,
});
const childComment = await buildComment({
documentId: document.id,
userId: documentAuthor.id,
parentCommentId: parentComment.id,
});
const recipients =
await NotificationHelper.getCommentNotificationRecipients(
document,
childComment,
childComment.createdById
);
expect(recipients.length).toEqual(0);
});
it("should return users who have notification enabled and are in the thread but not explicitly subscribed to document", async () => {
const documentAuthor = await buildUser();
const document = await buildDocument({
userId: documentAuthor.id,
teamId: documentAuthor.teamId,
});
const notificationEnabledUserInThread = await buildUser({
teamId: document.teamId,
notificationSettings: { [NotificationEventType.CreateComment]: true },
});
await buildUser({
teamId: document.teamId,
notificationSettings: {
[NotificationEventType.CreateComment]: false,
},
});
const parentComment = await buildComment({
documentId: document.id,
userId: notificationEnabledUserInThread.id,
});
const childComment = await buildComment({
documentId: document.id,
userId: documentAuthor.id,
parentCommentId: parentComment.id,
});
const recipients =
await NotificationHelper.getCommentNotificationRecipients(
document,
childComment,
childComment.createdById
);
expect(recipients.length).toEqual(1);
expect(recipients[0].id).toEqual(notificationEnabledUserInThread.id);
});
});
describe("getDocumentNotificationRecipients", () => {
it("should return all users who have notification enabled for the event", async () => {
const documentAuthor = await buildUser();
const document = await buildDocument({
userId: documentAuthor.id,
teamId: documentAuthor.teamId,
});
const notificationEnabledUser = await buildUser({
teamId: document.teamId,
notificationSettings: { [NotificationEventType.UpdateDocument]: true },
});
const recipients =
await NotificationHelper.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.UpdateDocument,
onlySubscribers: false,
actorId: documentAuthor.id,
});
expect(recipients.length).toEqual(1);
expect(recipients[0].id).toEqual(notificationEnabledUser.id);
});
it("should return users who have subscribed to the document", async () => {
const documentAuthor = await buildUser();
const document = await buildDocument({
@@ -226,6 +154,7 @@ describe("NotificationHelper", () => {
await NotificationHelper.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.UpdateDocument,
onlySubscribers: true,
actorId: documentAuthor.id,
});
@@ -249,6 +178,7 @@ describe("NotificationHelper", () => {
await NotificationHelper.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.UpdateDocument,
onlySubscribers: true,
actorId: documentAuthor.id,
});
@@ -286,6 +216,7 @@ describe("NotificationHelper", () => {
await NotificationHelper.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.UpdateDocument,
onlySubscribers: true,
actorId: documentAuthor.id,
});
@@ -304,19 +235,20 @@ describe("NotificationHelper", () => {
});
const notificationEnabledUser = await buildUser({
teamId: document.teamId,
notificationSettings: { [NotificationEventType.PublishDocument]: true },
notificationSettings: { [NotificationEventType.UpdateDocument]: true },
});
// suspended user
await buildUser({
suspendedAt: new Date(),
teamId: document.teamId,
notificationSettings: { [NotificationEventType.PublishDocument]: true },
notificationSettings: { [NotificationEventType.UpdateDocument]: true },
});
const recipients =
await NotificationHelper.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.PublishDocument,
notificationType: NotificationEventType.UpdateDocument,
onlySubscribers: false,
actorId: documentAuthor.id,
});
+34 -60
View File
@@ -14,7 +14,7 @@ import {
Comment,
View,
} from "@server/models";
import { canUserAccessDocument } from "@server/utils/permissions";
import { can } from "@server/policies";
import { ProsemirrorHelper } from "./ProsemirrorHelper";
export default class NotificationHelper {
@@ -60,12 +60,18 @@ export default class NotificationHelper {
comment: Comment,
actorId: string
): Promise<User[]> => {
let recipients: User[];
let recipients = await this.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.CreateComment,
onlySubscribers: !comment.parentCommentId,
actorId,
});
// If this is a reply to another comment, we want to notify all users
// that are involved in the thread of this comment (i.e. the original
// comment and all replies to it).
if (comment.parentCommentId) {
recipients = recipients.filter((recipient) =>
recipient.subscribedToEventType(NotificationEventType.CreateComment)
);
if (recipients.length > 0 && comment.parentCommentId) {
const contextComments = await Comment.findAll({
attributes: ["createdById", "data"],
where: {
@@ -89,37 +95,13 @@ export default class NotificationHelper {
const userIdsInThread = uniq([
...createdUserIdsInThread,
...mentionedUserIdsInThread,
]).filter((userId) => userId !== actorId);
recipients = await User.findAll({
where: {
id: {
[Op.in]: userIdsInThread,
},
teamId: document.teamId,
},
});
recipients = recipients.filter((recipient) =>
recipient.subscribedToEventType(NotificationEventType.CreateComment)
);
} else {
recipients = await this.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.CreateComment,
actorId,
// We will check below, this just prevents duplicate queries
disableAccessCheck: true,
});
]);
recipients = recipients.filter((r) => userIdsInThread.includes(r.id));
}
const filtered: User[] = [];
for (const recipient of recipients) {
if (recipient.isSuspended) {
continue;
}
// If this recipient has viewed the document since the comment was made
// then we can avoid sending them a useless notification, yay.
const view = await View.findOne({
@@ -137,13 +119,7 @@ export default class NotificationHelper {
"processor",
`suppressing notification to ${recipient.id} because doc viewed`
);
continue;
}
// Check the recipient has access to the collection this document is in. Just
// because they are subscribed doesn't mean they still have access to read
// the document.
if (await canUserAccessDocument(recipient, document.id)) {
} else {
filtered.push(recipient);
}
}
@@ -156,36 +132,24 @@ export default class NotificationHelper {
*
* @param document The document to get recipients for.
* @param notificationType The notification type for which to find the recipients.
* @param onlySubscribers Whether to consider only the users who have active subscription to the document.
* @param actorId The id of the user that performed the action.
* @param disableAccessCheck Whether to disable the access check for the document.
* @returns A list of recipients
*/
public static getDocumentNotificationRecipients = async ({
document,
notificationType,
onlySubscribers,
actorId,
disableAccessCheck = false,
}: {
document: Document;
notificationType: NotificationEventType;
onlySubscribers: boolean;
actorId: string;
disableAccessCheck?: boolean;
}): Promise<User[]> => {
let recipients: User[];
if (notificationType === NotificationEventType.PublishDocument) {
recipients = await User.findAll({
where: {
id: {
[Op.ne]: actorId,
},
teamId: document.teamId,
notificationSettings: {
[notificationType]: true,
},
},
});
} else {
if (onlySubscribers) {
const subscriptions = await Subscription.findAll({
attributes: ["userId"],
where: {
@@ -206,6 +170,15 @@ export default class NotificationHelper {
});
recipients = subscriptions.map((s) => s.user);
} else {
recipients = await User.findAll({
where: {
id: {
[Op.ne]: actorId,
},
teamId: document.teamId,
},
});
}
recipients = recipients.filter((recipient) =>
@@ -215,17 +188,18 @@ export default class NotificationHelper {
const filtered = [];
for (const recipient of recipients) {
if (recipient.isSuspended) {
if (!recipient.email || recipient.isSuspended) {
continue;
}
// Check the recipient has access to the collection this document is in. Just
// because they are subscribed doesn't mean they still have access to read
// the document.
if (
disableAccessCheck ||
(await canUserAccessDocument(recipient, document.id))
) {
const doc = await Document.findByPk(document.id, {
userId: recipient.id,
});
if (can(recipient, "read", doc)) {
filtered.push(recipient);
}
}
@@ -41,7 +41,6 @@ export default class CleanupOldImportsTask extends BaseTask<Props> {
],
batchLimit: 50,
totalLimit: maxImportsPerTask,
paranoid: false,
},
async (imports) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -54,6 +54,7 @@ export default class DocumentPublishedNotificationsTask extends BaseTask<Documen
await NotificationHelper.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.PublishDocument,
onlySubscribers: false,
actorId: document.lastModifiedById,
})
).filter((recipient) => !userIdsMentioned.includes(recipient.id));
@@ -76,6 +76,7 @@ export default class RevisionCreatedNotificationsTask extends BaseTask<RevisionE
await NotificationHelper.getDocumentNotificationRecipients({
document,
notificationType: NotificationEventType.UpdateDocument,
onlySubscribers: true,
actorId: document.lastModifiedById,
})
).filter((recipient) => !userIdsMentioned.includes(recipient.id));
@@ -1327,32 +1327,6 @@ describe("#collections.create", () => {
expect(body.policies[0].abilities.read).toBeTruthy();
});
it("should ensure unique index across the team", async () => {
const team = await buildTeam();
const [adminA, adminB] = await Promise.all([
buildAdmin({ teamId: team.id }),
buildAdmin({ teamId: team.id }),
]);
const resA = await server.post("/api/collections.create", {
body: {
token: adminA.getJwtToken(),
name: "Test A",
},
});
const resB = await server.post("/api/collections.create", {
body: {
token: adminB.getJwtToken(),
name: "Test B",
},
});
const [bodyA, bodyB] = await Promise.all([resA.json(), resB.json()]);
expect(resA.status).toEqual(200);
expect(resB.status).toEqual(200);
expect(bodyA.data.index).not.toEqual(bodyB.data.index);
});
it("if index collision, should updated index of other collection", async () => {
const user = await buildUser();
const createdCollectionAResponse = await server.post(
+22 -16
View File
@@ -1,3 +1,4 @@
import fractionalIndex from "fractional-index";
import invariant from "invariant";
import Router from "koa-router";
import { Sequelize, Op, WhereOptions } from "sequelize";
@@ -41,6 +42,7 @@ import {
import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import { collectionIndexing } from "@server/utils/indexing";
import removeIndexCollision from "@server/utils/removeIndexCollision";
import pagination from "../middlewares/pagination";
import * as T from "./schema";
@@ -53,21 +55,23 @@ router.post(
transaction(),
async (ctx: APIContext<T.CollectionsCreateReq>) => {
const { transaction } = ctx.state;
const {
name,
color,
description,
data,
permission,
sharing,
icon,
sort,
index,
} = ctx.input.body;
const { name, color, description, data, permission, sharing, icon, sort } =
ctx.input.body;
let { index } = ctx.input.body;
const { user } = ctx.state.auth;
authorize(user, "createCollection", user.team);
if (index) {
index = await removeIndexCollision(user.teamId, index, { transaction });
} else {
const first = await Collection.findFirstCollectionForUser(user, {
attributes: ["id", "index"],
transaction,
});
index = fractionalIndex(null, first ? first.index : null);
}
const collection = Collection.build({
name,
content: data,
@@ -955,16 +959,18 @@ router.post(
transaction(),
async (ctx: APIContext<T.CollectionsMoveReq>) => {
const { transaction } = ctx.state;
const { id, index } = ctx.input.body;
const { id } = ctx.input.body;
let { index } = ctx.input.body;
const { user } = ctx.state.auth;
let collection = await Collection.findByPk(id, {
const collection = await Collection.findByPk(id, {
transaction,
lock: transaction.LOCK.UPDATE,
});
authorize(user, "move", collection);
collection = await collection.update(
index = await removeIndexCollision(user.teamId, index, { transaction });
await collection.update(
{
index,
},
@@ -976,14 +982,14 @@ router.post(
name: "collections.move",
collectionId: collection.id,
data: {
index: collection.index,
index,
},
});
ctx.body = {
success: true,
data: {
index: collection.index,
index,
},
};
}
+4 -2
View File
@@ -1,5 +1,5 @@
import fractionalIndex from "fractional-index";
import { Sequelize, type FindOptions } from "sequelize";
import { Op, Sequelize, type FindOptions } from "sequelize";
import Collection from "@server/models/Collection";
/**
@@ -31,7 +31,9 @@ export default async function removeIndexCollision(
where: {
teamId,
deletedAt: null,
index: Sequelize.literal(`"collection"."index" collate "C" > '${index}'`),
index: {
[Op.gt]: index,
},
},
attributes: ["id", "index"],
limit: 1,
+1 -1
View File
@@ -232,7 +232,7 @@ export class ValidateDocumentId {
export class ValidateIndex {
public static regex = new RegExp("^[\x20-\x7E]+$");
public static message = "Must be between x20 to x7E ASCII";
public static maxLength = 256;
public static maxLength = 100;
}
export class ValidateURL {
+1 -9
View File
@@ -1,5 +1,5 @@
import { NodeType } from "prosemirror-model";
import { liftListItem, wrapInList } from "prosemirror-schema-list";
import { wrapInList, liftListItem } from "prosemirror-schema-list";
import { Command } from "prosemirror-state";
import { chainTransactions } from "../lib/chainTransactions";
import { findParentNode } from "../queries/findParentNode";
@@ -29,14 +29,6 @@ export default function toggleList(
return liftListItem(itemType)(state, dispatch);
}
const currentItemType = parentList.node.content.firstChild?.type;
if (currentItemType && currentItemType !== itemType) {
return chainTransactions(clearNodes(), wrapInList(listType))(
state,
dispatch
);
}
if (
isList(parentList.node, schema) &&
listType.validContent(parentList.node.content)
-64
View File
@@ -99,7 +99,6 @@ export default class Code extends Mark {
return false;
}
// Check if we're pasting inside backticks
const start = from - 1;
const end = to + 1;
if (
@@ -119,69 +118,6 @@ export default class Code extends Mark {
return true;
}
// Check if we're pasting over an existing inline code block
if (isInCode(state, { onlyMark: true })) {
// Get the range of the current code mark
const marks = state.doc.resolve(from).marks();
const codeMark = marks.find(
(mark) => mark.type === state.schema.marks.code_inline
);
if (codeMark) {
// Find the start and end of the code mark
let codeStart = from;
let codeEnd = to;
// Find the start of the code mark
for (let i = from; i > 0; i--) {
const resolvedPos = state.doc.resolve(i);
const marksAtPos = resolvedPos.marks();
const hasCodeMark = marksAtPos.some(
(mark) => mark.type === state.schema.marks.code_inline
);
if (!hasCodeMark) {
codeStart = i + 1;
break;
}
if (i === 1) {
codeStart = 1;
}
}
// Find the end of the code mark
for (let i = to; i < state.doc.nodeSize - 2; i++) {
const resolvedPos = state.doc.resolve(i);
const marksAtPos = resolvedPos.marks();
const hasCodeMark = marksAtPos.some(
(mark) => mark.type === state.schema.marks.code_inline
);
if (!hasCodeMark) {
codeEnd = i;
break;
}
if (i === state.doc.nodeSize - 3) {
codeEnd = state.doc.nodeSize - 2;
}
}
// Replace the content within the code mark
view.dispatch(
state.tr
.replaceRange(from, to, slice)
.addMark(
from,
from + slice.size,
state.schema.marks.code_inline.create()
)
);
return true;
}
}
return false;
},