mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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 useToast from 'hooks/useToast';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import useProjects from '../../../hooks/api/getters/useProjects/useProjects';
 | 
			
		||||
import { useEnvironments } from '../../../hooks/api/getters/useEnvironments/useEnvironments';
 | 
			
		||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
 | 
			
		||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
 | 
			
		||||
import { AddonMultiSelector } from './AddonMultiSelector/AddonMultiSelector';
 | 
			
		||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
			
		||||
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import PermissionButton from '../../common/PermissionButton/PermissionButton';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
 | 
			
		||||
import {
 | 
			
		||||
    CREATE_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 { PageHeader } from 'component/common/PageHeader/PageHeader';
 | 
			
		||||
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 { useMediaQuery } from '@mui/material';
 | 
			
		||||
import { Checkbox, useMediaQuery } from '@mui/material';
 | 
			
		||||
import { sortTypes } from 'utils/sortTypes';
 | 
			
		||||
import { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
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 { IFeatureToggle } from 'interfaces/featureToggle';
 | 
			
		||||
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 {
 | 
			
		||||
    archivedFeatures: FeatureSchema[];
 | 
			
		||||
@ -54,6 +64,7 @@ export const ArchiveTable = ({
 | 
			
		||||
    const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
 | 
			
		||||
    const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
 | 
			
		||||
    const [deleteModalOpen, setDeleteModalOpen] = useState(false);
 | 
			
		||||
    const [deletedFeature, setDeletedFeature] = useState<IFeatureToggle>();
 | 
			
		||||
@ -84,6 +95,24 @@ export const ArchiveTable = ({
 | 
			
		||||
 | 
			
		||||
    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',
 | 
			
		||||
                width: 85,
 | 
			
		||||
@ -203,12 +232,15 @@ export const ArchiveTable = ({
 | 
			
		||||
            },
 | 
			
		||||
        ],
 | 
			
		||||
        hiddenColumns: ['description'],
 | 
			
		||||
        selectedRowIds: {},
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    const getRowId = useCallback((row: any) => row.name, []);
 | 
			
		||||
 | 
			
		||||
    const {
 | 
			
		||||
        headerGroups,
 | 
			
		||||
        rows,
 | 
			
		||||
        state: { sortBy },
 | 
			
		||||
        state: { sortBy, selectedRowIds },
 | 
			
		||||
        prepareRow,
 | 
			
		||||
        setHiddenColumns,
 | 
			
		||||
    } = useTable(
 | 
			
		||||
@ -220,9 +252,11 @@ export const ArchiveTable = ({
 | 
			
		||||
            autoResetHiddenColumns: false,
 | 
			
		||||
            disableSortRemove: true,
 | 
			
		||||
            autoResetSortBy: false,
 | 
			
		||||
            getRowId,
 | 
			
		||||
        },
 | 
			
		||||
        useFlexLayout,
 | 
			
		||||
        useSortBy
 | 
			
		||||
        useSortBy,
 | 
			
		||||
        useRowSelect
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useConditionallyHiddenColumns(
 | 
			
		||||
@ -312,6 +346,19 @@ export const ArchiveTable = ({
 | 
			
		||||
                setOpen={setDeleteModalOpen}
 | 
			
		||||
                refetch={refetch}
 | 
			
		||||
            />
 | 
			
		||||
            <ConditionallyRender
 | 
			
		||||
                condition={Boolean(projectId)}
 | 
			
		||||
                show={
 | 
			
		||||
                    <BatchSelectionActionsBar
 | 
			
		||||
                        selectedIds={Object.keys(selectedRowIds)}
 | 
			
		||||
                    >
 | 
			
		||||
                        <ArchiveBatchActions
 | 
			
		||||
                            selectedIds={Object.keys(selectedRowIds)}
 | 
			
		||||
                            projectId={projectId!}
 | 
			
		||||
                        />
 | 
			
		||||
                    </BatchSelectionActionsBar>
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        </PageContent>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ import AccessContext from 'contexts/AccessContext';
 | 
			
		||||
import { ChangeRequestComment } from './ChangeRequestComments/ChangeRequestComment';
 | 
			
		||||
import { AddCommentField } from './ChangeRequestComments/AddCommentField';
 | 
			
		||||
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 { 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, {
 | 
			
		||||
    IPermissionButtonProps,
 | 
			
		||||
} from '../PermissionButton/PermissionButton';
 | 
			
		||||
} from 'component/common/PermissionButton/PermissionButton';
 | 
			
		||||
 | 
			
		||||
interface ICreateButtonProps extends IPermissionButtonProps {
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
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 { styled } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useMediaQuery } from '@mui/material';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import PermissionButton from '../PermissionButton/PermissionButton';
 | 
			
		||||
import PermissionIconButton from '../PermissionIconButton/PermissionIconButton';
 | 
			
		||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
 | 
			
		||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
 | 
			
		||||
import { ITooltipResolverProps } from '../TooltipResolver/TooltipResolver';
 | 
			
		||||
 | 
			
		||||
interface IResponsiveButtonProps {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import PermissionButton, {
 | 
			
		||||
    IPermissionButtonProps,
 | 
			
		||||
} from '../PermissionButton/PermissionButton';
 | 
			
		||||
} from 'component/common/PermissionButton/PermissionButton';
 | 
			
		||||
 | 
			
		||||
export const UpdateButton = ({ ...rest }: IPermissionButtonProps) => {
 | 
			
		||||
    return (
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,8 @@ import FileDownload from '@mui/icons-material/FileDownload';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
 | 
			
		||||
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)(() => ({
 | 
			
		||||
    whiteSpace: 'nowrap',
 | 
			
		||||
@ -714,11 +715,13 @@ export const ProjectFeatureToggles = ({
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
            <SelectionActionsBar
 | 
			
		||||
            <BatchSelectionActionsBar selectedIds={Object.keys(selectedRowIds)}>
 | 
			
		||||
                <ProjectFeaturesBatchActions
 | 
			
		||||
                    selectedIds={Object.keys(selectedRowIds)}
 | 
			
		||||
                    data={features}
 | 
			
		||||
                    projectId={projectId}
 | 
			
		||||
                />
 | 
			
		||||
            </BatchSelectionActionsBar>
 | 
			
		||||
        </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 { usePageTitle } from 'hooks/usePageTitle';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { useLastViewedProject } from '../../../hooks/useLastViewedProject';
 | 
			
		||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { ProjectStats } from './ProjectStats/ProjectStats';
 | 
			
		||||
 | 
			
		||||
@ -227,6 +227,16 @@ const useProjectApi = () => {
 | 
			
		||||
        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 (
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        featureIds: string[],
 | 
			
		||||
@ -259,6 +269,7 @@ const useProjectApi = () => {
 | 
			
		||||
        changeUserRole,
 | 
			
		||||
        changeGroupRole,
 | 
			
		||||
        archiveFeatures,
 | 
			
		||||
        reviveFeatures,
 | 
			
		||||
        staleFeatures,
 | 
			
		||||
        searchProjectUser,
 | 
			
		||||
        setDefaultProjectStickiness,
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,8 @@ import { BatchFeaturesSchema, createRequestSchema } from '../../../openapi';
 | 
			
		||||
import NotFoundError from '../../../error/notfound-error';
 | 
			
		||||
import Controller from '../../controller';
 | 
			
		||||
 | 
			
		||||
const PATH = '/:projectId/archive';
 | 
			
		||||
const PATH = '/:projectId';
 | 
			
		||||
const PATH_ARCHIVE = `${PATH}/archive`;
 | 
			
		||||
const PATH_DELETE = `${PATH}/delete`;
 | 
			
		||||
const PATH_REVIVE = `${PATH}/revive`;
 | 
			
		||||
 | 
			
		||||
@ -83,7 +84,7 @@ export default class ProjectArchiveController extends Controller {
 | 
			
		||||
 | 
			
		||||
        this.route({
 | 
			
		||||
            method: 'post',
 | 
			
		||||
            path: PATH,
 | 
			
		||||
            path: PATH_ARCHIVE,
 | 
			
		||||
            handler: this.archiveFeatures,
 | 
			
		||||
            permission: DELETE_FEATURE,
 | 
			
		||||
            middleware: [
 | 
			
		||||
 | 
			
		||||
@ -221,7 +221,7 @@ test('can bulk delete features and recreate after', async () => {
 | 
			
		||||
        })
 | 
			
		||||
        .expect(202);
 | 
			
		||||
    await app.request
 | 
			
		||||
        .post('/api/admin/projects/default/archive/delete')
 | 
			
		||||
        .post('/api/admin/projects/default/delete')
 | 
			
		||||
        .send({ features })
 | 
			
		||||
        .expect(200);
 | 
			
		||||
    for (const feature of features) {
 | 
			
		||||
@ -253,7 +253,7 @@ test('can bulk revive features', async () => {
 | 
			
		||||
        })
 | 
			
		||||
        .expect(202);
 | 
			
		||||
    await app.request
 | 
			
		||||
        .post('/api/admin/projects/default/archive/revive')
 | 
			
		||||
        .post('/api/admin/projects/default/revive')
 | 
			
		||||
        .send({ features })
 | 
			
		||||
        .expect(200);
 | 
			
		||||
    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": {
 | 
			
		||||
        "description": "This endpoint deletes the specified features, that are in archive.",
 | 
			
		||||
        "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": {
 | 
			
		||||
      "post": {
 | 
			
		||||
        "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": {
 | 
			
		||||
      "post": {
 | 
			
		||||
        "description": "This endpoint stales the specified features.",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user