mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
fix: resolve issues around changing a toggle's project (#978)
* refactor: show save button before using the dropdown * refactor: simplify FeatureSettingsProject toast message * refactor: fix FeatureProjectSelect filter prop type * refactor: hide change project page for non-enterprise * refactor: derive move targets from projects list instead of from permissions * refactor: align frontend project compat check with backend * refactor: fix useProject object stability * refactor: disable the save button for the current project * refactor: require equal environments when moving toggles * refactor: improve arraysHaveSameItems name
This commit is contained in:
parent
4aee33e189
commit
159c54ed37
@ -6,6 +6,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import FeatureSettingsProject from './FeatureSettingsProject/FeatureSettingsProject';
|
import FeatureSettingsProject from './FeatureSettingsProject/FeatureSettingsProject';
|
||||||
import { FeatureSettingsInformation } from './FeatureSettingsInformation/FeatureSettingsInformation';
|
import { FeatureSettingsInformation } from './FeatureSettingsInformation/FeatureSettingsInformation';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
|
||||||
const METADATA = 'metadata';
|
const METADATA = 'metadata';
|
||||||
const PROJECT = 'project';
|
const PROJECT = 'project';
|
||||||
@ -15,6 +16,7 @@ export const FeatureSettings = () => {
|
|||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const [settings, setSettings] = useState(METADATA);
|
const [settings, setSettings] = useState(METADATA);
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent header="Settings" bodyClass={styles.bodyContainer}>
|
<PageContent header="Settings" bodyClass={styles.bodyContainer}>
|
||||||
@ -36,6 +38,7 @@ export const FeatureSettings = () => {
|
|||||||
button
|
button
|
||||||
onClick={() => setSettings(PROJECT)}
|
onClick={() => setSettings(PROJECT)}
|
||||||
selected={settings === PROJECT}
|
selected={settings === PROJECT}
|
||||||
|
hidden={!uiConfig.flags.P}
|
||||||
>
|
>
|
||||||
Project
|
Project
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -52,7 +55,7 @@ export const FeatureSettings = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={settings === PROJECT}
|
condition={settings === PROJECT && uiConfig.flags.P}
|
||||||
show={<FeatureSettingsProject />}
|
show={<FeatureSettingsProject />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,7 @@ interface IFeatureProjectSelectProps
|
|||||||
extends Omit<IGeneralSelectProps, 'options'> {
|
extends Omit<IGeneralSelectProps, 'options'> {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
filter: (project: string) => void;
|
filter: (projectId: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeatureProjectSelect = ({
|
const FeatureProjectSelect = ({
|
||||||
|
@ -1,121 +1,79 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useState, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { MOVE_FEATURE_TOGGLE } from 'component/providers/AccessProvider/permissions';
|
import { MOVE_FEATURE_TOGGLE } from 'component/providers/AccessProvider/permissions';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect';
|
import FeatureProjectSelect from './FeatureProjectSelect/FeatureProjectSelect';
|
||||||
import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm/FeatureSettingsProjectConfirm';
|
import FeatureSettingsProjectConfirm from './FeatureSettingsProjectConfirm/FeatureSettingsProjectConfirm';
|
||||||
import { IPermission } from 'interfaces/user';
|
|
||||||
import { useAuthPermissions } from 'hooks/api/getters/useAuth/useAuthPermissions';
|
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
|
|
||||||
const FeatureSettingsProject = () => {
|
const FeatureSettingsProject = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const featureId = useRequiredPathParam('featureId');
|
const featureId = useRequiredPathParam('featureId');
|
||||||
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
const { feature, refetchFeature } = useFeature(projectId, featureId);
|
||||||
const [project, setProject] = useState(feature.project);
|
|
||||||
const [dirty, setDirty] = useState(false);
|
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const editable = hasAccess(MOVE_FEATURE_TOGGLE, projectId);
|
|
||||||
const { permissions = [] } = useAuthPermissions();
|
|
||||||
const { changeFeatureProject } = useFeatureApi();
|
const { changeFeatureProject } = useFeatureApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const [project, setProject] = useState(projectId);
|
||||||
|
const { projects } = useProjects();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
const onConfirm = async () => {
|
||||||
if (project !== feature.project) {
|
|
||||||
setDirty(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDirty(false);
|
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
||||||
}, [project]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const movableTargets = createMoveTargets();
|
|
||||||
|
|
||||||
if (!movableTargets[project]) {
|
|
||||||
setDirty(false);
|
|
||||||
setProject(projectId);
|
|
||||||
}
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
}, [permissions.length]);
|
|
||||||
|
|
||||||
const updateProject = async () => {
|
|
||||||
const newProject = project;
|
|
||||||
try {
|
try {
|
||||||
await changeFeatureProject(projectId, featureId, newProject);
|
if (project) {
|
||||||
refetchFeature();
|
await changeFeatureProject(projectId, featureId, project);
|
||||||
setToastData({
|
refetchFeature();
|
||||||
title: 'Updated project',
|
setToastData({ title: 'Project changed', type: 'success' });
|
||||||
confetti: true,
|
setShowConfirmDialog(false);
|
||||||
type: 'success',
|
navigate(
|
||||||
text: 'Successfully updated toggle project.',
|
`/projects/${project}/features/${featureId}/settings`,
|
||||||
});
|
{ replace: true }
|
||||||
setDirty(false);
|
);
|
||||||
setShowConfirmDialog(false);
|
}
|
||||||
navigate(`/projects/${newProject}/features/${featureId}/settings`, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastApiError(formatUnknownError(error));
|
setToastApiError(formatUnknownError(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMoveTargets = () => {
|
const targetProjectIds = useMemo(() => {
|
||||||
return permissions.reduce(
|
return projects
|
||||||
(acc: { [key: string]: boolean }, p: IPermission) => {
|
.map(project => project.id)
|
||||||
if (p.project && p.permission === MOVE_FEATURE_TOGGLE) {
|
.filter(projectId => hasAccess(MOVE_FEATURE_TOGGLE, projectId));
|
||||||
acc[p.project] = true;
|
}, [projects, hasAccess]);
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterProjects = () => {
|
if (targetProjectIds.length === 0) {
|
||||||
const validTargets = createMoveTargets();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (project: string) => {
|
|
||||||
if (validTargets[project]) {
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FeatureProjectSelect
|
<FeatureProjectSelect
|
||||||
value={project}
|
value={project}
|
||||||
onChange={setProject}
|
onChange={setProject}
|
||||||
label="Project"
|
label="Project"
|
||||||
enabled={editable}
|
filter={projectId => targetProjectIds.includes(projectId)}
|
||||||
filter={filterProjects()}
|
enabled
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={dirty}
|
|
||||||
show={
|
|
||||||
<PermissionButton
|
|
||||||
permission={MOVE_FEATURE_TOGGLE}
|
|
||||||
onClick={() => setShowConfirmDialog(true)}
|
|
||||||
projectId={projectId}
|
|
||||||
>
|
|
||||||
Save changes
|
|
||||||
</PermissionButton>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<PermissionButton
|
||||||
|
permission={MOVE_FEATURE_TOGGLE}
|
||||||
|
onClick={() => setShowConfirmDialog(true)}
|
||||||
|
disabled={project === projectId}
|
||||||
|
projectId={projectId}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</PermissionButton>
|
||||||
<FeatureSettingsProjectConfirm
|
<FeatureSettingsProjectConfirm
|
||||||
projectId={project}
|
projectId={project}
|
||||||
open={showConfirmDialog}
|
open={showConfirmDialog}
|
||||||
feature={feature}
|
feature={feature}
|
||||||
onClose={() => setShowConfirmDialog(false)}
|
onClose={() => setShowConfirmDialog(false)}
|
||||||
onClick={updateProject}
|
onClick={onConfirm}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,46 +1,9 @@
|
|||||||
import { makeStyles } from 'tss-react/mui';
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
export const useStyles = makeStyles()(theme => ({
|
export const useStyles = makeStyles()(theme => ({
|
||||||
compatability: {
|
container: {
|
||||||
padding: '1rem',
|
display: 'grid',
|
||||||
border: `1px solid ${theme.palette.grey[300]}`,
|
gap: theme.spacing(2),
|
||||||
marginTop: '1rem',
|
paddingTop: theme.spacing(2),
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
iconContainer: {
|
|
||||||
width: '50px',
|
|
||||||
height: '50px',
|
|
||||||
backgroundColor: theme.palette.success.main,
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
errorIconContainer: {
|
|
||||||
width: '50px',
|
|
||||||
height: '50px',
|
|
||||||
backgroundColor: theme.palette.error.main,
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
topContent: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
check: {
|
|
||||||
fill: '#fff',
|
|
||||||
width: '30px',
|
|
||||||
height: '30px',
|
|
||||||
},
|
|
||||||
paragraph: {
|
|
||||||
marginTop: '1rem',
|
|
||||||
},
|
|
||||||
cloud: {
|
|
||||||
fill: theme.palette.grey[500],
|
|
||||||
marginRight: '0.5rem',
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { List, ListItem } from '@mui/material';
|
import { useMemo } from 'react';
|
||||||
import { Check, Error, Cloud } from '@mui/icons-material';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||||
import { IFeatureEnvironment, IFeatureToggle } from 'interfaces/featureToggle';
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { useStyles } from './FeatureSettingsProjectConfirm.styles';
|
import { useStyles } from './FeatureSettingsProjectConfirm.styles';
|
||||||
|
import { arraysHaveSameItems } from 'utils/arraysHaveSameItems';
|
||||||
|
import { Alert } from '@mui/material';
|
||||||
|
|
||||||
interface IFeatureSettingsProjectConfirm {
|
interface IFeatureSettingsProjectConfirm {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -23,37 +23,18 @@ const FeatureSettingsProjectConfirm = ({
|
|||||||
feature,
|
feature,
|
||||||
}: IFeatureSettingsProjectConfirm) => {
|
}: IFeatureSettingsProjectConfirm) => {
|
||||||
const { project } = useProject(projectId);
|
const { project } = useProject(projectId);
|
||||||
const [incompatibleEnvs, setIncompatibleEnvs] = useState([]);
|
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
|
|
||||||
useEffect(() => {
|
const hasSameEnvironments: boolean = useMemo(() => {
|
||||||
calculateCompatability();
|
return arraysHaveSameItems(
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
feature.environments.map(env => env.name),
|
||||||
}, [projectId, project.name]);
|
project.environments
|
||||||
|
);
|
||||||
const calculateCompatability = () => {
|
}, [feature, project]);
|
||||||
const featureEnvWithStrategies = feature.environments
|
|
||||||
.filter((env: IFeatureEnvironment) => {
|
|
||||||
return env.strategies.length > 0;
|
|
||||||
})
|
|
||||||
.map((env: IFeatureEnvironment) => env.name);
|
|
||||||
|
|
||||||
const destinationProjectActiveEnvironments = project.environments;
|
|
||||||
|
|
||||||
let incompatible: string[] = [];
|
|
||||||
|
|
||||||
featureEnvWithStrategies.forEach((env: string) => {
|
|
||||||
if (destinationProjectActiveEnvironments.indexOf(env) === -1) {
|
|
||||||
incompatible = [...incompatible, env];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// @ts-expect-error
|
|
||||||
setIncompatibleEnvs(incompatible);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={incompatibleEnvs?.length === 0}
|
condition={hasSameEnvironments}
|
||||||
show={
|
show={
|
||||||
<Dialogue
|
<Dialogue
|
||||||
open={open}
|
open={open}
|
||||||
@ -63,14 +44,15 @@ const FeatureSettingsProjectConfirm = ({
|
|||||||
primaryButtonText="Change project"
|
primaryButtonText="Change project"
|
||||||
secondaryButtonText="Cancel"
|
secondaryButtonText="Cancel"
|
||||||
>
|
>
|
||||||
Are you sure you want to change the project for this feature
|
<div className={styles.container}>
|
||||||
toggle?
|
<Alert severity="success">
|
||||||
<div className={styles.compatability}>
|
This feature toggle is compatible with the new
|
||||||
This feature toggle is 100% compatible with the new
|
project.
|
||||||
project.
|
</Alert>
|
||||||
<div className={styles.iconContainer}>
|
<p>
|
||||||
<Check className={styles.check} />
|
Are you sure you want to change the project for this
|
||||||
</div>
|
toggle?
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Dialogue>
|
</Dialogue>
|
||||||
}
|
}
|
||||||
@ -81,44 +63,16 @@ const FeatureSettingsProjectConfirm = ({
|
|||||||
title="Confirm change project"
|
title="Confirm change project"
|
||||||
secondaryButtonText="OK"
|
secondaryButtonText="OK"
|
||||||
>
|
>
|
||||||
<div className={styles.topContent}>
|
<div className={styles.container}>
|
||||||
|
<Alert severity="warning">
|
||||||
|
Incompatible project environments
|
||||||
|
</Alert>
|
||||||
<p>
|
<p>
|
||||||
{' '}
|
In order to move a feature toggle between two
|
||||||
This feature toggle is not compatible with the new
|
projects, both projects must have the exact same
|
||||||
project destination.
|
environments enabled.
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.iconContainer}>
|
|
||||||
<div className={styles.errorIconContainer}>
|
|
||||||
<Error
|
|
||||||
className={styles.check}
|
|
||||||
titleAccess="Error"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.compatability}>
|
|
||||||
<div>
|
|
||||||
<p className={styles.paragraph}>
|
|
||||||
This feature toggle has strategy configuration
|
|
||||||
in an environment that is not activated in the
|
|
||||||
target project:
|
|
||||||
</p>
|
|
||||||
<List>
|
|
||||||
{incompatibleEnvs.map(env => {
|
|
||||||
return (
|
|
||||||
<ListItem key={env}>
|
|
||||||
<Cloud className={styles.cloud} />
|
|
||||||
{env}
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className={styles.paragraph}>
|
|
||||||
In order to move this feature toggle, make sure you
|
|
||||||
enable the required environments in the target project.
|
|
||||||
</p>
|
|
||||||
</Dialogue>
|
</Dialogue>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
import useSWR, { SWRConfiguration } from 'swr';
|
||||||
import { useState, useEffect } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { getProjectFetcher } from './getProjectFetcher';
|
import { getProjectFetcher } from './getProjectFetcher';
|
||||||
import { IProject } from 'interfaces/project';
|
import { IProject } from 'interfaces/project';
|
||||||
|
|
||||||
@ -15,22 +15,16 @@ const fallbackProject: IProject = {
|
|||||||
|
|
||||||
const useProject = (id: string, options: SWRConfiguration = {}) => {
|
const useProject = (id: string, options: SWRConfiguration = {}) => {
|
||||||
const { KEY, fetcher } = getProjectFetcher(id);
|
const { KEY, fetcher } = getProjectFetcher(id);
|
||||||
|
const { data, error, mutate } = useSWR<IProject>(KEY, fetcher, options);
|
||||||
|
|
||||||
const { data, error } = useSWR<IProject>(KEY, fetcher, options);
|
const refetch = useCallback(() => {
|
||||||
const [loading, setLoading] = useState(!error && !data);
|
mutate();
|
||||||
|
}, [mutate]);
|
||||||
const refetch = () => {
|
|
||||||
mutate(KEY);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(!error && !data);
|
|
||||||
}, [data, error]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
project: data || fallbackProject,
|
project: data || fallbackProject,
|
||||||
|
loading: !error && !data,
|
||||||
error,
|
error,
|
||||||
loading,
|
|
||||||
refetch,
|
refetch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
sortFeaturesByNameAscending,
|
sortFeaturesByNameAscending,
|
||||||
sortFeaturesByNameDescending,
|
sortFeaturesByNameDescending,
|
||||||
@ -63,25 +63,29 @@ const useSort = () => {
|
|||||||
return sortFeaturesByStatusDescending(features);
|
return sortFeaturesByStatusDescending(features);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sort = (features: IFeatureToggleListItem[]) => {
|
const sort = useCallback(
|
||||||
switch (sortData.sortKey) {
|
(features: IFeatureToggleListItem[]): IFeatureToggleListItem[] => {
|
||||||
case 'name':
|
switch (sortData.sortKey) {
|
||||||
return handleSortName(features);
|
case 'name':
|
||||||
case 'last-seen':
|
return handleSortName(features);
|
||||||
return handleSortLastSeen(features);
|
case 'last-seen':
|
||||||
case 'created':
|
return handleSortLastSeen(features);
|
||||||
return handleSortCreatedAt(features);
|
case 'created':
|
||||||
case 'expired':
|
return handleSortCreatedAt(features);
|
||||||
case 'report':
|
case 'expired':
|
||||||
return handleSortExpiredAt(features);
|
case 'report':
|
||||||
case 'status':
|
return handleSortExpiredAt(features);
|
||||||
return handleSortStatus(features);
|
case 'status':
|
||||||
default:
|
return handleSortStatus(features);
|
||||||
return features;
|
default:
|
||||||
}
|
return features;
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[sortData]
|
||||||
|
);
|
||||||
|
|
||||||
return [sort, setSortData];
|
return [sort, setSortData] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useSort;
|
export default useSort;
|
||||||
|
17
frontend/src/utils/arraysHaveSameItems.test.ts
Normal file
17
frontend/src/utils/arraysHaveSameItems.test.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { arraysHaveSameItems } from 'utils/arraysHaveSameItems';
|
||||||
|
|
||||||
|
test('arraysHaveSameItems', () => {
|
||||||
|
expect(arraysHaveSameItems([], [])).toEqual(true);
|
||||||
|
expect(arraysHaveSameItems([1], [1])).toEqual(true);
|
||||||
|
expect(arraysHaveSameItems([1], [1, 1])).toEqual(true);
|
||||||
|
expect(arraysHaveSameItems([1, 1], [1])).toEqual(true);
|
||||||
|
expect(arraysHaveSameItems([1, 2], [1, 2])).toEqual(true);
|
||||||
|
expect(arraysHaveSameItems([1, 2], [2, 1])).toEqual(true);
|
||||||
|
expect(arraysHaveSameItems([1, 2], [2, 2, 1])).toEqual(true);
|
||||||
|
expect(arraysHaveSameItems([1], [])).toEqual(false);
|
||||||
|
expect(arraysHaveSameItems([1], [2])).toEqual(false);
|
||||||
|
expect(arraysHaveSameItems([1, 2], [1, 3])).toEqual(false);
|
||||||
|
expect(arraysHaveSameItems([1, 2], [1, 2, 3])).toEqual(false);
|
||||||
|
expect(arraysHaveSameItems([1, 2, 3], [1, 2])).toEqual(false);
|
||||||
|
expect(arraysHaveSameItems([1, 2, 3], [1, 2, 4])).toEqual(false);
|
||||||
|
});
|
12
frontend/src/utils/arraysHaveSameItems.ts
Normal file
12
frontend/src/utils/arraysHaveSameItems.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export const arraysHaveSameItems = <T>(a: T[], b: T[]): boolean => {
|
||||||
|
const setA = new Set(a);
|
||||||
|
const setB = new Set(b);
|
||||||
|
|
||||||
|
if (setA.size !== setB.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...setA].every(itemA => {
|
||||||
|
return setB.has(itemA);
|
||||||
|
});
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import { ADMIN } from '../component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { IPermission } from '../interfaces/user';
|
import { IPermission } from 'interfaces/user';
|
||||||
|
|
||||||
type objectIdx = {
|
type objectIdx = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
@ -8,8 +8,9 @@ type objectIdx = {
|
|||||||
export const projectFilterGenerator = (
|
export const projectFilterGenerator = (
|
||||||
permissions: IPermission[] = [],
|
permissions: IPermission[] = [],
|
||||||
matcherPermission: string
|
matcherPermission: string
|
||||||
) => {
|
): ((projectId: string) => boolean) => {
|
||||||
let admin = false;
|
let admin = false;
|
||||||
|
|
||||||
const permissionMap: objectIdx = permissions.reduce(
|
const permissionMap: objectIdx = permissions.reduce(
|
||||||
(acc: objectIdx, p: IPermission) => {
|
(acc: objectIdx, p: IPermission) => {
|
||||||
if (p.permission === ADMIN) {
|
if (p.permission === ADMIN) {
|
||||||
@ -24,7 +25,8 @@ export const projectFilterGenerator = (
|
|||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (projectId: string) => {
|
return (projectId: string) => {
|
||||||
return admin || permissionMap[projectId];
|
return admin || Boolean(permissionMap[projectId]);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user