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: cycles and modules archive. #4005

Merged
merged 11 commits into from
Mar 20, 2024
Merged
2 changes: 1 addition & 1 deletion apiserver/plane/api/views/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ def get_queryset(self):
.distinct()
)

def list(self, request, slug, project_id):
def get(self, request, slug, project_id):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/api/views/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ def get_queryset(self):
.order_by(self.kwargs.get("order_by", "-created_at"))
)

def list(self, request, slug, project_id):
def get(self, request, slug, project_id):
return self.paginate(
request=request,
queryset=(self.get_queryset()),
Expand Down
9 changes: 4 additions & 5 deletions apiserver/plane/app/views/cycle/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,10 +714,8 @@ def get_queryset(self):
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
)
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
return (
Cycle.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(archived_at__isnull=False)
.filter(
Expand Down Expand Up @@ -831,7 +829,7 @@ def get_queryset(self):
.distinct()
)

def list(self, request, slug, project_id):
def get(self, request, slug, project_id):
queryset = (
self.get_queryset()
.annotate(
Expand Down Expand Up @@ -869,6 +867,7 @@ def list(self, request, slug, project_id):
"backlog_issues",
"assignee_ids",
"status",
"archived_at",
)
).order_by("-is_favorite", "-created_at")
return Response(queryset, status=status.HTTP_200_OK)
Expand Down
8 changes: 3 additions & 5 deletions apiserver/plane/app/views/module/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,10 +498,7 @@ def get_queryset(self):
workspace__slug=self.kwargs.get("slug"),
)
return (
super()
.get_queryset()
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
Module.objects.filter(workspace__slug=self.kwargs.get("slug"))
.filter(archived_at__isnull=False)
.annotate(is_favorite=Exists(favorite_subquery))
.select_related("project")
Expand Down Expand Up @@ -594,7 +591,7 @@ def get_queryset(self):
.order_by("-is_favorite", "-created_at")
)

