Merge branch 'develop' into fix/image-upload
This commit is contained in:
@@ -206,7 +206,7 @@ const progressColumns = computed(() => {
|
||||
{
|
||||
label: __('Member'),
|
||||
key: 'member_name',
|
||||
width: '50%',
|
||||
width: '60%',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,78 +1,129 @@
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-col text-base">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<!-- <div class="text-xs text-ink-gray-5">
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
{{ __(description) }}
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex item-center space-x-2">
|
||||
<FormControl
|
||||
v-model="search"
|
||||
:placeholder="__('Search')"
|
||||
type="text"
|
||||
:debounce="300"
|
||||
/>
|
||||
<Button @click="() => (showForm = !showForm)">
|
||||
<Button variant="solid" @click="() => (showForm = !showForm)">
|
||||
<template #prefix>
|
||||
<Plus v-if="!showForm" class="size-4 stroke-1.5" />
|
||||
<X v-else class="size-4 stroke-1.5" />
|
||||
<Plus class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ showForm ? __('Close') : __('New') }}
|
||||
{{ __('New') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form to add new member -->
|
||||
<div v-if="showForm" class="flex items-center space-x-2 my-4">
|
||||
<div class="mt-8 pb-5">
|
||||
<FormControl
|
||||
v-model="email"
|
||||
:placeholder="__('Email')"
|
||||
type="email"
|
||||
class="w-full"
|
||||
@keydown.enter="addEvaluator"
|
||||
/>
|
||||
<Button @click="addEvaluator()" variant="subtle">
|
||||
{{ __('Add') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-scroll">
|
||||
<div class="divide-y">
|
||||
<div
|
||||
v-for="evaluator in evaluators.data"
|
||||
@click="openProfile(evaluator.username)"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<Avatar
|
||||
:image="evaluator.user_image"
|
||||
:label="evaluator.full_name"
|
||||
size="lg"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-base font-semibold text-ink-gray-9">
|
||||
{{ evaluator.full_name }}
|
||||
</div>
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ evaluator.evaluator }}
|
||||
v-model="search"
|
||||
:placeholder="__('Search')"
|
||||
type="text"
|
||||
:debounce="300"
|
||||
class="w-1/4 mb-4"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="size-4 stroke-1.5 text-ink-gray-5" />
|
||||
</template>
|
||||
</FormControl>
|
||||
<div class="overflow-auto h-[60vh]">
|
||||
<div class="divide-y">
|
||||
<div
|
||||
v-for="evaluator in evaluators.data"
|
||||
:key="evaluator.evaluator"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between group py-3">
|
||||
<div
|
||||
class="flex items-center space-x-3"
|
||||
@click="openProfile(evaluator.username)"
|
||||
>
|
||||
<Avatar
|
||||
:image="evaluator.user_image"
|
||||
:label="evaluator.full_name"
|
||||
size="xl"
|
||||
/>
|
||||
<div class="space-y-1">
|
||||
<div class="text-base font-semibold text-ink-gray-9">
|
||||
{{ evaluator.full_name }}
|
||||
</div>
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ evaluator.evaluator }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="invisible group-hover:visible">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="deleteEvaluator(evaluator.evaluator)"
|
||||
>
|
||||
<template #icon>
|
||||
<Trash2 class="size-4 stroke-1.5 text-ink-red-3" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="evaluators.length && hasNextPage"
|
||||
class="flex justify-center mt-4"
|
||||
>
|
||||
<Button @click="evaluators.reload()">
|
||||
<template #prefix>
|
||||
<RefreshCw class="h-3 w-3 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog
|
||||
v-model="showForm"
|
||||
:options="{
|
||||
size: 'xl',
|
||||
title: __('Add Evaluator'),
|
||||
actions: [{
|
||||
label: __('Add'),
|
||||
variant: 'solid',
|
||||
onClick({ close }: any) {
|
||||
addEvaluator(close)
|
||||
},
|
||||
}]
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div v-if="showForm" class="flex items-center">
|
||||
<FormControl
|
||||
v-model="email"
|
||||
:label="__('Email')"
|
||||
placeholder="jane@doe.com"
|
||||
type="email"
|
||||
class="w-full"
|
||||
@keydown.enter="addEvaluator"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { createResource, Button, FormControl, call, Avatar } from 'frappe-ui'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { Plus, X } from 'lucide-vue-next'
|
||||
import { Plus, Search, Trash2, RefreshCw } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const show = defineModel('show')
|
||||
@@ -95,33 +146,39 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const evaluators = createResource({
|
||||
url: 'frappe.client.get_list',
|
||||
makeParams: () => {
|
||||
return {
|
||||
doctype: 'Course Evaluator',
|
||||
fields: ['evaluator', 'full_name', 'user_image', 'username'],
|
||||
filters: search.value ? { evaluator: ['like', `%${search.value}%`] } : {},
|
||||
}
|
||||
},
|
||||
const evaluators = createListResource({
|
||||
doctype: 'Course Evaluator',
|
||||
fields: ['evaluator', 'username', 'full_name', 'user_image'],
|
||||
auto: true,
|
||||
orderBy: 'creation desc',
|
||||
})
|
||||
|
||||
const addEvaluator = () => {
|
||||
const addEvaluator = (close: () => void) => {
|
||||
call('lms.lms.api.add_an_evaluator', {
|
||||
email: email.value,
|
||||
}).then((data) => {
|
||||
showForm.value = false
|
||||
email.value = ''
|
||||
evaluators.reload()
|
||||
})
|
||||
.then(() => {
|
||||
email.value = ''
|
||||
evaluators.reload()
|
||||
toast.success(__('Evaluator added successfully'))
|
||||
close()
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(__(error.messages[0] || error.messages))
|
||||
console.error('Error adding evaluator:', error)
|
||||
})
|
||||
}
|
||||
|
||||
watch(search, () => {
|
||||
evaluators.update({
|
||||
filters: {
|
||||
full_name: ['like', `%${search.value}%`],
|
||||
},
|
||||
})
|
||||
evaluators.reload()
|
||||
})
|
||||
|
||||
const openProfile = (username) => {
|
||||
const openProfile = (username: string) => {
|
||||
show.value = false
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
@@ -130,4 +187,18 @@ const openProfile = (username) => {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const deleteEvaluator = (evaluator: string) => {
|
||||
call('lms.lms.api.delete_evaluator', {
|
||||
evaluator: evaluator,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(__('Evaluator deleted successfully'))
|
||||
evaluators.reload()
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(__(error.messages[0] || error.messages))
|
||||
console.error('Error deleting evaluator:', error)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,53 +5,37 @@
|
||||
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
|
||||
{{ __(label) }}
|
||||
</div>
|
||||
<!-- <div class="text-xs text-ink-gray-5">
|
||||
<div class="text-ink-gray-6 leading-5">
|
||||
{{ __(description) }}
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex item-center space-x-2">
|
||||
<FormControl
|
||||
v-model="search"
|
||||
:placeholder="__('Search')"
|
||||
type="text"
|
||||
:debounce="300"
|
||||
/>
|
||||
<Button @click="() => (showForm = !showForm)">
|
||||
<Button variant="solid" @click="() => (showForm = !showForm)">
|
||||
<template #prefix>
|
||||
<Plus v-if="!showForm" class="size-4 stroke-1.5" />
|
||||
<X v-else class="size-4 stroke-1.5" />
|
||||
<Plus class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ showForm ? __('Close') : __('New') }}
|
||||
{{ __('New') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form to add new member -->
|
||||
<div v-if="showForm" class="flex items-center space-x-2 my-4">
|
||||
<div class="mt-8 pb-10">
|
||||
<FormControl
|
||||
v-model="member.email"
|
||||
:placeholder="__('Email')"
|
||||
type="email"
|
||||
class="w-full"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="member.first_name"
|
||||
:placeholder="__('First Name')"
|
||||
v-model="search"
|
||||
:placeholder="__('Search')"
|
||||
type="text"
|
||||
class="w-full"
|
||||
/>
|
||||
<Button @click="addMember()" variant="subtle">
|
||||
{{ __('Add') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 pb-10 overflow-auto">
|
||||
<!-- Member list -->
|
||||
<div class="overflow-y-scroll">
|
||||
:debounce="300"
|
||||
class="w-1/4 mb-4"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="size-4 stroke-1.5 text-ink-gray-5" />
|
||||
</template>
|
||||
</FormControl>
|
||||
<div class="overflow-y-scroll h-[60vh]">
|
||||
<ul class="divide-y">
|
||||
<li
|
||||
v-for="member in memberList"
|
||||
class="grid grid-cols-3 gap-10 py-2 cursor-pointer"
|
||||
class="flex items-center justify-between py-2 cursor-pointer"
|
||||
>
|
||||
<div
|
||||
@click="openProfile(member.username)"
|
||||
@@ -60,27 +44,13 @@
|
||||
<Avatar
|
||||
:image="member.user_image"
|
||||
:label="member.full_name"
|
||||
size="lg"
|
||||
size="xl"
|
||||
/>
|
||||
<div class="space-y-1">
|
||||
<div class="flex">
|
||||
<div class="text-ink-gray-9">
|
||||
{{ member.full_name }}
|
||||
</div>
|
||||
<div
|
||||
class="px-1"
|
||||
v-if="member.role && getRole(member.role) !== 'Student'"
|
||||
>
|
||||
<Badge
|
||||
:variant="'subtle'"
|
||||
:ref_for="true"
|
||||
theme="blue"
|
||||
size="sm"
|
||||
label="Badge"
|
||||
>
|
||||
{{ getRole(member.role) }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-ink-gray-7">
|
||||
{{ member.name }}
|
||||
@@ -88,43 +58,92 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-center text-ink-gray-7 text-sm"
|
||||
class="flex items-center space-x-1 bg-surface-gray-2 px-2 py-1.5 rounded-md"
|
||||
v-if="member.role && member.role !== 'LMS Student'"
|
||||
>
|
||||
<div v-if="member.last_active">
|
||||
{{ dayjs(member.last_active).format('DD MMM, YYYY HH:mm a') }}
|
||||
</div>
|
||||
<div v-else>-</div>
|
||||
<Shield class="size-4 stroke-1.5" />
|
||||
<span class="text-sm">
|
||||
{{ getRole(member.role) }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
v-if="memberList.length && hasNextPage"
|
||||
class="flex justify-center mt-4"
|
||||
>
|
||||
<Button @click="members.reload()">
|
||||
<template #prefix>
|
||||
<RefreshCw class="h-3 w-3 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
<div
|
||||
v-if="memberList.length && hasNextPage"
|
||||
class="flex justify-center mt-4"
|
||||
>
|
||||
<Button @click="members.reload()">
|
||||
<template #prefix>
|
||||
<RefreshCw class="h-3 w-3 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog
|
||||
v-model="showForm"
|
||||
:options="{
|
||||
title: __('Add a new member'),
|
||||
size: 'lg',
|
||||
actions: [{
|
||||
label: __('Add'),
|
||||
variant: 'solid',
|
||||
onClick({ close }: any) {
|
||||
addMember(close)
|
||||
}
|
||||
}]
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FormControl
|
||||
v-model="member.email"
|
||||
:label="__('Email')"
|
||||
placeholder="jane@doe.com"
|
||||
type="email"
|
||||
class="w-full"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="member.first_name"
|
||||
:label="__('First Name')"
|
||||
placeholder="Jane"
|
||||
type="text"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { createResource, Avatar, Button, FormControl, Badge } from 'frappe-ui'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
createResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
} from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, watch, reactive, inject } from 'vue'
|
||||
import { RefreshCw, Plus, X } from 'lucide-vue-next'
|
||||
import { RefreshCw, Plus, Search, Shield } from 'lucide-vue-next'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import type { User } from '@/components/Settings/types'
|
||||
|
||||
type Member = {
|
||||
username: string
|
||||
full_name: string
|
||||
name: string
|
||||
role?: string
|
||||
user_image?: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const show = defineModel('show')
|
||||
const search = ref('')
|
||||
const start = ref(0)
|
||||
const memberList = ref([])
|
||||
const memberList = ref<Member[]>([])
|
||||
const hasNextPage = ref(false)
|
||||
const showForm = ref(false)
|
||||
const dayjs = inject('$dayjs')
|
||||
@@ -158,7 +177,7 @@ const members = createResource({
|
||||
start: start.value,
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
onSuccess(data: Member[]) {
|
||||
memberList.value = memberList.value.concat(data)
|
||||
start.value = start.value + 20
|
||||
hasNextPage.value = data.length === 20
|
||||
@@ -166,7 +185,7 @@ const members = createResource({
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const openProfile = (username) => {
|
||||
const openProfile = (username: string) => {
|
||||
show.value = false
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
@@ -178,7 +197,7 @@ const openProfile = (username) => {
|
||||
|
||||
const newMember = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
makeParams() {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'User',
|
||||
@@ -188,13 +207,12 @@ const newMember = createResource({
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
onSuccess(data: Member) {
|
||||
show.value = false
|
||||
|
||||
if (user?.data?.is_system_manager) updateOnboardingStep('invite_students')
|
||||
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
name: 'ProfileRoles',
|
||||
params: {
|
||||
username: data.username,
|
||||
},
|
||||
@@ -202,8 +220,9 @@ const newMember = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
const addMember = () => {
|
||||
const addMember = (close: () => void) => {
|
||||
newMember.reload()
|
||||
close()
|
||||
}
|
||||
|
||||
watch(search, () => {
|
||||
@@ -212,8 +231,8 @@ watch(search, () => {
|
||||
members.reload()
|
||||
})
|
||||
|
||||
const getRole = (role) => {
|
||||
const map = {
|
||||
const getRole = (role: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'LMS Student': 'Student',
|
||||
'Course Creator': 'Instructor',
|
||||
Moderator: 'Moderator',
|
||||
|
||||
@@ -242,13 +242,16 @@ const tabsStructure = computed(() => {
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
description: 'Manage the members of your learning system',
|
||||
description:
|
||||
'Add new members or manage roles and permissions of existing members',
|
||||
icon: 'UserRoundPlus',
|
||||
},
|
||||
{
|
||||
label: 'Evaluators',
|
||||
description: 'Manage the evaluators of your learning system',
|
||||
description: '',
|
||||
icon: 'UserCheck',
|
||||
description:
|
||||
'Add new evaluators or check the slots existing evaluators',
|
||||
},
|
||||
{
|
||||
label: 'Categories',
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
v-model="activeTab"
|
||||
/>
|
||||
</div>
|
||||
<router-view :profile="profile" />
|
||||
<router-view :profile="profile" :key="profile.data?.name" />
|
||||
</div>
|
||||
</div>
|
||||
<EditProfile
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { FormControl, createResource, toast } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import { CircleAlert } from 'lucide-vue-next'
|
||||
|
||||
@@ -66,10 +66,9 @@ const roles = createResource({
|
||||
url: 'lms.lms.utils.get_roles',
|
||||
makeParams(values) {
|
||||
return {
|
||||
name: props.profile.data?.name,
|
||||
name: values.member,
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
let roles = [
|
||||
'moderator',
|
||||
@@ -83,6 +82,16 @@ const roles = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.profile,
|
||||
(newValue) => {
|
||||
roles.reload({
|
||||
member: newValue.data?.name,
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const updateRole = createResource({
|
||||
url: 'lms.lms.api.save_role',
|
||||
makeParams(values) {
|
||||
@@ -97,7 +106,10 @@ const updateRole = createResource({
|
||||
const changeRole = (role) => {
|
||||
updateRole.submit(
|
||||
{
|
||||
role: convertToTitleCase(role.split('_').join(' ')),
|
||||
role:
|
||||
role == 'lms_student'
|
||||
? 'LMS Student'
|
||||
: convertToTitleCase(role.split('_').join(' ')),
|
||||
value: eval(role).value,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user