Compare commits

..

4 Commits

Author SHA1 Message Date
Tom Moor 848260b22e Add webpackStatsFile 2025-04-12 12:42:24 -04:00
Tom Moor a5ffeff42c fix: bundle-size job not triggering 2025-04-12 12:27:44 -04:00
Tom Moor 98aa4d34c8 Remove vestigial referenecs to Prism 2025-04-12 10:16:02 -04:00
Tom Moor 2f1e137062 Move editor syntax highlighting to async, add a bunch more languages 2025-04-12 00:29:51 -04:00
10 changed files with 166 additions and 160 deletions
@@ -1,6 +1,5 @@
import { Plugin, PluginKey } from "prosemirror-state";
import Extension from "@shared/editor/lib/Extension";
import { isList } from "@shared/editor/queries/isList";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
/**
@@ -19,25 +18,17 @@ export default class ClipboardTextSerializer extends Extension {
new Plugin({
key: new PluginKey("clipboardTextSerializer"),
props: {
clipboardTextSerializer: (slice, view) => {
clipboardTextSerializer: (slice) => {
const isMultiline = slice.content.childCount > 1;
// This is a cheap way to determine if the content is "complex",
// aka it has multiple marks or formatting. In which case we'll use
// markdown formatting
const hasMultipleListItems = slice.content.content
.filter((node) => node.content.content.length > 1)
.some((node) => isList(node, view.state.schema));
const hasMultipleBlockTypes =
[
...new Set(
slice.content.content
.filter((node) => node.content.content.length > 1)
.map((node) => node.type.name)
),
].length > 1;
const copyAsMarkdown =
isMultiline || hasMultipleBlockTypes || hasMultipleListItems;
isMultiline ||
slice.content.content.some(
(node) => node.content.content.length > 1
);
return copyAsMarkdown
? mdSerializer.serialize(slice.content, {
+105 -90
View File
@@ -1,19 +1,19 @@
import invariant from "invariant";
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { useState, useRef } from "react";
import AvatarEditor from "react-avatar-editor";
import Dropzone from "react-dropzone";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import { AttachmentPreset } from "@shared/types";
import { AttachmentValidation } from "@shared/validations";
import RootStore from "~/stores/RootStore";
import ButtonLarge from "~/components/ButtonLarge";
import Flex from "~/components/Flex";
import LoadingIndicator from "~/components/LoadingIndicator";
import Modal from "~/components/Modal";
import useStores from "~/hooks/useStores";
import withStores from "~/components/withStores";
import { compressImage } from "~/utils/compressImage";
import { uploadFile, dataUrlToBlob } from "~/utils/files";
@@ -24,38 +24,41 @@ export type Props = {
borderRadius?: number;
};
const ImageUpload: React.FC<Props> = ({
onSuccess,
onError,
submitText,
borderRadius = 150,
children,
}) => {
const { ui } = useStores();
const { t } = useTranslation();
submitText || t("Crop image");
@observer
class ImageUpload extends React.Component<RootStore & Props> {
@observable
isUploading = false;
const [isUploading, setIsUploading] = useState(false);
const [isCropping, setIsCropping] = useState(false);
const [zoom, setZoom] = useState(1);
const [file, setFile] = useState<File | null>(null);
@observable
isCropping = false;
const avatarEditorRef = useRef<AvatarEditor>(null);
@observable
zoom = 1;
const onDropAccepted = async (files: File[]) => {
setIsCropping(true);
setFile(files[0]);
@observable
file: File;
avatarEditorRef = React.createRef<AvatarEditor>();
static defaultProps = {
submitText: "Crop Image",
borderRadius: 150,
};
const handleCrop = () => {
setIsUploading(true);
onDropAccepted = async (files: File[]) => {
this.isCropping = true;
this.file = files[0];
};
handleCrop = () => {
this.isUploading = true;
// allow the UI to update before converting the canvas to a Blob
// for large images this can cause the page rendering to hang.
setTimeout(uploadImage, 0);
setTimeout(this.uploadImage, 0);
};
const uploadImage = async () => {
const canvas = avatarEditorRef.current?.getImage();
uploadImage = async () => {
const canvas = this.avatarEditorRef.current?.getImage();
invariant(canvas, "canvas is not defined");
const imageBlob = dataUrlToBlob(canvas.toDataURL());
@@ -65,87 +68,99 @@ const ImageUpload: React.FC<Props> = ({
maxWidth: 512,
});
const attachment = await uploadFile(compressed, {
name: file!.name,
name: this.file.name,
preset: AttachmentPreset.Avatar,
});
void onSuccess(attachment.url);
void this.props.onSuccess(attachment.url);
} catch (err) {
onError(err.message);
this.props.onError(err.message);
} finally {
setIsUploading(false);
setIsCropping(false);
this.isUploading = false;
this.isCropping = false;
}
};
const handleClose = () => {
setIsUploading(false);
setIsCropping(false);
handleClose = () => {
this.isUploading = false;
this.isCropping = false;
};
const handleZoom = (event: React.ChangeEvent<HTMLInputElement>) => {
handleZoom = (event: React.ChangeEvent<HTMLInputElement>) => {
const target = event.target;
if (target instanceof HTMLInputElement) {
setZoom(parseFloat(target.value));
this.zoom = parseFloat(target.value);
}
};
const renderCropping = () => (
<Modal
onRequestClose={handleClose}
fullscreen={false}
title={<>&nbsp;</>}
isOpen
>
<Flex auto column align="center" justify="center">
{isUploading && <LoadingIndicator />}
<AvatarEditorContainer>
<AvatarEditor
ref={avatarEditorRef}
image={file!}
width={250}
height={250}
border={25}
borderRadius={borderRadius}
color={ui.theme === "light" ? [255, 255, 255, 0.6] : [0, 0, 0, 0.6]} // RGBA
scale={zoom}
rotate={0}
/>
</AvatarEditorContainer>
<RangeInput
type="range"
min="0.1"
max="2"
step="0.01"
defaultValue="1"
onChange={handleZoom}
/>
<br />
<ButtonLarge fullwidth onClick={handleCrop} disabled={isUploading}>
{isUploading ? `${t(`Uploading`)}` : submitText}
</ButtonLarge>
</Flex>
</Modal>
);
renderCropping() {
const { ui, submitText } = this.props;
if (isCropping && file) {
return renderCropping();
return (
<Modal
onRequestClose={this.handleClose}
fullscreen={false}
title={<>&nbsp;</>}
isOpen
>
<Flex auto column align="center" justify="center">
{this.isUploading && <LoadingIndicator />}
<AvatarEditorContainer>
<AvatarEditor
ref={this.avatarEditorRef}
image={this.file}
width={250}
height={250}
border={25}
borderRadius={this.props.borderRadius}
color={
ui.theme === "light" ? [255, 255, 255, 0.6] : [0, 0, 0, 0.6]
} // RGBA
scale={this.zoom}
rotate={0}
/>
</AvatarEditorContainer>
<RangeInput
type="range"
min="0.1"
max="2"
step="0.01"
defaultValue="1"
onChange={this.handleZoom}
/>
<br />
<ButtonLarge
fullwidth
onClick={this.handleCrop}
disabled={this.isUploading}
>
{this.isUploading ? "Uploading…" : submitText}
</ButtonLarge>
</Flex>
</Modal>
);
}
return (
<Dropzone
accept={AttachmentValidation.avatarContentTypes.join(", ")}
onDropAccepted={onDropAccepted}
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{children}
</div>
)}
</Dropzone>
);
};
render() {
if (this.isCropping) {
return this.renderCropping();
}
return (
<Dropzone
accept={AttachmentValidation.avatarContentTypes.join(", ")}
onDropAccepted={this.onDropAccepted}
>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
{this.props.children}
</div>
)}
</Dropzone>
);
}
}
const AvatarEditorContainer = styled(Flex)`
margin-bottom: 30px;
@@ -176,4 +191,4 @@ const RangeInput = styled.input`
}
`;
export default observer(ImageUpload);
export default withStores(ImageUpload);
+1 -1
View File
@@ -248,7 +248,7 @@
"uuid": "^8.3.2",
"validator": "13.12.0",
"vaul": "^1.1.2",
"vite": "^5.4.18",
"vite": "^5.4.17",
"vite-plugin-pwa": "^0.20.3",
"winston": "^3.17.0",
"ws": "^7.5.10",
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+11 -2
View File
@@ -2,7 +2,7 @@ import invariant from "invariant";
import filter from "lodash/filter";
import { CollectionPermission } from "@shared/types";
import { Collection, User, Team } from "@server/models";
import { allow } from "./cancan";
import { allow, can } from "./cancan";
import { and, isTeamAdmin, isTeamModel, isTeamMutable, or } from "./utils";
allow(User, "createCollection", Team, (actor, team) =>
@@ -67,6 +67,15 @@ allow(
}
);
allow(User, "export", Collection, (actor, collection) =>
and(
//
can(actor, "read", collection),
!actor.isViewer,
!actor.isGuest
)
);
allow(User, "share", Collection, (user, collection) => {
if (
!collection ||
@@ -152,7 +161,7 @@ allow(
}
);
allow(User, ["update", "export", "archive"], Collection, (user, collection) =>
allow(User, ["update", "archive"], Collection, (user, collection) =>
and(
!!collection,
!!collection?.isActive,
-7
View File
@@ -1335,13 +1335,6 @@ mark {
position: relative;
}
.code-block[data-language=none],
.code-block[data-language=markdown] {
pre code {
color: ${props.theme.text};
}
}
.code-block[data-language=mermaidjs] {
margin: 0.75em 0;
+1 -2
View File
@@ -1,4 +1,3 @@
import { BrowserIcon } from "outline-icons";
import * as React from "react";
import styled from "styled-components";
import { Primitive } from "utility-types";
@@ -666,7 +665,7 @@ const embeds: EmbedDescriptor[] = [
title: "Embed",
keywords: "iframe webpage",
placeholder: "Paste a URL to embed",
icon: <BrowserIcon />,
icon: <Img src="/images/embed.png" alt="Embed" />,
defaultHidden: false,
matchOnInput: false,
regexMatch: [new RegExp("^https?://(.*)$")],
+37 -37
View File
@@ -86,6 +86,19 @@ function getDecorations({
blocks.forEach((block) => {
let startPos = block.pos + 1;
const language = getRefractorLangForLanguage(block.node.attrs.language);
if (!language) {
return;
}
// If the language isn't registered yet, trigger loading it
if (!refractor.registered(language)) {
languagesToImport.add(language);
return;
} else {
languagesToImport.delete(language);
}
const lineDecorations = [];
if (!cache[block.pos] || !cache[block.pos].node.eq(block.node)) {
@@ -114,48 +127,35 @@ function getDecorations({
);
}
const nodes = refractor.highlight(block.node.textContent, language);
const newDecorations = parseNodes(nodes)
.map((node: ParsedNode) => {
const from = startPos;
const to = from + node.text.length;
startPos = to;
return {
...node,
from,
to,
};
})
.filter((node) => node.classes && node.classes.length)
.map((node) =>
Decoration.inline(node.from, node.to, {
class: node.classes.join(" "),
})
)
.concat(lineDecorations);
cache[block.pos] = {
node: block.node,
decorations: lineDecorations,
decorations: newDecorations,
};
if (!language) {
// do nothing
} else if (refractor.registered(language)) {
languagesToImport.delete(language);
const nodes = refractor.highlight(block.node.textContent, language);
const newDecorations = parseNodes(nodes)
.map((node: ParsedNode) => {
const from = startPos;
const to = from + node.text.length;
startPos = to;
return {
...node,
from,
to,
};
})
.filter((node) => node.classes && node.classes.length)
.map((node) =>
Decoration.inline(node.from, node.to, {
class: node.classes.join(" "),
})
)
.concat(lineDecorations);
cache[block.pos] = {
node: block.node,
decorations: newDecorations,
};
} else {
languagesToImport.add(language);
}
}
cache[block.pos]?.decorations.forEach((decoration) => {
cache[block.pos].decorations.forEach((decoration) => {
decorations.push(decoration);
});
});
+2 -3
View File
@@ -893,8 +893,6 @@
"No people left to add": "No people left to add",
"Date created": "Date created",
"Upload": "Upload",
"Crop image": "Crop image",
"Uploading": "Uploading",
"How does this work?": "How does this work?",
"You can import a zip file that was previously exported from the JSON option in another instance. In {{ appName }}, open <em>Export</em> in the Settings sidebar and click on <em>Export Data</em>.": "You can import a zip file that was previously exported from the JSON option in another instance. In {{ appName }}, open <em>Export</em> in the Settings sidebar and click on <em>Export Data</em>.",
"Drag and drop the zip file from the JSON export option in {{appName}}, or click to upload": "Drag and drop the zip file from the JSON export option in {{appName}}, or click to upload",
@@ -1155,5 +1153,6 @@
"You updated {{ timeAgo }}": "You updated {{ timeAgo }}",
"{{ user }} updated {{ timeAgo }}": "{{ user }} updated {{ timeAgo }}",
"You created {{ timeAgo }}": "You created {{ timeAgo }}",
"{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}"
"{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}",
"Uploading": "Uploading"
}
+4 -4
View File
@@ -15605,10 +15605,10 @@ 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@^5.4.17:
version "5.4.17"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.17.tgz#4bf61dd4cdbf64b0d6661f5dba76954cc81d5082"
integrity sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"