Added initial setup of members activity feed

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

- added `/members-activity` route with associated controller, template, and components behind labs flag
  - table component currently renders some dummy rows
- added navigation item to main menu
  - will use the currently set `?filter` query param unless clicked whilst already on the `/members-activity` screen in which case it will reset the query
- added link to dashboard members activity panel
- added link to member details activity panel
  - sets the filter param to `?filter=member:{member.id}` in preparation for the feed to be filtered to the member's activity
- updated the labs-flag test helper file to export both `enableLabsFlag()` and the new `disableLabsFlag()` so it's easier to test for flag-disabled functionality
This commit is contained in:
Kevin Ansfield 2022-01-17 18:05:10 +00:00
parent 3f33c4177d
commit 62a5757ace
16 changed files with 226 additions and 28 deletions

View File

@ -14,22 +14,26 @@
{{/each}}
{{/liquid-if}}
{{#if (and this.remainingActivities (not this.isShowingAll))}}
<button
type="button"
class="gh-btn gh-member-btn-expandfeed"
data-test-button="view-all-activity"
{{on "click" this.showAll}}
>
<span>View all activity</span>
</button>
{{#if (feature "membersActivityFeed")}}
<LinkTo class="gh-btn gh-member-btn-expandfeed" @route="members-activity" @query={{hash filter=(concat "member:" @member.id)}}><span>View all activity</span></LinkTo>
{{else}}
{{#if (and this.remainingActivities (not this.isShowingAll))}}
<button
type="button"
class="gh-btn gh-member-btn-expandfeed"
data-test-button="view-all-activity"
{{on "click" this.showAll}}
>
<span>View all activity</span>
</button>
{{/if}}
{{/if}}
{{else}}
<div class="gh-members-no-data gh-members-no-list">
<div class="lightgrey">{{svg-jar "no-data-list"}}</div>
<h4>Member activity</h4>
<p>
All events related to this member will be shown here.
All events related to this member will be shown here.
</p>
</div>
{{/if}}

View File

@ -349,7 +349,10 @@
{{/if}}
</div>
<GhMemberActivityFeed @emailRecipients={{this.member.emailRecipients}} />
<GhMemberActivityFeed
@member={{this.member}}
@emailRecipients={{this.member.emailRecipients}}
/>
</div>
</section>

View File

@ -82,6 +82,16 @@
{{/if}}
</li>
{{#if (feature "membersActivityFeed")}}
<li>
{{#if (eq this.router.currentRouteName "members-activity")}}
<LinkTo @route="members-activity" @query={{reset-query-params "members-activity"}} data-test-nav="members-activity">{{svg-jar "members"}} Members activity</LinkTo>
{{else}}
<LinkTo @route="members-activity" data-test-nav="members-activity">{{svg-jar "members"}} Members activity</LinkTo>
{{/if}}
</li>
{{/if}}
{{#if this.isStripeConnected}}
<li>
<LinkTo @route="offers" @alt="Offers">{{svg-jar "percentage"}}Offers</LinkTo>

View File

@ -0,0 +1,5 @@
<tr>
<div class="gh-list-data">{{@activity.member.name}}</div>
<div class="gh-list-data">{{@activity.event}}</div>
<div class="gh-list-data">{{moment-format @activity.timestamp "D MMM YYYY HH:MM"}}</div>
</tr>

View File

@ -0,0 +1,41 @@
<section class="view-container">
{{#if this.activities}}
<div class="gh-list-scrolling">
<table class="gh-list">
<thead>
<tr>
<th>Member</th>
<th>Event</th>
<th>Time</th>
</tr>
</thead>
<VerticalCollection
@tagName="tbody"
@items={{this.activities}}
@key="id"
@containerSelector=".gh-list-scrolling"
@estimateHeight={{69}}
@staticHeight={{true}}
@bufferSize={{20}}
as |activity|
>
<MembersActivity::TableItem @activity={{activity}} />
</VerticalCollection>
</table>
</div>
{{else}}
<div class="no-posts-box">
<div class="no-posts">
{{svg-jar "members-placeholder" class="gh-members-placeholder"}}
{{#if this.showingAll}}
<h3>No member activity yet</h3>
{{else}}
<h3>No activities match the current filter</h3>
<LinkTo @route="members-activity" @query={{hash filter=null}} class="gh-btn gh-btn-lg">
<span>Show all activity</span>
</LinkTo>
{{/if}}
</div>
</div>
{{/if}}
</section>

View File

@ -0,0 +1,17 @@
import Component from '@glimmer/component';
export default class MembersActivityTableComponent extends Component {
activities = (Array.from({length: 50}, () => {
return {
member: {
name: 'Example member'
},
event: 'Opened email',
timestamp: new Date()
};
}))
get showingAll() {
return !this.args.filter;
}
}

View File

@ -0,0 +1,8 @@
import Controller from '@ember/controller';
import {tracked} from '@glimmer/tracking';
export default class MembersActivityController extends Controller {
queryParams = ['filter'];
@tracked filter = null;
}

View File

@ -21,6 +21,9 @@ export const DEFAULT_QUERY_PARAMS = {
search: null,
filter: null,
order: null
},
'members-activity': {
filter: null
}
};

View File

@ -82,6 +82,7 @@ Router.map(function () {
});
this.route('member.new', {path: '/members/new'});
this.route('member', {path: '/members/:member_id'});
this.route('members-activity');
this.route('offers');

View File

@ -0,0 +1,14 @@
import AdminRoute from 'ghost-admin/routes/admin';
import {inject as service} from '@ember/service';
export default class MembersActivityRoute extends AdminRoute {
@service feature;
beforeModel() {
super.beforeModel(...arguments);
if (!this.feature.membersActivityFeed) {
return this.transitionTo('home');
}
}
}

View File

@ -0,0 +1,5 @@
import Service from '@ember/service';
export default class MembersActivityService extends Service {
}

View File

@ -295,6 +295,12 @@
</p>
{{else}}
<GhEventTimeline @events={{this.eventsData}}/>
{{#if (feature "membersActivityFeed")}}
<div class="gh-dashboard-top-members-footer">
<LinkTo @route="members-activity">See all activity {{svg-jar "arrow-right"}}</LinkTo>
</div>
{{/if}}
{{/if}}
{{/if}}
</div>

View File

@ -0,0 +1,9 @@
<section class="gh-canvas">
<GhCanvasHeader class="gh-canvas-header break tablet members-header">
<h2 class="gh-canvas-title" data-test-screen-title>Members Activity</h2>
</GhCanvasHeader>
<MembersActivity::Table @filter={{this.filter}} />
</section>
{{outlet}}

View File

@ -0,0 +1,54 @@
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
import {currentURL} from '@ember/test-helpers';
import {describe, it} from 'mocha';
import {disableLabsFlag, enableLabsFlag} from '../helpers/labs-flag';
import {expect} from 'chai';
import {setupApplicationTest} from 'ember-mocha';
import {setupMirage} from 'ember-cli-mirage/test-support';
import {visit} from '../helpers/visit';
describe('Acceptance: Members activity', function () {
const hooks = setupApplicationTest();
setupMirage(hooks);
beforeEach(function () {
enableLabsFlag(this.server, 'membersActivityFeed');
});
it('redirects when not authenticated', async function () {
await invalidateSession();
await visit('/members-activity');
expect(currentURL()).to.equal('/signin');
});
it('redirects non-admins', async function () {
await invalidateSession();
const role = this.server.create('role', {name: 'Editor'});
this.server.create('user', {roles: [role], slug: 'test-user'});
await authenticateSession();
await visit('/members-activity');
expect(currentURL()).to.equal('/site');
});
describe('as admin', function () {
beforeEach(async function () {
const role = this.server.create('role', {name: 'Administrator'});
this.server.create('user', {roles: [role]});
await authenticateSession();
});
it('renders', async function () {
await visit('/members-activity');
expect(currentURL()).to.equal('/members-activity');
});
it('requires feature flag', async function () {
disableLabsFlag(this.server, 'membersActivityFeed');
await visit('/members-activity');
expect(currentURL()).to.equal('/dashboard');
});
});
});

View File

@ -1,17 +0,0 @@
export default function (server, flag) {
if (!server.schema.configs.all().length) {
server.loadFixtures('configs');
}
if (!server.schema.settings.all().length) {
server.loadFixtures('settings');
}
const config = server.schema.configs.first();
config.update({enableDeveloperExperiments: true});
const labsSetting = {};
labsSetting[flag] = true;
server.db.settings.update({key: 'labs'}, {value: JSON.stringify(labsSetting)});
}

View File

@ -0,0 +1,35 @@
export function enableLabsFlag(server, flag) {
if (!server.schema.configs.all().length) {
server.loadFixtures('configs');
}
if (!server.schema.settings.all().length) {
server.loadFixtures('settings');
}
const config = server.schema.configs.first();
config.update({enableDeveloperExperiments: true});
const labsSetting = {};
labsSetting[flag] = true;
server.db.settings.update({key: 'labs'}, {value: JSON.stringify(labsSetting)});
}
export function disableLabsFlag(server, flag) {
if (!server.schema.configs.all().length) {
server.loadFixtures('configs');
}
if (!server.schema.settings.all().length) {
server.loadFixtures('settings');
}
const config = server.schema.configs.first();
config.update({enableDeveloperExperiments: true});
const labsSetting = {};
labsSetting[flag] = false;
server.db.settings.update({key: 'labs'}, {value: JSON.stringify(labsSetting)});
}