Wired UI for archiving tiers (#2231)

refs https://github.com/TryGhost/Team/issues/1252

- allows site owners to (un)archive existing tiers via Admin UI
- adds option to switch between archived or active tiers view

Co-authored-by: Djordje Vlaisavljevic <dzvlais@gmail.com>
This commit is contained in:
Rishabh Garg 2022-01-31 23:56:12 +05:30 committed by GitHub
parent 0f718c5a99
commit baf6ec07a8
12 changed files with 293 additions and 18 deletions

View File

@ -1,15 +1,43 @@
<label>Tiers</label>
<div class="gh-product-cards">
{{#each this.products as |product productIdx|}}
<GhProductCard
@product={{product}}
@openEditProduct={{this.openEditProduct}}
/>
{{/each}}
<div class="gh-product-cards-footer">
<button class="gh-btn gh-btn-link gh-btn-text gh-btn-icon gh-btn-add-product green" {{action "openNewProduct" product}}><span>{{svg-jar "add-stroke" class="stroke-green"}}Add tier</span></button>
<div class="flex justify-between items-center">
<label>Tiers</label>
<div>
<div class="gh-contentfilter-menu gh-contentfilter-type {{if (not (eq this.selectedType.value "active")) "gh-contentfilter-selected"}}" data-test-type-select="true">
<PowerSelect
@selected={{this.selectedType}}
@options={{this.availableTypes}}
@searchEnabled={{false}}
@onChange={{this.onTypeChange}}
@triggerComponent="gh-power-select/trigger"
@triggerClass="gh-contentfilter-menu-trigger gh-contentfilter-menu-trigger-tiers"
@dropdownClass="gh-contentfilter-menu-dropdown"
@matchTriggerWidth={{false}}
as |type|
>
{{#if type.name}}{{type.name}}{{else}}<span class="red">Unknown type</span>{{/if}}
</PowerSelect>
</div>
</div>
</div>
<div class="gh-product-cards">
{{#if this.isEmptyList}}
<div class="flex justify-center items-center gh-main-content-card">
<p style="color:#7c8b9a" class="mb0 pa8">You have no {{this.selectedType.value}} tiers.</p>
</div>
{{/if}}
{{#each this.products as |product productIdx|}}
<GhProductCard
@product={{product}}
@openEditProduct={{this.openEditProduct}}
/>
{{/each}}
{{#if (eq this.type "active" )}}
<div class="gh-product-cards-footer">
<button class="gh-btn gh-btn-link gh-btn-text gh-btn-icon gh-btn-add-product green" {{action "openNewProduct" product}}>
<span>{{svg-jar "add-stroke" class="stroke-green"}}Add tier</span>
</button>
</div>
{{/if}}
</div>
{{#if this.showProductModal}}

View File

@ -3,6 +3,14 @@ import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const TYPES = [{
name: 'Active',
value: 'active'
},{
name: 'Archived',
value: 'archived'
}];
export default class extends Component {
@service membersUtils;
@service ghostPaths;
@ -12,9 +20,35 @@ export default class extends Component {
@tracked showProductModal = false;
@tracked productModel = null;
@tracked type = 'active';
get products() {
return this.args.products;
return this.args.products.filter((product) => {
if (this.type === 'active') {
return !!product.active;
} else if (this.type === 'archived') {
return !product.active;
}
});
}
get availableTypes() {
return TYPES;
}
get selectedType() {
return this.type ? TYPES.find((d) => {
return this.type === d.value;
}) : TYPES[0];
}
get isEmptyList() {
return this.products.length === 0;
}
@action
onTypeChange(type) {
this.type = type.value;
}
@action

View File

@ -1,7 +1,4 @@
<div class="gh-main-content-card gh-product-card">
<button class="gh-product-card-editbutton gh-btn gh-btn-text gh-btn-link green" {{action "openEditProduct" this.product}}>
<span>Edit</span>
</button>
<div class="gh-product-card-block title-block">
<h3 class="gh-product-card-name">
{{this.product.name}}
@ -54,4 +51,35 @@
</div>
</div>
{{/if}}
<div class="gh-product-card-editbutton-container">
<span class="dropdown">
<GhDropdownButton
@dropdownName="tiers-actions-menu-{{this.product.name}}"
@classNames="gh-btn gh-btn-action-icon gh-btn-icon gh-btn-outline gh-product-card-editbutton icon-only"
@title="Tiers Actions"
data-test-button="tiers-actions"
>
<span>
{{svg-jar "dotdotdot"}}
<span class="hidden">Actions</span>
</span>
</GhDropdownButton>
{{#if (eq this.product.type "paid" )}}
<GhDropdown
@name="tiers-actions-menu-{{this.product.name}}"
@tagName="ul"
@classNames="gh-tier-actions-menu dropdown-menu dropdown-triangle-top-right"
>
<li>
<button class="mr2" {{action "openEditProduct" this.product}}>
<span>Edit</span>
</button>
</li>
<li>
<Settings::Members::ArchiveTier @product={{this.product}} />
</li>
</GhDropdown>
{{/if}}
</span>
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="modal-body">
<p>
Reactivating <strong>{{@data.offer.name}}</strong> will immediately allow new members to subscribe using this offer.
Reactivating <strong>{{@data.offer.name}}</strong> will allow new members to subscribe to this tier. Existing members will remain unchanged.
</p>
</div>

View File

@ -0,0 +1,23 @@
<div class="modal-content" {{on-key "Enter" (perform this.archiveTierTask)}}>
<header class="modal-header">
<h1>Archive tier</h1>
</header>
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body">
<p>
Members will no longer be able to subscribe to <strong>{{@data.product.name}}</strong> and it will be removed from the list of available tiers in portal. Existing members on this tier will remain unchanged.
</p>
</div>
<div class="modal-footer">
<button type="button" class="gh-btn" {{on "click" @close}}><span>Cancel</span></button>
<GhTaskButton
@buttonText="Archive"
@successText="Archived"
@task={{this.archiveTierTask}}
@class="gh-btn gh-btn-black gh-btn-icon"
/>
</div>
</div>

View File

@ -0,0 +1,31 @@
import Component from '@glimmer/component';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
export default class ArchiveTierModalComponent extends Component {
@service notifications;
@service router;
get isActive() {
const {product} = this.args.data;
return !!product.active;
}
@task({drop: true})
*archiveTierTask() {
const {product} = this.args.data;
product.active = false;
try {
yield product.save();
return product;
} catch (error) {
if (error) {
this.notifications.showAPIError(error, {key: 'tier.archive.failed'});
}
} finally {
this.args.close();
}
}
}

View File

@ -0,0 +1,22 @@
<div class="modal-content" {{on-key "Enter" (perform this.unarchiveTask)}}>
<header class="modal-header">
<h1>Reactivate tier</h1>
</header>
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body">
<p>
Reactivating <strong>{{@data.product.name}}</strong> will immediately allow new members to subscribe to this.
</p>
</div>
<div class="modal-footer">
<button type="button" class="gh-btn" {{on "click" @close}}><span>Cancel</span></button>
<GhTaskButton
@buttonText="Reactivate"
@task={{this.unarchiveTask}}
@class="gh-btn gh-btn-black gh-btn-icon"
/>
</div>
</div>

View File

@ -0,0 +1,31 @@
import Component from '@glimmer/component';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
export default class UnarchiveTierModalComponent extends Component {
@service notifications;
@service router;
get isActive() {
const {product} = this.args.data;
return !!product.active;
}
@task({drop: true})
*unarchiveTask() {
const {product} = this.args.data;
product.active = true;
try {
yield product.save();
return product;
} catch (error) {
if (error) {
this.notifications.showAPIError(error, {key: 'tier.unarchive.failed'});
}
} finally {
this.args.close();
}
}
}

View File

@ -0,0 +1,17 @@
{{#if this.product.active}}
{{#if (not this.product.isNew)}}
<button
type="button"
{{on "click" this.handleArchiveTier}}
>
<span>Archive</span>
</button>
{{/if}}
{{else}}
<button
type="button"
{{on "click" this.handleUnarchiveTier}}
>
<span>Reactivate</span>
</button>
{{/if}}

View File

@ -0,0 +1,40 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class ArchiveTierComponent extends Component {
@service notifications;
@service router;
@service modals;
get isActive() {
const {product} = this.args;
return !!product.active;
}
get product() {
return this.args.product;
}
@action
handleArchiveTier() {
if (!this.product.isNew) {
this.modals.open('modals/tiers/archive', {
product: this.product
}, {
className: 'fullscreen-modal fullscreen-modal-action fullscreen-modal-wide'
});
}
}
@action
handleUnarchiveTier() {
if (!this.product.isNew) {
this.modals.open('modals/tiers/unarchive', {
product: this.product
}, {
className: 'fullscreen-modal fullscreen-modal-action fullscreen-modal-wide'
});
}
}
}

View File

@ -55,6 +55,7 @@
}
.epm-modal-container {
z-index: 10000;
display: flex;
justify-content: center;
overflow: auto;

View File

@ -11,6 +11,11 @@
}
}
.gh-contentfilter-menu-trigger-tiers {
margin-right: 0 !important;
background: transparent !important;
}
.gh-product-cards {
margin: 0 0 24px;
}
@ -28,10 +33,25 @@
}
}
.gh-product-card-editbutton {
.gh-product-card-editbutton-container {
position: absolute;
right: 24px;
top: 16px;
top: 24px;
margin-right: 0;
}
.gh-product-card-editbutton {
margin-right: 0;
}
.gh-product-card-editbutton.gh-btn span {
height: 24px;
}
.gh-tier-actions-menu {
top: calc(100% + 6px);
left: auto;
right: 0;
}
.gh-product-card-block {