Merge pull request #511 from shariquerik/custom-actions-refactor

This commit is contained in:
Shariq Ansari 2025-01-03 19:07:50 +05:30 committed by GitHub
commit afb71fd759
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 128 additions and 154 deletions

View File

@ -228,6 +228,9 @@ watch(iframeRef, (iframe) => {
iframe.contentWindow.document.querySelector('.email-content') iframe.contentWindow.document.querySelector('.email-content')
let parent = emailContent.closest('html') let parent = emailContent.closest('html')
let theme = document.documentElement.getAttribute('data-theme')
parent.setAttribute('data-theme', theme)
iframe.style.height = parent.offsetHeight + 1 + 'px' iframe.style.height = parent.offsetHeight + 1 + 'px'
let replyCollapsers = emailContent.querySelectorAll('.replyCollapser') let replyCollapsers = emailContent.querySelectorAll('.replyCollapser')

View File

@ -31,7 +31,7 @@
</template> </template>
<script setup> <script setup>
import { computed, h } from 'vue' import { computed } from 'vue'
import { Dropdown } from 'frappe-ui' import { Dropdown } from 'frappe-ui'
import { isMobileView } from '@/composables/settings' import { isMobileView } from '@/composables/settings'

View File

@ -13,7 +13,7 @@
</div> </div>
<div v-else> <div v-else>
<div <div
class="flex flex-col items-center justify-center gap-4 rounded-lg border border-dashed min-h-64 text-ink-gray-5" class="flex flex-col items-center justify-center gap-4 rounded-lg border border-dashed border-outline-gray-modals min-h-64 text-ink-gray-5"
@dragover.prevent="dragover" @dragover.prevent="dragover"
@dragleave.prevent="dragleave" @dragleave.prevent="dragleave"
@drop.prevent="dropfiles" @drop.prevent="dropfiles"
@ -128,7 +128,7 @@ import FileAudioIcon from '@/components/Icons/FileAudioIcon.vue'
import FileVideoIcon from '@/components/Icons/FileVideoIcon.vue' import FileVideoIcon from '@/components/Icons/FileVideoIcon.vue'
import { createToast, formatDate, convertSize } from '@/utils' import { createToast, formatDate, convertSize } from '@/utils'
import { FormControl, CircularProgressBar, createResource } from 'frappe-ui' import { FormControl, CircularProgressBar, createResource } from 'frappe-ui'
import { ref, onMounted } from 'vue' import { ref, onMounted, watch, onUnmounted } from 'vue'
const props = defineProps({ const props = defineProps({
doctype: { doctype: {
@ -383,6 +383,12 @@ function fileIcon(type) {
return FileTextIcon return FileTextIcon
} }
watch(showCamera, (value) => {
if (!value) stopStream()
})
onUnmounted(() => stopStream())
defineExpose({ defineExpose({
showFileBrowser, showFileBrowser,
showWebLink, showWebLink,

View File

@ -22,9 +22,7 @@
size="sm" size="sm"
class="hover:!bg-surface-gray-2" class="hover:!bg-surface-gray-2"
> >
<IndicatorIcon <IndicatorIcon :class="parseColor(column.column.color)" />
:class="colorClasses(column.column.color, true)"
/>
</Button> </Button>
</template> </template>
<template #body="{ close }"> <template #body="{ close }">
@ -33,13 +31,12 @@
> >
<div class="flex gap-1"> <div class="flex gap-1">
<Button <Button
:class="colorClasses(color)"
variant="ghost" variant="ghost"
v-for="color in colors" v-for="color in colors"
:key="color" :key="color"
@click="() => (column.column.color = color)" @click="() => (column.column.color = color)"
> >
<IndicatorIcon /> <IndicatorIcon :class="parseColor(color)" />
</Button> </Button>
</div> </div>
<div class="flex flex-row-reverse"> <div class="flex flex-row-reverse">
@ -172,7 +169,7 @@
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue' import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
import NestedPopover from '@/components/NestedPopover.vue' import NestedPopover from '@/components/NestedPopover.vue'
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import { isTouchScreenDevice } from '@/utils' import { isTouchScreenDevice, colors, parseColor } from '@/utils'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { Dropdown } from 'frappe-ui' import { Dropdown } from 'frappe-ui'
import { computed } from 'vue' import { computed } from 'vue'
@ -265,33 +262,4 @@ function updateColumn(d) {
emit('update', data) emit('update', data)
} }
function colorClasses(color, onlyIcon = false) {
let textColor = `!text-${color}-600`
if (color == 'black') {
textColor = '!text-ink-gray-9'
} else if (['gray', 'green'].includes(color)) {
textColor = `!text-${color}-700`
}
let bgColor = `!bg-${color}-100 hover:!bg-${color}-200 active:!bg-${color}-300`
return [textColor, onlyIcon ? '' : bgColor]
}
const colors = [
'gray',
'blue',
'green',
'red',
'pink',
'orange',
'amber',
'yellow',
'cyan',
'teal',
'violet',
'purple',
'black',
]
</script> </script>

View File

@ -163,7 +163,7 @@ const tabs = createResource({
if (field.fieldname == 'status') { if (field.fieldname == 'status') {
field.fieldtype = 'Select' field.fieldtype = 'Select'
field.options = dealStatuses.value field.options = dealStatuses.value
field.prefix = getDealStatus(deal.status).iconColorClass field.prefix = getDealStatus(deal.status).color
} }
if (field.fieldtype === 'Table') { if (field.fieldtype === 'Table') {

View File

@ -76,7 +76,7 @@ const tabs = createResource({
if (field.fieldname == 'status') { if (field.fieldname == 'status') {
field.fieldtype = 'Select' field.fieldtype = 'Select'
field.options = leadStatuses.value field.options = leadStatuses.value
field.prefix = getLeadStatus(lead.status).iconColorClass field.prefix = getLeadStatus(lead.status).color
} }
if (field.fieldtype === 'Table') { if (field.fieldtype === 'Table') {

View File

@ -582,7 +582,7 @@ function getDealRowObject(deal) {
annual_revenue: getFormattedCurrency('annual_revenue', deal), annual_revenue: getFormattedCurrency('annual_revenue', deal),
status: { status: {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.color,
}, },
email: deal.email, email: deal.email,
mobile_no: deal.mobile_no, mobile_no: deal.mobile_no,

View File

@ -8,20 +8,22 @@
</Breadcrumbs> </Breadcrumbs>
</template> </template>
<template #right-header> <template #right-header>
<CustomActions v-if="customActions" :actions="customActions" /> <CustomActions
v-if="deal.data._customActions?.length"
:actions="deal.data._customActions"
/>
<AssignTo <AssignTo
v-model="deal.data._assignedTo" v-model="deal.data._assignedTo"
:data="deal.data" :data="deal.data"
doctype="CRM Deal" doctype="CRM Deal"
/> />
<Dropdown :options="statusOptions('deal', updateField, customStatuses)"> <Dropdown
:options="statusOptions('deal', updateField, deal.data._customStatuses)"
>
<template #default="{ open }"> <template #default="{ open }">
<Button <Button :label="deal.data.status">
:label="deal.data.status"
:class="getDealStatus(deal.data.status).colorClass"
>
<template #prefix> <template #prefix>
<IndicatorIcon /> <IndicatorIcon :class="getDealStatus(deal.data.status).color" />
</template> </template>
<template #suffix> <template #suffix>
<FeatherIcon <FeatherIcon
@ -317,14 +319,11 @@ const props = defineProps({
}, },
}) })
const customActions = ref([])
const customStatuses = ref([])
const deal = createResource({ const deal = createResource({
url: 'crm.fcrm.doctype.crm_deal.api.get_deal', url: 'crm.fcrm.doctype.crm_deal.api.get_deal',
params: { name: props.dealId }, params: { name: props.dealId },
cache: ['deal', props.dealId], cache: ['deal', props.dealId],
onSuccess: async (data) => { onSuccess: (data) => {
if (data.organization) { if (data.organization) {
organization.update({ organization.update({
params: { doctype: 'CRM Organization', name: data.organization }, params: { doctype: 'CRM Organization', name: data.organization },
@ -332,7 +331,8 @@ const deal = createResource({
organization.fetch() organization.fetch()
} }
let obj = { setupAssignees(deal)
setupCustomizations(deal, {
doc: data, doc: data,
$dialog, $dialog,
$socket, $socket,
@ -346,11 +346,7 @@ const deal = createResource({
sections, sections,
}, },
call, call,
} })
setupAssignees(data)
let customization = await setupCustomizations(data, obj)
customActions.value = customization.actions || []
customStatuses.value = customization.statuses || []
}, },
}) })

View File

@ -362,7 +362,7 @@ function getGroupedByRows(listRows, groupByField, columns) {
if (groupByField.name == 'status') { if (groupByField.name == 'status') {
groupDetail.icon = () => groupDetail.icon = () =>
h(IndicatorIcon, { h(IndicatorIcon, {
class: getDealStatus(option)?.iconColorClass, class: getDealStatus(option)?.color,
}) })
} }
groupedRows.push(groupDetail) groupedRows.push(groupDetail)
@ -421,7 +421,7 @@ function parseRows(rows, columns = []) {
} else if (row == 'status') { } else if (row == 'status') {
_rows[row] = { _rows[row] = {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.color,
} }
} else if (row == 'sla_status') { } else if (row == 'sla_status') {
let value = deal.sla_status let value = deal.sla_status

View File

@ -8,20 +8,24 @@
</Breadcrumbs> </Breadcrumbs>
</template> </template>
<template #right-header> <template #right-header>
<CustomActions v-if="customActions" :actions="customActions" /> <CustomActions
v-if="lead.data._customActions?.length"
:actions="lead.data._customActions"
/>
<AssignTo <AssignTo
v-model="lead.data._assignedTo" v-model="lead.data._assignedTo"
:data="lead.data" :data="lead.data"
doctype="CRM Lead" doctype="CRM Lead"
/> />
<Dropdown :options="statusOptions('lead', updateField, customStatuses)"> <Dropdown
:options="statusOptions('lead', updateField, lead.data._customStatuses)"
>
<template #default="{ open }"> <template #default="{ open }">
<Button <Button :label="lead.data.status">
:label="lead.data.status"
:class="getLeadStatus(lead.data.status).colorClass"
>
<template #prefix> <template #prefix>
<IndicatorIcon /> <IndicatorIcon
:class="getLeadStatus(lead.data.status).color"
/>
</template> </template>
<template #suffix> <template #suffix>
<FeatherIcon <FeatherIcon
@ -329,15 +333,13 @@ const props = defineProps({
}, },
}) })
const customActions = ref([])
const customStatuses = ref([])
const lead = createResource({ const lead = createResource({
url: 'crm.fcrm.doctype.crm_lead.api.get_lead', url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
params: { name: props.leadId }, params: { name: props.leadId },
cache: ['lead', props.leadId], cache: ['lead', props.leadId],
onSuccess: async (data) => { onSuccess: (data) => {
let obj = { setupAssignees(lead)
setupCustomizations(lead, {
doc: data, doc: data,
$dialog, $dialog,
$socket, $socket,
@ -345,16 +347,9 @@ const lead = createResource({
updateField, updateField,
createToast, createToast,
deleteDoc: deleteLead, deleteDoc: deleteLead,
resource: { resource: { lead, sections },
lead,
sections,
},
call, call,
} })
setupAssignees(data)
let customization = await setupCustomizations(data, obj)
customActions.value = customization.actions || []
customStatuses.value = customization.statuses || []
}, },
}) })

View File

@ -382,7 +382,7 @@ function getGroupedByRows(listRows, groupByField, columns) {
if (groupByField.name == 'status') { if (groupByField.name == 'status') {
groupDetail.icon = () => groupDetail.icon = () =>
h(IndicatorIcon, { h(IndicatorIcon, {
class: getLeadStatus(option)?.iconColorClass, class: getLeadStatus(option)?.color,
}) })
} }
groupedRows.push(groupDetail) groupedRows.push(groupDetail)
@ -444,7 +444,7 @@ function parseRows(rows, columns = []) {
} else if (row == 'status') { } else if (row == 'status') {
_rows[row] = { _rows[row] = {
label: lead.status, label: lead.status,
color: getLeadStatus(lead.status)?.iconColorClass, color: getLeadStatus(lead.status)?.color,
} }
} else if (row == 'sla_status') { } else if (row == 'sla_status') {
let value = lead.sla_status let value = lead.sla_status

View File

@ -592,7 +592,7 @@ function getDealRowObject(deal) {
annual_revenue: getFormattedCurrency('annual_revenue', deal), annual_revenue: getFormattedCurrency('annual_revenue', deal),
status: { status: {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.color,
}, },
email: deal.email, email: deal.email,
mobile_no: deal.mobile_no, mobile_no: deal.mobile_no,

View File

@ -9,14 +9,15 @@
</template> </template>
</Breadcrumbs> </Breadcrumbs>
<div class="absolute right-0"> <div class="absolute right-0">
<Dropdown :options="statusOptions('deal', updateField, customStatuses)"> <Dropdown
:options="
statusOptions('deal', updateField, deal.data._customStatuses)
"
>
<template #default="{ open }"> <template #default="{ open }">
<Button <Button :label="deal.data.status">
:label="deal.data.status"
:class="getDealStatus(deal.data.status).colorClass"
>
<template #prefix> <template #prefix>
<IndicatorIcon /> <IndicatorIcon :class="getDealStatus(deal.data.status).color" />
</template> </template>
<template #suffix> <template #suffix>
<FeatherIcon <FeatherIcon
@ -40,7 +41,10 @@
doctype="CRM Deal" doctype="CRM Deal"
/> />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<CustomActions v-if="customActions" :actions="customActions" /> <CustomActions
v-if="deal.data._customActions?.length"
:actions="deal.data._customActions"
/>
</div> </div>
</div> </div>
<div v-if="deal.data" class="flex h-full overflow-hidden"> <div v-if="deal.data" class="flex h-full overflow-hidden">
@ -257,14 +261,11 @@ const props = defineProps({
}, },
}) })
const customActions = ref([])
const customStatuses = ref([])
const deal = createResource({ const deal = createResource({
url: 'crm.fcrm.doctype.crm_deal.api.get_deal', url: 'crm.fcrm.doctype.crm_deal.api.get_deal',
params: { name: props.dealId }, params: { name: props.dealId },
cache: ['deal', props.dealId], cache: ['deal', props.dealId],
onSuccess: async (data) => { onSuccess: (data) => {
if (data.organization) { if (data.organization) {
organization.update({ organization.update({
params: { doctype: 'CRM Organization', name: data.organization }, params: { doctype: 'CRM Organization', name: data.organization },
@ -272,7 +273,8 @@ const deal = createResource({
organization.fetch() organization.fetch()
} }
let obj = { setupAssignees(deal)
setupCustomizations(deal, {
doc: data, doc: data,
$dialog, $dialog,
$socket, $socket,
@ -286,11 +288,7 @@ const deal = createResource({
sections, sections,
}, },
call, call,
} })
setupAssignees(data)
let customization = await setupCustomizations(data, obj)
customActions.value = customization.actions || []
customStatuses.value = customization.statuses || []
}, },
}) })

View File

@ -9,14 +9,15 @@
</template> </template>
</Breadcrumbs> </Breadcrumbs>
<div class="absolute right-0"> <div class="absolute right-0">
<Dropdown :options="statusOptions('lead', updateField, customStatuses)"> <Dropdown
:options="
statusOptions('lead', updateField, lead.data._customStatuses)
"
>
<template #default="{ open }"> <template #default="{ open }">
<Button <Button :label="lead.data.status">
:label="lead.data.status"
:class="getLeadStatus(lead.data.status).colorClass"
>
<template #prefix> <template #prefix>
<IndicatorIcon /> <IndicatorIcon :class="getLeadStatus(lead.data.status).color" />
</template> </template>
<template #suffix> <template #suffix>
<FeatherIcon <FeatherIcon
@ -40,7 +41,10 @@
doctype="CRM Lead" doctype="CRM Lead"
/> />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<CustomActions v-if="customActions" :actions="customActions" /> <CustomActions
v-if="lead.data._customActions?.length"
:actions="lead.data._customActions"
/>
<Button <Button
:label="__('Convert')" :label="__('Convert')"
variant="solid" variant="solid"
@ -211,15 +215,13 @@ const props = defineProps({
}, },
}) })
const customActions = ref([])
const customStatuses = ref([])
const lead = createResource({ const lead = createResource({
url: 'crm.fcrm.doctype.crm_lead.api.get_lead', url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
params: { name: props.leadId }, params: { name: props.leadId },
cache: ['lead', props.leadId], cache: ['lead', props.leadId],
onSuccess: async (data) => { onSuccess: (data) => {
let obj = { setupAssignees(lead)
setupCustomizations(lead, {
doc: data, doc: data,
$dialog, $dialog,
$socket, $socket,
@ -232,11 +234,7 @@ const lead = createResource({
sections, sections,
}, },
call, call,
} })
setupAssignees(data)
let customization = await setupCustomizations(data, obj)
customActions.value = customization.actions || []
customStatuses.value = customization.statuses || []
}, },
}) })

View File

@ -457,7 +457,7 @@ function getDealRowObject(deal) {
annual_revenue: getFormattedCurrency('annual_revenue', deal), annual_revenue: getFormattedCurrency('annual_revenue', deal),
status: { status: {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.color,
}, },
email: deal.email, email: deal.email,
mobile_no: deal.mobile_no, mobile_no: deal.mobile_no,

View File

@ -459,7 +459,7 @@ function getDealRowObject(deal) {
annual_revenue: getFormattedCurrency('annual_revenue', deal), annual_revenue: getFormattedCurrency('annual_revenue', deal),
status: { status: {
label: deal.status, label: deal.status,
color: getDealStatus(deal.status)?.iconColorClass, color: getDealStatus(deal.status)?.color,
}, },
email: deal.email, email: deal.email,
mobile_no: deal.mobile_no, mobile_no: deal.mobile_no,

View File

@ -1,5 +1,6 @@
import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue' import IndicatorIcon from '@/components/Icons/IndicatorIcon.vue'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { parseColor } from '@/utils'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { createListResource } from 'frappe-ui' import { createListResource } from 'frappe-ui'
import { reactive, h } from 'vue' import { reactive, h } from 'vue'
@ -18,8 +19,7 @@ export const statusesStore = defineStore('crm-statuses', () => {
auto: true, auto: true,
transform(statuses) { transform(statuses) {
for (let status of statuses) { for (let status of statuses) {
status.colorClass = colorClasses(status.color) status.color = parseColor(status.color)
status.iconColorClass = colorClasses(status.color, true)
leadStatusesByName[status.name] = status leadStatusesByName[status.name] = status
} }
return statuses return statuses
@ -35,8 +35,7 @@ export const statusesStore = defineStore('crm-statuses', () => {
auto: true, auto: true,
transform(statuses) { transform(statuses) {
for (let status of statuses) { for (let status of statuses) {
status.colorClass = colorClasses(status.color) status.color = parseColor(status.color)
status.iconColorClass = colorClasses(status.color, true)
dealStatusesByName[status.name] = status dealStatusesByName[status.name] = status
} }
return statuses return statuses
@ -57,19 +56,6 @@ export const statusesStore = defineStore('crm-statuses', () => {
}, },
}) })
function colorClasses(color, onlyIcon = false) {
let textColor = `!text-${color}-600`
if (color == 'black') {
textColor = '!text-ink-gray-9'
} else if (['gray', 'green'].includes(color)) {
textColor = `!text-${color}-700`
}
let bgColor = `!bg-${color}-100 hover:!bg-${color}-200 active:!bg-${color}-300`
return [textColor, onlyIcon ? '' : bgColor]
}
function getLeadStatus(name) { function getLeadStatus(name) {
if (!name) { if (!name) {
name = leadStatuses.data[0].name name = leadStatuses.data[0].name
@ -107,10 +93,7 @@ export const statusesStore = defineStore('crm-statuses', () => {
options.push({ options.push({
label: statusesByName[status]?.name, label: statusesByName[status]?.name,
value: statusesByName[status]?.name, value: statusesByName[status]?.name,
icon: () => icon: () => h(IndicatorIcon, { class: statusesByName[status]?.color }),
h(IndicatorIcon, {
class: statusesByName[status]?.iconColorClass,
}),
onClick: () => { onClick: () => {
capture('status_changed', { doctype, status }) capture('status_changed', { doctype, status })
action && action('status', statusesByName[status]?.name) action && action('status', statusesByName[status]?.name)

View File

@ -136,41 +136,41 @@ export function validateEmail(email) {
return regExp.test(email) return regExp.test(email)
} }
export function setupAssignees(data) { export function setupAssignees(doc) {
let { getUser } = usersStore() let { getUser } = usersStore()
let assignees = data._assign || [] let assignees = doc.data?._assign || []
data._assignedTo = assignees.map((user) => ({ doc.data._assignedTo = assignees.map((user) => ({
name: user, name: user,
image: getUser(user).user_image, image: getUser(user).user_image,
label: getUser(user).full_name, label: getUser(user).full_name,
})) }))
} }
async function getFromScript(script, obj) { async function getFormScript(script, obj) {
let scriptFn = new Function(script + '\nreturn setupForm')() let scriptFn = new Function(script + '\nreturn setupForm')()
let formScript = await scriptFn(obj) let formScript = await scriptFn(obj)
return formScript || {} return formScript || {}
} }
export async function setupCustomizations(data, obj) { export async function setupCustomizations(doc, obj) {
if (!data._form_script) return [] if (!doc.data?._form_script) return []
let statuses = [] let statuses = []
let actions = [] let actions = []
if (Array.isArray(data._form_script)) { if (Array.isArray(doc.data._form_script)) {
for (let script of data._form_script) { for (let script of doc.data._form_script) {
let _script = await getFromScript(script, obj) let _script = await getFormScript(script, obj)
actions = actions.concat(_script?.actions || []) actions = actions.concat(_script?.actions || [])
statuses = statuses.concat(_script?.statuses || []) statuses = statuses.concat(_script?.statuses || [])
} }
} else { } else {
let _script = await getFromScript(data._form_script, obj) let _script = await getFormScript(doc.data._form_script, obj)
actions = _script?.actions || [] actions = _script?.actions || []
statuses = _script?.statuses || [] statuses = _script?.statuses || []
} }
data._customStatuses = statuses doc.data._customStatuses = statuses
data._customActions = actions doc.data._customActions = actions
return { statuses, actions } return { statuses, actions }
} }
@ -234,6 +234,33 @@ export function copyToClipboard(text) {
} }
} }
export const colors = [
'gray',
'blue',
'green',
'red',
'pink',
'orange',
'amber',
'yellow',
'cyan',
'teal',
'violet',
'purple',
'black',
]
export function parseColor(color) {
let textColor = `!text-${color}-600`
if (color == 'black') {
textColor = '!text-ink-gray-9'
} else if (['gray', 'green'].includes(color)) {
textColor = `!text-${color}-700`
}
return textColor
}
export function isEmoji(str) { export function isEmoji(str) {
const emojiList = gemoji.map((emoji) => emoji.emoji) const emojiList = gemoji.map((emoji) => emoji.emoji)
return emojiList.includes(str) return emojiList.includes(str)
@ -306,7 +333,7 @@ export function isImage(extention) {
) )
} }
export function getRandom(len=4) { export function getRandom(len = 4) {
let text = '' let text = ''
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'