From bb8de1315464f579063a0927ddaa7856c9d037a6 Mon Sep 17 00:00:00 2001 From: "Frank Pigeon Jr." Date: Wed, 22 Jan 2025 17:46:22 -0600 Subject: [PATCH 1/9] feat: adds can history query and renders to can detais page --- frontend/src/api/opsAPI.js | 17 ++++++ .../src/components/CANs/CANDetailView.jsx | 0 .../CANs/CANDetailView/CANDetailView.jsx | 50 ++++++++++++++-- .../CANs/CANDetailView/CANDetailView.test.jsx | 19 ++++-- .../CANs/CANHistoryPanel/CANHistoryPanel.jsx | 31 ++++++++++ .../components/CANs/CANHistoryPanel/index.js | 1 + frontend/src/components/CANs/CANTypes.d.ts | 10 ++++ frontend/src/helpers/utils.js | 18 +++++- frontend/src/helpers/utils.test.js | 58 ++++++++++++++++++- frontend/src/pages/cans/detail/CanDetail.jsx | 30 ++++++---- 10 files changed, 211 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/CANs/CANDetailView.jsx create mode 100644 frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx create mode 100644 frontend/src/components/CANs/CANHistoryPanel/index.js diff --git a/frontend/src/api/opsAPI.js b/frontend/src/api/opsAPI.js index af50ee8d6c..007dc91ab1 100644 --- a/frontend/src/api/opsAPI.js +++ b/frontend/src/api/opsAPI.js @@ -288,6 +288,22 @@ export const opsApi = createApi({ }, providesTags: ["Cans", "CanFunding"] }), + getCanHistory: builder.query({ + query: ({ canId, offset, limit }) => { + const queryParams = []; + if (canId) { + queryParams.push(`can_id=${canId}`); + } + if (limit) { + queryParams.push(`limit=${limit}`); + } + if (offset) { + queryParams.push(`offset=${offset}`); + } + return `/can-history/?${queryParams.join("&")}`; + }, + providesTags: ["Cans"] + }), getNotificationsByUserId: builder.query({ query: ({ id, auth_header }) => { if (!id) { @@ -462,6 +478,7 @@ export const { useUpdateCanFundingReceivedMutation, useDeleteCanFundingReceivedMutation, useGetCanFundingSummaryQuery, + useGetCanHistoryQuery, useGetNotificationsByUserIdQuery, useGetNotificationsByUserIdAndAgreementIdQuery, useDismissNotificationMutation, diff --git a/frontend/src/components/CANs/CANDetailView.jsx b/frontend/src/components/CANs/CANDetailView.jsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx b/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx index 171f7ddb79..37a4a111f9 100644 --- a/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx +++ b/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx @@ -1,6 +1,9 @@ -import TermTag from "../../UI/Term/TermTag"; -import Term from "../../UI/Term"; +import React from "react"; +import InfiniteScroll from "../../Agreements/AgreementDetails/InfiniteScroll"; +import LogItem from "../../UI/LogItem"; import Tag from "../../UI/Tag"; +import Term from "../../UI/Term"; +import TermTag from "../../UI/Term/TermTag"; /** * @typedef {Object} CANDetailViewProps * @property {string} description @@ -10,6 +13,7 @@ import Tag from "../../UI/Tag"; * @property {import("../../Users/UserTypes").SafeUser[]} teamLeaders * @property {string} divisionDirectorFullName * @property {string} divisionName + * @property {import("../../CANs/CANTypes").CanHistoryItem[]} canHistoryItems */ /** * This component needs to wrapped in a
element. @@ -17,7 +21,18 @@ import Tag from "../../UI/Tag"; * @param {CANDetailViewProps} props - The properties passed to the component. * @returns {JSX.Element} - The rendered component. */ -const CANDetailView = ({ description, number, nickname, portfolioName, teamLeaders, divisionDirectorFullName, divisionName}) => { +const CANDetailView = ({ + canHistoryItems = [], + description, + number, + nickname, + portfolioName, + teamLeaders, + divisionDirectorFullName, + divisionName +}) => { + const [isLoading, setIsLoading] = React.useState(false); + return (
{/* // NOTE: Left Column */} @@ -33,7 +48,34 @@ const CANDetailView = ({ description, number, nickname, portfolioName, teamLeade

History

-

Not yet implemented

+
+ {canHistoryItems.length > 0 ? ( +
    + {canHistoryItems.map((canHistoryItem) => ( + + ))} + { + setIsLoading(true); + }} + isLoading={isLoading} + /> +
+ ) : ( +

No History

+ )} +
{/* // NOTE: Right Column */} diff --git a/frontend/src/components/CANs/CANDetailView/CANDetailView.test.jsx b/frontend/src/components/CANs/CANDetailView/CANDetailView.test.jsx index 02012620c0..6a91988aff 100644 --- a/frontend/src/components/CANs/CANDetailView/CANDetailView.test.jsx +++ b/frontend/src/components/CANs/CANDetailView/CANDetailView.test.jsx @@ -8,11 +8,22 @@ const mockProps = { nickname: "Test Nickname", portfolioName: "Test Portfolio", teamLeaders: [ - { id: 1, full_name: "John Doe" }, - { id: 2, full_name: "Jane Smith" } + { id: 1, full_name: "John Doe", email: "jdoe@example.com" }, + { id: 2, full_name: "Jane Smith", email: "jsmith@example.com" } ], divisionDirectorFullName: "Director Name", - divisionName: "Test Division" + divisionName: "Test Division", + canHistoryItems: [ + { + id: 1, + can_id: 500, + ops_event_id: 1, + history_title: "Test History Title", + history_message: "Test History Message", + timestamp: "2021-01-01T00:00:00Z", + history_type: "Test History Type" + } + ] }; describe("CANDetailView", () => { @@ -46,7 +57,7 @@ describe("CANDetailView", () => { ); expect(screen.getByText("History")).toBeInTheDocument(); - expect(screen.getByText("Not yet implemented")).toBeInTheDocument(); + // TODO: Add more specific tests for history section }); it("renders without team leaders", () => { diff --git a/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx b/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx new file mode 100644 index 0000000000..175cce75ff --- /dev/null +++ b/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import InfiniteScroll from "../../Agreements/AgreementDetails/InfiniteScroll"; + +const CanHistoryPanel = ({ canId, fetchMoreData, isLoading, stopped }) => { + const [canHistory, setCantHistory] = useState([]); + console.log(canHistory, setCantHistory, canId); + + return ( +
+ <> + {!stopped && ( + + )} + +
+ ); +}; + +export default CanHistoryPanel; diff --git a/frontend/src/components/CANs/CANHistoryPanel/index.js b/frontend/src/components/CANs/CANHistoryPanel/index.js new file mode 100644 index 0000000000..f56f2da4fe --- /dev/null +++ b/frontend/src/components/CANs/CANHistoryPanel/index.js @@ -0,0 +1 @@ +export { default } from "./CANHistoryPanel"; diff --git a/frontend/src/components/CANs/CANTypes.d.ts b/frontend/src/components/CANs/CANTypes.d.ts index d1ab9d2f6a..1f3380ab08 100644 --- a/frontend/src/components/CANs/CANTypes.d.ts +++ b/frontend/src/components/CANs/CANTypes.d.ts @@ -127,3 +127,13 @@ export type FundingSummaryCAN = { carry_forward_label: string; expiration_date: string; }; + +export type CanHistoryItem = { + id: number; + can_id: number; + ops_event_id: number; + history_title: string; + history_message: string; + timestamp: string; + history_type: string; +} diff --git a/frontend/src/helpers/utils.js b/frontend/src/helpers/utils.js index 1353beda37..48a0ad08f5 100644 --- a/frontend/src/helpers/utils.js +++ b/frontend/src/helpers/utils.js @@ -225,6 +225,16 @@ export const convertCodeForDisplay = (listName, code) => { return codeMap[code] ? codeMap[code] : code; }; +/** + * Converts a date to a relative time string (e.g., "2 hours ago", "about a minute ago") + * @param {(Date|string)} dateParam - The date to convert. Can be a Date object or an ISO string + * @returns {string|null} A human-readable string representing relative time, + * formatted date string for dates older than 24 hours, + * or null if no date provided + * @example + * timeAgo("2023-05-20T15:00:00") // returns "about a minute ago" + * timeAgo(new Date()) // returns "now" + */ export const timeAgo = (dateParam) => { if (!dateParam) { return null; @@ -238,8 +248,9 @@ export const timeAgo = (dateParam) => { const date = typeof dateParam === "object" ? dateParam : new Date(dateParam); const today = new Date(); - const seconds = Math.round((today - date) / 1000); + const seconds = Math.round((today.getTime() - date.getTime()) / 1000); const minutes = Math.round(seconds / 60); + const hours = Math.round(minutes / 60); if (seconds < 5) { return "now"; @@ -249,11 +260,12 @@ export const timeAgo = (dateParam) => { return "about a minute ago"; } else if (minutes < 60) { return `${minutes} minutes ago`; + } else if (hours < 24) { + return `${hours} hours ago`; } return new Date(date).toLocaleString("en-US", { - dateStyle: "long", - timeStyle: "short" + dateStyle: "long" }); }; diff --git a/frontend/src/helpers/utils.test.js b/frontend/src/helpers/utils.test.js index f47b25d103..245e274136 100644 --- a/frontend/src/helpers/utils.test.js +++ b/frontend/src/helpers/utils.test.js @@ -7,7 +7,8 @@ import { toSlugCase, toTitleCaseFromSlug, toLowerCaseFromSlug, - fromUpperCaseToTitleCase + fromUpperCaseToTitleCase, + timeAgo } from "./utils"; test("current federal fiscal year is calculated correctly", () => { @@ -97,3 +98,58 @@ test("renders uppercase to titlecase", () => { expect(fromUpperCaseToTitleCase(undefined)).toEqual(""); expect(fromUpperCaseToTitleCase(true)).toEqual(""); }); + +describe("timeAgo", () => { + beforeEach(() => { + // Mock the current date to be fixed + vi.useFakeTimers(); + vi.setSystemTime(new Date("2023-01-01T12:00:00Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + test("handles null and undefined input", () => { + expect(timeAgo(null)).toBeNull(); + expect(timeAgo(undefined)).toBeNull(); + }); + + test("shows 'now' for very recent dates", () => { + const date = new Date("2023-01-01T11:59:57Z"); // 3 seconds ago + expect(timeAgo(date)).toBe("now"); + }); + + test("shows seconds for recent dates", () => { + const date = new Date("2023-01-01T11:59:30Z"); // 30 seconds ago + expect(timeAgo(date)).toBe("30 seconds ago"); + }); + + test("shows 'about a minute ago' for ~1 minute", () => { + const date = new Date("2023-01-01T11:58:35Z"); // 85 seconds ago + expect(timeAgo(date)).toBe("about a minute ago"); + }); + + test("shows minutes for < 1 hour", () => { + const date = new Date("2023-01-01T11:30:00Z"); // 30 minutes ago + expect(timeAgo(date)).toBe("30 minutes ago"); + }); + + test("shows hours for < 24 hours", () => { + const date = new Date("2023-01-01T06:00:00Z"); // 6 hours ago + expect(timeAgo(date)).toBe("6 hours ago"); + }); + + test("shows full date for dates > 24 hours ago", () => { + const date = new Date("2022-12-30T12:00:00Z"); // 2 days ago + expect(timeAgo(date)).toBe("December 30, 2022"); + }); + + test("handles ISO string dates with timezone", () => { + expect(timeAgo("2023-01-01T11:30:00Z")).toBe("30 minutes ago"); + }); + + test("handles ISO string dates without timezone", () => { + expect(timeAgo("2023-01-01T11:30:00")).toBe("30 minutes ago"); + }); +}); diff --git a/frontend/src/pages/cans/detail/CanDetail.jsx b/frontend/src/pages/cans/detail/CanDetail.jsx index 8485986875..9738d7ef74 100644 --- a/frontend/src/pages/cans/detail/CanDetail.jsx +++ b/frontend/src/pages/cans/detail/CanDetail.jsx @@ -1,8 +1,8 @@ import { faPen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useGetDivisionQuery } from "../../../api/opsAPI"; +import { useGetCanHistoryQuery, useGetDivisionQuery } from "../../../api/opsAPI"; import CANDetailForm from "../../../components/CANs/CANDetailForm"; -import CANDetailView from "../../../components/CANs/CANDetailView"; +import CANDetailView from "../../../components/CANs/CANDetailView/CANDetailView"; import { NO_DATA } from "../../../constants.js"; import { getCurrentFiscalYear } from "../../../helpers/utils"; import useGetUserFullNameFromId from "../../../hooks/user.hooks"; @@ -48,11 +48,16 @@ const CanDetail = ({ }) => { const { data: division, isSuccess } = useGetDivisionQuery(divisionId); const divisionDirectorFullName = useGetUserFullNameFromId(isSuccess ? division.division_director_id : null); + const { data: canHistoryItems, isLoading } = useGetCanHistoryQuery({ canId, limit: 5, offset: 0 }); const currentFiscalYear = getCurrentFiscalYear(); const showButton = isBudgetTeamMember && fiscalYear === Number(currentFiscalYear); const divisionName = division?.display_name ?? NO_DATA; + if (isLoading) { + return
Loading...
; + } + return (
@@ -84,15 +89,18 @@ const CanDetail = ({ toggleEditMode={toggleEditMode} /> ) : ( - + <> + + )}
); From 09cd4c98015ada5ead640cd93335d10e65c154a0 Mon Sep 17 00:00:00 2001 From: weimiao67 Date: Thu, 23 Jan 2025 01:37:46 -0700 Subject: [PATCH 2/9] feat: wip, enable infinite scroll --- .../CANs/CANDetailView/CANDetailView.jsx | 39 +----- .../CANs/CANHistoryPanel/CANHistoryPanel.jsx | 115 ++++++++++++++---- frontend/src/pages/cans/detail/CanDetail.jsx | 10 +- 3 files changed, 101 insertions(+), 63 deletions(-) diff --git a/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx b/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx index 37a4a111f9..14b6e6ff82 100644 --- a/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx +++ b/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx @@ -1,9 +1,7 @@ -import React from "react"; -import InfiniteScroll from "../../Agreements/AgreementDetails/InfiniteScroll"; -import LogItem from "../../UI/LogItem"; import Tag from "../../UI/Tag"; import Term from "../../UI/Term"; import TermTag from "../../UI/Term/TermTag"; +import CanHistoryPanel from "../CANHistoryPanel"; /** * @typedef {Object} CANDetailViewProps * @property {string} description @@ -13,7 +11,7 @@ import TermTag from "../../UI/Term/TermTag"; * @property {import("../../Users/UserTypes").SafeUser[]} teamLeaders * @property {string} divisionDirectorFullName * @property {string} divisionName - * @property {import("../../CANs/CANTypes").CanHistoryItem[]} canHistoryItems + * @property {number} canId */ /** * This component needs to wrapped in a
element. @@ -22,7 +20,7 @@ import TermTag from "../../UI/Term/TermTag"; * @returns {JSX.Element} - The rendered component. */ const CANDetailView = ({ - canHistoryItems = [], + canId, description, number, nickname, @@ -31,8 +29,6 @@ const CANDetailView = ({ divisionDirectorFullName, divisionName }) => { - const [isLoading, setIsLoading] = React.useState(false); - return (
{/* // NOTE: Left Column */} @@ -48,34 +44,7 @@ const CANDetailView = ({

History

-
- {canHistoryItems.length > 0 ? ( -
    - {canHistoryItems.map((canHistoryItem) => ( - - ))} - { - setIsLoading(true); - }} - isLoading={isLoading} - /> -
- ) : ( -

No History

- )} -
+
{/* // NOTE: Right Column */} diff --git a/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx b/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx index 175cce75ff..d9dee5db4b 100644 --- a/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx +++ b/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx @@ -1,30 +1,99 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import InfiniteScroll from "../../Agreements/AgreementDetails/InfiniteScroll"; +import { useGetCanHistoryQuery } from "../../../api/opsAPI"; +import LogItem from "../../UI/LogItem"; -const CanHistoryPanel = ({ canId, fetchMoreData, isLoading, stopped }) => { - const [canHistory, setCantHistory] = useState([]); - console.log(canHistory, setCantHistory, canId); +/** + * @typedef {Object} CanHistoryPanelProps + * @property {number} canId + */ + +/** + * @param {CanHistoryPanelProps} props + */ + +const CanHistoryPanel = ({ canId }) => { + const [offset, setOffset] = useState(0); + const [isFetching, setIsFetching] = useState(false); + const [stopped, setStopped] = useState(false); + /** + * @type {CanHistoryItem[]} + */ + const initialHistory = []; + /** + * @typedef {import('../../CANs/CANTypes').CanHistoryItem} CanHistoryItem + * @type {[CanHistoryItem[], React.Dispatch>]} + */ + const [cantHistory, setCanHistory] = useState(initialHistory); + + const { + data: canHistoryItems, + error, + isLoading + } = useGetCanHistoryQuery({ + canId, + limit: 5, + offset: offset + }); + + useEffect(() => { + if (canHistoryItems && canHistoryItems.length > 0) { + console.log({ canHistoryItems }); + setCanHistory([...cantHistory, ...canHistoryItems]); + } + if (error) { + setStopped(true); + } + }, [canHistoryItems]); + + const fetchMoreData = () => { + if (stopped) return; + if (!isFetching && !stopped) { + setIsFetching(true); + setOffset(offset + 5); + setIsFetching(false); + } + }; + + if (isLoading) { + return
Loading...
; + } return ( -
- <> - {!stopped && ( - - )} - -
+ <> + {cantHistory.length > 0 ? ( +
+
    + {cantHistory.map((item) => ( + + ))} +
+ {!stopped && ( + + )} +
+ ) : ( +

No History

+ )} + ); }; diff --git a/frontend/src/pages/cans/detail/CanDetail.jsx b/frontend/src/pages/cans/detail/CanDetail.jsx index 9738d7ef74..ef4ffabdcb 100644 --- a/frontend/src/pages/cans/detail/CanDetail.jsx +++ b/frontend/src/pages/cans/detail/CanDetail.jsx @@ -48,15 +48,15 @@ const CanDetail = ({ }) => { const { data: division, isSuccess } = useGetDivisionQuery(divisionId); const divisionDirectorFullName = useGetUserFullNameFromId(isSuccess ? division.division_director_id : null); - const { data: canHistoryItems, isLoading } = useGetCanHistoryQuery({ canId, limit: 5, offset: 0 }); + //const { data: canHistoryItems, isLoading } = useGetCanHistoryQuery({ canId, limit: 5, offset: 0 }); const currentFiscalYear = getCurrentFiscalYear(); const showButton = isBudgetTeamMember && fiscalYear === Number(currentFiscalYear); const divisionName = division?.display_name ?? NO_DATA; - if (isLoading) { - return
Loading...
; - } + // if (isLoading) { + // return
Loading...
; + // } return (
@@ -91,7 +91,7 @@ const CanDetail = ({ ) : ( <> Date: Thu, 23 Jan 2025 13:34:09 -0700 Subject: [PATCH 3/9] feat: fix then chain error --- .../src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx b/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx index d9dee5db4b..71846a7714 100644 --- a/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx +++ b/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx @@ -41,6 +41,9 @@ const CanHistoryPanel = ({ canId }) => { console.log({ canHistoryItems }); setCanHistory([...cantHistory, ...canHistoryItems]); } + if (!isLoading && canHistoryItems && canHistoryItems.length === 0) { + setStopped(true); + } if (error) { setStopped(true); } @@ -48,11 +51,12 @@ const CanHistoryPanel = ({ canId }) => { const fetchMoreData = () => { if (stopped) return; - if (!isFetching && !stopped) { + if (!isFetching) { setIsFetching(true); setOffset(offset + 5); setIsFetching(false); } + return Promise.resolve(); }; if (isLoading) { From 87828d2876a20cb68e45343a083ee45cb9c25b69 Mon Sep 17 00:00:00 2001 From: "Frank Pigeon Jr." Date: Thu, 23 Jan 2025 16:52:08 -0600 Subject: [PATCH 4/9] test: adds e2e and unit-tests for can history --- frontend/cypress/e2e/canDetail.cy.js | 19 +++++++ frontend/src/api/opsAPI.js | 5 +- .../CANHistoryPanel/CANHIstoryPanel.test.jsx | 53 +++++++++++++++++++ .../CANs/CANHistoryPanel/CANHistoryPanel.jsx | 17 +++--- frontend/src/helpers/utils.js | 2 + frontend/src/pages/cans/detail/CanDetail.jsx | 8 +-- src/components/CANHistoryPanel.test.tsx | 0 7 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/CANs/CANHistoryPanel/CANHIstoryPanel.test.jsx create mode 100644 src/components/CANHistoryPanel.test.tsx diff --git a/frontend/cypress/e2e/canDetail.cy.js b/frontend/cypress/e2e/canDetail.cy.js index 3e441af5d1..72132b61fb 100644 --- a/frontend/cypress/e2e/canDetail.cy.js +++ b/frontend/cypress/e2e/canDetail.cy.js @@ -80,6 +80,13 @@ describe("CAN detail page", () => { cy.get("p").should("contain", can502Nickname); cy.get("dd").should("contain", can502Description); }); + it("handles history", () => { + cy.visit("/cans/500/"); + checkCANHistory(); + }); +}); + +describe("CAN spending page", () => { it("shows the CAN Spending page", () => { cy.visit("/cans/504/spending"); cy.get("#fiscal-year-select").select("2043"); @@ -132,6 +139,10 @@ describe("CAN detail page", () => { cy.get("li").should("have.class", "usa-pagination__item").contains("1").click(); cy.get("button").should("have.class", "usa-current").contains("1"); }); +}); + +// TODO: Add tests to check for history logs for each budget and funding change after backend is implemented +describe("CAN funding page", () => { it("shows the CAN Funding page", () => { cy.visit("/cans/504/funding"); cy.get("#fiscal-year-select").select("2024"); @@ -330,3 +341,11 @@ describe("CAN detail page", () => { cy.get("#carry-forward-card").should("not.exist"); }); }); + +const checkCANHistory = () => { + cy.get("h3").should("have.text", "History"); + cy.get('[data-cy="can-history-container"]').should("exist"); + cy.get('[data-cy="can-history-container"]').scrollIntoView(); + cy.get('[data-cy="can-history-list"]').should("exist"); + cy.get('[data-cy="can-history-list"] > :nth-child(1) > .flex-justify > [data-cy="log-item-title"]').should("exist"); +}; diff --git a/frontend/src/api/opsAPI.js b/frontend/src/api/opsAPI.js index 007dc91ab1..4f8adf2671 100644 --- a/frontend/src/api/opsAPI.js +++ b/frontend/src/api/opsAPI.js @@ -291,16 +291,13 @@ export const opsApi = createApi({ getCanHistory: builder.query({ query: ({ canId, offset, limit }) => { const queryParams = []; - if (canId) { - queryParams.push(`can_id=${canId}`); - } if (limit) { queryParams.push(`limit=${limit}`); } if (offset) { queryParams.push(`offset=${offset}`); } - return `/can-history/?${queryParams.join("&")}`; + return `/can-history/?can_id=${canId}&${queryParams.join("&")}`; }, providesTags: ["Cans"] }), diff --git a/frontend/src/components/CANs/CANHistoryPanel/CANHIstoryPanel.test.jsx b/frontend/src/components/CANs/CANHistoryPanel/CANHIstoryPanel.test.jsx new file mode 100644 index 0000000000..0866c4e6f5 --- /dev/null +++ b/frontend/src/components/CANs/CANHistoryPanel/CANHIstoryPanel.test.jsx @@ -0,0 +1,53 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { render, screen } from "@testing-library/react"; +// import userEvent from "@testing-library/user-event"; +import CANHistoryPanel from "./CANHistoryPanel"; + +describe("CANHistoryPanel", () => { + // const user = userEvent.setup(); + + beforeEach(() => { + // Mock any necessary providers or props here + }); + + it("renders without crashing", () => { + render(); + expect(screen.getByRole("region")).toBeInTheDocument(); + }); + + it("displays CAN history data when provided", () => { + const mockData = [ + { timestamp: "2023-01-01T00:00:00", id: "0x123", data: "0xFF 0x00" }, + { timestamp: "2023-01-01T00:00:01", id: "0x456", data: "0x01 0x02" } + ]; + + render(); + + expect(screen.getByText("0x123")).toBeInTheDocument(); + expect(screen.getByText("0x456")).toBeInTheDocument(); + }); + + // it('handles filter input changes', async () => { + // render(); + + // const filterInput = screen.getByRole('textbox', { name: /filter/i }); + // await user.type(filterInput, '0x123'); + + // expect(filterInput).toHaveValue('0x123'); + // }); + + it("shows empty state when no data is provided", () => { + render(); + + expect(screen.getByText(/no history/i)).toBeInTheDocument(); + }); + + // it("allows toggling of auto-scroll", async () => { + // render(); + + // const autoScrollToggle = screen.getByRole("checkbox", { name: /auto-scroll/i }); + // await user.click(autoScrollToggle); + + // expect(autoScrollToggle).toBeChecked(); + // }); +}); diff --git a/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx b/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx index 71846a7714..b15e3be2f1 100644 --- a/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx +++ b/frontend/src/components/CANs/CANHistoryPanel/CANHistoryPanel.jsx @@ -14,7 +14,6 @@ import LogItem from "../../UI/LogItem"; const CanHistoryPanel = ({ canId }) => { const [offset, setOffset] = useState(0); - const [isFetching, setIsFetching] = useState(false); const [stopped, setStopped] = useState(false); /** * @type {CanHistoryItem[]} @@ -28,8 +27,9 @@ const CanHistoryPanel = ({ canId }) => { const { data: canHistoryItems, - error, - isLoading + isError, + isLoading, + isFetching } = useGetCanHistoryQuery({ canId, limit: 5, @@ -38,13 +38,12 @@ const CanHistoryPanel = ({ canId }) => { useEffect(() => { if (canHistoryItems && canHistoryItems.length > 0) { - console.log({ canHistoryItems }); setCanHistory([...cantHistory, ...canHistoryItems]); } if (!isLoading && canHistoryItems && canHistoryItems.length === 0) { setStopped(true); } - if (error) { + if (isError) { setStopped(true); } }, [canHistoryItems]); @@ -52,17 +51,11 @@ const CanHistoryPanel = ({ canId }) => { const fetchMoreData = () => { if (stopped) return; if (!isFetching) { - setIsFetching(true); setOffset(offset + 5); - setIsFetching(false); } return Promise.resolve(); }; - if (isLoading) { - return
Loading...
; - } - return ( <> {cantHistory.length > 0 ? ( @@ -73,6 +66,8 @@ const CanHistoryPanel = ({ canId }) => { role="region" aria-live="polite" aria-label="CAN History" + // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex + tabIndex={0} >
    { return "about a minute ago"; } else if (minutes < 60) { return `${minutes} minutes ago`; + } else if (hours === 1) { + return `${hours} hour ago`; } else if (hours < 24) { return `${hours} hours ago`; } diff --git a/frontend/src/pages/cans/detail/CanDetail.jsx b/frontend/src/pages/cans/detail/CanDetail.jsx index ef4ffabdcb..2a2ddf01b4 100644 --- a/frontend/src/pages/cans/detail/CanDetail.jsx +++ b/frontend/src/pages/cans/detail/CanDetail.jsx @@ -1,6 +1,6 @@ import { faPen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useGetCanHistoryQuery, useGetDivisionQuery } from "../../../api/opsAPI"; +import { useGetDivisionQuery } from "../../../api/opsAPI"; import CANDetailForm from "../../../components/CANs/CANDetailForm"; import CANDetailView from "../../../components/CANs/CANDetailView/CANDetailView"; import { NO_DATA } from "../../../constants.js"; @@ -48,16 +48,10 @@ const CanDetail = ({ }) => { const { data: division, isSuccess } = useGetDivisionQuery(divisionId); const divisionDirectorFullName = useGetUserFullNameFromId(isSuccess ? division.division_director_id : null); - //const { data: canHistoryItems, isLoading } = useGetCanHistoryQuery({ canId, limit: 5, offset: 0 }); - const currentFiscalYear = getCurrentFiscalYear(); const showButton = isBudgetTeamMember && fiscalYear === Number(currentFiscalYear); const divisionName = division?.display_name ?? NO_DATA; - // if (isLoading) { - // return
    Loading...
    ; - // } - return (
    diff --git a/src/components/CANHistoryPanel.test.tsx b/src/components/CANHistoryPanel.test.tsx new file mode 100644 index 0000000000..e69de29bb2 From f18edd1c5860c4d3f12da0d47761d8983535f4bb Mon Sep 17 00:00:00 2001 From: weimiao67 Date: Thu, 23 Jan 2025 22:16:45 -0700 Subject: [PATCH 5/9] style: added horizontal spacing --- frontend/src/components/CANs/CANDetailView/CANDetailView.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx b/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx index 14b6e6ff82..a474d5d5bc 100644 --- a/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx +++ b/frontend/src/components/CANs/CANDetailView/CANDetailView.jsx @@ -30,7 +30,10 @@ const CANDetailView = ({ divisionName }) => { return ( -
    +
    {/* // NOTE: Left Column */}
    Date: Fri, 24 Jan 2025 08:42:03 -0600 Subject: [PATCH 6/9] chore: remove failing unit-test --- .../CANHistoryPanel/CANHIstoryPanel.test.jsx | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 frontend/src/components/CANs/CANHistoryPanel/CANHIstoryPanel.test.jsx diff --git a/frontend/src/components/CANs/CANHistoryPanel/CANHIstoryPanel.test.jsx b/frontend/src/components/CANs/CANHistoryPanel/CANHIstoryPanel.test.jsx deleted file mode 100644 index 0866c4e6f5..0000000000 --- a/frontend/src/components/CANs/CANHistoryPanel/CANHIstoryPanel.test.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { render, screen } from "@testing-library/react"; -// import userEvent from "@testing-library/user-event"; -import CANHistoryPanel from "./CANHistoryPanel"; - -describe("CANHistoryPanel", () => { - // const user = userEvent.setup(); - - beforeEach(() => { - // Mock any necessary providers or props here - }); - - it("renders without crashing", () => { - render(); - expect(screen.getByRole("region")).toBeInTheDocument(); - }); - - it("displays CAN history data when provided", () => { - const mockData = [ - { timestamp: "2023-01-01T00:00:00", id: "0x123", data: "0xFF 0x00" }, - { timestamp: "2023-01-01T00:00:01", id: "0x456", data: "0x01 0x02" } - ]; - - render(); - - expect(screen.getByText("0x123")).toBeInTheDocument(); - expect(screen.getByText("0x456")).toBeInTheDocument(); - }); - - // it('handles filter input changes', async () => { - // render(); - - // const filterInput = screen.getByRole('textbox', { name: /filter/i }); - // await user.type(filterInput, '0x123'); - - // expect(filterInput).toHaveValue('0x123'); - // }); - - it("shows empty state when no data is provided", () => { - render(); - - expect(screen.getByText(/no history/i)).toBeInTheDocument(); - }); - - // it("allows toggling of auto-scroll", async () => { - // render(); - - // const autoScrollToggle = screen.getByRole("checkbox", { name: /auto-scroll/i }); - // await user.click(autoScrollToggle); - - // expect(autoScrollToggle).toBeChecked(); - // }); -}); From 9e4b9199802a5646d44d735872a642e37243861c Mon Sep 17 00:00:00 2001 From: "Frank Pigeon Jr." Date: Fri, 24 Jan 2025 08:49:56 -0600 Subject: [PATCH 7/9] test: add provider for can detail --- .../CANs/CANDetailView/CANDetailView.test.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/CANs/CANDetailView/CANDetailView.test.jsx b/frontend/src/components/CANs/CANDetailView/CANDetailView.test.jsx index 6a91988aff..b4af52f1a6 100644 --- a/frontend/src/components/CANs/CANDetailView/CANDetailView.test.jsx +++ b/frontend/src/components/CANs/CANDetailView/CANDetailView.test.jsx @@ -1,8 +1,11 @@ import { describe, it, expect } from "vitest"; import { render, screen } from "@testing-library/react"; import CANDetailView from "./CANDetailView"; +import { Provider } from "react-redux"; +import store from "../../../store"; const mockProps = { + canId: 500, description: "Test CAN Description", number: "CAN-123", nickname: "Test Nickname", @@ -29,9 +32,9 @@ const mockProps = { describe("CANDetailView", () => { it("renders all CAN details correctly", () => { render( -
    + -
    + ); // Check for basic text content @@ -51,9 +54,9 @@ describe("CANDetailView", () => { it("renders history section", () => { render( -
    + -
    + ); expect(screen.getByText("History")).toBeInTheDocument(); @@ -62,12 +65,12 @@ describe("CANDetailView", () => { it("renders without team leaders", () => { render( -
    + -
    + ); // Verify other content still renders From 2cde7c8ad8c1a1d22078a85572dff93f5d749be2 Mon Sep 17 00:00:00 2001 From: "Frank Pigeon Jr." Date: Mon, 27 Jan 2025 09:13:11 -0600 Subject: [PATCH 8/9] chore: cleanup --- .../src/components/CANs/CANDetailView.jsx | 0 frontend/src/pages/cans/detail/CanDetail.jsx | 22 +++++++++---------- src/components/CANHistoryPanel.test.tsx | 0 3 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 frontend/src/components/CANs/CANDetailView.jsx delete mode 100644 src/components/CANHistoryPanel.test.tsx diff --git a/frontend/src/components/CANs/CANDetailView.jsx b/frontend/src/components/CANs/CANDetailView.jsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/pages/cans/detail/CanDetail.jsx b/frontend/src/pages/cans/detail/CanDetail.jsx index 2a2ddf01b4..d1f182e280 100644 --- a/frontend/src/pages/cans/detail/CanDetail.jsx +++ b/frontend/src/pages/cans/detail/CanDetail.jsx @@ -83,18 +83,16 @@ const CanDetail = ({ toggleEditMode={toggleEditMode} /> ) : ( - <> - - + )}
    ); diff --git a/src/components/CANHistoryPanel.test.tsx b/src/components/CANHistoryPanel.test.tsx deleted file mode 100644 index e69de29bb2..0000000000 From f9cc1a1190ae5d8c4973d7ec865ee3c61fdccd53 Mon Sep 17 00:00:00 2001 From: "Frank Pigeon Jr." Date: Mon, 27 Jan 2025 10:18:06 -0600 Subject: [PATCH 9/9] style: match grid for detail views --- .../details/AgreementDetailsView.jsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/pages/agreements/details/AgreementDetailsView.jsx b/frontend/src/pages/agreements/details/AgreementDetailsView.jsx index f3fb7eda5e..276da398a1 100644 --- a/frontend/src/pages/agreements/details/AgreementDetailsView.jsx +++ b/frontend/src/pages/agreements/details/AgreementDetailsView.jsx @@ -1,6 +1,7 @@ import PropTypes from "prop-types"; import AgreementHistoryPanel from "../../../components/Agreements/AgreementDetails/AgreementHistoryPanel"; import Tag from "../../../components/UI/Tag/Tag"; +import { NO_DATA } from "../../../constants"; import { convertCodeForDisplay } from "../../../helpers/utils"; /** @@ -12,20 +13,21 @@ import { convertCodeForDisplay } from "../../../helpers/utils"; * @returns {JSX.Element} - The rendered component. */ const AgreementDetailsView = ({ agreement, projectOfficer }) => { - const MISSING_VALUE_TEXT = "TBD"; - return (
    -
    +
    {/* // NOTE: Left Column */}
    Description
    - {agreement?.description ? agreement.description : MISSING_VALUE_TEXT} + {agreement?.description ? agreement.description : NO_DATA}

    Notes

    @@ -49,7 +51,7 @@ const AgreementDetailsView = ({ agreement, projectOfficer }) => {
    {/* // NOTE: Right Column */} @@ -68,7 +70,7 @@ const AgreementDetailsView = ({ agreement, projectOfficer }) => { text={ agreement?.product_service_code?.name ? agreement.product_service_code.name - : MISSING_VALUE_TEXT + : NO_DATA } /> @@ -82,7 +84,7 @@ const AgreementDetailsView = ({ agreement, projectOfficer }) => { text={ agreement?.product_service_code?.naics ? `${agreement.product_service_code.naics}` - : MISSING_VALUE_TEXT + : NO_DATA } /> @@ -95,7 +97,7 @@ const AgreementDetailsView = ({ agreement, projectOfficer }) => { text={ agreement?.product_service_code?.support_code ? agreement?.product_service_code?.support_code - : MISSING_VALUE_TEXT + : NO_DATA } /> @@ -121,7 +123,7 @@ const AgreementDetailsView = ({ agreement, projectOfficer }) => { text={ agreement?.agreement_reason ? convertCodeForDisplay("agreementReason", agreement?.agreement_reason) - : MISSING_VALUE_TEXT + : NO_DATA } /> @@ -146,7 +148,7 @@ const AgreementDetailsView = ({ agreement, projectOfficer }) => { text={ projectOfficer && Object.keys(projectOfficer).length !== 0 ? projectOfficer?.full_name - : MISSING_VALUE_TEXT + : NO_DATA } /> @@ -171,7 +173,7 @@ const AgreementDetailsView = ({ agreement, projectOfficer }) => {
    )}