diff --git a/app/src/app/analysis/analysis-page.tsx b/app/src/app/analysis/analysis-page.tsx index 6c54c64..355c05d 100644 --- a/app/src/app/analysis/analysis-page.tsx +++ b/app/src/app/analysis/analysis-page.tsx @@ -102,18 +102,18 @@ export default function AnalysisPage() { () => Object.keys(columnConfigs || []).map( (k) => - ({ - accessor: k, - sortType: !k.startsWith("date") - ? "alphanumeric" - : (a, b, column) => { - const aDate = a.original[column]?.getTime() ?? 0; - const bDate = b.original[column]?.getTime() ?? 0; - - return aDate - bDate; - }, - Header: t(k), - } as Column) + ({ + accessor: k, + sortType: !k.startsWith("date") + ? "alphanumeric" + : (a, b, column) => { + const aDate = a.original[column]?.getTime() ?? 0; + const bDate = b.original[column]?.getTime() ?? 0; + + return aDate - bDate; + }, + Header: t(k), + } as Column) ), [columnConfigs, t] ); @@ -159,23 +159,26 @@ export default function AnalysisPage() { (s) => s.view.view ) as UserDefinedViewInternal; + const [lastSearchQuery, setLastSearchQuery] = useState({ expression: {} }); + const onSearch = React.useCallback( - (q: AnalysisQuery) => { + (q: AnalysisQuery, pageSize: number) => { dispatch({ type: "RESET/Analysis" }); + setLastSearchQuery(q); // if we got an empty expression, just request a page if (q.expression && Object.keys(q.expression).length === 0) { dispatch( requestAsync({ - ...requestPageOfAnalysis({ pageSize: 100 }, false), + ...requestPageOfAnalysis({ pageSize: pageSize }, false), }) ); } else { dispatch( requestAsync({ - ...searchPageOfAnalysis({ query: { ...q, page_size: 100 } }), + ...searchPageOfAnalysis({ query: { ...q, page_size: pageSize } }), queryKey: JSON.stringify(q), }) - ); + ) } }, [dispatch] @@ -624,6 +627,9 @@ export default function AnalysisPage() { {!pageState.isNarrowed ? ( diff --git a/app/src/app/analysis/analysis-selection-configs.ts b/app/src/app/analysis/analysis-selection-configs.ts index 67f83a0..d55158b 100644 --- a/app/src/app/analysis/analysis-selection-configs.ts +++ b/app/src/app/analysis/analysis-selection-configs.ts @@ -1,11 +1,17 @@ -import { createAction, createReducer } from "@reduxjs/toolkit"; +import { createAction, createReducer, createAsyncThunk } from "@reduxjs/toolkit"; import { AnalysisResult } from "sap-client"; import { DataTableSelection } from "./data-table/data-table"; +import { AnalysisQuery } from "sap-client" interface SelectionState { selection: DataTableSelection; } +type Search = { + searchFunc: (query: AnalysisQuery, pageSize: number) => void; + query: AnalysisQuery; +} + export const updateSelectionOriginal = createAction< Record >("analysis/updateSelectionOriginal"); @@ -16,6 +22,19 @@ export const setSelection = createAction>( export const clearSelection = createAction("analysis/clearSelection"); +export const selectAllThunk = createAsyncThunk('analysis/selectAllThunk', async (search: Search, thunkAPI) => { + search.searchFunc(search.query, 1000); + + while (!results || Object.keys(results).length === 0 || results.length === 0) { + await new Promise(resolve => setTimeout(resolve, 10)); + var results = (thunkAPI.getState() as any).entities.analysis; + } + + return results; +}) + +export const selectAllInView = createAction("analysis/selectAllInView"); + const initialState: SelectionState = { selection: {} as DataTableSelection, }; @@ -50,5 +69,25 @@ export const selectionReducer = createReducer(initialState, (builder) => { }) .addCase(clearSelection, (state) => { state.selection = {} as DataTableSelection; + }) + .addCase(selectAllInView, (state, action) => { + state.selection = action.payload + .map(x => { + return ({ [x["sequence_id"]]: { original: x, cells: {} } } as DataTableSelection) + }) + .reduce((acc, cur) => { + return { ...acc, ...cur } + }, {} as DataTableSelection) + }) + .addCase(selectAllThunk.fulfilled, (state, action) => { + let analysis = action.payload; + + state.selection = Object.keys(analysis) + .map(x => { + return ({ [x]: { original: analysis[x], cells: {} } } as DataTableSelection); + }) + .reduce((acc, cur) => { + return { ...acc, ...cur } + }, {} as DataTableSelection); }); }); diff --git a/app/src/app/analysis/analysis-selection-menu.tsx b/app/src/app/analysis/analysis-selection-menu.tsx index f2a8973..6b4004d 100644 --- a/app/src/app/analysis/analysis-selection-menu.tsx +++ b/app/src/app/analysis/analysis-selection-menu.tsx @@ -1,46 +1,77 @@ import { AnalysisResult } from "sap-client"; import { DataTableSelection } from "./data-table/data-table"; -import { HamburgerIcon, SmallCloseIcon } from "@chakra-ui/icons"; +import { HamburgerIcon, SmallCloseIcon, SmallAddIcon } from "@chakra-ui/icons"; import { ResistanceMenuItem } from "./resistance/resistance-menu-item"; import { NearestNeighborMenuItem } from "./nearest-neighbor/nearest-neighbor-menu-item"; import { Menu, MenuList, MenuButton, Button, MenuItem } from "@chakra-ui/react"; import { useCallback } from "react"; -import { clearSelection } from "./analysis-selection-configs"; +import { + clearSelection, + selectAllInView, + selectAllThunk, +} from "./analysis-selection-configs"; import { useDispatch } from "react-redux"; import { SendToWorkspaceMenuItem } from "app/workspaces/send-to-workspace-menu-item"; +import { AnalysisQuery } from "sap-client"; type Props = { selection: DataTableSelection; isNarrowed: boolean; + data: AnalysisResult[]; + search: (query: AnalysisQuery, pageSize: number) => void; + lastSearchQuery: AnalysisQuery; }; export const AnalysisSelectionMenu = (props: Props) => { - const { selection, isNarrowed } = props; + const { selection, isNarrowed, data, search, lastSearchQuery } = props; const dispatch = useDispatch(); const onClear = useCallback(() => { dispatch(clearSelection()); }, [dispatch]); + const onSelectAllInView = useCallback(() => { + dispatch(selectAllInView(data)); + }, [dispatch, data]); + + const onSelectAll = useCallback(() => { + dispatch(selectAllThunk({ searchFunc: search, query: lastSearchQuery })); + }, [dispatch, lastSearchQuery, search]); + + const disabled = isNarrowed || Object.keys(selection).length == 0; + return (
- } - disabled={isNarrowed || Object.keys(selection).length == 0} - > + }> Selection - - - + + + + } + onClick={onSelectAllInView} + > + Select All In View + + } + onClick={onSelectAll} + > + Select All + } onClick={onClear} + isDisabled={disabled} > Clear Selection diff --git a/app/src/app/analysis/nearest-neighbor/nearest-neighbor-menu-item.tsx b/app/src/app/analysis/nearest-neighbor/nearest-neighbor-menu-item.tsx index dbee349..30e3780 100644 --- a/app/src/app/analysis/nearest-neighbor/nearest-neighbor-menu-item.tsx +++ b/app/src/app/analysis/nearest-neighbor/nearest-neighbor-menu-item.tsx @@ -8,10 +8,11 @@ import { NearestNeighborModal } from "./nearest-neighbor-modal"; type Props = { selection: DataTableSelection; + disabled: boolean; }; export const NearestNeighborMenuItem = (props: Props) => { - const { selection } = props; + const { selection, disabled } = props; const { isOpen, onOpen, onClose } = useDisclosure(); return ( @@ -24,6 +25,7 @@ export const NearestNeighborMenuItem = (props: Props) => { title="Nearest Neighbor" icon={} onClick={onOpen} + isDisabled={disabled} > Nearest Neighbor diff --git a/app/src/app/analysis/resistance/resistance-menu-item.tsx b/app/src/app/analysis/resistance/resistance-menu-item.tsx index 852d94b..d723e6d 100644 --- a/app/src/app/analysis/resistance/resistance-menu-item.tsx +++ b/app/src/app/analysis/resistance/resistance-menu-item.tsx @@ -19,12 +19,13 @@ import { ResistanceTable } from "./resistance-table"; type Props = { selection: DataTableSelection; + disabled: boolean; }; export const ResistanceMenuItem = (props: Props) => { const { t } = useTranslation(); const toast = useToast(); - const { selection } = props; + const { selection, disabled } = props; const { isOpen, onOpen, onClose } = useDisclosure(); const onClickCallback = useCallback(() => { @@ -65,6 +66,7 @@ export const ResistanceMenuItem = (props: Props) => { title="Resistance" icon={} onClick={onClickCallback} + isDisabled={disabled} > Resistance diff --git a/app/src/app/analysis/search/analysis-search.tsx b/app/src/app/analysis/search/analysis-search.tsx index abec0b7..558154e 100644 --- a/app/src/app/analysis/search/analysis-search.tsx +++ b/app/src/app/analysis/search/analysis-search.tsx @@ -16,7 +16,7 @@ import { getFieldInternalName } from "app/i18n"; import SearchHelpModal from "./search-help-modal"; type AnalysisSearchProps = { - onSubmit: (query: AnalysisQuery) => void; + onSubmit: (query: AnalysisQuery, pageSize: number) => void; isDisabled: boolean; }; @@ -52,7 +52,7 @@ const AnalysisSearch = (props: AnalysisSearchProps) => { ]); const submitQuery = React.useCallback( - (q?: string) => onSubmit({ expression: parseQuery(q || input, toast) }), + (q?: string) => onSubmit({ expression: parseQuery(q || input, toast) }, 100), [onSubmit, input, toast] ); diff --git a/app/src/app/workspaces/send-to-workspace-menu-item.tsx b/app/src/app/workspaces/send-to-workspace-menu-item.tsx index b984944..2712510 100644 --- a/app/src/app/workspaces/send-to-workspace-menu-item.tsx +++ b/app/src/app/workspaces/send-to-workspace-menu-item.tsx @@ -8,10 +8,11 @@ import { SendToWorkspaceModal } from "./send-to-workspace-modal"; type Props = { selection: DataTableSelection; + disabled: boolean; }; export const SendToWorkspaceMenuItem = (props: Props) => { - const { selection } = props; + const { selection, disabled } = props; const { isOpen, onOpen, onClose } = useDisclosure(); return ( @@ -24,6 +25,7 @@ export const SendToWorkspaceMenuItem = (props: Props) => { title="Send to Workspace" icon={} onClick={onOpen} + isDisabled={disabled} > Send to Workspace diff --git a/app/src/app/workspaces/tree-method-checkbox-group.tsx b/app/src/app/workspaces/tree-method-checkbox-group.tsx index bd9a4a3..fdef5b9 100644 --- a/app/src/app/workspaces/tree-method-checkbox-group.tsx +++ b/app/src/app/workspaces/tree-method-checkbox-group.tsx @@ -29,8 +29,12 @@ export const TreeMethodCheckboxGroup = (props: Props) => { return ( - {treeMethods?.map((treeMethod) => { - return {treeMethod}; + {treeMethods?.map((treeMethod, index) => { + return ( + + {treeMethod} + + ); })} diff --git a/app/src/middleware/jwt-middleware.ts b/app/src/middleware/jwt-middleware.ts index bd544bf..df3262b 100644 --- a/app/src/middleware/jwt-middleware.ts +++ b/app/src/middleware/jwt-middleware.ts @@ -31,8 +31,8 @@ export const jwtMiddleware = (store) => (next) => (action) => { // Let the action continue, but now with the JWT header. next(updatedAction); } else if ( - (action && action.type === REQUEST_FAILURE) || - action.type === MUTATE_FAILURE + action && (action.type === REQUEST_FAILURE || + action.type === MUTATE_FAILURE) ) { // If we failed a request, if it's due to a 401, redirect to signin if (action?.status === 401) { diff --git a/app/src/middleware/selection-middleware.ts b/app/src/middleware/selection-middleware.ts index cb23599..6771be5 100644 --- a/app/src/middleware/selection-middleware.ts +++ b/app/src/middleware/selection-middleware.ts @@ -4,7 +4,7 @@ import { updateSelectionOriginal } from "app/analysis/analysis-selection-configs const { MUTATE_SUCCESS, REQUEST_SUCCESS } = actionTypes; export const selectionMiddleware = (store) => (next) => (action) => { - if ( + if (action && action.type === MUTATE_SUCCESS && action.url.indexOf("/api/analysis/changes") !== -1 ) { @@ -13,6 +13,7 @@ export const selectionMiddleware = (store) => (next) => (action) => { } if ( + action && action.type === REQUEST_SUCCESS && action.url.indexOf("/api/analysis/by_id") !== -1 ) {