diff --git a/.storybook/dev-fonts.scss b/.storybook/dev-fonts.scss deleted file mode 100644 index 6ea8815f77..0000000000 --- a/.storybook/dev-fonts.scss +++ /dev/null @@ -1,4 +0,0 @@ -body { - font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; -} diff --git a/.storybook/preview.js b/.storybook/preview.js index 7fd3e1eec6..2d09129343 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,3 @@ -import "./dev-fonts.scss"; import "../src/components/components.scss"; import "@danskernesdigitalebibliotek/dpl-design-system/build/css/base.css"; import { diff --git a/package.json b/package.json index eeae433cf0..e17525ac03 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@types/node": "^20.11.5", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", + "@types/react-flatpickr": "^3.8.11", "@types/react-redux": "^7.1.24", "@typescript-eslint/eslint-plugin": "^5.23.0", "@typescript-eslint/parser": "^6.19.0", @@ -143,7 +144,7 @@ "prop-types": "Since we use former ddb-react components that depend on prop-types we keep this. Should be removed when usage of prop-types is deprecated." }, "dependencies": { - "@danskernesdigitalebibliotek/dpl-design-system": "^2024.4.0-174d424951b2cc03e92bd2522b707f59226e8f12", + "@danskernesdigitalebibliotek/dpl-design-system": "^2024.5.0-d17ec00d0db46973e99a8643a92b11e4eeb183d0", "@reach/alert": "^0.17.0", "@reach/dialog": "^0.18.0", "@reduxjs/toolkit": "^1.9.7", @@ -161,6 +162,7 @@ "react-device-detect": "^2.2.3", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", + "react-flatpickr": "^3.10.13", "react-query": "^3.39.3", "react-redux": "^8.1.3", "react-use": "^17.4.3", diff --git a/src/apps/dashboard/dashboard-notification-list/dashboard-notification-list.tsx b/src/apps/dashboard/dashboard-notification-list/dashboard-notification-list.tsx index 25058585f6..ea3351accf 100644 --- a/src/apps/dashboard/dashboard-notification-list/dashboard-notification-list.tsx +++ b/src/apps/dashboard/dashboard-notification-list/dashboard-notification-list.tsx @@ -18,7 +18,6 @@ import DeleteReservationModal, { deleteReservationModalId } from "../../reservation-list/modal/delete-reservation/delete-reservation-modal"; import Notifications from "./Notifications"; -import AcceptModal from "../../../components/accept-fees-modal/AcceptFeesModal"; import useReservations from "../../../core/utils/useReservations"; import useLoans from "../../../core/utils/useLoans"; import { ReservationType } from "../../../core/utils/types/reservation-type"; @@ -57,7 +56,6 @@ const DashboardNotificationList: FC = ({ farFromOverdue: loansFarFromOverdue } } = useLoans(); - const [accepted, setAccepted] = useState(false); const [reservationsForDeleting, setReservationsForDeleting] = useState< ReservationType[] >([]); @@ -65,7 +63,7 @@ const DashboardNotificationList: FC = ({ const [modalHeader, setModalHeader] = useState(""); const { open } = useModalButtonHandler(); - const { acceptModal, dueDateModal, deleteReservations } = getModalIds(); + const { dueDateModal, deleteReservations } = getModalIds(); const [dueDate, setDueDate] = useState(null); const [modalLoan, setModalLoan] = useState(null); const [reservationForModal, setReservationForModal] = @@ -178,10 +176,6 @@ const DashboardNotificationList: FC = ({ } ]; - const openAcceptModal = useCallback(() => { - open(`${acceptModal}`); - }, [acceptModal, open]); - const dashboardNotificationsReservations = [ { listLength: reservationsReadyToLoan.length, @@ -207,9 +201,6 @@ const DashboardNotificationList: FC = ({ : openModalHandler(reservationsQueueID as string) } ]; - const resetAccepted = () => { - setAccepted(false); - }; return ( <> @@ -254,9 +245,6 @@ const DashboardNotificationList: FC = ({ )} {dueDate && loans && loansToDisplay && ( resetAccepted()} - openAcceptModal={openAcceptModal} pageSize={pageSize} openDetailsModal={openLoanDetailsModal} dueDate={dueDate} @@ -296,7 +284,6 @@ const DashboardNotificationList: FC = ({ /> )} - setAccepted(true)} /> ); }; diff --git a/src/apps/dashboard/dashboard.dev.tsx b/src/apps/dashboard/dashboard.dev.tsx index 34e78a5651..c93f593c77 100644 --- a/src/apps/dashboard/dashboard.dev.tsx +++ b/src/apps/dashboard/dashboard.dev.tsx @@ -59,7 +59,7 @@ export default { control: { type: "text" } }, totalAmountFeeText: { - defaultValue: "@total,-", + defaultValue: "@total DKK", control: { type: "text" } }, physicalLoansText: { diff --git a/src/apps/dashboard/dashboard.test.tsx b/src/apps/dashboard/dashboard.test.tsx index 640e636263..8ef9da08a2 100644 --- a/src/apps/dashboard/dashboard.test.tsx +++ b/src/apps/dashboard/dashboard.test.tsx @@ -1363,7 +1363,7 @@ describe("Dashboard", () => { cy.getBySel("warning-bar-text").should("have.text", "You owe in total"); // The amount the patron ows - cy.getBySel("warning-bar-right-text").should("have.text", "265,06,-"); + cy.getBySel("warning-bar-right-text").should("have.text", "265,06 DKK"); // A pay button that links to fees page cy.getBySel("warning-bar-right-link") diff --git a/src/apps/fee-list/FeeList.dev.tsx b/src/apps/fee-list/FeeList.dev.tsx index 49551a0e8d..3fb6b77fbe 100644 --- a/src/apps/fee-list/FeeList.dev.tsx +++ b/src/apps/fee-list/FeeList.dev.tsx @@ -62,7 +62,7 @@ export default { control: { type: "text" } }, totalText: { - defaultValue: "Total @total,-", + defaultValue: "Total @total DKK", control: { type: "text" } }, expirationWarningDaysBeforeConfig: { @@ -147,7 +147,7 @@ export default { control: { type: "text" }, - defaultValue: "Fee @fee,-" + defaultValue: "Fee @fee DKK" }, feeCreatedText: { control: { @@ -159,18 +159,27 @@ export default { defaultValue: "https://unsplash.com/photos/JDzoTGfoogA", // Open source image of an adventurous duck control: { type: "text" } }, - paymentOverviewUrl: { - defaultValue: "https://unsplash.com/photos/yjI3ozta2Zk", // Open source image of a fluffy floofer - control: { type: "text" } - }, feeListAlreadyPaidInfoText: { defaultValue: "Already paid? It can take up to 72 hours to register the transaction.", control: { type: "text" } }, + feeListAlreadyPaidSecondInfoText: { + defaultValue: + "Already paid? It can take up to 72 hours to register the transaction. (not payable by user)", + control: { type: "text" } + }, feeListMaterialNumberText: { defaultValue: "# @materialNumber", control: { type: "text" } + }, + feeListPaymentSiteUrl: { + defaultValue: "https://google.com", + control: { type: "text" } + }, + feeListConfig: { + defaultValue: '{ "paymentSiteButtonLabel": "Go to payment page" }', + control: { type: "text" } } }, decorators: [withQuery] diff --git a/src/apps/fee-list/FeeList.entry.tsx b/src/apps/fee-list/FeeList.entry.tsx index 9d7d3ee72d..975f8821b0 100644 --- a/src/apps/fee-list/FeeList.entry.tsx +++ b/src/apps/fee-list/FeeList.entry.tsx @@ -18,12 +18,14 @@ export interface FeeListProps { emptyFeeListText: string; etAlText: string; feeCreatedText: string; - feeDetailsModalScreenReaderText: string; feeDetailsModalCloseModalAriaLabelText: string; feeDetailsModalDescriptionText: string; + feeDetailsModalScreenReaderText: string; feeListBodyText: string; + feeListConfig: string; feeListDaysText: string; feeListHeadlineText: string; + feeListPaymentSiteUrl: string; feePaymentModalBodyText: string; feePaymentModalCancelText: string; feePaymentModalGotoText: string; @@ -43,10 +45,12 @@ export interface FeeListProps { totalFeeAmountText: string; totalText: string; turnedInText: string; - unpaidFeesPayableByClientHeadlineText: string; unpaidFeesNotPayableByClientHeadlineText: string; + unpaidFeesPayableByClientHeadlineText: string; viewFeesAndCompensationRatesText: string; viewFeesAndCompensationRatesUrl: string; + feeListAlreadyPaidInfoText: string; + feeListAlreadyPaidSecondInfoText: string; } const FeeListEntry: FC< diff --git a/src/apps/fee-list/FeeList.tsx b/src/apps/fee-list/FeeList.tsx index 9ff6cdde31..d94ceea9f3 100644 --- a/src/apps/fee-list/FeeList.tsx +++ b/src/apps/fee-list/FeeList.tsx @@ -17,6 +17,7 @@ import { } from "./utils/helper"; import ListHeader from "../../components/list-header/list-header"; import EmptyList from "../../components/empty-list/empty-list"; +import FeePaymentButton from "./FeePaymentButton"; const FeeList: FC = () => { const t = useText(); @@ -55,12 +56,17 @@ const FeeList: FC = () => {

{t("feeListHeadlineText")}

- - {t("feeListBodyText")}{" "} - - {t("viewFeesAndCompensationRatesText")} - - +
+
{t("feeListBodyText")}
+
+ + {t("viewFeesAndCompensationRatesText")} + +
+
+
+ +
{!fbsFees.length && ( <> { {getFeesBasedOnPayableByClient(fbsFees, true).length > 0 && ( )} {/* List of fees that can only be paid by the user externally */} {getFeesBasedOnPayableByClient(fbsFees, false).length > 0 && ( )} diff --git a/src/apps/fee-list/FeePaymentButton.tsx b/src/apps/fee-list/FeePaymentButton.tsx new file mode 100644 index 0000000000..720bd81ad8 --- /dev/null +++ b/src/apps/fee-list/FeePaymentButton.tsx @@ -0,0 +1,39 @@ +import * as React from "react"; +import { FC } from "react"; +import LinkButton from "../../components/Buttons/LinkButton"; +import { useUrls } from "../../core/utils/url"; +import { useConfig } from "../../core/utils/config"; + +export interface FeePaymentButtonProps { + dataCy?: string; +} + +const FeePaymentButton: FC = ({ + dataCy = "fee-payment-button" +}) => { + const config = useConfig(); + const u = useUrls(); + + const { paymentSiteButtonLabel } = config<{ + paymentSiteButtonLabel: string; + }>("feeListConfig", { + transformer: "jsonParse" + }); + const url = u("feeListPaymentSiteUrl", true); + + if (!url) return null; + + return ( + + {paymentSiteButtonLabel} + + ); +}; + +export default FeePaymentButton; diff --git a/src/apps/fee-list/fee-list.test.ts b/src/apps/fee-list/fee-list.test.ts index 58900fa250..0b3fcc2ca8 100644 --- a/src/apps/fee-list/fee-list.test.ts +++ b/src/apps/fee-list/fee-list.test.ts @@ -123,7 +123,7 @@ describe("Fee list", () => { // 2.b text "Overdue fees and replacement costs that were created before dd/mm/åååå can still be paid on this page. See our fees and replacement costs" cy.getBySel("fee-list-body").should( "have.text", - "Overdue fees and replacement costs that were created before 27/10/2020 can still be paid on this page. See our fees and replacement costs" + "Overdue fees and replacement costs that were created before 27/10/2020 can still be paid on this page.See our fees and replacement costs" ); // 2.c // 2.e subheadline "Unsettled debt 1" @@ -213,7 +213,7 @@ describe("Fee list", () => { .find(".list-reservation__fee") .find(".text-body-medium-regular") .should("exist") - .should("have.text", "Fee 70,-"); + .should("have.text", "Fee 70 DKK"); // 4.b +x other materials cy.getBySel("fee-list-page") diff --git a/src/apps/fee-list/list.tsx b/src/apps/fee-list/list.tsx index cee0911bb4..67a6efb8bd 100644 --- a/src/apps/fee-list/list.tsx +++ b/src/apps/fee-list/list.tsx @@ -3,7 +3,6 @@ import ListHeader from "../../components/list-header/list-header"; import { FeeV2 } from "../../core/fbs/model"; import StackableFees from "./stackable-fees/stackable-fees"; import { FaustId } from "../../core/utils/types/ids"; -import { useText } from "../../core/utils/text"; interface ListProps { openDetailsModalClickEvent: (faustId: string) => void; @@ -11,20 +10,22 @@ interface ListProps { dataCy: string; listHeader: ReactNode; totalText: string; + className?: string; + alreadyPaidText: string; } const List: FC = ({ openDetailsModalClickEvent, fees, listHeader, dataCy, - totalText + totalText, + className, + alreadyPaidText }) => { - const t = useText(); - return (
{fees && ( -
+
{fees.map((itemData) => ( = ({

- {t("feeListAlreadyPaidInfoText")} + {alreadyPaidText}

{totalText}

diff --git a/src/apps/fee-list/modal/my-payment-overview-modal.tsx b/src/apps/fee-list/modal/my-payment-overview-modal.tsx index 87390e7c86..7c68401437 100644 --- a/src/apps/fee-list/modal/my-payment-overview-modal.tsx +++ b/src/apps/fee-list/modal/my-payment-overview-modal.tsx @@ -8,7 +8,10 @@ import { useUrls } from "../../../core/utils/url"; const MyPaymentOverviewModal: FC = () => { const t = useText(); const u = useUrls(); - const paymentOverviewUrl = u("paymentOverviewUrl"); + // This whole component seems deprecated. + // TODO: Investigate if it should be removed. + const feeListPaymentSiteUrl = u("feeListPaymentSiteUrl", true); + const { close } = useModalButtonHandler(); const handleClick = () => { @@ -33,14 +36,16 @@ const MyPaymentOverviewModal: FC = () => {

- - {t("feePaymentModalGotoText")}{" "} - - + {feeListPaymentSiteUrl && ( + + {t("feePaymentModalGotoText")}{" "} + + + )}
{openDetailsModal && ( -
- -
+ )}
diff --git a/src/apps/loan-list/materials/stackable-material/material-info.tsx b/src/apps/loan-list/materials/stackable-material/material-info.tsx index 0eccfb7692..302993120a 100644 --- a/src/apps/loan-list/materials/stackable-material/material-info.tsx +++ b/src/apps/loan-list/materials/stackable-material/material-info.tsx @@ -61,7 +61,10 @@ const MaterialInfo: FC = ({ handleDetailsModal(e); }} onKeyUp={(e) => { - if (e.key === "Enter" || e.key === "Space") { + // `!focused` prevents opening material details modal after clicking + // enter on pager. Pager gives focus to the next stackable material too + // quickly while still registering the enter key press. + if ((e.key === "Enter" || e.key === "Space") && !focused) { handleDetailsModal(e); } }} diff --git a/src/apps/loan-list/materials/stackable-material/stackable-material.tsx b/src/apps/loan-list/materials/stackable-material/stackable-material.tsx index 2f49ba5d94..7f45436bd5 100644 --- a/src/apps/loan-list/materials/stackable-material/stackable-material.tsx +++ b/src/apps/loan-list/materials/stackable-material/stackable-material.tsx @@ -48,7 +48,10 @@ const StackableMaterial: FC = ({ role="button" onClick={handleOpenDueDateModal} onKeyUp={(e) => { - if (e.key === "Enter" || e.key === "Space") { + // `!focused` prevents opening material details modal after clicking + // enter on pager. Pager gives focus to the next stackable material too + // quickly while still registering the enter key press. + if ((e.key === "Enter" || e.key === "Space") && !focused) { handleOpenDueDateModal(); } }} diff --git a/src/apps/patron-page/PatronPage.dev.tsx b/src/apps/patron-page/PatronPage.dev.tsx index fccf9eca60..a7af53682c 100644 --- a/src/apps/patron-page/PatronPage.dev.tsx +++ b/src/apps/patron-page/PatronPage.dev.tsx @@ -70,12 +70,12 @@ export default { defaultValue: "Close pause reservations modal", control: { type: "text" } }, - dateInputsStartDateLabelText: { - defaultValue: "Start date", + pauseReservationModalDateRangeLabelText: { + defaultValue: "Pause period", control: { type: "text" } }, - dateInputsEndDateLabelText: { - defaultValue: "End date", + pauseReservationModalDateRangePlaceholderText: { + defaultValue: "Choose pause period", control: { type: "text" } }, pauseReservationModalLinkText: { diff --git a/src/apps/patron-page/PatronPage.entry.tsx b/src/apps/patron-page/PatronPage.entry.tsx index 7b887476d5..8c4987a3b0 100644 --- a/src/apps/patron-page/PatronPage.entry.tsx +++ b/src/apps/patron-page/PatronPage.entry.tsx @@ -25,54 +25,54 @@ export interface PatronPageUrlProps { } interface PatronPageTextProps { - patronPageHeaderText: string; - pauseReservationModalHeaderText: string; - pauseReservationModalBodyText: string; - pauseReservationModalCloseModalText: string; - patronPinSavedSuccessText: string; - dateInputsStartDateLabelText: string; - dateInputsEndDateLabelText: string; - pauseReservationModalLinkText: string; - pauseReservationModalSaveButtonLabelText: string; - pauseReservationModalCancelButtonLabelText: string; - patronPageBasicDetailsHeaderText: string; - patronPageBasicDetailsNameLabelText: string; - patronPageBasicDetailsAddressLabelText: string; + patronContactEmailCheckboxText: string; + patronContactEmailLabelText: string; patronContactInfoHeaderText: string; - patronContactPhoneLabelText: string; patronContactPhoneCheckboxText: string; - patronContactEmailLabelText: string; - patronContactEmailCheckboxText: string; - patronPageStatusSectionHeaderText: string; - patronPageStatusSectionBodyText: string; - patronPageStatusSectionLinkText: string; - patronPageStatusSectionLoanHeaderText: string; - patronPageStatusSectionLoansEbooksText: string; - patronPageStatusSectionLoansAudioBooksText: string; - patronPageChangePickupHeaderText: string; + patronContactPhoneLabelText: string; + patronPageBasicDetailsAddressLabelText: string; + patronPageBasicDetailsHeaderText: string; + patronPageBasicDetailsNameLabelText: string; patronPageChangePickupBodyText: string; - pickupBranchesDropdownLabelText: string; - pickupBranchesDropdownNothingSelectedText: string; - patronPagePauseReservationsHeaderText: string; - patronPagePauseReservationsBodyText: string; - patronPageOpenPauseReservationsSectionText: string; - patronPageOpenPauseReservationsSectionAriaText: string; - patronPageChangePincodeHeaderText: string; + patronPageChangePickupHeaderText: string; patronPageChangePincodeBodyText: string; - patronPagePincodeLabelText: string; + patronPageChangePincodeHeaderText: string; patronPageConfirmPincodeLabelText: string; - patronPagePincodeTooShortValidationText: string; + patronPageDeleteProfileLinkText: string; + patronPageDeleteProfileText: string; + patronPageHandleResponseInformationText: string; + patronPageHeaderText: string; + patronPageLoadingText: string; + patronPageOpenPauseReservationsSectionAriaText: string; + patronPageOpenPauseReservationsSectionText: string; + patronPagePauseReservationsBodyText: string; + patronPagePauseReservationsHeaderText: string; + patronPagePhoneInputMessageText: string; + patronPagePincodeLabelText: string; patronPagePincodesNotTheSameText: string; + patronPagePincodeTooShortValidationText: string; patronPageSaveButtonText: string; - patronPageDeleteProfileText: string; - patronPageDeleteProfileLinkText: string; - patronPageStatusSectionReservationsText: string; - patronPageStatusSectionOutOfText: string; + patronPageStatusSectionBodyText: string; + patronPageStatusSectionHeaderText: string; + patronPageStatusSectionLinkText: string; + patronPageStatusSectionLoanHeaderText: string; + patronPageStatusSectionLoansAudioBooksText: string; + patronPageStatusSectionLoansEbooksText: string; patronPageStatusSectionOutOfAriaLabelAudioBooksText: string; patronPageStatusSectionOutOfAriaLabelEbooksText: string; - patronPagePhoneInputMessageText: string; - patronPageHandleResponseInformationText: string; - patronPageLoadingText: string; + patronPageStatusSectionOutOfText: string; + patronPageStatusSectionReservationsText: string; + patronPinSavedSuccessText: string; + pauseReservationModalBodyText: string; + pauseReservationModalCancelButtonLabelText: string; + pauseReservationModalCloseModalText: string; + pauseReservationModalDateRangeLabelText: string; + pauseReservationModalDateRangePlaceholderText: string; + pauseReservationModalHeaderText: string; + pauseReservationModalLinkText: string; + pauseReservationModalSaveButtonLabelText: string; + pickupBranchesDropdownLabelText: string; + pickupBranchesDropdownNothingSelectedText: string; } export interface PatronPageProps diff --git a/src/apps/reservation-list/list/reservation-list.dev.tsx b/src/apps/reservation-list/list/reservation-list.dev.tsx index 28519ee3a0..5654f213b4 100644 --- a/src/apps/reservation-list/list/reservation-list.dev.tsx +++ b/src/apps/reservation-list/list/reservation-list.dev.tsx @@ -224,14 +224,6 @@ export default { defaultValue: "Close pause reservations modal", control: { type: "text" } }, - dateInputsStartDateLabelText: { - defaultValue: "Start date", - control: { type: "text" } - }, - dateInputsEndDateLabelText: { - defaultValue: "End date", - control: { type: "text" } - }, pauseReservationModalLinkText: { defaultValue: "Read more about pausing reservertions and what that means here", diff --git a/src/apps/reservation-list/list/reservation-list.entry.tsx b/src/apps/reservation-list/list/reservation-list.entry.tsx index 3ef4de57cd..5a1de6f340 100644 --- a/src/apps/reservation-list/list/reservation-list.entry.tsx +++ b/src/apps/reservation-list/list/reservation-list.entry.tsx @@ -25,48 +25,48 @@ export interface ReservationListConfigProps { } export interface ReservationListTextProps { - reservationListHeaderText: string; - physicalLoansTitleText: string; - reservationListReadyText: string; - materialByAuthorText: string; - materialAndAuthorText: string; - reservationDetailsExpiresText: string; etAlText: string; - reservationListNumberInQueueText: string; - reservationListFirstInQueueText: string; - reservationListDigitalPickupText: string; expiresSoonText: string; - reservationListInQueueText: string; - reservationPickUpLatestText: string; - publizonEbookText: string; + materialAndAuthorText: string; + materialByAuthorText: string; + pauseReservationModalBodyText: string; + pauseReservationModalCancelButtonLabelText: string; + pauseReservationModalCloseModalText: string; + pauseReservationModalDateRangeLabelText: string; + pauseReservationModalDateRangePlaceholderText: string; + pauseReservationModalHeaderText: string; + pauseReservationModalLinkText: string; + pauseReservationModalSaveButtonLabelText: string; + physicalLoansTitleText: string; publizonAudioBookText: string; + publizonEbookText: string; publizonPodcastText: string; - reservationListLoanBeforeText: string; - reservationListDaysText: string; - reservationListDayText: string; - reservationListAvailableInText: string; + reservationDetailsExpiresText: string; reservationDetailsExpiresTitleText: string; reservationDetailsOthersInQueueText: string; - reservationListPauseReservationButtonText: string; - reservationListPauseReservationText: string; - reservationListPauseReservationOnHoldText: string; + reservationListAllEmptyText: string; + reservationListAvailableInText: string; + reservationListDaysText: string; + reservationListDayText: string; + reservationListDigitalPickupText: string; + reservationListDigitalReservationsEmptyText: string; + reservationListDigitalReservationsHeaderText: string; + reservationListFirstInQueueText: string; + reservationListHeaderText: string; + reservationListInQueueText: string; + reservationListLoanBeforeText: string; + reservationListNumberInQueueText: string; reservationListOnHoldAriaText: string; reservationListPauseReservationAriaModalText: string; - pauseReservationModalHeaderText: string; - pauseReservationModalBodyText: string; - pauseReservationModalCloseModalText: string; - dateInputsStartDateLabelText: string; - dateInputsEndDateLabelText: string; - pauseReservationModalLinkText: string; - pauseReservationModalSaveButtonLabelText: string; - pauseReservationModalCancelButtonLabelText: string; - reservationListReadyForPickupTitleText: string; - reservationListReadyForPickupEmptyText: string; + reservationListPauseReservationButtonText: string; + reservationListPauseReservationOnHoldText: string; + reservationListPauseReservationText: string; reservationListPhysicalReservationsEmptyText: string; reservationListPhysicalReservationsHeaderText: string; - reservationListDigitalReservationsEmptyText: string; - reservationListDigitalReservationsHeaderText: string; - reservationListAllEmptyText: string; + reservationListReadyForPickupEmptyText: string; + reservationListReadyForPickupTitleText: string; + reservationListReadyText: string; + reservationPickUpLatestText: string; } export interface ReservationListEntryWithPageSizeProps diff --git a/src/apps/reservation-list/modal/delete-reservation/pause-reservations.test.ts b/src/apps/reservation-list/modal/delete-reservation/pause-reservations.test.ts index 35e6f1bdc4..d7ec313996 100644 --- a/src/apps/reservation-list/modal/delete-reservation/pause-reservations.test.ts +++ b/src/apps/reservation-list/modal/delete-reservation/pause-reservations.test.ts @@ -80,13 +80,9 @@ describe("Pause reservation modal test", () => { "have.text", "Pause your reservations early, since reservations that are already being processed, will not be paused." ); - // ID 12 2.c. datepicker: start date - cy.get(".modal.modal-cta [data-cy='start-date']") - .should("exist") - .should("have.attr", "value"); - // ID 12 2.e. datepicker: end date - cy.get(".modal.modal-cta [data-cy='end-date']").should("exist"); + // There should be a date range component for choosing a pause period. + cy.getBySel("date-range").should("exist"); // ID 12 2.b. text: link "read more" cy.get(".modal") diff --git a/src/apps/reservation-list/modal/pause-reservation/pause-reservation.tsx b/src/apps/reservation-list/modal/pause-reservation/pause-reservation.tsx index f6477e474c..83f0ce903b 100644 --- a/src/apps/reservation-list/modal/pause-reservation/pause-reservation.tsx +++ b/src/apps/reservation-list/modal/pause-reservation/pause-reservation.tsx @@ -9,9 +9,9 @@ import { getGetPatronInformationByPatronIdV2QueryKey } from "../../../../core/fbs/fbs"; import { Patron, PatronV5 } from "../../../../core/fbs/model"; -import DateInputs from "../../../../components/date-inputs/date-inputs"; import { useUrls } from "../../../../core/utils/url"; import { getModalIds } from "../../../../core/utils/helpers/modal-helpers"; +import DateRangeInput from "../../../../components/date-inputs/DateRangeInput"; interface PauseReservationProps { id: string; @@ -100,8 +100,8 @@ const PauseReservation: FC = ({ id, user }) => {

{t("pauseReservationModalHeaderText")}

-
-

+

+

{t("pauseReservationModalBodyText")}

@@ -109,28 +109,37 @@ const PauseReservation: FC = ({ id, user }) => { id={saveFormId} onSubmit={(e) => { e.preventDefault(); - save(startDate, endDate); + if (startDate && endDate) { + save(startDate, endDate); + } }} > - +
+ +
-
+ +

{t("pauseReservationModalBelowInputsText")} - - {t("pauseReservationModalLinkText")} -

+

+ + {t("pauseReservationModalLinkText")} + +

); }; diff --git a/src/components/reservation/forms/ModalReservationFormSelect.tsx b/src/components/reservation/forms/ModalReservationFormSelect.tsx index 8d90de793c..6c8d2e5413 100644 --- a/src/components/reservation/forms/ModalReservationFormSelect.tsx +++ b/src/components/reservation/forms/ModalReservationFormSelect.tsx @@ -1,24 +1,36 @@ import React, { useState, ChangeEvent } from "react"; +import clsx from "clsx"; import Dropdown from "../../Dropdown/Dropdown"; import Modal, { useModalButtonHandler } from "../../../core/utils/modal"; import { useText, UseTextFunction } from "../../../core/utils/text"; -import { modalReservationFormId, ModalReservationFormTextType } from "./helper"; +import { + modalReservationFormId, + modalReservationFormSelectTypeIsInterestPeriod, + ModalReservationFormTextType +} from "./helper"; import ReservationForm from "./ReservationForm"; import { getReservationModalTypeTranslation } from "../helper"; import { RequestStatus } from "../../../core/utils/types/request"; import ModalMessage from "../../message/modal-message/ModalMessage"; +import { FormSelectValue } from "./types"; -export interface ModalReservationFormSelectProps { +export interface ModalReservationFormSelectProps< + TValue extends FormSelectValue +> { type: ModalReservationFormTextType; header: { title: string; description: string[]; }; - items: { label: string; value: string }[]; - defaultSelectedItem: string; - selectHandler: (value: string) => void; + items: { label: string; value: TValue }[]; + defaultSelectedItem: TValue; + selectHandler: ( + value: TSelectValue + ) => void; ariaLabel: string; - saveCallback?: () => void; + saveCallback?: ( + value: TSaveValue + ) => void; reservationStatus?: RequestStatus; setReservationStatus?: (status: RequestStatus) => void; } @@ -36,7 +48,7 @@ const modalProps = ( ) }); -const ModalReservationFormSelect = ({ +const ModalReservationFormSelect = ({ type, header, items, @@ -46,20 +58,23 @@ const ModalReservationFormSelect = ({ saveCallback, reservationStatus, setReservationStatus -}: ModalReservationFormSelectProps) => { +}: ModalReservationFormSelectProps) => { const { close } = useModalButtonHandler(); const t = useText(); - const [selectedItem, setSelectedItem] = useState(defaultSelectedItem); + const [selectedItem, setSelectedItem] = useState(defaultSelectedItem); const selectChange = (event: ChangeEvent) => { const { value } = event.target; - setSelectedItem(value); + const typedValue = modalReservationFormSelectTypeIsInterestPeriod(type) + ? Number(value) + : value; + setSelectedItem(typedValue as TValue); }; const onSubmit = () => { selectHandler(selectedItem); if (saveCallback) { - saveCallback(); + saveCallback(selectedItem); } else { close(modalReservationFormId(type)); } @@ -78,6 +93,12 @@ const ModalReservationFormSelect = ({ if (setReservationStatus) setReservationStatus("idle"); } }} + classNames={clsx([ + { + "modal-cta modal-padding": + reservationStatus === "success" || reservationStatus === "error" + } + ])} > {reservationStatus === "success" && ( - options={items.map(({ label, value }) => ({ label, value diff --git a/src/components/reservation/forms/NoInterestAfterModal.tsx b/src/components/reservation/forms/NoInterestAfterModal.tsx index f63fa63d6a..0a00f113df 100644 --- a/src/components/reservation/forms/NoInterestAfterModal.tsx +++ b/src/components/reservation/forms/NoInterestAfterModal.tsx @@ -4,11 +4,12 @@ import ModalReservationFormSelect from "./ModalReservationFormSelect"; import { useConfig } from "../../../core/utils/config"; import { RequestStatus } from "../../../core/utils/types/request"; import { Periods } from "../types"; +import { FormSelectValue } from "./types"; -export interface PickupModalProps { - selectedInterest: number | string; +interface NoInterestAfterModalProps { + selectedInterest: number; setSelectedInterest: (value: number) => void; - saveCallback?: () => void; + saveCallback?: (value: TValue) => void; reservationStatus?: RequestStatus; setReservationStatus?: (status: RequestStatus) => void; } @@ -19,7 +20,7 @@ const NoInterestAfterModal = ({ saveCallback, reservationStatus, setReservationStatus -}: PickupModalProps) => { +}: NoInterestAfterModalProps) => { const t = useText(); const config = useConfig(); const interstPeriods = config("interestPeriodsConfig", { @@ -27,7 +28,7 @@ const NoInterestAfterModal = ({ }); return ( - type="interestPeriod" header={{ title: t("modalReservationFormNoInterestAfterHeaderTitleText"), @@ -36,8 +37,10 @@ const NoInterestAfterModal = ({ ] }} items={interstPeriods.interestPeriods} - defaultSelectedItem={String(selectedInterest)} - selectHandler={(value: string) => setSelectedInterest(Number(value))} + defaultSelectedItem={selectedInterest} + selectHandler={(value: FormSelectValue) => { + setSelectedInterest(Number(value)); + }} ariaLabel={t("modalReservationFormNoInterestAfterLabelText")} saveCallback={saveCallback} reservationStatus={reservationStatus} diff --git a/src/components/reservation/forms/PickupModal.tsx b/src/components/reservation/forms/PickupModal.tsx index c1e73a5163..d2211ef098 100644 --- a/src/components/reservation/forms/PickupModal.tsx +++ b/src/components/reservation/forms/PickupModal.tsx @@ -3,12 +3,13 @@ import { AgencyBranch } from "../../../core/fbs/model"; import { useText } from "../../../core/utils/text"; import ModalReservationFormSelect from "./ModalReservationFormSelect"; import { RequestStatus } from "../../../core/utils/types/request"; +import { FormSelectValue } from "./types"; export interface PickupModalProps { branches: AgencyBranch[]; defaultBranch: string; selectBranchHandler: (value: string) => void; - saveCallback?: () => void; + saveCallback?: (value: TValue) => void; reservationStatus?: RequestStatus; setReservationStatus?: (status: RequestStatus) => void; } @@ -29,7 +30,7 @@ const PickupModal = ({ })); return ( - type="pickup" header={{ title: t("modalReservationFormPickupHeaderTitleText"), @@ -37,7 +38,9 @@ const PickupModal = ({ }} items={formatBranches} defaultSelectedItem={defaultBranch} - selectHandler={selectBranchHandler} + selectHandler={(value: FormSelectValue) => { + selectBranchHandler(String(value)); + }} ariaLabel={t("modalReservationFormPickupLabelText")} saveCallback={saveCallback} reservationStatus={reservationStatus} diff --git a/src/components/reservation/forms/helper.ts b/src/components/reservation/forms/helper.ts index ca7fdac9f2..57fb88b84f 100644 --- a/src/components/reservation/forms/helper.ts +++ b/src/components/reservation/forms/helper.ts @@ -122,4 +122,10 @@ export const saveText = ({ }); }; +export function modalReservationFormSelectTypeIsInterestPeriod( + type: ModalReservationFormTextType +): type is "interestPeriod" { + return type === "interestPeriod"; +} + export default {}; diff --git a/src/components/reservation/forms/types.ts b/src/components/reservation/forms/types.ts new file mode 100644 index 0000000000..0eac5c2d75 --- /dev/null +++ b/src/components/reservation/forms/types.ts @@ -0,0 +1 @@ +export type FormSelectValue = string | number; diff --git a/src/components/reservation/types.ts b/src/components/reservation/types.ts index 5780e3ebd3..092203dcf6 100644 --- a/src/components/reservation/types.ts +++ b/src/components/reservation/types.ts @@ -1,6 +1,6 @@ import { Option } from "../Dropdown/Dropdown"; export type Periods = { - interestPeriods: Option[]; - defaultInterestPeriod: Option; + interestPeriods: Option[]; + defaultInterestPeriod: Option; }; diff --git a/src/core/storybook/reservationListArgs.ts b/src/core/storybook/reservationListArgs.ts index 1252956127..679a55b265 100644 --- a/src/core/storybook/reservationListArgs.ts +++ b/src/core/storybook/reservationListArgs.ts @@ -189,16 +189,16 @@ export default { "Pause your reservations early, since reservations that are already being processed, will not be paused.", control: { type: "text" } }, - pauseReservationModalCloseModalText: { - defaultValue: "Close pause reservations modal", + pauseReservationModalDateRangeLabelText: { + defaultValue: "Pause period", control: { type: "text" } }, - dateInputsStartDateLabelText: { - defaultValue: "Start date", + pauseReservationModalDateRangePlaceholderText: { + defaultValue: "Choose pause period", control: { type: "text" } }, - dateInputsEndDateLabelText: { - defaultValue: "End date", + pauseReservationModalCloseModalText: { + defaultValue: "Close pause reservations modal", control: { type: "text" } }, pauseReservationModalLinkText: { diff --git a/src/core/storybook/reservationMaterialDetailsArgs.ts b/src/core/storybook/reservationMaterialDetailsArgs.ts index b168abbb69..896cb9edf8 100644 --- a/src/core/storybook/reservationMaterialDetailsArgs.ts +++ b/src/core/storybook/reservationMaterialDetailsArgs.ts @@ -16,7 +16,7 @@ export default { }, interestPeriodsConfig: { defaultValue: - '{ "interestPeriods":[ { "value":14, "label":"14 days" }, { "value":30, "label":"1 month" }, { "value":60, "label":"2 months" }, { "value":90, "label":"3 months" }, { "value":180, "label":"6 months" }, { "value":365, "label":"1 year" } ], "defaultInterestPeriod":{ "value":"14", "label":"14 days" } }', + '{ "interestPeriods":[ { "value":14, "label":"14 days" }, { "value":30, "label":"1 month" }, { "value":60, "label":"2 months" }, { "value":90, "label":"3 months" }, { "value":180, "label":"6 months" }, { "value":365, "label":"1 year" } ], "defaultInterestPeriod":{ "value":14, "label":"14 days" } }', control: { type: "text" } }, reservationDetailsRemoveDigitalReservationText: { diff --git a/src/core/utils/url.tsx b/src/core/utils/url.tsx index 6359b86f6b..499bd80f60 100644 --- a/src/core/utils/url.tsx +++ b/src/core/utils/url.tsx @@ -8,7 +8,11 @@ export const useUrls = () => { const { data } = useSelector((state: RootState) => state.url); const urls = useMemo(() => turnUrlStringsIntoObjects(data), [data]); - return (name: string) => { + return (name: string, returnFalseIfUndefined = false) => { + if (returnFalseIfUndefined) { + return urls[name] || false; + } + if (!urls[name]) { throw new Error(`The url ${name} is not defined`); } diff --git a/yarn.lock b/yarn.lock index 792a08db55..02e82c0a38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1469,10 +1469,10 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@danskernesdigitalebibliotek/dpl-design-system@^2024.4.0-174d424951b2cc03e92bd2522b707f59226e8f12": - version "2024.4.0-174d424951b2cc03e92bd2522b707f59226e8f12" - resolved "https://npm.pkg.github.com/download/@danskernesdigitalebibliotek/dpl-design-system/2024.4.0-174d424951b2cc03e92bd2522b707f59226e8f12/d41e0060d22bbef5d949bfadf1c8313df43748ea#d41e0060d22bbef5d949bfadf1c8313df43748ea" - integrity sha512-2X6JeQLeIlwmf+qMvr1i9LEudtehJSi8mwa98kqyBC0D9DU/N3XZ1rw3z/BRXjycC0MvVcuj5k1jkLli7RbJ+w== +"@danskernesdigitalebibliotek/dpl-design-system@^2024.5.0-d17ec00d0db46973e99a8643a92b11e4eeb183d0": + version "2024.5.0-d17ec00d0db46973e99a8643a92b11e4eeb183d0" + resolved "https://npm.pkg.github.com/download/@danskernesdigitalebibliotek/dpl-design-system/2024.5.0-d17ec00d0db46973e99a8643a92b11e4eeb183d0/c05ae7ea3496406123ad8aec95d64d8d405655c3#c05ae7ea3496406123ad8aec95d64d8d405655c3" + integrity sha512-5SdeviV9nGYnkFt/TR6wTH/HAKtJJw9GhTsb6dPCrHZAKZDMMkeepjK9uKE8nQP4KEGQm6bw4qY3TVcommzaVw== "@discoveryjs/json-ext@^0.5.0", "@discoveryjs/json-ext@^0.5.3": version "0.5.7" @@ -4646,6 +4646,14 @@ dependencies: "@types/react" "*" +"@types/react-flatpickr@^3.8.11": + version "3.8.11" + resolved "https://registry.yarnpkg.com/@types/react-flatpickr/-/react-flatpickr-3.8.11.tgz#cec7d9afa535bff0631842b286e149f47fbeadfb" + integrity sha512-wXGyGRpUjiGknioxWzWJdNvF2XxKw5lAI7H64Iv7w4iL+1iT7QvAzrigz5FkW4lTg9IJOww6t7g21FzsrmRV6A== + dependencies: + "@types/react" "*" + flatpickr "^4.0.6" + "@types/react-redux@^7.1.24": version "7.1.24" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" @@ -10141,6 +10149,11 @@ flat-cache@^3.1.1: keyv "^4.5.3" rimraf "^3.0.2" +flatpickr@^4.0.6, flatpickr@^4.6.2: + version "4.6.13" + resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.13.tgz#8a029548187fd6e0d670908471e43abe9ad18d94" + integrity sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw== + flatted@^3.1.0: version "3.2.5" resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz" @@ -15798,7 +15811,7 @@ prompts@^2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.0.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -16084,6 +16097,14 @@ react-fast-compare@^3.0.1: resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== +react-flatpickr@^3.10.13: + version "3.10.13" + resolved "https://registry.yarnpkg.com/react-flatpickr/-/react-flatpickr-3.10.13.tgz#5b9d8f35f84e43f342fb0f0334b78a8dcd7d67c4" + integrity sha512-4m+K1K8jhvRFI8J/AHmQfA5hLALzhebEtEK8mLevXjX24MV3u502crzBn+EGFIBOfNUtrL5PId9FsGwgtuz/og== + dependencies: + flatpickr "^4.6.2" + prop-types "^15.5.10" + react-focus-lock@2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.5.2.tgz#f1e4db5e25cd8789351f2bd5ebe91e9dcb9c2922"