fix: course form cleanup
This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full !justify-start"
|
||||
label="Create New"
|
||||
:label="__('Create New')"
|
||||
@click="attrs.onCreate(value, close)"
|
||||
>
|
||||
<template #prefix>
|
||||
|
||||
@@ -4,79 +4,84 @@
|
||||
{{ label }}
|
||||
<span class="text-ink-red-3" v-if="required">*</span>
|
||||
</label>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<Button
|
||||
ref="emails"
|
||||
v-for="value in values"
|
||||
:key="value"
|
||||
:label="value"
|
||||
theme="gray"
|
||||
variant="subtle"
|
||||
class="rounded-md word-break-all"
|
||||
@keydown.delete.capture.stop="removeLastValue"
|
||||
>
|
||||
<template #suffix>
|
||||
<X @click="removeValue(value)" class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
<div class="">
|
||||
<Combobox v-model="selectedValue" nullable>
|
||||
<Popover class="w-full" v-model:show="showOptions">
|
||||
<template #target="{ togglePopover }">
|
||||
<ComboboxInput
|
||||
ref="search"
|
||||
class="search-input form-input w-full focus-visible:!ring-0"
|
||||
type="text"
|
||||
:value="query"
|
||||
@change="
|
||||
(e) => {
|
||||
query = e.target.value
|
||||
showOptions = true
|
||||
}
|
||||
"
|
||||
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"
|
||||
<div class="w-full">
|
||||
<Combobox v-model="selectedValue" nullable>
|
||||
<Popover class="w-full" v-model:show="showOptions">
|
||||
<template #target="{ togglePopover }">
|
||||
<ComboboxInput
|
||||
ref="search"
|
||||
class="search-input form-input w-full focus-visible:!ring-0"
|
||||
type="text"
|
||||
:value="query"
|
||||
@change="
|
||||
(e) => {
|
||||
query = e.target.value
|
||||
showOptions = true
|
||||
}
|
||||
"
|
||||
autocomplete="off"
|
||||
@focus="() => togglePopover()"
|
||||
@keydown.delete.capture.stop="removeLastValue"
|
||||
/>
|
||||
</template>
|
||||
<template #body="{ isOpen, close }">
|
||||
<div v-show="isOpen">
|
||||
<div
|
||||
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
||||
>
|
||||
<ComboboxOptions
|
||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||
static
|
||||
>
|
||||
<ComboboxOptions
|
||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||
static
|
||||
<ComboboxOption
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
v-slot="{ active }"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
v-slot="{ active }"
|
||||
<li
|
||||
:class="[
|
||||
'flex cursor-pointer items-center rounded px-2 py-1 text-base',
|
||||
{ 'bg-surface-gray-2': active },
|
||||
]"
|
||||
>
|
||||
<li
|
||||
:class="[
|
||||
'flex cursor-pointer items-center rounded px-2 py-1 text-base',
|
||||
{ '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 class="flex flex-col gap-1 p-1">
|
||||
<div class="text-base font-medium text-ink-gray-8">
|
||||
{{ option.description }}
|
||||
</div>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
</ComboboxOptions>
|
||||
</div>
|
||||
<div class="text-sm text-ink-gray-5">
|
||||
{{ option.value }}
|
||||
</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>
|
||||
</template>
|
||||
</Popover>
|
||||
</Combobox>
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
<!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> -->
|
||||
</div>
|
||||
@@ -90,9 +95,9 @@ import {
|
||||
ComboboxOption,
|
||||
} from '@headlessui/vue'
|
||||
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 { X } from 'lucide-vue-next'
|
||||
import { X, Plus } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
@@ -124,7 +129,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const values = defineModel()
|
||||
|
||||
const attrs = useAttrs()
|
||||
const emails = ref([])
|
||||
const search = ref(null)
|
||||
const error = ref(null)
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
:debounce="300"
|
||||
/>
|
||||
<Button @click="() => (showForm = !showForm)">
|
||||
<template #icon>
|
||||
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
|
||||
<X v-else class="h-3 w-3 stroke-1.5" />
|
||||
<template #prefix>
|
||||
<Plus v-if="!showForm" class="size-4 stroke-1.5" />
|
||||
<X v-else class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ showForm ? __('Close') : __('New') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
:debounce="300"
|
||||
/>
|
||||
<Button @click="() => (showForm = !showForm)">
|
||||
<template #icon>
|
||||
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
|
||||
<X v-else class="h-3 w-3 stroke-1.5" />
|
||||
<template #prefix>
|
||||
<Plus v-if="!showForm" class="size-4 stroke-1.5" />
|
||||
<X v-else class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ showForm ? __('Close') : __('New') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,149 +19,130 @@
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mt-5 mb-10">
|
||||
<div class="container mb-5">
|
||||
<div class="mt-5 mb-5">
|
||||
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold mb-4">
|
||||
{{ __('Details') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="course.title"
|
||||
:label="__('Title')"
|
||||
class="mb-4"
|
||||
: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 class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="course.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
/>
|
||||
</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
|
||||
doctype="LMS Category"
|
||||
v-model="course.category"
|
||||
:label="__('Category')"
|
||||
:onCreate="(value, close) => openSettings(close)"
|
||||
:onCreate="(value, close) => openSettings('Categories', close)"
|
||||
/>
|
||||
</div>
|
||||
<MultiSelect
|
||||
v-model="instructors"
|
||||
doctype="User"
|
||||
:label="__('Instructors')"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="container border-t">
|
||||
<div class="text-lg font-semibold mt-5 mb-4">
|
||||
{{ __('Settings') }}
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<MultiSelect
|
||||
v-model="instructors"
|
||||
doctype="User"
|
||||
:label="__('Instructors')"
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
:onCreate="(close) => openSettings('Members', close)"
|
||||
:required="true"
|
||||
/>
|
||||
<div>
|
||||
<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 class="grid grid-cols-2 gap-10 mb-4">
|
||||
<div
|
||||
v-if="user.data?.is_moderator"
|
||||
class="flex flex-col space-y-4"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
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
|
||||
type="checkbox"
|
||||
v-model="course.published"
|
||||
@@ -171,10 +152,9 @@
|
||||
v-model="course.published_on"
|
||||
:label="__('Published On')"
|
||||
type="date"
|
||||
class="mb-5"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-5">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.upcoming"
|
||||
@@ -193,7 +173,36 @@
|
||||
</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">
|
||||
{{ __('Pricing and Certification') }}
|
||||
</div>
|
||||
@@ -214,19 +223,25 @@
|
||||
:label="__('Paid Certificate')"
|
||||
/>
|
||||
</div>
|
||||
<FormControl v-model="course.course_price" :label="__('Amount')" />
|
||||
<Link
|
||||
doctype="Currency"
|
||||
v-model="course.currency"
|
||||
:filters="{ enabled: 1 }"
|
||||
:label="__('Currency')"
|
||||
/>
|
||||
<Link
|
||||
v-if="course.paid_certificate"
|
||||
doctype="Course Evaluator"
|
||||
v-model="course.evaluator"
|
||||
:label="__('Evaluator')"
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<div class="space-y-5">
|
||||
<FormControl v-if="course.paid_course || course.paid_certificate" v-model="course.course_price" :label="__('Amount')" />
|
||||
<Link
|
||||
v-if="course.paid_certificate"
|
||||
doctype="Course Evaluator"
|
||||
v-model="course.evaluator"
|
||||
:label="__('Evaluator')"
|
||||
:onCreate="(value, close) => openSettings('Evaluators', close)"
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
v-if="course.paid_course || course.paid_certificate"
|
||||
doctype="Currency"
|
||||
v-model="course.currency"
|
||||
:filters="{ enabled: 1 }"
|
||||
:label="__('Currency')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -531,12 +546,6 @@ const removeImage = () => {
|
||||
course.course_image = null
|
||||
}
|
||||
|
||||
const openSettings = (close) => {
|
||||
close()
|
||||
settingsStore.activeTab = 'Categories'
|
||||
settingsStore.isSettingsOpen = true
|
||||
}
|
||||
|
||||
const check_permission = () => {
|
||||
let user_is_instructor = false
|
||||
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(() => {
|
||||
let crumbs = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user