Skip to content

Commit

Permalink
[WEB-447] feat: projects archive. (#4014)
Browse files Browse the repository at this point in the history
* dev: project archive response

* feat: projects archive.

* dev: response changes for cycle and module

* chore: status message changed

* chore: update clear all applied display filters logic.

* style: archived project card UI update.

* chore: archive/ restore taost message update.

* fix: clear all applied display filter logic.

* chore: project empty state update to handle archived projects.

* chore: minor typo fix in cycles and modules archive.

* chore: close cycle/ module overview sidebar if it's already open when clicked on overview button.

* chore: optimize current workspace applied display filter logic.

* chore: update all `archived_at` type from `Date` to `string`.

---------

Co-authored-by: NarayanBavisetti <[email protected]>
  • Loading branch information
prateekshourya29 and NarayanBavisetti authored Mar 21, 2024
1 parent 9642b76 commit 231fd52
Show file tree
Hide file tree
Showing 31 changed files with 748 additions and 161 deletions.
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
Loading

0 comments on commit 231fd52

Please sign in to comment.