mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
Wired new Products settings UI to API data (#1930)
refs TryGhost/Team#627 This updates the new Products settings screens to use real data from API for existing Custom Products and Prices to populate them on UI and allow site owners to edit them. - List all Products of site and allow editing Product name - List all Prices for a product and allow editing individual Price names - Add new Prices on a Product
This commit is contained in:
parent
540c50f331
commit
2555b70456
@ -6,6 +6,10 @@
|
|||||||
@optionValuePath="period"
|
@optionValuePath="period"
|
||||||
@optionLabelPath="label"
|
@optionLabelPath="label"
|
||||||
@optionTargetPath="period"
|
@optionTargetPath="period"
|
||||||
|
@includeBlank={{false}}
|
||||||
|
@promptIsSelectable={{false}}
|
||||||
|
@prompt="Select Interval"
|
||||||
|
@update={{action "updatePeriod"}}
|
||||||
/>
|
/>
|
||||||
{{svg-jar "arrow-down-small"}}
|
{{svg-jar "arrow-down-small"}}
|
||||||
</span>
|
</span>
|
@ -1,4 +1,5 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
|
import {action} from '@ember/object';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
const PERIODS = [
|
const PERIODS = [
|
||||||
@ -6,11 +7,16 @@ const PERIODS = [
|
|||||||
{label: 'Yearly', period: 'year'}
|
{label: 'Yearly', period: 'year'}
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class GhPostsListItemComponent extends Component {
|
export default class GhProductsPriceBillingPeriodComponent extends Component {
|
||||||
@service feature;
|
@service feature;
|
||||||
@service session;
|
@service session;
|
||||||
@service settings;
|
@service settings;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.availablePeriods = PERIODS;
|
||||||
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
const {value} = this.args;
|
const {value} = this.args;
|
||||||
return value;
|
return value;
|
||||||
@ -19,8 +25,11 @@ export default class GhPostsListItemComponent extends Component {
|
|||||||
const {disabled} = this.args;
|
const {disabled} = this.args;
|
||||||
return disabled || false;
|
return disabled || false;
|
||||||
}
|
}
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
@action
|
||||||
this.availablePeriods = PERIODS;
|
updatePeriod(newPeriod) {
|
||||||
|
if (this.args.updatePeriod) {
|
||||||
|
this.args.updatePeriod(this.args.value, newPeriod);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="name">
|
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="name">
|
||||||
<label for="name" class="fw6">Name</label>
|
<label for="name" class="fw6">Name</label>
|
||||||
<GhTextInput
|
<GhTextInput
|
||||||
@value={{this.price.nickname}}
|
@value={{readonly this.price.nickname}}
|
||||||
|
@input={{action (mut this.price.nickname) value="target.value"}}
|
||||||
@name="name"
|
@name="name"
|
||||||
@id="name"
|
@id="name"
|
||||||
@class="gh-input" />
|
@class="gh-input" />
|
||||||
@ -28,10 +29,29 @@
|
|||||||
@class="gh-input" />
|
@class="gh-input" />
|
||||||
<GhErrorMessage @errors={{this.price.errors}} @property="description" />
|
<GhErrorMessage @errors={{this.price.errors}} @property="description" />
|
||||||
</GhFormGroup>
|
</GhFormGroup>
|
||||||
|
<GhFormGroup @class="for-select">
|
||||||
|
<label class="fw6 f8"for="currency">Plan currency</label>
|
||||||
|
<span class="gh-select mt1">
|
||||||
|
{{one-way-select this.selectedCurrencyObj
|
||||||
|
id="currency"
|
||||||
|
name="currency"
|
||||||
|
options=(readonly this.allCurrencies)
|
||||||
|
optionValuePath="value"
|
||||||
|
optionLabelPath="label"
|
||||||
|
disabled=this.isExistingPrice
|
||||||
|
update=(action "setCurrency")
|
||||||
|
}}
|
||||||
|
{{svg-jar "arrow-down-small"}}
|
||||||
|
</span>
|
||||||
|
</GhFormGroup>
|
||||||
<div class="gh-product-priceform-period">
|
<div class="gh-product-priceform-period">
|
||||||
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="billing-period">
|
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="billing-period">
|
||||||
<label for="billing-period" class="fw6">Billing period</label>
|
<label for="billing-period" class="fw6">Billing period</label>
|
||||||
<GhProductsPriceBillingperiod @triggerId="period-input" @value={{this.price.interval}} @disabled={{this.isExistingPrice}} />
|
<GhProductsPriceBillingperiod
|
||||||
|
@updatePeriod={{action "updatePeriod"}}
|
||||||
|
@triggerId="period-input"
|
||||||
|
@value={{this.price.interval}} @disabled={{this.isExistingPrice}}
|
||||||
|
/>
|
||||||
</GhFormGroup>
|
</GhFormGroup>
|
||||||
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="price">
|
<GhFormGroup @errors={{this.price.errors}} @hasValidated={{this.price.hasValidated}} @property="price">
|
||||||
<label for="price" class="fw6">Price</label>
|
<label for="price" class="fw6">Price</label>
|
||||||
@ -40,10 +60,9 @@
|
|||||||
@value={{this.price.amount}}
|
@value={{this.price.amount}}
|
||||||
@type="number"
|
@type="number"
|
||||||
@disabled={{this.isExistingPrice}}
|
@disabled={{this.isExistingPrice}}
|
||||||
{{!-- @input={{action (mut this._scratchStripeMonthlyAmount) value="target.value"}} --}}
|
@input={{action "setAmount" value="target.value"}}
|
||||||
{{!-- @focus-out={{action "validateStripePlans"}} --}}
|
|
||||||
/>
|
/>
|
||||||
<span class="gh-input-append"><span class="ttu">{{capitalize (or this.price.currency 'usd')}} </span>/{{or this.price.interval 'month'}}</span>
|
<span class="gh-input-append"><span class="ttu">{{capitalize (or this.currencyVal "usd")}} </span>/{{or this.periodVal "month"}}</span>
|
||||||
</div>
|
</div>
|
||||||
<GhErrorMessage @errors={{this.price.errors}} @property="price" />
|
<GhErrorMessage @errors={{this.price.errors}} @property="price" />
|
||||||
</GhFormGroup>
|
</GhFormGroup>
|
||||||
@ -101,7 +120,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<GhTaskButton @buttonText="Save price"
|
<GhTaskButton @buttonText="Save price"
|
||||||
@successText={{this.successText}}
|
@successText={{this.successText}}
|
||||||
{{!-- @task={{this.saveWebhook}} --}}
|
@task={{this.savePrice}}
|
||||||
@class="gh-btn gh-btn-black gh-btn-icon"
|
@class="gh-btn gh-btn-black gh-btn-icon"
|
||||||
data-test-button="save-webhook" />
|
data-test-button="save-price" />
|
||||||
</div>
|
</div>
|
@ -1,12 +1,50 @@
|
|||||||
import ModalBase from 'ghost-admin/components/modal-base';
|
import ModalBase from 'ghost-admin/components/modal-base';
|
||||||
import classic from 'ember-classic-decorator';
|
import classic from 'ember-classic-decorator';
|
||||||
import {action} from '@ember/object';
|
import {action} from '@ember/object';
|
||||||
|
import {currencies} from 'ghost-admin/utils/currency';
|
||||||
|
import {task} from 'ember-concurrency-decorators';
|
||||||
import {tracked} from '@glimmer/tracking';
|
import {tracked} from '@glimmer/tracking';
|
||||||
|
|
||||||
// TODO: update modals to work fully with Glimmer components
|
// TODO: update modals to work fully with Glimmer components
|
||||||
@classic
|
@classic
|
||||||
export default class ModalProductPrice extends ModalBase {
|
export default class ModalProductPrice extends ModalBase {
|
||||||
@tracked model;
|
@tracked model;
|
||||||
|
@tracked price;
|
||||||
|
@tracked currencyVal;
|
||||||
|
@tracked periodVal;
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(...arguments);
|
||||||
|
this.price = {
|
||||||
|
...(this.model.price || {})
|
||||||
|
};
|
||||||
|
this.topCurrencies = currencies.slice(0, 5).map((currency) => {
|
||||||
|
return {
|
||||||
|
value: currency.isoCode.toLowerCase(),
|
||||||
|
label: `${currency.isoCode} - ${currency.name}`,
|
||||||
|
isoCode: currency.isoCode
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.currencies = currencies.slice(5, currencies.length).map((currency) => {
|
||||||
|
return {
|
||||||
|
value: currency.isoCode.toLowerCase(),
|
||||||
|
label: `${currency.isoCode} - ${currency.name}`,
|
||||||
|
isoCode: currency.isoCode
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.allCurrencies = [
|
||||||
|
{
|
||||||
|
groupName: '—',
|
||||||
|
options: this.get('topCurrencies')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupName: '—',
|
||||||
|
options: this.get('currencies')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.currencyVal = this.price.currency || 'usd';
|
||||||
|
this.periodVal = this.price.interval || 'month';
|
||||||
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
if (this.isExistingPrice) {
|
if (this.isExistingPrice) {
|
||||||
@ -15,18 +53,22 @@ export default class ModalProductPrice extends ModalBase {
|
|||||||
return 'New Price';
|
return 'New Price';
|
||||||
}
|
}
|
||||||
|
|
||||||
get price() {
|
|
||||||
return this.model.price || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
get isExistingPrice() {
|
get isExistingPrice() {
|
||||||
return !!this.model.price;
|
return !!this.model.price;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currency() {
|
||||||
|
return this.price.currency || 'usd';
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedCurrencyObj() {
|
||||||
|
return this.currencies.findBy('value', this.price.currency) || this.topCurrencies.findBy('value', this.price.currency);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: rename to confirm() when modals have full Glimmer support
|
// TODO: rename to confirm() when modals have full Glimmer support
|
||||||
@action
|
@action
|
||||||
confirmAction() {
|
confirmAction() {
|
||||||
this.confirm(this.role);
|
this.confirm(this.price);
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,16 +78,43 @@ export default class ModalProductPrice extends ModalBase {
|
|||||||
this.closeModal();
|
this.closeModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @action
|
@task({drop: true})
|
||||||
// setRoleFromModel() {
|
*savePrice() {
|
||||||
// this.role = this.model;
|
try {
|
||||||
// }
|
const priceObj = {
|
||||||
|
...this.price,
|
||||||
|
amount: (this.price.amount || 0) * 100
|
||||||
|
};
|
||||||
|
if (!priceObj.id) {
|
||||||
|
priceObj.active = 1;
|
||||||
|
priceObj.currency = priceObj.currency || 'usd';
|
||||||
|
priceObj.interval = priceObj.interval || 'month';
|
||||||
|
priceObj.type = 'recurring';
|
||||||
|
}
|
||||||
|
yield this.confirm(priceObj);
|
||||||
|
} catch (error) {
|
||||||
|
this.notifications.showAPIError(error, {key: 'price.save.failed'});
|
||||||
|
} finally {
|
||||||
|
this.send('closeModal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
confirm() {
|
confirm() {
|
||||||
this.confirmAction(...arguments);
|
this.confirmAction(...arguments);
|
||||||
},
|
},
|
||||||
|
updatePeriod(oldPeriod, newPeriod) {
|
||||||
|
this.price.interval = newPeriod;
|
||||||
|
this.periodVal = newPeriod;
|
||||||
|
},
|
||||||
|
setAmount(amount) {
|
||||||
|
this.price.amount = !isNaN(amount) ? parseInt(amount) : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
setCurrency(currency) {
|
||||||
|
this.price.currency = currency.value;
|
||||||
|
this.currencyVal = currency.value;
|
||||||
|
},
|
||||||
// needed because ModalBase uses .send() for keyboard events
|
// needed because ModalBase uses .send() for keyboard events
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.close();
|
this.close();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import {action} from '@ember/object';
|
import EmberObject, {action} from '@ember/object';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
import {task} from 'ember-concurrency-decorators';
|
import {task} from 'ember-concurrency-decorators';
|
||||||
import {tracked} from '@glimmer/tracking';
|
import {tracked} from '@glimmer/tracking';
|
||||||
@ -10,6 +10,7 @@ export default class ProductController extends Controller {
|
|||||||
@tracked showLeaveSettingsModal = false;
|
@tracked showLeaveSettingsModal = false;
|
||||||
@tracked showPriceModal = false;
|
@tracked showPriceModal = false;
|
||||||
@tracked priceModel = null;
|
@tracked priceModel = null;
|
||||||
|
@tracked showUnsavedChangesModal = false;
|
||||||
|
|
||||||
get product() {
|
get product() {
|
||||||
return this.model;
|
return this.model;
|
||||||
@ -29,12 +30,35 @@ export default class ProductController extends Controller {
|
|||||||
return (this.product.stripePrices || []).length;
|
return (this.product.stripePrices || []).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveRoute(transition) {
|
@action
|
||||||
if (this.settings.get('hasDirtyAttributes')) {
|
toggleUnsavedChangesModal(transition) {
|
||||||
transition.abort();
|
let leaveTransition = this.leaveScreenTransition;
|
||||||
this.leaveSettingsTransition = transition;
|
|
||||||
this.showLeaveSettingsModal = true;
|
if (!transition && this.showUnsavedChangesModal) {
|
||||||
|
this.leaveScreenTransition = null;
|
||||||
|
this.showUnsavedChangesModal = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
|
||||||
|
this.leaveScreenTransition = transition;
|
||||||
|
|
||||||
|
// if a save is running, wait for it to finish then transition
|
||||||
|
if (this.saveTask.isRunning) {
|
||||||
|
return this.saveTask.last.then(() => {
|
||||||
|
transition.retry();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// we genuinely have unsaved data, show the modal
|
||||||
|
this.showUnsavedChangesModal = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
leaveScreen() {
|
||||||
|
this.product.rollbackAttributes();
|
||||||
|
return this.leaveScreenTransition.retry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -62,6 +86,26 @@ export default class ProductController extends Controller {
|
|||||||
this.leaveSettingsTransition = null;
|
this.leaveSettingsTransition = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
save() {
|
||||||
|
return this.saveTask.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
savePrice(price) {
|
||||||
|
const stripePrices = this.product.stripePrices.map((d) => {
|
||||||
|
if (d.id === price.id) {
|
||||||
|
return EmberObject.create(price);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
if (!price.id) {
|
||||||
|
stripePrices.push(EmberObject.create(price));
|
||||||
|
}
|
||||||
|
this.product.set('stripePrices', stripePrices);
|
||||||
|
this.saveTask.perform();
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
closePriceModal() {
|
closePriceModal() {
|
||||||
this.showPriceModal = false;
|
this.showPriceModal = false;
|
||||||
@ -69,6 +113,6 @@ export default class ProductController extends Controller {
|
|||||||
|
|
||||||
@task({drop: true})
|
@task({drop: true})
|
||||||
*saveTask() {
|
*saveTask() {
|
||||||
return yield this.settings.save();
|
return yield this.product.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import Mixin from '@ember/object/mixin';
|
|||||||
import Model from '@ember-data/model';
|
import Model from '@ember-data/model';
|
||||||
import NavItemValidator from 'ghost-admin/validators/nav-item';
|
import NavItemValidator from 'ghost-admin/validators/nav-item';
|
||||||
import PostValidator from 'ghost-admin/validators/post';
|
import PostValidator from 'ghost-admin/validators/post';
|
||||||
|
import ProductValidator from 'ghost-admin/validators/product';
|
||||||
import RSVP from 'rsvp';
|
import RSVP from 'rsvp';
|
||||||
import ResetValidator from 'ghost-admin/validators/reset';
|
import ResetValidator from 'ghost-admin/validators/reset';
|
||||||
import SettingValidator from 'ghost-admin/validators/setting';
|
import SettingValidator from 'ghost-admin/validators/setting';
|
||||||
@ -54,7 +55,8 @@ export default Mixin.create({
|
|||||||
integration: IntegrationValidator,
|
integration: IntegrationValidator,
|
||||||
webhook: WebhookValidator,
|
webhook: WebhookValidator,
|
||||||
label: LabelValidator,
|
label: LabelValidator,
|
||||||
snippet: SnippetValidator
|
snippet: SnippetValidator,
|
||||||
|
product: ProductValidator
|
||||||
},
|
},
|
||||||
|
|
||||||
// This adds the Errors object to the validation engine, and shouldn't affect
|
// This adds the Errors object to the validation engine, and shouldn't affect
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||||
|
import {action} from '@ember/object';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
export default class ProductRoute extends AuthenticatedRoute {
|
export default class ProductRoute extends AuthenticatedRoute {
|
||||||
@service store
|
@service store
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
_requiresBackgroundRefresh = true;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.router.on('routeWillChange', (transition) => {
|
||||||
|
this.showUnsavedChangesModal(transition);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
if (params.product_id) {
|
if (params.product_id) {
|
||||||
@ -12,15 +23,54 @@ export default class ProductRoute extends AuthenticatedRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actions = {
|
beforeModel() {
|
||||||
willTransition(transition) {
|
super.beforeModel(...arguments);
|
||||||
return this.controller.leaveRoute(transition);
|
return this.session.user.then((user) => {
|
||||||
|
if (!user.isOwnerOrAdmin) {
|
||||||
|
return this.transitionTo('home');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupController(controller, product) {
|
||||||
|
super.setupController(...arguments);
|
||||||
|
if (this._requiresBackgroundRefresh) {
|
||||||
|
if (product.get('id')) {
|
||||||
|
return this.store.queryRecord('product', {id: product.get('id'), include: 'stripe_prices'});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
super.deactivate(...arguments);
|
||||||
|
// clean up newly created records and revert unsaved changes to existing
|
||||||
|
this.controller.product.rollbackAttributes();
|
||||||
|
this._requiresBackgroundRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
save() {
|
||||||
|
this.controller.save();
|
||||||
|
}
|
||||||
|
|
||||||
buildRouteInfoMetadata() {
|
buildRouteInfoMetadata() {
|
||||||
return {
|
return {
|
||||||
titleToken: 'Settings - Products'
|
titleToken: 'Settings - Products'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showUnsavedChangesModal(transition) {
|
||||||
|
if (transition.from && transition.from.name === this.routeName && transition.targetName) {
|
||||||
|
let {controller} = this;
|
||||||
|
|
||||||
|
// product.changedAttributes is always true for new products but number of changed attrs is reliable
|
||||||
|
let isChanged = Object.keys(controller.product.changedAttributes()).length > 0;
|
||||||
|
|
||||||
|
if (!controller.product.isDeleted && isChanged) {
|
||||||
|
transition.abort();
|
||||||
|
controller.toggleUnsavedChangesModal(transition);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,6 @@ export default class ProductsRoute extends AuthenticatedRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model() {
|
model() {
|
||||||
return this.store.findAll('product');
|
return this.store.findAll('product', {include: 'stripe_prices'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,7 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-price-list .gh-list-row:hover .gh-price-list-actionlist {
|
.gh-price-list .gh-list-row:hover .gh-price-list-actionlist {
|
||||||
@ -129,13 +130,32 @@
|
|||||||
|
|
||||||
.gh-price-list-actionlist a,
|
.gh-price-list-actionlist a,
|
||||||
.gh-price-list-actionlist button {
|
.gh-price-list-actionlist button {
|
||||||
|
margin-left: 15px;
|
||||||
|
padding: 0;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
margin-left: 24px;
|
|
||||||
color: var(--darkgrey);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-price-list-actionlist button.archive {
|
.gh-price-list-actionlist a span,
|
||||||
|
.gh-price-list-actionlist button span {
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1;
|
||||||
|
height: unset;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
color: var(--darkgrey);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.2rem !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-price-list-actionlist a:hover span,
|
||||||
|
.gh-price-list-actionlist button:hover span {
|
||||||
|
background: var(--whitegrey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-price-list-actionlist a.archived:hover span,
|
||||||
|
.gh-price-list-actionlist button.archived:hover span {
|
||||||
|
background: color-mod(var(--red) a(10%));
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,6 +177,14 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-price-list-noprices {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 48px 0;
|
||||||
|
color: var(--midgrey);
|
||||||
|
}
|
||||||
|
|
||||||
.product-actions-menu {
|
.product-actions-menu {
|
||||||
top: calc(100% + 6px);
|
top: calc(100% + 6px);
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@ -177,4 +205,4 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-gap: 20px;
|
grid-gap: 20px;
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,18 @@
|
|||||||
<div class="gh-list-header">Subscriptions</div>
|
<div class="gh-list-header">Subscriptions</div>
|
||||||
<div class="gh-list-header"></div>
|
<div class="gh-list-header"></div>
|
||||||
</li>
|
</li>
|
||||||
|
{{#unless this.stripePrices}}
|
||||||
|
<tr class="gh-list-row {{if price.active "" "gh-price-list-archived"}}">
|
||||||
|
<td colspan="4" class="gh-list-data">
|
||||||
|
<div class="gh-price-list-noprices">
|
||||||
|
<div class="mb2">There are no prices for this product.</div>
|
||||||
|
<button type="button" class="gh-btn gh-btn-green" {{action "openNewPrice"}}>
|
||||||
|
<span>Add price</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/unless}}
|
||||||
{{#each this.stripePrices as |price|}}
|
{{#each this.stripePrices as |price|}}
|
||||||
<li class="gh-list-row {{if price.active "" "gh-price-list-archived"}}">
|
<li class="gh-list-row {{if price.active "" "gh-price-list-archived"}}">
|
||||||
<div class="gh-list-data gh-price-list-title">
|
<div class="gh-list-data gh-price-list-title">
|
||||||
@ -102,11 +114,11 @@
|
|||||||
|
|
||||||
<div class="gh-list-data gh-price-list-actions">
|
<div class="gh-list-data gh-price-list-actions">
|
||||||
<div class="gh-price-list-actionlist">
|
<div class="gh-price-list-actionlist">
|
||||||
<button class="gh-btn gh-btn-link archive" {{action "openEditPrice" price}}>
|
<button class="gh-btn gh-btn-link" {{action "openEditPrice" price}}>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
{{#if price.active}}
|
{{#if price.active}}
|
||||||
<button class="gh-btn gh-btn-link archive">
|
<button class="gh-btn gh-btn-link archived">
|
||||||
<span>Archive</span>
|
<span>Archive</span>
|
||||||
</button>
|
</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -118,28 +130,38 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
{{#if this.stripePrices}}
|
||||||
<button type="button" class="gh-btn gh-btn-green" {{action "openNewPrice"}}>
|
<button type="button" class="gh-btn gh-btn-green" {{action "openNewPrice"}}>
|
||||||
<span>New price</span>
|
<span>Add price</span>
|
||||||
</button>
|
</button>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if this.showPriceModal}}
|
{{#if this.showPriceModal}}
|
||||||
|
{{!-- <GhFullscreenModal @modifier="action wide member-product">
|
||||||
|
<ModalProductPrice
|
||||||
|
@model={{hash
|
||||||
|
price=this.priceModel
|
||||||
|
}}
|
||||||
|
@confirm={{action "savePrice"}}
|
||||||
|
@closeModal={{this.closePriceModal}} />
|
||||||
|
</GhFullscreenModal> --}}
|
||||||
<GhFullscreenModal
|
<GhFullscreenModal
|
||||||
@modal="product-price"
|
@modal="product-price"
|
||||||
@model={{hash
|
@model={{hash
|
||||||
price=this.priceModel
|
price=this.priceModel
|
||||||
}}
|
}}
|
||||||
|
@confirm={{action "savePrice"}}
|
||||||
@close={{this.closePriceModal}}
|
@close={{this.closePriceModal}}
|
||||||
@modifier="action wide body-scrolling product-price" />
|
@modifier="action wide body-scrolling product-ssprice" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.showLeaveSettingsModal}}
|
{{#if this.showUnsavedChangesModal}}
|
||||||
<GhFullscreenModal
|
<GhFullscreenModal
|
||||||
@modal="leave-settings"
|
@modal="leave-settings"
|
||||||
@confirm={{this.confirmLeave}}
|
@confirm={{this.leaveScreen}}
|
||||||
@close={{this.cancelLeave}}
|
@close={{this.toggleUnsavedChangesModal}}
|
||||||
@modifier="action wide"
|
@modifier="action wide" />
|
||||||
/>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</section>
|
</section>
|
@ -4,7 +4,9 @@ import {A as emberA, isArray as isEmberArray} from '@ember/array';
|
|||||||
|
|
||||||
export default Transform.extend({
|
export default Transform.extend({
|
||||||
deserialize(serialized = []) {
|
deserialize(serialized = []) {
|
||||||
return emberA(serialized.map(StripePrice.create.bind(StripePrice)));
|
const stripePrices = serialized.map(itemDetails => StripePrice.create(itemDetails));
|
||||||
|
|
||||||
|
return emberA(stripePrices);
|
||||||
},
|
},
|
||||||
|
|
||||||
serialize(deserialized) {
|
serialize(deserialized) {
|
||||||
|
13
ghost/admin/app/validators/product.js
Normal file
13
ghost/admin/app/validators/product.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import BaseValidator from './base';
|
||||||
|
import validator from 'validator';
|
||||||
|
|
||||||
|
export default BaseValidator.create({
|
||||||
|
properties: ['name'],
|
||||||
|
|
||||||
|
name(model) {
|
||||||
|
if (!validator.isLength(model.name || '', 0, 191)) {
|
||||||
|
model.errors.add('name', 'Name cannot be longer than 191 characters.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user