96037069ff
Before, when autosaving a draft, removing a repetition row would send `_destroy` inputs to the controller – but not remove the row from the DOM. This led to the `_destroy` inputs being sent again on the next autosave request, which made the controller raise (because the row fields were already deleted before). To fix this, we let the controller response remove the deleted row(s) from the DOM. Doing it using a controller response avoids the need to keep track of operations on the Javascript side: the controller can easily know which row was just deleted, and emit the relevant changes for the DOM. This keeps the autosave requests robust: even if a request is skipped (e.g. because of a network interruption), the next request will still contain the relevant informations to succeed, and not let the form in an unstable state. Fix #5470
92 lines
2.8 KiB
JavaScript
92 lines
2.8 KiB
JavaScript
import { ajax, fire, timeoutable } from '@utils';
|
|
|
|
// Manages a queue of Autosave operations,
|
|
// and sends `autosave:*` events to indicate the state of the requests.
|
|
export default class AutoSaveController {
|
|
constructor() {
|
|
this.timeoutDelay = 60000; // 1mn
|
|
this.latestPromise = Promise.resolve();
|
|
}
|
|
|
|
// Add a new autosave request to the queue.
|
|
// It will be started after the previous one finishes (to prevent older form data
|
|
// to overwrite newer data if the server does not repond in order.)
|
|
enqueueAutosaveRequest(form) {
|
|
this.latestPromise = this.latestPromise.finally(() => {
|
|
return this._sendAutosaveRequest(form)
|
|
.then(this._didSucceed)
|
|
.catch(this._didFail);
|
|
});
|
|
this._didEnqueue();
|
|
}
|
|
|
|
// Create a fetch request that saves the form.
|
|
// Returns a promise fulfilled when the request completes.
|
|
_sendAutosaveRequest(form) {
|
|
const autosavePromise = new Promise((resolve, reject) => {
|
|
if (!document.body.contains(form)) {
|
|
return reject(new Error('The form can no longer be found.'));
|
|
}
|
|
|
|
const [formData, formDataError] = this._formDataForDraft(form);
|
|
if (formDataError) {
|
|
formDataError.message = `Error while generating the form data (${formDataError.message})`;
|
|
return reject(formDataError);
|
|
}
|
|
|
|
const params = {
|
|
url: form.action,
|
|
type: form.method,
|
|
data: formData,
|
|
dataType: 'script'
|
|
};
|
|
|
|
return ajax(params)
|
|
.then(({ response }) => {
|
|
resolve(response);
|
|
})
|
|
.catch((error) => {
|
|
reject(error);
|
|
});
|
|
});
|
|
|
|
// Time out the request after a while, to avoid recent requests not starting
|
|
// because an older one is stuck.
|
|
return timeoutable(autosavePromise, this.timeoutDelay);
|
|
}
|
|
|
|
// Extract a FormData object of the form fields.
|
|
_formDataForDraft(form) {
|
|
// File inputs are handled separatly by ActiveStorage:
|
|
// exclude them from the draft (by disabling them).
|
|
// (Also Safari has issue with FormData containing empty file inputs)
|
|
const fileInputs = form.querySelectorAll(
|
|
'input[type="file"]:not([disabled]), .editable-champ-piece_justificative input:not([disabled])'
|
|
);
|
|
fileInputs.forEach((fileInput) => (fileInput.disabled = true));
|
|
|
|
// Generate the form data
|
|
let formData = null;
|
|
try {
|
|
formData = new FormData(form);
|
|
return [formData, null];
|
|
} catch (error) {
|
|
return [null, error];
|
|
} finally {
|
|
// Re-enable disabled file inputs
|
|
fileInputs.forEach((fileInput) => (fileInput.disabled = false));
|
|
}
|
|
}
|
|
|
|
_didEnqueue() {
|
|
fire(document, 'autosave:enqueue');
|
|
}
|
|
|
|
_didSucceed(response) {
|
|
fire(document, 'autosave:end', response);
|
|
}
|
|
|
|
_didFail(error) {
|
|
fire(document, 'autosave:error', error);
|
|
}
|
|
}
|