Skip to content

Commit

Permalink
Feature: Grant followers modal (#3505)
Browse files Browse the repository at this point in the history
* replace computed flag

* add followers modal

* add/update tests

* merge testing setup

* adjustments

* update date display

* fix test

* include current user in results

* update event type

* use luxon

* replace order by pagination
  • Loading branch information
greg-adams authored Sep 19, 2024
1 parent 9488946 commit 647dacf
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 79 deletions.
9 changes: 8 additions & 1 deletion packages/client/src/components/CopyButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
>
<slot />
<b-icon
v-if="!hideIcon"
:icon="copySuccessTimeout === null ? 'files' : 'check2'"
:variant="copySuccessTimeout === null ? '' : 'success'"
/>
Expand All @@ -32,6 +33,10 @@ export default {
type: String,
required: true,
},
hideIcon: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand All @@ -46,7 +51,9 @@ export default {
// (Clear previous timeout to ensure multiple clicks in quick succession don't cause issues)
clearTimeout(this.copySuccessTimeout);
this.copySuccessTimeout = setTimeout(
() => { this.copySuccessTimeout = null; },
() => {
this.copySuccessTimeout = null;
},
1000,
);
},
Expand Down
128 changes: 74 additions & 54 deletions packages/client/src/components/GrantActivity.vue
Original file line number Diff line number Diff line change
@@ -1,78 +1,94 @@
<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">
<div>
<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
icon="check-circle-fill"
class="mr-2"
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"
/>
</span>
<span class="h5">
{{ followBtnLabel }}
</span>
</b-button>
<div>
<span
v-if="grantHasFollowers"
:class="followSummaryClass"
data-follow-summary
>{{ followSummaryText }}</span>
<span
v-if="grantHasFollowers && showNotesSummary"
class="mx-1"
>&bull;</span>
<span v-if="showNotesSummary">{{ notesSummaryText }}</span>
</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>
<b-link
v-if="grantHasFollowers"
:class="followSummaryClass"
data-follow-summary
@click="$bvModal.show('grant-followers-modal')"
>
{{ followSummaryText }}
</b-link>

<span
v-if="grantHasFollowers && showNotesSummary"
class="mx-1"
>&bull;</span>
<span v-if="showNotesSummary">{{ notesSummaryText }}</span>
</div>
</div>
</div>

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

<!-- Modals -->
<GrantFollowersModal
:key="grantFollowersModalKey"
modal-id="grant-followers-modal"
@close="handleModalClose"
/>
</div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex';
import GrantFollowersModal from '@/components/Modals/GrantFollowers.vue';
export default {
components: {},
components: {
GrantFollowersModal,
},
data() {
return {
userIsFollowing: false,
followStateLoaded: false,
followers: [],
notes: [],
grantFollowersModalKey: 0,
};
},
computed: {
...mapGetters({
loggedInUser: 'users/loggedInUser',
currentGrant: 'grants/currentGrant',
}),
followBtnLabel() {
Expand Down Expand Up @@ -158,6 +174,10 @@ export default {
}
await this.fetchFollowState();
},
handleModalClose() {
// Reset modal
this.grantFollowersModalKey += 1;
},
},
};
</script>
Expand Down
90 changes: 90 additions & 0 deletions packages/client/src/components/Modals/GrantFollowers.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
describe, beforeEach, it, expect, vi,
} from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createStore } from 'vuex';
import GrantFollowers from '@/components/Modals/GrantFollowers.vue';

const mockStore = {
getters: {
'grants/currentGrant': () => ({
grant_id: 55,
}),
},
actions: {
'grants/getFollowersForGrant': vi.fn(),
},
};

const store = createStore(mockStore);

const getMockFollowers = (count, hasMoreCursor = null) => ({
followers: Array.from(Array(count), () => ({
id: Math.random(),
user: {
id: Math.random(),
name: 'Follower',
email: 'name@email',
team: {
name: 'Team',
},
},
})),
pagination: {
next: hasMoreCursor,
},
});

let wrapper;

beforeEach(() => {
wrapper = mount(GrantFollowers, {
global: {
plugins: [store],
},
props: {
modalId: 'followers-modal',
},
});
});

describe('GrantFollowers.vue', () => {
it('should fetch the followers when loaded', async () => {
mockStore.actions['grants/getFollowersForGrant'].mockResolvedValue(getMockFollowers(20));

const modal = wrapper.findComponent({ name: 'b-modal' });
modal.trigger('show');

await flushPromises();

const followers = wrapper.findAll('[data-test-follower]');

expect(followers).toHaveLength(20);
});

it('should load re-fetch followers when user clicks Show More', async () => {
// Mock first batch of records
mockStore.actions['grants/getFollowersForGrant'].mockResolvedValue(getMockFollowers(20, 'id-x'));

const modal = wrapper.findComponent({ name: 'b-modal' });
modal.trigger('show');

await flushPromises();
expect(mockStore.actions['grants/getFollowersForGrant'].mock.lastCall[1].paginateFrom).toBeUndefined();

const showMoreBtn = wrapper.findComponent('[data-test-show-more-btn]');

// Mock second batch of records
mockStore.actions['grants/getFollowersForGrant'].mockResolvedValue(getMockFollowers(20));

showMoreBtn.trigger('click');

await flushPromises();
expect(mockStore.actions['grants/getFollowersForGrant'].mock.lastCall[1].paginateFrom).to.equal('id-x');

const followers = wrapper.findAll('[data-test-follower]');

expect(followers).toHaveLength(40);
expect(wrapper.findComponent('[data-test-show-more-btn]').exists()).to.equal(false);
});
});
Loading

0 comments on commit 647dacf

Please sign in to comment.