mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-30 21:40:39 +03:00
🔥 Removed all subscriber feature related code (#1337)
refs https://github.com/TryGhost/Ghost/pull/11153 - Removed all subscriber feature related code - The feature is being substituted by members
This commit is contained in:
parent
3c2185a3bf
commit
7e3412ce8e
@ -20,9 +20,6 @@ const FeatureFlagComponent = Component.extend({
|
|||||||
return this._flagValue;
|
return this._flagValue;
|
||||||
},
|
},
|
||||||
set(key, value) {
|
set(key, value) {
|
||||||
if (this.flag === 'members' && value === true) {
|
|
||||||
this.set(`feature.subscribers`, false);
|
|
||||||
}
|
|
||||||
return this.set(`feature.${this.flag}`, value);
|
return this.set(`feature.${this.flag}`, value);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
|
||||||
import {alias} from '@ember/object/computed';
|
|
||||||
import {task} from 'ember-concurrency';
|
|
||||||
|
|
||||||
export default ModalComponent.extend({
|
|
||||||
// Allowed actions
|
|
||||||
confirm: () => {},
|
|
||||||
|
|
||||||
subscriber: alias('model'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
confirm() {
|
|
||||||
this.deleteSubscriber.perform();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
deleteSubscriber: task(function* () {
|
|
||||||
yield this.confirm();
|
|
||||||
}).drop()
|
|
||||||
});
|
|
@ -1,43 +0,0 @@
|
|||||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
|
||||||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
|
||||||
import {computed} from '@ember/object';
|
|
||||||
|
|
||||||
export default ModalComponent.extend({
|
|
||||||
labelText: 'Select or drag-and-drop a CSV File',
|
|
||||||
|
|
||||||
response: null,
|
|
||||||
closeDisabled: false,
|
|
||||||
|
|
||||||
// Allowed actions
|
|
||||||
confirm: () => {},
|
|
||||||
|
|
||||||
uploadUrl: computed(function () {
|
|
||||||
return `${ghostPaths().apiRoot}/subscribers/csv/`;
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
uploadStarted() {
|
|
||||||
this.set('closeDisabled', true);
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadFinished() {
|
|
||||||
this.set('closeDisabled', false);
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadSuccess(response) {
|
|
||||||
this.set('response', response.meta.stats);
|
|
||||||
// invoke the passed in confirm action
|
|
||||||
this.confirm();
|
|
||||||
},
|
|
||||||
|
|
||||||
confirm() {
|
|
||||||
// noop - we don't want the enter key doing anything
|
|
||||||
},
|
|
||||||
|
|
||||||
closeModal() {
|
|
||||||
if (!this.closeDisabled) {
|
|
||||||
this._super(...arguments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,47 +0,0 @@
|
|||||||
import ModalComponent from 'ghost-admin/components/modal-base';
|
|
||||||
import {alias} from '@ember/object/computed';
|
|
||||||
import {A as emberA} from '@ember/array';
|
|
||||||
import {isInvalidError} from 'ember-ajax/errors';
|
|
||||||
import {task} from 'ember-concurrency';
|
|
||||||
|
|
||||||
export default ModalComponent.extend({
|
|
||||||
|
|
||||||
subscriber: alias('model'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
updateEmail(newEmail) {
|
|
||||||
this.set('subscriber.email', newEmail);
|
|
||||||
this.set('subscriber.hasValidated', emberA());
|
|
||||||
this.get('subscriber.errors').clear();
|
|
||||||
},
|
|
||||||
|
|
||||||
confirm() {
|
|
||||||
this.addSubscriber.perform();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addSubscriber: task(function* () {
|
|
||||||
try {
|
|
||||||
yield this.confirm();
|
|
||||||
this.send('closeModal');
|
|
||||||
} catch (error) {
|
|
||||||
// TODO: server-side validation errors should be serialized
|
|
||||||
// properly so that errors are added to model.errors automatically
|
|
||||||
if (error && isInvalidError(error)) {
|
|
||||||
let [firstError] = error.payload.errors;
|
|
||||||
let {context} = firstError;
|
|
||||||
|
|
||||||
if (context && context.match(/email/i)) {
|
|
||||||
this.get('subscriber.errors').add('email', context);
|
|
||||||
this.get('subscriber.hasValidated').pushObject('email');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// route action so it should bubble up to the global error handler
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).drop()
|
|
||||||
});
|
|
@ -1,116 +0,0 @@
|
|||||||
import $ from 'jquery';
|
|
||||||
import Controller from '@ember/controller';
|
|
||||||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
|
||||||
import moment from 'moment';
|
|
||||||
import {computed} from '@ember/object';
|
|
||||||
import {inject as service} from '@ember/service';
|
|
||||||
import {task} from 'ember-concurrency';
|
|
||||||
|
|
||||||
const orderMap = {
|
|
||||||
email: 'email',
|
|
||||||
created_at: 'createdAtUTC',
|
|
||||||
status: 'status'
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
|
||||||
export default Controller.extend({
|
|
||||||
session: service(),
|
|
||||||
|
|
||||||
queryParams: ['order', 'direction'],
|
|
||||||
order: 'created_at',
|
|
||||||
direction: 'desc',
|
|
||||||
|
|
||||||
subscribers: null,
|
|
||||||
subscriberToDelete: null,
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.set('subscribers', this.store.peekAll('subscriber'));
|
|
||||||
},
|
|
||||||
|
|
||||||
filteredSubscribers: computed('subscribers.@each.{email,createdAtUTC}', function () {
|
|
||||||
return this.subscribers.toArray().filter((subscriber) => {
|
|
||||||
return !subscriber.isNew && !subscriber.isDeleted;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
sortedSubscribers: computed('order', 'direction', 'subscribers.@each.{email,createdAtUTC,status}', function () {
|
|
||||||
let {filteredSubscribers, order, direction} = this;
|
|
||||||
|
|
||||||
let sorted = filteredSubscribers.sort((a, b) => {
|
|
||||||
let values = [a.get(orderMap[order]), b.get(orderMap[order])];
|
|
||||||
|
|
||||||
if (direction === 'desc') {
|
|
||||||
values = values.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof values[0] === 'string') {
|
|
||||||
return values[0].localeCompare(values[1], undefined, {ignorePunctuation: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof values[0] === 'object' && values[0]._isAMomentObject) {
|
|
||||||
return values[0].valueOf() - values[1].valueOf();
|
|
||||||
}
|
|
||||||
|
|
||||||
return values[0] - values[1];
|
|
||||||
});
|
|
||||||
|
|
||||||
return sorted;
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
deleteSubscriber(subscriber) {
|
|
||||||
this.set('subscriberToDelete', subscriber);
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmDeleteSubscriber() {
|
|
||||||
let subscriber = this.subscriberToDelete;
|
|
||||||
|
|
||||||
return subscriber.destroyRecord().then(() => {
|
|
||||||
this.set('subscriberToDelete', null);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelDeleteSubscriber() {
|
|
||||||
this.set('subscriberToDelete', null);
|
|
||||||
},
|
|
||||||
|
|
||||||
exportData() {
|
|
||||||
let exportUrl = ghostPaths().url.api('subscribers/csv');
|
|
||||||
let accessToken = this.get('session.data.authenticated.access_token');
|
|
||||||
let downloadURL = `${exportUrl}?access_token=${accessToken}`;
|
|
||||||
let iframe = $('#iframeDownload');
|
|
||||||
|
|
||||||
if (iframe.length === 0) {
|
|
||||||
iframe = $('<iframe>', {id: 'iframeDownload'}).hide().appendTo('body');
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe.attr('src', downloadURL);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchSubscribers: task(function* () {
|
|
||||||
let newFetchDate = new Date();
|
|
||||||
let results;
|
|
||||||
|
|
||||||
if (this._hasFetchedAll) {
|
|
||||||
// fetch any records modified since last fetch
|
|
||||||
results = yield this.store.query('subscriber', {
|
|
||||||
limit: 'all',
|
|
||||||
filter: `updated_at:>='${moment.utc(this._lastFetchDate).format('YYYY-MM-DD HH:mm:ss')}'`
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// fetch all records in batches of 200
|
|
||||||
while (!results || results.meta.pagination.page < results.meta.pagination.pages) {
|
|
||||||
results = yield this.store.query('subscriber', {
|
|
||||||
limit: 200,
|
|
||||||
order: `${this.order} ${this.direction}`,
|
|
||||||
page: results ? results.meta.pagination.page + 1 : 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._hasFetchedAll = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._lastFetchDate = newFetchDate;
|
|
||||||
})
|
|
||||||
});
|
|
@ -1,19 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import {inject as controller} from '@ember/controller';
|
|
||||||
import {inject as service} from '@ember/service';
|
|
||||||
|
|
||||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
|
||||||
export default Controller.extend({
|
|
||||||
subscribers: controller(),
|
|
||||||
router: service(),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
fetchNewSubscribers() {
|
|
||||||
this.subscribers.fetchSubscribers.perform();
|
|
||||||
},
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.router.transitionTo('subscribers');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -27,11 +27,7 @@ export const AVAILABLE_EVENTS = [
|
|||||||
// GROUPNAME: Tags
|
// GROUPNAME: Tags
|
||||||
{event: 'tag.added', name: 'Tag created', group: 'Tags'},
|
{event: 'tag.added', name: 'Tag created', group: 'Tags'},
|
||||||
{event: 'tag.edited', name: 'Tag updated', group: 'Tags'},
|
{event: 'tag.edited', name: 'Tag updated', group: 'Tags'},
|
||||||
{event: 'tag.deleted', name: 'Tag deleted', group: 'Tags'},
|
{event: 'tag.deleted', name: 'Tag deleted', group: 'Tags'}
|
||||||
|
|
||||||
// GROUPNAME: Subscribers
|
|
||||||
{event: 'subscriber.added', name: 'Subscriber added', group: 'Subscribers'},
|
|
||||||
{event: 'subscriber.deleted', name: 'Subscriber deleted', group: 'Subscribers'}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function eventName([event]/*, hash*/) {
|
export function eventName([event]/*, hash*/) {
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import {helper} from '@ember/component/helper';
|
|
||||||
|
|
||||||
export function subscribersQueryParams([order, currentOrder, direction]) {
|
|
||||||
// if order hasn't changed we toggle the direction
|
|
||||||
if (order === currentOrder) {
|
|
||||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'subscribers',
|
|
||||||
{
|
|
||||||
isQueryParams: true,
|
|
||||||
values: {
|
|
||||||
order,
|
|
||||||
direction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default helper(subscribersQueryParams);
|
|
@ -1,14 +0,0 @@
|
|||||||
import {helper} from '@ember/component/helper';
|
|
||||||
import {svgJar} from './svg-jar';
|
|
||||||
|
|
||||||
export function subscribersSortIcon([order, currentOrder, direction]) {
|
|
||||||
if (order === currentOrder) {
|
|
||||||
if (direction === 'asc') {
|
|
||||||
return svgJar('arrow-up', {class: 'ih2 mr2'});
|
|
||||||
} else {
|
|
||||||
return svgJar('arrow-down', {class: 'ih2 mr2'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default helper(subscribersSortIcon);
|
|
@ -12,7 +12,6 @@ import SetupValidator from 'ghost-admin/validators/setup';
|
|||||||
import SigninValidator from 'ghost-admin/validators/signin';
|
import SigninValidator from 'ghost-admin/validators/signin';
|
||||||
import SignupValidator from 'ghost-admin/validators/signup';
|
import SignupValidator from 'ghost-admin/validators/signup';
|
||||||
import SlackIntegrationValidator from 'ghost-admin/validators/slack-integration';
|
import SlackIntegrationValidator from 'ghost-admin/validators/slack-integration';
|
||||||
import SubscriberValidator from 'ghost-admin/validators/subscriber';
|
|
||||||
import TagSettingsValidator from 'ghost-admin/validators/tag-settings';
|
import TagSettingsValidator from 'ghost-admin/validators/tag-settings';
|
||||||
import UserValidator from 'ghost-admin/validators/user';
|
import UserValidator from 'ghost-admin/validators/user';
|
||||||
import WebhookValidator from 'ghost-admin/validators/webhook';
|
import WebhookValidator from 'ghost-admin/validators/webhook';
|
||||||
@ -42,7 +41,6 @@ export default Mixin.create({
|
|||||||
signin: SigninValidator,
|
signin: SigninValidator,
|
||||||
signup: SignupValidator,
|
signup: SignupValidator,
|
||||||
slackIntegration: SlackIntegrationValidator,
|
slackIntegration: SlackIntegrationValidator,
|
||||||
subscriber: SubscriberValidator,
|
|
||||||
tag: TagSettingsValidator,
|
tag: TagSettingsValidator,
|
||||||
user: UserValidator,
|
user: UserValidator,
|
||||||
integration: IntegrationValidator,
|
integration: IntegrationValidator,
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import Model from 'ember-data/model';
|
|
||||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
|
||||||
import attr from 'ember-data/attr';
|
|
||||||
import {belongsTo} from 'ember-data/relationships';
|
|
||||||
|
|
||||||
export default Model.extend(ValidationEngine, {
|
|
||||||
validationType: 'subscriber',
|
|
||||||
|
|
||||||
name: attr('string'),
|
|
||||||
email: attr('string'),
|
|
||||||
status: attr('string'),
|
|
||||||
subscribedUrl: attr('string'),
|
|
||||||
subscribedReferrer: attr('string'),
|
|
||||||
unsubscribedUrl: attr('string'),
|
|
||||||
unsubscribedAtUTC: attr('moment-utc'),
|
|
||||||
createdAtUTC: attr('moment-utc'),
|
|
||||||
updatedAtUTC: attr('moment-utc'),
|
|
||||||
createdBy: attr('number'),
|
|
||||||
updatedBy: attr('number'),
|
|
||||||
|
|
||||||
post: belongsTo('post')
|
|
||||||
});
|
|
@ -61,11 +61,6 @@ Router.map(function () {
|
|||||||
this.route('members');
|
this.route('members');
|
||||||
this.route('member', {path: '/members/:member_id'});
|
this.route('member', {path: '/members/:member_id'});
|
||||||
|
|
||||||
this.route('subscribers', function () {
|
|
||||||
this.route('new');
|
|
||||||
this.route('import');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.route('error404', {path: '/*path'});
|
this.route('error404', {path: '/*path'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
|
||||||
import RSVP from 'rsvp';
|
|
||||||
import {inject as service} from '@ember/service';
|
|
||||||
|
|
||||||
export default AuthenticatedRoute.extend({
|
|
||||||
feature: service(),
|
|
||||||
|
|
||||||
// redirect if subscribers is disabled or user isn't owner/admin
|
|
||||||
beforeModel() {
|
|
||||||
this._super(...arguments);
|
|
||||||
let promises = {
|
|
||||||
user: this.get('session.user'),
|
|
||||||
subscribers: this.get('feature.subscribers')
|
|
||||||
};
|
|
||||||
|
|
||||||
return RSVP.hash(promises).then((hash) => {
|
|
||||||
let {user, subscribers} = hash;
|
|
||||||
|
|
||||||
if (!subscribers || !user.isOwnerOrAdmin) {
|
|
||||||
return this.transitionTo('home');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setupController(controller) {
|
|
||||||
this._super(...arguments);
|
|
||||||
controller.fetchSubscribers.perform();
|
|
||||||
},
|
|
||||||
|
|
||||||
buildRouteInfoMetadata() {
|
|
||||||
return {
|
|
||||||
titleToken: 'Subscribers'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,4 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
});
|
|
@ -1,38 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
model() {
|
|
||||||
return this.store.createRecord('subscriber');
|
|
||||||
},
|
|
||||||
|
|
||||||
setupController(controller, model) {
|
|
||||||
controller.set('subscriber', model);
|
|
||||||
},
|
|
||||||
|
|
||||||
deactivate() {
|
|
||||||
let subscriber = this.controller.get('subscriber');
|
|
||||||
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
if (subscriber.get('isNew')) {
|
|
||||||
this.rollbackModel();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
save() {
|
|
||||||
let subscriber = this.controller.get('subscriber');
|
|
||||||
return subscriber.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
cancel() {
|
|
||||||
this.rollbackModel();
|
|
||||||
this.transitionTo('subscribers');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
rollbackModel() {
|
|
||||||
let subscriber = this.controller.get('subscriber');
|
|
||||||
subscriber.rollbackAttributes();
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,20 +0,0 @@
|
|||||||
import ApplicationSerializer from 'ghost-admin/serializers/application';
|
|
||||||
|
|
||||||
export default ApplicationSerializer.extend({
|
|
||||||
attrs: {
|
|
||||||
unsubscribedAtUTC: {key: 'unsubscribed_at'},
|
|
||||||
createdAtUTC: {key: 'created_at'},
|
|
||||||
updatedAtUTC: {key: 'updated_at'}
|
|
||||||
},
|
|
||||||
|
|
||||||
serialize() {
|
|
||||||
let json = this._super(...arguments);
|
|
||||||
|
|
||||||
// the API can't handle `status` being `null`
|
|
||||||
if (!json.status) {
|
|
||||||
delete json.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
});
|
|
@ -50,7 +50,6 @@ export default Service.extend({
|
|||||||
notifications: service(),
|
notifications: service(),
|
||||||
lazyLoader: service(),
|
lazyLoader: service(),
|
||||||
|
|
||||||
subscribers: feature('subscribers'),
|
|
||||||
members: feature('members'),
|
members: feature('members'),
|
||||||
nightShift: feature('nightShift', {user: true, onChange: '_setAdminTheme'}),
|
nightShift: feature('nightShift', {user: true, onChange: '_setAdminTheme'}),
|
||||||
|
|
||||||
|
@ -56,7 +56,6 @@
|
|||||||
@import "layouts/error.css";
|
@import "layouts/error.css";
|
||||||
@import "layouts/apps.css";
|
@import "layouts/apps.css";
|
||||||
@import "layouts/packages.css";
|
@import "layouts/packages.css";
|
||||||
@import "layouts/subscribers.css";
|
|
||||||
@import "layouts/labs.css";
|
@import "layouts/labs.css";
|
||||||
@import "layouts/whats-new.css";
|
@import "layouts/whats-new.css";
|
||||||
|
|
||||||
@ -397,10 +396,6 @@ input:focus,
|
|||||||
color: color-mod(#183691 l(+25%));
|
color: color-mod(#183691 l(+25%));
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribers-table table .gh-btn svg {
|
|
||||||
fill: var(--darkgrey);
|
|
||||||
}
|
|
||||||
|
|
||||||
.id-mailchimp img,
|
.id-mailchimp img,
|
||||||
.id-typeform img,
|
.id-typeform img,
|
||||||
.id-buffer img,
|
.id-buffer img,
|
||||||
|
@ -56,7 +56,6 @@
|
|||||||
@import "layouts/error.css";
|
@import "layouts/error.css";
|
||||||
@import "layouts/apps.css";
|
@import "layouts/apps.css";
|
||||||
@import "layouts/packages.css";
|
@import "layouts/packages.css";
|
||||||
@import "layouts/subscribers.css";
|
|
||||||
@import "layouts/labs.css";
|
@import "layouts/labs.css";
|
||||||
@import "layouts/whats-new.css";
|
@import "layouts/whats-new.css";
|
||||||
|
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
/* Subscribers Management /ghost/subscribers/
|
|
||||||
/* ---------------------------------------------------------- */
|
|
||||||
|
|
||||||
.view-subscribers .view-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Table (left pane)
|
|
||||||
/* ---------------------------------------------------------- */
|
|
||||||
|
|
||||||
.subscribers-table {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-table table {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-table th {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-table th a {
|
|
||||||
color: var(--midlightgrey);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-table td {
|
|
||||||
padding: 0;
|
|
||||||
border-top: var(--lightgrey) 1px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-table table .gh-btn {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-table table .gh-btn svg {
|
|
||||||
height: 13px;
|
|
||||||
width: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-table table tr:hover .gh-btn {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-table tbody td:last-of-type {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-subscribers-table-email-cell {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-subscribers-table-date-cell {
|
|
||||||
width: 194px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-subscribers-table-status-cell {
|
|
||||||
width: 114px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-subscribers-table-delete-cell {
|
|
||||||
width: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Sidebar (right pane)
|
|
||||||
/* ---------------------------------------------------------- */
|
|
||||||
|
|
||||||
.subscribers-sidebar {
|
|
||||||
width: 350px;
|
|
||||||
border-left: 1px solid #dfe1e3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-import-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-import-buttons .gh-btn {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers-import-buttons .gh-btn:last-of-type {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Import modal
|
|
||||||
/* ---------------------------------------------------------- */
|
|
||||||
|
|
||||||
.subscribers-import-results {
|
|
||||||
margin: 0;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
@ -56,9 +56,6 @@
|
|||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<li>{{#link-to "staff" data-test-nav="staff"}}{{svg-jar "staff"}}Staff{{/link-to}}</li>
|
<li>{{#link-to "staff" data-test-nav="staff"}}{{svg-jar "staff"}}Staff{{/link-to}}</li>
|
||||||
{{#if (and feature.subscribers (gh-user-can-admin session.user))}}
|
|
||||||
<li>{{#link-to "subscribers" data-test-nav="subscribers"}}{{svg-jar "email"}}Subscribers{{/link-to}}</li>
|
|
||||||
{{/if}}
|
|
||||||
</ul>
|
</ul>
|
||||||
{{#if (gh-user-can-admin session.user)}}
|
{{#if (gh-user-can-admin session.user)}}
|
||||||
<ul class="gh-nav-list gh-nav-settings">
|
<ul class="gh-nav-list gh-nav-settings">
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
<header class="modal-header" data-test-modal="delete-subscriber">
|
|
||||||
<h1>Are you sure?</h1>
|
|
||||||
</header>
|
|
||||||
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<strong>WARNING:</strong> All data for this subscriber will be deleted. There is no way to recover this.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button {{action "closeModal"}} class="gh-btn" data-test-button="cancel-delete-subscriber">
|
|
||||||
<span>Cancel</span>
|
|
||||||
</button>
|
|
||||||
{{gh-task-button "Delete"
|
|
||||||
successText="Deleted"
|
|
||||||
task=deleteSubscriber
|
|
||||||
class="gh-btn gh-btn-red gh-btn-icon"
|
|
||||||
data-test-button="confirm-delete-subscriber"}}
|
|
||||||
</div>
|
|
@ -1,47 +0,0 @@
|
|||||||
<header class="modal-header" data-test-modal="import-subscribers">
|
|
||||||
<h1>
|
|
||||||
{{#if response}}
|
|
||||||
Import Successful
|
|
||||||
{{else}}
|
|
||||||
Import Subscribers
|
|
||||||
{{/if}}
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
{{#if response}}
|
|
||||||
<table class="subscribers-import-results">
|
|
||||||
<tr>
|
|
||||||
<td>Imported:</td>
|
|
||||||
<td align="left" data-test-text="import-subscribers-imported">{{response.imported}}</td>
|
|
||||||
</tr>
|
|
||||||
{{#if response.duplicates}}
|
|
||||||
<tr>
|
|
||||||
<td>Duplicates:</td>
|
|
||||||
<td align="left" data-test-text="import-subscribers-duplicates">{{response.duplicates}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/if}}
|
|
||||||
{{#if response.invalid}}
|
|
||||||
<tr>
|
|
||||||
<td>Invalid:</td>
|
|
||||||
<td align="left" data-test-text="import-subscribers-invalid">{{response.invalid}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/if}}
|
|
||||||
</table>
|
|
||||||
{{else}}
|
|
||||||
{{gh-file-uploader
|
|
||||||
url=uploadUrl
|
|
||||||
paramName="subscribersfile"
|
|
||||||
labelText="Select or drag-and-drop a CSV file."
|
|
||||||
uploadStarted=(action "uploadStarted")
|
|
||||||
uploadFinished=(action "uploadFinished")
|
|
||||||
uploadSuccess=(action "uploadSuccess")}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button {{action "closeModal"}} disabled={{closeDisabled}} class="gh-btn" data-test-button="close-import-subscribers">
|
|
||||||
<span>{{#if response}}Close{{else}}Cancel{{/if}}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
@ -1,44 +0,0 @@
|
|||||||
<header class="modal-header" data-test-modal="new-subscriber">
|
|
||||||
<h1>Add a Subscriber</h1>
|
|
||||||
</header>
|
|
||||||
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
|
|
||||||
<a class="close" href="" title="Close" {{action "closeModal"}} {{action (optional noop) on="mouseDown"}}>
|
|
||||||
{{svg-jar "close"}}<span class="hidden">Close</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<fieldset>
|
|
||||||
{{#gh-form-group errors=subscriber.errors hasValidated=subscriber.hasValidated property="email"}}
|
|
||||||
<label for="new-subscriber-email">Email Address</label>
|
|
||||||
<input type="email"
|
|
||||||
value={{subscriber.email}}
|
|
||||||
oninput={{action "updateEmail" value="target.value"}}
|
|
||||||
id="new-subscriber-email"
|
|
||||||
class="gh-input email"
|
|
||||||
placeholder="Email Address"
|
|
||||||
name="email"
|
|
||||||
autofocus="autofocus"
|
|
||||||
autocapitalize="off"
|
|
||||||
autocorrect="off"
|
|
||||||
data-test-input="new-subscriber-email">
|
|
||||||
{{gh-error-message errors=subscriber.errors property="email" data-test-error="new-subscriber-email"}}
|
|
||||||
{{/gh-form-group}}
|
|
||||||
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="gh-btn"
|
|
||||||
{{action "closeModal"}}
|
|
||||||
{{!-- disable mouseDown so it doesn't trigger focus-out validations --}}
|
|
||||||
{{action (optional noop) on="mouseDown"}}
|
|
||||||
data-test-button="cancel-new-subscriber"
|
|
||||||
>
|
|
||||||
<span>Cancel</span>
|
|
||||||
</button>
|
|
||||||
{{gh-task-button "Add"
|
|
||||||
successText="Added"
|
|
||||||
task=addSubscriber
|
|
||||||
class="gh-btn gh-btn-green gh-btn-icon"
|
|
||||||
data-test-button="create-subscriber"}}
|
|
||||||
</div>
|
|
@ -98,20 +98,6 @@
|
|||||||
<div class="for-switch">{{gh-feature-flag "nightShift"}}</div>
|
<div class="for-switch">{{gh-feature-flag "nightShift"}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gh-setting {{if feature.members "gh-labs-disabled"}}" data-tooltip="{{if feature.members "Disabled when members is turned on"}}">
|
|
||||||
<div class="gh-setting-content">
|
|
||||||
<div class="gh-setting-title">Subscribers</div>
|
|
||||||
<div class="gh-setting-desc">Collect email addresses from your readers, more info in <a
|
|
||||||
href="https://ghost.org/faq/enable-subscribers-feature/">the docs</a></div>
|
|
||||||
</div>
|
|
||||||
<div class="gh-setting-action">
|
|
||||||
{{#if feature.members}}
|
|
||||||
<div class="for-switch">{{gh-feature-flag "subscribers" "disabled"}}</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="for-switch">{{gh-feature-flag "subscribers" }}</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{#if config.enableDeveloperExperiments}}
|
{{#if config.enableDeveloperExperiments}}
|
||||||
<div class="gh-setting">
|
<div class="gh-setting">
|
||||||
<div class="gh-setting-content">
|
<div class="gh-setting-content">
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
<section class="gh-canvas">
|
|
||||||
<GhCanvasHeader class="gh-canvas-header">
|
|
||||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
|
||||||
Subscribers
|
|
||||||
<span style="font-weight:200;margin-left:10px;display:inline-block;" data-test-total-subscribers>
|
|
||||||
{{#if this.fetchSubscribers.lastSuccessful}}
|
|
||||||
({{this.filteredSubscribers.length}})
|
|
||||||
{{else}}
|
|
||||||
(Loading...)
|
|
||||||
{{/if}}
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<div class="view-actions">
|
|
||||||
{{#link-to "subscribers.import" class="gh-btn gh-btn-white mr2" data-test-link="import-csv"}}<span>Import CSV</span>{{/link-to}}
|
|
||||||
<a href="#" {{action 'exportData'}} class="gh-btn gh-btn-white mr2"><span>Export CSV</span></a>
|
|
||||||
{{#link-to "subscribers.new" class="gh-btn gh-btn-green" data-test-link="add-subscriber"}}<span>Add Subscriber</span>{{/link-to}}
|
|
||||||
</div>
|
|
||||||
</GhCanvasHeader>
|
|
||||||
|
|
||||||
<section class="view-container">
|
|
||||||
<div class="subscribers-table bg-grouped-table br4 shadow-1 pl5 pb2 pt2">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="gh-subscribers-table-email-cell">
|
|
||||||
{{#link-to class="inline-flex items-center darkgrey" params=(subscribers-query-params "email" this.order this.direction)}}
|
|
||||||
{{subscribers-sort-icon "email" this.order this.direction}}
|
|
||||||
Email Address
|
|
||||||
{{/link-to}}
|
|
||||||
</th>
|
|
||||||
<th class="gh-subscribers-table-date-cell">
|
|
||||||
{{#link-to class="inline-flex items-center darkgrey" params=(subscribers-query-params "created_at" this.order this.direction)}}
|
|
||||||
{{subscribers-sort-icon "created_at" this.order this.direction}}
|
|
||||||
Subscription Date
|
|
||||||
{{/link-to}}
|
|
||||||
</th>
|
|
||||||
<th class="gh-subscribers-table-status-cell">
|
|
||||||
{{#link-to class="inline-flex items-center darkgrey" params=(subscribers-query-params "status" this.order this.direction)}}
|
|
||||||
{{subscribers-sort-icon "status" this.order this.direction}}
|
|
||||||
Status
|
|
||||||
{{/link-to}}
|
|
||||||
</th>
|
|
||||||
<th class="gh-subscribers-table-delete-cell"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</table>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{{#if this.sortedSubscribers}}
|
|
||||||
<VerticalCollection
|
|
||||||
@items={{this.sortedSubscribers}}
|
|
||||||
@key="id"
|
|
||||||
@containerSelector=".gh-main"
|
|
||||||
@estimateHeight=34
|
|
||||||
@bufferSize=30
|
|
||||||
as |subscriber|
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<td class="gh-subscribers-table-email-cell">
|
|
||||||
{{subscriber.email}}
|
|
||||||
</td>
|
|
||||||
<td class="gh-subscribers-table-date-cell">
|
|
||||||
{{moment-format subscriber.createdAtUTC 'MMMM DD, YYYY'}}
|
|
||||||
</td>
|
|
||||||
<td class="gh-subscribers-table-status-cell">
|
|
||||||
{{subscriber.status}}
|
|
||||||
</td>
|
|
||||||
<td class="gh-subscribers-table-delete-cell">
|
|
||||||
<button class="gh-btn gh-btn-link gh-btn-sm" {{action "deleteSubscriber" subscriber}}><span>{{svg-jar "trash"}}</span></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</VerticalCollection>
|
|
||||||
{{else}}
|
|
||||||
<tr>
|
|
||||||
<td colspan="4">
|
|
||||||
{{#if this.fetchSubscribers.isRunning}}
|
|
||||||
<div class="relative h50"><GhLoadingSpinner /></div>
|
|
||||||
{{else}}
|
|
||||||
{{!-- match height to delete button height for consistent spacing --}}
|
|
||||||
<span class="dib" style="line-height: 33px">No subscribers found.</span>
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{/if}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{#if this.subscriberToDelete}}
|
|
||||||
<GhFullscreenModal
|
|
||||||
@modal="delete-subscriber"
|
|
||||||
@model={{this.subscriberToDelete}}
|
|
||||||
@confirm={{action "confirmDeleteSubscriber"}}
|
|
||||||
@close={{action "cancelDeleteSubscriber"}}
|
|
||||||
@modifier="action wide" />
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{outlet}}
|
|
@ -1,4 +0,0 @@
|
|||||||
{{gh-fullscreen-modal "import-subscribers"
|
|
||||||
close=(action "close")
|
|
||||||
confirm=(action "fetchNewSubscribers")
|
|
||||||
modifier="action wide"}}
|
|
@ -1,5 +0,0 @@
|
|||||||
{{gh-fullscreen-modal "new-subscriber"
|
|
||||||
model=subscriber
|
|
||||||
confirm=(route-action "save")
|
|
||||||
close=(route-action "cancel")
|
|
||||||
modifier="action wide"}}
|
|
@ -10,7 +10,6 @@ import mockRoles from './config/roles';
|
|||||||
import mockSettings from './config/settings';
|
import mockSettings from './config/settings';
|
||||||
import mockSite from './config/site';
|
import mockSite from './config/site';
|
||||||
import mockSlugs from './config/slugs';
|
import mockSlugs from './config/slugs';
|
||||||
import mockSubscribers from './config/subscribers';
|
|
||||||
import mockTags from './config/tags';
|
import mockTags from './config/tags';
|
||||||
import mockThemes from './config/themes';
|
import mockThemes from './config/themes';
|
||||||
import mockUploads from './config/uploads';
|
import mockUploads from './config/uploads';
|
||||||
@ -65,7 +64,6 @@ export function testConfig() {
|
|||||||
mockSettings(this);
|
mockSettings(this);
|
||||||
mockSite(this);
|
mockSite(this);
|
||||||
mockSlugs(this);
|
mockSlugs(this);
|
||||||
mockSubscribers(this);
|
|
||||||
mockTags(this);
|
mockTags(this);
|
||||||
mockThemes(this);
|
mockThemes(this);
|
||||||
mockUploads(this);
|
mockUploads(this);
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
/* eslint-disable camelcase */
|
|
||||||
import {Response} from 'ember-cli-mirage';
|
|
||||||
import {paginatedResponse} from '../utils';
|
|
||||||
|
|
||||||
export default function mockSubscribers(server) {
|
|
||||||
server.get('/subscribers/', paginatedResponse('subscribers'));
|
|
||||||
|
|
||||||
server.post('/subscribers/', function ({subscribers}) {
|
|
||||||
let attrs = this.normalizedRequestAttrs();
|
|
||||||
let subscriber = subscribers.findBy({email: attrs.email});
|
|
||||||
|
|
||||||
if (subscriber) {
|
|
||||||
return new Response(422, {}, {
|
|
||||||
errors: [{
|
|
||||||
type: 'ValidationError',
|
|
||||||
message: 'Validation error, cannot save subscriber.',
|
|
||||||
context: 'Email address is already subscribed.',
|
|
||||||
property: null
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
attrs.createdAt = new Date();
|
|
||||||
attrs.createdBy = 0;
|
|
||||||
|
|
||||||
return subscribers.create(attrs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.put('/subscribers/:id/');
|
|
||||||
|
|
||||||
server.post('/subscribers/csv/', function () {
|
|
||||||
// NB: we get a raw FormData object with no way to inspect it in Chrome
|
|
||||||
// until version 50 adds the additional read methods
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/FormData#Browser_compatibility
|
|
||||||
|
|
||||||
server.createList('subscriber', 50);
|
|
||||||
|
|
||||||
return {
|
|
||||||
meta: {
|
|
||||||
stats: {
|
|
||||||
imported: 50,
|
|
||||||
duplicates: 3,
|
|
||||||
invalid: 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
server.del('/subscribers/:id/');
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import faker from 'faker';
|
|
||||||
import moment from 'moment';
|
|
||||||
import {Factory} from 'ember-cli-mirage';
|
|
||||||
|
|
||||||
let randomDate = function randomDate(start = moment().subtract(30, 'days').toDate(), end = new Date()) {
|
|
||||||
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
|
|
||||||
};
|
|
||||||
|
|
||||||
let statuses = ['pending', 'subscribed'];
|
|
||||||
|
|
||||||
export default Factory.extend({
|
|
||||||
name() { return `${faker.name.firstName()} ${faker.name.lastName()}`; },
|
|
||||||
email: faker.internet.email,
|
|
||||||
status() { return statuses[Math.floor(Math.random() * statuses.length)]; },
|
|
||||||
createdAt() { return randomDate(); },
|
|
||||||
updatedAt: null,
|
|
||||||
createdBy: 0,
|
|
||||||
updatedBy: null,
|
|
||||||
unsubscribedAt: null
|
|
||||||
});
|
|
@ -3,7 +3,7 @@ export default [{
|
|||||||
database: 'mysql',
|
database: 'mysql',
|
||||||
enableDeveloperExperiments: true,
|
enableDeveloperExperiments: true,
|
||||||
environment: 'development',
|
environment: 'development',
|
||||||
labs: {subscribers: false},
|
labs: {},
|
||||||
mail: 'SMTP',
|
mail: 'SMTP',
|
||||||
version: '2.15.0',
|
version: '2.15.0',
|
||||||
useGravatar: 'true'
|
useGravatar: 'true'
|
||||||
|
@ -73,7 +73,7 @@ export default [
|
|||||||
{
|
{
|
||||||
id: 12,
|
id: 12,
|
||||||
key: 'labs',
|
key: 'labs',
|
||||||
value: '{"subscribers":true}',
|
value: '{}',
|
||||||
type: 'blog',
|
type: 'blog',
|
||||||
created_at: '2015-01-12T18:29:01.000Z',
|
created_at: '2015-01-12T18:29:01.000Z',
|
||||||
created_by: 1,
|
created_by: 1,
|
||||||
|
@ -4,7 +4,6 @@ export default function (server) {
|
|||||||
|
|
||||||
// server.createList('contact', 10);
|
// server.createList('contact', 10);
|
||||||
|
|
||||||
server.createList('subscriber', 125);
|
|
||||||
server.createList('tag', 100);
|
server.createList('tag', 100);
|
||||||
|
|
||||||
server.create('integration', {name: 'Demo'});
|
server.create('integration', {name: 'Demo'});
|
||||||
|
@ -3,7 +3,6 @@ import {authenticateSession} from 'ember-simple-auth/test-support';
|
|||||||
import {beforeEach, describe, it} from 'mocha';
|
import {beforeEach, describe, it} from 'mocha';
|
||||||
import {click, currentRouteName, fillIn, find, findAll, visit} from '@ember/test-helpers';
|
import {click, currentRouteName, fillIn, find, findAll, visit} from '@ember/test-helpers';
|
||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {fileUpload} from '../helpers/file-upload';
|
|
||||||
import {setupApplicationTest} from 'ember-mocha';
|
import {setupApplicationTest} from 'ember-mocha';
|
||||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||||
import {versionMismatchResponse} from 'ghost-admin/mirage/utils';
|
import {versionMismatchResponse} from 'ghost-admin/mirage/utils';
|
||||||
@ -80,18 +79,6 @@ describe('Acceptance: Error Handling', function () {
|
|||||||
expect(findAll('.gh-alert').length).to.equal(1);
|
expect(findAll('.gh-alert').length).to.equal(1);
|
||||||
expect(find('.gh-alert').textContent).to.match(/refresh/);
|
expect(find('.gh-alert').textContent).to.match(/refresh/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be triggered when passed in to a component', async function () {
|
|
||||||
this.server.post('/subscribers/csv/', versionMismatchResponse);
|
|
||||||
|
|
||||||
await visit('/subscribers');
|
|
||||||
await click('[data-test-link="import-csv"]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'test.csv'});
|
|
||||||
|
|
||||||
// alert is shown
|
|
||||||
expect(findAll('.gh-alert').length).to.equal(1);
|
|
||||||
expect(find('.gh-alert').textContent).to.match(/refresh/);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('logged out', function () {
|
describe('logged out', function () {
|
||||||
|
@ -1,208 +0,0 @@
|
|||||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
|
||||||
import {beforeEach, describe, it} from 'mocha';
|
|
||||||
import {click, currentRouteName, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
|
|
||||||
import {expect} from 'chai';
|
|
||||||
import {fileUpload} from '../helpers/file-upload';
|
|
||||||
import {findAllWithText} from '../helpers/find';
|
|
||||||
import {setupApplicationTest} from 'ember-mocha';
|
|
||||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
|
||||||
import {visit} from '../helpers/visit';
|
|
||||||
|
|
||||||
describe('Acceptance: Subscribers', function () {
|
|
||||||
let hooks = setupApplicationTest();
|
|
||||||
setupMirage(hooks);
|
|
||||||
|
|
||||||
it('redirects to signin when not authenticated', async function () {
|
|
||||||
await invalidateSession();
|
|
||||||
await visit('/subscribers');
|
|
||||||
|
|
||||||
expect(currentURL()).to.equal('/signin');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('redirects editors to posts', async function () {
|
|
||||||
let role = this.server.create('role', {name: 'Editor'});
|
|
||||||
this.server.create('user', {roles: [role]});
|
|
||||||
|
|
||||||
await authenticateSession();
|
|
||||||
await visit('/subscribers');
|
|
||||||
|
|
||||||
expect(currentURL()).to.equal('/site');
|
|
||||||
expect(find('[data-test-nav="subscribers"]'), 'sidebar link')
|
|
||||||
.to.not.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('redirects authors to posts', async function () {
|
|
||||||
let role = this.server.create('role', {name: 'Author'});
|
|
||||||
this.server.create('user', {roles: [role]});
|
|
||||||
|
|
||||||
await authenticateSession();
|
|
||||||
await visit('/subscribers');
|
|
||||||
|
|
||||||
expect(currentURL()).to.equal('/site');
|
|
||||||
expect(find('[data-test-nav="subscribers"]'), 'sidebar link')
|
|
||||||
.to.not.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('redirects contributors to posts', async function () {
|
|
||||||
let role = this.server.create('role', {name: 'Contributor'});
|
|
||||||
this.server.create('user', {roles: [role]});
|
|
||||||
|
|
||||||
await authenticateSession();
|
|
||||||
await visit('/subscribers');
|
|
||||||
|
|
||||||
expect(currentURL()).to.equal('/site');
|
|
||||||
expect(find('[data-test-nav="subscribers"]'), 'sidebar link')
|
|
||||||
.to.not.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('an admin', function () {
|
|
||||||
beforeEach(async function () {
|
|
||||||
let role = this.server.create('role', {name: 'Administrator'});
|
|
||||||
this.server.create('user', {roles: [role]});
|
|
||||||
|
|
||||||
return await authenticateSession();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can manage subscribers', async function () {
|
|
||||||
this.server.createList('subscriber', 40);
|
|
||||||
|
|
||||||
await visit('/');
|
|
||||||
await click('[data-test-nav="subscribers"]');
|
|
||||||
|
|
||||||
// it navigates to the correct page
|
|
||||||
expect(currentRouteName()).to.equal('subscribers.index');
|
|
||||||
|
|
||||||
// it has correct page title
|
|
||||||
expect(document.title, 'page title')
|
|
||||||
.to.equal('Subscribers - Test Blog');
|
|
||||||
|
|
||||||
// it loads subscribers
|
|
||||||
// NOTE: we use vertical-collection for occlusion so the max number of rows can be less than the
|
|
||||||
// number of subscribers that are loaded
|
|
||||||
expect(findAll('.subscribers-table tbody tr').length, 'number of subscriber rows')
|
|
||||||
.to.be.at.least(30);
|
|
||||||
|
|
||||||
// it shows the total number of subscribers
|
|
||||||
expect(find('[data-test-total-subscribers]').textContent.trim(), 'displayed subscribers total')
|
|
||||||
.to.equal('(40)');
|
|
||||||
|
|
||||||
// it defaults to sorting by created_at desc
|
|
||||||
let [lastRequest] = this.server.pretender.handledRequests.slice(-1);
|
|
||||||
expect(lastRequest.queryParams.order).to.equal('created_at desc');
|
|
||||||
|
|
||||||
// click the add subscriber button
|
|
||||||
await click('[data-test-link="add-subscriber"]');
|
|
||||||
|
|
||||||
// it displays the add subscriber modal
|
|
||||||
expect(find('[data-test-modal="new-subscriber"]'), 'add subscriber modal displayed')
|
|
||||||
.to.exist;
|
|
||||||
|
|
||||||
// cancel the modal
|
|
||||||
await click('[data-test-button="cancel-new-subscriber"]');
|
|
||||||
|
|
||||||
// it closes the add subscriber modal
|
|
||||||
expect(find('[data-test-modal]'), 'add subscriber modal displayed after cancel')
|
|
||||||
.to.not.exist;
|
|
||||||
|
|
||||||
// save a new subscriber
|
|
||||||
await click('[data-test-link="add-subscriber"]');
|
|
||||||
await fillIn('[data-test-input="new-subscriber-email"]', 'test@example.com');
|
|
||||||
await click('[data-test-button="create-subscriber"]');
|
|
||||||
|
|
||||||
// the add subscriber modal is closed
|
|
||||||
expect(find('[data-test-modal]'), 'add subscriber modal displayed after save')
|
|
||||||
.to.not.exist;
|
|
||||||
|
|
||||||
// the subscriber is added to the table
|
|
||||||
expect(find('.subscribers-table tbody tr td'), 'first email in list after addition')
|
|
||||||
.to.contain.text('test@example.com');
|
|
||||||
|
|
||||||
// the table is scrolled to the top
|
|
||||||
// TODO: implement scroll to new record after addition
|
|
||||||
// expect(find('.subscribers-table').scrollTop(), 'scroll position after addition')
|
|
||||||
// .to.equal(0);
|
|
||||||
|
|
||||||
// the subscriber total is updated
|
|
||||||
expect(find('[data-test-total-subscribers]'), 'subscribers total after addition')
|
|
||||||
.to.have.trimmed.text('(41)');
|
|
||||||
|
|
||||||
// saving a duplicate subscriber
|
|
||||||
await click('[data-test-link="add-subscriber"]');
|
|
||||||
await fillIn('[data-test-input="new-subscriber-email"]', 'test@example.com');
|
|
||||||
await click('[data-test-button="create-subscriber"]');
|
|
||||||
|
|
||||||
// the validation error is displayed
|
|
||||||
expect(find('[data-test-error="new-subscriber-email"]'), 'duplicate email validation')
|
|
||||||
.to.have.trimmed.text('Email address is already subscribed.');
|
|
||||||
|
|
||||||
// the subscriber is not added to the table
|
|
||||||
expect(findAllWithText('td.gh-subscribers-table-email-cell', 'test@example.com').length, 'number of "test@example.com rows"')
|
|
||||||
.to.equal(1);
|
|
||||||
|
|
||||||
// the subscriber total is unchanged
|
|
||||||
expect(find('[data-test-total-subscribers]'), 'subscribers total after failed add')
|
|
||||||
.to.have.trimmed.text('(41)');
|
|
||||||
|
|
||||||
// deleting a subscriber
|
|
||||||
await click('[data-test-button="cancel-new-subscriber"]');
|
|
||||||
await click('.subscribers-table tbody tr:first-of-type button:last-of-type');
|
|
||||||
|
|
||||||
// it displays the delete subscriber modal
|
|
||||||
expect(find('[data-test-modal="delete-subscriber"]'), 'delete subscriber modal displayed')
|
|
||||||
.to.exist;
|
|
||||||
|
|
||||||
// cancel the modal
|
|
||||||
await click('[data-test-button="cancel-delete-subscriber"]');
|
|
||||||
|
|
||||||
// it closes the add subscriber modal
|
|
||||||
expect(find('[data-test-modal]'), 'delete subscriber modal displayed after cancel')
|
|
||||||
.to.not.exist;
|
|
||||||
|
|
||||||
await click('.subscribers-table tbody tr:first-of-type button:last-of-type');
|
|
||||||
await click('[data-test-button="confirm-delete-subscriber"]');
|
|
||||||
|
|
||||||
// the add subscriber modal is closed
|
|
||||||
expect(find('[data-test-modal]'), 'delete subscriber modal displayed after confirm')
|
|
||||||
.to.not.exist;
|
|
||||||
|
|
||||||
// the subscriber is removed from the table
|
|
||||||
expect(find('.subscribers-table tbody td.gh-subscribers-table-email-cell'), 'first email in list after addition')
|
|
||||||
.to.not.have.trimmed.text('test@example.com');
|
|
||||||
|
|
||||||
// the subscriber total is updated
|
|
||||||
expect(find('[data-test-total-subscribers]'), 'subscribers total after addition')
|
|
||||||
.to.have.trimmed.text('(40)');
|
|
||||||
|
|
||||||
// click the import subscribers button
|
|
||||||
await click('[data-test-link="import-csv"]');
|
|
||||||
|
|
||||||
// it displays the import subscribers modal
|
|
||||||
expect(find('[data-test-modal="import-subscribers"]'), 'import subscribers modal displayed')
|
|
||||||
.to.exist;
|
|
||||||
expect(find('.fullscreen-modal input[type="file"]'), 'import modal contains file input')
|
|
||||||
.to.exist;
|
|
||||||
|
|
||||||
// cancel the modal
|
|
||||||
await click('[data-test-button="close-import-subscribers"]');
|
|
||||||
|
|
||||||
// it closes the import subscribers modal
|
|
||||||
expect(find('[data-test-modal]'), 'import subscribers modal displayed after cancel')
|
|
||||||
.to.not.exist;
|
|
||||||
|
|
||||||
await click('[data-test-link="import-csv"]');
|
|
||||||
await fileUpload('.fullscreen-modal input[type="file"]', ['test'], {name: 'test.csv'});
|
|
||||||
|
|
||||||
// modal title changes
|
|
||||||
expect(find('[data-test-modal="import-subscribers"] h1'), 'import modal title after import')
|
|
||||||
.to.have.trimmed.text('Import Successful');
|
|
||||||
|
|
||||||
// modal button changes
|
|
||||||
expect(find('[data-test-button="close-import-subscribers"]'), 'import modal button text after import')
|
|
||||||
.to.have.trimmed.text('Close');
|
|
||||||
|
|
||||||
// subscriber total is updated
|
|
||||||
expect(find('[data-test-total-subscribers]'), 'subscribers total after import')
|
|
||||||
.to.have.trimmed.text('(90)');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,73 +0,0 @@
|
|||||||
import EmberObject from '@ember/object';
|
|
||||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
|
||||||
import {
|
|
||||||
describe,
|
|
||||||
it
|
|
||||||
} from 'mocha';
|
|
||||||
import {expect} from 'chai';
|
|
||||||
import {run} from '@ember/runloop';
|
|
||||||
|
|
||||||
const Subscriber = EmberObject.extend(ValidationEngine, {
|
|
||||||
validationType: 'subscriber',
|
|
||||||
|
|
||||||
email: null
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Unit: Validator: subscriber', function () {
|
|
||||||
it('validates email by default', function () {
|
|
||||||
let subscriber = Subscriber.create({});
|
|
||||||
let properties = subscriber.get('validators.subscriber.properties');
|
|
||||||
|
|
||||||
expect(properties, 'properties').to.include('email');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes with a valid email', function () {
|
|
||||||
let subscriber = Subscriber.create({email: 'test@example.com'});
|
|
||||||
let passed = false;
|
|
||||||
|
|
||||||
run(() => {
|
|
||||||
subscriber.validate({property: 'email'}).then(() => {
|
|
||||||
passed = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(passed, 'passed').to.be.true;
|
|
||||||
expect(subscriber.get('hasValidated'), 'hasValidated').to.include('email');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validates email presence', function () {
|
|
||||||
let subscriber = Subscriber.create({});
|
|
||||||
let passed = false;
|
|
||||||
|
|
||||||
run(() => {
|
|
||||||
subscriber.validate({property: 'email'}).then(() => {
|
|
||||||
passed = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let emailErrors = subscriber.get('errors').errorsFor('email').get(0);
|
|
||||||
expect(emailErrors.attribute, 'errors.email.attribute').to.equal('email');
|
|
||||||
expect(emailErrors.message, 'errors.email.message').to.equal('Please enter an email.');
|
|
||||||
|
|
||||||
expect(passed, 'passed').to.be.false;
|
|
||||||
expect(subscriber.get('hasValidated'), 'hasValidated').to.include('email');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validates email', function () {
|
|
||||||
let subscriber = Subscriber.create({email: 'foo'});
|
|
||||||
let passed = false;
|
|
||||||
|
|
||||||
run(() => {
|
|
||||||
subscriber.validate({property: 'email'}).then(() => {
|
|
||||||
passed = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
let emailErrors = subscriber.get('errors').errorsFor('email').get(0);
|
|
||||||
expect(emailErrors.attribute, 'errors.email.attribute').to.equal('email');
|
|
||||||
expect(emailErrors.message, 'errors.email.message').to.equal('Invalid email.');
|
|
||||||
|
|
||||||
expect(passed, 'passed').to.be.false;
|
|
||||||
expect(subscriber.get('hasValidated'), 'hasValidated').to.include('email');
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user