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

feat: improve filters panel #3248

Merged
merged 21 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
142bcb0
feat: section component
hamed-musallam Sep 17, 2024
e8c30ef
refactor: replace table with section component
hamed-musallam Sep 17, 2024
bcc8b72
refactor: render active Filter section
hamed-musallam Sep 18, 2024
c30927c
feat: improve apodization filter options
hamed-musallam Sep 18, 2024
272ae1d
refactor: open the processing panel if one of the filter is selected
hamed-musallam Sep 19, 2024
29c7981
feat: add new Filter section in case the filter does not exists
hamed-musallam Sep 19, 2024
dc89f09
chore: fix eslint, prettier, and stylelint
hamed-musallam Sep 19, 2024
262f9ce
test: fix filter selectors
hamed-musallam Sep 19, 2024
3e44419
feat: hook to manager syn filter options
hamed-musallam Sep 20, 2024
81c1630
feat: improve 1D phase correction options panel
hamed-musallam Sep 20, 2024
0dcc88a
refactor: sync in apodization to use the new hook
hamed-musallam Sep 20, 2024
d74b88c
refactor: zero filling filter options panel
hamed-musallam Sep 20, 2024
efcc0ed
feat: display 'No filters' when no filters
hamed-musallam Sep 20, 2024
ea7b1d2
chore: fix stylelint
hamed-musallam Sep 20, 2024
67da3a3
feat: improve 2D phase correction filter options panel
hamed-musallam Sep 23, 2024
c76d3a0
feat: improve baseline correction filter options panel
hamed-musallam Sep 23, 2024
286ab9f
refactor: relocate filter hooks to the hooks directory
hamed-musallam Sep 24, 2024
a7a5c5e
feat: improve editing of Shift filter options
hamed-musallam Sep 24, 2024
2bd5efd
feat: improve editing of exclusion zones filter options
hamed-musallam Sep 24, 2024
6928dfb
refactor: filter value should be null in case it a new filter in the …
hamed-musallam Sep 25, 2024
e1a15e5
refcator: close the filters sections after add a new filter
hamed-musallam Sep 25, 2024
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
89 changes: 89 additions & 0 deletions src/component/context/FilterSyncOptionsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';

interface FilterSyncOptionsState<T> {
sharedFilterOptions: T | null;
updateFilterOptions: (options: T) => void;
}

const FilterSyncOptionsContext =
createContext<FilterSyncOptionsState<any> | null>(null);

export function useFilterSyncOptions<T>(): FilterSyncOptionsState<T> {
const context = useContext(
FilterSyncOptionsContext,
) as FilterSyncOptionsState<T>;

if (!context) {
throw new Error(
'Filter sync options context must be used within an FilterSyncOptionsProvider',
);
}

return context;
}

export function useSyncedFilterOptions(onWatch: (options: any) => void) {
const { sharedFilterOptions, updateFilterOptions } = useFilterSyncOptions();
const isSyncOptionsDirty = useRef(true);

const watchRef = useRef(onWatch);

// Update the ref when onWatch changes
useEffect(() => {
watchRef.current = onWatch;
}, [onWatch]);

useEffect(() => {
if (sharedFilterOptions && isSyncOptionsDirty.current) {
watchRef.current(sharedFilterOptions);
} else {
isSyncOptionsDirty.current = true;
}
}, [sharedFilterOptions]);

const clearSyncFilterOptions = useCallback(() => {
isSyncOptionsDirty.current = true;
updateFilterOptions(null);
}, [updateFilterOptions]);

const syncFilterOptions = useCallback(
(options) => {
isSyncOptionsDirty.current = false;
updateFilterOptions(options);
},
[updateFilterOptions],
);

return { clearSyncFilterOptions, syncFilterOptions };
}

export function FilterSyncOptionsProvider({
children,
}: {
children: React.ReactNode;
}) {
const [sharedFilterOptions, updateFilterOptions] = useState<unknown | null>(
null,
);

const state = useMemo(() => {
return {
sharedFilterOptions,
updateFilterOptions: (options) => updateFilterOptions({ ...options }),
};
}, [sharedFilterOptions]);

return (
<FilterSyncOptionsContext.Provider value={state}>
{children}
</FilterSyncOptionsContext.Provider>
);
}
289 changes: 289 additions & 0 deletions src/component/elements/Sections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
/** @jsxImportSource @emotion/react */
import { Icon, Tag } from '@blueprintjs/core';
import styled from '@emotion/styled';
import React, {
createContext,
CSSProperties,
ReactNode,
useContext,
useMemo,
} from 'react';

interface SelectionsContextState {
overflow: boolean;
renderActiveSectionContentOnly: boolean;
}

const selectionState: SelectionsContextState = {
overflow: false,
renderActiveSectionContentOnly: false,
};

const SectionsContext = createContext<SelectionsContextState>(selectionState);

export function useSections() {
const context = useContext(SectionsContext);

if (!context) {
throw new Error('Section context was not found');
}
return context;
}

interface ActiveProps {
isOpen: boolean;
overflow?: boolean;
}

const Container = styled.div<{ overflow: boolean }>(
({ overflow }) => `
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: ${overflow ? 'auto' : 'hidden'};
border: 1px solid #ddd;
`,
);

const SectionWrapper = styled.div<ActiveProps>(
({ isOpen, overflow }) => `
display: flex;
flex-direction: column;
flex: ${isOpen ? (overflow ? '1' : overflow ? '1' : '1 1 1px') : 'none'};
`,
);

