Reviewed By: antonk52 Differential Revision: D51497340 fbshipit-source-id: 23bcc9a69c3e59e17ee9505c954c955f2a1c9e5f
216 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
};
|