feat: List UI and basic select source type UI
This commit is contained in:
parent
7d2ccc2e58
commit
5e5929ef40
@ -36,8 +36,8 @@ class LeadSyncSource(Document):
|
|||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
if self.type == "Facebook" and self.access_token:
|
if self.type == "Facebook" and self.access_token:
|
||||||
fetch_and_store_pages_from_facebook(self.access_token)
|
# fetch_and_store_pages_from_facebook(self.access_token)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def sync_leads(self):
|
def sync_leads(self):
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
@click="handleSelect(s)"
|
@click="handleSelect(s)"
|
||||||
>
|
>
|
||||||
<EmailProviderIcon
|
<EmailProviderIcon
|
||||||
:service-name="s.name"
|
:label="s.name"
|
||||||
:logo="s.icon"
|
:logo="s.icon"
|
||||||
:selected="selectedService?.name === s?.name"
|
:selected="selectedService?.name === s?.name"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<div class="w-fit">
|
<div class="w-fit">
|
||||||
<EmailProviderIcon
|
<EmailProviderIcon
|
||||||
:logo="emailIcon[accountData.service]"
|
:logo="emailIcon[accountData.service]"
|
||||||
:service-name="accountData.service"
|
:label="accountData.service"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- banner for setting up email account -->
|
<!-- banner for setting up email account -->
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
>
|
>
|
||||||
<img :src="logo" class="w-4 h-4" />
|
<img :src="logo" class="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="serviceName" class="text-xs text-center text-ink-gray-6 mt-2">
|
<p v-if="label" class="text-xs text-center text-ink-gray-6 mt-2">
|
||||||
{{ serviceName }}
|
{{ label }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
serviceName: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<NewLeadSyncSource
|
<div class="flex-1 p-6">
|
||||||
v-if="step === 'new-source'"
|
<NewLeadSyncSource
|
||||||
:templateData="source"
|
v-if="step === 'new-source'"
|
||||||
@updateStep="updateStep"
|
:templateData="source"
|
||||||
/>
|
@updateStep="updateStep"
|
||||||
<LeadSyncSources
|
/>
|
||||||
v-else-if="step === 'source-list'"
|
<LeadSyncSources
|
||||||
@updateStep="updateStep"
|
v-else-if="step === 'source-list'"
|
||||||
/>
|
@updateStep="updateStep"
|
||||||
<EditLeadSyncSource
|
/>
|
||||||
v-else-if="step === 'edit-source'"
|
<EditLeadSyncSource
|
||||||
:templateData="source"
|
v-else-if="step === 'edit-source'"
|
||||||
@updateStep="updateStep"
|
:templateData="source"
|
||||||
/>
|
@updateStep="updateStep"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@ -1,8 +1,245 @@
|
|||||||
<template>
|
<template>
|
||||||
<pre>{{ JSON.stringify(sources, null, 2) }}</pre>
|
<!-- <pre>{{ JSON.stringify(sources, null, 2) }}</pre> -->
|
||||||
|
|
||||||
|
<div class="flex h-full flex-col gap-6 text-ink-gray-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex justify-between px-2 pt-2">
|
||||||
|
<div class="flex flex-col gap-1 w-9/12">
|
||||||
|
<h2 class="flex gap-2 text-xl font-semibold leading-none h-5">
|
||||||
|
{{ __('Lead Sources') }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-p-base text-ink-gray-6">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Add, edit, and manage sources for automatic lead syncing to your CRM',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex item-center space-x-2 w-3/12 justify-end">
|
||||||
|
<Button
|
||||||
|
:label="__('New')"
|
||||||
|
icon-left="plus"
|
||||||
|
variant="solid"
|
||||||
|
@click="emit('updateStep', 'new-source')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- loading state -->
|
||||||
|
<div
|
||||||
|
v-if="sources.loading"
|
||||||
|
class="flex mt-28 justify-between w-full h-full"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
:loading="sources.loading"
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full"
|
||||||
|
size="2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div
|
||||||
|
v-if="!sources.loading && !sources.data?.length"
|
||||||
|
class="flex justify-between w-full h-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="text-ink-gray-4 border border-dashed rounded w-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
{{ __('No email sources found') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lead source list -->
|
||||||
|
<div
|
||||||
|
class="flex flex-col overflow-hidden"
|
||||||
|
v-if="!sources.loading && sources.data?.length"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="sources.data?.length > 10"
|
||||||
|
class="flex items-center justify-between mb-4 px-2 pt-0.5"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref="searchRef"
|
||||||
|
v-model="search"
|
||||||
|
:placeholder="__('Search template')"
|
||||||
|
class="w-1/3"
|
||||||
|
:debounce="300"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="search" class="h-4 w-4 text-ink-gray-6" />
|
||||||
|
</template>
|
||||||
|
</TextInput>
|
||||||
|
<FormControl
|
||||||
|
type="select"
|
||||||
|
v-model="currentDoctype"
|
||||||
|
:options="[
|
||||||
|
{ label: __('All'), value: 'All' },
|
||||||
|
{ label: __('Lead'), value: 'CRM Lead' },
|
||||||
|
{ label: __('Deal'), value: 'CRM Deal' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center py-2 px-4 text-sm text-ink-gray-5">
|
||||||
|
<div class="w-4/6">{{ __('Source') }}</div>
|
||||||
|
<div class="w-2/6">{{ __('Enabled') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-px border-t mx-4 border-outline-gray-modals" />
|
||||||
|
<ul class="overflow-y-auto px-2">
|
||||||
|
<template v-for="(source, i) in sourcesList" :key="source.name">
|
||||||
|
<li
|
||||||
|
class="flex items-center justify-between p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
|
||||||
|
@click="() => emit('updateStep', 'edit-source', { ...source })"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col w-4/6 pr-5">
|
||||||
|
<div class="text-p-base font-medium text-ink-gray-7 truncate">
|
||||||
|
{{ source.type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between w-2/6">
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
v-model="source.enabled"
|
||||||
|
@update:model-value="toggleLeadSyncSourceEnabled(source)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
class=""
|
||||||
|
:options="getDropdownOptions(source)"
|
||||||
|
placement="right"
|
||||||
|
:button="{
|
||||||
|
icon: 'more-horizontal',
|
||||||
|
variant: 'ghost',
|
||||||
|
onblur: (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
confirmDelete = false
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<div
|
||||||
|
v-if="sourcesList.length !== i + 1"
|
||||||
|
class="h-px border-t mx-2 border-outline-gray-modals"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- Load More Button -->
|
||||||
|
<div
|
||||||
|
v-if="!sources.loading && sources.hasNextPage"
|
||||||
|
class="flex justify-center"
|
||||||
|
>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
class="mt-3.5 p-2"
|
||||||
|
@click="() => sources.next()"
|
||||||
|
:loading="sources.loading"
|
||||||
|
:label="__('Load More')"
|
||||||
|
icon-left="refresh-cw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import {
|
||||||
|
TextInput,
|
||||||
|
FormControl,
|
||||||
|
Switch,
|
||||||
|
Dropdown,
|
||||||
|
FeatherIcon,
|
||||||
|
toast,
|
||||||
|
} from "frappe-ui";
|
||||||
|
import { ref, computed, inject } from "vue";
|
||||||
|
|
||||||
const sources = inject('sources')
|
const emit = defineEmits(["updateStep"]);
|
||||||
|
|
||||||
|
const sources = inject("sources");
|
||||||
|
|
||||||
|
const search = ref("");
|
||||||
|
// const currentDoctype = ref('All')
|
||||||
|
const confirmDelete = ref(false);
|
||||||
|
|
||||||
|
const sourcesList = computed(() => {
|
||||||
|
let list = sources.data || [];
|
||||||
|
if (search.value) {
|
||||||
|
list = list.filter(
|
||||||
|
(source) =>
|
||||||
|
source.name.toLowerCase().includes(search.value.toLowerCase()) ||
|
||||||
|
source.subject.toLowerCase().includes(search.value.toLowerCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// if (currentDoctype.value !== 'All') {
|
||||||
|
// list = list.filter(
|
||||||
|
// (template) => source.reference_doctype === currentDoctype.value,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleLeadSyncSourceEnabled(source) {
|
||||||
|
sources.setValue.submit(
|
||||||
|
{
|
||||||
|
name: source.name,
|
||||||
|
enabled: source.enabled ? 1 : 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(
|
||||||
|
source.enabled
|
||||||
|
? __('Source enabled successfully')
|
||||||
|
: __('Source disabled successfully'),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.messages[0] || __('Failed to update source'))
|
||||||
|
// Revert the change if there was an error
|
||||||
|
source.enabled = !source.enabled
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteLeadSource(source) {
|
||||||
|
confirmDelete.value = false;
|
||||||
|
sources.delete.submit(source.name, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(__("Lead Sync Source deleted successfully"));
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.messages[0] || __("Failed to delete Lead Sync Source"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDropdownOptions(source) {
|
||||||
|
let options = [
|
||||||
|
{
|
||||||
|
label: __("Duplicate"),
|
||||||
|
icon: "copy",
|
||||||
|
onClick: () => emit("updateStep", "new-source", { ...source }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Delete"),
|
||||||
|
icon: "trash-2",
|
||||||
|
onClick: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
confirmDelete.value = true;
|
||||||
|
},
|
||||||
|
condition: () => !confirmDelete.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Confirm Delete"),
|
||||||
|
icon: "trash-2",
|
||||||
|
theme: "red",
|
||||||
|
onClick: () => deleteLeadSource(source),
|
||||||
|
condition: () => confirmDelete.value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -1 +1,52 @@
|
|||||||
<template></template>
|
<template>
|
||||||
|
<div class="flex flex-col h-full gap-4">
|
||||||
|
<div role="heading" aria-level="1" class="flex flex-col gap-1">
|
||||||
|
<h2 class="text-xl font-semibold text-ink-gray-8">
|
||||||
|
{{ __('Setup Lead Syncing Source') }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-ink-gray-5">
|
||||||
|
{{ __('Choose the type of source you want to configure.') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- supported sources -->
|
||||||
|
<div class="flex flex-wrap items-center">
|
||||||
|
<div
|
||||||
|
v-for="s in supportedSourceTypes"
|
||||||
|
:key="s.name"
|
||||||
|
class="flex flex-col items-center gap-1 mt-4 w-[70px]"
|
||||||
|
@click="handleSelect(s)"
|
||||||
|
>
|
||||||
|
<EmailProviderIcon
|
||||||
|
:label="s.name"
|
||||||
|
:logo="s.icon"
|
||||||
|
:selected="selectedSourceType?.name === s?.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
import { createResource, toast } from 'frappe-ui'
|
||||||
|
import CircleAlert from '~icons/lucide/circle-alert'
|
||||||
|
// import {
|
||||||
|
// customProviderFields,
|
||||||
|
// popularProviderFields,
|
||||||
|
// services,
|
||||||
|
// validateInputs,
|
||||||
|
// incomingOutgoingFields,
|
||||||
|
// } from './emailConfig'
|
||||||
|
import { supportedSourceTypes } from './leadSyncSourceConfig'
|
||||||
|
import EmailProviderIcon from '../EmailProviderIcon.vue'
|
||||||
|
|
||||||
|
|
||||||
|
const selectedSourceType = ref(null)
|
||||||
|
|
||||||
|
function handleSelect(sourceType) {
|
||||||
|
selectedSourceType.value = sourceType
|
||||||
|
// state.service = service.name
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import LogoFacebook from '@/images/facebook.png'
|
||||||
|
|
||||||
|
|
||||||
|
export const supportedSourceTypes = [
|
||||||
|
{
|
||||||
|
name: 'Facebook',
|
||||||
|
icon: LogoFacebook,
|
||||||
|
link: 'https://support.google.com/accounts/answer/185833',
|
||||||
|
custom: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
frontend/src/images/facebook.png
Normal file
BIN
frontend/src/images/facebook.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
Loading…
x
Reference in New Issue
Block a user