refactor: unified new/edit form for sync source
This commit is contained in:
parent
5fc0ca9bf0
commit
872f271eb1
@ -138,6 +138,14 @@ def create_facebook_lead_form_in_db(form: dict, page_id: str) -> None:
|
||||
"questions": form["questions"],
|
||||
}
|
||||
)
|
||||
|
||||
frappe.errprint(form_doc.as_dict())
|
||||
form_doc.insert(ignore_permissions=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pages_with_forms() -> list[dict]:
|
||||
pages = frappe.db.get_all("Facebook Page", fields=["id", "name"])
|
||||
for page in pages:
|
||||
forms = frappe.db.get_all(
|
||||
"Facebook Lead Form", filters={"page": page["id"]}, fields=["id", "name"]
|
||||
)
|
||||
page["forms"] = forms
|
||||
return pages
|
||||
@ -41,7 +41,8 @@
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Password",
|
||||
"label": "Access Token",
|
||||
"length": 500
|
||||
"length": 500,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "facebook_page",
|
||||
@ -76,13 +77,14 @@
|
||||
"fieldname": "background_sync_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Background Sync Frequency",
|
||||
"options": "Every 5 Minutes\nEvery 10 Minutes\nEvery 15 Minutes\nHourly\nDaily\nMonthly"
|
||||
"options": "Every 5 Minutes\nEvery 10 Minutes\nEvery 15 Minutes\nHourly\nDaily\nMonthly",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-08 12:23:22.097933",
|
||||
"modified": "2025-10-19 15:07:26.256720",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Lead Syncing",
|
||||
"name": "Lead Sync Source",
|
||||
|
||||
@ -4,7 +4,10 @@
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from crm.lead_syncing.doctype.lead_sync_source.facebook import sync_leads_from_facebook
|
||||
from crm.lead_syncing.doctype.lead_sync_source.facebook import (
|
||||
fetch_and_store_pages_from_facebook,
|
||||
sync_leads_from_facebook,
|
||||
)
|
||||
|
||||
|
||||
class LeadSyncSource(Document):
|
||||
@ -16,10 +19,8 @@ class LeadSyncSource(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
access_token: DF.Password | None
|
||||
background_sync_frequency: DF.Literal[
|
||||
"Every 5 Minutes", "Every 10 Minutes", "Every 15 Minutes", "Hourly", "Daily", "Monthly"
|
||||
]
|
||||
access_token: DF.Password
|
||||
background_sync_frequency: DF.Literal["Every 5 Minutes", "Every 10 Minutes", "Every 15 Minutes", "Hourly", "Daily", "Monthly"]
|
||||
enabled: DF.Check
|
||||
facebook_lead_form: DF.Link | None
|
||||
facebook_page: DF.Link | None
|
||||
@ -45,10 +46,10 @@ class LeadSyncSource(Document):
|
||||
if already_active:
|
||||
frappe.throw(frappe._("A lead sync source is already enabled for this Facebook Lead Form!"))
|
||||
|
||||
def before_save(self):
|
||||
def before_insert(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)
|
||||
# rest of the source types can be added here
|
||||
|
||||
@frappe.whitelist()
|
||||
def sync_leads(self):
|
||||
|
||||
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@ -180,6 +180,7 @@ declare module 'vue' {
|
||||
LeadsIcon: typeof import('./src/components/Icons/LeadsIcon.vue')['default']
|
||||
LeadsListView: typeof import('./src/components/ListViews/LeadsListView.vue')['default']
|
||||
LeadSyncSettings: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSettings.vue')['default']
|
||||
LeadSyncSourceForm: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSourceForm.vue')['default']
|
||||
LeadSyncSourcePage: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSourcePage.vue')['default']
|
||||
LeadSyncSources: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSources.vue')['default']
|
||||
LightningIcon: typeof import('./src/components/Icons/LightningIcon.vue')['default']
|
||||
|
||||
@ -274,7 +274,8 @@
|
||||
<Autocomplete
|
||||
v-else-if="field.fieldtype === 'Autocomplete'"
|
||||
class="text-sm text-ink-gray-8"
|
||||
v-model="row[field.fieldname]"
|
||||
:modelValue="row[field.fieldname]"
|
||||
@update:modelValue="(v) => row[field.fieldname] = typeof v == 'object' ? v.value : v"
|
||||
@change="(v) => fieldChange(typeof v == 'object' ? v.value : v, field, row)"
|
||||
:options="field.options"
|
||||
:placeholder="field.placeholder"
|
||||
|
||||
@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-6 p-8 text-ink-gray-8">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-1 -ml-4 w-9/12">
|
||||
<Button variant="ghost" icon-left="chevron-left" :label="__(source.name)" size="md"
|
||||
@click="() => emit('updateStep', 'source-list')"
|
||||
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start" />
|
||||
|
||||
<div class="w-fit ml-1">
|
||||
<EmailProviderIcon :logo="sourceIcon[source.type]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex item-center space-x-4 w-3/12 justify-end">
|
||||
<div v-if="leadSyncSourceDoc.doc" class="flex items-center space-x-2">
|
||||
<Switch size="sm" v-model="leadSyncSourceDoc.doc.enabled" />
|
||||
<span class="text-sm text-ink-gray-7">{{ __('Enabled') }}</span>
|
||||
</div>
|
||||
<Button :label="__('Update')" icon-left="edit" variant="solid" :loading="sources.setValue.loading"
|
||||
@click="updateSource" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div v-if="leadSyncSourceDoc.doc">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div v-for="field in fbSourceFields" :key="field.name" class="flex flex-col gap-1">
|
||||
<FormControl v-model="leadSyncSourceDoc.doc[field.name]" :label="field.label" :name="field.name"
|
||||
:type="field.type" :placeholder="field.placeholder" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<Link
|
||||
label="Facebook Page"
|
||||
doctype="Facebook Page"
|
||||
v-model="leadSyncSourceDoc.doc.facebook_page"
|
||||
/>
|
||||
|
||||
<Link
|
||||
label="Facebook Lead Form"
|
||||
doctype="Facebook Lead Form"
|
||||
v-model="leadSyncSourceDoc.doc.facebook_lead_form"
|
||||
:filters="{
|
||||
page: leadSyncSourceDoc.doc.facebook_page
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mapping Table -->
|
||||
<div v-if="formDoc.doc">
|
||||
<Grid
|
||||
v-model="formDoc.doc.questions"
|
||||
v-model:parent="formDoc.doc"
|
||||
doctype="Facebook Lead Form Question"
|
||||
parentDoctype="Facebook Lead Form"
|
||||
parentFieldname="questions"
|
||||
:overrides="{
|
||||
fields: [
|
||||
{'fieldname': 'mapped_to_crm_field', 'options': getCRMLeadFields, 'placeholder': __('Not Synced')}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Switch, createResource } from "frappe-ui";
|
||||
import { useDocument } from "@/data/document";
|
||||
import { computed, inject, onMounted, ref } from "vue";
|
||||
import Grid from "@/components/Controls/Grid.vue";
|
||||
import { fbSourceFields } from "./leadSyncSourceConfig";
|
||||
import { sourceIcon } from "./leadSyncSourceConfig";
|
||||
import EmailProviderIcon from "../EmailProviderIcon.vue";
|
||||
import Link from "@/components/Controls/Link.vue";
|
||||
|
||||
const emit = defineEmits();
|
||||
const props = defineProps({
|
||||
sourceData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const sources = inject("sources");
|
||||
const source = ref({});
|
||||
|
||||
onMounted(() => {
|
||||
source.value = { ...props.sourceData };
|
||||
});
|
||||
|
||||
const { document: leadSyncSourceDoc } = useDocument(
|
||||
"Lead Sync Source",
|
||||
props.sourceData.name,
|
||||
);
|
||||
const { document: formDoc } = useDocument(
|
||||
"Facebook Lead Form",
|
||||
props.sourceData.facebook_lead_form,
|
||||
);
|
||||
|
||||
function updateSource() {
|
||||
leadSyncSourceDoc.save.submit();
|
||||
formDoc.save.submit();
|
||||
}
|
||||
|
||||
const fields = createResource({
|
||||
url: "crm.api.doc.get_fields_meta",
|
||||
params: {
|
||||
doctype: "CRM Lead",
|
||||
as_array: true,
|
||||
},
|
||||
cache: ["fieldsMeta", "CRM Lead"],
|
||||
auto: true,
|
||||
transform: (data) => {
|
||||
let restrictedFields = [
|
||||
"name",
|
||||
"owner",
|
||||
"creation",
|
||||
"modified",
|
||||
"modified_by",
|
||||
"docstatus",
|
||||
"_comments",
|
||||
"_user_tags",
|
||||
"_assign",
|
||||
"_liked_by",
|
||||
];
|
||||
console.log("data", data);
|
||||
return data.filter((field) => !restrictedFields.includes(field.fieldname));
|
||||
},
|
||||
});
|
||||
|
||||
const getCRMLeadFields = computed(() => {
|
||||
if (fields.data) {
|
||||
return fields.data.map((field) => ({
|
||||
label: field.label,
|
||||
value: field.fieldname,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<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 gap-1 -ml-4 w-9/12">
|
||||
<Button
|
||||
variant="ghost"
|
||||
icon-left="chevron-left"
|
||||
:label="isLocal ? __('New Lead Sync Source') : syncSource.name"
|
||||
size="md"
|
||||
@click="() => emit('updateStep', 'source-list')"
|
||||
class="cursor-pointer hover:bg-transparent focus:bg-transparent focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:none active:bg-transparent active:outline-none active:ring-0 active:ring-offset-0 active:text-ink-gray-5 font-semibold text-xl hover:opacity-70 !pr-0 !max-w-96 !justify-start"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex item-center space-x-4 w-3/12 justify-end">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch size="sm" v-model="syncSource.enabled" />
|
||||
<span class="text-sm text-ink-gray-7">{{ __('Enabled') }}</span>
|
||||
</div>
|
||||
<Button
|
||||
:label="isLocal ? __('Create') : __('Update')"
|
||||
icon-left="plus"
|
||||
variant="solid"
|
||||
:loading="sources.setValue.loading || sources.insert.loading || docResource?.loading"
|
||||
@click="createOrUpdateSource"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<FormControl
|
||||
type="text"
|
||||
v-if="isLocal"
|
||||
required="true"
|
||||
v-model="syncSource.name"
|
||||
:label="__('Source Name')"
|
||||
:placeholder="__('Enter Source Name')"
|
||||
/>
|
||||
|
||||
<FormControl
|
||||
type="autocomplete"
|
||||
required="true"
|
||||
v-model="syncSource.type"
|
||||
:options="supportedSourceTypes"
|
||||
:label="__('Source Type')"
|
||||
:placeholder="__('Select Source Type')"
|
||||
>
|
||||
<template v-if="syncSource.type" #prefix>
|
||||
<Avatar
|
||||
size="xs"
|
||||
class="mr-2"
|
||||
:image="syncSource.type.icon"
|
||||
/>
|
||||
</template>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
type="password"
|
||||
required="true"
|
||||
v-model="syncSource.access_token"
|
||||
:label="__('Access Token')"
|
||||
:placeholder="__('Enter Access Token')"
|
||||
/>
|
||||
|
||||
<Link
|
||||
v-if="!isLocal"
|
||||
label="Facebook Page"
|
||||
v-model="syncSource.facebook_page"
|
||||
doctype="Facebook Page"
|
||||
/>
|
||||
|
||||
<Link
|
||||
v-if="!isLocal && syncSource.facebook_page"
|
||||
label="Lead Form"
|
||||
v-model="syncSource.facebook_lead_form"
|
||||
doctype="Facebook Lead Form"
|
||||
:filters="{
|
||||
'page': syncSource.facebook_page
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormControl
|
||||
v-if="fieldsMap.background_sync_frequency"
|
||||
type="select"
|
||||
required="true"
|
||||
:options="fieldsMap.background_sync_frequency.options"
|
||||
v-model="syncSource.background_sync_frequency"
|
||||
:label="__('Background Sync Frequency')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mapping Grid -->
|
||||
<div v-if="syncSource.facebook_lead_form && formDocResource && formDocResource.document?.doc">
|
||||
<Grid
|
||||
v-model="formDocResource.document.doc.questions"
|
||||
v-model:parent="formDocResource.document.doc"
|
||||
doctype="Facebook Lead Form Question"
|
||||
parentDoctype="Facebook Lead Form"
|
||||
parentFieldname="questions"
|
||||
:overrides="{
|
||||
fields: [
|
||||
{'fieldname': 'mapped_to_crm_field', 'options': getCRMLeadFields, 'placeholder': __('Not Synced')}
|
||||
]
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDocument } from "@/data/document";
|
||||
import { onMounted, inject, ref, computed, watch } from "vue";
|
||||
import { supportedSourceTypes } from "./leadSyncSourceConfig";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Switch,
|
||||
Avatar,
|
||||
toast,
|
||||
createResource,
|
||||
} from "frappe-ui";
|
||||
|
||||
import { getMeta } from "@/stores/meta";
|
||||
import Link from "@/components/Controls/Link.vue";
|
||||
import Grid from "@/components/Controls/Grid.vue";
|
||||
|
||||
const props = defineProps({
|
||||
sourceData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["updateStep"]);
|
||||
|
||||
const docResource = ref(null);
|
||||
const formDocResource = ref(null);
|
||||
|
||||
const sourceDoc = computed(() => {
|
||||
if (!docResource.value) return;
|
||||
return docResource.value?.document?.doc;
|
||||
});
|
||||
|
||||
const { meta, getFields } = getMeta("Lead Sync Source");
|
||||
const fields = ref(getFields());
|
||||
|
||||
watch(
|
||||
() => meta.data,
|
||||
() => {
|
||||
fields.value = getFields();
|
||||
},
|
||||
);
|
||||
|
||||
const fieldsMap = computed(() => {
|
||||
if (!fields.value) return {};
|
||||
|
||||
const map = {};
|
||||
for (const field of fields.value) {
|
||||
map[field.fieldname] = field;
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
const sources = inject("sources");
|
||||
const syncSource = ref({
|
||||
name: "",
|
||||
type: "",
|
||||
access_token: "",
|
||||
facebook_page: "",
|
||||
facebook_lead_form: "",
|
||||
enabled: true,
|
||||
background_sync_frequency:
|
||||
fieldsMap.value.background_sync_frequency?.default || "Hourly",
|
||||
});
|
||||
|
||||
const isLocal = ref(true);
|
||||
|
||||
function updateSource(data) {
|
||||
if (formDocResource.value ?? formDocResource.value.document.isDirty)
|
||||
{
|
||||
formDocResource.value.document.save.submit();
|
||||
}
|
||||
|
||||
sources.setValue.submit(
|
||||
{
|
||||
name: syncSource.value.name,
|
||||
...data,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (docResource.value) {
|
||||
docResource.value.document.reload();
|
||||
}
|
||||
|
||||
toast.success(__("Lead Sync Source updated successfully"));
|
||||
},
|
||||
onError(e) {
|
||||
toast.error(e.messages[0] || __("Error updating Lead Sync Source"));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createSource() {
|
||||
sources.insert.submit(
|
||||
{
|
||||
...syncSource.value,
|
||||
type: syncSource.value.type.value,
|
||||
},
|
||||
{
|
||||
onSuccess: (newDoc) => {
|
||||
toast.success(__("Lead Sync Source created successfully"));
|
||||
isLocal.value = false;
|
||||
docResource.value = useDocument("Lead Sync Source", newDoc.name);
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.messages[0] || __("Error creating Lead Sync Source"));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createOrUpdateSource() {
|
||||
if (isLocal.value) {
|
||||
createSource();
|
||||
} else {
|
||||
updateSource({
|
||||
...syncSource.value,
|
||||
type: syncSource.value.type.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.sourceData?.name) {
|
||||
Object.assign(syncSource.value, props.sourceData);
|
||||
isLocal.value = false; // edit form
|
||||
docResource.value = useDocument("Lead Sync Source", props.sourceData.name);
|
||||
}
|
||||
|
||||
if (syncSource.value.facebook_lead_form) {
|
||||
formDocResource.value = useDocument(
|
||||
"Facebook Lead Form",
|
||||
syncSource.value.facebook_lead_form,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sourceDoc.value,
|
||||
(newDoc) => {
|
||||
if (newDoc) {
|
||||
Object.assign(syncSource.value, {
|
||||
...newDoc,
|
||||
type:
|
||||
supportedSourceTypes.find((type) => type.value === newDoc.type) ||
|
||||
newDoc.type,
|
||||
});
|
||||
|
||||
formDocResource.value = useDocument(
|
||||
"Facebook Lead Form",
|
||||
syncSource.value.facebook_lead_form,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => syncSource.value.facebook_page,
|
||||
(_, oldValue) => {
|
||||
if (!oldValue) return; // on mount, the value changes from empty
|
||||
syncSource.value.facebook_lead_form = "";
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => syncSource.value.facebook_lead_form,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
formDocResource.value = useDocument(
|
||||
"Facebook Lead Form",
|
||||
newVal,
|
||||
);
|
||||
} else {
|
||||
formDocResource.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const leadFields = createResource({
|
||||
url: "crm.api.doc.get_fields_meta",
|
||||
params: {
|
||||
doctype: "CRM Lead",
|
||||
as_array: true,
|
||||
},
|
||||
cache: ["fieldsMeta", "CRM Lead"],
|
||||
auto: true,
|
||||
transform: (data) => {
|
||||
let restrictedFields = [
|
||||
"name",
|
||||
"owner",
|
||||
"creation",
|
||||
"modified",
|
||||
"modified_by",
|
||||
"docstatus",
|
||||
"_comments",
|
||||
"_user_tags",
|
||||
"_assign",
|
||||
"_liked_by",
|
||||
];
|
||||
console.log("data", data);
|
||||
return data.filter((field) => !restrictedFields.includes(field.fieldname));
|
||||
},
|
||||
});
|
||||
|
||||
const getCRMLeadFields = computed(() => {
|
||||
if (leadFields.data) {
|
||||
return leadFields.data.map((field) => ({
|
||||
label: field.label,
|
||||
value: field.fieldname,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
</script>
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex-1 p-6">
|
||||
<NewLeadSyncSource
|
||||
<LeadSyncSourceForm
|
||||
v-if="step === 'new-source'"
|
||||
:sourceData="source"
|
||||
@updateStep="updateStep"
|
||||
@ -9,7 +9,7 @@
|
||||
v-else-if="step === 'source-list'"
|
||||
@updateStep="updateStep"
|
||||
/>
|
||||
<EditLeadSyncSource
|
||||
<LeadSyncSourceForm
|
||||
v-else-if="step === 'edit-source'"
|
||||
:sourceData="source"
|
||||
@updateStep="updateStep"
|
||||
@ -18,9 +18,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import EditLeadSyncSource from "./EditLeadSyncSource.vue"
|
||||
import LeadSyncSources from "./LeadSyncSources.vue"
|
||||
import NewLeadSyncSource from "./NewLeadSyncSource.vue";
|
||||
import LeadSyncSourceForm from "./LeadSyncSourceForm.vue";
|
||||
|
||||
import { createListResource } from 'frappe-ui'
|
||||
import { provide, ref } from 'vue'
|
||||
|
||||
@ -54,31 +54,6 @@
|
||||
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">{{ __('Name') }}</div>
|
||||
<div class="w-1/6">{{ __('Source') }}</div>
|
||||
|
||||
@ -1,182 +0,0 @@
|
||||
<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 v-if="selectedSourceType" class="flex flex-col gap-4">
|
||||
<!-- docs -->
|
||||
<div class="flex items-center gap-2 p-2 rounded-md ring-1 ring-outline-gray-3 text-ink-gray-6">
|
||||
<CircleAlert class="w-5 h-6 w-min-5 w-max-5 min-h-5 max-w-5" />
|
||||
<div class="text-xs text-wrap">
|
||||
{{ selectedSourceType.info }}
|
||||
<a :href="selectedSourceType.link" target="_blank" class="underline">
|
||||
{{ __('here') }}
|
||||
</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div v-if="selectedSourceType.name === 'Facebook'" class="flex flex-col gap-4">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div v-for="field in fbSourceFields" :key="field.name" class="flex flex-col gap-1">
|
||||
<FormControl v-model="syncSource[field.name]" :label="field.label" :name="field.name"
|
||||
:type="field.type" :placeholder="field.placeholder" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<div v-if="state.fbAccountPages.length">
|
||||
<FormControl type="autocomplete" :placeholder="__('Select an account page')"
|
||||
:options="state.fbAccountPages" :label="__('Facebook Page')"
|
||||
v-model="syncSource.facebook_page" />
|
||||
</div>
|
||||
|
||||
<div v-if="syncSource.facebook_page">
|
||||
<FormControl type="autocomplete" :placeholder="__('Select a lead gen form')"
|
||||
:options="leadFormsForSelectedPage" :label="__('Lead Form')"
|
||||
v-model="syncSource.facebook_lead_form" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- action button -->
|
||||
<div v-if="selectedSourceType" class="flex justify-between mt-auto">
|
||||
<Button :label="__('Back')" variant="outline" :disabled="sources.insert.loading"
|
||||
@click="emit('updateStep', 'source-list')" />
|
||||
|
||||
<Button v-if="state.fbPagesFetched" :label="__('Create')" variant="solid" :loading="sources.insert.loading"
|
||||
@click="createLeadSyncSource" />
|
||||
|
||||
<Button v-else="state.fbPagesFetched" :label="__('Fetch Account Pages')" variant="solid"
|
||||
:loading="state.fbPagesFetching" @click="getAccountPages(syncSource.access_token)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, inject, onMounted, reactive, watch, computed } from "vue";
|
||||
import { FormControl, toast, call } from "frappe-ui";
|
||||
import CircleAlert from "~icons/lucide/circle-alert";
|
||||
import { supportedSourceTypes, fbSourceFields } from "./leadSyncSourceConfig";
|
||||
import EmailProviderIcon from "../EmailProviderIcon.vue";
|
||||
|
||||
const syncSource = ref({
|
||||
name: "",
|
||||
type: "",
|
||||
access_token: "",
|
||||
facebook_page: "",
|
||||
facebook_lead_form: "",
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
fbPagesFetched: false,
|
||||
fbPagesFetching: false,
|
||||
fbAccountPages: [],
|
||||
});
|
||||
|
||||
const emit = defineEmits();
|
||||
|
||||
const props = defineProps({
|
||||
sourceData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const selectedSourceType = ref(supportedSourceTypes[0]);
|
||||
syncSource.value.type = selectedSourceType.value.name;
|
||||
|
||||
const sources = inject("sources");
|
||||
|
||||
function handleSelect(sourceType) {
|
||||
selectedSourceType.value = sourceType;
|
||||
syncSource.value.type = sourceType.name;
|
||||
}
|
||||
|
||||
function createLeadSyncSource() {
|
||||
sources.insert.submit(
|
||||
{
|
||||
...syncSource.value,
|
||||
facebook_page: syncSource.value.facebook_page.id,
|
||||
facebook_lead_form: syncSource.value.facebook_lead_form.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(__("New Lead Syncing Source created successfully"));
|
||||
emit("updateStep", "edit-source", {
|
||||
...syncSource.value,
|
||||
facebook_page: syncSource.value.facebook_page.id,
|
||||
facebook_lead_form: syncSource.value.facebook_lead_form.id,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.messages[0] || __("Failed to create source"));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const getAccountPages = (access_token) => {
|
||||
state.fbPagesFetching = true;
|
||||
call(
|
||||
"crm.lead_syncing.doctype.lead_sync_source.facebook.fetch_and_store_pages_from_facebook",
|
||||
{ access_token },
|
||||
)
|
||||
.then((data) => {
|
||||
state.fbPagesFetched = true;
|
||||
state.fbAccountPages = data.map((page) => ({
|
||||
label: page.name,
|
||||
value: page.id,
|
||||
...page,
|
||||
}));
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.messages[0] || __("Failed to fetch pages"));
|
||||
})
|
||||
.finally(() => {
|
||||
state.fbPagesFetching = false;
|
||||
});
|
||||
};
|
||||
|
||||
const leadFormsForSelectedPage = computed(() => {
|
||||
if (!state.fbAccountPages || !syncSource.value.facebook_page) {
|
||||
return [];
|
||||
}
|
||||
const selectedPage = state.fbAccountPages.find(
|
||||
(page) => page.id === syncSource.value.facebook_page.id,
|
||||
);
|
||||
return selectedPage.forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
...form,
|
||||
}));
|
||||
});
|
||||
|
||||
watch(syncSource.value.facebook_page, () => {
|
||||
syncSource.value.facebook_lead_form = null;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.sourceData?.name) {
|
||||
Object.assign(syncSource.value, props.sourceData);
|
||||
syncSource.value.name = `${syncSource.value.name} - Copy`;
|
||||
syncSource.value.enabled = true; // Default to enabled
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -3,7 +3,8 @@ import LogoFacebook from '@/images/facebook.png'
|
||||
|
||||
export const supportedSourceTypes = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
label: 'Facebook',
|
||||
value: 'Facebook',
|
||||
icon: LogoFacebook,
|
||||
info: __("You will need a Meta developer account and an access token to sync leads from Facebook. Read more "),
|
||||
link: 'https://www.facebook.com/business/help/503306463479099?id=2190812977867143',
|
||||
|
||||
@ -91,7 +91,7 @@ export function getMeta(doctype) {
|
||||
}
|
||||
})
|
||||
|
||||
if (f.options[0]?.value !== '') {
|
||||
if (f.options[0]?.value !== '' && f.mandatory !== 1) {
|
||||
f.options.unshift({
|
||||
label: '',
|
||||
value: '',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user