From b1d05168b6e98ad21ca9cd7a5cda48551265af1f Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 27 Sep 2024 19:16:31 +0200 Subject: [PATCH 1/8] feat: allow zip file upload --- apps/api/src/services/courses/activities/uploads/sub_file.py | 2 +- .../src/services/courses/activities/uploads/tasks_ref_files.py | 2 +- .../_components/TaskEditor/Subs/AssignmentTaskGeneralEdit.tsx | 2 +- .../_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api/src/services/courses/activities/uploads/sub_file.py b/apps/api/src/services/courses/activities/uploads/sub_file.py index 9ebd46f5..c6d16e74 100644 --- a/apps/api/src/services/courses/activities/uploads/sub_file.py +++ b/apps/api/src/services/courses/activities/uploads/sub_file.py @@ -19,5 +19,5 @@ async def upload_submission_file( org_uuid, contents, f"{name_in_disk}", - ["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx"], + ["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx", "zip"], ) diff --git a/apps/api/src/services/courses/activities/uploads/tasks_ref_files.py b/apps/api/src/services/courses/activities/uploads/tasks_ref_files.py index 56981cc0..af9814ef 100644 --- a/apps/api/src/services/courses/activities/uploads/tasks_ref_files.py +++ b/apps/api/src/services/courses/activities/uploads/tasks_ref_files.py @@ -19,5 +19,5 @@ async def upload_reference_file( org_uuid, contents, f"{name_in_disk}", - ["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx"], + ["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx", "zip"], ) diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskGeneralEdit.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskGeneralEdit.tsx index 55d44598..d9221af0 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskGeneralEdit.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskGeneralEdit.tsx @@ -90,7 +90,7 @@ export function AssignmentTaskGeneralEdit() {
-

Allowed formats : pdf, docx, mp4, jpg, jpeg, png, pptx

+

Allowed formats : pdf, docx, mp4, jpg, jpeg, png, pptx, zip

diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx index 946fae70..53c8b4f5 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx @@ -263,7 +263,7 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta )}
-

Allowed formats : pdf, docx, mp4, jpg, jpeg, png, pptx

+

Allowed formats : pdf, docx, mp4, jpg, jpeg, png, pptx, zip

