Merge branch 'master' into v3

This commit is contained in:
Kevin Ansfield 2019-10-11 10:43:38 +01:00
commit df4bdf1cf2
14 changed files with 321 additions and 167 deletions

View File

@ -10,7 +10,6 @@ export default Component.extend({
mediaQueries: service(),
isViewingSubview: false,
scratchDescription: '',
// Allowed actions
setProperty: () => {},
@ -18,6 +17,7 @@ export default Component.extend({
scratchName: boundOneWay('member.name'),
scratchEmail: boundOneWay('member.email'),
scratchNote: boundOneWay('member.note'),
subscriptions: computed('member.stripe', function () {
let subscriptions = this.member.get('stripe');
if (subscriptions && subscriptions.length > 0) {

View File

@ -21,8 +21,9 @@ export default Component.extend({
yearly: yearlyPlan
};
subscriptionSettings.stripeConfig = stripeProcessor.config;
subscriptionSettings.requirePaymentForSetup = !!subscriptionSettings.requirePaymentForSetup;
subscriptionSettings.allowSelfSignup = !!subscriptionSettings.allowSelfSignup;
subscriptionSettings.fromAddress = subscriptionSettings.fromAddress || 'noreply';
return subscriptionSettings;
}),
@ -58,8 +59,8 @@ export default Component.extend({
return plan;
});
}
if (key === 'requirePaymentForSignup') {
subscriptionSettings.requirePaymentForSignup = !subscriptionSettings.requirePaymentForSignup;
if (key === 'allowSelfSignup') {
subscriptionSettings.allowSelfSignup = !subscriptionSettings.allowSelfSignup;
}
if (key === 'fromAddress') {
subscriptionSettings.fromAddress = event.target.value;
@ -74,7 +75,7 @@ export default Component.extend({
} catch (e) {
return {
isPaid: false,
requirePaymentForSignup: false,
allowSelfSignup: true,
fromAddress: 'noreply',
paymentProcessors: [{
adapter: 'stripe',

View File

@ -12,8 +12,9 @@ export default Controller.extend({
router: service(),
member: alias('model'),
notifications: service(),
member: alias('model'),
subscribedAt: computed('member.createdAt', function () {
let memberSince = moment(this.member.createdAt).from(moment());
let createdDate = moment(this.member.createdAt).format('MMM DD, YYYY');
@ -21,10 +22,10 @@ export default Controller.extend({
}),
actions: {
setProperty() {
return;
setProperty(propKey, value) {
this._saveMemberProperty(propKey, value);
},
toggleDeleteTagModal() {
toggleDeleteMemberModal() {
this.toggleProperty('showDeleteMemberModal');
},
finaliseDeletion() {
@ -34,7 +35,61 @@ export default Controller.extend({
this.members.decrementProperty('meta.pagination.total');
}
this.router.transitionTo('members');
},
toggleUnsavedChangesModal(transition) {
let leaveTransition = this.leaveScreenTransition;
if (!transition && this.showUnsavedChangesModal) {
this.set('leaveScreenTransition', null);
this.set('showUnsavedChangesModal', false);
return;
}
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
this.set('leaveScreenTransition', transition);
// if a save is running, wait for it to finish then transition
if (this.save.isRunning) {
return this.save.last.then(() => {
transition.retry();
});
}
// we genuinely have unsaved data, show the modal
this.set('showUnsavedChangesModal', true);
}
},
leaveScreen() {
let transition = this.leaveScreenTransition;
if (!transition) {
this.notifications.showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
return;
}
// roll back changes on model props
this.member.rollbackAttributes();
return transition.retry();
}
},
save: task(function* () {
let member = this.member;
try {
return yield member.save();
} catch (error) {
if (error) {
this.notifications.showAPIError(error, {key: 'member.save'});
}
}
}).drop(),
_saveMemberProperty(propKey, newValue) {
let member = this.member;
member.set(propKey, newValue);
},
fetchMember: task(function* (memberId) {

View File

@ -8,7 +8,6 @@ import {
isRequestEntityTooLargeError,
isUnsupportedMediaTypeError
} from 'ghost-admin/services/ajax';
import {computed} from '@ember/object';
import {isBlank} from '@ember/utils';
import {isArray as isEmberArray} from '@ember/array';
import {run} from '@ember/runloop';
@ -63,25 +62,6 @@ export default Controller.extend({
this.yamlMimeType = YAML_MIME_TYPE;
},
subscriptionSettings: computed('settings.membersSubscriptionSettings', function () {
let subscriptionSettings = this.parseSubscriptionSettings(this.get('settings.membersSubscriptionSettings'));
let stripeProcessor = subscriptionSettings.paymentProcessors.find((proc) => {
return (proc.adapter === 'stripe');
});
let monthlyPlan = stripeProcessor.config.plans.find(plan => plan.interval === 'month');
let yearlyPlan = stripeProcessor.config.plans.find(plan => plan.interval === 'year');
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;
subscriptionSettings.fromAddress = subscriptionSettings.fromAddress || 'noreply';
return subscriptionSettings;
}),
actions: {
onUpload(file) {
let formData = new FormData();

View File

@ -4,6 +4,7 @@ import attr from 'ember-data/attr';
export default DS.Model.extend({
name: attr('string'),
email: attr('string'),
note: attr('string'),
createdAt: attr('moment-utc'),
stripe: attr('member-subscription')
});

View File

@ -1,6 +1,24 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
import {inject as service} from '@ember/service';
export default AuthenticatedRoute.extend(CurrentUserSettings, {
router: service(),
init() {
this._super(...arguments);
this.router.on('routeWillChange', (transition) => {
this.showUnsavedChangesModal(transition);
});
},
beforeModel() {
this._super(...arguments);
return this.get('session.user')
.then(this.transitionAuthor());
},
export default AuthenticatedRoute.extend({
model(params) {
this._isMemberUpdated = true;
return this.store.findRecord('member', params.member_id, {
@ -19,10 +37,31 @@ export default AuthenticatedRoute.extend({
this._super(...arguments);
// clear the properties
let {controller} = this;
controller.model.rollbackAttributes();
this.set('controller.model', null);
this._isMemberUpdated = false;
},
actions: {
save() {
this.controller.send('save');
}
},
titleToken() {
return this.controller.get('member.name');
},
showUnsavedChangesModal(transition) {
if (transition.from && transition.from.name.match(/^member$/) && transition.targetName) {
let {controller} = this;
if (!controller.member.isDeleted && controller.member.hasDirtyAttributes) {
transition.abort();
controller.send('toggleUnsavedChangesModal', transition);
return;
}
}
}
});

View File

@ -0,0 +1,15 @@
/* eslint-disable camelcase */
import ApplicationSerializer from 'ghost-admin/serializers/application';
export default ApplicationSerializer.extend({
serialize(/*snapshot, options*/) {
let json = this._super(...arguments);
// Properties that exist on the model but we don't want sent in the payload
delete json.stripe;
// Normalize properties
json.name = json.name || '';
json.note = json.note || '';
return json;
}
});

View File

@ -4,7 +4,6 @@
{{#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)
@ -29,19 +28,18 @@
</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>
<label for="member-note">Note</label>
{{gh-textarea
disabled=true
id="member-description"
name="description"
id="member-note"
name="note"
class="gh-member-details-textarea"
tabindex="3"
value=(readonly scratchDescription)
input=(action (mut scratchDescription) value="target.value")
focus-out=(action 'setProperty' 'description' scratchDescription)
value=(readonly scratchNote)
input=(action (mut scratchNote) value="target.value")
focus-out=(action 'setProperty' 'note' scratchNote)
}}
{{gh-error-message errors=member.errors property="description"}}
<p>Maximum: <b>500</b> characters. Youve used {{gh-count-down-characters scratchDescription 500}}</p>
{{gh-error-message errors=member.errors property="note"}}
<p>Maximum: <b>500</b> characters. Youve used {{gh-count-down-characters scratchNote 500}}</p>
{{/gh-form-group}}
</div>
</div>

View File

@ -1,7 +1,19 @@
<div class="flex flex-column b--whitegrey bt">
<section class="flex flex-column bb b--whitegrey pa5">
<div class="w-50 mb4">
<label class="fw6 f6">Stripe publishable API key</label>
<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>
</div>
<div>
<button type="button" class="gh-btn" {{action (toggle "membersStripeOpen" this)}} data-test-toggle-membersstripe><span>{{if membersStripeOpen "Close" "Expand"}}</span></button>
</div>
</div>
{{#liquid-if membersStripeOpen}}
<div class="w-50 mb4 mt5">
<label class="fw6 f8">Stripe publishable API key</label>
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.public_token)
input=(action "setSubscriptionSettings" "public_token")
@ -9,7 +21,7 @@
}}
</div>
<div class="w-50 mb4">
<label class="fw6 f6 mt4">Stripe secret API key</label>
<label class="fw6 f8 mt4">Stripe secret API key</label>
{{gh-text-input
value=(readonly subscriptionSettings.stripeConfig.secret_token)
input=(action "setSubscriptionSettings" "secret_token")
@ -19,11 +31,25 @@
Where to find Stripe API keys
</a>
</div>
{{/liquid-if}}
</section>
<div class="w-50 flex nb5">
<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>
</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 mr3">
{{#gh-form-group}}
<label class="fw6 f6">Monthly price</label>
<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)
@ -35,7 +61,7 @@
</div>
<div class="w-50 ml2">
{{#gh-form-group class="description-container"}}
<label class="fw6 f6">Yearly price</label>
<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)
@ -46,11 +72,42 @@
{{/gh-form-group}}
</div>
</div>
{{/liquid-if}}
</section>
<section class="flex flex-column bb b--whitegrey pa5">
<label class="dib f6 fw6 mb4">Default post access</label>
<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>
</div>
<div>
<div class="for-switch">
<label class="switch" for="members-allow-self-signup"
{{action "setSubscriptionSettings" "allowSelfSignup" bubbles="false"}}>
<input type="checkbox" checked={{subscriptionSettings.allowSelfSignup}} class="gh-input"
onclick={{action "setSubscriptionSettings" "allowSelfSignup"}}
data-test-checkbox="members-allow-self-signup">
<span class="input-toggle-component mt1"></span>
</label>
</div>
</div>
</div>
</section>
<section class="bb b--whitegrey pa5">
<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>
</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="gh-radio {{if (eq settings.defaultContentVisibility "public") "active"}}"
{{action "setDefaultContentVisibility" "public" on="click"}}>
<div class="gh-radio-button" data-test-publishmenu-unpublished-option></div>
@ -74,33 +131,36 @@
<div class="gh-radio-label">Paid-members only</div>
</div>
</div>
</div>
{{/liquid-if}}
</section>
<div class="flex flex-column bb b--whitegrey pa5">
<div class="for-checkbox">
<label class="checkbox flex items-center" for="members-require-payment"
{{action "setSubscriptionSettings" "requirePaymentForSignup" bubbles="false"}}>
<span class="f6 fw6"> Require payment for signups</span>
<input type="checkbox" checked={{subscriptionSettings.requirePaymentForSignup}} class="gh-input"
onclick={{action "setSubscriptionSettings" "requirePaymentForSignup"}}
data-test-checkbox="members-require-payment">
<span class="input-toggle-component mt1"></span>
</label>
<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>
</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>
<div class="flex flex-column pl5 pr5 pt5">
<div class="w-50 mr3">
{{#liquid-if membersEmailOpen}}
<div class="flex flex-column w-40 flex mb2 mt5">
{{#gh-form-group}}
<label class="dib f6 fw6 mb4">Email</label>
<div class="flex items-center justify-center">
<label class="fw6 f8">Sender email address</label>
<div class="flex items-center justify-center mt1">
{{gh-text-input
value=(readonly subscriptionSettings.fromAddress)
input=(action "setSubscriptionSettings" "fromAddress")
class="w20"
}}
<span class="ml3"> @{{config.blogDomain}}</span>
</div>
<div class="f6 fw4"> "From" address for sending sign up and sign in emails</div>
<div class="f8 fw4 midgrey mt1"> "From" address for sign up and sign in emails</div>
{{/gh-form-group}}
</div>
</div>
{{/liquid-if}}
</section>
</div>

View File

@ -10,16 +10,19 @@
{{member.email}}
{{/if}}
</h2>
<section class="view-actions">
{{gh-task-button task=save class="gh-btn gh-btn-blue gh-btn-icon" data-test-button="save"}}
</section>
</GhCanvasHeader>
<div class="flex items-center mb10 bt b--lightgrey-d1 pt8">
<GhMemberAvatar @member={{member}} @sizeClass={{'f-headline fw4 lh-zero'}} class="w18 h18 mr4" />
<GhMemberAvatar @member={{member}} @sizeClass={{if member.name 'f-subheadline fw4 lh-zero' '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>
<span class="darkgrey fw5">{{member.email}}</span>
{{/if}}
Member since {{this.subscribedAt}}
</p>
@ -28,7 +31,7 @@
{{gh-member-settings-form member=member
setProperty=(action "setProperty")
isLoading=this.isLoading
showDeleteTagModal=(action "toggleDeleteTagModal")}}
showDeleteTagModal=(action "toggleDeleteMemberModal")}}
</form>
<button
type="button"
@ -40,6 +43,13 @@
</button>
</section>
{{#if showUnsavedChangesModal}}
{{gh-fullscreen-modal "leave-settings"
confirm=(action "leaveScreen")
close=(action "toggleUnsavedChangesModal")
modifier="action wide"}}
{{/if}}
{{#if showDeleteMemberModal}}
{{gh-fullscreen-modal "delete-member"
model=(hash member=member onSuccess=(action "finaliseDeletion"))

View File

@ -86,42 +86,6 @@
</div>
</div>
</div>
{{#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 free or paid member registration.</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="mb2 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">Beta features</div>
<div class="flex flex-column br3 shadow-1 bg-grouped-table pa5 mt2">
@ -236,6 +200,43 @@
{{/gh-uploader}}
</div>
</div>
{{#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>
{{/if}}
</section>
</section>

View File

@ -18,13 +18,7 @@ export default Transform.extend({
if (isEmberArray(deserialized)) {
subscriptionArray = deserialized.map((item) => {
let adapter = item.get('adapter').trim();
let amount = item.get('amount');
let plan = item.get('plan').trim();
let status = item.get('status').trim();
let validUntil = item.get('validUntil');
return {adapter, amount, plan, status, validUntil};
return item;
}).compact();
} else {
subscriptionArray = [];

View File

@ -177,7 +177,7 @@ export default [
id: 23,
type: 'members',
key: 'members_subscription_settings',
value: '{"isPaid":false,"requirePaymentForSignup":false,"fromAddress":"noreply","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":""}]}}]}',
value: '{"isPaid":false,"allowSelfSignup":true,"fromAddress":"noreply","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',

View File

@ -1,6 +1,6 @@
{
"name": "ghost-admin",
"version": "2.34.0",
"version": "2.35.0",
"description": "Ember.js admin client for Ghost",
"author": "Ghost Foundation",
"homepage": "http://ghost.org",