fix(cropper): render reason checkboxes, gate the Others field, add cropped preview
Three defects from the Carbon rewrite, found while fixing the review panel: - reason checkboxes were invisible: Carbon's .cds--checkbox input is visually clipped and needs the .cds--form-item/.cds--checkbox-wrapper + .cds--checkbox-label markup to draw the box - 'Others (please specify)' now appears only when 'Others not specified.' is ticked, and clears when unticked (hidden text must not be submitted); the textarea gets Carbon's __wrapper so it has a proper height - new 'Cropped preview' between the original and cleaned previews, so the operator sees what will be uploaded; refreshed on pointer release over the pad and after load/clean - REGRESSION: Vue.use(Croppa) had been dropped — the crop canvas never rendered, and saving only worked because the cleaned image bypassed generateDataUrl. Restored. - REGRESSION: the OTP field bound v-model="otpSave" while the data property is "otp", so Vue's render threw ReferenceError and froze the panel as soon as accept was clicked (the OTP could never be entered). Bound to otp; the croppa instance is now reached via $refs.croppa (the v-model object is not always the live instance) - after a successful save the review panel stayed open: reset() recreated an empty message object, which the template treats as truthy. message is null when idle now. 45 tests pass; save round-trip and reject/Others flow verified in the browser.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
the save-time OTP (required by a2loft). Notifications are Carbon toasts. */
|
the save-time OTP (required by a2loft). Notifications are Carbon toasts. */
|
||||||
/* global Vue, axios, Croppa, customElements */
|
/* global Vue, axios, Croppa, customElements */
|
||||||
/* window.__SESSION__ is injected server-side by /cropper */
|
/* window.__SESSION__ is injected server-side by /cropper */
|
||||||
|
Vue.use(Croppa); // registers the <croppa> component — without this the crop UI never renders
|
||||||
Vue.config.ignoredElements = [/^cds-/];
|
Vue.config.ignoredElements = [/^cds-/];
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
@@ -15,14 +16,13 @@ Vue.config.ignoredElements = [/^cds-/];
|
|||||||
data: {
|
data: {
|
||||||
loading: true,
|
loading: true,
|
||||||
session: null,
|
session: null,
|
||||||
message: {
|
message: null, // set once a picture is loaded; null hides the review panel
|
||||||
message: { caption: null, file_url: null, domain: '' },
|
|
||||||
},
|
|
||||||
agent: null,
|
agent: null,
|
||||||
process: null,
|
process: null,
|
||||||
reviewed: false,
|
reviewed: false,
|
||||||
accepted: false,
|
accepted: false,
|
||||||
cleanedImage: null,
|
cleanedImage: null,
|
||||||
|
cropPreview: '',
|
||||||
requiresCleaning: '',
|
requiresCleaning: '',
|
||||||
otp: '',
|
otp: '',
|
||||||
reviewOptions: [
|
reviewOptions: [
|
||||||
@@ -62,6 +62,19 @@ Vue.config.ignoredElements = [/^cds-/];
|
|||||||
canSave: function () {
|
canSave: function () {
|
||||||
return !this.busy.save && this.otp.length > 0 && this.requiresCleaning !== '';
|
return !this.busy.save && this.otp.length > 0 && this.requiresCleaning !== '';
|
||||||
},
|
},
|
||||||
|
otherOptionSelected: function () {
|
||||||
|
return this.selectedReviewOptions.indexOf('Others not specified.') >= 0;
|
||||||
|
},
|
||||||
|
croppaInstance: function () {
|
||||||
|
// the v-model object is not always the live instance; the ref is
|
||||||
|
return this.$refs.croppa || this.myCroppa;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
otherOptionSelected: function (selected) {
|
||||||
|
if (!selected) this.otherReviewOption = ''; // hidden field must not contribute
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function () {
|
created: function () {
|
||||||
@@ -96,8 +109,31 @@ Vue.config.ignoredElements = [/^cds-/];
|
|||||||
this.toasts = this.toasts.filter(function (t) { return t.id !== id; });
|
this.toasts = this.toasts.filter(function (t) { return t.id !== id; });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current crop result — the operator should see what will be uploaded.
|
||||||
|
* croppa loads its source image asynchronously, so retry briefly until
|
||||||
|
* the canvas can produce a URL.
|
||||||
|
*/
|
||||||
|
refreshCropPreview: function (attempt) {
|
||||||
|
var self = this;
|
||||||
|
var tries = attempt || 0;
|
||||||
|
this.$nextTick(function () {
|
||||||
|
try {
|
||||||
|
var croppa = self.croppaInstance;
|
||||||
|
self.cropPreview =
|
||||||
|
croppa && croppa.generateDataUrl ? croppa.generateDataUrl('image/jpeg') || '' : '';
|
||||||
|
} catch (error) {
|
||||||
|
self.cropPreview = '';
|
||||||
|
}
|
||||||
|
if (!self.cropPreview && tries < 12) {
|
||||||
|
setTimeout(function () { self.refreshCropPreview(tries + 1); }, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
reset: function () {
|
reset: function () {
|
||||||
this.message = { message: { caption: null, file_url: null, domain: '' } };
|
this.cropPreview = '';
|
||||||
|
this.message = null;
|
||||||
this.agent = null;
|
this.agent = null;
|
||||||
this.process = null;
|
this.process = null;
|
||||||
this.reviewed = false;
|
this.reviewed = false;
|
||||||
@@ -137,6 +173,8 @@ Vue.config.ignoredElements = [/^cds-/];
|
|||||||
}
|
}
|
||||||
this.message.message.file_url = image.data.data;
|
this.message.message.file_url = image.data.data;
|
||||||
this.cleanedImage = null;
|
this.cleanedImage = null;
|
||||||
|
this.cropPreview = '';
|
||||||
|
this.$nextTick(this.refreshCropPreview); // croppa needs the image first
|
||||||
this.agent = null;
|
this.agent = null;
|
||||||
this.process = this.message.message.process || null;
|
this.process = this.message.message.process || null;
|
||||||
if (this.message.message.upload_id) {
|
if (this.message.message.upload_id) {
|
||||||
@@ -156,7 +194,7 @@ Vue.config.ignoredElements = [/^cds-/];
|
|||||||
var self = this;
|
var self = this;
|
||||||
this.busy.clean = true;
|
this.busy.clean = true;
|
||||||
try {
|
try {
|
||||||
var dataUrl = this.myCroppa.generateDataUrl('image/jpeg');
|
var dataUrl = this.croppaInstance.generateDataUrl('image/jpeg');
|
||||||
var response = await fetch('/cropper/api/removebg', {
|
var response = await fetch('/cropper/api/removebg', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -174,6 +212,7 @@ Vue.config.ignoredElements = [/^cds-/];
|
|||||||
var fr = new FileReader();
|
var fr = new FileReader();
|
||||||
fr.onload = function () { self.cleanedImage = fr.result; };
|
fr.onload = function () { self.cleanedImage = fr.result; };
|
||||||
fr.readAsDataURL(blob);
|
fr.readAsDataURL(blob);
|
||||||
|
this.refreshCropPreview();
|
||||||
} catch ({ message }) {
|
} catch ({ message }) {
|
||||||
this.toast('error', 'Image Cleanup Failed', message);
|
this.toast('error', 'Image Cleanup Failed', message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -252,7 +291,7 @@ Vue.config.ignoredElements = [/^cds-/];
|
|||||||
var { data } = await axios.post(
|
var { data } = await axios.post(
|
||||||
'/cropper/profile/picture?otp=' + encodeURIComponent(this.otp),
|
'/cropper/profile/picture?otp=' + encodeURIComponent(this.otp),
|
||||||
{
|
{
|
||||||
pictureDataUrl: this.cleanedImage || this.myCroppa.generateDataUrl('image/jpeg'),
|
pictureDataUrl: this.cleanedImage || this.croppaInstance.generateDataUrl('image/jpeg'),
|
||||||
phone: this.message.message.caption,
|
phone: this.message.message.caption,
|
||||||
message: this.message,
|
message: this.message,
|
||||||
requiresCleaning: this.requiresCleaning !== 'No',
|
requiresCleaning: this.requiresCleaning !== 'No',
|
||||||
|
|||||||
@@ -81,11 +81,17 @@
|
|||||||
<div class="editor-grid mt-2">
|
<div class="editor-grid mt-2">
|
||||||
<div class="editor-media">
|
<div class="editor-media">
|
||||||
<label class="cds--label">Original — crop it</label>
|
<label class="cds--label">Original — crop it</label>
|
||||||
<div class="croppa-pad mt-1">
|
<div class="croppa-pad mt-1" v-on:mouseup="refreshCropPreview" v-on:touchend="refreshCropPreview">
|
||||||
<croppa v-model="myCroppa" :show-remove-button="false" :width="250" :height="267.44" :quality="2">
|
<croppa ref="croppa" v-model="myCroppa" :show-remove-button="false" :width="250" :height="267.44" :quality="2">
|
||||||
<img slot="initial" crossOrigin="anonymous" :src="message.message.file_url" style="width: 300px">
|
<img slot="initial" crossOrigin="anonymous" :src="message.message.file_url" style="width: 300px">
|
||||||
</croppa>
|
</croppa>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="cropPreview">
|
||||||
|
<label class="cds--label mt-2" style="display:block;">Cropped preview</label>
|
||||||
|
<div class="croppa-pad mt-1">
|
||||||
|
<img :src="cropPreview" alt="cropped preview">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template v-if="cleanedImage">
|
<template v-if="cleanedImage">
|
||||||
<label class="cds--label mt-2" style="display:block;">Cleaned preview</label>
|
<label class="cds--label mt-2" style="display:block;">Cleaned preview</label>
|
||||||
<div class="croppa-pad mt-1">
|
<div class="croppa-pad mt-1">
|
||||||
@@ -122,7 +128,7 @@
|
|||||||
<div class="row-gap" style="align-items:flex-end;">
|
<div class="row-gap" style="align-items:flex-end;">
|
||||||
<div style="min-width:150px; max-width:200px;">
|
<div style="min-width:150px; max-width:200px;">
|
||||||
<label class="cds--label" for="otpSave">OTP</label>
|
<label class="cds--label" for="otpSave">OTP</label>
|
||||||
<input id="otpSave" class="cds--text-input" v-model.trim="otpSave" autocomplete="one-time-code">
|
<input id="otpSave" class="cds--text-input" v-model.trim="otp" autocomplete="one-time-code">
|
||||||
</div>
|
</div>
|
||||||
<cds-button v-bind:disabled="!canSave" v-on:click="saveChanges">
|
<cds-button v-bind:disabled="!canSave" v-on:click="saveChanges">
|
||||||
<span v-if="!busy.save">Crop and upload picture</span>
|
<span v-if="!busy.save">Crop and upload picture</span>
|
||||||
@@ -134,15 +140,20 @@
|
|||||||
|
|
||||||
<div v-if="reviewed && !accepted" class="mt-2">
|
<div v-if="reviewed && !accepted" class="mt-2">
|
||||||
<label class="cds--label">Please state why this picture is not good enough.</label>
|
<label class="cds--label">Please state why this picture is not good enough.</label>
|
||||||
<div class="review-options mt-1">
|
<div class="review-options mt-1" v-for="item in reviewOptions" v-bind:key="item.index">
|
||||||
<div v-for="item in reviewOptions" v-bind:key="item.index" style="display:flex; align-items:center; gap:0.5rem; margin-bottom:0.35rem;">
|
<div class="cds--form-item cds--checkbox-wrapper">
|
||||||
<input type="checkbox" class="cds--checkbox" v-bind:id="'reason-' + item.index" v-bind:value="item.name" v-model="selectedReviewOptions">
|
<input class="cds--checkbox" type="checkbox" v-bind:id="'reason-' + item.index"
|
||||||
<label v-bind:for="'reason-' + item.index" style="margin:0;" v-on:click.prevent>{{ item.name }}</label>
|
v-bind:value="item.name" v-model="selectedReviewOptions">
|
||||||
|
<label class="cds--checkbox-label" v-bind:for="'reason-' + item.index">
|
||||||
|
<span class="cds--checkbox-label-text">{{ item.name }}</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1" style="max-width:480px;">
|
<div class="mt-1" style="max-width:480px;" v-if="otherOptionSelected">
|
||||||
<label class="cds--label" for="otherReviewOption">Others (please specify).</label>
|
<label class="cds--label" for="otherReviewOption">Others (please specify).</label>
|
||||||
<textarea id="otherReviewOption" rows="3" class="cds--text-area" v-model="otherReviewOption"></textarea>
|
<div class="cds--text-area__wrapper">
|
||||||
|
<textarea id="otherReviewOption" rows="3" class="cds--text-area" v-model="otherReviewOption"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-gap mt-2">
|
<div class="row-gap mt-2">
|
||||||
<cds-button kind="danger" v-bind:disabled="!canReview" v-on:click="reviewPicture()">
|
<cds-button kind="danger" v-bind:disabled="!canReview" v-on:click="reviewPicture()">
|
||||||
|
|||||||
Reference in New Issue
Block a user