mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
commit
474224f49f
@ -20,9 +20,6 @@ const FeatureFlagComponent = Component.extend({
|
||||
return this._flagValue;
|
||||
},
|
||||
set(key, value) {
|
||||
if (this.flag === 'members' && value === true) {
|
||||
this.set(`feature.subscribers`, false);
|
||||
}
|
||||
return this.set(`feature.${this.flag}`, value);
|
||||
}
|
||||
}),
|
||||
|
@ -17,7 +17,7 @@ export default Component.extend({
|
||||
|
||||
member: null,
|
||||
initialsClass: computed('sizeClass', function () {
|
||||
return this.sizeClass || 'f5 fw4 lh-zero';
|
||||
return this.sizeClass || 'gh-member-list-avatar';
|
||||
}),
|
||||
|
||||
backgroundStyle: computed('member.{name,email}', function () {
|
||||
|
@ -29,6 +29,7 @@ export default Component.extend({
|
||||
status: subscription.status,
|
||||
startDate: subscription.start_date ? moment(subscription.start_date).format('MMM DD YYYY') : '-',
|
||||
plan: subscription.plan,
|
||||
dollarAmount: parseInt(subscription.plan.amount) ? (subscription.plan.amount / 100) : 0,
|
||||
validUntil: subscription.current_period_end ? moment(subscription.current_period_end).format('MMM DD YYYY') : '-'
|
||||
};
|
||||
}).reverse();
|
||||
|
@ -10,16 +10,17 @@ export default Component.extend({
|
||||
router: service(),
|
||||
|
||||
tagName: 'li',
|
||||
classNames: ['gh-flex-list-row'],
|
||||
classNames: ['gh-list-row'],
|
||||
|
||||
active: false,
|
||||
|
||||
id: alias('member.id'),
|
||||
email: alias('member.email'),
|
||||
name: alias('member.name'),
|
||||
subscribedAt: computed('member.createdAt', function () {
|
||||
let memberSince = moment(this.member.createdAt).from(moment());
|
||||
let createdDate = moment(this.member.createdAt).format('MMM DD, YYYY');
|
||||
return `${createdDate} (${memberSince})`;
|
||||
memberSince: computed('member.createdAt', function () {
|
||||
return moment(this.member.createdAt).from(moment());
|
||||
}),
|
||||
createdDate: computed('member.createdAt', function () {
|
||||
return moment(this.member.createdAt).format('MMM DD, YYYY');
|
||||
})
|
||||
});
|
||||
|
@ -161,7 +161,7 @@ export default Component.extend({
|
||||
_loadPosts() {
|
||||
let store = this.store;
|
||||
let postsUrl = `${store.adapterFor('post').urlForQuery({}, 'post')}/`;
|
||||
let postsQuery = {fields: 'id,title,page', limit: 'all', status: 'all'};
|
||||
let postsQuery = {fields: 'id,title,page', limit: 'all'};
|
||||
let content = this.content;
|
||||
|
||||
return this.ajax.request(postsUrl, {data: postsQuery}).then((posts) => {
|
||||
@ -178,7 +178,7 @@ export default Component.extend({
|
||||
_loadPages() {
|
||||
let store = this.store;
|
||||
let pagesUrl = `${store.adapterFor('page').urlForQuery({}, 'page')}/`;
|
||||
let pagesQuery = {fields: 'id,title,page', limit: 'all', status: 'all'};
|
||||
let pagesQuery = {fields: 'id,title,page', limit: 'all'};
|
||||
let content = this.content;
|
||||
|
||||
return this.ajax.request(pagesUrl, {data: pagesQuery}).then((pages) => {
|
||||
|
@ -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()
|
||||
});
|
@ -31,8 +31,8 @@ export default Controller.extend({
|
||||
finaliseDeletion() {
|
||||
// decrememnt the total member count manually so there's no flash
|
||||
// when transitioning back to the members list
|
||||
if (this.members.meta) {
|
||||
this.members.decrementProperty('meta.pagination.total');
|
||||
if (this.members.memberCount) {
|
||||
this.members.decrementProperty('memberCount');
|
||||
}
|
||||
this.router.transitionTo('members');
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ import {task} from 'ember-concurrency';
|
||||
export default Controller.extend({
|
||||
store: service(),
|
||||
|
||||
meta: null,
|
||||
memberCount: null,
|
||||
members: null,
|
||||
searchText: '',
|
||||
init() {
|
||||
@ -54,25 +54,23 @@ export default Controller.extend({
|
||||
|
||||
fetchMembers: task(function* () {
|
||||
let newFetchDate = new Date();
|
||||
let results;
|
||||
|
||||
if (this._hasFetchedAll) {
|
||||
// fetch any records modified since last fetch
|
||||
results = yield this.store.query('member', {
|
||||
yield this.store.query('member', {
|
||||
limit: 'all',
|
||||
filter: `updated_at:>='${moment.utc(this._lastFetchDate).format('YYYY-MM-DD HH:mm:ss')}'`,
|
||||
order: 'created_at desc'
|
||||
});
|
||||
} else {
|
||||
// fetch all records
|
||||
results = yield this.store.query('member', {
|
||||
yield this.store.query('member', {
|
||||
limit: 'all',
|
||||
order: 'created_at desc'
|
||||
});
|
||||
this._hasFetchedAll = true;
|
||||
this.set('meta', results.meta);
|
||||
}
|
||||
|
||||
this.set('memberCount', this.store.peekAll('member').length);
|
||||
this._lastFetchDate = newFetchDate;
|
||||
})
|
||||
});
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
});
|
@ -29,9 +29,10 @@ export const AVAILABLE_EVENTS = [
|
||||
{event: 'tag.edited', name: 'Tag updated', 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'}
|
||||
// GROUPNAME: Members
|
||||
{event: 'member.added', name: 'Member added', group: 'Members'}
|
||||
// TODO: enable once server-side payload is fixed
|
||||
// {event: 'member.deleted', name: 'Member deleted', group: 'Members'}
|
||||
];
|
||||
|
||||
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 SignupValidator from 'ghost-admin/validators/signup';
|
||||
import SlackIntegrationValidator from 'ghost-admin/validators/slack-integration';
|
||||
import SubscriberValidator from 'ghost-admin/validators/subscriber';
|
||||
import TagSettingsValidator from 'ghost-admin/validators/tag-settings';
|
||||
import UserValidator from 'ghost-admin/validators/user';
|
||||
import WebhookValidator from 'ghost-admin/validators/webhook';
|
||||
@ -42,7 +41,6 @@ export default Mixin.create({
|
||||
signin: SigninValidator,
|
||||
signup: SignupValidator,
|
||||
slackIntegration: SlackIntegrationValidator,
|
||||
subscriber: SubscriberValidator,
|
||||
tag: TagSettingsValidator,
|
||||
user: UserValidator,
|
||||
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')
|
||||
});
|
@ -63,11 +63,6 @@ Router.map(function () {
|
||||
});
|
||||
this.route('member', {path: '/members/:member_id'});
|
||||
|
||||
this.route('subscribers', function () {
|
||||
this.route('new');
|
||||
this.route('import');
|
||||
});
|
||||
|
||||
this.route('error404', {path: '/*path'});
|
||||
});
|
||||
|
||||
|
@ -24,8 +24,7 @@ export default AuthenticatedRoute.extend({
|
||||
}
|
||||
|
||||
let query = {
|
||||
id: post_id,
|
||||
status: 'all'
|
||||
id: post_id
|
||||
};
|
||||
|
||||
return this.store.query(modelName, query)
|
||||
|
@ -5,16 +5,11 @@ export default AuthenticatedRoute.extend({
|
||||
config: service(),
|
||||
|
||||
// redirect to posts screen if:
|
||||
// - developer experiments aren't enabled
|
||||
// - TODO: members is disabled?
|
||||
// - logged in user isn't owner/admin
|
||||
beforeModel() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (!this.config.get('enableDeveloperExperiments')) {
|
||||
return this.transitionTo('home');
|
||||
}
|
||||
|
||||
return this.session.user.then((user) => {
|
||||
if (!user.isOwnerOrAdmin) {
|
||||
return this.transitionTo('home');
|
||||
|
@ -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,8 +50,6 @@ export default Service.extend({
|
||||
notifications: service(),
|
||||
lazyLoader: service(),
|
||||
|
||||
publicAPI: feature('publicAPI'),
|
||||
subscribers: feature('subscribers'),
|
||||
members: feature('members'),
|
||||
nightShift: feature('nightShift', {user: true, onChange: '_setAdminTheme'}),
|
||||
|
||||
|
@ -57,7 +57,6 @@
|
||||
@import "layouts/error.css";
|
||||
@import "layouts/apps.css";
|
||||
@import "layouts/packages.css";
|
||||
@import "layouts/subscribers.css";
|
||||
@import "layouts/labs.css";
|
||||
@import "layouts/whats-new.css";
|
||||
|
||||
@ -110,6 +109,19 @@ input:focus,
|
||||
border-color: color-mod(var(--lightgrey) l(+10%));
|
||||
}
|
||||
|
||||
.gh-input-append {
|
||||
border-color: color-mod(var(--lightgrey));
|
||||
}
|
||||
|
||||
.gh-input-group .gh-input:focus + .gh-input-append {
|
||||
border-color: color-mod(var(--lightgrey) l(+10%));
|
||||
}
|
||||
|
||||
.gh-input-append,
|
||||
.gh-input-append:before {
|
||||
background: color-mod(var(--lightgrey));
|
||||
}
|
||||
|
||||
.settings-menu-container,
|
||||
.gh-main-white {
|
||||
background: #212A2E;
|
||||
@ -173,6 +185,14 @@ input:focus,
|
||||
border: color-mod(var(--darkgrey) l(-27%) blackness(+15%)) 1px solid;
|
||||
}
|
||||
|
||||
.view-actions .gh-btn:not(.gh-btn-green):not(.gh-btn-blue):not(.gh-btn-red) {
|
||||
border-color: color-mod(var(--darkgrey) l(-27%) blackness(+15%) alpha(50%));
|
||||
}
|
||||
|
||||
.gh-btn-group .gh-btn:first-of-type {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.gh-btn-blue,
|
||||
.gh-btn-green,
|
||||
.gh-btn-red {
|
||||
@ -189,6 +209,7 @@ input:focus,
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
|
||||
.settings-menu-delete-button {
|
||||
color: var(--red);
|
||||
border: none !important;
|
||||
@ -398,10 +419,6 @@ input:focus,
|
||||
color: color-mod(#183691 l(+25%));
|
||||
}
|
||||
|
||||
.subscribers-table table .gh-btn svg {
|
||||
fill: var(--darkgrey);
|
||||
}
|
||||
|
||||
.id-mailchimp img,
|
||||
.id-typeform img,
|
||||
.id-buffer img,
|
||||
|
@ -57,7 +57,6 @@
|
||||
@import "layouts/error.css";
|
||||
@import "layouts/apps.css";
|
||||
@import "layouts/packages.css";
|
||||
@import "layouts/subscribers.css";
|
||||
@import "layouts/labs.css";
|
||||
@import "layouts/whats-new.css";
|
||||
|
||||
|
@ -43,6 +43,16 @@
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-list-row:not(.header):first-of-type {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-list-row:last-of-type {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-list-cell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
@ -64,10 +74,12 @@
|
||||
|
||||
.gh-list:not(.tabbed) .gh-list-header:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.gh-list:not(.tabbed) .gh-list-header:last-child {
|
||||
border-top-right-radius: 5px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.gh-list-data {
|
||||
@ -82,6 +94,10 @@
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.gh-list-row .gh-list-data:first-child {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.gh-list-data.show-on-hover > *,
|
||||
.gh-list-cell.show-on-hover > * {
|
||||
opacity: 0;
|
||||
@ -102,6 +118,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gh-list-cellwidth-min {
|
||||
max-width: 0;
|
||||
}
|
||||
|
||||
.gh-list-cellwidth-2-3 {
|
||||
width: 67%;
|
||||
}
|
||||
@ -114,6 +134,21 @@
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.gh-list-cellwidth-10 { width: 10%; }
|
||||
.gh-list-cellwidth-20 { width: 20%; }
|
||||
.gh-list-cellwidth-30 { width: 30%; }
|
||||
.gh-list-cellwidth-40 { width: 40%; }
|
||||
.gh-list-cellwidth-50 { width: 50%; }
|
||||
.gh-list-cellwidth-60 { width: 60%; }
|
||||
.gh-list-cellwidth-70 { width: 70%; }
|
||||
.gh-list-cellwidth-80 { width: 80%; }
|
||||
.gh-list-cellwidth-90 { width: 90%; }
|
||||
.gh-list-cellwidth-100 { width: 100%; }
|
||||
|
||||
.gh-list-cellwidth-chevron {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
/* Typography
|
||||
/* --------------------------------------------------- */
|
||||
.gh-list h3 {
|
||||
|
@ -146,8 +146,9 @@
|
||||
}
|
||||
|
||||
.gh-post-list-featured {
|
||||
padding: 20px 0px 20px 10px;
|
||||
padding: 15px 0px 20px 10px;
|
||||
width: 1px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.gh-post-list-updated,
|
||||
@ -246,13 +247,27 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gh-posts-list-item:nth-of-type(2) {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-posts-list-item:hover {
|
||||
background: var(--whitegrey-l2);
|
||||
}
|
||||
|
||||
.gh-posts-list-item:hover .gh-list-data {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.gh-post-list-featured {
|
||||
display: block;
|
||||
order: 1;
|
||||
border-bottom: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -1px;
|
||||
left: -6px;
|
||||
top: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.gh-post-list-title {
|
||||
@ -323,12 +338,13 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-top: 18px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.post-header .view-actions .gh-contentfilter {
|
||||
order: 2;
|
||||
margin: 10px 0;
|
||||
padding: 6px 2px;
|
||||
margin: 10px 0 -20px;
|
||||
padding: 6px 2px 26px 22px;
|
||||
max-width: calc(100vw - 10px);
|
||||
overflow-x: auto;
|
||||
}
|
||||
@ -340,7 +356,7 @@
|
||||
}
|
||||
|
||||
.post-header .gh-canvas-title {
|
||||
left: 20px;
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
.gh-post-list-updated {
|
||||
@ -352,6 +368,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 430px) {
|
||||
.post-header .view-actions .gh-contentfilter {
|
||||
border-right: 1px solid var(--whitegrey-d1);
|
||||
}
|
||||
|
||||
.gh-contentfilter-sort {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 901px) {
|
||||
.gh-posts-list-item a:after {
|
||||
display: none;
|
||||
|
@ -1,7 +1,3 @@
|
||||
.gh-labs-price-label input {
|
||||
padding-right: 96px;
|
||||
}
|
||||
|
||||
.gh-labs-price-label input::-webkit-outer-spin-button, .gh-labs-price-label input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
@ -13,21 +9,6 @@
|
||||
/* Firefox */
|
||||
}
|
||||
|
||||
.gh-labs-price-label::after {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
color: var(--midlightgrey);
|
||||
}
|
||||
|
||||
.gh-labs-monthly-price::after {
|
||||
content: "USD/month";
|
||||
}
|
||||
|
||||
.gh-labs-yearly-price::after {
|
||||
content: "USD/year";
|
||||
}
|
||||
|
||||
.gh-labs-toggle-wrapper {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
@ -71,6 +52,57 @@
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.gh-members-stripe-info-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gh-members-stripe-info-header h4 {
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.gh-members-stripe-info {
|
||||
border-radius: 0.9rem;
|
||||
border: 1px solid var(--whitegrey);
|
||||
background: var(--whitegrey-l2);
|
||||
padding: 12px;
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
.gh-members-stripe-badge {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.gh-members-stripe-link {
|
||||
color: #555ABF;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.gh-members-stripe-info-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gh-members-stripe-info-header h4 {
|
||||
order: 2;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--whitegrey);
|
||||
}
|
||||
|
||||
.gh-members-stripe-badge {
|
||||
order: 1;
|
||||
/* margin: -10px 0 0 -10px; */
|
||||
}
|
||||
|
||||
.gh-members-stripe-info {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.gh-labs-disabled .for-checkbox label,
|
||||
.gh-labs-disabled .for-checkbox .input-toggle-component,
|
||||
.gh-labs-disabled .for-switch label,
|
||||
|
@ -658,6 +658,7 @@
|
||||
margin: -3px 0 0 1px;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
text-indent: -1px;
|
||||
white-space: nowrap;
|
||||
font-size: 2.8rem;
|
||||
line-height: 1.2em;
|
||||
@ -668,9 +669,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.gh-canvas-title {
|
||||
display: block;
|
||||
}
|
||||
.gh-canvas-title svg {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
@ -1,3 +1,10 @@
|
||||
/* Global
|
||||
/* ----------------------------------------- */
|
||||
.gh-member-avatar-label {
|
||||
display: block;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Members list
|
||||
/* ----------------------------------------- */
|
||||
|
||||
@ -6,6 +13,11 @@
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.gh-members-list-searchfield.active {
|
||||
border-color: #3eb0ef;
|
||||
box-shadow: inset 0 0 0 1px #3eb0ef;
|
||||
}
|
||||
|
||||
p.gh-members-list-email {
|
||||
margin: -2px 0 -1px;
|
||||
}
|
||||
@ -20,6 +32,21 @@ p.gh-members-list-email {
|
||||
margin: -30px 0 15px;
|
||||
}
|
||||
|
||||
.gh-member-list-avatar {
|
||||
font-size: 1.65rem;
|
||||
font-weight: 500;
|
||||
line-height: 0;
|
||||
letter-spacing: -0.6px;
|
||||
}
|
||||
|
||||
.gh-member-actions-menu {
|
||||
top: calc(100% + 6px);
|
||||
}
|
||||
|
||||
.gh-member-actions-menu.fade-out {
|
||||
animation-duration: .001s;
|
||||
}
|
||||
|
||||
|
||||
/* Member details
|
||||
/* ----------------------------------------- */
|
||||
|
@ -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;
|
||||
}
|
@ -1,21 +1,13 @@
|
||||
/* Tag list
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.gh-tags-count:hover {
|
||||
a.gh-tag-list-posts-count:hover {
|
||||
color: color-mod(var(--blue) l(-25%) s(+15%));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea.gh-tag-details-textarea {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.gh-tag-image-uploader .gh-image-uploader {
|
||||
margin: 4px 0 0;
|
||||
border: 1px solid var(--lightgrey);
|
||||
min-height: 147px;
|
||||
}
|
||||
|
||||
.gh-tags-placeholder {
|
||||
width: 118px;
|
||||
margin: -30px 0 15px;
|
||||
@ -23,12 +15,127 @@ textarea.gh-tag-details-textarea {
|
||||
|
||||
.gh-tag-list-slug {
|
||||
white-space: nowrap;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gh-tag-list-description {
|
||||
max-width: 320px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tags-view {
|
||||
max-width: 1220px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Mobile style of tag list */
|
||||
@media (max-width: 1000px) {
|
||||
.gh-tags-list-item {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid var(--lightgrey);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gh-tags-list-item:nth-of-type(2) {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-tags-list-item .gh-list-data {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.gh-tags-list-item:hover {
|
||||
background: var(--whitegrey-l2);
|
||||
}
|
||||
|
||||
.gh-tags-list-item:hover .gh-list-data {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.gh-tag-list-title {
|
||||
display: block;
|
||||
flex: 1 1 100%;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.gh-tag-list-slug {
|
||||
display: inline-block;
|
||||
width: unset;
|
||||
padding: 2px 0 20px 16px;
|
||||
}
|
||||
|
||||
.gh-tag-list-posts-count {
|
||||
display: inline-block;
|
||||
flex: 1 1 auto;
|
||||
width: unset;
|
||||
padding: 2px 0 20px 0;
|
||||
}
|
||||
|
||||
.gh-tag-list-slug:after {
|
||||
content: "\2022";
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
color: var(--midgrey-l2);
|
||||
}
|
||||
|
||||
.gh-tag-list-chevron {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
.tags-header {
|
||||
justify-content: flex-end;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.tags-header .gh-canvas-title {
|
||||
position: absolute;
|
||||
top: 29px;
|
||||
left: 21px;
|
||||
}
|
||||
|
||||
.tags-header .view-actions {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-top: 18px;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tags-header .view-actions .gh-contentfilter {
|
||||
order: 2;
|
||||
margin: 10px 0 -20px;
|
||||
padding: 6px 0 26px;
|
||||
max-width: calc(100vw - 10px);
|
||||
overflow-x: auto;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.tags-header .view-actions .gh-contentfilter button {
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
|
||||
.gh-tag-list-description {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tag details
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.gh-tag-image-uploader .gh-image-uploader {
|
||||
margin: 4px 0 0;
|
||||
border: 1px solid var(--lightgrey);
|
||||
min-height: 147px;
|
||||
}
|
@ -72,7 +72,7 @@
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
box-shadow: #fff 0 0 0 3px;
|
||||
box-shadow: var(--white) 0 0 0 3px;
|
||||
}
|
||||
|
||||
.user-list-item-figure img {
|
||||
|
@ -92,7 +92,7 @@
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
align-items: flex-start;
|
||||
justify-content: start;
|
||||
justify-content: flex-start;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@ -114,7 +114,7 @@
|
||||
color: color(var(--midgrey) l(-10%));
|
||||
font-weight: 400;
|
||||
margin-top: 12px;
|
||||
max-height: 48px;
|
||||
max-height: 36px;
|
||||
overflow-y: hidden;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
|
@ -489,6 +489,62 @@ textarea {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* Input appends
|
||||
/* ---------------------------------------------------------- */
|
||||
.gh-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gh-input-group .gh-input {
|
||||
border-right: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.gh-input-group .gh-input:focus + .gh-input-append {
|
||||
border-color: color-mod(var(--blue));
|
||||
box-shadow: inset 0 0 0 1px var(--blue);
|
||||
}
|
||||
|
||||
.gh-input-group .gh-input:focus + .gh-input-append,
|
||||
.gh-input-group .gh-input:focus + .gh-input-append:before {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.gh-input-append {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
padding: 6px 12px;
|
||||
border: var(--input-border);
|
||||
background: var(--input-bg-color);
|
||||
color: var(--darkgrey);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
user-select: text;
|
||||
border-radius: var(--border-radius);
|
||||
word-wrap: none;
|
||||
border-left: none;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
flex: 1;
|
||||
color: var(--midlightgrey);
|
||||
}
|
||||
|
||||
.gh-input-append:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 1px;
|
||||
left: -2px;
|
||||
bottom: 1px;
|
||||
width: 4px;
|
||||
background: var(--input-bg-color);
|
||||
}
|
||||
|
||||
|
||||
/* FFF: Fucking Firefox Fixes
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
|
@ -189,7 +189,6 @@ h1 {
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-indent: -1px;
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
|
@ -1,3 +1,3 @@
|
||||
<div class="flex items-center justify-center br-100" style={{this.backgroundStyle}} ...attributes>
|
||||
<span class="db white {{this.initialsClass}}">{{this.initials}}</span>
|
||||
<span class="gh-member-avatar-label {{this.initialsClass}}">{{this.initials}}</span>
|
||||
</div>
|
@ -39,7 +39,7 @@
|
||||
focus-out=(action 'setProperty' 'note' scratchNote)
|
||||
}}
|
||||
{{gh-error-message errors=member.errors property="note"}}
|
||||
<p>Maximum: <b>500</b> characters. You’ve used {{gh-count-down-characters scratchNote 500}}</p>
|
||||
<p>Not visible to member. Maximum: <b>500</b> characters. You’ve used {{gh-count-down-characters scratchNote 500}}</p>
|
||||
{{/gh-form-group}}
|
||||
</div>
|
||||
</div>
|
||||
@ -112,7 +112,7 @@
|
||||
<table class="gh-member-stripe-table">
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Plan</td>
|
||||
<td class="gh-member-stripe-data">{{subscription.plan.nickname}}</td>
|
||||
<td class="gh-member-stripe-data">{{subscription.plan.nickname}} <span class="midgrey">({{subscription.dollarAmount}} <span class="ttu">{{subscription.plan.currency}}</span>/{{subscription.plan.interval}})</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Current status</td>
|
||||
|
@ -3,8 +3,8 @@
|
||||
<section class="bb b--whitegrey pa5">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h4 class="gh-setting-title">Stripe settings</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">Configure Stripe API keys for signups</p>
|
||||
<h4 class="gh-setting-title">Connect to Stripe</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">Configure API keys to create subscriptions and take payments</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="gh-btn" {{action (toggle "membersStripeOpen" this)}} data-test-toggle-membersstripe><span>{{if membersStripeOpen "Close" "Expand"}}</span></button>
|
||||
@ -12,74 +12,96 @@
|
||||
</div>
|
||||
|
||||
{{#liquid-if membersStripeOpen}}
|
||||
<div class="w-50 mb4 mt5">
|
||||
<label class="fw6 f8">Stripe publishable API key</label>
|
||||
{{gh-text-input
|
||||
<div class="flex flex-column flex-row-l items-start justify-between mb4 mt6">
|
||||
<div class="w-100 w-50-l">
|
||||
<div class="mb4">
|
||||
<label class="fw6 f8">Stripe Publishable key</label>
|
||||
{{gh-text-input
|
||||
type="password"
|
||||
value=(readonly subscriptionSettings.stripeConfig.public_token)
|
||||
input=(action "setSubscriptionSettings" "public_token")
|
||||
class="mt1"
|
||||
class="mt1 password"
|
||||
}}
|
||||
</div>
|
||||
<div class="w-50 mb4">
|
||||
<label class="fw6 f8 mt4">Stripe secret API key</label>
|
||||
{{gh-text-input
|
||||
</div>
|
||||
<div class="nudge-top--3">
|
||||
<label class="fw6 f8 mt4">Stripe Secret key</label>
|
||||
{{gh-text-input
|
||||
type="password"
|
||||
value=(readonly subscriptionSettings.stripeConfig.secret_token)
|
||||
input=(action "setSubscriptionSettings" "secret_token")
|
||||
class="mt1"
|
||||
class="mt1 password"
|
||||
}}
|
||||
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank" class="mt1 fw4 f8">
|
||||
Where to find Stripe API keys
|
||||
</a>
|
||||
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank" class="mt1 fw4 f8">
|
||||
Find your Stripe API keys here »
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml0 ml5-l mt6">
|
||||
<div class="gh-members-stripe-info">
|
||||
<div class="gh-members-stripe-info-header">
|
||||
<h4>How you get paid</h4>
|
||||
{{svg-jar "stripe-verified-partner-badge" class="gh-members-stripe-badge"}}
|
||||
</div>
|
||||
<p class="f8 mt2 mb0">
|
||||
Stripe is our exclusive direct payments partner.<br />
|
||||
Ghost collects <strong>no fees</strong> on any payments! If you don’t have a Stripe account yet, you can <a href="https://stripe.com" target="_blank" rel="noopener" class="gh-members-stripe-link">sign up here</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
</section>
|
||||
|
||||
<section class="bb b--whitegrey pa5">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h4 class="gh-setting-title">Pricing</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">Set monthly and yearly subscription prices</p>
|
||||
<h4 class="gh-setting-title">Subscription pricing</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">Set monthly and yearly recurring subscription prices</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="gh-btn" {{action (toggle "membersPricingOpen" this)}} data-test-toggle-memberspricing><span>{{if membersPricingOpen "Close" "Expand"}}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{#liquid-if membersPricingOpen}}
|
||||
<div class="w-50 flex mb4 mt5">
|
||||
<div class="w-50 flex mt8">
|
||||
<div class="w-50 mr3">
|
||||
{{#gh-form-group}}
|
||||
<label class="fw6 f8">Monthly price</label>
|
||||
<div class="mt1 relative gh-labs-price-label gh-labs-monthly-price">
|
||||
|
||||
<div class="flex items-center justify-center mt1 gh-input-group gh-labs-price-label">
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.stripeConfig.plans.monthly.dollarAmount)
|
||||
type="number"
|
||||
input=(action "setSubscriptionSettings" "month")
|
||||
}}
|
||||
value=(readonly subscriptionSettings.stripeConfig.plans.monthly.dollarAmount)
|
||||
type="number"
|
||||
input=(action "setSubscriptionSettings" "month")
|
||||
}}
|
||||
<span class="gh-input-append">USD/month</span>
|
||||
</div>
|
||||
{{/gh-form-group}}
|
||||
</div>
|
||||
<div class="w-50 ml2">
|
||||
{{#gh-form-group class="description-container"}}
|
||||
<label class="fw6 f8">Yearly price</label>
|
||||
<div class="mt1 relative gh-labs-price-label gh-labs-yearly-price">
|
||||
<div class="flex items-center justify-center mt1 gh-input-group gh-labs-price-label">
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.stripeConfig.plans.yearly.dollarAmount)
|
||||
type="number"
|
||||
input=(action "setSubscriptionSettings" "year")
|
||||
}}
|
||||
<span class="gh-input-append">USD/year</span>
|
||||
</div>
|
||||
{{/gh-form-group}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="f8 fw4 midgrey">Currently only USD is supported, more currencies <a href="https://ghost.org/docs/members/" target="_blank" rel="noopener">coming soon</a></div>
|
||||
{{/liquid-if}}
|
||||
</section>
|
||||
|
||||
<section class="bb b--whitegrey pa5">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h4 class="gh-setting-title">Allow free members signup</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">Allow free members signup</p>
|
||||
<h4 class="gh-setting-title">Allow free member signup</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">If disabled, members can only be signed up via payment checkout or API integration</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="for-switch">
|
||||
@ -99,36 +121,39 @@
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h4 class="gh-setting-title">Default post access</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">Configure restrictions for new posts</p>
|
||||
<p class="gh-setting-desc pa0 ma0">When a new post is created, who should have access to it?</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="gh-btn" {{action (toggle "membersPostAccessOpen" this)}} data-test-toggle-memberspostaccess><span>{{if membersPostAccessOpen "Close" "Expand"}}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{#liquid-if membersPostAccessOpen}}
|
||||
<div class="flex flex-column w-50 flex mb4 mt5">
|
||||
<div class="flex flex-column w-50 flex mt8">
|
||||
<div class="gh-radio {{if (eq settings.defaultContentVisibility "public") "active"}}"
|
||||
{{action "setDefaultContentVisibility" "public" on="click"}}>
|
||||
<div class="gh-radio-button" data-test-publishmenu-unpublished-option></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">Public</div>
|
||||
<div class="gh-radio-label">Public<br>
|
||||
<small class="midgrey">All site visitors to your site, no login required</small></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="gh-radio {{if (eq settings.defaultContentVisibility "members") "active"}}"
|
||||
{{action "setDefaultContentVisibility" "members" on="click"}}>
|
||||
<div class="gh-radio-button" data-test-publishmenu-published-option></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">Members only</div>
|
||||
<div class="gh-radio-label">Members only<br>
|
||||
<small class="midgrey">All logged-in members</small></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="gh-radio {{if (eq settings.defaultContentVisibility "paid") "active"}}"
|
||||
{{action "setDefaultContentVisibility" "paid" on="click"}}>
|
||||
<div class="gh-radio-button" data-test-publishmenu-published-option></div>
|
||||
<div class="gh-radio-content">
|
||||
<div class="gh-radio-label">Paid-members only</div>
|
||||
<div class="gh-radio-label">Paid-members only<br>
|
||||
<small class="midgrey">Only logged-in members with an active Stripe subscription</small></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -138,27 +163,27 @@
|
||||
<section class="bb b--whitegrey pa5">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<h4 class="gh-setting-title">Emails</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">Membership related email settings</p>
|
||||
<h4 class="gh-setting-title">Email settings</h4>
|
||||
<p class="gh-setting-desc pa0 ma0">Customise signup, signin and subscription emails</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="gh-btn" {{action (toggle "membersEmailOpen" this)}} data-test-toggle-membersemail><span>{{if membersEmailOpen "Close" "Expand"}}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{#liquid-if membersEmailOpen}}
|
||||
<div class="flex flex-column w-40 flex mb2 mt5">
|
||||
<div class="flex flex-column w-40 flex mt8">
|
||||
{{#gh-form-group}}
|
||||
<label class="fw6 f8">Sender email address</label>
|
||||
<div class="flex items-center justify-center mt1">
|
||||
<label class="fw6 f8">From Address</label>
|
||||
<div class="flex items-center justify-center mt1 gh-input-group">
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.fromAddress)
|
||||
input=(action "setSubscriptionSettings" "fromAddress")
|
||||
class="w20"
|
||||
}}
|
||||
<span class="ml3"> @{{config.blogDomain}}</span>
|
||||
<span class="gh-input-append"> @{{config.blogDomain}}</span>
|
||||
</div>
|
||||
<div class="f8 fw4 midgrey mt1"> "From" address for sign up and sign in emails</div>
|
||||
<div class="f8 fw4 midgrey mt1">Your members will receive system emails from this address</div>
|
||||
{{/gh-form-group}}
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{{#link-to "member" member class="gh-flex-list-data gh-flex-list-auto" title="Member details"}}
|
||||
{{#link-to "member" member class="gh-list-data" title="Member details"}}
|
||||
<div class="flex items-center">
|
||||
<GhMemberAvatar @member={{member}} class="w9 h9 mr3" />
|
||||
<div>
|
||||
@ -16,10 +16,10 @@
|
||||
</div>
|
||||
{{/link-to}}
|
||||
|
||||
{{#link-to "member" member class="gh-flex-list-data gh-members-list-subscribed-at middarkgrey f8 nowrap" title="Member details" }}
|
||||
Member since {{this.subscribedAt}}
|
||||
{{#link-to "member" member class="gh-list-data gh-members-list-subscribed-at gh-list-cellwidth-20 nowrap middarkgrey f8 nowrap" title="Member details" }}
|
||||
{{this.createdDate}} <span class="midgrey-l2">({{this.memberSince}})</span>
|
||||
{{/link-to}}
|
||||
|
||||
{{#link-to "member" member class="gh-flex-list-data" title="Member details"}}
|
||||
{{#link-to "member" member class="gh-list-data gh-list-cellwidth-chevron align-right" title="Member details"}}
|
||||
{{svg-jar "arrow-right" class="w6 h6 fill-midgrey pa1 nr2"}}
|
||||
{{/link-to}}
|
@ -56,9 +56,6 @@
|
||||
</li>
|
||||
{{/if}}
|
||||
<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>
|
||||
{{#if (gh-user-can-admin session.user)}}
|
||||
<ul class="gh-nav-list gh-nav-settings">
|
||||
|
@ -1,4 +1,4 @@
|
||||
{{#link-to "tags.tag" tag class="gh-list-data" title="Edit tag"}}
|
||||
{{#link-to "tags.tag" tag class="gh-list-data gh-tag-list-title" title="Edit tag"}}
|
||||
<h3 class="gh-tag-list-name">
|
||||
{{this.tag.name}}
|
||||
</h3>
|
||||
@ -9,22 +9,22 @@
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
|
||||
{{#link-to "tags.tag" tag class="gh-list-data middarkgrey f8 gh-tag-list-slug" title="Edit tag"}}
|
||||
<span class="gh-tag-list-slug" title="{{this.slug}}">{{this.slug}}</span>
|
||||
{{#link-to "tags.tag" tag class="gh-list-data middarkgrey f8 gh-tag-list-slug gh-list-cellwidth-10" title="Edit tag"}}
|
||||
<span title="{{this.slug}}">{{this.slug}}</span>
|
||||
{{/link-to}}
|
||||
|
||||
{{#if this.postsCount}}
|
||||
{{#link-to "posts" (query-params type=null author=null tag=tag.slug order=null) class="gh-list-data blue gh-tag-list-posts-count gh-tags-count f8" title=(concat "List posts tagged with '" this.tag.name "'")}}
|
||||
{{#link-to "posts" (query-params type=null author=null tag=tag.slug order=null) class="gh-list-data blue gh-tag-list-posts-count gh-list-cellwidth-10 f8" title=(concat "List posts tagged with '" this.tag.name "'")}}
|
||||
<span class="nowrap">{{this.postsLabel}}</span>
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{#link-to "tags.tag" tag class="gh-list-data gh-tag-list-posts-count" title="Edit tag"}}
|
||||
{{#link-to "tags.tag" tag class="gh-list-data gh-tag-list-posts-count gh-list-cellwidth-10" title="Edit tag"}}
|
||||
<span class="nowrap f8 midlightgrey">{{this.postsLabel}}</span>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
|
||||
{{#link-to "tags.tag" tag class="gh-list-data" title="Edit tag"}}
|
||||
<div class="flex items-center justify-end w-100">
|
||||
{{#link-to "tags.tag" tag class="gh-list-data gh-list-cellwidth-min gh-tag-list-chevron" title="Edit tag"}}
|
||||
<div class="flex items-center justify-end w-100 h-100">
|
||||
<span class="nr2">{{svg-jar "arrow-right" class="w6 h6 fill-midgrey pa1"}}</span>
|
||||
</div>
|
||||
{{/link-to}}
|
@ -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>
|
@ -15,7 +15,7 @@
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
<div class="flex items-center mb10 bt b--lightgrey-d1 pt8">
|
||||
<GhMemberAvatar @member={{member}} @sizeClass={{if member.name 'f-subheadline fw4 lh-zero' 'f-headline fw4 lh-zero'}} class="w18 h18 mr4" />
|
||||
<GhMemberAvatar @member={{member}} @sizeClass={{if member.name 'f-subheadline fw4 lh-zero tracked-1' 'f-headline fw4 lh-zero tracked-1'}} class="w18 h18 mr4" />
|
||||
<div>
|
||||
<h3 class="f2 fw5 ma0 pa0">
|
||||
{{if member.name member.name member.email}}
|
||||
@ -24,7 +24,7 @@
|
||||
{{#if member.name}}
|
||||
<span class="darkgrey fw5">{{member.email}}</span> –
|
||||
{{/if}}
|
||||
Member since {{this.subscribedAt}}
|
||||
Created on {{this.subscribedAt}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<span class="hidden">Members Actions</span>
|
||||
</span>
|
||||
{{/gh-dropdown-button}}
|
||||
{{#gh-dropdown name="members-actions-menu" tagName="ul" classNames="user-actions-menu dropdown-menu dropdown-triangle-top-right"}}
|
||||
{{#gh-dropdown name="members-actions-menu" tagName="ul" classNames="gh-member-actions-menu dropdown-menu dropdown-triangle-top-right"}}
|
||||
<li>
|
||||
{{#link-to "members.import" class="mr2" data-test-link="import-csv"}}
|
||||
<span>Import CSV </span>
|
||||
@ -24,32 +24,29 @@
|
||||
</span>
|
||||
<div class="relative">
|
||||
{{svg-jar "search" class="gh-input-search-icon"}}
|
||||
<GhTextInput placeholder="Search members..." @value={{this.searchText}} @input={{action (mut this.searchText) value="target.value"}} class="gh-members-list-searchfield" />
|
||||
<GhTextInput placeholder="Search members..." @value={{this.searchText}} @input={{action (mut this.searchText) value="target.value"}} class="gh-members-list-searchfield {{if this.searchText "active"}}" />
|
||||
</div>
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
<section class="view-container">
|
||||
|
||||
{{#if (or filteredMembers this.fetchMembers.isRunning this.searchText)}}
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="f-small fw5 midlightgrey ttu mb2">
|
||||
{{#if this.searchText}}
|
||||
Search result
|
||||
{{else}}
|
||||
All members
|
||||
{{#if this.fetchMembers.lastSuccessful}}
|
||||
({{this.meta.pagination.total}})
|
||||
{{else}}
|
||||
(Loading...)
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</h2>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<section class="content-list">
|
||||
<ol class="members-list gh-list {{unless filteredMembers "no-posts"}}">
|
||||
{{#if filteredMembers}}
|
||||
<li class="gh-list-row header">
|
||||
<div class="gh-list-header">
|
||||
{{#if this.searchText}}
|
||||
Search result
|
||||
{{else}}
|
||||
{{#if this.fetchMembers.lastSuccessful}}
|
||||
{{pluralize memberCount "member"}}
|
||||
{{else}}
|
||||
Loading...
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-20 nowrap">Created</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-chevron"></div>
|
||||
</li>
|
||||
{{#vertical-collection
|
||||
items=filteredMembers
|
||||
key="id"
|
||||
|
@ -6,7 +6,44 @@
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="view-container settings-debug">
|
||||
<p class="gh-box gh-box-info">{{svg-jar "idea"}}This is a testing ground for experimental features which aren't quite ready for primetime. They may change, break or inexplicably disappear at any time.</p>
|
||||
<p class="gh-box gh-box-info">{{svg-jar "idea"}}This is a testing ground for new or experimental features. They may change, break or inexplicably disappear at any time.</p>
|
||||
|
||||
{{#if session.user.isOwner}}
|
||||
<div class="gh-setting-header">Members (BETA) </div>
|
||||
<div class="flex flex-column br3 shadow-1 bg-grouped-table mt2">
|
||||
<div class="gh-setting-first gh-setting-last">
|
||||
<div class="gh-members-setting-content">
|
||||
<div class="flex">
|
||||
<div class="flex flex-column flex-grow-1">
|
||||
<div class="gh-setting-title pl5 pt5">Members</div>
|
||||
<div class="gh-setting-desc pl5 pb5">Enable membership for your site</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="for-switch pa5">{{gh-feature-flag "members"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#liquid-if feature.labs.members}}
|
||||
{{gh-members-lab-setting
|
||||
settings=settings
|
||||
setDefaultContentVisibility=(action "setDefaultContentVisibility")
|
||||
setMembersSubscriptionSettings=(action "setMembersSubscriptionSettings")
|
||||
}}
|
||||
|
||||
<div class="mt5 pl5 pr5 pb5">
|
||||
{{gh-task-button "Save members settings"
|
||||
task=saveSettings
|
||||
successText="Saved"
|
||||
runningText="Saving"
|
||||
class="gh-btn gh-btn-blue gh-btn-icon"
|
||||
}}
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-setting-header">Migration options</div>
|
||||
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
|
||||
@ -74,19 +111,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gh-setting-header">Testing tools</div>
|
||||
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
|
||||
<div class="gh-setting-first gh-setting-last">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Test email configuration</div>
|
||||
<div class="gh-setting-desc">Send yourself a test email to make sure everything is working</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
{{gh-task-button "Send" successText="Sent" task=sendTestEmail class="gh-btn gh-btn-hover-blue gh-btn-icon"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gh-setting-header">Beta features</div>
|
||||
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
|
||||
<div class="gh-setting-first">
|
||||
@ -109,20 +133,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<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>
|
||||
<div class="gh-setting">
|
||||
{{#gh-uploader
|
||||
extensions=jsonExtension
|
||||
@ -214,44 +224,20 @@
|
||||
{{/gh-uploader}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if session.user.isOwner}}
|
||||
{{#if config.enableDeveloperExperiments}}
|
||||
<div class="gh-setting-header">Members (BETA) </div>
|
||||
<div class="flex flex-column br3 shadow-1 bg-grouped-table mt2">
|
||||
<div class="gh-setting-first gh-setting-last">
|
||||
<div class="gh-members-setting-content">
|
||||
<div class="flex">
|
||||
<div class="flex flex-column flex-grow-1">
|
||||
<div class="gh-setting-title pl5 pt5">Members</div>
|
||||
<div class="gh-setting-desc pl5 pb5">Enable membership for your site</div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="for-switch pa5">{{gh-feature-flag "members"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#liquid-if feature.labs.members}}
|
||||
{{gh-members-lab-setting
|
||||
settings=settings
|
||||
setDefaultContentVisibility=(action "setDefaultContentVisibility")
|
||||
setMembersSubscriptionSettings=(action "setMembersSubscriptionSettings")
|
||||
}}
|
||||
|
||||
<div class="mt5 pl5 pr5 pb5">
|
||||
{{gh-task-button "Save members settings"
|
||||
task=saveSettings
|
||||
successText="Saved"
|
||||
runningText="Saving"
|
||||
class="gh-btn gh-btn-blue gh-btn-icon"
|
||||
}}
|
||||
</div>
|
||||
{{/liquid-if}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="gh-setting-header">Testing tools</div>
|
||||
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
|
||||
<div class="gh-setting-first gh-setting-last">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Test email configuration</div>
|
||||
<div class="gh-setting-desc">Send yourself a test email to make sure everything is working</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="gh-setting-action">
|
||||
{{gh-task-button "Send" successText="Sent" task=sendTestEmail class="gh-btn gh-btn-hover-blue gh-btn-icon"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
@ -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"}}
|
@ -1,7 +1,7 @@
|
||||
{{#unless selectedTag}}
|
||||
<section class="gh-canvas tags-view">
|
||||
<header class="gh-canvas-header">
|
||||
{{#gh-view-title}}<span>Tags</span>{{/gh-view-title}}
|
||||
<GhCanvasHeader class="gh-canvas-header tags-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>Tags</h2>
|
||||
<section class="view-actions">
|
||||
<div class="gh-contentfilter gh-btn-group">
|
||||
<button class="gh-btn {{if (eq type "public") "gh-btn-group-selected"}}" {{action "changeType" "public"}}><span>Public tags</span></button>
|
||||
@ -9,16 +9,15 @@
|
||||
</div>
|
||||
{{#link-to "tags.new" class="gh-btn gh-btn-green"}}<span>New tag</span>{{/link-to}}
|
||||
</section>
|
||||
</header>
|
||||
|
||||
</GhCanvasHeader>
|
||||
<section class="content-list">
|
||||
<ol class="tags-list gh-list {{unless sortedTags "no-posts"}}">
|
||||
{{#if sortedTags}}
|
||||
<li class="gh-list-row header">
|
||||
<div class="gh-list-header gh-list-cellwidth-1-2">Tag</div>
|
||||
<div class="gh-list-header">Slug</div>
|
||||
<div class="gh-list-header">No. of posts</div>
|
||||
<div class="gh-list-header"></div>
|
||||
<div class="gh-list-header">Tag</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-10">Slug</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-10">No. of posts</div>
|
||||
<div class="gh-list-header gh-list-cellwidth-min"></div>
|
||||
</li>
|
||||
{{#vertical-collection
|
||||
items=sortedTags
|
||||
|
@ -18,7 +18,7 @@ export default function () {
|
||||
let subdir = path.substr(0, path.search('/ghost/'));
|
||||
let adminRoot = `${subdir}/ghost/`;
|
||||
let assetRoot = `${subdir}/ghost/assets/`;
|
||||
let apiRoot = `${subdir}/ghost/api/canary/admin`;
|
||||
let apiRoot = `${subdir}/ghost/api/v3/admin`;
|
||||
|
||||
function assetUrl(src) {
|
||||
return subdir + src;
|
||||
|
@ -10,7 +10,6 @@ import mockRoles from './config/roles';
|
||||
import mockSettings from './config/settings';
|
||||
import mockSite from './config/site';
|
||||
import mockSlugs from './config/slugs';
|
||||
import mockSubscribers from './config/subscribers';
|
||||
import mockTags from './config/tags';
|
||||
import mockThemes from './config/themes';
|
||||
import mockUploads from './config/uploads';
|
||||
@ -25,7 +24,7 @@ export default function () {
|
||||
this.passthrough('/ghost/assets/**');
|
||||
|
||||
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
|
||||
this.namespace = '/ghost/api/canary/admin'; // make this `api`, for example, if your API is namespaced
|
||||
this.namespace = '/ghost/api/v3/admin'; // make this `api`, for example, if your API is namespaced
|
||||
this.timing = 1000; // delay for each request, automatically set to 0 during testing
|
||||
this.logging = true;
|
||||
|
||||
@ -49,7 +48,7 @@ export default function () {
|
||||
// Mock all endpoints here as there is no real API during testing
|
||||
export function testConfig() {
|
||||
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
|
||||
this.namespace = '/ghost/api/canary/admin'; // make this `api`, for example, if your API is namespaced
|
||||
this.namespace = '/ghost/api/v3/admin'; // make this `api`, for example, if your API is namespaced
|
||||
// this.timing = 400; // delay for each request, automatically set to 0 during testing
|
||||
this.logging = false;
|
||||
|
||||
@ -65,7 +64,6 @@ export function testConfig() {
|
||||
mockSettings(this);
|
||||
mockSite(this);
|
||||
mockSlugs(this);
|
||||
mockSubscribers(this);
|
||||
mockTags(this);
|
||||
mockThemes(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',
|
||||
enableDeveloperExperiments: true,
|
||||
environment: 'development',
|
||||
labs: {publicAPI: true, subscribers: false},
|
||||
labs: {},
|
||||
mail: 'SMTP',
|
||||
version: '2.15.0',
|
||||
useGravatar: 'true'
|
||||
|
@ -73,7 +73,7 @@ export default [
|
||||
{
|
||||
id: 12,
|
||||
key: 'labs',
|
||||
value: '{"subscribers":true}',
|
||||
value: '{}',
|
||||
type: 'blog',
|
||||
created_at: '2015-01-12T18:29:01.000Z',
|
||||
created_by: 1,
|
||||
|
@ -4,7 +4,6 @@ export default function (server) {
|
||||
|
||||
// server.createList('contact', 10);
|
||||
|
||||
server.createList('subscriber', 125);
|
||||
server.createList('tag', 100);
|
||||
|
||||
server.create('integration', {name: 'Demo'});
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghost-admin",
|
||||
"version": "2.37.0",
|
||||
"version": "3.0.0-beta.6",
|
||||
"description": "Ember.js admin client for Ghost",
|
||||
"author": "Ghost Foundation",
|
||||
"homepage": "http://ghost.org",
|
||||
@ -22,7 +22,7 @@
|
||||
"lint:js": "eslint ."
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8.10.0 || ^10.13.0"
|
||||
"node": "^8.16.0 || ^10.13.0 || ^12.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ember/jquery": "1.1.0",
|
||||
|
@ -0,0 +1 @@
|
||||
<svg width="174" height="28" viewBox="0 0 174 28" xmlns="http://www.w3.org/2000/svg"><title>stripe-verfied-partner</title><g fill-rule="nonzero" fill="none"><rect fill="#FFF" width="174" height="28" rx="6"/><path d="M79.79 11.12L77.1 18h-1.44l-2.7-6.88h1.71l1.71 4.63 1.71-4.63h1.7zm.78 6.88v-6.88h4.34v1.36h-2.67v1.37h2.28v1.31h-2.28v1.48h2.75V18h-4.42zm7.34-5.56v1.82h.76c.57 0 .98-.39.98-.91 0-.54-.41-.91-.98-.91h-.76zM86.28 18v-6.88h2.61c1.42 0 2.43.92 2.43 2.23 0 .89-.47 1.61-1.26 1.99L91.59 18h-1.78l-1.34-2.43h-.56V18h-1.63zm6.4 0v-6.88h1.67V18h-1.67zm3.31 0v-6.88h4.34v1.36h-2.67v1.63h2.28v1.36h-2.28V18h-1.67zm5.47 0v-6.88h1.67V18h-1.67zm3.31 0v-6.88h4.34v1.36h-2.67v1.37h2.28v1.31h-2.28v1.48h2.75V18h-4.42zm5.71 0v-6.88h2.73c1.99 0 3.4 1.43 3.4 3.44S115.2 18 113.21 18h-2.73zm1.67-5.52v4.16h.98c1.05 0 1.78-.85 1.78-2.08s-.73-2.08-1.78-2.08h-.98zm8.17 5.52v-6.88h2.65c1.44 0 2.46.94 2.46 2.27s-1.02 2.25-2.46 2.25h-1.02V18h-1.63zm1.63-5.56v1.89h.8c.59 0 1.02-.39 1.02-.94 0-.57-.43-.95-1.02-.95h-.8zm3.12 5.56l2.7-6.88h1.44L131.9 18h-1.62l-.53-1.42h-2.53L126.7 18h-1.63zm3.42-4.87l-.81 2.21h1.61l-.8-2.21zm5.82-.69v1.82h.76c.57 0 .98-.39.98-.91 0-.54-.41-.91-.98-.91h-.76zM132.68 18v-6.88h2.61c1.42 0 2.43.92 2.43 2.23 0 .89-.47 1.61-1.26 1.99l1.53 2.66h-1.78l-1.34-2.43h-.56V18h-1.63zm7.64 0v-5.52h-1.99v-1.36h5.63v1.36h-1.98V18h-1.66zm4.7 0v-6.88h1.44l3.1 4.24v-4.24h1.55V18h-1.44l-3.09-4.24V18h-1.56zm7.73 0v-6.88h4.34v1.36h-2.67v1.37h2.28v1.31h-2.28v1.48h2.75V18h-4.42zm7.34-5.56v1.82h.76c.57 0 .98-.39.98-.91 0-.54-.41-.91-.98-.91h-.76zM158.46 18v-6.88h2.61c1.42 0 2.43.92 2.43 2.23 0 .89-.47 1.61-1.26 1.99l1.53 2.66h-1.78l-1.34-2.43h-.56V18h-1.63zM51 10.068L65 7v10.935L51 21V10.068zm5.323 6.469c.298.282.773.28 1.07-.004l3.814-3.664c.297-.29.297-.759 0-1.048-.298-.29-.78-.29-1.078 0l-3.27 3.129-.986-.9c-.297-.29-.78-.29-1.078 0-.298.29-.298.758 0 1.048l1.528 1.439zM42.97 15.07h-4.428c.1 1.093.877 1.415 1.759 1.415.899 0 1.606-.194 2.223-.516v1.88c-.615.351-1.427.605-2.508.605-2.204 0-3.748-1.423-3.748-4.237 0-2.376 1.31-4.263 3.462-4.263C41.88 9.954 43 11.84 43 14.23c0 .225-.02.714-.03.84zm-3.254-3.214c-.566 0-1.195.44-1.195 1.492h2.34c0-1.05-.59-1.492-1.145-1.492zm-7.037 6.598c-.791 0-1.275-.345-1.6-.59l-.005 2.64-2.262.496-.001-10.89h1.992l.118.576c.313-.302.886-.732 1.773-.732 1.588 0 3.085 1.476 3.085 4.192 0 2.965-1.48 4.308-3.1 4.308zm-.526-6.434c-.52 0-.845.196-1.08.463l.013 3.467c.219.245.536.443 1.067.443.836 0 1.397-.94 1.397-2.196 0-1.22-.57-2.177-1.397-2.177zm-6.538-1.91h2.271v8.177h-2.27v-8.178zm0-2.612L27.885 7v1.9l-2.27.498v-1.9zm-2.346 5.245v5.544h-2.262v-8.178h1.956l.143.69c.529-1.004 1.587-.8 1.888-.69v2.145c-.288-.096-1.19-.235-1.725.489zm-4.775 2.675c0 1.375 1.427.947 1.717.827v1.9c-.301.17-.848.309-1.588.309-1.343 0-2.35-1.02-2.35-2.401l.01-7.486 2.209-.484.002 2.026h1.718v1.99h-1.718v3.319zm-2.746.398c0 1.68-1.296 2.638-3.178 2.638-.78 0-1.633-.156-2.474-.53v-2.227c.76.426 1.727.745 2.477.745.504 0 .868-.14.868-.57 0-1.115-3.44-.695-3.44-3.278 0-1.652 1.224-2.64 3.059-2.64.75 0 1.499.119 2.248.427v2.197c-.688-.383-1.562-.6-2.25-.6-.474 0-.769.14-.769.505 0 1.05 3.46.551 3.46 3.333z" fill="#555ABF"/></g></svg>
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 1004 B |
@ -3,7 +3,6 @@ import {authenticateSession} from 'ember-simple-auth/test-support';
|
||||
import {beforeEach, describe, it} from 'mocha';
|
||||
import {click, currentRouteName, fillIn, find, findAll, visit} from '@ember/test-helpers';
|
||||
import {expect} from 'chai';
|
||||
import {fileUpload} from '../helpers/file-upload';
|
||||
import {setupApplicationTest} from 'ember-mocha';
|
||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||
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(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 () {
|
||||
|
@ -32,8 +32,6 @@ describe('Acceptance: Members', function () {
|
||||
describe('as owner', function () {
|
||||
beforeEach(async function () {
|
||||
this.server.loadFixtures('configs');
|
||||
let config = this.server.schema.configs.first();
|
||||
config.update({enableDeveloperExperiments: true});
|
||||
|
||||
let role = this.server.create('role', {name: 'Owner'});
|
||||
this.server.create('user', {roles: [role]});
|
||||
@ -41,17 +39,6 @@ describe('Acceptance: Members', function () {
|
||||
return await authenticateSession();
|
||||
});
|
||||
|
||||
it('redirects to home if developer experiments is disabled', async function () {
|
||||
let config = this.server.schema.configs.first();
|
||||
config.update({enableDeveloperExperiments: false});
|
||||
|
||||
await visit('/members');
|
||||
|
||||
expect(currentURL()).to.equal('/site');
|
||||
expect(find('[data-test-nav="members"]'), 'sidebar link')
|
||||
.to.not.exist;
|
||||
});
|
||||
|
||||
it('shows sidebar link which navigates to members list', async function () {
|
||||
await visit('/settings/labs');
|
||||
await click('#labs-members');
|
||||
|
@ -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)');
|
||||
});
|
||||
});
|
||||
});
|
@ -18,7 +18,7 @@ describe('Integration: Adapter: tag', function () {
|
||||
});
|
||||
|
||||
it('loads tags from regular endpoint when all are fetched', function (done) {
|
||||
server.get('/ghost/api/canary/admin/tags/', function () {
|
||||
server.get('/ghost/api/v3/admin/tags/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, JSON.stringify({tags: [
|
||||
{
|
||||
id: 1,
|
||||
@ -40,7 +40,7 @@ describe('Integration: Adapter: tag', function () {
|
||||
});
|
||||
|
||||
it('loads tag from slug endpoint when single tag is queried and slug is passed in', function (done) {
|
||||
server.get('/ghost/api/canary/admin/tags/slug/tag-1/', function () {
|
||||
server.get('/ghost/api/v3/admin/tags/slug/tag-1/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, JSON.stringify({tags: [
|
||||
{
|
||||
id: 1,
|
||||
|
@ -18,7 +18,7 @@ describe('Integration: Adapter: user', function () {
|
||||
});
|
||||
|
||||
it('loads users from regular endpoint when all are fetched', function (done) {
|
||||
server.get('/ghost/api/canary/admin/users/', function () {
|
||||
server.get('/ghost/api/v3/admin/users/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, JSON.stringify({users: [
|
||||
{
|
||||
id: 1,
|
||||
@ -40,7 +40,7 @@ describe('Integration: Adapter: user', function () {
|
||||
});
|
||||
|
||||
it('loads user from slug endpoint when single user is queried and slug is passed in', function (done) {
|
||||
server.get('/ghost/api/canary/admin/users/slug/user-1/', function () {
|
||||
server.get('/ghost/api/v3/admin/users/slug/user-1/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, JSON.stringify({users: [
|
||||
{
|
||||
id: 1,
|
||||
@ -58,7 +58,7 @@ describe('Integration: Adapter: user', function () {
|
||||
});
|
||||
|
||||
it('handles "include" parameter when querying single user via slug', function (done) {
|
||||
server.get('/ghost/api/canary/admin/users/slug/user-1/', (request) => {
|
||||
server.get('/ghost/api/v3/admin/users/slug/user-1/', (request) => {
|
||||
let params = request.queryParams;
|
||||
expect(params.include, 'include query').to.equal('roles,count.posts');
|
||||
|
||||
|
@ -18,13 +18,13 @@ const notificationsStub = Service.extend({
|
||||
});
|
||||
|
||||
const stubSuccessfulUpload = function (server, delay = 0) {
|
||||
server.post('/ghost/api/canary/admin/images/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, '{"url":"/content/images/test.png"}'];
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const stubFailedUpload = function (server, code, error, delay = 0) {
|
||||
server.post('/ghost/api/canary/admin/images/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/', function () {
|
||||
return [code, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||
errors: [{
|
||||
type: error,
|
||||
@ -41,7 +41,7 @@ describe('Integration: Component: gh-file-uploader', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
server = new Pretender();
|
||||
this.set('uploadUrl', '/ghost/api/canary/admin/images/');
|
||||
this.set('uploadUrl', '/ghost/api/v3/admin/images/');
|
||||
|
||||
this.owner.register('service:notifications', notificationsStub);
|
||||
});
|
||||
@ -86,7 +86,7 @@ describe('Integration: Component: gh-file-uploader', function () {
|
||||
await fileUpload('input[type="file"]', ['test'], {name: 'test.csv'});
|
||||
|
||||
expect(server.handledRequests.length).to.equal(1);
|
||||
expect(server.handledRequests[0].url).to.equal('/ghost/api/canary/admin/images/');
|
||||
expect(server.handledRequests[0].url).to.equal('/ghost/api/v3/admin/images/');
|
||||
});
|
||||
|
||||
it('fires uploadSuccess action on successful upload', async function () {
|
||||
@ -185,7 +185,7 @@ describe('Integration: Component: gh-file-uploader', function () {
|
||||
});
|
||||
|
||||
it('handles file too large error directly from the web server', async function () {
|
||||
server.post('/ghost/api/canary/admin/images/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/', function () {
|
||||
return [413, {}, ''];
|
||||
});
|
||||
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
|
||||
@ -205,7 +205,7 @@ describe('Integration: Component: gh-file-uploader', function () {
|
||||
});
|
||||
|
||||
it('handles unknown failure', async function () {
|
||||
server.post('/ghost/api/canary/admin/images/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/', function () {
|
||||
return [500, {'Content-Type': 'application/json'}, ''];
|
||||
});
|
||||
await render(hbs`{{gh-file-uploader url=uploadUrl}}`);
|
||||
|
@ -29,13 +29,13 @@ const sessionStub = Service.extend({
|
||||
});
|
||||
|
||||
const stubSuccessfulUpload = function (server, delay = 0) {
|
||||
server.post('/ghost/api/canary/admin/images/upload/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/upload/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, '{"images": [{"url":"/content/images/test.png"}]}'];
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const stubFailedUpload = function (server, code, error, delay = 0) {
|
||||
server.post('/ghost/api/canary/admin/images/upload/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/upload/', function () {
|
||||
return [code, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||
errors: [{
|
||||
type: error,
|
||||
@ -78,7 +78,7 @@ describe('Integration: Component: gh-image-uploader', function () {
|
||||
await fileUpload('input[type="file"]', ['test'], {name: 'test.png'});
|
||||
|
||||
expect(server.handledRequests.length).to.equal(1);
|
||||
expect(server.handledRequests[0].url).to.equal('/ghost/api/canary/admin/images/upload/');
|
||||
expect(server.handledRequests[0].url).to.equal('/ghost/api/v3/admin/images/upload/');
|
||||
expect(server.handledRequests[0].requestHeaders.Authorization).to.be.undefined;
|
||||
});
|
||||
|
||||
@ -177,7 +177,7 @@ describe('Integration: Component: gh-image-uploader', function () {
|
||||
});
|
||||
|
||||
it('handles file too large error directly from the web server', async function () {
|
||||
server.post('/ghost/api/canary/admin/images/upload/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/upload/', function () {
|
||||
return [413, {}, ''];
|
||||
});
|
||||
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||
@ -197,7 +197,7 @@ describe('Integration: Component: gh-image-uploader', function () {
|
||||
});
|
||||
|
||||
it('handles unknown failure', async function () {
|
||||
server.post('/ghost/api/canary/admin/images/upload/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/upload/', function () {
|
||||
return [500, {'Content-Type': 'application/json'}, ''];
|
||||
});
|
||||
await render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||
|
@ -8,13 +8,13 @@ import {expect} from 'chai';
|
||||
import {setupRenderingTest} from 'ember-mocha';
|
||||
|
||||
const stubSuccessfulUpload = function (server, delay = 0) {
|
||||
server.post('/ghost/api/canary/admin/images/upload/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/upload/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, '{"images": [{"url": "/content/images/test.png"}]}'];
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const stubFailedUpload = function (server, code, error, delay = 0) {
|
||||
server.post('/ghost/api/canary/admin/images/upload/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/upload/', function () {
|
||||
return [code, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||
errors: [{
|
||||
type: error,
|
||||
@ -50,7 +50,7 @@ describe('Integration: Component: gh-uploader', function () {
|
||||
|
||||
let [lastRequest] = server.handledRequests;
|
||||
expect(server.handledRequests.length).to.equal(1);
|
||||
expect(lastRequest.url).to.equal('/ghost/api/canary/admin/images/upload/');
|
||||
expect(lastRequest.url).to.equal('/ghost/api/v3/admin/images/upload/');
|
||||
// requestBody is a FormData object
|
||||
// this will fail in anything other than Chrome and Firefox
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FormData#Browser_compatibility
|
||||
@ -135,7 +135,7 @@ describe('Integration: Component: gh-uploader', function () {
|
||||
|
||||
it('onComplete returns results in same order as selected', async function () {
|
||||
// first request has a delay to simulate larger file
|
||||
server.post('/ghost/api/canary/admin/images/upload/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/upload/', function () {
|
||||
// second request has no delay to simulate small file
|
||||
stubSuccessfulUpload(server, 0);
|
||||
|
||||
@ -257,7 +257,7 @@ describe('Integration: Component: gh-uploader', function () {
|
||||
});
|
||||
|
||||
it('uploads to supplied `uploadUrl`', async function () {
|
||||
server.post('/ghost/api/canary/admin/images/', function () {
|
||||
server.post('/ghost/api/v3/admin/images/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, '{"images": [{"url": "/content/images/test.png"}]'];
|
||||
});
|
||||
|
||||
@ -266,7 +266,7 @@ describe('Integration: Component: gh-uploader', function () {
|
||||
await settled();
|
||||
|
||||
let [lastRequest] = server.handledRequests;
|
||||
expect(lastRequest.url).to.equal('/ghost/api/canary/admin/images/');
|
||||
expect(lastRequest.url).to.equal('/ghost/api/v3/admin/images/');
|
||||
});
|
||||
|
||||
it('passes supplied paramName in request', async function () {
|
||||
|
@ -32,7 +32,7 @@ describe('Integration: Service: config', function () {
|
||||
|
||||
it('normalizes blogUrl to non-trailing-slash', function (done) {
|
||||
let stubBlogUrl = function stubBlogUrl(url) {
|
||||
server.get('/ghost/api/canary/admin/config/', function () {
|
||||
server.get('/ghost/api/v3/admin/config/', function () {
|
||||
return [
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
@ -40,7 +40,7 @@ describe('Integration: Service: config', function () {
|
||||
];
|
||||
});
|
||||
|
||||
server.get('/ghost/api/canary/admin/site/', function () {
|
||||
server.get('/ghost/api/v3/admin/site/', function () {
|
||||
return [
|
||||
200,
|
||||
{'Content-Type': 'application/json'},
|
||||
|
@ -17,11 +17,11 @@ function stubSettings(server, labs, validSave = true) {
|
||||
}
|
||||
];
|
||||
|
||||
server.get('/ghost/api/canary/admin/settings/', function () {
|
||||
server.get('/ghost/api/v3/admin/settings/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, JSON.stringify({settings})];
|
||||
});
|
||||
|
||||
server.put('/ghost/api/canary/admin/settings/', function (request) {
|
||||
server.put('/ghost/api/v3/admin/settings/', function (request) {
|
||||
let statusCode = (validSave) ? 200 : 400;
|
||||
let response = (validSave) ? request.requestBody : JSON.stringify({
|
||||
errors: [{
|
||||
@ -47,11 +47,11 @@ function stubUser(server, accessibility, validSave = true) {
|
||||
}]
|
||||
}];
|
||||
|
||||
server.get('/ghost/api/canary/admin/users/me/', function () {
|
||||
server.get('/ghost/api/v3/admin/users/me/', function () {
|
||||
return [200, {'Content-Type': 'application/json'}, JSON.stringify({users})];
|
||||
});
|
||||
|
||||
server.put('/ghost/api/canary/admin/users/1/', function (request) {
|
||||
server.put('/ghost/api/v3/admin/users/1/', function (request) {
|
||||
let statusCode = (validSave) ? 200 : 400;
|
||||
let response = (validSave) ? request.requestBody : JSON.stringify({
|
||||
errors: [{
|
||||
|
@ -5,7 +5,7 @@ import {expect} from 'chai';
|
||||
import {setupTest} from 'ember-mocha';
|
||||
|
||||
function stubSlugEndpoint(server, type, slug) {
|
||||
server.get('/ghost/api/canary/admin/slugs/:type/:slug/', function (request) {
|
||||
server.get('/ghost/api/v3/admin/slugs/:type/:slug/', function (request) {
|
||||
expect(request.params.type).to.equal(type);
|
||||
expect(request.params.slug).to.equal(slug);
|
||||
|
||||
|
@ -21,7 +21,7 @@ describe('Integration: Service: store', function () {
|
||||
let {version} = config.APP;
|
||||
let store = this.owner.lookup('service:store');
|
||||
|
||||
server.get('/ghost/api/canary/admin/posts/1/', function () {
|
||||
server.get('/ghost/api/v3/admin/posts/1/', function () {
|
||||
return [
|
||||
404,
|
||||
{'Content-Type': 'application/json'},
|
||||
|
@ -42,7 +42,7 @@ const mockTour = Service.extend({
|
||||
});
|
||||
|
||||
const mockGhostPaths = Service.extend({
|
||||
apiRoot: '/ghost/api/canary/admin'
|
||||
apiRoot: '/ghost/api/v3/admin'
|
||||
});
|
||||
|
||||
describe('Unit: Authenticator: cookie', () => {
|
||||
@ -74,7 +74,7 @@ describe('Unit: Authenticator: cookie', () => {
|
||||
let tour = this.owner.lookup('service:tour');
|
||||
|
||||
return authenticator.authenticate('AzureDiamond', 'hunter2').then(() => {
|
||||
expect(post.args[0][0]).to.equal('/ghost/api/canary/admin/session');
|
||||
expect(post.args[0][0]).to.equal('/ghost/api/v3/admin/session');
|
||||
expect(post.args[0][1]).to.deep.include({
|
||||
data: {
|
||||
username: 'AzureDiamond',
|
||||
@ -103,7 +103,7 @@ describe('Unit: Authenticator: cookie', () => {
|
||||
let del = authenticator.ajax.del;
|
||||
|
||||
return authenticator.invalidate().then(() => {
|
||||
expect(del.args[0][0]).to.equal('/ghost/api/canary/admin/session');
|
||||
expect(del.args[0][0]).to.equal('/ghost/api/v3/admin/session');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -23,7 +23,7 @@ describe('Unit: Model: invite', function () {
|
||||
let model = store.createRecord('invite');
|
||||
let role;
|
||||
|
||||
server.post('/ghost/api/canary/admin/invites/', function () {
|
||||
server.post('/ghost/api/v3/admin/invites/', function () {
|
||||
return [200, {}, '{}'];
|
||||
});
|
||||
|
||||
|
@ -17,7 +17,7 @@ describe('Unit: Serializer: notification', function () {
|
||||
});
|
||||
|
||||
it('converts location->key when deserializing', function () {
|
||||
server.get('/ghost/api/canary/admin/notifications', function () {
|
||||
server.get('/ghost/api/v3/admin/notifications', function () {
|
||||
let response = {
|
||||
notifications: [{
|
||||
id: 1,
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -3693,6 +3693,11 @@ commander@0.6.1:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06"
|
||||
integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=
|
||||
|
||||
commander@2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
|
||||
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
|
||||
|
||||
commander@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873"
|
||||
@ -3705,7 +3710,7 @@ commander@2.8.x:
|
||||
dependencies:
|
||||
graceful-readlink ">= 1.0.0"
|
||||
|
||||
commander@^2.15.1, commander@^2.19.0, commander@^2.20.0, commander@^2.6.0, commander@~2.20.0:
|
||||
commander@^2.15.1, commander@^2.19.0, commander@^2.20.0, commander@^2.6.0:
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.1.tgz#3863ce3ca92d0831dcf2a102f5fb4b5926afd0f9"
|
||||
integrity sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==
|
||||
@ -5693,12 +5698,12 @@ engine.io@~3.4.0:
|
||||
ws "^7.1.2"
|
||||
|
||||
enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
|
||||
integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66"
|
||||
integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==
|
||||
dependencies:
|
||||
graceful-fs "^4.1.2"
|
||||
memory-fs "^0.4.0"
|
||||
memory-fs "^0.5.0"
|
||||
tapable "^1.0.0"
|
||||
|
||||
ensure-posix-path@^1.0.0, ensure-posix-path@^1.0.1, ensure-posix-path@^1.0.2, ensure-posix-path@^1.1.0:
|
||||
@ -7213,9 +7218,9 @@ hooker@~0.2.3:
|
||||
integrity sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=
|
||||
|
||||
hosted-git-info@^2.1.4, hosted-git-info@^2.7.1:
|
||||
version "2.8.4"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546"
|
||||
integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==
|
||||
version "2.8.5"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
|
||||
integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==
|
||||
|
||||
hsl-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
@ -8725,7 +8730,15 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
memory-fs@^0.4.0, memory-fs@~0.4.1:
|
||||
memory-fs@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
|
||||
integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==
|
||||
dependencies:
|
||||
errno "^0.1.3"
|
||||
readable-stream "^2.0.1"
|
||||
|
||||
memory-fs@~0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
||||
integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
|
||||
@ -9202,9 +9215,9 @@ node-pre-gyp@^0.12.0:
|
||||
tar "^4"
|
||||
|
||||
node-releases@^1.1.29:
|
||||
version "1.1.34"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.34.tgz#ced4655ee1ba9c3a2c5dcbac385e19434155fd40"
|
||||
integrity sha512-fNn12JTEfniTuCqo0r9jXgl44+KxRH/huV7zM/KAGOKxDKrHr6EbT7SSs4B+DNxyBE2mks28AD+Jw6PkfY5uwA==
|
||||
version "1.1.35"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.35.tgz#32a74a3cd497aa77f23d509f483475fd160e4c48"
|
||||
integrity sha512-JGcM/wndCN/2elJlU0IGdVEJQQnJwsLbgPCFd2pY7V0mxf17bZ0Gb/lgOtL29ZQhvEX5shnVhxQyZz3ex94N8w==
|
||||
dependencies:
|
||||
semver "^6.3.0"
|
||||
|
||||
@ -12135,11 +12148,11 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
|
||||
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5"
|
||||
integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==
|
||||
version "3.6.1"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.1.tgz#ae7688c50e1bdcf2f70a0e162410003cf9798311"
|
||||
integrity sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==
|
||||
dependencies:
|
||||
commander "~2.20.0"
|
||||
commander "2.20.0"
|
||||
source-map "~0.6.1"
|
||||
|
||||
underscore.string@^3.2.2, underscore.string@~3.3.4:
|
||||
|
Loading…
Reference in New Issue
Block a user