diff --git a/CHANGELOG.md b/CHANGELOG.md index d08993515..24cb03e37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,11 +45,13 @@ The types of changes are: * Added `due_date` sorting [#1284](https://github.com/ethyca/fidesops/pull/1284) * Added erasure endpoints for Shopify connector [#1289](https://github.com/ethyca/fidesops/pull/1289) * Adds ability to send email notification upon privacy request completion [#1282](https://github.com/ethyca/fidesops/pull/1282) -* Enable new manual webhooks in privacy request execution [#1285](https://github.com/ethyca/fidesops/pull/1285) * Added human readable label to ConnectionType endpoint [#1297](https://github.com/ethyca/fidesops/pull/1297) +* Enable new manual webhooks in privacy request execution [#1285](https://github.com/ethyca/fidesops/pull/1285) +* Add table for consent [#1301](https://github.com/ethyca/fidesops/pull/1301) * Adds ability to send email notification upon privacy request receipt [#1303](https://github.com/ethyca/fidesops/pull/1303) -* Add table for consent (#1301)[https://github.com/ethyca/fidesops/pull/1301] * Utility to update SaaS config instances based on template updates [#1307](https://github.com/ethyca/fidesops/pull/1307) +* Added generic request sorting button [#1320](https://github.com/ethyca/fidesops/pull/1320) + ### Docs diff --git a/clients/ops/admin-ui/src/features/common/Icon/SortArrow.tsx b/clients/ops/admin-ui/src/features/common/Icon/SortArrow.tsx new file mode 100644 index 000000000..fd87d1974 --- /dev/null +++ b/clients/ops/admin-ui/src/features/common/Icon/SortArrow.tsx @@ -0,0 +1,171 @@ +import { Icon } from "@fidesui/react"; +import React from "react"; + +type SortArrowProps = { + up?: boolean; +}; + +const SortArrow: React.FC = ({ up }) => { + if (up === undefined) { + return ( + + + + + + + ); + } + + if (up) { + return ( + + + + + + + + + + + + + + + + + + + + + + ); + } + + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SortArrow; diff --git a/clients/ops/admin-ui/src/features/common/Icon/index.tsx b/clients/ops/admin-ui/src/features/common/Icon/index.tsx index 280e2ae51..70049ceb7 100644 --- a/clients/ops/admin-ui/src/features/common/Icon/index.tsx +++ b/clients/ops/admin-ui/src/features/common/Icon/index.tsx @@ -8,4 +8,5 @@ export { default as GearIcon } from "./Gear"; export { default as GreenCheckCircleIcon } from "./GreenCheckCircle"; export { default as MoreIcon } from "./More"; export { default as SearchLineIcon } from "./SearchLine"; +export { default as SortArrowIcon } from "./SortArrow"; export { default as UserIcon } from "./User"; diff --git a/clients/ops/admin-ui/src/features/privacy-requests/RequestTable.tsx b/clients/ops/admin-ui/src/features/privacy-requests/RequestTable.tsx index b0bbf0dc9..f80115d18 100644 --- a/clients/ops/admin-ui/src/features/privacy-requests/RequestTable.tsx +++ b/clients/ops/admin-ui/src/features/privacy-requests/RequestTable.tsx @@ -1,4 +1,4 @@ -import { Table, Tbody, Th, Thead, Tr } from "@fidesui/react"; +import { Flex, Table, Tbody, Th, Thead, Tr } from "@fidesui/react"; import { debounce } from "common/utils"; import React, { useEffect, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -10,6 +10,7 @@ import { useGetAllPrivacyRequestsQuery, } from "./privacy-requests.slice"; import RequestRow from "./RequestRow"; +import SortRequestButton from "./SortRequestButton"; import { PrivacyRequest, PrivacyRequestParams } from "./types"; interface RequestTableProps { @@ -39,11 +40,13 @@ const useRequestTable = () => { dispatch(setPage(filters.page + 1)); }; - const { data, isLoading } = useGetAllPrivacyRequestsQuery(cachedFilters); + const { data, isLoading, isFetching } = + useGetAllPrivacyRequestsQuery(cachedFilters); const { items: requests, total } = data || { items: [], total: 0 }; return { ...filters, isLoading, + isFetching, requests, total, handleNextPage, @@ -52,8 +55,15 @@ const useRequestTable = () => { }; const RequestTable: React.FC = () => { - const { requests, total, page, size, handleNextPage, handlePreviousPage } = - useRequestTable(); + const { + requests, + total, + page, + size, + handleNextPage, + handlePreviousPage, + isFetching, + } = useRequestTable(); return ( <> @@ -61,7 +71,15 @@ const RequestTable: React.FC = () => { Status - Days Left + + + Days Left{" "} + + + Request Type Subject Identity Time Received diff --git a/clients/ops/admin-ui/src/features/privacy-requests/SortRequestButton.tsx b/clients/ops/admin-ui/src/features/privacy-requests/SortRequestButton.tsx new file mode 100644 index 000000000..bd9a20da5 --- /dev/null +++ b/clients/ops/admin-ui/src/features/privacy-requests/SortRequestButton.tsx @@ -0,0 +1,117 @@ +import { Flex, IconButton } from "@fidesui/react"; +import { SortArrowIcon } from "common/Icon"; +import React, { useCallback, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +import { + clearSortFields, + selectPrivacyRequestFilters, + setSortDirection, + setSortField, +} from "./privacy-requests.slice"; + +type UseSortRequestButtonParams = { + sortField: string; + isLoading: boolean; +}; + +export enum ButtonState { + ASC = "asc", + DESC = "desc", + UNSELECTED = "unselected", +} + +const useSortRequestButton = ({ + sortField, + isLoading, +}: UseSortRequestButtonParams) => { + const filters = useSelector(selectPrivacyRequestFilters); + const dispatch = useDispatch(); + const [buttonState, setButtonState] = useState( + ButtonState.UNSELECTED + ); + const [wasButtonJustClicked, setWasButtonJustClicked] = useState(false); + + useEffect(() => { + if (!isLoading) { + setWasButtonJustClicked(false); + } + }, [isLoading]); + + useEffect(() => { + if (filters.sort_direction === undefined) { + setButtonState(ButtonState.UNSELECTED); + } + }, [filters]); + + const handleButtonClick = useCallback(() => { + setWasButtonJustClicked(true); + dispatch(setSortField(sortField)); + + switch (buttonState) { + case ButtonState.UNSELECTED: + dispatch(setSortDirection(ButtonState.ASC)); + setButtonState(ButtonState.ASC); + break; + case ButtonState.ASC: + dispatch(setSortDirection(ButtonState.DESC)); + setButtonState(ButtonState.DESC); + break; + case ButtonState.DESC: + dispatch(clearSortFields()); + setButtonState(ButtonState.UNSELECTED); + break; + default: + break; + } + }, [buttonState, setButtonState, dispatch, sortField]); + + return { + handleButtonClick, + buttonState, + wasButtonJustClicked, + }; +}; + +type SortRequestButtonProps = { + sortField: string; + isLoading: boolean; +}; + +const SortRequestButton: React.FC = ({ + sortField, + isLoading, +}) => { + const { buttonState, handleButtonClick, wasButtonJustClicked } = + useSortRequestButton({ sortField, isLoading }); + + let icon = null; + + switch (buttonState) { + case ButtonState.ASC: + icon = ; + break; + case ButtonState.DESC: + icon = ; + break; + case ButtonState.UNSELECTED: + icon = ; + break; + default: + icon = ; + } + + return ( + + + + ); +}; + +export default SortRequestButton; diff --git a/clients/ops/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts b/clients/ops/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts index 55dae3032..a4ced9834 100644 --- a/clients/ops/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts +++ b/clients/ops/admin-ui/src/features/privacy-requests/privacy-requests.slice.ts @@ -22,6 +22,8 @@ export function mapFiltersToSearchParams({ page, size, verbose, + sort_direction, + sort_field, }: Partial): any { let fromISO; if (from) { @@ -44,6 +46,8 @@ export function mapFiltersToSearchParams({ ...(page ? { page: `${page}` } : {}), ...(typeof size !== "undefined" ? { size: `${size}` } : {}), ...(verbose ? { verbose } : {}), + ...(sort_direction ? { sort_direction } : {}), + ...(sort_field ? { sort_field } : {}), }; } @@ -166,6 +170,8 @@ interface SubjectRequestsState { page: number; size: number; verbose?: boolean; + sort_field?: string; + sort_direction?: string; } const initialState: SubjectRequestsState = { @@ -225,6 +231,19 @@ export const subjectRequestsSlice = createSlice({ ...state, verbose: action.payload, }), + setSortField: (state, action: PayloadAction) => ({ + ...state, + sort_field: action.payload, + }), + setSortDirection: (state, action: PayloadAction) => ({ + ...state, + sort_direction: action.payload, + }), + clearSortFields: (state) => ({ + ...state, + sort_direction: undefined, + sort_field: undefined, + }), }, }); @@ -236,7 +255,10 @@ export const { setRequestTo, setPage, setVerbose, + setSortField, + setSortDirection, clearAllFilters, + clearSortFields, } = subjectRequestsSlice.actions; export const selectRevealPII = (state: RootState) => @@ -254,6 +276,8 @@ export const selectPrivacyRequestFilters = ( page: state.subjectRequests.page, size: state.subjectRequests.size, verbose: state.subjectRequests.verbose, + sort_direction: state.subjectRequests.sort_direction, + sort_field: state.subjectRequests.sort_field, }); export const { reducer } = subjectRequestsSlice; diff --git a/clients/ops/admin-ui/src/features/privacy-requests/types.ts b/clients/ops/admin-ui/src/features/privacy-requests/types.ts index a54250d0e..198a44a57 100644 --- a/clients/ops/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/ops/admin-ui/src/features/privacy-requests/types.ts @@ -88,4 +88,6 @@ export interface PrivacyRequestParams { page: number; size: number; verbose?: boolean; + sort_field?: string; + sort_direction?: string; }