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