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

[WEB-447] feat: projects archive. #4014

Merged
merged 13 commits into from
Mar 21, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion apiserver/plane/app/views/cycle/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,10 @@ def post(self, request, slug, project_id, cycle_id):
)
cycle.archived_at = timezone.now()
cycle.save()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(
{"archived_at": str(cycle.archived_at)},
status=status.HTTP_200_OK,
)

def delete(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.get(
Expand Down
7 changes: 5 additions & 2 deletions apiserver/plane/app/views/module/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ def get(self, request, slug, project_id):
"backlog_issues",
"created_at",
"updated_at",
"archived_at"
"archived_at",
)
return Response(modules, status=status.HTTP_200_OK)

Expand All @@ -631,7 +631,10 @@ def post(self, request, slug, project_id, module_id):
)
module.archived_at = timezone.now()
module.save()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(
{"archived_at": str(module.archived_at)},
status=status.HTTP_200_OK,
)

def delete(self, request, slug, project_id, module_id):
module = Module.objects.get(
Expand Down
8 changes: 6 additions & 2 deletions apiserver/plane/app/views/project/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def partial_update(self, request, slug, pk=None):
return Response(
{"error": "Archived projects cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
)

serializer = ProjectSerializer(
project,
Expand Down Expand Up @@ -433,11 +433,15 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
permission_classes = [
ProjectBasePermission,
]

def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = timezone.now()
project.save()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(
{"archived_at": str(project.archived_at)},
status=status.HTTP_200_OK,
)

def delete(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/project/project_filters.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ export type TProjectOrderByOptions =

export type TProjectDisplayFilters = {
my_projects?: boolean;
archived_projects?: boolean;
order_by?: TProjectOrderByOptions;
};

export type TProjectAppliedDisplayFilterKeys =
| "my_projects"
| "archived_projects";

export type TProjectFilters = {
access?: string[] | null;
lead?: string[] | null;
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/project/projects.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type TProjectLogoProps = {

export interface IProject {
archive_in: number;
archived_at: string | null;
archived_issues: number;
archived_sub_issues: number;
close_in: number;
Expand Down
4 changes: 2 additions & 2 deletions web/components/cycles/archived-cycles/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const ArchiveCycleModal: React.FC<Props> = (props) => {
handleClose();
};

const handleArchiveIssue = async () => {
const handleArchiveCycle = async () => {
setIsArchiving(true);
await archiveCycle(workspaceSlug, projectId, cycleId)
.then(() => {
Expand Down Expand Up @@ -89,7 +89,7 @@ export const ArchiveCycleModal: React.FC<Props> = (props) => {
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Cancel
</Button>
<Button size="sm" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
<Button size="sm" tabIndex={1} onClick={handleArchiveCycle} loading={isArchiving}>
{isArchiving ? "Archiving" : "Archive"}
</Button>
</div>
Expand Down
20 changes: 14 additions & 6 deletions web/components/cycles/board/cycles-board-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
? cycleTotalIssues === 0
? "0 Issue"
: cycleTotalIssues === cycleDetails.completed_issues
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}`
: `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues`
: "0 Issue";

const handleAddToFavorites = (e: MouseEvent<HTMLButtonElement>) => {
Expand Down Expand Up @@ -134,10 +134,18 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = observer((props) => {
e.preventDefault();
e.stopPropagation();

router.push({
pathname: router.pathname,
query: { ...query, peekCycle: cycleId },
});
if (query.peekCycle) {
delete query.peekCycle;
router.push({
pathname: router.pathname,
query: { ...query },
});
} else {
router.push({
pathname: router.pathname,
query: { ...query, peekCycle: cycleId },
});
}
};

const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0;
Expand Down
16 changes: 12 additions & 4 deletions web/components/cycles/list/cycles-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,18 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
e.preventDefault();
e.stopPropagation();

router.push({
pathname: router.pathname,
query: { ...query, peekCycle: cycleId },
});
if (query.peekCycle) {
delete query.peekCycle;
router.push({
pathname: router.pathname,
query: { ...query },
});
} else {
router.push({
pathname: router.pathname,
query: { ...query, peekCycle: cycleId },
});
}
};

const cycleDetails = getCycleById(cycleId);
Expand Down
4 changes: 2 additions & 2 deletions web/components/modules/archived-modules/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const ArchiveModuleModal: React.FC<Props> = (props) => {
handleClose();
};

const handleArchiveIssue = async () => {
const handleArchiveModule = async () => {
setIsArchiving(true);
await archiveModule(workspaceSlug, projectId, moduleId)
.then(() => {
Expand Down Expand Up @@ -89,7 +89,7 @@ export const ArchiveModuleModal: React.FC<Props> = (props) => {
<Button variant="neutral-primary" size="sm" onClick={onClose}>
Cancel
</Button>
<Button size="sm" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
<Button size="sm" tabIndex={1} onClick={handleArchiveModule} loading={isArchiving}>
{isArchiving ? "Archiving" : "Archive"}
</Button>
</div>
Expand Down
16 changes: 12 additions & 4 deletions web/components/modules/module-card-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,18 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
e.preventDefault();
const { query } = router;

router.push({
pathname: router.pathname,
query: { ...query, peekModule: moduleId },
});
if (query.peekModule) {
delete query.peekModule;
router.push({
pathname: router.pathname,
query: { ...query },
});
} else {
router.push({
pathname: router.pathname,
query: { ...query, peekModule: moduleId },
});
}
};

if (!moduleDetails) return null;
Expand Down
16 changes: 12 additions & 4 deletions web/components/modules/module-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,18 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
e.preventDefault();
const { query } = router;

router.push({
pathname: router.pathname,
query: { ...query, peekModule: moduleId },
});
if (query.peekModule) {
delete query.peekModule;
router.push({
pathname: router.pathname,
query: { ...query },
});
} else {
router.push({
pathname: router.pathname,
query: { ...query, peekModule: moduleId },
});
}
};

if (!moduleDetails) return null;
Expand Down
1 change: 1 addition & 0 deletions web/components/project/applied-filters/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./access";
export * from "./date";
export * from "./members";
export * from "./project-display-filters";
export * from "./root";
39 changes: 39 additions & 0 deletions web/components/project/applied-filters/project-display-filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { observer } from "mobx-react-lite";
// icons
import { X } from "lucide-react";
// types
import { TProjectAppliedDisplayFilterKeys } from "@plane/types";
// constants
import { PROJECT_DISPLAY_FILTER_OPTIONS } from "@/constants/project";

type Props = {
handleRemove: (key: TProjectAppliedDisplayFilterKeys) => void;
values: TProjectAppliedDisplayFilterKeys[];
editable: boolean | undefined;
};

export const AppliedProjectDisplayFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values, editable } = props;

return (
<>
{values.map((key) => {
const filterLabel = PROJECT_DISPLAY_FILTER_OPTIONS.find((s) => s.key === key)?.label;
return (
<div key={key} className="flex items-center gap-1 rounded p-1 text-xs bg-custom-background-80">
{filterLabel}
{editable && (
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(key)}
>
<X size={10} strokeWidth={2} />
</button>
)}
</div>
);
})}
</>
);
});
40 changes: 33 additions & 7 deletions web/components/project/applied-filters/root.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { X } from "lucide-react";
import { TProjectFilters } from "@plane/types";
// components
import { Tooltip } from "@plane/ui";
import { AppliedAccessFilters, AppliedDateFilters, AppliedMembersFilters } from "@/components/project";
// types
import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types";
// ui
import { Tooltip } from "@plane/ui";
// components
import {
AppliedAccessFilters,
AppliedDateFilters,
AppliedMembersFilters,
AppliedProjectDisplayFilters,
} from "@/components/project";
// helpers
import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper";
// types

type Props = {
appliedFilters: TProjectFilters;
appliedDisplayFilters: TProjectAppliedDisplayFilterKeys[];
handleClearAllFilters: () => void;
handleRemoveFilter: (key: keyof TProjectFilters, value: string | null) => void;
handleRemoveDisplayFilter: (key: TProjectAppliedDisplayFilterKeys) => void;
alwaysAllowEditing?: boolean;
filteredProjects: number;
totalProjects: number;
Expand All @@ -23,21 +30,24 @@ const DATE_FILTERS = ["created_at"];
export const ProjectAppliedFiltersList: React.FC<Props> = (props) => {
const {
appliedFilters,
appliedDisplayFilters,
handleClearAllFilters,
handleRemoveFilter,
handleRemoveDisplayFilter,
alwaysAllowEditing,
filteredProjects,
totalProjects,
} = props;

if (!appliedFilters) return null;
if (Object.keys(appliedFilters).length === 0) return null;
if (!appliedFilters && !appliedDisplayFilters) return null;
if (Object.keys(appliedFilters).length === 0 && appliedDisplayFilters.length === 0) return null;

const isEditingAllowed = alwaysAllowEditing;

return (
<div className="flex items-start justify-between gap-1.5">
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
{/* Applied filters */}
{Object.entries(appliedFilters).map(([key, value]) => {
const filterKey = key as keyof TProjectFilters;

Expand Down Expand Up @@ -85,6 +95,22 @@ export const ProjectAppliedFiltersList: React.FC<Props> = (props) => {
</div>
);
})}
{/* Applied display filters */}
{appliedDisplayFilters.length > 0 && (
<div
key="project_display_filters"
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
>
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-xs text-custom-text-300">Projects</span>
<AppliedProjectDisplayFilters
editable={isEditingAllowed}
values={appliedDisplayFilters}
handleRemove={(key) => handleRemoveDisplayFilter(key)}
/>
</div>
</div>
)}
{isEditingAllowed && (
<button
type="button"
Expand Down
4 changes: 2 additions & 2 deletions web/components/project/card-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export const ProjectCardList = observer(() => {
const { commandPalette: commandPaletteStore } = useApplication();
const { setTrackElement } = useEventTracker();
const { workspaceProjectIds, filteredProjectIds, getProjectById } = useProject();
const { searchQuery } = useProjectFilter();
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();

if (workspaceProjectIds?.length === 0)
if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects)
return (
<EmptyState
type={EmptyStateType.WORKSPACE_PROJECTS}
Expand Down