import { ref, computed, watch, onMounted, onBeforeUnmount, type Ref, type ComputedRef } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useLabels } from '@/composables/useLabels' import { useApiClient } from '@/composables/useApiClient' import { useFormBuilder } from '@/stores/useFormBuilder' import { useProFeatureUpgrade } from '@/composables/useProFeatureUpgrade' import { useActionEntityStore } from '@/stores/actionEntityStore' import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore' import { useGoogleSheetsConnection } from '@/integrations/google-sheets/composables/useGoogleSheetsConnection' import IvyMessage from '@/views/_components/message/ivyMessage' import type { GoogleSheetIntegration } from '@/integrations/google-sheets/types/google-sheets.types' import type { Field } from '@/types/field.d.ts' const EMPTY_INTEGRATION = (): Partial => ({ spreadsheet_id: '', spreadsheet_name: '', worksheet_name: '', field_mapping: [], enabled: true, smart_logic: { enabled: false, match: 'any', rules: [] }, }) export type GoogleSheetsFormIntegrationsApi = { integrations: Ref isAddingIntegration: Ref editingIntegrationId: Ref loading: Ref initialLoading: Ref formLoading: Ref currentIntegration: Ref> componentKey: ComputedRef maxIntegrations: ComputedRef showLimitNotice: ComputedRef isAddMoreUpgradeMode: ComputedRef isSingleConfigMode: ComputedRef formFields: ComputedRef isGoogleConnected: Ref navigateToGlobalSettings: () => void startAddingIntegration: () => void startEditingIntegration: (integrationId: number) => void cancelEditing: () => void saveIntegration: (integrationData: Partial) => Promise confirmDeleteIntegration: (integrationId?: number) => void handleToggle: (integration: GoogleSheetIntegration) => Promise } /** * Shared form-builder state for Google Sheets integrations (Lite single + Pro multi shell). */ export function useGoogleSheetsFormIntegrations(options?: { maxIntegrations?: number | ComputedRef }): GoogleSheetsFormIntegrationsApi { const { getLabel } = useLabels() const { request } = useApiClient() const formBuilderStore = useFormBuilder() const route = useRoute() const router = useRouter() const { showUpgradeDialog } = useProFeatureUpgrade() const actionEntityStore = useActionEntityStore() const unsavedChangesStore = useUnsavedChangesStore() const { isConnected: isGoogleConnected, checkConnectionStatus } = useGoogleSheetsConnection() unsavedChangesStore.registerEntity('googleSheetsBuilder') const integrations = ref([]) const maxIntegrationsFromApi = ref(null) const isAddingIntegration = ref(false) const editingIntegrationId = ref(null) const loading = ref(false) const initialLoading = ref(true) const formLoading = ref(false) const currentIntegration = ref>(EMPTY_INTEGRATION()) const originalIntegration = ref(null) const componentKey = computed( () => `${String(route.name ?? 'unknown')}-${JSON.stringify(route.params)}`, ) const maxIntegrations = computed(() => { if (maxIntegrationsFromApi.value !== null && maxIntegrationsFromApi.value > 0) { return maxIntegrationsFromApi.value >= Number.MAX_SAFE_INTEGER ? Infinity : maxIntegrationsFromApi.value } if (options?.maxIntegrations === undefined) { return 1 } return typeof options.maxIntegrations === 'number' ? options.maxIntegrations : options.maxIntegrations.value }) const showLimitNotice = computed( () => integrations.value.length >= maxIntegrations.value && maxIntegrations.value !== Infinity, ) const isAddMoreUpgradeMode = computed(() => integrations.value.length >= maxIntegrations.value) /** Lite single: one config — show the form directly instead of a list shell. */ const isSingleConfigMode = computed( () => maxIntegrations.value === 1 && integrations.value.length === 1, ) const formFields = computed((): Field[] => { const fields = formBuilderStore.fields return Array.isArray(fields) ? fields : [] }) const resolveFormId = (): string | null => { const routeFormId = route.params.formId if (routeFormId) { return Array.isArray(routeFormId) ? routeFormId[0] : String(routeFormId) } const pathMatch = route.path.match(/\/manage\/(\d+)/) if (pathMatch?.[1]) { return pathMatch[1] } const hashMatch = window.location.hash.match(/\/manage\/(\d+)/) if (hashMatch?.[1]) { return hashMatch[1] } return formBuilderStore.formId ? String(formBuilderStore.formId) : null } const resetCurrentIntegration = () => { currentIntegration.value = EMPTY_INTEGRATION() } const loadIntegrations = async () => { initialLoading.value = true try { const formId = resolveFormId() if (!formId) { integrations.value = [] return } const { data, error } = await request<{ data?: { integrations?: GoogleSheetIntegration[]; max_integrations?: number } integrations?: GoogleSheetIntegration[] max_integrations?: number }>(`integrations/google-sheets?form_id=${encodeURIComponent(formId)}`, { method: 'GET' }) if (error) { integrations.value = [] maxIntegrationsFromApi.value = null return } const payload = data?.data ?? data integrations.value = payload?.integrations ?? [] maxIntegrationsFromApi.value = typeof payload?.max_integrations === 'number' ? payload.max_integrations : null } finally { initialLoading.value = false } } const navigateToGlobalSettings = () => { window.location.href = 'admin.php?page=ivyforms-settings#/integrations/google_sheets' } const beginEditing = (integration: GoogleSheetIntegration) => { currentIntegration.value = { ...integration } originalIntegration.value = JSON.stringify(integration) unsavedChangesStore.startEditing('googleSheetsBuilder') editingIntegrationId.value = integration.id ?? null isAddingIntegration.value = false } const startAddingIntegration = () => { if (integrations.value.length >= maxIntegrations.value) { showUpgradeDialog('essentials') return } resetCurrentIntegration() originalIntegration.value = JSON.stringify(currentIntegration.value) unsavedChangesStore.startEditing('googleSheetsBuilder') isAddingIntegration.value = true editingIntegrationId.value = null const formId = resolveFormId() if (formId) { router.replace({ path: `/manage/${formId}/settings/integrations/google_sheets/manage` }) } } const isIntegrationEditable = (integrationId: number): boolean => { if (maxIntegrations.value === Infinity) { return true } const rowIndex = integrations.value.findIndex((integration) => integration.id === integrationId) return rowIndex >= 0 && rowIndex < maxIntegrations.value } const startEditingIntegration = (integrationId: number) => { if (!isIntegrationEditable(integrationId)) { showUpgradeDialog('essentials') return } const integration = integrations.value.find((i) => i.id === integrationId) if (!integration) return beginEditing(integration) const formId = resolveFormId() if (formId) { router.push({ path: `/manage/${formId}/settings/integrations/google_sheets/manage/${integrationId}`, }) } } const cancelEditing = () => { const performCancel = async () => { unsavedChangesStore.markClean('googleSheetsBuilder') // Lite single: stay on the one config and reload saved values. if (isSingleConfigMode.value && !isAddingIntegration.value) { const only = integrations.value[0] if (only) { beginEditing(only) return } } isAddingIntegration.value = false editingIntegrationId.value = null unsavedChangesStore.stopEditing('googleSheetsBuilder') resetCurrentIntegration() originalIntegration.value = null const formId = resolveFormId() if (formId) { router.push({ path: `/manage/${formId}/settings/integrations/google_sheets` }) } } if (unsavedChangesStore.isDirty('googleSheetsBuilder')) { unsavedChangesStore.showUnsavedChangesDialog(() => { void performCancel() }) } else { void performCancel() } } const extractApiError = (error: unknown): string => { if ( typeof error === 'object' && error !== null && 'message' in error && typeof (error as { message?: string }).message === 'string' ) { return (error as { message: string }).message } return getLabel('error_saving_google_sheets') } const saveIntegration = async (integrationData: Partial) => { loading.value = true try { const formId = resolveFormId() if (!formId) throw new Error(getLabel('form_id_required')) const payload = { form_id: formId, spreadsheet_id: integrationData.spreadsheet_id, spreadsheet_name: integrationData.spreadsheet_name, worksheet_name: integrationData.worksheet_name, field_mapping: integrationData.field_mapping || [], smart_logic: integrationData.smart_logic || { enabled: false, match: 'any' as const, rules: [], }, enabled: integrationData.enabled ?? true, } const wasEditing = !!editingIntegrationId.value if (editingIntegrationId.value) { const { error } = await request( `integrations/google-sheets/update/${editingIntegrationId.value}`, { method: 'POST', data: payload, }, ) if (error) throw new Error(extractApiError(error)) } else { const { error } = await request('integrations/google-sheets/add', { method: 'POST', data: payload, }) if (error) throw new Error(extractApiError(error)) } unsavedChangesStore.markClean('googleSheetsBuilder') await loadIntegrations() if (maxIntegrations.value === 1 && integrations.value.length === 1) { const only = integrations.value[0] if (only) { beginEditing(only) } } else { cancelEditing() } IvyMessage({ message: getLabel( wasEditing ? 'google_sheets_updated_successfully' : 'google_sheets_created_successfully', ), type: 'success', }) } catch (error) { IvyMessage({ message: error instanceof Error ? error.message : getLabel('error_saving_google_sheets'), type: 'error', }) } finally { loading.value = false } } const confirmDeleteIntegration = (integrationId?: number) => { const id = integrationId ?? editingIntegrationId.value ?? integrations.value[0]?.id if (!id) return actionEntityStore.showDialog({ title: getLabel('delete_google_sheet_title'), subtitle: getLabel('delete_google_sheet_subtitle'), dialogType: 'warning', buttons: { close: { text: getLabel('cancel') }, confirm: { type: 'danger', text: getLabel('delete'), function: async () => { loading.value = true try { const { error } = await request(`integrations/google-sheets/${id}`, { method: 'DELETE', }) if (error) throw new Error('Delete failed') unsavedChangesStore.markClean('googleSheetsBuilder') unsavedChangesStore.stopEditing('googleSheetsBuilder') isAddingIntegration.value = false editingIntegrationId.value = null resetCurrentIntegration() await loadIntegrations() const formId = resolveFormId() if (formId) { router.push({ path: `/manage/${formId}/settings/integrations/google_sheets` }) } IvyMessage({ message: getLabel('google_sheets_deleted_successfully'), type: 'success', }) } catch { IvyMessage({ message: getLabel('error_deleting_google_sheets'), type: 'error' }) } finally { loading.value = false } }, }, }, }) } const handleToggle = async (integration: GoogleSheetIntegration) => { if (!integration.id) return if (!isIntegrationEditable(integration.id)) { showUpgradeDialog('essentials') return } loading.value = true try { const { error } = await request( `integrations/google-sheets/update/status/${integration.id}`, { method: 'POST', }, ) if (error) throw new Error('Toggle failed') await loadIntegrations() } catch { integration.enabled = !integration.enabled IvyMessage({ message: getLabel('error_updating_google_sheets'), type: 'error' }) } finally { loading.value = false } } const openSingleConfigIfNeeded = () => { if (!isSingleConfigMode.value || isAddingIntegration.value) { return } const only = integrations.value[0] if (only?.id && editingIntegrationId.value !== only.id) { beginEditing(only) } } watch( () => [route.name, route.params.integrationId] as const, async ([newRouteName, integrationId]) => { initialLoading.value = true formLoading.value = false if (newRouteName === 'google-sheets-edit' && integrationId) { editingIntegrationId.value = parseInt(integrationId as string) isAddingIntegration.value = false formLoading.value = true await loadIntegrations() const id = parseInt(integrationId as string) const integration = integrations.value.find((i) => i.id === id) if (integration) { beginEditing(integration) } formLoading.value = false } else if (newRouteName === 'google-sheets-manage') { await loadIntegrations() if (integrations.value.length >= maxIntegrations.value) { const formId = resolveFormId() if (formId) { router.replace({ path: `/manage/${formId}/settings/integrations/google_sheets` }) } showUpgradeDialog('essentials') initialLoading.value = false openSingleConfigIfNeeded() return } editingIntegrationId.value = null isAddingIntegration.value = true resetCurrentIntegration() originalIntegration.value = JSON.stringify(currentIntegration.value) unsavedChangesStore.startEditing('googleSheetsBuilder') } else { isAddingIntegration.value = false editingIntegrationId.value = null resetCurrentIntegration() originalIntegration.value = null unsavedChangesStore.stopEditing('googleSheetsBuilder') await loadIntegrations() openSingleConfigIfNeeded() } }, { immediate: true }, ) watch( () => [resolveFormId(), route.params.formId, formBuilderStore.formId] as const, async (formIdTuple) => { const formId = formIdTuple[0] if (!formId) { return } if ( !isAddingIntegration.value && !editingIntegrationId.value && integrations.value.length === 0 ) { await loadIntegrations() openSingleConfigIfNeeded() } }, ) watch( () => currentIntegration.value, () => { if ((isAddingIntegration.value || editingIntegrationId.value) && originalIntegration.value) { const currentState = JSON.stringify(currentIntegration.value) if (currentState !== originalIntegration.value) { unsavedChangesStore.markDirty('googleSheetsBuilder') } else { unsavedChangesStore.markClean('googleSheetsBuilder') } } }, { deep: true }, ) let removeGuard: (() => void) | null = null onMounted(async () => { await checkConnectionStatus() }) removeGuard = router.beforeEach((to, from, next) => { const fromGoogleSheets = from.path.includes('/integrations/google_sheets') const toGoogleSheets = to.path.includes('/integrations/google_sheets') const leaving = fromGoogleSheets && !toGoogleSheets // Browser back from add/edit to the list stays inside google_sheets, so `leaving` is false. const fromEditor = from.path.includes('/integrations/google_sheets/manage') const toEditor = to.path.includes('/integrations/google_sheets/manage') const leavingEditor = fromEditor && !toEditor const isEditing = !!(isAddingIntegration.value || editingIntegrationId.value) if ( (leaving || leavingEditor) && isEditing && unsavedChangesStore.isDirty('googleSheetsBuilder') ) { next(false) unsavedChangesStore.showUnsavedChangesDialog(() => { unsavedChangesStore.markClean('googleSheetsBuilder') unsavedChangesStore.stopEditing('googleSheetsBuilder') router.push(to) }) return } next() }) onBeforeUnmount(() => { removeGuard?.() }) return { integrations, isAddingIntegration, editingIntegrationId, loading, initialLoading, formLoading, currentIntegration, componentKey, maxIntegrations, showLimitNotice, isAddMoreUpgradeMode, isSingleConfigMode, formFields, isGoogleConnected, navigateToGlobalSettings, startAddingIntegration, startEditingIntegration, cancelEditing, saveIntegration, confirmDeleteIntegration, handleToggle, } }