mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: ui and filter store updates
This commit is contained in:
parent
cf10e3445d
commit
0fb531e4b7
@ -1,7 +1,7 @@
|
|||||||
import { FC, Fragment, useEffect, useMemo, useState } from "react";
|
import { FC, Fragment, useEffect, useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { CheckCircle, Pencil } from "lucide-react";
|
import { CheckCircle, ChevronDown, ChevronUp, Pencil } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useView, useViewDetail } from "hooks/store";
|
import { useView, useViewDetail } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// constants
|
// constants
|
||||||
import { VIEW_TYPES, viewLocalPayload } from "constants/view";
|
import { viewLocalPayload } from "constants/view";
|
||||||
// types
|
// types
|
||||||
import { TViewOperations } from "./types";
|
import { TViewOperations } from "./types";
|
||||||
import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties, TViewTypes } from "@plane/types";
|
import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties, TViewTypes } from "@plane/types";
|
||||||
@ -30,6 +30,7 @@ type TAllIssuesViewRoot = {
|
|||||||
viewId: string;
|
viewId: string;
|
||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
baseRoute: string;
|
baseRoute: string;
|
||||||
|
workspaceViewTabOptions: { key: TViewTypes; title: string; href: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type TViewOperationsToggle = {
|
type TViewOperationsToggle = {
|
||||||
@ -38,7 +39,7 @@ type TViewOperationsToggle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, baseRoute } = props;
|
const { workspaceSlug, projectId, viewId, viewType, baseRoute, workspaceViewTabOptions } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const viewStore = useView(workspaceSlug, projectId, viewType);
|
const viewStore = useView(workspaceSlug, projectId, viewType);
|
||||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
@ -51,28 +52,22 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) =>
|
const handleViewOperationsToggle = (type: TViewOperationsToggle["type"], viewId: string | undefined) =>
|
||||||
setViewOperationsToggle({ type, viewId });
|
setViewOperationsToggle({ type, viewId });
|
||||||
|
|
||||||
const workspaceViewTabOptions = useMemo(
|
const viewDetailCreateStore = useViewDetail(
|
||||||
() => [
|
workspaceSlug,
|
||||||
{
|
projectId,
|
||||||
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
viewOperationsToggle?.viewId || viewId,
|
||||||
title: "Private",
|
viewType
|
||||||
href: `/${workspaceSlug}/views/private/assigned`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
|
||||||
title: "Public",
|
|
||||||
href: `/${workspaceSlug}/views/public/all-issues`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[workspaceSlug]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewOperations: TViewOperations = useMemo(
|
const viewOperations: TViewOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
setName: (name: string) => viewDetailStore?.setName(name),
|
setName: (name: string) => viewDetailStore?.setName(name),
|
||||||
setDescription: (name: string) => viewDetailStore?.setDescription(name),
|
setDescription: (name: string) => viewDetailStore?.setDescription(name),
|
||||||
setFilters: (filterKey: keyof TViewFilters, filterValue: "clear_all" | string) =>
|
setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => {
|
||||||
viewDetailStore?.setFilters(filterKey, filterValue),
|
if (viewOperationsToggle.type && ["CREATE", "EDIT"].includes(viewOperationsToggle.type))
|
||||||
|
viewDetailCreateStore?.setFilters(filterKey, filterValue);
|
||||||
|
else viewDetailStore?.setFilters(filterKey, filterValue);
|
||||||
|
},
|
||||||
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) =>
|
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) =>
|
||||||
viewDetailStore?.setDisplayFilters(display_filters),
|
viewDetailStore?.setDisplayFilters(display_filters),
|
||||||
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) =>
|
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) =>
|
||||||
@ -114,17 +109,24 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[viewStore, viewDetailStore, setToastAlert]
|
[viewStore, viewDetailStore, setToastAlert, viewOperationsToggle, viewDetailCreateStore]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// fetch all issues
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchViews = async () => {
|
const fetchViews = async () => {
|
||||||
await viewStore?.fetch(viewStore?.viewIds.length > 0 ? "mutation-loader" : "init-loader");
|
await viewStore?.fetch(viewStore?.viewIds.length > 0 ? "mutation-loader" : "init-loader");
|
||||||
|
};
|
||||||
|
if (workspaceSlug && viewType && viewStore) fetchViews();
|
||||||
|
}, [workspaceSlug, projectId, viewType, viewStore]);
|
||||||
|
|
||||||
|
// fetch view by id
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchViews = async () => {
|
||||||
viewId && (await viewStore?.fetchById(viewId));
|
viewId && (await viewStore?.fetchById(viewId));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (workspaceSlug && viewId && viewType && viewStore) fetchViews();
|
if (workspaceSlug && viewId && viewType && viewStore) fetchViews();
|
||||||
}, [workspaceSlug, viewId, viewType, viewStore]);
|
}, [workspaceSlug, projectId, viewId, viewType, viewStore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full">
|
<div className="relative w-full h-full">
|
||||||
@ -170,7 +172,7 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-5 py-2 border-b border-custom-border-200 relative flex gap-2">
|
<div className="p-5 py-2 border-b border-custom-border-200 relative flex items-start gap-1">
|
||||||
<div className="w-full overflow-hidden">
|
<div className="w-full overflow-hidden">
|
||||||
<ViewAppliedFiltersRoot
|
<ViewAppliedFiltersRoot
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
@ -178,6 +180,7 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
|
propertyVisibleCount={5}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -198,7 +201,7 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
displayDropdownText={false}
|
displayDropdownText={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -209,15 +212,24 @@ export const AllIssuesViewRoot: FC<TAllIssuesViewRoot> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
displayDropdownText={false}
|
displayDropdownText={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-custom-border-300 relative flex items-center gap-1 h-7 rounded px-2 transition-all text-custom-text-200 hover:text-custom-text-100 bg-custom-background-100 hover:bg-custom-background-80 cursor-pointer shadow-custom-shadow-2xs">
|
<div className="relative flex items-center gap-1 rounded px-2 h-7 transition-all hover:bg-custom-background-80 cursor-pointer">
|
||||||
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
<div className="w-4 h-4 relative flex justify-center items-center overflow-hidden">
|
||||||
<Pencil size={12} />
|
<Pencil size={12} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className=" relative flex items-center rounded h-7 transition-all cursor-pointer bg-custom-primary-100/20 text-custom-primary-100">
|
||||||
|
<div className="text-sm px-3 font-medium h-full border-r border-white/50 flex justify-center items-center rounded-l transition-all hover:bg-custom-primary-100/30">
|
||||||
|
Update
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 px-1.5 hover:bg-custom-primary-100/30 h-full flex justify-center items-center rounded-r transition-all">
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useViewDetail } from "hooks/store";
|
import { useViewDetail, useViewFilter } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { ViewAppliedFiltersItem } from "./filter-item";
|
import { ViewAppliedFiltersItem } from "./filter-item";
|
||||||
// types
|
// types
|
||||||
@ -17,12 +17,16 @@ type TViewAppliedFilters = {
|
|||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
filterKey: keyof TViewFilters;
|
filterKey: keyof TViewFilters;
|
||||||
viewOperations: TViewOperations;
|
viewOperations: TViewOperations;
|
||||||
|
propertyVisibleCount?: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
|
export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, filterKey, viewOperations } = props;
|
const { workspaceSlug, projectId, viewId, viewType, filterKey, viewOperations, propertyVisibleCount } = props;
|
||||||
|
// hooks
|
||||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
|
const viewFilterStore = useViewFilter(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
const currentDefaultFilterDetails = viewFilterStore?.propertyDefaultDetails(filterKey);
|
||||||
|
|
||||||
const propertyValues =
|
const propertyValues =
|
||||||
viewDetailStore?.appliedFilters?.filters && !isEmpty(viewDetailStore?.appliedFilters?.filters)
|
viewDetailStore?.appliedFilters?.filters && !isEmpty(viewDetailStore?.appliedFilters?.filters)
|
||||||
@ -36,9 +40,12 @@ export const ViewAppliedFilters: FC<TViewAppliedFilters> = observer((props) => {
|
|||||||
<div className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 px-2 min-h-[32px]">
|
<div className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 px-2 min-h-[32px]">
|
||||||
<div className="flex-shrink-0 text-xs text-custom-text-200 capitalize">{filterKey.replaceAll("_", " ")}</div>
|
<div className="flex-shrink-0 text-xs text-custom-text-200 capitalize">{filterKey.replaceAll("_", " ")}</div>
|
||||||
<div className="relative flex items-center gap-1.5 flex-wrap">
|
<div className="relative flex items-center gap-1.5 flex-wrap">
|
||||||
{propertyValues.length >= 100 ? (
|
{propertyVisibleCount && propertyValues.length >= propertyVisibleCount ? (
|
||||||
<div className="text-xs font-medium bg-custom-primary-100/20 rounded relative flex items-center gap-1 p-1 px-2">
|
<div className="text-xs font-medium bg-custom-primary-100/20 rounded relative flex items-center gap-1 p-1 px-2">
|
||||||
{propertyValues.length} {filterKey.replaceAll("_", " ")}s
|
<div className="flex-shrink-0 w-4-h-4">{currentDefaultFilterDetails?.icon}</div>
|
||||||
|
<div className="whitespace-nowrap">
|
||||||
|
{propertyValues.length} {currentDefaultFilterDetails?.label}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -16,10 +16,20 @@ type TViewAppliedFiltersRoot = {
|
|||||||
viewId: string;
|
viewId: string;
|
||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
viewOperations: TViewOperations;
|
viewOperations: TViewOperations;
|
||||||
|
propertyVisibleCount?: number | undefined;
|
||||||
|
showClearAll?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((props) => {
|
export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
viewId,
|
||||||
|
viewType,
|
||||||
|
viewOperations,
|
||||||
|
propertyVisibleCount = undefined,
|
||||||
|
showClearAll = false,
|
||||||
|
} = props;
|
||||||
// hooks
|
// hooks
|
||||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
|
|
||||||
@ -28,7 +38,7 @@ export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((pro
|
|||||||
? Object.keys(viewDetailStore?.appliedFilters?.filters)
|
? Object.keys(viewDetailStore?.appliedFilters?.filters)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const clearAllFilters = () => viewDetailStore?.setFilters(undefined, "clear_all");
|
const clearAllFilters = () => viewOperations?.setFilters(undefined, "clear_all");
|
||||||
|
|
||||||
if (!filterKeys || !viewDetailStore?.isFiltersApplied)
|
if (!filterKeys || !viewDetailStore?.isFiltersApplied)
|
||||||
return (
|
return (
|
||||||
@ -49,20 +59,23 @@ export const ViewAppliedFiltersRoot: FC<TViewAppliedFiltersRoot> = observer((pro
|
|||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
filterKey={filterKey}
|
filterKey={filterKey}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
|
propertyVisibleCount={propertyVisibleCount}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div
|
{showClearAll && (
|
||||||
className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 px-2 cursor-pointer transition-all hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100 min-h-[36px]"
|
<div
|
||||||
onClick={clearAllFilters}
|
className="relative flex items-center gap-2 border border-custom-border-300 rounded p-1.5 px-2 cursor-pointer transition-all hover:bg-custom-background-80 text-custom-text-200 hover:text-custom-text-100 min-h-[36px]"
|
||||||
>
|
onClick={clearAllFilters}
|
||||||
<div className="text-xs">Clear All</div>
|
>
|
||||||
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4">
|
<div className="text-xs">Clear All</div>
|
||||||
<X size={10} />
|
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4">
|
||||||
|
<X size={10} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { FC, Fragment, ReactNode, useRef, useState } from "react";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
|
import { Placement } from "@popperjs/core";
|
||||||
import { ListFilter, Search } from "lucide-react";
|
import { ListFilter, Search } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
@ -21,13 +22,24 @@ type TViewFiltersDropdown = {
|
|||||||
viewOperations: TViewOperations;
|
viewOperations: TViewOperations;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
displayDropdownText?: boolean;
|
displayDropdownText?: boolean;
|
||||||
|
dropdownPlacement?: Placement;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) => {
|
export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations, children, displayDropdownText = true } = props;
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
viewId,
|
||||||
|
viewType,
|
||||||
|
viewOperations,
|
||||||
|
children,
|
||||||
|
displayDropdownText = true,
|
||||||
|
dropdownPlacement = "bottom-start",
|
||||||
|
} = props;
|
||||||
// state
|
// state
|
||||||
const [dropdownToggle, setDropdownToggle] = useState(false);
|
const [dropdownToggle, setDropdownToggle] = useState(false);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
const [dateCustomFilterToggle, setDateCustomFilterToggle] = useState<string | undefined>(undefined);
|
||||||
// refs
|
// refs
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
// popper-js refs
|
// popper-js refs
|
||||||
@ -35,7 +47,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
|
|||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
// popper-js init
|
// popper-js init
|
||||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: "bottom-start",
|
placement: dropdownPlacement,
|
||||||
modifiers: [
|
modifiers: [
|
||||||
{
|
{
|
||||||
name: "preventOverflow",
|
name: "preventOverflow",
|
||||||
@ -43,6 +55,12 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
|
|||||||
padding: 12,
|
padding: 12,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "offset",
|
||||||
|
options: {
|
||||||
|
offset: [0, 10],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,7 +73,7 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
|
|||||||
else handleDropdownClose();
|
else handleDropdownClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
useOutsideClickDetector(dropdownRef, handleDropdownClose);
|
useOutsideClickDetector(dropdownRef, () => dateCustomFilterToggle === undefined && handleDropdownClose());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Combobox as="div" ref={dropdownRef}>
|
<Combobox as="div" ref={dropdownRef}>
|
||||||
@ -109,13 +127,15 @@ export const ViewFiltersDropdown: FC<TViewFiltersDropdown> = observer((props) =>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-h-[460px] space-y-0.5 overflow-y-scroll mb-2">
|
<div className="max-h-[500px] space-y-0.5 overflow-y-scroll mb-2">
|
||||||
<ViewFiltersRoot
|
<ViewFiltersRoot
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
|
dateCustomFilterToggle={dateCustomFilterToggle}
|
||||||
|
setDateCustomFilterToggle={setDateCustomFilterToggle}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useViewFilter } from "hooks/store";
|
import { useViewDetail, useViewFilter } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { ViewFiltersItem, ViewFilterSelection } from "../";
|
import { ViewFiltersItem, ViewFilterSelection } from "../";
|
||||||
|
import { DateFilterModal } from "components/core";
|
||||||
// types
|
// types
|
||||||
import { TViewOperations } from "../types";
|
import { TViewOperations } from "../types";
|
||||||
import { TViewFilters, TViewTypes } from "@plane/types";
|
import { TViewFilters, TViewTypes } from "@plane/types";
|
||||||
@ -15,11 +16,23 @@ type TViewFiltersItemRoot = {
|
|||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
viewOperations: TViewOperations;
|
viewOperations: TViewOperations;
|
||||||
filterKey: keyof TViewFilters;
|
filterKey: keyof TViewFilters;
|
||||||
|
dateCustomFilterToggle: string | undefined;
|
||||||
|
setDateCustomFilterToggle: (value: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) => {
|
export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations, filterKey } = props;
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
viewId,
|
||||||
|
viewType,
|
||||||
|
viewOperations,
|
||||||
|
filterKey,
|
||||||
|
dateCustomFilterToggle,
|
||||||
|
setDateCustomFilterToggle,
|
||||||
|
} = props;
|
||||||
// hooks
|
// hooks
|
||||||
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
const viewFilterHelper = useViewFilter(workspaceSlug, projectId);
|
||||||
// state
|
// state
|
||||||
const [viewAll, setViewAll] = useState(false);
|
const [viewAll, setViewAll] = useState(false);
|
||||||
@ -28,7 +41,24 @@ export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) =>
|
|||||||
|
|
||||||
const filterPropertyIds = propertyIds.length > 5 ? (viewAll ? propertyIds : propertyIds.slice(0, 5)) : propertyIds;
|
const filterPropertyIds = propertyIds.length > 5 ? (viewAll ? propertyIds : propertyIds.slice(0, 5)) : propertyIds;
|
||||||
|
|
||||||
const handlePropertySelection = (_propertyId: string) => viewOperations?.setFilters(filterKey, _propertyId);
|
const handlePropertySelection = (_propertyId: string) => {
|
||||||
|
if (["start_date", "target_date"].includes(filterKey)) {
|
||||||
|
if (_propertyId === "custom") {
|
||||||
|
const _propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
|
||||||
|
const selectedDates = _propertyIds.filter((id) => id.includes("-"));
|
||||||
|
if (selectedDates.length > 0)
|
||||||
|
selectedDates.forEach((date: string) => viewOperations?.setFilters(filterKey, date));
|
||||||
|
else setDateCustomFilterToggle(filterKey);
|
||||||
|
} else viewOperations?.setFilters(filterKey, _propertyId);
|
||||||
|
} else viewOperations?.setFilters(filterKey, _propertyId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomDateSelection = (selectedDates: string[]) => {
|
||||||
|
selectedDates.forEach((date: string) => {
|
||||||
|
viewOperations?.setFilters(filterKey, date);
|
||||||
|
setDateCustomFilterToggle(undefined);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (propertyIds.length <= 0)
|
if (propertyIds.length <= 0)
|
||||||
return <div className="text-xs italic py-1 text-custom-text-300">No items are available.</div>;
|
return <div className="text-xs italic py-1 text-custom-text-300">No items are available.</div>;
|
||||||
@ -65,6 +95,15 @@ export const ViewFiltersItemRoot: FC<TViewFiltersItemRoot> = observer((props) =>
|
|||||||
{viewAll ? "View less" : "View all"}
|
{viewAll ? "View less" : "View all"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{dateCustomFilterToggle === filterKey && (
|
||||||
|
<DateFilterModal
|
||||||
|
handleClose={() => setDateCustomFilterToggle(undefined)}
|
||||||
|
isOpen={dateCustomFilterToggle === filterKey ? true : false}
|
||||||
|
onSelect={handleCustomDateSelection}
|
||||||
|
title="Start date"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -21,7 +21,14 @@ export const ViewFilterSelection: FC<TViewFilterSelection> = observer((props) =>
|
|||||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
|
|
||||||
const propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
|
const propertyIds = viewDetailStore?.appliedFilters?.filters?.[filterKey] || [];
|
||||||
const isSelected = propertyIds?.includes(propertyId) || false;
|
|
||||||
|
const isSelected = ["start_date", "target_date"].includes(filterKey)
|
||||||
|
? propertyId === "custom"
|
||||||
|
? propertyIds.filter((id) => id.includes("-")).length > 0
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: propertyIds?.includes(propertyId)
|
||||||
|
: propertyIds?.includes(propertyId) || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -19,10 +19,20 @@ type TViewFiltersRoot = {
|
|||||||
viewId: string;
|
viewId: string;
|
||||||
viewType: TViewTypes;
|
viewType: TViewTypes;
|
||||||
viewOperations: TViewOperations;
|
viewOperations: TViewOperations;
|
||||||
|
dateCustomFilterToggle: string | undefined;
|
||||||
|
setDateCustomFilterToggle: (value: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
|
export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
|
||||||
const { workspaceSlug, projectId, viewId, viewType, viewOperations } = props;
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
viewId,
|
||||||
|
viewType,
|
||||||
|
viewOperations,
|
||||||
|
dateCustomFilterToggle,
|
||||||
|
setDateCustomFilterToggle,
|
||||||
|
} = props;
|
||||||
// hooks
|
// hooks
|
||||||
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
const viewDetailStore = useViewDetail(workspaceSlug, projectId, viewId, viewType);
|
||||||
// state
|
// state
|
||||||
@ -62,6 +72,8 @@ export const ViewFiltersRoot: FC<TViewFiltersRoot> = observer((props) => {
|
|||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
filterKey={filterKey}
|
filterKey={filterKey}
|
||||||
|
dateCustomFilterToggle={dateCustomFilterToggle}
|
||||||
|
setDateCustomFilterToggle={setDateCustomFilterToggle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
2
web/components/view/types.d.ts
vendored
2
web/components/view/types.d.ts
vendored
@ -3,7 +3,7 @@ import { TView, TViewFilters, TViewDisplayFilters, TViewDisplayProperties } from
|
|||||||
export type TViewOperations = {
|
export type TViewOperations = {
|
||||||
setName: (name: string) => void;
|
setName: (name: string) => void;
|
||||||
setDescription: (description: string) => void;
|
setDescription: (description: string) => void;
|
||||||
setFilters: (filterKey: keyof TViewFilters, filterValue: "clear_all" | string) => void;
|
setFilters: (filterKey: keyof TViewFilters | undefined, filterValue: "clear_all" | string) => void;
|
||||||
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
|
setDisplayFilters: (display_filters: Partial<TViewDisplayFilters>) => void;
|
||||||
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void;
|
setDisplayProperties: (displayPropertyKey: keyof TViewDisplayProperties) => void;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { Briefcase, Globe2, Plus, X } from "lucide-react";
|
|||||||
// hooks
|
// hooks
|
||||||
import { useViewDetail, useProject } from "hooks/store";
|
import { useViewDetail, useProject } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { ViewAppliedFiltersRoot } from "../";
|
import { ViewAppliedFiltersRoot, ViewFiltersDropdown } from "../";
|
||||||
// ui
|
// ui
|
||||||
import { Input, Button } from "@plane/ui";
|
import { Input, Button } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
@ -85,7 +85,7 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem] py-5 border-[0.1px] border-custom-border-100">
|
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[40rem] py-5 border-[0.1px] border-custom-border-100">
|
||||||
<div className="p-3 px-5 relative flex items-center gap-2">
|
<div className="p-3 px-5 relative flex items-center gap-2">
|
||||||
{projectId && projectDetails ? (
|
{projectId && projectDetails ? (
|
||||||
<div className="relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
|
<div className="relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
|
||||||
@ -121,13 +121,27 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3 px-5 relative flex justify-between items-center gap-2">
|
<div className="p-3 px-5 relative flex justify-between items-center gap-2">
|
||||||
<div className="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
|
<ViewFiltersDropdown
|
||||||
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
|
workspaceSlug={workspaceSlug}
|
||||||
<Plus className="w-3 h-3" />
|
projectId={projectId}
|
||||||
|
viewId={viewId}
|
||||||
|
viewType={viewType}
|
||||||
|
viewOperations={viewOperations}
|
||||||
|
dropdownPlacement="right"
|
||||||
|
>
|
||||||
|
<div className="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-custom-border-100 bg-custom-background-80">
|
||||||
|
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">Filters</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs">Filters</div>
|
</ViewFiltersDropdown>
|
||||||
</div>
|
<div
|
||||||
<div className="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-dashed border-custom-border-100 bg-custom-background-80">
|
className="cursor-pointer relative rounded p-1.5 px-2 flex items-center gap-1 border border-dashed border-custom-border-100 bg-custom-background-80"
|
||||||
|
onClick={() => {
|
||||||
|
viewOperations.setFilters(undefined, "clear_all");
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="text-xs">Clear all filters</div>
|
<div className="text-xs">Clear all filters</div>
|
||||||
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
|
<div className="flex-shrink-0 relative flex justify-center items-center w-4 h-4 overflow-hidden">
|
||||||
<X className="w-3 h-3" />
|
<X className="w-3 h-3" />
|
||||||
@ -135,13 +149,14 @@ export const ViewCreateEditForm: FC<TViewCreateEditForm> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3 px-5 relative bg-custom-background-80">
|
<div className="p-3 px-5 relative bg-custom-background-90 max-h-36 overflow-hidden overflow-y-auto">
|
||||||
<ViewAppliedFiltersRoot
|
<ViewAppliedFiltersRoot
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
|
propertyVisibleCount={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -64,48 +64,46 @@ export const ViewRoot: FC<TViewRoot> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex justify-between px-5 gap-2">
|
<div className="relative flex justify-between px-5 gap-2">
|
||||||
{viewStore?.viewIds && viewStore?.viewIds.length > 0 && (
|
<div className="w-full">
|
||||||
<div
|
{viewStore?.viewIds && viewStore?.viewIds.length > 0 && (
|
||||||
key={`views_list_${viewId}`}
|
<div id="tab-container" className="relative flex items-center w-full overflow-hidden">
|
||||||
id="tab-container"
|
{viewIds.map((_viewId) => (
|
||||||
className="relative flex items-center w-full overflow-hidden"
|
<Fragment key={_viewId}>
|
||||||
>
|
<ViewItem
|
||||||
{viewIds.map((_viewId) => (
|
workspaceSlug={workspaceSlug}
|
||||||
<Fragment key={_viewId}>
|
projectId={projectId}
|
||||||
<ViewItem
|
viewId={viewId}
|
||||||
workspaceSlug={workspaceSlug}
|
viewType={viewType}
|
||||||
projectId={projectId}
|
viewItemId={_viewId}
|
||||||
viewId={viewId}
|
baseRoute={baseRoute}
|
||||||
viewType={viewType}
|
/>
|
||||||
viewItemId={_viewId}
|
</Fragment>
|
||||||
baseRoute={baseRoute}
|
))}
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div id="tab-item-view-more" className="min-w-[90px]">
|
<div id="tab-item-view-more" className="min-w-[90px]">
|
||||||
{viewStore?.viewIds.length <= (itemsToRenderViewsCount || viewStore?.viewIds.length) ? null : (
|
{viewStore?.viewIds.length <= (itemsToRenderViewsCount || viewStore?.viewIds.length) ? null : (
|
||||||
<ViewDropdown
|
<ViewDropdown
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
viewType={viewType}
|
viewType={viewType}
|
||||||
viewOperations={viewOperations}
|
viewOperations={viewOperations}
|
||||||
baseRoute={baseRoute}
|
baseRoute={baseRoute}
|
||||||
>
|
>
|
||||||
<div className="text-sm font-semibold mb-1 p-2 px-2.5 text-custom-text-200 cursor-pointer hover:bg-custom-background-80 whitespace-nowrap rounded relative flex items-center gap-1">
|
<div className="text-sm font-semibold mb-1 p-2 px-2.5 text-custom-text-200 cursor-pointer hover:bg-custom-background-80 whitespace-nowrap rounded relative flex items-center gap-1">
|
||||||
<span>
|
<span>
|
||||||
<Plus size={12} />
|
<Plus size={12} />
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{viewStore?.viewIds.length - (itemsToRenderViewsCount || viewStore?.viewIds.length)} More...
|
{viewStore?.viewIds.length - (itemsToRenderViewsCount || viewStore?.viewIds.length)} More...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ViewDropdown>
|
</ViewDropdown>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<div className="flex-shrink-0 my-auto pb-1">
|
<div className="flex-shrink-0 my-auto pb-1">
|
||||||
<Button size="sm" prependIcon={<Plus />} onClick={() => viewOperations?.localViewCreateEdit(undefined)}>
|
<Button size="sm" prependIcon={<Plus />} onClick={() => viewOperations?.localViewCreateEdit(undefined)}>
|
||||||
|
@ -47,10 +47,10 @@ export const DATE_PROPERTY: {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
last_week: { label: "Last Week" },
|
"1_weeks;after;fromnow": { label: "1 week from now" },
|
||||||
"2_weeks_from_now": { label: "2 weeks from now" },
|
"2_weeks;after;fromnow": { label: "2 weeks from now" },
|
||||||
"1_month_from_now": { label: "1 month from now" },
|
"1_months;after;fromnow": { label: "1 month from now" },
|
||||||
"2_months_from_now": { label: "2 months from now" },
|
"2_months;after;fromnow": { label: "2 months from now" },
|
||||||
custom: { label: "Custom" },
|
custom: { label: "Custom" },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -110,7 +110,8 @@ const ALL_FILTER_PERMISSIONS: TFilterPermissions["all"] = {
|
|||||||
"start_date",
|
"start_date",
|
||||||
"target_date",
|
"target_date",
|
||||||
],
|
],
|
||||||
display_filters: ["type"],
|
// display_filters: ["type"],
|
||||||
|
display_filters: ["group_by", "sub_group_by", "order_by", "type"],
|
||||||
extra_options: [],
|
extra_options: [],
|
||||||
display_properties: true,
|
display_properties: true,
|
||||||
},
|
},
|
||||||
|
@ -1,21 +1,35 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { Briefcase, CalendarDays, CircleUser, Tag } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject, useModule, useCycle, useProjectState, useMember, useLabel } from "hooks/store";
|
import { useProject, useModule, useCycle, useProjectState, useMember, useLabel } from "hooks/store";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
import {
|
||||||
|
Avatar,
|
||||||
|
ContrastIcon,
|
||||||
|
CycleGroupIcon,
|
||||||
|
DiceIcon,
|
||||||
|
DoubleCircleIcon,
|
||||||
|
PriorityIcon,
|
||||||
|
StateGroupIcon,
|
||||||
|
} from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { TIssuePriorities, TStateGroups, TViewFilters } from "@plane/types";
|
import { TIssuePriorities, TStateGroups, TViewFilters } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { STATE_GROUP_PROPERTY, PRIORITIES_PROPERTY, DATE_PROPERTY } from "constants/view/filters";
|
import { STATE_GROUP_PROPERTY, PRIORITIES_PROPERTY, DATE_PROPERTY } from "constants/view/filters";
|
||||||
import { Briefcase, CalendarDays } from "lucide-react";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
|
|
||||||
type TFilterPropertyDetails = {
|
type TFilterPropertyDetails = {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TFilterPropertyDefaultDetails = {
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => {
|
export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => {
|
||||||
const { projectMap, getProjectById } = useProject();
|
const { projectMap, getProjectById } = useProject();
|
||||||
const { getProjectModuleIds, getModuleById } = useModule();
|
const { getProjectModuleIds, getModuleById } = useModule();
|
||||||
@ -73,6 +87,80 @@ export const useViewFilter = (workspaceSlug: string, projectId: string | undefin
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const propertyDefaultDetails = (filterKey: keyof TViewFilters): TFilterPropertyDefaultDetails | undefined => {
|
||||||
|
if (!filterKey) return undefined;
|
||||||
|
|
||||||
|
switch (filterKey) {
|
||||||
|
case "project":
|
||||||
|
return {
|
||||||
|
icon: <Briefcase size={12} />,
|
||||||
|
label: "Projects",
|
||||||
|
};
|
||||||
|
case "module":
|
||||||
|
return {
|
||||||
|
icon: <DiceIcon className="w-3 h-3" />,
|
||||||
|
label: "Modules",
|
||||||
|
};
|
||||||
|
case "cycle":
|
||||||
|
return {
|
||||||
|
icon: <ContrastIcon className="w-3 h-3" />,
|
||||||
|
label: "Cycles",
|
||||||
|
};
|
||||||
|
case "priority":
|
||||||
|
return {
|
||||||
|
icon: <PriorityIcon priority="high" withContainer size={10} />,
|
||||||
|
label: "Priorities",
|
||||||
|
};
|
||||||
|
case "state":
|
||||||
|
return {
|
||||||
|
icon: <DoubleCircleIcon className="w-3 h-3" />,
|
||||||
|
label: "States",
|
||||||
|
};
|
||||||
|
case "state_group":
|
||||||
|
return {
|
||||||
|
icon: <DoubleCircleIcon className="w-3 h-3" />,
|
||||||
|
label: "State Groups",
|
||||||
|
};
|
||||||
|
case "assignees":
|
||||||
|
return {
|
||||||
|
icon: <CircleUser size={12} />,
|
||||||
|
label: "Assignees",
|
||||||
|
};
|
||||||
|
case "mentions":
|
||||||
|
return {
|
||||||
|
icon: <CircleUser size={12} />,
|
||||||
|
label: "Mentions",
|
||||||
|
};
|
||||||
|
case "subscriber":
|
||||||
|
return {
|
||||||
|
icon: <CircleUser size={12} />,
|
||||||
|
label: "Subscribers",
|
||||||
|
};
|
||||||
|
case "created_by":
|
||||||
|
return {
|
||||||
|
icon: <CircleUser size={12} />,
|
||||||
|
label: "Creators",
|
||||||
|
};
|
||||||
|
case "labels":
|
||||||
|
return {
|
||||||
|
icon: <Tag size={12} />,
|
||||||
|
label: "Labels",
|
||||||
|
};
|
||||||
|
case "start_date":
|
||||||
|
return {
|
||||||
|
icon: <CalendarDays size={12} />,
|
||||||
|
label: "Start Dates",
|
||||||
|
};
|
||||||
|
case "target_date":
|
||||||
|
return {
|
||||||
|
icon: <CalendarDays size={12} />,
|
||||||
|
label: "Target Dates",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const propertyDetails = (filterKey: keyof TViewFilters, propertyId: string): TFilterPropertyDetails | undefined => {
|
const propertyDetails = (filterKey: keyof TViewFilters, propertyId: string): TFilterPropertyDetails | undefined => {
|
||||||
if (!filterKey || !propertyId) return undefined;
|
if (!filterKey || !propertyId) return undefined;
|
||||||
|
|
||||||
@ -200,19 +288,39 @@ export const useViewFilter = (workspaceSlug: string, projectId: string | undefin
|
|||||||
label: labelPropertyDetail.name,
|
label: labelPropertyDetail.name,
|
||||||
};
|
};
|
||||||
case "start_date":
|
case "start_date":
|
||||||
const startDatePropertyDetail = DATE_PROPERTY?.[propertyId];
|
if (propertyId.includes("-")) {
|
||||||
if (!startDatePropertyDetail) return undefined;
|
const customDateString = propertyId.split(";");
|
||||||
return {
|
return {
|
||||||
icon: <CalendarDays size={12} />,
|
icon: <CalendarDays size={12} />,
|
||||||
label: startDatePropertyDetail.label,
|
label: `${customDateString[1].charAt(0).toUpperCase()}${customDateString[1].slice(1)} ${renderFormattedDate(
|
||||||
};
|
customDateString[0]
|
||||||
|
)}`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const startDatePropertyDetail = DATE_PROPERTY?.[propertyId];
|
||||||
|
if (!startDatePropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <CalendarDays size={12} />,
|
||||||
|
label: startDatePropertyDetail.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
case "target_date":
|
case "target_date":
|
||||||
const targetDatePropertyDetail = DATE_PROPERTY?.[propertyId];
|
if (propertyId.includes("-")) {
|
||||||
if (!targetDatePropertyDetail) return undefined;
|
const customDateString = propertyId.split(";");
|
||||||
return {
|
return {
|
||||||
icon: <CalendarDays size={12} />,
|
icon: <CalendarDays size={12} />,
|
||||||
label: targetDatePropertyDetail.label,
|
label: `${customDateString[1].charAt(0).toUpperCase()}${customDateString[1].slice(1)} ${renderFormattedDate(
|
||||||
};
|
customDateString[0]
|
||||||
|
)}`,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const targetDatePropertyDetail = DATE_PROPERTY?.[propertyId];
|
||||||
|
if (!targetDatePropertyDetail) return undefined;
|
||||||
|
return {
|
||||||
|
icon: <CalendarDays size={12} />,
|
||||||
|
label: targetDatePropertyDetail.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -220,6 +328,7 @@ export const useViewFilter = (workspaceSlug: string, projectId: string | undefin
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
filterIdsWithKey,
|
filterIdsWithKey,
|
||||||
|
propertyDefaultDetails,
|
||||||
propertyDetails,
|
propertyDetails,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { ReactElement, useMemo } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// layouts
|
||||||
|
import { AppLayout } from "layouts/app-layout";
|
||||||
|
// components
|
||||||
|
import { AllIssuesViewRoot } from "components/view";
|
||||||
|
// types
|
||||||
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// constants
|
||||||
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
|
const ProjectPrivateViewPage: NextPageWithLayout = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
|
const workspaceViewTabOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.PROJECT_PRIVATE_VIEWS,
|
||||||
|
title: "Private",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/views/private`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.PROJECT_PUBLIC_VIEWS,
|
||||||
|
title: "Public",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/views/public`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[workspaceSlug, projectId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<AllIssuesViewRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
viewId={viewId.toString()}
|
||||||
|
viewType={VIEW_TYPES.PROJECT_PRIVATE_VIEWS}
|
||||||
|
baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`}
|
||||||
|
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectPrivateViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectPrivateViewPage;
|
@ -0,0 +1,53 @@
|
|||||||
|
import { ReactElement, useMemo } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// layouts
|
||||||
|
import { AppLayout } from "layouts/app-layout";
|
||||||
|
// components
|
||||||
|
import { AllIssuesViewRoot } from "components/view";
|
||||||
|
// types
|
||||||
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// constants
|
||||||
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
|
const ProjectPrivateViewPage: NextPageWithLayout = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
|
const workspaceViewTabOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
||||||
|
title: "Private",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/views/private`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
||||||
|
title: "Public",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/views/public`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[workspaceSlug, projectId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<AllIssuesViewRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
viewId={viewId.toString()}
|
||||||
|
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
|
||||||
|
baseRoute={`/${workspaceSlug?.toString()}/projects/${projectId}/views/private`}
|
||||||
|
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectPrivateViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectPrivateViewPage;
|
@ -0,0 +1,54 @@
|
|||||||
|
import { ReactElement, useMemo } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// layouts
|
||||||
|
import { AppLayout } from "layouts/app-layout";
|
||||||
|
// components
|
||||||
|
import { AllIssuesViewRoot } from "components/view";
|
||||||
|
// types
|
||||||
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
// constants
|
||||||
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
|
const ProjectPublicViewPage: NextPageWithLayout = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
|
const workspaceViewTabOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.PROJECT_PRIVATE_VIEWS,
|
||||||
|
title: "Private",
|
||||||
|
href: `/${workspaceSlug}/views/private/assigned`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.PROJECT_PUBLIC_VIEWS,
|
||||||
|
title: "Public",
|
||||||
|
href: `/${workspaceSlug}/views/public/all-issues`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspaceSlug || !viewId) return <></>;
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
|
||||||
|
<div className="flex-shrink-0 w-full">
|
||||||
|
<AllIssuesViewRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={undefined}
|
||||||
|
viewId={viewId.toString()}
|
||||||
|
viewType={VIEW_TYPES.PROJECT_PUBLIC_VIEWS}
|
||||||
|
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
|
||||||
|
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full overflow-hidden">Issues render</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectPublicViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectPublicViewPage;
|
@ -1,4 +1,4 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement, useMemo } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
@ -9,10 +9,26 @@ import { NextPageWithLayout } from "lib/types";
|
|||||||
// constants
|
// constants
|
||||||
import { VIEW_TYPES } from "constants/view";
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
const WorkspacePrivateViewPage: NextPageWithLayout = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, viewId } = router.query;
|
const { workspaceSlug, viewId } = router.query;
|
||||||
|
|
||||||
|
const workspaceViewTabOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
||||||
|
title: "Private",
|
||||||
|
href: `/${workspaceSlug}/views/private/assigned`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
||||||
|
title: "Public",
|
||||||
|
href: `/${workspaceSlug}/views/public/all-issues`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
if (!workspaceSlug || !viewId) return <></>;
|
if (!workspaceSlug || !viewId) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-hidden bg-custom-background-100">
|
<div className="h-full overflow-hidden bg-custom-background-100">
|
||||||
@ -23,14 +39,15 @@ const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
|||||||
viewId={viewId.toString()}
|
viewId={viewId.toString()}
|
||||||
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
|
viewType={VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS}
|
||||||
baseRoute={`/${workspaceSlug?.toString()}/views/private`}
|
baseRoute={`/${workspaceSlug?.toString()}/views/private`}
|
||||||
|
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
WorkspacePrivateViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <AppLayout header={<></>}>{page}</AppLayout>;
|
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GlobalViewIssuesPage;
|
export default WorkspacePrivateViewPage;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement, useMemo } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
@ -9,10 +9,26 @@ import { NextPageWithLayout } from "lib/types";
|
|||||||
// constants
|
// constants
|
||||||
import { VIEW_TYPES } from "constants/view";
|
import { VIEW_TYPES } from "constants/view";
|
||||||
|
|
||||||
const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
const WorkspacePublicViewPage: NextPageWithLayout = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, viewId } = router.query;
|
const { workspaceSlug, viewId } = router.query;
|
||||||
|
|
||||||
|
const workspaceViewTabOptions = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.WORKSPACE_PRIVATE_VIEWS,
|
||||||
|
title: "Private",
|
||||||
|
href: `/${workspaceSlug}/views/private/assigned`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS,
|
||||||
|
title: "Public",
|
||||||
|
href: `/${workspaceSlug}/views/public/all-issues`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
if (!workspaceSlug || !viewId) return <></>;
|
if (!workspaceSlug || !viewId) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
|
<div className="w-full h-full overflow-hidden bg-custom-background-100 relative flex flex-col">
|
||||||
@ -23,6 +39,7 @@ const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
|||||||
viewId={viewId.toString()}
|
viewId={viewId.toString()}
|
||||||
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
|
viewType={VIEW_TYPES.WORKSPACE_PUBLIC_VIEWS}
|
||||||
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
|
baseRoute={`/${workspaceSlug?.toString()}/views/public`}
|
||||||
|
workspaceViewTabOptions={workspaceViewTabOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full overflow-hidden">Issues render</div>
|
<div className="w-full h-full overflow-hidden">Issues render</div>
|
||||||
@ -30,8 +47,8 @@ const GlobalViewIssuesPage: NextPageWithLayout = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
GlobalViewIssuesPage.getLayout = function getLayout(page: ReactElement) {
|
WorkspacePublicViewPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <AppLayout header={<></>}>{page}</AppLayout>;
|
return <AppLayout header={<></>}>{page}</AppLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GlobalViewIssuesPage;
|
export default WorkspacePublicViewPage;
|
||||||
|
Loading…
Reference in New Issue
Block a user