diff --git a/frontend/src/component/addons/AddonForm/AddonForm.tsx b/frontend/src/component/addons/AddonForm/AddonForm.tsx index 86612a936b..2baaf1a8e2 100644 --- a/frontend/src/component/addons/AddonForm/AddonForm.tsx +++ b/frontend/src/component/addons/AddonForm/AddonForm.tsx @@ -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, diff --git a/frontend/src/component/archive/ArchiveTable/ArchiveBatchActions.tsx b/frontend/src/component/archive/ArchiveTable/ArchiveBatchActions.tsx new file mode 100644 index 0000000000..0e8420a5a8 --- /dev/null +++ b/frontend/src/component/archive/ArchiveTable/ArchiveBatchActions.tsx @@ -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 = ({ + 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 ( + <> + + {({ hasAccess }) => ( + + )} + + + ); +}; diff --git a/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx b/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx index f41f26194a..7735e1e0a9 100644 --- a/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx +++ b/frontend/src/component/archive/ArchiveTable/ArchiveTable.tsx @@ -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(); @@ -84,6 +95,24 @@ export const ArchiveTable = ({ const columns = useMemo( () => [ + ...(uiConfig?.flags?.bulkOperations + ? [ + { + id: 'Select', + Header: ({ getToggleAllRowsSelectedProps }: any) => ( + + ), + Cell: ({ row }: any) => ( + + ), + 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} /> + + + + } + /> ); }; diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx index e57a591dc4..42e7bea81f 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx @@ -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'; diff --git a/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx b/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx new file mode 100644 index 0000000000..65439fdf8e --- /dev/null +++ b/frontend/src/component/common/BatchSelectionActionsBar/BatchSelectionActionsBar.tsx @@ -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 = ({ + selectedIds, + children, +}) => { + if (selectedIds.length === 0) { + return null; + } + + return ( + + + + {selectedIds.length} +  selected + + {children} + + + ); +}; diff --git a/frontend/src/component/common/CreateButton/CreateButton.tsx b/frontend/src/component/common/CreateButton/CreateButton.tsx index 05d9306af2..a7365d6999 100644 --- a/frontend/src/component/common/CreateButton/CreateButton.tsx +++ b/frontend/src/component/common/CreateButton/CreateButton.tsx @@ -1,6 +1,6 @@ import PermissionButton, { IPermissionButtonProps, -} from '../PermissionButton/PermissionButton'; +} from 'component/common/PermissionButton/PermissionButton'; interface ICreateButtonProps extends IPermissionButtonProps { name: string; diff --git a/frontend/src/component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog.tsx b/frontend/src/component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog.tsx index 87d6f1e048..162f1e0414 100644 --- a/frontend/src/component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog.tsx +++ b/frontend/src/component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog.tsx @@ -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'; diff --git a/frontend/src/component/common/ResponsiveButton/ResponsiveButton.tsx b/frontend/src/component/common/ResponsiveButton/ResponsiveButton.tsx index 40923fa42f..0a5724c4eb 100644 --- a/frontend/src/component/common/ResponsiveButton/ResponsiveButton.tsx +++ b/frontend/src/component/common/ResponsiveButton/ResponsiveButton.tsx @@ -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 { diff --git a/frontend/src/component/common/UpdateButton/UpdateButton.tsx b/frontend/src/component/common/UpdateButton/UpdateButton.tsx index d0e01f5c27..de4db8e6d7 100644 --- a/frontend/src/component/common/UpdateButton/UpdateButton.tsx +++ b/frontend/src/component/common/UpdateButton/UpdateButton.tsx @@ -1,6 +1,6 @@ import PermissionButton, { IPermissionButtonProps, -} from '../PermissionButton/PermissionButton'; +} from 'component/common/PermissionButton/PermissionButton'; export const UpdateButton = ({ ...rest }: IPermissionButtonProps) => { return ( diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx index 6fcf4f5cee..73247b6d6a 100644 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -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 = ({ /> } /> - + + + ); }; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ProjectFeaturesBatchActions.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ProjectFeaturesBatchActions.tsx new file mode 100644 index 0000000000..cc88ab3b86 --- /dev/null +++ b/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ProjectFeaturesBatchActions.tsx @@ -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 ( + <> + + + + + setShowExportDialog(false)} + environments={environments} + /> + } + /> + + ); +}; diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/SelectionActionsBar.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/SelectionActionsBar.tsx deleted file mode 100644 index e48be00bea..0000000000 --- a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/SelectionActionsBar.tsx +++ /dev/null @@ -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 = ({ - 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 ( - - - - {selectedIds.length} -  selected - - - - - - - setShowExportDialog(false)} - environments={environments} - /> - } - /> - - ); -}; diff --git a/frontend/src/component/project/Project/ProjectOverview.tsx b/frontend/src/component/project/Project/ProjectOverview.tsx index 39fd019c0a..7eff1fb1d2 100644 --- a/frontend/src/component/project/Project/ProjectOverview.tsx +++ b/frontend/src/component/project/Project/ProjectOverview.tsx @@ -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'; diff --git a/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts b/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts index 10b67e9cf9..9d251a1926 100644 --- a/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts +++ b/frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts @@ -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, diff --git a/src/lib/routes/admin-api/project/project-archive.ts b/src/lib/routes/admin-api/project/project-archive.ts index 46eccfe66a..13396a4046 100644 --- a/src/lib/routes/admin-api/project/project-archive.ts +++ b/src/lib/routes/admin-api/project/project-archive.ts @@ -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: [ diff --git a/src/test/e2e/api/admin/feature-archive.e2e.test.ts b/src/test/e2e/api/admin/feature-archive.e2e.test.ts index c4a5d231d4..fd88f49fad 100644 --- a/src/test/e2e/api/admin/feature-archive.e2e.test.ts +++ b/src/test/e2e/api/admin/feature-archive.e2e.test.ts @@ -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) { diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index a8b322054b..0cacad95ed 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -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.",