mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
feat: revive features (#3344)
This commit is contained in:
parent
2c2da4ad3f
commit
d28e65b94c
@ -16,12 +16,12 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
|
import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
import { useEnvironments } from '../../../hooks/api/getters/useEnvironments/useEnvironments';
|
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||||
import { AddonMultiSelector } from './AddonMultiSelector/AddonMultiSelector';
|
import { AddonMultiSelector } from './AddonMultiSelector/AddonMultiSelector';
|
||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import {
|
import {
|
||||||
CREATE_ADDON,
|
CREATE_ADDON,
|
||||||
UPDATE_ADDON,
|
UPDATE_ADDON,
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { Undo } from '@mui/icons-material';
|
||||||
|
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
||||||
|
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
|
||||||
|
interface IArchiveBatchActionsProps {
|
||||||
|
selectedIds: string[];
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArchiveBatchActions: FC<IArchiveBatchActionsProps> = ({
|
||||||
|
selectedIds,
|
||||||
|
projectId,
|
||||||
|
}) => {
|
||||||
|
const { reviveFeatures } = useProjectApi();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { refetchArchived } = useFeaturesArchive(projectId);
|
||||||
|
|
||||||
|
const onRevive = async () => {
|
||||||
|
try {
|
||||||
|
await reviveFeatures(projectId, selectedIds);
|
||||||
|
await refetchArchived();
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: "And we're back!",
|
||||||
|
text: 'The feature toggles have been revived.',
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PermissionHOC projectId={projectId} permission={UPDATE_FEATURE}>
|
||||||
|
{({ hasAccess }) => (
|
||||||
|
<Button
|
||||||
|
disabled={!hasAccess}
|
||||||
|
startIcon={<Undo />}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={onRevive}
|
||||||
|
>
|
||||||
|
Revive
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</PermissionHOC>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,9 +1,15 @@
|
|||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||||
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
|
import {
|
||||||
|
SortingRule,
|
||||||
|
useFlexLayout,
|
||||||
|
useRowSelect,
|
||||||
|
useSortBy,
|
||||||
|
useTable,
|
||||||
|
} from 'react-table';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { useMediaQuery } from '@mui/material';
|
import { Checkbox, useMediaQuery } from '@mui/material';
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell';
|
||||||
@ -27,6 +33,10 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { ArchivedFeatureDeleteConfirm } from './ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm';
|
import { ArchivedFeatureDeleteConfirm } from './ArchivedFeatureActionCell/ArchivedFeatureDeleteConfirm/ArchivedFeatureDeleteConfirm';
|
||||||
import { IFeatureToggle } from 'interfaces/featureToggle';
|
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
|
import { RowSelectCell } from '../../project/Project/ProjectFeatureToggles/RowSelectCell/RowSelectCell';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { BatchSelectionActionsBar } from '../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||||
|
import { ArchiveBatchActions } from './ArchiveBatchActions';
|
||||||
|
|
||||||
export interface IFeaturesArchiveTableProps {
|
export interface IFeaturesArchiveTableProps {
|
||||||
archivedFeatures: FeatureSchema[];
|
archivedFeatures: FeatureSchema[];
|
||||||
@ -54,6 +64,7 @@ export const ArchiveTable = ({
|
|||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
const [deletedFeature, setDeletedFeature] = useState<IFeatureToggle>();
|
const [deletedFeature, setDeletedFeature] = useState<IFeatureToggle>();
|
||||||
@ -84,6 +95,24 @@ export const ArchiveTable = ({
|
|||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
...(uiConfig?.flags?.bulkOperations
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: 'Select',
|
||||||
|
Header: ({ getToggleAllRowsSelectedProps }: any) => (
|
||||||
|
<Checkbox {...getToggleAllRowsSelectedProps()} />
|
||||||
|
),
|
||||||
|
Cell: ({ row }: any) => (
|
||||||
|
<RowSelectCell
|
||||||
|
{...row?.getToggleRowSelectedProps?.()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
maxWidth: 50,
|
||||||
|
disableSortBy: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
Header: 'Seen',
|
Header: 'Seen',
|
||||||
width: 85,
|
width: 85,
|
||||||
@ -203,12 +232,15 @@ export const ArchiveTable = ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
hiddenColumns: ['description'],
|
hiddenColumns: ['description'],
|
||||||
|
selectedRowIds: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const getRowId = useCallback((row: any) => row.name, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
headerGroups,
|
headerGroups,
|
||||||
rows,
|
rows,
|
||||||
state: { sortBy },
|
state: { sortBy, selectedRowIds },
|
||||||
prepareRow,
|
prepareRow,
|
||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
} = useTable(
|
} = useTable(
|
||||||
@ -220,9 +252,11 @@ export const ArchiveTable = ({
|
|||||||
autoResetHiddenColumns: false,
|
autoResetHiddenColumns: false,
|
||||||
disableSortRemove: true,
|
disableSortRemove: true,
|
||||||
autoResetSortBy: false,
|
autoResetSortBy: false,
|
||||||
|
getRowId,
|
||||||
},
|
},
|
||||||
useFlexLayout,
|
useFlexLayout,
|
||||||
useSortBy
|
useSortBy,
|
||||||
|
useRowSelect
|
||||||
);
|
);
|
||||||
|
|
||||||
useConditionallyHiddenColumns(
|
useConditionallyHiddenColumns(
|
||||||
@ -312,6 +346,19 @@ export const ArchiveTable = ({
|
|||||||
setOpen={setDeleteModalOpen}
|
setOpen={setDeleteModalOpen}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(projectId)}
|
||||||
|
show={
|
||||||
|
<BatchSelectionActionsBar
|
||||||
|
selectedIds={Object.keys(selectedRowIds)}
|
||||||
|
>
|
||||||
|
<ArchiveBatchActions
|
||||||
|
selectedIds={Object.keys(selectedRowIds)}
|
||||||
|
projectId={projectId!}
|
||||||
|
/>
|
||||||
|
</BatchSelectionActionsBar>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,7 @@ import AccessContext from 'contexts/AccessContext';
|
|||||||
import { ChangeRequestComment } from './ChangeRequestComments/ChangeRequestComment';
|
import { ChangeRequestComment } from './ChangeRequestComments/ChangeRequestComment';
|
||||||
import { AddCommentField } from './ChangeRequestComments/AddCommentField';
|
import { AddCommentField } from './ChangeRequestComments/AddCommentField';
|
||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
import { useChangeRequestsEnabled } from '../../../hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { changesCount } from '../changesCount';
|
import { changesCount } from '../changesCount';
|
||||||
|
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import { Box, Paper, styled, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
interface IBatchSelectionActionsBarProps {
|
||||||
|
selectedIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled(Box)(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledBar = styled(Paper)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
padding: theme.spacing(2, 3),
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
border: `1px solid ${theme.palette.secondary.main}`,
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCount = styled('span')(({ theme }) => ({
|
||||||
|
background: theme.palette.secondary.main,
|
||||||
|
color: theme.palette.background.paper,
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledText = styled(Typography)(({ theme }) => ({
|
||||||
|
paddingRight: theme.spacing(2),
|
||||||
|
marginRight: 'auto',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const BatchSelectionActionsBar: FC<IBatchSelectionActionsBarProps> = ({
|
||||||
|
selectedIds,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledBar elevation={4}>
|
||||||
|
<StyledText>
|
||||||
|
<StyledCount>{selectedIds.length}</StyledCount>
|
||||||
|
 selected
|
||||||
|
</StyledText>
|
||||||
|
{children}
|
||||||
|
</StyledBar>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import PermissionButton, {
|
import PermissionButton, {
|
||||||
IPermissionButtonProps,
|
IPermissionButtonProps,
|
||||||
} from '../PermissionButton/PermissionButton';
|
} from 'component/common/PermissionButton/PermissionButton';
|
||||||
|
|
||||||
interface ICreateButtonProps extends IPermissionButtonProps {
|
interface ICreateButtonProps extends IPermissionButtonProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import PermissionButton from '../PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
import { formatCreateStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useMediaQuery } from '@mui/material';
|
import { useMediaQuery } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import PermissionButton from '../PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
|
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||||
import { ITooltipResolverProps } from '../TooltipResolver/TooltipResolver';
|
import { ITooltipResolverProps } from '../TooltipResolver/TooltipResolver';
|
||||||
|
|
||||||
interface IResponsiveButtonProps {
|
interface IResponsiveButtonProps {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import PermissionButton, {
|
import PermissionButton, {
|
||||||
IPermissionButtonProps,
|
IPermissionButtonProps,
|
||||||
} from '../PermissionButton/PermissionButton';
|
} from 'component/common/PermissionButton/PermissionButton';
|
||||||
|
|
||||||
export const UpdateButton = ({ ...rest }: IPermissionButtonProps) => {
|
export const UpdateButton = ({ ...rest }: IPermissionButtonProps) => {
|
||||||
return (
|
return (
|
||||||
|
@ -64,7 +64,8 @@ import FileDownload from '@mui/icons-material/FileDownload';
|
|||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||||
import { RowSelectCell } from './RowSelectCell/RowSelectCell';
|
import { RowSelectCell } from './RowSelectCell/RowSelectCell';
|
||||||
import { SelectionActionsBar } from './SelectionActionsBar/SelectionActionsBar';
|
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
||||||
|
import { ProjectFeaturesBatchActions } from './SelectionActionsBar/ProjectFeaturesBatchActions';
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
@ -714,11 +715,13 @@ export const ProjectFeatureToggles = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SelectionActionsBar
|
<BatchSelectionActionsBar selectedIds={Object.keys(selectedRowIds)}>
|
||||||
|
<ProjectFeaturesBatchActions
|
||||||
selectedIds={Object.keys(selectedRowIds)}
|
selectedIds={Object.keys(selectedRowIds)}
|
||||||
data={features}
|
data={features}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
/>
|
/>
|
||||||
|
</BatchSelectionActionsBar>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
import { FC, useMemo, useState } from 'react';
|
||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { FileDownload, Label } from '@mui/icons-material';
|
||||||
|
import type { FeatureSchema } from 'openapi';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ArchiveButton } from './ArchiveButton/ArchiveButton';
|
||||||
|
import { MoreActions } from './MoreActions/MoreActions';
|
||||||
|
|
||||||
|
interface IProjectFeaturesBatchActionsProps {
|
||||||
|
selectedIds: string[];
|
||||||
|
data: FeatureSchema[];
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectFeaturesBatchActions: FC<
|
||||||
|
IProjectFeaturesBatchActionsProps
|
||||||
|
> = ({ selectedIds, data, projectId }) => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
|
const selectedData = useMemo(
|
||||||
|
() => data.filter(d => selectedIds.includes(d.name)),
|
||||||
|
[data, selectedIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
const environments = useMemo(() => {
|
||||||
|
const envs = selectedData
|
||||||
|
.flatMap(d => d.environments)
|
||||||
|
.map(env => env?.name)
|
||||||
|
.filter(env => env !== undefined) as string[];
|
||||||
|
return Array.from(new Set(envs));
|
||||||
|
}, [selectedData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ArchiveButton projectId={projectId} features={selectedIds} />
|
||||||
|
<Button
|
||||||
|
startIcon={<FileDownload />}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setShowExportDialog(true)}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled
|
||||||
|
startIcon={<Label />}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Tags
|
||||||
|
</Button>
|
||||||
|
<MoreActions projectId={projectId} data={selectedData} />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(uiConfig?.flags?.featuresExportImport)}
|
||||||
|
show={
|
||||||
|
<ExportDialog
|
||||||
|
showExportDialog={showExportDialog}
|
||||||
|
data={selectedData}
|
||||||
|
onClose={() => setShowExportDialog(false)}
|
||||||
|
environments={environments}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,113 +0,0 @@
|
|||||||
import { useMemo, useState, VFC } from 'react';
|
|
||||||
import { Box, Button, Paper, styled, Typography } from '@mui/material';
|
|
||||||
import { FileDownload, Label, WatchLater } from '@mui/icons-material';
|
|
||||||
import type { FeatureSchema } from 'openapi';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { ArchiveButton } from './ArchiveButton/ArchiveButton';
|
|
||||||
import { MoreActions } from './MoreActions/MoreActions';
|
|
||||||
|
|
||||||
interface ISelectionActionsBarProps {
|
|
||||||
selectedIds: string[];
|
|
||||||
data: FeatureSchema[];
|
|
||||||
projectId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledContainer = styled(Box)(() => ({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '100%',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledBar = styled(Paper)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
marginLeft: 'auto',
|
|
||||||
marginRight: 'auto',
|
|
||||||
padding: theme.spacing(2, 3),
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
border: `1px solid ${theme.palette.secondary.main}`,
|
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledCount = styled('span')(({ theme }) => ({
|
|
||||||
background: theme.palette.secondary.main,
|
|
||||||
color: theme.palette.background.paper,
|
|
||||||
padding: theme.spacing(0.5, 1),
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledText = styled(Typography)(({ theme }) => ({
|
|
||||||
paddingRight: theme.spacing(2),
|
|
||||||
marginRight: 'auto',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({
|
|
||||||
selectedIds,
|
|
||||||
data,
|
|
||||||
projectId,
|
|
||||||
}) => {
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
|
||||||
const selectedData = useMemo(
|
|
||||||
() => data.filter(d => selectedIds.includes(d.name)),
|
|
||||||
[data, selectedIds]
|
|
||||||
);
|
|
||||||
const environments = useMemo(() => {
|
|
||||||
const envs = selectedData
|
|
||||||
.flatMap(d => d.environments)
|
|
||||||
.map(env => env?.name)
|
|
||||||
.filter(env => env !== undefined) as string[];
|
|
||||||
return Array.from(new Set(envs));
|
|
||||||
}, [selectedData]);
|
|
||||||
|
|
||||||
if (selectedIds.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer>
|
|
||||||
<StyledBar elevation={4}>
|
|
||||||
<StyledText>
|
|
||||||
<StyledCount>{selectedIds.length}</StyledCount>
|
|
||||||
 selected
|
|
||||||
</StyledText>
|
|
||||||
<ArchiveButton projectId={projectId} features={selectedIds} />
|
|
||||||
<Button
|
|
||||||
startIcon={<FileDownload />}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onClick={() => setShowExportDialog(true)}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled
|
|
||||||
startIcon={<Label />}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
Tags
|
|
||||||
</Button>
|
|
||||||
<MoreActions projectId={projectId} data={selectedData} />
|
|
||||||
</StyledBar>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(uiConfig?.flags?.featuresExportImport)}
|
|
||||||
show={
|
|
||||||
<ExportDialog
|
|
||||||
showExportDialog={showExportDialog}
|
|
||||||
data={selectedData}
|
|
||||||
onClose={() => setShowExportDialog(false)}
|
|
||||||
environments={environments}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
|
@ -7,7 +7,7 @@ import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureTog
|
|||||||
import ProjectInfo from './ProjectInfo/ProjectInfo';
|
import ProjectInfo from './ProjectInfo/ProjectInfo';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useLastViewedProject } from '../../../hooks/useLastViewedProject';
|
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ProjectStats } from './ProjectStats/ProjectStats';
|
import { ProjectStats } from './ProjectStats/ProjectStats';
|
||||||
|
@ -227,6 +227,16 @@ const useProjectApi = () => {
|
|||||||
return makeRequest(req.caller, req.id);
|
return makeRequest(req.caller, req.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reviveFeatures = async (projectId: string, featureIds: string[]) => {
|
||||||
|
const path = `api/admin/projects/${projectId}/revive`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ features: featureIds }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeRequest(req.caller, req.id);
|
||||||
|
};
|
||||||
|
|
||||||
const staleFeatures = async (
|
const staleFeatures = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
featureIds: string[],
|
featureIds: string[],
|
||||||
@ -259,6 +269,7 @@ const useProjectApi = () => {
|
|||||||
changeUserRole,
|
changeUserRole,
|
||||||
changeGroupRole,
|
changeGroupRole,
|
||||||
archiveFeatures,
|
archiveFeatures,
|
||||||
|
reviveFeatures,
|
||||||
staleFeatures,
|
staleFeatures,
|
||||||
searchProjectUser,
|
searchProjectUser,
|
||||||
setDefaultProjectStickiness,
|
setDefaultProjectStickiness,
|
||||||
|
@ -17,7 +17,8 @@ import { BatchFeaturesSchema, createRequestSchema } from '../../../openapi';
|
|||||||
import NotFoundError from '../../../error/notfound-error';
|
import NotFoundError from '../../../error/notfound-error';
|
||||||
import Controller from '../../controller';
|
import Controller from '../../controller';
|
||||||
|
|
||||||
const PATH = '/:projectId/archive';
|
const PATH = '/:projectId';
|
||||||
|
const PATH_ARCHIVE = `${PATH}/archive`;
|
||||||
const PATH_DELETE = `${PATH}/delete`;
|
const PATH_DELETE = `${PATH}/delete`;
|
||||||
const PATH_REVIVE = `${PATH}/revive`;
|
const PATH_REVIVE = `${PATH}/revive`;
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ export default class ProjectArchiveController extends Controller {
|
|||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: PATH,
|
path: PATH_ARCHIVE,
|
||||||
handler: this.archiveFeatures,
|
handler: this.archiveFeatures,
|
||||||
permission: DELETE_FEATURE,
|
permission: DELETE_FEATURE,
|
||||||
middleware: [
|
middleware: [
|
||||||
|
@ -221,7 +221,7 @@ test('can bulk delete features and recreate after', async () => {
|
|||||||
})
|
})
|
||||||
.expect(202);
|
.expect(202);
|
||||||
await app.request
|
await app.request
|
||||||
.post('/api/admin/projects/default/archive/delete')
|
.post('/api/admin/projects/default/delete')
|
||||||
.send({ features })
|
.send({ features })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
@ -253,7 +253,7 @@ test('can bulk revive features', async () => {
|
|||||||
})
|
})
|
||||||
.expect(202);
|
.expect(202);
|
||||||
await app.request
|
await app.request
|
||||||
.post('/api/admin/projects/default/archive/revive')
|
.post('/api/admin/projects/default/revive')
|
||||||
.send({ features })
|
.send({ features })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
|
@ -6130,7 +6130,7 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"/api/admin/projects/{projectId}/archive/delete": {
|
"/api/admin/projects/{projectId}/delete": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "This endpoint deletes the specified features, that are in archive.",
|
"description": "This endpoint deletes the specified features, that are in archive.",
|
||||||
"operationId": "deleteFeatures",
|
"operationId": "deleteFeatures",
|
||||||
@ -6166,42 +6166,6 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"/api/admin/projects/{projectId}/archive/revive": {
|
|
||||||
"post": {
|
|
||||||
"description": "This endpoint revives the specified features.",
|
|
||||||
"operationId": "reviveFeatures",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "path",
|
|
||||||
"name": "projectId",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/batchFeaturesSchema",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"description": "batchFeaturesSchema",
|
|
||||||
"required": true,
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "This response has no body.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"summary": "Revives a list of features",
|
|
||||||
"tags": [
|
|
||||||
"Archive",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"/api/admin/projects/{projectId}/environments": {
|
"/api/admin/projects/{projectId}/environments": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "addEnvironmentToProject",
|
"operationId": "addEnvironmentToProject",
|
||||||
@ -7582,6 +7546,42 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/api/admin/projects/{projectId}/revive": {
|
||||||
|
"post": {
|
||||||
|
"description": "This endpoint revives the specified features.",
|
||||||
|
"operationId": "reviveFeatures",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "projectId",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/batchFeaturesSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "batchFeaturesSchema",
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Revives a list of features",
|
||||||
|
"tags": [
|
||||||
|
"Archive",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
"/api/admin/projects/{projectId}/stale": {
|
"/api/admin/projects/{projectId}/stale": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "This endpoint stales the specified features.",
|
"description": "This endpoint stales the specified features.",
|
||||||
|
Loading…
Reference in New Issue
Block a user