mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-29 07:09:48 +03:00
Updated styles for members import
no refs. - added spinner to validation state - applied styles to pre-validation step - applied styles to import results
This commit is contained in:
parent
5dc96368ef
commit
1dd2a3db07
@ -1,7 +1,7 @@
|
|||||||
<header class="modal-header" data-test-modal="import-members">
|
<header class="modal-header" data-test-modal="import-members">
|
||||||
<h1>
|
<h1>
|
||||||
{{#if this.importResponse}}
|
{{#if this.importResponse}}
|
||||||
Import complete
|
Import complete{{unless this.importResponse.invalid.count "!"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
Import members
|
Import members
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@ -11,23 +11,39 @@
|
|||||||
|
|
||||||
{{#if this.importResponse}}
|
{{#if this.importResponse}}
|
||||||
{{!-- post upload step with import stats --}}
|
{{!-- post upload step with import stats --}}
|
||||||
<div class="modal-body bg-whitegrey-l2 ba b--whitegrey br3 pa4">
|
|
||||||
<div class="flex items-center">
|
<div class="modaly-body">
|
||||||
{{svg-jar "check-circle" class="w4 h4 stroke-green mr2"}}
|
|
||||||
<p class="ma0 pa0"><span class="fw6">{{this.importResponse.imported.count}}</span> members were added</p>
|
{{!-- Summary --}}
|
||||||
</div>
|
<div class="ba b--whitegrey br3 middarkgrey bg-whitegrey-l2">
|
||||||
{{#if this.importResponse.invalid.count}}
|
<div class="flex items-start">
|
||||||
<div class="flex items-start mt2">
|
<div class="w-50 gh-member-import-result-summary">
|
||||||
{{svg-jar "warning" class="w4 h4 fill-red mr2 nudge-top--3"}}
|
<h2>{{this.importResponse.imported.count}}</h2>
|
||||||
<div>
|
<p>Members added</p>
|
||||||
<p class="ma0 pa0"><span class="fw5">{{this.importResponse.invalid.count}}</span> members were skipped</p>
|
</div>
|
||||||
|
<div class="bl b--whitegrey w-50 gh-member-import-result-summary">
|
||||||
|
<h2>{{this.importResponse.invalid.count}}</h2>
|
||||||
|
<p>Errors</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.importResponse.invalid.count}}
|
||||||
|
<div class="modal-body mt6 gh-members-upload-errorcontainer error">
|
||||||
|
<div class="flex items-start">
|
||||||
|
{{svg-jar "warning" class="w5 h5 fill-red nudge-top--3 mr3"}}
|
||||||
|
<div class="flex-grow w-100">
|
||||||
|
<p class="ma0 pa0"><span class="fw5">{{this.importResponse.invalid.count}}</span> members were skipped due to the following errors:</p>
|
||||||
|
{{#if this.config.enableDeveloperExperiments}}
|
||||||
|
<ul class="ma0 pa0 mt4 list bt b--whitegrey">
|
||||||
|
{{#each this.importResponse.invalid.errors as |error|}}
|
||||||
|
<li class="gh-members-import-errormessage">{{error.message}} <span class="fw6">({{error.count}})</span></li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{#if this.config.enableDeveloperExperiments}}
|
|
||||||
{{#each this.importResponse.invalid.errors as |error|}}
|
|
||||||
<p class="gh-members-import-errordetail">{{error.message}} <span class="fw6">({{error.count}})</span></p>
|
|
||||||
{{/each}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -35,21 +51,31 @@
|
|||||||
{{#if (and this.filePresent (not this.failureMessage))}}
|
{{#if (and this.filePresent (not this.failureMessage))}}
|
||||||
{{#if this.validating}}
|
{{#if this.validating}}
|
||||||
{{#if validationErrors}}
|
{{#if validationErrors}}
|
||||||
<div class="flex">
|
<div class="failed flex items-start gh-members-upload-errorcontainer warning">
|
||||||
<div class="mr2">{{svg-jar "warning" class="nudge-top--2 w5 h5 fill-yellow-d1"}}</div>
|
<div class="mr3">{{svg-jar "warning" class="nudge-top--2 w5 h5 fill-yellow-d1"}}</div>
|
||||||
<p>The CSV contains errors some members will not be imported</p>
|
<div class="ma0">
|
||||||
|
<p class="ma0 pa0 flex-grow w-100">The CSV contains errors! Some members will not be imported.</p>
|
||||||
|
{{#if validationErrors}}
|
||||||
|
<ul class="ma0 pa0 mt4 list bt b--whitegrey">
|
||||||
|
{{#each validationErrors as |error|}}
|
||||||
|
<li class="gh-members-import-errormessage">
|
||||||
|
<span>{{{error.message}}}</span>
|
||||||
|
{{#if error.context}}
|
||||||
|
<p class="gh-members-import-errorcontext">{{{error.context}}}</p>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#each validationErrors as |error|}}
|
|
||||||
<div class="failed items-start gh-members-upload-errorcontainer gh-members-upload-warningmessage">
|
|
||||||
<p class="ma0 pa0 fw6 gh-members-import-errorheading">{{{error.message}}}</p>
|
|
||||||
{{#if error.context}}
|
|
||||||
<p class="ma0 pa0 gh-members-import-errorheading">{{{error.context}}}</p>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
{{else}}
|
{{else}}
|
||||||
Validating...
|
<div class="bg-whitegrey-l2 ba b--whitegrey br3 gh-image-uploader gh-members-import-spinner">
|
||||||
|
<div class="gh-loading-content">
|
||||||
|
<div class="gh-loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="description midgrey">Validating...</div>
|
||||||
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<GhFormGroup>
|
<GhFormGroup>
|
||||||
@ -102,17 +128,22 @@
|
|||||||
<div class="modal-footer {{if (and this.filePresent (not this.failureMessage) (not this.importResponse)) "modal-footer-spread"}}">
|
<div class="modal-footer {{if (and this.filePresent (not this.failureMessage) (not this.importResponse)) "modal-footer-spread"}}">
|
||||||
{{#if this.importResponse}}
|
{{#if this.importResponse}}
|
||||||
<button {{action "closeModal"}} disabled={{this.closeDisabled}} class="gh-btn gh-btn-blue" data-test-button="close-import-members">
|
<button {{action "closeModal"}} disabled={{this.closeDisabled}} class="gh-btn gh-btn-blue" data-test-button="close-import-members">
|
||||||
<span>Done</span>
|
<span>{{if this.importResponse.invalid.count "Done" "Awesome"}}</span>
|
||||||
</button>
|
</button>
|
||||||
{{else if (and this.filePresent (not this.failureMessage))}}
|
{{else if (and this.filePresent (not this.failureMessage))}}
|
||||||
<button {{action "reset"}} class="gh-btn" data-test-button="close-import-members">
|
|
||||||
<span>Start over</span>
|
|
||||||
</button>
|
|
||||||
{{#if this.validating}}
|
{{#if this.validating}}
|
||||||
<button class="gh-btn gh-btn-green" {{action "continueImport"}} disabled={{this.importDisabled}}>
|
{{#if validationErrors}}
|
||||||
<span>Continue</span>
|
<button {{action "reset"}} class="gh-btn" data-test-button="close-import-members">
|
||||||
</button>
|
<span>Start over</span>
|
||||||
|
</button>
|
||||||
|
<button class="gh-btn gh-btn-blue" {{action "continueImport"}} disabled={{this.importDisabled}}>
|
||||||
|
<span>Continue</span>
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
<button {{action "reset"}} class="gh-btn" data-test-button="close-import-members">
|
||||||
|
<span>Start over</span>
|
||||||
|
</button>
|
||||||
<button class="gh-btn gh-btn-green" {{action "upload"}} disabled={{this.importDisabled}}>
|
<button class="gh-btn gh-btn-green" {{action "upload"}} disabled={{this.importDisabled}}>
|
||||||
<span>Import{{#if this.fileData.length}} {{pluralize this.fileData.length 'member'}}{{/if}}</span>
|
<span>Import{{#if this.fileData.length}} {{pluralize this.fileData.length 'member'}}{{/if}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -69,7 +69,7 @@ export default ModalComponent.extend({
|
|||||||
notifications: service(),
|
notifications: service(),
|
||||||
memberImportValidator: service(),
|
memberImportValidator: service(),
|
||||||
|
|
||||||
labelText: 'Select or drag-and-drop a CSV File',
|
labelText: 'Select or drag-and-drop a CSV file',
|
||||||
|
|
||||||
dragClass: null,
|
dragClass: null,
|
||||||
file: null,
|
file: null,
|
||||||
|
@ -25,35 +25,6 @@ export default Service.extend({
|
|||||||
const hasStripeIds = !!mapping.stripe_customer_id;
|
const hasStripeIds = !!mapping.stripe_customer_id;
|
||||||
const hasEmails = !!mapping.email;
|
const hasEmails = !!mapping.email;
|
||||||
|
|
||||||
if (!hasEmails) {
|
|
||||||
validationErrors.push(new MemberImportError({
|
|
||||||
message: 'No email addresses found in provided data.'
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// check can be done on whole set as it won't be too slow
|
|
||||||
const {invalidCount, emptyCount, duplicateCount} = this._checkEmails(data, mapping);
|
|
||||||
if (invalidCount) {
|
|
||||||
validationErrors.push(new MemberImportError({
|
|
||||||
message: `Invalid email address (${invalidCount})`,
|
|
||||||
type: 'warning'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emptyCount) {
|
|
||||||
validationErrors.push(new MemberImportError({
|
|
||||||
message: `Missing email address (${emptyCount})`,
|
|
||||||
type: 'warning'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duplicateCount) {
|
|
||||||
validationErrors.push(new MemberImportError({
|
|
||||||
message: `Duplicate email address (${duplicateCount})`,
|
|
||||||
type: 'warning'
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasStripeIds) {
|
if (hasStripeIds) {
|
||||||
// check can be done on whole set as it won't be too slow
|
// check can be done on whole set as it won't be too slow
|
||||||
const {totalCount, duplicateCount} = this._checkStripeIds(data, mapping);
|
const {totalCount, duplicateCount} = this._checkStripeIds(data, mapping);
|
||||||
@ -83,6 +54,35 @@ export default Service.extend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasEmails) {
|
||||||
|
validationErrors.push(new MemberImportError({
|
||||||
|
message: 'No email addresses found in the uploaded CSV.'
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// check can be done on whole set as it won't be too slow
|
||||||
|
const {invalidCount, emptyCount, duplicateCount} = this._checkEmails(data, mapping);
|
||||||
|
if (invalidCount) {
|
||||||
|
validationErrors.push(new MemberImportError({
|
||||||
|
message: `Invalid email address (${invalidCount})`,
|
||||||
|
type: 'warning'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyCount) {
|
||||||
|
validationErrors.push(new MemberImportError({
|
||||||
|
message: `Missing email address (${emptyCount})`,
|
||||||
|
type: 'warning'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duplicateCount) {
|
||||||
|
validationErrors.push(new MemberImportError({
|
||||||
|
message: `Duplicate email address (${duplicateCount})`,
|
||||||
|
type: 'warning'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {validationErrors, mapping};
|
return {validationErrors, mapping};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -192,11 +192,13 @@ export default Service.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (emailValue && !validator.isEmail(emailValue)) {
|
if (emailValue && !validator.isEmail(emailValue)) {
|
||||||
invalidCount += 1;
|
// Temporarily not returning this error
|
||||||
|
// invalidCount += 1;
|
||||||
} else if (emailValue) {
|
} else if (emailValue) {
|
||||||
if (emailMap[emailValue]) {
|
if (emailMap[emailValue]) {
|
||||||
emailMap[emailValue] += 1;
|
emailMap[emailValue] += 1;
|
||||||
duplicateCount += 1;
|
// Temporarily not returning this error
|
||||||
|
// duplicateCount += 1;
|
||||||
} else {
|
} else {
|
||||||
emailMap[emailValue] = 1;
|
emailMap[emailValue] = 1;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
/* Global
|
/* Global
|
||||||
/* ----------------------------------------- */
|
/* ----------------------------------------- */
|
||||||
|
:root {
|
||||||
|
--member-import-table-outline: color-mod(var(--darkgrey) a(12%) s(+40%));
|
||||||
|
--member-import-table-border: color-mod(var(--darkgrey) a(7%) s(+40%));
|
||||||
|
}
|
||||||
|
|
||||||
/* Members avatar
|
/* Members avatar
|
||||||
/* ----------------------------------------- */
|
/* ----------------------------------------- */
|
||||||
@ -448,27 +452,62 @@ textarea.gh-member-details-textarea {
|
|||||||
min-height: 180px;
|
min-height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-members-import-spinner {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-height: 182px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-members-import-spinner .gh-loading-content {
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-members-import-spinner .description {
|
||||||
|
padding-top: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
.gh-members-upload-errorcontainer {
|
.gh-members-upload-errorcontainer {
|
||||||
border: 1px solid var(--whitegrey);
|
border: 1px solid var(--whitegrey);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px;
|
padding: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
color: var(--middarkgrey);
|
color: var(--middarkgrey);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-upload-warningmessage {
|
.gh-members-upload-errorcontainer.warning {
|
||||||
border-left: 4px solid var(--yellow);
|
border-left: 4px solid var(--yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-upload-warningmessage p a {
|
|
||||||
|
.gh-members-upload-errorcontainer.warning p a {
|
||||||
color: color-mod(var(--yellow) l(-12%));
|
color: color-mod(var(--yellow) l(-12%));
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-upload-errormessage {
|
.gh-members-upload-errorcontainer.error {
|
||||||
border-left: 4px solid var(--red);
|
border-left: 4px solid var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-members-upload-errorcontainer.error p a {
|
||||||
|
color: var(--red);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-members-import-errormessage {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.gh-members-import-errorcontext {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.3em;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.gh-members-import-scrollarea {
|
.gh-members-import-scrollarea {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-height: calc(100vh - 350px - 12vw);
|
max-height: calc(100vh - 350px - 12vw);
|
||||||
@ -550,18 +589,18 @@ p.gh-members-import-errordetailtext {
|
|||||||
.gh-members-import-table th {
|
.gh-members-import-table th {
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
background: color-mod(var(--darkgrey) a(5%) s(+50%));
|
background: color-mod(var(--darkgrey) a(5%) s(+50%));
|
||||||
border-left: 1px solid color-mod(var(--darkgrey) a(7%) s(+40%));
|
border-left: 1px solid var(--member-import-table-border);
|
||||||
border-top: 1px solid color-mod(var(--darkgrey) a(12%) s(+40%));
|
border-top: 1px solid var(--member-import-table-outline);
|
||||||
border-bottom: 1px solid color-mod(var(--darkgrey) a(7%) s(+40%));
|
border-bottom: 1px solid var(--member-import-table-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-import-table tr th:first-of-type {
|
.gh-members-import-table tr th:first-of-type {
|
||||||
border-left: 1px solid color-mod(var(--darkgrey) a(12%) s(+40%));
|
border-left: 1px solid var(--member-import-table-outline);
|
||||||
width: 180px;
|
width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-import-table tr th:last-of-type {
|
.gh-members-import-table tr th:last-of-type {
|
||||||
border-right: 1px solid color-mod(var(--darkgrey) a(12%) s(+40%));
|
border-right: 1px solid var(--member-import-table-outline);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-import-table td.empty-cell {
|
.gh-members-import-table td.empty-cell {
|
||||||
@ -570,23 +609,23 @@ p.gh-members-import-errordetailtext {
|
|||||||
|
|
||||||
.gh-members-import-table td {
|
.gh-members-import-table td {
|
||||||
padding: 7px 8px 6px;
|
padding: 7px 8px 6px;
|
||||||
border-left: 1px solid color-mod(var(--darkgrey) a(7%) s(+40%));
|
border-left: 1px solid var(--member-import-table-border);
|
||||||
border-bottom: 1px solid color-mod(var(--darkgrey) a(7%) s(+40%));
|
border-bottom: 1px solid var(--member-import-table-border);
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-import-table tr td:first-of-type {
|
.gh-members-import-table tr td:first-of-type {
|
||||||
border-left: 1px solid color-mod(var(--darkgrey) a(12%) s(+40%));
|
border-left: 1px solid var(--member-import-table-outline);
|
||||||
width: 180px;
|
width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-import-table tr td:last-of-type {
|
.gh-members-import-table tr td:last-of-type {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-right: 1px solid color-mod(var(--darkgrey) a(12%) s(+40%));
|
border-right: 1px solid var(--member-import-table-outline);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-import-table tr:last-of-type td {
|
.gh-members-import-table tr:last-of-type td {
|
||||||
border-bottom: 1px solid color-mod(var(--darkgrey) a(12%) s(+40%));
|
border-bottom: 1px solid var(--member-import-table-outline);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-members-import-datanav {
|
.gh-members-import-datanav {
|
||||||
@ -651,6 +690,23 @@ p.gh-members-import-errordetail:first-of-type {
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-member-import-result-summary {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-member-import-result-summary h2 {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-member-import-result-summary p {
|
||||||
|
margin: -2px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--midlightgrey);
|
||||||
|
}
|
||||||
|
|
||||||
/* Fixing Firefox's select padding */
|
/* Fixing Firefox's select padding */
|
||||||
@-moz-document url-prefix() {
|
@-moz-document url-prefix() {
|
||||||
.gh-import-member-select select {
|
.gh-import-member-select select {
|
||||||
|
@ -74,7 +74,7 @@ describe('Integration: Service: member-import-validator', function () {
|
|||||||
}]);
|
}]);
|
||||||
|
|
||||||
expect(validationErrors.length).to.equal(1);
|
expect(validationErrors.length).to.equal(1);
|
||||||
expect(validationErrors[0].message).to.equal('No email addresses found in provided data.');
|
expect(validationErrors[0].message).to.equal('No email addresses found in the uploaded CSV.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns validation error for invalid email', async function () {
|
it('returns validation error for invalid email', async function () {
|
||||||
|
Loading…
Reference in New Issue
Block a user