Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dialogs for project revive and delete #7863

Merged
merged 5 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we relax the constraint?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Component was already written in a way that if any other string is provided, it will be ignored

}

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 = {
Tymek marked this conversation as resolved.
Show resolved Hide resolved
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
Loading