[WEB-310] dev: private bucket implementation (#5793)
* chore: migrations and backmigration to move attachments to file asset * chore: move attachments to file assets * chore: update migration file to include created by and updated by and size * chore: remove uninmport errors * chore: make size as float field * fix: file asset uploads * chore: asset uploads migration changes * chore: v2 assets endpoint * chore: remove unused imports * chore: issue attachments * chore: issue attachments * chore: workspace logo endpoints * chore: private bucket changes * chore: user asset endpoint * chore: add logo_url validation * chore: cover image urlk * chore: change asset max length * chore: pages endpoint * chore: store the storage_metadata only when none * chore: attachment asset apis * chore: update create private bucket * chore: make bucket private * chore: fix response of user uploads * fix: response of user uploads * fix: job to fix file asset uploads * fix: user asset endpoints * chore: avatar for user profile * chore: external apis user url endpoint * chore: upload workspace and user asset actions updated * chore: analytics endpoint * fix: analytics export * chore: avatar urls * chore: update user avatar instances * chore: avatar urls for assignees and creators * chore: bucket permission script * fix: all user avatr instances in the web app * chore: update project cover image logic * fix: issue attachment endpoint * chore: patch endpoint for issue attachment * chore: attachments * chore: change attachment storage class * chore: update issue attachment endpoints * fix: issue attachment * chore: update issue attachment implementation * chore: page asset endpoints * fix: web build errors * chore: attachments * chore: page asset urls * chore: comment and issue asset endpoints * chore: asset endpoints * chore: attachment endpoints * chore: bulk asset endpoint * chore: restore endpoint * chore: project assets endpoints * chore: asset url * chore: add delete asset endpoints * chore: fix asset upload endpoint * chore: update patch endpoints * chore: update patch endpoint * chore: update editor image handling * chore: asset restore endpoints * chore: avatar url for space assets * chore: space app assets migration * fix: space app urls * chore: space endpoints * fix: old editor images rendering logic * fix: issue archive and attachment activity * chore: asset deletes * chore: attachment delete * fix: issue attachment * fix: issue attachment get * chore: cover image url for projects * chore: remove duplicate py file * fix: url check function * chore: chore project cover asset delete * fix: migrations * chore: delete migration files * chore: update bucket * fix: build errors * chore: add asset url in intake attachment * chore: project cover fix * chore: update next.config * chore: delete old workspace logos * chore: workspace assets * chore: asset get for space * chore: update project modal * chore: remove unused imports * fix: space app editor helper * chore: update rich-text read-only editor * chore: create multiple column for entity identifiers * chore: update migrations * chore: remove entity identifier * fix: issue assets * chore: update maximum file size logic * chore: update editor max file size logic * fix: close modal after removing workspace logo * chore: update uploaded asstes' status post issue creation * chore: added file size limit to the space app * dev: add file size limit restriction on all endpoints * fix: remove old workspace logo and user avatar --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
parent
c9580ab794
commit
7e334203f1
241 changed files with 5326 additions and 2518 deletions
|
|
@ -18,6 +18,7 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn
|
|||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
embedHandler,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
id,
|
||||
|
|
@ -38,6 +39,7 @@ const CollaborativeDocumentReadOnlyEditor = (props: ICollaborativeDocumentReadOn
|
|||
const { editor, hasServerConnectionFailed, hasServerSynced } = useReadOnlyCollaborativeEditor({
|
||||
editorClassName,
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { getEditorClassNames } from "@/helpers/common";
|
|||
// hooks
|
||||
import { useReadOnlyEditor } from "@/hooks/use-read-only-editor";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig } from "@/types";
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TDisplayConfig, TFileHandler } from "@/types";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
id: string;
|
||||
|
|
@ -19,6 +19,7 @@ interface IDocumentReadOnlyEditor {
|
|||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
embedHandler: any;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
tabIndex?: number;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
mentionHandler: {
|
||||
|
|
@ -33,6 +34,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
|||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
embedHandler,
|
||||
fileHandler,
|
||||
id,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
|
|
@ -51,6 +53,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
|
|||
const editor = useReadOnlyEditor({
|
||||
editorClassName,
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
initialValue,
|
||||
|
|
|
|||
|
|
@ -14,14 +14,16 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
|||
containerClassName,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
id,
|
||||
initialValue,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
} = props;
|
||||
|
||||
const editor = useReadOnlyEditor({
|
||||
editorClassName,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
initialValue,
|
||||
mentionHandler,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ type CustomImageBlockProps = CustomImageNodeViewProps & {
|
|||
setFailedToLoadImage: (isError: boolean) => void;
|
||||
editorContainer: HTMLDivElement | null;
|
||||
setEditorContainer: (editorContainer: HTMLDivElement | null) => void;
|
||||
src: string;
|
||||
};
|
||||
|
||||
export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
|
|
@ -55,9 +56,10 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
getPos,
|
||||
editor,
|
||||
editorContainer,
|
||||
src: remoteImageSrc,
|
||||
setEditorContainer,
|
||||
} = props;
|
||||
const { src: remoteImageSrc, width, height, aspectRatio } = node.attrs;
|
||||
const { width, height, aspectRatio } = node.attrs;
|
||||
// states
|
||||
const [size, setSize] = useState<Size>({
|
||||
width: ensurePixelString(width, "35%"),
|
||||
|
|
@ -206,7 +208,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
const showImageResizer = editor.isEditable && remoteImageSrc && initialResizeComplete;
|
||||
// show the preview image from the file system if the remote image's src is not set
|
||||
const displayedImageSrc = remoteImageSrc ?? imageFromFileSystem;
|
||||
const displayedImageSrc = remoteImageSrc || imageFromFileSystem;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
|||
imageFromFileSystem={imageFromFileSystem}
|
||||
editorContainer={editorContainer}
|
||||
editor={editor}
|
||||
// @ts-expect-error function not expected here, but will still work
|
||||
src={editor?.commands?.getImageSource?.(node.attrs.src)}
|
||||
getPos={getPos}
|
||||
node={node}
|
||||
setEditorContainer={setEditorContainer}
|
||||
|
|
@ -67,6 +69,7 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
|||
failedToLoadImage={failedToLoadImage}
|
||||
getPos={getPos}
|
||||
loadImageFromFileSystem={setImageFromFileSystem}
|
||||
maxFileSize={editor.storage.imageComponent.maxFileSize}
|
||||
node={node}
|
||||
setIsUploaded={setIsUploaded}
|
||||
selected={selected}
|
||||
|
|
|
|||
|
|
@ -10,33 +10,34 @@ import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/
|
|||
import { getImageComponentImageFileMap, ImageAttributes } from "@/extensions/custom-image";
|
||||
|
||||
export const CustomImageUploader = (props: {
|
||||
failedToLoadImage: boolean;
|
||||
editor: Editor;
|
||||
selected: boolean;
|
||||
failedToLoadImage: boolean;
|
||||
getPos: () => number;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
setIsUploaded: (isUploaded: boolean) => void;
|
||||
maxFileSize: number;
|
||||
node: ProsemirrorNode & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
selected: boolean;
|
||||
setIsUploaded: (isUploaded: boolean) => void;
|
||||
updateAttributes: (attrs: Record<string, any>) => void;
|
||||
getPos: () => number;
|
||||
}) => {
|
||||
const {
|
||||
selected,
|
||||
failedToLoadImage,
|
||||
editor,
|
||||
failedToLoadImage,
|
||||
getPos,
|
||||
loadImageFromFileSystem,
|
||||
maxFileSize,
|
||||
node,
|
||||
selected,
|
||||
setIsUploaded,
|
||||
updateAttributes,
|
||||
getPos,
|
||||
} = props;
|
||||
// ref
|
||||
// refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const hasTriggeredFilePickerRef = useRef(false);
|
||||
// derived values
|
||||
const imageEntityId = node.attrs.id;
|
||||
|
||||
const imageComponentImageFileMap = useMemo(() => getImageComponentImageFileMap(editor), [editor]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
|
|
@ -71,11 +72,17 @@ export const CustomImageUploader = (props: {
|
|||
[imageComponentImageFileMap, imageEntityId, updateAttributes, getPos]
|
||||
);
|
||||
// hooks
|
||||
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({ onUpload, editor, loadImageFromFileSystem });
|
||||
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
|
||||
uploader: uploadFile,
|
||||
const { uploading: isImageBeingUploaded, uploadFile } = useUploader({
|
||||
editor,
|
||||
loadImageFromFileSystem,
|
||||
maxFileSize,
|
||||
onUpload,
|
||||
});
|
||||
const { draggedInside, onDrop, onDragEnter, onDragLeave } = useDropZone({
|
||||
editor,
|
||||
maxFileSize,
|
||||
pos: getPos(),
|
||||
uploader: uploadFile,
|
||||
});
|
||||
|
||||
// the meta data of the image component
|
||||
|
|
@ -102,11 +109,17 @@ export const CustomImageUploader = (props: {
|
|||
const onFileChange = useCallback(
|
||||
async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const fileList = e.target.files;
|
||||
if (!fileList) {
|
||||
const filesList = e.target.files;
|
||||
if (!filesList) {
|
||||
return;
|
||||
}
|
||||
await uploadFirstImageAndInsertRemaining(editor, fileList, getPos(), uploadFile);
|
||||
await uploadFirstImageAndInsertRemaining({
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
pos: getPos(),
|
||||
uploader: uploadFile,
|
||||
});
|
||||
},
|
||||
[uploadFile, editor, getPos]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ declare module "@tiptap/core" {
|
|||
imageComponent: {
|
||||
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
|
||||
uploadImage: (file: File) => () => Promise<string> | undefined;
|
||||
getImageSource?: (path: string) => () => string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +37,13 @@ export interface UploadImageExtensionStorage {
|
|||
export type UploadEntity = ({ event: "insert" } | { event: "drop"; file: File }) & { hasOpenedFileInputOnce?: boolean };
|
||||
|
||||
export const CustomImageExtension = (props: TFileHandler) => {
|
||||
const { upload, delete: deleteImage, restore: restoreImage } = props;
|
||||
const {
|
||||
getAssetSrc,
|
||||
upload,
|
||||
delete: deleteImage,
|
||||
restore: restoreImage,
|
||||
validation: { maxFileSize },
|
||||
} = props;
|
||||
|
||||
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
name: "imageComponent",
|
||||
|
|
@ -87,8 +94,7 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
await restoreImage(assetUrlWithWorkspaceId);
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
|
|
@ -114,6 +120,7 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
fileMap: new Map(),
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
maxFileSize,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -123,7 +130,13 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
(props: { file?: File; pos?: number; event: "insert" | "drop" }) =>
|
||||
({ commands }) => {
|
||||
// Early return if there's an invalid file being dropped
|
||||
if (props?.file && !isFileValid(props.file)) {
|
||||
if (
|
||||
props?.file &&
|
||||
!isFileValid({
|
||||
file: props.file,
|
||||
maxFileSize,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +179,7 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
const fileUrl = await upload(file);
|
||||
return fileUrl;
|
||||
},
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ import { Image } from "@tiptap/extension-image";
|
|||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// components
|
||||
import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custom-image";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
|
||||
export const CustomReadOnlyImageExtension = () =>
|
||||
Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
|
||||
const { getAssetSrc } = props;
|
||||
|
||||
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
|
||||
name: "imageComponent",
|
||||
selectable: false,
|
||||
group: "block",
|
||||
|
|
@ -51,7 +55,14 @@ export const CustomReadOnlyImageExtension = () =>
|
|||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomImageNode);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,16 +31,11 @@ import {
|
|||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// types
|
||||
import { DeleteImage, IMentionHighlight, IMentionSuggestion, RestoreImage, UploadImage } from "@/types";
|
||||
import { IMentionHighlight, IMentionSuggestion, TFileHandler } from "@/types";
|
||||
|
||||
type TArguments = {
|
||||
enableHistory: boolean;
|
||||
fileConfig: {
|
||||
deleteFile: DeleteImage;
|
||||
restoreFile: RestoreImage;
|
||||
cancelUploadImage?: () => void;
|
||||
uploadFile: UploadImage;
|
||||
};
|
||||
fileHandler: TFileHandler;
|
||||
mentionConfig: {
|
||||
mentionSuggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
mentionHighlights?: () => Promise<IMentionHighlight[]>;
|
||||
|
|
@ -49,125 +44,118 @@ type TArguments = {
|
|||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const CoreEditorExtensions = ({
|
||||
enableHistory,
|
||||
fileConfig: { deleteFile, restoreFile, cancelUploadImage, uploadFile },
|
||||
mentionConfig,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
}: TArguments) => [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc pl-7 space-y-2",
|
||||
export const CoreEditorExtensions = (args: TArguments) => {
|
||||
const { enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args;
|
||||
|
||||
return [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal pl-7 space-y-2",
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "not-prose space-y-2",
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "not-prose space-y-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
blockquote: false,
|
||||
dropcursor: {
|
||||
class: "text-custom-text-300",
|
||||
},
|
||||
...(enableHistory ? {} : { history: false }),
|
||||
}),
|
||||
CustomQuoteExtension,
|
||||
DropHandlerExtension(),
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
class: "my-4 border-custom-border-400",
|
||||
},
|
||||
}),
|
||||
CustomKeymap,
|
||||
ListKeymap({ tabIndex }),
|
||||
CustomLinkExtension.configure({
|
||||
openOnClick: true,
|
||||
autolink: true,
|
||||
linkOnPaste: true,
|
||||
protocols: ["http", "https"],
|
||||
validate: (url: string) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
CustomTypographyExtension,
|
||||
ImageExtension(deleteFile, restoreFile, cancelUploadImage).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomImageExtension({
|
||||
delete: deleteFile,
|
||||
restore: restoreFile,
|
||||
upload: uploadFile,
|
||||
cancel: cancelUploadImage ?? (() => {}),
|
||||
}),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2 space-y-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "relative",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
CustomCodeBlockExtension.configure({
|
||||
HTMLAttributes: {
|
||||
class: "",
|
||||
},
|
||||
}),
|
||||
CustomCodeMarkPlugin,
|
||||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformPastedText: true,
|
||||
breaks: true,
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
CustomMention({
|
||||
mentionSuggestions: mentionConfig.mentionSuggestions,
|
||||
mentionHighlights: mentionConfig.mentionHighlights,
|
||||
readonly: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ editor, node }) => {
|
||||
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
blockquote: false,
|
||||
dropcursor: {
|
||||
class: "text-custom-text-300",
|
||||
},
|
||||
...(enableHistory ? {} : { history: false }),
|
||||
}),
|
||||
CustomQuoteExtension,
|
||||
DropHandlerExtension(),
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
class: "my-4 border-custom-border-400",
|
||||
},
|
||||
}),
|
||||
CustomKeymap,
|
||||
ListKeymap({ tabIndex }),
|
||||
CustomLinkExtension.configure({
|
||||
openOnClick: true,
|
||||
autolink: true,
|
||||
linkOnPaste: true,
|
||||
protocols: ["http", "https"],
|
||||
validate: (url: string) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
CustomTypographyExtension,
|
||||
ImageExtension(fileHandler).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomImageExtension(fileHandler),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2 space-y-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "relative",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
CustomCodeBlockExtension.configure({
|
||||
HTMLAttributes: {
|
||||
class: "",
|
||||
},
|
||||
}),
|
||||
CustomCodeMarkPlugin,
|
||||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformPastedText: true,
|
||||
breaks: true,
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
CustomMention({
|
||||
mentionSuggestions: mentionConfig.mentionSuggestions,
|
||||
mentionHighlights: mentionConfig.mentionHighlights,
|
||||
readonly: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ editor, node }) => {
|
||||
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
|
||||
|
||||
if (editor.storage.imageComponent.uploadInProgress) return "";
|
||||
if (editor.storage.imageComponent.uploadInProgress) return "";
|
||||
|
||||
const shouldHidePlaceholder =
|
||||
editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");
|
||||
const shouldHidePlaceholder =
|
||||
editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");
|
||||
|
||||
if (shouldHidePlaceholder) return "";
|
||||
if (shouldHidePlaceholder) return "";
|
||||
|
||||
if (placeholder) {
|
||||
if (typeof placeholder === "string") return placeholder;
|
||||
else return placeholder(editor.isFocused, editor.getHTML());
|
||||
}
|
||||
if (placeholder) {
|
||||
if (typeof placeholder === "string") return placeholder;
|
||||
else return placeholder(editor.isFocused, editor.getHTML());
|
||||
}
|
||||
|
||||
return "Press '/' for commands...";
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomTextColorExtension,
|
||||
CustomBackgroundColorExtension,
|
||||
];
|
||||
return "Press '/' for commands...";
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomTextColorExtension,
|
||||
CustomBackgroundColorExtension,
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,19 @@ import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-par
|
|||
// plugins
|
||||
import { ImageExtensionStorage, TrackImageDeletionPlugin, TrackImageRestorationPlugin } from "@/plugins/image";
|
||||
// types
|
||||
import { DeleteImage, RestoreImage } from "@/types";
|
||||
import { TFileHandler } from "@/types";
|
||||
// extensions
|
||||
import { CustomImageNode } from "@/extensions";
|
||||
|
||||
export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreImage, cancelUploadImage?: () => void) =>
|
||||
ImageExt.extend<any, ImageExtensionStorage>({
|
||||
export const ImageExtension = (fileHandler: TFileHandler) => {
|
||||
const {
|
||||
delete: deleteImage,
|
||||
getAssetSrc,
|
||||
restore: restoreImage,
|
||||
validation: { maxFileSize },
|
||||
} = fileHandler;
|
||||
|
||||
return ImageExt.extend<any, ImageExtensionStorage>({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name),
|
||||
|
|
@ -33,8 +40,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreIm
|
|||
});
|
||||
imageSources.forEach(async (src) => {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
await restoreImage(assetUrlWithWorkspaceId);
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
}
|
||||
|
|
@ -46,6 +52,7 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreIm
|
|||
return {
|
||||
deletedImageSet: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
maxFileSize,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -61,8 +68,15 @@ export const ImageExtension = (deleteImage: DeleteImage, restoreImage: RestoreIm
|
|||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
// render custom image node
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomImageNode);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,20 +2,33 @@ import Image from "@tiptap/extension-image";
|
|||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
// extensions
|
||||
import { CustomImageNode } from "@/extensions";
|
||||
// types
|
||||
import { TFileHandler } from "@/types";
|
||||
|
||||
export const ReadOnlyImageExtension = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: "35%",
|
||||
},
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomImageNode);
|
||||
},
|
||||
});
|
||||
export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">) => {
|
||||
const { getAssetSrc } = props;
|
||||
|
||||
return Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: "35%",
|
||||
},
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomImageNode);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,91 +27,104 @@ import {
|
|||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// types
|
||||
import { IMentionHighlight } from "@/types";
|
||||
import { IMentionHighlight, TFileHandler } from "@/types";
|
||||
|
||||
export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
mentionHighlights?: () => Promise<IMentionHighlight[]>;
|
||||
}) => [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc pl-7 space-y-2",
|
||||
type Props = {
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
mentionConfig: {
|
||||
mentionHighlights?: () => Promise<IMentionHighlight[]>;
|
||||
};
|
||||
};
|
||||
|
||||
export const CoreReadOnlyEditorExtensions = (props: Props) => {
|
||||
const { fileHandler, mentionConfig } = props;
|
||||
|
||||
return [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal pl-7 space-y-2",
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "not-prose space-y-2",
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "not-prose space-y-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
blockquote: false,
|
||||
dropcursor: false,
|
||||
gapcursor: false,
|
||||
}),
|
||||
CustomQuoteExtension,
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
class: "my-4 border-custom-border-400",
|
||||
},
|
||||
}),
|
||||
CustomLinkExtension.configure({
|
||||
openOnClick: true,
|
||||
autolink: true,
|
||||
linkOnPaste: true,
|
||||
protocols: ["http", "https"],
|
||||
validate: (url: string) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
CustomTypographyExtension,
|
||||
ReadOnlyImageExtension.configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomReadOnlyImageExtension(),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2 space-y-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "relative pointer-events-none",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
CustomCodeBlockExtension.configure({
|
||||
HTMLAttributes: {
|
||||
class: "",
|
||||
},
|
||||
}),
|
||||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
CustomMention({
|
||||
mentionHighlights: mentionConfig.mentionHighlights,
|
||||
readonly: true,
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomTextColorExtension,
|
||||
CustomBackgroundColorExtension,
|
||||
HeadingListExtension,
|
||||
];
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
blockquote: false,
|
||||
dropcursor: false,
|
||||
gapcursor: false,
|
||||
}),
|
||||
CustomQuoteExtension,
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
class: "my-4 border-custom-border-400",
|
||||
},
|
||||
}),
|
||||
CustomLinkExtension.configure({
|
||||
openOnClick: true,
|
||||
autolink: true,
|
||||
linkOnPaste: true,
|
||||
protocols: ["http", "https"],
|
||||
validate: (url: string) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
CustomTypographyExtension,
|
||||
ReadOnlyImageExtension({
|
||||
getAssetSrc: fileHandler.getAssetSrc,
|
||||
}).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
CustomReadOnlyImageExtension({
|
||||
getAssetSrc: fileHandler.getAssetSrc,
|
||||
}),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2 space-y-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "relative pointer-events-none",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
CustomCodeBlockExtension.configure({
|
||||
HTMLAttributes: {
|
||||
class: "",
|
||||
},
|
||||
}),
|
||||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
CustomMention({
|
||||
mentionHighlights: mentionConfig.mentionHighlights,
|
||||
readonly: true,
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomTextColorExtension,
|
||||
CustomBackgroundColorExtension,
|
||||
HeadingListExtension,
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -75,12 +75,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
extensions: [
|
||||
...CoreEditorExtensions({
|
||||
enableHistory,
|
||||
fileConfig: {
|
||||
uploadFile: fileHandler.upload,
|
||||
deleteFile: fileHandler.delete,
|
||||
restoreFile: fileHandler.restore,
|
||||
cancelUploadImage: fileHandler.cancel,
|
||||
},
|
||||
fileHandler,
|
||||
mentionConfig: {
|
||||
mentionSuggestions: mentionHandler.suggestions ?? (() => Promise.resolve<IMentionSuggestion[]>([])),
|
||||
mentionHighlights: mentionHandler.highlights,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import { DragEvent, useCallback, useEffect, useState } from "react";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { isFileValid } from "@/plugins/image";
|
||||
// extensions
|
||||
import { insertImagesSafely } from "@/extensions/drop";
|
||||
// plugins
|
||||
import { isFileValid } from "@/plugins/image";
|
||||
|
||||
export const useUploader = ({
|
||||
onUpload,
|
||||
editor,
|
||||
loadImageFromFileSystem,
|
||||
}: {
|
||||
onUpload: (url: string) => void;
|
||||
type TUploaderArgs = {
|
||||
editor: Editor;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
}) => {
|
||||
maxFileSize: number;
|
||||
onUpload: (url: string) => void;
|
||||
};
|
||||
|
||||
export const useUploader = (args: TUploaderArgs) => {
|
||||
const { editor, loadImageFromFileSystem, maxFileSize, onUpload } = args;
|
||||
// states
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const uploadFile = useCallback(
|
||||
|
|
@ -23,7 +26,10 @@ export const useUploader = ({
|
|||
setUploading(true);
|
||||
const fileNameTrimmed = trimFileName(file.name);
|
||||
const fileWithTrimmedName = new File([file], fileNameTrimmed, { type: file.type });
|
||||
const isValid = isFileValid(fileWithTrimmedName);
|
||||
const isValid = isFileValid({
|
||||
file: fileWithTrimmedName,
|
||||
maxFileSize,
|
||||
});
|
||||
if (!isValid) {
|
||||
setImageUploadInProgress(false);
|
||||
return;
|
||||
|
|
@ -64,15 +70,16 @@ export const useUploader = ({
|
|||
return { uploading, uploadFile };
|
||||
};
|
||||
|
||||
export const useDropZone = ({
|
||||
uploader,
|
||||
editor,
|
||||
pos,
|
||||
}: {
|
||||
uploader: (file: File) => Promise<void>;
|
||||
type TDropzoneArgs = {
|
||||
editor: Editor;
|
||||
maxFileSize: number;
|
||||
pos: number;
|
||||
}) => {
|
||||
uploader: (file: File) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useDropZone = (args: TDropzoneArgs) => {
|
||||
const { editor, maxFileSize, pos, uploader } = args;
|
||||
// states
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [draggedInside, setDraggedInside] = useState<boolean>(false);
|
||||
|
||||
|
|
@ -101,8 +108,14 @@ export const useDropZone = ({
|
|||
if (e.dataTransfer.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
const fileList = e.dataTransfer.files;
|
||||
await uploadFirstImageAndInsertRemaining(editor, fileList, pos, uploader);
|
||||
const filesList = e.dataTransfer.files;
|
||||
await uploadFirstImageAndInsertRemaining({
|
||||
editor,
|
||||
filesList,
|
||||
maxFileSize,
|
||||
pos,
|
||||
uploader,
|
||||
});
|
||||
},
|
||||
[uploader, editor, pos]
|
||||
);
|
||||
|
|
@ -129,22 +142,33 @@ function trimFileName(fileName: string, maxLength = 100) {
|
|||
return fileName;
|
||||
}
|
||||
|
||||
type TMultipleImagesArgs = {
|
||||
editor: Editor;
|
||||
filesList: FileList;
|
||||
maxFileSize: number;
|
||||
pos: number;
|
||||
uploader: (file: File) => Promise<void>;
|
||||
};
|
||||
|
||||
// Upload the first image and insert the remaining images for uploading multiple image
|
||||
// post insertion of image-component
|
||||
export async function uploadFirstImageAndInsertRemaining(
|
||||
editor: Editor,
|
||||
fileList: FileList,
|
||||
pos: number,
|
||||
uploaderFn: (file: File) => Promise<void>
|
||||
) {
|
||||
export async function uploadFirstImageAndInsertRemaining(args: TMultipleImagesArgs) {
|
||||
const { editor, filesList, maxFileSize, pos, uploader } = args;
|
||||
const filteredFiles: File[] = [];
|
||||
for (let i = 0; i < fileList.length; i += 1) {
|
||||
const item = fileList.item(i);
|
||||
if (item && item.type.indexOf("image") !== -1 && isFileValid(item)) {
|
||||
for (let i = 0; i < filesList.length; i += 1) {
|
||||
const item = filesList.item(i);
|
||||
if (
|
||||
item &&
|
||||
item.type.indexOf("image") !== -1 &&
|
||||
isFileValid({
|
||||
file: item,
|
||||
maxFileSize,
|
||||
})
|
||||
) {
|
||||
filteredFiles.push(item);
|
||||
}
|
||||
}
|
||||
if (filteredFiles.length !== fileList.length) {
|
||||
if (filteredFiles.length !== filesList.length) {
|
||||
console.warn("Some files were not images and have been ignored.");
|
||||
}
|
||||
if (filteredFiles.length === 0) {
|
||||
|
|
@ -154,7 +178,7 @@ export async function uploadFirstImageAndInsertRemaining(
|
|||
|
||||
// Upload the first image
|
||||
const firstFile = filteredFiles[0];
|
||||
uploaderFn(firstFile);
|
||||
uploader(firstFile);
|
||||
|
||||
// Insert the remaining images
|
||||
const remainingFiles = filteredFiles.slice(1);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
|
|||
editorClassName,
|
||||
editorProps = {},
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
id,
|
||||
|
|
@ -74,6 +75,7 @@ export const useReadOnlyCollaborativeEditor = (props: TReadOnlyCollaborativeEdit
|
|||
document: provider.document,
|
||||
}),
|
||||
],
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
|||
// props
|
||||
import { CoreReadOnlyEditorProps } from "@/props";
|
||||
// types
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight } from "@/types";
|
||||
import { EditorReadOnlyRefApi, IMentionHighlight, TFileHandler } from "@/types";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
initialValue?: string;
|
||||
|
|
@ -19,6 +19,7 @@ interface CustomReadOnlyEditorProps {
|
|||
forwardedRef?: MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
|
|
@ -33,6 +34,7 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
|||
forwardedRef,
|
||||
extensions = [],
|
||||
editorProps = {},
|
||||
fileHandler,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
provider,
|
||||
|
|
@ -52,7 +54,10 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
|||
},
|
||||
extensions: [
|
||||
...CoreReadOnlyEditorExtensions({
|
||||
mentionHighlights: mentionHandler.highlights,
|
||||
mentionConfig: {
|
||||
mentionHighlights: mentionHandler.highlights,
|
||||
},
|
||||
fileHandler,
|
||||
}),
|
||||
...extensions,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -47,10 +47,9 @@ export const TrackImageDeletionPlugin = (editor: Editor, deleteImage: DeleteImag
|
|||
});
|
||||
|
||||
async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
|
||||
if (!src) return;
|
||||
try {
|
||||
if (!src) return;
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
await deleteImage(assetUrlWithWorkspaceId);
|
||||
await deleteImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error deleting image: ", error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,10 +48,9 @@ export const TrackImageRestorationPlugin = (editor: Editor, restoreImage: Restor
|
|||
});
|
||||
|
||||
async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise<void> {
|
||||
if (!src) return;
|
||||
try {
|
||||
if (!src) return;
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
await restoreImage(assetUrlWithWorkspaceId);
|
||||
await restoreImage(src);
|
||||
} catch (error) {
|
||||
console.error("Error restoring image: ", error);
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
export function isFileValid(file: File, showAlert = true): boolean {
|
||||
type TArgs = {
|
||||
file: File;
|
||||
maxFileSize: number;
|
||||
};
|
||||
|
||||
export const isFileValid = (args: TArgs): boolean => {
|
||||
const { file, maxFileSize } = args;
|
||||
|
||||
if (!file) {
|
||||
if (showAlert) {
|
||||
alert("No file selected. Please select a file to upload.");
|
||||
}
|
||||
alert("No file selected. Please select a file to upload.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp"];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
if (showAlert) {
|
||||
alert("Invalid file type. Please select a JPEG, JPG, PNG, or WEBP image file.");
|
||||
}
|
||||
alert("Invalid file type. Please select a JPEG, JPG, PNG, or WEBP image file.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
if (showAlert) {
|
||||
alert("File size too large. Please select a file smaller than 5MB.");
|
||||
}
|
||||
if (file.size > maxFileSize) {
|
||||
alert(`File size too large. Please select a file smaller than ${maxFileSize / 1024 / 1024}MB.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,5 +44,6 @@ export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
|||
};
|
||||
|
||||
export type TReadOnlyCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import { DeleteImage, RestoreImage, UploadImage } from "@/types";
|
||||
|
||||
export type TFileHandler = {
|
||||
getAssetSrc: (path: string) => string;
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
restore: RestoreImage;
|
||||
validation: {
|
||||
/**
|
||||
* @description max file size in bytes
|
||||
* @example enter 5242880( 5* 1024 * 1024) for 5MB
|
||||
*/
|
||||
maxFileSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TEditorFontStyle = "sans-serif" | "serif" | "monospace";
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export interface IReadOnlyEditorProps {
|
|||
containerClassName?: string;
|
||||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
fileHandler: Pick<TFileHandler, "getAssetSrc">;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
id: string;
|
||||
initialValue: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<void>;
|
||||
|
||||
export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||
export type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise<void>;
|
||||
|
||||
export type UploadImage = (file: File) => Promise<string>;
|
||||
|
|
|
|||
6
packages/types/src/analytics.d.ts
vendored
6
packages/types/src/analytics.d.ts
vendored
|
|
@ -20,7 +20,7 @@ export interface IAnalyticsData {
|
|||
}
|
||||
|
||||
export interface IAnalyticsAssigneeDetails {
|
||||
assignees__avatar: string | null;
|
||||
assignees__avatar_url: string | null;
|
||||
assignees__display_name: string | null;
|
||||
assignees__first_name: string;
|
||||
assignees__id: string | null;
|
||||
|
|
@ -87,7 +87,7 @@ export interface IExportAnalyticsFormData {
|
|||
}
|
||||
|
||||
export interface IDefaultAnalyticsUser {
|
||||
assignees__avatar: string | null;
|
||||
assignees__avatar_url: string | null;
|
||||
assignees__first_name: string;
|
||||
assignees__last_name: string;
|
||||
assignees__display_name: string;
|
||||
|
|
@ -99,7 +99,7 @@ export interface IDefaultAnalyticsResponse {
|
|||
issue_completed_month_wise: { month: number; count: number }[];
|
||||
most_issue_closed_user: IDefaultAnalyticsUser[];
|
||||
most_issue_created_user: {
|
||||
created_by__avatar: string | null;
|
||||
created_by__avatar_url: string | null;
|
||||
created_by__first_name: string;
|
||||
created_by__last_name: string;
|
||||
created_by__display_name: string;
|
||||
|
|
|
|||
17
packages/types/src/current-user/accounts.d.ts
vendored
17
packages/types/src/current-user/accounts.d.ts
vendored
|
|
@ -1,17 +0,0 @@
|
|||
export type TCurrentUserAccount = {
|
||||
id: string | undefined;
|
||||
|
||||
user: string | undefined;
|
||||
|
||||
provider_account_id: string | undefined;
|
||||
provider: "google" | "github" | "gitlab" | string | undefined;
|
||||
access_token: string | undefined;
|
||||
access_token_expired_at: Date | undefined;
|
||||
refresh_token: string | undefined;
|
||||
refresh_token_expired_at: Date | undefined;
|
||||
last_connected_at: Date | undefined;
|
||||
metadata: object | undefined;
|
||||
|
||||
created_at: Date | undefined;
|
||||
updated_at: Date | undefined;
|
||||
};
|
||||
|
|
@ -1,3 +1 @@
|
|||
export * from "./user";
|
||||
export * from "./profile";
|
||||
export * from "./accounts";
|
||||
|
|
|
|||
30
packages/types/src/current-user/user.d.ts
vendored
30
packages/types/src/current-user/user.d.ts
vendored
|
|
@ -1,30 +0,0 @@
|
|||
export type TCurrentUser = {
|
||||
id: string | undefined;
|
||||
avatar: string | undefined;
|
||||
cover_image: string | undefined;
|
||||
date_joined: Date | undefined;
|
||||
display_name: string | undefined;
|
||||
email: string | undefined;
|
||||
first_name: string | undefined;
|
||||
last_name: string | undefined;
|
||||
is_active: boolean;
|
||||
is_bot: boolean;
|
||||
is_email_verified: boolean;
|
||||
is_managed: boolean;
|
||||
mobile_number: string | undefined;
|
||||
user_timezone: string | undefined;
|
||||
username: string | undefined;
|
||||
is_password_autoset: boolean;
|
||||
};
|
||||
|
||||
export type TCurrentUserSettings = {
|
||||
id: string | undefined;
|
||||
email: string | undefined;
|
||||
workspace: {
|
||||
last_workspace_id: string | undefined;
|
||||
last_workspace_slug: string | undefined;
|
||||
fallback_workspace_id: string | undefined;
|
||||
fallback_workspace_slug: string | undefined;
|
||||
invites: number | undefined;
|
||||
};
|
||||
};
|
||||
2
packages/types/src/cycle/cycle.d.ts
vendored
2
packages/types/src/cycle/cycle.d.ts
vendored
|
|
@ -20,7 +20,7 @@ export type TCycleEstimateDistributionBase = {
|
|||
|
||||
export type TCycleAssigneesDistribution = {
|
||||
assignee_id: string | null;
|
||||
avatar: string | null;
|
||||
avatar_url: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
display_name: string | null;
|
||||
|
|
|
|||
|
|
@ -48,3 +48,14 @@ export enum ENotificationFilterType {
|
|||
ASSIGNED = "assigned",
|
||||
SUBSCRIBED = "subscribed",
|
||||
}
|
||||
|
||||
export enum EFileAssetType {
|
||||
COMMENT_DESCRIPTION = "COMMENT_DESCRIPTION",
|
||||
ISSUE_ATTACHMENT = "ISSUE_ATTACHMENT",
|
||||
ISSUE_DESCRIPTION = "ISSUE_DESCRIPTION",
|
||||
PAGE_DESCRIPTION = "PAGE_DESCRIPTION",
|
||||
PROJECT_COVER = "PROJECT_COVER",
|
||||
USER_AVATAR = "USER_AVATAR",
|
||||
USER_COVER = "USER_COVER",
|
||||
WORKSPACE_LOGO = "WORKSPACE_LOGO",
|
||||
}
|
||||
32
packages/types/src/file.d.ts
vendored
Normal file
32
packages/types/src/file.d.ts
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { EFileAssetType } from "./enums"
|
||||
|
||||
export type TFileMetaDataLite = {
|
||||
name: string;
|
||||
// file size in bytes
|
||||
size: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type TFileEntityInfo = {
|
||||
entity_identifier: string;
|
||||
entity_type: EFileAssetType;
|
||||
}
|
||||
|
||||
export type TFileMetaData = TFileMetaDataLite & TFileEntityInfo;
|
||||
|
||||
export type TFileSignedURLResponse = {
|
||||
asset_id: string;
|
||||
asset_url: string;
|
||||
upload_data: {
|
||||
url: string;
|
||||
fields: {
|
||||
"Content-Type": string;
|
||||
key: string;
|
||||
"x-amz-algorithm": string;
|
||||
"x-amz-credential": string;
|
||||
"x-amz-date": string;
|
||||
policy: string;
|
||||
"x-amz-signature": string;
|
||||
};
|
||||
};
|
||||
};
|
||||
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
|
|
@ -29,4 +29,5 @@ export * from "./pragmatic";
|
|||
export * from "./publish";
|
||||
export * from "./workspace-notifications";
|
||||
export * from "./favorite";
|
||||
export * from "./file";
|
||||
export * from "./workspace-draft-issues/base";
|
||||
|
|
|
|||
1
packages/types/src/integration.d.ts
vendored
1
packages/types/src/integration.d.ts
vendored
|
|
@ -1,7 +1,6 @@
|
|||
// All the app integrations that are available
|
||||
export interface IAppIntegration {
|
||||
author: string;
|
||||
author: "";
|
||||
avatar_url: string | null;
|
||||
created_at: string;
|
||||
created_by: string | null;
|
||||
|
|
|
|||
2
packages/types/src/issues/activity/base.d.ts
vendored
2
packages/types/src/issues/activity/base.d.ts
vendored
|
|
@ -40,7 +40,7 @@ export type TIssueActivityUserDetail = {
|
|||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
avatar: string;
|
||||
avatar_url: string;
|
||||
is_bot: boolean;
|
||||
display_name: string;
|
||||
};
|
||||
|
|
|
|||
2
packages/types/src/issues/issue.d.ts
vendored
2
packages/types/src/issues/issue.d.ts
vendored
|
|
@ -45,7 +45,7 @@ export type TIssue = TBaseIssue & {
|
|||
is_subscribed?: boolean;
|
||||
parent?: Partial<TBaseIssue>;
|
||||
issue_reactions?: TIssueReaction[];
|
||||
issue_attachment?: TIssueAttachment[];
|
||||
issue_attachments?: TIssueAttachment[];
|
||||
issue_link?: TIssueLink[];
|
||||
// tempId is used for optimistic updates. It is not a part of the API response.
|
||||
tempId?: string;
|
||||
|
|
|
|||
11
packages/types/src/issues/issue_attachment.d.ts
vendored
11
packages/types/src/issues/issue_attachment.d.ts
vendored
|
|
@ -1,17 +1,22 @@
|
|||
import { TFileSignedURLResponse } from "../file";
|
||||
|
||||
export type TIssueAttachment = {
|
||||
id: string;
|
||||
attributes: {
|
||||
name: string;
|
||||
size: number;
|
||||
};
|
||||
asset: string;
|
||||
asset_url: string;
|
||||
issue_id: string;
|
||||
|
||||
//need
|
||||
// required
|
||||
updated_at: string;
|
||||
updated_by: string;
|
||||
};
|
||||
|
||||
export type TIssueAttachmentUploadResponse = TFileSignedURLResponse & {
|
||||
attachment: TIssueAttachment
|
||||
};
|
||||
|
||||
export type TIssueAttachmentMap = {
|
||||
[issue_id: string]: TIssueAttachment;
|
||||
};
|
||||
|
|
|
|||
2
packages/types/src/module/modules.d.ts
vendored
2
packages/types/src/module/modules.d.ts
vendored
|
|
@ -26,7 +26,7 @@ export type TModuleEstimateDistributionBase = {
|
|||
|
||||
export type TModuleAssigneesDistribution = {
|
||||
assignee_id: string | null;
|
||||
avatar: string | null;
|
||||
avatar_url: string | null;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
display_name: string | null;
|
||||
|
|
|
|||
4
packages/types/src/project/projects.d.ts
vendored
4
packages/types/src/project/projects.d.ts
vendored
|
|
@ -18,7 +18,7 @@ export interface IProject {
|
|||
close_in: number;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
cover_image: string | null;
|
||||
cover_image_url: string;
|
||||
cycle_view: boolean;
|
||||
issue_views_view: boolean;
|
||||
module_view: boolean;
|
||||
|
|
@ -75,7 +75,7 @@ export interface IProjectMap {
|
|||
|
||||
export interface IProjectMemberLite {
|
||||
id: string;
|
||||
member__avatar: string;
|
||||
member__avatar_url: string;
|
||||
member__display_name: string;
|
||||
member_id: string;
|
||||
}
|
||||
|
|
|
|||
36
packages/types/src/users.d.ts
vendored
36
packages/types/src/users.d.ts
vendored
|
|
@ -3,17 +3,21 @@ import { TUserPermissions } from "./enums";
|
|||
|
||||
type TLoginMediums = "email" | "magic-code" | "github" | "gitlab" | "google";
|
||||
|
||||
export interface IUser {
|
||||
id: string;
|
||||
avatar: string | null;
|
||||
cover_image: string | null;
|
||||
date_joined: string;
|
||||
|
||||
export interface IUserLite {
|
||||
avatar_url: string;
|
||||
display_name: string;
|
||||
email: string;
|
||||
email?: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
is_active: boolean;
|
||||
id: string;
|
||||
is_bot: boolean;
|
||||
last_name: string;
|
||||
}
|
||||
export interface IUser extends IUserLite {
|
||||
cover_image_url: string | null;
|
||||
date_joined: string;
|
||||
email: string;
|
||||
is_active: boolean;
|
||||
is_email_verified: boolean;
|
||||
is_password_autoset: boolean;
|
||||
is_tour_completed: boolean;
|
||||
|
|
@ -86,15 +90,6 @@ export interface IUserTheme {
|
|||
sidebarBackground: string | undefined;
|
||||
}
|
||||
|
||||
export interface IUserLite {
|
||||
avatar: string;
|
||||
display_name: string;
|
||||
email?: string;
|
||||
first_name: string;
|
||||
id: string;
|
||||
is_bot: boolean;
|
||||
last_name: string;
|
||||
}
|
||||
|
||||
export interface IUserMemberLite extends IUserLite {
|
||||
email?: string;
|
||||
|
|
@ -158,13 +153,8 @@ export interface IUserProfileProjectSegregation {
|
|||
id: string;
|
||||
pending_issues: number;
|
||||
}[];
|
||||
user_data: {
|
||||
avatar: string;
|
||||
cover_image: string | null;
|
||||
user_data: Pick<IUser, "avatar_url" | "cover_image_url" | "display_name" | "first_name" | "last_name"> & {
|
||||
date_joined: Date;
|
||||
display_name: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
user_timezone: string;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
5
packages/types/src/workspace.d.ts
vendored
5
packages/types/src/workspace.d.ts
vendored
|
|
@ -14,8 +14,7 @@ export interface IWorkspace {
|
|||
readonly updated_at: Date;
|
||||
name: string;
|
||||
url: string;
|
||||
logo: string | null;
|
||||
slug: string;
|
||||
logo_url: string | null;
|
||||
readonly total_members: number;
|
||||
readonly slug: string;
|
||||
readonly created_by: string;
|
||||
|
|
@ -71,7 +70,7 @@ export interface IWorkspaceMember {
|
|||
member: IUserLite;
|
||||
role: TUserPermissions;
|
||||
created_at?: string;
|
||||
avatar?: string;
|
||||
avatar_url?: string;
|
||||
email?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue