mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 06:35:49 +03:00
Merge branch 'master' into v3
This commit is contained in:
commit
159e810d5c
@ -16,10 +16,7 @@ install:
|
||||
- yarn
|
||||
script:
|
||||
- yarn run lint:js && yarn run test
|
||||
notifications:
|
||||
slack:
|
||||
rooms:
|
||||
- secure: qEi0rR3dNcOthdz5VItSrYv73hUFE19cRqNl6qkaoN5uyOxrV75W2HeR7zQ7qbIi6MhHvV+Y9kT96RKPXtBilTm5/+pZ2bogglnrR8kGy8YkWdDgEqCUPqTLX1zgm92tjFsNia5CYFwxnasUhogZ0G3uSYVjJjoNzQ8HZdk/DTfl52jmJhJ4cXT2Hd2lvREm+iNwgwy2ena5trJOYqWS/5aov3ls1vK4H2qQOOVdeQ0Jc/hpipSm/WIR0wQal58tnQUbnQ2LhyCP6EhtrlINhQBboRcD8J7fTXlfAZdy4HrEnbEhnrYQefBsqYnQL1s+q8kvjsMi7hujo/O0kywaE1tLDLJL98I+JLA3k1JZeFfXxU9rwXsTDF8McQbaeqYZb/4NtU6aO3By45S8mz2Y9L6nLREmmeVsiFkelkiKHaWkeOMmgji0lHZ/56NyN5jidbRKW+PevGFQacsNhs3pBglvqLwZUVCcXfSGAUVM88HHbVqZOq/nktatqC41qhv+V5eLksJ1bHAz2iO3/6QPVfCkw4iqJd+iWkMIZY9Pmvh8JwxajflNvemiGUOO+2vz9gt4ua7IKbM+bX1UH19nPmeADnv+0r65YcnHSntSD3rYFQuHQInthnadyUeTKx/mWTOTIC1+bmnI3Ltkd2qxG1ui4riHDBXbEXDFFjBfRhI=
|
||||
on_success: change
|
||||
on_failure: always
|
||||
on_pull_requests: false
|
||||
after_failure: |
|
||||
if [ "${TRAVIS_EVENT_TYPE}" == "cron" ]; then
|
||||
curl -X POST --data-urlencode "payload={\"attachments\": [{\"color\": \"danger\", \"fallback\": \"Build Failure: $TRAVIS_JOB_WEB_URL\", \"title\": \"Build Failure\", \"text\": \"$TRAVIS_JOB_WEB_URL\"}]}" $SLACK_URL
|
||||
fi
|
||||
|
@ -16,11 +16,12 @@ export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
member: null,
|
||||
initialsClass: 'f6 fw4',
|
||||
|
||||
backgroundStyle: computed('member.name', function () {
|
||||
let name = this.member.name;
|
||||
initialsClass: computed('sizeClass', function () {
|
||||
return this.sizeClass || 'f5 fw4 lh-zero';
|
||||
}),
|
||||
|
||||
backgroundStyle: computed('member.{name,email}', function () {
|
||||
let name = this.member.name || this.member.email;
|
||||
if (name) {
|
||||
let color = stringToHslColor(name, 55, 55);
|
||||
return htmlSafe(`background-color: ${color}`);
|
||||
@ -29,11 +30,11 @@ export default Component.extend({
|
||||
return htmlSafe('');
|
||||
}),
|
||||
|
||||
initials: computed('member.name', function () {
|
||||
let name = this.member.name;
|
||||
initials: computed('member.{name,email}', function () {
|
||||
let name = this.member.name || this.member.email;
|
||||
if (name) {
|
||||
let names = name.split(' ');
|
||||
let intials = [names[0][0], names[names.length - 1][0]];
|
||||
let intials = names.length > 1 ? [names[0][0], names[names.length - 1][0]] : [names[0][0]];
|
||||
return intials.join('').toUpperCase();
|
||||
}
|
||||
return '';
|
||||
|
56
ghost/admin/app/components/gh-member-settings-form.js
Normal file
56
ghost/admin/app/components/gh-member-settings-form.js
Normal file
@ -0,0 +1,56 @@
|
||||
import Component from '@ember/component';
|
||||
import boundOneWay from 'ghost-admin/utils/bound-one-way';
|
||||
import moment from 'moment';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
feature: service(),
|
||||
config: service(),
|
||||
mediaQueries: service(),
|
||||
|
||||
isViewingSubview: false,
|
||||
scratchDescription: '',
|
||||
|
||||
// Allowed actions
|
||||
setProperty: () => {},
|
||||
showDeleteTagModal: () => {},
|
||||
|
||||
scratchName: boundOneWay('member.name'),
|
||||
scratchEmail: boundOneWay('member.email'),
|
||||
subscriptions: computed('member.stripe', function () {
|
||||
let subscriptions = this.member.get('stripe');
|
||||
if (subscriptions && subscriptions.length > 0) {
|
||||
return subscriptions.map((subscription) => {
|
||||
return {
|
||||
customer: subscription.customer,
|
||||
name: '',
|
||||
email: subscription.email || '',
|
||||
status: subscription.status,
|
||||
startDate: '',
|
||||
planName: subscription.name,
|
||||
validUntil: moment(subscription.validUntil * 1000).format('MMM DD YYYY')
|
||||
};
|
||||
}).reverse();
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
hasMultipleSubscriptions: computed('member.stripe', function () {
|
||||
let subscriptions = this.member.get('stripe');
|
||||
if (subscriptions && subscriptions.length > 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setProperty(property, value) {
|
||||
this.setProperty(property, value);
|
||||
},
|
||||
|
||||
deleteTag() {
|
||||
this.showDeleteTagModal();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
25
ghost/admin/app/components/gh-members-list-item.js
Normal file
25
ghost/admin/app/components/gh-members-list-item.js
Normal file
@ -0,0 +1,25 @@
|
||||
import Component from '@ember/component';
|
||||
import moment from 'moment';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
ghostPaths: service(),
|
||||
notifications: service(),
|
||||
router: service(),
|
||||
|
||||
tagName: 'li',
|
||||
classNames: ['gh-flex-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})`;
|
||||
})
|
||||
});
|
@ -12,6 +12,7 @@ import {task, timeout} from 'ember-concurrency';
|
||||
const PSM_ANIMATION_LENGTH = 400;
|
||||
|
||||
export default Component.extend(SettingsMenuMixin, {
|
||||
feature: service(),
|
||||
store: service(),
|
||||
config: service(),
|
||||
ghostPaths: service(),
|
||||
|
32
ghost/admin/app/components/gh-psm-visibility-input.js
Normal file
32
ghost/admin/app/components/gh-psm-visibility-input.js
Normal file
@ -0,0 +1,32 @@
|
||||
import Component from '@ember/component';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const VISIBILITIES = [
|
||||
{label: 'Public', name: 'public'},
|
||||
{label: 'Members only', name: 'members'},
|
||||
{label: 'Paid-members only', name: 'paid'}
|
||||
];
|
||||
|
||||
export default Component.extend({
|
||||
|
||||
settings: service(),
|
||||
|
||||
// public attrs
|
||||
post: null,
|
||||
|
||||
selectedVisibility: computed('post.visibility', function () {
|
||||
return this.get('post.visibility') || this.settings.get('defaultContentVisibility');
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.availableVisibilities = VISIBILITIES;
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateVisibility(newVisibility) {
|
||||
this.post.set('visibility', newVisibility);
|
||||
}
|
||||
}
|
||||
});
|
43
ghost/admin/app/components/modal-import-members.js
Normal file
43
ghost/admin/app/components/modal-import-members.js
Normal file
@ -0,0 +1,43 @@
|
||||
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}/members/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,4 +1,5 @@
|
||||
import Controller from '@ember/controller';
|
||||
import moment from 'moment';
|
||||
import {alias} from '@ember/object/computed';
|
||||
import {computed} from '@ember/object';
|
||||
import {inject as controller} from '@ember/controller';
|
||||
@ -13,24 +14,19 @@ export default Controller.extend({
|
||||
|
||||
member: alias('model'),
|
||||
|
||||
subscription: computed('member.subscriptions', function () {
|
||||
let subscriptions = this.member.get('subscriptions');
|
||||
if (!subscriptions) {
|
||||
return {
|
||||
amount: '...',
|
||||
status: '...',
|
||||
plan: '...'
|
||||
};
|
||||
}
|
||||
let subscription = subscriptions[0] || {};
|
||||
return {
|
||||
amount: subscription.amount ? (subscription.amount / 100) : 0,
|
||||
status: subscription.status || '-',
|
||||
plan: subscription.plan || 'Free'
|
||||
};
|
||||
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})`;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setProperty() {
|
||||
return;
|
||||
},
|
||||
toggleDeleteTagModal() {
|
||||
this.toggleProperty('showDeleteMemberModal');
|
||||
},
|
||||
finaliseDeletion() {
|
||||
// decrememnt the total member count manually so there's no flash
|
||||
// when transitioning back to the members list
|
||||
@ -42,10 +38,12 @@ export default Controller.extend({
|
||||
},
|
||||
|
||||
fetchMember: task(function* (memberId) {
|
||||
this.set('isLoading', true);
|
||||
yield this.store.findRecord('member', memberId, {
|
||||
reload: true
|
||||
}).then((data) => {
|
||||
this.set('member', data);
|
||||
this.set('isLoading', false);
|
||||
});
|
||||
})
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
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';
|
||||
@ -11,7 +12,6 @@ export default Controller.extend({
|
||||
meta: null,
|
||||
members: null,
|
||||
searchText: '',
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('members', this.store.peekAll('member'));
|
||||
@ -27,13 +27,31 @@ export default Controller.extend({
|
||||
}
|
||||
|
||||
let {name, email} = member;
|
||||
return name.toLowerCase().indexOf(searchText) >= 0
|
||||
|| email.toLowerCase().indexOf(searchText) >= 0;
|
||||
return (name && name.toLowerCase().indexOf(searchText) >= 0)
|
||||
|| (email && email.toLowerCase().indexOf(searchText) >= 0);
|
||||
}).sort((a, b) => {
|
||||
return b.get('createdAt').valueOf() - a.get('createdAt').valueOf();
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}),
|
||||
|
||||
actions: {
|
||||
exportData() {
|
||||
let exportUrl = ghostPaths().url.api('members/csv');
|
||||
let downloadURL = `${exportUrl}`;
|
||||
let iframe = document.getElementById('iframeDownload');
|
||||
|
||||
if (!iframe) {
|
||||
iframe = document.createElement('iframe');
|
||||
iframe.id = 'iframeDownload';
|
||||
iframe.style.display = 'none';
|
||||
document.body.append(iframe);
|
||||
}
|
||||
iframe.setAttribute('src', downloadURL);
|
||||
}
|
||||
},
|
||||
|
||||
fetchMembers: task(function* () {
|
||||
let newFetchDate = new Date();
|
||||
let results;
|
||||
@ -42,17 +60,19 @@ export default Controller.extend({
|
||||
// fetch any records modified since last fetch
|
||||
results = yield this.store.query('member', {
|
||||
limit: 'all',
|
||||
filter: `updated_at:>='${moment.utc(this._lastFetchDate).format('YYYY-MM-DD HH:mm:ss')}'`
|
||||
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', {
|
||||
limit: 'all'
|
||||
limit: 'all',
|
||||
order: 'created_at desc'
|
||||
});
|
||||
this._hasFetchedAll = true;
|
||||
this.set('meta', results.meta);
|
||||
}
|
||||
|
||||
this.set('meta', results.meta);
|
||||
this._lastFetchDate = newFetchDate;
|
||||
})
|
||||
});
|
||||
|
19
ghost/admin/app/controllers/members/import.js
Normal file
19
ghost/admin/app/controllers/members/import.js
Normal file
@ -0,0 +1,19 @@
|
||||
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({
|
||||
members: controller(),
|
||||
router: service(),
|
||||
|
||||
actions: {
|
||||
fetchNewMembers() {
|
||||
this.members.fetchMembers.perform();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.router.transitionTo('members');
|
||||
}
|
||||
}
|
||||
});
|
@ -46,7 +46,6 @@ export default Controller.extend({
|
||||
importErrors: null,
|
||||
importSuccessful: false,
|
||||
showDeleteAllModal: false,
|
||||
showMemberConfig: false,
|
||||
submitting: false,
|
||||
uploadButtonText: 'Import',
|
||||
|
||||
@ -71,13 +70,14 @@ export default Controller.extend({
|
||||
});
|
||||
let monthlyPlan = stripeProcessor.config.plans.find(plan => plan.interval === 'month');
|
||||
let yearlyPlan = stripeProcessor.config.plans.find(plan => plan.interval === 'year');
|
||||
monthlyPlan.dollarAmount = (monthlyPlan.amount / 100);
|
||||
yearlyPlan.dollarAmount = (yearlyPlan.amount / 100);
|
||||
monthlyPlan.dollarAmount = parseInt(monthlyPlan.amount) ? (monthlyPlan.amount / 100) : 0;
|
||||
yearlyPlan.dollarAmount = parseInt(yearlyPlan.amount) ? (yearlyPlan.amount / 100) : 0;
|
||||
stripeProcessor.config.plans = {
|
||||
monthly: monthlyPlan,
|
||||
yearly: yearlyPlan
|
||||
};
|
||||
subscriptionSettings.stripeConfig = stripeProcessor.config;
|
||||
subscriptionSettings.requirePaymentForSetup = !!subscriptionSettings.requirePaymentForSetup;
|
||||
return subscriptionSettings;
|
||||
}),
|
||||
|
||||
@ -161,10 +161,6 @@ export default Controller.extend({
|
||||
this.toggleProperty('showDeleteAllModal');
|
||||
},
|
||||
|
||||
toggleMemberConfig() {
|
||||
this.toggleProperty('showMemberConfig');
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a file selection dialog - Triggered by "Upload x" buttons,
|
||||
* searches for the hidden file input within the .gh-setting element
|
||||
@ -180,6 +176,10 @@ export default Controller.extend({
|
||||
.click();
|
||||
},
|
||||
|
||||
setDefaultContentVisibility(value) {
|
||||
this.set('settings.defaultContentVisibility', value);
|
||||
},
|
||||
|
||||
setSubscriptionSettings(key, event) {
|
||||
let subscriptionSettings = this.parseSubscriptionSettings(this.get('settings.membersSubscriptionSettings'));
|
||||
let stripeProcessor = subscriptionSettings.paymentProcessors.find((proc) => {
|
||||
@ -189,6 +189,7 @@ export default Controller.extend({
|
||||
stripeConfig.product = {
|
||||
name: this.settings.get('title')
|
||||
};
|
||||
// TODO: this flag has to be removed as it doesn't serve any purpose
|
||||
if (key === 'isPaid') {
|
||||
subscriptionSettings.isPaid = event;
|
||||
}
|
||||
@ -198,11 +199,14 @@ export default Controller.extend({
|
||||
if (key === 'month' || key === 'year') {
|
||||
stripeConfig.plans = stripeConfig.plans.map((plan) => {
|
||||
if (key === plan.interval) {
|
||||
plan.amount = event.target.value * 100;
|
||||
plan.amount = parseInt(event.target.value) ? (event.target.value * 100) : 0;
|
||||
}
|
||||
return plan;
|
||||
});
|
||||
}
|
||||
if (key === 'requirePaymentForSignup') {
|
||||
subscriptionSettings.requirePaymentForSignup = !subscriptionSettings.requirePaymentForSignup;
|
||||
}
|
||||
this.set('settings.membersSubscriptionSettings', JSON.stringify(subscriptionSettings));
|
||||
}
|
||||
},
|
||||
|
@ -89,6 +89,9 @@ export default Controller.extend({
|
||||
// Generate slug based on name for new tag when empty
|
||||
if (propKey === 'name' && !tag.get('slug') && isNewTag) {
|
||||
let slugValue = slugify(newValue);
|
||||
if (/^#/.test(newValue)) {
|
||||
slugValue = 'hash-' + slugValue;
|
||||
}
|
||||
tag.set('slug', slugValue);
|
||||
}
|
||||
// TODO: This is required until .validate/.save mark fields as validated
|
||||
|
@ -1,9 +1,14 @@
|
||||
import EmberObject from '@ember/object';
|
||||
|
||||
export default EmberObject.extend({
|
||||
adapter: '',
|
||||
amount: 0,
|
||||
plan: '',
|
||||
status: '',
|
||||
validUntil: 0
|
||||
customer: 'customer_id',
|
||||
subscription: 'subscription_id',
|
||||
plan: 'plan_id',
|
||||
status: 'subscription status',
|
||||
name: 'plan nickname e.g. "Monthly"',
|
||||
interval: 'how often plan charges e.g "month", "year"',
|
||||
amount: 'amount in smallest denomination e.g. cents, so value for 5 dollars would be 500',
|
||||
currency: 'e.g. usd',
|
||||
last4: 'last four digits of card OR null',
|
||||
validUntil: 'epoch timestamp of when current interval ends IN SECONDS'
|
||||
});
|
||||
|
@ -5,5 +5,5 @@ export default DS.Model.extend({
|
||||
name: attr('string'),
|
||||
email: attr('string'),
|
||||
createdAt: attr('moment-utc'),
|
||||
subscriptions: attr('member-subscription')
|
||||
stripe: attr('member-subscription')
|
||||
});
|
||||
|
@ -93,6 +93,7 @@ export default Model.extend(Comparable, ValidationEngine, {
|
||||
twitterDescription: attr('string'),
|
||||
html: attr('string'),
|
||||
locale: attr('string'),
|
||||
visibility: attr('string'),
|
||||
metaDescription: attr('string'),
|
||||
metaTitle: attr('string'),
|
||||
mobiledoc: attr('json-string'),
|
||||
|
@ -29,6 +29,7 @@ export default Model.extend(ValidationEngine, {
|
||||
return {isActive: true};
|
||||
}
|
||||
}),
|
||||
defaultContentVisibility: attr('string'),
|
||||
membersSubscriptionSettings: attr('string'),
|
||||
metaTitle: attr('string'),
|
||||
metaDescription: attr('string'),
|
||||
|
@ -58,7 +58,9 @@ Router.map(function () {
|
||||
this.route('settings.integrations.unsplash', {path: '/settings/integrations/unsplash'});
|
||||
this.route('settings.integrations.zapier', {path: '/settings/integrations/zapier'});
|
||||
|
||||
this.route('members');
|
||||
this.route('members', function () {
|
||||
this.route('import');
|
||||
});
|
||||
this.route('member', {path: '/members/:member_id'});
|
||||
|
||||
this.route('error404', {path: '/*path'});
|
||||
|
4
ghost/admin/app/routes/members/import.js
Normal file
4
ghost/admin/app/routes/members/import.js
Normal file
@ -0,0 +1,4 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
});
|
@ -41,6 +41,10 @@ export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
|
||||
// Deprecated property (replaced with data.authors)
|
||||
delete json.author;
|
||||
|
||||
if (json.visibility === null) {
|
||||
delete json.visibility;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
});
|
||||
|
@ -53,6 +53,7 @@
|
||||
@import "layouts/user.css";
|
||||
@import "layouts/about.css";
|
||||
@import "layouts/tags.css";
|
||||
@import "layouts/members.css";
|
||||
@import "layouts/error.css";
|
||||
@import "layouts/apps.css";
|
||||
@import "layouts/packages.css";
|
||||
|
@ -53,6 +53,7 @@
|
||||
@import "layouts/user.css";
|
||||
@import "layouts/about.css";
|
||||
@import "layouts/tags.css";
|
||||
@import "layouts/members.css";
|
||||
@import "layouts/error.css";
|
||||
@import "layouts/apps.css";
|
||||
@import "layouts/packages.css";
|
||||
|
@ -23,23 +23,23 @@
|
||||
background: var(--whitegrey-l2);
|
||||
}
|
||||
|
||||
.gh-list-row:first-child .gh-list-cell:first-child,
|
||||
.gh-list-row:first-child .gh-list-data:first-child {
|
||||
.gh-list-row:first-of-type .gh-list-cell:first-child,
|
||||
.gh-list-row:first-of-type .gh-list-data:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-list-row:first-child .gh-list-cell:last-child,
|
||||
.gh-list-row:first-child .gh-list-data:last-child {
|
||||
.gh-list-row:first-of-type .gh-list-cell:last-child,
|
||||
.gh-list-row:first-of-type .gh-list-data:last-child {
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-list-row:last-child .gh-list-cell:first-child,
|
||||
.gh-list-row:last-child .gh-list-data:first-child {
|
||||
.gh-list-row:last-of-type .gh-list-cell:first-child,
|
||||
.gh-list-row:last-of-type .gh-list-data:first-child {
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-list-row:last-child .gh-list-cell:last-child,
|
||||
.gh-list-row:last-child .gh-list-data:last-child {
|
||||
.gh-list-row:last-of-type .gh-list-cell:last-child,
|
||||
.gh-list-row:last-of-type .gh-list-data:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@
|
||||
}
|
||||
|
||||
.gh-list-row:nth-of-type(2) .gh-list-data {
|
||||
border: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.gh-list-data.show-on-hover > *,
|
||||
@ -98,6 +98,10 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.gh-list-cellwidth-max {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gh-list-cellwidth-2-3 {
|
||||
width: 67%;
|
||||
}
|
||||
@ -152,6 +156,55 @@
|
||||
}
|
||||
|
||||
|
||||
/* Flex lists - header and responsive sizes must be custom
|
||||
/* -------------------------------------------------- */
|
||||
.gh-flex-list-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: var(--whitegrey) 1px solid;
|
||||
}
|
||||
|
||||
.gh-flex-list-row:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.gh-flex-list-row:not(.header):not(.loading):hover {
|
||||
background: var(--whitegrey-l2);
|
||||
}
|
||||
|
||||
.gh-flex-list-row:first-of-type {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-flex-list-row:last-of-type {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-flex-list-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.gh-flex-list-auto {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.gh-flex-list-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gh-flex-list-shrink {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Fake loading list
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
|
@ -147,7 +147,7 @@
|
||||
|
||||
.modal-body .gh-image-uploader {
|
||||
margin: 0;
|
||||
background: color-mod(var(--lightgrey) l(+4%));
|
||||
background: color-mod(var(--whitegrey) h(+7) s(-4%) l(+5%));
|
||||
}
|
||||
|
||||
|
||||
|
@ -235,6 +235,16 @@
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.gh-flow-content .login span {
|
||||
height: 37px !important;
|
||||
line-height: 37px !important;
|
||||
}
|
||||
|
||||
.gh-flow-content .gh-input:focus {
|
||||
box-shadow: none;
|
||||
border-color: color-mod(var(--midlightgrey) l(+10%));
|
||||
}
|
||||
|
||||
.gh-flow-content .gh-flow-skip {
|
||||
display: inline-block;
|
||||
margin-top: 5px;
|
||||
@ -343,7 +353,7 @@
|
||||
}
|
||||
|
||||
.gh-flow-content .form-group {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.gh-flow-content .form-group label {
|
||||
@ -358,7 +368,7 @@
|
||||
|
||||
.gh-flow-content input {
|
||||
padding: 10px 10px 10px 30px;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.4em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
@ -348,6 +348,18 @@
|
||||
fill: var(--active-menu-color);
|
||||
}
|
||||
|
||||
.gh-nav-list a svg g {
|
||||
stroke: var(--midgrey);
|
||||
}
|
||||
|
||||
.gh-nav-list a:not(.active):hover svg g {
|
||||
stroke: var(--darkgrey);
|
||||
}
|
||||
|
||||
.gh-nav-list .active svg g {
|
||||
stroke: var(--active-menu-color);
|
||||
}
|
||||
|
||||
.gh-nav-list .gh-secondary-action {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
@ -423,6 +435,11 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.gh-nav-pro .gh-btn-green {
|
||||
margin: 7px 10px 7px 20px !important;
|
||||
width: calc(100% - 40px) !important;
|
||||
}
|
||||
|
||||
/* Mobile Nav Menu (Slides out)
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
@ -752,6 +769,46 @@
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.view-actions input[type="text"] {
|
||||
padding: 8px 8px 9px;
|
||||
height: 35px;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.view-actions .gh-input-search-icon {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 9px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: color-mod(var(--midlightgrey) l(+5%));
|
||||
}
|
||||
|
||||
.gh-actions-cog {
|
||||
margin-right: 10px
|
||||
}
|
||||
|
||||
.gh-actions-cog svg {
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
fill: var(--midgrey);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.gh-actions-cog svg path {
|
||||
stroke: initial;
|
||||
}
|
||||
|
||||
.gh-actions-menu {
|
||||
top: calc(100% + 6px);
|
||||
right: 10px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.gh-actions-menu.fade-out {
|
||||
animation-duration: 0.01s;
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.view-header {
|
||||
padding: 0 7px;
|
||||
|
72
ghost/admin/app/styles/layouts/members.css
Normal file
72
ghost/admin/app/styles/layouts/members.css
Normal file
@ -0,0 +1,72 @@
|
||||
/* Members list
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.view-actions input.gh-members-list-searchfield {
|
||||
min-width: 220px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
p.gh-members-list-email {
|
||||
margin: -2px 0 -1px;
|
||||
}
|
||||
|
||||
.gh-members-list-subscribed-at {
|
||||
margin-right: -8px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.gh-members-placeholder {
|
||||
width: 118px;
|
||||
margin: -30px 0 15px;
|
||||
}
|
||||
|
||||
|
||||
/* Member details
|
||||
/* ----------------------------------------- */
|
||||
|
||||
label[for="member-description"] + p {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
textarea.gh-member-details-textarea {
|
||||
max-width: 100%;
|
||||
height: 123px;
|
||||
}
|
||||
|
||||
.gh-member-info-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.gh-member-stripe-info {
|
||||
border-bottom: 1px solid var(--whitegrey);
|
||||
}
|
||||
|
||||
.gh-member-stripe-info:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.gh-member-stripe-table {
|
||||
width: 100%;
|
||||
margin: 10px 0 12px;
|
||||
}
|
||||
|
||||
.gh-member-stripe-table td {
|
||||
vertical-align: top;
|
||||
font-size: 1.4rem;
|
||||
padding: 6px 12px 6px 0;
|
||||
}
|
||||
|
||||
.gh-member-stripe-label {
|
||||
color: var(--midgrey-d1);
|
||||
white-space: nowrap;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/* Import modal
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.gh-members-import-results {
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
@ -21,8 +21,8 @@
|
||||
}
|
||||
|
||||
.user-actions-menu {
|
||||
top: calc(100% + 17px);
|
||||
right: 0;
|
||||
top: calc(100% + 6px);
|
||||
right: 10px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
|
@ -103,12 +103,12 @@ input {
|
||||
z-index: 2;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: color-mod(var(--midgrey) l(+15%));
|
||||
fill: color-mod(var(--lightgrey) l(-15%) s(-10%));
|
||||
transform: translateY(-7px);
|
||||
}
|
||||
|
||||
.gh-input-icon.gh-icon-link svg path {
|
||||
stroke: color-mod(var(--midgrey) l(+15%));
|
||||
stroke: color-mod(var(--lightgrey) l(-15%) s(-10%));
|
||||
}
|
||||
|
||||
.gh-input-icon input {
|
||||
@ -133,7 +133,6 @@ select {
|
||||
background: var(--input-bg-color);
|
||||
color: var(--darkgrey);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1em;
|
||||
font-weight: 400;
|
||||
user-select: text;
|
||||
border-radius: var(--border-radius);
|
||||
@ -174,13 +173,15 @@ textarea {
|
||||
min-width: 250px;
|
||||
min-height: 10rem;
|
||||
max-width: 500px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.5em;
|
||||
user-select: text;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
textarea.gh-input {
|
||||
line-height: 1.5em;
|
||||
.gh-input[disabled] {
|
||||
background: var(--whitegrey-l2);
|
||||
color: var(--midlightgrey);
|
||||
border-color: var(--whitegrey);
|
||||
}
|
||||
|
||||
|
||||
@ -290,6 +291,64 @@ textarea.gh-input {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gh-radio {
|
||||
display: flex;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
.gh-radio-button {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: color-mod(var(--lightgrey) l(-10%)) 1px solid;
|
||||
border-radius: 100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.gh-radio-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 0 0 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gh-radio-label {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.gh-radio-desc {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.4em;
|
||||
font-weight: 200;
|
||||
color: color-mod(var(--midgrey) l(+10%));
|
||||
}
|
||||
|
||||
.gh-radio-label:hover,
|
||||
.gh-radio-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gh-radio.active .gh-radio-button {
|
||||
border-color: color-mod(var(--blue) l(-12%));
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.gh-radio.active .gh-radio-button:before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: #fff;
|
||||
border-radius: 100%;
|
||||
box-shadow: rgba(0,0,0,0.25) 0 1px 3px;
|
||||
}
|
||||
|
||||
|
||||
/* Switch
|
||||
/* ---------------------------------------------------------- */
|
||||
|
@ -21,6 +21,7 @@
|
||||
.lh-copy { line-height: var(--lh-1-6); }
|
||||
.lh-list { line-height: 3.2rem; }
|
||||
.lh-code { line-height: var(--lh-1-3); }
|
||||
.lh-zero { line-height: 0; }
|
||||
|
||||
@media (--breakpoint-not-small) {
|
||||
.lh-solid-ns { line-height: var(--lh-1-1); }
|
||||
@ -29,6 +30,7 @@
|
||||
.lh-copy-ns { line-height: var(--lh-1-6); }
|
||||
.lh-list-ns { line-height: var(--lh-2-0); }
|
||||
.lh-code-ns { line-height: var(--lh-1-3); }
|
||||
.lh-zero-ns { line-height: 0; }
|
||||
}
|
||||
|
||||
@media (--breakpoint-medium) {
|
||||
@ -38,6 +40,7 @@
|
||||
.lh-copy-m { line-height: var(--lh-1-6); }
|
||||
.lh-list-m { line-height: var(--lh-2-0); }
|
||||
.lh-code-m { line-height: var(--lh-1-3); }
|
||||
.lh-zero-m { line-height: 0; }
|
||||
}
|
||||
|
||||
@media (--breakpoint-large) {
|
||||
@ -47,4 +50,5 @@
|
||||
.lh-copy-l { line-height: var(--lh-1-6); }
|
||||
.lh-list-l { line-height: var(--lh-2-0); }
|
||||
.lh-code-l { line-height: var(--lh-1-3); }
|
||||
.lh-zero-l { line-height: 0; }
|
||||
}
|
||||
|
134
ghost/admin/app/templates/components/gh-member-settings-form.hbs
Normal file
134
ghost/admin/app/templates/components/gh-member-settings-form.hbs
Normal file
@ -0,0 +1,134 @@
|
||||
<h4 class="midlightgrey f-small fw5 ttu">Basic info</h4>
|
||||
<div class="pa5 pb0 pt4 br4 shadow-1 bg-grouped-table mt2 flex flex-column flex-row-ns items-start justify-between gh-tag-basic-settings-form">
|
||||
<div class="flex flex-column items-start mr8 w-100 w-50-ns">
|
||||
{{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="name"}}
|
||||
<label for="member-name">Name</label>
|
||||
{{gh-text-input
|
||||
disabled=true
|
||||
id="member-name"
|
||||
name="name"
|
||||
value=(readonly scratchName)
|
||||
tabindex="1"
|
||||
input=(action (mut scratchName) value="target.value")
|
||||
focus-out=(action 'setProperty' 'name' scratchName)}}
|
||||
{{gh-error-message errors=member.errors property="name"}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
{{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="email"}}
|
||||
<label for="member-email">Email</label>
|
||||
{{gh-text-input
|
||||
disabled=true
|
||||
value=(readonly scratchEmail)
|
||||
id="member-email"
|
||||
name="email"
|
||||
tabindex="2"
|
||||
focus-out=(action 'setProperty' 'email' scratchEmail)
|
||||
input=(action (mut scratchEmail) value="target.value")}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
</div>
|
||||
<div class="mb6 mb0-ns w-100 w-50-ns">
|
||||
{{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="note"}}
|
||||
<label for="member-description">Note</label>
|
||||
{{gh-textarea
|
||||
disabled=true
|
||||
id="member-description"
|
||||
name="description"
|
||||
class="gh-member-details-textarea"
|
||||
tabindex="3"
|
||||
value=(readonly scratchDescription)
|
||||
input=(action (mut scratchDescription) value="target.value")
|
||||
focus-out=(action 'setProperty' 'description' scratchDescription)
|
||||
}}
|
||||
{{gh-error-message errors=member.errors property="description"}}
|
||||
<p>Maximum: <b>500</b> characters. You’ve used {{gh-count-down-characters scratchDescription 500}}</p>
|
||||
{{/gh-form-group}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="midlightgrey f-small fw5 ttu mt12">Stripe info</h4>
|
||||
|
||||
{{#if isLoading}}
|
||||
<div class="pa20 br4 shadow-1 bg-grouped-table mt2">
|
||||
<div class="flex justify-center flex-auto">
|
||||
<div class="gh-loading-spinner"> </div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if subscriptions}}
|
||||
<div class="br4 shadow-1 bg-grouped-table mt2">
|
||||
{{#if hasMultipleSubscriptions}}
|
||||
<div class="pa2 flex flex-column flex-row-ns items-center justify-center f7 fw5 bg-whitegrey-l2 bb b--whitegrey br4 br--top">
|
||||
{{svg-jar "info" class="gh-member-info-icon mr2 fill-darkgrey"}} Member has multiple Stripe subscriptions
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#each subscriptions as |subscription|}}
|
||||
<section class="gh-member-stripe-info pa5 pb0 pt4 flex flex-column flex-row-ns items-start justify-between">
|
||||
<div class="flex items-start w-100">
|
||||
<div class="w-50 flex-auto mr8">
|
||||
<h4 class="f8 fw6">Customer data</h4>
|
||||
<table class="gh-member-stripe-table">
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Stripe customer ID</td>
|
||||
<td class="gh-member-stripe-data"><a href="https://dashboard.stripe.com/test/customers/{{subscription.customer}}" target="_blank" rel="noopener" data-tooltip="View on Stripe">{{subscription.customer}}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Name</td>
|
||||
<td class="gh-member-stripe-data">
|
||||
{{#if subscription.name}}
|
||||
{{subscription.name}}
|
||||
{{else}}
|
||||
<span class="midlightgrey-l2">No name</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Email</td>
|
||||
<td class="gh-member-stripe-data">
|
||||
{{#if subscription.email}}
|
||||
{{subscription.email}}
|
||||
{{else}}
|
||||
<span class="midlightgrey-l2">No email</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Customer since</td>
|
||||
<td class="gh-member-stripe-data">
|
||||
{{#if subscription.startDate}}
|
||||
{{subscription.startDate}}
|
||||
{{else}}
|
||||
<span class="midlightgrey-l2">No data</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="w-50 flex-auto">
|
||||
<h4 class="f8 fw6">Subscription data</h4>
|
||||
<table class="gh-member-stripe-table">
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Plan</td>
|
||||
<td class="gh-member-stripe-data">{{subscription.planName}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Current status</td>
|
||||
<td class="gh-member-stripe-data">{{subscription.status}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="gh-member-stripe-label">Next renewal date</td>
|
||||
<td class="gh-member-stripe-data">{{subscription.validUntil}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="pa20 br4 shadow-1 bg-grouped-table mt2">
|
||||
<p class="ma0 pa0 tc midgrey">Member doesn't have Stripe subscription</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
@ -0,0 +1,25 @@
|
||||
{{#link-to "member" member class="gh-flex-list-data gh-flex-list-auto" title="Member details"}}
|
||||
<div class="flex items-center">
|
||||
<GhMemberAvatar @member={{member}} class="w9 h9 mr3" />
|
||||
<div>
|
||||
<h3 class="ma0 pa0 gh-members-list-name">
|
||||
{{#if this.name}}
|
||||
{{this.name}}
|
||||
{{else}}
|
||||
{{this.email}}
|
||||
{{/if}}
|
||||
</h3>
|
||||
{{#if this.name}}
|
||||
<p class="ma0 pa0 middarkgrey f8 gh-members-list-email">{{this.email}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</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}}
|
||||
|
||||
{{#link-to "member" member class="gh-flex-list-data" title="Member details"}}
|
||||
{{svg-jar "arrow-right" class="w6 h6 fill-midgrey pa1 nr2"}}
|
||||
{{/link-to}}
|
@ -75,6 +75,17 @@
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
|
||||
{{#if feature.members}}
|
||||
{{#unless session.user.isContributor}}
|
||||
<div class="form-group">
|
||||
<label for="visibility-input">Post access</label>
|
||||
{{gh-psm-visibility-input post=post triggerId="visibility-input"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#gh-form-group errors=post.errors hasValidated=post.hasValidated property="customExcerpt"}}
|
||||
<label for="custom-excerpt">Excerpt</label>
|
||||
{{gh-textarea
|
||||
|
@ -0,0 +1,10 @@
|
||||
<span class="gh-select">
|
||||
{{one-way-select selectedVisibility
|
||||
options=availableVisibilities
|
||||
optionValuePath="name"
|
||||
optionLabelPath="label"
|
||||
optionTargetPath="name"
|
||||
update=(action "updateVisibility")
|
||||
}}
|
||||
{{svg-jar "arrow-down-small"}}
|
||||
</span>
|
@ -0,0 +1,47 @@
|
||||
<header class="modal-header" data-test-modal="import-members">
|
||||
<h1>
|
||||
{{#if response}}
|
||||
Import result
|
||||
{{else}}
|
||||
Import members
|
||||
{{/if}}
|
||||
</h1>
|
||||
</header>
|
||||
<a class="close" href="" title="Close" {{action "closeModal"}}>{{svg-jar "close"}}<span class="hidden">Close</span></a>
|
||||
|
||||
<div class="modal-body bg-whitegrey-l2 ba b--whitegrey br3">
|
||||
{{#if response}}
|
||||
<table class="gh-members-import-results">
|
||||
<tr>
|
||||
<td>Imported:</td>
|
||||
<td align="left" data-test-text="import-members-imported">{{response.imported}}</td>
|
||||
</tr>
|
||||
{{#if response.duplicates}}
|
||||
<tr>
|
||||
<td>Duplicates:</td>
|
||||
<td align="left" data-test-text="import-members-duplicates">{{response.duplicates}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
{{#if response.invalid}}
|
||||
<tr>
|
||||
<td class="red">Invalid:</td>
|
||||
<td align="left" data-test-text="import-members-invalid" class="red">{{response.invalid}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</table>
|
||||
{{else}}
|
||||
{{gh-file-uploader
|
||||
url=uploadUrl
|
||||
paramName="membersfile"
|
||||
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-members">
|
||||
<span>{{#if response}}Close{{else}}Cancel{{/if}}</span>
|
||||
</button>
|
||||
</div>
|
@ -1,42 +1,35 @@
|
||||
<section class="gh-canvas">
|
||||
<form class="mb10 member-basic-info-form" {{action (perform "save") on="submit"}}>
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
{{link-to "Members" "members" data-test-link="members"}}
|
||||
{{#link-to "members" data-test-link="members-back"}}Members{{/link-to}}
|
||||
<span>{{svg-jar "arrow-right"}}</span>
|
||||
{{#if member.name}}
|
||||
{{member.name}}
|
||||
{{else}}
|
||||
{{member.email}}
|
||||
{{/if}}
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
|
||||
<section class="view-container">
|
||||
<div class="br3 bg-white mt2 shadow-1">
|
||||
<div class="flex flex-row pa5 bb b--whitegrey">
|
||||
<GhMemberAvatar class="w20 h20 mr4" @initialsClass="f-subheadline" @member={{member}} />
|
||||
<div class="flex flex-column justify-center">
|
||||
<h3 class="ma0 pa0 fw5">{{member.name}}</h3>
|
||||
<span class="db">
|
||||
<a class="midlightgrey" href="mailto:{{member.email}}">
|
||||
{{member.email}}
|
||||
</a>
|
||||
</span>
|
||||
<div class="flex items-center mb10 bt b--lightgrey-d1 pt8">
|
||||
<GhMemberAvatar @member={{member}} @sizeClass={{'f-headline fw4 lh-zero'}} class="w18 h18 mr4" />
|
||||
<div>
|
||||
<h3 class="f2 fw5 ma0 pa0">
|
||||
{{if member.name member.name member.email}}
|
||||
</h3>
|
||||
<p class="f6 pa0 ma0 midgrey">
|
||||
{{#if member.name}}
|
||||
<span class="darkgrey">{{member.email}}</span> –
|
||||
{{/if}}
|
||||
Member since {{this.subscribedAt}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-column flex-grow-1 pa5 br b--whitegrey w-50">
|
||||
<span class="db ttu f8 midlightgrey">Member since</span>
|
||||
<span class="db f5">{{moment-format member.createdAt "MMMM Do"}}</span>
|
||||
<span class="db f8 midlightgrey">({{moment-to-now member.createdAt hideAffix=true}})</span>
|
||||
</div>
|
||||
<div class="flex flex-column flex-grow-1 pa5 w-50">
|
||||
<span class="db ttu f8 midlightgrey">Current plan</span>
|
||||
<span class="db f5 {{if (eq subscription.plan '...') "midlightgrey"}}">{{subscription.plan}}</span>
|
||||
{{#unless (eq subscription.amount '...')}}
|
||||
<span class="db f8 midlightgrey">{{subscription.amount}} USD/month</span>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="pb1 bb b--whitegrey-d1 f-small fw5 midlightgrey ttu mt10 pb2">Danger zone</h2>
|
||||
{{gh-member-settings-form member=member
|
||||
setProperty=(action "setProperty")
|
||||
isLoading=this.isLoading
|
||||
showDeleteTagModal=(action "toggleDeleteTagModal")}}
|
||||
</form>
|
||||
<button
|
||||
type="button"
|
||||
class="gh-btn gh-btn-red gh-btn-icon mt3"
|
||||
@ -45,7 +38,6 @@
|
||||
>
|
||||
<span>Delete member</span>
|
||||
</button>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{{#if showDeleteMemberModal}}
|
||||
|
@ -1,15 +1,38 @@
|
||||
<section class="gh-canvas">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>Members</h2>
|
||||
<div>
|
||||
<GhTextInput placeholder="Search members..." @value={{this.searchText}}
|
||||
@input={{action (mut this.searchText) value="target.value"}} />
|
||||
<section class="view-actions">
|
||||
<span class="dropdown">
|
||||
{{#gh-dropdown-button dropdownName="members-actions-menu" classNames="gh-btn gh-btn-white gh-btn-icon only-has-icon gh-actions-cog" title="Members Actions" data-test-user-actions=true}}
|
||||
<span>
|
||||
{{svg-jar "settings"}}
|
||||
<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"}}
|
||||
<li>
|
||||
{{#link-to "members.import" class="mr2" data-test-link="import-csv"}}
|
||||
<span>Import CSV </span>
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" {{action 'exportData'}} class="mr2">
|
||||
<span>Export CSV </span>
|
||||
</a>
|
||||
</li>
|
||||
{{/gh-dropdown}}
|
||||
</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" />
|
||||
</div>
|
||||
</section>
|
||||
</GhCanvasHeader>
|
||||
<section class="view-container">
|
||||
|
||||
<section class="view-container h-100">
|
||||
{{#if (or filteredMembers this.fetchMembers.isRunning this.searchText)}}
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="f-small fw5 midlightgrey ttu">
|
||||
<h2 class="f-small fw5 midlightgrey ttu mb2">
|
||||
{{#if this.searchText}}
|
||||
Search result
|
||||
{{else}}
|
||||
@ -22,41 +45,44 @@
|
||||
{{/if}}
|
||||
</h2>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.filteredMembers}}
|
||||
{{!-- members list, styles taken from .apps-grid --}}
|
||||
<div class="flex flex-row flex-wrap items-start br3 mt2 mb10 bg-grouped-table shadow-1">
|
||||
<VerticalCollection
|
||||
@items={{this.filteredMembers}}
|
||||
@key="id"
|
||||
@containerSelector=".gh-main"
|
||||
@estimateHeight=76
|
||||
as |member index|
|
||||
>
|
||||
<div class="flex-grow-1 flex-shrink-1" style="flex-basis: 100%">
|
||||
{{#link-to "member" member classNames="link whitegrey"}}
|
||||
<article class="flex items-center justify-between pa4 pt3 pb3 {{if index "bt"}} highlight-whitegrey">
|
||||
<div class="flex items-center">
|
||||
<GhMemberAvatar @member={{member}} class="w9 h9 mr4" />
|
||||
<div class="flex flex-column">
|
||||
<h3 class="darkgrey ma0 f6 fw6">{{member.name}}</h3>
|
||||
<p class="midgrey ma0 pa0 f7">{{member.email}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">{{svg-jar "arrow-right" class="w4 h4 fill-midlightgrey"}}</div>
|
||||
</article>
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</VerticalCollection>
|
||||
</div>
|
||||
<section class="content-list">
|
||||
<ol class="members-list gh-list {{unless filteredMembers "no-posts"}}">
|
||||
{{#if filteredMembers}}
|
||||
{{#vertical-collection
|
||||
items=filteredMembers
|
||||
key="id"
|
||||
containerSelector=".gh-main"
|
||||
estimateHeight=60
|
||||
bufferSize=20
|
||||
as |member|
|
||||
}}
|
||||
{{gh-members-list-item
|
||||
member=member
|
||||
data-test-member-id=member.id
|
||||
}}
|
||||
{{/vertical-collection}}
|
||||
{{else}}
|
||||
{{#if this.fetchMembers.isRunning}}
|
||||
<div class="gh-content">
|
||||
<GhLoadingSpinner />
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="mt2 pt10 bt b--whitegrey tc midlightgrey">No members found.</p>
|
||||
<li class="no-posts-box">
|
||||
<div class="no-posts">
|
||||
{{svg-jar "members-placeholder" class="gh-members-placeholder"}}
|
||||
{{#if this.searchText}}
|
||||
<h3>No members found!</h3>
|
||||
{{else}}
|
||||
<h3>You don't have any members</h3>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ol>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
{{outlet}}
|
4
ghost/admin/app/templates/members/import.hbs
Normal file
4
ghost/admin/app/templates/members/import.hbs
Normal file
@ -0,0 +1,4 @@
|
||||
{{gh-fullscreen-modal "import-members"
|
||||
close=(action "close")
|
||||
confirm=(action "fetchNewMembers")
|
||||
modifier="action wide"}}
|
@ -87,6 +87,126 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gh-setting-header">Members</div>
|
||||
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
|
||||
{{#if config.enableDeveloperExperiments}}
|
||||
<div class="gh-setting-first gh-setting-last">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Members</div>
|
||||
<div class="gh-setting-desc">Enable free or paid member registration.</div>
|
||||
|
||||
{{#liquid-if feature.labs.members class="nr25"}}
|
||||
|
||||
<div class="flex ba br4 b--whitegrey pa5 pt4 mt5">
|
||||
<section class="flex flex-column">
|
||||
<label class="fw6 f8">Stripe publishable API key</label>
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.stripeConfig.public_token)
|
||||
input=(action "setSubscriptionSettings" "public_token")
|
||||
class="mt1"
|
||||
}}
|
||||
|
||||
<label class="fw6 f8 mt4">Stripe secret API key</label>
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.stripeConfig.secret_token)
|
||||
input=(action "setSubscriptionSettings" "secret_token")
|
||||
class="mt1"
|
||||
}}
|
||||
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank" class="mt1 fw4 f8">Where to find Stripe
|
||||
API keys</a>
|
||||
|
||||
<div class="mt5 flex nb5">
|
||||
<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">
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.stripeConfig.plans.monthly.dollarAmount)
|
||||
type="number"
|
||||
input=(action "setSubscriptionSettings" "month")
|
||||
}}
|
||||
</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">
|
||||
{{gh-text-input
|
||||
value=(readonly subscriptionSettings.stripeConfig.plans.yearly.dollarAmount)
|
||||
type="number"
|
||||
input=(action "setSubscriptionSettings" "year")
|
||||
}}
|
||||
</div>
|
||||
{{/gh-form-group}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt5 flex nb5">
|
||||
{{#gh-form-group class="require-payment-container"}}
|
||||
<div class="form-group for-checkbox">
|
||||
<label class="checkbox" for="members-require-payment" {{action "setSubscriptionSettings" "requirePaymentForSignup" bubbles="false"}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={{subscriptionSettings.requirePaymentForSignup}}
|
||||
class="gh-input"
|
||||
onclick={{action "setSubscriptionSettings" "requirePaymentForSignup"}}
|
||||
data-test-checkbox="members-require-payment"
|
||||
>
|
||||
<span class="input-toggle-component"></span>
|
||||
<p> Require payment before signing up members</p>
|
||||
</label>
|
||||
</div>
|
||||
{{/gh-form-group}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gh-visibility-menu-content w-50 ml10">
|
||||
<label class="dib f8 fw6 mb4">Default post access</label>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mb2 mt5">
|
||||
{{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 class="gh-setting-action">
|
||||
<div class="for-switch">{{gh-feature-flag "members"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</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">
|
||||
@ -208,6 +328,17 @@
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (not-eq feature.labs.publicAPI undefined)}}
|
||||
<div class="gh-setting">
|
||||
<div class="gh-setting-content">
|
||||
<div class="gh-setting-title">Public API (deprecated)</div>
|
||||
<div class="gh-setting-desc">⚠️ Please use the Content API instead, more info in <a href="https://ghost.org/docs/api/content/">the docs</a></div>
|
||||
</div>
|
||||
<div class="gh-setting-action">
|
||||
<div class="for-switch">{{gh-feature-flag "publicAPI"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="gh-setting">
|
||||
{{#gh-uploader
|
||||
extensions=jsonExtension
|
||||
|
@ -6,7 +6,7 @@ export default Transform.extend({
|
||||
deserialize(serialized) {
|
||||
let subscriptions, subscriptionArray;
|
||||
|
||||
subscriptionArray = serialized || [];
|
||||
subscriptionArray = serialized.subscriptions || [];
|
||||
|
||||
subscriptions = subscriptionArray.map(itemDetails => MemberSubscription.create(itemDetails));
|
||||
|
||||
@ -30,6 +30,8 @@ export default Transform.extend({
|
||||
subscriptionArray = [];
|
||||
}
|
||||
|
||||
return subscriptionArray;
|
||||
return {
|
||||
subscriptions: subscriptionArray
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -80,7 +80,7 @@
|
||||
{{#if isSelected}}
|
||||
<button
|
||||
title="Toggle between editing alt text and caption"
|
||||
class="absolute right-0 bottom-0 ma2 pl1 pr1 ba br3 f7 sans-serif fw4 lh-title tracked-2 {{if this.isEditingAlt "bg-blue b--blue white" "b--midlightgrey midlightgrey"}}"
|
||||
class="absolute right-0 bottom-0 ma2 pl1 pr1 ba br3 f8 sans-serif fw4 lh-title tracked-2 bg-white {{if this.isEditingAlt "bg-blue b--blue white" "b--midlightgrey midlightgrey"}}"
|
||||
{{on "click" this.toggleAltEditing passive=true}}
|
||||
>
|
||||
Alt
|
||||
|
@ -13,6 +13,7 @@ export default Factory.extend({
|
||||
featureImage(i) { return `/content/images/2015/10/post-${i}.jpg`; },
|
||||
html(i) { return `<p>HTML for post ${i}.</p>`; },
|
||||
locale: null,
|
||||
visibility: 'public',
|
||||
metaDescription(i) { return `Meta description for post ${i}.`; },
|
||||
metaTitle(i) { return `Meta Title for post ${i}`; },
|
||||
ogDescription: null,
|
||||
|
@ -98,7 +98,7 @@ export default [
|
||||
created_at: '2015-09-11T09:44:30.810Z',
|
||||
created_by: 1,
|
||||
key: 'is_private',
|
||||
type: 'blog',
|
||||
type: 'private',
|
||||
updated_at: '2015-09-23T13:32:49.868Z',
|
||||
updated_by: 1,
|
||||
value: false
|
||||
@ -108,7 +108,7 @@ export default [
|
||||
created_at: '2015-09-11T09:44:30.810Z',
|
||||
created_by: 1,
|
||||
key: 'password',
|
||||
type: 'blog',
|
||||
type: 'private',
|
||||
updated_at: '2015-09-23T13:32:49.868Z',
|
||||
updated_by: 1,
|
||||
value: ''
|
||||
@ -121,7 +121,7 @@ export default [
|
||||
type: 'blog',
|
||||
updated_at: '2016-05-05T18:33:09.168Z',
|
||||
updated_by: 1,
|
||||
value: '[{"url":""}]'
|
||||
value: '[{"url":"", "username":"Ghost"}]'
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
@ -172,5 +172,15 @@ export default [
|
||||
created_by: 1,
|
||||
updated_at: '2015-10-27T17:39:58.276Z',
|
||||
updated_by: 1
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
type: 'members',
|
||||
key: 'members_subscription_settings',
|
||||
value: '{"isPaid":false,"paymentProcessors":[{"adapter":"stripe","config":{"secret_token":"","public_token":"","product":{"name":"Ghost Subscription"},"plans":[{"name":"Monthly","currency":"usd","interval":"month","amount":""},{"name":"Yearly","currency":"usd","interval":"year","amount":""}]}}]}',
|
||||
created_at: '2019-10-09T09:49:00.000Z',
|
||||
created_by: 1,
|
||||
updated_at: '2019-10-09T09:49:00.000Z',
|
||||
updated_by: 1
|
||||
}
|
||||
];
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghost-admin",
|
||||
"version": "2.31.0",
|
||||
"version": "2.33.0",
|
||||
"description": "Ember.js admin client for Ghost",
|
||||
"author": "Ghost Foundation",
|
||||
"homepage": "http://ghost.org",
|
||||
@ -25,17 +25,17 @@
|
||||
"node": "^8.10.0 || ^10.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ember/jquery": "0.6.1",
|
||||
"@ember/optional-features": "0.7.0",
|
||||
"@ember/render-modifiers": "1.0.1",
|
||||
"@ember/jquery": "1.1.0",
|
||||
"@ember/optional-features": "1.0.0",
|
||||
"@ember/render-modifiers": "1.0.2",
|
||||
"@html-next/vertical-collection": "1.0.0",
|
||||
"@tryghost/helpers": "1.1.9",
|
||||
"@tryghost/helpers": "1.1.12",
|
||||
"@tryghost/kg-clean-basic-html": "0.1.3",
|
||||
"@tryghost/kg-parser-plugins": "0.8.0",
|
||||
"@tryghost/mobiledoc-kit": "0.11.2-ghost.4",
|
||||
"@tryghost/string": "0.1.5",
|
||||
"@tryghost/timezone-data": "0.2.8",
|
||||
"autoprefixer": "9.6.1",
|
||||
"@tryghost/timezone-data": "0.2.11",
|
||||
"autoprefixer": "9.6.4",
|
||||
"blueimp-md5": "2.12.0",
|
||||
"broccoli-asset-rev": "3.0.0",
|
||||
"broccoli-concat": "3.7.4",
|
||||
@ -52,32 +52,33 @@
|
||||
"ember-auto-import": "1.5.2",
|
||||
"ember-cli": "3.12.0",
|
||||
"ember-cli-app-version": "3.2.0",
|
||||
"ember-cli-babel": "7.11.0",
|
||||
"ember-cli-babel": "7.12.0",
|
||||
"ember-cli-chai": "0.5.0",
|
||||
"ember-cli-chart": "3.5.0",
|
||||
"ember-cli-dependency-checker": "3.2.0",
|
||||
"ember-cli-deprecation-workflow": "1.0.1",
|
||||
"ember-cli-eslint": "5.1.0",
|
||||
"ember-cli-htmlbars": "3.1.0",
|
||||
"ember-cli-htmlbars-inline-precompile": "3.0.0",
|
||||
"ember-cli-htmlbars": "4.0.5",
|
||||
"ember-cli-htmlbars-inline-precompile": "3.0.1",
|
||||
"ember-cli-inject-live-reload": "2.0.1",
|
||||
"ember-cli-mirage": "1.1.1",
|
||||
"ember-cli-mirage": "1.1.3",
|
||||
"ember-cli-moment-shim": "3.7.1",
|
||||
"ember-cli-node-assets": "0.2.2",
|
||||
"ember-cli-postcss": "5.0.0",
|
||||
"ember-cli-pretender": "3.1.1",
|
||||
"ember-cli-shims": "1.2.0",
|
||||
"ember-cli-string-helpers": "4.0.4",
|
||||
"ember-cli-string-helpers": "4.0.5",
|
||||
"ember-cli-test-loader": "2.2.0",
|
||||
"ember-cli-uglify": "3.0.0",
|
||||
"ember-composable-helpers": "2.3.1",
|
||||
"ember-concurrency": "1.0.0",
|
||||
"ember-data": "3.12.2",
|
||||
"ember-concurrency": "1.1.0",
|
||||
"ember-data": "3.12.4",
|
||||
"ember-drag-drop": "0.4.8",
|
||||
"ember-exam": "4.0.1",
|
||||
"ember-exam": "4.0.4",
|
||||
"ember-export-application-global": "2.0.0",
|
||||
"ember-fetch": "6.7.1",
|
||||
"ember-in-viewport": "3.5.8",
|
||||
"ember-infinity": "1.4.8",
|
||||
"ember-infinity": "1.4.9",
|
||||
"ember-load": "0.0.17",
|
||||
"ember-load-initializers": "2.1.0",
|
||||
"ember-mocha": "0.16.0",
|
||||
@ -86,18 +87,18 @@
|
||||
"ember-power-calendar-moment": "0.1.7",
|
||||
"ember-power-datepicker": "0.6.2",
|
||||
"ember-power-select": "2.3.5",
|
||||
"ember-resolver": "5.2.1",
|
||||
"ember-resolver": "5.3.0",
|
||||
"ember-route-action-helper": "2.0.7",
|
||||
"ember-simple-auth": "1.9.2",
|
||||
"ember-sinon": "4.1.1",
|
||||
"ember-source": "3.12.0",
|
||||
"ember-sticky-element": "0.2.3",
|
||||
"ember-svg-jar": "2.2.1",
|
||||
"ember-svg-jar": "2.2.3",
|
||||
"ember-test-selectors": "2.1.0",
|
||||
"ember-truth-helpers": "2.1.0",
|
||||
"ember-useragent": "0.9.1",
|
||||
"emberx-file-input": "1.2.1",
|
||||
"eslint": "6.2.2",
|
||||
"eslint": "6.5.1",
|
||||
"eslint-plugin-ghost": "0.5.0",
|
||||
"faker": "4.1.0",
|
||||
"fs-extra": "8.1.0",
|
||||
@ -122,7 +123,7 @@
|
||||
"postcss-easy-import": "3.0.0",
|
||||
"reframe.js": "2.2.5",
|
||||
"simplemde": "https://github.com/kevinansfield/simplemde-markdown-editor.git#ghost",
|
||||
"testem": "2.17.0",
|
||||
"testem": "3.0.0",
|
||||
"top-gh-contribs": "2.0.4",
|
||||
"validator": "7.2.0",
|
||||
"walk-sync": "2.0.2"
|
||||
|
1
ghost/admin/public/assets/icons/info.svg
Normal file
1
ghost/admin/public/assets/icons/info.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>information-circle</title><path d="M12 23.501c-6.341 0-11.5-5.159-11.5-11.5S5.659.501 12 .501s11.5 5.159 11.5 11.5-5.159 11.5-11.5 11.5zm0-22c-5.79 0-10.5 4.71-10.5 10.5s4.71 10.5 10.5 10.5 10.5-4.71 10.5-10.5-4.71-10.5-10.5-10.5z"/><path d="M13 17.505c-.827 0-1.5-.673-1.5-1.5v-6.5H10c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h1.5c.551 0 1 .449 1 1v6.5c0 .276.224.5.5.5h1.5c.276 0 .5.224.5.5s-.224.5-.5.5H13zM11.745 7.5c-.414 0-.75-.336-.75-.75s.336-.75.75-.75.75.336.75.75-.336.75-.75.75z"/></svg>
|
After Width: | Height: | Size: 559 B |
1
ghost/admin/public/assets/icons/members-placeholder.svg
Normal file
1
ghost/admin/public/assets/icons/members-placeholder.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="138" height="138" viewBox="0 0 138 138" xmlns="http://www.w3.org/2000/svg"><title>Combined Shape</title><g fill="none" fill-rule="evenodd"><path d="M69 .5c37.832 0 68.5 30.668 68.5 68.5s-30.668 68.5-68.5 68.5S.5 106.832.5 69 31.168.5 69 .5zM24.82 98.183h50.347C75.025 88.69 70.84 81.579 62.58 76.79c-8.404-4.871-16.772-4.871-25.175 0-8.26 4.788-12.445 11.899-12.586 21.392zm50.347 0h.503l-.5.5c0-.168 0-.335-.003-.5zm-.663-21.045c5.182 5.666 8.348 12.685 9.498 21.044h28.848c-.14-9.492-4.325-16.603-12.585-21.391-8.404-4.871-16.772-4.871-25.176 0-.198.115-.393.23-.585.347zM49.994 37.58c-4.173 0-7.72 1.475-10.67 4.437s-4.42 6.525-4.42 10.714c0 4.19 1.47 7.752 4.42 10.714 2.95 2.962 6.497 4.437 10.67 4.437 4.171 0 7.718-1.475 10.669-4.437 2.95-2.962 4.42-6.524 4.42-10.714s-1.47-7.752-4.42-10.714c-2.95-2.962-6.498-4.437-10.67-4.437zm37.683 0c-4.172 0-7.719 1.475-10.67 4.437-2.95 2.962-4.42 6.525-4.42 10.714 0 4.19 1.47 7.752 4.42 10.714 2.951 2.962 6.498 4.437 10.67 4.437s7.72-1.475 10.67-4.437 4.42-6.524 4.42-10.714-1.47-7.752-4.42-10.714c-2.95-2.962-6.498-4.437-10.67-4.437z" stroke="#9BAEB8" fill-opacity=".1" fill="#9BAEB8" stroke-linecap="square"/><path stroke-opacity=".012" stroke="#000" stroke-width="0" d="M25 25h88v88H25z"/><path d="M62.832 76.358C71.39 81.32 75.67 88.761 75.67 98.683H24.316c0-9.922 4.28-17.364 12.839-22.325 8.559-4.96 17.118-4.96 25.677 0zm37.684 0c8.559 4.961 12.838 12.403 12.838 22.325l-29.79-.001c-1.114-8.672-4.394-15.888-9.84-21.648.363-.23.734-.455 1.115-.676 8.559-4.96 17.118-4.96 25.677 0zM49.993 37.08c4.305 0 7.98 1.528 11.024 4.584 3.044 3.056 4.566 6.745 4.566 11.067s-1.522 8.01-4.566 11.067c-3.044 3.056-6.719 4.584-11.024 4.584-4.305 0-7.98-1.528-11.023-4.584-3.044-3.056-4.566-6.745-4.566-11.067s1.522-8.01 4.566-11.067c3.044-3.056 6.718-4.584 11.023-4.584zm37.684 0c4.305 0 7.98 1.528 11.024 4.584 3.044 3.056 4.566 6.745 4.566 11.067s-1.522 8.01-4.566 11.067c-3.044 3.056-6.719 4.584-11.024 4.584-4.305 0-7.98-1.528-11.023-4.584-3.045-3.056-4.567-6.745-4.567-11.067s1.522-8.01 4.567-11.067c3.044-3.056 6.718-4.584 11.023-4.584z" stroke="#9BAEB8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g></svg>
|
After Width: | Height: | Size: 2.1 KiB |
@ -63,7 +63,7 @@ describe('Acceptance: Members', function () {
|
||||
await click('[data-test-nav="members"]');
|
||||
|
||||
expect(currentURL()).to.equal('/members');
|
||||
expect(currentRouteName()).to.equal('members');
|
||||
expect(currentRouteName()).to.equal('members.index');
|
||||
expect(find('[data-test-screen-title]')).to.have.text('Members');
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,43 @@
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import {blur, fillIn, find, findAll, render} from '@ember/test-helpers';
|
||||
import {describe, it} from 'mocha';
|
||||
import {expect} from 'chai';
|
||||
import {setupRenderingTest} from 'ember-mocha';
|
||||
|
||||
describe('Integration: Component: gh-psm-visibility-input', function () {
|
||||
setupRenderingTest();
|
||||
|
||||
it('renders', async function () {
|
||||
this.set('post', {
|
||||
visibility: 'members'
|
||||
});
|
||||
|
||||
await render(hbs`{{gh-psm-visibility-input post=post}}`);
|
||||
|
||||
expect(this.element, 'top-level elements').to.exist;
|
||||
expect(findAll('option'), 'number of options').to.have.length(3);
|
||||
expect(find('select').value, 'selected option value').to.equal('members');
|
||||
});
|
||||
|
||||
it('updates post visibility on change', async function () {
|
||||
let setVisibility = sinon.spy();
|
||||
|
||||
this.set('post', {
|
||||
visibility: 'public',
|
||||
set: setVisibility
|
||||
});
|
||||
|
||||
await render(hbs`{{gh-psm-visibility-input post=post}}`);
|
||||
|
||||
expect(this.element, 'top-level elements').to.exist;
|
||||
expect(findAll('option'), 'number of options').to.have.length(3);
|
||||
expect(find('select').value, 'selected option value').to.equal('public');
|
||||
|
||||
await fillIn('select', 'paid');
|
||||
await blur('select');
|
||||
|
||||
expect(setVisibility.calledOnce).to.be.true;
|
||||
expect(setVisibility.calledWith('visibility', 'paid')).to.be.true;
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user