diff --git a/packages/client/src/components/CopyButton.vue b/packages/client/src/components/CopyButton.vue index c19305973..8dfb5f60b 100644 --- a/packages/client/src/components/CopyButton.vue +++ b/packages/client/src/components/CopyButton.vue @@ -6,6 +6,7 @@ > @@ -32,6 +33,10 @@ export default { type: String, required: true, }, + hideIcon: { + type: Boolean, + default: false, + }, }, data() { return { @@ -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, ); }, diff --git a/packages/client/src/components/GrantActivity.vue b/packages/client/src/components/GrantActivity.vue index 4351e9de1..acab3b4fe 100644 --- a/packages/client/src/components/GrantActivity.vue +++ b/packages/client/src/components/GrantActivity.vue @@ -1,78 +1,94 @@ diff --git a/packages/client/src/components/Modals/GrantFollowers.spec.js b/packages/client/src/components/Modals/GrantFollowers.spec.js new file mode 100644 index 000000000..2fe5bde53 --- /dev/null +++ b/packages/client/src/components/Modals/GrantFollowers.spec.js @@ -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); + }); +}); diff --git a/packages/client/src/components/Modals/GrantFollowers.vue b/packages/client/src/components/Modals/GrantFollowers.vue new file mode 100644 index 000000000..4290e6264 --- /dev/null +++ b/packages/client/src/components/Modals/GrantFollowers.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/packages/client/vitest.setup.ts b/packages/client/vitest.setup.ts index 76764bb95..4bd92562f 100644 --- a/packages/client/vitest.setup.ts +++ b/packages/client/vitest.setup.ts @@ -42,7 +42,14 @@ const stubs = [ 'b-card-img', 'b-card-text', 'b-tabs', + 'b-tooltip', + 'router-link', ]; +const directives = { + 'b-tooltip': true, +}; + config.global.renderStubDefaultSlot = true; config.global.stubs = stubs; +config.global.directives = directives; diff --git a/packages/server/__tests__/api/utils.js b/packages/server/__tests__/api/utils.js index 62102ab4f..e8c63676b 100644 --- a/packages/server/__tests__/api/utils.js +++ b/packages/server/__tests__/api/utils.js @@ -86,7 +86,10 @@ async function makeTestServer(configureAppFn = defaultConfigureApp) { const { port } = server.address(); return fetch(`http://localhost:${port}${url}`, options); }, - fetchApi: (url, agencyId, fetchOptions) => extraProps.fetch(`/api/organizations/${agencyId}${url}`, fetchOptions), + fetchApi: (url, agencyId, fetchOptions, params = {}) => { + const queryParams = Object.keys(params).length ? `?${new URLSearchParams(params).toString()}` : ''; + return extraProps.fetch(`/api/organizations/${agencyId}${url}${queryParams}`, fetchOptions); + }, }; Object.keys(extraProps).forEach((prop) => { diff --git a/packages/server/__tests__/lib/grants-collaboration.test.js b/packages/server/__tests__/lib/grants-collaboration.test.js index 9ecff66ed..8bb1d1d1c 100644 --- a/packages/server/__tests__/lib/grants-collaboration.test.js +++ b/packages/server/__tests__/lib/grants-collaboration.test.js @@ -210,7 +210,7 @@ describe('Grants Collaboration', () => { expect(result.followers).to.have.lengthOf(1); expect(result.followers[0].id).to.equal(follower1.id); - expect(result.pagination.from).to.equal(follower1.id); + expect(result.pagination.next).to.equal(null); }); it('retrieves followers for a grant with LIMIT', async () => { @@ -219,6 +219,7 @@ describe('Grants Collaboration', () => { }); expect(result.followers).to.have.lengthOf(1); + expect(result.pagination.next).to.equal(follower2.id); }); }); }); diff --git a/packages/server/src/lib/grantsCollaboration/followers.js b/packages/server/src/lib/grantsCollaboration/followers.js index 2491dc9f7..60ed4a95f 100644 --- a/packages/server/src/lib/grantsCollaboration/followers.js +++ b/packages/server/src/lib/grantsCollaboration/followers.js @@ -44,7 +44,9 @@ async function getFollowerForGrant(knex, grantId, userId) { }; } -async function getFollowersForGrant(knex, grantId, organizationId, { beforeFollow, limit = 50 } = {}) { +async function getFollowersForGrant(knex, grantId, organizationId, { + beforeFollow, limit = 50, +} = {}) { const query = knex('grant_followers') .select( 'grant_followers.id', @@ -64,37 +66,44 @@ async function getFollowersForGrant(knex, grantId, organizationId, { beforeFollo .where('grant_followers.grant_id', grantId) .andWhere('tenants.id', organizationId) .orderBy('created_at', 'desc') - .limit(limit); + .limit(limit + 1); if (beforeFollow) { query.andWhere('grant_followers.id', '<', beforeFollow); } - const grantFollowers = await query; + const grantFollowersResult = await query; + const hasMore = grantFollowersResult.length > limit; + + // remove forward looking extra + const grantFollowersReturn = hasMore + ? grantFollowersResult.slice(0, -1) + : grantFollowersResult; return { - followers: grantFollowers.map((grantFollower) => ({ - id: grantFollower.id, - createdAt: grantFollower.created_at, - grant: { - id: grantFollower.grant_id, - }, - user: { - id: grantFollower.user_id, - name: grantFollower.user_name, - email: grantFollower.user_email, - team: { - id: grantFollower.team_id, - name: grantFollower.team_name, + followers: grantFollowersReturn + .map((grantFollower) => ({ + id: grantFollower.id, + createdAt: grantFollower.created_at, + grant: { + id: grantFollower.grant_id, }, - organization: { - id: grantFollower.organization_id, - name: grantFollower.organizationName, + user: { + id: grantFollower.user_id, + name: grantFollower.user_name, + email: grantFollower.user_email, + team: { + id: grantFollower.team_id, + name: grantFollower.team_name, + }, + organization: { + id: grantFollower.organization_id, + name: grantFollower.organizationName, + }, }, - }, - })), + })), pagination: { - from: grantFollowers.length > 0 ? grantFollowers[grantFollowers.length - 1].id : beforeFollow, + next: hasMore ? grantFollowersReturn[grantFollowersReturn.length - 1].id : null, }, }; }