Compare commits

...

10 Commits

Author SHA1 Message Date
Tom Moor 07ae7fca81 0.77.3 2024-07-14 12:17:34 -04:00
Tom Moor e2a2145bcd fix: Unable to scroll until multiple comments (#7112)
* fix: Unable to scroll in comments
fix: Missing highlighted text on first comment while composing

* docs
2024-07-14 12:16:53 -04:00
Tom Moor 76030f8951 fix: parseAttachmentIds error when node data contains empty href/src
closes #7230
2024-07-14 12:15:22 -04:00
Tom Moor f01aa4a0d5 Cast node attrs to string 2024-07-14 12:15:11 -04:00
Tom Moor f4c7a99a36 0.77.2 2024-06-17 22:02:38 -04:00
Michael Fowler 2ec2c2b299 fix: Use the default credential strategy in S3Client construction (#7061)
By omitting this option, we fall back to the hierarchy used by S3Client by
default.  When defined, the provider chain will use the values of
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (and AWS_SESSION_TOKEN); in their
absence, the provider chain can retrieve credentials from a range of other
sources, including e.g. ECS credentials.

Although there are no longer any application reads from `env.AWS_ACCESS_KEY_ID`
and `env.AWS_SECRET_ACCESS_KEY`, they continue to serve a useful documentary
role.
2024-06-17 22:02:22 -04:00
Tom Moor b806ceaa56 fix: Attributes lost creating template on server (#7049) 2024-06-17 22:02:08 -04:00
Tom Moor 19af5ef09e fix: Signed file urls not returning inline content disposition 2024-06-17 22:02:02 -04:00
Tom Moor 34faf434d4 fix: Escape does not close CMD+K when viewing a document or collection 2024-06-17 22:01:37 -04:00
Tom Moor da5e5c0ca0 fix: Tweak top padding on TOC to always align with metadata 2024-06-17 22:01:28 -04:00
18 changed files with 118 additions and 56 deletions
@@ -59,6 +59,9 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
useKeyDown(
"Escape",
(ev) => {
if (!visible) {
return;
}
ev.preventDefault();
ev.stopImmediatePropagation();
@@ -67,6 +67,9 @@ function SharePopover({
useKeyDown(
"Escape",
(ev) => {
if (!visible) {
return;
}
ev.preventDefault();
ev.stopImmediatePropagation();
@@ -24,6 +24,7 @@ import useOnClickOutside from "~/hooks/useOnClickOutside";
import useStores from "~/hooks/useStores";
import CommentEditor from "./CommentEditor";
import { Bubble } from "./CommentThreadItem";
import { HighlightedText } from "./HighlightText";
type Props = {
/** Callback when the draft should be saved. */
@@ -42,6 +43,8 @@ type Props = {
standalone?: boolean;
/** Whether to animate the comment form in and out */
animatePresence?: boolean;
/** Text to highlight at the top of the comment */
highlightedText?: string;
/** The text direction of the editor */
dir?: "rtl" | "ltr";
/** Callback when the user is typing in the editor */
@@ -64,6 +67,7 @@ function CommentForm({
standalone,
placeholder,
animatePresence,
highlightedText,
dir,
...rest
}: Props) {
@@ -274,6 +278,9 @@ function CommentForm({
$firstOfThread={standalone}
column
>
{highlightedText && (
<HighlightedText>{highlightedText}</HighlightedText>
)}
<CommentEditor
key={`${forceRender}`}
ref={editorRef}
@@ -210,6 +210,9 @@ function CommentThread({
standalone={commentsInThread.length === 0}
dir={document.dir}
autoFocus={autoFocus}
highlightedText={
commentsInThread.length === 0 ? highlightedText : undefined
}
/>
</Fade>
)}
@@ -19,8 +19,9 @@ import Text from "~/components/Text";
import Time from "~/components/Time";
import useBoolean from "~/hooks/useBoolean";
import CommentMenu from "~/menus/CommentMenu";
import { hover, truncateMultiline } from "~/styles";
import { hover } from "~/styles";
import CommentEditor from "./CommentEditor";
import { HighlightedText } from "./HighlightText";
/**
* Hook to calculate if we should display a timestamp on a comment
@@ -127,12 +128,12 @@ function CommentThreadItem({
const handleCancel = () => {
setData(toJS(comment.data));
setReadOnly();
setForceRender((s) => ++s);
setForceRender((i) => ++i);
};
React.useEffect(() => {
setData(toJS(comment.data));
setForceRender((s) => ++s);
setForceRender((i) => ++i);
}, [comment.data]);
return (
@@ -240,28 +241,6 @@ const Body = styled.form`
border-radius: 2px;
`;
const HighlightedText = styled(Text)`
position: relative;
color: ${s("textSecondary")};
font-size: 14px;
padding: 0 8px;
margin: 4px 0;
display: inline-block;
${truncateMultiline(3)}
&:after {
content: "";
width: 2px;
position: absolute;
left: 0;
top: 2px;
bottom: 2px;
background: ${s("commentMarkBackground")};
border-radius: 2px;
}
`;
const Menu = styled(CommentMenu)<{ dir?: "rtl" | "ltr" }>`
position: absolute;
left: ${(props) => (props.dir !== "rtl" ? "auto" : "4px")};
@@ -42,7 +42,6 @@ function Comments() {
.threadsInDocument(document.id)
.filter((thread) => !thread.isNew || thread.createdById === user.id);
const hasComments = threads.length > 0;
const hasMultipleComments = comments.inDocument(document.id).length > 1;
return (
<Sidebar
@@ -52,7 +51,6 @@ function Comments() {
>
<Scrollable
id="comments"
overflow={hasMultipleComments ? undefined : "initial"}
bottomShadow={!focusedComment}
hiddenScrollbars
topShadow
+1 -1
View File
@@ -107,7 +107,7 @@ const Sticky = styled.div`
background: ${s("background")};
transition: ${s("backgroundTransition")};
margin-top: 80px;
margin-top: calc(50px + 6vh);
margin-right: 52px;
min-width: 204px;
width: 228px;
@@ -0,0 +1,29 @@
import styled from "styled-components";
import { s } from "@shared/styles";
import Text from "~/components/Text";
import { truncateMultiline } from "~/styles";
/**
* Highlighted text associated with a comment.
*/
export const HighlightedText = styled(Text)`
position: relative;
color: ${s("textSecondary")};
font-size: 14px;
padding: 0 8px;
margin: 4px 0;
display: inline-block;
${truncateMultiline(3)}
&:after {
content: "";
width: 2px;
position: absolute;
left: 0;
top: 2px;
bottom: 2px;
background: ${s("commentMarkBackground")};
border-radius: 2px;
}
`;
+1 -1
View File
@@ -367,5 +367,5 @@
"qs": "6.9.7",
"rollup": "^4.5.1"
},
"version": "0.77.1"
"version": "0.77.3"
}
+14 -16
View File
@@ -72,34 +72,32 @@ router.get(
async (ctx: APIContext<T.FilesGetReq>) => {
const actor = ctx.state.auth.user;
const key = getKeyFromContext(ctx);
const forceDownload = !!ctx.input.query.download;
const isSignedRequest = !!ctx.input.query.sig;
const { isPublicBucket, fileName } = AttachmentHelper.parseKey(key);
const skipAuthorize = isPublicBucket || isSignedRequest;
const cacheHeader = "max-age=604800, immutable";
let contentType =
(fileName ? mime.lookup(fileName) : undefined) ||
"application/octet-stream";
if (skipAuthorize) {
ctx.set("Cache-Control", cacheHeader);
ctx.set(
"Content-Type",
(fileName ? mime.lookup(fileName) : undefined) ||
"application/octet-stream"
);
ctx.attachment(fileName);
} else {
if (!skipAuthorize) {
const attachment = await Attachment.findOne({
where: { key },
rejectOnEmpty: true,
});
authorize(actor, "read", attachment);
ctx.set("Cache-Control", cacheHeader);
ctx.set("Content-Type", attachment.contentType);
ctx.attachment(attachment.name, {
type: FileStorage.getContentDisposition(attachment.contentType),
});
contentType = attachment.contentType;
}
ctx.set("Cache-Control", cacheHeader);
ctx.set("Content-Type", contentType);
ctx.attachment(fileName, {
type: forceDownload
? "attachment"
: FileStorage.getContentDisposition(contentType),
});
// Handle byte range requests
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
const stats = await (FileStorage as LocalStorage).stat(key);
+1
View File
@@ -24,6 +24,7 @@ export const FilesGetSchema = z.object({
.optional()
.transform((val) => (val ? ValidateKey.sanitize(val) : undefined)),
sig: z.string().optional(),
download: z.string().optional(),
})
.refine((obj) => !(isEmpty(obj.key) && isEmpty(obj.sig)), {
message: "One of key or sig is required",
+13
View File
@@ -1,6 +1,7 @@
import { Transaction } from "sequelize";
import { Optional } from "utility-types";
import { Document, Event, User } from "@server/models";
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
import { TextHelper } from "@server/models/helpers/TextHelper";
type Props = Optional<
@@ -58,6 +59,12 @@ export default async function documentCreator({
}: Props): Promise<Document> {
const templateId = templateDocument ? templateDocument.id : undefined;
if (state && templateDocument) {
throw new Error(
"State cannot be set when creating a document from a template"
);
}
if (urlId) {
const existing = await Document.unscoped().findOne({
attributes: ["id"],
@@ -103,6 +110,12 @@ export default async function documentCreator({
ip,
transaction
),
content: templateDocument
? ProsemirrorHelper.replaceTemplateVariables(
templateDocument.content,
user
)
: undefined,
state,
},
{
+34 -3
View File
@@ -19,7 +19,9 @@ import { schema, parser } from "@server/editor";
import Logger from "@server/logging/Logger";
import { trace } from "@server/logging/tracing";
import Attachment from "@server/models/Attachment";
import User from "@server/models/User";
import FileStorage from "@server/storage/files";
import { TextHelper } from "./TextHelper";
export type HTMLOptions = {
/** A title, if it should be included */
@@ -172,6 +174,29 @@ export class ProsemirrorHelper {
return removeMarksInner(data);
}
/**
* Replaces all template variables in the node.
*
* @param data The ProsemirrorData object to replace variables in
* @param user The user to use for replacing variables
* @returns The content with variables replaced
*/
static replaceTemplateVariables(data: ProsemirrorData, user: User) {
function replace(node: ProsemirrorData) {
if (node.type === "text" && node.text) {
node.text = TextHelper.replaceTemplateVariables(node.text, user);
}
if (node.content) {
node.content.forEach(replace);
}
return node;
}
return replace(data);
}
/**
* Returns the document as a plain JSON object with attachment URLs signed.
*
@@ -260,14 +285,20 @@ export class ProsemirrorHelper {
doc.descendants((node) => {
node.marks.forEach((mark) => {
if (mark.type.name === "link") {
urls.push(mark.attrs.href);
if (mark.attrs.href) {
urls.push(mark.attrs.href);
}
}
});
if (["image", "video"].includes(node.type.name)) {
urls.push(node.attrs.src);
if (node.attrs.src) {
urls.push(node.attrs.src);
}
}
if (node.type.name === "attachment") {
urls.push(node.attrs.href);
if (node.attrs.href) {
urls.push(node.attrs.href);
}
}
});
+2 -1
View File
@@ -32,7 +32,8 @@ export class TextHelper {
return text
.replace(/{date}/g, startCase(getCurrentDateAsString(locales)))
.replace(/{time}/g, startCase(getCurrentTimeAsString(locales)))
.replace(/{datetime}/g, startCase(getCurrentDateTimeAsString(locales)));
.replace(/{datetime}/g, startCase(getCurrentDateTimeAsString(locales)))
.replace(/{author}/g, user.name);
}
/**
-4
View File
@@ -28,10 +28,6 @@ export default class S3Storage extends BaseStorage {
this.client = new S3Client({
bucketEndpoint: env.AWS_S3_ACCELERATE_URL ? true : false,
forcePathStyle: env.AWS_S3_FORCE_PATH_STYLE,
credentials: {
accessKeyId: env.AWS_ACCESS_KEY_ID || "",
secretAccessKey: env.AWS_SECRET_ACCESS_KEY || "",
},
region: env.AWS_REGION,
endpoint: this.getEndpoint(),
});
+1 -1
View File
@@ -63,7 +63,7 @@ export default class Attachment extends Node {
download: node.attrs.title,
"data-size": node.attrs.size,
},
node.attrs.title,
String(node.attrs.title),
],
toPlainText: (node) => node.attrs.title,
};
+1 -1
View File
@@ -59,7 +59,7 @@ export default class Mention extends Extension {
"data-actorId": node.attrs.actorId,
"data-url": `mention://${node.attrs.id}/${node.attrs.type}/${node.attrs.modelId}`,
},
node.attrs.label,
String(node.attrs.label),
],
toPlainText: (node) => `@${node.attrs.label}`,
};
+1 -1
View File
@@ -71,7 +71,7 @@ export default class Video extends Node {
width: node.attrs.width,
height: node.attrs.height,
},
node.attrs.title,
String(node.attrs.title),
],
],
toPlainText: (node) => node.attrs.title,