Compare commits

..

1 Commits

Author SHA1 Message Date
Tom Moor 51d39d915d chore: Missing react key in SuggestionMenu 2025-03-30 10:30:42 -04:00
15 changed files with 788 additions and 862 deletions
+1 -5
View File
@@ -2,11 +2,6 @@ import React from "react";
import styled from "styled-components";
import { fadeIn } from "~/styles/animations";
/**
* Fade in animation for a component.
*
* @param timing - The duration of the fade in animation, default is 250ms.
*/
const Fade = styled.span<{ timing?: number | string }>`
animation: ${fadeIn} ${(props) => props.timing || "250ms"} ease-in-out;
`;
@@ -22,6 +17,7 @@ type Props = {
*/
export const ConditionalFade = ({ animate, children }: Props) => {
const [isAnimated] = React.useState(animate);
return isAnimated ? <Fade>{children}</Fade> : <>{children}</>;
};
+1 -1
View File
@@ -88,7 +88,7 @@ export default class PasteHandler extends Extension {
// If the users selection is currently in a code block then paste
// as plain text, ignore all formatting and HTML content.
if (isInCode(state, { inclusive: true })) {
if (isInCode(state)) {
event.preventDefault();
view.dispatch(state.tr.insertText(text));
return true;
@@ -16,7 +16,6 @@ import { useDocumentContext } from "~/components/DocumentContext";
import Facepile from "~/components/Facepile";
import Fade from "~/components/Fade";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
import useBoolean from "~/hooks/useBoolean";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import usePersistedState from "~/hooks/usePersistedState";
@@ -64,7 +63,7 @@ function CommentThread({
const history = useHistory();
const location = useLocation();
const sidebarContext = useLocationSidebarContext();
const [autoFocus, setAutoFocusOn, setAutoFocusOff] = useBoolean(thread.isNew);
const [autoFocus, setAutoFocus] = React.useState(thread.isNew);
const can = usePolicy(document);
@@ -157,9 +156,9 @@ function CommentThread({
React.useEffect(() => {
if (!focused && autoFocus) {
setAutoFocusOff();
setAutoFocus(false);
}
}, [focused, autoFocus, setAutoFocusOff]);
}, [focused, autoFocus]);
React.useEffect(() => {
if (focused) {
@@ -274,7 +273,7 @@ function CommentThread({
)}
</ResizingHeightContainer>
{!focused && !recessed && !draft && canReply && (
<Reply onClick={setAutoFocusOn}>{t("Reply")}</Reply>
<Reply onClick={() => setAutoFocus(true)}>{t("Reply")}</Reply>
)}
</Thread>
);
+10 -10
View File
@@ -48,11 +48,11 @@
"> 0.25%, not dead"
],
"dependencies": {
"@aws-sdk/client-s3": "3.777.0",
"@aws-sdk/lib-storage": "3.777.0",
"@aws-sdk/s3-presigned-post": "3.777.0",
"@aws-sdk/s3-request-presigner": "3.777.0",
"@aws-sdk/signature-v4-crt": "^3.775.0",
"@aws-sdk/client-s3": "3.774.0",
"@aws-sdk/lib-storage": "3.774.0",
"@aws-sdk/s3-presigned-post": "3.774.0",
"@aws-sdk/s3-request-presigner": "3.774.0",
"@aws-sdk/signature-v4-crt": "^3.774.0",
"@babel/core": "^7.26.10",
"@babel/plugin-proposal-decorators": "^7.25.9",
"@babel/plugin-transform-class-properties": "^7.25.9",
@@ -89,7 +89,7 @@
"@sentry/node": "^7.120.3",
"@sentry/react": "^7.120.3",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.13.6",
"@tanstack/react-virtual": "^3.11.3",
"@tippyjs/react": "^4.2.6",
"@types/form-data": "^2.5.2",
"@types/mailparser": "^3.4.5",
@@ -185,8 +185,8 @@
"prosemirror-history": "^1.4.1",
"prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.2",
"prosemirror-markdown": "^1.13.2",
"prosemirror-model": "^1.25.0",
"prosemirror-markdown": "^1.13.1",
"prosemirror-model": "^1.24.0",
"prosemirror-schema-list": "^1.4.1",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.4",
@@ -248,7 +248,7 @@
"uuid": "^8.3.2",
"validator": "13.12.0",
"vaul": "^1.1.2",
"vite": "^5.4.16",
"vite": "^5.4.15",
"vite-plugin-pwa": "^0.20.3",
"winston": "^3.17.0",
"ws": "^7.5.10",
@@ -263,7 +263,7 @@
"@babel/cli": "^7.27.0",
"@babel/preset-typescript": "^7.27.0",
"@faker-js/faker": "^8.4.1",
"@relative-ci/agent": "^4.3.0",
"@relative-ci/agent": "^4.2.14",
"@testing-library/react": "^12.0.0",
"@types/addressparser": "^1.0.3",
"@types/body-scroll-lock": "^3.1.2",
@@ -597,7 +597,6 @@ router.post(
createdById: user.id,
},
transaction,
hooks: false,
});
}
@@ -2,7 +2,6 @@ import queryString from "query-string";
import { v4 as uuidv4 } from "uuid";
import { randomElement } from "@shared/random";
import { NotificationEventType } from "@shared/types";
import NotificationSettingsHelper from "@server/models/helpers/NotificationSettingsHelper";
import {
buildCollection,
buildDocument,
@@ -698,40 +697,3 @@ describe("#notifications.update_all", () => {
expect(body.data.total).toBe(2);
});
});
describe("#notifications.unsubscribe", () => {
it("should allow unsubscribe with valid token", async () => {
const user = await buildUser();
const token = NotificationSettingsHelper.unsubscribeToken(
user.id,
NotificationEventType.UpdateDocument
);
const res = await server.get(
`/api/notifications.unsubscribe?userId=${user.id}&token=${token}&eventType=documents.update&follow=true`,
{
redirect: "manual",
}
);
expect(res.status).toBe(302);
expect(res.headers.get("location")).toContain(
"/settings/notifications?success"
);
const events = (await user.reload()).notificationSettings;
expect(events).not.toContain("documents.update");
});
it("should not allow unsubscribe with invalid token", async () => {
const user = await buildUser();
const res = await server.get(
`/api/notifications.unsubscribe?userId=${user.id}&token=invalid-token&eventType=documents.update&follow=true`,
{
redirect: "manual",
}
);
expect(res.status).toBe(302);
expect(res.headers.get("location")).toContain("?notice=invalid-auth");
});
});
@@ -53,7 +53,7 @@ const handleUnsubscribe = async (
});
user.setNotificationEventType(eventType, false);
await user.save({ transaction });
await user.save();
ctx.redirect(`${user.team.url}/settings/notifications?success`);
};
-4
View File
@@ -313,10 +313,6 @@ width: 100%;
background: ${props.theme.mentionHoverBackground};
}
&[data-type="user"] {
gap: 0;
}
&.mention-user::before {
content: "@";
}
+3 -5
View File
@@ -28,10 +28,9 @@ export function getCellAttrs(dom: HTMLElement | string): Attrs {
const widthAttr = dom.getAttribute("data-colwidth");
const widths =
widthAttr && /^\d+(,\d+)*$/.test(widthAttr)
? widthAttr.split(",").map(Number)
? widthAttr.split(",").map((s) => Number(s))
: null;
const colspan = Number(dom.getAttribute("colspan") || 1);
return {
colspan,
rowspan: Number(dom.getAttribute("rowspan") || 1),
@@ -64,11 +63,10 @@ export function setCellAttrs(node: Node): Attrs {
}
if (node.attrs.colwidth) {
if (isBrowser) {
attrs["data-colwidth"] = node.attrs.colwidth.map(parseInt).join(",");
attrs["data-colwidth"] = node.attrs.colwidth.join(",");
} else {
attrs.style =
(attrs.style ?? "") +
`min-width: ${parseInt(node.attrs.colwidth[0])}px;`;
(attrs.style ?? "") + `min-width: ${node.attrs.colwidth}px;`;
}
}
+1 -2
View File
@@ -21,10 +21,9 @@ export default class Code extends Mark {
get schema(): MarkSpec {
return {
excludes: "mention placeholder highlight",
excludes: "mention placeholder highlight em strong",
parseDOM: [{ tag: "code", preserveWhitespace: true }],
toDOM: () => ["code", { class: "inline", spellCheck: "false" }],
code: true,
};
}
+3 -17
View File
@@ -7,8 +7,6 @@ type Options = {
onlyBlock?: boolean;
/** Only check if the selection is inside a code mark. */
onlyMark?: boolean;
/** If true then code must contain entire selection */
inclusive?: boolean;
};
/**
@@ -22,29 +20,17 @@ export function isInCode(state: EditorState, options?: Options): boolean {
const { nodes, marks } = state.schema;
if (!options?.onlyMark) {
if (
nodes.code_block &&
isNodeActive(nodes.code_block, undefined, {
inclusive: options?.inclusive,
})(state)
) {
if (nodes.code_block && isNodeActive(nodes.code_block)(state)) {
return true;
}
if (
nodes.code_fence &&
isNodeActive(nodes.code_fence, undefined, {
inclusive: options?.inclusive,
})(state)
) {
if (nodes.code_fence && isNodeActive(nodes.code_fence)(state)) {
return true;
}
}
if (!options?.onlyBlock) {
if (marks.code_inline) {
return isMarkActive(marks.code_inline, undefined, {
inclusive: options?.inclusive,
})(state);
return isMarkActive(marks.code_inline)(state);
}
}
+1 -4
View File
@@ -6,8 +6,6 @@ import { getMarksBetween } from "./getMarksBetween";
type Options = {
/** Only return match if the range and attrs is exact */
exact?: boolean;
/** If true then mark must contain entire selection */
inclusive?: boolean;
};
/**
@@ -42,8 +40,7 @@ export const isMarkActive =
Object.keys(attrs).every(
(key) => mark.attrs[key] === attrs[key]
)) &&
(!options?.exact || (start === from && end === to)) &&
(!options?.inclusive || (start <= from && end >= to))
(!options?.exact || (start === from && end === to))
);
}
+10 -34
View File
@@ -3,55 +3,31 @@ import { EditorState } from "prosemirror-state";
import { Primitive } from "utility-types";
import { findParentNode } from "./findParentNode";
type Options = {
/** Only return match if the range and attrs is exact */
exact?: boolean;
/** If true then node must contain entire selection */
inclusive?: boolean;
};
/**
* Checks if a node is active in the current selection or not.
*
* @param type The node type to check.
* @param attrs The attributes to check.
* @param options The options to use.
* @returns A function that checks if a node is active in the current selection or not.
*/
export const isNodeActive =
(type: NodeType, attrs?: Record<string, Primitive>, options?: Options) =>
(state: EditorState): boolean => {
(type: NodeType, attrs: Record<string, Primitive> = {}) =>
(state: EditorState) => {
if (!type) {
return false;
}
const { from, to } = state.selection;
const nodeWithPos = findParentNode(
(node) =>
node.type === type &&
(!attrs ||
Object.keys(attrs).every((key) => node.attrs[key] === attrs[key]))
)(state.selection);
const nodeAfter = state.selection.$from.nodeAfter;
let node = nodeAfter?.type === type ? nodeAfter : undefined;
if (!nodeWithPos) {
return false;
if (!node) {
const parent = findParentNode((n) => n.type === type)(state.selection);
node = parent?.node;
}
if (options?.inclusive) {
// Check if the node's position contains the entire selection
return (
nodeWithPos.pos <= from &&
nodeWithPos.pos + nodeWithPos.node.nodeSize >= to
);
if (!Object.keys(attrs).length || !node) {
return !!node;
}
if (options?.exact) {
// Check if node's range exactly matches selection
return (
nodeWithPos.pos === from &&
nodeWithPos.pos + nodeWithPos.node.nodeSize === to
);
}
return true;
return node.hasMarkup(type, { ...node.attrs, ...attrs });
};
+24 -16
View File
@@ -1,5 +1,16 @@
import { useState, useLayoutEffect } from "react";
const defaultRect = {
top: 0,
left: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
width: 0,
height: 0,
};
/**
* A hook that returns the size of an element or ref.
*
@@ -8,11 +19,19 @@ import { useState, useLayoutEffect } from "react";
*/
export function useComponentSize(
input: HTMLElement | null | React.RefObject<HTMLElement | null>
) {
): DOMRect | typeof defaultRect {
const element = input instanceof HTMLElement ? input : input?.current;
const [size, setSize] = useState<DOMRect | undefined>(
() => element?.getBoundingClientRect() || new DOMRect()
);
const [size, setSize] = useState(() => element?.getBoundingClientRect());
useLayoutEffect(() => {
const sizeObserver = new ResizeObserver(() => {
element?.dispatchEvent(new CustomEvent("resize"));
});
if (element) {
sizeObserver.observe(element);
}
return () => sizeObserver.disconnect();
}, [element]);
useLayoutEffect(() => {
const handleResize = () => {
@@ -36,7 +55,6 @@ export function useComponentSize(
window.addEventListener("click", handleResize);
window.addEventListener("resize", handleResize);
element?.addEventListener("resize", handleResize);
handleResize();
return () => {
window.removeEventListener("click", handleResize);
@@ -45,15 +63,5 @@ export function useComponentSize(
};
});
useLayoutEffect(() => {
const sizeObserver = new ResizeObserver(() => {
element?.dispatchEvent(new CustomEvent("resize"));
});
if (element) {
sizeObserver.observe(element);
}
return () => sizeObserver.disconnect();
}, [element]);
return size ?? new DOMRect();
return size ?? defaultRect;
}
+729 -719
View File
File diff suppressed because it is too large Load Diff