mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
10 Commits
fix/react-key
...
v0.77.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 07ae7fca81 | |||
| e2a2145bcd | |||
| 76030f8951 | |||
| f01aa4a0d5 | |||
| f4c7a99a36 | |||
| 2ec2c2b299 | |||
| b806ceaa56 | |||
| 19af5ef09e | |||
| 34faf434d4 | |||
| da5e5c0ca0 |
@@ -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
|
||||
|
||||
@@ -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
@@ -367,5 +367,5 @@
|
||||
"qs": "6.9.7",
|
||||
"rollup": "^4.5.1"
|
||||
},
|
||||
"version": "0.77.1"
|
||||
"version": "0.77.3"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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}`,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user