diff --git a/ghost/admin/app/components/gh-member-settings-form.js b/ghost/admin/app/components/gh-member-settings-form.js index 84898c0247..fb6e69dcca 100644 --- a/ghost/admin/app/components/gh-member-settings-form.js +++ b/ghost/admin/app/components/gh-member-settings-form.js @@ -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) { diff --git a/ghost/admin/app/components/gh-members-lab-setting.js b/ghost/admin/app/components/gh-members-lab-setting.js index 209cce5faf..13392ba0c0 100644 --- a/ghost/admin/app/components/gh-members-lab-setting.js +++ b/ghost/admin/app/components/gh-members-lab-setting.js @@ -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', diff --git a/ghost/admin/app/controllers/member.js b/ghost/admin/app/controllers/member.js index 405384b58d..599faae44b 100644 --- a/ghost/admin/app/controllers/member.js +++ b/ghost/admin/app/controllers/member.js @@ -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,9 +35,63 @@ 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) { this.set('isLoading', true); yield this.store.findRecord('member', memberId, { diff --git a/ghost/admin/app/controllers/settings/labs.js b/ghost/admin/app/controllers/settings/labs.js index b23c41557a..7963d93bd4 100644 --- a/ghost/admin/app/controllers/settings/labs.js +++ b/ghost/admin/app/controllers/settings/labs.js @@ -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(); diff --git a/ghost/admin/app/models/member.js b/ghost/admin/app/models/member.js index 094d8564c7..601793abb2 100644 --- a/ghost/admin/app/models/member.js +++ b/ghost/admin/app/models/member.js @@ -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') }); diff --git a/ghost/admin/app/routes/member.js b/ghost/admin/app/routes/member.js index 028bf8b66a..97e474af9b 100644 --- a/ghost/admin/app/routes/member.js +++ b/ghost/admin/app/routes/member.js @@ -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; + } + } } }); diff --git a/ghost/admin/app/serializers/member.js b/ghost/admin/app/serializers/member.js new file mode 100644 index 0000000000..76eff17cb9 --- /dev/null +++ b/ghost/admin/app/serializers/member.js @@ -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; + } +}); diff --git a/ghost/admin/app/templates/components/gh-member-settings-form.hbs b/ghost/admin/app/templates/components/gh-member-settings-form.hbs index 044487cf5d..a5c546bcbe 100644 --- a/ghost/admin/app/templates/components/gh-member-settings-form.hbs +++ b/ghost/admin/app/templates/components/gh-member-settings-form.hbs @@ -4,7 +4,6 @@ {{#gh-form-group errors=member.errors hasValidated=member.hasValidated property="name"}} {{gh-text-input - disabled=true id="member-name" name="name" value=(readonly scratchName) @@ -29,19 +28,18 @@
Maximum: 500 characters. You’ve used {{gh-count-down-characters scratchDescription 500}}
+ {{gh-error-message errors=member.errors property="note"}} +Maximum: 500 characters. You’ve used {{gh-count-down-characters scratchNote 500}}
{{/gh-form-group}}Configure Stripe API keys for signups
+Set monthly and yearly subscription prices
Allow free members signup
+Configure restrictions for new posts
+Membership related email settings
+{{#if member.name}} - {{member.email}} – + {{member.email}} – {{/if}} Member since {{this.subscribedAt}}
@@ -28,7 +31,7 @@ {{gh-member-settings-form member=member setProperty=(action "setProperty") isLoading=this.isLoading - showDeleteTagModal=(action "toggleDeleteTagModal")}} + showDeleteTagModal=(action "toggleDeleteMemberModal")}}