mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-14 00:15:52 +01:00
refactor: replace react-dnd with custom implementation (#988)
* refactor: replace react-dnd with custom implementation * refactor: add TextCell, IconCell, and ActionCell * refactor: port environments list to react-table * refactor: change OfflineBolt to PowerSettingsNew * refactor: simplify environment toast text * refactor: improve IToast type type * refactor: improve useSearchHighlightContext naming * refactor: clarify enableDragAndDrop logic
This commit is contained in:
parent
c073908027
commit
34f848ce8a
@ -83,8 +83,6 @@
|
||||
"prop-types": "15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react-chartjs-2": "4.1.0",
|
||||
"react-dnd": "15.1.2",
|
||||
"react-dnd-html5-backend": "15.1.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-hooks-global-state": "1.0.2",
|
||||
"react-router-dom": "6.3.0",
|
||||
|
@ -4,5 +4,6 @@ const SearchHighlightContext = createContext('');
|
||||
|
||||
export const SearchHighlightProvider = SearchHighlightContext.Provider;
|
||||
|
||||
export const useSearchHighlightContext = () =>
|
||||
useContext(SearchHighlightContext);
|
||||
export const useSearchHighlightContext = (): { searchQuery: string } => {
|
||||
return { searchQuery: useContext(SearchHighlightContext) };
|
||||
};
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface IContextActionsCellProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ActionCell = ({ children }: IContextActionsCellProps) => {
|
||||
return (
|
||||
<Box
|
||||
data-loading
|
||||
sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
import { VFC } from 'react';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { formatDateYMD, formatDateYMDHMS } from 'utils/formatDate';
|
||||
import { Box, Tooltip } from '@mui/material';
|
||||
import { formatDateYMD } from 'utils/formatDate';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
|
||||
interface IDateCellProps {
|
||||
value?: Date | string | null;
|
||||
@ -11,22 +11,11 @@ interface IDateCellProps {
|
||||
export const DateCell: VFC<IDateCellProps> = ({ value }) => {
|
||||
const { locationSettings } = useLocationSettings();
|
||||
|
||||
if (!value) {
|
||||
return <Box sx={{ py: 1.5, px: 2 }} />;
|
||||
}
|
||||
const date = value
|
||||
? value instanceof Date
|
||||
? formatDateYMD(value, locationSettings.locale)
|
||||
: formatDateYMD(parseISO(value), locationSettings.locale)
|
||||
: undefined;
|
||||
|
||||
const date = value instanceof Date ? value : parseISO(value);
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 1.5, px: 2 }}>
|
||||
<Tooltip
|
||||
title={formatDateYMDHMS(date, locationSettings.locale)}
|
||||
arrow
|
||||
>
|
||||
<span data-loading role="tooltip">
|
||||
{formatDateYMD(date, locationSettings.locale)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
return <TextCell>{date}</TextCell>;
|
||||
};
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface IIconCellProps {
|
||||
icon: ReactNode;
|
||||
}
|
||||
|
||||
export const IconCell = ({ icon }: IIconCellProps) => {
|
||||
return (
|
||||
<Box
|
||||
data-loading
|
||||
sx={{
|
||||
pl: 2,
|
||||
pr: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: 60,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -20,7 +20,7 @@ export const LinkCell: FC<ILinkCellProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const search = useSearchHighlightContext();
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
const content = (
|
||||
<div className={styles.container}>
|
||||
@ -32,7 +32,7 @@ export const LinkCell: FC<ILinkCellProps> = ({
|
||||
lineClamp: Boolean(subtitle) ? 1 : 2,
|
||||
}}
|
||||
>
|
||||
<Highlighter search={search}>{title}</Highlighter>
|
||||
<Highlighter search={searchQuery}>{title}</Highlighter>
|
||||
{children}
|
||||
</span>
|
||||
<ConditionallyRender
|
||||
@ -44,7 +44,7 @@ export const LinkCell: FC<ILinkCellProps> = ({
|
||||
component="span"
|
||||
data-loading
|
||||
>
|
||||
<Highlighter search={search}>
|
||||
<Highlighter search={searchQuery}>
|
||||
{subtitle}
|
||||
</Highlighter>
|
||||
</Typography>
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { VFC, ReactNode } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
interface IDateCellProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const TextCell: VFC<IDateCellProps> = ({ children }) => {
|
||||
if (!children) {
|
||||
return <Box sx={{ py: 1.5, px: 2 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 1.5, px: 2 }}>
|
||||
<span data-loading role="tooltip">
|
||||
{children}
|
||||
</span>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,12 +1,12 @@
|
||||
import { VFC } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Delete, Edit } from '@mui/icons-material';
|
||||
import { Box } from '@mui/material';
|
||||
import {
|
||||
DELETE_CONTEXT_FIELD,
|
||||
UPDATE_CONTEXT_FIELD,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||
|
||||
interface IContextActionsCellProps {
|
||||
name: string;
|
||||
@ -20,14 +20,7 @@ export const ContextActionsCell: VFC<IContextActionsCellProps> = ({
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-loading
|
||||
sx={{
|
||||
display: 'flex',
|
||||
px: 2,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<ActionCell>
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_CONTEXT_FIELD}
|
||||
onClick={() => navigate(`/context/edit/${name}`)}
|
||||
@ -50,6 +43,6 @@ export const ContextActionsCell: VFC<IContextActionsCellProps> = ({
|
||||
>
|
||||
<Delete />
|
||||
</PermissionIconButton>
|
||||
</Box>
|
||||
</ActionCell>
|
||||
);
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ import { sortTypes } from 'utils/sortTypes';
|
||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
||||
import { ContextActionsCell } from './ContextActionsCell/ContextActionsCell';
|
||||
import { Adjust } from '@mui/icons-material';
|
||||
import { Box } from '@mui/material';
|
||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||
|
||||
const ContextList: VFC = () => {
|
||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||
@ -53,19 +53,7 @@ const ContextList: VFC = () => {
|
||||
() => [
|
||||
{
|
||||
id: 'Icon',
|
||||
Cell: () => (
|
||||
<Box
|
||||
data-loading
|
||||
sx={{
|
||||
pl: 2,
|
||||
pr: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Adjust color="disabled" />
|
||||
</Box>
|
||||
),
|
||||
Cell: () => <IconCell icon={<Adjust color="disabled" />} />,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
|
@ -0,0 +1,22 @@
|
||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||
import { Add } from '@mui/icons-material';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const CreateEnvironmentButton = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<ResponsiveButton
|
||||
onClick={() => navigate('/environments/create')}
|
||||
maxWidth="700px"
|
||||
Icon={Add}
|
||||
permission={ADMIN}
|
||||
disabled={!Boolean(uiConfig.flags.EEA)}
|
||||
>
|
||||
New Environment
|
||||
</ResponsiveButton>
|
||||
);
|
||||
};
|
@ -0,0 +1,193 @@
|
||||
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
|
||||
import {
|
||||
DELETE_ENVIRONMENT,
|
||||
UPDATE_ENVIRONMENT,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import {
|
||||
Edit,
|
||||
Delete,
|
||||
OfflineBolt,
|
||||
DragIndicator,
|
||||
PowerSettingsNew,
|
||||
} from '@mui/icons-material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { IconButton, Tooltip } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import { useContext, useState } from 'react';
|
||||
import { IEnvironment } from 'interfaces/environments';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import EnvironmentToggleConfirm from '../EnvironmentToggleConfirm/EnvironmentToggleConfirm';
|
||||
import EnvironmentDeleteConfirm from '../EnvironmentDeleteConfirm/EnvironmentDeleteConfirm';
|
||||
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { useId } from 'hooks/useId';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
|
||||
interface IEnvironmentTableActionsProps {
|
||||
environment: IEnvironment;
|
||||
}
|
||||
|
||||
export const EnvironmentActionCell = ({
|
||||
environment,
|
||||
}: IEnvironmentTableActionsProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const updatePermission = hasAccess(UPDATE_ENVIRONMENT);
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
|
||||
const { setToastApiError, setToastData } = useToast();
|
||||
const { refetchEnvironments } = useEnvironments();
|
||||
const { refetch: refetchPermissions } = useProjectRolePermissions();
|
||||
const { deleteEnvironment, toggleEnvironmentOn, toggleEnvironmentOff } =
|
||||
useEnvironmentApi();
|
||||
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [toggleModal, setToggleModal] = useState(false);
|
||||
const [confirmName, setConfirmName] = useState('');
|
||||
|
||||
const handleDeleteEnvironment = async () => {
|
||||
try {
|
||||
await deleteEnvironment(environment.name);
|
||||
refetchPermissions();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Project environment deleted',
|
||||
text: 'You have successfully deleted the project environment.',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
} finally {
|
||||
setDeleteModal(false);
|
||||
setConfirmName('');
|
||||
await refetchEnvironments();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmToggleEnvironment = () => {
|
||||
return environment.enabled
|
||||
? handleToggleEnvironmentOff()
|
||||
: handleToggleEnvironmentOn();
|
||||
};
|
||||
|
||||
const handleToggleEnvironmentOn = async () => {
|
||||
try {
|
||||
await toggleEnvironmentOn(environment.name);
|
||||
setToggleModal(false);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Project environment enabled',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
} finally {
|
||||
await refetchEnvironments();
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleEnvironmentOff = async () => {
|
||||
try {
|
||||
await toggleEnvironmentOff(environment.name);
|
||||
setToggleModal(false);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Project environment disabled',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
} finally {
|
||||
await refetchEnvironments();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleIconTooltip = environment.enabled
|
||||
? 'Disable environment'
|
||||
: 'Enable environment';
|
||||
|
||||
const editId = useId();
|
||||
const deleteId = useId();
|
||||
|
||||
// Allow drag and drop if the user is permitted to reorder environments.
|
||||
// Disable drag and drop while searching since some rows may be hidden.
|
||||
const enableDragAndDrop = updatePermission && !searchQuery;
|
||||
|
||||
return (
|
||||
<ActionCell>
|
||||
<ConditionallyRender
|
||||
condition={enableDragAndDrop}
|
||||
show={
|
||||
<IconButton size="large">
|
||||
<DragIndicator titleAccess="Drag" cursor="grab" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={updatePermission}
|
||||
show={
|
||||
<Tooltip title={toggleIconTooltip} arrow>
|
||||
<IconButton
|
||||
onClick={() => setToggleModal(true)}
|
||||
size="large"
|
||||
>
|
||||
<PowerSettingsNew />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={updatePermission}
|
||||
show={
|
||||
<Tooltip title="Edit environment" arrow>
|
||||
<span id={editId}>
|
||||
<IconButton
|
||||
aria-describedby={editId}
|
||||
disabled={environment.protected}
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/environments/${environment.name}`
|
||||
);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(DELETE_ENVIRONMENT)}
|
||||
show={
|
||||
<Tooltip title="Delete environment" arrow>
|
||||
<span id={deleteId}>
|
||||
<IconButton
|
||||
aria-describedby={deleteId}
|
||||
disabled={environment.protected}
|
||||
onClick={() => setDeleteModal(true)}
|
||||
size="large"
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<EnvironmentDeleteConfirm
|
||||
env={environment}
|
||||
setDeldialogue={setDeleteModal}
|
||||
open={deleteModal}
|
||||
handleDeleteEnvironment={handleDeleteEnvironment}
|
||||
confirmName={confirmName}
|
||||
setConfirmName={setConfirmName}
|
||||
/>
|
||||
<EnvironmentToggleConfirm
|
||||
env={environment}
|
||||
open={toggleModal}
|
||||
setToggleDialog={setToggleModal}
|
||||
handleConfirmToggleEnvironment={handleConfirmToggleEnvironment}
|
||||
/>
|
||||
</ActionCell>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { CloudCircle } from '@mui/icons-material';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { useStyles } from './EnvironmentCard.styles';
|
||||
import { useStyles } from 'component/environments/EnvironmentCard/EnvironmentCard.styles';
|
||||
|
||||
interface IEnvironmentProps {
|
||||
name: string;
|
@ -3,13 +3,12 @@ import React from 'react';
|
||||
import { IEnvironment } from 'interfaces/environments';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
|
||||
import { useStyles } from './EnvironmentDeleteConfirm.styles';
|
||||
import EnvironmentCard from 'component/environments/EnvironmentCard/EnvironmentCard';
|
||||
import { useStyles } from 'component/environments/EnvironmentDeleteConfirm/EnvironmentDeleteConfirm.styles';
|
||||
|
||||
interface IEnviromentDeleteConfirmProps {
|
||||
env: IEnvironment;
|
||||
open: boolean;
|
||||
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteEnvironment: () => Promise<void>;
|
||||
confirmName: string;
|
@ -1,202 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { List } from '@mui/material';
|
||||
import { Add } from '@mui/icons-material';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { IEnvironment, ISortOrderPayload } from 'interfaces/environments';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import EnvironmentListItem from './EnvironmentListItem/EnvironmentListItem';
|
||||
import EnvironmentToggleConfirm from './EnvironmentToggleConfirm/EnvironmentToggleConfirm';
|
||||
import EnvironmentDeleteConfirm from './EnvironmentDeleteConfirm/EnvironmentDeleteConfirm';
|
||||
|
||||
const EnvironmentList = () => {
|
||||
const defaultEnv = {
|
||||
name: '',
|
||||
type: '',
|
||||
sortOrder: 0,
|
||||
createdAt: '',
|
||||
enabled: true,
|
||||
protected: false,
|
||||
};
|
||||
const { environments, mutateEnvironments, refetchEnvironments } =
|
||||
useEnvironments();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { refetch: refetchProjectRolePermissions } =
|
||||
useProjectRolePermissions();
|
||||
|
||||
const [selectedEnv, setSelectedEnv] = useState(defaultEnv);
|
||||
const [delDialog, setDeldialogue] = useState(false);
|
||||
const [toggleDialog, setToggleDialog] = useState(false);
|
||||
const [confirmName, setConfirmName] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { setToastApiError, setToastData } = useToast();
|
||||
const {
|
||||
deleteEnvironment,
|
||||
changeSortOrder,
|
||||
toggleEnvironmentOn,
|
||||
toggleEnvironmentOff,
|
||||
} = useEnvironmentApi();
|
||||
|
||||
const moveListItem = (dragIndex: number, hoverIndex: number) => {
|
||||
const newEnvList = [...environments];
|
||||
if (newEnvList.length === 0) return newEnvList;
|
||||
|
||||
const item = newEnvList.splice(dragIndex, 1)[0];
|
||||
|
||||
newEnvList.splice(hoverIndex, 0, item);
|
||||
mutateEnvironments(newEnvList);
|
||||
return newEnvList;
|
||||
};
|
||||
|
||||
const moveListItemApi = async (dragIndex: number, hoverIndex: number) => {
|
||||
const newEnvList = moveListItem(dragIndex, hoverIndex);
|
||||
const sortOrder = newEnvList.reduce(
|
||||
(acc: ISortOrderPayload, env: IEnvironment, index: number) => {
|
||||
acc[env.name] = index + 1;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
try {
|
||||
await sortOrderAPICall(sortOrder);
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const sortOrderAPICall = async (sortOrder: ISortOrderPayload) => {
|
||||
try {
|
||||
await changeSortOrder(sortOrder);
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteEnvironment = async () => {
|
||||
try {
|
||||
await deleteEnvironment(selectedEnv.name);
|
||||
refetchProjectRolePermissions();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Project environment deleted',
|
||||
text: 'You have successfully deleted the project environment.',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
} finally {
|
||||
setDeldialogue(false);
|
||||
setSelectedEnv(defaultEnv);
|
||||
setConfirmName('');
|
||||
refetchEnvironments();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmToggleEnvironment = () => {
|
||||
if (selectedEnv.enabled) {
|
||||
return handleToggleEnvironmentOff();
|
||||
}
|
||||
handleToggleEnvironmentOn();
|
||||
};
|
||||
|
||||
const handleToggleEnvironmentOn = async () => {
|
||||
try {
|
||||
await toggleEnvironmentOn(selectedEnv.name);
|
||||
setToggleDialog(false);
|
||||
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Project environment enabled',
|
||||
text: 'Your environment is enabled',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
} finally {
|
||||
refetchEnvironments();
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleEnvironmentOff = async () => {
|
||||
try {
|
||||
await toggleEnvironmentOff(selectedEnv.name);
|
||||
setToggleDialog(false);
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Project environment disabled',
|
||||
text: 'Your environment is disabled.',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
} finally {
|
||||
refetchEnvironments();
|
||||
}
|
||||
};
|
||||
|
||||
const environmentList = () =>
|
||||
environments.map((env: IEnvironment, index: number) => (
|
||||
<EnvironmentListItem
|
||||
key={env.name}
|
||||
env={env}
|
||||
setDeldialogue={setDeldialogue}
|
||||
setSelectedEnv={setSelectedEnv}
|
||||
setToggleDialog={setToggleDialog}
|
||||
index={index}
|
||||
moveListItem={moveListItem}
|
||||
moveListItemApi={moveListItemApi}
|
||||
/>
|
||||
));
|
||||
|
||||
const navigateToCreateEnvironment = () => {
|
||||
navigate('/environments/create');
|
||||
};
|
||||
return (
|
||||
<PageContent
|
||||
header={
|
||||
<PageHeader
|
||||
title="Environments"
|
||||
actions={
|
||||
<>
|
||||
<ResponsiveButton
|
||||
onClick={navigateToCreateEnvironment}
|
||||
maxWidth="700px"
|
||||
Icon={Add}
|
||||
permission={ADMIN}
|
||||
disabled={!Boolean(uiConfig.flags.EEA)}
|
||||
>
|
||||
New Environment
|
||||
</ResponsiveButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<List>{environmentList()}</List>
|
||||
<EnvironmentDeleteConfirm
|
||||
env={selectedEnv}
|
||||
setSelectedEnv={setSelectedEnv}
|
||||
setDeldialogue={setDeldialogue}
|
||||
open={delDialog}
|
||||
handleDeleteEnvironment={handleDeleteEnvironment}
|
||||
confirmName={confirmName}
|
||||
setConfirmName={setConfirmName}
|
||||
/>
|
||||
<EnvironmentToggleConfirm
|
||||
env={selectedEnv}
|
||||
open={toggleDialog}
|
||||
setToggleDialog={setToggleDialog}
|
||||
handleConfirmToggleEnvironment={handleConfirmToggleEnvironment}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentList;
|
@ -1,219 +0,0 @@
|
||||
import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CloudCircle,
|
||||
Delete,
|
||||
DragIndicator,
|
||||
Edit,
|
||||
OfflineBolt,
|
||||
} from '@mui/icons-material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
import { IEnvironment } from 'interfaces/environments';
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import {
|
||||
DELETE_ENVIRONMENT,
|
||||
UPDATE_ENVIRONMENT,
|
||||
} from 'component/providers/AccessProvider/permissions';
|
||||
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
|
||||
import { XYCoord, Identifier } from 'dnd-core';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
|
||||
|
||||
interface IEnvironmentListItemProps {
|
||||
env: IEnvironment;
|
||||
setSelectedEnv: React.Dispatch<React.SetStateAction<IEnvironment>>;
|
||||
setDeldialogue: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setToggleDialog: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
index: number;
|
||||
moveListItem: (dragIndex: number, hoverIndex: number) => IEnvironment[];
|
||||
moveListItemApi: (dragIndex: number, hoverIndex: number) => Promise<void>;
|
||||
}
|
||||
|
||||
interface IDragItem {
|
||||
index: number;
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface ICollectedProps {
|
||||
handlerId: Identifier | null;
|
||||
}
|
||||
|
||||
const EnvironmentListItem = ({
|
||||
env,
|
||||
setSelectedEnv,
|
||||
setDeldialogue,
|
||||
index,
|
||||
moveListItem,
|
||||
moveListItemApi,
|
||||
setToggleDialog,
|
||||
}: IEnvironmentListItemProps) => {
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
const ACCEPT_TYPE = 'LIST_ITEM';
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: ACCEPT_TYPE,
|
||||
item: () => {
|
||||
return { env, index };
|
||||
},
|
||||
collect: (monitor: any) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [{ handlerId }, drop] = useDrop<IDragItem, unknown, ICollectedProps>({
|
||||
accept: ACCEPT_TYPE,
|
||||
collect(monitor) {
|
||||
return {
|
||||
handlerId: monitor.getHandlerId(),
|
||||
};
|
||||
},
|
||||
drop(item: IDragItem, monitor: DropTargetMonitor) {
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
moveListItemApi(dragIndex, hoverIndex);
|
||||
},
|
||||
hover(item: IDragItem, monitor: DropTargetMonitor) {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
|
||||
if (dragIndex === hoverIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
|
||||
const hoverMiddleY =
|
||||
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
|
||||
const hoverClientY =
|
||||
(clientOffset as XYCoord).y - hoverBoundingRect.top;
|
||||
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveListItem(dragIndex, hoverIndex);
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
|
||||
const opacity = isDragging ? 0 : 1;
|
||||
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const updatePermission = hasAccess(UPDATE_ENVIRONMENT);
|
||||
const tooltipText = env.enabled ? 'Disable' : 'Enable';
|
||||
|
||||
if (updatePermission) {
|
||||
drag(drop(ref));
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
style={{ position: 'relative', opacity }}
|
||||
ref={ref}
|
||||
data-handler-id={handlerId}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<CloudCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<>
|
||||
<strong>
|
||||
<StringTruncator
|
||||
text={env.name}
|
||||
maxWidth={'125'}
|
||||
maxLength={25}
|
||||
/>
|
||||
</strong>
|
||||
<ConditionallyRender
|
||||
condition={!env.enabled}
|
||||
show={
|
||||
<StatusBadge severity="warning">
|
||||
Disabled
|
||||
</StatusBadge>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={updatePermission}
|
||||
show={
|
||||
<IconButton size="large">
|
||||
<DragIndicator titleAccess="Drag" cursor="grab" />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={updatePermission}
|
||||
show={
|
||||
<Tooltip title={`${tooltipText} environment`} arrow>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setSelectedEnv(env);
|
||||
setToggleDialog(prev => !prev);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<OfflineBolt />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={updatePermission}
|
||||
show={
|
||||
<Tooltip title="Edit environment" arrow>
|
||||
<IconButton
|
||||
disabled={env.protected}
|
||||
onClick={() => {
|
||||
navigate(`/environments/${env.name}`);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(DELETE_ENVIRONMENT)}
|
||||
show={
|
||||
<Tooltip title="Delete environment" arrow>
|
||||
<IconButton
|
||||
disabled={env.protected}
|
||||
onClick={() => {
|
||||
setDeldialogue(true);
|
||||
setSelectedEnv(env);
|
||||
}}
|
||||
size="large"
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentListItem;
|
@ -0,0 +1,22 @@
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { IEnvironment } from 'interfaces/environments';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
|
||||
|
||||
interface IEnvironmentNameCellProps {
|
||||
environment: IEnvironment;
|
||||
}
|
||||
|
||||
export const EnvironmentNameCell = ({
|
||||
environment,
|
||||
}: IEnvironmentNameCellProps) => {
|
||||
return (
|
||||
<TextCell>
|
||||
{environment.name}
|
||||
<ConditionallyRender
|
||||
condition={!environment.enabled}
|
||||
show={<StatusBadge severity="warning">Disabled</StatusBadge>}
|
||||
/>
|
||||
</TextCell>
|
||||
);
|
||||
};
|
@ -0,0 +1,26 @@
|
||||
import { useDragItem, MoveListItem } from 'hooks/useDragItem';
|
||||
import { Row } from 'react-table';
|
||||
import { TableRow } from '@mui/material';
|
||||
import { TableCell } from 'component/common/Table';
|
||||
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
|
||||
interface IEnvironmentRowProps {
|
||||
row: Row;
|
||||
moveListItem: MoveListItem;
|
||||
}
|
||||
|
||||
export const EnvironmentRow = ({ row, moveListItem }: IEnvironmentRowProps) => {
|
||||
const dragItemRef = useDragItem(row.index, moveListItem);
|
||||
const { searchQuery } = useSearchHighlightContext();
|
||||
const draggable = !searchQuery;
|
||||
|
||||
return (
|
||||
<TableRow hover ref={draggable ? dragItemRef : undefined}>
|
||||
{row.cells.map((cell: any) => (
|
||||
<TableCell {...cell.getCellProps()}>
|
||||
{cell.render('Cell')}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
};
|
@ -0,0 +1,128 @@
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||
import { CreateEnvironmentButton } from 'component/environments/CreateEnvironmentButton/CreateEnvironmentButton';
|
||||
import { useTable, useGlobalFilter } from 'react-table';
|
||||
import {
|
||||
TableSearch,
|
||||
SortableTableHeader,
|
||||
Table,
|
||||
} from 'component/common/Table';
|
||||
import { useCallback } from 'react';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import { TableBody } from '@mui/material';
|
||||
import { CloudCircle } from '@mui/icons-material';
|
||||
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
|
||||
import { EnvironmentActionCell } from 'component/environments/EnvironmentActionCell/EnvironmentActionCell';
|
||||
import { EnvironmentNameCell } from 'component/environments/EnvironmentNameCell/EnvironmentNameCell';
|
||||
import { EnvironmentRow } from 'component/environments/EnvironmentRow/EnvironmentRow';
|
||||
import { MoveListItem } from 'hooks/useDragItem';
|
||||
import useToast from 'hooks/useToast';
|
||||
import useEnvironmentApi, {
|
||||
createSortOrderPayload,
|
||||
} from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
|
||||
export const EnvironmentTable = () => {
|
||||
const { changeSortOrder } = useEnvironmentApi();
|
||||
const { setToastApiError } = useToast();
|
||||
const { environments, mutateEnvironments } = useEnvironments();
|
||||
|
||||
const moveListItem: MoveListItem = useCallback(
|
||||
async (dragIndex: number, dropIndex: number, save = false) => {
|
||||
const copy = [...environments];
|
||||
const tmp = copy[dragIndex];
|
||||
copy.splice(dragIndex, 1);
|
||||
copy.splice(dropIndex, 0, tmp);
|
||||
await mutateEnvironments(copy);
|
||||
|
||||
if (save) {
|
||||
try {
|
||||
await changeSortOrder(createSortOrderPayload(copy));
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
[changeSortOrder, environments, mutateEnvironments, setToastApiError]
|
||||
);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
state: { globalFilter },
|
||||
setGlobalFilter,
|
||||
} = useTable(
|
||||
{
|
||||
columns: COLUMNS as any,
|
||||
data: environments,
|
||||
autoResetGlobalFilter: false,
|
||||
},
|
||||
useGlobalFilter
|
||||
);
|
||||
|
||||
const headerSearch = (
|
||||
<TableSearch initialValue={globalFilter} onChange={setGlobalFilter} />
|
||||
);
|
||||
|
||||
const headerActions = (
|
||||
<>
|
||||
{headerSearch}
|
||||
<PageHeader.Divider />
|
||||
<CreateEnvironmentButton />
|
||||
</>
|
||||
);
|
||||
|
||||
const header = <PageHeader title="Environments" actions={headerActions} />;
|
||||
|
||||
return (
|
||||
<PageContent header={header}>
|
||||
<SearchHighlightProvider value={globalFilter}>
|
||||
<Table {...getTableProps()}>
|
||||
<SortableTableHeader headerGroups={headerGroups as any} />
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map(row => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<EnvironmentRow
|
||||
row={row as any}
|
||||
moveListItem={moveListItem}
|
||||
key={row.original.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</SearchHighlightProvider>
|
||||
</PageContent>
|
||||
);
|
||||
};
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
id: 'Icon',
|
||||
canSort: false,
|
||||
Cell: () => <IconCell icon={<CloudCircle color="disabled" />} />,
|
||||
},
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
width: '100%',
|
||||
canSort: false,
|
||||
Cell: (props: any) => (
|
||||
<EnvironmentNameCell environment={props.row.original} />
|
||||
),
|
||||
},
|
||||
{
|
||||
Header: 'Actions',
|
||||
id: 'Actions',
|
||||
align: 'center',
|
||||
canSort: false,
|
||||
Cell: (props: any) => (
|
||||
<EnvironmentActionCell environment={props.row.original} />
|
||||
),
|
||||
},
|
||||
];
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||
import { IEnvironment } from 'interfaces/environments';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import EnvironmentCard from '../EnvironmentCard/EnvironmentCard';
|
||||
import EnvironmentCard from 'component/environments/EnvironmentCard/EnvironmentCard';
|
||||
|
||||
interface IEnvironmentToggleConfirmProps {
|
||||
env: IEnvironment;
|
@ -18,7 +18,6 @@ import ForgottenPassword from 'component/user/ForgottenPassword/ForgottenPasswor
|
||||
import { ProjectListNew } from 'component/project/ProjectList/ProjectList';
|
||||
import Project from 'component/project/Project/Project';
|
||||
import RedirectArchive from 'component/archive/RedirectArchive';
|
||||
import EnvironmentList from 'component/environments/EnvironmentList/EnvironmentList';
|
||||
import { FeatureView } from 'component/feature/FeatureView/FeatureView';
|
||||
import ProjectRoles from 'component/admin/projectRoles/ProjectRoles/ProjectRoles';
|
||||
import CreateProjectRole from 'component/admin/projectRoles/CreateProjectRole/CreateProjectRole';
|
||||
@ -52,6 +51,7 @@ import { CreateSegment } from 'component/segments/CreateSegment/CreateSegment';
|
||||
import { EditSegment } from 'component/segments/EditSegment/EditSegment';
|
||||
import { SegmentsList } from 'component/segments/SegmentList/SegmentList';
|
||||
import { IRoute } from 'interfaces/route';
|
||||
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
|
||||
|
||||
export const routes: IRoute[] = [
|
||||
// Splash
|
||||
@ -266,7 +266,7 @@ export const routes: IRoute[] = [
|
||||
{
|
||||
path: '/environments',
|
||||
title: 'Environments',
|
||||
component: EnvironmentList,
|
||||
component: EnvironmentTable,
|
||||
type: 'protected',
|
||||
flag: EEA,
|
||||
menu: { mobile: true, advanced: true },
|
||||
|
@ -10,12 +10,12 @@ interface IUIContext {
|
||||
|
||||
export const createEmptyToast = (): IToast => {
|
||||
return {
|
||||
type: 'success',
|
||||
title: '',
|
||||
text: '',
|
||||
components: [],
|
||||
show: false,
|
||||
persist: false,
|
||||
type: '',
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
IEnvironmentPayload,
|
||||
ISortOrderPayload,
|
||||
IEnvironmentEditPayload,
|
||||
IEnvironment,
|
||||
} from 'interfaces/environments';
|
||||
import useAPI from '../useApi/useApi';
|
||||
|
||||
@ -145,4 +146,13 @@ const useEnvironmentApi = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createSortOrderPayload = (
|
||||
environments: Readonly<IEnvironment[]>
|
||||
): ISortOrderPayload => {
|
||||
return environments.reduce((payload, env, index) => {
|
||||
payload[env.name] = index + 1;
|
||||
return payload;
|
||||
}, {} as ISortOrderPayload);
|
||||
};
|
||||
|
||||
export default useEnvironmentApi;
|
||||
|
@ -13,13 +13,13 @@ interface IUseEnvironmentsOutput {
|
||||
}
|
||||
|
||||
export const useEnvironments = (): IUseEnvironmentsOutput => {
|
||||
const { data, error, mutate } = useSWR<IEnvironmentResponse>(
|
||||
const { data, error, mutate } = useSWR<IEnvironment[]>(
|
||||
formatApiPath(`api/admin/environments`),
|
||||
fetcher
|
||||
);
|
||||
|
||||
const environments = useMemo(() => {
|
||||
return data?.environments || [];
|
||||
return data || [];
|
||||
}, [data]);
|
||||
|
||||
const refetchEnvironments = useCallback(async () => {
|
||||
@ -28,7 +28,7 @@ export const useEnvironments = (): IUseEnvironmentsOutput => {
|
||||
|
||||
const mutateEnvironments = useCallback(
|
||||
async (environments: IEnvironment[]) => {
|
||||
await mutate({ environments }, false);
|
||||
await mutate(environments, false);
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
@ -42,8 +42,12 @@ export const useEnvironments = (): IUseEnvironmentsOutput => {
|
||||
};
|
||||
};
|
||||
|
||||
const fetcher = (path: string): Promise<IEnvironmentResponse> => {
|
||||
return fetch(path)
|
||||
const fetcher = async (path: string): Promise<IEnvironment[]> => {
|
||||
const res: IEnvironmentResponse = await fetch(path)
|
||||
.then(handleErrorResponses('Environments'))
|
||||
.then(res => res.json());
|
||||
|
||||
return res.environments.sort((a, b) => {
|
||||
return a.sortOrder - b.sortOrder;
|
||||
});
|
||||
};
|
||||
|
72
frontend/src/hooks/useDragItem.ts
Normal file
72
frontend/src/hooks/useDragItem.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { useRef, useEffect, RefObject } from 'react';
|
||||
|
||||
export type MoveListItem = (
|
||||
dragIndex: number,
|
||||
dropIndex: number,
|
||||
save?: boolean
|
||||
) => void;
|
||||
|
||||
export const useDragItem = (
|
||||
listItemIndex: number,
|
||||
moveListItem: MoveListItem
|
||||
): RefObject<HTMLTableRowElement> => {
|
||||
const ref = useRef<HTMLTableRowElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.draggable = true;
|
||||
ref.current.style.cursor = 'grab';
|
||||
ref.current.dataset.index = String(listItemIndex);
|
||||
return addEventListeners(ref.current, moveListItem);
|
||||
}
|
||||
}, [listItemIndex, moveListItem]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
|
||||
const addEventListeners = (
|
||||
el: HTMLTableRowElement,
|
||||
moveListItem: MoveListItem
|
||||
): (() => void) => {
|
||||
const moveDraggedElement = (save: boolean) => {
|
||||
if (globalDraggedElement) {
|
||||
moveListItem(
|
||||
Number(globalDraggedElement.dataset.index),
|
||||
Number(el.dataset.index),
|
||||
save
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragStart = () => {
|
||||
globalDraggedElement = el;
|
||||
};
|
||||
|
||||
const onDragEnter = () => {
|
||||
moveDraggedElement(false);
|
||||
};
|
||||
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const onDrop = () => {
|
||||
moveDraggedElement(true);
|
||||
globalDraggedElement = null;
|
||||
};
|
||||
|
||||
el.addEventListener('dragstart', onDragStart);
|
||||
el.addEventListener('dragenter', onDragEnter);
|
||||
el.addEventListener('dragover', onDragOver);
|
||||
el.addEventListener('drop', onDrop);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('dragstart', onDragStart);
|
||||
el.removeEventListener('dragenter', onDragEnter);
|
||||
el.removeEventListener('dragover', onDragOver);
|
||||
el.removeEventListener('drop', onDrop);
|
||||
};
|
||||
};
|
||||
|
||||
// The element being dragged in the browser.
|
||||
let globalDraggedElement: HTMLTableRowElement | null;
|
@ -4,8 +4,6 @@ import 'regenerator-runtime/runtime';
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { ThemeProvider } from 'themes/ThemeProvider';
|
||||
import { App } from 'component/App';
|
||||
import { ScrollTop } from 'component/common/ScrollTop/ScrollTop';
|
||||
@ -17,7 +15,6 @@ import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/
|
||||
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
|
||||
|
||||
ReactDOM.render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<UIProvider>
|
||||
<AccessProvider>
|
||||
<BrowserRouter basename={basePath}>
|
||||
@ -33,7 +30,6 @@ ReactDOM.render(
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</AccessProvider>
|
||||
</UIProvider>
|
||||
</DndProvider>,
|
||||
</UIProvider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
export interface IToast {
|
||||
type: string;
|
||||
type: 'success' | 'error';
|
||||
title: string;
|
||||
text?: string;
|
||||
components?: JSX.Element[];
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"globalDependencies": {
|
||||
"react-dnd": "registry:dt/react-dnd#2.0.2+20161111212335",
|
||||
}
|
||||
}
|
@ -1538,21 +1538,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
|
||||
integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
|
||||
|
||||
"@react-dnd/asap@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-4.0.1.tgz#5291850a6b58ce6f2da25352a64f1b0674871aab"
|
||||
integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==
|
||||
|
||||
"@react-dnd/invariant@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-3.0.1.tgz#7e70be19ea21b539e8bf1da28466f4f05df2a4cc"
|
||||
integrity sha512-blqduwV86oiKw2Gr44wbe3pj3Z/OsXirc7ybCv9F/pLAR+Aih8F3rjeJzK0ANgtYKv5lCpkGVoZAeKitKDaD/g==
|
||||
|
||||
"@react-dnd/shallowequal@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-3.0.1.tgz#8056fe046a8d10a275e321ec0557ae652d7a4d06"
|
||||
integrity sha512-XjDVbs3ZU16CO1h5Q3Ew2RPJqmZBDE/EVf1LYp6ePEffs3V/MX9ZbL5bJr8qiK5SbGmUMuDoaFgyKacYz8prRA==
|
||||
|
||||
"@rollup/pluginutils@^4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
|
||||
@ -3040,15 +3025,6 @@ dir-glob@^3.0.1:
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
dnd-core@15.1.2:
|
||||
version "15.1.2"
|
||||
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-15.1.2.tgz#0983bce555c4985f58b731ffe1faed31e1ea7f6f"
|
||||
integrity sha512-EOec1LyJUuGRFg0LDa55rSRAUe97uNVKVkUo8iyvzQlcECYTuPblVQfRWXWj1OyPseFIeebWpNmKFy0h6BcF1A==
|
||||
dependencies:
|
||||
"@react-dnd/asap" "4.0.1"
|
||||
"@react-dnd/invariant" "3.0.1"
|
||||
redux "^4.1.2"
|
||||
|
||||
doctrine@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
|
||||
@ -5149,24 +5125,6 @@ react-chartjs-2@4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz#2a123df16d3a987c54eb4e810ed766d3c03adf8d"
|
||||
integrity sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==
|
||||
|
||||
react-dnd-html5-backend@15.1.3:
|
||||
version "15.1.3"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.3.tgz#57b4f47e0f23923e7c243d2d0eefe490069115a9"
|
||||
integrity sha512-HH/8nOEmrrcRGHMqJR91FOwhnLlx5SRLXmsQwZT3IPcBjx88WT+0pWC5A4tDOYDdoooh9k+KMPvWfxooR5TcOA==
|
||||
dependencies:
|
||||
dnd-core "15.1.2"
|
||||
|
||||
react-dnd@15.1.2:
|
||||
version "15.1.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-15.1.2.tgz#211b30fd842326209c63f26f1bdf1bc52eef4f64"
|
||||
integrity sha512-EaSbMD9iFJDY/o48T3c8wn3uWU+2uxfFojhesZN3LhigJoAIvH2iOjxofSA9KbqhAKP6V9P853G6XG8JngKVtA==
|
||||
dependencies:
|
||||
"@react-dnd/invariant" "3.0.1"
|
||||
"@react-dnd/shallowequal" "3.0.1"
|
||||
dnd-core "15.1.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
|
||||
react-dom@17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
@ -5303,13 +5261,6 @@ redent@^3.0.0:
|
||||
indent-string "^4.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
redux@^4.1.2:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
|
||||
integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
|
||||
reflect-metadata@0.1.13:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
||||
|
Loading…
Reference in New Issue
Block a user