refactor: enhanced assignment form

This commit is contained in:
Jannat Patel
2025-04-17 21:42:43 +05:30
parent 650594d9ea
commit 60ad86f79c
7 changed files with 179 additions and 233 deletions

View File

@@ -16,6 +16,7 @@ declare module 'vue' {
AssessmentPlugin: typeof import('./src/components/AssessmentPlugin.vue')['default'] AssessmentPlugin: typeof import('./src/components/AssessmentPlugin.vue')['default']
Assessments: typeof import('./src/components/Assessments.vue')['default'] Assessments: typeof import('./src/components/Assessments.vue')['default']
Assignment: typeof import('./src/components/Assignment.vue')['default'] Assignment: typeof import('./src/components/Assignment.vue')['default']
AssignmentForm: typeof import('./src/components/Modals/AssignmentForm.vue')['default']
AudioBlock: typeof import('./src/components/AudioBlock.vue')['default'] AudioBlock: typeof import('./src/components/AudioBlock.vue')['default']
Autocomplete: typeof import('./src/components/Controls/Autocomplete.vue')['default'] Autocomplete: typeof import('./src/components/Controls/Autocomplete.vue')['default']
BatchCard: typeof import('./src/components/BatchCard.vue')['default'] BatchCard: typeof import('./src/components/BatchCard.vue')['default']

View File

@@ -0,0 +1,147 @@
<template>
<Dialog
v-model="show"
:options="{
title:
assignmentID === 'new'
? __('Create an Assignment')
: __('Edit Assignment'),
size: 'lg',
actions: [
{
label: __('Save'),
variant: 'solid',
onClick: (close) => saveAssignment(close),
},
],
}"
>
<template #body-content>
<div class="space-y-4 text-base">
<FormControl
v-model="assignment.title"
:label="__('Title')"
:required="true"
/>
<FormControl
v-model="assignment.type"
type="select"
:options="assignmentOptions"
:label="__('Submission Type')"
:required="true"
/>
<div>
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Question') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="assignment.question"
@change="(val) => (assignment.question = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Dialog, FormControl, TextEditor } from 'frappe-ui'
import { computed, reactive, watch } from 'vue'
import { showToast } from '@/utils'
const show = defineModel('show')
const assignments = defineModel<Assignments>('assignments')
interface Assignment {
title: string
type: string
question: string
}
interface Assignments {
data: Assignment[]
get: (params: { doctype: string; name: string }) => Promise<Assignment>
insert: {
submit: (params: Assignment, options: { onSuccess: () => void }) => void
}
}
const assignment = reactive({
title: '',
type: '',
question: '',
})
const props = defineProps({
assignmentID: {
type: String,
default: 'new',
},
})
watch(
() => props.assignmentID,
(val) => {
if (val !== 'new') {
assignments.value?.data.forEach((row) => {
if (row.name === val) {
assignment.title = row.title
assignment.type = row.type
assignment.question = row.question
}
})
}
},
{ flush: 'post' }
)
const saveAssignment = (close) => {
if (props.assignmentID == 'new') {
assignments.value.insert.submit(
{
...assignment,
},
{
onSuccess() {
close()
showToast(
__('Success'),
__('Assignment created successfully'),
'check'
)
},
}
)
} else {
assignments.value.setValue.submit(
{
...assignment,
name: props.assignmentID,
},
{
onSuccess() {
close()
showToast(
__('Success'),
__('Assignment updated successfully'),
'check'
)
},
}
)
}
}
const assignmentOptions = computed(() => {
return [
{ label: 'PDF', value: 'PDF' },
{ label: 'Image', value: 'Image' },
{ label: 'Document', value: 'Document' },
{ label: 'Text', value: 'Text' },
{ label: 'URL', value: 'URL' },
]
})
</script>

View File