const Active = styled(Tag)<ActiveProps>(
({ isOpen }) => `
background-color: ${isOpen ? '#4CAF50' : '#ccc'};
color: ${isOpen ? 'white' : 'black'};
margin-right: 10px;
flex-shrink: 0;
`,
);

const TitleContainer = styled.div`
display: flex;
align-items: center;
min-width: 0;
flex-grow: 1;
`;

const Title = styled.div`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
font-weight: 500;
`;

const OpenIcon = styled(Icon)<ActiveProps>`
transform: ${({ isOpen }) => (isOpen ? 'rotate(90deg)' : 'rotate(0deg)')};
transition: transform 0.3s ease;
margin-left: 10px;
`;

const ContentWrapper = styled.div<ActiveProps>(
({ isOpen, overflow }) => `
background-color: white;
// overflow: hidden;
display: ${isOpen ? 'flex' : 'none'};
flex: ${isOpen ? (overflow ? '1' : '1 1 1px') : 'none'};
max-height: 100%;
flex-direction:column;

`,
);
const Content = styled.div`
height: 100%;
width: 100%;
overflow: auto;
display: flex;
flex-direction: column;
padding: 10px;
`;
const RightElementsContainer = styled.div`
display: flex;
align-items: center;
`;

const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 10px;
background-color: #f5f5f5;

&:hover {
background-color: #e0e0e0;
}

&:active {
background-color: #d0d0d0;
}
`;

const InnerHeader = styled.div`
padding: 5px;
background-color: white;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
`;

interface BaseSectionProps {
title: string;
serial?: number;
rightElement?: ReactNode | ((isOpen) => ReactNode);
headerStyle?: CSSProperties;
}

interface SectionItemProps extends BaseSectionProps {
id?: string;
onClick?: (id, event?: React.MouseEvent<HTMLDivElement>) => void;
children?: ReactNode | ((options: { isOpen?: boolean }) => ReactNode);
selectedSectionId?: string;
sticky?: boolean;
}

interface SectionProps {
children?: ReactNode;
overflow?: boolean;
renderActiveSectionContentOnly?: boolean;
}

export function Sections(props: SectionProps) {
const {
children,
overflow = false,
renderActiveSectionContentOnly = false,
} = props;

const state = useMemo(() => {
return { overflow, renderActiveSectionContentOnly };
}, [overflow, renderActiveSectionContentOnly]);
return (
<SectionsContext.Provider value={state}>
<Container overflow={overflow}>{children}</Container>
</SectionsContext.Provider>
);
}

function SectionHeader(props: React.HTMLAttributes<HTMLDivElement>) {
const { children, ...otherProps } = props;
return <InnerHeader {...otherProps}>{children}</InnerHeader>;
}

function SectionBody(props: React.HTMLAttributes<HTMLDivElement>) {
const { children, ...otherProps } = props;

return <Content {...otherProps}>{children}</Content>;
}

function SectionItem(props: SectionItemProps) {
const {
id = props.title,
title,
onClick,
serial,
rightElement,
children,
selectedSectionId,
headerStyle,
sticky = false,
} = props;

const isOpen = selectedSectionId === id;
const { overflow } = useSections();

return (
<SectionWrapper isOpen={isOpen} overflow={overflow}>
<MainSectionHeader
title={title}
isOpen={isOpen}
onClick={(event) => onClick?.(id, event)}
serial={serial}
rightElement={rightElement}
headerStyle={headerStyle}
sticky={sticky}
/>
<Wrapper isOpen={isOpen} id={id} selectedSectionId={selectedSectionId}>
{children}
</Wrapper>
</SectionWrapper>
);
}

interface WrapperProps {
children: ReactNode | ((options: { isOpen?: boolean }) => ReactNode);
isOpen: boolean;
id: string;
selectedSectionId?: string;
}

function Wrapper(props: WrapperProps) {
const { overflow, renderActiveSectionContentOnly } = useSections();

const { children, isOpen, selectedSectionId, id } = props;

if (renderActiveSectionContentOnly && id !== selectedSectionId) {
return null;
}

return (
<ContentWrapper isOpen={isOpen} overflow={overflow}>
{typeof children === 'function' ? children({ isOpen }) : children}
</ContentWrapper>
);
}

interface MainSectionHeaderProps
extends Pick<React.HTMLProps<HTMLDivElement>, 'onClick'>,
BaseSectionProps {
isOpen: boolean;
sticky: boolean;
}

function MainSectionHeader(props: MainSectionHeaderProps) {
const {
title,
isOpen = false,
onClick,
serial,
rightElement,
headerStyle = {},
sticky,
} = props;
return (
<Header
onClick={onClick}
style={{
...headerStyle,
...(sticky && {
position: 'sticky',
top: 0,
zIndex: 1,
}),
}}
>
<TitleContainer>
<Active round isOpen={isOpen}>
{serial}
</Active>
<Title>{title}</Title>
</TitleContainer>
<RightElementsContainer>
<RightElementsContainer onClick={(event) => event.stopPropagation()}>
{typeof rightElement === 'function'
? rightElement(isOpen)
: rightElement}
</RightElementsContainer>
<OpenIcon icon="chevron-right" isOpen={isOpen} />
</RightElementsContainer>
</Header>
);
}

Sections.Header = SectionHeader;
Sections.Body = SectionBody;
Sections.Item = SectionItem;
Loading
Loading