mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	UI/bulk archive (#3319)
Ability to archive multiple feature toggles from project overview
This commit is contained in:
		
							parent
							
								
									a5f1b89b4a
								
							
						
					
					
						commit
						0784afd255
					
				| @ -3,13 +3,14 @@ import { Dialogue } from 'component/common/Dialogue/Dialogue'; | ||||
| import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; | ||||
| 
 | ||||
| interface IFeatureArchiveDialogProps { | ||||
|     isOpen: boolean; | ||||
|     onConfirm: () => void; | ||||
|     onClose: () => void; | ||||
|     projectId: string; | ||||
|     featureId: string; | ||||
|     featureIds: string[]; | ||||
| } | ||||
| 
 | ||||
| export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({ | ||||
| @ -17,14 +18,15 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({ | ||||
|     onClose, | ||||
|     onConfirm, | ||||
|     projectId, | ||||
|     featureId, | ||||
|     featureIds, | ||||
| }) => { | ||||
|     const { archiveFeatureToggle } = useFeatureApi(); | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const isBulkArchive = featureIds?.length > 1; | ||||
| 
 | ||||
|     const archiveToggle = async () => { | ||||
|         try { | ||||
|             await archiveFeatureToggle(projectId, featureId); | ||||
|             await archiveFeatureToggle(projectId, featureIds[0]); | ||||
|             setToastData({ | ||||
|                 text: 'Your feature toggle has been archived', | ||||
|                 type: 'success', | ||||
| @ -38,16 +40,69 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({ | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const archiveToggles = async () => { | ||||
|         try { | ||||
|             // TODO: bulk archive
 | ||||
|             await Promise.allSettled( | ||||
|                 featureIds.map(id => { | ||||
|                     archiveFeatureToggle(projectId, id); | ||||
|                 }) | ||||
|             ); | ||||
|             setToastData({ | ||||
|                 text: 'Selected feature toggles have been archived', | ||||
|                 type: 'success', | ||||
|                 title: 'Feature toggles archived', | ||||
|             }); | ||||
|             onConfirm(); | ||||
|             onClose(); | ||||
|         } catch (error: unknown) { | ||||
|             setToastApiError(formatUnknownError(error)); | ||||
|             onClose(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <Dialogue | ||||
|             onClick={() => archiveToggle()} | ||||
|             onClick={isBulkArchive ? archiveToggles : archiveToggle} | ||||
|             open={isOpen} | ||||
|             onClose={onClose} | ||||
|             primaryButtonText="Archive toggle" | ||||
|             primaryButtonText={ | ||||
|                 isBulkArchive ? 'Archive toggles' : 'Archive toggle' | ||||
|             } | ||||
|             secondaryButtonText="Cancel" | ||||
|             title="Archive feature toggle" | ||||
|             title={ | ||||
|                 isBulkArchive | ||||
|                     ? 'Archive feature toggles' | ||||
|                     : 'Archive feature toggle' | ||||
|             } | ||||
|         > | ||||
|             Are you sure you want to archive this feature toggle? | ||||
|             <ConditionallyRender | ||||
|                 condition={isBulkArchive} | ||||
|                 show={ | ||||
|                     <> | ||||
|                         <p> | ||||
|                             Are you sure you want to archive{' '} | ||||
|                             <strong>{featureIds?.length}</strong> feature | ||||
|                             toggles? | ||||
|                         </p> | ||||
|                         <ConditionallyRender | ||||
|                             condition={featureIds?.length <= 5} | ||||
|                             show={ | ||||
|                                 <ul> | ||||
|                                     {featureIds?.map(id => ( | ||||
|                                         <li key={id}>{id}</li> | ||||
|                                     ))} | ||||
|                                 </ul> | ||||
|                             } | ||||
|                         /> | ||||
|                     </> | ||||
|                 } | ||||
|                 elseShow={ | ||||
|                     <p> | ||||
|                         Are you sure you want to archive these feature toggles? | ||||
|                     </p> | ||||
|                 } | ||||
|             /> | ||||
|         </Dialogue> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -248,7 +248,7 @@ export const FeatureView = () => { | ||||
|                 }} | ||||
|                 onClose={() => setShowDelDialog(false)} | ||||
|                 projectId={projectId} | ||||
|                 featureId={featureId} | ||||
|                 featureIds={[featureId]} | ||||
|             /> | ||||
|             <FeatureStaleDialog | ||||
|                 isStale={feature.stale} | ||||
|  | ||||
| @ -685,7 +685,7 @@ export const ProjectFeatureToggles = ({ | ||||
|                 onClose={() => { | ||||
|                     setFeatureArchiveState(undefined); | ||||
|                 }} | ||||
|                 featureId={featureArchiveState || ''} | ||||
|                 featureIds={[featureArchiveState || '']} | ||||
|                 projectId={projectId} | ||||
|             />{' '} | ||||
|             <ChangeRequestDialogue | ||||
| @ -717,6 +717,7 @@ export const ProjectFeatureToggles = ({ | ||||
|             <SelectionActionsBar | ||||
|                 selectedIds={Object.keys(selectedRowIds)} | ||||
|                 data={features} | ||||
|                 projectId={projectId} | ||||
|             /> | ||||
|         </PageContent> | ||||
|     ); | ||||
|  | ||||
| @ -0,0 +1,50 @@ | ||||
| import { useState, VFC } from 'react'; | ||||
| import { Button } from '@mui/material'; | ||||
| import { Archive } from '@mui/icons-material'; | ||||
| import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC'; | ||||
| import { DELETE_FEATURE } from 'component/providers/AccessProvider/permissions'; | ||||
| import useProject from 'hooks/api/getters/useProject/useProject'; | ||||
| import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; | ||||
| 
 | ||||
| interface IArchiveButtonProps { | ||||
|     projectId: string; | ||||
|     features: string[]; | ||||
| } | ||||
| 
 | ||||
| export const ArchiveButton: VFC<IArchiveButtonProps> = ({ | ||||
|     projectId, | ||||
|     features, | ||||
| }) => { | ||||
|     const { refetch } = useProject(projectId); | ||||
|     const [isDialogOpen, setIsDialogOpen] = useState(false); | ||||
| 
 | ||||
|     const onConfirm = async () => { | ||||
|         setIsDialogOpen(false); | ||||
|         await refetch(); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <PermissionHOC projectId={projectId} permission={DELETE_FEATURE}> | ||||
|                 {({ hasAccess }) => ( | ||||
|                     <Button | ||||
|                         disabled={!hasAccess || isDialogOpen} | ||||
|                         startIcon={<Archive />} | ||||
|                         variant="outlined" | ||||
|                         size="small" | ||||
|                         onClick={() => setIsDialogOpen(true)} | ||||
|                     > | ||||
|                         Archive | ||||
|                     </Button> | ||||
|                 )} | ||||
|             </PermissionHOC> | ||||
|             <FeatureArchiveDialog | ||||
|                 projectId={projectId} | ||||
|                 featureIds={features} | ||||
|                 onConfirm={onConfirm} | ||||
|                 isOpen={isDialogOpen} | ||||
|                 onClose={() => setIsDialogOpen(false)} | ||||
|             /> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,54 @@ | ||||
| import { VFC } from 'react'; | ||||
| import { Button } from '@mui/material'; | ||||
| import { WatchLater } from '@mui/icons-material'; | ||||
| import type { FeatureSchema } from 'openapi'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; | ||||
| import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC'; | ||||
| 
 | ||||
| interface IMarkAsStaleButtonsProps { | ||||
|     projectId: string; | ||||
|     data: FeatureSchema[]; | ||||
| } | ||||
| 
 | ||||
| export const MarkAsStaleButtons: VFC<IMarkAsStaleButtonsProps> = ({ | ||||
|     projectId, | ||||
|     data, | ||||
| }) => { | ||||
|     const hasStale = data.some(d => d.stale); | ||||
|     const hasUnstale = data.some(d => !d.stale); | ||||
| 
 | ||||
|     return ( | ||||
|         <PermissionHOC projectId={projectId} permission={UPDATE_FEATURE}> | ||||
|             {({ hasAccess }) => ( | ||||
|                 <> | ||||
|                     <ConditionallyRender | ||||
|                         condition={hasUnstale || !hasAccess} | ||||
|                         show={ | ||||
|                             <Button | ||||
|                                 startIcon={<WatchLater />} | ||||
|                                 variant="outlined" | ||||
|                                 size="small" | ||||
|                                 disabled={!hasAccess} | ||||
|                             > | ||||
|                                 Mark as stale | ||||
|                             </Button> | ||||
|                         } | ||||
|                     /> | ||||
|                     <ConditionallyRender | ||||
|                         condition={Boolean(hasAccess && hasStale)} | ||||
|                         show={ | ||||
|                             <Button | ||||
|                                 startIcon={<WatchLater />} | ||||
|                                 variant="outlined" | ||||
|                                 size="small" | ||||
|                             > | ||||
|                                 Un-mark as stale | ||||
|                             </Button> | ||||
|                         } | ||||
|                     /> | ||||
|                 </> | ||||
|             )} | ||||
|         </PermissionHOC> | ||||
|     ); | ||||
| }; | ||||
| @ -1,26 +1,30 @@ | ||||
| import { useMemo, useState, VFC } from 'react'; | ||||
| import { Box, Button, Paper, styled, Typography } from '@mui/material'; | ||||
| import { Archive, FileDownload, Label, WatchLater } from '@mui/icons-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 { MarkAsStaleButtons } from './MarkAsStaleButtons/MarkAsStaleButtons'; | ||||
| 
 | ||||
| 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: 'space-between', | ||||
|     justifyContent: 'flex-end', | ||||
|     marginTop: theme.spacing(2), | ||||
|     marginLeft: 'auto', | ||||
|     marginRight: 'auto', | ||||
| @ -28,7 +32,8 @@ const StyledBar = styled(Paper)(({ theme }) => ({ | ||||
|     backgroundColor: theme.palette.background.paper, | ||||
|     border: `1px solid ${theme.palette.secondary.main}`, | ||||
|     borderRadius: theme.shape.borderRadiusLarge, | ||||
|     columnGap: theme.spacing(1), | ||||
|     gap: theme.spacing(1), | ||||
|     flexWrap: 'wrap', | ||||
| })); | ||||
| 
 | ||||
| const StyledCount = styled('span')(({ theme }) => ({ | ||||
| @ -39,12 +44,14 @@ const StyledCount = styled('span')(({ theme }) => ({ | ||||
| })); | ||||
| 
 | ||||
| const StyledText = styled(Typography)(({ theme }) => ({ | ||||
|     marginRight: theme.spacing(2), | ||||
|     paddingRight: theme.spacing(2), | ||||
|     marginRight: 'auto', | ||||
| })); | ||||
| 
 | ||||
| export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({ | ||||
|     selectedIds, | ||||
|     data, | ||||
|     projectId, | ||||
| }) => { | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const [showExportDialog, setShowExportDialog] = useState(false); | ||||
| @ -71,22 +78,8 @@ export const SelectionActionsBar: VFC<ISelectionActionsBarProps> = ({ | ||||
|                     <StyledCount>{selectedIds.length}</StyledCount> | ||||
|                      selected | ||||
|                 </StyledText> | ||||
|                 <Button | ||||
|                     disabled | ||||
|                     startIcon={<Archive />} | ||||
|                     variant="outlined" | ||||
|                     size="small" | ||||
|                 > | ||||
|                     Archive | ||||
|                 </Button> | ||||
|                 <Button | ||||
|                     disabled | ||||
|                     startIcon={<WatchLater />} | ||||
|                     variant="outlined" | ||||
|                     size="small" | ||||
|                 > | ||||
|                     Mark as stale | ||||
|                 </Button> | ||||
|                 <ArchiveButton projectId={projectId} features={selectedIds} /> | ||||
|                 <MarkAsStaleButtons projectId={projectId} data={selectedData} /> | ||||
|                 <Button | ||||
|                     startIcon={<FileDownload />} | ||||
|                     variant="outlined" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user