Skip to content

Commit

Permalink
feat(notes+follow) - grant activity card (#3470)
Browse files Browse the repository at this point in the history
* add grant activity card

* add tests

* adjust styling

* resolve pr comments

* adjust loading flow

* error handling
  • Loading branch information
greg-adams authored Sep 11, 2024
1 parent a8db1e7 commit 7a82db1
Show file tree
Hide file tree
Showing 9 changed files with 597 additions and 138 deletions.
5 changes: 1 addition & 4 deletions packages/client/src/components/BaseLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,7 @@
</b-col>

<div style="margin-top: 10px">
<section
class="container-fluid"
style="display: flex; justify-content: center;"
>
<section class="container-fluid d-flex flex-column align-items-center">
<AlertBox
v-for="(alert, alertId) in alerts"
v-bind="alert"
Expand Down
108 changes: 108 additions & 0 deletions packages/client/src/components/GrantActivity.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {
describe, it, expect, vi, beforeEach,
} from 'vitest';
import { shallowMount, flushPromises } from '@vue/test-utils';
import { createStore } from 'vuex';
import GrantActivity from '@/components/GrantActivity.vue';

const mockFollowers = {
followers: Array.from(Array(20), (item, i) => ({
id: i,
user: {
id: i + 100,
name: `Follower ${i}`,
},
})),
};

describe('GrantActivity', () => {
const mockStore = {
getters: {
'users/loggedInUser': () => null,
'grants/currentGrant': () => ({
grant_id: 55,
}),
},
actions: {
'grants/getFollowerForGrant': vi.fn(),
'grants/getFollowersForGrant': vi.fn(),
'grants/getNotesForGrant': vi.fn(),
'grants/followGrantForCurrentUser': vi.fn(),
'grants/unfollowGrantForCurrentUser': vi.fn(),
},
};

const store = createStore(mockStore);

beforeEach(() => {
vi.resetAllMocks();
});

const buttonSelector = '[data-follow-btn]';
const followSummarySelector = '[data-follow-summary]';

it('renders', () => {
const wrapper = shallowMount(GrantActivity, {
global: {
plugins: [store],
},
});
expect(wrapper.exists()).toBe(true);
});

it('Correctly displays followers summary', async () => {
mockStore.actions['grants/getFollowersForGrant'].mockResolvedValue(mockFollowers);

const wrapper = shallowMount(GrantActivity, {
global: {
plugins: [store],
},
});

await flushPromises();

const summary = wrapper.find(followSummarySelector);

expect(summary.text()).equal('Followed by Follower 0 and 19 others');
});

it('Correctly follows grants when not followed by user', async () => {
mockStore.actions['grants/getFollowerForGrant'].mockResolvedValue(null);

const wrapper = shallowMount(GrantActivity, {
global: {
plugins: [store],
},
});

await flushPromises();

const btn = wrapper.find(buttonSelector);

expect(btn.text()).equal('Follow');

btn.trigger('click');
expect(mockStore.actions['grants/followGrantForCurrentUser']).toHaveBeenCalled();
});

it('Correctly unfollows grants when followed by user', async () => {
mockStore.actions['grants/getFollowerForGrant'].mockResolvedValue({
id: 1,
});

const wrapper = shallowMount(GrantActivity, {
global: {
plugins: [store],
},
});

await flushPromises();

const btn = wrapper.find(buttonSelector);

expect(btn.text()).equal('Following');

btn.trigger('click');
expect(mockStore.actions['grants/unfollowGrantForCurrentUser']).toHaveBeenCalled();
});
});
169 changes: 169 additions & 0 deletions packages/client/src/components/GrantActivity.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<template>
<b-card
header-bg-variant="white"
footer-bg-variant="white"
>
<template #header>
<h3 class="my-2">
Grant Activity
</h3>
</template>
<div>
<div class="feature-text">
Stay up to date with this grant.
<b-icon
v-b-tooltip
class="ml-2"
title="Follow this grant to receive an email notification when others follow or leave a note."
icon="info-circle-fill"
/>
</div>
<b-button
block
size="lg"
:variant="followBtnVariant"
class="mb-4"
data-follow-btn
:disabled="!followStateLoaded"
@click="toggleFollowState"
>
<span class="h4">
<b-icon
icon="check-circle-fill"
class="mr-2"
/>
</span>
<span class="h5">
{{ followBtnLabel }}
</span>
</b-button>
<div>
<span
v-if="grantHasFollowers"
:class="followSummaryClass"
data-follow-summary
>{{ followSummaryText }}</span>
<span
v-if="showFollowSummary && showNotesSummary"
class="mx-1"
>&bull;</span>
<span v-if="showNotesSummary">{{ notesSummaryText }}</span>
</div>
</div>

<template #footer>
<!-- Feed -->
</template>
</b-card>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';
export default {
components: {},
data() {
return {
userIsFollowing: false,
followStateLoaded: false,
followers: [],
notes: [],
};
},
computed: {
...mapGetters({
loggedInUser: 'users/loggedInUser',
currentGrant: 'grants/currentGrant',
}),
followBtnLabel() {
return this.userIsFollowing ? 'Following' : 'Follow';
},
followBtnVariant() {
return this.userIsFollowing ? 'success' : 'primary';
},
followSummaryClass() {
return this.followStateLoaded ? 'visible' : 'invisible';
},
followSummaryText() {
const userIsFollower = this.userIsFollowing;
const firstFollowerName = userIsFollower ? 'you' : this.followers[0].user.name;
let otherFollowerText = '';
if (this.followers.length > 1) {
const otherFollowersCount = this.followers.length - 1;
if (otherFollowersCount === 1) {
otherFollowerText += ' and 1 other';
} else if (otherFollowersCount < 50) {
otherFollowerText += ` and ${otherFollowersCount} others`;
} else if (otherFollowersCount > 50) {
otherFollowerText += ' and 50+ others';
}
}
return `Followed by ${firstFollowerName}${otherFollowerText}`;
},
grantHasFollowers() {
return this.followers.length > 0;
},
showNotesSummary() {
return this.notes.length > 0;
},
notesSummaryText() {
let textCount = `${this.notes.length}`;
if (this.notes.length > 50) {
textCount = '50+';
}
return `${textCount} notes`;
},
},
async beforeMount() {
this.fetchFollowState();
this.fetchAllNotes();
},
methods: {
...mapActions({
getFollowerForGrant: 'grants/getFollowerForGrant',
getFollowersForGrant: 'grants/getFollowersForGrant',
getNotesForGrant: 'grants/getNotesForGrant',
followGrantForCurrentUser: 'grants/followGrantForCurrentUser',
unfollowGrantForCurrentUser: 'grants/unfollowGrantForCurrentUser',
}),
async fetchFollowState() {
const followCalls = [
this.getFollowerForGrant({ grantId: this.currentGrant.grant_id }),
this.getFollowersForGrant({ grantId: this.currentGrant.grant_id, limit: 51 }),
];
const [userFollowsResult, followersResult] = await Promise.all(followCalls);
this.userIsFollowing = Boolean(userFollowsResult);
this.followers = followersResult ? followersResult.followers : [];
this.followStateLoaded = true;
},
async fetchAllNotes() {
const result = await this.getNotesForGrant({ grantId: this.currentGrant.grant_id, limit: 51 });
if (result) {
this.notes = result.notes;
}
},
async toggleFollowState() {
this.followStateLoaded = false;
if (this.userIsFollowing) {
await this.unfollowGrantForCurrentUser({ grantId: this.currentGrant.grant_id });
} else {
await this.followGrantForCurrentUser({ grantId: this.currentGrant.grant_id });
}
await this.fetchFollowState();
},
},
};
</script>

<style scoped>
.feature-text {
margin-bottom: .8rem;
}
</style>
41 changes: 41 additions & 0 deletions packages/client/src/components/ShareGrant.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
describe, it, expect, vi, beforeEach,
} from 'vitest';
import {
shallowMount,
} from '@vue/test-utils';
import { createStore } from 'vuex';

import ShareGrant from '@/components/ShareGrant.vue';

describe('ShareGrant', () => {
const mockStore = {
getters: {
'agencies/agencies': () => [],
'grants/currentGrant': () => ({
grant_id: 55,
}),
},
actions: {
'grants/getGrantAssignedAgencies': vi.fn(),
'grants/assignAgenciesToGrant': vi.fn(),
'grants/unassignAgenciesToGrant': vi.fn(),
'agencies/fetchAgencies': vi.fn(),
},
};

const store = createStore(mockStore);

beforeEach(() => {
vi.resetAllMocks();
});

it('renders', () => {
const wrapper = shallowMount(ShareGrant, {
global: {
plugins: [store],
},
});
expect(wrapper.exists()).toBe(true);
});
});
Loading

0 comments on commit 7a82db1

Please sign in to comment.