mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: init usergroup linking to a course
This commit is contained in:
parent
a6152ef1f5
commit
4f75e6a90a
12 changed files with 428 additions and 55 deletions
|
|
@ -9,6 +9,7 @@ from src.services.users.usergroups import (
|
||||||
add_users_to_usergroup,
|
add_users_to_usergroup,
|
||||||
create_usergroup,
|
create_usergroup,
|
||||||
delete_usergroup_by_id,
|
delete_usergroup_by_id,
|
||||||
|
get_usergroups_by_resource,
|
||||||
read_usergroup_by_id,
|
read_usergroup_by_id,
|
||||||
read_usergroups_by_org_id,
|
read_usergroups_by_org_id,
|
||||||
remove_resources_from_usergroup,
|
remove_resources_from_usergroup,
|
||||||
|
|
@ -64,6 +65,19 @@ async def api_get_usergroups(
|
||||||
"""
|
"""
|
||||||
return await read_usergroups_by_org_id(request, db_session, current_user, org_id)
|
return await read_usergroups_by_org_id(request, db_session, current_user, org_id)
|
||||||
|
|
||||||
|
@router.get("/resource/{resource_uuid}", response_model=list[UserGroupRead], tags=["usergroups"])
|
||||||
|
async def api_get_usergroupsby_resource(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
resource_uuid: str,
|
||||||
|
) -> list[UserGroupRead]:
|
||||||
|
"""
|
||||||
|
Get UserGroups by Org
|
||||||
|
"""
|
||||||
|
return await get_usergroups_by_resource(request, db_session, current_user, resource_uuid)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"])
|
@router.put("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"])
|
||||||
async def api_update_usergroup(
|
async def api_update_usergroup(
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,41 @@ async def read_usergroups_by_org_id(
|
||||||
return usergroups
|
return usergroups
|
||||||
|
|
||||||
|
|
||||||
|
async def get_usergroups_by_resource(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
resource_uuid: str,
|
||||||
|
) -> list[UserGroupRead]:
|
||||||
|
|
||||||
|
statement = select(UserGroupResource).where(
|
||||||
|
UserGroupResource.resource_uuid == resource_uuid
|
||||||
|
)
|
||||||
|
usergroup_resources = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request,
|
||||||
|
usergroup_uuid="usergroup_X",
|
||||||
|
current_user=current_user,
|
||||||
|
action="read",
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
usergroup_ids = [usergroup.usergroup_id for usergroup in usergroup_resources]
|
||||||
|
|
||||||
|
# get usergroups
|
||||||
|
usergroups = []
|
||||||
|
for usergroup_id in usergroup_ids:
|
||||||
|
statement = select(UserGroup).where(UserGroup.id == usergroup_id)
|
||||||
|
usergroup = db_session.exec(statement).first()
|
||||||
|
usergroups.append(usergroup)
|
||||||
|
|
||||||
|
usergroups = [UserGroupRead.from_orm(usergroup) for usergroup in usergroups]
|
||||||
|
|
||||||
|
return usergroups
|
||||||
|
|
||||||
|
|
||||||
async def update_usergroup_by_id(
|
async def update_usergroup_by_id(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
|
@ -258,7 +293,6 @@ async def remove_users_from_usergroup(
|
||||||
detail="UserGroup not found",
|
detail="UserGroup not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
# RBAC check
|
|
||||||
# RBAC check
|
# RBAC check
|
||||||
await rbac_check(
|
await rbac_check(
|
||||||
request,
|
request,
|
||||||
|
|
@ -312,8 +346,21 @@ async def add_resources_to_usergroup(
|
||||||
resources_uuids_array = resources_uuids.split(",")
|
resources_uuids_array = resources_uuids.split(",")
|
||||||
|
|
||||||
for resource_uuid in resources_uuids_array:
|
for resource_uuid in resources_uuids_array:
|
||||||
# TODO : Find a way to check if resource exists
|
# Check if a link between UserGroup and Resource already exists
|
||||||
|
statement = select(UserGroupResource).where(
|
||||||
|
UserGroupResource.usergroup_id == usergroup_id,
|
||||||
|
UserGroupResource.resource_uuid == resource_uuid,
|
||||||
|
)
|
||||||
|
usergroup_resource = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if usergroup_resource:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Resource {resource_uuid} already exists in UserGroup",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TODO : Find a way to check if resource really exists
|
||||||
usergroup_obj = UserGroupResource(
|
usergroup_obj = UserGroupResource(
|
||||||
usergroup_id=usergroup_id,
|
usergroup_id=usergroup_id,
|
||||||
resource_uuid=resource_uuid,
|
resource_uuid=resource_uuid,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import Link from 'next/link'
|
||||||
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop'
|
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral'
|
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral'
|
||||||
import { GalleryVerticalEnd, Info } from 'lucide-react'
|
import { GalleryVerticalEnd, Info, Lock, UserRoundCog } from 'lucide-react'
|
||||||
|
import EditCourseAccess from '@components/Dashboard/Course/EditCourseAccess/EditCourseAccess'
|
||||||
|
|
||||||
export type CourseOverviewParams = {
|
export type CourseOverviewParams = {
|
||||||
orgslug: string
|
orgslug: string
|
||||||
|
|
@ -24,9 +25,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]">
|
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]">
|
||||||
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
|
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
|
||||||
<div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">
|
<div className="pl-10 pr-10 text-sm tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">
|
||||||
<CourseOverviewTop params={params} />
|
<CourseOverviewTop params={params} />
|
||||||
<div className="flex space-x-5 font-black text-sm">
|
<div className="flex space-x-3 font-black text-xs">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
getUriWithOrg(params.orgslug, '') +
|
getUriWithOrg(params.orgslug, '') +
|
||||||
|
|
@ -34,8 +35,7 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general'
|
||||||
params.subpage.toString() === 'general'
|
|
||||||
? 'border-b-4'
|
? 'border-b-4'
|
||||||
: 'opacity-50'
|
: 'opacity-50'
|
||||||
} cursor-pointer`}
|
} cursor-pointer`}
|
||||||
|
|
@ -46,6 +46,24 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
getUriWithOrg(params.orgslug, '') +
|
||||||
|
`/dash/courses/course/${params.courseuuid}/access`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'access'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
|
<UserRoundCog size={16} />
|
||||||
|
<div>Access</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
getUriWithOrg(params.orgslug, '') +
|
getUriWithOrg(params.orgslug, '') +
|
||||||
|
|
@ -53,8 +71,7 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${
|
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'content'
|
||||||
params.subpage.toString() === 'content'
|
|
||||||
? 'border-b-4'
|
? 'border-b-4'
|
||||||
: 'opacity-50'
|
: 'opacity-50'
|
||||||
} cursor-pointer`}
|
} cursor-pointer`}
|
||||||
|
|
@ -65,7 +82,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|
@ -74,16 +93,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
|
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
|
||||||
className="h-full overflow-y-auto"
|
className="h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
{params.subpage == 'content' ? (
|
{params.subpage == 'content' ? (<EditCourseStructure orgslug={params.orgslug} />) : ('')}
|
||||||
<EditCourseStructure orgslug={params.orgslug} />
|
{params.subpage == 'general' ? (<EditCourseGeneral orgslug={params.orgslug} />) : ('')}
|
||||||
) : (
|
{params.subpage == 'access' ? (<EditCourseAccess orgslug={params.orgslug} />) : ('')}
|
||||||
''
|
|
||||||
)}
|
|
||||||
{params.subpage == 'general' ? (
|
|
||||||
<EditCourseGeneral orgslug={params.orgslug} />
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</CourseProvider>
|
</CourseProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client'
|
'use client'
|
||||||
import { OrgProvider } from '@components/Contexts/OrgContext'
|
import { OrgProvider } from '@components/Contexts/OrgContext'
|
||||||
import SessionProvider from '@components/Contexts/SessionContext'
|
import SessionProvider from '@components/Contexts/SessionContext'
|
||||||
|
import Toast from '@components/StyledElements/Toast/Toast'
|
||||||
import '@styles/globals.css'
|
import '@styles/globals.css'
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
@ -12,6 +13,7 @@ export default function RootLayout({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Toast />
|
||||||
<OrgProvider orgslug={params.orgslug}>
|
<OrgProvider orgslug={params.orgslug}>
|
||||||
<SessionProvider>{children}</SessionProvider>
|
<SessionProvider>{children}</SessionProvider>
|
||||||
</OrgProvider>
|
</OrgProvider>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'
|
||||||
|
import LinkToUserGroup from '@components/Objects/Modals/Dash/EditCourseAccess/LinkToUserGroup'
|
||||||
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
import { unLinkResourcesToUserGroup } from '@services/usergroups/usergroups'
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
|
import { Globe, Users, UsersRound, X } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
|
type EditCourseAccessProps = {
|
||||||
|
orgslug: string
|
||||||
|
course_uuid?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditCourseAccess(props: EditCourseAccessProps) {
|
||||||
|
const [error, setError] = React.useState('')
|
||||||
|
|
||||||
|
const course = useCourse() as any
|
||||||
|
const dispatchCourse = useCourseDispatch() as any
|
||||||
|
const courseStructure = course.courseStructure
|
||||||
|
const { data: usergroups } = useSWR(
|
||||||
|
courseStructure ? `${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
const [isPublic, setIsPublic] = React.useState(courseStructure.public)
|
||||||
|
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// This code will run whenever form values are updated
|
||||||
|
if (isPublic !== courseStructure.public) {
|
||||||
|
dispatchCourse({ type: 'setIsNotSaved' })
|
||||||
|
const updatedCourse = {
|
||||||
|
...courseStructure,
|
||||||
|
public: isPublic,
|
||||||
|
}
|
||||||
|
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse })
|
||||||
|
}
|
||||||
|
}, [course, isPublic])
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{' '}
|
||||||
|
<div className="h-6"></div>
|
||||||
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4">
|
||||||
|
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||||
|
<h1 className="font-bold text-xl text-gray-800">Access to the course</h1>
|
||||||
|
<h2 className="text-gray-500 text-sm">
|
||||||
|
{' '}
|
||||||
|
Choose if want your course to be publicly available on the internet or only accessible to signed in users{' '}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 mx-auto mb-3">
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationButtonText="Change to Public"
|
||||||
|
confirmationMessage="Are you sure you want this course to be publicly available on the internet ?"
|
||||||
|
dialogTitle={'Change to Public ?'}
|
||||||
|
dialogTrigger={
|
||||||
|
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
|
||||||
|
{isPublic ? (
|
||||||
|
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
|
||||||
|
Active
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||||
|
<Globe className="text-slate-400" size={40}></Globe>
|
||||||
|
<div className="text-2xl text-slate-700 font-bold">
|
||||||
|
Public
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-md tracking-tight w-[500px] leading-5 text-center">
|
||||||
|
The Course is publicly available on the internet, it is indexed by search engines and can be accessed by anyone
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
setIsPublic(true)
|
||||||
|
}}
|
||||||
|
status="info"
|
||||||
|
></ConfirmationModal>
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationButtonText="Change to Users Only"
|
||||||
|
confirmationMessage="Are you sure you want this course to be only accessible to signed in users ?"
|
||||||
|
dialogTitle={'Change to Users Only ?'}
|
||||||
|
dialogTrigger={
|
||||||
|
<div className="w-full h-[200px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
|
||||||
|
{!isPublic ? (
|
||||||
|
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
|
||||||
|
Active
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||||
|
<Users className="text-slate-400" size={40}></Users>
|
||||||
|
<div className="text-2xl text-slate-700 font-bold">
|
||||||
|
Users Only
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-md tracking-tight w-[500px] leading-5 text-center">
|
||||||
|
The Course is only accessible to signed in users, additionaly you can choose which UserGroups can access this course
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
setIsPublic(false)
|
||||||
|
}}
|
||||||
|
status="info"
|
||||||
|
></ConfirmationModal>
|
||||||
|
</div>
|
||||||
|
<UserGroupsSection usergroups={usergroups} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function UserGroupsSection({ usergroups }: { usergroups: any[] }) {
|
||||||
|
const course = useCourse() as any
|
||||||
|
const [userGroupModal, setUserGroupModal] = React.useState(false)
|
||||||
|
|
||||||
|
const removeUserGroupLink = async (usergroup_id: number) => {
|
||||||
|
const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid)
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('Successfully unliked from usergroup')
|
||||||
|
mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||||
|
<h1 className="font-bold text-xl text-gray-800">UserGroups</h1>
|
||||||
|
<h2 className="text-gray-500 text-sm">
|
||||||
|
{' '}
|
||||||
|
Choose which UserGroups can access this course{' '}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||||
|
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||||
|
<tr className="font-bolder text-sm">
|
||||||
|
<th className="py-3 px-4">Name</th>
|
||||||
|
<th className="py-3 px-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<>
|
||||||
|
<tbody className="mt-5 bg-white rounded-md">
|
||||||
|
{usergroups?.map((usergroup: any) => (
|
||||||
|
<tr
|
||||||
|
key={usergroup.invite_code_uuid}
|
||||||
|
className="border-b border-gray-100 text-sm"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-4">{usergroup.name}</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationButtonText="Delete Link"
|
||||||
|
confirmationMessage="Users from this UserGroup will no longer have access to this course"
|
||||||
|
dialogTitle={'Unlink UserGroup ?'}
|
||||||
|
dialogTrigger={
|
||||||
|
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
<span> Delete link</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
removeUserGroupLink(usergroup.id)
|
||||||
|
}}
|
||||||
|
status="warning"
|
||||||
|
></ConfirmationModal>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</>
|
||||||
|
</table>
|
||||||
|
<div className='flex flex-row-reverse mt-3 mr-2'>
|
||||||
|
<Modal
|
||||||
|
isDialogOpen={
|
||||||
|
userGroupModal
|
||||||
|
}
|
||||||
|
onOpenChange={() =>
|
||||||
|
setUserGroupModal(!userGroupModal)
|
||||||
|
}
|
||||||
|
minHeight="no-min"
|
||||||
|
dialogContent={
|
||||||
|
<LinkToUserGroup setUserGroupModal={setUserGroupModal} />
|
||||||
|
|
||||||
|
}
|
||||||
|
dialogTitle="Link Course to a UserGroup"
|
||||||
|
dialogDescription={
|
||||||
|
'Choose which UserGroups can access this course'
|
||||||
|
}
|
||||||
|
dialogTrigger={
|
||||||
|
<button
|
||||||
|
className=" flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||||
|
>
|
||||||
|
<UsersRound className="w-4 h-4" />
|
||||||
|
<span>Link to a UserGroup</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div></>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditCourseAccess
|
||||||
|
|
@ -116,10 +116,11 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
message={formik.errors.description}
|
message={formik.errors.description}
|
||||||
/>
|
/>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Textarea
|
<Input
|
||||||
style={{ backgroundColor: 'white' }}
|
style={{ backgroundColor: 'white' }}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value={formik.values.description}
|
value={formik.values.description}
|
||||||
|
type='text'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|
@ -177,26 +178,7 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField className="flex items-center h-10" name="public">
|
|
||||||
<div className="flex my-auto items-center">
|
|
||||||
<label
|
|
||||||
className="text-black text-[15px] leading-none pr-[15px]"
|
|
||||||
htmlFor="public-course"
|
|
||||||
>
|
|
||||||
Public Course
|
|
||||||
</label>
|
|
||||||
<Switch.Root
|
|
||||||
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
|
|
||||||
id="public-course"
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
formik.setFieldValue('public', checked)
|
|
||||||
}
|
|
||||||
checked={formik.values.public === 'true'}
|
|
||||||
>
|
|
||||||
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-neutral-300 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
|
||||||
</Switch.Root>
|
|
||||||
</div>
|
|
||||||
</FormField>
|
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -46,18 +46,17 @@ function ThumbnailUpdate() {
|
||||||
{localThumbnail ? (
|
{localThumbnail ? (
|
||||||
<img
|
<img
|
||||||
src={URL.createObjectURL(localThumbnail)}
|
src={URL.createObjectURL(localThumbnail)}
|
||||||
className={`${
|
className={`${isLoading ? 'animate-pulse' : ''
|
||||||
isLoading ? 'animate-pulse' : ''
|
|
||||||
} shadow w-[200px] h-[100px] rounded-md`}
|
} shadow w-[200px] h-[100px] rounded-md`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={`${getCourseThumbnailMediaDirectory(
|
src={`${course.courseStructure.thumbnail_image ? getCourseThumbnailMediaDirectory(
|
||||||
org?.org_uuid,
|
org?.org_uuid,
|
||||||
course.courseStructure.course_uuid,
|
course.courseStructure.course_uuid,
|
||||||
course.courseStructure.thumbnail_image
|
course.courseStructure.thumbnail_image
|
||||||
)}`}
|
) : '/empty_thumbnail.png'}`}
|
||||||
className="shadow w-[200px] h-[100px] rounded-md"
|
className="shadow w-[200px] h-[100px] rounded-md bg-gray-200"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ function BreadCrumbs(props: BreadCrumbsProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-2"></div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function CourseOverviewTop({
|
||||||
last_breadcrumb={course.courseStructure.name}
|
last_breadcrumb={course.courseStructure.name}
|
||||||
></BreadCrumbs>
|
></BreadCrumbs>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex py-5 grow items-center">
|
<div className="flex py-3 grow items-center">
|
||||||
<Link
|
<Link
|
||||||
href={getUriWithOrg(org?.slug, '') + `/course/${params.courseuuid}`}
|
href={getUriWithOrg(org?.slug, '') + `/course/${params.courseuuid}`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
'use client';
|
||||||
|
import { useCourse } from '@components/Contexts/CourseContext';
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
|
import { getAPIUrl } from '@services/config/config';
|
||||||
|
import { linkResourcesToUserGroup } from '@services/usergroups/usergroups';
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
|
type LinkToUserGroupProps = {
|
||||||
|
// React function, todo: fix types
|
||||||
|
setUserGroupModal: any
|
||||||
|
}
|
||||||
|
|
||||||
|
function LinkToUserGroup(props: LinkToUserGroupProps) {
|
||||||
|
const course = useCourse() as any
|
||||||
|
const org = useOrg() as any
|
||||||
|
const courseStructure = course.courseStructure
|
||||||
|
|
||||||
|
const { data: usergroups } = useSWR(
|
||||||
|
courseStructure && org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any
|
||||||
|
|
||||||
|
|
||||||
|
const handleLink = async () => {
|
||||||
|
console.log('selectedUserGroup', selectedUserGroup)
|
||||||
|
const res = await linkResourcesToUserGroup(selectedUserGroup, courseStructure.course_uuid)
|
||||||
|
if (res.status === 200) {
|
||||||
|
props.setUserGroupModal(false)
|
||||||
|
toast.success('Successfully linked to usergroup')
|
||||||
|
mutate(`${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (usergroups && usergroups.length > 0) {
|
||||||
|
setSelectedUserGroup(usergroups[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, [usergroups])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='p-4 flex-row flex justify-between items-center'>
|
||||||
|
|
||||||
|
<div className='py-3'>
|
||||||
|
<span className='px-3 text-gray-400 font-bold rounded-full py-1 bg-gray-100 mx-3'>UserGroup Name </span>
|
||||||
|
<select
|
||||||
|
onChange={(e) => setSelectedUserGroup(e.target.value)}
|
||||||
|
defaultValue={selectedUserGroup}
|
||||||
|
>
|
||||||
|
{usergroups && usergroups.map((group: any) => (
|
||||||
|
<option key={group.id} value={group.id}>{group.name}</option>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className='py-3'>
|
||||||
|
<button onClick={() => { handleLink() }} className='bg-green-700 text-white font-bold px-4 py-2 rounded-md shadow'>Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LinkToUserGroup
|
||||||
|
|
@ -15,7 +15,7 @@ export const FormLabelAndMessage = (props: {
|
||||||
message?: string
|
message?: string
|
||||||
}) => (
|
}) => (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<FormLabel className="grow">{props.label}</FormLabel>
|
<FormLabel className="grow text-sm">{props.label}</FormLabel>
|
||||||
{(props.message && (
|
{(props.message && (
|
||||||
<div className="text-red-700 text-sm items-center rounded-md flex space-x-1">
|
<div className="text-red-700 text-sm items-center rounded-md flex space-x-1">
|
||||||
<Info size={10} />
|
<Info size={10} />
|
||||||
|
|
@ -35,7 +35,6 @@ export const FormField = styled(Form.Field, {
|
||||||
})
|
})
|
||||||
|
|
||||||
export const FormLabel = styled(Form.Label, {
|
export const FormLabel = styled(Form.Label, {
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
lineHeight: '35px',
|
lineHeight: '35px',
|
||||||
color: 'black',
|
color: 'black',
|
||||||
|
|
|
||||||
35
apps/web/services/usergroups/usergroups.ts
Normal file
35
apps/web/services/usergroups/usergroups.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
import { RequestBody, getResponseMetadata } from '@services/utils/ts/requests'
|
||||||
|
|
||||||
|
export async function getUserGroups(org_id: any) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/org/${org_id}`,
|
||||||
|
RequestBody('GET', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function linkResourcesToUserGroup(
|
||||||
|
usergroup_id: any,
|
||||||
|
resource_uuids: any
|
||||||
|
) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/${usergroup_id}/add_resources?resource_uuids=${resource_uuids}`,
|
||||||
|
RequestBody('POST', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unLinkResourcesToUserGroup(
|
||||||
|
usergroup_id: any,
|
||||||
|
resource_uuids: any
|
||||||
|
) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}usergroups/${usergroup_id}/remove_resources?resource_uuids=${resource_uuids}`,
|
||||||
|
RequestBody('DELETE', null, null)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue