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 +408 -55 3.12.4 → 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 /**
@@ -878,9 +903,48 @@
878 903 $('#wpfnl-export-import-warning').css('display', 'none')
879 904 }
880 905
881 906 /**
907 + * Show a validation/error message inside the import funnel modal.
882 908 *
909 + * The message element carries a default text, so it is always set
910 + * explicitly here to avoid leaking a previous message.
911 + *
912 + * @param message
913 + * @since 2.6.3
914 + */
915 + FunnelHandler.prototype.ShowImportWarning = function (message) {
916 + $('#wpfnl-export-import-warning')
917 + .text(message || 'Please select a valid file.')
918 + .css('display', 'inline-block')
919 + $('#wpfnl-import-funnel .wpfnl-loader').css('display', 'none')
920 + }
921 +
922 + /**
923 + * Pull the error message out of an ajax response.
924 + *
925 + * wp_send_json_error() responses arrive either as a parsed body
926 + * (HTTP 200) or on the jqXHR when a status code was passed.
927 + *
928 + * @param response
929 + * @since 2.6.3
930 + */
931 + FunnelHandler.prototype.GetImportErrorMessage = function (response) {
932 + const body = response && response.responseJSON ? response.responseJSON : response
933 +
934 + if (body && body.data && body.data.message) {
935 + return body.data.message
936 + }
937 +
938 + if (body && typeof body.data === 'string') {
939 + return body.data
940 + }
941 +
942 + return ''
943 + }
944 +
945 + /**
946 + *
883 947 * Ajax handler for import funnels
884 948 * funnel action
885 949 *
886 950 * @param event
@@ -887,8 +951,10 @@
887 951 * @since 2.6.3
888 952 */
889 953 FunnelHandler.prototype.ImportFunnels = function (event) {
890 954 event.preventDefault()
955 + // `this` is the submitted form here, so reach the helpers via the prototype.
956 + const handler = FunnelHandler.prototype
891 957 $('#wpfnl-import-funnel .wpfnl-loader').css('display', 'inline-block')
892 958 $('#wpfnl-export-import-warning').css('display', 'none')
893 959 let formData = new FormData(this)
894 960 let importFile = $('#wpfnl-file-import')[0].files[0]
@@ -908,28 +974,40 @@
908 974 dataType: 'json',
909 975 processData: false,
910 976 data: formData,
911 977 success: function (response) {
912 - window.location.reload();
978 + // wp_send_json_error() without a status code still returns HTTP 200.
979 + if (response && false === response.success) {
980 + handler.ShowImportWarning(handler.GetImportErrorMessage(response))
981 + return
982 + }
913 983 $('#wpfnl-import-funnel .wpfnl-loader').css('display', 'none')
914 - $('.import-funnel-modal').removeClass('show-modal')
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 || '')
915 990 },
916 - }).done(function (request, status, XHR) {
917 - if (status.success) {
918 - // window.location.reload();
919 - }
920 - $('#wpfnl-import-funnel .wpfnl-loader').css('display', 'none')
921 - $('.import-funnel-modal').removeClass('show-modal')
991 + error: function (jqXHR) {
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 + }
1002 + handler.ShowImportWarning(handler.GetImportErrorMessage(jqXHR))
1003 + },
922 1004 })
923 1005 } else {
924 - $('#wpfnl-export-import-warning').css('display', 'inline-block')
925 - $('#wpfnl-export-import-warning').css('color', '#d63638 !important')
926 - $('#wpfnl-import-funnel .wpfnl-loader').css('display', 'none')
1006 + handler.ShowImportWarning('Please select a valid file.')
927 1007 }
928 1008 } else {
929 - $('#wpfnl-export-import-warning').css('display', 'inline-block')
930 - $('#wpfnl-export-import-warning').css('color', '#d63638 !important')
931 - $('#wpfnl-import-funnel .wpfnl-loader').css('display', 'none')
1009 + handler.ShowImportWarning('Please select a valid file.')
932 1010 }
933 1011 }
934 1012
935 1013 /**
@@ -955,10 +1033,9 @@
955 1033 $('.import-funnel-modal .import-label > h4').text('Json file ready for import.')
956 1034 $('.import-funnel-modal .import-label > p').text(fileName)
957 1035 } else {
958 1036 $('.import-funnel-modal .import-label').removeClass('file-added')
959 - $('#wpfnl-export-import-warning').css('display', 'inline-block')
960 - $('#wpfnl-export-import-warning').css('color', '#d63638 !important')
1037 + FunnelHandler.prototype.ShowImportWarning('Please select a valid file.')
961 1038
962 1039 $('.import-funnel-modal .import-label > h4').html(
963 1040 'Drag & Drop or <span class="primary-color">Choose file</span> to upload.',
964 1041 )
@@ -967,10 +1044,9 @@
967 1044 )
968 1045 }
969 1046 } else {
970 1047 $('.import-funnel-modal .import-label').removeClass('file-added')
971 - $('#wpfnl-export-import-warning').css('display', 'inline-block')
972 - $('#wpfnl-export-import-warning').css('color', '#d63638 !important')
1048 + FunnelHandler.prototype.ShowImportWarning('Please select a valid file.')
973 1049
974 1050 $('.import-funnel-modal .import-label > h4').html(
975 1051 'Drag & Drop or <span class="primary-color">Choose file</span> to upload.',
976 1052 )
@@ -978,8 +1054,322 @@
978 1054 }
979 1055 }
980 1056
981 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 + /**
982 1372 * Download json file
983 1373 *
984 1374 * @param content
985 1375 * @param fileName
@@ -1989,34 +2379,10 @@
1989 2379 }
1990 2380 }
1991 2381 }
1992 2382
1993 - /**
1994 - * settings page tab nav
1995 - *
1996 - * @since 1.0.0
1997 - */
1998 - $('.wpfn-settings__nav .nav-li:not(.disabled)').on('click', function (e) {
1999 - var dataID = $(this).attr('data-id');
2000 -
2001 - $(this).addClass('active').siblings('.nav-li').removeClass('active');
2002 2383
2003 - // Show the selected panel, hide others
2004 - $('#' + dataID).show().siblings('.wpfnl-funnel__single-settings').hide();
2005 2384
2006 - // Inside the visible panel, activate the first inner-tab
2007 - var $container = $('#' + dataID);
2008 - $container.find('.inner-tab').removeClass('active');
2009 - $container.find('.wpfnl-tab-content').removeClass('active');
2010 -
2011 - var $firstInnerTab = $container.find('.inner-tab').first();
2012 - var tabId = $firstInnerTab.data('tab');
2013 -
2014 - $firstInnerTab.addClass('active');
2015 - $('#' + tabId).addClass('active');
2016 - });
2017 -
2018 -
2019 2385 /**
2020 2386 * Checkout Edit Field delete alert modal
2021 2387 *
2022 2388 * @since 1.0.0
@@ -2446,21 +2812,8 @@
2446 2812 )
2447 2813 }
2448 2814 },
2449 2815 )
2450 -
2451 - /**
2452 - * Funnel window page and Empty funnel page help resource show/hide.
2453 - *
2454 - */
2455 - $(document).on('click', '.wpfnl-helper-btn', function (e) {
2456 - e.stopPropagation()
2457 - $(this).parent('.wpfnl-canvas-helper').toggleClass('show-helpers')
2458 - })
2459 -
2460 - $(document).on('click', 'body', function () {
2461 - $('.wpfnl-canvas-helper').removeClass('show-helpers')
2462 - })
2463 2816
2464 2817 /**
2465 2818 * add new step option dropdown show/hide.
2466 2819 *