From d29e6362a9c74cb1ac341489dc0e6dcda39044c5 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 20 Dec 2024 08:07:00 -0600 Subject: [PATCH 1/4] Allow for anonymous pulse submission and viewing in the pulse report. --- .../services/pulseresponse/PulseResponse.java | 6 +- .../pulseresponse/PulseResponseCreateDTO.java | 2 +- .../PulseResponseServicesImpl.java | 11 ++- .../V119__alter_pulse_response_team_id.sql | 1 + web-ui/package.json | 1 + web-ui/src/pages/PulsePage.css | 6 ++ web-ui/src/pages/PulsePage.jsx | 74 +++++++++++-------- web-ui/src/pages/PulseReportPage.jsx | 27 ++++--- web-ui/yarn.lock | 5 ++ 9 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java index 9c22589300..1b59be5c40 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java @@ -52,7 +52,7 @@ public class PulseResponse { @Column(name="teammemberid") @TypeDef(type=DataType.STRING) - @NotNull + @Nullable @Schema(description = "id of the teamMember this entry is associated with") private UUID teamMemberId; @@ -77,7 +77,7 @@ public class PulseResponse { protected PulseResponse() { } - public PulseResponse(UUID id, Integer internalScore, Integer externalScore, LocalDate submissionDate, UUID teamMemberId, String internalFeelings, String externalFeelings) { + public PulseResponse(UUID id, Integer internalScore, Integer externalScore, LocalDate submissionDate, @Nullable UUID teamMemberId, String internalFeelings, String externalFeelings) { this.id = id; this.internalScore = internalScore; this.externalScore = externalScore; @@ -88,7 +88,7 @@ public PulseResponse(UUID id, Integer internalScore, Integer externalScore, Loca } public PulseResponse(Integer internalScore, Integer externalScore, LocalDate submissionDate, UUID teamMemberId, String internalFeelings, String externalFeelings) { - this(null,internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings); + this(null, internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings); } public UUID getId() { diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java index aecb35e607..a5a289afa8 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java @@ -27,7 +27,7 @@ public class PulseResponseCreateDTO { @Schema(description = "date for submissionDate") private LocalDate submissionDate; - @NotNull + @Nullable @Schema(description = "id of the associated member") private UUID teamMemberId; diff --git a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java index 50004da289..0cd2ee2177 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java @@ -55,11 +55,14 @@ public PulseResponse save(PulseResponse pulseResponse) { LocalDate pulseSubDate = pulseResponse.getSubmissionDate(); if (pulseResponse.getId() != null) { throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId())); - } else if (memberRepo.findById(memberId).isEmpty()) { + } else if (memberId != null && + memberRepo.findById(memberId).isEmpty()) { throw new BadArgException(String.format("Member %s doesn't exists", memberId)); } else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) { throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId)); - } else if (!currentUserId.equals(memberId) && !isSubordinateTo(memberId, currentUserId)) { + } else if (memberId != null && + !currentUserId.equals(memberId) && + !isSubordinateTo(memberId, currentUserId)) { throw new BadArgException(String.format("User %s does not have permission to create pulse response for user %s", currentUserId, memberId)); } pulseResponseRet = pulseResponseRepo.save(pulseResponse); @@ -94,7 +97,7 @@ public PulseResponse update(PulseResponse pulseResponse) { } else if (memberRepo.findById(memberId).isEmpty()) { throw new BadArgException(String.format("Member %s doesn't exist", memberId)); } else if (memberId == null) { - throw new BadArgException(String.format("Invalid pulseresponse %s", pulseResponse)); + throw new BadArgException("Cannot update anonymous pulse response"); } else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) { throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId)); } else if (!currentUserId.equals(memberId) && !isSubordinateTo(memberId, currentUserId)) { @@ -191,4 +194,4 @@ public void sendPulseLowScoreEmail(PulseResponse pulseResponse) { emailSender.sendEmail(null, null, subject, bodyBuilder.toString(), recipients.toArray(new String[0])); } } -} \ No newline at end of file +} diff --git a/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql b/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql new file mode 100644 index 0000000000..746e8657d4 --- /dev/null +++ b/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql @@ -0,0 +1 @@ +ALTER TABLE pulse_response DROP CONSTRAINT pulse_response_teamMemberId_fkey; diff --git a/web-ui/package.json b/web-ui/package.json index 32ee428d5e..49d35345a6 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -28,6 +28,7 @@ "fuse.js": "^6.4.6", "html-react-parser": "^5.1.12", "isomorphic-fetch": "^3.0.0", + "js-cookie": "^3.0.5", "js-file-download": "^0.4.12", "lodash": "^4.17.21", "markdown-builder": "^0.9.0", diff --git a/web-ui/src/pages/PulsePage.css b/web-ui/src/pages/PulsePage.css index c2f22a657c..923924b421 100644 --- a/web-ui/src/pages/PulsePage.css +++ b/web-ui/src/pages/PulsePage.css @@ -12,3 +12,9 @@ text-align: center; } } + +.submit-row { + align-items: center; + display: flex; + margin-top: 2rem; /* This is the default top margin for Buttons */ +} diff --git a/web-ui/src/pages/PulsePage.jsx b/web-ui/src/pages/PulsePage.jsx index 81f821d249..863741c012 100644 --- a/web-ui/src/pages/PulsePage.jsx +++ b/web-ui/src/pages/PulsePage.jsx @@ -1,8 +1,9 @@ +import Cookies from 'js-cookie'; import { format } from 'date-fns'; import React, { useContext, useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { Button, Typography } from '@mui/material'; -import { resolve } from '../api/api.js'; +import { Button, Checkbox, Typography } from '@mui/material'; +import { downloadData, initiate } from '../api/generic.js'; import Pulse from '../components/pulse/Pulse.jsx'; import { AppContext } from '../context/AppContext'; import { selectCsrfToken, selectCurrentUser } from '../context/selectors'; @@ -23,9 +24,19 @@ const PulsePage = () => { const [internalScore, setInternalScore] = useState(center); const [pulse, setPulse] = useState(null); const [submittedToday, setSubmittedToday] = useState(false); + const [submitAnonymously, setSubmitAnonymously] = useState(false); + const today = format(new Date(), 'yyyy-MM-dd'); + const cookieName = "pulse_submitted_anonymously"; + const pulseURL = '/services/pulse-responses'; useEffect(() => { + const submitted = Cookies.get(cookieName); + if (submitted) { + setSubmittedToday(true); + return; + } + if (!pulse) return; const now = new Date(); @@ -52,19 +63,8 @@ const PulsePage = () => { dateTo: today, teamMemberId: currentUser.id }; - const queryString = Object.entries(query) - .map(([key, value]) => `${key}=${value}`) - .join('&'); - - const res = await resolve({ - method: 'GET', - url: `/services/pulse-responses?${queryString}`, - headers: { - 'X-CSRF-Header': csrf, - Accept: 'application/json', - 'Content-Type': 'application/json;charset=UTF-8' - } - }); + + const res = await downloadData(pulseURL, csrf, query); if (res.error) return; // Sort pulse responses by date, latest to earliest @@ -97,22 +97,18 @@ const PulsePage = () => { internalScore: internalScore + 1, // converts to 1-based submissionDate: today, updatedDate: today, - teamMemberId: myId + teamMemberId: submitAnonymously ? null : myId, }; - const res = await resolve({ - method: 'POST', - url: '/services/pulse-responses', - headers: { - 'X-CSRF-Header': csrf, - Accept: 'application/json', - 'Content-Type': 'application/json;charset=UTF-8' - }, - data - }); + const res = await initiate(pulseURL, csrf, data); if (res.error) return; - // Refresh browser to show that pulses where already submitted today. - history.go(0); + if (submitAnonymously) { + setSubmittedToday(true); + Cookies.set(cookieName, 'true', { expires: 1 }); + } else { + // Refresh browser to show that pulses where already submitted today. + history.go(0); + } }; return ( @@ -141,9 +137,25 @@ const PulsePage = () => { setScore={setExternalScore} title="How are you feeling about life outside of work?" /> - +
+ +
+ +
)}
diff --git a/web-ui/src/pages/PulseReportPage.jsx b/web-ui/src/pages/PulseReportPage.jsx index 385db37ddf..4fc4415ca5 100644 --- a/web-ui/src/pages/PulseReportPage.jsx +++ b/web-ui/src/pages/PulseReportPage.jsx @@ -161,7 +161,7 @@ const PulseReportPage = () => { for (const pulse of pulses) { const memberId = pulse.teamMemberId; - if (!teamMemberIds.includes(memberId)) continue; + if (memberId && !teamMemberIds.includes(memberId)) continue; const { externalScore, internalScore, submissionDate } = pulse; const [year, month, day] = submissionDate; @@ -181,18 +181,21 @@ const PulseReportPage = () => { frequencies[internalScore - 1].internal++; frequencies[externalScore - 1].external++; - const member = memberMap[memberId]; - const { supervisorid } = member; - const memberIdToUse = managerMode ? supervisorid : memberId; - - /* For debugging ... - if (supervisorid) { - const supervisor = memberMap[supervisorid]; - console.log(`The supervisor of ${member.name} is ${supervisor.name}`); - } else { - console.log(`${member.name} has no supervisor`); + let memberIdToUse; + if (memberId) { + const member = memberMap[memberId]; + const { supervisorid } = member; + memberIdToUse = managerMode ? supervisorid : memberId; + + /* For debugging ... + if (supervisorid) { + const supervisor = memberMap[supervisorid]; + console.log(`The supervisor of ${member.name} is ${supervisor.name}`); + } else { + console.log(`${member.name} has no supervisor`); + } + */ } - */ // When in manager mode, if the member // doesn't have a supervisor then skip this data. diff --git a/web-ui/yarn.lock b/web-ui/yarn.lock index 51d7dff66c..caac92af81 100644 --- a/web-ui/yarn.lock +++ b/web-ui/yarn.lock @@ -4627,6 +4627,11 @@ jest-fetch-mock@^3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + js-file-download@^0.4.12: version "0.4.12" resolved "https://registry.yarnpkg.com/js-file-download/-/js-file-download-0.4.12.tgz#10c70ef362559a5b23cdbdc3bd6f399c3d91d821" From fb62f8375a6d35bda510d56f754b15972042aa46 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 20 Dec 2024 08:32:58 -0600 Subject: [PATCH 2/4] Fixed tests. --- .../pulseresponse/PulseResponseControllerTest.java | 10 +++++++--- .../pulseresponse/PulseResponseCreateDTOTest.java | 2 +- web-ui/src/pages/PulsePage.jsx | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java index 777f61bac6..912038b5f5 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java @@ -76,8 +76,12 @@ void testCreateAnInvalidPulseResponse() { JsonNode body = responseException.getResponse().getBody(JsonNode.class).orElse(null); JsonNode errors = Objects.requireNonNull(body).get("_embedded").get("errors"); JsonNode href = Objects.requireNonNull(body).get("_links").get("self").get("href"); - List errorList = Stream.of(errors.get(0).get("message").asText(), errors.get(1).get("message").asText(), errors.get(2).get("message").asText()).sorted().collect(Collectors.toList()); - assertEquals(3, errorList.size()); + List errorList = Stream.of( + errors.get(0).get("message").asText(), + errors.get(1).get("message").asText() + ).sorted().collect(Collectors.toList()); + + assertEquals(2, errorList.size()); assertEquals(request.getPath(), href.asText()); assertEquals(HttpStatus.BAD_REQUEST, responseException.getStatus()); } @@ -523,4 +527,4 @@ private MemberProfile profile(String key) { private UUID id(String key) { return profile(key).getId(); } -} \ No newline at end of file +} diff --git a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java index 7b964d776c..761a41a495 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java @@ -35,7 +35,7 @@ void testConstraintViolation() { PulseResponseCreateDTO dto = new PulseResponseCreateDTO(); Set> violations = validator.validate(dto); - assertEquals(3, violations.size()); + assertEquals(2, violations.size()); for (ConstraintViolation violation : violations) { assertEquals("must not be null", violation.getMessage()); } diff --git a/web-ui/src/pages/PulsePage.jsx b/web-ui/src/pages/PulsePage.jsx index 863741c012..7149136570 100644 --- a/web-ui/src/pages/PulsePage.jsx +++ b/web-ui/src/pages/PulsePage.jsx @@ -145,7 +145,7 @@ const PulsePage = () => { Submit
-
- +
- + +
`; From 3dd68a303c3c1fb8f7fbf58bc2aa5d45ca5ad2a1 Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 20 Dec 2024 13:53:26 -0600 Subject: [PATCH 4/4] Removed unnecessary db file and unnecessary refresh. --- .../db/common/V119__alter_pulse_response_team_id.sql | 1 - web-ui/src/pages/PulsePage.jsx | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql diff --git a/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql b/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql deleted file mode 100644 index 746e8657d4..0000000000 --- a/server/src/main/resources/db/common/V119__alter_pulse_response_team_id.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE pulse_response DROP CONSTRAINT pulse_response_teamMemberId_fkey; diff --git a/web-ui/src/pages/PulsePage.jsx b/web-ui/src/pages/PulsePage.jsx index 7149136570..ef6877f019 100644 --- a/web-ui/src/pages/PulsePage.jsx +++ b/web-ui/src/pages/PulsePage.jsx @@ -16,7 +16,6 @@ const PulsePage = () => { const { state } = useContext(AppContext); const currentUser = selectCurrentUser(state); const csrf = selectCsrfToken(state); - const history = useHistory(); const [externalComment, setExternalComment] = useState(''); const [externalScore, setExternalScore] = useState(center); @@ -102,12 +101,9 @@ const PulsePage = () => { const res = await initiate(pulseURL, csrf, data); if (res.error) return; + setSubmittedToday(true); if (submitAnonymously) { - setSubmittedToday(true); Cookies.set(cookieName, 'true', { expires: 1 }); - } else { - // Refresh browser to show that pulses where already submitted today. - history.go(0); } };