Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Copy ID / Delete functionality to My Queries page #202

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/redirectToast/RedirectToast";
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;
robertandremitchell marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,25 @@ 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
*/
export function showRedirectConfirmation(content: {
export function showToastConfirmation(content: {
heading: string;
body: string;
body?: string;
variant?: AlertType;
headingLevel?: HeadingLevel;
}) {
toast.success(
const toastVariant = content.variant ?? "success";
toast[toastVariant](
<RedirectToast
toastVariant="success"
toastVariant={toastVariant}
heading={content.heading}
headingLevel={content.headingLevel}
body={content.body}
body={content.body ?? ""}
/>,
options,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React, { useState, useContext, useRef } from "react";
import { Button, Icon, Table } from "@trussworks/react-uswds";
import { useState } from "react";
import { ModalRef } from "@/app/query/designSystem/modal/Modal";
import { useRouter } from "next/navigation";
import styles from "@/app/queryBuilding/queryBuilding.module.scss";
import { CustomUserQuery } from "@/app/query-building";
import { DataContext } from "@/app/utils";
import {
handleDelete,
confirmDelete,
handleCopy,
handleClick,
renderModal,
} from "@/app/queryBuilding/dataState/utils";

interface UserQueriesDisplayProps {
queries: CustomUserQuery[];
Expand All @@ -15,25 +24,34 @@ interface UserQueriesDisplayProps {
* @returns the UserQueriesDisplay to render the queries with edit/delete options
*/
export const UserQueriesDisplay: React.FC<UserQueriesDisplayProps> = ({
queries,
queries: initialQueries,
}) => {
const router = useRouter();
const context = useContext(DataContext);
const [queries, setQueries] = useState<CustomUserQuery[]>(initialQueries);
const [loading, setLoading] = useState(false);

const handleClick = async () => {
setLoading(true);

// Redirect to query updating/editing page
router.push("/queryBuilding/buildFromTemplates");
};
const modalRef = useRef<ModalRef>(null);
const [selectedQuery, setSelectedQuery] = useState<{
queryName: string;
queryId: string;
} | null>(null);

return (
<div>
{context &&
renderModal(
modalRef,
selectedQuery,
handleDelete,
queries,
setQueries,
context,
)}
<div className="display-flex flex-justify-between flex-align-center width-full margin-bottom-4">
<h1 className="{styles.queryTitle} flex-align-center">My queries</h1>
<div className="margin-left-auto">
<Button
onClick={handleClick}
onClick={() => handleClick(router, setLoading)}
className={styles.createQueryButton}
type="button"
>
Expand Down Expand Up @@ -62,33 +80,36 @@ export const UserQueriesDisplay: React.FC<UserQueriesDisplayProps> = ({
className="usa-button--unstyled text-bold text-no-underline"
onClick={() => console.log("Edit", query.query_id)}
>
<span className="icon-text padding-right-4">
<span className="icon-text padding-right-4 display-flex flex-align-center">
<Icon.Edit className="height-3 width-3" />
<span>Edit</span>
</span>
</Button>
<Button
type="button"
className="usa-button--unstyled text-bold text-no-underline"
onClick={() => console.log("Delete", query.query_id)}
onClick={() =>
confirmDelete(
query.query_name,
query.query_id,
setSelectedQuery,
modalRef,
)
}
>
<span className="icon-text padding-right-4">
<span className="icon-text padding-right-4 display-flex flex-align-center">
<Icon.Delete className="height-3 width-3" />
<span>Delete</span>
</span>
</Button>
<Button
type="button"
className="usa-button--unstyled text-bold text-no-underline"
onClick={() => {
navigator.clipboard
.writeText(query.query_id)
.catch((error) =>
console.error("Failed to copy text:", error),
);
}}
onClick={() =>
handleCopy(query.query_name, query.query_id)
}
>
<span className="icon-text padding-right-1">
<span className="icon-text padding-right-1 display-flex flex-align-center">
<Icon.ContentCopy className="height-3 width-3" />
<span>Copy ID</span>
</span>
Expand Down
Loading
Loading