Skip to content
Snippets Groups Projects
Commit 8fff8993 authored by Lukas's avatar Lukas
Browse files

Merge branch 'develop' into lbudginas/#424_add_new_tab_on_account_page

parents b6663ada 412f77db
No related branches found
No related tags found
1 merge request!524Lbudginas/#424 add new tab on account page
Pipeline #113884 canceled
import { useState } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
...@@ -10,13 +10,13 @@ import { ...@@ -10,13 +10,13 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import Hive from "@/types/Hive";
import { getOperationTypeForDisplay } from "@/utils/UI"; import { getOperationTypeForDisplay } from "@/utils/UI";
import { useUserSettingsContext } from "../contexts/UserSettingsContext"; import { useUserSettingsContext } from "../contexts/UserSettingsContext";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Chip from "./Chip"; import Chip from "./Chip";
import { categorizedOperationTypes } from "@/utils/CategorizedOperationTypes"; import { categorizedOperationTypes } from "@/utils/CategorizedOperationTypes";
import Explorer from "@/types/Explorer"; import Explorer from "@/types/Explorer";
import { ChevronDown, Search} from "lucide-react";
type OperationTypesDialogProps = { type OperationTypesDialogProps = {
operationTypes?: Explorer.ExtendedOperationTypePattern[]; operationTypes?: Explorer.ExtendedOperationTypePattern[];
...@@ -40,6 +40,24 @@ export const colorByOperationCategory: Record<string, string> = { ...@@ -40,6 +40,24 @@ export const colorByOperationCategory: Record<string, string> = {
Other: "bg-explorer-operations-other", Other: "bg-explorer-operations-other",
}; };
const getCategoriesToExpand = (
selectedIds: number[],
operationTypes: Explorer.ExtendedOperationTypePattern[] | undefined,
categorizedOperationTypes: { name: string; types: string[] }[]
): string[] => {
if (!operationTypes) return [];
return categorizedOperationTypes
.filter((cat) =>
cat.types.some((type) => {
const operationType = operationTypes.find(
(op) => op.operation_name === type
);
return selectedIds.includes(operationType?.op_type_id || 0);
})
)
.map((cat) => cat.name);
};
const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
operationTypes, operationTypes,
triggerTitle, triggerTitle,
...@@ -51,10 +69,33 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -51,10 +69,33 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
[] []
); );
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [isOperationFilterHover, setIsOperationFilterHover] = useState(false);
const { settings } = useUserSettingsContext(); const { settings } = useUserSettingsContext();
const [searchTerm, setSearchTerm] = useState("");
const [expandedSections, setExpandedSections] = useState<string[]>([]);
if (!operationTypes || !operationTypes.length) return; const categoryHeadersRef = useRef<Record<string, HTMLDivElement | undefined>>(
{} as Record<string, HTMLDivElement | undefined>
);
useEffect(() => {
if (searchTerm) {
const allMatchingCategories = categorizedOperationTypes
.filter((cat) =>
cat.types.some((type) =>
operationTypes
?.find((op) => op.operation_name === type)
?.operation_name.toLowerCase()
.includes(searchTerm.toLowerCase())
)
)
.map((cat) => cat.name);
setExpandedSections(allMatchingCategories);
} else {
setExpandedSections([]);
}
}, [searchTerm,operationTypes]);
if (!operationTypes || !operationTypes.length) return null;
const nonDisabledOperationTypes = operationTypes.filter( const nonDisabledOperationTypes = operationTypes.filter(
(operationType) => !operationType.isDisabled (operationType) => !operationType.isDisabled
...@@ -67,6 +108,18 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -67,6 +108,18 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
(operationType) => !operationType.is_virtual (operationType) => !operationType.is_virtual
); );
const filterOperations = (
operationType: Explorer.ExtendedOperationTypePattern
) => {
if (!searchTerm) return true;
return operationType.operation_name
.toLowerCase()
.includes(searchTerm.toLowerCase());
};
const filteredNonDisabledOperations =
nonDisabledOperationTypes.filter(filterOperations);
const onFiltersSelect = (id: number) => { const onFiltersSelect = (id: number) => {
if (selectedOperationsIds.includes(id)) { if (selectedOperationsIds.includes(id)) {
setSelectedOperationsIds( setSelectedOperationsIds(
...@@ -93,41 +146,52 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -93,41 +146,52 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
const onOpenChange = (open: boolean) => { const onOpenChange = (open: boolean) => {
if (open) { if (open) {
setSelectedOperationsIds(selectedOperations); setSelectedOperationsIds(selectedOperations);
if (searchTerm) {
const allMatchingCategories = getCategoriesToExpand(selectedOperations, operationTypes, categorizedOperationTypes);
setExpandedSections(allMatchingCategories);
}
} }
setIsOpen(open); setIsOpen(open);
}; };
const selectAll = () => { const selectAll = () => {
const allIds = nonDisabledOperationTypes.map( const allIds = filteredNonDisabledOperations.map(
(operationType) => operationType.op_type_id (operationType) => operationType.op_type_id
); );
setSelectedOperationsIds(allIds); setSelectedOperationsIds(allIds);
const allMatchingCategories = getCategoriesToExpand(allIds, operationTypes, categorizedOperationTypes);
setExpandedSections(allMatchingCategories);
}; };
const selectReal = () => { const selectReal = () => {
const realIds = nonVirtualOperations.map( const realIds = nonVirtualOperations
(operationType) => operationType.op_type_id .filter(filterOperations)
); .map((operationType) => operationType.op_type_id);
let finaList = [...realIds, ...selectedOperationsIds]; let finaList = [...realIds, ...selectedOperationsIds];
finaList = finaList.filter((id, index) => finaList.indexOf(id) === index); finaList = finaList.filter((id, index) => finaList.indexOf(id) === index);
setSelectedOperationsIds([...finaList]); setSelectedOperationsIds([...finaList]);
const allMatchingCategories = getCategoriesToExpand(finaList, operationTypes, categorizedOperationTypes);
setExpandedSections(allMatchingCategories);
}; };
const selectVirtual = () => { const selectVirtual = () => {
const virtualIds = virtualOperations.map( const virtualIds = virtualOperations
(operationType) => operationType.op_type_id .filter(filterOperations)
); .map((operationType) => operationType.op_type_id);
let finaList = [...virtualIds, ...selectedOperationsIds]; let finaList = [...virtualIds, ...selectedOperationsIds];
finaList = finaList.filter((id, index) => finaList.indexOf(id) === index); finaList = finaList.filter((id, index) => finaList.indexOf(id) === index);
setSelectedOperationsIds(finaList); setSelectedOperationsIds([...finaList]);
const allMatchingCategories = getCategoriesToExpand(finaList, operationTypes, categorizedOperationTypes);
setExpandedSections(allMatchingCategories);
}; };
const selectAllOfCategory = ( const selectAllOfCategory = (
operationTypes: Explorer.ExtendedOperationTypePattern[] operationTypes: Explorer.ExtendedOperationTypePattern[]
) => { ) => {
const nonDisabledOperationTypesForCategory = operationTypes.filter( const nonDisabledOperationTypesForCategory = operationTypes
(operationType) => !operationType.isDisabled .filter(filterOperations)
); .filter((operationType) => !operationType.isDisabled);
const operationsIds = nonDisabledOperationTypesForCategory.map( const operationsIds = nonDisabledOperationTypesForCategory.map(
(operationType) => operationType.op_type_id (operationType) => operationType.op_type_id
); );
...@@ -139,17 +203,17 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -139,17 +203,17 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
const clearCategory = ( const clearCategory = (
operationTypes: Explorer.ExtendedOperationTypePattern[] operationTypes: Explorer.ExtendedOperationTypePattern[]
) => { ) => {
const operationsIds = operationTypes.map( const operationsIds = operationTypes
(operationType) => operationType.op_type_id .filter(filterOperations)
); .map((operationType) => operationType.op_type_id);
const finalOperations = [...selectedOperationsIds].filter( const finalOperations = [...selectedOperationsIds].filter(
(selectedOperationId) => !operationsIds.includes(selectedOperationId) (selectedOperationId) => !operationsIds.includes(selectedOperationId)
); );
setSelectedOperationsIds(finalOperations); setSelectedOperationsIds(finalOperations);
}; };
const invertSelection = () => { const invertSelection = () => {
const allIds = nonDisabledOperationTypes.map( const allIds = filteredNonDisabledOperations.map(
(operationType) => operationType.op_type_id (operationType) => operationType.op_type_id
); );
const finaList = allIds.filter( const finaList = allIds.filter(
...@@ -158,6 +222,8 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -158,6 +222,8 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
undefined undefined
); );
setSelectedOperationsIds(finaList); setSelectedOperationsIds(finaList);
const allMatchingCategories = getCategoriesToExpand(finaList, operationTypes, categorizedOperationTypes);
setExpandedSections(allMatchingCategories);
}; };
const renderOperationType = ( const renderOperationType = (
...@@ -167,13 +233,13 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -167,13 +233,13 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
<li <li
onClick={() => onFiltersSelect(operationType.op_type_id)} onClick={() => onFiltersSelect(operationType.op_type_id)}
key={operationType.op_type_id} key={operationType.op_type_id}
className="col-span-3 pl-2 md:col-span-1 flex items-center font-bold text-text rounded-lg bg-inherit hover:bg-rowHover" className="flex items-center py-1 px-2 rounded-md hover:bg-rowHover cursor-pointer"
> >
<Input <Input
type="checkbox" type="checkbox"
checked={selectedOperationsIds.includes(operationType.op_type_id)} checked={selectedOperationsIds.includes(operationType.op_type_id)}
name="bordered-checkbox" name={`operation-checkbox-${operationType.op_type_id}`}
className=" w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 " className=" w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
{...{ {...{
"data-testid": `operation-type-checkbox-${operationType.operation_name}`, "data-testid": `operation-type-checkbox-${operationType.operation_name}`,
}} }}
...@@ -181,18 +247,18 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -181,18 +247,18 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
disabled={operationType.isDisabled} disabled={operationType.isDisabled}
/> />
<Label <Label
htmlFor="bordered-checkbox-1" htmlFor={`operation-checkbox-${operationType.op_type_id}`}
className={cn( className={cn(
"p-1 ml-2 text-sm font-medium text-text whitespace-nowrap overflow-hidden text-ellipsis", "p-1 ml-1 text-sm font-medium text-text whitespace-normal overflow-hidden text-ellipsis",
{ {
"text-sky-500 dark:text-sky-200": operationType.is_virtual, "text-sky-500 dark:text-sky-200": operationType.is_virtual,
"opacity-50": operationType.isDisabled, "opacity-50": operationType.isDisabled,
} }
)} )}
{...{ {...{
"data-testid": `operation-type-label-${operationType.operation_name}`, "data-testid": `operation-type-label-${operationType.operation_name}`,
}} }}
> >
{settings.rawJsonView {settings.rawJsonView
? operationType.operation_name ? operationType.operation_name
: getOperationTypeForDisplay(operationType.operation_name)} : getOperationTypeForDisplay(operationType.operation_name)}
...@@ -201,6 +267,25 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -201,6 +267,25 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
); );
}; };
const toggleSection = (sectionName: string) => {
setExpandedSections((prev) => {
if (prev.includes(sectionName)) {
return prev.filter((s) => s !== sectionName);
} else {
return [...prev, sectionName];
}
});
};
const handleExpandAll = () => {
const allCategories = categorizedOperationTypes.map((cat) => cat.name);
setExpandedSections(allCategories);
};
const handleCollapseAll = () => {
setExpandedSections([]);
};
const renderSection = (sectionName: string, operationsNames: string[]) => { const renderSection = (sectionName: string, operationsNames: string[]) => {
const operations = operationsNames const operations = operationsNames
.map((name) => .map((name) =>
...@@ -214,52 +299,80 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -214,52 +299,80 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
const sortedOperations = operations.sort((a, b) => const sortedOperations = operations.sort((a, b) =>
a?.operation_name.localeCompare(b?.operation_name) a?.operation_name.localeCompare(b?.operation_name)
); );
const nonDisabledOperationTypesForSection = operations.filter( const filteredOperations = sortedOperations.filter(filterOperations);
const nonDisabledOperationTypesForSection = filteredOperations.filter(
(operationType) => !operationType.isDisabled (operationType) => !operationType.isDisabled
); );
const isExpanded = expandedSections.includes(sectionName);
const allIdsInCategory = nonDisabledOperationTypesForSection.map(
(operationType) => operationType.op_type_id
);
const isCategoryChecked = nonDisabledOperationTypesForSection.length > 0 ? allIdsInCategory.every((id) =>
selectedOperationsIds.includes(id)
) : false;
const handleCategoryCheckboxChange = () => {
if (isCategoryChecked) {
clearCategory(operations);
setExpandedSections((prev) => prev.filter((s) => s !== sectionName));
} else {
selectAllOfCategory(operations);
setExpandedSections((prev) => [...prev, sectionName]);
}
};
if (searchTerm && filteredOperations.length === 0) {
return null;
}
return ( return (
<div <div className=" border-t px-2" key={sectionName}>
className=" border-t px-2" <div
key={sectionName} className="flex items-center justify-between py-2 cursor-pointer z-10"
> onClick={() => toggleSection(sectionName)}
<div className="flex justify-between"> ref={(el) => {
<div className="flex items-center justify-center"> if (el) {
categoryHeadersRef.current[sectionName] = el;
}
}}
>
<div className="flex items-center flex-1 ">
<Input
type="checkbox"
checked={isCategoryChecked}
onChange={handleCategoryCheckboxChange}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 mr-2"
id={`category-checkbox-${sectionName}`}
/>
<span <span
className={`rounded w-4 h-4 mr-2 ${colorByOperationCategory[sectionName]}`} className={`rounded w-4 h-4 mr-2 ${colorByOperationCategory[sectionName]}`}
></span> ></span>
<span>{sectionName}</span> <span className="font-semibold flex-1 truncate">{sectionName}</span>
</div>
<div>
<Button
className="bg-inherit text-text"
disabled={!nonDisabledOperationTypesForSection.length}
onClick={() => selectAllOfCategory(operations)}
>
Select
</Button>
<Button
className="bg-inherit text-text"
disabled={!nonDisabledOperationTypesForSection.length}
onClick={() => clearCategory(operations)}
>
Clear
</Button>
</div> </div>
<ChevronDown
className={cn("h-5 w-5 transition-transform transform", {
"rotate-180": isExpanded,
})}
/>
</div> </div>
<ul {isExpanded && (
className="my-4 grid grid-cols-4 gap-4 place-items-stretch text-text " <ul className={cn("my-2 grid gap-y-2", {
data-testid="virtual-operations-list" "sm:grid-cols-4": true,
> })}>
{sortedOperations.map( {filteredOperations.map(
(operation) => !!operation && renderOperationType(operation) (operation) => !!operation && renderOperationType(operation)
)} )}
</ul> </ul>
)}
</div> </div>
); );
}; };
const handleClearOperationsFilter = () => { const handleClearOperationsFilter = () => {
setSelectedOperations([]); setSelectedOperations([]);
setSearchTerm("");
}; };
return ( return (
...@@ -279,68 +392,98 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -279,68 +392,98 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
/> />
) : null} ) : null}
<DialogContent <DialogContent
className="max-w-[95%] md:max-w-[80%] max-h-[90%] md:max-h-[80%] flex-column justify-center align-center bg-white text-black dark:bg-theme dark:text-white overflow-auto px-0" className=" pt-10 max-w-[95%] h-[90%] sm:max-h-[90%] sm:w-[90%] flex flex-col align-center overflow-auto px-0"
data-testid="operation-types-dialog" data-testid="operation-types-dialog"
> >
<DialogHeader> <DialogHeader className="pb-0">
<DialogTitle className="flex justify-center pt-2"> <div className="flex flex-col sm:flex-row justify-between items-center px-2">
Operation types filters <DialogTitle className="flex pb-1">
</DialogTitle> Operation Types Filters
</DialogHeader> </DialogTitle>
<div className="overflow-auto max-h-[500px] md:max-h-[600px]"> <div className="flex space-x-1">
{categorizedOperationTypes.map((categorizedOperationType) =>
renderSection(
categorizedOperationType.name,
categorizedOperationType.types
)
)}
</div>
<DialogFooter>
<div
className="flex flex-wrap justify-between w-full gap-y-4 border-t pt-4 px-2"
data-testid="operation-types-dialog-footer"
>
<div className="flex">
<Button <Button
type="button" type="button"
className="bg-inherit text-black dark:text-white" className="operations-button text-xs"
onClick={selectAll} onClick={selectAll}
> >
Select all All
</Button> </Button>
<Button <Button
type="button" type="button"
className="bg-inherit text-black dark:text-white" className="operations-button text-xs"
onClick={selectReal} onClick={selectReal}
> >
Select real Real
</Button> </Button>
<Button <Button
type="button" type="button"
className="bg-inherit text-black dark:text-white" className="operations-button text-xs"
onClick={selectVirtual} onClick={selectVirtual}
> >
Select virtual Virtual
</Button> </Button>
<Button <Button
type="button" type="button"
className="bg-inherit text-black dark:text-white" className="operations-button text-xs"
onClick={invertSelection} onClick={invertSelection}
> >
Invert Invert
</Button> </Button>
<Button <Button
type="button" type="button"
className="bg-inherit text-black dark:text-white" className="operations-button text-xs"
onClick={handleOnClear} onClick={handleOnClear}
> >
Clear Clear
</Button> </Button>
</div> </div>
</div>
<div className="px-2 mt-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4" />
<Input
type="text"
placeholder="Search operations..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-3 w-full"
/>
</div>
</div>
<div className="flex space-x-1 px-1">
<Button
type="button"
className="bg-inherit text-xs px-2"
onClick={handleExpandAll}
>
Expand All
</Button>
<Button
type="button"
className="bg-inherit text-xs px-2"
onClick={handleCollapseAll}
>
Collapse All
</Button>
</div>
</DialogHeader>
<div className="overflow-auto">
{categorizedOperationTypes.map((categorizedOperationType) =>
renderSection(
categorizedOperationType.name,
categorizedOperationType.types
)
)}
</div>
<DialogFooter className="pt-0">
<div
className="flex flex-wrap justify-end w-full gap-y-4 border-t pt-4 px-2"
data-testid="operation-types-dialog-footer"
>
<div className="flex w-full md:w-auto justify-center"> <div className="flex w-full md:w-auto justify-center">
<Button <Button
type="button" type="button"
className="bg-inherit text-black dark:text-white mx-2" className="bg-inherit mx-2"
onClick={() => { onClick={() => {
onOpenChange(false); onOpenChange(false);
}} }}
...@@ -361,4 +504,4 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({ ...@@ -361,4 +504,4 @@ const OperationTypesDialog: React.FC<OperationTypesDialogProps> = ({
); );
}; };
export default OperationTypesDialog; export default OperationTypesDialog;
\ No newline at end of file
...@@ -347,4 +347,9 @@ pre { ...@@ -347,4 +347,9 @@ pre {
.data-box span { .data-box span {
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 14px;
}
.operations-button {
border: 1px solid var(--color-light-gray);
background-color: inherit;
font-size: small;
} }
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment