Skip to content

Commit

Permalink
feat: dialogs for project revive and delete (#7863)
Browse files Browse the repository at this point in the history
Dialog needed to confirm revive/delete actions
  • Loading branch information
Tymek authored Aug 15, 2024
1 parent 627768b commit 3baeb4c
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,59 +22,35 @@ import TimeAgo from 'react-timeago';
import { Box, Link, Tooltip } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import {
CREATE_PROJECT,
DELETE_PROJECT,
UPDATE_PROJECT,
} from 'component/providers/AccessProvider/permissions';
import Undo from '@mui/icons-material/Undo';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import Delete from '@mui/icons-material/Delete';

interface IProjectArchiveCardProps {
export type ProjectArchiveCardProps = {
id: string;
name: string;
createdAt?: string;
archivedAt?: string;
featureCount: number;
archivedFeaturesCount?: number;
onRevive: () => void;
onDelete: () => void;
mode: string;
mode?: string;
owners?: ProjectSchemaOwners;
}
};

export const ProjectArchiveCard: FC<IProjectArchiveCardProps> = ({
export const ProjectArchiveCard: FC<ProjectArchiveCardProps> = ({
id,
name,
archivedAt,
featureCount = 0,
archivedFeaturesCount,
onRevive,
onDelete,
mode,
owners,
}) => {
const { locationSettings } = useLocationSettings();
const Actions: FC<{
id: string;
}> = ({ id }) => (
<StyledActions>
<PermissionIconButton
onClick={onRevive}
projectId={id}
permission={CREATE_PROJECT}
tooltipProps={{ title: 'Restore project' }}
data-testid={`revive-feature-flag-button`}
>
<Undo />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_PROJECT}
projectId={id}
tooltipProps={{ title: 'Permanently delete project' }}
onClick={onDelete}
>
<Delete />
</PermissionIconButton>
</StyledActions>
);

return (
<StyledProjectCard disabled>
Expand All @@ -89,17 +65,6 @@ export const ProjectArchiveCard: FC<IProjectArchiveCardProps> = ({
<ProjectModeBadge mode={mode} />
</StyledDivHeader>
<StyledDivInfo>
<Link
component={RouterLink}
to={`/archive?search=project%3A${encodeURI(id)}`}
>
<StyledParagraphInfo disabled data-loading>
{featureCount}
</StyledParagraphInfo>
<p data-loading>
archived {featureCount === 1 ? 'flag' : 'flags'}
</p>
</Link>
<ConditionallyRender
condition={Boolean(archivedAt)}
show={
Expand Down Expand Up @@ -129,15 +94,47 @@ export const ProjectArchiveCard: FC<IProjectArchiveCardProps> = ({
</Tooltip>
}
/>
<ConditionallyRender
condition={typeof archivedFeaturesCount !== 'undefined'}
show={
<Link
component={RouterLink}
to={`/archive?search=project%3A${encodeURI(id)}`}
>
<StyledParagraphInfo disabled data-loading>
{archivedFeaturesCount}
</StyledParagraphInfo>
<p data-loading>
archived{' '}
{archivedFeaturesCount === 1
? 'flag'
: 'flags'}
</p>
</Link>
}
/>
</StyledDivInfo>
</StyledProjectCardBody>
<ProjectCardFooter
id={id}
Actions={Actions}
disabled
owners={owners}
>
<Actions id={id} />
<ProjectCardFooter id={id} disabled owners={owners}>
<StyledActions>
<PermissionIconButton
onClick={onRevive}
projectId={id}
permission={UPDATE_PROJECT}
tooltipProps={{ title: 'Restore project' }}
data-testid={`revive-feature-flag-button`}
>
<Undo />
</PermissionIconButton>
<PermissionIconButton
permission={DELETE_PROJECT}
projectId={id}
tooltipProps={{ title: 'Permanently delete project' }}
onClick={onDelete}
>
<Delete />
</PermissionIconButton>
</StyledActions>
</ProjectCardFooter>
</StyledProjectCard>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ interface IProjectCardFooterProps {
id: string;
isFavorite?: boolean;
children?: React.ReactNode;
Actions?: FC<{ id: string; isFavorite?: boolean }>;
disabled?: boolean;
owners: IProjectOwnersProps['owners'];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import { Badge } from 'component/common/Badge/Badge';

interface IProjectModeBadgeProps {
mode: 'private' | 'protected' | 'public' | string;
mode?: 'private' | 'protected' | 'public' | string;
}

export const ProjectModeBadge: FC<IProjectModeBadgeProps> = ({ mode }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const DeleteProjectDialogue = ({
onSuccess,
}: IDeleteProjectDialogueProps) => {
const { deleteProject } = useProjectApi();
const { refetch: refetchProjectOverview } = useProjects();
const { refetch: refetchProjects } = useProjects();
const { refetch: refetchProjectArchive } = useProjects({ archived: true });
const { setToastData, setToastApiError } = useToast();
const { isEnterprise } = useUiConfig();
const automatedActionsEnabled = useUiFlag('automatedActions');
Expand All @@ -32,7 +33,8 @@ export const DeleteProjectDialogue = ({
e.preventDefault();
try {
await deleteProject(project);
refetchProjectOverview();
refetchProjects();
refetchProjectArchive();
setToastData({
title: 'Deleted project',
type: 'success',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export const ArchiveProject = ({
}: IDeleteProjectProps) => {
const { isEnterprise } = useUiConfig();
const automatedActionsEnabled = useUiFlag('automatedActions');
const archiveProjectsEnabled = useUiFlag('archiveProjects');
const { actions } = useActions(projectId);
const [showArchiveDialog, setShowArchiveDialog] = useState(false);
const actionsCount = actions.filter(({ enabled }) => enabled).length;
Expand Down
61 changes: 57 additions & 4 deletions frontend/src/component/project/ProjectList/ArchiveProjectList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { type FC, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import useProjectsArchive from 'hooks/api/getters/useProjectsArchive/useProjectsArchive';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
Expand All @@ -9,7 +8,13 @@ import { styled, useMediaQuery } from '@mui/material';
import theme from 'themes/theme';
import { Search } from 'component/common/Search/Search';
import { ProjectGroup } from './ProjectGroup';
import { ProjectArchiveCard } from '../NewProjectCard/ProjectArchiveCard';
import {
ProjectArchiveCard,
type ProjectArchiveCardProps,
} from '../NewProjectCard/ProjectArchiveCard';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { ReviveProjectDialog } from './ReviveProjectDialog/ReviveProjectDialog';
import { DeleteProjectDialogue } from '../Project/DeleteProject/DeleteProjectDialogue';

const StyledApiError = styled(ApiError)(({ theme }) => ({
maxWidth: '500px',
Expand All @@ -25,13 +30,24 @@ const StyledContainer = styled('div')(({ theme }) => ({
type PageQueryType = Partial<Record<'search', string>>;

export const ArchiveProjectList: FC = () => {
const { projects, loading, error, refetch } = useProjectsArchive();
const { projects, loading, error, refetch } = useProjects({
archived: true,
});

const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
const [searchParams, setSearchParams] = useSearchParams();
const [searchValue, setSearchValue] = useState(
searchParams.get('search') || '',
);
const [reviveProject, setReviveProject] = useState<{
isOpen: boolean;
id?: string;
name?: string;
}>({ isOpen: false });
const [deleteProject, setDeleteProject] = useState<{
isOpen: boolean;
id?: string;
}>({ isOpen: false });

useEffect(() => {
const tableState: PageQueryType = {};
Expand All @@ -44,6 +60,28 @@ export const ArchiveProjectList: FC = () => {
});
}, [searchValue, setSearchParams]);

const ProjectCard: FC<
Omit<ProjectArchiveCardProps, 'onRevive' | 'onDelete'>
> = ({ id, ...props }) => (
<ProjectArchiveCard
onRevive={() =>
setReviveProject({
isOpen: true,
id,
name: projects?.find((project) => project.id === id)?.name,
})
}
onDelete={() =>
setDeleteProject({
id,
isOpen: true,
})
}
id={id}
{...props}
/>
);

return (
<PageContent
isLoading={loading}
Expand Down Expand Up @@ -90,10 +128,25 @@ export const ArchiveProjectList: FC = () => {
searchValue={searchValue}
projects={projects}
placeholder='No archived projects found'
ProjectCardComponent={ProjectArchiveCard}
ProjectCardComponent={ProjectCard}
link={false}
/>
</StyledContainer>
<ReviveProjectDialog
id={reviveProject.id || ''}
name={reviveProject.name || ''}
open={reviveProject.isOpen}
onClose={() =>
setReviveProject((state) => ({ ...state, isOpen: false }))
}
/>
<DeleteProjectDialogue
project={deleteProject.id || ''}
open={deleteProject.isOpen}
onClose={() => {
setDeleteProject((state) => ({ ...state, isOpen: false }));
}}
/>
</PageContent>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';

type ReviveProjectDialogProps = {
name: string;
id: string;
open: boolean;
onClose: () => void;
};

export const ReviveProjectDialog = ({
name,
id,
open,
onClose,
}: ReviveProjectDialogProps) => {
const { reviveProject } = useProjectApi();
const { refetch: refetchProjects } = useProjects();
const { refetch: refetchProjectArchive } = useProjects({ archived: true });
const { setToastData, setToastApiError } = useToast();

const onClick = async (e: React.SyntheticEvent) => {
e.preventDefault();
if (!id) return;
try {
await reviveProject(id);
refetchProjects();
refetchProjectArchive();
setToastData({
title: 'Restored project',
type: 'success',
text: 'Successfully restored project',
});
} catch (ex: unknown) {
setToastApiError(formatUnknownError(ex));
}
onClose();
};

return (
<Dialogue
open={open}
secondaryButtonText='Close'
onClose={onClose}
onClick={onClick}
title='Restore archived project'
>
Are you sure you'd like to restore project <strong>{name}</strong>{' '}
(id: <code>{id}</code>)?
{/* TODO: more explanation */}
</Dialogue>
);
};
12 changes: 11 additions & 1 deletion frontend/src/hooks/api/actions/useProjectApi/useProjectApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,16 @@ const useProjectApi = () => {
};

const archiveProject = async (projectId: string) => {
const path = `api/admin/projects/${projectId}/archive`;
const path = `api/admin/projects/archive/${projectId}`;
const req = createRequest(path, { method: 'POST' });

const res = await makeRequest(req.caller, req.id);

return res;
};

const reviveProject = async (projectId: string) => {
const path = `api/admin/projects/revive/${projectId}`;
const req = createRequest(path, { method: 'POST' });

const res = await makeRequest(req.caller, req.id);
Expand Down Expand Up @@ -263,6 +272,7 @@ const useProjectApi = () => {
editProjectSettings,
deleteProject,
archiveProject,
reviveProject,
addEnvironmentToProject,
removeEnvironmentFromProject,
addAccessToProject,
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/hooks/api/getters/useProjects/useProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import { formatApiPath } from 'utils/formatPath';

import type { IProjectCard } from 'interfaces/project';
import handleErrorResponses from '../httpErrorResponseHandler';
import type { GetProjectsParams } from 'openapi';

const useProjects = (options: SWRConfiguration & GetProjectsParams = {}) => {
const KEY = `api/admin/projects${options.archived ? '?archived=true' : ''}`;

const useProjects = (options: SWRConfiguration = {}) => {
const fetcher = () => {
const path = formatApiPath(`api/admin/projects`);
const path = formatApiPath(KEY);
return fetch(path, {
method: 'GET',
})
.then(handleErrorResponses('Projects'))
.then((res) => res.json());
};

const KEY = `api/admin/projects`;

const { data, error } = useSWR<{ projects: IProjectCard[] }>(
KEY,
fetcher,
Expand Down
Loading

0 comments on commit 3baeb4c

Please sign in to comment.