fix: course form cleanup

This commit is contained in:
Jannat Patel
2025-05-12 13:07:06 +05:30
parent 49d3dc0aa0
commit 52b925b306
5 changed files with 253 additions and 231 deletions

View File

@@ -34,7 +34,7 @@
<Button <Button
variant="ghost" variant="ghost"
class="w-full !justify-start" class="w-full !justify-start"
label="Create New" :label="__('Create New')"
@click="attrs.onCreate(value, close)" @click="attrs.onCreate(value, close)"
> >
<template #prefix> <template #prefix>

View File

@@ -4,79 +4,84 @@
{{ label }} {{ label }}
<span class="text-ink-red-3" v-if="required">*</span> <span class="text-ink-red-3" v-if="required">*</span>
</label> </label>
<div class="grid grid-cols-3 gap-2"> <div class="w-full">
<Button <Combobox v-model="selectedValue" nullable>
ref="emails" <Popover class="w-full" v-model:show="showOptions">
v-for="value in values" <template #target="{ togglePopover }">
:key="value" <ComboboxInput
:label="value" ref="search"
theme="gray" class="search-input form-input w-full focus-visible:!ring-0"
variant="subtle" type="text"
class="rounded-md word-break-all" :value="query"
@keydown.delete.capture.stop="removeLastValue" @change="
> (e) => {
<template #suffix> query = e.target.value
<X @click="removeValue(value)" class="h-4 w-4 stroke-1.5" /> showOptions = true
</template> }
</Button> "
<div class=""> autocomplete="off"
<Combobox v-model="selectedValue" nullable> @focus="() => togglePopover()"
<Popover class="w-full" v-model:show="showOptions"> @keydown.delete.capture.stop="removeLastValue"
<template #target="{ togglePopover }"> />
<ComboboxInput </template>
ref="search" <template #body="{ isOpen, close }">
class="search-input form-input w-full focus-visible:!ring-0" <div v-show="isOpen">
type="text" <div
:value="query" class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
@change=" >
(e) => { <ComboboxOptions
query = e.target.value class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
showOptions = true static
}
"
autocomplete="off"
@focus="() => togglePopover()"
@keydown.delete.capture.stop="removeLastValue"
/>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
> >
<ComboboxOptions <ComboboxOption
class="my-1 max-h-[12rem] overflow-y-auto px-1.5" v-for="option in options"
static :key="option.value"
:value="option"
v-slot="{ active }"
> >
<ComboboxOption <li
v-for="option in options" :class="[
:key="option.value" 'flex cursor-pointer items-center rounded px-2 py-1 text-base',
:value="option" { 'bg-surface-gray-2': active },
v-slot="{ active }" ]"
> >
<li <div class="flex flex-col gap-1 p-1">
:class="[ <div class="text-base font-medium text-ink-gray-8">
'flex cursor-pointer items-center rounded px-2 py-1 text-base', {{ option.description }}
{ 'bg-surface-gray-2': active },
]"
>
<div class="flex flex-col gap-1 p-1">
<div class="text-base font-medium text-ink-gray-8">
{{ option.description }}
</div>
<div class="text-sm text-ink-gray-5">
{{ option.value }}
</div>
</div> </div>
</li> <div class="text-sm text-ink-gray-5">
</ComboboxOption> {{ option.value }}
</ComboboxOptions> </div>
</div> </div>
</li>
</ComboboxOption>
<div v-if="attrs.onCreate" class="absolute bottom-2 left-1 w-[98%] pt-2 bg-white border-t">
<Button
variant="ghost"
class="w-full !justify-start"
:label="__('Create New')"
@click="attrs.onCreate(close)"
>
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</div>
</ComboboxOptions>
</div> </div>
</template> </div>
</Popover> </template>
</Combobox> </Popover>
</Combobox>
</div>
<div v-if="values.length" class="grid grid-cols-2 gap-2 mt-4">
<div v-for="value in values" class="flex items-center justify-between break-all bg-surface-gray-2 text-ink-gray-7 word-wrap p-2 rounded-md mr-2">
<span class="break-all">
{{ value }}
</span>
<X class="size-4 stroke-1.5 cursor-pointer" @click="removeValue(value)" />
</div> </div>
</div> </div>
<!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> --> <!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> -->
</div> </div>
@@ -90,9 +95,9 @@ import {
ComboboxOption, ComboboxOption,
} from '@headlessui/vue' } from '@headlessui/vue'
import { createResource, Popover, Button } from 'frappe-ui' import { createResource, Popover, Button } from 'frappe-ui'
import { ref, computed, nextTick } from 'vue' import { ref, computed, nextTick, useAttrs } from 'vue'
import { watchDebounced } from '@vueuse/core' import { watchDebounced } from '@vueuse/core'
import { X } from 'lucide-vue-next' import { X, Plus } from 'lucide-vue-next'
const props = defineProps({ const props = defineProps({
label: { label: {
@@ -124,7 +129,7 @@ const props = defineProps({
}) })
const values = defineModel() const values = defineModel()
const attrs = useAttrs()
const emails = ref([]) const emails = ref([])
const search = ref(null) const search = ref(null)
const error = ref(null) const error = ref(null)

View File

@@ -17,10 +17,11 @@
:debounce="300" :debounce="300"
/> />
<Button @click="() => (showForm = !showForm)"> <Button @click="() => (showForm = !showForm)">
<template #icon> <template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" /> <Plus v-if="!showForm" class="size-4 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" /> <X v-else class="size-4 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -17,10 +17,11 @@
:debounce="300" :debounce="300"
/> />
<Button @click="() => (showForm = !showForm)"> <Button @click="() => (showForm = !showForm)">
<template #icon> <template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" /> <Plus v-if="!showForm" class="size-4 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" /> <X v-else class="size-4 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -19,149 +19,130 @@
</Button> </Button>
</div> </div>
</header> </header>
<div class="mt-5 mb-10"> <div class="mt-5 mb-5">
<div class="container mb-5"> <div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Details') }} {{ __('Details') }}
</div> </div>
<FormControl <div class="grid grid-cols-2 gap-5">
v-model="course.title" <FormControl
:label="__('Title')" v-model="course.title"
class="mb-4" :label="__('Title')"
:required="true" :required="true"
/>
<FormControl
v-model="course.short_introduction"
:label="__('Short Introduction')"
:placeholder="
__(
'A one line introduction to the course that appears on the course card'
)
"
class="mb-4"
:required="true"
/>
<div class="mb-4">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val) => (course.description = 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 class="mb-4">
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Course Image') }}
<span class="text-ink-red-3">*</span>
</div>
<FileUploader
v-if="!course.course_image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="flex items-center">
<div class="border rounded-md w-fit py-5 px-20">
<Image class="size-5 stroke-1 text-ink-gray-7" />
</div>
<div class="ml-4">
<Button @click="openFileSelector">
{{ __('Upload') }}
</Button>
<div class="mt-2 text-ink-gray-5 text-sm">
{{
__('Appears on the course card in the course list')
}}
</div>
</div>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="course.course_image.file_url"
class="border rounded-md w-40"
/>
<div class="ml-4">
<Button @click="removeImage()">
{{ __('Remove') }}
</Button>
<div class="mt-2 text-ink-gray-5 text-sm">
{{ __('Appears on the course card in the course list') }}
</div>
</div>
</div>
</div>
</div>
<FormControl
v-model="course.video_link"
:label="__('Preview Video')"
:placeholder="
__(
'Paste the youtube link of a short video introducing the course'
)
"
class="mb-4"
/>
<div class="mb-4">
<div class="mb-1.5 text-xs text-ink-gray-5">
{{ __('Tags') }}
</div>
<div class="flex items-center">
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md mr-2"
>
{{ tag }}
<X
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
@click="removeTag(tag)"
/>
</div>
<FormControl
v-model="newTag"
:placeholder="__('Add a keyword and then press enter')"
class="w-72"
@keyup.enter="updateTags()"
id="tags"
/>
</div>
</div>
<div class="w-1/2 mb-4">
<Link <Link
doctype="LMS Category" doctype="LMS Category"
v-model="course.category" v-model="course.category"
:label="__('Category')" :label="__('Category')"
:onCreate="(value, close) => openSettings(close)" :onCreate="(value, close) => openSettings('Categories', close)"
/> />
</div> </div>
<MultiSelect <div class="grid grid-cols-2 gap-5">
v-model="instructors" <MultiSelect
doctype="User" v-model="instructors"
:label="__('Instructors')" doctype="User"
:filters="{ ignore_user_type: 1 }" :label="__('Instructors')"
:required="true" :filters="{ ignore_user_type: 1 }"
/> :onCreate="(close) => openSettings('Members', close)"
</div> :required="true"
<div class="container border-t"> />
<div class="text-lg font-semibold mt-5 mb-4"> <div>
{{ __('Settings') }} <div class="mb-1.5 text-xs text-ink-gray-5">
{{ __('Tags') }}
</div>
<div class="flex items-center">
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md mr-2"
>
{{ tag }}
<X
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
@click="removeTag(tag)"
/>
</div>
<FormControl
v-model="newTag"
:placeholder="__('Add a keyword and then press enter')"
class="w-full"
@keyup.enter="updateTags()"
id="tags"
/>
</div>
</div>
</div> </div>
<div class="grid grid-cols-2 gap-10 mb-4"> <div class="grid grid-cols-2 gap-5">
<div <FormControl
v-if="user.data?.is_moderator" v-model="course.short_introduction"
class="flex flex-col space-y-4" type="textarea"
> :rows="4"
:label="__('Short Introduction')"
:placeholder="
__(
'A one line introduction to the course that appears on the course card'
)
"
:required="true"
/>
<div class="mb-4">
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Course Image') }}
<span class="text-ink-red-3">*</span>
</div>
<FileUploader
v-if="!course.course_image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="flex items-center">
<div class="border rounded-md w-fit py-5 px-20">
<Image class="size-5 stroke-1 text-ink-gray-7" />
</div>
<div class="ml-4">
<Button @click="openFileSelector">
{{ __('Upload') }}
</Button>
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
{{
__('Appears on the course card in the course list')
}}
</div>
</div>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="course.course_image.file_url"
class="border rounded-md w-40"
/>
<div class="ml-4">
<Button @click="removeImage()">
{{ __('Remove') }}
</Button>
<div class="mt-2 text-ink-gray-5 text-sm">
{{ __('Appears on the course card in the course list') }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
{{ __("Settings") }}
</div>
<div class="grid grid-cols-2 gap-5">
<div class="flex flex-col space-y-5">
<FormControl <FormControl
type="checkbox" type="checkbox"
v-model="course.published" v-model="course.published"
@@ -171,10 +152,9 @@
v-model="course.published_on" v-model="course.published_on"
:label="__('Published On')" :label="__('Published On')"
type="date" type="date"
class="mb-5"
/> />
</div> </div>
<div class="flex flex-col space-y-3"> <div class="flex flex-col space-y-5">
<FormControl <FormControl
type="checkbox" type="checkbox"
v-model="course.upcoming" v-model="course.upcoming"
@@ -193,7 +173,36 @@
</div> </div>
</div> </div>
</div> </div>
<div class="container border-t space-y-4">
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val) => (course.description = 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>
<FormControl
v-model="course.video_link"
:label="__('Preview Video')"
:placeholder="
__(
'Paste the youtube link of a short video introducing the course'
)
"
/>
</div>
<div class="px-10 pb-5 space-y-5">
<div class="text-lg font-semibold mt-5"> <div class="text-lg font-semibold mt-5">
{{ __('Pricing and Certification') }} {{ __('Pricing and Certification') }}
</div> </div>
@@ -214,19 +223,25 @@
:label="__('Paid Certificate')" :label="__('Paid Certificate')"
/> />
</div> </div>
<FormControl v-model="course.course_price" :label="__('Amount')" /> <div class="grid grid-cols-2 gap-5">
<Link <div class="space-y-5">
doctype="Currency" <FormControl v-if="course.paid_course || course.paid_certificate" v-model="course.course_price" :label="__('Amount')" />
v-model="course.currency" <Link
:filters="{ enabled: 1 }" v-if="course.paid_certificate"
:label="__('Currency')" doctype="Course Evaluator"
/> v-model="course.evaluator"
<Link :label="__('Evaluator')"
v-if="course.paid_certificate" :onCreate="(value, close) => openSettings('Evaluators', close)"
doctype="Course Evaluator" />
v-model="course.evaluator" </div>
:label="__('Evaluator')" <Link
/> v-if="course.paid_course || course.paid_certificate"
doctype="Currency"
v-model="course.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -531,12 +546,6 @@ const removeImage = () => {
course.course_image = null course.course_image = null
} }
const openSettings = (close) => {
close()
settingsStore.activeTab = 'Categories'
settingsStore.isSettingsOpen = true
}
const check_permission = () => { const check_permission = () => {
let user_is_instructor = false let user_is_instructor = false
if (user.data?.is_moderator) return if (user.data?.is_moderator) return
@@ -552,6 +561,12 @@ const check_permission = () => {
} }
} }
const openSettings = (category, close) => {
close()
settingsStore.activeTab = category
settingsStore.isSettingsOpen = true
}
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
let crumbs = [ let crumbs = [
{ {