mirror of
https://github.com/frappe/lms.git
synced 2026-04-19 22:52:29 +03:00
Merge pull request #2209 from raizasafeel/fix/ui-teardown
feat: add member modal, refactor control filters
This commit is contained in:
@@ -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");
|
||||
|
||||
2
frontend/components.d.ts
vendored
2
frontend/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
65
frontend/src/components/Modals/AddEvaluatorModal.vue
Normal file
65
frontend/src/components/Modals/AddEvaluatorModal.vue
Normal 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>
|
||||
173
frontend/src/components/Modals/NewMemberModal.vue
Normal file
173
frontend/src/components/Modals/NewMemberModal.vue
Normal 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>
|
||||
@@ -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: {
|
||||
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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 }
|
||||
: {}),
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user