Added one-time payments under "payments" for filtering (#20807)
ref PLG-153 - Scoped one-time payments (`donation_event`) under the "payments" category in the member activity feed filter. - Updated `toggleEventType` logic to ensure that toggling "payments" also toggles one-time payments when the `tipsAndDonations` feature is enabled. - Refactored event type handling into utility functions for easier testing. - Added unit tests for the new utility functions to ensure correct behaviour. - Added acceptance testing.
This commit is contained in:
parent
ad3751bfa6
commit
f2206fb232
@ -1,5 +1,5 @@
|
|||||||
<GhBasicDropdown @verticalPosition="below" as |dd|>
|
<GhBasicDropdown @verticalPosition="below" as |dd|>
|
||||||
<dd.Trigger class="gh-btn gh-btn-icon gh-btn-action-icon">
|
<dd.Trigger class="gh-btn gh-btn-icon gh-btn-action-icon" data-test-id="filter-events-button">
|
||||||
<span class={{if @excludedEvents "gh-btn-label-green"}}>
|
<span class={{if @excludedEvents "gh-btn-label-green"}}>
|
||||||
{{svg-jar "filter"}}
|
{{svg-jar "filter"}}
|
||||||
Filter events
|
Filter events
|
||||||
@ -13,7 +13,7 @@
|
|||||||
{{#if type.divider}}
|
{{#if type.divider}}
|
||||||
<li class="gh-member-activity-actions-menu-divider"></li>
|
<li class="gh-member-activity-actions-menu-divider"></li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<li class="ember-power-select-option mb0 gh-member-activity-actions-menu-item">
|
<li class="ember-power-select-option mb0 gh-member-activity-actions-menu-item">
|
||||||
<label for="type-{{idx}}">
|
<label for="type-{{idx}}">
|
||||||
{{svg-jar type.icon class="gh-member-activity-actions-menu-icon"}}
|
{{svg-jar type.icon class="gh-member-activity-actions-menu-icon"}}
|
||||||
<span>{{type.name}}</span>
|
<span>{{type.name}}</span>
|
||||||
@ -21,6 +21,7 @@
|
|||||||
<div class="for-switch x-small">
|
<div class="for-switch x-small">
|
||||||
<label class="switch" for="type-{{idx}}">
|
<label class="switch" for="type-{{idx}}">
|
||||||
<input
|
<input
|
||||||
|
data-test-id="event-type-filter-checkbox-{{type.event}}"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={{type.isSelected}}
|
checked={{type.isSelected}}
|
||||||
id="type-{{idx}}"
|
id="type-{{idx}}"
|
||||||
|
@ -1,42 +1,14 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import {action} from '@ember/object';
|
import {action} from '@ember/object';
|
||||||
|
import {getAvailableEventTypes, needDivider, toggleEventType} from 'ghost-admin/utils/member-event-types';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
const ALL_EVENT_TYPES = [
|
|
||||||
{event: 'signup_event', icon: 'filter-dropdown-signups', name: 'Signups', group: 'auth'},
|
|
||||||
{event: 'login_event', icon: 'filter-dropdown-logins', name: 'Logins', group: 'auth'},
|
|
||||||
{event: 'subscription_event', icon: 'filter-dropdown-paid-subscriptions', name: 'Paid subscriptions', group: 'payments'},
|
|
||||||
{event: 'payment_event', icon: 'filter-dropdown-payments', name: 'Payments', group: 'payments'},
|
|
||||||
{event: 'newsletter_event', icon: 'filter-dropdown-email-subscriptions', name: 'Email subscriptions', group: 'emails'},
|
|
||||||
{event: 'email_opened_event', icon: 'filter-dropdown-email-opened', name: 'Email opened', group: 'emails'},
|
|
||||||
{event: 'email_delivered_event', icon: 'filter-dropdown-email-received', name: 'Email received', group: 'emails'},
|
|
||||||
{event: 'email_complaint_event', icon: 'filter-dropdown-email-flagged-as-spam', name: 'Email flagged as spam', group: 'emails'},
|
|
||||||
{event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email bounced', group: 'emails'},
|
|
||||||
{event: 'email_change_event', icon: 'filter-dropdown-email-address-changed', name: 'Email address changed', group: 'emails'}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default class MembersActivityEventTypeFilter extends Component {
|
export default class MembersActivityEventTypeFilter extends Component {
|
||||||
@service settings;
|
@service settings;
|
||||||
@service feature;
|
@service feature;
|
||||||
|
|
||||||
getAvailableEventTypes() {
|
getAvailableEventTypes() {
|
||||||
const extended = [...ALL_EVENT_TYPES];
|
return getAvailableEventTypes(this.settings, this.feature, this.args.hiddenEvents);
|
||||||
|
|
||||||
if (this.settings.commentsEnabled !== 'off') {
|
|
||||||
extended.push({event: 'comment_event', icon: 'filter-dropdown-comments', name: 'Comments', group: 'others'});
|
|
||||||
}
|
|
||||||
if (this.feature.audienceFeedback) {
|
|
||||||
extended.push({event: 'feedback_event', icon: 'filter-dropdown-feedback', name: 'Feedback', group: 'others'});
|
|
||||||
}
|
|
||||||
if (this.settings.emailTrackClicks) {
|
|
||||||
extended.push({event: 'click_event', icon: 'filter-dropdown-clicked-in-email', name: 'Clicked link in email', group: 'others'});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.args.hiddenEvents?.length) {
|
|
||||||
return extended.filter(t => !this.args.hiddenEvents.includes(t.event));
|
|
||||||
} else {
|
|
||||||
return extended;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get eventTypes() {
|
get eventTypes() {
|
||||||
@ -47,30 +19,14 @@ export default class MembersActivityEventTypeFilter extends Component {
|
|||||||
event: type.event,
|
event: type.event,
|
||||||
icon: type.icon,
|
icon: type.icon,
|
||||||
name: type.name,
|
name: type.name,
|
||||||
divider: this.needDivider(type, availableEventTypes[i - 1]),
|
divider: needDivider(type, availableEventTypes[i - 1]),
|
||||||
isSelected: !excludedEvents.includes(type.event)
|
isSelected: !excludedEvents.includes(type.event)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
needDivider(event, prevEvent) {
|
|
||||||
if (!event?.group || !prevEvent?.group) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return event.group !== prevEvent.group;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleEventType(eventType) {
|
toggleEventType(eventType) {
|
||||||
const excludedEvents = new Set(this.eventTypes.reject(type => type.isSelected).map(type => type.event));
|
const newExcludedEvents = toggleEventType(eventType, this.eventTypes);
|
||||||
|
this.args.onChange(newExcludedEvents || null);
|
||||||
if (excludedEvents.has(eventType)) {
|
|
||||||
excludedEvents.delete(eventType);
|
|
||||||
} else {
|
|
||||||
excludedEvents.add(eventType);
|
|
||||||
}
|
|
||||||
|
|
||||||
const excludeString = Array.from(excludedEvents).join(',');
|
|
||||||
|
|
||||||
this.args.onChange(excludeString || null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
57
ghost/admin/app/utils/member-event-types.js
Normal file
57
ghost/admin/app/utils/member-event-types.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
export const ALL_EVENT_TYPES = [
|
||||||
|
{event: 'signup_event', icon: 'filter-dropdown-signups', name: 'Signups', group: 'auth'},
|
||||||
|
{event: 'login_event', icon: 'filter-dropdown-logins', name: 'Logins', group: 'auth'},
|
||||||
|
{event: 'subscription_event', icon: 'filter-dropdown-paid-subscriptions', name: 'Paid subscriptions', group: 'payments'},
|
||||||
|
{event: 'payment_event', icon: 'filter-dropdown-payments', name: 'Payments', group: 'payments'},
|
||||||
|
{event: 'newsletter_event', icon: 'filter-dropdown-email-subscriptions', name: 'Email subscriptions', group: 'emails'},
|
||||||
|
{event: 'email_opened_event', icon: 'filter-dropdown-email-opened', name: 'Email opened', group: 'emails'},
|
||||||
|
{event: 'email_delivered_event', icon: 'filter-dropdown-email-received', name: 'Email received', group: 'emails'},
|
||||||
|
{event: 'email_complaint_event', icon: 'filter-dropdown-email-flagged-as-spam', name: 'Email flagged as spam', group: 'emails'},
|
||||||
|
{event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email bounced', group: 'emails'},
|
||||||
|
{event: 'email_change_event', icon: 'filter-dropdown-email-address-changed', name: 'Email address changed', group: 'emails'}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getAvailableEventTypes(settings, feature, hiddenEvents = []) {
|
||||||
|
const extended = [...ALL_EVENT_TYPES];
|
||||||
|
|
||||||
|
if (settings.commentsEnabled !== 'off') {
|
||||||
|
extended.push({event: 'comment_event', icon: 'filter-dropdown-comments', name: 'Comments', group: 'others'});
|
||||||
|
}
|
||||||
|
if (feature.audienceFeedback) {
|
||||||
|
extended.push({event: 'feedback_event', icon: 'filter-dropdown-feedback', name: 'Feedback', group: 'others'});
|
||||||
|
}
|
||||||
|
if (settings.emailTrackClicks) {
|
||||||
|
extended.push({event: 'click_event', icon: 'filter-dropdown-clicked-in-email', name: 'Clicked link in email', group: 'others'});
|
||||||
|
}
|
||||||
|
|
||||||
|
return extended.filter(t => !hiddenEvents.includes(t.event));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleEventType(eventType, eventTypes) {
|
||||||
|
const excludedEvents = new Set(eventTypes.filter(type => !type.isSelected).map(type => type.event));
|
||||||
|
|
||||||
|
if (eventType === 'payment_event') {
|
||||||
|
if (excludedEvents.has('payment_event')) {
|
||||||
|
excludedEvents.delete('payment_event');
|
||||||
|
excludedEvents.delete('donation_event');
|
||||||
|
} else {
|
||||||
|
excludedEvents.add('payment_event');
|
||||||
|
excludedEvents.add('donation_event');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (excludedEvents.has(eventType)) {
|
||||||
|
excludedEvents.delete(eventType);
|
||||||
|
} else {
|
||||||
|
excludedEvents.add(eventType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(excludedEvents).join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needDivider(event, prevEvent) {
|
||||||
|
if (!event?.group || !prevEvent?.group) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return event.group !== prevEvent.group;
|
||||||
|
}
|
@ -7,6 +7,7 @@ const EVENT_TYPES = [
|
|||||||
'login_event',
|
'login_event',
|
||||||
'subscription_event',
|
'subscription_event',
|
||||||
'payment_event',
|
'payment_event',
|
||||||
|
'donation_event',
|
||||||
'login_event',
|
'login_event',
|
||||||
'signup_event',
|
'signup_event',
|
||||||
'email_delivered_event',
|
'email_delivered_event',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import moment from 'moment-timezone';
|
||||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||||
import {currentURL} from '@ember/test-helpers';
|
import {click, currentURL, findAll} from '@ember/test-helpers';
|
||||||
import {describe, it} from 'mocha';
|
import {describe, it} from 'mocha';
|
||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {setupApplicationTest} from 'ember-mocha';
|
import {setupApplicationTest} from 'ember-mocha';
|
||||||
@ -40,4 +41,61 @@ describe('Acceptance: Members activity', function () {
|
|||||||
expect(currentURL()).to.equal('/members-activity');
|
expect(currentURL()).to.equal('/members-activity');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('as owner', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
const role = this.server.create('role', {name: 'Owner'});
|
||||||
|
this.server.create('user', {roles: [role]});
|
||||||
|
|
||||||
|
await authenticateSession();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', async function () {
|
||||||
|
await visit('/members-activity');
|
||||||
|
expect(currentURL()).to.equal('/members-activity');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('members activity filter', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
const role = this.server.create('role', {name: 'Administrator'});
|
||||||
|
await this.server.create('user', {roles: [role]});
|
||||||
|
|
||||||
|
await authenticateSession();
|
||||||
|
|
||||||
|
// this.server.createList('member', 3, {status: 'free'});
|
||||||
|
// this.server.createList('member', 4, {status: 'paid'});
|
||||||
|
// this.server.createList('member-activity-event', 10, {createdAt: moment('2024-08-18 08:18:08').format('YYYY-MM-DD HH:mm:ss')});
|
||||||
|
|
||||||
|
// create 1 member with id 1
|
||||||
|
this.server.create('member', {id: 1, name: 'Member 1', email: '', status: 'free'});
|
||||||
|
|
||||||
|
// create an event for member 1
|
||||||
|
this.server.create('member-activity-event', {memberId: 1, createdAt: moment('2024-08-18 08:18:08').format('YYYY-MM-DD HH:mm:ss'), type: 'payment_event'});
|
||||||
|
this.server.create('member-activity-event', {memberId: 1, createdAt: moment('2024-08-18 08:18:08').format('YYYY-MM-DD HH:mm:ss'), type: 'subscription_event'});
|
||||||
|
this.server.create('member-activity-event', {memberId: 1, createdAt: moment('2024-08-18 08:18:08').format('YYYY-MM-DD HH:mm:ss'), type: 'donation_event'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', async function () {
|
||||||
|
await visit('/members-activity');
|
||||||
|
expect(currentURL()).to.equal('/members-activity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lists all events', async function () {
|
||||||
|
await visit('/members-activity');
|
||||||
|
expect(findAll('.gh-members-activity-event').length).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters events payment and donation events', async function () {
|
||||||
|
await visit('/members-activity?excludedEvents=payment_event%2Cdonation_event');
|
||||||
|
expect(findAll('.gh-members-activity-event').length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes one time (donation) payments under payments filtering', async function () {
|
||||||
|
await visit('/members-activity');
|
||||||
|
await click('[data-test-id="filter-events-button"]');
|
||||||
|
await click('[data-test-id="event-type-filter-checkbox-payment_event"]');
|
||||||
|
expect(findAll('.gh-members-activity-event').length).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
68
ghost/admin/tests/unit/utils/member-event-types-test.js
Normal file
68
ghost/admin/tests/unit/utils/member-event-types-test.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import {ALL_EVENT_TYPES, getAvailableEventTypes, needDivider, toggleEventType} from 'ghost-admin/utils/member-event-types';
|
||||||
|
import {describe, it} from 'mocha';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
|
||||||
|
describe('Unit | Utility | event-type-utils', function () {
|
||||||
|
it('should return available event types with settings and features applied', function () {
|
||||||
|
const settings = {
|
||||||
|
commentsEnabled: 'on',
|
||||||
|
emailTrackClicks: true
|
||||||
|
};
|
||||||
|
const feature = {
|
||||||
|
audienceFeedback: true,
|
||||||
|
tipsAndDonations: true
|
||||||
|
};
|
||||||
|
const hiddenEvents = [];
|
||||||
|
|
||||||
|
const eventTypes = getAvailableEventTypes(settings, feature, hiddenEvents);
|
||||||
|
|
||||||
|
expect(eventTypes).to.deep.include({event: 'comment_event', icon: 'filter-dropdown-comments', name: 'Comments', group: 'others'});
|
||||||
|
expect(eventTypes).to.deep.include({event: 'feedback_event', icon: 'filter-dropdown-feedback', name: 'Feedback', group: 'others'});
|
||||||
|
expect(eventTypes).to.deep.include({event: 'click_event', icon: 'filter-dropdown-clicked-in-email', name: 'Clicked link in email', group: 'others'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle both payment_event and donation_event when toggling payment_event', function () {
|
||||||
|
const eventTypes = [
|
||||||
|
{event: 'payment_event', isSelected: true}
|
||||||
|
];
|
||||||
|
|
||||||
|
const newExcludedEvents = toggleEventType('payment_event', eventTypes);
|
||||||
|
|
||||||
|
expect(newExcludedEvents).to.equal('payment_event,donation_event');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle both payment_event and donation_event off when toggling payment_event off', function () {
|
||||||
|
const eventTypes = [
|
||||||
|
{event: 'payment_event', isSelected: false}
|
||||||
|
];
|
||||||
|
|
||||||
|
const newExcludedEvents = toggleEventType('payment_event', eventTypes);
|
||||||
|
|
||||||
|
expect(newExcludedEvents).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct divider need based on event groups', function () {
|
||||||
|
const event = {group: 'auth'};
|
||||||
|
const prevEvent = {group: 'payments'};
|
||||||
|
|
||||||
|
const result = needDivider(event, prevEvent);
|
||||||
|
|
||||||
|
expect(result).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return only base event types when no settings or features are enabled', function () {
|
||||||
|
const settings = {
|
||||||
|
commentsEnabled: 'off',
|
||||||
|
emailTrackClicks: false
|
||||||
|
};
|
||||||
|
const feature = {
|
||||||
|
audienceFeedback: false,
|
||||||
|
tipsAndDonations: false
|
||||||
|
};
|
||||||
|
const hiddenEvents = [];
|
||||||
|
|
||||||
|
const eventTypes = getAvailableEventTypes(settings, feature, hiddenEvents);
|
||||||
|
|
||||||
|
expect(eventTypes).to.deep.equal(ALL_EVENT_TYPES);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user