Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added one-time payments under "payments" for filtering #20807

Merged
merged 6 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,14 +13,15 @@
{{#if type.divider}}
<li class="gh-member-activity-actions-menu-divider"></li>
{{/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}}">
{{svg-jar type.icon class="gh-member-activity-actions-menu-icon"}}
<span>{{type.name}}</span>
</label>
<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}}"
Expand Down
54 changes: 5 additions & 49 deletions ghost/admin/app/components/members-activity/event-type-filter.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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 changes: 57 additions & 0 deletions ghost/admin/app/utils/member-event-types.js
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions ghost/admin/mirage/factories/member-activity-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const EVENT_TYPES = [
'login_event',
'subscription_event',
'payment_event',
'donation_event',
'login_event',
'signup_event',
'email_delivered_event',
Expand Down
60 changes: 59 additions & 1 deletion ghost/admin/tests/acceptance/members-activity-test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 changes: 68 additions & 0 deletions ghost/admin/tests/unit/utils/member-event-types-test.js
Original file line number Diff line number Diff line change
@@ -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