Skip to content

Commit

Permalink
Add Copy ID / Delete functionality to My Queries page (#202)
Browse files Browse the repository at this point in the history
Co-authored-by: fzhao99 <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 10, 2024
1 parent 32b4596 commit 541172a
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 82 deletions.
37 changes: 37 additions & 0 deletions query-connector/src/app/database-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,43 @@ export async function getCustomQueries(): Promise<CustomUserQuery[]> {
return Object.values(formattedData);
}

/**
* Deletes a query from the database by its unique ID.
* @param queryId - The unique identifier of the query to delete.
* @returns A success or error response indicating the result.
*/
export const deleteQueryById = async (queryId: string) => {
// TODO: should be able to simplified when it is just deleting query table
const deleteQuerySql1 = `
DELETE FROM query_included_concepts
WHERE query_by_valueset_id IN (
SELECT id FROM query_to_valueset WHERE query_id = $1
);
`;
const deleteQuerySql2 = `
DELETE FROM query_to_valueset WHERE query_id = $1;
`;
const deleteQuerySql3 = `
DELETE FROM query WHERE id = $1;
`;

try {
await dbClient.query("BEGIN");

// Execute deletion queries in the correct order
await dbClient.query(deleteQuerySql1, [queryId]);
await dbClient.query(deleteQuerySql2, [queryId]);
await dbClient.query(deleteQuerySql3, [queryId]);

await dbClient.query("COMMIT");
return { success: true };
} catch (error) {
await dbClient.query("ROLLBACK");
console.error(`Failed to delete query with ID ${queryId}:`, error);
return { success: false, error: "Failed to delete the query." };
}
};

/**
* Checks the database to see if data has been loaded into the valuesets table by
* estmating the number of rows in the table. If the estimated count is greater than
Expand Down
5 changes: 2 additions & 3 deletions query-connector/src/app/query/components/CustomizeQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "../../constants";
import { UseCaseQueryResponse } from "@/app/query-service";
import LoadingView from "./LoadingView";
import { showRedirectConfirmation } from "../designSystem/redirectToast/RedirectToast";
import { showToastConfirmation } from "../designSystem/toast/Toast";
import styles from "./customizeQuery/customizeQuery.module.css";
import CustomizeQueryAccordionHeader from "./customizeQuery/CustomizeQueryAccordionHeader";
import CustomizeQueryAccordionBody from "./customizeQuery/CustomizeQueryAccordionBody";
Expand Down Expand Up @@ -172,9 +172,8 @@ const CustomizeQuery: React.FC<CustomizeQueryProps> = ({
}, [] as ValueSet[]);
setQueryValuesets(selectedItems);
goBack();
showRedirectConfirmation({
showToastConfirmation({
heading: QUERY_CUSTOMIZATION_CONFIRMATION_HEADER,
body: "",
headingLevel: "h4",
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ import {
Modal as TrussModal,
ModalHeading,
ModalFooter,
Button,
ButtonGroup,
ModalToggleButton,
ModalRef,
ModalRef as TrussModalRef,
} from "@trussworks/react-uswds";
import { RefObject } from "react";
import React, { RefObject } from "react";

export type ModalRef = TrussModalRef;

type ModalButton = {
text: string;
type: "button" | "submit" | "reset";
className?: string; // Optional classes for styling
onClick: () => void; // Action to perform when the button is clicked
};

type ModalProps = {
id: string;
heading: string;
description: string;
id: string;
modalRef: RefObject<ModalRef>;
// expand this to support more interesting button use cases when needed
buttons: ModalButton[]; // Dynamic buttons
};

/**
Expand All @@ -25,13 +34,15 @@ type ModalProps = {
* @param param0.description - Modal body
* @param param0.modalRef - ref object to connect the toggle button with the
* actual modal.
* @returns A modal component
* @param param0.buttons - Array of button definitions for the modal footer.
* @returns A customizable modal component
*/
export const Modal: React.FC<ModalProps> = ({
id,
heading,
description,
modalRef,
buttons,
}) => {
return (
<TrussModal
Expand All @@ -46,41 +57,18 @@ export const Modal: React.FC<ModalProps> = ({
</div>
<ModalFooter>
<ButtonGroup>
<ModalToggleButton modalRef={modalRef} closer>
Close
</ModalToggleButton>
{buttons.map((button, index) => (
<Button
key={index}
type={button.type}
className={button.className}
onClick={button.onClick}
>
{button.text}
</Button>
))}
</ButtonGroup>
</ModalFooter>
</TrussModal>
);
};

type ModalButtonProps = {
modalRef: RefObject<ModalRef>;
title: string;
className?: string;
};
/**
* Modal button trigger the opening of a modal
* @param param0 params
* @param param0.modalRef - Ref object to connect button to the parent modal instance
* @param param0.title - What text to display on the button
* @param param0.className - optional styling classes
* @returns A modal button that should open the modal
*/
export const ModalButton: React.FC<ModalButtonProps> = ({
modalRef,
title,
className,
}) => {
return (
<ModalToggleButton
modalRef={modalRef}
opener
className={className}
title={title}
>
{title}
</ModalToggleButton>
);
};
63 changes: 63 additions & 0 deletions query-connector/src/app/query/designSystem/modal/deleteModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { RefObject } from "react";
import { Modal, ModalRef } from "@/app/query/designSystem/modal/Modal";

interface DeleteModalProps {
modalRef: RefObject<ModalRef>;
heading: string;
description: string;
onDelete: () => void;
onCancel?: () => void;
additionalProps?: Record<string, unknown>; // Additional props for extensibility
}

/**
* DeleteModal component with default Delete and Cancel buttons.
* @param root0 - The DeleteModal props object.
* @param root0.modalRef - Reference to the modal component.
* @param root0.heading - The heading/title text for the modal.
* @param root0.description - The descriptive text explaining the modal's purpose.
* @param root0.onDelete - Callback function to execute when the Delete button is clicked.
* @param root0.onCancel - Optional callback function to execute when the Cancel button is clicked.
* @param root0.additionalProps - Optional additional props to pass to the Modal component, such as a custom ID.
* @returns The JSX element for the DeleteModal component.
*/
export const DeleteModal: React.FC<DeleteModalProps> = ({
modalRef,
heading,
description,
onDelete,
onCancel,
additionalProps = {},
}) => {
return (
<Modal
id="delete-confirmation-modal"
modalRef={modalRef}
heading={heading}
description={description}
buttons={[
{
text: "Delete",
type: "button",
className: "usa-button--secondary",
onClick: () => {
onDelete();
modalRef.current?.toggleModal();
},
},
{
text: "Cancel",
type: "button",
className: "usa-button--outline",
onClick: () => {
if (onCancel) onCancel();
modalRef.current?.toggleModal();
},
},
]}
{...additionalProps}
/>
);
};

export default DeleteModal;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export type AlertType = "info" | "success" | "warning" | "error";

type RedirectToastProps = {
toastVariant: AlertType;
heading: string;
heading?: string;
body: string;
headingLevel?: HeadingLevel;
};
Expand All @@ -26,13 +26,19 @@ const RedirectToast: React.FC<RedirectToastProps> = ({
toastVariant,
heading,
body,
headingLevel,
headingLevel = "h4",
}) => {
return (
<Alert
type={toastVariant}
heading={heading}
headingLevel={headingLevel ? headingLevel : "h4"}
heading={
heading ? (
<span className={`usa-alert__heading ${headingLevel}`}>
{heading}
</span>
) : undefined
}
headingLevel={heading ? headingLevel : "h4"}
>
{body}
</Alert>
Expand All @@ -53,23 +59,31 @@ const options = {
*
* @param content - content object to configure the redirect confirmation toast
* @param content.heading - heading of the redirect toast
* @param content.variant - one of "info", "success", "warning", "error" to
* render the relevant toast variant
* @param content.body - body text of the redirect toast
* @param content.headingLevel - h1-6 level of the heading tag associated with
* content.heading. defaults to h4
* @param content.duration - Duration in milliseconds for how long the toast is visible. Defaults to 5000ms.
*/
export function showRedirectConfirmation(content: {
heading: string;
body: string;
export function showToastConfirmation(content: {
heading?: string;
body?: string;
variant?: AlertType;
headingLevel?: HeadingLevel;
duration?: number;
}) {
toast.success(
const toastVariant = content.variant ?? "success";
const toastDuration = content.duration ?? 5000; // Default to 5000ms

toast[toastVariant](
<RedirectToast
toastVariant="success"
toastVariant={toastVariant}
heading={content.heading}
headingLevel={content.headingLevel}
body={content.body}
body={content.body ?? ""}
/>,
options,
{ ...options, autoClose: toastDuration },
);
}

Expand Down
Loading

0 comments on commit 541172a

Please sign in to comment.