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|>
|
||||
<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}}"
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
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',
|
||||
'subscription_event',
|
||||
'payment_event',
|
||||
'donation_event',
|
||||
'login_event',
|
||||
'signup_event',
|
||||
'email_delivered_event',
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
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