Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 172 additions & 7 deletions app/assets/javascripts/bulkrax/importers_stepper.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
ENDPOINTS: {
DEMO_SCENARIOS: '/importers/guided_import/demo_scenarios',
VALIDATE: '/importers/guided_import/validate',
DOWNLOAD_VALIDATION_ERRORS: '/importers/guided_import/download_validation_errors'
DOWNLOAD_VALIDATION_ERRORS: '/importers/guided_import/download_validation_errors',
METRICS: null // set from data-metrics-url on container init
}
}

Expand Down Expand Up @@ -141,6 +142,9 @@

eventsInitialized = false

var metricsUrl = $('.bulk-import-stepper-container').data('metrics-url')
MetricsTracker.init(metricsUrl)

bindEvents()
initAdminSetState()
updateDownloadTemplateLink()
Expand Down Expand Up @@ -1374,7 +1378,8 @@
},
admin_set_id: StepperState.adminSetId
},
locale: $('input[name="locale"]').val()
locale: $('input[name="locale"]').val(),
metrics_session_id: MetricsTracker.sessionId
},
timeout: CONSTANTS.AJAX_TIMEOUT_LONG
})
Expand All @@ -1397,6 +1402,7 @@
// Update UI after successful validation
function handleValidationSuccess(data, $btn) {
var normalized = normalizeValidationData(data)
MetricsTracker.recordValidation(normalized)
StepperState.validated = true
StepperState.validationData = normalized

Expand Down Expand Up @@ -1459,6 +1465,7 @@

// Validate files (AJAX call to backend)
function validateFiles() {
MetricsTracker.recordValidationStart()
var $btn = StepperState.uploadMode === 'file_path' ? $('#validate-path-btn') : $('#validate-upload-btn')
$btn
.prop('disabled', true)
Expand Down Expand Up @@ -1486,7 +1493,8 @@
importer: {
admin_set_id: StepperState.adminSetId
},
locale: $('input[name="locale"]').val()
locale: $('input[name="locale"]').val(),
metrics_session_id: MetricsTracker.sessionId
}
}

Expand Down Expand Up @@ -2105,6 +2113,7 @@
// Navigate to step
function goToStep(stepNum) {
StepperState.currentStep = stepNum
MetricsTracker.recordStep(stepNum)
updateStepperUI()

// Scroll to top, then move focus to the new step's heading
Expand Down Expand Up @@ -2358,19 +2367,90 @@
// Disable the file input so raw files aren't sent with the form
$('#file-input').prop('disabled', true)

var formData = new FormData($form[0])

// Only append uploaded file IDs in upload mode; in file_path mode the import_file_path
// param is used and appending IDs would cause GuidedImportsController#create to ignore the path.
if (StepperState.uploadMode === 'upload' && Array.isArray(StepperState.uploadedFiles)) {
StepperState.uploadedFiles.forEach(function (f) {
if (f.uploadId) {
var $input = $('<input>', { type: 'hidden', name: 'uploaded_files[]' }).val(f.uploadId)
$form.append($input)
formData.append('uploaded_files[]', f.uploadId)
}
})
}

// Submit the form so the request hits GuidedImportsController#create and creates the importer / enqueues job
$form[0].submit()
// Record funnel event and timing before submit
MetricsTracker.send('funnel', 'step_reached', { step: 4 })
MetricsTracker.recordSubmit()

$.ajax({
url: $form.attr('action'),
method: 'POST',
data: formData,
processData: false,
contentType: false,
dataType: 'json',
headers: { 'Accept': 'application/json' }
}).done(function (data) {
if (data && data.success) {
showImportSuccess(data.importer_id)
} else {
handleImportSubmitError()
}
}).fail(function () {
handleImportSubmitError()
})
}

function showImportSuccess(importerId) {
$('.stepper-content-wrapper').hide()
$('.stepper-header').hide()
$('.import-success-state').show()

// Reveal SEQ after short delay
setTimeout(function () {
$('#seq-feedback').fadeIn(300)
}, 400)

// Store importer ID for SEQ submission
StepperState.importerId = importerId

initSeqHandlers()
}

function handleImportSubmitError() {
var $btn = $('#start-import-btn')
$btn
.prop('disabled', false)
.html(t('start_import') || 'Start Import')
showNotification(t('import_submit_error') || 'Import submission failed. Please try again.', 'error')
}

