Files
flipper/desktop/flipper-plugin/src/ui/PowerSearch/index.tsx
Andrey Goncharov 877253191d Support multi-line wrapping for long queries / small screens
Reviewed By: antonk52

Differential Revision: D51497340

fbshipit-source-id: 23bcc9a69c3e59e17ee9505c954c955f2a1c9e5f
2023-11-21 07:56:19 -08:00

216 lines
6.4 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import * as React from 'react';
import {Space} from 'antd';
import {
PowerSearchConfig,
FieldConfig,
OperatorConfig,
EnumLabels,
} from './PowerSearchConfig';
import {PowerSearchContainer} from './PowerSearchContainer';
import {
PowerSearchTermFinder,
PowerSearchTermFinderOption,
PowerSearchTermFinderOptionGroup,
} from './PowerSearchTermFinder';
import {
IncompleteSearchExpressionTerm,
PowerSearchTerm,
SearchExpressionTerm,
} from './PowerSearchTerm';
import {useLatestRef} from '../../utils/useLatestRef';
import {useUpdateEffect} from 'react-use';
import {theme} from '../theme';
import {SearchOutlined} from '@ant-design/icons';
import {getFlipperLib} from 'flipper-plugin-core';
export {
PowerSearchConfig,
EnumLabels,
OperatorConfig,
FieldConfig,
SearchExpressionTerm,
};
type PowerSearchProps = {
config: PowerSearchConfig;
// Overrides current state of the component with every update.
// It is the way to continuously force update the state of the power search externally.
// Takes prefernce over `initialSearchExpression`.
searchExpression?: SearchExpressionTerm[];
// Component stays uncontrolled and maintains its own state.
// It is respected only on initialization and any future updates are ignored.
initialSearchExpression?: SearchExpressionTerm[];
onSearchExpressionChange: (searchExpression: SearchExpressionTerm[]) => void;
onConfirmUnknownOption?: (
searchString: string,
) => SearchExpressionTerm | undefined;
};
const OPTION_KEY_DELIMITER = '::';
export const PowerSearch: React.FC<PowerSearchProps> = ({
config,
searchExpression: searchExpressionExternal,
initialSearchExpression,
onSearchExpressionChange,
onConfirmUnknownOption,
}) => {
const [searchExpression, setSearchExpression] = React.useState<
IncompleteSearchExpressionTerm[]
>(() => {
if (searchExpressionExternal) {
return searchExpressionExternal;
}
if (initialSearchExpression) {
return initialSearchExpression;
}
return [];
});
const onSearchExpressionChangeLatestRef = useLatestRef(
onSearchExpressionChange,
);
useUpdateEffect(() => {
if (searchExpression.every((term) => term.searchValue !== undefined)) {
onSearchExpressionChangeLatestRef.current(
searchExpression as SearchExpressionTerm[],
);
getFlipperLib().logger.track(
'usage',
'power-search:search-expression-finalize',
);
}
}, [searchExpression, onSearchExpressionChangeLatestRef]);
React.useEffect(() => {
if (searchExpressionExternal) {
setSearchExpression(searchExpressionExternal);
}
}, [searchExpressionExternal]);
const options: PowerSearchTermFinderOptionGroup[] = React.useMemo(() => {
const groupedOptions: PowerSearchTermFinderOptionGroup[] = [];
for (const field of Object.values(config.fields)) {
const group: PowerSearchTermFinderOptionGroup = {
label: field.label,
options: [],
value: field.key,
};
for (const operator of Object.values(field.operators)) {
const option: PowerSearchTermFinderOption = {
label: `${field.label} ${operator.label}`,
value: `${field.key}${OPTION_KEY_DELIMITER}${operator.key}`,
};
group.options.push(option);
}
groupedOptions.push(group);
}
return groupedOptions;
}, [config.fields]);
const searchTermFinderRef = React.useRef<{
focus: () => void;
blur: () => void;
scrollTo: () => void;
}>(null);
return (
<PowerSearchContainer>
<Space size={[theme.space.tiny, 0]} wrap>
<SearchOutlined
style={{
marginLeft: theme.space.tiny,
marginRight: theme.space.tiny,
color: theme.textColorSecondary,
}}
/>
{searchExpression.map((searchTerm, i) => {
return (
<PowerSearchTerm
key={JSON.stringify(searchTerm)}
searchTerm={searchTerm}
onCancel={() => {
setSearchExpression((prevSearchExpression) => {
if (prevSearchExpression[i]) {
return [
...prevSearchExpression.slice(0, i),
...prevSearchExpression.slice(i + 1),
];
}
return prevSearchExpression;
});
}}
onFinalize={(finalSearchTerm) => {
setSearchExpression((prevSearchExpression) => {
return [
...prevSearchExpression.slice(0, i),
finalSearchTerm,
...prevSearchExpression.slice(i + 1),
];
});
searchTermFinderRef.current?.focus();
}}
/>
);
})}
</Space>
<PowerSearchTermFinder
ref={searchTermFinderRef}
options={options}
onSelect={(selectedOption) => {
const [fieldKey, operatorKey] =
selectedOption.value.split(OPTION_KEY_DELIMITER);
const fieldConfig = config.fields[fieldKey];
const operatorConfig = fieldConfig.operators[operatorKey];
setSearchExpression((prevSearchExpression) => [
...prevSearchExpression,
{
field: fieldConfig,
operator: operatorConfig,
searchValue:
operatorConfig.valueType === 'NO_VALUE' ? null : undefined,
},
]);
}}
onBackspacePressWhileEmpty={() => {
setSearchExpression((prevSearchExpression) => {
return prevSearchExpression.slice(
0,
prevSearchExpression.length - 1,
);
});
}}
onConfirmUnknownOption={
onConfirmUnknownOption
? (searchString) => {
const searchExpressionTerm =
onConfirmUnknownOption(searchString);
if (searchExpressionTerm) {
setSearchExpression((prevSearchExpression) => [
...prevSearchExpression,
searchExpressionTerm,
]);
}
}
: undefined
}
/>
</PowerSearchContainer>
);
};