chore: fixed merge conflicts
This commit is contained in:
@@ -179,6 +179,9 @@
|
||||
"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
:uploadArgs="{
|
||||
private: 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>
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
{{ __('Enroll Now') }}
|
||||
</Button>
|
||||
<router-link
|
||||
v-if="isModerator"
|
||||
v-if="canEditBatch"
|
||||
:to="{
|
||||
name: 'BatchForm',
|
||||
params: {
|
||||
@@ -209,7 +209,19 @@ const isEvaluator = computed(() => {
|
||||
return user.data?.is_evaluator
|
||||
})
|
||||
|
||||
const isInstructor = computed(() => {
|
||||
return (
|
||||
props.batch.data?.instructors?.filter(
|
||||
(instructor) => instructor.name === user.data?.name
|
||||
).length > 0
|
||||
)
|
||||
})
|
||||
|
||||
const canAccessBatch = computed(() => {
|
||||
return isModerator.value || isStudent.value || isEvaluator.value
|
||||
})
|
||||
|
||||
const canEditBatch = computed(() => {
|
||||
return isModerator.value || isInstructor.value
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
:style="
|
||||
modelValue
|
||||
? {
|
||||
backgroundColor:
|
||||
theme.backgroundColor[modelValue.toLowerCase()][400],
|
||||
backgroundColor: getColor(
|
||||
modelValue.toLowerCase(),
|
||||
400
|
||||
),
|
||||
}
|
||||
: {}
|
||||
"
|
||||
@@ -55,8 +57,7 @@
|
||||
:key="color"
|
||||
class="size-5 rounded-full cursor-pointer"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
theme.backgroundColor[color.toLowerCase()][400],
|
||||
backgroundColor: getColor(color.toLowerCase(), 400),
|
||||
}"
|
||||
@click="
|
||||
(e) => {
|
||||
@@ -79,7 +80,7 @@
|
||||
import { Button, FormControl, Popover } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
import { Palette, X } from 'lucide-vue-next'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { getColor } from '@/utils'
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
class="w-4 h-4 text-ink-gray-7 stroke-1.5"
|
||||
:is="icons.Folder"
|
||||
/>
|
||||
<span v-if="selectedIcon">
|
||||
<span v-if="selectedIcon" class="text-ink-gray-7">
|
||||
{{ selectedIcon }}
|
||||
</span>
|
||||
<span v-else class="text-ink-gray-5">
|
||||
|
||||
@@ -28,10 +28,12 @@
|
||||
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
||||
>
|
||||
<ComboboxOptions
|
||||
class="my-1 min-h-[6rem] max-h-[12rem] overflow-y-auto px-1.5"
|
||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||
:class="options.length ? 'min-h-[6rem]' : 'min-h-[3.8rem]'"
|
||||
static
|
||||
>
|
||||
<ComboboxOption
|
||||
v-if="options.length"
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
@@ -53,7 +55,9 @@
|
||||
</div>
|
||||
</li>
|
||||
</ComboboxOption>
|
||||
<div class="h-10"></div>
|
||||
<div v-else class="text-ink-gray-7 px-4">
|
||||
{{ __('No results found') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="attrs.onCreate"
|
||||
class="absolute bottom-2 left-1 w-[95%] pt-2 bg-white border-t"
|
||||
|
||||
@@ -136,11 +136,11 @@
|
||||
import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Tooltip } from 'frappe-ui'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { formatAmount } from '@/utils'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
import colors from '@/utils/frappe-ui-colors.json'
|
||||
|
||||
const { user } = sessionStore()
|
||||
|
||||
@@ -152,8 +152,10 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const getGradientColor = () => {
|
||||
let theme =
|
||||
localStorage.getItem('theme') == 'light' ? 'lightMode' : 'darkMode'
|
||||
let color = props.course.card_gradient?.toLowerCase() || 'blue'
|
||||
let colorMap = theme.backgroundColor[color]
|
||||
let colorMap = colors[theme][color]
|
||||
return `linear-gradient(to top right, black, ${colorMap[400]})`
|
||||
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
|
||||
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import AppSidebar from './AppSidebar.vue'
|
||||
import AppSidebar from '@/components/Sidebar/AppSidebar.vue'
|
||||
</script>
|
||||
|
||||
@@ -114,11 +114,11 @@
|
||||
categoryColumn: 'category',
|
||||
valueColumn: 'count',
|
||||
colors: [
|
||||
theme.colors.red['400'],
|
||||
theme.colors.amber['400'],
|
||||
theme.colors.pink['400'],
|
||||
theme.colors.blue['400'],
|
||||
theme.colors.green['400'],
|
||||
getColor('red', 400),
|
||||
getColor('amber', 400),
|
||||
getColor('pink', 400),
|
||||
getColor('blue', 400),
|
||||
getColor('green', 400),
|
||||
],
|
||||
}"
|
||||
/>
|
||||
@@ -146,7 +146,7 @@ import {
|
||||
NumberChart,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { getColor } from '@/utils'
|
||||
|
||||
const show = defineModel<boolean>({ default: false })
|
||||
const searchFilter = ref<string | null>(null)
|
||||
|
||||
@@ -222,8 +222,8 @@ watch(
|
||||
|
||||
watch(
|
||||
() => profile.language,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
() => {
|
||||
if (profile.language !== props.profile.data.language) {
|
||||
hasLanguageChanged.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +66,18 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, createResource, Select, FormControl, toast } from 'frappe-ui'
|
||||
import {
|
||||
dayjs,
|
||||
Dialog,
|
||||
createResource,
|
||||
Select,
|
||||
FormControl,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, watch, inject } from 'vue'
|
||||
import { formatTime } from '@/utils/'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
const show = defineModel()
|
||||
const evaluations = defineModel('reloadEvals')
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
class="text-base"
|
||||
:options="{
|
||||
title: __('Add web page to sidebar'),
|
||||
size: 'lg',
|
||||
@@ -17,15 +16,17 @@
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<Link
|
||||
v-model="page.webpage"
|
||||
doctype="Web Page"
|
||||
:label="__('Web Page')"
|
||||
:filters="{
|
||||
published: 1,
|
||||
}"
|
||||
/>
|
||||
<IconPicker v-model="page.icon" :label="__('Icon')" class="mt-4" />
|
||||
<div class="text-base">
|
||||
<Link
|
||||
v-model="page.webpage"
|
||||
doctype="Web Page"
|
||||
:label="__('Web Page')"
|
||||
:filters="{
|
||||
published: 1,
|
||||
}"
|
||||
/>
|
||||
<IconPicker v-model="page.icon" :label="__('Icon')" class="mt-4" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -117,32 +117,27 @@ const props = defineProps({
|
||||
watch(
|
||||
() => props.accountID,
|
||||
(val) => {
|
||||
if (val != 'new') {
|
||||
zoomAccounts.value?.data.forEach((acc) => {
|
||||
if (acc.name === val) {
|
||||
account.name = acc.name
|
||||
account.enabled = acc.enabled || false
|
||||
account.member = acc.member
|
||||
account.account_id = acc.account_id
|
||||
account.client_id = acc.client_id
|
||||
account.client_secret = acc.client_secret
|
||||
}
|
||||
})
|
||||
if (val === 'new') {
|
||||
account.name = ''
|
||||
account.enabled = false
|
||||
account.member = user?.data?.name || ''
|
||||
account.account_id = ''
|
||||
account.client_id = ''
|
||||
account.client_secret = ''
|
||||
} else if (val && val !== 'new') {
|
||||
const acc = zoomAccounts.value?.data.find((acc) => acc.name === val)
|
||||
if (acc) {
|
||||
account.name = acc.name
|
||||
account.enabled = acc.enabled || false
|
||||
account.member = acc.member
|
||||
account.account_id = acc.account_id
|
||||
account.client_id = acc.client_id
|
||||
account.client_secret = acc.client_secret
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(show, (val) => {
|
||||
if (!val) {
|
||||
account.name = ''
|
||||
account.enabled = false
|
||||
account.member = user?.data?.name || ''
|
||||
account.account_id = ''
|
||||
account.client_id = ''
|
||||
account.client_secret = ''
|
||||
}
|
||||
})
|
||||
|
||||
const saveAccount = (close: () => void) => {
|
||||
if (props.accountID == 'new') {
|
||||
createAccount(close)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<span
|
||||
class="size-3 rounded-full"
|
||||
:style="{
|
||||
backgroundColor: theme.backgroundColor[color.toLowerCase()][400],
|
||||
backgroundColor: getColor(color.toLowerCase(), 400),
|
||||
}"
|
||||
></span>
|
||||
<span>
|
||||
@@ -55,9 +55,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { NotepadText, Trash2 } from 'lucide-vue-next'
|
||||
import { theme } from '@/utils/theme'
|
||||
import type { Note, Notes } from '@/components/Notes/types'
|
||||
import { blockQuotesClick, highlightText } from '@/utils'
|
||||
import { blockQuotesClick, getColor, highlightText } from '@/utils'
|
||||
|
||||
const user = inject<any>('$user')
|
||||
const show = defineModel()
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
:placeholder="__('Make notes for quick revision. Press / for menu.')"
|
||||
@change="(val: string) => updateNoteText(val)"
|
||||
:editable="true"
|
||||
:uploadArgs="{
|
||||
private: true,
|
||||
}"
|
||||
editorClass="prose prose-sm min-h-[200px] max-w-none"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div
|
||||
v-if="activeTab && data.doc"
|
||||
:key="activeTab.label"
|
||||
class="flex flex-1 flex-col p-8 bg-surface-modal"
|
||||
class="flex flex-1 flex-col p-8 bg-surface-modal overflow-x-auto"
|
||||
>
|
||||
<component
|
||||
v-if="activeTab.template"
|
||||
@@ -71,7 +71,7 @@ import { Dialog, createDocumentResource } from 'frappe-ui'
|
||||
import { computed, markRaw, ref, watch } from 'vue'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import SettingDetails from '@/components/Settings/SettingDetails.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
import SidebarLink from '@/components/Sidebar/SidebarLink.vue'
|
||||
import Members from '@/components/Settings/Members.vue'
|
||||
import Evaluators from '@/components/Settings/Evaluators.vue'
|
||||
import Categories from '@/components/Settings/Categories.vue'
|
||||
@@ -181,6 +181,53 @@ const tabsStructure = computed(() => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Lists',
|
||||
hideLabel: false,
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
description:
|
||||
'Add new members or manage roles and permissions of existing members',
|
||||
icon: 'UserRoundPlus',
|
||||
template: markRaw(Members),
|
||||
},
|
||||
{
|
||||
label: 'Evaluators',
|
||||
description: '',
|
||||
icon: 'UserCheck',
|
||||
description:
|
||||
'Add new evaluators or check the slots existing evaluators',
|
||||
template: markRaw(Evaluators),
|
||||
},
|
||||
{
|
||||
label: 'Zoom Accounts',
|
||||
description:
|
||||
'Manage zoom accounts to conduct live classes from batches',
|
||||
icon: 'Video',
|
||||
template: markRaw(ZoomSettings),
|
||||
},
|
||||
{
|
||||
label: 'Badges',
|
||||
description:
|
||||
'Create badges and assign them to students to acknowledge their achievements',
|
||||
icon: 'Award',
|
||||
template: markRaw(Badges),
|
||||
},
|
||||
{
|
||||
label: 'Categories',
|
||||
description: 'Double click to edit the category',
|
||||
icon: 'Network',
|
||||
template: markRaw(Categories),
|
||||
},
|
||||
{
|
||||
label: 'Email Templates',
|
||||
description: 'Manage the email templates for your learning system',
|
||||
icon: 'MailPlus',
|
||||
template: markRaw(EmailTemplates),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Payment',
|
||||
hideLabel: false,
|
||||
@@ -242,53 +289,6 @@ const tabsStructure = computed(() => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Lists',
|
||||
hideLabel: false,
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
description:
|
||||
'Add new members or manage roles and permissions of existing members',
|
||||
icon: 'UserRoundPlus',
|
||||
template: markRaw(Members),
|
||||
},
|
||||
{
|
||||
label: 'Evaluators',
|
||||
description: '',
|
||||
icon: 'UserCheck',
|
||||
description:
|
||||
'Add new evaluators or check the slots existing evaluators',
|
||||
template: markRaw(Evaluators),
|
||||
},
|
||||
{
|
||||
label: 'Zoom Accounts',
|
||||
description:
|
||||
'Manage zoom accounts to conduct live classes from batches',
|
||||
icon: 'Video',
|
||||
template: markRaw(ZoomSettings),
|
||||
},
|
||||
{
|
||||
label: 'Badges',
|
||||
description:
|
||||
'Create badges and assign them to students to acknowledge their achievements',
|
||||
icon: 'Award',
|
||||
template: markRaw(Badges),
|
||||
},
|
||||
{
|
||||
label: 'Categories',
|
||||
description: 'Double click to edit the category',
|
||||
icon: 'Network',
|
||||
template: markRaw(Categories),
|
||||
},
|
||||
{
|
||||
label: 'Email Templates',
|
||||
description: 'Manage the email templates for your learning system',
|
||||
icon: 'MailPlus',
|
||||
template: markRaw(EmailTemplates),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Customize',
|
||||
hideLabel: false,
|
||||
|
||||
@@ -187,11 +187,12 @@ import { usersStore } from '@/stores/user'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useSidebar } from '@/stores/sidebar'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { Button, call, createResource, Tooltip } from 'frappe-ui'
|
||||
import { Button, call, createResource, Tooltip, toast } from 'frappe-ui'
|
||||
import PageModal from '@/components/Modals/PageModal.vue'
|
||||
import { capture } from '@/telemetry'
|
||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import InviteIcon from '@/components/Icons/InviteIcon.vue'
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
@@ -456,21 +457,13 @@ const openPageModal = (link) => {
|
||||
}
|
||||
|
||||
const deletePage = (link) => {
|
||||
createResource({
|
||||
url: 'lms.lms.api.delete_sidebar_item',
|
||||
makeParams(values) {
|
||||
return {
|
||||
webpage: link.web_page,
|
||||
}
|
||||
},
|
||||
}).submit(
|
||||
{},
|
||||
{
|
||||
onSuccess() {
|
||||
sidebarSettings.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
call('lms.lms.api.delete_documents', {
|
||||
doctype: 'LMS Sidebar Item',
|
||||
documents: [link.name],
|
||||
}).then(() => {
|
||||
sidebarSettings.reload()
|
||||
toast.success(__('Page deleted successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<Popover placement="right-start" class="flex w-full">
|
||||
<Popover placement="right-start" trigger="hover" class="flex w-full">
|
||||
<template #target="{ togglePopover }">
|
||||
<button
|
||||
:class="[
|
||||
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-ink-gray-7 hover:bg-surface-gray-2',
|
||||
]"
|
||||
@click.prevent="togglePopover()"
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<LayoutGrid class="size-4 stroke-1.5" />
|
||||
26
frontend/src/components/Sidebar/Configuration.vue
Normal file
26
frontend/src/components/Sidebar/Configuration.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-3 justify-between bg-surface-white">
|
||||
<div key="name" class="py-1 px-2 hover:bg-surface-gray-2 rounded">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'DataImportList',
|
||||
query: {
|
||||
step: 'list',
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col items-center space-y-1">
|
||||
<ArrowDownToLine
|
||||
class="size-9 text-ink-gray-7 p-2 bg-surface-gray-2 rounded-md"
|
||||
/>
|
||||
<div class="text-sm text-ink-gray-7">
|
||||
{{ __('Import') }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ArrowDownToLine } from 'lucide-vue-next'
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<Dropdown :options="userDropdownOptions">
|
||||
<template v-slot="{ open }">
|
||||
<template v-slot="{ open, close }">
|
||||
<button
|
||||
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
||||
:class="
|
||||
@@ -64,18 +64,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Dropdown } from 'frappe-ui'
|
||||
import Apps from '@/components/Apps.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import { usersStore } from '@/stores/user'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
||||
import { createDialog } from '@/utils/dialogs'
|
||||
import SettingsModal from '@/components/Settings/Settings.vue'
|
||||
import Apps from '@/components/Sidebar/Apps.vue'
|
||||
import Configuration from '@/components/Sidebar/Configuration.vue'
|
||||
import FrappeCloudIcon from '@/components/Icons/FrappeCloudIcon.vue'
|
||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||
import SettingsModal from '@/components/Settings/Settings.vue'
|
||||
import {
|
||||
ChevronDown,
|
||||
LogIn,
|
||||
@@ -84,6 +85,7 @@ import {
|
||||
User,
|
||||
Settings,
|
||||
Sun,
|
||||
Wrench,
|
||||
Zap,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
@@ -168,6 +170,18 @@ const userDropdownOptions = computed(() => {
|
||||
return userResource.data?.is_moderator
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Configuration',
|
||||
icon: Wrench,
|
||||
submenu: [
|
||||
{
|
||||
component: markRaw(Configuration),
|
||||
},
|
||||
],
|
||||
condition: () => {
|
||||
return userResource.data?.is_moderator
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: FrappeCloudIcon,
|
||||
label: 'Login to Frappe Cloud',
|
||||
@@ -17,7 +17,7 @@
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import ApexChart from 'vue3-apexcharts'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { getColor } from '@/utils'
|
||||
|
||||
const user = inject('$user')
|
||||
const labels = ref([])
|
||||
@@ -81,11 +81,11 @@ const chartOptions = computed(() => {
|
||||
enableShades: true,
|
||||
colorScale: {
|
||||
ranges: [
|
||||
{ from: 0, to: 0, color: theme.colors.gray[400] },
|
||||
{ from: 1, to: 5, color: theme.colors.green[200] },
|
||||
{ from: 6, to: 15, color: theme.colors.green[500] },
|
||||
{ from: 16, to: 30, color: theme.colors.green[700] },
|
||||
{ from: 31, to: 100, color: theme.colors.green[800] },
|
||||
{ from: 0, to: 0, color: getColor('green', 400) },
|
||||
{ from: 1, to: 5, color: getColor('green', 200) },
|
||||
{ from: 6, to: 15, color: getColor('green', 500) },
|
||||
{ from: 16, to: 30, color: getColor('green', 700) },
|
||||
{ from: 31, to: 100, color: getColor('green', 800) },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@import './assets/Inter/inter.css';
|
||||
@import 'frappe-ui/src/style.css';
|
||||
@import 'frappe-ui/style.css';
|
||||
@import './styles/codemirror.css';
|
||||
@@ -3,7 +3,49 @@
|
||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
<router-link
|
||||
<Dropdown
|
||||
v-if="canCreateBatch()"
|
||||
:options="[
|
||||
{
|
||||
label: __('New Batch'),
|
||||
icon: 'users',
|
||||
onClick() {
|
||||
router.push({
|
||||
name: 'BatchForm',
|
||||
params: { batchName: 'new' },
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __('Import Batch'),
|
||||
icon: 'upload',
|
||||
onClick() {
|
||||
router.push({
|
||||
name: 'NewDataImport',
|
||||
params: { doctype: 'LMS Batch' },
|
||||
})
|
||||
},
|
||||
},
|
||||
]"
|
||||
>
|
||||
<template v-slot="{ open }">
|
||||
<Button variant="solid">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Create') }}
|
||||
<template #suffix>
|
||||
<ChevronDown
|
||||
:class="[
|
||||
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
|
||||
open ? 'rotate-180' : '',
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<!-- <router-link
|
||||
v-if="canCreateBatch()"
|
||||
:to="{
|
||||
name: 'BatchForm',
|
||||
@@ -16,7 +58,7 @@
|
||||
</template>
|
||||
{{ __('Create') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
</router-link> -->
|
||||
</header>
|
||||
<div class="p-5 pb-10">
|
||||
<div
|
||||
@@ -90,13 +132,15 @@ import {
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
Dropdown,
|
||||
FormControl,
|
||||
Select,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ChevronDown, Plus } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import BatchCard from '@/components/BatchCard.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
@@ -115,6 +159,7 @@ const is_student = computed(() => user.data?.is_student)
|
||||
const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
|
||||
const orderBy = ref('start_date')
|
||||
const readOnlyMode = window.read_only_mode
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
setFiltersFromQuery()
|
||||
|
||||
@@ -3,20 +3,51 @@
|
||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
<router-link
|
||||
|
||||
<Dropdown
|
||||
placement="start"
|
||||
side="bottom"
|
||||
v-if="canCreateCourse()"
|
||||
:to="{
|
||||
name: 'CourseForm',
|
||||
params: { courseName: 'new' },
|
||||
}"
|
||||
:options="[
|
||||
{
|
||||
label: __('New Course'),
|
||||
icon: 'book-open',
|
||||
onClick() {
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
params: { courseName: 'new' },
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __('Import Course'),
|
||||
icon: 'upload',
|
||||
onClick() {
|
||||
router.push({
|
||||
name: 'NewDataImport',
|
||||
params: { doctype: 'LMS Course' },
|
||||
})
|
||||
},
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Button variant="solid">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Create') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
<template v-slot="{ open }">
|
||||
<Button variant="solid">
|
||||
<template #prefix>
|
||||
<Plus class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Create') }}
|
||||
<template #suffix>
|
||||
<ChevronDown
|
||||
:class="[
|
||||
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
|
||||
open ? 'rotate-180' : '',
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</header>
|
||||
<div class="p-5 pb-10">
|
||||
<div
|
||||
@@ -85,13 +116,14 @@ import {
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
Dropdown,
|
||||
FormControl,
|
||||
Select,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { ChevronDown, Plus } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { canCreateCourse } from '@/utils'
|
||||
import CourseCard from '@/components/CourseCard.vue'
|
||||
|
||||
50
frontend/src/pages/DataImport.vue
Normal file
50
frontend/src/pages/DataImport.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<DataImport
|
||||
:doctype="route.params.doctype"
|
||||
:importName="route.params.importName"
|
||||
:doctypeMap="doctypeMap"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { usePageMeta } from 'frappe-ui'
|
||||
import { DataImport } from 'frappe-ui/frappe'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { inject, onMounted } from 'vue'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const user = inject<any>('$user')
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data?.is_moderator) {
|
||||
router.push({
|
||||
name: 'Courses',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const doctypeMap = {
|
||||
'LMS Course': {
|
||||
title: 'Courses',
|
||||
listRoute: '/courses',
|
||||
pageRoute: `/courses/docname`,
|
||||
},
|
||||
'LMS Batch': {
|
||||
title: 'Batches',
|
||||
listRoute: '/batches',
|
||||
},
|
||||
'LMS Category': {
|
||||
title: 'Categories',
|
||||
listRoute: '/lms',
|
||||
},
|
||||
}
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: __('Data Import'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<div v-if="createdBatches.data?.length" class="mt-10">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="font-semibold text-lg">
|
||||
<span class="font-semibold text-lg text-ink-gray-9">
|
||||
{{ __('Upcoming Batches') }}
|
||||
</span>
|
||||
<router-link
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
<div class="grid grid-cols-2 gap-5 mt-10">
|
||||
<div v-if="evals?.data?.length">
|
||||
<div class="font-semibold text-lg mb-3">
|
||||
<div class="font-semibold text-lg text-ink-gray-9 mb-3">
|
||||
{{ __('Upcoming Evaluations') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
@@ -124,7 +124,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="liveClasses?.data?.length">
|
||||
<div class="font-semibold text-lg mb-3">
|
||||
<div class="font-semibold text-lg text-ink-gray-9 mb-3">
|
||||
{{ __('Upcoming Live Classes') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
|
||||
@@ -17,86 +17,91 @@
|
||||
</header>
|
||||
<div class="max-w-4xl mx-auto pt-5 p-4">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-lg font-semibold text-ink-gray-9 mb-2">
|
||||
{{ applications.data?.length || 0 }}
|
||||
{{
|
||||
applications.data?.length === 1
|
||||
? __('Application')
|
||||
: __('Applications')
|
||||
}}
|
||||
<h1 class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
|
||||
{{ applicationCount }}
|
||||
{{ applicationCount === 1 ? __('Application') : __('Applications') }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<ListView
|
||||
v-if="applications.data?.length"
|
||||
:columns="applicationColumns"
|
||||
:rows="applicantRows"
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
<div v-if="applications.data?.length">
|
||||
<ListView
|
||||
:columns="applicationColumns"
|
||||
:rows="applicantRows"
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
}"
|
||||
>
|
||||
<ListHeaderItem
|
||||
:item="item"
|
||||
v-for="item in applicationColumns"
|
||||
:key="item.key"
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
>
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon?.toString()"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow
|
||||
:row="row"
|
||||
v-slot="{ column, item }"
|
||||
v-for="row in applicantRows"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<ListRowItem :item="item">
|
||||
<div
|
||||
v-if="column.key === 'full_name'"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
:image="row['user_image']"
|
||||
:label="row['full_name']"
|
||||
<ListHeaderItem
|
||||
:item="item"
|
||||
v-for="item in applicationColumns"
|
||||
:key="item.key"
|
||||
>
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon
|
||||
v-if="item.icon"
|
||||
:name="item.icon?.toString()"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow
|
||||
:row="row"
|
||||
v-slot="{ column, item }"
|
||||
v-for="row in applicantRows"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<ListRowItem :item="item">
|
||||
<div
|
||||
v-if="column.key === 'full_name'"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
:image="row['user_image']"
|
||||
:label="row['full_name']"
|
||||
/>
|
||||
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'actions'"
|
||||
class="flex justify-center"
|
||||
>
|
||||
<Dropdown :options="getActionOptions(row)">
|
||||
<Button variant="ghost">
|
||||
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'applied_on'"
|
||||
class="text-sm text-ink-gray-6"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ item }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
<span>{{ item }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'actions'"
|
||||
class="flex justify-center"
|
||||
>
|
||||
<Dropdown :options="getActionOptions(row)">
|
||||
<Button variant="ghost">
|
||||
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key === 'applied_on'"
|
||||
class="text-sm text-ink-gray-6"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ item }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
<div class="flex justify-center mt-5">
|
||||
<Button v-if="applications.hasNextPage" @click="applications.next()">
|
||||
<template #prefix>
|
||||
<RefreshCw class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState v-else-if="!applications.loading" type="Job Applications" />
|
||||
</div>
|
||||
|
||||
@@ -150,6 +155,7 @@ import {
|
||||
Avatar,
|
||||
Button,
|
||||
Breadcrumbs,
|
||||
call,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
FeatherIcon,
|
||||
@@ -166,8 +172,8 @@ import {
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
|
||||
import { inject, ref, computed, reactive } from 'vue'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
import { computed, inject, onMounted, ref, reactive } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
@@ -175,6 +181,7 @@ const dayjs = inject('$dayjs')
|
||||
const { brand } = sessionStore()
|
||||
const showEmailModal = ref(false)
|
||||
const selectedApplicant = ref(null)
|
||||
const applicationCount = ref(0)
|
||||
const emailForm = reactive({
|
||||
subject: '',
|
||||
message: '',
|
||||
@@ -188,6 +195,19 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getApplicationCount()
|
||||
})
|
||||
|
||||
const getApplicationCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Job Application',
|
||||
filters: { job: props.job },
|
||||
}).then((count) => {
|
||||
applicationCount.value = count
|
||||
})
|
||||
}
|
||||
|
||||
const applications = createListResource({
|
||||
doctype: 'LMS Job Application',
|
||||
fields: [
|
||||
@@ -253,7 +273,6 @@ const sendEmail = (close) => {
|
||||
}
|
||||
|
||||
const downloadResume = (resumeUrl) => {
|
||||
console.log(resumeUrl)
|
||||
window.open(resumeUrl, '_blank')
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</Button>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="user.data.name == job.data?.owner"
|
||||
v-if="canManageJob"
|
||||
:to="{
|
||||
name: 'JobForm',
|
||||
params: { jobName: job.data?.name },
|
||||
@@ -240,9 +240,7 @@ const redirectToWebsite = (url) => {
|
||||
|
||||
const canManageJob = computed(() => {
|
||||
if (!user.data?.name || !job.data) return false
|
||||
return (
|
||||
user.data.name === job.data.owner || user.data.roles?.includes('Moderator')
|
||||
)
|
||||
return user.data.name === job.data.owner || user.data?.is_moderator
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
|
||||
@@ -207,6 +207,11 @@ const jobDetail = createResource({
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (data.owner != user.data?.name && !user.data?.is_moderator) {
|
||||
router.push({
|
||||
name: 'Jobs',
|
||||
})
|
||||
}
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (Object.hasOwn(job, key)) job[key] = data[key]
|
||||
})
|
||||
@@ -242,7 +247,11 @@ const job = reactive({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data) window.location.href = '/login'
|
||||
if (!user.data) {
|
||||
router.push({
|
||||
name: 'Jobs',
|
||||
})
|
||||
}
|
||||
|
||||
if (props.jobName != 'new') jobDetail.reload()
|
||||
})
|
||||
|
||||
@@ -32,10 +32,13 @@
|
||||
{{ __('{0} Open Jobs').format(jobCount) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 gap-2 md:grid-cols-4"
|
||||
:class="user.data ? 'md:grid-cols-3' : 'md:grid-cols-2'"
|
||||
>
|
||||
<div class="flex items-center justify-between space-x-4">
|
||||
<TabButtons
|
||||
v-if="tabs.length > 1"
|
||||
v-model="activeTab"
|
||||
:buttons="tabs"
|
||||
@change="updateJobs"
|
||||
/>
|
||||
<FormControl
|
||||
type="text"
|
||||
:placeholder="__('Search')"
|
||||
@@ -55,13 +58,13 @@
|
||||
doctype="Country"
|
||||
v-model="country"
|
||||
:placeholder="__('Country')"
|
||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="jobType"
|
||||
type="select"
|
||||
:options="jobTypes"
|
||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||
:placeholder="__('Type')"
|
||||
@change="updateJobs"
|
||||
/>
|
||||
@@ -69,7 +72,7 @@
|
||||
v-model="workMode"
|
||||
type="select"
|
||||
:options="workModes"
|
||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||
:placeholder="__('Work Mode')"
|
||||
@change="updateJobs"
|
||||
/>
|
||||
@@ -100,6 +103,7 @@ import {
|
||||
call,
|
||||
createResource,
|
||||
FormControl,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { Plus, Search } from 'lucide-vue-next'
|
||||
@@ -118,9 +122,38 @@ const country = ref(null)
|
||||
const filters = ref({})
|
||||
const orFilters = ref({})
|
||||
const jobCount = ref(0)
|
||||
const closedJobs = ref(0)
|
||||
const activeTab = ref('Open')
|
||||
const readOnlyMode = window.read_only_mode
|
||||
|
||||
onMounted(() => {
|
||||
getClosedJobCount()
|
||||
setFiltersFromURL()
|
||||
updateJobs()
|
||||
})
|
||||
|
||||
const isModerator = computed(() => {
|
||||
return user.data?.is_moderator
|
||||
})
|
||||
|
||||
const getClosedJobCount = () => {
|
||||
const filters = {
|
||||
status: 'Closed',
|
||||
}
|
||||
|
||||
if (!isModerator.value) {
|
||||
filters.owner = user.data?.name
|
||||
}
|
||||
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'Job Opportunity',
|
||||
filters: filters,
|
||||
}).then((count) => {
|
||||
closedJobs.value = count
|
||||
})
|
||||
}
|
||||
|
||||
const setFiltersFromURL = () => {
|
||||
let queries = new URLSearchParams(location.search)
|
||||
if (queries.has('type')) {
|
||||
jobType.value = queries.get('type')
|
||||
@@ -128,7 +161,22 @@ onMounted(() => {
|
||||
if (queries.has('work_mode')) {
|
||||
workMode.value = queries.get('work_mode')
|
||||
}
|
||||
updateJobs()
|
||||
}
|
||||
|
||||
const tabs = computed(() => {
|
||||
const tabsArray = [
|
||||
{
|
||||
label: __('Open'),
|
||||
},
|
||||
]
|
||||
|
||||
if (closedJobs.value) {
|
||||
tabsArray.push({
|
||||
label: __('Closed'),
|
||||
})
|
||||
}
|
||||
|
||||
return tabsArray
|
||||
})
|
||||
|
||||
const jobs = createResource({
|
||||
@@ -149,7 +197,6 @@ const updateJobs = () => {
|
||||
|
||||
const updateFilters = () => {
|
||||
filters.value.status = 'Open'
|
||||
filters.value.disabled = 0
|
||||
|
||||
if (jobType.value) {
|
||||
filters.value.type = jobType.value
|
||||
@@ -178,8 +225,22 @@ const updateFilters = () => {
|
||||
} else {
|
||||
delete filters.value.country
|
||||
}
|
||||
|
||||
if (activeTab.value === 'Closed') {
|
||||
filters.value.status = 'Closed'
|
||||
if (!isModerator.value) {
|
||||
filters.value.owner = user.data?.name
|
||||
}
|
||||
} else {
|
||||
filters.value.status = 'Open'
|
||||
delete filters.value.owner
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeTab, (val) => {
|
||||
updateJobs()
|
||||
})
|
||||
|
||||
watch(country, (val) => {
|
||||
updateJobs()
|
||||
})
|
||||
@@ -190,7 +251,7 @@ watch(jobs, () => {
|
||||
|
||||
const jobTypes = computed(() => {
|
||||
return [
|
||||
'',
|
||||
{ label: '', value: '' },
|
||||
{ label: __('Full Time'), value: 'Full Time' },
|
||||
{ label: __('Part Time'), value: 'Part Time' },
|
||||
{ label: __('Contract'), value: 'Contract' },
|
||||
@@ -200,7 +261,7 @@ const jobTypes = computed(() => {
|
||||
|
||||
const workModes = computed(() => {
|
||||
return [
|
||||
'',
|
||||
{ label: '', value: '' },
|
||||
{ label: 'On site', value: 'On-site' },
|
||||
{ label: 'Hybrid', value: 'Hybrid' },
|
||||
{ label: 'Remote', value: 'Remote' },
|
||||
|
||||
@@ -58,15 +58,15 @@
|
||||
</Button>
|
||||
</div>
|
||||
<ListView
|
||||
v-if="programCourses.data.length > 0"
|
||||
v-if="program.program_courses?.length > 0"
|
||||
:columns="courseColumns"
|
||||
:rows="programCourses.data"
|
||||
:rows="program.program_courses"
|
||||
:options="{
|
||||
selectable: true,
|
||||
resizeColumn: true,
|
||||
showTooltip: false,
|
||||
}"
|
||||
rowKey="name"
|
||||
:rowKey="programName === 'new' ? 'course' : 'name'"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
@@ -75,8 +75,8 @@
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<Draggable
|
||||
:list="programCourses.data"
|
||||
item-key="name"
|
||||
:list="program.program_courses"
|
||||
:item-key="programName === 'new' ? 'course' : 'name'"
|
||||
group="items"
|
||||
@end="updateOrder"
|
||||
class="cursor-move"
|
||||
@@ -133,14 +133,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<ListView
|
||||
v-if="programMembers.data.length > 0"
|
||||
v-if="program.program_members?.length > 0"
|
||||
:columns="memberColumns"
|
||||
:rows="programMembers.data"
|
||||
:rows="program.program_members"
|
||||
:options="{
|
||||
selectable: true,
|
||||
resizeColumn: true,
|
||||
}"
|
||||
rowKey="name"
|
||||
:rowKey="programName === 'new' ? 'member' : 'name'"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
@@ -148,7 +148,7 @@
|
||||
<ListHeaderItem :item="item" v-for="item in memberColumns" />
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow :row="row" v-for="row in programMembers.data" />
|
||||
<ListRow :row="row" v-for="row in program.program_members" />
|
||||
</ListRows>
|
||||
<ListSelectBanner>
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
@@ -217,13 +217,12 @@
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
<div class="flex justify-end space-x-2 group">
|
||||
<div class="flex justify-end space-x-2">
|
||||
<Button
|
||||
v-if="programName != 'new'"
|
||||
@click="deleteProgram(close)"
|
||||
variant="outline"
|
||||
theme="red"
|
||||
class="invisible group-hover:visible"
|
||||
>
|
||||
<template #prefix>
|
||||
<Trash2 class="size-4 stroke-1.5" />
|
||||
@@ -252,7 +251,7 @@ import {
|
||||
ListRow,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch, getCurrentInstance } from 'vue'
|
||||
import { Plus, Trash2, TrendingUp } from 'lucide-vue-next'
|
||||
import { Programs, Program } from '@/types/programs'
|
||||
import { escapeHTML, openSettings } from '@/utils'
|
||||
@@ -269,6 +268,9 @@ const member = ref<string>('')
|
||||
const showProgressDialog = ref(false)
|
||||
const dirty = ref(false)
|
||||
|
||||
const app = getCurrentInstance()
|
||||
const { $dialog } = app.appContext.config.globalProperties
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
programName: string | null
|
||||
@@ -427,25 +429,22 @@ const addCourse = (close: () => void) => {
|
||||
return
|
||||
}
|
||||
|
||||
programCourses.insert.submit(
|
||||
{
|
||||
parent: props.programName,
|
||||
parenttype: 'LMS Program',
|
||||
parentfield: 'program_courses',
|
||||
course: course.value,
|
||||
idx: programCourses.data.length + 1,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
updateCounts('course', 'add')
|
||||
close()
|
||||
toast.success(__('Course added to program successfully'))
|
||||
},
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
},
|
||||
}
|
||||
const existingCourse = program.value.program_courses.find(
|
||||
(c: any) => c.course === course.value
|
||||
)
|
||||
if (!existingCourse) {
|
||||
program.value.program_courses.push({
|
||||
course: course.value,
|
||||
idx: program.value.program_courses.length + 1,
|
||||
})
|
||||
if (props.programName !== 'new') {
|
||||
dirty.value = true
|
||||
}
|
||||
close()
|
||||
toast.success(__('Course added to program successfully'))
|
||||
} else {
|
||||
toast.warning(__('Course already added to program'))
|
||||
}
|
||||
}
|
||||
|
||||
const addMember = (close: () => void) => {
|
||||
@@ -454,24 +453,21 @@ const addMember = (close: () => void) => {
|
||||
return
|
||||
}
|
||||
|
||||
programMembers.insert.submit(
|
||||
{
|
||||
parent: props.programName,
|
||||
parenttype: 'LMS Program',
|
||||
parentfield: 'program_members',
|
||||
member: member.value,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
updateCounts('member', 'add')
|
||||
close()
|
||||
toast.success(__('Member added to program successfully'))
|
||||
},
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
},
|
||||
}
|
||||
const existingMember = program.value.program_members.find(
|
||||
(m) => m.member === member.value
|
||||
)
|
||||
if (!existingMember) {
|
||||
program.value.program_members.push({
|
||||
member: member.value,
|
||||
})
|
||||
if (props.programName !== 'new') {
|
||||
dirty.value = true
|
||||
}
|
||||
close()
|
||||
toast.success(__('Member added to program successfully'))
|
||||
} else {
|
||||
toast.warning(__('Member already added to program'))
|
||||
}
|
||||
}
|
||||
|
||||
const updateCounts = async (
|
||||
@@ -509,57 +505,83 @@ const updateCounts = async (
|
||||
const updateOrder = async (e: DragEvent) => {
|
||||
let sourceIdx = e.from.dataset.idx
|
||||
let targetIdx = e.to.dataset.idx
|
||||
let courses = programCourses.data
|
||||
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
|
||||
|
||||
for (const [index, course] of courses.entries()) {
|
||||
programCourses.setValue.submit(
|
||||
{
|
||||
name: course.name,
|
||||
idx: index + 1,
|
||||
},
|
||||
{
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
if (props.programName === 'new') {
|
||||
let courses = program.value.program_courses
|
||||
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
|
||||
courses.forEach((course, index) => {
|
||||
course.idx = index + 1
|
||||
})
|
||||
dirty.value = true
|
||||
} else {
|
||||
let courses = programCourses.data
|
||||
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
|
||||
|
||||
for (const [index, course] of courses.entries()) {
|
||||
programCourses.setValue.submit(
|
||||
{
|
||||
name: course.name,
|
||||
idx: index + 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
await wait(100)
|
||||
{
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
},
|
||||
}
|
||||
)
|
||||
await wait(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
|
||||
|
||||
const remove = async (
|
||||
const remove = (
|
||||
selections: string[],
|
||||
unselectAll: () => void,
|
||||
type: string
|
||||
) => {
|
||||
selections = Array.from(selections)
|
||||
for (const selection of selections) {
|
||||
if (type == 'courses') {
|
||||
await programCourses.delete.submit(selection)
|
||||
await updateCounts('course', 'remove')
|
||||
} else {
|
||||
await programMembers.delete.submit(selection)
|
||||
await updateCounts('member', 'remove')
|
||||
}
|
||||
await programs.value.reload()
|
||||
await wait(100)
|
||||
const selectionsArray = Array.from(selections)
|
||||
if (type === 'courses') {
|
||||
program.value.program_courses = program.value.program_courses.filter(
|
||||
(c: any) => !selectionsArray.includes(c.name || c.course)
|
||||
)
|
||||
} else {
|
||||
program.value.program_members = program.value.program_members.filter(
|
||||
(m: any) => !selectionsArray.includes(m.name || m.member)
|
||||
)
|
||||
}
|
||||
dirty.value = true
|
||||
unselectAll()
|
||||
}
|
||||
|
||||
const deleteProgram = (close: () => void) => {
|
||||
if (props.programName == 'new') return
|
||||
programs.value?.delete.submit(props.programName, {
|
||||
onSuccess() {
|
||||
toast.success(__('Program deleted successfully'))
|
||||
close()
|
||||
},
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
},
|
||||
$dialog({
|
||||
title: __('Delete Program'),
|
||||
message: __(
|
||||
'Are you sure you want to delete this program? This action cannot be undone.'
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
label: __('Delete'),
|
||||
theme: 'red',
|
||||
variant: 'solid',
|
||||
onClick(closeDialog) {
|
||||
programs.value?.delete.submit(props.programName, {
|
||||
onSuccess() {
|
||||
toast.success(__('Program deleted successfully'))
|
||||
close()
|
||||
closeDialog()
|
||||
},
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
closeDialog()
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -567,7 +589,7 @@ const courseColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Title',
|
||||
key: 'course_title',
|
||||
key: props.programName === 'new' ? 'course' : 'course_title',
|
||||
width: 1,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
categoryColumn: 'category',
|
||||
valueColumn: 'count',
|
||||
colors: [
|
||||
theme.colors.red['400'],
|
||||
theme.colors.amber['400'],
|
||||
theme.colors.pink['400'],
|
||||
theme.colors.blue['400'],
|
||||
theme.colors.green['400'],
|
||||
getColor('red', 400),
|
||||
getColor('amber', 400),
|
||||
getColor('pink', 400),
|
||||
getColor('blue', 400),
|
||||
getColor('green', 400),
|
||||
],
|
||||
}"
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import type { ProgramMember } from '@/types'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { getColor } from '@/utils'
|
||||
|
||||
const show = defineModel<boolean>({ default: false })
|
||||
const searchFilter = ref<string | null>(null)
|
||||
|
||||
@@ -248,6 +248,23 @@ const routes = [
|
||||
name: 'Search',
|
||||
component: () => import('@/pages/Search/Search.vue'),
|
||||
},
|
||||
{
|
||||
path: '/data-import',
|
||||
name: 'DataImportList',
|
||||
component: () => import('@/pages/DataImport.vue'),
|
||||
},
|
||||
{
|
||||
path: '/data-import/doctype/:doctype',
|
||||
name: 'NewDataImport',
|
||||
component: () => import('@/pages/DataImport.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/data-import/:importName',
|
||||
name: 'DataImport',
|
||||
component: () => import('@/pages/DataImport.vue'),
|
||||
props: true,
|
||||
},
|
||||
]
|
||||
|
||||
let router = createRouter({
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { io } from 'socket.io-client'
|
||||
import { socketio_port } from '../../../../sites/common_site_config.json'
|
||||
import { getCachedListResource } from 'frappe-ui/src/resources/listResource'
|
||||
import { getCachedResource } from 'frappe-ui/src/resources/resources'
|
||||
|
||||
export function initSocket() {
|
||||
let host = window.location.hostname
|
||||
@@ -14,15 +12,5 @@ export function initSocket() {
|
||||
withCredentials: true,
|
||||
reconnectionAttempts: 5,
|
||||
})
|
||||
socket.on('refetch_resource', (data) => {
|
||||
if (data.cache_key) {
|
||||
let resource =
|
||||
getCachedResource(data.cache_key) ||
|
||||
getCachedListResource(data.cache_key)
|
||||
if (resource) {
|
||||
resource.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
return socket
|
||||
}
|
||||
|
||||
499
frontend/src/utils/frappe-ui-colors.json
Normal file
499
frontend/src/utils/frappe-ui-colors.json
Normal file
@@ -0,0 +1,499 @@
|
||||
{
|
||||
"lightMode": {
|
||||
"gray": {
|
||||
"50": "#F8F8F8",
|
||||
"100": "#F3F3F3",
|
||||
"200": "#EDEDED",
|
||||
"300": "#E2E2E2",
|
||||
"400": "#C7C7C7",
|
||||
"500": "#999999",
|
||||
"600": "#7C7C7C",
|
||||
"700": "#525252",
|
||||
"800": "#383838",
|
||||
"900": "#171717"
|
||||
},
|
||||
"blue": {
|
||||
"50": "#F2F9FF",
|
||||
"100": "#E6F4FF",
|
||||
"200": "#C8E6FF",
|
||||
"300": "#A7D7FD",
|
||||
"400": "#73BBF6",
|
||||
"500": "#0289F7",
|
||||
"600": "#007BE0",
|
||||
"700": "#0070CC",
|
||||
"800": "#005CA3",
|
||||
"900": "#004880"
|
||||
},
|
||||
"green": {
|
||||
"50": "#F2FDF4",
|
||||
"100": "#E4FAEB",
|
||||
"200": "#C3F9D3",
|
||||
"300": "#A6EFC0",
|
||||
"400": "#86E0A8",
|
||||
"500": "#46B37E",
|
||||
"600": "#278F5E",
|
||||
"700": "#137949",
|
||||
"800": "#075E35",
|
||||
"900": "#173B2C"
|
||||
},
|
||||
"red": {
|
||||
"50": "#FFF7F7",
|
||||
"100": "#FFE7E7",
|
||||
"200": "#FFD8D8",
|
||||
"300": "#FDC2C2",
|
||||
"400": "#F79596",
|
||||
"500": "#E03636",
|
||||
"600": "#CC2929",
|
||||
"700": "#B52A2A",
|
||||
"800": "#941F1F",
|
||||
"900": "#6B1515"
|
||||
},
|
||||
"amber": {
|
||||
"50": "#FDFAED",
|
||||
"100": "#FFF7D3",
|
||||
"200": "#FEEDA9",
|
||||
"300": "#FBDB73",
|
||||
"400": "#FBCC55",
|
||||
"500": "#E79913",
|
||||
"600": "#DB7706",
|
||||
"700": "#B35309",
|
||||
"800": "#91400D",
|
||||
"900": "#763813"
|
||||
},
|
||||
"orange": {
|
||||
"50": "#FFF9F5",
|
||||
"100": "#FFEFE4",
|
||||
"200": "#FFDEC5",
|
||||
"300": "#FFCBA3",
|
||||
"400": "#F4B07F",
|
||||
"500": "#E86C13",
|
||||
"600": "#D45A08",
|
||||
"700": "#BD3E0C",
|
||||
"800": "#9E3513",
|
||||
"900": "#6B2711"
|
||||
},
|
||||
"yellow": {
|
||||
"50": "#FFFCEF",
|
||||
"100": "#FFF7D3",
|
||||
"200": "#F7E9A8",
|
||||
"300": "#F5E171",
|
||||
"400": "#F2D14B",
|
||||
"500": "#EDBA13",
|
||||
"600": "#D1930D",
|
||||
"700": "#AB6E05",
|
||||
"800": "#8C5600",
|
||||
"900": "#733F12"
|
||||
},
|
||||
"teal": {
|
||||
"50": "#F0FDFA",
|
||||
"100": "#E6F7F4",
|
||||
"200": "#BAE8E1",
|
||||
"300": "#97DED4",
|
||||
"400": "#73D1C4",
|
||||
"500": "#36BAAD",
|
||||
"600": "#0B9E92",
|
||||
"700": "#0F736B",
|
||||
"800": "#115C57",
|
||||
"900": "#114541"
|
||||
},
|
||||
"cyan": {
|
||||
"50": "#F5FBFC",
|
||||
"100": "#DDF7FF",
|
||||
"200": "#B3E8F7",
|
||||
"300": "#99E2F8",
|
||||
"400": "#72D5F3",
|
||||
"500": "#3BBDE5",
|
||||
"600": "#32A4C7",
|
||||
"700": "#267A94",
|
||||
"800": "#125C73",
|
||||
"900": "#164759"
|
||||
},
|
||||
"purple": {
|
||||
"50": "#FDFAFF",
|
||||
"100": "#F6E9FF",
|
||||
"200": "#ECD3FF",
|
||||
"300": "#E2B9FC",
|
||||
"400": "#CFA1F2",
|
||||
"500": "#9C45E3",
|
||||
"600": "#8642C2",
|
||||
"700": "#6E399D",
|
||||
"800": "#5C2F83",
|
||||
"900": "#401863"
|
||||
},
|
||||
"pink": {
|
||||
"50": "#FFF7FC",
|
||||
"100": "#FDE8F5",
|
||||
"200": "#FFD5F0",
|
||||
"300": "#F9B9E0",
|
||||
"400": "#F6A7D6",
|
||||
"500": "#E34AA6",
|
||||
"600": "#CF3A96",
|
||||
"700": "#9C2671",
|
||||
"800": "#801458",
|
||||
"900": "#570F3E"
|
||||
},
|
||||
"violet": {
|
||||
"50": "#FBFAFF",
|
||||
"100": "#F0EBFF",
|
||||
"200": "#DBD5FF",
|
||||
"300": "#C9BAFB",
|
||||
"400": "#B3A1F5",
|
||||
"500": "#6846E3",
|
||||
"600": "#5F46C7",
|
||||
"700": "#4F3DA1",
|
||||
"800": "#392980",
|
||||
"900": "#251959"
|
||||
}
|
||||
},
|
||||
"darkMode": {
|
||||
"gray": {
|
||||
"50": "#F8F8F8",
|
||||
"100": "#D4D4D4",
|
||||
"200": "#AFAFAF",
|
||||
"250": "#999999",
|
||||
"300": "#808080",
|
||||
"400": "#717171",
|
||||
"500": "#424242",
|
||||
"600": "#343434",
|
||||
"650": "#2B2B2B",
|
||||
"700": "#232323",
|
||||
"800": "#1C1C1C",
|
||||
"900": "#0F0F0F"
|
||||
},
|
||||
"blue": {
|
||||
"50": "#C9E0F5",
|
||||
"100": "#ADD2F5",
|
||||
"200": "#8CC1EC",
|
||||
"300": "#5AAEF2",
|
||||
"400": "#3294E3",
|
||||
"500": "#1580D8",
|
||||
"600": "#155999",
|
||||
"700": "#063D71",
|
||||
"800": "#052B53",
|
||||
"900": "#0E2037",
|
||||
"900-80": "#0E2037CC"
|
||||
},
|
||||
"green": {
|
||||
"50": "#C8F3DE",
|
||||
"100": "#9BE6C1",
|
||||
"200": "#78D7A9",
|
||||
"300": "#58C08E",
|
||||
"400": "#1BA964",
|
||||
"500": "#0A9752",
|
||||
"600": "#0F814A",
|
||||
"700": "#035831",
|
||||
"800": "#0A3F27",
|
||||
"900": "#0B2E1C"
|
||||
},
|
||||
"red": {
|
||||
"50": "#FFC1C1",
|
||||
"100": "#FF9595",
|
||||
"200": "#FC7474",
|
||||
"300": "#EB4D52",
|
||||
"400": "#E43838",
|
||||
"500": "#C12020",
|
||||
"600": "#901818",
|
||||
"700": "#681916",
|
||||
"800": "#521515",
|
||||
"900": "#361515",
|
||||
"800-90": "#521515E6",
|
||||
"900-90": "#361515E6"
|
||||
},
|
||||
"amber": {
|
||||
"50": "#F9E8A5",
|
||||
"100": "#F8D16E",
|
||||
"200": "#F0BA31",
|
||||
"300": "#E79913",
|
||||
"400": "#E37D00",
|
||||
"500": "#CB6D10",
|
||||
"600": "#824108",
|
||||
"700": "#603007",
|
||||
"800": "#4B2606",
|
||||
"900": "#371E06"
|
||||
},
|
||||
"orange": {
|
||||
"50": "#FFCDAD",
|
||||
"100": "#FFA873",
|
||||
"200": "#FA8A40",
|
||||
"300": "#DE6D1B",
|
||||
"400": "#C45A0E",
|
||||
"500": "#984509",
|
||||
"600": "#823906",
|
||||
"700": "#683108",
|
||||
"800": "#532707",
|
||||
"900": "#401F07",
|
||||
"900-80": "#401F07CC"
|
||||
},
|
||||
"yellow": {
|
||||
"50": "#FFE89D",
|
||||
"100": "#F8D76A",
|
||||
"200": "#ECC02E",
|
||||
"300": "#DAAE15",
|
||||
"400": "#C69C12",
|
||||
"500": "#9C7A0A",
|
||||
"600": "#705606",
|
||||
"700": "#5B4605",
|
||||
"800": "#3F3004",
|
||||
"900": "#322604"
|
||||
},
|
||||
"teal": {
|
||||
"50": "#93F2E8",
|
||||
"100": "#6EE7DB",
|
||||
"200": "#52DACC",
|
||||
"300": "#3DC6B8",
|
||||
"400": "#219C8F",
|
||||
"500": "#1B7169",
|
||||
"600": "#13564F",
|
||||
"700": "#0C423C",
|
||||
"800": "#0B3A35",
|
||||
"900": "#0A2D29"
|
||||
},
|
||||
"cyan": {
|
||||
"50": "#D0F0FA",
|
||||
"100": "#A0E6F7",
|
||||
"200": "#68D3F3",
|
||||
"300": "#3CB8DC",
|
||||
"400": "#2B8DAB",
|
||||
"500": "#23728B",
|
||||
"600": "#155266",
|
||||
"700": "#0E3B49",
|
||||
"800": "#0D2B36",
|
||||
"900": "#0B252D"
|
||||
},
|
||||
"purple": {
|
||||
"50": "#E5C6FB",
|
||||
"100": "#D9AFF5",
|
||||
"200": "#C993EF",
|
||||
"300": "#B168E8",
|
||||
"400": "#984BD8",
|
||||
"500": "#7A2DB9",
|
||||
"600": "#591F89",
|
||||
"700": "#47176E",
|
||||
"800": "#391457",
|
||||
"900": "#2E1146"
|
||||
},
|
||||
"pink": {
|
||||
"50": "#F6C5DE",
|
||||
"100": "#F69AD1",
|
||||
"200": "#ED77BE",
|
||||
"300": "#E359AB",
|
||||
"400": "#CB4394",
|
||||
"500": "#AC377D",
|
||||
"600": "#822A5F",
|
||||
"700": "#68204B",
|
||||
"800": "#601D46",
|
||||
"900": "#471432",
|
||||
"900-80": "#471432CC"
|
||||
},
|
||||
"violet": {
|
||||
"50": "#DACBF7",
|
||||
"100": "#C4AFEE",
|
||||
"200": "#B398EF",
|
||||
"300": "#9D7CEA",
|
||||
"400": "#8867E8",
|
||||
"500": "#5C3FC2",
|
||||
"600": "#4639A6",
|
||||
"700": "#332978",
|
||||
"800": "#281E5D",
|
||||
"900": "#221C42"
|
||||
}
|
||||
},
|
||||
"overlay": {
|
||||
"white": {
|
||||
"50": "#FFFFFF1A",
|
||||
"100": "#FFFFFF2E",
|
||||
"200": "#FFFFFF45",
|
||||
"300": "#FFFFFF5C",
|
||||
"400": "#FFFFFF73",
|
||||
"500": "#FFFFFF8A",
|
||||
"600": "#FFFFFFA1",
|
||||
"700": "#FFFFFFB8",
|
||||
"800": "#FFFFFFCF",
|
||||
"900": "#FFFFFFE6"
|
||||
},
|
||||
"black": {
|
||||
"50": "#00000017",
|
||||
"100": "#0000002E",
|
||||
"200": "#00000045",
|
||||
"300": "#0000005C",
|
||||
"400": "#00000073",
|
||||
"500": "#0000008A",
|
||||
"600": "#000000A1",
|
||||
"700": "#000000B8",
|
||||
"800": "#000000CF",
|
||||
"900": "#000000E6"
|
||||
}
|
||||
},
|
||||
"neutral": {
|
||||
"white": "#FFFFFF",
|
||||
"black": "#000000"
|
||||
},
|
||||
"themedVariables": {
|
||||
"light": {
|
||||
"outline": {
|
||||
"white": "neutral/white",
|
||||
"gray-1": "lightMode/gray/200",
|
||||
"gray-2": "lightMode/gray/300",
|
||||
"gray-3": "lightMode/gray/400",
|
||||
"gray-4": "lightMode/gray/500",
|
||||
"gray-5": "lightMode/gray/800",
|
||||
"red-1": "lightMode/red/300",
|
||||
"red-2": "lightMode/red/400",
|
||||
"red-3": "lightMode/red/500",
|
||||
"green-1": "lightMode/green/300",
|
||||
"green-2": "lightMode/green/400",
|
||||
"amber-1": "lightMode/amber/300",
|
||||
"amber-2": "lightMode/amber/400",
|
||||
"blue-1": "lightMode/blue/300",
|
||||
"orange-1": "lightMode/orange/400",
|
||||
"gray-modals": "lightMode/gray/200"
|
||||
},
|
||||
"surface": {
|
||||
"white": "neutral/white",
|
||||
"gray-1": "lightMode/gray/50",
|
||||
"gray-2": "lightMode/gray/100",
|
||||
"gray-3": "lightMode/gray/200",
|
||||
"gray-4": "lightMode/gray/300",
|
||||
"gray-5": "lightMode/gray/700",
|
||||
"gray-6": "lightMode/gray/800",
|
||||
"gray-7": "lightMode/gray/900",
|
||||
"red-1": "lightMode/red/50",
|
||||
"red-2": "lightMode/red/100",
|
||||
"red-3": "lightMode/red/200",
|
||||
"red-4": "lightMode/red/300",
|
||||
"red-5": "lightMode/red/600",
|
||||
"red-6": "lightMode/red/700",
|
||||
"red-7": "lightMode/red/800",
|
||||
"green-1": "lightMode/green/50",
|
||||
"green-2": "lightMode/green/100",
|
||||
"green-3": "lightMode/green/600",
|
||||
"amber-1": "lightMode/amber/50",
|
||||
"amber-2": "lightMode/amber/100",
|
||||
"amber-3": "lightMode/amber/600",
|
||||
"blue-1": "lightMode/blue/50",
|
||||
"blue-2": "lightMode/blue/100",
|
||||
"blue-3": "lightMode/blue/600",
|
||||
"orange-1": "lightMode/orange/100",
|
||||
"violet-1": "lightMode/violet/100",
|
||||
"cyan-1": "lightMode/cyan/100",
|
||||
"pink-1": "lightMode/pink/100",
|
||||
"menu-bar": "lightMode/gray/50",
|
||||
"cards": "neutral/white",
|
||||
"modal": "neutral/white",
|
||||
"selected": "neutral/white"
|
||||
},
|
||||
"ink": {
|
||||
"white": "neutral/white",
|
||||
"gray-1": "lightMode/gray/200",
|
||||
"gray-2": "lightMode/gray/300",
|
||||
"gray-3": "lightMode/gray/400",
|
||||
"gray-4": "lightMode/gray/500",
|
||||
"gray-5": "lightMode/gray/600",
|
||||
"gray-6": "lightMode/gray/700",
|
||||
"gray-7": "lightMode/gray/700",
|
||||
"gray-8": "lightMode/gray/800",
|
||||
"gray-9": "lightMode/gray/900",
|
||||
"red-1": "lightMode/red/50",
|
||||
"red-2": "lightMode/red/400",
|
||||
"red-3": "lightMode/red/500",
|
||||
"red-4": "lightMode/red/600",
|
||||
"green-1": "lightMode/green/50",
|
||||
"green-2": "lightMode/green/500",
|
||||
"green-3": "lightMode/green/600",
|
||||
"amber-1": "lightMode/amber/50",
|
||||
"amber-2": "lightMode/amber/500",
|
||||
"amber-3": "lightMode/amber/600",
|
||||
"blue-1": "lightMode/blue/50",
|
||||
"blue-2": "lightMode/blue/500",
|
||||
"blue-3": "lightMode/blue/600",
|
||||
"cyan-1": "lightMode/cyan/500",
|
||||
"pink-1": "lightMode/pink/500",
|
||||
"violet-1": "lightMode/violet/500",
|
||||
"blue-link": "lightMode/blue/400"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"outline": {
|
||||
"white": "darkMode/gray/800",
|
||||
"gray-1": "darkMode/gray/700",
|
||||
"gray-2": "darkMode/gray/600",
|
||||
"gray-3": "darkMode/gray/500",
|
||||
"gray-4": "darkMode/gray/300",
|
||||
"gray-5": "lightMode/gray/200",
|
||||
"red-1": "darkMode/red/800",
|
||||
"red-2": "darkMode/red/700",
|
||||
"red-3": "darkMode/red/600",
|
||||
"green-1": "darkMode/green/800",
|
||||
"green-2": "darkMode/green/700",
|
||||
"amber-1": "darkMode/amber/800",
|
||||
"amber-2": "darkMode/amber/700",
|
||||
"blue-1": "darkMode/blue/800",
|
||||
"orange-1": "darkMode/orange/700",
|
||||
"gray-modals": "darkMode/gray/600"
|
||||
},
|
||||
"surface": {
|
||||
"white": "darkMode/gray/900",
|
||||
"gray-1": "darkMode/gray/700",
|
||||
"gray-2": "darkMode/gray/650",
|
||||
"gray-3": "darkMode/gray/600",
|
||||
"gray-4": "darkMode/gray/500",
|
||||
"gray-5": "darkMode/gray/200",
|
||||
"gray-6": "darkMode/gray/100",
|
||||
"gray-7": "darkMode/gray/50",
|
||||
"red-1": "darkMode/red/900",
|
||||
"red-2": "darkMode/red/900-90",
|
||||
"red-3": "darkMode/red/800-90",
|
||||
"red-4": "darkMode/red/700",
|
||||
"red-5": "darkMode/red/400",
|
||||
"red-6": "darkMode/red/500",
|
||||
"red-7": "darkMode/red/600",
|
||||
"green-1": "darkMode/green/900",
|
||||
"green-2": "darkMode/green/800",
|
||||
"green-3": "darkMode/green/400",
|
||||
"amber-1": "darkMode/amber/900",
|
||||
"amber-2": "darkMode/amber/800",
|
||||
"amber-3": "darkMode/amber/400",
|
||||
"blue-1": "darkMode/blue/900",
|
||||
"blue-2": "darkMode/blue/800",
|
||||
"blue-3": "darkMode/blue/400",
|
||||
"orange-1": "darkMode/orange/900-80",
|
||||
"violet-1": "darkMode/violet/900",
|
||||
"cyan-1": "darkMode/cyan/900",
|
||||
"pink-1": "darkMode/pink/900-80",
|
||||
"menu-bar": "darkMode/gray/900",
|
||||
"cards": "darkMode/gray/800",
|
||||
"modal": "darkMode/gray/700",
|
||||
"selected": "darkMode/gray/500"
|
||||
},
|
||||
"ink": {
|
||||
"white": "darkMode/gray/900",
|
||||
"gray-1": "darkMode/gray/700",
|
||||
"gray-2": "darkMode/gray/500",
|
||||
"gray-3": "darkMode/gray/400",
|
||||
"gray-4": "darkMode/gray/400",
|
||||
"gray-5": "darkMode/gray/300",
|
||||
"gray-6": "darkMode/gray/250",
|
||||
"gray-7": "darkMode/gray/200",
|
||||
"gray-8": "darkMode/gray/100",
|
||||
"gray-9": "darkMode/gray/50",
|
||||
"red-1": "neutral/white",
|
||||
"red-2": "darkMode/red/700",
|
||||
"red-3": "darkMode/red/400",
|
||||
"red-4": "darkMode/red/200",
|
||||
"green-1": "neutral/white",
|
||||
"green-2": "darkMode/green/400",
|
||||
"green-3": "darkMode/green/300",
|
||||
"amber-1": "neutral/white",
|
||||
"amber-2": "darkMode/amber/400",
|
||||
"amber-3": "darkMode/amber/300",
|
||||
"blue-1": "neutral/white",
|
||||
"blue-2": "darkMode/blue/400",
|
||||
"blue-3": "darkMode/blue/300",
|
||||
"cyan-1": "darkMode/cyan/300",
|
||||
"pink-1": "darkMode/pink/300",
|
||||
"violet-1": "darkMode/violet/300",
|
||||
"blue-link": "darkMode/blue/500"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { call, toast } from 'frappe-ui'
|
||||
import { useTimeAgo } from '@vueuse/core'
|
||||
import { theme } from '@/utils/theme'
|
||||
import colorsJSON from '@/utils/frappe-ui-colors.json'
|
||||
import { Quiz } from '@/utils/quiz'
|
||||
import { Program } from '@/utils/program'
|
||||
import { Assignment } from '@/utils/assignment'
|
||||
@@ -680,7 +680,7 @@ export const getMetaInfo = (type, route, meta) => {
|
||||
|
||||
export const updateMetaInfo = (type, route, meta) => {
|
||||
call('lms.lms.api.update_meta_info', {
|
||||
type: type,
|
||||
meta_type: type,
|
||||
route: route,
|
||||
meta_tags: [
|
||||
{ key: 'description', value: meta.description },
|
||||
@@ -734,10 +734,10 @@ const createHighlightSpan = (color, name, scrollIntoView) => {
|
||||
const span = document.createElement('span')
|
||||
span.className = 'highlighted-text'
|
||||
if (scrollIntoView) {
|
||||
span.style.border = `2px solid ${theme.backgroundColor[color][400]}`
|
||||
span.style.border = `2px solid ${getColor(color, 400)}`
|
||||
span.style.borderRadius = '4px'
|
||||
} else {
|
||||
span.style.backgroundColor = theme.backgroundColor[color][200]
|
||||
span.style.backgroundColor = getColor(color, 200)
|
||||
}
|
||||
span.dataset.name = name
|
||||
return span
|
||||
@@ -810,3 +810,9 @@ export const decodeEntities = (encodedString) => {
|
||||
textarea.innerHTML = encodedString
|
||||
return textarea.value
|
||||
}
|
||||
|
||||
export const getColor = (color, shade) => {
|
||||
let theme =
|
||||
localStorage.getItem('theme') == 'light' ? 'lightMode' : 'darkMode'
|
||||
return colorsJSON[theme][color][shade]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export class Markdown {
|
||||
this.api = api
|
||||
this.data = data || {}
|
||||
this.config = config || {}
|
||||
this.text = data.text || ''
|
||||
this.readOnly = readOnly
|
||||
this.text = data.text || ''
|
||||
this.placeholder = __("Type '/' for commands or select text to format")
|
||||
}
|
||||
|
||||
@@ -30,65 +30,28 @@ export class Markdown {
|
||||
|
||||
const div = document.createElement('div')
|
||||
app.mount(div)
|
||||
return {
|
||||
title: '',
|
||||
icon: div.innerHTML,
|
||||
}
|
||||
}
|
||||
|
||||
onPaste(event) {
|
||||
const data = {
|
||||
text: event.detail.data.innerHTML,
|
||||
}
|
||||
|
||||
this.data = data
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!this.wrapper) {
|
||||
return
|
||||
}
|
||||
this.wrapper.innerHTML = this.data.text || ''
|
||||
})
|
||||
return { title: '', icon: div.innerHTML }
|
||||
}
|
||||
|
||||
static get pasteConfig() {
|
||||
return {
|
||||
tags: ['P'],
|
||||
}
|
||||
return { tags: ['P'] }
|
||||
}
|
||||
|
||||
render() {
|
||||
this.wrapper = document.createElement('div')
|
||||
this.wrapper.classList.add('cdx-block', 'ce-paragraph')
|
||||
this.wrapper.contentEditable = !this.readOnly
|
||||
this.wrapper.dataset.placeholder = this.placeholder
|
||||
this.wrapper.innerHTML = this.text
|
||||
|
||||
if (!this.readOnly) {
|
||||
this.wrapper.contentEditable = true
|
||||
this.wrapper.innerHTML = this.text
|
||||
|
||||
this.wrapper.addEventListener('focus', () =>
|
||||
this._togglePlaceholder()
|
||||
)
|
||||
this.wrapper.addEventListener('blur', () =>
|
||||
this._togglePlaceholder()
|
||||
)
|
||||
|
||||
this.wrapper.addEventListener('input', (event) => {
|
||||
this._togglePlaceholder()
|
||||
let value = event.target.textContent
|
||||
if (event.keyCode === 32 && value.startsWith('#')) {
|
||||
this.convertToHeader(event, value)
|
||||
} else if (event.keyCode == 189) {
|
||||
this.convertBlock('list', {
|
||||
style: 'unordered',
|
||||
})
|
||||
} else if (/^[a-zA-Z]/.test(event.key)) {
|
||||
this.convertBlock('paragraph', {
|
||||
text: value,
|
||||
})
|
||||
} else if (event.keyCode === 13 || event.keyCode === 190) {
|
||||
this.parseContent(event)
|
||||
}
|
||||
})
|
||||
this.wrapper.addEventListener('keydown', (e) => this._onKeyDown(e))
|
||||
}
|
||||
|
||||
return this.wrapper
|
||||
@@ -99,10 +62,9 @@ export class Markdown {
|
||||
'.cdx-block.ce-paragraph[data-placeholder]'
|
||||
)
|
||||
blocks.forEach((block) => {
|
||||
if (block !== this.wrapper) {
|
||||
delete block.dataset.placeholder
|
||||
}
|
||||
if (block !== this.wrapper) delete block.dataset.placeholder
|
||||
})
|
||||
|
||||
if (this.wrapper.innerHTML.trim() === '') {
|
||||
this.wrapper.dataset.placeholder = this.placeholder
|
||||
} else {
|
||||
@@ -110,102 +72,107 @@ export class Markdown {
|
||||
}
|
||||
}
|
||||
|
||||
convertToHeader(event, value) {
|
||||
event.preventDefault()
|
||||
if (['#', '##', '###', '####', '#####', '######'].includes(value)) {
|
||||
let level = value.length
|
||||
event.target.textContent = ''
|
||||
this.convertBlock('header', {
|
||||
level: level,
|
||||
})
|
||||
}
|
||||
}
|
||||
_onKeyDown(event) {
|
||||
const text = this.wrapper.textContent
|
||||
|
||||
parseContent(event) {
|
||||
event.preventDefault()
|
||||
let previousLine = this.wrapper.textContent
|
||||
if (event.keyCode === 190) {
|
||||
previousLine = previousLine + '.'
|
||||
}
|
||||
|
||||
if (previousLine && this.hasImage(previousLine)) {
|
||||
if (event.key === ' ' && /^#{1,6}$/.test(text)) {
|
||||
event.preventDefault()
|
||||
const level = text.length
|
||||
this.wrapper.textContent = ''
|
||||
this.convertBlock('image')
|
||||
} else if (previousLine && this.hasLink(previousLine)) {
|
||||
const { text, url } = this.extractLink(previousLine)
|
||||
const anchorTag = `<a href="${url}" target="_blank">${text}</a>`
|
||||
this.convertBlock('paragraph', {
|
||||
text: previousLine.replace(/\[.+?\]\(.+?\)/, anchorTag),
|
||||
})
|
||||
} else if (previousLine && previousLine.startsWith('- ')) {
|
||||
this.convertBlock('list', {
|
||||
this._convertBlock('header', { level })
|
||||
} else if (event.key === ' ' && text === '-') {
|
||||
event.preventDefault()
|
||||
this.wrapper.textContent = ''
|
||||
this._convertBlock('list', {
|
||||
style: 'unordered',
|
||||
items: [
|
||||
{
|
||||
content: previousLine.replace('- ', ''),
|
||||
},
|
||||
],
|
||||
items: [{ content: '' }],
|
||||
})
|
||||
} else if (previousLine && previousLine.startsWith('1.')) {
|
||||
this.convertBlock('list', {
|
||||
style: 'ordered',
|
||||
items: [
|
||||
{
|
||||
content: previousLine.replace('1.', ''),
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if (previousLine && this.canBeEmbed(previousLine)) {
|
||||
} else if (event.key === ' ' && /^1\.$/.test(text)) {
|
||||
event.preventDefault()
|
||||
this.wrapper.textContent = ''
|
||||
this.convertBlock('embed', {
|
||||
source: previousLine,
|
||||
this._convertBlock('list', {
|
||||
style: 'ordered',
|
||||
items: [{ content: '' }],
|
||||
})
|
||||
} else {
|
||||
this.convertBlock('paragraph', {
|
||||
text: previousLine,
|
||||
} else if (this._isEmbed(text) && event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
this.wrapper.textContent = ''
|
||||
this._convertBlock('embed', { source: text })
|
||||
} else if (event.key === 'Enter') {
|
||||
setTimeout(() => this._checkMarkdownAfterEnter(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
_checkMarkdownAfterEnter() {
|
||||
const text = this.wrapper.textContent.trim()
|
||||
|
||||
if (this._isImage(text)) {
|
||||
this._convertBlock('image', {
|
||||
file: { url: this._extractImage(text).url },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async convertBlock(type, data, index = null) {
|
||||
async _convertBlock(type, data) {
|
||||
const currentIndex = this.api.blocks.getCurrentBlockIndex()
|
||||
const currentBlock = this.api.blocks.getBlockByIndex(currentIndex)
|
||||
|
||||
if (!currentBlock) return
|
||||
|
||||
await this.api.blocks.convert(currentBlock.id, type, data)
|
||||
this.api.caret.focus(true)
|
||||
|
||||
setTimeout(() => {
|
||||
const newIndex = this.api.blocks.getCurrentBlockIndex()
|
||||
const newBlock = this.api.blocks.getBlockByIndex(newIndex)
|
||||
|
||||
if (newBlock && newBlock.holder) {
|
||||
const holder = newBlock.holder.querySelector(
|
||||
'[contenteditable="true"]'
|
||||
)
|
||||
if (holder) {
|
||||
holder.focus()
|
||||
// Place caret at end
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(holder)
|
||||
range.collapse(false)
|
||||
const sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
} else {
|
||||
this.api.caret.focus(true)
|
||||
}
|
||||
} else {
|
||||
this.api.caret.focus(true)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
save(blockContent) {
|
||||
return {
|
||||
text: blockContent.innerHTML,
|
||||
}
|
||||
return { text: blockContent.innerHTML }
|
||||
}
|
||||
|
||||
hasImage(line) {
|
||||
return /!\[.+?\]\(.+?\)/.test(line)
|
||||
_isImage(text) {
|
||||
return /!\[.+?\]\(.+?\)/.test(text)
|
||||
}
|
||||
|
||||
extractImage(line) {
|
||||
const match = line.match(/!\[(.+?)\]\((.+?)\)/)
|
||||
if (match) {
|
||||
return { alt: match[1], url: match[2] }
|
||||
}
|
||||
_extractImage(text) {
|
||||
const match = text.match(/!\[(.+?)\]\((.+?)\)/)
|
||||
if (match) return { alt: match[1], url: match[2] }
|
||||
return { alt: '', url: '' }
|
||||
}
|
||||
|
||||
hasLink(line) {
|
||||
return /\[.+?\]\(.+?\)/.test(line)
|
||||
_isLink(text) {
|
||||
return /\[.+?\]\(.+?\)/.test(text)
|
||||
}
|
||||
|
||||
extractLink(line) {
|
||||
const match = line.match(/\[(.+?)\]\((.+?)\)/)
|
||||
if (match) {
|
||||
return { text: match[1], url: match[2] }
|
||||
}
|
||||
_extractLink(text) {
|
||||
const match = text.match(/\[(.+?)\]\((.+?)\)/)
|
||||
if (match) return { text: match[1], url: match[2] }
|
||||
return { text: '', url: '' }
|
||||
}
|
||||
|
||||
canBeEmbed(line) {
|
||||
return /^https?:\/\/.+/.test(line.trim())
|
||||
_isEmbed(text) {
|
||||
return /^https?:\/\/.+/.test(text.trim())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import resolveConfig from 'tailwindcss/resolveConfig'
|
||||
import tailwindConfig from 'tailwind.config.js'
|
||||
|
||||
export const config = resolveConfig(tailwindConfig)
|
||||
export const theme = config.theme
|
||||
Reference in New Issue
Block a user