1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-26 01:17:00 +02:00

Refine playground form (#1136)

* integrate results table with playground form

* fix playground api integration

* fix: playground loading state from api
This commit is contained in:
Tymoteusz Czech 2022-07-13 16:35:43 +02:00 committed by GitHub
parent 1f931c1ecc
commit 9d74fd976d
14 changed files with 318 additions and 87 deletions

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8.84835 0.184159C8.56384 -0.0789547 8.12358 -0.0575747 7.86499 0.231912C7.6064 0.521399 7.62741 0.96937 7.91192 1.23248C9.4831 2.68551 10.6078 4.74934 10.6078 7C10.6078 7.40738 10.5709 7.80863 10.5012 8.20125L11.6597 9.35972C11.8783 8.60937 12 7.81877 12 7C12 4.26298 10.6397 1.84083 8.84835 0.184159ZM9.35443 6.99995L9.35418 7.0542L7.63252 5.33255C7.30901 4.52372 6.76993 3.80029 6.11728 3.23173C5.82523 2.97731 5.79118 2.53017 6.04122 2.23302C6.29127 1.93586 6.73072 1.90121 7.02276 2.15563C8.33164 3.29586 9.35443 5.02309 9.35443 6.99995ZM7.57827 8.79752L6.48307 7.70233C6.17606 8.92745 5.08437 9.83342 3.78471 9.83342C2.24685 9.83342 1.00017 8.56492 1.00017 7.00015C1.00017 5.69492 1.8676 4.59583 3.04789 4.26714L0.458225 1.67748C0.172794 1.39205 0.178843 0.923223 0.471736 0.63033C0.764629 0.337437 1.23345 0.331388 1.51889 0.616819L13.2132 12.3111C13.4986 12.5965 13.4926 13.0654 13.1997 13.3583C12.9068 13.6511 12.4379 13.6572 12.1525 13.3718L10.556 11.7753C10.0694 12.539 9.48623 13.2259 8.84835 13.8158C8.56384 14.079 8.12358 14.0576 7.86499 13.7681C7.6064 13.4786 7.62741 13.0306 7.91192 12.7675C8.54021 12.1865 9.0971 11.5078 9.53922 10.7585L8.62356 9.84281C8.20906 10.6195 7.64861 11.2989 7.02276 11.8441C6.73072 12.0986 6.29127 12.0639 6.04122 11.7668C5.79118 11.4696 5.82523 11.0225 6.11728 10.768C6.73471 10.2302 7.25048 9.55374 7.57827 8.79752Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8.84818 0.184159C8.56367 -0.0789547 8.1234 -0.0575747 7.86482 0.231912C7.60623 0.521399 7.62724 0.96937 7.91175 1.23248C9.48292 2.68551 10.6076 4.74934 10.6076 7C10.6076 9.25066 9.48292 11.3145 7.91175 12.7675C7.62724 13.0306 7.60623 13.4786 7.86482 13.7681C8.1234 14.0576 8.56367 14.079 8.84818 13.8158C10.6396 12.1592 11.9998 9.73702 11.9998 7C11.9998 4.26298 10.6396 1.84083 8.84818 0.184159ZM6.04105 2.23302C6.29109 1.93586 6.73054 1.90121 7.02259 2.15563C8.33146 3.29586 9.35426 5.02309 9.35426 6.99995C9.35426 8.97682 8.33145 10.7039 7.02259 11.8441C6.73054 12.0986 6.29109 12.0639 6.04105 11.7668C5.79101 11.4696 5.82506 11.0225 6.11711 10.768C7.19425 9.82969 7.96199 8.46967 7.96199 6.99995C7.96199 5.53021 7.19423 4.17007 6.11711 3.23173C5.82506 2.97731 5.79101 2.53017 6.04105 2.23302ZM6.56908 7.00015C6.56908 8.56492 5.3224 9.83342 3.78454 9.83342C2.24668 9.83342 1 8.56492 1 7.00015C1 5.43539 2.24668 4.16689 3.78454 4.16689C5.3224 4.16689 6.56908 5.43539 6.56908 7.00015Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -17,9 +17,23 @@ interface IPageContentProps extends PaperProps {
* @deprecated fix feature event log and remove
*/
disableBorder?: boolean;
disableLoading?: boolean;
bodyClass?: string;
}
const PageContentLoading: FC<{ isLoading: boolean }> = ({
children,
isLoading,
}) => {
const ref = useLoading(isLoading);
return (
<div ref={ref} aria-busy={isLoading} aria-live="polite">
{children}
</div>
);
};
export const PageContent: FC<IPageContentProps> = ({
children,
header,
@ -27,11 +41,11 @@ export const PageContent: FC<IPageContentProps> = ({
disableBorder = false,
bodyClass = '',
isLoading = false,
disableLoading = false,
className,
...rest
}) => {
const { classes: styles } = useStyles();
const ref = useLoading(isLoading);
const headerClasses = classnames(styles.headerContainer, {
[styles.paddingDisabled]: disablePadding,
@ -48,27 +62,33 @@ export const PageContent: FC<IPageContentProps> = ({
const paperProps = disableBorder ? { elevation: 0 } : {};
const content = (
<Paper
{...rest}
{...paperProps}
className={classnames(styles.container, className)}
>
<ConditionallyRender
condition={Boolean(header)}
show={
<div className={headerClasses}>
<ConditionallyRender
condition={typeof header === 'string'}
show={<PageHeader title={header as string} />}
elseShow={header}
/>
</div>
}
/>
<div className={bodyClasses}>{children}</div>
</Paper>
);
if (disableLoading) {
return content;
}
return (
<div ref={ref} aria-busy={isLoading} aria-live="polite">
<Paper
{...rest}
{...paperProps}
className={classnames(styles.container, className)}
>
<ConditionallyRender
condition={Boolean(header)}
show={
<div className={headerClasses}>
<ConditionallyRender
condition={typeof header === 'string'}
show={<PageHeader title={header as string} />}
elseShow={header}
/>
</div>
}
/>
<div className={bodyClasses}>{children}</div>
</Paper>
</div>
<PageContentLoading isLoading={isLoading}>{content}</PageContentLoading>
);
};

View File

@ -15,6 +15,7 @@ interface ISearchProps {
className?: string;
placeholder?: string;
hasFilters?: boolean;
disabled?: boolean;
getSearchContext?: () => IGetSearchContextOutput;
}
@ -24,6 +25,7 @@ export const Search = ({
className,
placeholder: customPlaceholder,
hasFilters,
disabled,
getSearchContext,
}: ISearchProps) => {
const ref = useRef<HTMLInputElement>();
@ -79,6 +81,7 @@ export const Search = ({
onChange={e => onSearchChange(e.target.value)}
onFocus={() => setShowSuggestions(true)}
onBlur={() => setShowSuggestions(false)}
disabled={disabled}
/>
<div
className={classnames(

View File

@ -1,4 +1,5 @@
import { FormEventHandler, useState, VFC } from 'react';
import { FormEventHandler, useEffect, useState, VFC } from 'react';
import { useSearchParams } from 'react-router-dom';
import {
Box,
Button,
@ -13,22 +14,87 @@ import { PlaygroundConnectionFieldset } from './PlaygroundConnectionFieldset/Pla
import { PlaygroundCodeFieldset } from './PlaygroundCodeFieldset/PlaygroundCodeFieldset';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import { PlaygroundResultsTable } from './PlaygroundResultsTable/PlaygroundResultsTable';
import { ContextBanner } from './PlaygroundResultsTable/ContextBanner/ContextBanner';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import usePlaygroundApi from 'hooks/api/actions/usePlayground/usePlayground';
import { PlaygroundResponseSchema } from 'hooks/api/actions/usePlayground/playground.model';
interface IPlaygroundProps {}
export const Playground: VFC<IPlaygroundProps> = () => {
const theme = useTheme();
const [environment, onSetEnvironment] = useState<string>('');
const [projects, onSetProjects] = useState<string[]>([]);
const [context, setContext] = useState<string>();
const [contextObject, setContextObject] = useState<string>();
const [results, setResults] = useState<
PlaygroundResponseSchema | undefined
>();
const { setToastData } = useToast();
const [searchParams, setSearchParams] = useSearchParams();
const { evaluatePlayground, loading } = usePlaygroundApi();
const onSubmit: FormEventHandler<HTMLFormElement> = event => {
useEffect(() => {
// Load initial values from URL
try {
const environmentFromUrl = searchParams.get('environment');
if (environmentFromUrl) {
onSetEnvironment(environmentFromUrl);
}
const projectsFromUrl = searchParams.get('projects');
if (projectsFromUrl) {
onSetProjects(projectsFromUrl.split(','));
}
const contextFromUrl = searchParams.get('context');
if (contextFromUrl) {
setContext(decodeURI(contextFromUrl));
}
} catch (error) {
setToastData({
type: 'error',
title: `Failed to parse URL parameters: ${formatUnknownError(
error
)}`,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSubmit: FormEventHandler<HTMLFormElement> = async event => {
event.preventDefault();
try {
setContextObject(
JSON.stringify(JSON.parse(context || '{}'), null, 2)
);
const parsedContext = JSON.parse(context || '{}');
const response = await evaluatePlayground({
environment,
projects:
!projects ||
projects.length === 0 ||
(projects.length === 1 && projects[0] === '*')
? '*'
: projects,
context: {
appName: 'playground',
...parsedContext,
},
});
// Set URL search parameters
searchParams.set('context', encodeURI(context || '')); // always set because of native validation
searchParams.set('environment', environment);
if (
Array.isArray(projects) &&
projects.length > 0 &&
!(projects.length === 1 && projects[0] === '*')
) {
searchParams.set('projects', projects.join(','));
} else {
searchParams.delete('projects');
}
setSearchParams(searchParams);
// Display results
setResults(response);
} catch (error: unknown) {
setToastData({
type: 'error',
@ -38,12 +104,18 @@ export const Playground: VFC<IPlaygroundProps> = () => {
};
return (
<PageContent header={<PageHeader title="Unleash playground" />}>
<PageContent
header={<PageHeader title="Unleash playground" />}
disableLoading
bodyClass={'no-padding'}
>
<Paper
elevation={0}
sx={{
px: 4,
py: 3,
mb: 4,
m: 4,
background: theme.palette.grey[200],
}}
>
@ -55,7 +127,12 @@ export const Playground: VFC<IPlaygroundProps> = () => {
>
Configure playground
</Typography>
<PlaygroundConnectionFieldset />
<PlaygroundConnectionFieldset
environment={environment}
projects={projects}
setEnvironment={onSetEnvironment}
setProjects={onSetProjects}
/>
<Divider
variant="fullWidth"
sx={{
@ -81,12 +158,33 @@ export const Playground: VFC<IPlaygroundProps> = () => {
</Button>
</Box>
</Paper>
{Boolean(contextObject) && (
<Box sx={{ p: 4 }}>
<Typography>TODO: Request</Typography>
<pre>{contextObject}</pre>
</Box>
)}
<ConditionallyRender
condition={Boolean(results)}
show={
<>
<Divider />
<ContextBanner
environment={
(results as PlaygroundResponseSchema)?.input
?.environment
}
projects={
(results as PlaygroundResponseSchema)?.input
?.projects
}
context={
(results as PlaygroundResponseSchema)?.input
?.context
}
/>
</>
}
/>
<PlaygroundResultsTable
loading={loading}
features={results?.features}
/>
</PageContent>
);
};

View File

@ -33,12 +33,15 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
}) => {
const theme = useTheme();
const { setToastData } = useToast();
const { context } = useUnleashContext();
const contextOptions = context
const { context: contextData } = useUnleashContext();
const contextOptions = contextData
.sort((a, b) => a.sortOrder - b.sortOrder)
.map(({ name }) => name);
const [error, setError] = useState<string>();
const debounceSetError = useMemo(
const [fieldExist, setFieldExist] = useState<boolean>(false);
const [contextField, setContextField] = useState<string>('');
const [contextValue, setContextValue] = useState<string>('');
const debounceJsonParsing = useMemo(
() =>
debounce((input?: string) => {
if (!input) {
@ -46,22 +49,22 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
}
try {
JSON.parse(input);
const contextValue = JSON.parse(input);
setFieldExist(contextValue[contextField] !== undefined);
} catch (error: unknown) {
return setError(formatUnknownError(error));
}
return setError(undefined);
}, 250),
[setError]
[setError, contextField, setFieldExist]
);
useEffect(() => {
debounceSetError(value);
}, [debounceSetError, value]);
debounceJsonParsing(value);
}, [debounceJsonParsing, value]);
const [contextField, setContextField] = useState<string>('');
const [contextValue, setContextValue] = useState<string>('');
const onAddField = () => {
try {
const currentValue = JSON.parse(value || '{}');
@ -75,6 +78,7 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
2
)
);
setContextValue('');
} catch (error) {
setToastData({
type: 'error',
@ -154,7 +158,11 @@ export const PlaygroundCodeFieldset: VFC<IPlaygroundCodeFieldsetProps> = ({
disabled={!contextField || Boolean(error)}
onClick={onAddField}
>
Add context field
{`${
!fieldExist
? 'Add context field'
: 'Replace context field value'
} `}
</Button>
</Box>
</Box>

View File

@ -1,4 +1,4 @@
import { ComponentProps, useState, VFC } from 'react';
import { ComponentProps, VFC } from 'react';
import {
Autocomplete,
Box,
@ -9,7 +9,12 @@ import {
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
interface IPlaygroundConnectionFieldsetProps {}
interface IPlaygroundConnectionFieldsetProps {
environment: string;
projects: string[];
setProjects: (projects: string[]) => void;
setEnvironment: (environment: string) => void;
}
interface IOption {
label: string;
@ -20,7 +25,7 @@ const allOption: IOption = { label: 'ALL', id: '*' };
export const PlaygroundConnectionFieldset: VFC<
IPlaygroundConnectionFieldsetProps
> = () => {
> = ({ environment, projects, setProjects, setEnvironment }) => {
const theme = useTheme();
const { environments } = useEnvironments();
const environmentOptions = environments
@ -36,7 +41,6 @@ export const PlaygroundConnectionFieldset: VFC<
id,
})),
];
const [projects, setProjects] = useState<IOption | IOption[]>(allOption);
const onProjectsChange: ComponentProps<typeof Autocomplete>['onChange'] = (
event,
@ -45,26 +49,29 @@ export const PlaygroundConnectionFieldset: VFC<
) => {
const newProjects = value as IOption | IOption[];
if (reason === 'clear' || newProjects === null) {
return setProjects(allOption);
return setProjects([allOption.id]);
}
if (Array.isArray(newProjects)) {
if (newProjects.length === 0) {
return setProjects(allOption);
return setProjects([allOption.id]);
}
if (
newProjects.find(({ id }) => id === allOption.id) !== undefined
) {
return setProjects(allOption);
return setProjects([allOption.id]);
}
return setProjects(newProjects);
return setProjects(newProjects.map(({ id }) => id));
}
if (newProjects.id === allOption.id) {
return setProjects(allOption);
return setProjects([allOption.id]);
}
return setProjects([newProjects]);
return setProjects([newProjects.id]);
};
const isAllProjects =
projects.length === 0 || (projects.length === 1 && projects[0] === '*');
return (
<Box sx={{ pb: 2 }}>
<Typography
@ -83,19 +90,27 @@ export const PlaygroundConnectionFieldset: VFC<
renderInput={params => (
<TextField {...params} label="Environment" required />
)}
value={environment}
onChange={(event, value) => setEnvironment(value || '')}
size="small"
/>
<Autocomplete
disablePortal
id="projects"
multiple={Array.isArray(projects)}
multiple={!isAllProjects}
options={projectsOptions}
sx={{ width: 300, maxWidth: '100%' }}
renderInput={params => (
<TextField {...params} label="Projects" />
)}
size="small"
value={projects}
value={
isAllProjects
? allOption
: projectsOptions.filter(({ id }) =>
projects.includes(id)
)
}
onChange={onProjectsChange}
/>
</Box>

View File

@ -1,24 +1,37 @@
import { colors } from 'themes/colors';
import { Alert, styled } from '@mui/material';
import { SdkContextSchema } from '../../playground.model';
import { SdkContextSchema } from 'hooks/api/actions/usePlayground/playground.model';
interface IContextBannerProps {
environment: string;
projects?: string | string[];
context: SdkContextSchema;
}
const StyledContextFieldList = styled('ul')(({ theme }) => ({
color: colors.black,
listStyleType: 'none',
paddingInlineStart: theme.spacing(16),
padding: theme.spacing(2),
}));
export const ContextBanner = ({ context }: IContextBannerProps) => {
export const ContextBanner = ({
environment,
projects = [],
context,
}: IContextBannerProps) => {
return (
<Alert severity="info" sx={{ my: 2 }}>
Your results are generated based on this configuration
<Alert severity="info" sx={{ mt: 4, mb: 2, mx: 4 }}>
Your results are generated based on this configuration:
<StyledContextFieldList>
<li>environment: {environment}</li>
<li>
projects:{' '}
{Array.isArray(projects) ? projects.join(', ') : projects}
</li>
{Object.entries(context).map(([key, value]) => (
<li key={key}>{`${key}: ${value}`}</li>
<li key={key}>
<span>{key}:</span> {value}
</li>
))}
</StyledContextFieldList>
</Alert>

View File

@ -1,9 +1,8 @@
import React from 'react';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { colors } from 'themes/colors';
import { ReactComponent as FeatureEnabledIcon } from 'assets/icons/isenabled-true.svg';
import { ReactComponent as FeatureDisabledIcon } from 'assets/icons/isenabled-false.svg';
import { Chip, styled, useTheme } from '@mui/material';
import { Box, Chip, styled, useTheme } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
interface IFeatureStatusCellProps {
@ -36,6 +35,16 @@ const StyledTrueChip = styled(Chip)(({ theme }) => ({
},
}));
const StyledCellBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(1, 2),
}));
const StyledChipWrapper = styled(Box)(() => ({
marginRight: 'auto',
}));
export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
const theme = useTheme();
const icon = (
@ -43,13 +52,13 @@ export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
condition={enabled}
show={
<FeatureEnabledIcon
stroke={theme.palette.success.main}
color={theme.palette.success.main}
strokeWidth="0.25"
/>
}
elseShow={
<FeatureDisabledIcon
stroke={theme.palette.error.main}
color={theme.palette.error.main}
strokeWidth="0.25"
/>
}
@ -59,12 +68,14 @@ export const FeatureStatusCell = ({ enabled }: IFeatureStatusCellProps) => {
const label = enabled ? 'True' : 'False';
return (
<TextCell>
<ConditionallyRender
condition={enabled}
show={<StyledTrueChip icon={icon} label={label} />}
elseShow={<StyledFalseChip icon={icon} label={label} />}
/>
</TextCell>
<StyledCellBox>
<StyledChipWrapper data-loading>
<ConditionallyRender
condition={enabled}
show={<StyledTrueChip icon={icon} label={label} />}
elseShow={<StyledFalseChip icon={icon} label={label} />}
/>
</StyledChipWrapper>
</StyledCellBox>
);
};

View File

@ -20,7 +20,7 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { useSearch } from 'hooks/useSearch';
import { createLocalStorage } from 'utils/createLocalStorage';
import { FeatureStatusCell } from './FeatureStatusCell/FeatureStatusCell';
import { PlaygroundFeatureSchema } from '../playground.model';
import { PlaygroundFeatureSchema } from 'hooks/api/actions/usePlayground/playground.model';
const defaultSort: SortingRule<string> = { id: 'name' };
const { value, setValue } = createLocalStorage(
@ -53,9 +53,9 @@ export const PlaygroundResultsTable = ({
return loading
? Array(5).fill({
name: 'Feature name',
project: 'Feature Project',
variant: 'Feature variant',
enabled: 'Feature state',
projectId: 'FeatureProject',
variant: { name: 'FeatureVariant' },
enabled: true,
})
: searchedData;
}, [searchedData, loading]);
@ -109,6 +109,8 @@ export const PlaygroundResultsTable = ({
}
if (searchValue) {
tableState.search = searchValue;
} else {
delete tableState.search;
}
setSearchParams(tableState, {
@ -138,6 +140,7 @@ export const PlaygroundResultsTable = ({
onChange={setSearchValue}
hasFilters
getSearchContext={getSearchContext}
disabled={loading}
/>
}
/>
@ -145,10 +148,12 @@ export const PlaygroundResultsTable = ({
isLoading={loading}
>
<ConditionallyRender
condition={!loading && data.length === 0}
condition={!loading && (!data || data.length === 0)}
show={() => (
<TablePlaceholder>
None of the feature toggles were evaluated yet.
{data === undefined
? 'None of the feature toggles were evaluated yet.'
: 'No results found.'}
</TablePlaceholder>
)}
elseShow={() => (
@ -203,8 +208,11 @@ const COLUMNS = [
accessor: 'name',
searchable: true,
width: '60%',
Cell: ({ value }: any) => (
<LinkCell title={value} to={`/feature/${value}`} />
Cell: ({ value, row: { original } }: any) => (
<LinkCell
title={value}
to={`/projects/${original?.projectId}/features/${value}`}
/>
),
},
{
@ -226,7 +234,12 @@ const COLUMNS = [
filterName: 'variant',
searchable: true,
maxWidth: 170,
Cell: ({ value }: any) => <HighlightCell value={value} />,
Cell: ({
value,
row: {
original: { variant },
},
}: any) => <HighlightCell value={variant?.enabled ? value : ''} />,
},
{
Header: 'isEnabled',
@ -234,5 +247,6 @@ const COLUMNS = [
maxWidth: 170,
Cell: ({ value }: any) => <FeatureStatusCell enabled={value} />,
sortType: 'boolean',
sortInverted: true,
},
];

View File

@ -1,3 +1,5 @@
// TODO: replace with auto-generated openapi code
export enum PlaygroundFeatureSchemaVariantPayloadTypeEnum {
Json = 'json',
Csv = 'csv',

View File

@ -0,0 +1,37 @@
import useAPI from '../useApi/useApi';
import {
PlaygroundRequestSchema,
PlaygroundResponseSchema,
} from './playground.model';
const usePlaygroundApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
});
const URI = 'api/admin/playground';
const evaluatePlayground = async (payload: PlaygroundRequestSchema) => {
const path = URI;
const req = createRequest(path, {
method: 'POST',
body: JSON.stringify(payload),
});
try {
const res = await makeRequest(req.caller, req.id);
return res.json() as Promise<PlaygroundResponseSchema>;
} catch (error) {
throw error;
}
};
return {
evaluatePlayground,
errors,
loading,
};
};
export default usePlaygroundApi;

View File

@ -112,7 +112,7 @@ export const getColumnValues = (column: any, row: any) => {
: column.accessor.includes('.')
? column.accessor
.split('.')
.reduce((object: any, key: string) => object[key], row)
.reduce((object: any, key: string) => object?.[key], row)
: row[column.accessor];
if (column.filterParsing) {

View File

@ -15,7 +15,7 @@ export const sortTypes = {
return a === b ? 0 : a ? 1 : -1;
},
alphanumeric: (a: any, b: any, id: string) =>
a?.values?.[id]
(a?.values?.[id] || '')
?.toLowerCase()
.localeCompare(b?.values?.[id]?.toLowerCase()),
.localeCompare(b?.values?.[id]?.toLowerCase() || ''),
};