mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-02 08:13:34 +03:00
060d791a63
no issue The `settings` service has been a source of confusion when writing with modern Ember patterns because it's use of the deprecated `ProxyMixin` forced all property access/setting to go via `.get()` and `.set()` whereas the rest of the system has mostly (there are a few other uses of ProxyObjects remaining) eliminated the use of the non-native get/set methods. - removed use of `ProxyMixin` in the `settings` service by grabbing the attributes off the setting model after fetching and using `Object.defineProperty()` to add native getters/setters that pass through to the model's getters/setters. Ember's autotracking automatically works across the native getters/setters so we can then use the service as if it was any other native object - updated all code to use `settings.{attrName}` directly for getting/setting instead of `.get()` and `.set()` - removed use of observer in the `customViews` service because it was being set up before the native properties had been added on the settings service meaning autotracking wasn't able to set up properly
404 lines
14 KiB
JavaScript
404 lines
14 KiB
JavaScript
import ConfirmEmailModal from './modals/settings/confirm-email';
|
|
import ModalComponent from 'ghost-admin/components/modal-base';
|
|
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
|
|
import {action, computed} from '@ember/object';
|
|
import {htmlSafe} from '@ember/template';
|
|
import {inject as service} from '@ember/service';
|
|
import {task, timeout} from 'ember-concurrency';
|
|
const ICON_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
|
|
|
|
export default ModalComponent.extend({
|
|
config: service(),
|
|
modals: service(),
|
|
membersUtils: service(),
|
|
settings: service(),
|
|
store: service(),
|
|
session: service(),
|
|
feature: service(),
|
|
ghostPaths: service(),
|
|
ajax: service(),
|
|
|
|
page: 'signup',
|
|
iconExtensions: null,
|
|
isShowModalLink: true,
|
|
customIcon: null,
|
|
showLinksPage: false,
|
|
showLeaveSettingsModal: false,
|
|
isPreloading: true,
|
|
changedTiers: null,
|
|
openSection: null,
|
|
portalPreviewGuid: 'modal-portal-settings',
|
|
|
|
confirm() {},
|
|
|
|
backgroundStyle: computed('settings.accentColor', function () {
|
|
let color = this.settings.accentColor || '#ffffff';
|
|
return htmlSafe(`background-color: ${color}`);
|
|
}),
|
|
|
|
showModalLinkOrAttribute: computed('isShowModalLink', function () {
|
|
if (this.isShowModalLink) {
|
|
return `#/portal`;
|
|
}
|
|
return `data-portal`;
|
|
}),
|
|
|
|
portalPreviewUrl: computed('page', 'model.tiers.[]', 'changedTiers.[]', 'membersUtils.{isFreeChecked,isMonthlyChecked,isYearlyChecked}', 'settings.{portalName,portalButton,portalButtonIcon,portalButtonSignupText,portalButtonStyle,accentColor,portalPlans.[]}', function () {
|
|
const options = this.getProperties(['page']);
|
|
options.portalTiers = this.model.tiers?.filter((tier) => {
|
|
return tier.get('visibility') === 'public'
|
|
&& tier.get('active') === true
|
|
&& tier.get('type') === 'paid';
|
|
}).map((tier) => {
|
|
return tier.id;
|
|
});
|
|
const freeTier = this.model.tiers?.find((tier) => {
|
|
return tier.type === 'free';
|
|
});
|
|
options.isFreeChecked = freeTier?.visibility === 'public';
|
|
return this.membersUtils.getPortalPreviewUrl(options);
|
|
}),
|
|
|
|
showIconSetting: computed('selectedButtonStyle', function () {
|
|
const selectedButtonStyle = this.get('selectedButtonStyle.name') || '';
|
|
return selectedButtonStyle.includes('icon');
|
|
}),
|
|
|
|
showButtonTextSetting: computed('selectedButtonStyle', function () {
|
|
const selectedButtonStyle = this.get('selectedButtonStyle.name') || '';
|
|
return selectedButtonStyle.includes('text');
|
|
}),
|
|
|
|
selectedButtonStyle: computed('settings.portalButtonStyle', function () {
|
|
return this.buttonStyleOptions.find((buttonStyle) => {
|
|
return (buttonStyle.name === this.settings.portalButtonStyle);
|
|
});
|
|
}),
|
|
|
|
isFreeChecked: computed('settings.{portalPlans.[],membersSignupAccess}', function () {
|
|
const allowedPlans = this.settings.portalPlans || [];
|
|
return (this.settings.membersSignupAccess === 'all' && allowedPlans.includes('free'));
|
|
}),
|
|
isMonthlyChecked: computed('settings.portalPlans.[]', 'membersUtils.paidMembersEnabled', function () {
|
|
const allowedPlans = this.settings.portalPlans || [];
|
|
return (this.membersUtils.paidMembersEnabled && allowedPlans.includes('monthly'));
|
|
}),
|
|
isYearlyChecked: computed('settings.portalPlans.[]', 'membersUtils.paidMembersEnabled', function () {
|
|
const allowedPlans = this.settings.portalPlans || [];
|
|
return (this.membersUtils.paidMembersEnabled && allowedPlans.includes('yearly'));
|
|
}),
|
|
tiers: computed('model.tiers.[]', 'changedTiers.[]', 'isPreloading', function () {
|
|
const paidTiers = this.model.tiers?.filter(tier => tier.type === 'paid' && tier.active === true);
|
|
if (this.isPreloading || !paidTiers?.length) {
|
|
return [];
|
|
}
|
|
|
|
const tiers = paidTiers.map((tier) => {
|
|
return {
|
|
id: tier.id,
|
|
name: tier.name,
|
|
checked: tier.visibility === 'public'
|
|
};
|
|
});
|
|
return tiers;
|
|
}),
|
|
|
|
showPortalPrices: computed('tiers', function () {
|
|
const visibleTiers = this.model.tiers?.filter((tier) => {
|
|
return tier.visibility === 'public' && tier.type === 'paid';
|
|
});
|
|
|
|
return !!visibleTiers?.length;
|
|
}),
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
this.buttonStyleOptions = [
|
|
{name: 'icon-and-text', label: 'Icon and text'},
|
|
{name: 'icon-only', label: 'Icon only'},
|
|
{name: 'text-only', label: 'Text only'}
|
|
];
|
|
this.availablePages = [{
|
|
name: 'signup',
|
|
label: 'Signup'
|
|
}, {
|
|
name: 'accountHome',
|
|
label: 'Account'
|
|
}, {
|
|
name: 'links',
|
|
label: 'Links'
|
|
}];
|
|
this.iconExtensions = ICON_EXTENSIONS;
|
|
this.changedTiers = [];
|
|
this.set('supportAddress', this.parseEmailAddress(this.settings.membersSupportAddress));
|
|
this.set('openSection', 'signup-options');
|
|
},
|
|
|
|
didInsertElement() {
|
|
this._super(...arguments);
|
|
this.settings.errors.clear();
|
|
},
|
|
|
|
actions: {
|
|
toggleFreePlan(isChecked) {
|
|
this.updateAllowedPlan('free', isChecked);
|
|
},
|
|
togglePlan(plan, event) {
|
|
this.updateAllowedPlan(plan, event.target.checked);
|
|
},
|
|
toggleTier(tierId, event) {
|
|
this.updateAllowedTier(tierId, event.target.checked);
|
|
},
|
|
togglePortalButton(showButton) {
|
|
this.settings.portalButton = showButton;
|
|
},
|
|
|
|
togglePortalName(showSignupName) {
|
|
this.settings.portalName = showSignupName;
|
|
},
|
|
toggleSection(section) {
|
|
if (this.get('openSection') === section) {
|
|
this.set('openSection', null);
|
|
} else {
|
|
this.set('openSection', section);
|
|
}
|
|
},
|
|
|
|
confirm() {
|
|
return this.saveTask.perform();
|
|
},
|
|
|
|
isPlanSelected(plan) {
|
|
const allowedPlans = this.settings.portalPlans;
|
|
return allowedPlans.includes(plan);
|
|
},
|
|
|
|
switchPreviewPage(page) {
|
|
if (page.name === 'links') {
|
|
this.set('showLinksPage', true);
|
|
this.set('page', '');
|
|
} else {
|
|
this.set('showLinksPage', false);
|
|
this.set('page', page.name);
|
|
}
|
|
},
|
|
|
|
switchToSignupPage() {
|
|
if (this.showLinksPage) {
|
|
this.set('showLinksPage', false);
|
|
this.set('page', 'signup');
|
|
}
|
|
},
|
|
|
|
setButtonStyle(buttonStyle) {
|
|
this.settings.portalButtonStyle = buttonStyle.name;
|
|
},
|
|
|
|
setSignupButtonText(event) {
|
|
this.settings.portalButtonSignupText = event.target.value;
|
|
},
|
|
/**
|
|
* Fired after an image upload completes
|
|
* @param {string} property - Property name to be set on `this.settings`
|
|
* @param {UploadResult[]} results - Array of UploadResult objects
|
|
* @return {string} The URL that was set on `this.settings.property`
|
|
*/
|
|
imageUploaded(property, results) {
|
|
if (results[0]) {
|
|
this.set('customIcon', results[0].url);
|
|
this.settings.portalButtonIcon = results[0].url;
|
|
}
|
|
},
|
|
/**
|
|
* Opens a file selection dialog - Triggered by "Upload Image" buttons,
|
|
* searches for the hidden file input within the .gh-setting element
|
|
* containing the clicked button then simulates a click
|
|
* @param {MouseEvent} event - MouseEvent fired by the button click
|
|
*/
|
|
triggerFileDialog(event) {
|
|
event?.target.closest('.gh-setting-action')?.querySelector('input[type="file"]')?.click();
|
|
},
|
|
|
|
deleteCustomIcon() {
|
|
this.set('customIcon', null);
|
|
this.settings.portalButtonIcon = this.membersUtils.defaultIconKeys[0];
|
|
},
|
|
|
|
selectDefaultIcon(icon) {
|
|
this.settings.portalButtonIcon = icon;
|
|
},
|
|
|
|
closeLeaveSettingsModal() {
|
|
this.set('showLeaveSettingsModal', false);
|
|
},
|
|
|
|
openStripeConnect() {
|
|
this.isWaitingForStripeConnection = true;
|
|
this.model.openStripeConnect();
|
|
},
|
|
|
|
leaveSettings() {
|
|
this.closeModal();
|
|
},
|
|
|
|
validateFreeSignupRedirect() {
|
|
return this._validateSignupRedirect(this.freeSignupRedirect, 'membersFreeSignupRedirect');
|
|
},
|
|
|
|
validatePaidSignupRedirect() {
|
|
return this._validateSignupRedirect(this.paidSignupRedirect, 'membersPaidSignupRedirect');
|
|
},
|
|
|
|
setSupportAddress(supportAddress) {
|
|
this.set('supportAddress', supportAddress);
|
|
|
|
if (this.config.emailDomain && supportAddress === `noreply@${this.config.emailDomain}`) {
|
|
this.settings.membersSupportAddress = 'noreply';
|
|
} else {
|
|
this.settings.membersSupportAddress = supportAddress;
|
|
}
|
|
}
|
|
},
|
|
|
|
parseEmailAddress(address) {
|
|
const emailAddress = address || 'noreply';
|
|
// Adds default domain as site domain
|
|
if (emailAddress.indexOf('@') < 0 && this.config.emailDomain) {
|
|
return `${emailAddress}@${this.config.emailDomain}`;
|
|
}
|
|
return emailAddress;
|
|
},
|
|
|
|
updateAllowedPlan(plan, isChecked) {
|
|
const portalPlans = this.settings.portalPlans || [];
|
|
const allowedPlans = [...portalPlans];
|
|
const freeTier = this.model.tiers.find(p => p.type === 'free');
|
|
|
|
if (!isChecked) {
|
|
this.settings.portalPlans = allowedPlans.filter(p => p !== plan);
|
|
if (plan === 'free') {
|
|
freeTier.set('visibility', 'none');
|
|
}
|
|
} else {
|
|
allowedPlans.push(plan);
|
|
this.settings.portalPlans = allowedPlans;
|
|
if (plan === 'free') {
|
|
freeTier.set('visibility', 'public');
|
|
}
|
|
}
|
|
},
|
|
|
|
updateAllowedTier(tierId, isChecked) {
|
|
const tier = this.model.tiers.find(p => p.id === tierId);
|
|
if (!isChecked) {
|
|
tier.set('visibility', 'none');
|
|
} else {
|
|
tier.set('visibility', 'public');
|
|
}
|
|
let portalTiers = this.model.tiers.filter((p) => {
|
|
return p.visibility === 'public';
|
|
}).map(p => p.id);
|
|
this.set('changedTiers', portalTiers);
|
|
},
|
|
|
|
_validateSignupRedirect(url, type) {
|
|
let errMessage = `Please enter a valid URL`;
|
|
this.settings.errors.remove(type);
|
|
this.settings.hasValidated.removeObject(type);
|
|
|
|
if (url === null) {
|
|
this.settings.errors.add(type, errMessage);
|
|
this.settings.hasValidated.pushObject(type);
|
|
return false;
|
|
}
|
|
|
|
if (url === undefined) {
|
|
// Not initialised
|
|
return;
|
|
}
|
|
|
|
if (url.href.startsWith(this.siteUrl)) {
|
|
const path = url.href.replace(this.siteUrl, '');
|
|
this.settings[type] = path;
|
|
} else {
|
|
this.settings[type] = url.href;
|
|
}
|
|
},
|
|
|
|
finishPreloading: action(async function () {
|
|
if (this.model.preloadTask?.isRunning) {
|
|
await this.model.preloadTask;
|
|
}
|
|
|
|
const portalButtonIcon = this.settings.portalButtonIcon || '';
|
|
if (portalButtonIcon && !this.membersUtils.defaultIconKeys.includes(portalButtonIcon)) {
|
|
this.set('customIcon', this.settings.portalButtonIcon);
|
|
}
|
|
|
|
this.siteUrl = this.config.get('blogUrl');
|
|
this.set('isPreloading', false);
|
|
}),
|
|
|
|
refreshAfterStripeConnected: action(async function () {
|
|
if (this.isWaitingForStripeConnection) {
|
|
await this.finishPreloading();
|
|
this.notifyPropertyChange('page'); // force preview url to recompute
|
|
this.set('portalPreviewGuid', Date.now().valueOf()); // force preview re-render
|
|
this.isWaitingForStripeConnection = false;
|
|
}
|
|
}),
|
|
|
|
copyLinkOrAttribute: task(function* () {
|
|
copyTextToClipboard(this.showModalLinkOrAttribute);
|
|
yield timeout(this.isTesting ? 50 : 3000);
|
|
}),
|
|
|
|
saveTask: task(function* () {
|
|
this.send('validateFreeSignupRedirect');
|
|
this.send('validatePaidSignupRedirect');
|
|
|
|
this.settings.errors.remove('members_support_address');
|
|
this.settings.hasValidated.removeObject('members_support_address');
|
|
|
|
if (this.settings.errors.length !== 0) {
|
|
return;
|
|
}
|
|
|
|
// Save tier visibility if changed
|
|
yield Promise.all(
|
|
this.model.tiers.filter((tier) => {
|
|
const changedAttrs = tier.changedAttributes();
|
|
return !!changedAttrs.visibility;
|
|
}).map((tier) => {
|
|
return tier.save();
|
|
})
|
|
);
|
|
|
|
const newEmail = this.settings.membersSupportAddress;
|
|
|
|
try {
|
|
const result = yield this.settings.save();
|
|
if (result._meta?.sent_email_verification) {
|
|
yield this.modals.open(ConfirmEmailModal, {
|
|
newEmail,
|
|
currentEmail: this.settings.membersSupportAddress
|
|
});
|
|
}
|
|
|
|
this.closeModal();
|
|
} catch (error) {
|
|
// Do we have an error that we can show inline?
|
|
if (error.payload && error.payload.errors) {
|
|
for (const payloadError of error.payload.errors) {
|
|
if (payloadError.type === 'ValidationError' && payloadError.property && (payloadError.context || payloadError.message)) {
|
|
// Context has a better error message for validation errors
|
|
this.settings.errors.add(payloadError.property, payloadError.context || payloadError.message);
|
|
this.settings.hasValidated.pushObject(payloadError.property);
|
|
}
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}).drop()
|
|
});
|