diff --git a/config/dev-custom-host.js b/config/dev-custom-host.js new file mode 100644 index 0000000000..1ce6977f63 --- /dev/null +++ b/config/dev-custom-host.js @@ -0,0 +1,37 @@ +const { merge } = require('webpack-merge'); +var base = require('./index.js') +var devVm = require('./dev-vm.js') + +module.exports = merge(base, devVm, { + app: { + apolloBatching: false, + host: 'kiva-ui.local:8888', + publicPath: '/', + photoPath: 'https://www.development.kiva.org/img/', + graphqlUri: 'https://gateway.development.kiva.org/graphql', + enableAnalytics: false, + enableSnowplow: false, + snowplowUri: 'events.fivetran.com/snowplow/v5qt54ocr2nm', + enableGA: false, + gaId: 'UA-11686022-7', // dev-vm property + enableSentry: false, + sentryURI: 'https://7ce141b23c4a4e6091c206d08442f0e9@o7540.ingest.sentry.io/1201287', + auth0: { + loginRedirectUrls: { + xOXldYg02WsLnlnn0D5xoPWI2i3aNsFD: 'https://www.development.kiva.org/authenticate?authLevel=recent', + KIzjUBQjKZwMRgYSn6NvMxsUwNppwnLH: 'http://kiva-ui.local:8888/ui-login?force=true', + ouGKxT4mE4wQEKqpfsHSE96c9rHXQqZF: 'http://kiva-ui.local:8888/ui-login?force=true', + }, + enable: true, + browserCallbackUri: 'http://kiva-ui.local:8888/process-browser-auth', + serverCallbackUri: 'http://kiva-ui.local:8888/process-ssr-auth', + apiAudience: 'https://gateway.development.kiva.org/graphql', + }, + }, + server: { + graphqlUri: 'https://gateway.development.kiva.org/graphql', + sessionUri: 'https://www.development.kiva.org/start-ui-session', + memcachedEnabled: false, + disableCluster: true, + } +}) diff --git a/package.json b/package.json index c6e5796bee..e41a3d9d52 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "fetchSchema": "node build/fetch-schema.js && wait-on build/schema.graphql", "report": "webpack-bundle-analyzer dist/client-bundle-stats.json", "unit": "jest --config test/unit/jest.conf.js --coverage", + "unit-watch": "jest --config test/unit/jest.conf.js --watch", "test": "npm run lint && npm run unit && npm run build && npm run test:e2e", "test:e2e": "npm start -- --config=dev-vm-mac && wait-on tcp:8888 && npm run e2e:headless", "coverage:merge": "node test/mergeCoverage.js", diff --git a/src/components/Checkout/BasketItem.vue b/src/components/Checkout/BasketItem.vue index 2cc9d443d7..e02b724773 100644 --- a/src/components/Checkout/BasketItem.vue +++ b/src/components/Checkout/BasketItem.vue @@ -122,8 +122,7 @@ import LoanReservation from '@/components/Checkout/LoanReservation'; import LoanPrice from '@/components/Checkout/LoanPrice'; import RemoveBasketItem from '@/components/Checkout/RemoveBasketItem'; import TeamAttribution from '@/components/Checkout/TeamAttribution'; - -const teamChallengeCookieName = 'kv-team-challenge'; +import { getForcedTeamId, removeLoansFromChallengeCookie } from '@/util/teamChallengeUtils'; export default { name: 'BasketItem', @@ -197,6 +196,7 @@ export default { this.$emit('refreshtotals', $event); if ($event === 'removeLoan') { this.loanVisible = false; + removeLoansFromChallengeCookie(this.cookieStore, [this.loan.id]); } }, }, @@ -207,32 +207,7 @@ export default { } }, mounted() { - // Team Challenge MVP Code - // If team challenge cookie is present, the user has added a loan to basket from the challenge page - // In that case, append the team info to the list of teams and attribute this loan to that team - if (this.cookieStore.get(teamChallengeCookieName)) { - const teamChallengeLoanData = JSON.parse(this.cookieStore.get(teamChallengeCookieName)); - teamChallengeLoanData.forEach(loan => { - if (loan.loanId === this.loan.id) { - // Loan has a different team attribution, we should override the default - // Is team not in the users list, append it - if (!this.combinedTeams.some(team => team.id === loan.teamId)) { - this.appendedTeams.push({ - id: loan.teamId, - name: loan.teamName - }); - } - this.forceTeamId = loan.teamId; - } - }); - // Remove this loan from the cookie object after we've used it - teamChallengeLoanData.splice( - teamChallengeLoanData.findIndex(loan => loan.loanId === this.loan.id), - 1 - ); - // overwrite the cookie with the new data - this.cookieStore.set(teamChallengeCookieName, JSON.stringify(teamChallengeLoanData)); - } + this.forceTeamId = getForcedTeamId(this.cookieStore, this.loan.id, this.combinedTeams, this.appendedTeams); } }; diff --git a/src/components/Teams/TeamGoal.vue b/src/components/Teams/TeamGoal.vue index 5e9d367b2b..697195a566 100644 --- a/src/components/Teams/TeamGoal.vue +++ b/src/components/Teams/TeamGoal.vue @@ -37,12 +37,39 @@
- {{ membersParticipating }} members participating +
+
+ Lender photo +
+ + {{ participationTotalCount }} members participating + +
View @@ -55,6 +82,7 @@ import TeamInfoFromId from '@/graphql/query/teamInfoFromId.graphql'; import teamNoImage from '@/assets/images/team_s135.png'; import teamGoalInfo from '@/plugins/team-goal-mixin'; +import { isLegacyPlaceholderAvatar } from '@/util/imageUtils'; import KvProgressBar from '~/@kiva/kv-components/vue/KvProgressBar'; import KvButton from '~/@kiva/kv-components/vue/KvButton'; import KvLoadingPlaceholder from '~/@kiva/kv-components/vue/KvLoadingPlaceholder'; @@ -84,9 +112,15 @@ export default { teamImage() { return this.teamImageUrl || this.teamNoImage; }, - membersParticipating() { + participationTotalCount() { return this.goal?.participation?.totalCount ?? 0; }, + participationLendersDisplayed() { + return (this.goal?.participation?.values ?? []).map(p => ({ + ...p.lender, + isLegacyPlaceholder: isLegacyPlaceholderAvatar(p.lender.image.url.split('/').pop()), + })).slice(0, 4); + }, }, methods: { fetchTeamData() { diff --git a/src/graphql/query/teamsGoals.graphql b/src/graphql/query/teamsGoals.graphql index 5a4fc84af0..62238aea4c 100644 --- a/src/graphql/query/teamsGoals.graphql +++ b/src/graphql/query/teamsGoals.graphql @@ -10,6 +10,9 @@ query GetGoals ($teamId: Int, $limit: Int) { values { lender { id + image { + url + } } } } diff --git a/src/pages/Checkout/CheckoutPage.vue b/src/pages/Checkout/CheckoutPage.vue index d2f7c82149..2db6e6ddda 100644 --- a/src/pages/Checkout/CheckoutPage.vue +++ b/src/pages/Checkout/CheckoutPage.vue @@ -324,6 +324,7 @@ import experimentAssignmentQuery from '@/graphql/query/experimentAssignment.grap import fiveDollarsTest, { FIVE_DOLLARS_NOTES_EXP } from '@/plugins/five-dollars-test-mixin'; import FtdsMessage from '@/components/Checkout/FtdsMessage'; import FtdsDisclaimer from '@/components/Checkout/FtdsDisclaimer'; +import { removeLoansFromChallengeCookie } from '@/util/teamChallengeUtils'; import KvLoadingPlaceholder from '~/@kiva/kv-components/vue/KvLoadingPlaceholder'; import KvPageContainer from '~/@kiva/kv-components/vue/KvPageContainer'; import KvButton from '~/@kiva/kv-components/vue/KvButton'; @@ -877,6 +878,8 @@ export default { 800 ); }); + + removeLoansFromChallengeCookie(this.cookieStore, this.loanIdsInBasket); }, setUpdatingTotals(state) { this.updatingTotals = state; diff --git a/src/util/imageUtils.js b/src/util/imageUtils.js index ee989a5004..61564d3fc7 100644 --- a/src/util/imageUtils.js +++ b/src/util/imageUtils.js @@ -78,6 +78,7 @@ export function getKivaImageUrl({ * The legacy avatars are found exclusively at the following urls: * /img//726677.jpg * /img//315726.jpg + * for images from Fastly, urls, like: /img/s100/4d844ac2c0b77a8a522741b908ea5c32.jpg * @param {String|Number} filename or hash of avatar image * @returns {Boolean} full url for the image */ @@ -92,6 +93,6 @@ export function isLegacyPlaceholderAvatar(filename) { if (filenameCleaned.indexOf('.') > -1) { [filenameCleaned] = filenameCleaned.split('.'); } - const defaultProfileIds = ['726677', '315726']; + const defaultProfileIds = ['726677', '315726', '4d844ac2c0b77a8a522741b908ea5c32']; return defaultProfileIds.some(id => id === filenameCleaned); } diff --git a/src/util/teamChallengeUtils.js b/src/util/teamChallengeUtils.js new file mode 100644 index 0000000000..d1ce8a1857 --- /dev/null +++ b/src/util/teamChallengeUtils.js @@ -0,0 +1,58 @@ +export const TEAM_CHALLENGE_COOKIE_NAME = 'kv-team-challenge'; + +const getChallengeCookieData = cookieStore => { + const cookie = cookieStore.get(TEAM_CHALLENGE_COOKIE_NAME); + if (cookie) { + return JSON.parse(cookie); + } +}; + +/** + * If team challenge cookie is present, the user has added a loan to basket from the challenge page. + * In that case, append the team info to the list of teams and attribute this loan to that team. + * + * @param cookieStore The object for affecting cookies + * @param loanId The ID of the loan to check + * @param combinedTeams The combined team list for the loan + * @param appendedTeams The extra teams added to the loan + * @returns The team ID possibly forced for the loan + */ +export const getForcedTeamId = (cookieStore, loanId, combinedTeams, appendedTeams) => { + const data = getChallengeCookieData(cookieStore); + if (Array.isArray(data)) { + let forcedTeamId; + data.forEach(loan => { + if (loan.loanId === loanId) { + // Loan has a different team attribution, we should override the default + // Is team not in the users list, append it + if (!combinedTeams.some(team => team.id === loan.teamId)) { + appendedTeams.push({ + id: loan.teamId, + name: loan.teamName + }); + forcedTeamId = loan.teamId; + } + } + }); + return forcedTeamId; + } +}; + +/** + * Removes loans from the challenge cookie. + * Used after checkout. + * + * @param cookieStore The object for affecting cookies + * @param loanIds The IDs of the loans to check + */ +export const removeLoansFromChallengeCookie = (cookieStore, loanIds) => { + const data = getChallengeCookieData(cookieStore); + if (Array.isArray(data)) { + loanIds.forEach(loanId => { + // Remove this loan from the cookie object after checkout + data.splice(data.findIndex(loan => loan.loanId === loanId), 1); + }); + // Overwrite the cookie with the new data + cookieStore.set(TEAM_CHALLENGE_COOKIE_NAME, JSON.stringify(data)); + } +}; diff --git a/test/unit/specs/util/imageUtils.spec.js b/test/unit/specs/util/imageUtils.spec.js index 2f7322df9b..db2c7d8e6c 100644 --- a/test/unit/specs/util/imageUtils.spec.js +++ b/test/unit/specs/util/imageUtils.spec.js @@ -64,6 +64,7 @@ describe('imageUtils.js', () => { expect(isLegacyPlaceholderAvatar('123abc')).toBe(false); expect(isLegacyPlaceholderAvatar(12344)).toBe(false); }); + it('Returns if legacy placeholder avatar', () => { expect(isLegacyPlaceholderAvatar('726677.jpg')).toBe(true); expect(isLegacyPlaceholderAvatar('315726.jpg')).toBe(true); @@ -73,6 +74,8 @@ describe('imageUtils.js', () => { expect(isLegacyPlaceholderAvatar('315726')).toBe(true); expect(isLegacyPlaceholderAvatar(726677)).toBe(true); expect(isLegacyPlaceholderAvatar(315726)).toBe(true); + expect(isLegacyPlaceholderAvatar('4d844ac2c0b77a8a522741b908ea5c32')).toBe(true); + expect(isLegacyPlaceholderAvatar('4d844ac2c0b77a8a522741b908ea5c32.jpg')).toBe(true); }); }); }); diff --git a/test/unit/specs/util/teamChallengeUtils.spec.js b/test/unit/specs/util/teamChallengeUtils.spec.js new file mode 100644 index 0000000000..315289f005 --- /dev/null +++ b/test/unit/specs/util/teamChallengeUtils.spec.js @@ -0,0 +1,129 @@ +import { + getForcedTeamId, + removeLoansFromChallengeCookie, + TEAM_CHALLENGE_COOKIE_NAME, +} from '@/util/teamChallengeUtils'; + +describe('teamChallengeUtils.js', () => { + const mockLoans = [ + { loanId: 123, teamId: 456, teamName: 'Team Name 1' }, + { loanId: 234, teamId: 345, teamName: 'Team Name 2' }, + ]; + const mockCookieJson = JSON.stringify(mockLoans); + + describe('getForcedTeamId', () => { + it('should use expected cookie name', () => { + const cookieStore = { get: jest.fn() }; + const result = getForcedTeamId(cookieStore, 1, [], []); + expect(result).toEqual(undefined); + expect(cookieStore.get).toBeCalledTimes(1); + expect(cookieStore.get).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME); + }); + + it('should handle empty cookie', () => { + const cookieStore = { get: jest.fn() }; + const result = getForcedTeamId(cookieStore, 1, [], []); + expect(result).toEqual(undefined); + expect(cookieStore.get).toBeCalledTimes(1); + }); + + it('should handle bad cookie value', () => { + const cookieStore = { get: jest.fn().mockReturnValue(JSON.stringify({})), set: jest.fn() }; + const result = getForcedTeamId(cookieStore, [mockLoans[0].loanId]); + expect(result).toEqual(undefined); + expect(cookieStore.get).toBeCalledTimes(1); + }); + + it('should handle non-matching loan ID', () => { + const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() }; + const result = getForcedTeamId(cookieStore, -1, [], []); + expect(result).toEqual(undefined); + expect(cookieStore.get).toBeCalledTimes(1); + }); + + it('should append when team arrays are empty', () => { + const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() }; + const appendedTeams = []; + const result = getForcedTeamId(cookieStore, mockLoans[0].loanId, [], appendedTeams); + expect(result).toEqual(mockLoans[0].teamId); + expect(cookieStore.get).toBeCalledTimes(1); + expect(appendedTeams).toEqual([{ id: mockLoans[0].teamId, name: mockLoans[0].teamName }]); + }); + + it('should skip when team ID already in combined', () => { + const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() }; + const appendedTeams = []; + const result = getForcedTeamId( + cookieStore, + mockLoans[0].loanId, + [{ id: mockLoans[0].teamId }], + appendedTeams + ); + expect(result).toEqual(undefined); + expect(cookieStore.get).toBeCalledTimes(1); + expect(appendedTeams.length).toBe(0); + }); + + it('should append when non-matching team ID', () => { + const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() }; + const appendedTeams = []; + const result = getForcedTeamId(cookieStore, mockLoans[0].loanId, [{ id: -1 }], appendedTeams); + expect(result).toEqual(mockLoans[0].teamId); + expect(cookieStore.get).toBeCalledTimes(1); + expect(appendedTeams[0]).toEqual({ id: mockLoans[0].teamId, name: mockLoans[0].teamName }); + }); + }); + + describe('removeLoansFromChallengeCookie', () => { + it('should use expected cookie name', () => { + const cookieStore = { get: jest.fn() }; + removeLoansFromChallengeCookie(cookieStore, 1); + expect(cookieStore.get).toBeCalledTimes(1); + expect(cookieStore.get).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME); + }); + + it('should handle empty cookie', () => { + const cookieStore = { get: jest.fn(), set: jest.fn() }; + removeLoansFromChallengeCookie(cookieStore, []); + expect(cookieStore.set).toBeCalledTimes(0); + }); + + it('should handle bad cookie value', () => { + const cookieStore = { get: jest.fn().mockReturnValue(JSON.stringify({})), set: jest.fn() }; + removeLoansFromChallengeCookie(cookieStore, [mockLoans[0].loanId]); + expect(cookieStore.set).toBeCalledTimes(0); + }); + + it('should handle bad array', () => { + const cookieStore = { get: jest.fn(), set: jest.fn() }; + removeLoansFromChallengeCookie(cookieStore, 1); + removeLoansFromChallengeCookie(cookieStore, {}); + removeLoansFromChallengeCookie(cookieStore, undefined); + expect(cookieStore.set).toBeCalledTimes(0); + }); + + it('should handle empty loan IDs', () => { + const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() }; + removeLoansFromChallengeCookie(cookieStore, []); + expect(cookieStore.set).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME, mockCookieJson); + }); + + it('should handle empty array in cookie', () => { + const cookieStore = { get: jest.fn().mockReturnValue(JSON.stringify([])), set: jest.fn() }; + removeLoansFromChallengeCookie(cookieStore, []); + expect(cookieStore.set).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME, JSON.stringify([])); + }); + + it('should remove loan from cookie', () => { + const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() }; + removeLoansFromChallengeCookie(cookieStore, [mockLoans[0].loanId]); + expect(cookieStore.set).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME, JSON.stringify([mockLoans[1]])); + }); + + it('should remove loans from cookie', () => { + const cookieStore = { get: jest.fn().mockReturnValue(mockCookieJson), set: jest.fn() }; + removeLoansFromChallengeCookie(cookieStore, [mockLoans[0].loanId, mockLoans[1].loanId]); + expect(cookieStore.set).toBeCalledWith(TEAM_CHALLENGE_COOKIE_NAME, JSON.stringify([])); + }); + }); +});