| 1 |
/* eslint no-unused-vars: "off" */ |
| 2 |
import type { Component } from 'vue' |
| 3 |
import * as Vue from 'vue' |
| 4 |
import { createApp } from 'vue' |
| 5 |
import { createPinia } from 'pinia' |
| 6 |
import App from '../../../views/admin/App.vue' |
| 7 |
import EntryPage from '@/views/admin/entries/entry-page/EntryPage.vue' |
| 8 |
import router from '@/router' |
| 9 |
import '@/assets/scss/main.scss' |
| 10 |
import 'element-plus/dist/index.css' |
| 11 |
import { useLabelsStore } from '@/stores/labels' |
| 12 |
import { useLabels } from '@/composables/useLabels' |
| 13 |
import api, { dispatchIvyReady, installIvyGlobals } from '@/composables/IvyFormAPI' |
| 14 |
import { useProFeatures } from '@/composables/useProFeatures' |
| 15 |
import { useFormBuilder } from '@/stores/useFormBuilder' |
| 16 |
import { useEntry } from '@/stores/useEntry' |
| 17 |
import { useNavigation } from '@/composables/useNavigation' |
| 18 |
import { registerLiteComponents } from '@/composables/registerLiteComponents' |
| 19 |
import { registerLiteFieldOptions } from '@/views/admin/form-builder/form-builder-page/form-builder/panel/tabs/field-options/registerLiteFieldOptions' |
| 20 |
import { registerLiteStyleSectionPromos } from '@/views/admin/form-builder/form-builder-page/form-builder/panel/tabs/style-controls/registerLiteStyleSectionPromos' |
| 21 |
import * as LiteTypes from '@/types/index' |
| 22 |
import type { IvyProFeatures } from '@/types/global' |
| 23 |
import { useWcagColors } from '@/composables/useWcagColors' |
| 24 |
import { useSettingsStore } from '@/stores/useSettingsStore' |
| 25 |
import IvyMessage from '@/views/_components/message/ivyMessage' |
| 26 |
import { useApiClient } from '@/composables/useApiClient' |
| 27 |
import { useActionEntityStore } from '@/stores/actionEntityStore' |
| 28 |
import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore' |
| 29 |
import { useProUpgradeDialogStore } from '@/stores/proUpgradeDialogStore' |
| 30 |
import { useGlobalState } from '@/stores/useGlobalState' |
| 31 |
import { denormalizeFieldsForApi } from '@/utils/fieldsNormalization' |
| 32 |
import { countryList } from '@/constants/countries' |
| 33 |
import { isSecurityField } from '@/constants/securityFields' |
| 34 |
import { useProFeatureUpgrade } from '@/composables/useProFeatureUpgrade' |
| 35 |
import { useConfirmationMultiSetting } from '@/stores/useConfirmationMultiSetting' |
| 36 |
import { useGoogleSheetsFormIntegrations } from '@/integrations/google-sheets/composables/useGoogleSheetsFormIntegrations' |
| 37 |
import { |
| 38 |
registerFormBuilderExtension, |
| 39 |
getFormBuilderExtensions, |
| 40 |
} from '@/extensions/formBuilderExtensions' |
| 41 |
import { useAllEntries } from '@/stores/useAllEntries' |
| 42 |
import { getPreviewFieldComponent } from '@/composables/useFieldPreviewComponents' |
| 43 |
import { normalizeFieldsForBuilder } from '@/utils/fieldsNormalization' |
| 44 |
import { registerLitePublicFieldComponents } from '@/entries/publicFieldComponentRegistry' |
| 45 |
import { registerGoogleSheetsAdmin } from '@/integrations/google-sheets/registerGoogleSheetsAdmin' |
| 46 |
|
| 47 |
// Note: Window interface extensions are now defined in src/types/global.d.ts |
| 48 |
// No need to duplicate declarations here |
| 49 |
|
| 50 |
const app = createApp(App) |
| 51 |
const pinia = createPinia() |
| 52 |
app.use(pinia) |
| 53 |
app.use(router) |
| 54 |
|
| 55 |
// Shared settings store: must exist before mount so Pro views (e.g. Zapier) can read the same |
| 56 |
// `settings/all` payload as the rest of admin via `window.IvyForms.settingsStore`. |
| 57 |
const settingsStore = useSettingsStore() |
| 58 |
|
| 59 |
// Initialize the labels store with WordPress data |
| 60 |
const labelsStore = useLabelsStore() |
| 61 |
labelsStore.initialize() |
| 62 |
|
| 63 |
// Make labels globally available to all components |
| 64 |
app.config.globalProperties.$labels = useLabels() |
| 65 |
|
| 66 |
// Expose IvyFormsApi and Lite globals for extenders |
| 67 |
;(function exposeGlobals() { |
| 68 |
// Ensure the API is present on window |
| 69 |
installIvyGlobals() |
| 70 |
const w = window |
| 71 |
const { getLabel } = useLabels() |
| 72 |
const formBuilder = useFormBuilder() |
| 73 |
const globalState = useGlobalState() |
| 74 |
|
| 75 |
// Proxy to resolve labels dynamically via getLabel |
| 76 |
const labelsProxy = new Proxy( |
| 77 |
{}, |
| 78 |
{ |
| 79 |
get: (_t, key: string | symbol) => { |
| 80 |
try { |
| 81 |
return getLabel(String(key)) |
| 82 |
} catch { |
| 83 |
return String(key) |
| 84 |
} |
| 85 |
}, |
| 86 |
}, |
| 87 |
) |
| 88 |
|
| 89 |
w.IvyForms = w.IvyForms || { api, hooks: api.hooks, fields: api.fields } |
| 90 |
w.IvyForms.api = api |
| 91 |
// Always sync to canonical api.hooks so third-party registrations land on the same registry. |
| 92 |
w.IvyForms.hooks = api.hooks |
| 93 |
|
| 94 |
// Ensure hook registration triggers reactive updates in Lite components |
| 95 |
if (w.IvyForms.hooks === api.hooks && api.hooks.triggerUpdate) { |
| 96 |
w.IvyForms.hooks.triggerUpdate = api.hooks.triggerUpdate.bind(api.hooks) |
| 97 |
} |
| 98 |
|
| 99 |
w.IvyForms.fields = api.fields |
| 100 |
// Expose label helpers for Pro plugin and other extenders |
| 101 |
w.IvyForms.getLabel = getLabel |
| 102 |
w.IvyForms.labels = labelsProxy |
| 103 |
// Set WordPress admin URL |
| 104 |
w.IvyForms.adminUrl = w.wpIvyApiSettings?.root?.replace(/\/wp-json\/?$/, '') + '/wp-admin/' |
| 105 |
// Expose Vue for pro plugins with guaranteed compatibility |
| 106 |
w.IvyForms.Vue = Vue |
| 107 |
//w.Vue = w.Vue || Vue // Global fallback for compatibility with other plugins |
| 108 |
// Expose IvyMessage utility for Pro plugin |
| 109 |
w.IvyForms.IvyMessage = IvyMessage |
| 110 |
// Expose useApiClient for Pro plugin (call it once to prevent tree-shaking) |
| 111 |
w.IvyForms.useApiClient = useApiClient |
| 112 |
// Shared Google Sheets form-builder CRUD for Lite single + Pro multi shells |
| 113 |
w.IvyForms.useGoogleSheetsFormIntegrations = useGoogleSheetsFormIntegrations |
| 114 |
// Expose stores for Pro plugin to use dialogs and entity actions |
| 115 |
w.IvyForms.useActionEntityStore = useActionEntityStore |
| 116 |
// Expose the actual store instance (not the function) so Pro uses the same instance |
| 117 |
w.IvyForms.unsavedChangesStore = useUnsavedChangesStore() |
| 118 |
w.IvyForms.useProUpgradeDialogStore = useProUpgradeDialogStore |
| 119 |
// Expose the form builder store so external components can read/write fields |
| 120 |
w.IvyForms.formBuilder = formBuilder |
| 121 |
w.IvyForms.registerFormBuilderExtension = registerFormBuilderExtension |
| 122 |
w.IvyForms.getFormBuilderExtensions = getFormBuilderExtensions |
| 123 |
w.IvyForms.useEntry = useEntry |
| 124 |
// Expose global state for Pro plugins to set page title and active page |
| 125 |
w.IvyForms.globalState = globalState |
| 126 |
// Expose shared types for Pro plugin and other extenders |
| 127 |
w.IvyForms.types = LiteTypes |
| 128 |
// Expose shared utility helpers for Pro plugin and other extenders |
| 129 |
const { showUpgradeDialog } = useProFeatureUpgrade() |
| 130 |
w.IvyForms.helpers = { |
| 131 |
isSecurityField, |
| 132 |
showUpgradeDialog, |
| 133 |
getPreviewFieldComponent: getPreviewFieldComponent as (fieldType: string) => unknown, |
| 134 |
normalizeFieldsForBuilder: normalizeFieldsForBuilder as (fields: unknown[]) => unknown[], |
| 135 |
denormalizeFieldsForApi, |
| 136 |
countryList, |
| 137 |
registerLitePublicFieldComponents, |
| 138 |
} |
| 139 |
w.IvyForms.confirmationMulti = { |
| 140 |
refreshTable: async (formId: number) => { |
| 141 |
await useConfirmationMultiSetting().searchConfirmations({ formId }) |
| 142 |
}, |
| 143 |
} |
| 144 |
|
| 145 |
// Expose entry stores so Pro can read/refresh entries (add-entry is a Pro feature). |
| 146 |
w.IvyForms.useAllEntries = useAllEntries |
| 147 |
w.IvyForms.useEntry = useEntry |
| 148 |
// Create a callable components registry: |
| 149 |
// - Use as function: w.IvyForms.components('MyComp', Comp) |
| 150 |
// - Or as object map: w.IvyForms.components.MyComp |
| 151 |
// - Or helper alias: w.IvyForms.registerComponent('MyComp', Comp) |
| 152 |
type ComponentRegistry = { |
| 153 |
(name?: string, component?: Component): Component | ComponentRegistry |
| 154 |
[key: string]: Component | unknown |
| 155 |
} |
| 156 |
const componentsRegistry = ((name?: string, component?: Component) => { |
| 157 |
if (name && component) { |
| 158 |
app.component(name, component) |
| 159 |
;(componentsRegistry as Record<string, Component>)[name] = component |
| 160 |
return component |
| 161 |
} |
| 162 |
// If only name is provided, return the component |
| 163 |
if (name) { |
| 164 |
return (componentsRegistry as Record<string, Component>)[name] |
| 165 |
} |
| 166 |
return componentsRegistry |
| 167 |
}) as ComponentRegistry |
| 168 |
|
| 169 |
// Register all Lite components for Pro plugin access |
| 170 |
registerLiteComponents(componentsRegistry, app) |
| 171 |
|
| 172 |
w.IvyForms.components = componentsRegistry |
| 173 |
w.IvyForms.registerComponent = (name: string, component: Component) => |
| 174 |
componentsRegistry(name, component) |
| 175 |
w.IvyForms.registerComponent('EntryPage', EntryPage) |
| 176 |
|
| 177 |
// Expose router for Pro plugins to add routes dynamically |
| 178 |
w.IvyForms._router = router |
| 179 |
|
| 180 |
// Initialize pro features bridge if not already provided by Pro plugin |
| 181 |
if (!w.IvyForms.pro) { |
| 182 |
const pro = useProFeatures() |
| 183 |
// Fire fetch; backend route may 404 in Lite-only context which is fine. |
| 184 |
pro |
| 185 |
.fetch() |
| 186 |
.then(() => { |
| 187 |
window.dispatchEvent(new CustomEvent('ivyforms:pro_features_loaded')) |
| 188 |
}) |
| 189 |
.catch(() => {}) |
| 190 |
w.IvyForms.pro = { |
| 191 |
get loaded() { |
| 192 |
return pro.loaded.value |
| 193 |
}, |
| 194 |
get loading() { |
| 195 |
return pro.loading.value |
| 196 |
}, |
| 197 |
get plan() { |
| 198 |
return pro.plan.value |
| 199 |
}, |
| 200 |
get upgradeMap() { |
| 201 |
return pro.upgradeMap.value |
| 202 |
}, |
| 203 |
get groups() { |
| 204 |
return pro.groups.value |
| 205 |
}, |
| 206 |
get license() { |
| 207 |
return pro.license.value |
| 208 |
}, |
| 209 |
get features(): Record<string, boolean> { |
| 210 |
const rawFeatures = |
| 211 |
(pro as { data?: { value?: { features?: Record<string, unknown> } } }).data?.value |
| 212 |
?.features || {} |
| 213 |
// Convert unknown to boolean |
| 214 |
const booleanFeatures: Record<string, boolean> = {} |
| 215 |
for (const key in rawFeatures) { |
| 216 |
booleanFeatures[key] = !!rawFeatures[key] |
| 217 |
} |
| 218 |
return booleanFeatures |
| 219 |
}, |
| 220 |
hasFeature: (slug: string) => pro.hasFeature(slug), |
| 221 |
fetch: (opts?: Record<string, unknown>) => pro.fetch(opts), |
| 222 |
refresh: () => pro.refresh(), |
| 223 |
} |
| 224 |
} |
| 225 |
|
| 226 |
// Simple onProReady queue so other scripts can safely register logic dependent on features |
| 227 |
if (!w.IvyForms.onProReady) { |
| 228 |
w.IvyForms._proReadyQueue = [] |
| 229 |
w.IvyForms.onProReady = (cb: (pro: IvyProFeatures) => void) => { |
| 230 |
try { |
| 231 |
if (w.IvyForms.pro?.loaded) return cb(w.IvyForms.pro) |
| 232 |
w.IvyForms._proReadyQueue?.push(cb) |
| 233 |
} catch { |
| 234 |
/* noop */ |
| 235 |
} |
| 236 |
} |
| 237 |
window.addEventListener('ivyforms:pro_features_loaded', () => { |
| 238 |
const q = Array.isArray(w.IvyForms._proReadyQueue) ? w.IvyForms._proReadyQueue : [] |
| 239 |
while (q.length) { |
| 240 |
const fn = q.shift() |
| 241 |
try { |
| 242 |
if (fn) { |
| 243 |
fn(w.IvyForms.pro) |
| 244 |
} |
| 245 |
} catch { |
| 246 |
/* ignore */ |
| 247 |
} |
| 248 |
} |
| 249 |
}) |
| 250 |
} |
| 251 |
|
| 252 |
// Convenience global helper (returns boolean) – safe to call anytime |
| 253 |
if (!w.IvyForms.hasProFeature) { |
| 254 |
w.IvyForms.hasProFeature = (slug: string): boolean => { |
| 255 |
try { |
| 256 |
if (!w.IvyForms.pro) return false |
| 257 |
if (!w.IvyForms.pro.loaded) return false // treat not-yet-loaded as false for simplicity |
| 258 |
return !!w.IvyForms.pro.hasFeature?.(slug) |
| 259 |
} catch { |
| 260 |
return false |
| 261 |
} |
| 262 |
} |
| 263 |
} |
| 264 |
|
| 265 |
// Pinia settings store (same instance as Lite admin) — Pro reads `integrations.*` here. |
| 266 |
w.IvyForms.settingsStore = settingsStore |
| 267 |
})() |
| 268 |
|
| 269 |
// Register Google Sheets after hooks + router are exposed |
| 270 |
registerGoogleSheetsAdmin() |
| 271 |
|
| 272 |
// Register Lite field options |
| 273 |
registerLiteFieldOptions() |
| 274 |
|
| 275 |
// Register Lite advanced style section promos (replaced by Pro via hook when available) |
| 276 |
registerLiteStyleSectionPromos() |
| 277 |
|
| 278 |
// Bind navigation helper under app context so useRouter() resolves correctly. |
| 279 |
app.runWithContext(() => { |
| 280 |
window.IvyForms.navigateToAdminPage = useNavigation().navigateToAdminPage |
| 281 |
}) |
| 282 |
|
| 283 |
app.mount('#ivyforms-app') |
| 284 |
|
| 285 |
// Load settings from backend first, then initialize WCAG colors |
| 286 |
settingsStore |
| 287 |
.loadAllSettings() |
| 288 |
.then(() => { |
| 289 |
// Initialize WCAG colors watcher globally after settings are loaded |
| 290 |
const { startWatching } = useWcagColors() |
| 291 |
startWatching() |
| 292 |
}) |
| 293 |
.catch(() => { |
| 294 |
// Still start watcher even if settings fail to load (will use default value) |
| 295 |
const { startWatching } = useWcagColors() |
| 296 |
startWatching() |
| 297 |
}) |
| 298 |
|
| 299 |
// Signal readiness for any external extenders waiting on Lite |
| 300 |
dispatchIvyReady({ scope: 'admin' }) |
| 301 |
|