PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
← All changes | admin/assets/js/wpfnl-admin.js +354 -39 3.12.12 → 3.13.3 View file →
@@ -256,8 +256,20 @@
256 256 $(document.body).on('click', '.wpfnl-bulk-trash', this.bulkTrashFunnel)
257 257 $(document.body).on('click', '.wpfnl-bulk-delete', this.bulkDeleteFunnel)
258 258 $(document.body).on('click', '.wpfnl-bulk-restore', this.bulkRestoreFunnel)
259 259 $(document.body).on('click', '.wpfnl-bulk-export', this.bulkExportFunnel)
260 + // Bound directly, NOT delegated through document.body: the modal wrapper has its
261 + // own click handler calling stopPropagation() to keep backdrop clicks from closing
262 + // the modal, so events from inside it never reach body.
263 + $('.wpfnl-import-cancel').on('click', this.CancelImport)
264 + $('.wpfnl-import-view-funnels').on('click', function (e) {
265 + e.preventDefault()
266 + window.location.reload()
267 + })
268 +
269 + // An import started in an earlier page view keeps running in the background, so
270 + // pick it back up rather than leaving the user with no sign of it.
271 + resumeImportIfRunning()
260 272 }
261 273
262 274 /**
263 275 * Select quantity
@@ -476,8 +488,9 @@
476 488 status_updated: 'Funnel status has been updated successfully.',
477 489 bulk_trash_success: 'The selected funnels have been moved to Trash.',
478 490 bulk_delete_success: 'The selected funnels have been deleted permanently.',
479 491 bulk_restore_success: 'The selected funnels have been restored successfully.',
492 + import_success: 'The funnels have been imported successfully.',
480 493 }
481 494
482 495 const toastKey = localStorage.getItem('wpfnl_show_toast')
483 496 if (!toastKey || !toastMap[toastKey]) return
@@ -822,8 +835,14 @@
822 835 */
823 836 FunnelHandler.prototype.ShowImportFunnelModal = function (e) {
824 837 e.stopPropagation()
825 838 e.preventDefault()
839 + // Reset back to the upload form: a previous run may have left the modal showing
840 + // progress or a result card.
841 + $('.wpfnl-import-progress').hide().removeClass('is-stalled')
842 + $('.wpfnl-import-result').hide()
843 + $('#wpfnl-import-funnels').show()
844 + $('#wpfnl-export-import-warning').css('display', 'none')
826 845 $('.import-funnel-modal').addClass('show-modal')
827 846 }
828 847
829 848 /**
@@ -854,8 +873,14 @@
854 873 */
855 874 FunnelHandler.prototype.ShowImportFunnelModal = function (e) {
856 875 e.stopPropagation()
857 876 e.preventDefault()
877 + // Reset back to the upload form: a previous run may have left the modal showing
878 + // progress or a result card.
879 + $('.wpfnl-import-progress').hide().removeClass('is-stalled')
880 + $('.wpfnl-import-result').hide()
881 + $('#wpfnl-import-funnels').show()
882 + $('#wpfnl-export-import-warning').css('display', 'none')
858 883 $('.import-funnel-modal').addClass('show-modal')
859 884 }
860 885
861 886 /**
@@ -955,13 +980,26 @@
955 980 handler.ShowImportWarning(handler.GetImportErrorMessage(response))
956 981 return
957 982 }
958 983 $('#wpfnl-import-funnel .wpfnl-loader').css('display', 'none')
959 - $('.import-funnel-modal').removeClass('show-modal')
960 - window.location.reload()
984 + // Deliberately no page reload here. The job has only been *queued*
985 + // at this point, so reloading used to land the user on an unchanged
986 + // funnel list and force them to keep refreshing to find out what
987 + // happened. Stay open and follow the job instead.
988 + showImportProgress()
989 + pollImportStatus(response?.data?.run_id || '')
961 990 },
962 991 error: function (jqXHR) {
963 992 // Validation failures are sent with a 4xx status code.
993 + const body =
994 + jqXHR && jqXHR.responseJSON ? jqXHR.responseJSON : null
995 + // 409: a run really is still going. Offer to clear it rather than
996 + // leaving the user stuck behind "Import already scheduled".
997 + if (body && body.data && body.data.can_reset) {
998 + showImportProgress()
999 + pollImportStatus(body.data.run_id || '')
1000 + return
1001 + }
964 1002 handler.ShowImportWarning(handler.GetImportErrorMessage(jqXHR))
965 1003 },
966 1004 })
967 1005 } else {
@@ -1016,8 +1054,322 @@
1016 1054 }
1017 1055 }
1018 1056
1019 1057 /**
1058 + * Translated string with an English fallback.
1059 + *
1060 + * Strings in this file are historically hardcoded; new ones go through
1061 + * WPFunnelVars.getText so they are actually translatable.
1062 + *
1063 + * @param key
1064 + * @param fallback
1065 + *
1066 + * @since 2.5.16
1067 + */
1068 + function importText(key, fallback) {
1069 + const bag = window.WPFunnelVars && WPFunnelVars.getText ? WPFunnelVars.getText : {}
1070 + return bag[key] || fallback
1071 + }
1072 +
1073 + /**
1074 + * Swap the import modal into its progress state.
1075 + *
1076 + * @since 2.5.16
1077 + */
1078 + function showImportProgress() {
1079 + $('.import-funnel-modal').addClass('show-modal')
1080 + $('#wpfnl-import-funnels').hide()
1081 + $('.wpfnl-import-result').hide()
1082 + $('.wpfnl-import-progress').show()
1083 + $('.wpfnl-import-progress').removeClass('is-stalled')
1084 + $('.wpfnl-import-progress__label').text(
1085 + importText('import_preparing', 'Preparing import…'),
1086 + )
1087 + $('.wpfnl-import-progress__bar').css('width', '0%')
1088 + $('.wpfnl-import-progress__meta').text('')
1089 + }
1090 +
1091 + /**
1092 + * Read the current import status.
1093 + *
1094 + * @param runId
1095 + *
1096 + * @since 2.5.16
1097 + */
1098 + function fetchImportStatus(runId) {
1099 + const base = window.WPFunnelVars ? WPFunnelVars.rest_api_url : ''
1100 + const url =
1101 + base +
1102 + 'wpfunnels/v1/import-export/import-status' +
1103 + (runId ? '?run_id=' + encodeURIComponent(runId) : '')
1104 +
1105 + return fetch(url, {
1106 + method: 'GET',
1107 + headers: { 'X-WP-Nonce': WPFunnelVars.nonce },
1108 + }).then(function (response) {
1109 + if (!response.ok) {
1110 + throw new Error('status_request_failed')
1111 + }
1112 + return response.json()
1113 + })
1114 + }
1115 +
1116 + /**
1117 + * Paint one status tick into the progress UI.
1118 + *
1119 + * @param status
1120 + *
1121 + * @since 2.5.16
1122 + */
1123 + function renderImportProgress(status) {
1124 + const pct = Math.max(0, Math.min(100, parseInt(status.pct, 10) || 0))
1125 +
1126 + $('.wpfnl-import-progress__label').text(
1127 + status.label || importText('import_in_progress', 'Importing funnels…'),
1128 + )
1129 + $('.wpfnl-import-progress__bar').css('width', pct + '%')
1130 +
1131 + let meta = ''
1132 + if (status.total_funnels) {
1133 + meta =
1134 + status.done_funnels +
1135 + ' / ' +
1136 + status.total_funnels +
1137 + ' ' +
1138 + importText('import_funnels_word', 'funnels')
1139 + if (status.total_steps) {
1140 + meta +=
1141 + ' · ' +
1142 + status.done_steps +
1143 + ' / ' +
1144 + status.total_steps +
1145 + ' ' +
1146 + importText('import_steps_word', 'steps')
1147 + }
1148 + }
1149 + $('.wpfnl-import-progress__meta').text(meta)
1150 + }
1151 +
1152 + /**
1153 + * Render the terminal state: what landed, what did not.
1154 + *
1155 + * @param status
1156 + *
1157 + * @since 2.5.16
1158 + */
1159 + function renderImportResult(status) {
1160 + const imported = Array.isArray(status.imported) ? status.imported : []
1161 + const failed = Array.isArray(status.failed) ? status.failed : []
1162 +
1163 + $('.wpfnl-import-progress').hide()
1164 + $('.wpfnl-import-result').show()
1165 +
1166 + let summary = status.message || ''
1167 + if (!summary) {
1168 + summary =
1169 + 'failed' === status.state
1170 + ? importText('import_failed', 'The import could not be completed.')
1171 + : importText('import_completed', 'Import complete.')
1172 + }
1173 + $('.wpfnl-import-result__summary').text(summary)
1174 +
1175 + const $list = $('.wpfnl-import-result__list').empty()
1176 +
1177 + imported.forEach(function (item) {
1178 + const stepsWord = importText('import_steps_word', 'steps')
1179 + let note = item.steps + ' ' + stepsWord
1180 +
1181 + // A partially imported funnel is the case worth being precise about: say how
1182 + // many steps are missing rather than just flagging it as odd.
1183 + if ('partial' === item.status) {
1184 + const total = (item.steps || 0) + (item.failed_steps || 0)
1185 + note =
1186 + item.steps +
1187 + ' / ' +
1188 + total +
1189 + ' ' +
1190 + stepsWord +
1191 + ' · ' +
1192 + importText('import_partial', 'some steps failed')
1193 + }
1194 +
1195 + $list.append(
1196 + $('<li/>')
1197 + .addClass('partial' === item.status ? 'is-failed' : '')
1198 + .append($('<span/>').text(item.name || ''))
1199 + .append($('<span/>').addClass('wpfnl-import-result__note').text(note)),
1200 + )
1201 + })
1202 +
1203 + failed.forEach(function (item) {
1204 + $list.append(
1205 + $('<li/>')
1206 + .addClass('is-failed')
1207 + .append($('<span/>').text(item.name || ''))
1208 + .append(
1209 + $('<span/>')
1210 + .addClass('wpfnl-import-result__note')
1211 + .text(item.message || importText('import_failed_item', 'failed')),
1212 + ),
1213 + )
1214 + })
1215 +
1216 + // Only claim success when nothing failed. A partial run has already been spelled
1217 + // out in the list above, and a cheerful toast on top of it would misrepresent it.
1218 + if (imported.length && !failed.length) {
1219 + try {
1220 + localStorage.setItem('wpfnl_show_toast', 'import_success')
1221 + } catch (e) {
1222 + // Private mode / blocked storage: the result card already says what happened.
1223 + }
1224 + }
1225 + }
1226 +
1227 + /**
1228 + * Follow a background import to completion.
1229 + *
1230 + * Bounded polling with backoff rather than setInterval: the job can legitimately run
1231 + * for minutes, and a fixed short interval would hammer the REST route for the whole
1232 + * duration. Stall detection happens server-side — the process that died cannot report
1233 + * on itself — so this only has to react to the state it is handed.
1234 + *
1235 + * @param runId
1236 + *
1237 + * @since 2.5.16
1238 + */
1239 + async function pollImportStatus(runId) {
1240 + const started = Date.now()
1241 + const maxMs = 15 * 60 * 1000
1242 + let attempt = 0
1243 + let consecutiveErrors = 0
1244 +
1245 + while (Date.now() - started < maxMs) {
1246 + const delay = attempt < 5 ? 1000 : attempt < 20 ? 2000 : 5000
1247 + await new Promise(function (resolve) {
1248 + setTimeout(resolve, delay)
1249 + })
1250 + attempt++
1251 +
1252 + let status
1253 + try {
1254 + status = await fetchImportStatus(runId)
1255 + consecutiveErrors = 0
1256 + } catch (e) {
1257 + // A blip should not kill a long-running import view; three in a row means
1258 + // something is actually wrong.
1259 + consecutiveErrors++
1260 + if (consecutiveErrors >= 3) {
1261 + renderImportResult({
1262 + state: 'failed',
1263 + message: importText(
1264 + 'import_status_unavailable',
1265 + 'Lost contact with the import. Refresh the page to check on it.',
1266 + ),
1267 + })
1268 + return
1269 + }
1270 + continue
1271 + }
1272 +
1273 + if (!runId && status.run_id) {
1274 + runId = status.run_id
1275 + }
1276 +
1277 + // Somebody else started an import; this view is no longer authoritative.
1278 + if ('superseded' === status.state || 'idle' === status.state) {
1279 + return
1280 + }
1281 +
1282 + if ('stalled' === status.state) {
1283 + $('.wpfnl-import-progress').addClass('is-stalled')
1284 + $('.wpfnl-import-progress__label').text(
1285 + importText(
1286 + 'import_stalled',
1287 + 'The import seems to have stopped responding. Cancel it to try again.',
1288 + ),
1289 + )
1290 + return
1291 + }
1292 +
1293 + renderImportProgress(status)
1294 +
1295 + if ('completed' === status.state || 'failed' === status.state) {
1296 + renderImportResult(status)
1297 + return
1298 + }
1299 + }
1300 +
1301 + $('.wpfnl-import-progress__label').text(
1302 + importText(
1303 + 'import_taking_long',
1304 + 'This import is taking longer than expected. Refresh the page to check on it.',
1305 + ),
1306 + )
1307 + }
1308 +
1309 + /**
1310 + * Resume the progress view for an import still running from an earlier page view.
1311 + *
1312 + * @since 2.5.16
1313 + */
1314 + function resumeImportIfRunning() {
1315 + if (!$('.import-funnel-modal').length || !window.WPFunnelVars) {
1316 + return
1317 + }
1318 +
1319 + fetchImportStatus('')
1320 + .then(function (status) {
1321 + if ('running' === status.state || 'queued' === status.state) {
1322 + showImportProgress()
1323 + renderImportProgress(status)
1324 + pollImportStatus(status.run_id || '')
1325 + }
1326 + })
1327 + .catch(function () {
1328 + // No import to resume, or the route is unreachable — stay quiet.
1329 + })
1330 + }
1331 +
1332 + /**
1333 + * Clear a stuck import so a new one can be started.
1334 + *
1335 + * @param event
1336 + *
1337 + * @since 2.5.16
1338 + */
1339 + FunnelHandler.prototype.CancelImport = function (event) {
1340 + event.preventDefault()
1341 +
1342 + const $button = $(this)
1343 + $button.find('.wpfnl-loader').css('display', 'inline-block')
1344 +
1345 + fetch(WPFunnelVars.rest_api_url + 'wpfunnels/v1/import-export/import-reset', {
1346 + method: 'POST',
1347 + headers: {
1348 + 'Content-Type': 'application/json',
1349 + 'X-WP-Nonce': WPFunnelVars.nonce,
1350 + },
1351 + })
1352 + .then(function () {
1353 + $button.find('.wpfnl-loader').css('display', 'none')
1354 + $('.wpfnl-import-progress').hide().removeClass('is-stalled')
1355 + $('.import-funnel-modal').removeClass('show-modal')
1356 + $('#wpfnl-import-funnels').show()
1357 + showToast(
1358 + 'success',
1359 + importText('import_cancelled', 'The import has been cancelled.'),
1360 + )
1361 + })
1362 + .catch(function () {
1363 + $button.find('.wpfnl-loader').css('display', 'none')
1364 + showToast(
1365 + 'error',
1366 + importText('import_cancel_failed', 'Could not cancel the import.'),
1367 + )
1368 + })
1369 + }
1370 +
1371 + /**
1020 1372 * Download json file
1021 1373 *
1022 1374 * @param content
1023 1375 * @param fileName
@@ -2027,34 +2379,10 @@
2027 2379 }
2028 2380 }
2029 2381 }
2030 2382
2031 - /**
2032 - * settings page tab nav
2033 - *
2034 - * @since 1.0.0
2035 - */
2036 - $('.wpfn-settings__nav .nav-li:not(.disabled)').on('click', function (e) {
2037 - var dataID = $(this).attr('data-id');
2038 -
2039 - $(this).addClass('active').siblings('.nav-li').removeClass('active');
2040 2383
2041 - // Show the selected panel, hide others
2042 - $('#' + dataID).show().siblings('.wpfnl-funnel__single-settings').hide();
2043 2384
2044 - // Inside the visible panel, activate the first inner-tab
2045 - var $container = $('#' + dataID);
2046 - $container.find('.inner-tab').removeClass('active');
2047 - $container.find('.wpfnl-tab-content').removeClass('active');
2048 -
2049 - var $firstInnerTab = $container.find('.inner-tab').first();
2050 - var tabId = $firstInnerTab.data('tab');
2051 -
2052 - $firstInnerTab.addClass('active');
2053 - $('#' + tabId).addClass('active');
2054 - });
2055 -
2056 -
2057 2385 /**
2058 2386 * Checkout Edit Field delete alert modal
2059 2387 *
2060 2388 * @since 1.0.0
@@ -2484,21 +2812,8 @@
2484 2812 )
2485 2813 }
2486 2814 },
2487 2815 )
2488 -
2489 - /**
2490 - * Funnel window page and Empty funnel page help resource show/hide.
2491 - *
2492 - */
2493 - $(document).on('click', '.wpfnl-helper-btn', function (e) {
2494 - e.stopPropagation()
2495 - $(this).parent('.wpfnl-canvas-helper').toggleClass('show-helpers')
2496 - })
2497 -
2498 - $(document).on('click', 'body', function () {
2499 - $('.wpfnl-canvas-helper').removeClass('show-helpers')
2500 - })
2501 2816
2502 2817 /**
2503 2818 * add new step option dropdown show/hide.
2504 2819 *