mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-21 13:47:39 +02:00
feat: Feature type lifetime API integration (#4295)
## About the changes API integration and tests.
This commit is contained in:
parent
c99b6b3abc
commit
464297d4be
@ -1,3 +1,4 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
import { Button, styled, Typography } from '@mui/material';
|
import { Button, styled, Typography } from '@mui/material';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ const StyledHomeButton = styled(Button)(({ theme }) => ({
|
|||||||
top: 45,
|
top: 45,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const NotFound = () => {
|
const NotFound: FC = ({ children }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onClickHome = () => {
|
const onClickHome = () => {
|
||||||
@ -70,6 +71,7 @@ const NotFound = () => {
|
|||||||
</StyledHomeButton>
|
</StyledHomeButton>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</div>
|
</div>
|
||||||
|
{children}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { VFC } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { FeatureTypeSchema } from 'openapi';
|
||||||
|
import { FeatureTypeForm } from './FeatureTypeForm/FeatureTypeForm';
|
||||||
|
|
||||||
|
type FeatureTypeEditProps = {
|
||||||
|
featureTypes: FeatureTypeSchema[];
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeatureTypeEdit: VFC<FeatureTypeEditProps> = ({
|
||||||
|
featureTypes,
|
||||||
|
loading,
|
||||||
|
}) => {
|
||||||
|
const { featureTypeId } = useParams();
|
||||||
|
const featureType = featureTypes.find(
|
||||||
|
featureType => featureType.id === featureTypeId
|
||||||
|
);
|
||||||
|
|
||||||
|
return <FeatureTypeForm featureType={featureType} loading={loading} />;
|
||||||
|
};
|
@ -0,0 +1,92 @@
|
|||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import { render } from 'utils/testRenderer';
|
||||||
|
import { FeatureTypeForm } from './FeatureTypeForm';
|
||||||
|
|
||||||
|
const mockFeatureType = {
|
||||||
|
id: '1',
|
||||||
|
name: 'Test',
|
||||||
|
description: 'Test',
|
||||||
|
lifetimeDays: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('FeatureTypeForm', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
render(
|
||||||
|
<FeatureTypeForm featureType={mockFeatureType} loading={false} />
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Edit toggle type: Test')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Expected lifetime')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render 404 if feature type is not found', () => {
|
||||||
|
render(<FeatureTypeForm featureType={undefined} loading={false} />);
|
||||||
|
expect(screen.getByTestId('404_NOT_FOUND')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not enable inputs and submit button when loading', () => {
|
||||||
|
render(
|
||||||
|
<FeatureTypeForm featureType={mockFeatureType} loading={true} />
|
||||||
|
);
|
||||||
|
expect(screen.getByLabelText('Expected lifetime')).toBeDisabled();
|
||||||
|
expect(screen.getByLabelText("doesn't expire")).toBeDisabled();
|
||||||
|
expect(screen.getByText('Save feature toggle type')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check "doesn\'t expire" when lifetime is 0', () => {
|
||||||
|
render(
|
||||||
|
<FeatureTypeForm
|
||||||
|
featureType={{
|
||||||
|
...mockFeatureType,
|
||||||
|
lifetimeDays: 0,
|
||||||
|
}}
|
||||||
|
loading={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const doesntExpire = screen.getByLabelText("doesn't expire");
|
||||||
|
expect(doesntExpire).toBeChecked();
|
||||||
|
expect(screen.getByLabelText('Expected lifetime')).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable lifetime input when "doesn\'t expire" is checked', () => {
|
||||||
|
render(
|
||||||
|
<FeatureTypeForm featureType={mockFeatureType} loading={false} />
|
||||||
|
);
|
||||||
|
const doesntExpire = screen.getByLabelText("doesn't expire");
|
||||||
|
const lifetime = screen.getByLabelText('Expected lifetime');
|
||||||
|
expect(lifetime).toBeEnabled();
|
||||||
|
doesntExpire.click();
|
||||||
|
expect(lifetime).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores lifetime input when "doesn\'t expire" is unchecked', () => {
|
||||||
|
render(
|
||||||
|
<FeatureTypeForm
|
||||||
|
featureType={{
|
||||||
|
...mockFeatureType,
|
||||||
|
lifetimeDays: 7,
|
||||||
|
}}
|
||||||
|
loading={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const doesntExpire = screen.getByLabelText("doesn't expire");
|
||||||
|
const lifetime = screen.getByLabelText('Expected lifetime');
|
||||||
|
doesntExpire.click();
|
||||||
|
expect(lifetime).toBeDisabled();
|
||||||
|
doesntExpire.click();
|
||||||
|
expect(lifetime).toBeEnabled();
|
||||||
|
expect(lifetime).toHaveValue(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable submit button when form is invalid', () => {
|
||||||
|
render(
|
||||||
|
<FeatureTypeForm
|
||||||
|
featureType={{
|
||||||
|
...mockFeatureType,
|
||||||
|
lifetimeDays: -6,
|
||||||
|
}}
|
||||||
|
loading={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Save feature toggle type')).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,8 @@
|
|||||||
import { type FormEventHandler, type VFC, useState } from 'react';
|
import { type FormEventHandler, type VFC, useState, useCallback } from 'react';
|
||||||
import { Box, Button, Typography, Checkbox, styled } from '@mui/material';
|
import { Box, Button, Typography, Checkbox, styled } from '@mui/material';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { useFeatureTypeApi } from 'hooks/api/actions/useFeatureTypeApi/useFeatureTypeApi';
|
||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
import NotFound from 'component/common/NotFound/NotFound';
|
import NotFound from 'component/common/NotFound/NotFound';
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
@ -10,9 +12,13 @@ import Input from 'component/common/Input/Input';
|
|||||||
import { FeatureTypeSchema } from 'openapi';
|
import { FeatureTypeSchema } from 'openapi';
|
||||||
import { trim } from 'component/common/util';
|
import { trim } from 'component/common/util';
|
||||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||||
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
|
||||||
type FeatureTypeFormProps = {
|
type FeatureTypeFormProps = {
|
||||||
featureTypes: FeatureTypeSchema[];
|
featureType?: FeatureTypeSchema;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -31,24 +37,22 @@ const StyledForm = styled(Box)(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export const FeatureTypeForm: VFC<FeatureTypeFormProps> = ({
|
export const FeatureTypeForm: VFC<FeatureTypeFormProps> = ({
|
||||||
featureTypes,
|
featureType,
|
||||||
loading,
|
loading,
|
||||||
}) => {
|
}) => {
|
||||||
const { featureTypeId } = useParams();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const featureType = featureTypes.find(
|
const { uiConfig } = useUiConfig();
|
||||||
featureType => featureType.id === featureTypeId
|
const { refetch } = useFeatureTypes();
|
||||||
);
|
const { updateFeatureTypeLifetime, loading: actionLoading } =
|
||||||
|
useFeatureTypeApi();
|
||||||
const [lifetime, setLifetime] = useState<number>(
|
const [lifetime, setLifetime] = useState<number>(
|
||||||
featureType?.lifetimeDays || 0
|
featureType?.lifetimeDays || 0
|
||||||
);
|
);
|
||||||
const [doesntExpire, setDoesntExpire] = useState<boolean>(
|
const [doesntExpire, setDoesntExpire] = useState<boolean>(
|
||||||
!featureType?.lifetimeDays
|
!featureType?.lifetimeDays
|
||||||
);
|
);
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
if (!loading && !featureType) {
|
const tracker = usePlausibleTracker();
|
||||||
return <NotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChangeLifetime = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const onChangeLifetime = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = parseInt(trim(e.target.value), 10);
|
const value = parseInt(trim(e.target.value), 10);
|
||||||
@ -68,14 +72,45 @@ export const FeatureTypeForm: VFC<FeatureTypeFormProps> = ({
|
|||||||
const isIncorrect =
|
const isIncorrect =
|
||||||
!doesntExpire && (Number.isNaN(lifetime) || lifetime < 0);
|
!doesntExpire && (Number.isNaN(lifetime) || lifetime < 0);
|
||||||
|
|
||||||
const onSubmit: FormEventHandler = e => {
|
const onSubmit: FormEventHandler = async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isIncorrect) return;
|
try {
|
||||||
|
if (!featureType?.id)
|
||||||
|
throw new Error('No feature toggle type loaded');
|
||||||
|
|
||||||
const value = doesntExpire ? 0 : lifetime;
|
const value = doesntExpire ? 0 : lifetime;
|
||||||
console.log('FIXME: onSubmit', value);
|
await updateFeatureTypeLifetime(featureType?.id, value);
|
||||||
|
refetch();
|
||||||
|
setToastData({
|
||||||
|
title: 'Feature type updated',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
navigate('/feature-toggle-type');
|
||||||
|
tracker.trackEvent('feature-type-edit');
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatApiCode = useCallback(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
`curl --location --request PUT '${uiConfig.unleashUrl}/api/admin/feature-types/${featureType?.id}/lifetime`,
|
||||||
|
"--header 'Authorization: INSERT_API_KEY'",
|
||||||
|
"--header 'Content-Type: application/json'",
|
||||||
|
'--data-raw \'{\n "lifetimeDays": 7\n}\'',
|
||||||
|
].join(' \\\n'),
|
||||||
|
[uiConfig, featureType?.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!loading && !featureType) {
|
||||||
|
return (
|
||||||
|
<NotFound>
|
||||||
|
<div data-testid="404_NOT_FOUND" />
|
||||||
|
</NotFound>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
modal
|
modal
|
||||||
@ -87,7 +122,7 @@ export const FeatureTypeForm: VFC<FeatureTypeFormProps> = ({
|
|||||||
description={featureType?.description || ''}
|
description={featureType?.description || ''}
|
||||||
documentationLink="https://docs.getunleash.io/reference/feature-toggle-types"
|
documentationLink="https://docs.getunleash.io/reference/feature-toggle-types"
|
||||||
documentationLinkLabel="Feature toggle types documentation"
|
documentationLinkLabel="Feature toggle types documentation"
|
||||||
formatApiCode={() => 'FIXME: formatApiCode'}
|
formatApiCode={formatApiCode}
|
||||||
>
|
>
|
||||||
<StyledForm component="form" onSubmit={onSubmit}>
|
<StyledForm component="form" onSubmit={onSubmit}>
|
||||||
<Typography
|
<Typography
|
||||||
@ -135,12 +170,13 @@ export const FeatureTypeForm: VFC<FeatureTypeFormProps> = ({
|
|||||||
checked={doesntExpire || lifetime === 0}
|
checked={doesntExpire || lifetime === 0}
|
||||||
id="feature-toggle-expire"
|
id="feature-toggle-expire"
|
||||||
onChange={onChangeDoesntExpire}
|
onChange={onChangeDoesntExpire}
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<Box>doesn't expire</Box>
|
<Box>doesn't expire</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Input
|
<Input
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={doesntExpire}
|
disabled={doesntExpire || loading}
|
||||||
type="number"
|
type="number"
|
||||||
label="Lifetime in days"
|
label="Lifetime in days"
|
||||||
id="feature-toggle-lifetime"
|
id="feature-toggle-lifetime"
|
||||||
@ -154,7 +190,7 @@ export const FeatureTypeForm: VFC<FeatureTypeFormProps> = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || isIncorrect}
|
disabled={loading || actionLoading}
|
||||||
>
|
>
|
||||||
Save feature toggle type
|
Save feature toggle type
|
||||||
</PermissionButton>
|
</PermissionButton>
|
||||||
@ -162,7 +198,6 @@ export const FeatureTypeForm: VFC<FeatureTypeFormProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => navigate(GO_BACK)}
|
onClick={() => navigate(GO_BACK)}
|
||||||
disabled={loading}
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
@ -5,7 +5,7 @@ import { sortTypes } from 'utils/sortTypes';
|
|||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { Box, Typography, useMediaQuery, useTheme } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -20,16 +20,14 @@ import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
|||||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { Edit } from '@mui/icons-material';
|
import { Edit } from '@mui/icons-material';
|
||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
|
||||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||||
import { FeatureTypeForm } from './FeatureTypeForm/FeatureTypeForm';
|
import { FeatureTypeEdit } from './FeatureTypeEdit/FeatureTypeEdit';
|
||||||
|
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||||
|
|
||||||
const basePath = '/feature-toggle-type';
|
const basePath = '/feature-toggle-type';
|
||||||
|
|
||||||
export const FeatureTypesList = () => {
|
export const FeatureTypesList = () => {
|
||||||
const { featureTypes, loading } = useFeatureTypes();
|
const { featureTypes, loading } = useFeatureTypes();
|
||||||
const theme = useTheme();
|
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
@ -55,24 +53,21 @@ export const FeatureTypesList = () => {
|
|||||||
{
|
{
|
||||||
Header: 'Name',
|
Header: 'Name',
|
||||||
accessor: 'name',
|
accessor: 'name',
|
||||||
minWidth: 125,
|
width: '90%',
|
||||||
Cell: TextCell,
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { name, description },
|
||||||
},
|
},
|
||||||
{
|
}: any) => {
|
||||||
Header: 'Description',
|
return (
|
||||||
accessor: 'description',
|
<LinkCell
|
||||||
width: '80%',
|
data-loading
|
||||||
Cell: ({ value }: { value: string }) => (
|
title={name}
|
||||||
<Typography
|
subtitle={description}
|
||||||
component="div"
|
/>
|
||||||
variant="body2"
|
);
|
||||||
color="text.secondary"
|
},
|
||||||
lineHeight={2}
|
sortType: 'alphanumeric',
|
||||||
>
|
|
||||||
<TextCell lineClamp={1}>{value}</TextCell>
|
|
||||||
</Typography>
|
|
||||||
),
|
|
||||||
disableSortBy: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Lifetime',
|
Header: 'Lifetime',
|
||||||
@ -88,8 +83,8 @@ export const FeatureTypesList = () => {
|
|||||||
|
|
||||||
return <TextCell>doesn't expire</TextCell>;
|
return <TextCell>doesn't expire</TextCell>;
|
||||||
},
|
},
|
||||||
sortInverted: true,
|
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
|
sortType: 'numericZeroLast',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Actions',
|
Header: 'Actions',
|
||||||
@ -133,33 +128,23 @@ export const FeatureTypesList = () => {
|
|||||||
[loading, featureTypes]
|
[loading, featureTypes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
|
||||||
getTableProps,
|
useTable(
|
||||||
getTableBodyProps,
|
|
||||||
headerGroups,
|
|
||||||
rows,
|
|
||||||
prepareRow,
|
|
||||||
setHiddenColumns,
|
|
||||||
} = useTable(
|
|
||||||
{
|
{
|
||||||
columns: columns as any[],
|
columns: columns as any[],
|
||||||
data,
|
data,
|
||||||
sortTypes,
|
sortTypes,
|
||||||
autoResetSortBy: false,
|
autoResetSortBy: false,
|
||||||
disableSortRemove: true,
|
disableSortRemove: true,
|
||||||
},
|
initialState: {
|
||||||
useSortBy
|
sortBy: [
|
||||||
);
|
|
||||||
|
|
||||||
useConditionallyHiddenColumns(
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
condition: isSmallScreen,
|
id: 'lifetimeDays',
|
||||||
columns: ['description'],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
setHiddenColumns,
|
},
|
||||||
columns
|
},
|
||||||
|
useSortBy
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -204,7 +189,7 @@ export const FeatureTypesList = () => {
|
|||||||
onClose={() => navigate(basePath)}
|
onClose={() => navigate(basePath)}
|
||||||
open
|
open
|
||||||
>
|
>
|
||||||
<FeatureTypeForm
|
<FeatureTypeEdit
|
||||||
featureTypes={featureTypes}
|
featureTypes={featureTypes}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import { FeatureTypeSchema, UpdateFeatureTypeLifetimeSchema } from 'openapi';
|
||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
export const useFeatureTypeApi = () => {
|
||||||
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateFeatureTypeLifetime = async (
|
||||||
|
featureTypeId: FeatureTypeSchema['id'],
|
||||||
|
lifetimeDays: UpdateFeatureTypeLifetimeSchema['lifetimeDays']
|
||||||
|
) => {
|
||||||
|
const payload: UpdateFeatureTypeLifetimeSchema = {
|
||||||
|
lifetimeDays,
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = `api/admin/feature-types/${featureTypeId}/lifetime`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await makeRequest(req.caller, req.id);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateFeatureTypeLifetime,
|
||||||
|
errors,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
@ -40,7 +40,8 @@ export type CustomEvents =
|
|||||||
| 'context-usage'
|
| 'context-usage'
|
||||||
| 'segment-usage'
|
| 'segment-usage'
|
||||||
| 'strategy-add'
|
| 'strategy-add'
|
||||||
| 'playground';
|
| 'playground'
|
||||||
|
| 'feature-type-edit';
|
||||||
|
|
||||||
export const usePlausibleTracker = () => {
|
export const usePlausibleTracker = () => {
|
||||||
const plausible = useContext(PlausibleContext);
|
const plausible = useContext(PlausibleContext);
|
||||||
|
@ -6,26 +6,31 @@ const data = [
|
|||||||
id: 1,
|
id: 1,
|
||||||
age: 42,
|
age: 42,
|
||||||
bool: true,
|
bool: true,
|
||||||
|
value: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
age: 35,
|
age: 35,
|
||||||
bool: false,
|
bool: false,
|
||||||
|
value: 9999999,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
age: 25,
|
age: 25,
|
||||||
bool: true,
|
bool: true,
|
||||||
|
value: 3456,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
age: 32,
|
age: 32,
|
||||||
bool: false,
|
bool: false,
|
||||||
|
value: 3455,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
age: 18,
|
age: 18,
|
||||||
bool: true,
|
bool: true,
|
||||||
|
value: '49585',
|
||||||
},
|
},
|
||||||
].map(d => ({ values: d })) as unknown as Row<{
|
].map(d => ({ values: d })) as unknown as Row<{
|
||||||
id: number;
|
id: number;
|
||||||
@ -45,4 +50,10 @@ test('sortTypes', () => {
|
|||||||
.sort((a, b) => sortTypes.alphanumeric(a, b, 'age'))
|
.sort((a, b) => sortTypes.alphanumeric(a, b, 'age'))
|
||||||
.map(({ values: { age } }) => age)
|
.map(({ values: { age } }) => age)
|
||||||
).toEqual([18, 25, 32, 35, 42]);
|
).toEqual([18, 25, 32, 35, 42]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
data
|
||||||
|
.sort((a, b) => sortTypes.numericZeroLast(a, b, 'value'))
|
||||||
|
.map(({ values: { value } }) => value)
|
||||||
|
).toEqual([3455, 3456, '49585', 9999999, 0]);
|
||||||
});
|
});
|
||||||
|
@ -51,4 +51,16 @@ export const sortTypes = {
|
|||||||
if (b === 'false') return 1;
|
if (b === 'false') return 1;
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
numericZeroLast: <D extends object>(
|
||||||
|
a: Row<D>,
|
||||||
|
b: Row<D>,
|
||||||
|
id: IdType<D>,
|
||||||
|
_desc?: boolean
|
||||||
|
) => {
|
||||||
|
let aVal =
|
||||||
|
parseInt(`${a?.values?.[id] || 0}`, 10) || Number.MAX_SAFE_INTEGER;
|
||||||
|
let bVal =
|
||||||
|
parseInt(`${b?.values?.[id] || 0}`, 10) || Number.MAX_SAFE_INTEGER;
|
||||||
|
return aVal - bVal;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user