@@ -54,7 +54,7 @@
<div v-else> <div v-else>
<div class="flex items-center text-sm space-x-2"> <div class="flex items-center text-sm space-x-2">
<div <div
class="flex items-center justify-center rounded border border-outline-gray-modals w-[10rem] py-5" class="flex items-center justify-center rounded border border-outline-gray-modals bg-white w-[10rem] py-5"
> >
<img :src="data[field.name]?.file_url" class="h-6 rounded" /> <img :src="data[field.name]?.file_url" class="h-6 rounded" />
</div> </div>
@@ -68,7 +68,7 @@
</div> </div>
<X <X
@click="data[field.name] = null" @click="data[field.name] = null"
class="bg-surface-gray-5 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4" class="border text-ink-gray-7 border-outline-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/> />
</div> </div>
</div> </div>
@@ -99,7 +99,7 @@
</template> </template>
<script setup> <script setup>
import { FormControl, FileUploader, Button, Switch } from 'frappe-ui' import { FormControl, FileUploader, Button, Switch } from 'frappe-ui'
import { computed, onMounted } from 'vue' import { computed } from 'vue'
import { getFileSize, validateFile } from '@/utils' import { getFileSize, validateFile } from '@/utils'
import { X } from 'lucide-vue-next' import { X } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'

View File

@@ -1,201 +0,0 @@
<template>
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadcrumbs" />
<div class="space-x-2">
<router-link
v-if="assignment.doc?.name"
:to="{
name: 'AssignmentSubmissionList',
query: {
assignmentID: assignment.doc.name,
},
}"
>
<Button>
{{ __('Submission List') }}
</Button>
</router-link>
<Button variant="solid" @click="saveAssignment()">
{{ __('Save') }}
</Button>
</div>
</header>
<div class="w-3/4 mx-auto py-5">
<div class="font-semibold mb-4">
{{ __('Details') }}
</div>
<div class="grid grid-cols-2 gap-5 mt-4 mb-8">
<FormControl
v-model="model.title"
:label="__('Title')"
:required="true"
/>
<FormControl
v-model="model.type"
type="select"
:options="assignmentOptions"
:label="__('Type')"
:required="true"
/>
</div>
<div>
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Question') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="model.question"
@change="(val) => (model.question = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/>
</div>
</div>
</template>
<script setup>
import {
Breadcrumbs,
Button,
createDocumentResource,
createResource,
FormControl,
TextEditor,
usePageMeta,
} from 'frappe-ui'
import {
computed,
inject,
onMounted,
onBeforeUnmount,
reactive,
watch,
} from 'vue'
import { sessionStore } from '../stores/session'
import { showToast } from '@/utils'
import { useRouter } from 'vue-router'
const user = inject('$user')
const router = useRouter()
const { brand } = sessionStore()
const props = defineProps({
assignmentID: {
type: String,
required: true,
},
})
const model = reactive({
title: '',
type: 'PDF',
question: '',
})
onMounted(() => {
if (
props.assignmentID == 'new' &&
!user.data?.is_moderator &&
!user.data?.is_instructor
) {
router.push({ name: 'Courses' })
}
if (props.assignmentID !== 'new') {
assignment.reload()
}
window.addEventListener('keydown', keyboardShortcut)
})
const keyboardShortcut = (e) => {
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
saveAssignment()
e.preventDefault()
}
}
onBeforeUnmount(() => {
window.removeEventListener('keydown', keyboardShortcut)
})
const assignment = createDocumentResource({
doctype: 'LMS Assignment',
name: props.assignmentID,
auto: false,
})
const newAssignment = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'LMS Assignment',
...values,
},
}
},
onSuccess(data) {
router.push({ name: 'AssignmentForm', params: { assignmentID: data.name } })
},
onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
},
})
const saveAssignment = () => {
if (props.assignmentID == 'new') {
newAssignment.submit({
...model,
})
} else {
assignment.setValue.submit(
{
...model,
},
{
onSuccess(data) {
showToast(__('Success'), __('Assignment saved successfully'), 'check')
assignment.reload()
},
onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
},
}
)
}
}
watch(assignment, () => {
Object.keys(assignment.doc).forEach((key) => {
model[key] = assignment.doc[key]
})
})
const breadcrumbs = computed(() => [
{
label: __('Assignments'),
route: { name: 'Assignments' },
},
{
label: assignment.doc ? assignment.doc.title : __('New Assignment'),
},
])
const assignmentOptions = computed(() => {
return [
{ label: 'PDF', value: 'PDF' },
{ label: 'Image', value: 'Image' },
{ label: 'Document', value: 'Document' },
{ label: 'Text', value: 'Text' },
{ label: 'URL', value: 'URL' },
]
})
usePageMeta(() => {
return {
title: assignment.doc ? assignment.doc.title : __('New Assignment'),
icon: brand.favicon,
}
})
</script>

