Skip to content

Commit

Permalink
Merge pull request #106 from ssi-dk/feat/add-select-all-functionality
Browse files Browse the repository at this point in the history
Feat/add select all functionality
  • Loading branch information
DG-KFuglsang authored Oct 2, 2024
2 parents 9bbdba2 + a15e7cc commit c2697a3
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 38 deletions.
38 changes: 22 additions & 16 deletions app/src/app/analysis/analysis-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnalysisResult>)
({
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<AnalysisResult>)
),
[columnConfigs, t]
);
Expand Down Expand Up @@ -159,23 +159,26 @@ export default function AnalysisPage() {
(s) => s.view.view
) as UserDefinedViewInternal;

const [lastSearchQuery, setLastSearchQuery] = useState<AnalysisQuery>({ 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]
Expand Down Expand Up @@ -624,6 +627,9 @@ export default function AnalysisPage() {
<AnalysisSelectionMenu
selection={selection}
isNarrowed={pageState.isNarrowed}
data={filteredData}
search={onSearch}
lastSearchQuery={lastSearchQuery}
/>
<Flex grow={1} width="100%" />
{!pageState.isNarrowed ? (
Expand Down
41 changes: 40 additions & 1 deletion app/src/app/analysis/analysis-selection-configs.ts
Original file line number Diff line number Diff line change
@@ -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<AnalysisResult>;
}

type Search = {
searchFunc: (query: AnalysisQuery, pageSize: number) => void;
query: AnalysisQuery;
}

export const updateSelectionOriginal = createAction<
Record<string, AnalysisResult>
>("analysis/updateSelectionOriginal");
Expand All @@ -16,6 +22,19 @@ export const setSelection = createAction<DataTableSelection<AnalysisResult>>(

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<AnalysisResult[]>("analysis/selectAllInView");

const initialState: SelectionState = {
selection: {} as DataTableSelection<AnalysisResult>,
};
Expand Down Expand Up @@ -50,5 +69,25 @@ export const selectionReducer = createReducer(initialState, (builder) => {
})
.addCase(clearSelection, (state) => {
state.selection = {} as DataTableSelection<AnalysisResult>;
})
.addCase(selectAllInView, (state, action) => {
state.selection = action.payload
.map(x => {
return ({ [x["sequence_id"]]: { original: x, cells: {} } } as DataTableSelection<AnalysisResult>)
})
.reduce((acc, cur) => {
return { ...acc, ...cur }
}, {} as DataTableSelection<AnalysisResult>)
})
.addCase(selectAllThunk.fulfilled, (state, action) => {
let analysis = action.payload;

state.selection = Object.keys(analysis)
.map(x => {
return ({ [x]: { original: analysis[x], cells: {} } } as DataTableSelection<AnalysisResult>);
})
.reduce((acc, cur) => {
return { ...acc, ...cur }
}, {} as DataTableSelection<AnalysisResult>);
});
});
53 changes: 42 additions & 11 deletions app/src/app/analysis/analysis-selection-menu.tsx
Original file line number Diff line number Diff line change
@@ -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<AnalysisResult>;
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 (
<div>
<Menu>
<MenuButton
as={Button}
leftIcon={<HamburgerIcon />}
disabled={isNarrowed || Object.keys(selection).length == 0}
>
<MenuButton as={Button} leftIcon={<HamburgerIcon />}>
Selection
</MenuButton>
<MenuList>
<ResistanceMenuItem selection={selection} />
<NearestNeighborMenuItem selection={selection} />
<SendToWorkspaceMenuItem selection={selection} />
<ResistanceMenuItem selection={selection} disabled={disabled} />
<NearestNeighborMenuItem selection={selection} disabled={disabled} />
<SendToWorkspaceMenuItem selection={selection} disabled={disabled} />
<MenuItem
aria-label="Select All In view"
title="SelectAllInView"
icon={<SmallAddIcon />}
onClick={onSelectAllInView}
>
Select All In View
</MenuItem>
<MenuItem
aria-label="Select All"
title="SelectAll"
icon={<SmallAddIcon />}
onClick={onSelectAll}
>
Select All
</MenuItem>
<MenuItem
aria-label="Clear Selection"
title="Clear Selection"
icon={<SmallCloseIcon />}
onClick={onClear}
isDisabled={disabled}
>
Clear Selection
</MenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { NearestNeighborModal } from "./nearest-neighbor-modal";

type Props = {
selection: DataTableSelection<AnalysisResult>;
disabled: boolean;
};

export const NearestNeighborMenuItem = (props: Props) => {
const { selection } = props;
const { selection, disabled } = props;
const { isOpen, onOpen, onClose } = useDisclosure();

return (
Expand All @@ -24,6 +25,7 @@ export const NearestNeighborMenuItem = (props: Props) => {
title="Nearest Neighbor"
icon={<PlusSquareIcon />}
onClick={onOpen}
isDisabled={disabled}
>
Nearest Neighbor
</MenuItem>
Expand Down
4 changes: 3 additions & 1 deletion app/src/app/analysis/resistance/resistance-menu-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ import { ResistanceTable } from "./resistance-table";

type Props = {
selection: DataTableSelection<AnalysisResult>;
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(() => {
Expand Down Expand Up @@ -65,6 +66,7 @@ export const ResistanceMenuItem = (props: Props) => {
title="Resistance"
icon={<Bacteria />}
onClick={onClickCallback}
isDisabled={disabled}
>
Resistance
</MenuItem>
Expand Down
4 changes: 2 additions & 2 deletions app/src/app/analysis/search/analysis-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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]
);

Expand Down
4 changes: 3 additions & 1 deletion app/src/app/workspaces/send-to-workspace-menu-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { SendToWorkspaceModal } from "./send-to-workspace-modal";

type Props = {
selection: DataTableSelection<AnalysisResult>;
disabled: boolean;
};

export const SendToWorkspaceMenuItem = (props: Props) => {
const { selection } = props;
const { selection, disabled } = props;
const { isOpen, onOpen, onClose } = useDisclosure();

return (
Expand All @@ -24,6 +25,7 @@ export const SendToWorkspaceMenuItem = (props: Props) => {
title="Send to Workspace"
icon={<ArrowForwardIcon />}
onClick={onOpen}
isDisabled={disabled}
>
Send to Workspace
</MenuItem>
Expand Down
8 changes: 6 additions & 2 deletions app/src/app/workspaces/tree-method-checkbox-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ export const TreeMethodCheckboxGroup = (props: Props) => {
return (
<CheckboxGroup defaultValue={value} onChange={onChangeCallback}>
<Stack spacing={2}>
{treeMethods?.map((treeMethod) => {
return <Checkbox value={treeMethod}>{treeMethod}</Checkbox>;
{treeMethods?.map((treeMethod, index) => {
return (
<Checkbox key={`tree-method-${index}`} value={treeMethod}>
{treeMethod}
</Checkbox>
);
})}
</Stack>
</CheckboxGroup>
Expand Down
4 changes: 2 additions & 2 deletions app/src/middleware/jwt-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion app/src/middleware/selection-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand All @@ -13,6 +13,7 @@ export const selectionMiddleware = (store) => (next) => (action) => {
}

if (
action &&
action.type === REQUEST_SUCCESS &&
action.url.indexOf("/api/analysis/by_id") !== -1
) {
Expand Down

0 comments on commit c2697a3

Please sign in to comment.