def list(self, request, slug, project_id):
def get(self, request, slug, project_id):
queryset = self.get_queryset()
modules = queryset.values( # Required fields
"id",
Expand Down Expand Up @@ -624,6 +621,7 @@ def list(self, request, slug, project_id):
"backlog_issues",
"created_at",
"updated_at",
"archived_at"
)
return Response(modules, status=status.HTTP_200_OK)

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/cycle/cycle.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface ICycle {
unstarted_issues: number;
updated_at: Date;
updated_by: string;
archived_at: string | null;
assignee_ids: string[];
view_props: {
filters: IIssueFilterOptions;
Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/cycle/cycle_filters.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export type TCycleFilters = {
status?: string[] | null;
};

export type TCycleFiltersByState = {
default: TCycleFilters;
archived: TCycleFilters;
};

export type TCycleStoredFilters = {
display_filters?: TCycleDisplayFilters;
filters?: TCycleFilters;
Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/module/module_filters.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export type TModuleFilters = {
target_date?: string[] | null;
};

export type TModuleFiltersByState = {
default: TModuleFilters;
archived: TModuleFilters;
};

export type TModuleStoredFilters = {
display_filters?: TModuleDisplayFilters;
filters?: TModuleFilters;
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/module/modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface IModule {
unstarted_issues: number;
updated_at: Date;
updated_by: string;
archived_at: string | null;
view_props: {
filters: IIssueFilterOptions;
};
Expand Down
43 changes: 43 additions & 0 deletions web/components/archives/archive-tabs-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FC } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { useRouter } from "next/router";
// constants
import { ARCHIVES_TAB_LIST } from "@/constants/archives";
// hooks
import { useProject } from "@/hooks/store";

export const ArchiveTabsList: FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const activeTab = router.pathname.split("/").pop();
// store hooks
const { getProjectById } = useProject();

// derived values
if (!projectId) return null;
const projectDetails = getProjectById(projectId?.toString());
if (!projectDetails) return null;

return (
<>
{ARCHIVES_TAB_LIST.map(
(tab) =>
tab.shouldRender(projectDetails) && (
<Link key={tab.key} href={`/${workspaceSlug}/projects/${projectId}/archives/${tab.key}`}>
Dismissed Show dismissed Hide dismissed
<span
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 py-3 px-4 text-sm font-medium outline-none ${
tab.key === activeTab
? "border-custom-primary-100 text-custom-primary-100"
: "border-transparent hover:border-custom-border-200 text-custom-text-300 hover:text-custom-text-400"
}`}
>
{tab.label}
</span>
</Link>
)
)}
</>
);
});
1 change: 1 addition & 0 deletions web/components/archives/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./archive-tabs-list";
5 changes: 3 additions & 2 deletions web/components/core/sidebar/links-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ type Props = {
handleDeleteLink: (linkId: string) => void;
handleEditLink: (link: ILinkDetails) => void;
userAuth: UserAuth;
disabled?: boolean;
};

export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth }) => {
export const LinksList: React.FC<Props> = observer(({ links, handleDeleteLink, handleEditLink, userAuth, disabled }) => {
const { getUserDetails } = useMember();
const { isMobile } = usePlatformOS();
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;

const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
Expand Down
123 changes: 123 additions & 0 deletions web/components/cycles/archived-cycles/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { FC, useCallback, useRef, useState } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/router";
// icons
import { ListFilter, Search, X } from "lucide-react";
// types
import type { TCycleFilters } from "@plane/types";
// components
import { ArchiveTabsList } from "@/components/archives";
import { CycleFiltersSelection } from "@/components/cycles";
import { FiltersDropdown } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useCycleFilter } from "@/hooks/store";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";

export const ArchivedCyclesHeader: FC = observer(() => {
// router
const router = useRouter();
const { projectId } = router.query;
// refs
const inputRef = useRef<HTMLInputElement>(null);
// hooks
const { currentProjectArchivedFilters, archivedCyclesSearchQuery, updateFilters, updateArchivedCyclesSearchQuery } =
useCycleFilter();
// states
const [isSearchOpen, setIsSearchOpen] = useState(archivedCyclesSearchQuery !== "" ? true : false);
// outside click detector hook
useOutsideClickDetector(inputRef, () => {
if (isSearchOpen && archivedCyclesSearchQuery.trim() === "") setIsSearchOpen(false);
});

const handleFilters = useCallback(
(key: keyof TCycleFilters, value: string | string[]) => {
if (!projectId) return;

const newValues = currentProjectArchivedFilters?.[key] ?? [];

if (Array.isArray(value))
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
else {
if (currentProjectArchivedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}

updateFilters(projectId.toString(), { [key]: newValues }, "archived");
},
[currentProjectArchivedFilters, projectId, updateFilters]
);

const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Escape") {
if (archivedCyclesSearchQuery && archivedCyclesSearchQuery.trim() !== "") updateArchivedCyclesSearchQuery("");
else {
setIsSearchOpen(false);
inputRef.current?.blur();
}
}
};

return (
<div className="group relative flex border-b border-custom-border-200">
<div className="flex w-full items-center overflow-x-auto px-4 gap-2 horizontal-scrollbar scrollbar-sm">
<ArchiveTabsList />
</div>
{/* filter options */}
<div className="h-full flex items-center gap-3 self-end px-8">
{!isSearchOpen && (
<button
type="button"
className="-mr-5 p-2 hover:bg-custom-background-80 rounded text-custom-text-400 grid place-items-center"
onClick={() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}}
>
<Search className="h-3.5 w-3.5" />
</button>
)}
<div
className={cn(
"ml-auto flex items-center justify-start gap-1 rounded-md border border-transparent bg-custom-background-100 text-custom-text-400 w-0 transition-[width] ease-linear overflow-hidden opacity-0",
{
"w-64 px-2.5 py-1.5 border-custom-border-200 opacity-100": isSearchOpen,
}
)}
>
<Search className="h-3.5 w-3.5" />
<input
ref={inputRef}
className="w-full max-w-[234px] border-none bg-transparent text-sm text-custom-text-100 placeholder:text-custom-text-400 focus:outline-none"
placeholder="Search"
value={archivedCyclesSearchQuery}
onChange={(e) => updateArchivedCyclesSearchQuery(e.target.value)}
onKeyDown={handleInputKeyDown}
/>
{isSearchOpen && (
<button
type="button"
className="grid place-items-center"
onClick={() => {
updateArchivedCyclesSearchQuery("");
setIsSearchOpen(false);
}}
>
<X className="h-3 w-3" />
</button>
)}
</div>
<FiltersDropdown icon={<ListFilter className="h-3 w-3" />} title="Filters" placement="bottom-end">
<CycleFiltersSelection
filters={currentProjectArchivedFilters ?? {}}
handleFiltersUpdate={handleFilters}
isArchived
/>
</FiltersDropdown>
</div>
</div>
);
});
4 changes: 4 additions & 0 deletions web/components/cycles/archived-cycles/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./root";
export * from "./view";
export * from "./header";
export * from "./modal";
Loading
Loading