Merge pull request #25 from shariquerik/create-new-from-autocomplete

feat: Create new organization from Autocomplete dropdown in Lead
This commit is contained in:
Shariq Ansari 2023-11-09 11:50:28 +05:30 committed by GitHub
commit cae4517c7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 274 additions and 35 deletions

View File

@ -39,7 +39,7 @@ def get_organizations():
organizations = frappe.qb.get_query(
"CRM Organization",
fields=['name', 'organization_logo', 'website'],
fields=['name', 'organization_name', 'organization_logo', 'website'],
order_by="name asc",
distinct=True,
).run(as_dict=1)

View File

@ -8,7 +8,8 @@
{
label: editMode ? 'Update' : 'Create',
variant: 'solid',
onClick: ({ close }) => updateOrganization(close),
onClick: ({ close }) =>
editMode ? updateOrganization(close) : callInsertDoc(close),
},
],
}"
@ -20,7 +21,7 @@
<TextInput
ref="title"
variant="outline"
v-model="_organization.name"
v-model="_organization.organization_name"
placeholder="Add organization name"
/>
</div>
@ -47,6 +48,13 @@ const props = defineProps({
type: Object,
default: {},
},
options: {
type: Object,
default: {
redirect: true,
afterInsert: () => {},
},
},
})
const show = defineModel()
@ -73,19 +81,14 @@ async function updateOrganization(close) {
return
}
if (editMode.value) {
let name
if (nameChanged) {
name = await callRenameDoc()
}
if (otherFieldChanged) {
name = await callSetValue(values)
}
handleOrganizationUpdate(name)
} else {
await callInsertDoc()
let name
if (nameChanged) {
name = await callRenameDoc()
}
close()
if (otherFieldChanged) {
name = await callSetValue(values)
}
handleOrganizationUpdate({ name }, close)
}
async function callRenameDoc() {
@ -106,25 +109,27 @@ async function callSetValue(values) {
return d.name
}
async function callInsertDoc() {
const d = await call('frappe.client.insert', {
async function callInsertDoc(close) {
const doc = await call('frappe.client.insert', {
doc: {
doctype: 'CRM Organization',
organization_name: _organization.value.name,
organization_name: _organization.value.organization_name,
website: _organization.value.website,
},
})
d.name && handleOrganizationUpdate()
doc.name && handleOrganizationUpdate(doc, close)
}
function handleOrganizationUpdate(name) {
organizations.value.reload()
if (name) {
function handleOrganizationUpdate(doc, close) {
organizations.value?.reload()
if (doc.name && props.options.redirect) {
router.push({
name: 'Organization',
params: { organizationId: name },
params: { organizationId: doc.name },
})
}
close && close()
props.options.afterInsert && props.options.afterInsert(doc)
}
watch(

View File

@ -0,0 +1,204 @@
<template>
<Combobox v-model="selectedValue" nullable v-slot="{ open: isComboboxOpen }">
<Popover class="w-full" v-model:show="showOptions">
<template #target="{ open: openPopover, togglePopover }">
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
<div class="w-full">
<button
class="flex h-7 w-full items-center justify-between gap-2 rounded bg-gray-100 px-2 py-1 transition-colors hover:bg-gray-200 focus:ring-2 focus:ring-gray-400"
:class="{ 'bg-gray-200': isComboboxOpen }"
@click="() => togglePopover()"
>
<div class="flex items-center">
<slot name="prefix" />
<span
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
v-if="selectedValue"
>
{{ displayValue(selectedValue) }}
</span>
<span class="text-base leading-5 text-gray-500" v-else>
{{ placeholder || '' }}
</span>
</div>
<FeatherIcon
name="chevron-down"
class="h-4 w-4 text-gray-600"
aria-hidden="true"
/>
</button>
</div>
</slot>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div class="mt-1 rounded-lg bg-white text-base shadow-2xl">
<div class="relative p-1.5 pb-0">
<ComboboxInput
ref="search"
class="form-input w-full"
type="text"
@change="
(e) => {
query = e.target.value
}
"
:value="query"
autocomplete="off"
placeholder="Search"
/>
<button
class="absolute right-1.5 inline-flex h-7 w-7 items-center justify-center"
@click="selectedValue = null"
>
<FeatherIcon name="x" class="w-4" />
</button>
</div>
<ComboboxOptions
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
static
>
<div
class="mt-1.5"
v-for="group in groups"
:key="group.key"
v-show="group.items.length > 0"
>
<div
v-if="group.group && !group.hideLabel"
class="px-2.5 py-1.5 text-sm font-medium text-gray-500"
>
{{ group.group }}
</div>
<ComboboxOption
as="template"
v-for="option in group.items"
:key="option.value"
:value="option"
v-slot="{ active, selected }"
>
<li
:class="[
'flex items-center rounded px-2.5 py-1.5 text-base',
{ 'bg-gray-100': active },
]"
>
<slot
name="item-prefix"
v-bind="{ active, selected, option }"
/>
{{ option.label }}
</li>
</ComboboxOption>
</div>
<li
v-if="groups.length == 0"
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-gray-600"
>
No results found
</li>
</ComboboxOptions>
<div v-if="slots.footer" class="border-t p-1.5">
<slot
name="footer"
v-bind="{ value: search?.el._value, close }"
></slot>
</div>
</div>
</div>
</template>
</Popover>
</Combobox>
</template>
<script setup>
import {
Combobox,
ComboboxInput,
ComboboxOptions,
ComboboxOption,
} from '@headlessui/vue'
import { Popover, Button, FeatherIcon } from 'frappe-ui'
import { ref, computed, useAttrs, useSlots, watch, nextTick } from 'vue'
const props = defineProps(['modelValue', 'options', 'placeholder'])
const emit = defineEmits(['update:modelValue', 'update:query', 'change'])
const query = ref('')
const showOptions = ref(false)
const search = ref(null)
const attrs = useAttrs()
const slots = useSlots()
const valuePropPassed = computed(() => 'value' in attrs)
const selectedValue = computed({
get() {
return valuePropPassed.value ? attrs.value : props.modelValue
},
set(val) {
query.value = ''
if (val) {
showOptions.value = false
}
emit(valuePropPassed.value ? 'change' : 'update:modelValue', val)
},
})
function close() {
showOptions.value = false
}
const groups = computed(() => {
if (!props.options || props.options.length == 0) return []
let groups = props.options[0]?.group
? props.options
: [{ group: '', items: props.options }]
return groups
.map((group, i) => {
return {
key: i,
group: group.group,
hideLabel: group.hideLabel || false,
items: filterOptions(group.items),
}
})
.filter((group) => group.items.length > 0)
})
function filterOptions(options) {
if (!query.value) {
return options
}
return options.filter((option) => {
let searchTexts = [option.label, option.value]
return searchTexts.some((text) =>
(text || '').toString().toLowerCase().includes(query.value.toLowerCase())
)
})
}
function displayValue(option) {
if (typeof option === 'string') {
let allOptions = groups.value.flatMap((group) => group.items)
let selectedOption = allOptions.find((o) => o.value === option)
return selectedOption?.label || option
}
return option?.label
}
watch(query, (q) => {
emit('update:query', q)
})
watch(showOptions, (val) => {
if (val) {
nextTick(() => {
search.value.el.focus()
})
}
})
</script>

View File

@ -8,7 +8,7 @@
type="autocomplete"
:options="activeAgents"
:value="getUser(lead.data.lead_owner).full_name"
@change="(option) => updateAssignedAgent(option.email)"
@change="(option) => updateField('lead_owner', option.email)"
placeholder="Lead owner"
>
<template #prefix>
@ -197,21 +197,37 @@
"
:debounce="500"
/>
<FormControl
<Autocomplete
v-else-if="field.type === 'link'"
type="autocomplete"
:value="lead.data[field.name]"
:options="field.options"
@change="(e) => field.change(e)"
:placeholder="field.placeholder"
class="form-control"
/>
>
<template #footer="{ value, close }">
<div>
<Button
variant="ghost"
class="w-full !justify-start"
label="Create one"
@click="field.create(value, close)"
>
<template #prefix>
<FeatherIcon name="plus" class="h-4" />
</template>
</Button>
</div>
</template>
</Autocomplete>
<FormControl
v-else-if="field.type === 'user'"
type="autocomplete"
:options="activeAgents"
:value="getUser(lead.data[field.name]).full_name"
@change="(option) => updateAssignedAgent(option.email)"
@change="
(option) => updateField('lead_owner', option.email)
"
class="form-control"
:placeholder="field.placeholder"
>
@ -305,6 +321,14 @@
</div>
</div>
</div>
<OrganizationModal
v-model="showOrganizationModal"
:organization="_organization"
:options="{
redirect: false,
afterInsert: (doc) => updateField('organiation', doc.name),
}"
/>
</template>
<script setup>
import ActivityIcon from '@/components/Icons/ActivityIcon.vue'
@ -320,6 +344,8 @@ import LayoutHeader from '@/components/LayoutHeader.vue'
import Toggler from '@/components/Toggler.vue'
import Activities from '@/components/Activities.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import OrganizationModal from '@/components/Modals/OrganizationModal.vue'
import Autocomplete from '@/components/frappe-ui/Autocomplete.vue'
import {
leadStatuses,
statusDropdownOptions,
@ -366,6 +392,8 @@ const lead = createResource({
})
const reload = ref(false)
const showOrganizationModal = ref(false)
const _organization = ref({})
function updateLead(fieldname, value) {
createResource({
@ -455,9 +483,11 @@ const detailSections = computed(() => {
name: 'organization',
placeholder: 'Select organization',
options: getOrganizationOptions(),
change: (data) => {
lead.data.organization = data.value
updateLead('organization', data.value)
change: (data) => data && updateField('organization', data.value),
create: (value, close) => {
_organization.value.organization_name = value
showOrganizationModal.value = true
close()
},
link: () => {
router.push({
@ -587,9 +617,9 @@ async function createDeal(lead) {
}
}
function updateAssignedAgent(email) {
lead.data.lead_owner = email
updateLead('lead_owner', email)
function updateField(name, value) {
lead.data[name] = value
updateLead(name, value)
}
</script>

View File

@ -29,7 +29,7 @@ export const organizationsStore = defineStore('crm-organizations', () => {
function getOrganizationOptions() {
return [
{ label: '', value: '' },
{ label: '---', value: '' },
...organizations.data?.map((org) => ({
label: org.name,
value: org.name,