Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5fa46929b | ||
|
|
e866571e04 | ||
|
|
3c3fc7cd6d |
@@ -404,11 +404,7 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the cycle issues
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk"),
|
||||
).delete()
|
||||
cycle.delete(soft=False)
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="cycle",
|
||||
|
||||
@@ -490,12 +490,9 @@ class CycleViewSet(BaseViewSet):
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the cycle issues
|
||||
CycleIssue.objects.filter(
|
||||
cycle_id=self.kwargs.get("pk"),
|
||||
).delete()
|
||||
# TODO: Soft delete the cycle break the onetoone relationship with cycle issue
|
||||
cycle.delete(soft=False)
|
||||
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
user=request.user,
|
||||
|
||||
@@ -312,8 +312,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(labels__deleted_at__isnull=True)
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__labels__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@@ -322,8 +322,8 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
filter=~Q(issue__assignees__id__isnull=True)
|
||||
& Q(issue__assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@@ -60,8 +60,8 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
.annotate(
|
||||
cycle_id=Case(
|
||||
When(
|
||||
issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("issue_cycle__cycle_id"),
|
||||
draft_issue_cycle__cycle__deleted_at__isnull=True,
|
||||
then=F("draft_issue_cycle__cycle_id"),
|
||||
),
|
||||
default=None,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TReadOnlyFileHandler } from "@/types";
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TFileHandler } from "@/types";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
id: string;
|
||||
@@ -19,7 +19,7 @@ interface IDocumentReadOnlyEditor {
|
||||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
embedHandler: any;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
tabIndex?: number;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
mentionHandler: {
|
||||
|
||||
@@ -11,12 +11,12 @@ import { CustomCodeInlineExtension } from "./code-inline";
|
||||
import { CustomLinkExtension } from "./custom-link";
|
||||
import { CustomHorizontalRule } from "./horizontal-rule";
|
||||
import { ImageExtensionWithoutProps } from "./image";
|
||||
import { CustomImageComponentWithoutProps } from "./image/image-component-without-props";
|
||||
import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props";
|
||||
import { CustomMentionWithoutProps } from "./mentions/mentions-without-props";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { TableHeader, TableCell, TableRow, Table } from "./table";
|
||||
import { CustomColorExtension } from "./custom-color";
|
||||
import { CustomImageExtensionConfig } from "./custom-image/extension-config";
|
||||
|
||||
export const CoreEditorExtensionsWithoutProps = [
|
||||
StarterKit.configure({
|
||||
@@ -63,7 +63,7 @@ export const CoreEditorExtensionsWithoutProps = [
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomImageExtensionConfig(),
|
||||
CustomImageComponentWithoutProps(),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { Editor, NodeViewWrapper } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image";
|
||||
|
||||
export type CustomImageNodeViewProps = NodeViewProps & {
|
||||
export type CustomImageNodeViewProps = {
|
||||
getPos: () => number;
|
||||
editor: Editor;
|
||||
node: NodeViewProps["node"] & {
|
||||
node: ProsemirrorNode & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
updateAttributes: (attrs: Record<string, any>) => void;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Editor, mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// extensions
|
||||
import { CustomImageExtensionConfig, CustomImageNode, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
import { CustomImageNode } from "@/extensions/custom-image";
|
||||
// plugins
|
||||
import { TrackImageDeletionPlugin, TrackImageRestorationPlugin, isFileValid } from "@/plugins/image";
|
||||
// types
|
||||
@@ -25,18 +27,80 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
}
|
||||
|
||||
export const getImageComponentImageFileMap = (editor: Editor) =>
|
||||
(editor.storage.imageComponent as UploadImageExtensionStorage | undefined)?.fileMap;
|
||||
|
||||
export interface UploadImageExtensionStorage {
|
||||
fileMap: Map<string, UploadEntity>;
|
||||
}
|
||||
|
||||
export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean };
|
||||
|
||||
export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const {
|
||||
getAssetSrc,
|
||||
upload,
|
||||
delete: deleteImage,
|
||||
restore: restoreImage,
|
||||
validation: { maxFileSize },
|
||||
} = props;
|
||||
|
||||
return CustomImageExtensionConfig(props).extend({
|
||||
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
name: "imageComponent",
|
||||
selectable: true,
|
||||
group: "block",
|
||||
atom: true,
|
||||
draggable: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: "35%",
|
||||
},
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
default: "auto",
|
||||
},
|
||||
["id"]: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "image-component",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
|
||||
@@ -51,6 +115,15 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
];
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
maxFileSize,
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
insertImageComponent:
|
||||
@@ -106,6 +179,7 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const fileUrl = await upload(file);
|
||||
return fileUrl;
|
||||
},
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Editor, mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
// types
|
||||
import { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||
|
||||
export const getImageComponentImageFileMap = (editor: Editor) =>
|
||||
(editor.storage.imageComponent as UploadImageExtensionStorage | undefined)?.fileMap;
|
||||
|
||||
export interface UploadImageExtensionStorage {
|
||||
fileMap: Map<string, UploadEntity>;
|
||||
}
|
||||
|
||||
export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean };
|
||||
|
||||
type Props = TReadOnlyFileHandler & Pick<TFileHandler, "validation">;
|
||||
|
||||
const fallbackProps: Props = {
|
||||
getAssetSrc: () => "",
|
||||
restore: async () => {},
|
||||
validation: {
|
||||
maxFileSize: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomImageExtensionConfig = (props: Props = fallbackProps) => {
|
||||
const {
|
||||
getAssetSrc,
|
||||
restore: restoreImage,
|
||||
validation: { maxFileSize },
|
||||
} = props;
|
||||
|
||||
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
name: "imageComponent",
|
||||
group: "block",
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: "35%",
|
||||
},
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
default: "auto",
|
||||
},
|
||||
["id"]: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "image-component",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
onCreate(this) {
|
||||
const imageSources = new Set<string>();
|
||||
this.editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === this.name) {
|
||||
imageSources.add(node.attrs.src);
|
||||
}
|
||||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
maxFileSize,
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./components";
|
||||
export * from "./custom-image";
|
||||
export * from "./extension-config";
|
||||
export * from "./read-only-custom-image";
|
||||
|
||||
@@ -1,20 +1,68 @@
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// components
|
||||
import { CustomImageExtensionConfig, CustomImageNode } from "@/extensions/custom-image";
|
||||
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
|
||||
// types
|
||||
import { TReadOnlyFileHandler } from "@/types";
|
||||
import { TFileHandler } from "@/types";
|
||||
|
||||
export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) =>
|
||||
CustomImageExtensionConfig({
|
||||
...props,
|
||||
validation: {
|
||||
maxFileSize: 5 * 1024 * 1024,
|
||||
},
|
||||
}).extend({
|
||||
export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
|
||||
const { getAssetSrc } = props;
|
||||
|
||||
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
name: "imageComponent",
|
||||
selectable: false,
|
||||
group: "block",
|
||||
atom: true,
|
||||
draggable: false,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: "35%",
|
||||
},
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
default: "auto",
|
||||
},
|
||||
["id"]: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "image-component",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomImageNode);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
CustomLinkExtension,
|
||||
CustomMention,
|
||||
CustomQuoteExtension,
|
||||
CustomToggleHeadingExtension,
|
||||
CustomTypographyExtension,
|
||||
DropHandlerExtension,
|
||||
ImageExtension,
|
||||
@@ -155,5 +156,6 @@ export const CoreEditorExtensions = (args: TArguments) => {
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomColorExtension,
|
||||
CustomToggleHeadingExtension,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
// extensions
|
||||
import { UploadImageExtensionStorage } from "@/extensions";
|
||||
|
||||
export const CustomImageComponentWithoutProps = () =>
|
||||
Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
name: "imageComponent",
|
||||
selectable: true,
|
||||
group: "block",
|
||||
atom: true,
|
||||
draggable: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: "35%",
|
||||
},
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
default: "auto",
|
||||
},
|
||||
["id"]: {
|
||||
default: null,
|
||||
},
|
||||
aspectRatio: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: "image-component",
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["image-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
fileMap: new Map(),
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default CustomImageComponentWithoutProps;
|
||||
@@ -3,9 +3,9 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomImageNode } from "@/extensions";
|
||||
// types
|
||||
import { TReadOnlyFileHandler } from "@/types";
|
||||
import { TFileHandler } from "@/types";
|
||||
|
||||
export const ReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
|
||||
export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
|
||||
const { getAssetSrc } = props;
|
||||
|
||||
return Image.extend({
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from "./issue-embed";
|
||||
export * from "./mentions";
|
||||
export * from "./slash-commands";
|
||||
export * from "./table";
|
||||
export * from "./toggle-heading";
|
||||
export * from "./typography";
|
||||
export * from "./core-without-props";
|
||||
export * from "./custom-code-inline";
|
||||
|
||||
@@ -22,14 +22,15 @@ import {
|
||||
HeadingListExtension,
|
||||
CustomReadOnlyImageExtension,
|
||||
CustomColorExtension,
|
||||
CustomToggleHeadingReadOnlyExtension,
|
||||
} from "@/extensions";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// types
|
||||
import { IMentionHighlight, TReadOnlyFileHandler } from "@/types";
|
||||
import { IMentionHighlight, TFileHandler } from "@/types";
|
||||
|
||||
type Props = {
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
mentionConfig: {
|
||||
mentionHighlights?: () => Promise<IMentionHighlight[]>;
|
||||
};
|
||||
@@ -82,7 +83,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
|
||||
CustomTypographyExtension,
|
||||
ReadOnlyImageExtension({
|
||||
getAssetSrc: fileHandler.getAssetSrc,
|
||||
restore: fileHandler.restore,
|
||||
}).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
@@ -90,7 +90,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
|
||||
}),
|
||||
CustomReadOnlyImageExtension({
|
||||
getAssetSrc: fileHandler.getAssetSrc,
|
||||
restore: fileHandler.restore,
|
||||
}),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
@@ -126,5 +125,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
|
||||
CharacterCount,
|
||||
CustomColorExtension,
|
||||
HeadingListExtension,
|
||||
CustomToggleHeadingReadOnlyExtension,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Heading6,
|
||||
ImageIcon,
|
||||
List,
|
||||
ListCollapse,
|
||||
ListOrdered,
|
||||
ListTodo,
|
||||
MinusSquare,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
toggleTextColor,
|
||||
toggleBackgroundColor,
|
||||
insertImage,
|
||||
insertToggleHeading,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { CommandProps, ISlashCommandItem } from "@/types";
|
||||
@@ -49,7 +51,8 @@ export const getSlashCommandFilteredSections =
|
||||
({ query }: { query: string }): TSlashCommandSection[] => {
|
||||
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
|
||||
{
|
||||
key: "general",
|
||||
key: "basic",
|
||||
title: "Basic blocks",
|
||||
items: [
|
||||
{
|
||||
commandKey: "text",
|
||||
@@ -193,6 +196,39 @@ export const getSlashCommandFilteredSections =
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "advanced",
|
||||
title: "Advanced blocks",
|
||||
items: [
|
||||
{
|
||||
commandKey: "toggle-heading",
|
||||
key: "toggle-heading-1",
|
||||
title: "Toggle heading 1",
|
||||
icon: <ListCollapse className="size-3.5" />,
|
||||
description: "Insert toggle heading 1",
|
||||
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||
command: ({ editor, range }) => insertToggleHeading(1, editor, range),
|
||||
},
|
||||
{
|
||||
commandKey: "toggle-heading",
|
||||
key: "toggle-heading-2",
|
||||
title: "Toggle heading 2",
|
||||
icon: <ListCollapse className="size-3.5" />,
|
||||
description: "Insert toggle heading 2",
|
||||
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||
command: ({ editor, range }) => insertToggleHeading(2, editor, range),
|
||||
},
|
||||
{
|
||||
commandKey: "toggle-heading",
|
||||
key: "toggle-heading-3",
|
||||
title: "Toggle heading 3",
|
||||
icon: <ListCollapse className="size-3.5" />,
|
||||
description: "Insert toggle heading 3",
|
||||
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||
command: ({ editor, range }) => insertToggleHeading(3, editor, range),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "text-color",
|
||||
title: "Colors",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
// types
|
||||
import { TToggleHeadingBlockAttributes } from "./types";
|
||||
import { cn } from "@/helpers/common";
|
||||
|
||||
type Props = NodeViewProps & {
|
||||
node: NodeViewProps["node"] & {
|
||||
attrs: TToggleHeadingBlockAttributes;
|
||||
};
|
||||
};
|
||||
|
||||
export const CustomToggleHeadingBlock: React.FC<Props> = (props) => {
|
||||
const { node, updateAttributes } = props;
|
||||
// derived values
|
||||
const headingLevel = Number(node.attrs["data-heading-level"] ?? 1);
|
||||
const isToggleOpen = node.attrs["data-toggle-status"] === "open";
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="editor-toggle-heading-component flex items-start gap-1 my-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateAttributes({
|
||||
"data-toggle-status": isToggleOpen ? "close" : "open",
|
||||
});
|
||||
}}
|
||||
className="flex-shrink-0 size-5 grid place-items-center rounded hover:bg-custom-background-80 transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("size-3.5 transition-all", {
|
||||
"rotate-90": isToggleOpen,
|
||||
})}
|
||||
/>
|
||||
</button>
|
||||
<NodeViewContent as="div" className="w-full break-words" />
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Node, mergeAttributes } from "@tiptap/core";
|
||||
import { Node as NodeType } from "@tiptap/pm/model";
|
||||
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||
|
||||
// Extend Tiptap's Commands interface
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
toggleHeadingComponent: {
|
||||
insertToggleHeading: ({ headingLevel }: { headingLevel: number }) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomToggleHeadingExtensionConfig = Node.create({
|
||||
name: "toggleHeadingComponent",
|
||||
group: "block",
|
||||
content: "block+",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
"data-heading-level": {
|
||||
default: 1,
|
||||
},
|
||||
"data-background-color": {
|
||||
default: null,
|
||||
},
|
||||
"data-toggle-status": {
|
||||
default: "close",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
markdown: {
|
||||
serialize(state: MarkdownSerializerState, node: NodeType) {},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "toggle-heading-component" }];
|
||||
},
|
||||
|
||||
// Render HTML for the callout node
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["toggle-heading-component", mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// components
|
||||
import { CustomToggleHeadingBlock } from "./block";
|
||||
// config
|
||||
import { CustomToggleHeadingExtensionConfig } from "./extension-config";
|
||||
// utils
|
||||
import { DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES } from "./utils";
|
||||
|
||||
export const CustomToggleHeadingExtension = CustomToggleHeadingExtensionConfig.extend({
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
insertToggleHeading:
|
||||
({ headingLevel }) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent({
|
||||
type: this.name,
|
||||
content: [
|
||||
{
|
||||
type: "heading",
|
||||
attrs: {
|
||||
level: headingLevel,
|
||||
},
|
||||
},
|
||||
],
|
||||
attrs: {
|
||||
"data-heading-level": headingLevel ?? 1,
|
||||
"data-toggle-status": DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES["data-toggle-status"],
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomToggleHeadingBlock);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./extension";
|
||||
export * from "./read-only-extension";
|
||||
@@ -0,0 +1,3 @@
|
||||
import { CustomToggleHeadingExtensionConfig } from "./extension-config";
|
||||
|
||||
export const CustomToggleHeadingReadOnlyExtension = CustomToggleHeadingExtensionConfig;
|
||||
@@ -0,0 +1,5 @@
|
||||
export type TToggleHeadingBlockAttributes = {
|
||||
"data-heading-level": number | undefined;
|
||||
"data-background-color": string | undefined;
|
||||
"data-toggle-status": "open" | "close" | undefined;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
// types
|
||||
import { TToggleHeadingBlockAttributes } from "./types";
|
||||
|
||||
export const DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES: TToggleHeadingBlockAttributes = {
|
||||
"data-heading-level": undefined,
|
||||
"data-background-color": undefined,
|
||||
"data-toggle-status": "close",
|
||||
};
|
||||
@@ -180,3 +180,8 @@ export const toggleBackgroundColor = (color: string | undefined, editor: Editor,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const insertToggleHeading = (headingLevel: number, editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).insertToggleHeading({ headingLevel }).run();
|
||||
else editor.chain().focus().insertToggleHeading({ headingLevel }).run();
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
||||
// props
|
||||
import { CoreReadOnlyEditorProps } from "@/props";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TReadOnlyFileHandler } from "@/types";
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TFileHandler } from "@/types";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
initialValue?: string;
|
||||
@@ -19,7 +19,7 @@ interface CustomReadOnlyEditorProps {
|
||||
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
|
||||
@@ -43,6 +43,7 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||
".issue-embed",
|
||||
".image-component",
|
||||
".image-upload-component",
|
||||
".editor-toggle-heading-component",
|
||||
].join(", ");
|
||||
|
||||
for (const elem of elements) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
IMentionSuggestion,
|
||||
TExtensions,
|
||||
TFileHandler,
|
||||
TReadOnlyFileHandler,
|
||||
TRealtimeConfig,
|
||||
TUserDetails,
|
||||
} from "@/types";
|
||||
@@ -45,6 +44,6 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||
};
|
||||
|
||||
export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { DeleteImage, RestoreImage, UploadImage } from "@/types";
|
||||
|
||||
export type TReadOnlyFileHandler = {
|
||||
export type TFileHandler = {
|
||||
getAssetSrc: (path: string) => string;
|
||||
restore: RestoreImage;
|
||||
};
|
||||
|
||||
export type TFileHandler = TReadOnlyFileHandler & {
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
restore: RestoreImage;
|
||||
validation: {
|
||||
/**
|
||||
* @description max file size in bytes
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
TExtensions,
|
||||
TFileHandler,
|
||||
TNonColorEditorCommands,
|
||||
TReadOnlyFileHandler,
|
||||
TServerHandler,
|
||||
} from "@/types";
|
||||
// editor refs
|
||||
@@ -109,7 +108,7 @@ export interface IReadOnlyEditorProps {
|
||||
containerClassName?: string;
|
||||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
id: string;
|
||||
initialValue: string;
|
||||
|
||||
@@ -23,7 +23,8 @@ export type TEditorCommands =
|
||||
| "divider"
|
||||
| "issue-embed"
|
||||
| "text-color"
|
||||
| "background-color";
|
||||
| "background-color"
|
||||
| "toggle-heading";
|
||||
|
||||
export type TColorEditorCommands = Extract<TEditorCommands, "text-color" | "background-color">;
|
||||
export type TNonColorEditorCommands = Exclude<TEditorCommands, "text-color" | "background-color">;
|
||||
|
||||
@@ -316,7 +316,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
|
||||
/* tailwind typography */
|
||||
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 2rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
margin-bottom: 4px;
|
||||
font-size: var(--font-size-h1);
|
||||
line-height: var(--line-height-h1);
|
||||
@@ -324,7 +327,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1.4rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1.4rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h2);
|
||||
line-height: var(--line-height-h2);
|
||||
@@ -332,7 +338,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h3);
|
||||
line-height: var(--line-height-h3);
|
||||
@@ -340,7 +349,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h4);
|
||||
line-height: var(--line-height-h4);
|
||||
@@ -348,7 +360,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h5);
|
||||
line-height: var(--line-height-h5);
|
||||
@@ -356,7 +371,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 1rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
font-size: var(--font-size-h6);
|
||||
line-height: var(--line-height-h6);
|
||||
@@ -364,7 +382,14 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
||||
}
|
||||
|
||||
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||
margin-top: 0.25rem;
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
margin-bottom: 1px;
|
||||
padding: 3px 0;
|
||||
font-size: var(--font-size-regular);
|
||||
|
||||
@@ -9,11 +9,10 @@ import { useMention } from "@/hooks/use-mention";
|
||||
|
||||
type LiteTextReadOnlyEditorWrapperProps = Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
|
||||
anchor: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, LiteTextReadOnlyEditorWrapperProps>(
|
||||
({ anchor, workspaceId, ...props }, ref) => {
|
||||
({ anchor, ...props }, ref) => {
|
||||
const { mentionHighlights } = useMention();
|
||||
|
||||
return (
|
||||
@@ -21,7 +20,6 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
|
||||
ref={ref}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
anchor,
|
||||
workspaceId,
|
||||
})}
|
||||
mentionHandler={{
|
||||
highlights: mentionHighlights,
|
||||
|
||||
@@ -9,11 +9,10 @@ import { useMention } from "@/hooks/use-mention";
|
||||
|
||||
type RichTextReadOnlyEditorWrapperProps = Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
|
||||
anchor: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
|
||||
({ anchor, workspaceId, ...props }, ref) => {
|
||||
({ anchor, ...props }, ref) => {
|
||||
const { mentionHighlights } = useMention();
|
||||
|
||||
return (
|
||||
@@ -21,7 +20,6 @@ export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Ric
|
||||
ref={ref}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
anchor,
|
||||
workspaceId,
|
||||
})}
|
||||
mentionHandler={{ highlights: mentionHighlights }}
|
||||
{...props}
|
||||
|
||||
@@ -140,7 +140,6 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
<div className={`${isEditing ? "hidden" : ""}`}>
|
||||
<LiteTextReadOnlyEditor
|
||||
anchor={anchor}
|
||||
workspaceId={workspaceID?.toString() ?? ""}
|
||||
ref={showEditorRef}
|
||||
id={comment.id}
|
||||
initialValue={comment.comment_html}
|
||||
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
|
||||
const { anchor, issueDetails } = props;
|
||||
|
||||
const { project_details, workspace: workspaceId } = usePublish(anchor);
|
||||
const { project_details } = usePublish(anchor);
|
||||
|
||||
const description = issueDetails.description_html;
|
||||
|
||||
@@ -27,7 +27,6 @@ export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
|
||||
{description !== "" && description !== "<p></p>" && (
|
||||
<RichTextReadOnlyEditor
|
||||
anchor={anchor}
|
||||
workspaceId={workspaceId?.toString() ?? ""}
|
||||
id={issueDetails.id}
|
||||
initialValue={
|
||||
!description ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// plane editor
|
||||
import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor";
|
||||
import { TFileHandler } from "@plane/editor";
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
@@ -25,10 +25,11 @@ type TArgs = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the read-only editors
|
||||
* @description this function returns the file handler required by the editors
|
||||
* @param {TArgs} args
|
||||
*/
|
||||
export const getReadOnlyEditorFileHandlers = (args: Pick<TArgs, "anchor" | "workspaceId">): TReadOnlyFileHandler => {
|
||||
const { anchor, workspaceId } = args;
|
||||
export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
const { anchor, uploadFile, workspaceId } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
@@ -39,28 +40,6 @@ export const getReadOnlyEditorFileHandlers = (args: Pick<TArgs, "anchor" | "work
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(anchor, src);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the editors
|
||||
* @param {TArgs} args
|
||||
*/
|
||||
export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
const { anchor, uploadFile, workspaceId } = args;
|
||||
|
||||
return {
|
||||
...getReadOnlyEditorFileHandlers({
|
||||
anchor,
|
||||
workspaceId,
|
||||
}),
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
@@ -69,9 +48,36 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
await fileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? "");
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(anchor, src);
|
||||
}
|
||||
},
|
||||
cancel: fileService.cancelUpload,
|
||||
validation: {
|
||||
maxFileSize: MAX_FILE_SIZE,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the read-only editors
|
||||
*/
|
||||
export const getReadOnlyEditorFileHandlers = (
|
||||
args: Pick<TArgs, "anchor">
|
||||
): { getAssetSrc: TFileHandler["getAssetSrc"] } => {
|
||||
const { anchor } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (checkURLValidity(path)) {
|
||||
return path;
|
||||
} else {
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EditorReadOnlyRefApi, ILiteTextReadOnlyEditor, LiteTextReadOnlyEditorWi
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useMention, useUser, useWorkspace } from "@/hooks/store";
|
||||
import { useMention, useUser } from "@/hooks/store";
|
||||
|
||||
type LiteTextReadOnlyEditorWrapperProps = Omit<ILiteTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
|
||||
workspaceSlug: string;
|
||||
@@ -19,16 +19,12 @@ export const LiteTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, Lit
|
||||
const { mentionHighlights } = useMention({
|
||||
user: currentUser,
|
||||
});
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
|
||||
|
||||
return (
|
||||
<LiteTextReadOnlyEditorWithRef
|
||||
ref={ref}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor, RichTextReadOnlyEditorWi
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useMention, useWorkspace } from "@/hooks/store";
|
||||
import { useMention } from "@/hooks/store";
|
||||
|
||||
type RichTextReadOnlyEditorWrapperProps = Omit<IRichTextReadOnlyEditor, "fileHandler" | "mentionHandler"> & {
|
||||
workspaceSlug: string;
|
||||
@@ -14,18 +14,13 @@ type RichTextReadOnlyEditorWrapperProps = Omit<IRichTextReadOnlyEditor, "fileHan
|
||||
|
||||
export const RichTextReadOnlyEditor = React.forwardRef<EditorReadOnlyRefApi, RichTextReadOnlyEditorWrapperProps>(
|
||||
({ workspaceSlug, projectId, ...props }, ref) => {
|
||||
// store hooks
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
const { mentionHighlights } = useMention({});
|
||||
// derived values
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
|
||||
|
||||
return (
|
||||
<RichTextReadOnlyEditorWithRef
|
||||
ref={ref}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
||||
@@ -54,7 +54,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
|
||||
const comment = getCommentById(commentId);
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(comment?.workspace_detail?.slug ?? "")?.id;
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(comment?.workspace_detail?.slug as string)?.id as string;
|
||||
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
@@ -143,7 +143,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||
}}
|
||||
>
|
||||
<LiteTextEditor
|
||||
workspaceId={workspaceId ?? ""}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
ref={editorRef}
|
||||
|
||||
@@ -37,7 +37,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
const workspaceStore = useWorkspace();
|
||||
const { peekIssue } = useIssueDetail();
|
||||
// derived values
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug ?? "")?.id;
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||
// form info
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -92,7 +92,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<LiteTextEditor
|
||||
workspaceId={workspaceId ?? ""}
|
||||
workspaceId={workspaceId}
|
||||
id={"add_comment_" + issueId}
|
||||
value={"<p></p>"}
|
||||
projectId={projectId}
|
||||
|
||||
@@ -230,7 +230,6 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
ref={readOnlyEditorRef}
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceId,
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
})}
|
||||
handleEditorReady={handleReadOnlyEditorReady}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Loader } from "@plane/ui";
|
||||
// helpers
|
||||
import { getReadOnlyEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { useMember, useMention, useUser, useWorkspace } from "@/hooks/store";
|
||||
import { useMember, useMention, useUser } from "@/hooks/store";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// plane web hooks
|
||||
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
|
||||
@@ -31,11 +31,9 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
|
||||
getUserDetails,
|
||||
project: { getProjectMemberIds },
|
||||
} = useMember();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : [];
|
||||
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
|
||||
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString() ?? "")?.id ?? "";
|
||||
// issue-embed
|
||||
const { issueEmbedProps } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? "");
|
||||
// use-mention
|
||||
@@ -107,7 +105,6 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
|
||||
editorClassName="pl-10"
|
||||
fileHandler={getReadOnlyEditorFileHandlers({
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceId,
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
})}
|
||||
mentionHandler={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// plane editor
|
||||
import { TFileHandler, TReadOnlyFileHandler } from "@plane/editor";
|
||||
import { TFileHandler } from "@plane/editor";
|
||||
// helpers
|
||||
import { getBase64Image, getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
@@ -37,12 +37,11 @@ type TArgs = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the read-only editors
|
||||
* @description this function returns the file handler required by the editors
|
||||
* @param {TArgs} args
|
||||
*/
|
||||
export const getReadOnlyEditorFileHandlers = (
|
||||
args: Pick<TArgs, "projectId" | "workspaceId" | "workspaceSlug">
|
||||
): TReadOnlyFileHandler => {
|
||||
const { projectId, workspaceId, workspaceSlug } = args;
|
||||
export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
const { maxFileSize, projectId, uploadFile, workspaceId, workspaceSlug } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
@@ -59,29 +58,6 @@ export const getReadOnlyEditorFileHandlers = (
|
||||
);
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(workspaceSlug, src);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the editors
|
||||
* @param {TArgs} args
|
||||
*/
|
||||
export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
const { maxFileSize, projectId, uploadFile, workspaceId, workspaceSlug } = args;
|
||||
|
||||
return {
|
||||
...getReadOnlyEditorFileHandlers({
|
||||
projectId,
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
}),
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
@@ -96,6 +72,13 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
);
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (checkURLValidity(src)) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(workspaceSlug, src);
|
||||
}
|
||||
},
|
||||
cancel: fileService.cancelUpload,
|
||||
validation: {
|
||||
maxFileSize,
|
||||
@@ -103,6 +86,32 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the read-only editors
|
||||
*/
|
||||
export const getReadOnlyEditorFileHandlers = (
|
||||
args: Pick<TArgs, "projectId" | "workspaceSlug">
|
||||
): { getAssetSrc: TFileHandler["getAssetSrc"] } => {
|
||||
const { projectId, workspaceSlug } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
if (!path) return "";
|
||||
if (checkURLValidity(path)) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
getEditorAssetSrc({
|
||||
assetId: path,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}) ?? ""
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description function to replace all the custom components from the html component to make it pdf compatible
|
||||
* @param props
|
||||
|
||||
Reference in New Issue
Block a user