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"],
|
"questions": form["questions"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.errprint(form_doc.as_dict())
|
|
||||||
form_doc.insert(ignore_permissions=True)
|
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",
|
"fieldname": "access_token",
|
||||||
"fieldtype": "Password",
|
"fieldtype": "Password",
|
||||||
"label": "Access Token",
|
"label": "Access Token",
|
||||||
"length": 500
|
"length": 500,
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "facebook_page",
|
"fieldname": "facebook_page",
|
||||||
@ -76,13 +77,14 @@
|
|||||||
"fieldname": "background_sync_frequency",
|
"fieldname": "background_sync_frequency",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Background Sync Frequency",
|
"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,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-08 12:23:22.097933",
|
"modified": "2025-10-19 15:07:26.256720",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Lead Syncing",
|
"module": "Lead Syncing",
|
||||||
"name": "Lead Sync Source",
|
"name": "Lead Sync Source",
|
||||||
|
|||||||
@ -4,7 +4,10 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
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):
|
class LeadSyncSource(Document):
|
||||||
@ -16,10 +19,8 @@ class LeadSyncSource(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
access_token: DF.Password | None
|
access_token: DF.Password
|
||||||
background_sync_frequency: DF.Literal[
|
background_sync_frequency: DF.Literal["Every 5 Minutes", "Every 10 Minutes", "Every 15 Minutes", "Hourly", "Daily", "Monthly"]
|
||||||
"Every 5 Minutes", "Every 10 Minutes", "Every 15 Minutes", "Hourly", "Daily", "Monthly"
|
|
||||||
]
|
|
||||||
enabled: DF.Check
|
enabled: DF.Check
|
||||||
facebook_lead_form: DF.Link | None
|
facebook_lead_form: DF.Link | None
|
||||||
facebook_page: DF.Link | None
|
facebook_page: DF.Link | None
|
||||||
@ -45,10 +46,10 @@ class LeadSyncSource(Document):
|
|||||||
if already_active:
|
if already_active:
|
||||||
frappe.throw(frappe._("A lead sync source is already enabled for this Facebook Lead Form!"))
|
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:
|
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
|
# rest of the source types can be added here
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def sync_leads(self):
|
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']
|
LeadsIcon: typeof import('./src/components/Icons/LeadsIcon.vue')['default']
|
||||||
LeadsListView: typeof import('./src/components/ListViews/LeadsListView.vue')['default']
|
LeadsListView: typeof import('./src/components/ListViews/LeadsListView.vue')['default']
|
||||||
LeadSyncSettings: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSettings.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']
|
LeadSyncSourcePage: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSourcePage.vue')['default']
|
||||||
LeadSyncSources: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSources.vue')['default']
|
LeadSyncSources: typeof import('./src/components/Settings/LeadSyncing/LeadSyncSources.vue')['default']
|
||||||
LightningIcon: typeof import('./src/components/Icons/LightningIcon.vue')['default']
|
LightningIcon: typeof import('./src/components/Icons/LightningIcon.vue')['default']
|
||||||
|
|||||||
@ -274,7 +274,8 @@
|
|||||||
<Autocomplete
|
<Autocomplete
|
||||||
v-else-if="field.fieldtype === 'Autocomplete'"
|
v-else-if="field.fieldtype === 'Autocomplete'"
|
||||||
class="text-sm text-ink-gray-8"
|
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)"
|
@change="(v) => fieldChange(typeof v == 'object' ? v.value : v, field, row)"
|
||||||
:options="field.options"
|
:options="field.options"
|
||||||
:placeholder="field.placeholder"
|
: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>
|
<template>
|
||||||
<div class="flex-1 p-6">
|
<div class="flex-1 p-6">
|
||||||
<NewLeadSyncSource
|
<LeadSyncSourceForm
|
||||||
v-if="step === 'new-source'"
|
v-if="step === 'new-source'"
|
||||||
:sourceData="source"
|
:sourceData="source"
|
||||||
@updateStep="updateStep"
|
@updateStep="updateStep"
|
||||||
@ -9,7 +9,7 @@
|
|||||||
v-else-if="step === 'source-list'"
|
v-else-if="step === 'source-list'"
|
||||||
@updateStep="updateStep"
|
@updateStep="updateStep"
|
||||||
/>
|
/>
|
||||||
<EditLeadSyncSource
|
<LeadSyncSourceForm
|
||||||
v-else-if="step === 'edit-source'"
|
v-else-if="step === 'edit-source'"
|
||||||
:sourceData="source"
|
:sourceData="source"
|
||||||
@updateStep="updateStep"
|
@updateStep="updateStep"
|
||||||
@ -18,9 +18,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import EditLeadSyncSource from "./EditLeadSyncSource.vue"
|
|
||||||
import LeadSyncSources from "./LeadSyncSources.vue"
|
import LeadSyncSources from "./LeadSyncSources.vue"
|
||||||
import NewLeadSyncSource from "./NewLeadSyncSource.vue";
|
import LeadSyncSourceForm from "./LeadSyncSourceForm.vue";
|
||||||
|
|
||||||
import { createListResource } from 'frappe-ui'
|
import { createListResource } from 'frappe-ui'
|
||||||
import { provide, ref } from 'vue'
|
import { provide, ref } from 'vue'
|
||||||
|
|||||||
@ -54,31 +54,6 @@
|
|||||||
class="flex flex-col overflow-hidden"
|
class="flex flex-col overflow-hidden"
|
||||||
v-if="!sources.loading && sources.data?.length"
|
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="flex items-center py-2 px-4 text-sm text-ink-gray-5">
|
||||||
<div class="w-4/6">{{ __('Name') }}</div>
|
<div class="w-4/6">{{ __('Name') }}</div>
|
||||||
<div class="w-1/6">{{ __('Source') }}</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 = [
|
export const supportedSourceTypes = [
|
||||||
{
|
{
|
||||||
name: 'Facebook',
|
label: 'Facebook',
|
||||||
|
value: 'Facebook',
|
||||||
icon: LogoFacebook,
|
icon: LogoFacebook,
|
||||||
info: __("You will need a Meta developer account and an access token to sync leads from Facebook. Read more "),
|
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',
|
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({
|
f.options.unshift({
|
||||||
label: '',
|
label: '',
|
||||||
value: '',
|
value: '',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user