diff --git a/apps/app/components/core/modals/gpt-assistant-modal.tsx b/apps/app/components/core/modals/gpt-assistant-modal.tsx index a3b748d66..35f282040 100644 --- a/apps/app/components/core/modals/gpt-assistant-modal.tsx +++ b/apps/app/components/core/modals/gpt-assistant-modal.tsx @@ -140,14 +140,14 @@ export const GptAssistantModal: React.FC = ({ return (
{((content && content !== "") || (htmlContent && htmlContent !== "

")) && (
Content: ${content}

`} customClassName="-m-3" noBorder @@ -161,6 +161,7 @@ export const GptAssistantModal: React.FC = ({
Response: ${response}

`} customClassName="-mx-3 -my-3" noBorder @@ -179,11 +180,10 @@ export const GptAssistantModal: React.FC = ({ type="text" name="task" register={register} - placeholder={`${ - content && content !== "" + placeholder={`${content && content !== "" ? "Tell AI what action to perform on this content..." : "Ask AI anything..." - }`} + }`} autoComplete="off" />
@@ -219,8 +219,8 @@ export const GptAssistantModal: React.FC = ({ {isSubmitting ? "Generating response..." : response === "" - ? "Generate response" - : "Generate again"} + ? "Generate response" + : "Generate again"}
diff --git a/apps/app/components/inbox/inbox-main-content.tsx b/apps/app/components/inbox/inbox-main-content.tsx index af1234859..bd4f0ab01 100644 --- a/apps/app/components/inbox/inbox-main-content.tsx +++ b/apps/app/components/inbox/inbox-main-content.tsx @@ -293,6 +293,7 @@ export const InboxMainContent: React.FC = () => {
= ({ issueId, user }) => { return (
= ({ issueId, user, disabled = false }) control={control} render={({ field: { value, onChange } }) => ( void; handleCommentDeletion: (comment: string) => void; }; -export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentDeletion }) => { +export const CommentCard: React.FC = ({ comment, workspaceSlug, onSubmit, handleCommentDeletion }) => { const { user } = useUser(); const editorRef = React.useRef(null); @@ -109,6 +110,7 @@ export const CommentCard: React.FC = ({ comment, onSubmit, handleCommentD >
= ({ comment, onSubmit, handleCommentD
Promise; isAllowed: boolean; } @@ -31,6 +32,7 @@ export interface IssueDetailsProps { export const IssueDescriptionForm: FC = ({ issue, handleFormSubmit, + workspaceSlug, isAllowed, }) => { const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); @@ -69,11 +71,15 @@ export const IssueDescriptionForm: FC = ({ useEffect(() => { if (isSubmitting === "submitted") { + setShowAlert(false); setTimeout(async () => { setIsSubmitting("saved"); }, 2000); + } else if (isSubmitting === "submitting") { + setShowAlert(true); } - }, [isSubmitting]); + }, [isSubmitting, setShowAlert]); + // reset form values useEffect(() => { @@ -112,9 +118,8 @@ export const IssueDescriptionForm: FC = ({ {characterLimit && (
255 ? "text-red-500" : "" - }`} + className={`${watch("name").length === 0 || watch("name").length > 255 ? "text-red-500" : "" + }`} > {watch("name").length} @@ -134,16 +139,19 @@ export const IssueDescriptionForm: FC = ({ { + setShowAlert(true); setIsSubmitting("submitting"); onChange(description_html); setValue("description", description); @@ -156,9 +164,8 @@ export const IssueDescriptionForm: FC = ({ }} />
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
diff --git a/apps/app/components/issues/form.tsx b/apps/app/components/issues/form.tsx index 0e73ca349..c15c61d57 100644 --- a/apps/app/components/issues/form.tsx +++ b/apps/app/components/issues/form.tsx @@ -370,6 +370,7 @@ export const IssueForm: FC = ({ return ( = ({
) : null} = ({ description: !data.description || data.description === "" ? { - type: "doc", - content: [{ type: "paragraph" }], - } + type: "doc", + content: [{ type: "paragraph" }], + } : data.description, description_html: data.description_html ?? "

", }); @@ -292,6 +292,7 @@ export const CreateUpdateBlockInline: React.FC = ({ if (!data) return (

"} debouncedUpdatesEnabled={false} @@ -311,12 +312,11 @@ export const CreateUpdateBlockInline: React.FC = ({ return ( 0 ? value - : watch("description_html") && watch("description_html") !== "" - ? watch("description_html") : { type: "doc", content: [{ type: "paragraph" }] } } debouncedUpdatesEnabled={false} @@ -334,9 +334,8 @@ export const CreateUpdateBlockInline: React.FC = ({
diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx index 8a13e3ea1..0c192990a 100644 --- a/apps/app/components/pages/single-page-block.tsx +++ b/apps/app/components/pages/single-page-block.tsx @@ -456,6 +456,7 @@ export const SinglePageBlock: React.FC = ({ {showBlockDetails ? block.description_html.length > 7 && ( >; } + export const LinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { const inputRef = useRef(null); + const onLinkSubmit = useCallback(() => { + const input = inputRef.current; + const url = input?.value; + if (url && isValidHttpUrl(url)) { + editor.chain().focus().setLink({ href: url }).run(); + setIsOpen(false); + } + }, [editor, inputRef, setIsOpen]); + useEffect(() => { inputRef.current && inputRef.current?.focus(); }); @@ -38,15 +48,13 @@ export const LinkSelector: FC = ({ editor, isOpen, setIsOpen

{isOpen && ( -
{ - e.preventDefault(); - const form = e.target as HTMLFormElement; - const input = form.elements[0] as HTMLInputElement; - editor.chain().focus().setLink({ href: input.value }).run(); - setIsOpen(false); - }} +
{ + if (e.key === "Enter") { + e.preventDefault(); onLinkSubmit(); + } + }} > = ({ editor, isOpen, setIsOpen /> {editor.getAttributes("link").href ? ( ) : ( - )} - +
)}
); diff --git a/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx b/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx new file mode 100644 index 000000000..5b05811d6 --- /dev/null +++ b/apps/app/components/tiptap/bubble-menu/utils/link-validator.tsx @@ -0,0 +1,12 @@ +export default function isValidHttpUrl(string: string): boolean { + let url; + + try { + url = new URL(string); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; +} + diff --git a/apps/app/components/tiptap/extensions/image-resize.tsx b/apps/app/components/tiptap/extensions/image-resize.tsx new file mode 100644 index 000000000..7b2d1a2d3 --- /dev/null +++ b/apps/app/components/tiptap/extensions/image-resize.tsx @@ -0,0 +1,57 @@ +import { Editor } from "@tiptap/react"; +import Moveable from "react-moveable"; + +export const ImageResizer = ({ editor }: { editor: Editor }) => { + const updateMediaSize = () => { + const imageInfo = document.querySelector( + ".ProseMirror-selectednode", + ) as HTMLImageElement; + if (imageInfo) { + const selection = editor.state.selection; + editor.commands.setImage({ + src: imageInfo.src, + width: Number(imageInfo.style.width.replace("px", "")), + height: Number(imageInfo.style.height.replace("px", "")), + } as any); + editor.commands.setNodeSelection(selection.from); + } + }; + + return ( + <> + { + delta[0] && (target!.style.width = `${width}px`); + delta[1] && (target!.style.height = `${height}px`); + }} + onResizeEnd={() => { + updateMediaSize(); + }} + scalable={true} + renderDirections={["w", "e"]} + onScale={({ + target, + transform, + }: + any) => { + target!.style.transform = transform; + }} + /> + + ); +}; + diff --git a/apps/app/components/tiptap/extensions/index.tsx b/apps/app/components/tiptap/extensions/index.tsx index 45dee0929..2c5ffd10a 100644 --- a/apps/app/components/tiptap/extensions/index.tsx +++ b/apps/app/components/tiptap/extensions/index.tsx @@ -1,7 +1,6 @@ import StarterKit from "@tiptap/starter-kit"; import HorizontalRule from "@tiptap/extension-horizontal-rule"; import TiptapLink from "@tiptap/extension-link"; -import TiptapImage from "@tiptap/extension-image"; import Placeholder from "@tiptap/extension-placeholder"; import TiptapUnderline from "@tiptap/extension-underline"; import TextStyle from "@tiptap/extension-text-style"; @@ -18,18 +17,13 @@ import { InputRule } from "@tiptap/core"; import ts from "highlight.js/lib/languages/typescript"; import "highlight.js/styles/github-dark.css"; -import UploadImagesPlugin from "../plugins/upload-image"; import UniqueID from "@tiptap-pro/extension-unique-id"; +import UpdatedImage from "./updated-image"; +import isValidHttpUrl from "../bubble-menu/utils/link-validator"; lowlight.registerLanguage("ts", ts); -const CustomImage = TiptapImage.extend({ - addProseMirrorPlugins() { - return [UploadImagesPlugin()]; - }, -}); - -export const TiptapExtensions = [ +export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => [ StarterKit.configure({ bulletList: { HTMLAttributes: { @@ -93,13 +87,14 @@ export const TiptapExtensions = [ }, }), TiptapLink.configure({ + protocols: ["http", "https"], + validate: (url) => isValidHttpUrl(url), HTMLAttributes: { class: "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), - CustomImage.configure({ - allowBase64: true, + UpdatedImage.configure({ HTMLAttributes: { class: "rounded-lg border border-custom-border-300", }, @@ -117,7 +112,7 @@ export const TiptapExtensions = [ UniqueID.configure({ types: ["image"], }), - SlashCommand, + SlashCommand(workspaceSlug, setIsSubmitting), TiptapUnderline, TextStyle, Color, diff --git a/apps/app/components/tiptap/extensions/updated-image.tsx b/apps/app/components/tiptap/extensions/updated-image.tsx new file mode 100644 index 000000000..01648dcd7 --- /dev/null +++ b/apps/app/components/tiptap/extensions/updated-image.tsx @@ -0,0 +1,22 @@ +import Image from "@tiptap/extension-image"; +import TrackImageDeletionPlugin from "../plugins/delete-image"; +import UploadImagesPlugin from "../plugins/upload-image"; + +const UpdatedImage = Image.extend({ + addProseMirrorPlugins() { + return [UploadImagesPlugin(), TrackImageDeletionPlugin()]; + }, + addAttributes() { + return { + ...this.parent?.(), + width: { + default: '35%', + }, + height: { + default: null, + }, + }; + }, +}); + +export default UpdatedImage; diff --git a/apps/app/components/tiptap/index.tsx b/apps/app/components/tiptap/index.tsx index 72a914bc9..418449c08 100644 --- a/apps/app/components/tiptap/index.tsx +++ b/apps/app/components/tiptap/index.tsx @@ -1,14 +1,10 @@ -// @ts-nocheck import { useEditor, EditorContent, Editor } from "@tiptap/react"; import { useDebouncedCallback } from "use-debounce"; import { EditorBubbleMenu } from "./bubble-menu"; import { TiptapExtensions } from "./extensions"; import { TiptapEditorProps } from "./props"; -import { Node } from "@tiptap/pm/model"; -import { Editor as CoreEditor } from "@tiptap/core"; -import { useCallback, useImperativeHandle, useRef } from "react"; -import { EditorState } from "@tiptap/pm/state"; -import fileService from "services/file.service"; +import { useImperativeHandle, useRef } from "react"; +import { ImageResizer } from "./extensions/image-resize"; export interface ITiptapRichTextEditor { value: string; @@ -18,6 +14,8 @@ export interface ITiptapRichTextEditor { editorContentCustomClassNames?: string; onChange?: (json: any, html: string) => void; setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; + setShouldShowAlert?: (showAlert: boolean) => void; + workspaceSlug: string; editable?: boolean; forwardedRef?: any; debouncedUpdatesEnabled?: boolean; @@ -30,22 +28,24 @@ const Tiptap = (props: ITiptapRichTextEditor) => { forwardedRef, editable, setIsSubmitting, + setShouldShowAlert, editorContentCustomClassNames, value, noBorder, + workspaceSlug, borderOnFocus, customClassName, } = props; const editor = useEditor({ editable: editable ?? true, - editorProps: TiptapEditorProps, - extensions: TiptapExtensions, + editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting), + extensions: TiptapExtensions(workspaceSlug, setIsSubmitting), content: value, onUpdate: async ({ editor }) => { // for instant feedback loop setIsSubmitting?.("submitting"); - checkForNodeDeletions(editor); + setShouldShowAlert?.(true); if (debouncedUpdatesEnabled) { debouncedUpdates({ onChange, editor }); } else { @@ -65,45 +65,6 @@ const Tiptap = (props: ITiptapRichTextEditor) => { }, })); - const previousState = useRef(); - - const onNodeDeleted = useCallback(async (node: Node) => { - if (node.type.name === "image") { - const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1); - const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); - if (resStatus === 204) { - console.log("file deleted successfully"); - } - } - }, []); - - const checkForNodeDeletions = useCallback( - (editor: CoreEditor) => { - const prevNodesById: Record = {}; - previousState.current?.doc.forEach((node) => { - if (node.attrs.id) { - prevNodesById[node.attrs.id] = node; - } - }); - - const nodesById: Record = {}; - editor.state?.doc.forEach((node) => { - if (node.attrs.id) { - nodesById[node.attrs.id] = node; - } - }); - - previousState.current = editor.state; - - for (const [id, node] of Object.entries(prevNodesById)) { - if (nodesById[id] === undefined) { - onNodeDeleted(node); - } - } - }, - [onNodeDeleted] - ); - const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => { setTimeout(async () => { if (onChange) { @@ -112,10 +73,9 @@ const Tiptap = (props: ITiptapRichTextEditor) => { }, 500); }, 1000); - const editorClassNames = `relative w-full max-w-screen-lg mt-2 p-3 relative focus:outline-none rounded-lg - ${noBorder ? "" : "border border-custom-border-200"} ${ - borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" - } ${customClassName}`; + const editorClassNames = `relative w-full max-w-screen-lg sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md + ${noBorder ? "" : "border border-custom-border-200"} ${borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0" + } ${customClassName}`; if (!editor) return null; editorRef.current = editor; @@ -131,6 +91,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => { {editor && }
+ {editor?.isActive("image") && }
); diff --git a/apps/app/components/tiptap/plugins/delete-image.tsx b/apps/app/components/tiptap/plugins/delete-image.tsx new file mode 100644 index 000000000..57ab65c63 --- /dev/null +++ b/apps/app/components/tiptap/plugins/delete-image.tsx @@ -0,0 +1,56 @@ +import { Plugin, PluginKey } from "@tiptap/pm/state"; +import { Node as ProseMirrorNode } from '@tiptap/pm/model'; +import fileService from "services/file.service"; + +const deleteKey = new PluginKey("delete-image"); + +const TrackImageDeletionPlugin = () => + new Plugin({ + key: deleteKey, + appendTransaction: (transactions, oldState, newState) => { + transactions.forEach((transaction) => { + if (!transaction.docChanged) return; + + const removedImages: ProseMirrorNode[] = []; + + oldState.doc.descendants((oldNode, oldPos) => { + if (oldNode.type.name !== 'image') return; + + if (!newState.doc.resolve(oldPos).parent) return; + const newNode = newState.doc.nodeAt(oldPos); + + // Check if the node has been deleted or replaced + if (!newNode || newNode.type.name !== 'image') { + // Check if the node still exists elsewhere in the document + let nodeExists = false; + newState.doc.descendants((node) => { + if (node.attrs.id === oldNode.attrs.id) { + nodeExists = true; + } + }); + + if (!nodeExists) { + removedImages.push(oldNode as ProseMirrorNode); + } + } + }); + + removedImages.forEach((node) => { + const src = node.attrs.src; + onNodeDeleted(src); + }); + }); + + return null; + }, + }); + +export default TrackImageDeletionPlugin; + +async function onNodeDeleted(src: string) { + const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); + const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId); + if (resStatus === 204) { + console.log("Image deleted successfully"); + } +} diff --git a/apps/app/components/tiptap/plugins/upload-image.tsx b/apps/app/components/tiptap/plugins/upload-image.tsx index ed44aa379..0657bc82b 100644 --- a/apps/app/components/tiptap/plugins/upload-image.tsx +++ b/apps/app/components/tiptap/plugins/upload-image.tsx @@ -57,7 +57,7 @@ function findPlaceholder(state: EditorState, id: {}) { return found.length ? found[0].from : null; } -export async function startImageUpload(file: File, view: EditorView, pos: number) { +export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) { if (!file.type.includes("image/")) { return; } else if (file.size / 1024 / 1024 > 20) { @@ -82,7 +82,11 @@ export async function startImageUpload(file: File, view: EditorView, pos: number view.dispatch(tr); }; - const src = await UploadImageHandler(file); + if (!workspaceSlug) { + return; + } + setIsSubmitting?.("submitting") + const src = await UploadImageHandler(file, workspaceSlug); const { schema } = view.state; pos = findPlaceholder(view.state, id); @@ -96,7 +100,10 @@ export async function startImageUpload(file: File, view: EditorView, pos: number view.dispatch(transaction); } -const UploadImageHandler = (file: File): Promise => { +const UploadImageHandler = (file: File, workspaceSlug: string): Promise => { + if (!workspaceSlug) { + return Promise.reject("Workspace slug is missing"); + } try { const formData = new FormData(); formData.append("asset", file); @@ -104,7 +111,7 @@ const UploadImageHandler = (file: File): Promise => { return new Promise(async (resolve, reject) => { const imageUrl = await fileService - .uploadFile("plane", formData) + .uploadFile(workspaceSlug, formData) .then((response) => response.asset); const image = new Image(); diff --git a/apps/app/components/tiptap/props.tsx b/apps/app/components/tiptap/props.tsx index 1ffbebe6d..d50fc29b0 100644 --- a/apps/app/components/tiptap/props.tsx +++ b/apps/app/components/tiptap/props.tsx @@ -1,56 +1,56 @@ import { EditorProps } from "@tiptap/pm/view"; import { startImageUpload } from "./plugins/upload-image"; -export const TiptapEditorProps: EditorProps = { - attributes: { - class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, - }, - handleDOMEvents: { - keydown: (_view, event) => { - // prevent default event listeners from firing when slash command is active - if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { - const slashCommand = document.querySelector("#slash-command"); - if (slashCommand) { - return true; - } - } +export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps { + return { + attributes: { + class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`, }, - }, - handlePaste: (view, event) => { - if ( - event.clipboardData && - event.clipboardData.files && - event.clipboardData.files[0] - ) { - event.preventDefault(); - const file = event.clipboardData.files[0]; - const pos = view.state.selection.from; - - startImageUpload(file, view, pos); - return true; - } - return false; - }, - handleDrop: (view, event, _slice, moved) => { - if ( - !moved && - event.dataTransfer && - event.dataTransfer.files && - event.dataTransfer.files[0] - ) { - event.preventDefault(); - const file = event.dataTransfer.files[0]; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - // here we deduct 1 from the pos or else the image will create an extra node - if (coordinates) { - startImageUpload(file, view, coordinates.pos - 1); + handleDOMEvents: { + keydown: (_view, event) => { + // prevent default event listeners from firing when slash command is active + if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { + const slashCommand = document.querySelector("#slash-command"); + if (slashCommand) { + return true; + } + } + }, + }, + handlePaste: (view, event) => { + if ( + event.clipboardData && + event.clipboardData.files && + event.clipboardData.files[0] + ) { + event.preventDefault(); + const file = event.clipboardData.files[0]; + const pos = view.state.selection.from; + startImageUpload(file, view, pos, workspaceSlug, setIsSubmitting); + return true; } - return true; - } - return false; - }, -}; - + return false; + }, + handleDrop: (view, event, _slice, moved) => { + if ( + !moved && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files[0] + ) { + event.preventDefault(); + const file = event.dataTransfer.files[0]; + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + // here we deduct 1 from the pos or else the image will create an extra node + if (coordinates) { + startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, setIsSubmitting); + } + return true; + } + return false; + }, + }; +} diff --git a/apps/app/components/tiptap/slash-command/index.tsx b/apps/app/components/tiptap/slash-command/index.tsx index 7c686e06b..38f5c9c0a 100644 --- a/apps/app/components/tiptap/slash-command/index.tsx +++ b/apps/app/components/tiptap/slash-command/index.tsx @@ -52,7 +52,7 @@ const Command = Extension.create({ }, }); -const getSuggestionItems = ({ query }: { query: string }) => +const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => ({ query }: { query: string }) => [ { title: "Text", @@ -163,7 +163,7 @@ const getSuggestionItems = ({ query }: { query: string }) => if (input.files?.length) { const file = input.files[0]; const pos = editor.view.state.selection.from; - startImageUpload(file, editor.view, pos); + startImageUpload(file, editor.view, pos, workspaceSlug, setIsSubmitting); } }; input.click(); @@ -328,11 +328,12 @@ const renderItems = () => { }; }; -const SlashCommand = Command.configure({ - suggestion: { - items: getSuggestionItems, - render: renderItems, - }, -}); +export const SlashCommand = (workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) => + Command.configure({ + suggestion: { + items: getSuggestionItems(workspaceSlug, setIsSubmitting), + render: renderItems, + }, + }); export default SlashCommand; diff --git a/apps/app/package.json b/apps/app/package.json index 89b5de611..578a95716 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -52,10 +52,10 @@ "highlight.js": "^11.8.0", "js-cookie": "^3.0.1", "lodash.debounce": "^4.0.8", - "mobx": "^6.10.0", - "mobx-react-lite": "^4.0.3", "lowlight": "^2.9.0", "lucide-react": "^0.263.1", + "mobx": "^6.10.0", + "mobx-react-lite": "^4.0.3", "next": "12.3.2", "next-pwa": "^5.6.0", "next-themes": "^0.2.1", @@ -68,6 +68,7 @@ "react-dropzone": "^14.2.3", "react-hook-form": "^7.38.0", "react-markdown": "^8.0.7", + "react-moveable": "^0.54.1", "sharp": "^0.32.1", "sonner": "^0.6.2", "swr": "^2.1.3", diff --git a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx index 67f3a77ee..148d738c0 100644 --- a/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx +++ b/apps/app/pages/[workspaceSlug]/me/profile/activity.tsx @@ -106,6 +106,7 @@ const ProfileActivity = () => {
div > p { transition: opacity 0.2s ease-out; } +.img-placeholder { + position: relative; + width: 35%; + + &:before { + content: ""; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 45%; + width: 20px; + height: 20px; + border-radius: 50%; + border: 3px solid rgba(var(--color-text-200)); + border-top-color: rgba(var(--color-text-800)); + animation: spinning 0.6s linear infinite; + } +} + +@keyframes spinning { + to { + transform: rotate(360deg); + } +} diff --git a/yarn.lock b/yarn.lock index b4cf55f2f..8bc1fec30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1006,6 +1006,40 @@ react-popper "^2.3.0" tslib "~2.5.0" +"@cfcs/core@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.0.6.tgz#9f8499dcd2ad29fd96d8fa72055411cd4a249121" + integrity sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw== + dependencies: + "@egjs/component" "^3.0.2" + +"@daybrush/utils@^1.1.1", "@daybrush/utils@^1.13.0", "@daybrush/utils@^1.4.0", "@daybrush/utils@^1.6.0", "@daybrush/utils@^1.7.1": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@daybrush/utils/-/utils-1.13.0.tgz#ea70a60864130da476406fdd1d465e3068aea0ff" + integrity sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ== + +"@egjs/agent@^2.2.1": + version "2.4.3" + resolved "https://registry.yarnpkg.com/@egjs/agent/-/agent-2.4.3.tgz#6d44e2fb1ff7bab242c07f82732fe60305ac6f06" + integrity sha512-XvksSENe8wPeFlEVouvrOhKdx8HMniJ3by7sro2uPF3M6QqWwjzVcmvwoPtdjiX8O1lfRoLhQMp1a7NGlVTdIA== + +"@egjs/children-differ@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@egjs/children-differ/-/children-differ-1.0.1.tgz#5465fa80671d5ca3564ebe912f48b05b3e8a14fd" + integrity sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ== + dependencies: + "@egjs/list-differ" "^1.0.0" + +"@egjs/component@^3.0.2": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.4.tgz#ad7b53794b2a612806179a188ad828acb9525f61" + integrity sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g== + +"@egjs/list-differ@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@egjs/list-differ/-/list-differ-1.0.1.tgz#5772b0f8b87973bb67827f6c7d7df8d7f64a22eb" + integrity sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg== + "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -1990,6 +2024,28 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69" integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== +"@scena/dragscroll@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scena/dragscroll/-/dragscroll-1.4.0.tgz#220b2430c16119cd3e70044ee533a5b9a43cffd7" + integrity sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA== + dependencies: + "@daybrush/utils" "^1.6.0" + "@scena/event-emitter" "^1.0.2" + +"@scena/event-emitter@^1.0.2", "@scena/event-emitter@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@scena/event-emitter/-/event-emitter-1.0.5.tgz#047e3acef93cf238d7ce3a8cc5a12ec6bd9c3bb1" + integrity sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg== + dependencies: + "@daybrush/utils" "^1.1.1" + +"@scena/matrix@^1.0.0", "@scena/matrix@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scena/matrix/-/matrix-1.1.1.tgz#5297f71825c72e2c2c8f802f924f482ed200c43c" + integrity sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg== + dependencies: + "@daybrush/utils" "^1.4.0" + "@sentry-internal/tracing@7.63.0": version "7.63.0" resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.63.0.tgz#58903b2205456034611cc5bc1b5b2479275f89c7" @@ -3462,6 +3518,21 @@ css-box-model@^1.2.0: dependencies: tiny-invariant "^1.0.6" +css-styled@^1.0.8, css-styled@~1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/css-styled/-/css-styled-1.0.8.tgz#c9c05dc4abdef5571033090bfb8cfc5e19429974" + integrity sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g== + dependencies: + "@daybrush/utils" "^1.13.0" + +css-to-mat@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-to-mat/-/css-to-mat-1.1.1.tgz#0dd10dcf9ec17df15708c8ff07a74fbd0b9a3fe5" + integrity sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ== + dependencies: + "@daybrush/utils" "^1.13.0" + "@scena/matrix" "^1.0.0" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4484,6 +4555,11 @@ fraction.js@^4.2.0: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== +framework-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/framework-utils/-/framework-utils-1.1.0.tgz#a3b528bce838dfd623148847dc92371b09d0da2d" + integrity sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -4539,6 +4615,14 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +gesto@^1.19.0, gesto@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.1.tgz#b2a29730663eecf77b248982bbff929e79d4a461" + integrity sha512-ofWVEdqmnpFm3AFf7aoclhoayseb3OkwSiXbXusKYu/99iN5HgeWP+SWqdghQ5TFlOgP5Zlz+6SY8mP2V0kFaQ== + dependencies: + "@daybrush/utils" "^1.13.0" + "@scena/event-emitter" "^1.0.2" + get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" @@ -5244,6 +5328,21 @@ jsonpointer@^5.0.0: object.assign "^4.1.4" object.values "^1.1.6" +keycode@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" + integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg== + +keycon@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/keycon/-/keycon-1.4.0.tgz#bf2a633f3c3b659ea564045938cff33e584cebd5" + integrity sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A== + dependencies: + "@cfcs/core" "^0.0.6" + "@daybrush/utils" "^1.7.1" + "@scena/event-emitter" "^1.0.2" + keycode "^2.2.0" + kleur@^4.0.3: version "4.1.5" resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" @@ -6081,6 +6180,13 @@ orderedmap@^2.0.0: resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2" integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g== +overlap-area@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/overlap-area/-/overlap-area-1.1.0.tgz#1fcaa21bdb9cb1ace973d9aa299ae6b56557a4c2" + integrity sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw== + dependencies: + "@daybrush/utils" "^1.7.1" + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -6452,20 +6558,13 @@ prosemirror-menu@^1.2.1: prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.8.1: +prosemirror-model@1.18.1, prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.18.1, prosemirror-model@^1.19.0, prosemirror-model@^1.8.1: version "1.18.1" resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.18.1.tgz#1d5d6b6de7b983ee67a479dc607165fdef3935bd" integrity sha512-IxSVBKAEMjD7s3n8cgtwMlxAXZrC7Mlag7zYsAKDndAqnDScvSmp/UdnRTV/B33lTCVU3CCm7dyAn/rVVD0mcw== dependencies: orderedmap "^2.0.0" -prosemirror-model@^1.19.0: - version "1.19.3" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.3.tgz#f0d55285487fefd962d0ac695f716f4ec6705006" - integrity sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ== - dependencies: - orderedmap "^2.0.0" - prosemirror-schema-basic@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7" @@ -6603,6 +6702,14 @@ react-color@^2.19.3: reactcss "^1.2.0" tinycolor2 "^1.4.1" +react-css-styled@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/react-css-styled/-/react-css-styled-1.1.9.tgz#a7cc948e49f72b2f7fb1393bd85416a8293afab3" + integrity sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw== + dependencies: + css-styled "~1.0.8" + framework-utils "^1.1.0" + react-datepicker@^4.8.0: version "4.16.0" resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.16.0.tgz#b9dd389bb5611a1acc514bba1dd944be21dd877f" @@ -6683,6 +6790,25 @@ react-markdown@^8.0.7: unist-util-visit "^4.0.0" vfile "^5.0.0" +react-moveable@^0.54.1: + version "0.54.1" + resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.54.1.tgz#3c69748c444184700e6999501b0da953c934205e" + integrity sha512-Kj2ifw9nk3LZvu7ezhst8Z5WBPRr+yVv9oROwrBirFlHmwGHHZXUGk5Gaezu+JGqqNRsQJncVMW5Uf68KSSOvg== + dependencies: + "@daybrush/utils" "^1.13.0" + "@egjs/agent" "^2.2.1" + "@egjs/children-differ" "^1.0.1" + "@egjs/list-differ" "^1.0.0" + "@scena/dragscroll" "^1.4.0" + "@scena/event-emitter" "^1.0.5" + "@scena/matrix" "^1.1.1" + css-to-mat "^1.1.1" + framework-utils "^1.1.0" + gesto "^1.19.0" + overlap-area "^1.1.0" + react-css-styled "^1.1.9" + react-selecto "^1.25.0" + react-onclickoutside@^6.12.2: version "6.13.0" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz#e165ea4e5157f3da94f4376a3ab3e22a565f4ffc" @@ -6740,6 +6866,13 @@ react-remove-scroll@2.5.4: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-selecto@^1.25.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/react-selecto/-/react-selecto-1.26.0.tgz#9157ff0a732fc426602b30c08ec21b6ca0a9c472" + integrity sha512-aBTZEYA68uE+o8TytNjTb2GpIn4oKEv0U4LIow3cspJQlF/PdAnBwkq9UuiKVuFluu5kfLQ7Keu3S2Tihlmw0g== + dependencies: + selecto "~1.26.0" + react-style-singleton@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" @@ -7023,6 +7156,22 @@ schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +selecto@~1.26.0: + version "1.26.0" + resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.0.tgz#f3f04fb6409112b198243458f6c9963946d5ba2f" + integrity sha512-cEFKdv5rmkF6pf2OScQJllaNp4UJy/FvviB40ZaMSHrQCxC72X/Q6uhzW1tlb2RE+0danvUNJTs64cI9VXtUyg== + dependencies: + "@daybrush/utils" "^1.13.0" + "@egjs/children-differ" "^1.0.1" + "@scena/dragscroll" "^1.4.0" + "@scena/event-emitter" "^1.0.5" + css-styled "^1.0.8" + css-to-mat "^1.1.1" + framework-utils "^1.1.0" + gesto "^1.19.1" + keycon "^1.2.0" + overlap-area "^1.1.0" + semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"