Skip to content
Open
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
9 changes: 9 additions & 0 deletions code/test-data-tracker/test-data-tracker.css
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,13 @@ img.knHeader__logo-image {
width: 80% !important; /* Medium width on medium screens */
max-width: 800px !important;
}
}

/*********************************************/
/******** Add Multiple Attachments ********/
/*********************************************/
#dropzone-field_4212 {
border: 2px dashed #A8B2BC;
border-radius: 5%;
font-weight: 500;
}
179 changes: 178 additions & 1 deletion code/test-data-tracker/test-data-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -1177,4 +1177,181 @@ $(document).on("knack-scene-render.scene_1607", function (event, scene) {
/***************************************************************/
$(document).on("knack-scene-render.any", function (event, scene) {
$(".kn-modal-bg").off("click");
});
});

/***************************************/
/*** TEST - Add multiple attachments ***/
/***************************************/
const KNACK_APP_ID = Knack.application_id;
var fieldId = 'field_4212';
var viewId = 'view_4214';
var sceneId = 'scene_1688';
var maxFilesLoaded = 10; // Maximum amount of files uploaded in dropzone

$(document).on(`knack-view-render.${viewId}`, function (event, view, data) {
var knackHeaders = {
'X-Knack-Application-ID': KNACK_APP_ID,
'Authorization': Knack.getUserToken()
}
LazyLoad.css('https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.7.2/dropzone.min.css', function () {});
LazyLoad.js(['https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.7.2/min/dropzone.min.js'], function () {
Dropzone.autoDiscover = false;
initialize();
});

var $fileInputBtn=$(`#kn-input-${fieldId} input[type=file]`);
var $formSubmitBtn = $(`#${viewId} .kn-submit button[type=submit]`);
$formSubmitBtn.prop('disabled', true);

$fileInputBtn.prop("multiple", true);
$fileInputBtn.after(`<div id="dropzone-${fieldId}" class="dropzone dropzone-knack" />`);
$fileInputBtn.hide();

var $buFiledrag=$(`#filedrag-${fieldId}`);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is declared but never used

var $form = $(`#${viewId} form`);
var $formConfirmation = $(`#${viewId} .kn-form-confirmation`);

var files = [];

function initialize() {
var upurl = `${Knack.api_url}/v1/applications/${KNACK_APP_ID}/assets/file/upload`;

var dz = new Dropzone(`div#dropzone-${fieldId}`,
{
url: upurl,
headers: knackHeaders,
paramName: 'files',
addRemoveLinks: true,
maxFiles: maxFilesLoaded,
dictDefaultMessage: '<div class="dz-message needsclick"><button type="button" class="dz-button">Drop files here or click to upload.</button><br></div>',
});
// Only allow submission when we are not actively processing images
dz.on("addedfile", function(file) { $formSubmitBtn.prop('disabled', true); });
dz.on("queuecomplete", function(file) { $formSubmitBtn.prop('disabled', false); });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is currently happening is when the upload request times out (takes longer than 30 sec to upload), the queue is considered complete and this is enabling the submit button.

I added an error event listener

    dz.on("error", function(file, msg) {
      $formSubmitBtn.prop('disabled', true)
      console.error(msg)
      })

But the queue still is being completed, so theres a race condition where the button will still be enabled. so I took that queuecomplete event handler out.

But now we need to know when to enable the button. I experimented with enabling the submit button on the success event a few lines below but its not perfect. Like if you upload two, the first one works fine (button enabled) and then the second one fails, the error has disabled the button. We could reenable the button when a person removes the broken file, but need to check that there are still successfully uploaded files

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I shared this in dev sync and @mateoclarke had a good idea, that if a file upload fails, to automatically remove it from the UI instead of making a user do that step. I can look at the code to this if you want!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of the drop zone automatically removing the failed uploaded file from the UI to enable the submit button.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo I really like that approach! If all of them failed would they essentially just start back at the beginning with the original step to add files again? And if failed upload files were removed, could we have a message at the top of the pop up letting users know that occurred?

dz.on("removedfile", deleteFile);
dz.on("maxfilesexceeded", function(file) { this.removeFile(file); });
dz.on("success", function(file, responseText, e) {
file['responseText'] = responseText;
files.push(file);
});
$formSubmitBtn.on('click', handleFormSubmit);
}

function deleteFile(removedFile) {
// Update the files array
files = files.filter(file => file.name != removedFile.name);
}

function showMessage(err, message) {
$formConfirmation.show();
var clazz = err === true ? "is-error" : "success";
$formConfirmation.append(`<div class="kn-message ${clazz}"><p>${message}</p></div>`);
if (err === true) {
$formConfirmation.find('.kn-form-reload').hide();
} else {
$formConfirmation.find('.kn-form-reload').show();
}
}

function clearMessages() {
// Form messages come from Knack JS - they know the form is still visible. form-confirm is used
// when form might have been hidden
$formConfirmation.find('.kn-message').remove();
$form.find('.kn-message').remove();
$formConfirmation.hide();
}

function handleFormSubmit(event) {
event.preventDefault();

// Show progress and prevent an impatient click
Knack.showSpinner();
$formSubmitBtn.addClass("is-loading");
$formSubmitBtn.prop('disabled', true);

// Grab all other data items from the form into a simple key-value object
var formData = $form.serializeArray().reduce(function (output, value) {
var field = value.name.split('-').pop(); // Not sure why but multichoice does `view_id-field_id`
output[field] = value.value
return output
}, {});

var targetUrl = `${Knack.api_url}/v1/pages/${sceneId}/views/${viewId}/records`

const upload = file => new Promise((res, rej) => {
// Add the one file we are submitting to the existing form data
formDataClone = { ...formData };
formDataClone[fieldId] = file.responseText.id;

var jqXHR = $.ajax({
type: 'POST',
url: targetUrl,
headers: knackHeaders,
data: formDataClone,
enctype: 'multipart/form-data'
});

// Mark uploaded file with record created
// TODO - should we just remove it from the dropzone? That might be
// simpler than maintaining a list in memory
jqXHR.done(() => {
file['knackRecordCreated'] = true;
res();
});

jqXHR.fail((xhr, textStatus, errorThrown) => {
let json = JSON.parse(xhr.responseText);
let errMsg = "Unknown Error";
if (json && json.errors) {
errMsg = json.errors.reduce((acc, cur) => acc + `<p>${cur.message}</p>`, "");
}
rej(errMsg);
});
});

clearMessages();
// TODO handle HTTP 429 events
// TODO try #s > 2 to see how well it works
asyncPool(2, files, upload)
.then(() => {
console.log("success");
$form.hide();
history.back(); // go back to parent page
//showMessage(false, "Form submitted"); // reload form
})
.catch((errors) => {
log("errors");
console.log(errors);
$formSubmitBtn.removeClass("is-loading");
Knack.hideSpinner();
showMessage(true, errors);
$formSubmitBtn.prop('disabled', false);
});
}
});

function asyncPool(poolLimit, array, iteratorFn) {
let i = 0;
const ret = [];
const executing = [];
const enqueue = function() {
if (i === array.length) {
return Promise.resolve();
}
const item = array[i++];
const p = Promise.resolve().then(() => iteratorFn(item, array));
ret.push(p);

let r = Promise.resolve();

if (poolLimit <= array.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
r = Promise.race(executing);
}
}
return r.then(() => enqueue());
};
return enqueue().then(() => Promise.all(ret));
}