From 2e618d9c5abd6bcc3c642b0972c283600fb8ad07 Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 26 Nov 2024 19:05:44 +0100 Subject: [PATCH 01/10] feat: add orgconfig 1.2 migration --- .../{v0tov1.py => orgconfigs_migrations.py} | 30 +++++++++++++++++++ apps/api/migrations/orgconfigs/v1_1.py | 14 --------- apps/api/src/routers/dev.py | 21 +++++++++++-- 3 files changed, 49 insertions(+), 16 deletions(-) rename apps/api/migrations/orgconfigs/{v0tov1.py => orgconfigs_migrations.py} (76%) delete mode 100644 apps/api/migrations/orgconfigs/v1_1.py diff --git a/apps/api/migrations/orgconfigs/v0tov1.py b/apps/api/migrations/orgconfigs/orgconfigs_migrations.py similarity index 76% rename from apps/api/migrations/orgconfigs/v0tov1.py rename to apps/api/migrations/orgconfigs/orgconfigs_migrations.py index b010340f..f9d8efe0 100644 --- a/apps/api/migrations/orgconfigs/v0tov1.py +++ b/apps/api/migrations/orgconfigs/orgconfigs_migrations.py @@ -67,3 +67,33 @@ def migrate_v0_to_v1(v0_config): } return v1_config + + +def migrate_to_v1_1(v1_config): + # Start by copying the existing configuration + v1_1_config = v1_config.copy() + + # Update the config version + v1_1_config["config_version"] = "1.1" + + # Add the new 'cloud' object at the end + v1_1_config['cloud'] = { + "plan": "free", + "custom_domain": False + } + + return v1_1_config + +def migrate_to_v1_2(v1_1_config): + v1_2_config = v1_1_config.copy() + + v1_2_config['config_version'] = '1.2' + + # Enable payments for everyone + v1_2_config['features']['payments']['enabled'] = True + + # Only delete stripe_key if it exists + if 'stripe_key' in v1_2_config['features']['payments']: + del v1_2_config['features']['payments']['stripe_key'] + + return v1_2_config \ No newline at end of file diff --git a/apps/api/migrations/orgconfigs/v1_1.py b/apps/api/migrations/orgconfigs/v1_1.py deleted file mode 100644 index e3a3fabb..00000000 --- a/apps/api/migrations/orgconfigs/v1_1.py +++ /dev/null @@ -1,14 +0,0 @@ -def migrate_to_v1_1(v1_config): - # Start by copying the existing configuration - v1_1_config = v1_config.copy() - - # Update the config version - v1_1_config["config_version"] = "1.1" - - # Add the new 'cloud' object at the end - v1_1_config['cloud'] = { - "plan": "free", - "custom_domain": False - } - - return v1_1_config diff --git a/apps/api/src/routers/dev.py b/apps/api/src/routers/dev.py index 5ce8c1b8..52472875 100644 --- a/apps/api/src/routers/dev.py +++ b/apps/api/src/routers/dev.py @@ -1,8 +1,7 @@ from fastapi import APIRouter, Depends from sqlmodel import Session, select from config.config import get_learnhouse_config -from migrations.orgconfigs.v0tov1 import migrate_v0_to_v1 -from migrations.orgconfigs.v1_1 import migrate_to_v1_1 +from migrations.orgconfigs.orgconfigs_migrations import migrate_to_v1_1, migrate_to_v1_2, migrate_v0_to_v1 from src.core.events.database import get_db_session from src.db.organization_config import OrganizationConfig @@ -51,4 +50,22 @@ async def migratev1_1( db_session.add(orgConfig) db_session.commit() + return {"message": "Migration successful"} + +@router.post("/migrate_orgconfig_v1_to_v1.2") +async def migratev1_2( + db_session: Session = Depends(get_db_session), +): + """ + Migrate organization config from v0 to v1 + """ + statement = select(OrganizationConfig) + result = db_session.exec(statement) + + for orgConfig in result: + orgConfig.config = migrate_to_v1_2(orgConfig.config) + + db_session.add(orgConfig) + db_session.commit() + return {"message": "Migration successful"} \ No newline at end of file From f4794ebaf2a0b9741fe79b9068d2a4927f0c49fd Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 28 Nov 2024 21:47:41 +0100 Subject: [PATCH 02/10] fix: login and signup orgslug missing bug --- apps/web/app/auth/signup/signup.tsx | 4 ++-- .../Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx | 12 +++++------- .../Objects/Courses/CourseActions/CoursesActions.tsx | 4 ++-- apps/web/components/Security/AdminAuthorization.tsx | 5 ++++- apps/web/components/Security/HeaderProfileBox.tsx | 4 ++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/web/app/auth/signup/signup.tsx b/apps/web/app/auth/signup/signup.tsx index 71e977e1..8ed3b064 100644 --- a/apps/web/app/auth/signup/signup.tsx +++ b/apps/web/app/auth/signup/signup.tsx @@ -3,7 +3,7 @@ import learnhouseIcon from 'public/learnhouse_bigicon_1.png' import Image from 'next/image' import { getOrgLogoMediaDirectory } from '@services/media/media' import Link from 'next/link' -import { getUriWithOrg } from '@services/config/config' +import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { useLHSession } from '@components/Contexts/LHSessionContext' import React, { useEffect } from 'react' import { MailWarning, Ticket, UserPlus } from 'lucide-react' @@ -191,7 +191,7 @@ const NoTokenScreen = (props: any) => { "Invite code is valid, you'll be redirected to the signup page in a few seconds" ) setTimeout(() => { - router.push(`/signup?inviteCode=${inviteCode}&orgslug=${org.slug}`) + router.push(getUriWithoutOrg(`/signup?inviteCode=${inviteCode}&orgslug=${org.slug}`)) }, 2000) } else { toast.error('Invite code is invalid') diff --git a/apps/web/components/Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx b/apps/web/components/Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx index b5221e66..ae14fe86 100644 --- a/apps/web/components/Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx +++ b/apps/web/components/Dashboard/Pages/Users/OrgAccess/OrgAccess.tsx @@ -1,7 +1,7 @@ import { useOrg } from '@components/Contexts/OrgContext' import PageLoading from '@components/Objects/Loaders/PageLoading' import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal' -import { getAPIUrl, getUriWithOrg } from '@services/config/config' +import { getAPIUrl, getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { swrFetcher } from '@services/utils/ts/requests' import { Globe, Ticket, UserSquare, Users, X } from 'lucide-react' import Link from 'next/link' @@ -173,14 +173,12 @@ function OrgAccess() { - {getUriWithOrg( - org?.slug, - `/signup?inviteCode=${invite.invite_code}` + {getUriWithoutOrg( + `/signup?inviteCode=${invite.invite_code}&orgslug=${org.slug}` )} diff --git a/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx b/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx index bbcabe0d..2d69cdb8 100644 --- a/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx +++ b/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx @@ -6,7 +6,7 @@ import { revalidateTags } from '@services/utils/ts/requests' import { useRouter } from 'next/navigation' import { useLHSession } from '@components/Contexts/LHSessionContext' import { useMediaQuery } from 'usehooks-ts' -import { getUriWithOrg } from '@services/config/config' +import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { getProductsByCourse } from '@services/payments/products' import { LogIn, LogOut, ShoppingCart, AlertCircle } from 'lucide-react' import Modal from '@components/Objects/StyledElements/Modal/Modal' @@ -126,7 +126,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => { const handleCourseAction = async () => { if (!session.data?.user) { - router.push(getUriWithOrg(orgslug, '/signup?orgslug=' + orgslug)) + router.push(getUriWithoutOrg(`/signup?orgslug=${orgslug}`)) return } const action = isStarted ? removeCourse : startCourse diff --git a/apps/web/components/Security/AdminAuthorization.tsx b/apps/web/components/Security/AdminAuthorization.tsx index 7840317f..7afdefe8 100644 --- a/apps/web/components/Security/AdminAuthorization.tsx +++ b/apps/web/components/Security/AdminAuthorization.tsx @@ -4,6 +4,8 @@ import { useLHSession } from '@components/Contexts/LHSessionContext'; import useAdminStatus from '@components/Hooks/useAdminStatus'; import { usePathname, useRouter } from 'next/navigation'; import PageLoading from '@components/Objects/Loaders/PageLoading'; +import { getUriWithoutOrg } from '@services/config/config'; +import { useOrg } from '@components/Contexts/OrgContext'; type AuthorizationProps = { children: React.ReactNode; @@ -22,6 +24,7 @@ const ADMIN_PATHS = [ const AdminAuthorization: React.FC = ({ children, authorizationMode }) => { const session = useLHSession() as any; + const org = useOrg() as any; const pathname = usePathname(); const router = useRouter(); const { isAdmin, loading } = useAdminStatus() as any @@ -51,7 +54,7 @@ const AdminAuthorization: React.FC = ({ children, authorizat } if (!isUserAuthenticated) { - router.push('/login'); + router.push(getUriWithoutOrg('/login?orgslug=' + org.slug)); return; } diff --git a/apps/web/components/Security/HeaderProfileBox.tsx b/apps/web/components/Security/HeaderProfileBox.tsx index 6fbddd2b..f61d875d 100644 --- a/apps/web/components/Security/HeaderProfileBox.tsx +++ b/apps/web/components/Security/HeaderProfileBox.tsx @@ -25,10 +25,10 @@ export const HeaderProfileBox = () => {
  • Login + href={{ pathname: getUriWithoutOrg('/login?orgslug=' + org.slug), query: org ? { orgslug: org.slug } : null }} >Login
  • - Sign up + { From 90a47880cbf4b790f22d384a0d1c10e6809c2030 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 28 Nov 2024 22:25:19 +0100 Subject: [PATCH 05/10] fix: No way to add an answer for question after deleting all answers for quizzes on admin assignment page #359 --- .../TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx index beeb26b0..7c4849ee 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx @@ -74,8 +74,12 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro const removeOption = (qIndex: number, oIndex: number) => { const updatedQuestions = [...questions]; - updatedQuestions[qIndex].options.splice(oIndex, 1); - setQuestions(updatedQuestions); + if (updatedQuestions[qIndex].options.length > 1) { + updatedQuestions[qIndex].options.splice(oIndex, 1); + setQuestions(updatedQuestions); + } else { + toast.error('Cannot delete the last option. At least one option is required.'); + } }; const addQuestion = () => { From 780968ba06be2f5e885ad089b652989b81c55811 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 28 Nov 2024 22:29:03 +0100 Subject: [PATCH 06/10] fix: Interactive quizzes, add and removing answers is challenge due to CSS scaling effect in editor page #343 --- .../Objects/Editor/Extensions/Quiz/QuizBlockComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/components/Objects/Editor/Extensions/Quiz/QuizBlockComponent.tsx b/apps/web/components/Objects/Editor/Extensions/Quiz/QuizBlockComponent.tsx index 3e81aa8e..a9c44b2d 100644 --- a/apps/web/components/Objects/Editor/Extensions/Quiz/QuizBlockComponent.tsx +++ b/apps/web/components/Objects/Editor/Extensions/Quiz/QuizBlockComponent.tsx @@ -291,7 +291,7 @@ function QuizBlockComponent(props: any) {
    From a6506d5339c3585bb61fbf33e2e7e5a3fc540bad Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 28 Nov 2024 22:42:03 +0100 Subject: [PATCH 07/10] fix: Embed block inputs accepts empty input #364 --- .../EmbedObjects/EmbedObjectsComponent.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx index 1cf77791..c5172c94 100644 --- a/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx +++ b/apps/web/components/Objects/Editor/Extensions/EmbedObjects/EmbedObjectsComponent.tsx @@ -53,22 +53,29 @@ function EmbedObjectsComponent(props: any) { const handleUrlChange = (event: React.ChangeEvent) => { const newUrl = event.target.value; - // Sanitize the URL - const sanitizedUrl = DOMPurify.sanitize(newUrl); - setEmbedUrl(sanitizedUrl); - props.updateAttributes({ - embedUrl: sanitizedUrl, - embedType: 'url', - }); + const trimmedUrl = newUrl.trim(); + // Only update if URL is not just whitespace + if (newUrl === '' || trimmedUrl) { + const sanitizedUrl = DOMPurify.sanitize(newUrl); + setEmbedUrl(sanitizedUrl); + props.updateAttributes({ + embedUrl: sanitizedUrl, + embedType: 'url', + }); + } }; const handleCodeChange = (event: React.ChangeEvent) => { const newCode = event.target.value; - setEmbedCode(newCode); - props.updateAttributes({ - embedCode: newCode, - embedType: 'code', - }); + const trimmedCode = newCode.trim(); + // Only update if code is not just whitespace + if (newCode === '' || trimmedCode) { + setEmbedCode(newCode); + props.updateAttributes({ + embedCode: newCode, + embedType: 'code', + }); + } }; const handleResizeStart = (event: React.MouseEvent, direction: 'horizontal' | 'vertical') => { From 8acceb5ba955f18bd82b3bb7bf749be792a5bf09 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 28 Nov 2024 22:51:06 +0100 Subject: [PATCH 08/10] fix: Badge template floating element covers toggle, makes it hard to close for when don't want to choose in editor page #366 --- .../Editor/Extensions/Badges/BadgesExtension.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx b/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx index 888eb730..41e1a2fc 100644 --- a/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx +++ b/apps/web/components/Objects/Editor/Extensions/Badges/BadgesExtension.tsx @@ -135,13 +135,11 @@ const BadgesExtension: React.FC = (props: any) => { return ( -
    -
    +
    +
    {emoji} {isEditable && ( @@ -176,6 +174,7 @@ const BadgesExtension: React.FC = (props: any) => {
    )}
    + {isEditable && ( )} + {isEditable && showPredefinedCallouts && ( -
    +
    {predefinedBadges.map((badge, index) => (