{isLoading ? (
From d8be3210215b51e2a9421f714c16575a45d540c7 Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 27 Sep 2024 20:58:10 +0200 Subject: [PATCH 2/8] feat: add ExternalObjects (Embeds) to editor --- .../Activities/DynamicCanva/DynamicCanva.tsx | 5 + apps/web/components/Objects/Editor/Editor.tsx | 5 + .../Extensions/EmbedObjects/EmbedObjects.ts | 49 +++++ .../EmbedObjects/EmbedObjectsComponent.tsx | 197 ++++++++++++++++++ .../Objects/Editor/Toolbar/ToolbarButtons.tsx | 12 +- apps/web/package.json | 1 + apps/web/pnpm-lock.yaml | 12 ++ 7 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects.ts create mode 100644 apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx diff --git a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx index e5dbf880..ceda4d69 100644 --- a/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx +++ b/apps/web/components/Objects/Activities/DynamicCanva/DynamicCanva.tsx @@ -24,6 +24,7 @@ import java from 'highlight.js/lib/languages/java' import { NoTextInput } from '@components/Objects/Editor/Extensions/NoTextInput/NoTextInput' import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext' import AICanvaToolkit from './AI/AICanvaToolkit' +import EmbedObjects from '@components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects' interface Editor { content: string @@ -85,6 +86,10 @@ function Canva(props: Editor) { CodeBlockLowlight.configure({ lowlight, }), + EmbedObjects.configure({ + editable: isEditable, + activity: props.activity, + }), ], content: props.content, diff --git a/apps/web/components/Objects/Editor/Editor.tsx b/apps/web/components/Objects/Editor/Editor.tsx index 185c8907..22d4c13d 100644 --- a/apps/web/components/Objects/Editor/Editor.tsx +++ b/apps/web/components/Objects/Editor/Editor.tsx @@ -48,6 +48,7 @@ import Collaboration from '@tiptap/extension-collaboration' import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import ActiveAvatars from './ActiveAvatars' import { getUriWithOrg } from '@services/config/config' +import EmbedObjects from './Extensions/EmbedObjects/EmbedObjects' interface Editor { content: string @@ -133,6 +134,10 @@ function Editor(props: Editor) { CodeBlockLowlight.configure({ lowlight, }), + EmbedObjects.configure({ + editable: true, + activity: props.activity, + }), // Add Collaboration and CollaborationCursor only if isCollabEnabledOnThisOrg is true ...(props.isCollabEnabledOnThisOrg ? [ diff --git a/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects.ts b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects.ts new file mode 100644 index 00000000..32d8eb4f --- /dev/null +++ b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjects.ts @@ -0,0 +1,49 @@ +import { mergeAttributes, Node } from '@tiptap/core' +import { ReactNodeViewRenderer } from '@tiptap/react' +import EmbedObjectsComponent from './EmbedObjectsComponent' + + +export default Node.create({ + name: 'blockEmbed', + group: 'block', + + addAttributes() { + return { + embedUrl: { + default: null, + }, + embedCode: { + default: null, + }, + embedType: { + default: null, + }, + embedHeight: { + default: 300, + }, + embedWidth: { + default: '100%', + }, + alignment: { + default: 'left', + }, + } + }, + + parseHTML() { + return [ + { + tag: 'block-embed', + }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['block-embed', mergeAttributes(HTMLAttributes), 0] + }, + + addNodeView() { + return ReactNodeViewRenderer(EmbedObjectsComponent) + }, + +}) \ No newline at end of file diff --git a/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx new file mode 100644 index 00000000..60b058c7 --- /dev/null +++ b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx @@ -0,0 +1,197 @@ +import { NodeViewWrapper } from '@tiptap/react' +import React, { useState, useRef } from 'react' +import { Upload, Link as LinkIcon, GripVertical, GripHorizontal, AlignCenter, Cuboid, Code } from 'lucide-react' +import { useEditorProvider } from '@components/Contexts/Editor/EditorContext' +import { SiGithub, SiReplit, SiSpotify, SiLoom, SiGooglemaps, SiCodepen, SiCanva, SiNotion, SiGoogledocs, SiGitlab, SiX, SiFigma, SiGiphy } from '@icons-pack/react-simple-icons' +import { useRouter } from 'next/navigation' + +function EmbedObjectsComponent(props: any) { + const [embedType, setEmbedType] = useState<'url' | 'code'>(props.node.attrs.embedType || 'url') + const [embedUrl, setEmbedUrl] = useState(props.node.attrs.embedUrl || '') + const [embedCode, setEmbedCode] = useState(props.node.attrs.embedCode || '') + const [embedHeight, setEmbedHeight] = useState(props.node.attrs.embedHeight || 300) + const [embedWidth, setEmbedWidth] = useState(props.node.attrs.embedWidth || '100%') + const [alignment, setAlignment] = useState(props.node.attrs.alignment || 'left') + + const resizeRef = useRef(null) + const editorState = useEditorProvider() as any + const isEditable = editorState.isEditable + const router = useRouter() + + const supportedProducts = [ + { name: 'GitHub', icon: SiGithub, color: '#181717', guide: 'https://emgithub.com/' }, + { name: 'Replit', icon: SiReplit, color: '#F26207', guide: 'https://docs.replit.com/hosting/embedding-repls' }, + { name: 'Spotify', icon: SiSpotify, color: '#1DB954', guide: 'https://developer.spotify.com/documentation/embeds' }, + { name: 'Loom', icon: SiLoom, color: '#625DF5', guide: 'https://support.loom.com/hc/en-us/articles/360002208317-How-to-embed-your-video-into-a-webpage' }, + { name: 'GMaps', icon: SiGooglemaps, color: '#4285F4', guide: 'https://developers.google.com/maps/documentation/embed/get-started' }, + { name: 'CodePen', icon: SiCodepen, color: '#000000', guide: 'https://blog.codepen.io/documentation/embedded-pens/' }, + { name: 'Canva', icon: SiCanva, color: '#00C4CC', guide: 'https://www.canva.com/help/article/embed-designs' }, + { name: 'Notion', icon: SiNotion, color: '#878787', guide: 'https://www.notion.so/help/embed-and-connect-other-apps#7a70ac4b5c5f4ec889e69d262e0de9e7' }, + { name: 'G Docs', icon: SiGoogledocs, color: '#4285F4', guide: 'https://support.google.com/docs/answer/183965?hl=en&co=GENIE.Platform%3DDesktop' }, + { name: 'X', icon: SiX, color: '#000000', guide: 'https://help.twitter.com/en/using-twitter/how-to-embed-a-tweet' }, + { name: 'Figma', icon: SiFigma, color: '#F24E1E', guide: 'https://help.figma.com/hc/en-us/articles/360041057214-Embed-files-and-prototypes' }, + { name: 'Giphy', icon: SiGiphy, color: '#FF6666', guide: 'https://developers.giphy.com/docs/embed/' }, + ] + + const handleEmbedTypeChange = (type: 'url' | 'code') => { + setEmbedType(type) + props.updateAttributes({ embedType: type }) + } + + const handleUrlChange = (event: React.ChangeEvent) => { + const newUrl = event.target.value; + setEmbedUrl(newUrl); + props.updateAttributes({ + embedUrl: newUrl, + embedType: 'url', + }); + }; + + const handleCodeChange = (event: React.ChangeEvent) => { + const newCode = event.target.value; + setEmbedCode(newCode); + props.updateAttributes({ + embedCode: newCode, + embedType: 'code', + }); + }; + + const handleResizeStart = (event: React.MouseEvent, direction: 'horizontal' | 'vertical') => { + event.preventDefault() + const startX = event.clientX + const startY = event.clientY + const startWidth = resizeRef.current?.offsetWidth || 0 + const startHeight = resizeRef.current?.offsetHeight || 0 + + const handleMouseMove = (e: MouseEvent) => { + if (resizeRef.current) { + if (direction === 'horizontal') { + const newWidth = startWidth + e.clientX - startX + const parentWidth = resizeRef.current.parentElement?.offsetWidth || 1 + const widthPercentage = Math.min(100, Math.max(10, (newWidth / parentWidth) * 100)) + const newWidthValue = `${widthPercentage}%` + setEmbedWidth(newWidthValue) + props.updateAttributes({ embedWidth: newWidthValue }) + } else { + const newHeight = Math.max(100, startHeight + e.clientY - startY) + setEmbedHeight(newHeight) + props.updateAttributes({ embedHeight: newHeight }) + } + } + } + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } + + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + } + + const handleCenterBlock = () => { + const newAlignment = alignment === 'center' ? 'left' : 'center' + setAlignment(newAlignment) + props.updateAttributes({ alignment: newAlignment }) + } + + const handleProductClick = (guide: string) => { + window.open(guide, '_blank', 'noopener,noreferrer') + } + + return ( + +
+ {embedType === 'url' && embedUrl ? ( +