View File

@@ -3,21 +3,20 @@
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
<router-link <Button
:to="{ variant="solid"
name: 'AssignmentForm', @click="
params: { () => {
assignmentID: 'new', assignmentID = 'new'
}, showAssignmentForm = true
}" }
"
> >
<Button variant="solid">
<template #prefix> <template #prefix>
<Plus class="w-4 h-4" /> <Plus class="w-4 h-4" />
</template> </template>
{{ __('New') }} {{ __('New') }}
</Button> </Button>
</router-link>
</header> </header>
<div class="md:w-3/4 md:mx-auto py-5 mx-5"> <div class="md:w-3/4 md:mx-auto py-5 mx-5">
@@ -38,12 +37,10 @@
:options="{ :options="{
showTooltip: false, showTooltip: false,
selectable: false, selectable: false,
getRowRoute: (row) => ({ onRowClick: (row) => {
name: 'AssignmentForm', assignmentID = row.name
params: { showAssignmentForm = true
assignmentID: row.name,
}, },
}),
}" }"
> >
</ListView> </ListView>
@@ -72,6 +69,11 @@
</Button> </Button>
</div> </div>
</div> </div>
<AssignmentForm
v-model="showAssignmentForm"
v-model:assignments="assignments"
:assignmentID="assignmentID"
/>
</template> </template>
<script setup> <script setup>
import { import {
@@ -86,11 +88,14 @@ import { computed, inject, onMounted, ref, watch } from 'vue'
import { Plus, Pencil } from 'lucide-vue-next' import { Plus, Pencil } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import AssignmentForm from '@/components/Modals/AssignmentForm.vue'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const titleFilter = ref('') const titleFilter = ref('')
const typeFilter = ref('') const typeFilter = ref('')
const showAssignmentForm = ref(false)
const assignmentID = ref('new')
const { brand } = sessionStore() const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
@@ -136,7 +141,7 @@ const assignmentFilter = computed(() => {
const assignments = createListResource({ const assignments = createListResource({
doctype: 'LMS Assignment', doctype: 'LMS Assignment',
fields: ['name', 'title', 'type', 'creation'], fields: ['name', 'title', 'type', 'creation', 'question'],
orderBy: 'modified desc', orderBy: 'modified desc',
cache: ['assignments'], cache: ['assignments'],
transform(data) { transform(data) {
@@ -166,7 +171,7 @@ const assignmentColumns = computed(() => {
label: __('Created'), label: __('Created'),
key: 'creation', key: 'creation',
width: 1, width: 1,
align: 'center', align: 'right',
}, },
] ]
}) })

View File

@@ -199,12 +199,6 @@ const routes = [
name: 'Assignments', name: 'Assignments',
component: () => import('@/pages/Assignments.vue'), component: () => import('@/pages/Assignments.vue'),
}, },
{
path: '/assignments/:assignmentID',
name: 'AssignmentForm',
component: () => import('@/pages/AssignmentForm.vue'),
props: true,
},
{ {
path: '/assignment-submission/:assignmentID/:submissionName', path: '/assignment-submission/:assignmentID/:submissionName',
name: 'AssignmentSubmission', name: 'AssignmentSubmission',

View File

@@ -25,7 +25,7 @@ export default defineConfig({
}), }),
], ],
server: { server: {
allowedHosts: ['fs', 'onb2'], allowedHosts: ['fs', 'bs'],
}, },
resolve: { resolve: {
alias: { alias: {