function initSeqHandlers() {
// Rating selection
$('#seq-feedback').on('click', '.seq-scale-circle', function () {
$('.seq-scale-circle').removeClass('active')
$(this).addClass('active')
$('input[name="seq_score"][value="' + $(this).data('value') + '"]').prop('checked', true)
$('.seq-comment-group').slideDown(200)
$('.seq-actions').slideDown(200)
})

// Form submit
$('#seq-feedback-form').on('submit', function (e) {
e.preventDefault()
var rating = parseInt($('input[name="seq_score"]:checked').val(), 10)
if (!rating) return
var comment = $('#seq-comment').val()
MetricsTracker.recordFeedback(rating, comment, StepperState.importerId)
$('#seq-feedback-form').hide()
$('.seq-thank-you').show()
})

// Skip or dismiss
$('#seq-skip-btn, #seq-dismiss').on('click', function () {
$('#seq-feedback').fadeOut(200)
})
}

// Look up mock validation data from cached demo scenarios JSON
Expand Down Expand Up @@ -2415,6 +2495,91 @@
})
}

// ============================================================================
// METRICS TRACKER
// ============================================================================

var MetricsTracker = {
sessionId: null,
stepTimestamps: {},

init: function (metricsEndpoint) {
CONSTANTS.ENDPOINTS.METRICS = metricsEndpoint || null
this.sessionId = 'gi_' + Math.random().toString(36).substr(2, 9)
$('#metrics-session-id').val(this.sessionId)
this.stepTimestamps = {}
this.stepTimestamps.step1 = Date.now()
this.send('funnel', 'step_reached', { step: 1 })
},

recordStep: function (stepNum) {
this.stepTimestamps['step' + stepNum] = Date.now()
this.send('funnel', 'step_reached', { step: stepNum })
},

recordValidationStart: function () {
this.stepTimestamps.validationStart = Date.now()
},

recordValidation: function (validationData) {
var start = this.stepTimestamps.validationStart || Date.now()
this.send('validation', 'validation_complete', {
outcome: validationData.isValid
? (validationData.hasWarnings ? 'pass_with_warnings' : 'pass')
: 'fail',
row_count: validationData.rowCount || 0,
duration_ms: Date.now() - start,
has_zip: !!validationData.zipIncluded
})
},

recordSubmit: function () {
var now = Date.now()
var timings = { total_session_ms: now - (this.stepTimestamps.step1 || now) }
var steps = ['step1', 'step2', 'step3']
var nexts = ['step2', 'step3', null]
steps.forEach(function (step, i) {
var nextStep = nexts[i]
if (MetricsTracker.stepTimestamps[step]) {
var end = nextStep && MetricsTracker.stepTimestamps[nextStep]
? MetricsTracker.stepTimestamps[nextStep]
: now
timings[step + '_duration_ms'] = end - MetricsTracker.stepTimestamps[step]
}
})
this.send('timing', 'session_complete', timings)
},

recordFeedback: function (rating, comment, importerId) {
this.send('feedback', 'seq_rating', {
seq_rating: rating,
comment: comment || '',
importer_id: importerId
})
},

send: function (metricType, event, payload) {
if (!CONSTANTS.ENDPOINTS.METRICS) return
var data = JSON.stringify({
metric_type: metricType,
event: event,
session_id: this.sessionId,
payload: payload
})
if (navigator.sendBeacon) {
navigator.sendBeacon(CONSTANTS.ENDPOINTS.METRICS, new Blob([data], { type: 'application/json' }))
} else {
$.ajax({
url: CONSTANTS.ENDPOINTS.METRICS,
method: 'POST',
data: data,
contentType: 'application/json',
headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }
})
}
}
}

// Initialize on document ready and turbolinks load
$(document).on('turbolinks:load', initBulkImportStepper)
})(jQuery, window.BulkraxUtils || {})
1 change: 1 addition & 0 deletions app/assets/stylesheets/bulkrax/stepper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

@import 'stepper/header';
@import 'stepper/success';
@import 'stepper/seq_feedback';
@import 'stepper/upload';
@import 'stepper/validation';
@import 'stepper/summary';
Expand Down
Loading
Loading