mirror of
https://github.com/frappe/lms.git
synced 2026-04-19 22:52:29 +03:00
Merge pull request #2296 from pateljannat/issues-219
fix: improved list for assignments and programming exercises
This commit is contained in:
@@ -20,18 +20,15 @@
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div class="md:w-3/4 md:mx-auto py-5 mx-5">
|
||||
<div class="py-5 mx-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div v-if="assignmentCount" class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('{0} Assignments').format(assignmentCount) }}
|
||||
<div class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('{0} Assignments').format(assignments.data?.length) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="assignments.data?.length || assignmentCount > 0"
|
||||
class="grid grid-cols-2 gap-5"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="titleFilter"
|
||||
:placeholder="__('Search by title')"
|
||||
:placeholder="__('Search by Title')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="typeFilter"
|
||||
@@ -48,23 +45,75 @@
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
selectable: true,
|
||||
onRowClick: (row) => {
|
||||
if (readOnlyMode) return
|
||||
assignmentID = row.name
|
||||
showAssignmentForm = true
|
||||
},
|
||||
}"
|
||||
class="h-[79vh] border-b"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in assignmentColumns">
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon :name="item.icon?.toString()" class="h-4 w-4" />
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow
|
||||
v-for="row in assignments.data"
|
||||
:row="row"
|
||||
class="hover:bg-surface-gray-2"
|
||||
>
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||
<div v-if="column.key == 'show_answers'">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="row[column.key]"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key == 'modified'"
|
||||
class="text-sm text-ink-gray-5"
|
||||
>
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
<ListSelectBanner class="bottom-50">
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="deleteAssignment(selections, unselectAll)"
|
||||
>
|
||||
<FeatherIcon name="trash-2" class="h-4 w-4 stroke-1.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
<EmptyState v-else type="Assignments" />
|
||||
<div
|
||||
v-if="assignments.data && assignments.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
>
|
||||
<Button @click="assignments.next()">
|
||||
<div class="flex items-center justify-end space-x-3 mt-3">
|
||||
<Button v-if="assignments.hasNextPage" @click="assignments.next()">
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
<div v-if="assignments.hasNextPage" class="h-8 border-l"></div>
|
||||
<div class="text-ink-gray-5">
|
||||
{{ assignments.data?.length }} {{ __('of') }}
|
||||
{{ totalAssignments.data }} {{ __('Assignments') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AssignmentForm
|
||||
@@ -79,8 +128,17 @@ import {
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
createResource,
|
||||
FormControl,
|
||||
ListView,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
ListSelectBanner,
|
||||
FeatherIcon,
|
||||
toast,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
@@ -96,7 +154,6 @@ const titleFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const showAssignmentForm = ref(false)
|
||||
const assignmentID = ref('new')
|
||||
const assignmentCount = ref(0)
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -110,7 +167,6 @@ onMounted(() => {
|
||||
assignmentID.value = 'new'
|
||||
showAssignmentForm.value = true
|
||||
}
|
||||
getAssignmentCount()
|
||||
titleFilter.value = router.currentRoute.value.query.title
|
||||
typeFilter.value = router.currentRoute.value.query.type
|
||||
})
|
||||
@@ -123,6 +179,9 @@ watch([titleFilter, typeFilter], () => {
|
||||
},
|
||||
})
|
||||
reloadAssignments()
|
||||
totalAssignments.update({
|
||||
filters: assignmentFilter.value,
|
||||
})
|
||||
})
|
||||
|
||||
const reloadAssignments = () => {
|
||||
@@ -137,7 +196,7 @@ const assignmentFilter = computed(() => {
|
||||
if (titleFilter.value) {
|
||||
filters.title = ['like', `%${titleFilter.value}%`]
|
||||
}
|
||||
if (typeFilter.value) {
|
||||
if (typeFilter.value && typeFilter.value.trim() !== '') {
|
||||
filters.type = typeFilter.value
|
||||
}
|
||||
return filters
|
||||
@@ -145,51 +204,60 @@ const assignmentFilter = computed(() => {
|
||||
|
||||
const assignments = createListResource({
|
||||
doctype: 'LMS Assignment',
|
||||
fields: ['name', 'title', 'type', 'creation', 'question', 'course'],
|
||||
fields: ['name', 'title', 'type', 'modified', 'question', 'course'],
|
||||
orderBy: 'modified desc',
|
||||
cache: ['assignments'],
|
||||
transform(data) {
|
||||
return data.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
creation: dayjs(row.creation).fromNow(),
|
||||
modified: dayjs(row.modified).format('DD MMM YYYY'),
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const totalAssignments = createResource({
|
||||
url: 'frappe.client.get_count',
|
||||
params: {
|
||||
doctype: 'LMS Assignment',
|
||||
filters: assignmentFilter.value,
|
||||
},
|
||||
auto: true,
|
||||
cache: ['assignments_count', user.data?.name],
|
||||
onError(err) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
console.error(err)
|
||||
},
|
||||
})
|
||||
|
||||
const assignmentColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: __('Title'),
|
||||
key: 'title',
|
||||
width: 2,
|
||||
width: 1,
|
||||
icon: 'file-text',
|
||||
},
|
||||
{
|
||||
label: __('Type'),
|
||||
key: 'type',
|
||||
width: 1,
|
||||
align: 'left',
|
||||
icon: 'tag',
|
||||
},
|
||||
{
|
||||
label: __('Created'),
|
||||
key: 'creation',
|
||||
label: __('Modified'),
|
||||
key: 'modified',
|
||||
width: 1,
|
||||
align: 'right',
|
||||
icon: 'clock',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const getAssignmentCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Assignment',
|
||||
}).then((data) => {
|
||||
assignmentCount.value = data
|
||||
})
|
||||
}
|
||||
|
||||
const assignmentTypes = computed(() => {
|
||||
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
|
||||
let types = [' ', 'Document', 'Image', 'PDF', 'URL', 'Text']
|
||||
return types.map((type) => {
|
||||
return {
|
||||
label: __(type),
|
||||
@@ -198,6 +266,14 @@ const assignmentTypes = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const deleteAssignment = (selections, unselectAll) => {
|
||||
Array.from(selections).forEach(async (assignmentName) => {
|
||||
await assignments.delete.submit(assignmentName)
|
||||
})
|
||||
unselectAll()
|
||||
toast.success(__('Assignments deleted successfully'))
|
||||
}
|
||||
|
||||
const breadcrumbs = computed(() => [
|
||||
{
|
||||
label: __('Assignments'),
|
||||
|
||||
@@ -132,6 +132,7 @@ import ChildTable from '@/components/Controls/ChildTable.vue'
|
||||
|
||||
const show = defineModel()
|
||||
const exercises = defineModel<ProgrammingExercises>('exercises')
|
||||
const totalExercises = defineModel<number>('totalExercises')
|
||||
const isDirty = ref(false)
|
||||
const originalTestCaseCount = ref(0)
|
||||
|
||||
@@ -150,7 +151,6 @@ const languageOptions = [
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
exerciseID: string
|
||||
getExerciseCount: () => Promise<number>
|
||||
}>(),
|
||||
{
|
||||
exerciseID: 'new',
|
||||
@@ -257,7 +257,7 @@ const createNewExercise = (close: () => void) => {
|
||||
close()
|
||||
isDirty.value = false
|
||||
exercises.value?.reload()
|
||||
props.getExerciseCount()
|
||||
totalExercises.value.reload()
|
||||
toast.success(__('Programming Exercise created successfully'))
|
||||
},
|
||||
onError(err: any) {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="p-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('{0} Exercises').format(exerciseCount) }}
|
||||
{{ __('{0} Exercises').format(exercises.data?.length) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
@@ -69,9 +69,10 @@
|
||||
showForm = true
|
||||
},
|
||||
}"
|
||||
class="h-[79vh] border-b"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center rounded bg-surface-gray-2 p-2"
|
||||
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in columns">
|
||||
<template #prefix="{ item }">
|
||||
@@ -115,20 +116,22 @@
|
||||
</ListView>
|
||||
</div>
|
||||
<EmptyState v-else type="Programming Exercises" />
|
||||
<div
|
||||
v-if="exercises.data && exercises.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
>
|
||||
<Button @click="exercises.next()">
|
||||
<div class="flex items-center justify-end space-x-3 mt-3">
|
||||
<Button v-if="exercises.hasNextPage" @click="exercises.next()">
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
<div v-if="exercises.hasNextPage" class="h-8 border-l"></div>
|
||||
<div class="text-ink-gray-5">
|
||||
{{ exercises.data?.length }} {{ __('of') }} {{ totalExercises.data }}
|
||||
{{ __('Exercises') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProgrammingExerciseForm
|
||||
v-model="showForm"
|
||||
v-model:exercises="exercises"
|
||||
:exerciseID="exerciseID"
|
||||
:getExerciseCount="getExerciseCount"
|
||||
v-model:totalExercises="totalExercises"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -137,6 +140,7 @@ import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
call,
|
||||
createResource,
|
||||
createListResource,
|
||||
dayjs,
|
||||
FeatherIcon,
|
||||
@@ -156,7 +160,6 @@ import { sessionStore } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ProgrammingExerciseForm from '@/pages/ProgrammingExercises/ProgrammingExerciseForm.vue'
|
||||
|
||||
const exerciseCount = ref<number>(0)
|
||||
const readOnlyMode = window.read_only_mode
|
||||
const { brand } = sessionStore()
|
||||
const showForm = ref<boolean>(false)
|
||||
@@ -170,7 +173,6 @@ const { $dialog } = app?.appContext.config.globalProperties
|
||||
|
||||
onMounted(() => {
|
||||
validatePermissions()
|
||||
getExerciseCount()
|
||||
})
|
||||
|
||||
const validatePermissions = () => {
|
||||
@@ -185,19 +187,6 @@ const validatePermissions = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getExerciseCount = (filters: any = {}) => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Programming Exercise',
|
||||
filters: filters,
|
||||
})
|
||||
.then((count: number) => {
|
||||
exerciseCount.value = count
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Error fetching exercise count:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const exercises = createListResource({
|
||||
doctype: 'LMS Programming Exercise',
|
||||
cache: ['programmingExercises'],
|
||||
@@ -212,7 +201,9 @@ const updateList = () => {
|
||||
filters: filters,
|
||||
})
|
||||
exercises.reload()
|
||||
getExerciseCount(filters)
|
||||
totalExercises.update({
|
||||
filters: filters,
|
||||
})
|
||||
}
|
||||
|
||||
const getFilters = () => {
|
||||
@@ -266,6 +257,20 @@ const deleteExercises = (selections: Set<string>, unselectAll: () => void) => {
|
||||
unselectAll()
|
||||
}
|
||||
|
||||
const totalExercises = createResource({
|
||||
url: 'frappe.client.get_count',
|
||||
params: {
|
||||
doctype: 'LMS Programming Exercise',
|
||||
filters: getFilters(),
|
||||
},
|
||||
auto: true,
|
||||
cache: ['programming_exercises_count', user.data?.name],
|
||||
onError(err: any) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
console.error(err)
|
||||
},
|
||||
})
|
||||
|
||||
const languages = [
|
||||
{ label: ' ', value: ' ' },
|
||||
{ label: 'Python', value: 'Python' },
|
||||
@@ -277,13 +282,13 @@ const columns = computed(() => {
|
||||
{
|
||||
label: __('Title'),
|
||||
key: 'title',
|
||||
width: 3,
|
||||
width: 1,
|
||||
icon: 'file-text',
|
||||
},
|
||||
{
|
||||
label: __('Language'),
|
||||
key: 'language',
|
||||
width: 2,
|
||||
width: 1,
|
||||
align: 'left',
|
||||
icon: 'code',
|
||||
},
|
||||
@@ -292,6 +297,7 @@ const columns = computed(() => {
|
||||
key: 'modified',
|
||||
width: 1,
|
||||
icon: 'clock',
|
||||
align: 'right',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -205,7 +205,7 @@ const quizzes = createListResource({
|
||||
return data.map((quiz) => {
|
||||
return {
|
||||
...quiz,
|
||||
modified: dayjs(quiz.modified).fromNow(true),
|
||||
modified: dayjs(quiz.modified).format('DD MMM YYYY'),
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -303,7 +303,7 @@ const quizColumns = computed(() => {
|
||||
label: __('Updated On'),
|
||||
key: 'modified',
|
||||
width: 1,
|
||||
align: 'center',
|
||||
align: 'right',
|
||||
icon: 'clock',
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user