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:
Ronald Langeveld 2024-08-22 17:26:46 +07:00 committed by GitHub
parent ad3751bfa6
commit f2206fb232
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 193 additions and 52 deletions

View File

@ -1,5 +1,5 @@
<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"}}>
{{svg-jar "filter"}}
Filter events
@ -21,6 +21,7 @@
<div class="for-switch x-small">
<label class="switch" for="type-{{idx}}">
<input
data-test-id="event-type-filter-checkbox-{{type.event}}"
type="checkbox"
checked={{type.isSelected}}
id="type-{{idx}}"

View File

@ -1,42 +1,14 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {getAvailableEventTypes, needDivider, toggleEventType} from 'ghost-admin/utils/member-event-types';
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 {
@service settings;
@service feature;
getAvailableEventTypes() {
const extended = [...ALL_EVENT_TYPES];
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;
}
return getAvailableEventTypes(this.settings, this.feature, this.args.hiddenEvents);
}
get eventTypes() {
@ -47,30 +19,14 @@ export default class MembersActivityEventTypeFilter extends Component {
event: type.event,
icon: type.icon,
name: type.name,
divider: this.needDivider(type, availableEventTypes[i - 1]),
divider: needDivider(type, availableEventTypes[i - 1]),
isSelected: !excludedEvents.includes(type.event)
}));
}
needDivider(event, prevEvent) {
if (!event?.group || !prevEvent?.group) {
return false;
}
return event.group !== prevEvent.group;
}
@action
toggleEventType(eventType) {
const excludedEvents = new Set(this.eventTypes.reject(type => type.isSelected).map(type => type.event));
if (excludedEvents.has(eventType)) {
excludedEvents.delete(eventType);
} else {
excludedEvents.add(eventType);
}
const excludeString = Array.from(excludedEvents).join(',');
this.args.onChange(excludeString || null);
const newExcludedEvents = toggleEventType(eventType, this.eventTypes);
this.args.onChange(newExcludedEvents || null);
}
}

View 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;
}

View File

@ -7,6 +7,7 @@ const EVENT_TYPES = [
'login_event',
'subscription_event',
'payment_event',
'donation_event',
'login_event',
'signup_event',
'email_delivered_event',

View File

@ -1,5 +1,6 @@
import moment from 'moment-timezone';
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 {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
@ -40,4 +41,61 @@ describe('Acceptance: Members activity', function () {
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);
});
});
});

View 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);
});
});