Skip to content
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
93 changes: 90 additions & 3 deletions web/components/headers/project-archived-issues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,82 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
// helper
import { truncateText } from "helpers/string.helper";
// ui
import { Breadcrumbs, LayersIcon } from "@plane/ui";
import { Breadcrumbs, BreadcrumbItem, LayersIcon } from "@plane/ui";
// icons
import { ArrowLeft } from "lucide-react";
// components
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "components/issues";
// types
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "types";
// helper
import { renderEmoji } from "helpers/emoji.helper";

export const ProjectArchivedIssuesHeader: FC = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;

const { project: projectStore } = useMobxStore();
const { project: projectStore, archivedIssueFilters: archivedIssueFiltersStore } = useMobxStore();

const { currentProjectDetails } = projectStore;

// for archived issues list layout is the only option
const activeLayout = "list";

const handleFiltersUpdate = (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;

const newValues = archivedIssueFiltersStore.userFilters?.[key] ?? [];

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

archivedIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: newValues,
},
});
};

const handleDisplayFiltersUpdate = (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;

archivedIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...archivedIssueFiltersStore.userDisplayFilters,
...updatedDisplayFilter,
},
});
};

const handleDisplayPropertiesUpdate = (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;

archivedIssueFiltersStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
};

return (
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
<div className="block md:hidden">
<button
type="button"
className="grid h-8 w-8 place-items-center rounded border border-custom-border-200"
onClick={() => router.back()}
>
<ArrowLeft fontSize={14} strokeWidth={2} />
</button>
</div>
<div>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
Expand Down Expand Up @@ -46,6 +106,33 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
</Breadcrumbs>
</div>
</div>

{/* filter options */}
<div className="flex items-center gap-2">
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={archivedIssueFiltersStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.archived_issues[activeLayout] : undefined
}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
displayFilters={archivedIssueFiltersStore.userDisplayFilters}
displayProperties={archivedIssueFiltersStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined
}
/>
</FiltersDropdown>
</div>
</div>
);
});
131 changes: 131 additions & 0 deletions web/components/issues/delete-archived-issue-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { useEffect, useState, Fragment } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Dialog, Transition } from "@headlessui/react";
import { AlertTriangle } from "lucide-react";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Button } from "@plane/ui";
// types
import type { IIssue } from "types";

type Props = {
isOpen: boolean;
handleClose: () => void;
data: IIssue;
onSubmit?: () => Promise<void>;
};

export const DeleteArchivedIssueModal: React.FC<Props> = observer((props) => {
const { data, isOpen, handleClose, onSubmit } = props;

const router = useRouter();
const { workspaceSlug } = router.query;

const { setToastAlert } = useToast();

const { archivedIssueDetail: archivedIssueDetailStore } = useMobxStore();

const [isDeleteLoading, setIsDeleteLoading] = useState(false);

useEffect(() => {
setIsDeleteLoading(false);
}, [isOpen]);

const onClose = () => {
setIsDeleteLoading(false);
handleClose();
};

const handleIssueDelete = async () => {
if (!workspaceSlug) return;

setIsDeleteLoading(true);

await archivedIssueDetailStore
.deleteArchivedIssue(workspaceSlug.toString(), data.project, data.id)
.then(() => {
if (onSubmit) onSubmit();
})
.catch((err) => {
const error = err?.detail;
const errorString = Array.isArray(error) ? error[0] : error;

setToastAlert({
title: "Error",
type: "error",
message: errorString || "Something went wrong.",
});
})
.finally(() => {
setIsDeleteLoading(false);
onClose();
});
};

return (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-custom-backdrop bg-opacity-50 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-custom-border-200 bg-custom-background-100 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl">
<div className="flex flex-col gap-6 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="place-items-center rounded-full bg-red-500/20 p-4">
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
</span>
<span className="flex items-center justify-start">
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Archived Issue</h3>
</span>
</div>
<span>
<p className="text-sm text-custom-text-200">
Are you sure you want to delete issue{" "}
<span className="break-words font-medium text-custom-text-100">
{data?.project_detail.identifier}-{data?.sequence_id}
</span>
{""}? All of the data related to the archived issue will be permanently removed. This action
cannot be undone.
</p>
</span>
<div className="flex justify-end gap-2">
<Button variant="neutral-primary" onClick={onClose}>
Cancel
</Button>
<Button variant="danger" onClick={handleIssueDelete} loading={isDeleteLoading}>
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
</Button>
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
});
3 changes: 3 additions & 0 deletions web/components/issues/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ export * from "./confirm-issue-discard";
export * from "./draft-issue-form";
export * from "./draft-issue-modal";
export * from "./delete-draft-issue-modal";

// archived issue
export * from "./delete-archived-issue-modal";
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";

// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { AppliedFiltersList } from "components/issues";
// types
import { IIssueFilterOptions } from "types";

export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

const { archivedIssueFilters: archivedIssueFiltersStore, project: projectStore } = useMobxStore();

const userFilters = archivedIssueFiltersStore.userFilters;

// filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {};
Object.entries(userFilters).forEach(([key, value]) => {
if (!value) return;

if (Array.isArray(value) && value.length === 0) return;

appliedFilters[key as keyof IIssueFilterOptions] = value;
});

const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId) return;

// remove all values of the key if value is null
if (!value) {
archivedIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: null,
},
});
return;
}

// remove the passed value from the key
let newValues = archivedIssueFiltersStore.userFilters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value);

archivedIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: newValues,
},
});
};

const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;

const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});

archivedIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: { ...newFilters },
});
};

// return if no filters are applied
if (Object.keys(appliedFilters).length === 0) return null;

return (
<div className="p-4">
<AppliedFiltersList
appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
states={projectStore.states?.[projectId?.toString() ?? ""]}
/>
</div>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./global-view-root";
export * from "./module-root";
export * from "./project-view-root";
export * from "./project-root";
export * from "./archived-issue";
5 changes: 4 additions & 1 deletion web/components/issues/issue-layouts/list/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ interface IssueBlockProps {
handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void;
quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode;
display_properties: any;
isReadonly?: boolean;
showEmptyGroup?: boolean;
}

export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
const { columnId, issue, handleIssues, quickActions, display_properties, showEmptyGroup } = props;
const { columnId, issue, handleIssues, quickActions, display_properties, showEmptyGroup, isReadonly } = props;

const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => {
handleIssues(group_by, issueToUpdate, "update");
Expand All @@ -37,6 +38,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
workspaceSlug={issue?.workspace_detail?.slug}
projectId={issue?.project_detail?.id}
issueId={issue?.id}
isArchived={issue?.archived_at !== null}
handleIssue={(issueToUpdate) => {
handleIssues(!columnId && columnId === "null" ? null : columnId, issueToUpdate as IIssue, "update");
}}
Expand All @@ -50,6 +52,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = (props) => {
<KanBanProperties
columnId={columnId}
issue={issue}
isReadonly={isReadonly}
handleIssues={updateIssue}
display_properties={display_properties}
showEmptyGroup={showEmptyGroup}
Expand Down
Loading