feat: List UI and basic select source type UI

This commit is contained in:
Hussain Nagaria 2025-09-30 19:16:22 +05:30
parent 7d2ccc2e58
commit 5e5929ef40
9 changed files with 326 additions and 25 deletions

View File

@ -36,8 +36,8 @@ class LeadSyncSource(Document):
def before_save(self):
if self.type == "Facebook" and self.access_token:
fetch_and_store_pages_from_facebook(self.access_token)
pass
# fetch_and_store_pages_from_facebook(self.access_token)
pass
@frappe.whitelist()
def sync_leads(self):

View File

@ -18,7 +18,7 @@
@click="handleSelect(s)"
>
<EmailProviderIcon
:service-name="s.name"
:label="s.name"
:logo="s.icon"
:selected="selectedService?.name === s?.name"
/>

View File

@ -9,7 +9,7 @@
<div class="w-fit">
<EmailProviderIcon
:logo="emailIcon[accountData.service]"
:service-name="accountData.service"
:label="accountData.service"
/>
</div>
<!-- banner for setting up email account -->

View File

@ -5,8 +5,8 @@
>
<img :src="logo" class="w-4 h-4" />
</div>
<p v-if="serviceName" class="text-xs text-center text-ink-gray-6 mt-2">
{{ serviceName }}
<p v-if="label" class="text-xs text-center text-ink-gray-6 mt-2">
{{ label }}
</p>
</template>
@ -16,7 +16,7 @@ defineProps({
type: String,
required: true,
},
serviceName: {
label: {
type: String,
default: '',
},

View File

@ -1,18 +1,20 @@
<template>
<NewLeadSyncSource
v-if="step === 'new-source'"
:templateData="source"
@updateStep="updateStep"
/>
<LeadSyncSources
v-else-if="step === 'source-list'"
@updateStep="updateStep"
/>
<EditLeadSyncSource
v-else-if="step === 'edit-source'"
:templateData="source"
@updateStep="updateStep"
/>
<div class="flex-1 p-6">
<NewLeadSyncSource
v-if="step === 'new-source'"
:templateData="source"
@updateStep="updateStep"
/>
<LeadSyncSources
v-else-if="step === 'source-list'"
@updateStep="updateStep"
/>
<EditLeadSyncSource
v-else-if="step === 'edit-source'"
:templateData="source"
@updateStep="updateStep"
/>
</div>
</template>
<script setup>

View File

@ -1,8 +1,245 @@
<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>
<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>

View File

@ -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>

View File

@ -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,
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB