Merge pull request #2209 from raizasafeel/fix/ui-teardown

feat: add member modal, refactor control filters
This commit is contained in:
Raizaaa
2026-03-17 02:13:10 +05:30
committed by GitHub
14 changed files with 472 additions and 206 deletions

View File

@@ -27,24 +27,24 @@ describe("Batch Creation", () => {
cy.get("input[placeholder='Jane']").type(randomName);
cy.get("button").contains("Add").click();
// Open Settings
cy.get("span").contains("Learning").click();
cy.get("span").contains("Settings").click();
// Add evaluator
// Switch to Evaluators tab
cy.get("[data-dismissable-layer]")
.find("span")
.contains(/^Evaluators$/)
.click();
// Click "New" dropdown and select "New Evaluator"
cy.get("[data-dismissable-layer]")
.find("button")
.contains("New")
.click();
const randomEvaluator = `evaluator${dateNow}@example.com`;
cy.get("span").contains("New Evaluator").click();
const randomEvaluator = `evaluator${dateNow}@example.com`;
cy.get("input[placeholder='jane@doe.com']").type(randomEvaluator);
cy.get("input[placeholder='Jane']").type("Evaluator");
cy.get("button").contains("Add").click();
cy.wait(500);
cy.get("div").contains(randomEvaluator).should("be.visible").click();
cy.visit("/lms/batches");

View File

@@ -8,6 +8,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AddEvaluatorModal: typeof import('./src/components/Modals/AddEvaluatorModal.vue')['default']
Apps: typeof import('./src/components/Sidebar/Apps.vue')['default']
AppSidebar: typeof import('./src/components/Sidebar/AppSidebar.vue')['default']
AssessmentModal: typeof import('./src/components/Modals/AssessmentModal.vue')['default']
@@ -78,6 +79,7 @@ declare module 'vue' {
Members: typeof import('./src/components/Settings/Members.vue')['default']
MobileLayout: typeof import('./src/components/MobileLayout.vue')['default']
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
NewMemberModal: typeof import('./src/components/Modals/NewMemberModal.vue')['default']
NoPermission: typeof import('./src/components/NoPermission.vue')['default']
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
Notes: typeof import('./src/components/Notes/Notes.vue')['default']

View File

@@ -140,7 +140,7 @@ const options = createResource({
params: {
txt: text.value,
doctype: props.doctype,
filters: props.filters,
filters: JSON.stringify(props.filters),
},
transform: (data) => {
return data.map((option) => {
@@ -158,7 +158,7 @@ const reload = (val) => {
params: {
txt: val,
doctype: props.doctype,
filters: props.filters,
filters: JSON.stringify(props.filters),
},
})
options.reload()

View File

@@ -10,6 +10,7 @@
ref="search"
class="form-input w-full focus-visible:!ring-0"
type="text"
:displayValue="() => query"
@change="
(e) => {
query = e.target.value
@@ -106,7 +107,7 @@ import {
ComboboxOptions,
ComboboxOption,
} from '@headlessui/vue'
import { createResource, Button } from 'frappe-ui'
import { createResource, Button, toast } from 'frappe-ui'
import { ref, computed, useAttrs, watch } from 'vue'
import { watchDebounced } from '@vueuse/core'
import { X, Plus } from 'lucide-vue-next'
@@ -115,7 +116,9 @@ const props = defineProps({
label: String,
size: { type: String, default: 'sm' },
doctype: { type: String, required: true },
filters: { type: Object, default: () => ({}) },
filters: { type: [Object, Array], default: () => ({}) },
url: { type: String, default: 'frappe.desk.search.search_link' },
searchParams: { type: Object, default: () => ({}) },
validate: Function,
errorMessage: {
type: Function,
@@ -124,22 +127,18 @@ const props = defineProps({
required: Boolean,
})
const values = defineModel()
const values = defineModel({ default: () => [] })
const attrs = useAttrs()
const trigger = ref(null)
const query = ref('')
const text = ref('')
const selectedValue = ref(null)
const error = ref(null)
const emit = defineEmits(['update:modelValue'])
watch(selectedValue, (val) => {
if (!val?.value) return
query.value = ''
addValue(val.value)
selectedValue.value = null
emit('update:modelValue', values.value)
})
watchDebounced(
@@ -153,14 +152,27 @@ watchDebounced(
{ debounce: 300, immediate: true }
)
const filterOptions = createResource({
url: 'frappe.desk.search.search_link',
method: 'POST',
auto: true,
params: {
txt: text.value,
doctype: props.doctype,
// Refetch when filters or searchParams change
watch(
() => [props.filters, props.searchParams],
() => {
reload(text.value)
},
{ deep: true }
)
function getParams(txt) {
return {
txt,
doctype: props.doctype,
filters: JSON.stringify(props.filters),
...props.searchParams,
}
}
const filterOptions = createResource({
url: props.url,
method: 'POST',
})
const options = computed(() => {
@@ -170,10 +182,7 @@ const options = computed(() => {
function reload(val) {
filterOptions.update({
params: {
txt: val,
doctype: props.doctype,
},
params: getParams(val),
})
filterOptions.reload()
}
@@ -186,34 +195,30 @@ function onFocus() {
}
function addValue(value) {
error.value = null
if (!value) return
const splitValues = value.split(',')
let newValues = [...(values.value || [])]
splitValues.forEach((val) => {
val = val.trim()
if (!val) return
if (values.value?.includes(val)) return
if (newValues.includes(val)) return
if (props.validate && !props.validate(val)) {
error.value = props.errorMessage(val)
toast.error(props.errorMessage(val))
return
}
if (!values.value) values.value = [val]
else values.value.push(val)
newValues.push(val)
})
values.value = newValues
}
function removeValue(value) {
let indexToRemove = values.value.indexOf(value)
if (indexToRemove > -1) {
values.value.splice(indexToRemove, 1)
}
emit('update:modelValue', values.value)
values.value = values.value.filter((v) => v !== value)
}
const labelClasses = computed(() => [

View File

@@ -0,0 +1,65 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Add Existing User as Evaluator'),
size: 'md',
actions: [
{
label: __('Add'),
variant: 'solid',
loading: submitting,
onClick: ({ close }: any) => addEvaluator(close),
},
],
}"
>
<template #body-content>
<Link doctype="User" v-model="selectedUser" :label="__('Select User')" />
</template>
</Dialog>
</template>
<script setup lang="ts">
import { call, Dialog, toast } from 'frappe-ui'
import { ref, watch } from 'vue'
import { cleanError } from '@/utils'
import Link from '@/components/Controls/Link.vue'
const show = defineModel<boolean>({ default: false })
const selectedUser = ref('')
const submitting = ref(false)
const emit = defineEmits<{
added: []
}>()
watch(show, (isOpen) => {
if (isOpen) {
selectedUser.value = ''
}
})
const addEvaluator = async (close?: () => void) => {
if (!selectedUser.value?.trim()) {
toast.error(__('Please select a user'))
return
}
submitting.value = true
try {
await call('lms.lms.api.save_role', {
user: selectedUser.value,
role: 'Batch Evaluator',
value: 1,
})
toast.success(__('Evaluator added successfully'))
emit('added')
close?.()
} catch (err: any) {
toast.error(cleanError(err.messages?.[0]) || __('Unable to add evaluator'))
} finally {
submitting.value = false
}
}
</script>

View File

@@ -0,0 +1,173 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Add New Member'),
size: 'lg',
actions: [
{
label: __('Add'),
variant: 'solid',
loading: submitting,
onClick: ({ close }: any) => addMember(close),
},
],
}"
>
<template #body-content>
<div class="space-y-4">
<FormControl
v-model="member.email"
:label="__('Email')"
placeholder="jane@doe.com"
type="email"
:required="true"
@keyup.enter="addMember()"
/>
<div class="flex items-center gap-3">
<FormControl
v-model="member.first_name"
:label="__('First Name')"
placeholder="Jane"
type="text"
class="w-full"
/>
<FormControl
v-model="member.last_name"
:label="__('Last Name')"
placeholder="Doe"
type="text"
class="w-full"
/>
</div>
<div class="flex flex-col gap-2">
<div class="text-sm text-ink-gray-5">
{{ __('Roles') }}
</div>
<div class="flex flex-wrap gap-x-6">
<FormControl
:label="__('Student')"
v-model="roles.lms_student"
type="checkbox"
/>
<FormControl
:label="__('Course Creator')"
v-model="roles.course_creator"
type="checkbox"
/>
<FormControl
:label="__('Evaluator')"
v-model="roles.batch_evaluator"
type="checkbox"
/>
<FormControl
:label="__('Moderator')"
v-model="roles.moderator"
type="checkbox"
/>
</div>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { call, Dialog, FormControl, toast } from 'frappe-ui'
import { reactive, ref, watch } from 'vue'
import { cleanError } from '@/utils'
const show = defineModel<boolean>({ default: false })
const submitting = ref(false)
const props = defineProps<{
defaultRoles?: string[]
}>()
const emit = defineEmits<{
created: [user: any]
}>()
const ROLE_MAP: Record<string, string> = {
moderator: 'Moderator',
course_creator: 'Course Creator',
batch_evaluator: 'Batch Evaluator',
lms_student: 'LMS Student',
}
const member = reactive({
email: '',
first_name: '',
last_name: '',
})
const roles = reactive({
moderator: false,
course_creator: false,
batch_evaluator: false,
lms_student: false,
})
const resetForm = () => {
member.email = ''
member.first_name = ''
member.last_name = ''
applyDefaultRoles()
}
const applyDefaultRoles = () => {
roles.moderator = props.defaultRoles?.includes('moderator') ?? false
roles.course_creator = props.defaultRoles?.includes('course_creator') ?? false
roles.batch_evaluator =
props.defaultRoles?.includes('batch_evaluator') ?? false
roles.lms_student = props.defaultRoles?.includes('lms_student') ?? false
}
watch(show, (isOpen) => {
if (isOpen) {
resetForm()
}
})
const assignRoles = async (userEmail: string) => {
const selectedRoles = Object.entries(roles).filter(([_, checked]) => checked)
for (const [key, _] of selectedRoles) {
await call('lms.lms.api.save_role', {
user: userEmail,
role: ROLE_MAP[key],
value: 1,
})
}
}
const addMember = async (close?: () => void) => {
if (!member.email?.trim()) {
toast.error(__('Email is required'))
return
}
submitting.value = true
try {
const user = await call('frappe.client.insert', {
doc: {
doctype: 'User',
email: member.email.trim(),
first_name: member.first_name.trim() || undefined,
last_name: member.last_name.trim() || undefined,
},
})
await assignRoles(user.name)
toast.success(__('Member added successfully'))
emit('created', user)
resetForm()
close?.()
} catch (err: any) {
toast.error(cleanError(err.messages?.[0]) || __('Unable to add member'))
} finally {
submitting.value = false
}
}
</script>

View File

@@ -10,12 +10,43 @@
</div>
</div>
<div class="flex item-center space-x-2">
<Button @click="() => (showForm = !showForm)">
<template #prefix>
<Plus class="size-4 stroke-1.5" />
<Dropdown
placement="right"
side="bottom"
:options="[
{
label: __('New Evaluator'),
icon: 'user-plus',
onClick() {
showNewEvaluator = true
},
},
{
label: __('Existing User'),
icon: 'user-check',
onClick() {
showExistingUser = true
},
},
]"
>
<template v-slot="{ open }">
<Button variant="solid">
<template #prefix>
<Plus class="size-4 stroke-1.5" />
</template>
{{ __('New') }}
<template #suffix>
<ChevronDown
:class="[
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
open ? 'rotate-180' : '',
]"
/>
</template>
</Button>
</template>
{{ __('New') }}
</Button>
</Dropdown>
</div>
</div>
@@ -71,11 +102,8 @@
</div>
</div>
</div>
<div
v-if="evaluators.length && hasNextPage"
class="flex justify-center mt-4"
>
<Button @click="evaluators.reload()">
<div v-if="evaluators.hasNextPage" class="flex justify-center mt-4">
<Button @click="evaluators.next()">
<template #prefix>
<RefreshCw class="h-3 w-3 stroke-1.5" />
</template>
@@ -85,33 +113,12 @@
</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>
<AddEvaluatorModal v-model="showExistingUser" @added="evaluators.reload()" />
<NewMemberModal
v-model="showNewEvaluator"
:defaultRoles="['batch_evaluator']"
@created="onMemberCreated"
/>
</template>
<script setup lang="ts">
import {
@@ -119,18 +126,19 @@ import {
Button,
call,
createListResource,
Dialog,
Dropdown,
FormControl,
toast,
} from 'frappe-ui'
import { ref, watch } from 'vue'
import { Plus, Search, Trash2, RefreshCw } from 'lucide-vue-next'
import { Plus, Search, Trash2, RefreshCw, ChevronDown } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
import AddEvaluatorModal from '@/components/Modals/AddEvaluatorModal.vue'
const show = defineModel('show')
const search = ref('')
const showForm = ref(false)
const email = ref('')
const showExistingUser = ref(false)
const showNewEvaluator = ref(false)
const router = useRouter()
const props = defineProps({
@@ -151,20 +159,8 @@ const evaluators = createListResource({
orderBy: 'creation desc',
})
const addEvaluator = (close: () => void) => {
call('lms.lms.api.add_an_evaluator', {
email: email.value,
})
.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)
})
const onMemberCreated = () => {
evaluators.reload()
}
watch(search, () => {
@@ -177,7 +173,6 @@ watch(search, () => {
})
const openProfile = (username: string) => {
show.value = false
router.push({
name: 'Profile',
params: {

View File

@@ -10,7 +10,7 @@
</div>
</div>
<div class="flex item-center space-x-2">
<Button @click="() => (showForm = !showForm)">
<Button @click="showNewMember = true">
<template #prefix>
<Plus class="size-4 stroke-1.5" />
</template>
@@ -82,56 +82,16 @@
</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>
<NewMemberModal v-model="showNewMember" @created="onMemberCreated" />
</template>
<script setup lang="ts">
import {
Avatar,
Button,
call,
createResource,
Dialog,
FormControl,
toast,
} from 'frappe-ui'
import { Avatar, Button, createResource, FormControl } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch, reactive, inject } from 'vue'
import { ref, watch, inject } from 'vue'
import { RefreshCw, Plus, Search, Shield } from 'lucide-vue-next'
import { useOnboarding } from 'frappe-ui/frappe'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import type { User } from '@/components/Settings/types'
import { useTelemetry } from 'frappe-ui/frappe'
import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
type Member = {
username: string
@@ -147,16 +107,11 @@ const search = ref('')
const start = ref(0)
const memberList = ref<Member[]>([])
const hasNextPage = ref(false)
const showForm = ref(false)
const showNewMember = ref(false)
const user = inject<User | null>('$user')
const { updateOnboardingStep } = useOnboarding('learning')
const { capture } = useTelemetry()
const member = reactive({
email: '',
first_name: '',
})
const props = defineProps({
label: {
type: String,
@@ -194,30 +149,12 @@ const openProfile = (username: string) => {
})
}
const addMember = (close: () => void) => {
call('frappe.client.insert', {
doc: {
doctype: 'User',
first_name: member.first_name,
email: member.email,
},
})
.then((data: Member) => {
if (user?.data?.is_system_manager) updateOnboardingStep('invite_students')
capture('user_added')
show.value = false
router.push({
name: 'ProfileRoles',
params: {
username: data.username,
},
})
close()
})
.catch((err: any) => {
console.error(err)
toast.error(__(err.messages?.[0] || err))
})
const onMemberCreated = (data: any) => {
if (user?.data?.is_system_manager) updateOnboardingStep('invite_students')
capture('user_added')
memberList.value = []
start.value = 0
members.reload()
}
watch(search, () => {

View File

@@ -42,8 +42,7 @@
...(activeTab.label == 'Branding'
? { sections: activeTab.sections }
: {}),
...(activeTab.label == 'Evaluators' ||
activeTab.label == 'Members' ||
...(activeTab.label == 'Members' ||
activeTab.label == 'Transactions'
? { 'onUpdate:show': (val) => (show = val), show }
: {}),

View File

@@ -114,11 +114,12 @@
<div class="grid grid-cols-2 gap-5">
<MultiSelect
v-model="instructors"
doctype="Course Evaluator"
doctype="User"
:label="__('Instructors')"
:required="true"
:onCreate="(close) => openSettings('Evaluators', close)"
:filters="{ ignore_user_type: 1 }"
:onCreate="() => (showMemberModal = true)"
url="lms.lms.api.search_users_by_role"
:searchParams="{ roles: JSON.stringify(['Batch Evaluator']) }"
/>
<FormControl
v-model="batchDetail.doc.description"
@@ -274,6 +275,11 @@
</div>
</div>
</div>
<NewMemberModal
v-model="showMemberModal"
:defaultRoles="['batch_evaluator']"
@created="onInstructorCreated"
/>
</template>
<script setup>
import {
@@ -310,6 +316,7 @@ import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue'
import BatchCourses from '@/pages/Batches/components/BatchCourses.vue'
import Assessments from '@/pages/Batches/components/Assessments.vue'
import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
const router = useRouter()
const user = inject('$user')
@@ -321,6 +328,11 @@ const { capture } = useTelemetry()
const { $dialog } = app.appContext.config.globalProperties
const isDirty = ref(false)
const originalDoc = ref(null)
const showMemberModal = ref(false)
const onInstructorCreated = (user) => {
instructors.value = [...instructors.value, user.name]
}
const meta = reactive({
description: '',

View File

@@ -83,11 +83,12 @@
/>
<MultiSelect
v-model="batch.instructors"
doctype="Course Evaluator"
doctype="User"
:label="__('Instructors')"
:required="true"
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
:filters="{ ignore_user_type: 1 }"
:onCreate="() => (showMemberModal = true)"
url="lms.lms.api.search_users_by_role"
:searchParams="{ roles: JSON.stringify(['Batch Evaluator']) }"
/>
</div>
<div class="">
@@ -114,6 +115,11 @@
</div>
</template>
</Dialog>
<NewMemberModal
v-model="showMemberModal"
:defaultRoles="['batch_evaluator']"
@created="onInstructorCreated"
/>
</template>
<script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
@@ -121,14 +127,16 @@ import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue'
import { useRouter } from 'vue-router'
import { cleanError, openSettings, sanitizeHTML, escapeHTML } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue'
import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
const show = defineModel<boolean>({ required: true, default: false })
const router = useRouter()
const { capture } = useTelemetry()
const { updateOnboardingStep } = useOnboarding('learning')
const user = inject<any>('$user')
const showMemberModal = ref(false)
const props = defineProps<{
batches: any
@@ -164,6 +172,10 @@ const batch = ref<Batch>({
medium: null,
})
const onInstructorCreated = (user: any) => {
batch.value.instructors = [...batch.value.instructors, user.name]
}
const validateFields = () => {
batch.value.description = sanitizeHTML(batch.value.description)

View File

@@ -30,8 +30,16 @@
v-model="instructors"
doctype="User"
:label="__('Instructors')"
:filters="{ ignore_user_type: 1 }"
:onCreate="(close) => openSettings('Members', close)"
url="lms.lms.api.search_users_by_role"
:searchParams="{
roles: JSON.stringify(['Course Creator', 'Batch Evaluator']),
}"
:onCreate="
() => {
memberModalRoles = ['course_creator']
showMemberModal = true
}
"
:required="true"
@update:modelValue="makeFormDirty()"
/>
@@ -250,7 +258,10 @@
:label="__('Evaluator')"
:required="courseResource.doc.paid_certificate"
:onCreate="
(value, close) => openSettings('Evaluators', close)
() => {
memberModalRoles = ['batch_evaluator']
showMemberModal = true
}
"
@update:modelValue="makeFormDirty()"
/>
@@ -298,6 +309,11 @@
</div>
</div>
</div>
<NewMemberModal
v-model="showMemberModal"
:defaultRoles="memberModalRoles"
@created="onMemberCreated"
/>
</template>
<script setup>
import {
@@ -332,6 +348,7 @@ import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import ColorSwatches from '@/components/Controls/ColorSwatches.vue'
import Uploader from '@/components/Controls/Uploader.vue'
import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
const user = inject('$user')
const newTag = ref('')
@@ -342,6 +359,8 @@ const related_courses = ref([])
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
const isDirty = ref(false)
const showMemberModal = ref(false)
const memberModalRoles = ref(['course_creator'])
const props = defineProps({
course: {
@@ -410,6 +429,15 @@ const submitCourse = () => {
updateCourse()
}
const onMemberCreated = (user) => {
if (memberModalRoles.value.includes('batch_evaluator')) {
courseResource.doc.evaluator = user.name
makeFormDirty()
} else {
instructors.value = [...instructors.value, user.name]
}
}
const validateFields = () => {
courseResource.doc.description = sanitizeHTML(courseResource.doc.description)

View File

@@ -30,8 +30,11 @@
v-model="course.instructors"
doctype="User"
:label="__('Instructors')"
:filters="{ ignore_user_type: 1 }"
:onCreate="(close: () => void) => openSettings('Members', close)"
url="lms.lms.api.search_users_by_role"
:searchParams="{
roles: JSON.stringify(['Course Creator', 'Batch Evaluator']),
}"
:onCreate="() => (showMemberModal = true)"
:required="true"
/>
<Uploader
@@ -72,6 +75,11 @@
</div>
</template>
</Dialog>
<NewMemberModal
v-model="showMemberModal"
:defaultRoles="['course_creator']"
@created="onInstructorCreated"
/>
</template>
<script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
@@ -82,6 +90,7 @@ import { cleanError, openSettings, sanitizeHTML, escapeHTML } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Uploader from '@/components/Controls/Uploader.vue'
import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
const show = defineModel<boolean>({ required: true, default: false })
const router = useRouter()
@@ -89,6 +98,7 @@ const { capture } = useTelemetry()
const { updateOnboardingStep } = useOnboarding('learning')
const user = inject<any>('$user')
const courseCreated = ref(false)
const showMemberModal = ref(false)
const props = defineProps<{
courses: any
@@ -112,6 +122,10 @@ const course = ref<Course>({
image: null,
})
const onInstructorCreated = (user: any) => {
course.value.instructors = [...course.value.instructors, user.name]
}
const validateFields = () => {
course.value.description = sanitizeHTML(course.value.description)

View File

@@ -1468,28 +1468,6 @@ def save_evaluator_role(user: str, value: int):
return True
@frappe.whitelist()
def add_an_evaluator(email: str):
frappe.only_for("Moderator")
if not frappe.db.exists("User", email):
user = frappe.new_doc("User")
user.update(
{
"email": email,
"first_name": email.split("@")[0].capitalize(),
"enabled": 1,
}
)
user.insert()
user.add_roles("Batch Evaluator")
evaluator = frappe.new_doc("Course Evaluator")
evaluator.evaluator = email
evaluator.insert()
return evaluator
@frappe.whitelist()
def capture_user_persona(responses: str):
frappe.only_for("System Manager")
@@ -2329,3 +2307,49 @@ def clear_demo_data():
frappe.delete_doc("User", user, ignore_permissions=True)
frappe.db.set_single_value("LMS Settings", "demo_data_present", False)
@frappe.whitelist()
def search_users_by_role(txt: str = "", roles: str | list | None = None, page_length: int = 10):
"""Returns users with `roles` in search_link format"""
frappe.only_for(["Moderator", "Course Creator", "Batch Evaluator"])
if not roles:
return []
if isinstance(roles, str):
roles = json.loads(roles)
invalid_roles = set(roles) - set(LMS_ROLES)
if invalid_roles:
frappe.throw(_("Cannot search for roles: {0}").format(", ".join(invalid_roles)))
users_with_roles = frappe.get_all(
"Has Role",
filters={"role": ["in", roles], "parenttype": "User"},
pluck="parent",
distinct=True,
)
if not users_with_roles:
return []
results = frappe.get_all(
"User",
filters=[
["name", "in", users_with_roles],
["name", "not in", ["Administrator", "Guest"]],
["enabled", "=", 1],
],
or_filters=[
["full_name", "like", f"%{txt}%"],
["name", "like", f"%{txt}%"],
],
fields=["name", "full_name"],
limit_page_length=cint(page_length),
order_by="full_name asc",
)
return [
{"value": r.name, "description": r.full_name or r.name, "label": r.full_name or r.name}
for r in results
]