[WEB-4951] [WEB-4884] feat: work item filters revamp (#7810)
This commit is contained in:
parent
e6a7ca4c72
commit
9aef5d4aa9
160 changed files with 5879 additions and 4881 deletions
|
|
@ -14,19 +14,16 @@ import {
|
|||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
TIssueLayouts,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
|
||||
export const ProfileIssuesMobileHeader = observer(() => {
|
||||
// plane i18n
|
||||
|
|
@ -37,14 +34,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
|||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROFILE);
|
||||
|
||||
const { workspaceLabels } = useLabel();
|
||||
// derived values
|
||||
const states = undefined;
|
||||
// const members = undefined;
|
||||
// const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
// const states = undefined;
|
||||
const members = undefined;
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
|
|
@ -61,31 +51,6 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
|||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: newValues },
|
||||
userId.toString()
|
||||
);
|
||||
},
|
||||
[workspaceSlug, issueFilters, updateFilters, userId]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
|
|
@ -145,32 +110,6 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
|||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<div className="flex flex-center text-sm text-custom-text-200">
|
||||
{t("common.filters")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" strokeWidth={2} />
|
||||
</div>
|
||||
}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
|
||||
}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
states={states}
|
||||
labels={workspaceLabels}
|
||||
memberIds={members}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title={t("common.display")}
|
||||
|
|
@ -184,7 +123,7 @@ export const ProfileIssuesMobileHeader = observer(() => {
|
|||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ChartNoAxesColumn, ListFilter, PanelRight, SlidersHorizontal } from "lucide-react";
|
||||
import { ChartNoAxesColumn, PanelRight, SlidersHorizontal } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
EIssueFilterType,
|
||||
|
|
@ -23,11 +23,10 @@ import {
|
|||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { Breadcrumbs, Button, BreadcrumbNavigationSearchDropdown, Header } from "@plane/ui";
|
||||
import { cn, isIssueFilterActive } from "@plane/utils";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { SwitcherLabel } from "@/components/common/switcher-label";
|
||||
|
|
@ -35,7 +34,6 @@ import { CycleQuickActions } from "@/components/cycles/quick-actions";
|
|||
import {
|
||||
DisplayFiltersSelection,
|
||||
FiltersDropdown,
|
||||
FilterSelection,
|
||||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
|
|
@ -43,10 +41,7 @@ import {
|
|||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
|
|
@ -75,11 +70,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||
const { currentProjectCycleIds, getCycleById } = useCycle();
|
||||
const { toggleCreateIssueModal } = useCommandPalette();
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
|
|
@ -100,27 +90,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, cycleId);
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
|
@ -239,27 +208,6 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
miniIcon={<ListFilter className="size-3.5" />}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown
|
||||
title={t("common.display")}
|
||||
placement="bottom-end"
|
||||
|
|
@ -267,7 +215,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
|
|||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
|
|
|||
|
|
@ -7,48 +7,39 @@ import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
|||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType, IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
|
||||
const SUPPORTED_LAYOUTS = [
|
||||
{ key: "list", titleTranslationKey: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar },
|
||||
];
|
||||
|
||||
export const CycleIssuesMobileHeader = () => {
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { getCycleById } = useCycle();
|
||||
const layouts = [
|
||||
{ key: "list", titleTranslationKey: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", titleTranslationKey: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", titleTranslationKey: "issue.layouts.calendar", icon: Calendar },
|
||||
];
|
||||
|
||||
// router
|
||||
const { workspaceSlug, projectId, cycleId } = useParams();
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
|
||||
// states
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getCycleById } = useCycle();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
|
|
@ -64,37 +55,6 @@ export const CycleIssuesMobileHeader = () => {
|
|||
[workspaceSlug, projectId, cycleId, updateFilters]
|
||||
);
|
||||
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: newValues },
|
||||
cycleId.toString()
|
||||
);
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
|
|
@ -142,7 +102,7 @@ export const CycleIssuesMobileHeader = () => {
|
|||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
{layouts.map((layout, index) => (
|
||||
{SUPPORTED_LAYOUTS.map((layout, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={ISSUE_LAYOUTS[index].key}
|
||||
onClick={() => {
|
||||
|
|
@ -155,34 +115,6 @@ export const CycleIssuesMobileHeader = () => {
|
|||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-custom-text-200 text-sm">
|
||||
{t("common.filters")}
|
||||
<ChevronDown className="text-custom-text-200 h-4 w-4 ml-2" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center border-l border-custom-border-200 items-center text-custom-text-200 text-sm">
|
||||
<FiltersDropdown
|
||||
title={t("common.display")}
|
||||
|
|
@ -196,7 +128,7 @@ export const CycleIssuesMobileHeader = () => {
|
|||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
|
|
|||
|
|
@ -7,28 +7,17 @@ import { ChevronDown } from "lucide-react";
|
|||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { EIssuesStoreType, IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FilterSelection,
|
||||
FiltersDropdown,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
|
||||
export const ProjectIssuesMobileHeader = observer(() => {
|
||||
// i18n
|
||||
|
|
@ -39,16 +28,11 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
|||
projectId: string;
|
||||
};
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT);
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
|
|
@ -59,27 +43,6 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
|||
[workspaceSlug, projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[workspaceSlug, projectId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
|
@ -108,34 +71,6 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
|||
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN, EIssueLayoutTypes.CALENDAR]}
|
||||
onChange={handleLayoutChange}
|
||||
/>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-sm text-custom-text-200">
|
||||
{t("common.filters")}
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title={t("common.display")}
|
||||
|
|
@ -149,7 +84,7 @@ export const ProjectIssuesMobileHeader = observer(() => {
|
|||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// icons
|
||||
import { ChartNoAxesColumn, ListFilter, PanelRight, SlidersHorizontal } from "lucide-react";
|
||||
import { ChartNoAxesColumn, PanelRight, SlidersHorizontal } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
EIssueFilterType,
|
||||
|
|
@ -21,18 +21,16 @@ import {
|
|||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { Breadcrumbs, Button, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
|
||||
import { cn, isIssueFilterActive } from "@plane/utils";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { SwitcherLabel } from "@/components/common/switcher-label";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FiltersDropdown,
|
||||
FilterSelection,
|
||||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
|
|
@ -41,11 +39,8 @@ import { ModuleQuickActions } from "@/components/modules";
|
|||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
|
|
@ -74,21 +69,22 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
const { toggleCreateIssueModal } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { projectLabels } = useLabel();
|
||||
const { projectStates } = useProjectState();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
|
||||
// local storage
|
||||
const { setValue, storedValue } = useLocalStorage("module_sidebar_collapsed", "false");
|
||||
|
||||
// derived values
|
||||
const isSidebarCollapsed = storedValue ? (storedValue === "true" ? true : false) : false;
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
const canUserCreateIssue = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
const workItemsCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setValue(`${!isSidebarCollapsed}`);
|
||||
};
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!projectId) return;
|
||||
|
|
@ -97,27 +93,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
[projectId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(projectId.toString(), EIssueFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[projectId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!projectId) return;
|
||||
|
|
@ -134,15 +109,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
[projectId, updateFilters]
|
||||
);
|
||||
|
||||
// derived values
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
const canUserCreateIssue = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
const workItemsCount = getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
const switcherOptions = projectModuleIds
|
||||
?.map((id) => {
|
||||
const _module = id === moduleId ? moduleDetails : getModuleById(id);
|
||||
|
|
@ -230,27 +196,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
miniIcon={<ListFilter className="size-3.5" />}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
placement="bottom-end"
|
||||
|
|
@ -258,7 +203,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
|
|||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
|
|
|||
|
|
@ -8,53 +8,43 @@ import { Calendar, ChevronDown, Kanban, List } from "lucide-react";
|
|||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType, IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
|
||||
const SUPPORTED_LAYOUTS = [
|
||||
{ key: "list", i18n_title: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", i18n_title: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", i18n_title: "issue.layouts.calendar", icon: Calendar },
|
||||
];
|
||||
|
||||
export const ModuleIssuesMobileHeader = observer(() => {
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getModuleById } = useModule();
|
||||
const { t } = useTranslation();
|
||||
const layouts = [
|
||||
{ key: "list", i18n_title: "issue.layouts.list", icon: List },
|
||||
{ key: "kanban", i18n_title: "issue.layouts.kanban", icon: Kanban },
|
||||
{ key: "calendar", i18n_title: "issue.layouts.calendar", icon: Calendar },
|
||||
];
|
||||
// router
|
||||
const { workspaceSlug, projectId, moduleId } = useParams() as {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
moduleId: string;
|
||||
};
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
|
||||
// states
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { getModuleById } = useModule();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.MODULE);
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
|
|
@ -64,27 +54,6 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
|||
[workspaceSlug, projectId, moduleId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
|
@ -118,7 +87,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
|||
customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm"
|
||||
closeOnSelect
|
||||
>
|
||||
{layouts.map((layout, index) => (
|
||||
{SUPPORTED_LAYOUTS.map((layout, index) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={layout.key}
|
||||
onClick={() => {
|
||||
|
|
@ -131,34 +100,6 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
|||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
menuButton={
|
||||
<span className="flex items-center text-sm text-custom-text-200">
|
||||
Filters
|
||||
<ChevronDown className="ml-2 h-4 w-4 text-custom-text-200" />
|
||||
</span>
|
||||
}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-center border-l border-custom-border-200 text-sm text-custom-text-200">
|
||||
<FiltersDropdown
|
||||
title="Display"
|
||||
|
|
@ -172,7 +113,7 @@ export const ModuleIssuesMobileHeader = observer(() => {
|
|||
>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
|
|
|||
|
|
@ -21,29 +21,19 @@ import {
|
|||
ICustomSearchSelectOption,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { SwitcherIcon, SwitcherLabel } from "@/components/common/switcher-label";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FiltersDropdown,
|
||||
FilterSelection,
|
||||
LayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, LayoutSelection } from "@/components/issues/issue-layouts/filters";
|
||||
// constants
|
||||
import { ViewQuickActions } from "@/components/views/quick-actions";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane web
|
||||
|
|
@ -65,11 +55,6 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { projectViewIds, getViewById } = useProjectView();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
|
|
@ -87,33 +72,6 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||
[workspaceSlug, projectId, viewId, updateFilters]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: newValues },
|
||||
viewId.toString()
|
||||
);
|
||||
},
|
||||
[workspaceSlug, projectId, viewId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
|
|
@ -217,33 +175,10 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
|
|||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
||||
<FiltersDropdown
|
||||
title="Filters"
|
||||
placement="bottom-end"
|
||||
disabled={!canUserCreateIssue}
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
}
|
||||
projectId={projectId.toString()}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title="Display" placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useParams } from "next/navigation";
|
|||
import { DEFAULT_GLOBAL_VIEWS_LIST } from "@plane/constants";
|
||||
// components
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { GlobalViewsAppliedFiltersRoot } from "@/components/issues/issue-layouts/filters";
|
||||
import { AllIssueLayoutRoot } from "@/components/issues/issue-layouts/roots/all-issue-layout-root";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
|
|
@ -29,14 +28,7 @@ const GlobalViewIssuesPage = observer(() => {
|
|||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
{globalViewId && (
|
||||
<GlobalViewsAppliedFiltersRoot globalViewId={globalViewId.toString()} isLoading={isLoading} />
|
||||
)}
|
||||
<AllIssueLayoutRoot isDefaultView={!!defaultView} isLoading={isLoading} toggleLoading={toggleLoading} />
|
||||
</div>
|
||||
</div>
|
||||
<AllIssueLayoutRoot isDefaultView={!!defaultView} isLoading={isLoading} toggleLoading={toggleLoading} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,24 +16,20 @@ import {
|
|||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
ICustomSearchSelectOption,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { Breadcrumbs, Button, Header, BreadcrumbNavigationSearchDropdown } from "@plane/ui";
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||
import { SwitcherLabel } from "@/components/common/switcher-label";
|
||||
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection } from "@/components/issues/issue-layouts/filters";
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { DefaultWorkspaceViewQuickActions } from "@/components/workspace/views/default-view-quick-action";
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace/views/modal";
|
||||
import { WorkspaceViewQuickActions } from "@/components/workspace/views/quick-action";
|
||||
// hooks
|
||||
import { useGlobalView } from "@/hooks/store/use-global-view";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { GlobalViewLayoutSelection } from "@/plane-web/components/views/helper";
|
||||
|
||||
|
|
@ -48,10 +44,6 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
issuesFilter: { filters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||
const { getViewDetailsById, currentWorkspaceViews } = useGlobalView();
|
||||
const { workspaceLabels } = useLabel();
|
||||
const {
|
||||
workspace: { workspaceMemberIds },
|
||||
} = useMember();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const issueFilters = globalViewId ? filters[globalViewId.toString()] : undefined;
|
||||
|
|
@ -59,33 +51,6 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const viewDetails = getViewDetailsById(globalViewId.toString());
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: newValues },
|
||||
globalViewId.toString()
|
||||
);
|
||||
},
|
||||
[workspaceSlug, issueFilters, updateFilters, globalViewId]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
|
|
@ -155,7 +120,7 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
) as ICustomSearchSelectOption[];
|
||||
const currentLayoutFilters = useMemo(() => {
|
||||
const layout = activeLayout ?? EIssueLayoutTypes.SPREADSHEET;
|
||||
return ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues[layout];
|
||||
return ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.layoutOptions[layout];
|
||||
}, [activeLayout]);
|
||||
|
||||
return (
|
||||
|
|
@ -199,21 +164,6 @@ export const GlobalIssuesHeader = observer(() => {
|
|||
selectedLayout={activeLayout ?? EIssueLayoutTypes.SPREADSHEET}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
/>
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={currentLayoutFilters}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
labels={workspaceLabels ?? undefined}
|
||||
memberIds={workspaceMemberIds ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={currentLayoutFilters}
|
||||
|
|
|
|||
16
apps/web/ce/helpers/work-item-filters/project-level.ts
Normal file
16
apps/web/ce/helpers/work-item-filters/project-level.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { TWorkItemFiltersEntityProps } from "@/plane-web/hooks/work-item-filters/use-work-item-filters-config";
|
||||
|
||||
export type TGetAdditionalPropsForProjectLevelFiltersHOCParams = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type TGetAdditionalPropsForProjectLevelFiltersHOC = (
|
||||
params: TGetAdditionalPropsForProjectLevelFiltersHOCParams
|
||||
) => TWorkItemFiltersEntityProps;
|
||||
|
||||
export const getAdditionalProjectLevelFiltersHOCProps: TGetAdditionalPropsForProjectLevelFiltersHOC = ({
|
||||
workspaceSlug,
|
||||
}) => ({
|
||||
workspaceSlug,
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { CORE_OPERATORS, TSupportedOperators } from "@plane/types";
|
||||
|
||||
export type TFiltersOperatorConfigs = {
|
||||
allowedOperators: Set<TSupportedOperators>;
|
||||
allowNegative: boolean;
|
||||
};
|
||||
|
||||
export type TUseFiltersOperatorConfigsProps = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const useFiltersOperatorConfigs = (_props: TUseFiltersOperatorConfigsProps): TFiltersOperatorConfigs => ({
|
||||
allowedOperators: new Set(Object.values(CORE_OPERATORS)),
|
||||
allowNegative: false,
|
||||
});
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
import { useCallback, useMemo } from "react";
|
||||
import {
|
||||
AtSign,
|
||||
Briefcase,
|
||||
CalendarCheck2,
|
||||
CalendarClock,
|
||||
CircleUserRound,
|
||||
SignalHigh,
|
||||
Tag,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
ContrastIcon,
|
||||
CycleGroupIcon,
|
||||
DiceIcon,
|
||||
DoubleCircleIcon,
|
||||
PriorityIcon,
|
||||
StateGroupIcon,
|
||||
} from "@plane/propel/icons";
|
||||
import {
|
||||
ICycle,
|
||||
IState,
|
||||
IUserLite,
|
||||
TFilterConfig,
|
||||
TFilterValue,
|
||||
IIssueLabel,
|
||||
IModule,
|
||||
IProject,
|
||||
TWorkItemFilterProperty,
|
||||
} from "@plane/types";
|
||||
import { Avatar, Logo } from "@plane/ui";
|
||||
import {
|
||||
getAssigneeFilterConfig,
|
||||
getCreatedByFilterConfig,
|
||||
getCycleFilterConfig,
|
||||
getFileURL,
|
||||
getLabelFilterConfig,
|
||||
getMentionFilterConfig,
|
||||
getModuleFilterConfig,
|
||||
getPriorityFilterConfig,
|
||||
getProjectFilterConfig,
|
||||
getStartDateFilterConfig,
|
||||
getStateFilterConfig,
|
||||
getStateGroupFilterConfig,
|
||||
getSubscriberFilterConfig,
|
||||
getTargetDateFilterConfig,
|
||||
} from "@plane/utils";
|
||||
// store hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
// plane web imports
|
||||
import { useFiltersOperatorConfigs } from "@/plane-web/hooks/rich-filters/use-filters-operator-configs";
|
||||
|
||||
export type TWorkItemFiltersEntityProps = {
|
||||
workspaceSlug: string;
|
||||
cycleIds?: string[];
|
||||
labelIds?: string[];
|
||||
memberIds?: string[];
|
||||
moduleIds?: string[];
|
||||
projectId?: string;
|
||||
projectIds?: string[];
|
||||
stateIds?: string[];
|
||||
};
|
||||
|
||||
export type TUseWorkItemFiltersConfigProps = {
|
||||
allowedFilters: TWorkItemFilterProperty[];
|
||||
} & TWorkItemFiltersEntityProps;
|
||||
|
||||
export type TWorkItemFiltersConfig = {
|
||||
configs: TFilterConfig<TWorkItemFilterProperty, TFilterValue>[];
|
||||
configMap: {
|
||||
[key in TWorkItemFilterProperty]?: TFilterConfig<TWorkItemFilterProperty, TFilterValue>;
|
||||
};
|
||||
isFilterEnabled: (key: TWorkItemFilterProperty) => boolean;
|
||||
};
|
||||
|
||||
export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): TWorkItemFiltersConfig => {
|
||||
const { allowedFilters, cycleIds, labelIds, memberIds, moduleIds, projectId, projectIds, stateIds, workspaceSlug } =
|
||||
props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { getCycleById } = useCycle();
|
||||
const { getLabelById } = useLabel();
|
||||
const { getModuleById } = useModule();
|
||||
const { getStateById } = useProjectState();
|
||||
const { getUserDetails } = useMember();
|
||||
// derived values
|
||||
const operatorConfigs = useFiltersOperatorConfigs({ workspaceSlug });
|
||||
const filtersToShow = useMemo(() => new Set(allowedFilters), [allowedFilters]);
|
||||
const project = useMemo(() => getProjectById(projectId), [projectId, getProjectById]);
|
||||
const members: IUserLite[] | undefined = useMemo(
|
||||
() =>
|
||||
memberIds
|
||||
? (memberIds.map((memberId) => getUserDetails(memberId)).filter((member) => member) as IUserLite[])
|
||||
: undefined,
|
||||
[memberIds, getUserDetails]
|
||||
);
|
||||
const workItemStates: IState[] | undefined = useMemo(
|
||||
() =>
|
||||
stateIds ? (stateIds.map((stateId) => getStateById(stateId)).filter((state) => state) as IState[]) : undefined,
|
||||
[stateIds, getStateById]
|
||||
);
|
||||
const workItemLabels: IIssueLabel[] | undefined = useMemo(
|
||||
() =>
|
||||
labelIds
|
||||
? (labelIds.map((labelId) => getLabelById(labelId)).filter((label) => label) as IIssueLabel[])
|
||||
: undefined,
|
||||
[labelIds, getLabelById]
|
||||
);
|
||||
const cycles = useMemo(
|
||||
() => (cycleIds ? (cycleIds.map((cycleId) => getCycleById(cycleId)).filter((cycle) => cycle) as ICycle[]) : []),
|
||||
[cycleIds, getCycleById]
|
||||
);
|
||||
const modules = useMemo(
|
||||
() =>
|
||||
moduleIds ? (moduleIds.map((moduleId) => getModuleById(moduleId)).filter((module) => module) as IModule[]) : [],
|
||||
[moduleIds, getModuleById]
|
||||
);
|
||||
const projects = useMemo(
|
||||
() =>
|
||||
projectIds
|
||||
? (projectIds.map((projectId) => getProjectById(projectId)).filter((project) => project) as IProject[])
|
||||
: [],
|
||||
[projectIds, getProjectById]
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks if a filter is enabled based on the filters to show.
|
||||
* @param key - The filter key.
|
||||
* @param level - The level of the filter.
|
||||
* @returns True if the filter is enabled, false otherwise.
|
||||
*/
|
||||
const isFilterEnabled = useCallback((key: TWorkItemFilterProperty) => filtersToShow.has(key), [filtersToShow]);
|
||||
|
||||
// state group filter config
|
||||
const stateGroupFilterConfig = useMemo(
|
||||
() =>
|
||||
getStateGroupFilterConfig<TWorkItemFilterProperty>("state_group")({
|
||||
isEnabled: isFilterEnabled("state_group"),
|
||||
filterIcon: DoubleCircleIcon,
|
||||
getOptionIcon: (stateGroupKey) => <StateGroupIcon stateGroup={stateGroupKey} />,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, operatorConfigs]
|
||||
);
|
||||
|
||||
// state filter config
|
||||
const stateFilterConfig = useMemo(
|
||||
() =>
|
||||
getStateFilterConfig<TWorkItemFilterProperty>("state_id")({
|
||||
isEnabled: isFilterEnabled("state_id") && workItemStates !== undefined,
|
||||
filterIcon: DoubleCircleIcon,
|
||||
getOptionIcon: (state) => <StateGroupIcon stateGroup={state.group} color={state.color} />,
|
||||
states: workItemStates ?? [],
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, workItemStates, operatorConfigs]
|
||||
);
|
||||
|
||||
// label filter config
|
||||
const labelFilterConfig = useMemo(
|
||||
() =>
|
||||
getLabelFilterConfig<TWorkItemFilterProperty>("label_id")({
|
||||
isEnabled: isFilterEnabled("label_id") && workItemLabels !== undefined,
|
||||
filterIcon: Tag,
|
||||
labels: workItemLabels ?? [],
|
||||
getOptionIcon: (color) => (
|
||||
<span className="flex flex-shrink-0 size-2.5 rounded-full" style={{ backgroundColor: color }} />
|
||||
),
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, workItemLabels, operatorConfigs]
|
||||
);
|
||||
|
||||
// cycle filter config
|
||||
const cycleFilterConfig = useMemo(
|
||||
() =>
|
||||
getCycleFilterConfig<TWorkItemFilterProperty>("cycle_id")({
|
||||
isEnabled: isFilterEnabled("cycle_id") && project?.cycle_view === true && cycles !== undefined,
|
||||
filterIcon: ContrastIcon,
|
||||
getOptionIcon: (cycleGroup) => <CycleGroupIcon cycleGroup={cycleGroup} className="h-3.5 w-3.5 flex-shrink-0" />,
|
||||
cycles: cycles ?? [],
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, project?.cycle_view, cycles, operatorConfigs]
|
||||
);
|
||||
|
||||
// module filter config
|
||||
const moduleFilterConfig = useMemo(
|
||||
() =>
|
||||
getModuleFilterConfig<TWorkItemFilterProperty>("module_id")({
|
||||
isEnabled: isFilterEnabled("module_id") && project?.module_view === true && modules !== undefined,
|
||||
filterIcon: DiceIcon,
|
||||
getOptionIcon: () => <DiceIcon className="h-3 w-3 flex-shrink-0" />,
|
||||
modules: modules ?? [],
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, project?.module_view, modules, operatorConfigs]
|
||||
);
|
||||
|
||||
// assignee filter config
|
||||
const assigneeFilterConfig = useMemo(
|
||||
() =>
|
||||
getAssigneeFilterConfig<TWorkItemFilterProperty>("assignee_id")({
|
||||
isEnabled: isFilterEnabled("assignee_id") && members !== undefined,
|
||||
filterIcon: Users,
|
||||
members: members ?? [],
|
||||
getOptionIcon: (memberDetails) => (
|
||||
<Avatar
|
||||
name={memberDetails.display_name}
|
||||
src={getFileURL(memberDetails.avatar_url)}
|
||||
showTooltip={false}
|
||||
size="sm"
|
||||
/>
|
||||
),
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, members, operatorConfigs]
|
||||
);
|
||||
|
||||
// mention filter config
|
||||
const mentionFilterConfig = useMemo(
|
||||
() =>
|
||||
getMentionFilterConfig<TWorkItemFilterProperty>("mention_id")({
|
||||
isEnabled: isFilterEnabled("mention_id") && members !== undefined,
|
||||
filterIcon: AtSign,
|
||||
members: members ?? [],
|
||||
getOptionIcon: (memberDetails) => (
|
||||
<Avatar
|
||||
name={memberDetails.display_name}
|
||||
src={getFileURL(memberDetails.avatar_url)}
|
||||
showTooltip={false}
|
||||
size="sm"
|
||||
/>
|
||||
),
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, members, operatorConfigs]
|
||||
);
|
||||
|
||||
// created by filter config
|
||||
const createdByFilterConfig = useMemo(
|
||||
() =>
|
||||
getCreatedByFilterConfig<TWorkItemFilterProperty>("created_by_id")({
|
||||
isEnabled: isFilterEnabled("created_by_id") && members !== undefined,
|
||||
filterIcon: CircleUserRound,
|
||||
members: members ?? [],
|
||||
getOptionIcon: (memberDetails) => (
|
||||
<Avatar
|
||||
name={memberDetails.display_name}
|
||||
src={getFileURL(memberDetails.avatar_url)}
|
||||
showTooltip={false}
|
||||
size="sm"
|
||||
/>
|
||||
),
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, members, operatorConfigs]
|
||||
);
|
||||
|
||||
// subscriber filter config
|
||||
const subscriberFilterConfig = useMemo(
|
||||
() =>
|
||||
getSubscriberFilterConfig<TWorkItemFilterProperty>("subscriber_id")({
|
||||
isEnabled: isFilterEnabled("subscriber_id") && members !== undefined,
|
||||
filterIcon: Users,
|
||||
members: members ?? [],
|
||||
getOptionIcon: (memberDetails) => (
|
||||
<Avatar
|
||||
name={memberDetails.display_name}
|
||||
src={getFileURL(memberDetails.avatar_url)}
|
||||
showTooltip={false}
|
||||
size="sm"
|
||||
/>
|
||||
),
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, members, operatorConfigs]
|
||||
);
|
||||
|
||||
// priority filter config
|
||||
const priorityFilterConfig = useMemo(
|
||||
() =>
|
||||
getPriorityFilterConfig<TWorkItemFilterProperty>("priority")({
|
||||
isEnabled: isFilterEnabled("priority"),
|
||||
filterIcon: SignalHigh,
|
||||
getOptionIcon: (priority) => <PriorityIcon priority={priority} />,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, operatorConfigs]
|
||||
);
|
||||
|
||||
// start date filter config
|
||||
const startDateFilterConfig = useMemo(
|
||||
() =>
|
||||
getStartDateFilterConfig<TWorkItemFilterProperty>("start_date")({
|
||||
isEnabled: true,
|
||||
filterIcon: CalendarClock,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[operatorConfigs]
|
||||
);
|
||||
|
||||
// target date filter config
|
||||
const targetDateFilterConfig = useMemo(
|
||||
() =>
|
||||
getTargetDateFilterConfig<TWorkItemFilterProperty>("target_date")({
|
||||
isEnabled: true,
|
||||
filterIcon: CalendarCheck2,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[operatorConfigs]
|
||||
);
|
||||
|
||||
// project filter config
|
||||
const projectFilterConfig = useMemo(
|
||||
() =>
|
||||
getProjectFilterConfig<TWorkItemFilterProperty>("project_id")({
|
||||
isEnabled: isFilterEnabled("project_id") && projects !== undefined,
|
||||
filterIcon: Briefcase,
|
||||
projects: projects,
|
||||
getOptionIcon: (project) => <Logo logo={project.logo_props} size={12} />,
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[isFilterEnabled, projects, operatorConfigs]
|
||||
);
|
||||
|
||||
return {
|
||||
configs: [
|
||||
stateFilterConfig,
|
||||
stateGroupFilterConfig,
|
||||
assigneeFilterConfig,
|
||||
priorityFilterConfig,
|
||||
projectFilterConfig,
|
||||
mentionFilterConfig,
|
||||
labelFilterConfig,
|
||||
cycleFilterConfig,
|
||||
moduleFilterConfig,
|
||||
startDateFilterConfig,
|
||||
targetDateFilterConfig,
|
||||
createdByFilterConfig,
|
||||
subscriberFilterConfig,
|
||||
],
|
||||
configMap: {
|
||||
project_id: projectFilterConfig,
|
||||
state_group: stateGroupFilterConfig,
|
||||
state_id: stateFilterConfig,
|
||||
label_id: labelFilterConfig,
|
||||
cycle_id: cycleFilterConfig,
|
||||
module_id: moduleFilterConfig,
|
||||
assignee_id: assigneeFilterConfig,
|
||||
mention_id: mentionFilterConfig,
|
||||
created_by_id: createdByFilterConfig,
|
||||
subscriber_id: subscriberFilterConfig,
|
||||
priority: priorityFilterConfig,
|
||||
start_date: startDateFilterConfig,
|
||||
target_date: targetDateFilterConfig,
|
||||
},
|
||||
isFilterEnabled,
|
||||
};
|
||||
};
|
||||
|
|
@ -48,7 +48,7 @@ export const ArchiveTabsList: FC = observer(() => {
|
|||
tab.shouldRender(projectDetails) && (
|
||||
<Link key={tab.key} href={`/${workspaceSlug}/projects/${projectId}/archives/${tab.key}`}>
|
||||
<span
|
||||
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 py-3 px-4 text-sm font-medium outline-none ${
|
||||
className={`flex min-w-min flex-shrink-0 whitespace-nowrap border-b-2 py-4 px-4 text-sm font-medium outline-none ${
|
||||
pathname.includes(tab.key)
|
||||
? "border-custom-primary-100 text-custom-primary-100"
|
||||
: "border-transparent hover:border-custom-border-200 text-custom-text-300 hover:text-custom-text-400"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// components
|
||||
import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats";
|
||||
// public
|
||||
import emptyMembers from "@/public/empty-state/empty_members.svg";
|
||||
|
||||
export type TAssigneeData = {
|
||||
id: string | undefined;
|
||||
title: string | undefined;
|
||||
avatar_url: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TAssigneeStatComponent = {
|
||||
selectedAssigneeIds: string[];
|
||||
handleAssigneeFiltersUpdate: (assigneeId: string | undefined) => void;
|
||||
distribution: TAssigneeData;
|
||||
isEditable?: boolean;
|
||||
};
|
||||
|
||||
export const AssigneeStatComponent = observer((props: TAssigneeStatComponent) => {
|
||||
const { distribution, isEditable, selectedAssigneeIds, handleAssigneeFiltersUpdate } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{distribution && distribution.length > 0 ? (
|
||||
distribution.map((assignee, index) => {
|
||||
if (assignee?.id)
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={assignee?.id}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={assignee?.title ?? undefined} src={getFileURL(assignee?.avatar_url ?? "")} />
|
||||
<span>{assignee?.title ?? ""}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee?.completed ?? 0}
|
||||
total={assignee?.total ?? 0}
|
||||
{...(isEditable && {
|
||||
onClick: () => handleAssigneeFiltersUpdate(assignee.id),
|
||||
selected: assignee.id ? selectedAssigneeIds.includes(assignee.id) : false,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`unassigned-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
||||
</div>
|
||||
<span>{t("no_assignee")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee?.completed ?? 0}
|
||||
total={assignee?.total ?? 0}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<Image src={emptyMembers} className="h-12 w-12" alt="empty members" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">{t("no_assignee")}</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// components
|
||||
import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats";
|
||||
// public
|
||||
import emptyLabel from "@/public/empty-state/empty_label.svg";
|
||||
|
||||
export type TLabelData = {
|
||||
id: string | undefined;
|
||||
title: string | undefined;
|
||||
color: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TLabelStatComponent = {
|
||||
selectedLabelIds: string[];
|
||||
handleLabelFiltersUpdate: (labelId: string | undefined) => void;
|
||||
distribution: TLabelData;
|
||||
isEditable?: boolean;
|
||||
};
|
||||
|
||||
export const LabelStatComponent = observer((props: TLabelStatComponent) => {
|
||||
const { distribution, isEditable, selectedLabelIds, handleLabelFiltersUpdate } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{distribution && distribution.length > 0 ? (
|
||||
distribution.map((label, index) => {
|
||||
if (label.id) {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={label.id}
|
||||
title={
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-ellipsis truncate">{label.title ?? t("no_labels_yet")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
total={label.total}
|
||||
{...(isEditable && {
|
||||
onClick: () => handleLabelFiltersUpdate(label.id),
|
||||
selected: label.id ? selectedLabelIds.includes(label.id) : false,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`no-label-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label.title ?? t("no_labels_yet")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
total={label.total}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<Image src={emptyLabel} className="h-12 w-12" alt="empty label" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">{t("no_labels_yet")}</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||
import { TFilterConditionNodeForDisplay, TFilterValue, TWorkItemFilterProperty } from "@plane/types";
|
||||
|
||||
export const PROGRESS_STATS = [
|
||||
{
|
||||
key: "stat-states",
|
||||
i18n_title: "common.states",
|
||||
},
|
||||
{
|
||||
key: "stat-assignees",
|
||||
i18n_title: "common.assignees",
|
||||
},
|
||||
{
|
||||
key: "stat-labels",
|
||||
i18n_title: "common.labels",
|
||||
},
|
||||
];
|
||||
|
||||
type TSelectedFilterProgressStatsType = TFilterConditionNodeForDisplay<TWorkItemFilterProperty, TFilterValue>;
|
||||
|
||||
export type TSelectedFilterProgressStats = {
|
||||
assignees: TSelectedFilterProgressStatsType | undefined;
|
||||
labels: TSelectedFilterProgressStatsType | undefined;
|
||||
stateGroups: TSelectedFilterProgressStatsType | undefined;
|
||||
};
|
||||
|
||||
export const createFilterUpdateHandler =
|
||||
<T extends string>(
|
||||
property: TWorkItemFilterProperty,
|
||||
selectedValues: T[],
|
||||
handleFiltersUpdate: (condition: TWorkItemFilterCondition) => void
|
||||
) =>
|
||||
(value: T | undefined) => {
|
||||
const updatedValues = value ? [...selectedValues] : [];
|
||||
|
||||
if (value) {
|
||||
if (updatedValues.includes(value)) {
|
||||
updatedValues.splice(updatedValues.indexOf(value), 1);
|
||||
} else {
|
||||
updatedValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
handleFiltersUpdate({ property, operator: "in", value: updatedValues });
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import { TStateGroups } from "@plane/types";
|
||||
// components
|
||||
import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats";
|
||||
|
||||
export type TStateGroupData = {
|
||||
state: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TStateGroupStatComponent = {
|
||||
selectedStateGroups: string[];
|
||||
handleStateGroupFiltersUpdate: (stateGroup: string | undefined) => void;
|
||||
distribution: TStateGroupData;
|
||||
totalIssuesCount: number;
|
||||
isEditable?: boolean;
|
||||
};
|
||||
|
||||
export const StateGroupStatComponent = observer((props: TStateGroupStatComponent) => {
|
||||
const { distribution, isEditable, totalIssuesCount, selectedStateGroups, handleStateGroupFiltersUpdate } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{distribution.map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={group.state as TStateGroups} />
|
||||
<span className="text-xs capitalize">{group.state}</span>
|
||||
</div>
|
||||
}
|
||||
completed={group.completed}
|
||||
total={totalIssuesCount}
|
||||
{...(isEditable && {
|
||||
onClick: () => group.state && handleStateGroupFiltersUpdate(group.state),
|
||||
selected: group.state ? selectedStateGroups.includes(group.state) : false,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -18,7 +18,7 @@ export const SingleProgressStats: React.FC<TSingleProgressStatsProps> = ({
|
|||
<div
|
||||
className={`flex w-full items-center justify-between gap-4 rounded-sm p-1 text-xs ${
|
||||
onClick ? "cursor-pointer hover:bg-custom-background-90" : ""
|
||||
} ${selected ? "bg-custom-background-90" : ""}`}
|
||||
} ${selected ? "bg-custom-background-80" : ""}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="w-4/6">{title}</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import { Tab } from "@headlessui/react";
|
|||
import { useTranslation } from "@plane/i18n";
|
||||
import { PriorityIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { EIssuesStoreType, ICycle, IIssueFilterOptions } from "@plane/types";
|
||||
import { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||
import { EIssuesStoreType, ICycle } from "@plane/types";
|
||||
// ui
|
||||
import { Loader, Avatar } from "@plane/ui";
|
||||
import { cn, renderFormattedDate, renderFormattedDateWithoutYear, getFileURL } from "@plane/utils";
|
||||
|
|
@ -35,7 +36,7 @@ export type ActiveCycleStatsProps = {
|
|||
projectId: string;
|
||||
cycle: ICycle | null;
|
||||
cycleId?: string | null;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
|
||||
handleFiltersUpdate: (conditions: TWorkItemFilterCondition[]) => void;
|
||||
cycleIssueDetails: ActiveCycleIssueDetails;
|
||||
};
|
||||
|
||||
|
|
@ -185,7 +186,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
issueId: issue.id,
|
||||
isArchived: !!issue.archived_at,
|
||||
});
|
||||
handleFiltersUpdate("priority", ["urgent", "high"], true);
|
||||
handleFiltersUpdate([
|
||||
{ property: "priority", operator: "in", value: ["urgent", "high"] },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
@ -275,7 +278,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
total={assignee.total_issues}
|
||||
onClick={() => {
|
||||
if (assignee.assignee_id) {
|
||||
handleFiltersUpdate("assignees", [assignee.assignee_id], true);
|
||||
handleFiltersUpdate([
|
||||
{ property: "assignee_id", operator: "in", value: [assignee.assignee_id] },
|
||||
]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -332,11 +337,15 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
|
|||
}
|
||||
completed={label.completed_issues}
|
||||
total={label.total_issues}
|
||||
onClick={() => {
|
||||
if (label.label_id) {
|
||||
handleFiltersUpdate("labels", [label.label_id], true);
|
||||
}
|
||||
}}
|
||||
onClick={
|
||||
label.label_id
|
||||
? () => {
|
||||
if (label.label_id) {
|
||||
handleFiltersUpdate([{ property: "label_id", operator: "in", value: [label.label_id] }]);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -2,30 +2,28 @@
|
|||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane package imports
|
||||
// plane imports
|
||||
import { PROGRESS_STATE_GROUPS_DETAILS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { ICycle, IIssueFilterOptions } from "@plane/types";
|
||||
import { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||
import { ICycle } from "@plane/types";
|
||||
import { LinearProgressIndicator, Loader } from "@plane/ui";
|
||||
// components
|
||||
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
export type ActiveCycleProgressProps = {
|
||||
cycle: ICycle | null;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => void;
|
||||
handleFiltersUpdate: (conditions: TWorkItemFilterCondition[]) => void;
|
||||
};
|
||||
|
||||
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props) => {
|
||||
const { handleFiltersUpdate, cycle } = props;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { groupedProjectStates } = useProjectState();
|
||||
// derived values
|
||||
const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({
|
||||
id: index,
|
||||
|
|
@ -68,10 +66,7 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = observer((props
|
|||
<div
|
||||
className="flex items-center justify-between gap-2 text-sm cursor-pointer"
|
||||
onClick={() => {
|
||||
if (groupedProjectStates) {
|
||||
const states = groupedProjectStates[group].map((state) => state.id);
|
||||
handleFiltersUpdate("state", states, true);
|
||||
}
|
||||
handleFiltersUpdate([{ property: "state_group", operator: "in", value: [group] }]);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { useCallback } from "react";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { useRouter } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { EIssuesStoreType, IIssueFilterOptions } from "@plane/types";
|
||||
// plane imports
|
||||
import { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// constants
|
||||
import { CYCLE_ISSUES_WITH_PARAMS } from "@/constants/fetch-keys";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
|
||||
interface IActiveCycleDetails {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -21,9 +24,10 @@ const useCyclesDetails = (props: IActiveCycleDetails) => {
|
|||
const router = useRouter();
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
issuesFilter: { updateFilterExpression },
|
||||
issues: { getActiveCycleById: getActiveCycleByIdFromIssue, fetchActiveCycleIssues },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { updateFilterExpressionFromConditions } = useWorkItemFilters();
|
||||
|
||||
const { fetchActiveCycleProgress, getCycleById, fetchActiveCycleAnalytics } = useCycle();
|
||||
// derived values
|
||||
|
|
@ -62,29 +66,19 @@ const useCyclesDetails = (props: IActiveCycleDetails) => {
|
|||
const cycleIssueDetails = cycle?.id ? getActiveCycleByIdFromIssue(cycle?.id) : { nextPageResults: false };
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string[], redirect?: boolean) => {
|
||||
async (conditions: TWorkItemFilterCondition[]) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(issueFilters?.filters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
|
||||
let newValues: string[] = [];
|
||||
|
||||
if (isEqual(newValues, value)) newValues = [];
|
||||
else newValues = value;
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{ ...newFilters, [key]: newValues },
|
||||
cycleId.toString()
|
||||
await updateFilterExpressionFromConditions(
|
||||
EIssuesStoreType.CYCLE,
|
||||
cycleId,
|
||||
conditions,
|
||||
updateFilterExpression.bind(updateFilterExpression, workspaceSlug, projectId, cycleId)
|
||||
);
|
||||
if (redirect) router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`);
|
||||
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`);
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters, router]
|
||||
[workspaceSlug, projectId, cycleId, updateFilterExpressionFromConditions, updateFilterExpression, router]
|
||||
);
|
||||
return {
|
||||
cycle,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useCallback, useMemo } from "react";
|
||||
import { FC, useMemo } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { ChevronUp, ChevronDown } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EIssuesStoreType, ICycle, IIssueFilterOptions, TCyclePlotType, TProgressSnapshot } from "@plane/types";
|
||||
import { EIssuesStoreType, ICycle, TCyclePlotType, TProgressSnapshot } from "@plane/types";
|
||||
import { getDate } from "@plane/utils";
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
// plane web components
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
import { SidebarChartRoot } from "@/plane-web/components/cycles";
|
||||
// local imports
|
||||
import { CycleProgressStats } from "./progress-stats";
|
||||
|
|
@ -60,23 +58,23 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
|||
// router
|
||||
const searchParams = useSearchParams();
|
||||
const peekCycle = searchParams.get("peekCycle") || undefined;
|
||||
const { getPlotTypeByCycleId, getEstimateTypeByCycleId, getCycleById } = useCycle();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
// store hooks
|
||||
const { getPlotTypeByCycleId, getEstimateTypeByCycleId, getCycleById } = useCycle();
|
||||
const { getFilter, updateFilterValueFromSidebar } = useWorkItemFilters();
|
||||
// derived values
|
||||
const cycleFilter = getFilter(EIssuesStoreType.CYCLE, cycleId);
|
||||
const selectedAssignees = cycleFilter?.findFirstConditionByPropertyAndOperator("assignee_id", "in");
|
||||
const selectedLabels = cycleFilter?.findFirstConditionByPropertyAndOperator("label_id", "in");
|
||||
const selectedStateGroups = cycleFilter?.findFirstConditionByPropertyAndOperator("state_group", "in");
|
||||
const cycleDetails = validateCycleSnapshot(getCycleById(cycleId));
|
||||
const plotType: TCyclePlotType = getPlotTypeByCycleId(cycleId);
|
||||
const estimateType = getEstimateTypeByCycleId(cycleId);
|
||||
|
||||
const totalIssues = cycleDetails?.total_issues || 0;
|
||||
const totalEstimatePoints = cycleDetails?.total_estimate_points || 0;
|
||||
|
||||
const chartDistributionData =
|
||||
estimateType === "points" ? cycleDetails?.estimate_distribution : cycleDetails?.distribution || undefined;
|
||||
|
||||
const groupedIssues = useMemo(
|
||||
() => ({
|
||||
backlog:
|
||||
|
|
@ -92,44 +90,12 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
|||
}),
|
||||
[estimateType, cycleDetails]
|
||||
);
|
||||
|
||||
const cycleStartDate = getDate(cycleDetails?.start_date);
|
||||
const cycleEndDate = getDate(cycleDetails?.end_date);
|
||||
const isCycleStartDateValid = cycleStartDate && cycleStartDate <= new Date();
|
||||
const isCycleEndDateValid = cycleStartDate && cycleEndDate && cycleEndDate >= cycleStartDate;
|
||||
const isCycleDateValid = isCycleStartDateValid && isCycleEndDateValid;
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (key === "state") {
|
||||
if (isEqual(newValues, value)) newValues = [];
|
||||
else newValues = value;
|
||||
} else {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: newValues },
|
||||
cycleId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, projectId, cycleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
if (!cycleDetails) return <></>;
|
||||
return (
|
||||
<div className="border-t border-custom-border-200 space-y-4 py-5">
|
||||
|
|
@ -159,7 +125,6 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Transition show={open}>
|
||||
<Disclosure.Panel className="flex flex-col divide-y divide-custom-border-200">
|
||||
{cycleStartDate && cycleEndDate ? (
|
||||
|
|
@ -172,16 +137,24 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
|
|||
<div className="w-full py-4">
|
||||
<CycleProgressStats
|
||||
cycleId={cycleId}
|
||||
plotType={plotType}
|
||||
distribution={chartDistributionData}
|
||||
groupedIssues={groupedIssues}
|
||||
totalIssuesCount={estimateType === "points" ? totalEstimatePoints || 0 : totalIssues || 0}
|
||||
isEditable={Boolean(!peekCycle)}
|
||||
size="xs"
|
||||
roundedTab={false}
|
||||
handleFiltersUpdate={updateFilterValueFromSidebar.bind(
|
||||
updateFilterValueFromSidebar,
|
||||
EIssuesStoreType.CYCLE,
|
||||
cycleId
|
||||
)}
|
||||
isEditable={Boolean(!peekCycle) && cycleFilter !== undefined}
|
||||
noBackground={false}
|
||||
filters={issueFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
plotType={plotType}
|
||||
roundedTab={false}
|
||||
selectedFilters={{
|
||||
assignees: selectedAssignees,
|
||||
labels: selectedLabels,
|
||||
stateGroups: selectedStateGroups,
|
||||
}}
|
||||
size="xs"
|
||||
totalIssuesCount={estimateType === "points" ? totalEstimatePoints || 0 : totalIssues || 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,280 +2,67 @@
|
|||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
IIssueFilters,
|
||||
TCycleDistribution,
|
||||
TCycleEstimateDistribution,
|
||||
TCyclePlotType,
|
||||
TStateGroups,
|
||||
} from "@plane/types";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { cn, getFileURL } from "@plane/utils";
|
||||
import { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||
import { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types";
|
||||
import { cn, toFilterArray } from "@plane/utils";
|
||||
// components
|
||||
import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats";
|
||||
import { AssigneeStatComponent, TAssigneeData } from "@/components/core/sidebar/progress-stats/assignee";
|
||||
import { LabelStatComponent, TLabelData } from "@/components/core/sidebar/progress-stats/label";
|
||||
import {
|
||||
createFilterUpdateHandler,
|
||||
PROGRESS_STATS,
|
||||
TSelectedFilterProgressStats,
|
||||
} from "@/components/core/sidebar/progress-stats/shared";
|
||||
import { StateGroupStatComponent, TStateGroupData } from "@/components/core/sidebar/progress-stats/state_group";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// public
|
||||
import emptyLabel from "@/public/empty-state/empty_label.svg";
|
||||
import emptyMembers from "@/public/empty-state/empty_members.svg";
|
||||
|
||||
// assignee types
|
||||
type TAssigneeData = {
|
||||
id: string | undefined;
|
||||
title: string | undefined;
|
||||
avatar_url: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TAssigneeStatComponent = {
|
||||
distribution: TAssigneeData;
|
||||
isEditable?: boolean;
|
||||
filters?: IIssueFilters | undefined;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
// labelTypes
|
||||
type TLabelData = {
|
||||
id: string | undefined;
|
||||
title: string | undefined;
|
||||
color: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TLabelStatComponent = {
|
||||
distribution: TLabelData;
|
||||
isEditable?: boolean;
|
||||
filters?: IIssueFilters | undefined;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
// stateTypes
|
||||
type TStateData = {
|
||||
state: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TStateStatComponent = {
|
||||
distribution: TStateData;
|
||||
totalIssuesCount: number;
|
||||
isEditable?: boolean;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
export const AssigneeStatComponent = observer((props: TAssigneeStatComponent) => {
|
||||
const { distribution, isEditable, filters, handleFiltersUpdate } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{distribution && distribution.length > 0 ? (
|
||||
distribution.map((assignee, index) => {
|
||||
if (assignee?.id)
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={assignee?.id}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={assignee?.title ?? undefined} src={getFileURL(assignee?.avatar_url ?? "")} />
|
||||
<span>{assignee?.title ?? ""}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee?.completed ?? 0}
|
||||
total={assignee?.total ?? 0}
|
||||
{...(isEditable && {
|
||||
onClick: () => handleFiltersUpdate("assignees", assignee.id ?? ""),
|
||||
selected: filters?.filters?.assignees?.includes(assignee.id ?? ""),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`unassigned-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
||||
</div>
|
||||
<span>{t("no_assignee")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee?.completed ?? 0}
|
||||
total={assignee?.total ?? 0}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<Image src={emptyMembers} className="h-12 w-12" alt="empty members" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">{t("no_assignee")}</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const LabelStatComponent = observer((props: TLabelStatComponent) => {
|
||||
const { distribution, isEditable, filters, handleFiltersUpdate } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{distribution && distribution.length > 0 ? (
|
||||
distribution.map((label, index) => {
|
||||
if (label.id) {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={label.id}
|
||||
title={
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-ellipsis truncate">{label.title ?? t("no_labels_yet")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
total={label.total}
|
||||
{...(isEditable && {
|
||||
onClick: () => handleFiltersUpdate("labels", label.id ?? ""),
|
||||
selected: filters?.filters?.labels?.includes(label.id ?? `no-label-${index}`),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`no-label-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label.title ?? t("no_labels_yet")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
total={label.total}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<Image src={emptyLabel} className="h-12 w-12" alt="empty label" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">{t("no_labels_yet")}</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const StateStatComponent = observer((props: TStateStatComponent) => {
|
||||
const { distribution, isEditable, totalIssuesCount, handleFiltersUpdate } = props;
|
||||
// hooks
|
||||
const { groupedProjectStates } = useProjectState();
|
||||
// derived values
|
||||
const getStateGroupState = (stateGroup: string) => {
|
||||
const stateGroupStates = groupedProjectStates?.[stateGroup];
|
||||
const stateGroupStatesId = stateGroupStates?.map((state) => state.id);
|
||||
return stateGroupStatesId;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{distribution.map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={group.state as TStateGroups} />
|
||||
<span className="text-xs capitalize">{group.state}</span>
|
||||
</div>
|
||||
}
|
||||
completed={group.completed}
|
||||
total={totalIssuesCount}
|
||||
{...(isEditable && {
|
||||
onClick: () => group.state && handleFiltersUpdate("state", getStateGroupState(group.state) ?? []),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const progressStats = [
|
||||
{
|
||||
key: "stat-states",
|
||||
i18n_title: "common.states",
|
||||
},
|
||||
{
|
||||
key: "stat-assignees",
|
||||
i18n_title: "common.assignees",
|
||||
},
|
||||
{
|
||||
key: "stat-labels",
|
||||
i18n_title: "common.labels",
|
||||
},
|
||||
];
|
||||
|
||||
type TCycleProgressStats = {
|
||||
cycleId: string;
|
||||
plotType: TCyclePlotType;
|
||||
distribution: TCycleDistribution | TCycleEstimateDistribution | undefined;
|
||||
groupedIssues: Record<string, number>;
|
||||
totalIssuesCount: number;
|
||||
handleFiltersUpdate: (condition: TWorkItemFilterCondition) => void;
|
||||
isEditable?: boolean;
|
||||
filters?: IIssueFilters | undefined;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
size?: "xs" | "sm";
|
||||
roundedTab?: boolean;
|
||||
noBackground?: boolean;
|
||||
plotType: TCyclePlotType;
|
||||
roundedTab?: boolean;
|
||||
selectedFilters: TSelectedFilterProgressStats;
|
||||
size?: "xs" | "sm";
|
||||
totalIssuesCount: number;
|
||||
};
|
||||
|
||||
export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
|
||||
const {
|
||||
cycleId,
|
||||
plotType,
|
||||
distribution,
|
||||
groupedIssues,
|
||||
totalIssuesCount,
|
||||
isEditable = false,
|
||||
filters,
|
||||
handleFiltersUpdate,
|
||||
size = "sm",
|
||||
roundedTab = false,
|
||||
isEditable = false,
|
||||
noBackground = false,
|
||||
plotType,
|
||||
roundedTab = false,
|
||||
selectedFilters,
|
||||
size = "sm",
|
||||
totalIssuesCount,
|
||||
} = props;
|
||||
// hooks
|
||||
// plane imports
|
||||
const { t } = useTranslation();
|
||||
// store imports
|
||||
const { storedValue: currentTab, setValue: setCycleTab } = useLocalStorage(
|
||||
`cycle-analytics-tab-${cycleId}`,
|
||||
"stat-assignees"
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
// derived values
|
||||
const currentTabIndex = (tab: string): number => progressStats.findIndex((stat) => stat.key === tab);
|
||||
|
||||
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
|
||||
const currentDistribution = distribution as TCycleDistribution;
|
||||
const currentEstimateDistribution = distribution as TCycleEstimateDistribution;
|
||||
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
|
||||
const selectedLabelIds = toFilterArray(selectedFilters?.labels?.value || []) as string[];
|
||||
const selectedStateGroups = toFilterArray(selectedFilters?.stateGroups?.value || []) as string[];
|
||||
|
||||
const distributionAssigneeData: TAssigneeData =
|
||||
plotType === "burndown"
|
||||
|
|
@ -311,12 +98,24 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
|
|||
total: label.total_estimates,
|
||||
}));
|
||||
|
||||
const distributionStateData: TStateData = Object.keys(groupedIssues || {}).map((state) => ({
|
||||
const distributionStateData: TStateGroupData = Object.keys(groupedIssues || {}).map((state) => ({
|
||||
state: state,
|
||||
completed: groupedIssues?.[state] || 0,
|
||||
total: totalIssuesCount || 0,
|
||||
}));
|
||||
|
||||
const handleAssigneeFiltersUpdate = createFilterUpdateHandler(
|
||||
"assignee_id",
|
||||
selectedAssigneeIds,
|
||||
handleFiltersUpdate
|
||||
);
|
||||
const handleLabelFiltersUpdate = createFilterUpdateHandler("label_id", selectedLabelIds, handleFiltersUpdate);
|
||||
const handleStateGroupFiltersUpdate = createFilterUpdateHandler(
|
||||
"state_group",
|
||||
selectedStateGroups,
|
||||
handleFiltersUpdate
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
|
||||
|
|
@ -329,7 +128,7 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
|
|||
size === "xs" ? `text-xs` : `text-sm`
|
||||
)}
|
||||
>
|
||||
{progressStats.map((stat) => (
|
||||
{PROGRESS_STATS.map((stat) => (
|
||||
<Tab
|
||||
className={cn(
|
||||
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
|
||||
|
|
@ -347,27 +146,28 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
|
|||
</Tab.List>
|
||||
<Tab.Panels className="py-3 text-custom-text-200">
|
||||
<Tab.Panel key={"stat-states"}>
|
||||
<StateStatComponent
|
||||
<StateGroupStatComponent
|
||||
distribution={distributionStateData}
|
||||
totalIssuesCount={totalIssuesCount}
|
||||
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
|
||||
isEditable={isEditable}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
selectedStateGroups={selectedStateGroups}
|
||||
totalIssuesCount={totalIssuesCount}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel key={"stat-assignees"}>
|
||||
<AssigneeStatComponent
|
||||
distribution={distributionAssigneeData}
|
||||
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
|
||||
isEditable={isEditable}
|
||||
filters={filters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
selectedAssigneeIds={selectedAssigneeIds}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel key={"stat-labels"}>
|
||||
<LabelStatComponent
|
||||
distribution={distributionLabelData}
|
||||
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
|
||||
isEditable={isEditable}
|
||||
filters={filters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
selectedLabelIds={selectedLabelIds}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,17 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane constants
|
||||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
import { EHeaderVariant, Header } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import { ArchiveTabsList } from "@/components/archives";
|
||||
import { DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
|
||||
export const ArchivedIssuesHeader: FC = observer(() => {
|
||||
// router
|
||||
|
|
@ -32,34 +21,10 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
|||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// for archived issues list layout is the only option
|
||||
const activeLayout = "list";
|
||||
// hooks
|
||||
const handleFiltersUpdate = (key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisplayFiltersUpdate = (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
|
@ -77,42 +42,25 @@ export const ArchivedIssuesHeader: FC = observer(() => {
|
|||
};
|
||||
|
||||
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">
|
||||
<Header variant={EHeaderVariant.SECONDARY}>
|
||||
<Header.LeftItem>
|
||||
<ArchiveTabsList />
|
||||
</div>
|
||||
{/* filter options */}
|
||||
<div className="flex items-center gap-2 px-8">
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters || {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.archived_issues[activeLayout] : undefined
|
||||
}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem className="items-center">
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
displayFilters={issueFilters?.displayFilters || {}}
|
||||
displayProperties={issueFilters?.displayProperties || {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined}
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.archived_issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,35 +2,21 @@
|
|||
|
||||
import { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChartNoAxesColumn, ListFilter, SlidersHorizontal } from "lucide-react";
|
||||
// plane constants
|
||||
import { ChartNoAxesColumn, SlidersHorizontal } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_STORE_TO_FILTERS_MAP } from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
|
||||
import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
// plane web types
|
||||
// plane web imports
|
||||
import { TProject } from "@/plane-web/types";
|
||||
// local imports
|
||||
import { WorkItemsModal } from "../analytics/work-items/modal";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FiltersDropdown,
|
||||
FilterSelection,
|
||||
LayoutSelection,
|
||||
MobileLayoutSelection,
|
||||
} from "./issue-layouts/filters";
|
||||
|
|
@ -63,38 +49,13 @@ export const HeaderFilters = observer((props: Props) => {
|
|||
// states
|
||||
const [analyticsModal, setAnalyticsModal] = useState(false);
|
||||
// store hooks
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(storeType);
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
const layoutDisplayFiltersOptions = ISSUE_STORE_TO_FILTERS_MAP[storeType]?.[activeLayout];
|
||||
const layoutDisplayFiltersOptions = ISSUE_STORE_TO_FILTERS_MAP[storeType]?.layoutOptions[activeLayout];
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
|
||||
},
|
||||
[workspaceSlug, projectId, issueFilters, updateFilters]
|
||||
);
|
||||
const handleLayoutChange = useCallback(
|
||||
(layout: EIssueLayoutTypes) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
|
@ -141,27 +102,6 @@ export const HeaderFilters = observer((props: Props) => {
|
|||
activeLayout={activeLayout}
|
||||
/>
|
||||
</div>
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
miniIcon={<ListFilter className="size-3.5" />}
|
||||
>
|
||||
<FilterSelection
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
|
||||
labels={projectLabels}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
projectId={projectId}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
isEpic={storeType === EIssuesStoreType.EPIC}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
<FiltersDropdown
|
||||
miniIcon={<SlidersHorizontal className="size-3.5" />}
|
||||
title={t("common.display")}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
FiltersDropdown,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
import { isDisplayFiltersApplied } from "@/components/issues/issue-layouts/utils";
|
||||
|
||||
type TSubIssueDisplayFiltersProps = {
|
||||
displayProperties: IIssueDisplayProperties;
|
||||
displayFilters: IIssueDisplayFilterOptions;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { FC, useMemo, useState } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
import { ListFilter, Search, X } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IIssueFilterOptions, ILayoutDisplayFiltersOptions, IState } from "@plane/types";
|
||||
import { IIssueFilterOptions, IState } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
import {
|
||||
FilterAssignees,
|
||||
|
|
@ -16,26 +16,24 @@ import {
|
|||
} from "@/components/issues/issue-layouts/filters";
|
||||
import { isFiltersApplied } from "@/components/issues/issue-layouts/utils";
|
||||
import { FilterIssueTypes } from "@/plane-web/components/issues/filters/issue-types";
|
||||
|
||||
type TSubIssueFiltersProps = {
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
filters: IIssueFilterOptions;
|
||||
memberIds: string[] | undefined;
|
||||
states?: IState[];
|
||||
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
|
||||
availableFilters: (keyof IIssueFilterOptions)[];
|
||||
};
|
||||
|
||||
export const SubIssueFilters: FC<TSubIssueFiltersProps> = observer((props) => {
|
||||
const { handleFiltersUpdate, filters, memberIds, states, layoutDisplayFiltersOptions } = props;
|
||||
|
||||
const { handleFiltersUpdate, filters, memberIds, states, availableFilters } = props;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// states
|
||||
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||
|
||||
const isFilterEnabled = (filter: keyof IIssueFilterOptions) =>
|
||||
!!layoutDisplayFiltersOptions?.filters.includes(filter);
|
||||
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => !!availableFilters.includes(filter);
|
||||
|
||||
const isFilterApplied = useMemo(() => isFiltersApplied(filters), [filters]);
|
||||
// hooks
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export * from "./title";
|
|||
export * from "./root";
|
||||
export * from "./quick-action-button";
|
||||
export * from "./display-filters";
|
||||
export * from "./content";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { FC, useCallback } from "react";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { observer } from "mobx-react";
|
||||
import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import {
|
||||
EIssueFilterType,
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE,
|
||||
SUB_WORK_ITEM_AVAILABLE_FILTERS_FOR_WORK_ITEM_PAGE,
|
||||
} from "@plane/constants";
|
||||
import {
|
||||
EIssueServiceType,
|
||||
IIssueDisplayFilterOptions,
|
||||
|
|
@ -38,11 +42,10 @@ export const SubWorkItemTitleActions: FC<TSubWorkItemTitleActionsProps> = observ
|
|||
} = useMember();
|
||||
|
||||
// derived values
|
||||
const subIssueFilters = getSubIssueFilters(parentId);
|
||||
const projectStates = getProjectStates(projectId);
|
||||
const projectMemberIds = getProjectMemberIds(projectId, false);
|
||||
|
||||
const layoutDisplayFiltersOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE["sub_work_items"].list;
|
||||
const subIssueFilters = getSubIssueFilters(parentId);
|
||||
const layoutDisplayFiltersOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE["sub_work_items"].layoutOptions.list;
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
|
|
@ -72,7 +75,6 @@ export const SubWorkItemTitleActions: FC<TSubWorkItemTitleActionsProps> = observ
|
|||
if (subIssueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateSubWorkItemFilters(EIssueFilterType.FILTERS, { [key]: newValues }, parentId);
|
||||
},
|
||||
[subIssueFilters?.filters, updateSubWorkItemFilters, parentId]
|
||||
|
|
@ -100,7 +102,7 @@ export const SubWorkItemTitleActions: FC<TSubWorkItemTitleActionsProps> = observ
|
|||
filters={subIssueFilters?.filters ?? {}}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
|
||||
availableFilters={SUB_WORK_ITEM_AVAILABLE_FILTERS_FOR_WORK_ITEM_PAGE}
|
||||
/>
|
||||
{!disabled && (
|
||||
<SubIssuesActionButton issueId={parentId} disabled={disabled} issueServiceType={issueServiceType} />
|
||||
|
|
|
|||
|
|
@ -5,20 +5,17 @@ import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|||
import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element";
|
||||
import { observer } from "mobx-react";
|
||||
// plane constants
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
// types
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
TGroupedIssues,
|
||||
TIssue,
|
||||
TIssueKanbanFilters,
|
||||
TIssueMap,
|
||||
TPaginationData,
|
||||
ICalendarWeek,
|
||||
EIssueLayoutTypes,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
|
|
@ -71,8 +68,8 @@ type Props = {
|
|||
readOnly?: boolean;
|
||||
updateFilters?: (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => Promise<void>;
|
||||
canEditProperties: (projectId: string | undefined) => boolean;
|
||||
isEpic?: boolean;
|
||||
|
|
|
|||
|
|
@ -9,15 +9,9 @@ import { Popover, Transition } from "@headlessui/react";
|
|||
// hooks
|
||||
// ui
|
||||
// icons
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { EIssueFilterType, TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
TCalendarLayouts,
|
||||
TIssueKanbanFilters,
|
||||
} from "@plane/types";
|
||||
import { TCalendarLayouts, TSupportedFilterForUpdate } from "@plane/types";
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
// types
|
||||
// constants
|
||||
|
|
@ -39,8 +33,8 @@ interface ICalendarHeader {
|
|||
| IProjectEpicsFilter;
|
||||
updateFilters?: (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,9 @@ import { observer } from "mobx-react";
|
|||
|
||||
// components
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
TIssueKanbanFilters,
|
||||
} from "@plane/types";
|
||||
import { TSupportedFilterForUpdate } from "@plane/types";
|
||||
import { Row } from "@plane/ui";
|
||||
// icons
|
||||
import { useCalendarView } from "@/hooks/store/use-calendar-view";
|
||||
|
|
@ -29,8 +24,8 @@ interface ICalendarHeader {
|
|||
| IProjectEpicsFilter;
|
||||
updateFilters?: (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => Promise<void>;
|
||||
setSelectedDate: (date: Date) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,35 @@
|
|||
import size from "lodash/size";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueFilterType, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EIssuesStoreType, EUserProjectRoles, IIssueFilterOptions } from "@plane/types";
|
||||
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
|
||||
// components
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
export const ProjectArchivedEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const archivedWorkItemFilter = projectId
|
||||
? useWorkItemFilterInstance(EIssuesStoreType.ARCHIVED, projectId)
|
||||
: undefined;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
)
|
||||
);
|
||||
const additionalPath = issueFilterCount > 0 ? (activeLayout ?? "list") : undefined;
|
||||
const additionalPath = archivedWorkItemFilter?.hasActiveFilters ? (activeLayout ?? "list") : undefined;
|
||||
const canPerformEmptyStateActions = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
|
|
@ -43,27 +42,16 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => {
|
|||
basePath: "/empty-state/archived/empty-issues",
|
||||
});
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
|
||||
...newFilters,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
{issueFilterCount > 0 ? (
|
||||
{archivedWorkItemFilter?.hasActiveFilters ? (
|
||||
<DetailedEmptyState
|
||||
title={t("project_issues.empty_state.issues_empty_filter.title")}
|
||||
assetPath={emptyFilterResolvedPath}
|
||||
secondaryButton={{
|
||||
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
|
||||
onClick: handleClearAllFilters,
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
onClick: archivedWorkItemFilter?.clearFilters,
|
||||
disabled: !canPerformEmptyStateActions || !archivedWorkItemFilter,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import size from "lodash/size";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueFilterType, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EIssuesStoreType, EUserProjectRoles, IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
|
||||
import { EIssuesStoreType, EUserProjectRoles, ISearchIssueResponse } from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal";
|
||||
|
|
@ -18,11 +17,15 @@ import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
|||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
export const CycleEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, cycleId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, cycleId: routerCycleId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
const cycleId = routerCycleId ? routerCycleId.toString() : undefined;
|
||||
// states
|
||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||
// plane hooks
|
||||
|
|
@ -33,16 +36,10 @@ export const CycleEmptyState: React.FC = observer(() => {
|
|||
const { toggleCreateIssueModal } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const cycleWorkItemFilter = cycleId ? useWorkItemFilterInstance(EIssuesStoreType.CYCLE, cycleId) : undefined;
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
)
|
||||
);
|
||||
const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {});
|
||||
const isEmptyFilters = issueFilterCount > 0;
|
||||
const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status?.toLowerCase() === "completed";
|
||||
const additionalPath = activeLayout ?? "list";
|
||||
const canPerformEmptyStateActions = allowPermissions(
|
||||
|
|
@ -84,23 +81,6 @@ export const CycleEmptyState: React.FC = observer(() => {
|
|||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
issuesFilter.updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
...newFilters,
|
||||
},
|
||||
cycleId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ExistingIssuesListModal
|
||||
|
|
@ -118,14 +98,14 @@ export const CycleEmptyState: React.FC = observer(() => {
|
|||
description={t("project_cycles.empty_state.completed_no_issues.description")}
|
||||
assetPath={completedNoIssuesResolvedPath}
|
||||
/>
|
||||
) : isEmptyFilters ? (
|
||||
) : cycleWorkItemFilter?.hasActiveFilters ? (
|
||||
<DetailedEmptyState
|
||||
title={t("project_issues.empty_state.issues_empty_filter.title")}
|
||||
assetPath={emptyFilterResolvedPath}
|
||||
secondaryButton={{
|
||||
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
|
||||
onClick: handleClearAllFilters,
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
onClick: cycleWorkItemFilter?.clearFilters,
|
||||
disabled: !canPerformEmptyStateActions || !cycleWorkItemFilter,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// plane web components
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { TeamEmptyState, TeamViewEmptyState } from "@/plane-web/components/issues/issue-layouts/empty-states";
|
||||
import { TeamEmptyState } from "@/plane-web/components/issues/issue-layouts/empty-states/team-issues";
|
||||
import { TeamProjectWorkItemEmptyState } from "@/plane-web/components/issues/issue-layouts/empty-states/team-project";
|
||||
import { TeamViewEmptyState } from "@/plane-web/components/issues/issue-layouts/empty-states/team-view-issues";
|
||||
// components
|
||||
import { ProjectArchivedEmptyState } from "./archived-issues";
|
||||
import { CycleEmptyState } from "./cycle";
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import size from "lodash/size";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueFilterType, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EIssuesStoreType, EUserProjectRoles, IIssueFilterOptions, ISearchIssueResponse } from "@plane/types";
|
||||
import { EIssuesStoreType, EUserProjectRoles, ISearchIssueResponse } from "@plane/types";
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ExistingIssuesListModal } from "@/components/core/modals/existing-issues-list-modal";
|
||||
|
|
@ -17,11 +16,15 @@ import { captureClick } from "@/helpers/event-tracker.helper";
|
|||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
export const ModuleEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, moduleId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, moduleId: routerModuleId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
const moduleId = routerModuleId ? routerModuleId.toString() : undefined;
|
||||
// states
|
||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||
// plane hooks
|
||||
|
|
@ -31,14 +34,8 @@ export const ModuleEmptyState: React.FC = observer(() => {
|
|||
const { toggleCreateIssueModal } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const moduleWorkItemFilter = moduleId ? useWorkItemFilterInstance(EIssuesStoreType.MODULE, moduleId) : undefined;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
)
|
||||
);
|
||||
const isEmptyFilters = issueFilterCount > 0;
|
||||
const additionalPath = activeLayout ?? "list";
|
||||
const canPerformEmptyStateActions = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
|
|
@ -75,23 +72,6 @@ export const ModuleEmptyState: React.FC = observer(() => {
|
|||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||
});
|
||||
issuesFilter.updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
...newFilters,
|
||||
},
|
||||
moduleId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<ExistingIssuesListModal
|
||||
|
|
@ -103,14 +83,14 @@ export const ModuleEmptyState: React.FC = observer(() => {
|
|||
handleOnSubmit={handleAddIssuesToModule}
|
||||
/>
|
||||
<div className="grid h-full w-full place-items-center">
|
||||
{isEmptyFilters ? (
|
||||
{moduleWorkItemFilter?.hasActiveFilters ? (
|
||||
<DetailedEmptyState
|
||||
title={t("project_issues.empty_state.issues_empty_filter.title")}
|
||||
assetPath={emptyFilterResolvedPath}
|
||||
secondaryButton={{
|
||||
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
|
||||
onClick: handleClearAllFilters,
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
onClick: moduleWorkItemFilter?.clearFilters,
|
||||
disabled: !canPerformEmptyStateActions || !moduleWorkItemFilter,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import size from "lodash/size";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueFilterType, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EIssuesStoreType, EUserProjectRoles, IIssueFilterOptions } from "@plane/types";
|
||||
import { EIssuesStoreType, EUserProjectRoles } from "@plane/types";
|
||||
// components
|
||||
import { ComicBoxButton } from "@/components/empty-state/comic-box-button";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
|
|
@ -13,11 +12,13 @@ import { captureClick } from "@/helpers/event-tracker.helper";
|
|||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useWorkItemFilterInstance } from "@/hooks/store/work-item-filters/use-work-item-filter-instance";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
export const ProjectEmptyState: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const { projectId: routerProjectId } = useParams();
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
// plane imports
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
|
|
@ -25,14 +26,9 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
|||
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
// derived values
|
||||
const userFilters = issuesFilter?.issueFilters?.filters;
|
||||
const projectWorkItemFilter = projectId ? useWorkItemFilterInstance(EIssuesStoreType.PROJECT, projectId) : undefined;
|
||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||
const issueFilterCount = size(
|
||||
Object.fromEntries(
|
||||
Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0)
|
||||
)
|
||||
);
|
||||
const additionalPath = issueFilterCount > 0 ? (activeLayout ?? "list") : undefined;
|
||||
const additionalPath = projectWorkItemFilter?.hasActiveFilters ? (activeLayout ?? "list") : undefined;
|
||||
const canPerformEmptyStateActions = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
|
|
@ -45,27 +41,16 @@ export const ProjectEmptyState: React.FC = observer(() => {
|
|||
basePath: "/empty-state/onboarding/issues",
|
||||
});
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
|
||||
...newFilters,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
{issueFilterCount > 0 ? (
|
||||
{projectWorkItemFilter?.hasActiveFilters ? (
|
||||
<DetailedEmptyState
|
||||
title={t("project_issues.empty_state.issues_empty_filter.title")}
|
||||
assetPath={emptyFilterResolvedPath}
|
||||
secondaryButton={{
|
||||
text: t("project_issues.empty_state.issues_empty_filter.secondary_button.text"),
|
||||
onClick: handleClearAllFilters,
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
onClick: projectWorkItemFilter?.clearFilters,
|
||||
disabled: !canPerformEmptyStateActions || !projectWorkItemFilter,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,166 +0,0 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { IIssueFilterOptions, IIssueLabel, IState } from "@plane/types";
|
||||
import { Tag } from "@plane/ui";
|
||||
import { replaceUnderscoreIfSnakeCase } from "@plane/utils";
|
||||
// components
|
||||
import {
|
||||
AppliedCycleFilters,
|
||||
AppliedDateFilters,
|
||||
AppliedLabelsFilters,
|
||||
AppliedMembersFilters,
|
||||
AppliedModuleFilters,
|
||||
AppliedPriorityFilters,
|
||||
AppliedProjectFilters,
|
||||
AppliedStateFilters,
|
||||
AppliedStateGroupFilters,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane web components
|
||||
import { AppliedIssueTypeFilters } from "@/plane-web/components/issues/filters/applied-filters/issue-types";
|
||||
|
||||
type Props = {
|
||||
appliedFilters: IIssueFilterOptions;
|
||||
handleClearAllFilters: () => void;
|
||||
handleRemoveFilter: (key: keyof IIssueFilterOptions, value: string | null) => void;
|
||||
labels?: IIssueLabel[] | undefined;
|
||||
states?: IState[] | undefined;
|
||||
alwaysAllowEditing?: boolean;
|
||||
disableEditing?: boolean;
|
||||
};
|
||||
|
||||
const membersFilters = ["assignees", "mentions", "created_by", "subscriber"];
|
||||
const dateFilters = ["start_date", "target_date"];
|
||||
|
||||
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
appliedFilters,
|
||||
handleClearAllFilters,
|
||||
handleRemoveFilter,
|
||||
labels,
|
||||
states,
|
||||
alwaysAllowEditing,
|
||||
disableEditing = false,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!appliedFilters) return null;
|
||||
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
const isEditingAllowed =
|
||||
!disableEditing &&
|
||||
(alwaysAllowEditing ||
|
||||
allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT));
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate my-auto">
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
const filterKey = key as keyof IIssueFilterOptions;
|
||||
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
return (
|
||||
<Tag key={filterKey}>
|
||||
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
|
||||
{membersFilters.includes(filterKey) && (
|
||||
<AppliedMembersFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{dateFilters.includes(filterKey) && (
|
||||
<AppliedDateFilters handleRemove={(val) => handleRemoveFilter(filterKey, val)} values={value} />
|
||||
)}
|
||||
{filterKey === "labels" && (
|
||||
<AppliedLabelsFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("labels", val)}
|
||||
labels={labels}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "priority" && (
|
||||
<AppliedPriorityFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("priority", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "state" && states && (
|
||||
<AppliedStateFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("state", val)}
|
||||
states={states}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "state_group" && (
|
||||
<AppliedStateGroupFilters handleRemove={(val) => handleRemoveFilter("state_group", val)} values={value} />
|
||||
)}
|
||||
{filterKey === "project" && (
|
||||
<AppliedProjectFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("project", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "cycle" && (
|
||||
<AppliedCycleFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("cycle", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "module" && (
|
||||
<AppliedModuleFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("module", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "issue_type" && (
|
||||
<AppliedIssueTypeFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("issue_type", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{filterKey === "team_project" && (
|
||||
<AppliedProjectFilters
|
||||
editable={isEditingAllowed}
|
||||
handleRemove={(val) => handleRemoveFilter("team_project", val)}
|
||||
values={value}
|
||||
/>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemoveFilter(filterKey, null)}
|
||||
>
|
||||
<X size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
{isEditingAllowed && (
|
||||
<button type="button" onClick={handleClearAllFilters}>
|
||||
<Tag>
|
||||
{t("common.clear_all")}
|
||||
<X size={12} strokeWidth={2} />
|
||||
</Tag>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
export * from "./roots";
|
||||
export * from "./date";
|
||||
export * from "./filters-list";
|
||||
export * from "./label";
|
||||
export * from "./members";
|
||||
export * from "./priority";
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { EIssuesStoreType, IIssueFilterOptions } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
// local imports
|
||||
import { AppliedFiltersList } from "../filters-list";
|
||||
|
||||
export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
|
||||
// store hooks
|
||||
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
const { projectLabels } = useLabel();
|
||||
const { projectStates } = useProjectState();
|
||||
// derived values
|
||||
const userFilters = issueFilters?.filters;
|
||||
// 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) {
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the passed value from the key
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
|
||||
...newFilters,
|
||||
});
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between p-4 gap-2.5">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueFilterType, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EIssuesStoreType, IIssueFilterOptions } from "@plane/types";
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
// local imports
|
||||
import { SaveFilterView } from "../../../save-filter-view";
|
||||
import { AppliedFiltersList } from "../filters-list";
|
||||
|
||||
export const CycleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, cycleId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
|
||||
const { projectLabels } = useLabel();
|
||||
const { projectStates } = useProjectState();
|
||||
// derived values
|
||||
const userFilters = issueFilters?.filters;
|
||||
// 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 || !cycleId) return;
|
||||
if (!value) {
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: null,
|
||||
},
|
||||
cycleId.toString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
cycleId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{ ...newFilters },
|
||||
cycleId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0 || !workspaceSlug || !projectId || !cycleId) return null;
|
||||
|
||||
return (
|
||||
<Header variant={EHeaderVariant.TERNARY}>
|
||||
<Header.LeftItem>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
</Header.LeftItem>
|
||||
<SaveFilterView
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
filterParams={{
|
||||
filters: { ...appliedFilters, cycle: [cycleId?.toString()] },
|
||||
display_filters: issueFilters?.displayFilters,
|
||||
display_properties: issueFilters?.displayProperties,
|
||||
}}
|
||||
trackerElement={PROJECT_VIEW_TRACKER_ELEMENTS.CYCLE_HEADER_SAVE_AS_VIEW_BUTTON}
|
||||
/>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// Plane imports
|
||||
import {
|
||||
DEFAULT_GLOBAL_VIEWS_LIST,
|
||||
EIssueFilterType,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
GLOBAL_VIEW_TRACKER_ELEMENTS,
|
||||
GLOBAL_VIEW_TRACKER_EVENTS,
|
||||
} from "@plane/constants";
|
||||
import { EIssuesStoreType, EViewAccess, IIssueFilterOptions, TStaticViewTypes } from "@plane/types";
|
||||
import { Header, EHeaderVariant, Loader } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace/views/modal";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useGlobalView } from "@/hooks/store/use-global-view";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
import { getAreFiltersEqual } from "../../../utils";
|
||||
import { AppliedFiltersList } from "../filters-list";
|
||||
|
||||
type Props = {
|
||||
globalViewId: string;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
|
||||
const { globalViewId, isLoading = false } = props;
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { filters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||
const { workspaceLabels } = useLabel();
|
||||
const { globalViewMap, updateGlobalView } = useGlobalView();
|
||||
const { data } = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// derived values
|
||||
const issueFilters = filters?.[globalViewId];
|
||||
const userFilters = issueFilters?.filters;
|
||||
const viewDetails = globalViewMap[globalViewId];
|
||||
|
||||
// filters whose value not null or empty array
|
||||
let appliedFilters: IIssueFilterOptions | undefined = undefined;
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
if (!appliedFilters) appliedFilters = {};
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
|
||||
if (!value) {
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: null },
|
||||
globalViewId.toString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = userFilters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: newValues },
|
||||
globalViewId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
{ ...newFilters },
|
||||
globalViewId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
const viewFilters = {
|
||||
filters: cloneDeep(appliedFilters ?? {}),
|
||||
display_filters: cloneDeep(issueFilters?.displayFilters),
|
||||
display_properties: cloneDeep(issueFilters?.displayProperties),
|
||||
};
|
||||
const handleUpdateView = () => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
|
||||
updateGlobalView(workspaceSlug.toString(), globalViewId.toString(), viewFilters)
|
||||
.then((res) => {
|
||||
if (res)
|
||||
captureSuccess({
|
||||
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
|
||||
payload: {
|
||||
view_id: globalViewId,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
captureError({
|
||||
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
|
||||
payload: {
|
||||
view_id: globalViewId,
|
||||
},
|
||||
error: error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// add a placeholder object instead of appliedFilters if it is undefined
|
||||
const areFiltersEqual = getAreFiltersEqual(appliedFilters ?? {}, issueFilters, viewDetails);
|
||||
|
||||
const isAuthorizedUser = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
const isDefaultView = DEFAULT_GLOBAL_VIEWS_LIST.map((view) => view.key).includes(globalViewId as TStaticViewTypes);
|
||||
|
||||
const isLocked = viewDetails?.is_locked;
|
||||
const isOwner = viewDetails?.owned_by === data?.id;
|
||||
const areAppliedFiltersEmpty = isEmpty(appliedFilters);
|
||||
|
||||
// return if no filters are applied
|
||||
|
||||
if (areAppliedFiltersEmpty && areFiltersEqual) return null;
|
||||
|
||||
return (
|
||||
<Header
|
||||
variant={EHeaderVariant.TERNARY}
|
||||
className={cn({
|
||||
"justify-end": areAppliedFiltersEmpty,
|
||||
})}
|
||||
>
|
||||
<CreateUpdateWorkspaceViewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
preLoadedData={{
|
||||
name: `${viewDetails?.name} 2`,
|
||||
description: viewDetails?.description,
|
||||
access: viewDetails?.access ?? EViewAccess.PUBLIC,
|
||||
...viewFilters,
|
||||
}}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<Loader className="flex flex-wrap items-stretch gap-2 bg-custom-background-100 truncate my-auto">
|
||||
<Loader.Item height="36px" width="150px" />
|
||||
<Loader.Item height="36px" width="100px" />
|
||||
<Loader.Item height="36px" width="300px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<AppliedFiltersList
|
||||
labels={workspaceLabels ?? undefined}
|
||||
appliedFilters={appliedFilters ?? {}}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
disableEditing={isLocked}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isDefaultView ? (
|
||||
<UpdateViewComponent
|
||||
isLocked={isLocked}
|
||||
areFiltersEqual={!!areFiltersEqual}
|
||||
isOwner={isOwner}
|
||||
isAuthorizedUser={isAuthorizedUser}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
handleUpdateView={handleUpdateView}
|
||||
trackerElement={GLOBAL_VIEW_TRACKER_ELEMENTS.HEADER_SAVE_VIEW_BUTTON}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export * from "./cycle-root";
|
||||
export * from "./global-view-root";
|
||||
export * from "./module-root";
|
||||
export * from "./project-view-root";
|
||||
export * from "./project-root";
|
||||
export * from "./archived-issue";
|
||||
export * from "./profile-issues-root";
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueFilterType, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EIssuesStoreType, IIssueFilterOptions } from "@plane/types";
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { SaveFilterView } from "../../../save-filter-view";
|
||||
import { AppliedFiltersList } from "../filters-list";
|
||||
|
||||
export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, moduleId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.MODULE);
|
||||
const { projectLabels } = useLabel();
|
||||
const { projectStates } = useProjectState();
|
||||
// derived values
|
||||
const userFilters = issueFilters?.filters;
|
||||
// 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 || !moduleId) return;
|
||||
if (!value) {
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: null,
|
||||
},
|
||||
moduleId.toString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
moduleId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{ ...newFilters },
|
||||
moduleId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (!workspaceSlug || !projectId || !moduleId || Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<Header variant={EHeaderVariant.TERNARY}>
|
||||
<Header.LeftItem>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
</Header.LeftItem>
|
||||
<SaveFilterView
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
filterParams={{
|
||||
filters: { ...appliedFilters, module: [moduleId.toString()] },
|
||||
display_filters: issueFilters?.displayFilters,
|
||||
display_properties: issueFilters?.displayProperties,
|
||||
}}
|
||||
trackerElement={PROJECT_VIEW_TRACKER_ELEMENTS.MODULE_HEADER_SAVE_AS_VIEW_BUTTON}
|
||||
/>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { EIssuesStoreType, IIssueFilterOptions } from "@plane/types";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
import { AppliedFiltersList } from "../filters-list";
|
||||
|
||||
export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, userId } = useParams();
|
||||
//swr hook for fetching issue properties
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROFILE);
|
||||
|
||||
const { workspaceLabels } = useLabel();
|
||||
// derived values
|
||||
const userFilters = issueFilters?.filters;
|
||||
|
||||
// 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 || !userId) return;
|
||||
if (!value) {
|
||||
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { [key]: null }, userId.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
userId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
updateFilters(workspaceSlug.toString(), undefined, EIssueFilterType.FILTERS, { ...newFilters }, userId.toString());
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="p-4 flex-shrink-0">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={workspaceLabels ?? []}
|
||||
states={[]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import {
|
||||
EIssueFilterType,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
PROJECT_VIEW_TRACKER_ELEMENTS,
|
||||
} from "@plane/constants";
|
||||
import { EIssuesStoreType, IIssueFilterOptions } from "@plane/types";
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// local imports
|
||||
import { SaveFilterView } from "../../../save-filter-view";
|
||||
import { AppliedFiltersList } from "../filters-list";
|
||||
|
||||
type TProjectAppliedFiltersRootProps = {
|
||||
storeType?: EIssuesStoreType.PROJECT | EIssuesStoreType.EPIC;
|
||||
};
|
||||
|
||||
export const ProjectAppliedFiltersRoot: React.FC<TProjectAppliedFiltersRootProps> = observer((props) => {
|
||||
const { storeType = EIssuesStoreType.PROJECT } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(storeType);
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const { projectStates } = useProjectState();
|
||||
// derived values
|
||||
const isEditingAllowed = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
const userFilters = issueFilters?.filters;
|
||||
// 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;
|
||||
if (!value) {
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters });
|
||||
};
|
||||
|
||||
// return if no filters are applied
|
||||
if (Object.keys(appliedFilters).length === 0) return null;
|
||||
|
||||
return (
|
||||
<Header variant={EHeaderVariant.TERNARY}>
|
||||
<Header.LeftItem>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
{isEditingAllowed && storeType === EIssuesStoreType.PROJECT && (
|
||||
<SaveFilterView
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
projectId={projectId?.toString()}
|
||||
filterParams={{
|
||||
filters: appliedFilters,
|
||||
display_filters: issueFilters?.displayFilters,
|
||||
display_properties: issueFilters?.displayProperties,
|
||||
}}
|
||||
trackerElement={PROJECT_VIEW_TRACKER_ELEMENTS.PROJECT_HEADER_SAVE_AS_VIEW_BUTTON}
|
||||
/>
|
||||
)}
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// types
|
||||
import {
|
||||
EIssueFilterType,
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
PROJECT_VIEW_TRACKER_ELEMENTS,
|
||||
} from "@plane/constants";
|
||||
import { EIssuesStoreType, EViewAccess, IIssueFilterOptions } from "@plane/types";
|
||||
// components
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
import { CreateUpdateProjectViewModal } from "@/components/views/modal";
|
||||
import { UpdateViewComponent } from "@/components/views/update-view-component";
|
||||
// constants
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
import { getAreFiltersEqual } from "../../../utils";
|
||||
import { AppliedFiltersList } from "../filters-list";
|
||||
|
||||
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, viewId } = useParams();
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
const { projectLabels } = useLabel();
|
||||
const { projectStates } = useProjectState();
|
||||
const { viewMap, updateView } = useProjectView();
|
||||
const { data } = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
// derived values
|
||||
const viewDetails = viewId ? viewMap[viewId.toString()] : null;
|
||||
const userFilters = issueFilters?.filters;
|
||||
// filters whose value not null or empty array
|
||||
let appliedFilters: IIssueFilterOptions | undefined = undefined;
|
||||
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
if (!appliedFilters) appliedFilters = {};
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
if (!value) {
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: null,
|
||||
},
|
||||
viewId.toString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{
|
||||
[key]: newValues,
|
||||
},
|
||||
viewId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearAllFilters = () => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(userFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{ ...newFilters },
|
||||
viewId.toString()
|
||||
);
|
||||
};
|
||||
|
||||
// add a placeholder object instead of appliedFilters if it is undefined
|
||||
const areFiltersEqual = getAreFiltersEqual(appliedFilters ?? {}, issueFilters, viewDetails);
|
||||
const viewFilters = {
|
||||
filters: cloneDeep(appliedFilters ?? {}),
|
||||
display_filters: cloneDeep(issueFilters?.displayFilters),
|
||||
display_properties: cloneDeep(issueFilters?.displayProperties),
|
||||
};
|
||||
// return if no filters are applied
|
||||
if (isEmpty(appliedFilters) && areFiltersEqual) return null;
|
||||
|
||||
const handleUpdateView = () => {
|
||||
if (!workspaceSlug || !projectId || !viewId || !viewDetails) return;
|
||||
|
||||
updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), viewFilters);
|
||||
};
|
||||
|
||||
const isAuthorizedUser = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
const isLocked = !!viewDetails?.is_locked;
|
||||
const isOwner = viewDetails?.owned_by === data?.id;
|
||||
|
||||
return (
|
||||
<Header variant={EHeaderVariant.TERNARY}>
|
||||
<CreateUpdateProjectViewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
preLoadedData={{
|
||||
name: `${viewDetails?.name} 2`,
|
||||
description: viewDetails?.description,
|
||||
logo_props: viewDetails?.logo_props,
|
||||
access: viewDetails?.access ?? EViewAccess.PUBLIC,
|
||||
...viewFilters,
|
||||
}}
|
||||
/>
|
||||
<Header.LeftItem className="w-[70%]">
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters ?? {}}
|
||||
handleClearAllFilters={handleClearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
disableEditing={isLocked}
|
||||
/>
|
||||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
<UpdateViewComponent
|
||||
isLocked={isLocked}
|
||||
areFiltersEqual={!!areFiltersEqual}
|
||||
isOwner={isOwner}
|
||||
isAuthorizedUser={isAuthorizedUser}
|
||||
setIsModalOpen={setIsModalOpen}
|
||||
handleUpdateView={handleUpdateView}
|
||||
trackerElement={PROJECT_VIEW_TRACKER_ELEMENTS.HEADER_SAVE_VIEW_BUTTON}
|
||||
/>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import { IState } from "@plane/types";
|
||||
// types
|
||||
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,5 @@ export * from "./display-filters-selection";
|
|||
export * from "./display-properties";
|
||||
export * from "./extra-options";
|
||||
export * from "./group-by";
|
||||
export * from "./issue-grouping";
|
||||
export * from "./order-by";
|
||||
export * from "./sub-group-by";
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TIssueGroupingFilters } from "@plane/types";
|
||||
// components
|
||||
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
|
||||
// constants
|
||||
|
||||
type Props = {
|
||||
selectedIssueType: TIssueGroupingFilters | undefined;
|
||||
handleUpdate: (val: TIssueGroupingFilters) => void;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
|
||||
const ISSUE_FILTER_OPTIONS: {
|
||||
key: TIssueGroupingFilters;
|
||||
title: string;
|
||||
}[] = [
|
||||
{ key: null, title: "All" },
|
||||
{ key: "active", title: "Active" },
|
||||
{ key: "backlog", title: "Backlog" },
|
||||
];
|
||||
|
||||
export const FilterIssueGrouping: React.FC<Props> = observer((props) => {
|
||||
const { selectedIssueType, handleUpdate, isEpic = false } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||
|
||||
const activeIssueType = selectedIssueType ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterHeader
|
||||
title={`${isEpic ? "Epic" : "Work item"} Grouping`}
|
||||
isPreviewEnabled={previewEnabled}
|
||||
handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)}
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div>
|
||||
{ISSUE_FILTER_OPTIONS.map((issueType) => (
|
||||
<FilterOption
|
||||
key={issueType?.key}
|
||||
isChecked={activeIssueType === issueType?.key ? true : false}
|
||||
onClick={() => handleUpdate(issueType?.key)}
|
||||
title={`${issueType.title} ${isEpic ? "Epics" : "Work items"}`}
|
||||
multiple={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,289 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Search, X } from "lucide-react";
|
||||
// plane imports
|
||||
import { EUserPermissions } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueFilterOptions,
|
||||
IIssueLabel,
|
||||
ILayoutDisplayFiltersOptions,
|
||||
IState,
|
||||
} from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
FilterAssignees,
|
||||
FilterMentions,
|
||||
FilterCreatedBy,
|
||||
FilterDueDate,
|
||||
FilterLabels,
|
||||
FilterPriority,
|
||||
FilterProjects,
|
||||
FilterStartDate,
|
||||
FilterState,
|
||||
FilterStateGroup,
|
||||
FilterCycle,
|
||||
FilterModule,
|
||||
FilterIssueGrouping,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web imports
|
||||
import { FilterIssueTypes } from "@/plane-web/components/issues/filters/issue-types";
|
||||
import { FilterTeamProjects } from "@/plane-web/components/issues/filters/team-project";
|
||||
|
||||
type Props = {
|
||||
filters: IIssueFilterOptions;
|
||||
displayFilters?: IIssueDisplayFilterOptions | undefined;
|
||||
handleDisplayFiltersUpdate?: (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => void;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
|
||||
projectId?: string;
|
||||
labels?: IIssueLabel[] | undefined;
|
||||
memberIds?: string[] | undefined;
|
||||
states?: IState[] | undefined;
|
||||
cycleViewDisabled?: boolean;
|
||||
moduleViewDisabled?: boolean;
|
||||
isEpic?: boolean;
|
||||
};
|
||||
|
||||
export const FilterSelection: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
filters,
|
||||
displayFilters,
|
||||
handleDisplayFiltersUpdate,
|
||||
handleFiltersUpdate,
|
||||
layoutDisplayFiltersOptions,
|
||||
projectId,
|
||||
labels,
|
||||
memberIds,
|
||||
states,
|
||||
cycleViewDisabled = false,
|
||||
moduleViewDisabled = false,
|
||||
isEpic = false,
|
||||
} = props;
|
||||
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { moduleId, cycleId } = useParams();
|
||||
const {
|
||||
project: { getProjectMemberDetails },
|
||||
} = useMember();
|
||||
// states
|
||||
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
|
||||
|
||||
// filter guests from assignees
|
||||
const assigneeIds = memberIds?.filter((id) => {
|
||||
if (projectId) {
|
||||
const memeberDetails = getProjectMemberDetails(id, projectId);
|
||||
const isGuest = (memeberDetails?.role || EUserPermissions.GUEST) === EUserPermissions.GUEST;
|
||||
if (isGuest && memeberDetails) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const isFilterEnabled = (filter: keyof IIssueFilterOptions) => layoutDisplayFiltersOptions?.filters.includes(filter);
|
||||
|
||||
const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) =>
|
||||
Object.keys(layoutDisplayFiltersOptions?.display_filters ?? {}).includes(displayFilter);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="bg-custom-background-100 p-2.5 pb-0">
|
||||
<div className="flex items-center gap-1.5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 px-1.5 py-1 text-xs">
|
||||
<Search className="text-custom-text-400" size={12} strokeWidth={2} />
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-custom-background-90 outline-none placeholder:text-custom-text-400"
|
||||
placeholder={t("common.search.label")}
|
||||
value={filtersSearchQuery}
|
||||
onChange={(e) => setFiltersSearchQuery(e.target.value)}
|
||||
autoFocus={!isMobile}
|
||||
/>
|
||||
{filtersSearchQuery !== "" && (
|
||||
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>
|
||||
<X className="text-custom-text-300" size={12} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="vertical-scrollbar scrollbar-sm h-full w-full divide-y divide-custom-border-200 overflow-y-auto px-2.5">
|
||||
{/* priority */}
|
||||
{isFilterEnabled("priority") && (
|
||||
<div className="py-2">
|
||||
<FilterPriority
|
||||
appliedFilters={filters.priority ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("priority", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* state group */}
|
||||
{isFilterEnabled("state_group") && (
|
||||
<div className="py-2">
|
||||
<FilterStateGroup
|
||||
appliedFilters={filters.state_group ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("state_group", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* state */}
|
||||
{isFilterEnabled("state") && (
|
||||
<div className="py-2">
|
||||
<FilterState
|
||||
appliedFilters={filters.state ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("state", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
states={states}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* issue type */}
|
||||
{isFilterEnabled("issue_type") && (
|
||||
<FilterIssueTypes
|
||||
appliedFilters={filters.issue_type ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("issue_type", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* assignees */}
|
||||
{isFilterEnabled("assignees") && (
|
||||
<div className="py-2">
|
||||
<FilterAssignees
|
||||
appliedFilters={filters.assignees ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("assignees", val)}
|
||||
memberIds={assigneeIds}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* cycle */}
|
||||
{isFilterEnabled("cycle") && !cycleId && !cycleViewDisabled && (
|
||||
<div className="py-2">
|
||||
<FilterCycle
|
||||
appliedFilters={filters.cycle ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("cycle", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* module */}
|
||||
{isFilterEnabled("module") && !moduleId && !moduleViewDisabled && (
|
||||
<div className="py-2">
|
||||
<FilterModule
|
||||
appliedFilters={filters.module ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("module", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* assignees */}
|
||||
{isFilterEnabled("mentions") && (
|
||||
<div className="py-2">
|
||||
<FilterMentions
|
||||
appliedFilters={filters.mentions ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("mentions", val)}
|
||||
memberIds={memberIds}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* created_by */}
|
||||
{isFilterEnabled("created_by") && (
|
||||
<div className="py-2">
|
||||
<FilterCreatedBy
|
||||
appliedFilters={filters.created_by ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("created_by", val)}
|
||||
memberIds={memberIds}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* labels */}
|
||||
{isFilterEnabled("labels") && (
|
||||
<div className="py-2">
|
||||
<FilterLabels
|
||||
appliedFilters={filters.labels ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("labels", val)}
|
||||
labels={labels}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* project */}
|
||||
{isFilterEnabled("project") && (
|
||||
<div className="py-2">
|
||||
<FilterProjects
|
||||
appliedFilters={filters.project ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("project", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* team project */}
|
||||
{isFilterEnabled("team_project") && (
|
||||
<div className="py-2">
|
||||
<FilterTeamProjects
|
||||
appliedFilters={filters.team_project ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("team_project", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* issue type */}
|
||||
{isDisplayFilterEnabled("type") && displayFilters && handleDisplayFiltersUpdate && (
|
||||
<div className="py-2">
|
||||
<FilterIssueGrouping
|
||||
selectedIssueType={displayFilters.type}
|
||||
handleUpdate={(val) =>
|
||||
handleDisplayFiltersUpdate({
|
||||
type: val,
|
||||
})
|
||||
}
|
||||
isEpic={isEpic}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* start_date */}
|
||||
{isFilterEnabled("start_date") && (
|
||||
<div className="py-2">
|
||||
<FilterStartDate
|
||||
appliedFilters={filters.start_date ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("start_date", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* target_date */}
|
||||
{isFilterEnabled("target_date") && (
|
||||
<div className="py-2">
|
||||
<FilterDueDate
|
||||
appliedFilters={filters.target_date ?? null}
|
||||
handleUpdate={(val) => handleFiltersUpdate("target_date", val)}
|
||||
searchQuery={filtersSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -2,7 +2,6 @@ export * from "./assignee";
|
|||
export * from "./mentions";
|
||||
export * from "./created-by";
|
||||
export * from "./due-date";
|
||||
export * from "./filters-selection";
|
||||
export * from "./labels";
|
||||
export * from "./priority";
|
||||
export * from "./project";
|
||||
|
|
|
|||
|
|
@ -116,7 +116,9 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
|
|||
>
|
||||
<div className="inline-block line-clamp-1 truncate font-medium text-custom-text-100">{title}</div>
|
||||
<div className="pl-2 text-sm font-medium text-custom-text-300">{count || 0}</div>
|
||||
<WorkFlowGroupTree groupBy={groupBy} groupId={groupID} />
|
||||
<div className="px-2.5">
|
||||
<WorkFlowGroupTree groupBy={groupBy} groupId={groupID} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!disableIssueCreation &&
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { useProject } from "@/hooks/store/use-project";
|
|||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane-web components
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal";
|
||||
// helper
|
||||
import { ArchiveIssueModal } from "../../archive-issue-modal";
|
||||
import { DeleteIssueModal } from "../../delete-issue-modal";
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { useProject } from "@/hooks/store/use-project";
|
|||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane-web components
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal";
|
||||
// helper
|
||||
import { ArchiveIssueModal } from "../../archive-issue-modal";
|
||||
import { DeleteIssueModal } from "../../delete-issue-modal";
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import { useIssues } from "@/hooks/store/use-issues";
|
|||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
// plane-web components
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
// plane-web imports
|
||||
import { DuplicateWorkItemModal } from "@/plane-web/components/issues/issue-layouts/quick-action-dropdowns/duplicate-modal";
|
||||
// helper
|
||||
import { ArchiveIssueModal } from "../../archive-issue-modal";
|
||||
import { DeleteIssueModal } from "../../delete-issue-modal";
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { isEmpty } from "lodash";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { GLOBAL_VIEW_TRACKER_ELEMENTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import { EmptyState } from "@/components/common/empty-state";
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
import { WorkspaceActiveLayout } from "@/components/views/helper";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
// hooks
|
||||
import { useGlobalView } from "@/hooks/store/use-global-view";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
// store
|
||||
// public imports
|
||||
import emptyView from "@/public/empty-state/view.svg";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -24,61 +28,44 @@ type Props = {
|
|||
|
||||
export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
|
||||
const { isDefaultView, isLoading = false, toggleLoading } = props;
|
||||
|
||||
// Router hooks
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug, globalViewId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, globalViewId: routerGlobalViewId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const globalViewId = routerGlobalViewId ? routerGlobalViewId.toString() : undefined;
|
||||
// search params
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Store hooks
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { fetchFilters, updateFilters },
|
||||
issuesFilter: { filters, fetchFilters, updateFilterExpression },
|
||||
issues: { clear, groupedIssueIds, fetchIssues, fetchNextIssues },
|
||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||
const { fetchAllGlobalViews, getViewDetailsById } = useGlobalView();
|
||||
// Derived values
|
||||
const viewDetails = globalViewId ? getViewDetailsById(globalViewId) : undefined;
|
||||
const workItemFilters = globalViewId ? filters?.[globalViewId] : undefined;
|
||||
const activeLayout: EIssueLayoutTypes | undefined = workItemFilters?.displayFilters?.layout;
|
||||
const initialWorkItemFilters = viewDetails
|
||||
? {
|
||||
displayFilters: workItemFilters?.displayFilters,
|
||||
displayProperties: workItemFilters?.displayProperties,
|
||||
kanbanFilters: workItemFilters?.kanbanFilters,
|
||||
richFilters: viewDetails?.rich_filters ?? {},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Custom hooks
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
|
||||
// Derived values
|
||||
const viewDetails = getViewDetailsById(globalViewId?.toString());
|
||||
const activeLayout: EIssueLayoutTypes | undefined = EIssueLayoutTypes.SPREADSHEET;
|
||||
|
||||
// Route filters
|
||||
const routeFilters: { [key: string]: string } = {};
|
||||
searchParams.forEach((value: string, key: string) => {
|
||||
routeFilters[key] = value;
|
||||
});
|
||||
|
||||
// Apply route filters to store
|
||||
const routerFilterParams = () => {
|
||||
if (
|
||||
workspaceSlug &&
|
||||
globalViewId &&
|
||||
["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString())
|
||||
) {
|
||||
let issueFilters: any = {};
|
||||
Object.keys(routeFilters).forEach((key) => {
|
||||
const filterKey: any = key;
|
||||
const filterValue = routeFilters[key]?.toString() || undefined;
|
||||
if (ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet.filters.includes(filterKey) && filterKey && filterValue)
|
||||
issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") };
|
||||
});
|
||||
|
||||
if (!isEmpty(routeFilters))
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
issueFilters,
|
||||
globalViewId.toString()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch next pages callback
|
||||
const fetchNextPages = useCallback(() => {
|
||||
if (workspaceSlug && globalViewId) fetchNextIssues(workspaceSlug.toString(), globalViewId.toString());
|
||||
if (workspaceSlug && globalViewId) fetchNextIssues(workspaceSlug, globalViewId);
|
||||
}, [fetchNextIssues, workspaceSlug, globalViewId]);
|
||||
|
||||
// Fetch global views
|
||||
|
|
@ -86,7 +73,7 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
|
|||
workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug) {
|
||||
await fetchAllGlobalViews(workspaceSlug.toString());
|
||||
await fetchAllGlobalViews(workspaceSlug);
|
||||
}
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
|
|
@ -99,17 +86,11 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
|
|||
if (workspaceSlug && globalViewId) {
|
||||
clear();
|
||||
toggleLoading(true);
|
||||
await fetchFilters(workspaceSlug.toString(), globalViewId.toString());
|
||||
await fetchIssues(
|
||||
workspaceSlug.toString(),
|
||||
globalViewId.toString(),
|
||||
groupedIssueIds ? "mutation" : "init-loader",
|
||||
{
|
||||
canGroup: false,
|
||||
perPageCount: 100,
|
||||
}
|
||||
);
|
||||
routerFilterParams();
|
||||
await fetchFilters(workspaceSlug, globalViewId);
|
||||
await fetchIssues(workspaceSlug, globalViewId, groupedIssueIds ? "mutation" : "init-loader", {
|
||||
canGroup: false,
|
||||
perPageCount: 100,
|
||||
});
|
||||
toggleLoading(false);
|
||||
}
|
||||
},
|
||||
|
|
@ -131,18 +112,51 @@ export const AllIssueLayoutRoot: React.FC<Props> = observer((props: Props) => {
|
|||
);
|
||||
}
|
||||
|
||||
if (!workspaceSlug || !globalViewId) return null;
|
||||
return (
|
||||
<WorkspaceActiveLayout
|
||||
activeLayout={activeLayout}
|
||||
isDefaultView={isDefaultView}
|
||||
isLoading={isLoading}
|
||||
toggleLoading={toggleLoading}
|
||||
workspaceSlug={workspaceSlug?.toString()}
|
||||
globalViewId={globalViewId?.toString()}
|
||||
routeFilters={routeFilters}
|
||||
fetchNextPages={fetchNextPages}
|
||||
globalViewsLoading={globalViewsLoading}
|
||||
issuesLoading={issuesLoading}
|
||||
/>
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.GLOBAL}>
|
||||
<WorkspaceLevelWorkItemFiltersHOC
|
||||
enableSaveView
|
||||
saveViewOptions={{
|
||||
label: "Save as",
|
||||
}}
|
||||
enableUpdateView
|
||||
entityId={globalViewId}
|
||||
entityType={EIssuesStoreType.GLOBAL}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.filters}
|
||||
initialWorkItemFilters={initialWorkItemFilters}
|
||||
updateFilters={updateFilterExpression.bind(updateFilterExpression, workspaceSlug, globalViewId)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: globalWorkItemsFilter }) => (
|
||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||
<div className="flex h-full w-full flex-col border-b border-custom-border-300">
|
||||
{globalWorkItemsFilter && (
|
||||
<WorkItemFiltersRow
|
||||
filter={globalWorkItemsFilter}
|
||||
trackerElements={{
|
||||
saveView: GLOBAL_VIEW_TRACKER_ELEMENTS.HEADER_SAVE_VIEW_BUTTON,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<WorkspaceActiveLayout
|
||||
activeLayout={activeLayout}
|
||||
isDefaultView={isDefaultView}
|
||||
isLoading={isLoading}
|
||||
toggleLoading={toggleLoading}
|
||||
workspaceSlug={workspaceSlug}
|
||||
globalViewId={globalViewId}
|
||||
routeFilters={routeFilters}
|
||||
fetchNextPages={fetchNextPages}
|
||||
globalViewsLoading={globalViewsLoading}
|
||||
issuesLoading={issuesLoading}
|
||||
/>
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
)}
|
||||
</WorkspaceLevelWorkItemFiltersHOC>
|
||||
</IssuesStoreContext.Provider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,30 @@
|
|||
import React, { Fragment } from "react";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
// hooks
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
import { IssuePeekOverview } from "../../peek-overview";
|
||||
import { ArchivedIssueAppliedFiltersRoot } from "../filters";
|
||||
import { ArchivedIssueListLayout } from "../list/roots/archived-issue-root";
|
||||
|
||||
export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
// hooks
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
// derived values
|
||||
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
|
||||
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||
|
|
@ -29,11 +36,9 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters)
|
||||
if (isLoading && !workItemFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
|
|
@ -42,13 +47,25 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
|||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.ARCHIVED}>
|
||||
<ArchivedIssueAppliedFiltersRoot />
|
||||
<Fragment>
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ArchivedIssueListLayout />
|
||||
</div>
|
||||
<IssuePeekOverview />
|
||||
</Fragment>
|
||||
<ProjectLevelWorkItemFiltersHOC
|
||||
entityType={EIssuesStoreType.ARCHIVED}
|
||||
entityId={projectId?.toString()}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.archived_issues.filters}
|
||||
initialWorkItemFilters={workItemFilters}
|
||||
updateFilters={issuesFilter?.updateFilterExpression.bind(issuesFilter, workspaceSlug, projectId)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: archivedWorkItemsFilter }) => (
|
||||
<>
|
||||
{archivedWorkItemsFilter && <WorkItemFiltersRow filter={archivedWorkItemsFilter} />}
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ArchivedIssueListLayout />
|
||||
</div>
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
</IssuesStoreContext.Provider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,19 +4,21 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { TransferIssues } from "@/components/cycles/transfer-issues";
|
||||
import { TransferIssuesModal } from "@/components/cycles/transfer-issues-modal";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
import { IssuePeekOverview } from "../../peek-overview";
|
||||
import { CycleCalendarLayout } from "../calendar/roots/cycle-root";
|
||||
import { CycleAppliedFiltersRoot } from "../filters";
|
||||
import { BaseGanttRoot } from "../gantt";
|
||||
import { CycleKanBanLayout } from "../kanban/roots/cycle-root";
|
||||
import { CycleListLayout } from "../list/roots/cycle-root";
|
||||
|
|
@ -44,29 +46,30 @@ const CycleIssueLayout = (props: {
|
|||
};
|
||||
|
||||
export const CycleLayoutRoot: React.FC = observer(() => {
|
||||
const { workspaceSlug, projectId, cycleId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, cycleId: routerCycleId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
const cycleId = routerCycleId ? routerCycleId.toString() : undefined;
|
||||
// store hooks
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { getCycleById } = useCycle();
|
||||
// state
|
||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||
// derived values
|
||||
const workItemFilters = cycleId ? issuesFilter?.getIssueFilters(cycleId) : undefined;
|
||||
const activeLayout = workItemFilters?.displayFilters?.layout;
|
||||
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId && cycleId
|
||||
? `CYCLE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${cycleId.toString()}`
|
||||
: null,
|
||||
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_${workspaceSlug}_${projectId}_${cycleId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && cycleId) {
|
||||
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
|
||||
await issuesFilter?.fetchFilters(workspaceSlug, projectId, cycleId);
|
||||
}
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const issueFilters = issuesFilter?.getIssueFilters(cycleId?.toString());
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||
const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
|
||||
const cycleStatus = cycleDetails?.status?.toLocaleLowerCase() ?? "draft";
|
||||
const isCompletedCycle = cycleStatus === "completed";
|
||||
const isProgressSnapshotEmpty = isEmpty(cycleDetails?.progress_snapshot);
|
||||
|
|
@ -77,7 +80,7 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||
|
||||
if (!workspaceSlug || !projectId || !cycleId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters)
|
||||
if (isLoading && !workItemFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
|
|
@ -86,31 +89,48 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.CYCLE}>
|
||||
<TransferIssuesModal
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
cycleId={cycleId.toString()}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{cycleStatus === "completed" && (
|
||||
<TransferIssues
|
||||
handleClick={() => setTransferIssuesModal(true)}
|
||||
canTransferIssues={canTransferIssues}
|
||||
disabled={!isEmpty(cycleDetails?.progress_snapshot)}
|
||||
/>
|
||||
<ProjectLevelWorkItemFiltersHOC
|
||||
enableSaveView
|
||||
entityType={EIssuesStoreType.CYCLE}
|
||||
entityId={cycleId}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.filters}
|
||||
initialWorkItemFilters={workItemFilters}
|
||||
updateFilters={issuesFilter?.updateFilterExpression.bind(issuesFilter, workspaceSlug, projectId, cycleId)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: cycleWorkItemsFilter }) => (
|
||||
<>
|
||||
<TransferIssuesModal
|
||||
handleClose={() => setTransferIssuesModal(false)}
|
||||
cycleId={cycleId}
|
||||
isOpen={transferIssuesModal}
|
||||
/>
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{cycleStatus === "completed" && (
|
||||
<TransferIssues
|
||||
handleClick={() => setTransferIssuesModal(true)}
|
||||
canTransferIssues={canTransferIssues}
|
||||
disabled={!isEmpty(cycleDetails?.progress_snapshot)}
|
||||
/>
|
||||
)}
|
||||
{cycleWorkItemsFilter && (
|
||||
<WorkItemFiltersRow
|
||||
filter={cycleWorkItemsFilter}
|
||||
trackerElements={{
|
||||
saveView: PROJECT_VIEW_TRACKER_ELEMENTS.CYCLE_HEADER_SAVE_AS_VIEW_BUTTON,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="h-full w-full overflow-auto">
|
||||
<CycleIssueLayout activeLayout={activeLayout} cycleId={cycleId} isCompletedCycle={isCompletedCycle} />
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<CycleAppliedFiltersRoot />
|
||||
|
||||
<div className="h-full w-full overflow-auto">
|
||||
<CycleIssueLayout
|
||||
activeLayout={activeLayout}
|
||||
cycleId={cycleId?.toString()}
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
/>
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
</IssuesStoreContext.Provider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,17 +3,19 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
import { Row, ERowVariant } from "@plane/ui";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
import { IssuePeekOverview } from "../../peek-overview";
|
||||
import { ModuleCalendarLayout } from "../calendar/roots/module-root";
|
||||
import { ModuleAppliedFiltersRoot } from "../filters";
|
||||
import { BaseGanttRoot } from "../gantt";
|
||||
import { ModuleKanBanLayout } from "../kanban/roots/module-root";
|
||||
import { ModuleListLayout } from "../list/roots/module-root";
|
||||
|
|
@ -38,9 +40,15 @@ const ModuleIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined;
|
|||
|
||||
export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, moduleId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, moduleId: routerModuleId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
const moduleId = routerModuleId ? routerModuleId.toString() : undefined;
|
||||
// hooks
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.MODULE);
|
||||
// derived values
|
||||
const workItemFilters = moduleId ? issuesFilter?.getIssueFilters(moduleId) : undefined;
|
||||
const activeLayout = workItemFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId && moduleId
|
||||
|
|
@ -54,29 +62,45 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const issueFilters = issuesFilter?.getIssueFilters(moduleId?.toString());
|
||||
|
||||
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters)
|
||||
if (isLoading && !workItemFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.MODULE}>
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ModuleAppliedFiltersRoot />
|
||||
<Row variant={ERowVariant.HUGGING} className="h-full w-full overflow-auto">
|
||||
<ModuleIssueLayout activeLayout={activeLayout} moduleId={moduleId?.toString()} />
|
||||
</Row>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
<ProjectLevelWorkItemFiltersHOC
|
||||
enableSaveView
|
||||
entityType={EIssuesStoreType.MODULE}
|
||||
entityId={moduleId}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.filters}
|
||||
initialWorkItemFilters={workItemFilters}
|
||||
updateFilters={issuesFilter?.updateFilterExpression.bind(issuesFilter, workspaceSlug, projectId, moduleId)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: moduleWorkItemsFilter }) => (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{moduleWorkItemsFilter && (
|
||||
<WorkItemFiltersRow
|
||||
filter={moduleWorkItemsFilter}
|
||||
trackerElements={{
|
||||
saveView: PROJECT_VIEW_TRACKER_ELEMENTS.MODULE_HEADER_SAVE_AS_VIEW_BUTTON,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Row variant={ERowVariant.HUGGING} className="h-full w-full overflow-auto">
|
||||
<ModuleIssueLayout activeLayout={activeLayout} moduleId={moduleId} />
|
||||
</Row>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
)}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
</IssuesStoreContext.Provider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
"use client";
|
||||
|
||||
import { FC, Fragment } from "react";
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane constants
|
||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EIssueLayoutTypes, EIssuesStoreType } from "@plane/types";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
import { IssuePeekOverview } from "../../peek-overview";
|
||||
import { CalendarLayout } from "../calendar/roots/project-root";
|
||||
import { ProjectAppliedFiltersRoot } from "../filters";
|
||||
import { BaseGanttRoot } from "../gantt";
|
||||
import { KanBanLayout } from "../kanban/roots/project-root";
|
||||
import { ListLayout } from "../list/roots/project-root";
|
||||
|
|
@ -40,26 +42,28 @@ const ProjectIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined
|
|||
|
||||
export const ProjectLayoutRoot: FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId.toString() : undefined;
|
||||
// hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||
// derived values
|
||||
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
|
||||
const activeLayout = workItemFilters?.displayFilters?.layout;
|
||||
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||
await issuesFilter?.fetchFilters(workspaceSlug, projectId);
|
||||
}
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
if (!workspaceSlug || !projectId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters)
|
||||
if (isLoading && !workItemFilters)
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<LogoSpinner />
|
||||
|
|
@ -68,21 +72,40 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
|||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT}>
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ProjectAppliedFiltersRoot />
|
||||
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
||||
{/* mutation loader */}
|
||||
{issues?.getIssueLoader() === "mutation" && (
|
||||
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
|
||||
<Spinner className="w-4 h-4" />
|
||||
<ProjectLevelWorkItemFiltersHOC
|
||||
enableSaveView
|
||||
entityType={EIssuesStoreType.PROJECT}
|
||||
entityId={projectId}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.filters}
|
||||
initialWorkItemFilters={workItemFilters}
|
||||
updateFilters={issuesFilter?.updateFilterExpression.bind(issuesFilter, workspaceSlug, projectId)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: projectWorkItemsFilter }) => (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{projectWorkItemsFilter && (
|
||||
<WorkItemFiltersRow
|
||||
filter={projectWorkItemsFilter}
|
||||
trackerElements={{
|
||||
saveView: PROJECT_VIEW_TRACKER_ELEMENTS.PROJECT_HEADER_SAVE_AS_VIEW_BUTTON,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
||||
{/* mutation loader */}
|
||||
{issues?.getIssueLoader() === "mutation" && (
|
||||
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
|
||||
<Spinner className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
<ProjectIssueLayout activeLayout={activeLayout} />
|
||||
</div>
|
||||
)}
|
||||
<ProjectIssueLayout activeLayout={activeLayout} />
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
)}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
</IssuesStoreContext.Provider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane constants
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE, PROJECT_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
// hooks
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
// local imports
|
||||
import { IssuePeekOverview } from "../../peek-overview";
|
||||
import { ProjectViewCalendarLayout } from "../calendar/roots/project-view-root";
|
||||
import { ProjectViewAppliedFiltersRoot } from "../filters";
|
||||
import { BaseGanttRoot } from "../gantt";
|
||||
import { ProjectViewKanBanLayout } from "../kanban/roots/project-view-root";
|
||||
import { ProjectViewListLayout } from "../list/roots/project-view-root";
|
||||
import { ProjectViewSpreadsheetLayout } from "../spreadsheet/roots/project-view-root";
|
||||
// types
|
||||
|
||||
const ProjectViewIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined; viewId: string }) => {
|
||||
switch (props.activeLayout) {
|
||||
|
|
@ -38,26 +40,47 @@ const ProjectViewIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undef
|
|||
|
||||
export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug, projectId, viewId } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, viewId: routerViewId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug?.toString() : undefined;
|
||||
const projectId = routerProjectId ? routerProjectId?.toString() : undefined;
|
||||
const viewId = routerViewId ? routerViewId?.toString() : undefined;
|
||||
// hooks
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
const { getViewById } = useProjectView();
|
||||
// derived values
|
||||
const projectView = viewId ? getViewById(viewId) : undefined;
|
||||
const workItemFilters = viewId ? issuesFilter?.getIssueFilters(viewId) : undefined;
|
||||
const activeLayout = workItemFilters?.displayFilters?.layout;
|
||||
const initialWorkItemFilters = projectView
|
||||
? {
|
||||
displayFilters: workItemFilters?.displayFilters,
|
||||
displayProperties: workItemFilters?.displayProperties,
|
||||
kanbanFilters: workItemFilters?.kanbanFilters,
|
||||
richFilters: projectView.rich_filters,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null,
|
||||
async () => {
|
||||
if (workspaceSlug && projectId && viewId) {
|
||||
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString());
|
||||
await issuesFilter?.fetchFilters(workspaceSlug, projectId, viewId);
|
||||
}
|
||||
},
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
}
|
||||
);
|
||||
|
||||
const issueFilters = issuesFilter?.getIssueFilters(viewId?.toString());
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (workspaceSlug && viewId) {
|
||||
issuesFilter?.resetFilters(workspaceSlug, viewId);
|
||||
}
|
||||
},
|
||||
[issuesFilter, workspaceSlug, viewId]
|
||||
);
|
||||
|
||||
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||
|
||||
if (isLoading && !issueFilters) {
|
||||
if (isLoading && !workItemFilters) {
|
||||
return (
|
||||
<div className="relative flex h-screen w-full items-center justify-center">
|
||||
<LogoSpinner />
|
||||
|
|
@ -67,15 +90,38 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
|||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.PROJECT_VIEW}>
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<ProjectViewAppliedFiltersRoot />
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ProjectViewIssueLayout activeLayout={activeLayout} viewId={viewId.toString()} />
|
||||
</div>
|
||||
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
<ProjectLevelWorkItemFiltersHOC
|
||||
enableSaveView
|
||||
saveViewOptions={{
|
||||
label: "Save as",
|
||||
}}
|
||||
enableUpdateView
|
||||
entityId={viewId}
|
||||
entityType={EIssuesStoreType.PROJECT_VIEW}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.filters}
|
||||
initialWorkItemFilters={initialWorkItemFilters}
|
||||
updateFilters={issuesFilter?.updateFilterExpression.bind(issuesFilter, workspaceSlug, projectId, viewId)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: projectViewWorkItemsFilter }) => (
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
{projectViewWorkItemsFilter && (
|
||||
<WorkItemFiltersRow
|
||||
filter={projectViewWorkItemsFilter}
|
||||
trackerElements={{
|
||||
saveView: PROJECT_VIEW_TRACKER_ELEMENTS.HEADER_SAVE_VIEW_BUTTON,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ProjectViewIssueLayout activeLayout={activeLayout} viewId={viewId.toString()} />
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
)}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
</IssuesStoreContext.Provider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types";
|
||||
import { Button } from "@plane/ui";
|
||||
// components
|
||||
import { CreateUpdateProjectViewModal } from "@/components/views/modal";
|
||||
|
||||
interface ISaveFilterView {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
filterParams: {
|
||||
filters: IIssueFilterOptions;
|
||||
display_filters?: IIssueDisplayFilterOptions;
|
||||
display_properties?: IIssueDisplayProperties;
|
||||
};
|
||||
trackerElement: string;
|
||||
}
|
||||
|
||||
export const SaveFilterView: FC<ISaveFilterView> = (props) => {
|
||||
const { workspaceSlug, projectId, filterParams, trackerElement } = props;
|
||||
|
||||
const [viewModal, setViewModal] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CreateUpdateProjectViewModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
preLoadedData={{ ...filterParams }}
|
||||
isOpen={viewModal}
|
||||
onClose={() => setViewModal(false)}
|
||||
/>
|
||||
|
||||
<Button size="sm" onClick={() => setViewModal(true)} data-ph-element={trackerElement}>
|
||||
Save View
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -25,7 +25,7 @@ export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) =
|
|||
<div className="h-11 border-b-[0.5px] border-custom-border-200 w-full">
|
||||
<IssuePropertyLabels
|
||||
projectId={issue.project_id ?? null}
|
||||
value={issue.label_ids}
|
||||
value={issue.label_ids || []}
|
||||
defaultOptions={defaultLabelOptions}
|
||||
onChange={(data) => onChange(issue, { label_ids: data }, { changed_property: "labels", change_details: data })}
|
||||
className="h-full w-full "
|
||||
|
|
|
|||
|
|
@ -9,11 +9,9 @@ import { SpreadsheetLayoutLoader } from "@/components/ui/loader/layouts/spreadsh
|
|||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
import { useIssuesActions } from "@/hooks/use-issues-actions";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
// store
|
||||
import { IssuePeekOverview } from "../../../peek-overview";
|
||||
import { IssueLayoutHOC } from "../../issue-layout-HOC";
|
||||
import { TRenderQuickActions } from "../../list/list-view-types";
|
||||
import { SpreadsheetView } from "../spreadsheet-view";
|
||||
|
|
@ -108,23 +106,19 @@ export const WorkspaceSpreadsheetRoot: React.FC<Props> = observer((props: Props)
|
|||
|
||||
// Render spreadsheet
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.GLOBAL}>
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.SPREADSHEET}>
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issueIds={Array.isArray(issueIds) ? issueIds : []}
|
||||
quickActions={renderQuickActions}
|
||||
updateIssue={updateIssue}
|
||||
canEditProperties={canEditProperties}
|
||||
canLoadMoreIssues={!!nextPageResults}
|
||||
loadMoreIssues={fetchNextPages}
|
||||
isWorkspaceLevel
|
||||
/>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</IssueLayoutHOC>
|
||||
</IssuesStoreContext.Provider>
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.SPREADSHEET}>
|
||||
<SpreadsheetView
|
||||
displayProperties={issueFilters?.displayProperties ?? {}}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFilterUpdate={handleDisplayFiltersUpdate}
|
||||
issueIds={Array.isArray(issueIds) ? issueIds : []}
|
||||
quickActions={renderQuickActions}
|
||||
updateIssue={updateIssue}
|
||||
canEditProperties={canEditProperties}
|
||||
canLoadMoreIssues={!!nextPageResults}
|
||||
loadMoreIssues={fetchNextPages}
|
||||
isWorkspaceLevel
|
||||
/>
|
||||
</IssueLayoutHOC>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { CSSProperties, FC } from "react";
|
|||
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
|
||||
import clone from "lodash/clone";
|
||||
import concat from "lodash/concat";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import isNil from "lodash/isNil";
|
||||
import pull from "lodash/pull";
|
||||
import uniq from "lodash/uniq";
|
||||
|
|
@ -24,9 +23,7 @@ import {
|
|||
TIssueGroupByOptions,
|
||||
IIssueFilterOptions,
|
||||
IIssueFilters,
|
||||
IProjectView,
|
||||
TGroupedIssues,
|
||||
IWorkspaceView,
|
||||
IIssueDisplayFilterOptions,
|
||||
TGetColumns,
|
||||
} from "@plane/types";
|
||||
|
|
@ -593,27 +590,6 @@ export const handleGroupDragDrop = async (
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This Method compares filters and returns a boolean based on which and updateView button is shown
|
||||
* @param appliedFilters
|
||||
* @param issueFilters
|
||||
* @param viewDetails
|
||||
* @returns
|
||||
*/
|
||||
export const getAreFiltersEqual = (
|
||||
appliedFilters: IIssueFilterOptions | undefined,
|
||||
issueFilters: IIssueFilters | undefined,
|
||||
viewDetails: IProjectView | IWorkspaceView | null
|
||||
) => {
|
||||
if (isNil(appliedFilters) || isNil(issueFilters) || isNil(viewDetails)) return true;
|
||||
|
||||
return (
|
||||
isEqual(appliedFilters, viewDetails.filters) &&
|
||||
isEqual(issueFilters.displayFilters, viewDetails.display_filters) &&
|
||||
isEqual(removeNillKeys(issueFilters.displayProperties), removeNillKeys(viewDetails.display_properties))
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* method that removes Null or undefined Keys from object
|
||||
* @param obj
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { FC, Fragment, useCallback, useMemo, useState } from "react";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { FC, Fragment, useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { AlertCircle, ChevronUp, ChevronDown } from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
import { EIssueFilterType, EEstimateSystem } from "@plane/constants";
|
||||
import { EEstimateSystem } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EIssuesStoreType, IIssueFilterOptions, TModulePlotType } from "@plane/types";
|
||||
import { EIssuesStoreType, TModulePlotType } from "@plane/types";
|
||||
import { CustomSelect, Spinner } from "@plane/ui";
|
||||
// components
|
||||
// constants
|
||||
|
|
@ -18,8 +17,8 @@ import ProgressChart from "@/components/core/sidebar/progress-chart";
|
|||
import { ModuleProgressStats } from "@/components/modules";
|
||||
// hooks
|
||||
import { useProjectEstimates } from "@/hooks/store/estimates";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
// plane web constants
|
||||
type TModuleAnalyticsProgress = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -38,31 +37,30 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
|
|||
// router
|
||||
const searchParams = useSearchParams();
|
||||
const peekModule = searchParams.get("peekModule") || undefined;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
||||
const { getPlotTypeByModuleId, setPlotType, getModuleById, fetchModuleDetails, fetchArchivedModuleDetails } =
|
||||
useModule();
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.MODULE);
|
||||
const { getFilter, updateFilterValueFromSidebar } = useWorkItemFilters();
|
||||
// state
|
||||
const [loader, setLoader] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const moduleFilter = getFilter(EIssuesStoreType.MODULE, moduleId);
|
||||
const selectedAssignees = moduleFilter?.findFirstConditionByPropertyAndOperator("assignee_id", "in");
|
||||
const selectedLabels = moduleFilter?.findFirstConditionByPropertyAndOperator("label_id", "in");
|
||||
const selectedStateGroups = moduleFilter?.findFirstConditionByPropertyAndOperator("state_group", "in");
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
const plotType: TModulePlotType = getPlotTypeByModuleId(moduleId);
|
||||
const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false;
|
||||
const estimateDetails =
|
||||
isCurrentProjectEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId);
|
||||
const isCurrentEstimateTypeIsPoints = estimateDetails && estimateDetails?.type === EEstimateSystem.POINTS;
|
||||
|
||||
const completedIssues = moduleDetails?.completed_issues || 0;
|
||||
const totalIssues = moduleDetails?.total_issues || 0;
|
||||
const completedEstimatePoints = moduleDetails?.completed_estimate_points || 0;
|
||||
const totalEstimatePoints = moduleDetails?.total_estimate_points || 0;
|
||||
|
||||
const progressHeaderPercentage = moduleDetails
|
||||
? plotType === "points"
|
||||
? completedEstimatePoints != 0 && totalEstimatePoints != 0
|
||||
|
|
@ -72,11 +70,9 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
|
|||
? Math.round((completedIssues / totalIssues) * 100)
|
||||
: 0
|
||||
: 0;
|
||||
|
||||
const chartDistributionData =
|
||||
plotType === "points" ? moduleDetails?.estimate_distribution : moduleDetails?.distribution || undefined;
|
||||
const completionChartDistributionData = chartDistributionData?.completion_chart || undefined;
|
||||
|
||||
const groupedIssues = useMemo(
|
||||
() => ({
|
||||
backlog: plotType === "points" ? moduleDetails?.backlog_estimate_points || 0 : moduleDetails?.backlog_issues || 0,
|
||||
|
|
@ -90,7 +86,6 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
|
|||
}),
|
||||
[plotType, moduleDetails]
|
||||
);
|
||||
|
||||
const moduleStartDate = getDate(moduleDetails?.start_date);
|
||||
const moduleEndDate = getDate(moduleDetails?.target_date);
|
||||
const isModuleStartDateValid = moduleStartDate && moduleStartDate <= new Date();
|
||||
|
|
@ -116,37 +111,6 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
|
|||
}
|
||||
};
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (key === "state") {
|
||||
if (isEqual(newValues, value)) newValues = [];
|
||||
else newValues = value;
|
||||
} else {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: newValues },
|
||||
moduleId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, projectId, moduleId, issueFilters, updateFilters]
|
||||
);
|
||||
|
||||
if (!moduleDetails) return <></>;
|
||||
return (
|
||||
<div className="border-t border-custom-border-200 space-y-4 py-4 px-3">
|
||||
|
|
@ -234,17 +198,25 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
|
|||
{chartDistributionData && (
|
||||
<div className="w-full border-t border-custom-border-200 pt-5">
|
||||
<ModuleProgressStats
|
||||
moduleId={moduleId}
|
||||
plotType={plotType}
|
||||
distribution={chartDistributionData}
|
||||
groupedIssues={groupedIssues}
|
||||
totalIssuesCount={plotType === "points" ? totalEstimatePoints || 0 : totalIssues || 0}
|
||||
isEditable={Boolean(!peekModule)}
|
||||
size="xs"
|
||||
roundedTab={false}
|
||||
handleFiltersUpdate={updateFilterValueFromSidebar.bind(
|
||||
updateFilterValueFromSidebar,
|
||||
EIssuesStoreType.MODULE,
|
||||
moduleId
|
||||
)}
|
||||
isEditable={Boolean(!peekModule) && moduleFilter !== undefined}
|
||||
moduleId={moduleId}
|
||||
noBackground={false}
|
||||
filters={issueFilters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
plotType={plotType}
|
||||
roundedTab={false}
|
||||
selectedFilters={{
|
||||
assignees: selectedAssignees,
|
||||
labels: selectedLabels,
|
||||
stateGroups: selectedStateGroups,
|
||||
}}
|
||||
size="xs"
|
||||
totalIssuesCount={plotType === "points" ? totalEstimatePoints || 0 : totalIssues || 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,278 +2,65 @@
|
|||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
IIssueFilters,
|
||||
TModuleDistribution,
|
||||
TModuleEstimateDistribution,
|
||||
TModulePlotType,
|
||||
TStateGroups,
|
||||
} from "@plane/types";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { cn, getFileURL } from "@plane/utils";
|
||||
import { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||
import { TModuleDistribution, TModuleEstimateDistribution, TModulePlotType } from "@plane/types";
|
||||
import { cn, toFilterArray } from "@plane/utils";
|
||||
// components
|
||||
import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats";
|
||||
// helpers
|
||||
import { AssigneeStatComponent, TAssigneeData } from "@/components/core/sidebar/progress-stats/assignee";
|
||||
import { LabelStatComponent, TLabelData } from "@/components/core/sidebar/progress-stats/label";
|
||||
import {
|
||||
createFilterUpdateHandler,
|
||||
PROGRESS_STATS,
|
||||
TSelectedFilterProgressStats,
|
||||
} from "@/components/core/sidebar/progress-stats/shared";
|
||||
import { StateGroupStatComponent, TStateGroupData } from "@/components/core/sidebar/progress-stats/state_group";
|
||||
// hooks
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// public
|
||||
import emptyLabel from "@/public/empty-state/empty_label.svg";
|
||||
import emptyMembers from "@/public/empty-state/empty_members.svg";
|
||||
|
||||
// assignee types
|
||||
type TAssigneeData = {
|
||||
id: string | undefined;
|
||||
title: string | undefined;
|
||||
avatar_url: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TAssigneeStatComponent = {
|
||||
distribution: TAssigneeData;
|
||||
isEditable?: boolean;
|
||||
filters?: IIssueFilters | undefined;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
// labelTypes
|
||||
type TLabelData = {
|
||||
id: string | undefined;
|
||||
title: string | undefined;
|
||||
color: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TLabelStatComponent = {
|
||||
distribution: TLabelData;
|
||||
isEditable?: boolean;
|
||||
filters?: IIssueFilters | undefined;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
// stateTypes
|
||||
type TStateData = {
|
||||
state: string | undefined;
|
||||
completed: number;
|
||||
total: number;
|
||||
}[];
|
||||
|
||||
type TStateStatComponent = {
|
||||
distribution: TStateData;
|
||||
totalIssuesCount: number;
|
||||
isEditable?: boolean;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
export const AssigneeStatComponent = observer((props: TAssigneeStatComponent) => {
|
||||
const { distribution, isEditable, filters, handleFiltersUpdate } = props;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
{distribution && distribution.length > 0 ? (
|
||||
distribution.map((assignee, index) => {
|
||||
if (assignee?.id)
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={assignee?.id}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar name={assignee?.title ?? undefined} src={getFileURL(assignee?.avatar_url ?? "")} />
|
||||
<span>{assignee?.title ?? ""}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee?.completed ?? 0}
|
||||
total={assignee?.total ?? 0}
|
||||
{...(isEditable && {
|
||||
onClick: () => handleFiltersUpdate("assignees", assignee.id ?? ""),
|
||||
selected: filters?.filters?.assignees?.includes(assignee.id ?? ""),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`unassigned-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-4 w-4 rounded-full border-2 border-custom-border-200 bg-custom-background-80">
|
||||
<img src="/user.png" height="100%" width="100%" className="rounded-full" alt="User" />
|
||||
</div>
|
||||
<span>{t("no_assignee")}</span>
|
||||
</div>
|
||||
}
|
||||
completed={assignee?.completed ?? 0}
|
||||
total={assignee?.total ?? 0}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<Image src={emptyMembers} className="h-12 w-12" alt="empty members" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">{t("no_assignees_yet")}</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const LabelStatComponent = observer((props: TLabelStatComponent) => {
|
||||
const { distribution, isEditable, filters, handleFiltersUpdate } = props;
|
||||
return (
|
||||
<div>
|
||||
{distribution && distribution.length > 0 ? (
|
||||
distribution.map((label, index) => {
|
||||
if (label.id) {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={label.id}
|
||||
title={
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<div
|
||||
className="h-3 w-3 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-ellipsis truncate">{label.title ?? "No labels"}</p>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
total={label.total}
|
||||
{...(isEditable && {
|
||||
onClick: () => handleFiltersUpdate("labels", label.id ?? ""),
|
||||
selected: filters?.filters?.labels?.includes(label.id ?? `no-label-${index}`),
|
||||
})}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SingleProgressStats
|
||||
key={`no-label-${index}`}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="block h-3 w-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.color ?? "transparent",
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{label.title ?? "No labels"}</span>
|
||||
</div>
|
||||
}
|
||||
completed={label.completed}
|
||||
total={label.total}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<Image src={emptyLabel} className="h-12 w-12" alt="empty label" />
|
||||
</div>
|
||||
<h6 className="text-base text-custom-text-300">No labels yet</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const StateStatComponent = observer((props: TStateStatComponent) => {
|
||||
const { distribution, isEditable, totalIssuesCount, handleFiltersUpdate } = props;
|
||||
// hooks
|
||||
const { groupedProjectStates } = useProjectState();
|
||||
// derived values
|
||||
const getStateGroupState = (stateGroup: string) => {
|
||||
const stateGroupStates = groupedProjectStates?.[stateGroup];
|
||||
const stateGroupStatesId = stateGroupStates?.map((state) => state.id);
|
||||
return stateGroupStatesId;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{distribution.map((group, index) => (
|
||||
<SingleProgressStats
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<StateGroupIcon stateGroup={group.state as TStateGroups} />
|
||||
<span className="text-xs capitalize">{group.state}</span>
|
||||
</div>
|
||||
}
|
||||
completed={group.completed}
|
||||
total={totalIssuesCount}
|
||||
{...(isEditable && {
|
||||
onClick: () => group.state && handleFiltersUpdate("state", getStateGroupState(group.state) ?? []),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const progressStats = [
|
||||
{
|
||||
key: "stat-assignees",
|
||||
title: "Assignees",
|
||||
},
|
||||
{
|
||||
key: "stat-labels",
|
||||
title: "Labels",
|
||||
},
|
||||
{
|
||||
key: "stat-states",
|
||||
title: "States",
|
||||
},
|
||||
];
|
||||
|
||||
type TModuleProgressStats = {
|
||||
moduleId: string;
|
||||
plotType: TModulePlotType;
|
||||
distribution: TModuleDistribution | TModuleEstimateDistribution | undefined;
|
||||
groupedIssues: Record<string, number>;
|
||||
totalIssuesCount: number;
|
||||
handleFiltersUpdate: (condition: TWorkItemFilterCondition) => void;
|
||||
isEditable?: boolean;
|
||||
filters?: IIssueFilters | undefined;
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
size?: "xs" | "sm";
|
||||
roundedTab?: boolean;
|
||||
moduleId: string;
|
||||
noBackground?: boolean;
|
||||
plotType: TModulePlotType;
|
||||
roundedTab?: boolean;
|
||||
selectedFilters: TSelectedFilterProgressStats;
|
||||
size?: "xs" | "sm";
|
||||
totalIssuesCount: number;
|
||||
};
|
||||
|
||||
export const ModuleProgressStats: FC<TModuleProgressStats> = observer((props) => {
|
||||
const {
|
||||
moduleId,
|
||||
plotType,
|
||||
distribution,
|
||||
groupedIssues,
|
||||
totalIssuesCount,
|
||||
isEditable = false,
|
||||
filters,
|
||||
handleFiltersUpdate,
|
||||
size = "sm",
|
||||
roundedTab = false,
|
||||
isEditable = false,
|
||||
moduleId,
|
||||
noBackground = false,
|
||||
plotType,
|
||||
roundedTab = false,
|
||||
selectedFilters,
|
||||
size = "sm",
|
||||
totalIssuesCount,
|
||||
} = props;
|
||||
// plane imports
|
||||
const { t } = useTranslation();
|
||||
// hooks
|
||||
const { storedValue: currentTab, setValue: setModuleTab } = useLocalStorage(
|
||||
`module-analytics-tab-${moduleId}`,
|
||||
"stat-assignees"
|
||||
);
|
||||
// derived values
|
||||
const currentTabIndex = (tab: string): number => progressStats.findIndex((stat) => stat.key === tab);
|
||||
|
||||
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
|
||||
const currentDistribution = distribution as TModuleDistribution;
|
||||
const currentEstimateDistribution = distribution as TModuleEstimateDistribution;
|
||||
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
|
||||
const selectedLabelIds = toFilterArray(selectedFilters?.labels?.value || []) as string[];
|
||||
const selectedStateGroups = toFilterArray(selectedFilters?.stateGroups?.value || []) as string[];
|
||||
|
||||
const distributionAssigneeData: TAssigneeData =
|
||||
plotType === "burndown"
|
||||
|
|
@ -309,12 +96,24 @@ export const ModuleProgressStats: FC<TModuleProgressStats> = observer((props) =>
|
|||
total: label.total_estimates,
|
||||
}));
|
||||
|
||||
const distributionStateData: TStateData = Object.keys(groupedIssues || {}).map((state) => ({
|
||||
const distributionStateData: TStateGroupData = Object.keys(groupedIssues || {}).map((state) => ({
|
||||
state: state,
|
||||
completed: groupedIssues?.[state] || 0,
|
||||
total: totalIssuesCount || 0,
|
||||
}));
|
||||
|
||||
const handleAssigneeFiltersUpdate = createFilterUpdateHandler(
|
||||
"assignee_id",
|
||||
selectedAssigneeIds,
|
||||
handleFiltersUpdate
|
||||
);
|
||||
const handleLabelFiltersUpdate = createFilterUpdateHandler("label_id", selectedLabelIds, handleFiltersUpdate);
|
||||
const handleStateGroupFiltersUpdate = createFilterUpdateHandler(
|
||||
"state_group",
|
||||
selectedStateGroups,
|
||||
handleFiltersUpdate
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
|
||||
|
|
@ -327,7 +126,7 @@ export const ModuleProgressStats: FC<TModuleProgressStats> = observer((props) =>
|
|||
size === "xs" ? `text-xs` : `text-sm`
|
||||
)}
|
||||
>
|
||||
{progressStats.map((stat) => (
|
||||
{PROGRESS_STATS.map((stat) => (
|
||||
<Tab
|
||||
className={cn(
|
||||
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
|
||||
|
|
@ -339,7 +138,7 @@ export const ModuleProgressStats: FC<TModuleProgressStats> = observer((props) =>
|
|||
key={stat.key}
|
||||
onClick={() => setModuleTab(stat.key)}
|
||||
>
|
||||
{stat.title}
|
||||
{t(stat.i18n_title)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
|
|
@ -347,25 +146,26 @@ export const ModuleProgressStats: FC<TModuleProgressStats> = observer((props) =>
|
|||
<Tab.Panel key={"stat-assignees"}>
|
||||
<AssigneeStatComponent
|
||||
distribution={distributionAssigneeData}
|
||||
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
|
||||
isEditable={isEditable}
|
||||
filters={filters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
selectedAssigneeIds={selectedAssigneeIds}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel key={"stat-labels"}>
|
||||
<LabelStatComponent
|
||||
distribution={distributionLabelData}
|
||||
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
|
||||
isEditable={isEditable}
|
||||
filters={filters}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
selectedLabelIds={selectedLabelIds}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel key={"stat-states"}>
|
||||
<StateStatComponent
|
||||
<StateGroupStatComponent
|
||||
distribution={distributionStateData}
|
||||
totalIssuesCount={totalIssuesCount}
|
||||
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
|
||||
isEditable={isEditable}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
selectedStateGroups={selectedStateGroups}
|
||||
totalIssuesCount={totalIssuesCount}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
|
|
|
|||
|
|
@ -6,25 +6,11 @@ import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constant
|
|||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
EIssueLayoutTypes,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType, IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
|
||||
// components
|
||||
import { isIssueFilterActive } from "@plane/utils";
|
||||
import {
|
||||
DisplayFiltersSelection,
|
||||
FilterSelection,
|
||||
FiltersDropdown,
|
||||
LayoutSelection,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
import { DisplayFiltersSelection, FiltersDropdown, LayoutSelection } from "@/components/issues/issue-layouts/filters";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
|
||||
export const ProfileIssuesFilter = observer(() => {
|
||||
// i18n
|
||||
|
|
@ -35,11 +21,7 @@ export const ProfileIssuesFilter = observer(() => {
|
|||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROFILE);
|
||||
|
||||
const { workspaceLabels } = useLabel();
|
||||
// derived values
|
||||
const states = undefined;
|
||||
const members = undefined;
|
||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
||||
|
||||
const handleLayoutChange = useCallback(
|
||||
|
|
@ -56,33 +38,6 @@ export const ProfileIssuesFilter = observer(() => {
|
|||
[workspaceSlug, updateFilters, userId]
|
||||
);
|
||||
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
{ [key]: newValues },
|
||||
userId.toString()
|
||||
);
|
||||
},
|
||||
[workspaceSlug, issueFilters, updateFilters, userId]
|
||||
);
|
||||
|
||||
const handleDisplayFilters = useCallback(
|
||||
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
|
|
@ -118,30 +73,10 @@ export const ProfileIssuesFilter = observer(() => {
|
|||
onChange={(layout) => handleLayoutChange(layout)}
|
||||
selectedLayout={activeLayout}
|
||||
/>
|
||||
|
||||
<FiltersDropdown
|
||||
title={t("common.filters")}
|
||||
placement="bottom-end"
|
||||
isFiltersApplied={isIssueFilterActive(issueFilters)}
|
||||
>
|
||||
<FilterSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
|
||||
}
|
||||
filters={issueFilters?.filters ?? {}}
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
states={states}
|
||||
labels={workspaceLabels}
|
||||
memberIds={members}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
|
||||
<FiltersDropdown title={t("common.display")} placement="bottom-end">
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues[activeLayout] : undefined
|
||||
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues.layoutOptions[activeLayout] : undefined
|
||||
}
|
||||
displayFilters={issueFilters?.displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
// components
|
||||
import { ProfileIssuesAppliedFiltersRoot } from "@/components/issues/issue-layouts/filters";
|
||||
import { ProfileIssuesKanBanLayout } from "@/components/issues/issue-layouts/kanban/roots/profile-issues-root";
|
||||
import { ProfileIssuesListLayout } from "@/components/issues/issue-layouts/list/roots/profile-issues-root";
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
// hooks
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
|
|
@ -19,7 +21,6 @@ type Props = {
|
|||
|
||||
export const ProfileIssuesPage = observer((props: Props) => {
|
||||
const { type } = props;
|
||||
|
||||
const { workspaceSlug, userId } = useParams() as {
|
||||
workspaceSlug: string;
|
||||
userId: string;
|
||||
|
|
@ -27,8 +28,10 @@ export const ProfileIssuesPage = observer((props: Props) => {
|
|||
// store hooks
|
||||
const {
|
||||
issues: { setViewId },
|
||||
issuesFilter: { issueFilters, fetchFilters },
|
||||
issuesFilter: { issueFilters, fetchFilters, updateFilterExpression },
|
||||
} = useIssues(EIssuesStoreType.PROFILE);
|
||||
// derived values
|
||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (setViewId) setViewId(type);
|
||||
|
|
@ -44,22 +47,33 @@ export const ProfileIssuesPage = observer((props: Props) => {
|
|||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
||||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.PROFILE}>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<ProfileIssuesAppliedFiltersRoot />
|
||||
<div className="-z-1 relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProfileIssuesListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProfileIssuesKanBanLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
<WorkspaceLevelWorkItemFiltersHOC
|
||||
entityId={userId}
|
||||
entityType={EIssuesStoreType.PROFILE}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues.filters}
|
||||
initialWorkItemFilters={issueFilters}
|
||||
updateFilters={updateFilterExpression.bind(updateFilterExpression, workspaceSlug, userId)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: profileWorkItemsFilter }) => (
|
||||
<>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
{profileWorkItemsFilter && <WorkItemFiltersRow filter={profileWorkItemsFilter} />}
|
||||
<div className="-z-1 relative h-full w-full overflow-auto">
|
||||
{activeLayout === "list" ? (
|
||||
<ProfileIssuesListLayout />
|
||||
) : activeLayout === "kanban" ? (
|
||||
<ProfileIssuesKanBanLayout />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
</>
|
||||
)}
|
||||
</WorkspaceLevelWorkItemFiltersHOC>
|
||||
</IssuesStoreContext.Provider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export const ProfileSidebar: FC<TProfileSidebar> = observer((props) => {
|
|||
className="h-full w-full rounded object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-[52px] w-[52px] items-center justify-center rounded bg-custom-background-90 capitalize text-custom-text-100">
|
||||
<div className="flex h-[52px] w-[52px] items-center justify-center rounded bg-[#028375] capitalize text-white">
|
||||
{userData?.first_name?.[0]}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ListFilter } from "lucide-react";
|
|||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { LOGICAL_OPERATOR, TExternalFilter, TFilterProperty } from "@plane/types";
|
||||
import { CustomSearchSelect, getButtonStyling, TButtonVariant } from "@plane/ui";
|
||||
import { CustomSearchSelect, getButtonStyling, setToast, TButtonVariant, TOAST_TYPE } from "@plane/ui";
|
||||
import { cn, getOperatorForPayload } from "@plane/utils";
|
||||
|
||||
export type TAddFilterButtonProps<P extends TFilterProperty, E extends TExternalFilter> = {
|
||||
|
|
@ -15,7 +15,7 @@ export type TAddFilterButtonProps<P extends TFilterProperty, E extends TExternal
|
|||
defaultOpen?: boolean;
|
||||
iconConfig?: {
|
||||
shouldShowIcon: boolean;
|
||||
iconComponent?: React.ReactNode;
|
||||
iconComponent?: React.ElementType;
|
||||
};
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
|
@ -34,6 +34,8 @@ export const AddFilterButton = observer(
|
|||
iconConfig = { shouldShowIcon: true },
|
||||
isDisabled = false,
|
||||
} = buttonConfig || {};
|
||||
// derived values
|
||||
const FilterIcon = iconConfig.iconComponent || ListFilter;
|
||||
|
||||
// Transform available filter configs to CustomSearchSelect options format
|
||||
const filterOptions = filter.configManager.allAvailableConfigs.map((config) => ({
|
||||
|
|
@ -64,7 +66,7 @@ export const AddFilterButton = observer(
|
|||
|
||||
const handleFilterSelect = (property: P) => {
|
||||
const config = filter.configManager.getConfigByProperty(property);
|
||||
if (config && config.firstOperator) {
|
||||
if (config?.firstOperator) {
|
||||
const { operator, isNegation } = getOperatorForPayload(config.firstOperator);
|
||||
filter.addCondition(
|
||||
LOGICAL_OPERATOR.AND,
|
||||
|
|
@ -76,6 +78,12 @@ export const AddFilterButton = observer(
|
|||
isNegation
|
||||
);
|
||||
onFilterSelect?.(property);
|
||||
} else {
|
||||
setToast({
|
||||
title: "Filter configuration error",
|
||||
message: "This filter is not properly configured and cannot be applied",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -91,11 +99,10 @@ export const AddFilterButton = observer(
|
|||
maxHeight="full"
|
||||
placement="bottom-start"
|
||||
disabled={isDisabled}
|
||||
customButtonClassName={cn(getButtonStyling(variant, "sm"), className)}
|
||||
customButtonClassName={cn(getButtonStyling(variant, "sm"), "py-[5px]", className)}
|
||||
customButton={
|
||||
<div className="flex items-center gap-1">
|
||||
{iconConfig.shouldShowIcon &&
|
||||
(iconConfig.iconComponent || <ListFilter className="size-4 text-custom-text-200" />)}
|
||||
{iconConfig.shouldShowIcon && <FilterIcon className="size-4 text-custom-text-200" />}
|
||||
{label}
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
|
|
@ -15,7 +15,6 @@ export type TFiltersRowProps<K extends TFilterProperty, E extends TExternalFilte
|
|||
filter: IFilterInstance<K, E>;
|
||||
variant?: "default" | "header";
|
||||
visible?: boolean;
|
||||
maxVisibleConditions?: number;
|
||||
trackerElements?: {
|
||||
clearFilter?: string;
|
||||
saveView?: string;
|
||||
|
|
@ -31,25 +30,10 @@ export const FiltersRow = observer(
|
|||
filter,
|
||||
variant = "header",
|
||||
visible = true,
|
||||
maxVisibleConditions = 3,
|
||||
trackerElements,
|
||||
} = props;
|
||||
// states
|
||||
const [showAllConditions, setShowAllConditions] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
// derived values
|
||||
const visibleConditions = useMemo(() => {
|
||||
if (variant === "default" || !maxVisibleConditions || showAllConditions) {
|
||||
return filter.allConditionsForDisplay;
|
||||
}
|
||||
return filter.allConditionsForDisplay.slice(0, maxVisibleConditions);
|
||||
}, [filter.allConditionsForDisplay, maxVisibleConditions, showAllConditions, variant]);
|
||||
const hiddenConditionsCount = useMemo(() => {
|
||||
if (variant === "default" || !maxVisibleConditions || showAllConditions) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, filter.allConditionsForDisplay.length - maxVisibleConditions);
|
||||
}, [filter.allConditionsForDisplay.length, maxVisibleConditions, showAllConditions, variant]);
|
||||
|
||||
const handleUpdate = useCallback(async () => {
|
||||
setIsUpdating(true);
|
||||
|
|
@ -61,44 +45,17 @@ export const FiltersRow = observer(
|
|||
|
||||
const leftContent = (
|
||||
<>
|
||||
{filter.allConditionsForDisplay.map((condition) => (
|
||||
<FilterItem key={condition.id} filter={filter} condition={condition} isDisabled={disabledAllOperations} />
|
||||
))}
|
||||
<AddFilterButton
|
||||
filter={filter}
|
||||
buttonConfig={{
|
||||
variant: "neutral-primary",
|
||||
...buttonConfig,
|
||||
isDisabled: disabledAllOperations,
|
||||
}}
|
||||
onFilterSelect={() => {
|
||||
if (variant === "header") {
|
||||
setShowAllConditions(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{visibleConditions.map((condition) => (
|
||||
<FilterItem key={condition.id} filter={filter} condition={condition} isDisabled={disabledAllOperations} />
|
||||
))}
|
||||
{variant === "header" && hiddenConditionsCount > 0 && (
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
className={COMMON_VISIBILITY_BUTTON_CLASSNAME}
|
||||
onClick={() => setShowAllConditions(true)}
|
||||
>
|
||||
+{hiddenConditionsCount} more
|
||||
</Button>
|
||||
)}
|
||||
{variant === "header" &&
|
||||
showAllConditions &&
|
||||
maxVisibleConditions &&
|
||||
filter.allConditionsForDisplay.length > maxVisibleConditions && (
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
className={COMMON_VISIBILITY_BUTTON_CLASSNAME}
|
||||
onClick={() => setShowAllConditions(false)}
|
||||
>
|
||||
Show less
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
@ -162,7 +119,6 @@ export const FiltersRow = observer(
|
|||
}
|
||||
);
|
||||
|
||||
const COMMON_VISIBILITY_BUTTON_CLASSNAME = "py-0.5 px-2 text-custom-text-300 hover:text-custom-text-100 rounded-full";
|
||||
const COMMON_OPERATION_BUTTON_CLASSNAME = "py-1";
|
||||
|
||||
type TElementTransitionProps = {
|
||||
|
|
|
|||
|
|
@ -1,53 +1,47 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Layers } from "lucide-react";
|
||||
// plane constants
|
||||
// plane imports
|
||||
import { ETabIndices, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmojiPicker, EmojiIconPickerTypes } from "@plane/propel/emoji-icon-picker";
|
||||
// types
|
||||
import {
|
||||
EViewAccess,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
IProjectView,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
IIssueFilters,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties, getTabIndex } from "@plane/utils";
|
||||
// components
|
||||
import { Logo } from "@/components/common/logo";
|
||||
import {
|
||||
AppliedFiltersList,
|
||||
DisplayFiltersSelection,
|
||||
FilterSelection,
|
||||
FiltersDropdown,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
// hooks
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
// plane web imports
|
||||
import { AccessController } from "@/plane-web/components/views/access-controller";
|
||||
// local imports
|
||||
import { LayoutDropDown } from "../dropdowns/layout";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "../work-item-filters/filters-hoc/project-level";
|
||||
|
||||
type Props = {
|
||||
data?: IProjectView | null;
|
||||
handleClose: () => void;
|
||||
handleFormSubmit: (values: IProjectView) => Promise<void>;
|
||||
preLoadedData?: Partial<IProjectView> | null;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IProjectView> = {
|
||||
const DEFAULT_VALUES: Partial<IProjectView> = {
|
||||
name: "",
|
||||
description: "",
|
||||
access: EViewAccess.PUBLIC,
|
||||
|
|
@ -56,23 +50,24 @@ const defaultValues: Partial<IProjectView> = {
|
|||
};
|
||||
|
||||
export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
||||
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
|
||||
const { handleFormSubmit, handleClose, data, preLoadedData, projectId, workspaceSlug } = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// store hooks
|
||||
const { currentProjectDetails } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const { projectLabels } = useLabel();
|
||||
const {
|
||||
project: { projectMemberIds },
|
||||
} = useMember();
|
||||
const { getProjectById } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
// form info
|
||||
const defaultValues = {
|
||||
...DEFAULT_VALUES,
|
||||
...preLoadedData,
|
||||
...data,
|
||||
};
|
||||
const {
|
||||
control,
|
||||
formState: { errors, isSubmitting },
|
||||
getValues,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
|
|
@ -80,53 +75,23 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
} = useForm<IProjectView>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
// derived values
|
||||
const projectDetails = getProjectById(projectId);
|
||||
const logoValue = watch("logo_props");
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.PROJECT_VIEW, isMobile);
|
||||
|
||||
const selectedFilters: IIssueFilterOptions = {};
|
||||
Object.entries(watch("filters") ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
|
||||
selectedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
// for removing filters from a key
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
// If value is null then remove all the filters of that key
|
||||
if (!value) {
|
||||
setValue("filters", {
|
||||
...selectedFilters,
|
||||
[key]: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newValues = selectedFilters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (newValues.includes(val)) newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
} else {
|
||||
if (selectedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
}
|
||||
|
||||
setValue("filters", {
|
||||
...selectedFilters,
|
||||
[key]: newValues,
|
||||
});
|
||||
const workItemFilters: IIssueFilters = {
|
||||
richFilters: getValues("rich_filters"),
|
||||
displayFilters: getValues("display_filters"),
|
||||
displayProperties: getValues("display_properties"),
|
||||
kanbanFilters: undefined,
|
||||
};
|
||||
const { getIndex } = getTabIndex(ETabIndices.PROJECT_VIEW, isMobile);
|
||||
|
||||
const handleCreateUpdateView = async (formData: IProjectView) => {
|
||||
await handleFormSubmit({
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
logo_props: formData.logo_props,
|
||||
filters: formData.filters,
|
||||
rich_filters: formData.rich_filters,
|
||||
display_filters: formData.display_filters,
|
||||
display_properties: formData.display_properties,
|
||||
access: formData.access,
|
||||
|
|
@ -137,20 +102,6 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
});
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
if (!selectedFilters) return;
|
||||
|
||||
setValue("filters", {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...preLoadedData,
|
||||
...data,
|
||||
});
|
||||
}, [data, preLoadedData, reset]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
||||
<div className="space-y-5 p-5">
|
||||
|
|
@ -263,43 +214,6 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
}
|
||||
value={displayFilters.layout}
|
||||
/>
|
||||
|
||||
{/* filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="filters"
|
||||
render={({ field: { onChange, value: filters } }) => (
|
||||
<FiltersDropdown title={t("common.filters")} tabIndex={getIndex("filters")}>
|
||||
<FilterSelection
|
||||
filters={filters ?? {}}
|
||||
handleFiltersUpdate={(key, value) => {
|
||||
const newValues = filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
onChange({
|
||||
...filters,
|
||||
[key]: newValues,
|
||||
});
|
||||
}}
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[displayFilters.layout]}
|
||||
labels={projectLabels ?? undefined}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* display filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -307,7 +221,9 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
render={({ field: { onChange: onDisplayPropertiesChange, value: displayProperties } }) => (
|
||||
<FiltersDropdown title={t("common.display")}>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[displayFilters.layout]}
|
||||
layoutDisplayFiltersOptions={
|
||||
ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.layoutOptions[displayFilters.layout]
|
||||
}
|
||||
displayFilters={displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
onDisplayFiltersChange({
|
||||
|
|
@ -324,8 +240,8 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
...updatedDisplayProperties,
|
||||
});
|
||||
}}
|
||||
cycleViewDisabled={!currentProjectDetails?.cycle_view}
|
||||
moduleViewDisabled={!currentProjectDetails?.module_view}
|
||||
cycleViewDisabled={!projectDetails?.cycle_view}
|
||||
moduleViewDisabled={!projectDetails?.module_view}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
|
|
@ -334,17 +250,31 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
{selectedFilters && Object.keys(selectedFilters).length > 0 && (
|
||||
<div>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={selectedFilters}
|
||||
handleClearAllFilters={clearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={projectLabels ?? []}
|
||||
states={projectStates}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{/* filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="rich_filters"
|
||||
render={({ field: { onChange: onFiltersChange } }) => (
|
||||
<ProjectLevelWorkItemFiltersHOC
|
||||
entityId={data?.id}
|
||||
entityType={EIssuesStoreType.PROJECT_VIEW}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.issues.filters}
|
||||
initialWorkItemFilters={workItemFilters}
|
||||
isTemporary
|
||||
updateFilters={(updateFilters) => onFiltersChange(updateFilters)}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: projectViewWorkItemsFilter }) =>
|
||||
projectViewWorkItemsFilter && (
|
||||
<WorkItemFiltersRow filter={projectViewWorkItemsFilter} variant="default" />
|
||||
)
|
||||
}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { EIssueLayoutTypes } from "@plane/types";
|
||||
import { WorkspaceSpreadsheetRoot } from "@/components/issues/issue-layouts/spreadsheet/roots/workspace-root";
|
||||
import { WorkspaceAdditionalLayouts } from "@/plane-web/components/views/helper";
|
||||
import { WorkspaceSpreadsheetRoot } from "../issues/issue-layouts/spreadsheet/roots/workspace-root";
|
||||
|
||||
export type TWorkspaceLayoutProps = {
|
||||
activeLayout: EIssueLayoutTypes | undefined;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import { FC } from "react";
|
|||
import { observer } from "mobx-react";
|
||||
// types
|
||||
import { PROJECT_VIEW_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { IProjectView } from "@plane/types";
|
||||
import { EIssuesStoreType, IProjectView } from "@plane/types";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useIssues } from "@/hooks/store/use-issues";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import useKeypress from "@/hooks/use-keypress";
|
||||
// local imports
|
||||
|
|
@ -30,6 +32,10 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
|
|||
const router = useAppRouter();
|
||||
// store hooks
|
||||
const { createView, updateView } = useProjectView();
|
||||
const {
|
||||
issuesFilter: { mutateFilters },
|
||||
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
const { resetExpression } = useWorkItemFilters();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
|
|
@ -66,7 +72,9 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
|
|||
|
||||
const handleUpdateView = async (payload: IProjectView) => {
|
||||
await updateView(workspaceSlug, projectId, data?.id as string, payload)
|
||||
.then(() => {
|
||||
.then((viewDetails) => {
|
||||
mutateFilters(workspaceSlug, viewDetails.id, viewDetails);
|
||||
resetExpression(EIssuesStoreType.PROJECT_VIEW, viewDetails.id, viewDetails.rich_filters);
|
||||
handleClose();
|
||||
captureSuccess({
|
||||
eventName: PROJECT_VIEW_TRACKER_EVENTS.update,
|
||||
|
|
@ -106,6 +114,8 @@ export const CreateUpdateProjectViewModal: FC<Props> = observer((props) => {
|
|||
handleClose={handleClose}
|
||||
handleFormSubmit={handleFormSubmit}
|
||||
preLoadedData={preLoadedData}
|
||||
projectId={projectId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import { SetStateAction, useEffect, useState } from "react";
|
||||
import { Button } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
isLocked: boolean;
|
||||
areFiltersEqual: boolean;
|
||||
isOwner: boolean;
|
||||
isAuthorizedUser: boolean;
|
||||
setIsModalOpen: (value: SetStateAction<boolean>) => void;
|
||||
handleUpdateView: () => void;
|
||||
lockedTooltipContent?: string;
|
||||
trackerElement: string;
|
||||
};
|
||||
|
||||
export const UpdateViewComponent = (props: Props) => {
|
||||
const { isLocked, areFiltersEqual, isOwner, isAuthorizedUser, setIsModalOpen, handleUpdateView, trackerElement } =
|
||||
props;
|
||||
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (areFiltersEqual) {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [areFiltersEqual]);
|
||||
|
||||
// Change state while updating view to have a feedback
|
||||
const updateButton = isUpdating ? (
|
||||
<Button variant="primary" size="sm" className="flex-shrink-0">
|
||||
Updating...
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="flex-shrink-0"
|
||||
onClick={() => {
|
||||
setIsUpdating(true);
|
||||
handleUpdateView();
|
||||
}}
|
||||
>
|
||||
Update view
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 h-fit">
|
||||
{!isLocked && !areFiltersEqual && isAuthorizedUser && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="md"
|
||||
className="flex-shrink-0"
|
||||
data-ph-element={trackerElement}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
>
|
||||
Save as
|
||||
</Button>
|
||||
{isOwner && <>{updateButton}</>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@ import { useLocalStorage } from "@plane/hooks";
|
|||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { EViewAccess, IProjectView } from "@plane/types";
|
||||
import { FavoriteStar } from "@plane/ui";
|
||||
import { calculateTotalFilters, getPublishViewLink } from "@plane/utils";
|
||||
import { getPublishViewLink } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
|
|
@ -52,8 +52,6 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
|||
EUserPermissionsLevel.PROJECT
|
||||
);
|
||||
|
||||
const totalFilters = calculateTotalFilters(view.filters ?? {});
|
||||
|
||||
const access = view.access;
|
||||
|
||||
const publishLink = getPublishViewLink(view?.anchor);
|
||||
|
|
@ -87,10 +85,6 @@ export const ViewListItemAction: FC<Props> = observer((props) => {
|
|||
/>
|
||||
)}
|
||||
<DeleteProjectViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||
<p className="hidden rounded bg-custom-background-80 px-2 py-1 text-xs text-custom-text-200 group-hover:block">
|
||||
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
|
||||
</p>
|
||||
|
||||
<div className="cursor-default text-custom-text-300">
|
||||
<Tooltip tooltipContent={access === EViewAccess.PUBLIC ? "Public" : "Private"}>
|
||||
{access === EViewAccess.PUBLIC ? <Earth className="h-4 w-4" /> : <Lock className="h-4 w-4" />}
|
||||
|
|
|
|||
100
apps/web/core/components/work-item-filters/filters-hoc/base.tsx
Normal file
100
apps/web/core/components/work-item-filters/filters-hoc/base.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// plane imports
|
||||
import { TSaveViewOptions, TUpdateViewOptions } from "@plane/constants";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { IIssueFilters, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
// store hooks
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
// plane web imports
|
||||
import {
|
||||
TWorkItemFiltersEntityProps,
|
||||
useWorkItemFiltersConfig,
|
||||
} from "@/plane-web/hooks/work-item-filters/use-work-item-filters-config";
|
||||
// local imports
|
||||
import { TSharedWorkItemFiltersHOCProps, TSharedWorkItemFiltersProps } from "./shared";
|
||||
|
||||
type TAdditionalWorkItemFiltersProps = {
|
||||
saveViewOptions?: TSaveViewOptions<TWorkItemFilterExpression>;
|
||||
updateViewOptions?: TUpdateViewOptions<TWorkItemFilterExpression>;
|
||||
} & TWorkItemFiltersEntityProps;
|
||||
|
||||
type TWorkItemFiltersHOCProps = TSharedWorkItemFiltersHOCProps & TAdditionalWorkItemFiltersProps;
|
||||
|
||||
export const WorkItemFiltersHOC = observer((props: TWorkItemFiltersHOCProps) => {
|
||||
const { children, initialWorkItemFilters } = props;
|
||||
|
||||
// Only initialize filter instance when initial work item filters are defined
|
||||
if (!initialWorkItemFilters)
|
||||
return <>{typeof children === "function" ? children({ filter: undefined }) : children}</>;
|
||||
|
||||
return (
|
||||
<WorkItemFilterRoot {...props} initialWorkItemFilters={initialWorkItemFilters}>
|
||||
{children}
|
||||
</WorkItemFilterRoot>
|
||||
);
|
||||
});
|
||||
|
||||
type TWorkItemFilterProps = TSharedWorkItemFiltersProps &
|
||||
TAdditionalWorkItemFiltersProps & {
|
||||
initialWorkItemFilters: IIssueFilters;
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((props: { filter: IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> }) => React.ReactNode);
|
||||
};
|
||||
|
||||
const WorkItemFilterRoot = observer((props: TWorkItemFilterProps) => {
|
||||
const {
|
||||
children,
|
||||
entityType,
|
||||
entityId,
|
||||
filtersToShowByLayout,
|
||||
initialWorkItemFilters,
|
||||
isTemporary,
|
||||
saveViewOptions,
|
||||
updateFilters,
|
||||
updateViewOptions,
|
||||
...entityConfigProps
|
||||
} = props;
|
||||
// store hooks
|
||||
const { getOrCreateFilter, deleteFilter } = useWorkItemFilters();
|
||||
// derived values
|
||||
const workItemEntityID = useMemo(
|
||||
() => (isTemporary ? `TEMP-${entityId ?? uuidv4()}` : entityId),
|
||||
[isTemporary, entityId]
|
||||
);
|
||||
// memoize initial values to prevent re-computations when reference changes
|
||||
const initialUserFilters = useMemo(
|
||||
() => initialWorkItemFilters.richFilters,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
); // Empty dependency array to capture only the initial value
|
||||
const workItemFiltersConfig = useWorkItemFiltersConfig({
|
||||
allowedFilters: filtersToShowByLayout ? filtersToShowByLayout : [],
|
||||
...entityConfigProps,
|
||||
});
|
||||
// get or create filter instance
|
||||
const workItemLayoutFilter = getOrCreateFilter({
|
||||
entityType,
|
||||
entityId: workItemEntityID,
|
||||
initialExpression: initialUserFilters,
|
||||
onExpressionChange: updateFilters,
|
||||
expressionOptions: {
|
||||
saveViewOptions,
|
||||
updateViewOptions,
|
||||
},
|
||||
});
|
||||
|
||||
// delete filter instance when component unmounts
|
||||
useEffect(
|
||||
() => () => {
|
||||
deleteFilter(entityType, workItemEntityID);
|
||||
},
|
||||
[deleteFilter, entityType, workItemEntityID]
|
||||
);
|
||||
|
||||
workItemLayoutFilter.configManager.registerAll(workItemFiltersConfig.configs);
|
||||
|
||||
return <>{typeof children === "function" ? children({ filter: workItemLayoutFilter }) : children}</>;
|
||||
});
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
import { useCallback, useMemo, useState } from "react";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, PROJECT_VIEW_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { EUserProjectRoles, EViewAccess, IProjectView, TWorkItemFilterExpression } from "@plane/types";
|
||||
// components
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { removeNillKeys } from "@/components/issues/issue-layouts/utils";
|
||||
import { CreateUpdateProjectViewModal } from "@/components/views/modal";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
// plane web imports
|
||||
import { getAdditionalProjectLevelFiltersHOCProps } from "@/plane-web/helpers/work-item-filters/project-level";
|
||||
// local imports
|
||||
import { WorkItemFiltersHOC } from "./base";
|
||||
import { TEnableSaveViewProps, TEnableUpdateViewProps, TSharedWorkItemFiltersHOCProps } from "./shared";
|
||||
|
||||
type TProjectLevelWorkItemFiltersHOCProps = TSharedWorkItemFiltersHOCProps & {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
} & TEnableSaveViewProps &
|
||||
TEnableUpdateViewProps;
|
||||
|
||||
export const ProjectLevelWorkItemFiltersHOC = observer((props: TProjectLevelWorkItemFiltersHOCProps) => {
|
||||
const { children, enableSaveView, enableUpdateView, entityId, initialWorkItemFilters, projectId, workspaceSlug } =
|
||||
props;
|
||||
// states
|
||||
const [isCreateViewModalOpen, setIsCreateViewModalOpen] = useState(false);
|
||||
const [createViewPayload, setCreateViewPayload] = useState<Partial<IProjectView> | null>(null);
|
||||
// hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { getViewById, updateView } = useProjectView();
|
||||
const { data: currentUser } = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { getProjectCycleIds } = useCycle();
|
||||
const { getProjectLabelIds } = useLabel();
|
||||
const {
|
||||
project: { getProjectMemberIds },
|
||||
} = useMember();
|
||||
const { getProjectModuleIds } = useModule();
|
||||
const { getProjectStateIds } = useProjectState();
|
||||
// derived values
|
||||
const hasProjectMemberLevelPermissions = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug,
|
||||
projectId
|
||||
);
|
||||
const projectDetails = getProjectById(projectId);
|
||||
const viewDetails = entityId ? getViewById(entityId) : null;
|
||||
const isViewLocked = viewDetails ? viewDetails?.is_locked : false;
|
||||
const isCurrentUserOwner = viewDetails ? viewDetails.owned_by === currentUser?.id : false;
|
||||
const canCreateView = useMemo(
|
||||
() =>
|
||||
projectDetails?.issue_views_view === true &&
|
||||
enableSaveView &&
|
||||
!props.saveViewOptions?.isDisabled &&
|
||||
hasProjectMemberLevelPermissions,
|
||||
[
|
||||
projectDetails?.issue_views_view,
|
||||
enableSaveView,
|
||||
props.saveViewOptions?.isDisabled,
|
||||
hasProjectMemberLevelPermissions,
|
||||
]
|
||||
);
|
||||
const canUpdateView = useMemo(
|
||||
() =>
|
||||
enableUpdateView &&
|
||||
!props.updateViewOptions?.isDisabled &&
|
||||
!isViewLocked &&
|
||||
hasProjectMemberLevelPermissions &&
|
||||
isCurrentUserOwner,
|
||||
[
|
||||
enableUpdateView,
|
||||
props.updateViewOptions?.isDisabled,
|
||||
isViewLocked,
|
||||
hasProjectMemberLevelPermissions,
|
||||
isCurrentUserOwner,
|
||||
]
|
||||
);
|
||||
|
||||
const getDefaultViewDetailPayload: () => Partial<IProjectView> = useCallback(
|
||||
() => ({
|
||||
name: viewDetails ? `${viewDetails?.name} 2` : "Untitled",
|
||||
description: viewDetails ? viewDetails.description : "",
|
||||
logo_props: viewDetails ? viewDetails.logo_props : undefined,
|
||||
access: viewDetails ? viewDetails.access : EViewAccess.PUBLIC,
|
||||
}),
|
||||
[viewDetails]
|
||||
);
|
||||
|
||||
const getViewFilterPayload: (filterExpression: TWorkItemFilterExpression) => Partial<IProjectView> = useCallback(
|
||||
(filterExpression: TWorkItemFilterExpression) => ({
|
||||
rich_filters: cloneDeep(filterExpression),
|
||||
display_filters: cloneDeep(initialWorkItemFilters?.displayFilters),
|
||||
display_properties: cloneDeep(initialWorkItemFilters?.displayProperties),
|
||||
}),
|
||||
[initialWorkItemFilters]
|
||||
);
|
||||
|
||||
const handleViewUpdate = useCallback(
|
||||
(filterExpression: TWorkItemFilterExpression) => {
|
||||
if (!viewDetails) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "We couldn't find the view",
|
||||
message: "The view you're trying to update doesn't exist.",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateView(workspaceSlug, projectId, viewDetails.id, {
|
||||
...getViewFilterPayload(filterExpression),
|
||||
})
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Your view has been updated successfully.",
|
||||
});
|
||||
captureSuccess({
|
||||
eventName: PROJECT_VIEW_TRACKER_EVENTS.update,
|
||||
payload: {
|
||||
view_id: viewDetails.id,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Your view could not be updated. Please try again.",
|
||||
});
|
||||
captureError({
|
||||
eventName: PROJECT_VIEW_TRACKER_EVENTS.update,
|
||||
payload: {
|
||||
view_id: viewDetails.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[viewDetails, updateView, workspaceSlug, projectId, getViewFilterPayload]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateProjectViewModal
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
preLoadedData={createViewPayload}
|
||||
isOpen={isCreateViewModalOpen}
|
||||
onClose={() => {
|
||||
setCreateViewPayload(null);
|
||||
setIsCreateViewModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
<WorkItemFiltersHOC
|
||||
{...props}
|
||||
{...getAdditionalProjectLevelFiltersHOCProps({
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
})}
|
||||
cycleIds={getProjectCycleIds(projectId) ?? undefined}
|
||||
labelIds={getProjectLabelIds(projectId)}
|
||||
memberIds={getProjectMemberIds(projectId, false) ?? undefined}
|
||||
moduleIds={getProjectModuleIds(projectId) ?? undefined}
|
||||
stateIds={getProjectStateIds(projectId)}
|
||||
saveViewOptions={{
|
||||
label: props.saveViewOptions?.label,
|
||||
isDisabled: !canCreateView,
|
||||
onViewSave: (expression) => {
|
||||
setCreateViewPayload({
|
||||
...getDefaultViewDetailPayload(),
|
||||
...getViewFilterPayload(expression),
|
||||
});
|
||||
setIsCreateViewModalOpen(true);
|
||||
},
|
||||
}}
|
||||
updateViewOptions={{
|
||||
label: props.updateViewOptions?.label,
|
||||
isDisabled: !canUpdateView,
|
||||
hasAdditionalChanges:
|
||||
!isEqual(initialWorkItemFilters?.displayFilters, viewDetails?.display_filters) ||
|
||||
!isEqual(
|
||||
removeNillKeys(initialWorkItemFilters?.displayProperties),
|
||||
removeNillKeys(viewDetails?.display_properties)
|
||||
),
|
||||
onViewUpdate: handleViewUpdate,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WorkItemFiltersHOC>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// plane imports
|
||||
import { TSaveViewOptions, TUpdateViewOptions } from "@plane/constants";
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { EIssuesStoreType, IIssueFilters, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
|
||||
export type TSharedWorkItemFiltersProps = {
|
||||
entityType: EIssuesStoreType; // entity type (project, cycle, workspace, teamspace, etc)
|
||||
filtersToShowByLayout: TWorkItemFilterProperty[];
|
||||
updateFilters: (updatedFilters: TWorkItemFilterExpression) => void;
|
||||
isTemporary?: boolean;
|
||||
} & ({ isTemporary: true; entityId?: string } | { isTemporary?: false; entityId: string }); // entity id (project_id, cycle_id, workspace_id, etc)
|
||||
|
||||
export type TSharedWorkItemFiltersHOCProps = TSharedWorkItemFiltersProps & {
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((props: {
|
||||
filter: IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined;
|
||||
}) => React.ReactNode);
|
||||
initialWorkItemFilters: IIssueFilters | undefined;
|
||||
};
|
||||
|
||||
export type TEnableSaveViewProps = {
|
||||
enableSaveView?: boolean;
|
||||
saveViewOptions?: Omit<TSaveViewOptions<TWorkItemFilterExpression>, "onViewSave">;
|
||||
};
|
||||
|
||||
export type TEnableUpdateViewProps = {
|
||||
enableUpdateView?: boolean;
|
||||
updateViewOptions?: Omit<TUpdateViewOptions<TWorkItemFilterExpression>, "onViewUpdate">;
|
||||
};
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
import { useCallback, useMemo, useState } from "react";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { DEFAULT_GLOBAL_VIEWS_LIST, EUserPermissionsLevel, GLOBAL_VIEW_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { EUserProjectRoles, EViewAccess, IWorkspaceView, TWorkItemFilterExpression } from "@plane/types";
|
||||
// components
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { removeNillKeys } from "@/components/issues/issue-layouts/utils";
|
||||
import { CreateUpdateWorkspaceViewModal } from "@/components/workspace/views/modal";
|
||||
// hooks
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useGlobalView } from "@/hooks/store/use-global-view";
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
// local imports
|
||||
import { WorkItemFiltersHOC } from "./base";
|
||||
import { TEnableSaveViewProps, TEnableUpdateViewProps, TSharedWorkItemFiltersHOCProps } from "./shared";
|
||||
|
||||
type TWorkspaceLevelWorkItemFiltersHOCProps = TSharedWorkItemFiltersHOCProps & {
|
||||
workspaceSlug: string;
|
||||
} & TEnableSaveViewProps &
|
||||
TEnableUpdateViewProps;
|
||||
|
||||
export const WorkspaceLevelWorkItemFiltersHOC = observer((props: TWorkspaceLevelWorkItemFiltersHOCProps) => {
|
||||
const { children, enableSaveView, enableUpdateView, entityId, initialWorkItemFilters, workspaceSlug } = props;
|
||||
// states
|
||||
const [isCreateViewModalOpen, setIsCreateViewModalOpen] = useState(false);
|
||||
const [createViewPayload, setCreateViewPayload] = useState<Partial<IWorkspaceView> | undefined>(undefined);
|
||||
// hooks
|
||||
const { getViewDetailsById, updateGlobalView } = useGlobalView();
|
||||
const { data: currentUser } = useUser();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { joinedProjectIds } = useProject();
|
||||
const {
|
||||
workspace: { getWorkspaceMemberIds },
|
||||
} = useMember();
|
||||
const { getWorkspaceLabelIds } = useLabel();
|
||||
// derived values
|
||||
const hasWorkspaceMemberLevelPermissions = allowPermissions(
|
||||
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE,
|
||||
workspaceSlug
|
||||
);
|
||||
const viewDetails = entityId ? getViewDetailsById(entityId) : null;
|
||||
const isDefaultView = typeof entityId === "string" && DEFAULT_GLOBAL_VIEWS_LIST.some((view) => view.key === entityId);
|
||||
const isViewLocked = viewDetails ? viewDetails?.is_locked : false;
|
||||
const isCurrentUserOwner = viewDetails ? viewDetails.owned_by === currentUser?.id : false;
|
||||
const canCreateView = useMemo(
|
||||
() => enableSaveView && !props.saveViewOptions?.isDisabled && hasWorkspaceMemberLevelPermissions,
|
||||
[enableSaveView, props.saveViewOptions?.isDisabled, hasWorkspaceMemberLevelPermissions]
|
||||
);
|
||||
const canUpdateView = useMemo(
|
||||
() =>
|
||||
enableUpdateView &&
|
||||
!isDefaultView &&
|
||||
!props.updateViewOptions?.isDisabled &&
|
||||
!isViewLocked &&
|
||||
hasWorkspaceMemberLevelPermissions &&
|
||||
isCurrentUserOwner,
|
||||
[
|
||||
enableUpdateView,
|
||||
props.updateViewOptions?.isDisabled,
|
||||
isDefaultView,
|
||||
isViewLocked,
|
||||
hasWorkspaceMemberLevelPermissions,
|
||||
isCurrentUserOwner,
|
||||
]
|
||||
);
|
||||
|
||||
const getDefaultViewDetailPayload: () => Partial<IWorkspaceView> = useCallback(
|
||||
() => ({
|
||||
name: viewDetails ? `${viewDetails?.name} 2` : "Untitled",
|
||||
description: viewDetails ? viewDetails.description : "",
|
||||
access: viewDetails ? viewDetails.access : EViewAccess.PUBLIC,
|
||||
}),
|
||||
[viewDetails]
|
||||
);
|
||||
|
||||
const getViewFilterPayload: (filterExpression: TWorkItemFilterExpression) => Partial<IWorkspaceView> = useCallback(
|
||||
(filterExpression: TWorkItemFilterExpression) => ({
|
||||
rich_filters: cloneDeep(filterExpression),
|
||||
display_filters: cloneDeep(initialWorkItemFilters?.displayFilters),
|
||||
display_properties: cloneDeep(initialWorkItemFilters?.displayProperties),
|
||||
}),
|
||||
[initialWorkItemFilters]
|
||||
);
|
||||
|
||||
const handleViewUpdate = useCallback(
|
||||
(filterExpression: TWorkItemFilterExpression) => {
|
||||
if (!viewDetails) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "We couldn't find the view",
|
||||
message: "The view you're trying to update doesn't exist.",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateGlobalView(
|
||||
workspaceSlug,
|
||||
viewDetails.id,
|
||||
{
|
||||
...getViewFilterPayload(filterExpression),
|
||||
},
|
||||
/* No need to sync filters here as updateFilters already handles it */
|
||||
false
|
||||
)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Your view has been updated successfully.",
|
||||
});
|
||||
captureSuccess({
|
||||
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
|
||||
payload: {
|
||||
view_id: viewDetails.id,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Your view could not be updated. Please try again.",
|
||||
});
|
||||
captureError({
|
||||
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
|
||||
payload: {
|
||||
view_id: viewDetails.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[viewDetails, updateGlobalView, workspaceSlug, getViewFilterPayload]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateWorkspaceViewModal
|
||||
preLoadedData={createViewPayload}
|
||||
isOpen={isCreateViewModalOpen}
|
||||
onClose={() => {
|
||||
setCreateViewPayload(undefined);
|
||||
setIsCreateViewModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
<WorkItemFiltersHOC
|
||||
{...props}
|
||||
memberIds={getWorkspaceMemberIds(workspaceSlug)}
|
||||
labelIds={getWorkspaceLabelIds(workspaceSlug)}
|
||||
projectIds={joinedProjectIds}
|
||||
saveViewOptions={{
|
||||
label: props.saveViewOptions?.label,
|
||||
isDisabled: !canCreateView,
|
||||
onViewSave: (expression) => {
|
||||
setCreateViewPayload({
|
||||
...getDefaultViewDetailPayload(),
|
||||
...getViewFilterPayload(expression),
|
||||
});
|
||||
setIsCreateViewModalOpen(true);
|
||||
},
|
||||
}}
|
||||
updateViewOptions={{
|
||||
label: props.updateViewOptions?.label,
|
||||
isDisabled: !canUpdateView,
|
||||
hasAdditionalChanges:
|
||||
!isEqual(initialWorkItemFilters?.displayFilters, viewDetails?.display_filters) ||
|
||||
!isEqual(
|
||||
removeNillKeys(initialWorkItemFilters?.displayProperties),
|
||||
removeNillKeys(viewDetails?.display_properties)
|
||||
),
|
||||
onViewUpdate: handleViewUpdate,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WorkItemFiltersHOC>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
// components
|
||||
import { FiltersRow, TFiltersRowProps } from "@/components/rich-filters/filters-row";
|
||||
|
||||
type TWorkItemFiltersRowProps = TFiltersRowProps<TWorkItemFilterProperty, TWorkItemFilterExpression>;
|
||||
|
||||
export const WorkItemFiltersRow = observer((props: TWorkItemFiltersRowProps) => <FiltersRow {...props} />);
|
||||
|
|
@ -2,12 +2,11 @@
|
|||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText, HelpCircle, MessagesSquare, User } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { CustomMenu, ToggleSwitch } from "@plane/ui";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { cn } from "@plane/utils";
|
||||
import { ProductUpdatesModal } from "@/components/global";
|
||||
|
|
@ -16,7 +15,6 @@ import { ProductUpdatesModal } from "@/components/global";
|
|||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import { useTransient } from "@/hooks/store/use-transient";
|
||||
import { useUserSettings } from "@/hooks/store/user";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { PlaneVersionNumber } from "@/plane-web/components/global";
|
||||
|
|
@ -26,14 +24,12 @@ export interface WorkspaceHelpSectionProps {
|
|||
}
|
||||
|
||||
export const HelpMenu: React.FC<WorkspaceHelpSectionProps> = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleShortcutModal } = useCommandPalette();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { config } = useInstance();
|
||||
const { isIntercomToggle, toggleIntercom } = useTransient();
|
||||
const { canUseLocalDB, toggleLocalDB } = useUserSettings();
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
|
||||
|
|
@ -101,21 +97,6 @@ export const HelpMenu: React.FC<WorkspaceHelpSectionProps> = observer(() => {
|
|||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="my-1 border-t border-custom-border-200" />
|
||||
<CustomMenu.MenuItem>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="racking-tight">{t("hyper_mode")}</span>
|
||||
<ToggleSwitch
|
||||
value={canUseLocalDB}
|
||||
onChange={() => toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText, HelpCircle, MessagesSquare, MoveLeft, User } from "lucide-react";
|
||||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { CustomMenu, ToggleSwitch } from "@plane/ui";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { ProductUpdatesModal } from "@/components/global";
|
||||
|
|
@ -16,7 +15,6 @@ import { useAppTheme } from "@/hooks/store/use-app-theme";
|
|||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import { useTransient } from "@/hooks/store/use-transient";
|
||||
import { useUserSettings } from "@/hooks/store/user";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { PlaneVersionNumber } from "@/plane-web/components/global";
|
||||
|
|
@ -27,7 +25,6 @@ export interface WorkspaceHelpSectionProps {
|
|||
}
|
||||
|
||||
export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { sidebarCollapsed: isCollapsed, toggleSidebar, sidebarPeek, toggleSidebarPeek } = useAppTheme();
|
||||
|
|
@ -35,7 +32,6 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
const { isMobile } = usePlatformOS();
|
||||
const { config } = useInstance();
|
||||
const { isIntercomToggle, toggleIntercom } = useTransient();
|
||||
const { canUseLocalDB, toggleLocalDB } = useUserSettings();
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
|
||||
|
|
@ -104,21 +100,6 @@ export const SidebarHelpSection: React.FC<WorkspaceHelpSectionProps> = observer(
|
|||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="my-1 border-t border-custom-border-200" />
|
||||
<CustomMenu.MenuItem>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="racking-tight">{t("hyper_mode")}</span>
|
||||
<ToggleSwitch
|
||||
value={canUseLocalDB}
|
||||
onChange={() => toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@
|
|||
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { FileText, HelpCircle, MessagesSquare, User } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { CustomMenu, ToggleSwitch } from "@plane/ui";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { ProductUpdatesModal } from "@/components/global";
|
||||
import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
||||
|
|
@ -14,18 +13,15 @@ import { AppSidebarItem } from "@/components/sidebar/sidebar-item";
|
|||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
import { useTransient } from "@/hooks/store/use-transient";
|
||||
import { useUserSettings } from "@/hooks/store/user";
|
||||
// plane web components
|
||||
import { PlaneVersionNumber } from "@/plane-web/components/global";
|
||||
|
||||
export const HelpMenuRoot = observer(() => {
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
// store hooks
|
||||
const { t } = useTranslation();
|
||||
const { toggleShortcutModal } = useCommandPalette();
|
||||
const { config } = useInstance();
|
||||
const { isIntercomToggle, toggleIntercom } = useTransient();
|
||||
const { canUseLocalDB, toggleLocalDB } = useUserSettings();
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
const [isProductUpdatesModalOpen, setProductUpdatesModalOpen] = useState(false);
|
||||
|
|
@ -88,21 +84,6 @@ export const HelpMenuRoot = observer(() => {
|
|||
</a>
|
||||
</CustomMenu.MenuItem>
|
||||
<div className="my-1 border-t border-custom-border-200" />
|
||||
<CustomMenu.MenuItem>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex w-full items-center justify-between text-xs hover:bg-custom-background-80"
|
||||
>
|
||||
<span className="racking-tight">{t("hyper_mode")}</span>
|
||||
<ToggleSwitch
|
||||
value={canUseLocalDB}
|
||||
onChange={() => toggleLocalDB(workspaceSlug?.toString(), projectId?.toString())}
|
||||
/>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,35 +1,26 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// constant
|
||||
// plane imports
|
||||
import { ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
|
||||
// i18n
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// types
|
||||
import {
|
||||
EViewAccess,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
IWorkspaceView,
|
||||
EIssueLayoutTypes,
|
||||
EIssuesStoreType,
|
||||
IIssueFilters,
|
||||
} from "@plane/types";
|
||||
// ui
|
||||
import { Button, Input, TextArea } from "@plane/ui";
|
||||
// components
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/utils";
|
||||
import {
|
||||
AppliedFiltersList,
|
||||
DisplayFiltersSelection,
|
||||
FilterSelection,
|
||||
FiltersDropdown,
|
||||
} from "@/components/issues/issue-layouts/filters";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useLabel } from "@/hooks/store/use-label";
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
// components
|
||||
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
|
||||
import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
|
||||
// plane web imports
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/work-item-filters-row";
|
||||
import { AccessController } from "@/plane-web/components/views/access-controller";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -37,9 +28,10 @@ type Props = {
|
|||
handleClose: () => void;
|
||||
data?: IWorkspaceView;
|
||||
preLoadedData?: Partial<IWorkspaceView>;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IWorkspaceView> = {
|
||||
const DEFAULT_VALUES: Partial<IWorkspaceView> = {
|
||||
name: "",
|
||||
description: "",
|
||||
access: EViewAccess.PUBLIC,
|
||||
|
|
@ -51,78 +43,39 @@ const defaultValues: Partial<IWorkspaceView> = {
|
|||
};
|
||||
|
||||
export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
||||
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
|
||||
const { handleFormSubmit, handleClose, data, preLoadedData, workspaceSlug } = props;
|
||||
// i18n
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { workspaceLabels } = useLabel();
|
||||
const {
|
||||
workspace: { workspaceMemberIds },
|
||||
} = useMember();
|
||||
|
||||
// form info
|
||||
const defaultValues = {
|
||||
...DEFAULT_VALUES,
|
||||
...preLoadedData,
|
||||
...data,
|
||||
};
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
control,
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
getValues,
|
||||
} = useForm<IWorkspaceView>({
|
||||
defaultValues,
|
||||
});
|
||||
// derived values
|
||||
const workItemFilters: IIssueFilters = {
|
||||
richFilters: getValues("rich_filters"),
|
||||
displayFilters: getValues("display_filters"),
|
||||
displayProperties: getValues("display_properties"),
|
||||
kanbanFilters: undefined,
|
||||
};
|
||||
|
||||
const handleCreateUpdateView = async (formData: Partial<IWorkspaceView>) => {
|
||||
await handleFormSubmit(formData);
|
||||
|
||||
reset({
|
||||
...defaultValues,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
reset({
|
||||
...defaultValues,
|
||||
...preLoadedData,
|
||||
...data,
|
||||
});
|
||||
}, [data, preLoadedData, reset]);
|
||||
|
||||
const selectedFilters: IIssueFilterOptions = watch("filters");
|
||||
|
||||
// filters whose value not null or empty array
|
||||
let appliedFilters: IIssueFilterOptions | undefined = undefined;
|
||||
Object.entries(selectedFilters ?? {}).forEach(([key, value]) => {
|
||||
if (!value) return;
|
||||
if (Array.isArray(value) && value.length === 0) return;
|
||||
if (!appliedFilters) appliedFilters = {};
|
||||
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||
});
|
||||
|
||||
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||
// To clear all filters of any particular filter key.
|
||||
if (!value) {
|
||||
setValue("filters", {
|
||||
...selectedFilters,
|
||||
[key]: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let newValues = selectedFilters?.[key] ?? [];
|
||||
newValues = newValues.filter((val) => val !== value);
|
||||
|
||||
setValue("filters", {
|
||||
...selectedFilters,
|
||||
[key]: newValues,
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
if (!selectedFilters) return;
|
||||
|
||||
setValue("filters", {});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
||||
<div className="space-y-5 p-5">
|
||||
|
|
@ -176,39 +129,6 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
<div className="flex gap-2">
|
||||
<AccessController control={control} />
|
||||
{/* filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="filters"
|
||||
render={({ field: { onChange, value: filters } }) => (
|
||||
<FiltersDropdown title={t("common.filters")}>
|
||||
<FilterSelection
|
||||
filters={filters ?? {}}
|
||||
handleFiltersUpdate={(key, value) => {
|
||||
const newValues = filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
});
|
||||
} else {
|
||||
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
}
|
||||
|
||||
onChange({
|
||||
...filters,
|
||||
[key]: newValues,
|
||||
});
|
||||
}}
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
|
||||
labels={workspaceLabels ?? undefined}
|
||||
memberIds={workspaceMemberIds ?? undefined}
|
||||
/>
|
||||
</FiltersDropdown>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* display filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -220,7 +140,7 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
|||
render={({ field: { onChange: onDisplayPropertiesChange, value: displayProperties } }) => (
|
||||
<FiltersDropdown title={t("common.display")}>
|
||||
<DisplayFiltersSelection
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.spreadsheet}
|
||||
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.layoutOptions.spreadsheet}
|
||||
displayFilters={displayFilters ?? {}}
|
||||
handleDisplayFiltersUpdate={(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||
onDisplayFiltersChange({
|
||||
|
|
@ -242,18 +162,30 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
{selectedFilters && Object.keys(selectedFilters).length > 0 && (
|
||||
<div>
|
||||
<AppliedFiltersList
|
||||
appliedFilters={appliedFilters ?? {}}
|
||||
handleClearAllFilters={clearAllFilters}
|
||||
handleRemoveFilter={handleRemoveFilter}
|
||||
labels={workspaceLabels ?? undefined}
|
||||
states={undefined}
|
||||
alwaysAllowEditing
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{/* filters dropdown */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="rich_filters"
|
||||
render={({ field: { onChange: onFiltersChange } }) => (
|
||||
<WorkspaceLevelWorkItemFiltersHOC
|
||||
entityId={data?.id}
|
||||
entityType={EIssuesStoreType.GLOBAL}
|
||||
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.filters}
|
||||
initialWorkItemFilters={workItemFilters}
|
||||
isTemporary
|
||||
updateFilters={(updateFilters) => onFiltersChange(updateFilters)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
>
|
||||
{({ filter: workspaceViewWorkItemsFilter }) =>
|
||||
workspaceViewWorkItemsFilter && (
|
||||
<WorkItemFiltersRow filter={workspaceViewWorkItemsFilter} variant="default" />
|
||||
)
|
||||
}
|
||||
</WorkspaceLevelWorkItemFiltersHOC>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import { observer } from "mobx-react";
|
|||
import { useParams } from "next/navigation";
|
||||
// plane imports
|
||||
import { GLOBAL_VIEW_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { IWorkspaceView } from "@plane/types";
|
||||
import { EIssuesStoreType, IWorkspaceView } from "@plane/types";
|
||||
import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// helpers
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
import { useGlobalView } from "@/hooks/store/use-global-view";
|
||||
import { useWorkItemFilters } from "@/hooks/store/work-item-filters/use-work-item-filters";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
// local imports
|
||||
import { WorkspaceViewForm } from "./form";
|
||||
|
|
@ -26,9 +27,11 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||
const { isOpen, onClose, data, preLoadedData } = props;
|
||||
// router
|
||||
const router = useAppRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
const { workspaceSlug: routerWorkspaceSlug } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug ? routerWorkspaceSlug.toString() : undefined;
|
||||
// store hooks
|
||||
const { createGlobalView, updateGlobalView } = useGlobalView();
|
||||
const { resetExpression } = useWorkItemFilters();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
|
|
@ -39,12 +42,12 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||
|
||||
const payloadData: Partial<IWorkspaceView> = {
|
||||
...payload,
|
||||
filters: {
|
||||
...payload?.filters,
|
||||
rich_filters: {
|
||||
...payload?.rich_filters,
|
||||
},
|
||||
};
|
||||
|
||||
await createGlobalView(workspaceSlug.toString(), payloadData)
|
||||
await createGlobalView(workspaceSlug, payloadData)
|
||||
.then((res) => {
|
||||
captureSuccess({
|
||||
eventName: GLOBAL_VIEW_TRACKER_EVENTS.create,
|
||||
|
|
@ -79,13 +82,14 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||
const payloadData: Partial<IWorkspaceView> = {
|
||||
...payload,
|
||||
query: {
|
||||
...payload?.filters,
|
||||
...payload?.rich_filters,
|
||||
},
|
||||
};
|
||||
|
||||
await updateGlobalView(workspaceSlug.toString(), data.id, payloadData)
|
||||
await updateGlobalView(workspaceSlug, data.id, payloadData)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
resetExpression(EIssuesStoreType.GLOBAL, data.id, res.rich_filters);
|
||||
captureSuccess({
|
||||
eventName: GLOBAL_VIEW_TRACKER_EVENTS.update,
|
||||
payload: {
|
||||
|
|
@ -122,6 +126,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||
else await handleUpdateView(formData);
|
||||
};
|
||||
|
||||
if (!workspaceSlug) return null;
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.TOP} width={EModalWidth.XXL}>
|
||||
<WorkspaceViewForm
|
||||
|
|
@ -129,6 +134,7 @@ export const CreateUpdateWorkspaceViewModal: React.FC<Props> = observer((props)
|
|||
handleClose={handleClose}
|
||||
data={data}
|
||||
preLoadedData={preLoadedData}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</ModalCore>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import { copyUrlToClipboard, cn } from "@plane/utils";
|
|||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
import { useUser, useUserPermissions } from "@/hooks/store/user";
|
||||
// local imports
|
||||
import { useViewMenuItems } from "@/plane-web/components/views/helper";
|
||||
// local imports
|
||||
import { DeleteGlobalViewModal } from "./delete-view-modal";
|
||||
import { CreateUpdateWorkspaceViewModal } from "./modal";
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Pencil, Trash2 } from "lucide-react";
|
|||
// plane imports
|
||||
import { GLOBAL_VIEW_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { calculateTotalFilters, truncateText } from "@plane/utils";
|
||||
import { truncateText } from "@plane/utils";
|
||||
// helpers
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
|
|
@ -33,8 +33,6 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
|
|||
|
||||
if (!view) return null;
|
||||
|
||||
const totalFilters = calculateTotalFilters(view.filters ?? {});
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateWorkspaceViewModal data={view} isOpen={updateViewModal} onClose={() => setUpdateViewModal(false)} />
|
||||
|
|
@ -51,9 +49,6 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
<div className="ml-2 flex flex-shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<p className="hidden rounded bg-custom-background-80 px-2 py-1 text-xs text-custom-text-200 group-hover:block">
|
||||
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
|
||||
</p>
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
// plane imports
|
||||
import { IFilterInstance } from "@plane/shared-state";
|
||||
import { EIssuesStoreType, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
|
||||
// local imports
|
||||
import { useWorkItemFilters } from "./use-work-item-filters";
|
||||
|
||||
export const useWorkItemFilterInstance = (
|
||||
entityType: EIssuesStoreType,
|
||||
entityId: string
|
||||
): IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined => {
|
||||
const { getFilter } = useWorkItemFilters();
|
||||
return getFilter(entityType, entityId);
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { useContext } from "react";
|
||||
// plane imports
|
||||
import { IWorkItemFilterStore } from "@plane/shared-state";
|
||||
// context
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
|
||||
export const useWorkItemFilters = (): IWorkItemFilterStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkItemFilters must be used within StoreProvider");
|
||||
return context.workItemFilters;
|
||||
};
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useCallback, useMemo } from "react";
|
||||
// types
|
||||
import { useParams } from "next/navigation";
|
||||
import { EIssueFilterType, EDraftIssuePaginationType } from "@plane/constants";
|
||||
import { EDraftIssuePaginationType, TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
IssuePaginationOptions,
|
||||
TIssue,
|
||||
TIssueKanbanFilters,
|
||||
TIssuesResponse,
|
||||
TLoader,
|
||||
TProfileViews,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import {
|
||||
useTeamIssueActions,
|
||||
useTeamViewIssueActions,
|
||||
useTeamProjectWorkItemsActions,
|
||||
useTeamViewIssueActions,
|
||||
} from "@/plane-web/helpers/issue-action-helper";
|
||||
import { useIssues } from "./store/use-issues";
|
||||
|
||||
|
|
@ -37,8 +37,8 @@ export interface IssueActions {
|
|||
restoreIssue?: (projectId: string | undefined | null, issueId: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
|
|
@ -146,11 +146,7 @@ const useProjectIssueActions = () => {
|
|||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters);
|
||||
},
|
||||
|
|
@ -232,11 +228,7 @@ const useProjectEpicsActions = () => {
|
|||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters);
|
||||
},
|
||||
|
|
@ -332,11 +324,7 @@ const useCycleIssueActions = () => {
|
|||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!cycleId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, cycleId);
|
||||
},
|
||||
|
|
@ -443,11 +431,7 @@ const useModuleIssueActions = () => {
|
|||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!moduleId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, moduleId);
|
||||
},
|
||||
|
|
@ -529,11 +513,7 @@ const useProfileIssueActions = () => {
|
|||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!userId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, userId);
|
||||
},
|
||||
|
|
@ -615,11 +595,7 @@ const useProjectViewIssueActions = () => {
|
|||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!viewId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, viewId);
|
||||
},
|
||||
|
|
@ -680,11 +656,7 @@ const useArchivedIssueActions = () => {
|
|||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters);
|
||||
},
|
||||
|
|
@ -749,11 +721,7 @@ const useGlobalIssueActions = () => {
|
|||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!globalViewId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, globalViewId);
|
||||
},
|
||||
|
|
@ -781,7 +749,7 @@ const useWorkspaceDraftIssueActions = () => {
|
|||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT);
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions) => {
|
||||
async (loadType: TLoader, _options: IssuePaginationOptions) => {
|
||||
if (!workspaceSlug) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), loadType, EDraftIssuePaginationType.INIT);
|
||||
},
|
||||
|
|
@ -824,12 +792,8 @@ const useWorkspaceDraftIssueActions = () => {
|
|||
// );
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
filters = filters as IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties;
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
filters = filters as IIssueDisplayFilterOptions | IIssueDisplayProperties;
|
||||
if (!globalViewId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, filterType, filters);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import set from "lodash/set";
|
||||
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { IIssueFilterOptions, IWorkspaceView } from "@plane/types";
|
||||
// constants
|
||||
// plane imports
|
||||
import { IWorkspaceView } from "@plane/types";
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// store
|
||||
|
|
@ -28,7 +26,8 @@ export interface IGlobalViewStore {
|
|||
updateGlobalView: (
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<IWorkspaceView>
|
||||
data: Partial<IWorkspaceView>,
|
||||
shouldSyncFilters?: boolean
|
||||
) => Promise<IWorkspaceView | undefined>;
|
||||
deleteGlobalView: (workspaceSlug: string, viewId: string) => Promise<any>;
|
||||
}
|
||||
|
|
@ -156,7 +155,8 @@ export class GlobalViewStore implements IGlobalViewStore {
|
|||
async updateGlobalView(
|
||||
workspaceSlug: string,
|
||||
viewId: string,
|
||||
data: Partial<IWorkspaceView>
|
||||
data: Partial<IWorkspaceView>,
|
||||
shouldSyncFilters: boolean = true
|
||||
): Promise<IWorkspaceView | undefined> {
|
||||
const currentViewData = this.getViewDetailsById(viewId) ? cloneDeep(this.getViewDetailsById(viewId)) : undefined;
|
||||
try {
|
||||
|
|
@ -168,31 +168,12 @@ export class GlobalViewStore implements IGlobalViewStore {
|
|||
const currentView = await this.workspaceService.updateView(workspaceSlug, viewId, data);
|
||||
|
||||
// applying the filters in the global view
|
||||
if (!isEqual(currentViewData?.filters || {}, currentView?.filters || {})) {
|
||||
if (!currentView?.filters || isEmpty(currentView?.filters)) {
|
||||
const currentGlobalViewFilters: IIssueFilterOptions = this.rootStore.issue.workspaceIssuesFilter.filters[
|
||||
viewId
|
||||
].filters as IIssueFilterOptions;
|
||||
const newFilters: IIssueFilterOptions = {};
|
||||
Object.keys(currentGlobalViewFilters ?? {}).forEach((key) => {
|
||||
newFilters[key as keyof IIssueFilterOptions] = [];
|
||||
});
|
||||
await this.rootStore.issue.workspaceIssuesFilter.updateFilters(
|
||||
workspaceSlug,
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
newFilters,
|
||||
viewId
|
||||
);
|
||||
} else {
|
||||
await this.rootStore.issue.workspaceIssuesFilter.updateFilters(
|
||||
workspaceSlug,
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
currentView?.filters,
|
||||
viewId
|
||||
);
|
||||
}
|
||||
if (shouldSyncFilters && !isEqual(currentViewData?.rich_filters || {}, currentView?.rich_filters || {})) {
|
||||
await this.rootStore.issue.workspaceIssuesFilter.updateFilterExpression(
|
||||
workspaceSlug,
|
||||
viewId,
|
||||
currentView?.rich_filters || {}
|
||||
);
|
||||
this.rootStore.issue.workspaceIssues.fetchIssuesWithExistingPagination(workspaceSlug, viewId, "mutation");
|
||||
}
|
||||
return currentView as IWorkspaceView;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import isArray from "lodash/isArray";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import pickBy from "lodash/pickBy";
|
||||
import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { EIssueFilterType, TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueFilterOptions,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
|
|
@ -37,11 +36,16 @@ export interface IArchivedIssuesFilter extends IBaseIssueFilterStore {
|
|||
getIssueFilters(projectId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
updateFilterExpression: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filters: TWorkItemFilterExpression
|
||||
) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
|
|
@ -102,8 +106,8 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||
if (!filteredParams) return undefined;
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.filters as IIssueFilterOptions,
|
||||
userFilters?.displayFilters as IIssueDisplayFilterOptions,
|
||||
userFilters?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
|
|
@ -128,7 +132,7 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||
fetchFilters = async (workspaceSlug: string, projectId: string) => {
|
||||
const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.ARCHIVED, workspaceSlug, projectId, undefined);
|
||||
|
||||
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
|
||||
const richFilters: TWorkItemFilterExpression = _filters?.richFilters;
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters({
|
||||
..._filters?.display_filters,
|
||||
sub_issue: true,
|
||||
|
|
@ -142,51 +146,57 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||
kanbanFilters.sub_group_by = _filters?.kanban_filters?.sub_group_by || [];
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [projectId, "filters"], filters);
|
||||
set(this.filters, [projectId, "richFilters"], richFilters);
|
||||
set(this.filters, [projectId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [projectId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [projectId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
updateFilters = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
type: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: IArchivedIssuesFilter["updateFilterExpression"] = async (
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
filters
|
||||
) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[projectId]) || isEmpty(filters)) return;
|
||||
runInAction(() => {
|
||||
set(this.filters, [projectId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.archivedIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
|
||||
this.handleIssuesLocalFilters.set(
|
||||
EIssuesStoreType.ARCHIVED,
|
||||
EIssueFilterType.FILTERS,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
undefined,
|
||||
{
|
||||
rich_filters: filters,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: IArchivedIssuesFilter["updateFilters"] = async (workspaceSlug, projectId, type, filters) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[projectId])) return;
|
||||
|
||||
const _filters = {
|
||||
filters: this.filters[projectId].filters as IIssueFilterOptions,
|
||||
richFilters: this.filters[projectId].richFilters,
|
||||
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[projectId].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case EIssueFilterType.FILTERS: {
|
||||
const updatedFilters = filters as IIssueFilterOptions;
|
||||
_filters.filters = { ..._filters.filters, ...updatedFilters };
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedFilters).forEach((_key) => {
|
||||
set(this.filters, [projectId, "filters", _key], updatedFilters[_key as keyof IIssueFilterOptions]);
|
||||
});
|
||||
});
|
||||
const appliedFilters = _filters.filters || {};
|
||||
const filteredFilters = pickBy(appliedFilters, (value) => value && isArray(value) && value.length > 0);
|
||||
this.rootIssueStore.archivedIssues.fetchIssuesWithExistingPagination(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
isEmpty(filteredFilters) ? "init-loader" : "mutation"
|
||||
);
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, {
|
||||
filters: _filters.filters,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.DISPLAY_FILTERS: {
|
||||
const updatedDisplayFilters = filters as IIssueDisplayFilterOptions;
|
||||
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@ import set from "lodash/set";
|
|||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import { EIssueFilterType } from "@plane/constants";
|
||||
import { EIssueFilterType, TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import {
|
||||
EIssuesStoreType,
|
||||
IIssueFilterOptions,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
TWorkItemFilterExpression,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { handleIssueQueryParamsByLayout } from "@plane/utils";
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
|
|
@ -35,11 +36,17 @@ export interface ICycleIssuesFilter extends IBaseIssueFilterStore {
|
|||
getIssueFilters(cycleId: string): IIssueFilters | undefined;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<void>;
|
||||
updateFilterExpression: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
cycleId: string,
|
||||
filters: TWorkItemFilterExpression
|
||||
) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate,
|
||||
cycleId: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
|
@ -103,8 +110,8 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||
if (filteredParams.includes("cycle")) filteredParams.splice(filteredParams.indexOf("cycle"), 1);
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.filters as IIssueFilterOptions,
|
||||
userFilters?.displayFilters as IIssueDisplayFilterOptions,
|
||||
userFilters?.richFilters,
|
||||
userFilters?.displayFilters,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
|
|
@ -134,7 +141,7 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||
fetchFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||
const _filters = await this.issueFilterService.fetchCycleIssueFilters(workspaceSlug, projectId, cycleId);
|
||||
|
||||
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
|
||||
const richFilters: TWorkItemFilterExpression = _filters?.rich_filters;
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
|
||||
|
|
@ -156,52 +163,51 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||
}
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [cycleId, "filters"], filters);
|
||||
set(this.filters, [cycleId, "richFilters"], richFilters);
|
||||
set(this.filters, [cycleId, "displayFilters"], displayFilters);
|
||||
set(this.filters, [cycleId, "displayProperties"], displayProperties);
|
||||
set(this.filters, [cycleId, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
updateFilters = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
type: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters,
|
||||
cycleId: string
|
||||
/**
|
||||
* NOTE: This method is designed as a fallback function for the work item filter store.
|
||||
* Only use this method directly when initializing filter instances.
|
||||
* For regular filter updates, use this method as a fallback function for the work item filter store methods instead.
|
||||
*/
|
||||
updateFilterExpression: ICycleIssuesFilter["updateFilterExpression"] = async (
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
cycleId,
|
||||
filters
|
||||
) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[cycleId]) || isEmpty(filters)) return;
|
||||
runInAction(() => {
|
||||
set(this.filters, [cycleId, "richFilters"], filters);
|
||||
});
|
||||
|
||||
this.rootIssueStore.cycleIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation", cycleId);
|
||||
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
|
||||
rich_filters: filters,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error while updating rich filters", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateFilters: ICycleIssuesFilter["updateFilters"] = async (workspaceSlug, projectId, type, filters, cycleId) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[cycleId])) return;
|
||||
|
||||
const _filters = {
|
||||
filters: this.filters[cycleId].filters as IIssueFilterOptions,
|
||||
richFilters: this.filters[cycleId].richFilters as TWorkItemFilterExpression,
|
||||
displayFilters: this.filters[cycleId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[cycleId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[cycleId].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case EIssueFilterType.FILTERS: {
|
||||
const updatedFilters = filters as IIssueFilterOptions;
|
||||
_filters.filters = { ..._filters.filters, ...updatedFilters };
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedFilters).forEach((_key) => {
|
||||
set(this.filters, [cycleId, "filters", _key], updatedFilters[_key as keyof IIssueFilterOptions]);
|
||||
});
|
||||
});
|
||||
|
||||
this.rootIssueStore.cycleIssues.fetchIssuesWithExistingPagination(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
"mutation",
|
||||
cycleId
|
||||
);
|
||||
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
|
||||
filters: _filters.filters,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.DISPLAY_FILTERS: {
|
||||
const updatedDisplayFilters = filters as IIssueDisplayFilterOptions;
|
||||
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
|
||||
|
|
|
|||
|
|
@ -141,15 +141,20 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
|||
fetchParentStats = (workspaceSlug: string, projectId?: string | undefined, id?: string | undefined) => {
|
||||
const cycleId = id ?? this.cycleId;
|
||||
|
||||
projectId && cycleId && this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
||||
if (projectId && cycleId) {
|
||||
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
||||
}
|
||||
// fetch cycle progress
|
||||
const isSidebarCollapsed = storage.get("cycle_sidebar_collapsed");
|
||||
projectId &&
|
||||
if (
|
||||
projectId &&
|
||||
cycleId &&
|
||||
this.rootIssueStore.rootStore.cycle.getCycleById(cycleId)?.version === 2 &&
|
||||
isSidebarCollapsed &&
|
||||
JSON.parse(isSidebarCollapsed) === false &&
|
||||
JSON.parse(isSidebarCollapsed) === false
|
||||
) {
|
||||
this.rootIssueStore.rootStore.cycle.fetchActiveCycleProgressPro(workspaceSlug, projectId, cycleId);
|
||||
}
|
||||
};
|
||||
|
||||
updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string | undefined) => {
|
||||
|
|
@ -162,8 +167,9 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues {
|
|||
);
|
||||
|
||||
const cycleId = id ?? this.cycleId;
|
||||
|
||||
cycleId && this.rootIssueStore.rootStore.cycle.updateCycleDistribution(distributionUpdates, cycleId);
|
||||
if (cycleId) {
|
||||
this.rootIssueStore.rootStore.cycle.updateCycleDistribution(distributionUpdates, cycleId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("could not update cycle statistics");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IIssueFilterOptions,
|
||||
IIssueFilters,
|
||||
ISubWorkItemFilters,
|
||||
TIssue,
|
||||
TIssueGroupByOptions,
|
||||
TIssueOrderByOptions,
|
||||
|
|
@ -357,8 +357,8 @@ export const getGroupedWorkItemIds = (
|
|||
* @param filters - The filters to update.
|
||||
* @param workItemId - The ID of the work item to update.
|
||||
*/
|
||||
export const updateFilters = (
|
||||
filtersMap: Record<string, Partial<IIssueFilters>>,
|
||||
export const updateSubWorkItemFilters = (
|
||||
filtersMap: Record<string, Partial<ISubWorkItemFilters>>,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueDisplayFilterOptions | IIssueDisplayProperties | IIssueFilterOptions,
|
||||
workItemId: string
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import get from "lodash/get";
|
|||
import indexOf from "lodash/indexOf";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import isNil from "lodash/isNil";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import pull from "lodash/pull";
|
||||
import set from "lodash/set";
|
||||
|
|
@ -95,6 +94,8 @@ export interface IBaseIssuesStore {
|
|||
addCycleToIssue: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
removeCycleFromIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
|
||||
addIssueToList: (issueId: string) => void;
|
||||
removeIssueFromList: (issueId: string) => void;
|
||||
addIssuesToModule: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
TIssueParams,
|
||||
TStaticViewTypes,
|
||||
EIssueLayoutTypes,
|
||||
TWorkItemFilterExpression,
|
||||
} from "@plane/types";
|
||||
// helpers
|
||||
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@plane/utils";
|
||||
|
|
@ -43,11 +44,15 @@ export interface IBaseIssueFilterStore {
|
|||
export interface IIssueFilterHelperStore {
|
||||
computedIssueFilters(filters: IIssueFilters): IIssueFilters;
|
||||
computedFilteredParams(
|
||||
filters: IIssueFilterOptions,
|
||||
displayFilters: IIssueDisplayFilterOptions,
|
||||
filteredParams: TIssueParams[]
|
||||
richFilters: TWorkItemFilterExpression,
|
||||
displayFilters: IIssueDisplayFilterOptions | undefined,
|
||||
acceptableParamsByLayout: TIssueParams[]
|
||||
): Partial<Record<TIssueParams, string | boolean>>;
|
||||
computedFilters(filters: IIssueFilterOptions): IIssueFilterOptions;
|
||||
getFilterConditionBasedOnViews: (
|
||||
currentUserId: string | undefined,
|
||||
type: TStaticViewTypes
|
||||
) => Partial<Record<TIssueParams, string>> | undefined;
|
||||
computedDisplayFilters(
|
||||
displayFilters: IIssueDisplayFilterOptions,
|
||||
defaultValues?: IIssueDisplayFilterOptions
|
||||
|
|
@ -64,7 +69,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||
* @returns {IIssueFilters}
|
||||
*/
|
||||
computedIssueFilters = (filters: IIssueFilters): IIssueFilters => ({
|
||||
filters: isEmpty(filters?.filters) ? undefined : filters?.filters,
|
||||
richFilters: isEmpty(filters?.richFilters) ? {} : filters?.richFilters,
|
||||
displayFilters: isEmpty(filters?.displayFilters) ? undefined : filters?.displayFilters,
|
||||
displayProperties: isEmpty(filters?.displayProperties) ? undefined : filters?.displayProperties,
|
||||
kanbanFilters: isEmpty(filters?.kanbanFilters) ? undefined : filters?.kanbanFilters,
|
||||
|
|
@ -72,47 +77,29 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||
|
||||
/**
|
||||
* @description This method is used to convert the filters array params to string params
|
||||
* @param {IIssueFilterOptions} filters
|
||||
* @param {TWorkItemFilterExpression} richFilters
|
||||
* @param {IIssueDisplayFilterOptions} displayFilters
|
||||
* @param {string[]} acceptableParamsByLayout
|
||||
* @returns {Partial<Record<TIssueParams, string | boolean>>}
|
||||
*/
|
||||
computedFilteredParams = (
|
||||
filters: IIssueFilterOptions,
|
||||
displayFilters: IIssueDisplayFilterOptions,
|
||||
richFilters: TWorkItemFilterExpression,
|
||||
displayFilters: IIssueDisplayFilterOptions | undefined,
|
||||
acceptableParamsByLayout: TIssueParams[]
|
||||
) => {
|
||||
const computedFilters: Partial<Record<TIssueParams, undefined | string[] | boolean | string>> = {
|
||||
// issue filters
|
||||
priority: filters?.priority || undefined,
|
||||
state_group: filters?.state_group || undefined,
|
||||
state: filters?.state || undefined,
|
||||
assignees: filters?.assignees || undefined,
|
||||
mentions: filters?.mentions || undefined,
|
||||
created_by: filters?.created_by || undefined,
|
||||
labels: filters?.labels || undefined,
|
||||
cycle: filters?.cycle || undefined,
|
||||
module: filters?.module || undefined,
|
||||
start_date: filters?.start_date || undefined,
|
||||
target_date: filters?.target_date || undefined,
|
||||
project: filters?.project || undefined,
|
||||
team_project: filters?.team_project || undefined,
|
||||
subscriber: filters?.subscriber || undefined,
|
||||
issue_type: filters?.issue_type || undefined,
|
||||
// display filters
|
||||
): Partial<Record<TIssueParams, string | boolean>> => {
|
||||
const computedDisplayFilters: Partial<Record<TIssueParams, undefined | string[] | boolean | string>> = {
|
||||
group_by: displayFilters?.group_by ? EIssueGroupByToServerOptions[displayFilters.group_by] : undefined,
|
||||
sub_group_by: displayFilters?.sub_group_by
|
||||
? EIssueGroupByToServerOptions[displayFilters.sub_group_by]
|
||||
: undefined,
|
||||
order_by: displayFilters?.order_by || undefined,
|
||||
type: displayFilters?.type || undefined,
|
||||
sub_issue: displayFilters?.sub_issue ?? true,
|
||||
};
|
||||
|
||||
const issueFiltersParams: Partial<Record<TIssueParams, boolean | string>> = {};
|
||||
Object.keys(computedFilters).forEach((key) => {
|
||||
Object.keys(computedDisplayFilters).forEach((key) => {
|
||||
const _key = key as TIssueParams;
|
||||
const _value: string | boolean | string[] | undefined = computedFilters[_key];
|
||||
const _value: string | boolean | string[] | undefined = computedDisplayFilters[_key];
|
||||
const nonEmptyArrayValue = Array.isArray(_value) && _value.length === 0 ? undefined : _value;
|
||||
if (nonEmptyArrayValue != undefined && acceptableParamsByLayout.includes(_key))
|
||||
issueFiltersParams[_key] = Array.isArray(nonEmptyArrayValue)
|
||||
|
|
@ -120,9 +107,12 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||
: nonEmptyArrayValue;
|
||||
});
|
||||
|
||||
// work item filters
|
||||
if (richFilters) issueFiltersParams.filters = JSON.stringify(richFilters);
|
||||
|
||||
if (displayFilters?.layout) issueFiltersParams.layout = displayFilters?.layout;
|
||||
|
||||
if (ENABLE_ISSUE_DEPENDENCIES && displayFilters.layout === EIssueLayoutTypes.GANTT)
|
||||
if (ENABLE_ISSUE_DEPENDENCIES && displayFilters?.layout === EIssueLayoutTypes.GANTT)
|
||||
issueFiltersParams["expand"] = "issue_relation,issue_related";
|
||||
|
||||
return issueFiltersParams;
|
||||
|
|
@ -152,35 +142,29 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||
});
|
||||
|
||||
/**
|
||||
* This PR is to get the filters of the fixed global views
|
||||
* @param currentUserId current logged in user id
|
||||
* @param type fixed view type
|
||||
* @returns filterOptions based on views
|
||||
* @description This method is used to get the filter conditions based on the views
|
||||
* @param currentUserId
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
getComputedFiltersBasedOnViews = (currentUserId: string | undefined, type: TStaticViewTypes) => {
|
||||
const noFilters = this.computedFilters({});
|
||||
|
||||
if (!currentUserId) return noFilters;
|
||||
|
||||
getFilterConditionBasedOnViews: IIssueFilterHelperStore["getFilterConditionBasedOnViews"] = (currentUserId, type) => {
|
||||
if (!currentUserId) return undefined;
|
||||
switch (type) {
|
||||
case "assigned":
|
||||
return {
|
||||
...noFilters,
|
||||
assignees: [currentUserId],
|
||||
assignees: currentUserId,
|
||||
};
|
||||
case "created":
|
||||
return {
|
||||
...noFilters,
|
||||
created_by: [currentUserId],
|
||||
created_by: currentUserId,
|
||||
};
|
||||
case "subscribed":
|
||||
return {
|
||||
...noFilters,
|
||||
subscriber: [currentUserId],
|
||||
subscriber: currentUserId,
|
||||
};
|
||||
case "all-issues":
|
||||
default:
|
||||
return noFilters;
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -257,7 +241,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||
...storageFilters[currentFilterIndex],
|
||||
filters: {
|
||||
...storageFilters[currentFilterIndex].filters,
|
||||
[filterType]: filters[filterType],
|
||||
[filterType]: filters[filterType as keyof IIssueFiltersResponse],
|
||||
},
|
||||
};
|
||||
// All group_by "filters" are stored in a single array, will cause inconsistency in case of duplicated values
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue