From c44cbc5a6b68e3367cc65ec2a423f494aed0e708 Mon Sep 17 00:00:00 2001 From: Michal Klimek Date: Wed, 5 Feb 2025 10:49:15 -0600 Subject: [PATCH] fix(TAA-136): addresses type error, wires up now returning decision notes --- assets/approval-requests-bundle.js | 42 ++++++++++++++----- .../ApprovalRequestListPage.tsx | 2 + .../ApprovalRequestListFilters.tsx | 19 ++++++++- .../ApprovalRequestDetails.tsx | 26 +++++++++++- .../ApprovalTicketDetails.tsx | 9 +++- .../approval-request/ApproverActions.tsx | 29 +++++++++---- .../hooks/useSearchApprovalRequests.tsx | 2 + src/modules/approval-requests/types.ts | 4 +- 8 files changed, 107 insertions(+), 26 deletions(-) diff --git a/assets/approval-requests-bundle.js b/assets/approval-requests-bundle.js index ee26b3936..3ff2a1ecd 100644 --- a/assets/approval-requests-bundle.js +++ b/assets/approval-requests-bundle.js @@ -45,6 +45,7 @@ function useSearchApprovalRequests() { return { approvalRequests, errorFetchingApprovalRequests: error, + approvalRequestStatus, setApprovalRequestStatus, isLoading: isLoading, }; @@ -70,7 +71,14 @@ const StyledMediaInput = styled(MediaInput) ` const DropdownFilterField = styled(Field) ` flex: 1; `; -function ApprovalRequestListFilters({ setApprovalRequestStatus, setSearchTerm, }) { +const ApprovalRequestStatusInputMap = { + any: "Any", + active: "Decision pending", + approved: "Approved", + rejected: "Denied", + withdrawn: "Withdrawn", +}; +function ApprovalRequestListFilters({ approvalRequestStatus, setApprovalRequestStatus, setSearchTerm, }) { const handleChange = reactExports.useCallback((changes) => { if (!changes.selectionValue) { return; @@ -82,7 +90,7 @@ function ApprovalRequestListFilters({ setApprovalRequestStatus, setSearchTerm, } const handleSearch = reactExports.useCallback((event) => { debouncedSetSearchTerm(event.target.value); }, [debouncedSetSearchTerm]); - return (jsxRuntimeExports.jsxs(FiltersContainer, { children: [jsxRuntimeExports.jsxs(SearchField, { children: [jsxRuntimeExports.jsx(Label, { hidden: true, children: "Search approval requests" }), jsxRuntimeExports.jsx(StyledMediaInput, { start: jsxRuntimeExports.jsx(SvgSearchStroke, {}), placeholder: "Search approval requests", onChange: handleSearch })] }), jsxRuntimeExports.jsxs(DropdownFilterField, { children: [jsxRuntimeExports.jsx(Label, { children: "Status:" }), jsxRuntimeExports.jsxs(Combobox, { isEditable: false, onChange: handleChange, children: [jsxRuntimeExports.jsx(Option, { value: "any", isSelected: true, label: "Any" }), jsxRuntimeExports.jsx(Option, { value: "active", label: "Decision pending" }), jsxRuntimeExports.jsx(Option, { value: "approved", label: "Approved" }), jsxRuntimeExports.jsx(Option, { value: "rejected", label: "Denied" }), jsxRuntimeExports.jsx(Option, { value: "withdrawn", label: "Withdrawn" })] })] })] })); + return (jsxRuntimeExports.jsxs(FiltersContainer, { children: [jsxRuntimeExports.jsxs(SearchField, { children: [jsxRuntimeExports.jsx(Label, { hidden: true, children: "Search approval requests" }), jsxRuntimeExports.jsx(StyledMediaInput, { start: jsxRuntimeExports.jsx(SvgSearchStroke, {}), placeholder: "Search approval requests", onChange: handleSearch })] }), jsxRuntimeExports.jsxs(DropdownFilterField, { children: [jsxRuntimeExports.jsx(Label, { children: "Status:" }), jsxRuntimeExports.jsxs(Combobox, { isEditable: false, onChange: handleChange, selectionValue: approvalRequestStatus, inputValue: ApprovalRequestStatusInputMap[approvalRequestStatus], children: [jsxRuntimeExports.jsx(Option, { value: "any", label: "Any" }), jsxRuntimeExports.jsx(Option, { value: "active", label: "Decision pending" }), jsxRuntimeExports.jsx(Option, { value: "approved", label: "Approved" }), jsxRuntimeExports.jsx(Option, { value: "rejected", label: "Denied" }), jsxRuntimeExports.jsx(Option, { value: "withdrawn", label: "Withdrawn" })] })] })] })); } var ApprovalRequestListFilters$1 = reactExports.memo(ApprovalRequestListFilters); @@ -144,7 +152,7 @@ const LoadingContainer$1 = styled.div ` `; function ApprovalRequestListPage({ baseLocale, helpCenterPath, }) { const [searchTerm, setSearchTerm] = reactExports.useState(""); - const { approvalRequests, errorFetchingApprovalRequests: error, setApprovalRequestStatus, isLoading, } = useSearchApprovalRequests(); + const { approvalRequests, errorFetchingApprovalRequests: error, approvalRequestStatus, setApprovalRequestStatus, isLoading, } = useSearchApprovalRequests(); const filteredRequests = reactExports.useMemo(() => { if (!searchTerm) return approvalRequests; @@ -157,7 +165,7 @@ function ApprovalRequestListPage({ baseLocale, helpCenterPath, }) { if (isLoading) { return (jsxRuntimeExports.jsx(LoadingContainer$1, { children: jsxRuntimeExports.jsx(Spinner, { size: "64" }) })); } - return (jsxRuntimeExports.jsxs(Container$2, { children: [jsxRuntimeExports.jsx(XXL, { isBold: true, children: "Approval requests" }), jsxRuntimeExports.jsx(ApprovalRequestListFilters$1, { setApprovalRequestStatus: setApprovalRequestStatus, setSearchTerm: setSearchTerm }), jsxRuntimeExports.jsx(ApprovalRequestListTable$1, { requests: filteredRequests, baseLocale: baseLocale, helpCenterPath: helpCenterPath })] })); + return (jsxRuntimeExports.jsxs(Container$2, { children: [jsxRuntimeExports.jsx(XXL, { isBold: true, children: "Approval requests" }), jsxRuntimeExports.jsx(ApprovalRequestListFilters$1, { approvalRequestStatus: approvalRequestStatus, setApprovalRequestStatus: setApprovalRequestStatus, setSearchTerm: setSearchTerm }), jsxRuntimeExports.jsx(ApprovalRequestListTable$1, { requests: filteredRequests, baseLocale: baseLocale, helpCenterPath: helpCenterPath })] })); } var ApprovalRequestListPage$1 = reactExports.memo(ApprovalRequestListPage); @@ -184,7 +192,7 @@ const DetailRow = styled(Row$1) ` } `; function ApprovalRequestDetails({ approvalRequest, baseLocale, }) { - return (jsxRuntimeExports.jsxs(Container$1, { children: [jsxRuntimeExports.jsx(ApprovalRequestHeader, { isBold: true, children: "Approval request details" }), jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Sent by" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: approvalRequest.created_by_user.name }) })] }), jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Sent on" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: formatApprovalRequestDate(approvalRequest.created_at, baseLocale) }) })] }), jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Approver" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: approvalRequest.assignee_user.name }) })] }), jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Status" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: jsxRuntimeExports.jsx(ApprovalStatusTag$1, { status: approvalRequest.status }) }) })] })] })); + return (jsxRuntimeExports.jsxs(Container$1, { children: [jsxRuntimeExports.jsx(ApprovalRequestHeader, { isBold: true, children: "Approval request details" }), jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Sent by" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: approvalRequest.created_by_user.name }) })] }), jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Sent on" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: formatApprovalRequestDate(approvalRequest.created_at, baseLocale) }) })] }), jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Approver" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: approvalRequest.assignee_user.name }) })] }), jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Status" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: jsxRuntimeExports.jsx(ApprovalStatusTag$1, { status: approvalRequest.status }) }) })] }), approvalRequest.decided_at && (jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Decided" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: formatApprovalRequestDate(approvalRequest.decided_at, baseLocale) }) })] })), approvalRequest.decision_notes.length > 0 && (jsxRuntimeExports.jsxs(DetailRow, { children: [jsxRuntimeExports.jsx(Col, { size: 4, children: jsxRuntimeExports.jsx(FieldLabel$1, { children: "Comment" }) }), jsxRuntimeExports.jsx(Col, { size: 8, children: jsxRuntimeExports.jsx(MD, { children: approvalRequest.decision_notes[0] }) })] }))] })); } var ApprovalRequestDetails$1 = reactExports.memo(ApprovalRequestDetails); @@ -213,7 +221,11 @@ const CustomFieldsGrid = styled.div ` grid-template-columns: repeat(2, 1fr); } `; +const NULL_VALUE_PLACEHOLDER = "-"; function CustomFieldValue({ value, }) { + if (!value) { + return jsxRuntimeExports.jsx(MD, { children: NULL_VALUE_PLACEHOLDER }); + } if (Array.isArray(value)) { return (jsxRuntimeExports.jsx(MD, { children: value.map((val) => (jsxRuntimeExports.jsx(MultiselectTag, { hue: "grey", children: val }, val))) })); } @@ -253,6 +265,10 @@ async function submitApprovalDecision(approvalWorkflowInstanceId, approvalReques } } +const PENDING_APPROVAL_STATUS = { + APPROVED: "APPROVED", + REJECTED: "REJECTED", +}; const ButtonContainer = styled.div ` display: flex; flex-direction: row; @@ -274,14 +290,14 @@ function ApproverActions({ approvalRequestId, approvalWorkflowInstanceId, setApp const [pendingStatus, setPendingStatus] = reactExports.useState(null); const [isSubmitting, setIsSubmitting] = reactExports.useState(false); const [showValidation, setShowValidation] = reactExports.useState(false); - const isCommentValid = pendingStatus !== "REJECTED" || comment.trim() !== ""; + const isCommentValid = pendingStatus !== PENDING_APPROVAL_STATUS.REJECTED || comment.trim() !== ""; const shouldShowValidationError = showValidation && !isCommentValid; const handleApproveRequestClick = reactExports.useCallback(() => { - setPendingStatus("APPROVED"); + setPendingStatus(PENDING_APPROVAL_STATUS.APPROVED); setShowValidation(false); }, []); const handleDenyRequestClick = reactExports.useCallback(() => { - setPendingStatus("REJECTED"); + setPendingStatus(PENDING_APPROVAL_STATUS.REJECTED); setShowValidation(false); }, []); const handleInputValueChange = reactExports.useCallback((e) => { @@ -298,7 +314,9 @@ function ApproverActions({ approvalRequestId, approvalWorkflowInstanceId, setApp return; setIsSubmitting(true); try { - const decision = pendingStatus === "APPROVED" ? "approved" : "rejected"; + const decision = pendingStatus === PENDING_APPROVAL_STATUS.APPROVED + ? "approved" + : "rejected"; const response = await submitApprovalDecision(approvalWorkflowInstanceId, approvalRequestId, decision, comment); if (response.ok) { const data = await response.json(); @@ -326,10 +344,12 @@ function ApproverActions({ approvalRequestId, approvalWorkflowInstanceId, setApp } }; if (pendingStatus) { - const fieldLabel = pendingStatus === "APPROVED" + const fieldLabel = pendingStatus === PENDING_APPROVAL_STATUS.APPROVED ? "Additional note" : "Reason for denial* (Required)"; - return (jsxRuntimeExports.jsxs(CommentSection, { children: [jsxRuntimeExports.jsxs(Field$1, { children: [jsxRuntimeExports.jsx(Label$1, { children: fieldLabel }), jsxRuntimeExports.jsx(Textarea, { minRows: 5, value: comment, onChange: handleInputValueChange, disabled: isSubmitting, validation: shouldShowValidationError ? "error" : undefined }), shouldShowValidationError && (jsxRuntimeExports.jsx(Message, { validation: "error", children: "Enter a reason for denial" }))] }), jsxRuntimeExports.jsxs(ButtonContainer, { children: [jsxRuntimeExports.jsx(Button, { isPrimary: pendingStatus === "APPROVED", onClick: handleSubmitDecisionClick, disabled: isSubmitting, children: pendingStatus === "APPROVED" ? "Submit approval" : "Submit denial" }), jsxRuntimeExports.jsx(Button, { onClick: handleCancelClick, disabled: isSubmitting, children: "Cancel" })] })] })); + return (jsxRuntimeExports.jsxs(CommentSection, { children: [jsxRuntimeExports.jsxs(Field$1, { children: [jsxRuntimeExports.jsx(Label$1, { children: fieldLabel }), jsxRuntimeExports.jsx(Textarea, { minRows: 5, value: comment, onChange: handleInputValueChange, disabled: isSubmitting, validation: shouldShowValidationError ? "error" : undefined }), shouldShowValidationError && (jsxRuntimeExports.jsx(Message, { validation: "error", children: "Enter a reason for denial" }))] }), jsxRuntimeExports.jsxs(ButtonContainer, { children: [jsxRuntimeExports.jsx(Button, { isPrimary: pendingStatus === PENDING_APPROVAL_STATUS.APPROVED, onClick: handleSubmitDecisionClick, disabled: isSubmitting, children: pendingStatus === PENDING_APPROVAL_STATUS.APPROVED + ? "Submit approval" + : "Submit denial" }), jsxRuntimeExports.jsx(Button, { onClick: handleCancelClick, disabled: isSubmitting, children: "Cancel" })] })] })); } return (jsxRuntimeExports.jsxs(ButtonContainer, { children: [jsxRuntimeExports.jsx(Button, { isPrimary: true, onClick: handleApproveRequestClick, children: "Approve request" }), jsxRuntimeExports.jsx(Button, { onClick: handleDenyRequestClick, children: "Deny request" })] })); } diff --git a/src/modules/approval-requests/ApprovalRequestListPage.tsx b/src/modules/approval-requests/ApprovalRequestListPage.tsx index a7c5d8f64..c0dc946ea 100644 --- a/src/modules/approval-requests/ApprovalRequestListPage.tsx +++ b/src/modules/approval-requests/ApprovalRequestListPage.tsx @@ -31,6 +31,7 @@ function ApprovalRequestListPage({ const { approvalRequests, errorFetchingApprovalRequests: error, + approvalRequestStatus, setApprovalRequestStatus, isLoading, } = useSearchApprovalRequests(); @@ -60,6 +61,7 @@ function ApprovalRequestListPage({ Approval requests diff --git a/src/modules/approval-requests/components/approval-request-list/ApprovalRequestListFilters.tsx b/src/modules/approval-requests/components/approval-request-list/ApprovalRequestListFilters.tsx index 4794b937f..5fb51d369 100644 --- a/src/modules/approval-requests/components/approval-request-list/ApprovalRequestListFilters.tsx +++ b/src/modules/approval-requests/components/approval-request-list/ApprovalRequestListFilters.tsx @@ -42,7 +42,16 @@ const DropdownFilterField = styled(Field)` flex: 1; `; +const ApprovalRequestStatusInputMap = { + any: "Any", + active: "Decision pending", + approved: "Approved", + rejected: "Denied", + withdrawn: "Withdrawn", +}; + interface ApprovalRequestListFiltersProps { + approvalRequestStatus: ApprovalRequestDropdownStatus; setApprovalRequestStatus: Dispatch< SetStateAction >; @@ -50,6 +59,7 @@ interface ApprovalRequestListFiltersProps { } function ApprovalRequestListFilters({ + approvalRequestStatus, setApprovalRequestStatus, setSearchTerm, }: ApprovalRequestListFiltersProps) { @@ -91,8 +101,13 @@ function ApprovalRequestListFilters({ - - ); } diff --git a/src/modules/approval-requests/components/approval-request/ApprovalTicketDetails.tsx b/src/modules/approval-requests/components/approval-request/ApprovalTicketDetails.tsx index c42f54225..87cb4d0fb 100644 --- a/src/modules/approval-requests/components/approval-request/ApprovalTicketDetails.tsx +++ b/src/modules/approval-requests/components/approval-request/ApprovalTicketDetails.tsx @@ -3,8 +3,8 @@ import styled from "styled-components"; import { MD } from "@zendeskgarden/react-typography"; import { getColorV8 } from "@zendeskgarden/react-theming"; import { Grid } from "@zendeskgarden/react-grid"; -import type { MockTicket } from "../../types"; import { Tag } from "@zendeskgarden/react-tags"; +import type { ApprovalRequestTicket } from "../../types"; const TicketContainer = styled(Grid)` padding: ${(props) => props.theme.space.md}; /* 20px */ @@ -36,11 +36,16 @@ const CustomFieldsGrid = styled.div` } `; +const NULL_VALUE_PLACEHOLDER = "-"; + function CustomFieldValue({ value, }: { value: string | boolean | Array | undefined; }) { + if (!value) { + return {NULL_VALUE_PLACEHOLDER}; + } if (Array.isArray(value)) { return ( @@ -61,7 +66,7 @@ function CustomFieldValue({ } interface ApprovalTicketDetailsProps { - ticket: MockTicket; + ticket: ApprovalRequestTicket; } function ApprovalTicketDetails({ ticket }: ApprovalTicketDetailsProps) { diff --git a/src/modules/approval-requests/components/approval-request/ApproverActions.tsx b/src/modules/approval-requests/components/approval-request/ApproverActions.tsx index 4998b0119..06a17286d 100644 --- a/src/modules/approval-requests/components/approval-request/ApproverActions.tsx +++ b/src/modules/approval-requests/components/approval-request/ApproverActions.tsx @@ -2,11 +2,16 @@ import { useState, useCallback, memo } from "react"; import styled from "styled-components"; import { Button } from "@zendeskgarden/react-buttons"; import { Field, Label, Message, Textarea } from "@zendeskgarden/react-forms"; +import { useNotify } from "../../../shared/notifications/useNotify"; import { submitApprovalDecision } from "../../submitApprovalDecision"; import type { ApprovalDecision } from "../../submitApprovalDecision"; -import { useNotify } from "../../../shared/notifications/useNotify"; import type { ApprovalRequest } from "../../types"; +const PENDING_APPROVAL_STATUS = { + APPROVED: "APPROVED", + REJECTED: "REJECTED", +} as const; + const ButtonContainer = styled.div` display: flex; flex-direction: row; @@ -38,21 +43,23 @@ function ApproverActions({ const notify = useNotify(); const [comment, setComment] = useState(""); const [pendingStatus, setPendingStatus] = useState< - "APPROVED" | "REJECTED" | null + | (typeof PENDING_APPROVAL_STATUS)[keyof typeof PENDING_APPROVAL_STATUS] + | null >(null); const [isSubmitting, setIsSubmitting] = useState(false); const [showValidation, setShowValidation] = useState(false); - const isCommentValid = pendingStatus !== "REJECTED" || comment.trim() !== ""; + const isCommentValid = + pendingStatus !== PENDING_APPROVAL_STATUS.REJECTED || comment.trim() !== ""; const shouldShowValidationError = showValidation && !isCommentValid; const handleApproveRequestClick = useCallback(() => { - setPendingStatus("APPROVED"); + setPendingStatus(PENDING_APPROVAL_STATUS.APPROVED); setShowValidation(false); }, []); const handleDenyRequestClick = useCallback(() => { - setPendingStatus("REJECTED"); + setPendingStatus(PENDING_APPROVAL_STATUS.REJECTED); setShowValidation(false); }, []); @@ -76,7 +83,9 @@ function ApproverActions({ setIsSubmitting(true); try { const decision: ApprovalDecision = - pendingStatus === "APPROVED" ? "approved" : "rejected"; + pendingStatus === PENDING_APPROVAL_STATUS.APPROVED + ? "approved" + : "rejected"; const response = await submitApprovalDecision( approvalWorkflowInstanceId, approvalRequestId, @@ -111,7 +120,7 @@ function ApproverActions({ if (pendingStatus) { const fieldLabel = - pendingStatus === "APPROVED" + pendingStatus === PENDING_APPROVAL_STATUS.APPROVED ? "Additional note" : "Reason for denial* (Required)"; return ( @@ -131,11 +140,13 @@ function ApproverActions({