diff --git a/web/components/views/applied-filters/filter-item-map.tsx b/web/components/views/applied-filters/filter-item-map.tsx new file mode 100644 index 000000000..c821ea696 --- /dev/null +++ b/web/components/views/applied-filters/filter-item-map.tsx @@ -0,0 +1,57 @@ +import { FC, useMemo } from "react"; +// components +import { ViewAppliedFiltersItem } from "./"; +// hooks +import { useViewFilter } from "hooks/user-view-filters"; +// types +import { IIssueFilterOptions } from "@plane/types"; + +type TViewAppliedFiltersItemMap = { + workspaceSlug: string; + projectId: string; + filterKey: keyof IIssueFilterOptions; + filterValue: string[]; +}; + +export const ViewAppliedFiltersItemMap: FC = (props) => { + const { workspaceSlug, projectId, filterKey, filterValue } = props; + // hooks + const viewFilterStore = useViewFilter(workspaceSlug, projectId); + + const currentDefaultFilterDetails = useMemo( + () => viewFilterStore?.propertyDefaultDetails(filterKey), + [viewFilterStore, filterKey] + ); + + const propertyVisibleCount = 5; + + if (!filterValue) return <>; + + return ( +
+
{filterKey.replaceAll("_", " ")}
+
+ {propertyVisibleCount && filterValue.length >= propertyVisibleCount ? ( +
+
{currentDefaultFilterDetails?.icon}
+
+ {filterValue.length} {currentDefaultFilterDetails?.label} +
+
+ ) : ( + <> + {filterValue.map((propertyId) => ( + + ))} + + )} +
+
+ ); +}; diff --git a/web/components/views/applied-filters/filter-item.tsx b/web/components/views/applied-filters/filter-item.tsx new file mode 100644 index 000000000..6d2382a3b --- /dev/null +++ b/web/components/views/applied-filters/filter-item.tsx @@ -0,0 +1,32 @@ +import { FC } from "react"; +import { ImagePlus } from "lucide-react"; +// types +import { IIssueFilterOptions } from "@plane/types"; +import { useViewFilter } from "hooks/user-view-filters"; + +type TViewAppliedFiltersItem = { + workspaceSlug: string; + projectId: string; + filterKey: keyof IIssueFilterOptions; + propertyId: string; +}; + +export const ViewAppliedFiltersItem: FC = (props) => { + const { workspaceSlug, projectId, filterKey, propertyId } = props; + // hooks + const viewFilterHelper = useViewFilter(workspaceSlug, projectId); + const propertyDetail = viewFilterHelper?.propertyDetails(filterKey, propertyId) || undefined; + + if (!filterKey || !propertyId) return <>; + return ( +
+
+ {propertyDetail?.icon || } +
+
{propertyDetail?.label || propertyId}
+
+ ); +}; diff --git a/web/components/views/applied-filters/filters-root.tsx b/web/components/views/applied-filters/filters-root.tsx new file mode 100644 index 000000000..b0ea050eb --- /dev/null +++ b/web/components/views/applied-filters/filters-root.tsx @@ -0,0 +1,36 @@ +import { FC } from "react"; +// components +import { ViewAppliedFiltersItemMap } from "./"; +// types +import { IIssueFilterOptions } from "@plane/types"; + +type TViewAppliedFilters = { + workspaceSlug: string; + projectId: string; + filters: Partial>; +}; + +export const ViewAppliedFilters: FC = (props) => { + const { workspaceSlug, projectId, filters } = props; + + if (filters && Object.keys(filters).length <= 0) + return ( +
+
0 Filters
+
+ ); + + return ( +
+ {Object.entries(filters || {}).map(([key, value]) => ( + + ))} +
+ ); +}; diff --git a/web/components/views/applied-filters/index.ts b/web/components/views/applied-filters/index.ts new file mode 100644 index 000000000..bd3df1412 --- /dev/null +++ b/web/components/views/applied-filters/index.ts @@ -0,0 +1,3 @@ +export * from "./filters-root"; +export * from "./filter-item-map"; +export * from "./filter-item"; diff --git a/web/components/views/index.ts b/web/components/views/index.ts index b7ebe5081..ab9a0c048 100644 --- a/web/components/views/index.ts +++ b/web/components/views/index.ts @@ -3,3 +3,4 @@ export * from "./form"; export * from "./modal"; export * from "./view-list-item"; export * from "./views-list"; +export * from "./applied-filters"; diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index 29d5bac57..2777b4776 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -6,11 +6,11 @@ import { LinkIcon, PencilIcon, StarIcon, TrashIcon } from "lucide-react"; // ui import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { CreateUpdateProjectViewModal, DeleteProjectViewModal } from "components/views"; +import { CreateUpdateProjectViewModal, DeleteProjectViewModal, ViewAppliedFilters } from "components/views"; // constants import { EUserProjectRoles } from "constants/project"; // helpers -import { calculateTotalFilters } from "helpers/filter.helper"; +// import { calculateTotalFilters } from "helpers/filter.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; // hooks import { useProjectView, useUser } from "hooks/store"; @@ -59,7 +59,7 @@ export const ProjectViewListItem: React.FC = observer((props) => { }); }; - const totalFilters = calculateTotalFilters(view.filters ?? {}); + // const totalFilters = calculateTotalFilters(view.filters ?? {}); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; @@ -77,84 +77,85 @@ export const ProjectViewListItem: React.FC = observer((props) => { setDeleteViewModal(false)} />
-
-
-
-
-

{view.name}

- {view?.description &&

{view.description}

} -
+
+
+
+

{view.name}

+ {view?.description &&

{view.description}

}
-
-
-

- {totalFilters} {totalFilters === 1 ? "filter" : "filters"} -

- {isEditingAllowed && - (view.is_favorite ? ( - - ) : ( - - ))} +
- - {isEditingAllowed && ( - <> - { - e.preventDefault(); - e.stopPropagation(); - setCreateUpdateViewModal(true); - }} - > - - - Edit View - - - { - e.preventDefault(); - e.stopPropagation(); - setDeleteViewModal(true); - }} - > - - - Delete View - - - - )} - +
+ {workspaceSlug && projectId && ( + + )} + + {isEditingAllowed && + (view.is_favorite ? ( + + ) : ( + + ))} + + {isEditingAllowed && ( + <> + { + e.preventDefault(); + e.stopPropagation(); + setCreateUpdateViewModal(true); + }} + > - - Copy view link + + Edit View - -
-
+ { + e.preventDefault(); + e.stopPropagation(); + setDeleteViewModal(true); + }} + > + + + Delete View + + + + )} + + + + Copy view link + + +
diff --git a/web/constants/views/filters.ts b/web/constants/views/filters.ts new file mode 100644 index 000000000..378bee594 --- /dev/null +++ b/web/constants/views/filters.ts @@ -0,0 +1,26 @@ +import { TIssuePriorities, TStateGroups } from "@plane/types"; + +// filters constants +export const STATE_GROUP_PROPERTY: Record = { + backlog: { label: "Backlog", color: "#d9d9d9" }, + unstarted: { label: "Unstarted", color: "#3f76ff" }, + started: { label: "Started", color: "#f59e0b" }, + completed: { label: "Completed", color: "#16a34a" }, + cancelled: { label: "Canceled", color: "#dc2626" }, +}; + +export const PRIORITIES_PROPERTY: Record = { + urgent: { label: "Urgent" }, + high: { label: "High" }, + medium: { label: "Medium" }, + low: { label: "Low" }, + none: { label: "None" }, +}; + +export const DATE_PROPERTY: Record = { + "1_weeks;after;fromnow": { label: "1 week from now" }, + "2_weeks;after;fromnow": { label: "2 weeks from now" }, + "1_months;after;fromnow": { label: "1 month from now" }, + "2_months;after;fromnow": { label: "2 months from now" }, + custom: { label: "Custom" }, +}; diff --git a/web/hooks/user-view-filters.tsx b/web/hooks/user-view-filters.tsx new file mode 100644 index 000000000..243ebf6c0 --- /dev/null +++ b/web/hooks/user-view-filters.tsx @@ -0,0 +1,302 @@ +import { ReactNode } from "react"; +import { Briefcase, CalendarDays, CircleUser, Tag } from "lucide-react"; +// hooks +import { useProject, useModule, useCycle, useProjectState, useMember, useLabel } from "hooks/store"; +// ui +import { + Avatar, + ContrastIcon, + CycleGroupIcon, + DiceIcon, + DoubleCircleIcon, + PriorityIcon, + StateGroupIcon, +} from "@plane/ui"; +// types +import { TIssuePriorities, TStateGroups, IIssueFilterOptions } from "@plane/types"; +// constants +import { STATE_GROUP_PROPERTY, PRIORITIES_PROPERTY, DATE_PROPERTY } from "constants/views/filters"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; + +type TFilterPropertyDetails = { + icon: ReactNode; + label: string; +}; + +type TFilterPropertyDefaultDetails = { + icon: ReactNode; + label: string; +}; + +export const useViewFilter = (workspaceSlug: string, projectId: string | undefined) => { + const { getProjectById } = useProject(); + const { getModuleById } = useModule(); + const { getCycleById } = useCycle(); + const { getStateById } = useProjectState(); + const { getUserDetails } = useMember(); + const { getLabelById } = useLabel(); + + if (!workspaceSlug || !projectId) return undefined; + + const propertyDefaultDetails = (filterKey: keyof IIssueFilterOptions): TFilterPropertyDefaultDetails | undefined => { + if (!filterKey) return undefined; + + switch (filterKey) { + case "project": + return { + icon: , + label: "Projects", + }; + case "module": + return { + icon: , + label: "Modules", + }; + case "cycle": + return { + icon: , + label: "Cycles", + }; + case "priority": + return { + icon: , + label: "Priorities", + }; + case "state": + return { + icon: , + label: "States", + }; + case "state_group": + return { + icon: , + label: "State Groups", + }; + case "assignees": + return { + icon: , + label: "Assignees", + }; + case "mentions": + return { + icon: , + label: "Mentions", + }; + case "subscriber": + return { + icon: , + label: "Subscribers", + }; + case "created_by": + return { + icon: , + label: "Creators", + }; + case "labels": + return { + icon: , + label: "Labels", + }; + case "start_date": + return { + icon: , + label: "Start Dates", + }; + case "target_date": + return { + icon: , + label: "Target Dates", + }; + default: + return undefined; + } + }; + + const propertyDetails = ( + filterKey: keyof IIssueFilterOptions, + propertyId: string + ): TFilterPropertyDetails | undefined => { + if (!filterKey || !propertyId) return undefined; + + switch (filterKey) { + case "project": + const projectPropertyDetail = getProjectById(propertyId); + if (!projectPropertyDetail) return undefined; + return { + icon: ( + <> + {projectPropertyDetail?.logo_props?.in_use === "emoji" && + projectPropertyDetail?.logo_props?.emoji?.value ? ( +
+ {projectPropertyDetail?.logo_props?.emoji.value + ?.split("-") + .map((emoji) => String.fromCodePoint(parseInt(emoji, 10)))} +
+ ) : projectPropertyDetail?.logo_props?.in_use === "icon" && + projectPropertyDetail?.logo_props?.icon?.name ? ( +
+ {renderEmoji(projectPropertyDetail?.logo_props?.icon?.name)} +
+ ) : ( + + )} + + ), + label: projectPropertyDetail.name, + }; + case "module": + const modulePropertyDetail = getModuleById(propertyId); + if (!modulePropertyDetail) return undefined; + return { + icon: , + label: modulePropertyDetail.name, + }; + case "cycle": + const cyclePropertyDetail = getCycleById(propertyId); + if (!cyclePropertyDetail) return undefined; + return { + icon: , + label: cyclePropertyDetail.name, + }; + case "priority": + const priorityPropertyDetail = PRIORITIES_PROPERTY?.[propertyId as TIssuePriorities]; + if (!priorityPropertyDetail) return undefined; + return { + icon: , + label: priorityPropertyDetail.label, + }; + case "state": + const statePropertyDetail = getStateById(propertyId); + if (!statePropertyDetail) return undefined; + return { + icon: , + label: statePropertyDetail.name, + }; + case "state_group": + const stateGroupPropertyDetail = STATE_GROUP_PROPERTY?.[propertyId as TStateGroups]; + if (!stateGroupPropertyDetail) return undefined; + return { + icon: , + label: stateGroupPropertyDetail.label, + }; + case "assignees": + const assigneePropertyDetail = getUserDetails(propertyId); + if (!assigneePropertyDetail) return undefined; + return { + icon: ( + + ), + label: assigneePropertyDetail.display_name, + }; + case "mentions": + const mentionPropertyDetail = getUserDetails(propertyId); + if (!mentionPropertyDetail) return undefined; + return { + icon: ( + + ), + label: mentionPropertyDetail.display_name, + }; + case "subscriber": + const subscribedPropertyDetail = getUserDetails(propertyId); + if (!subscribedPropertyDetail) return undefined; + return { + icon: ( + + ), + label: subscribedPropertyDetail.display_name, + }; + case "created_by": + const createdByPropertyDetail = getUserDetails(propertyId); + if (!createdByPropertyDetail) return undefined; + return { + icon: ( + + ), + label: createdByPropertyDetail.display_name, + }; + case "labels": + const labelPropertyDetail = getLabelById(propertyId); + if (!labelPropertyDetail) return undefined; + return { + icon: ( +
+ ), + label: labelPropertyDetail.name, + }; + case "start_date": + if (propertyId.includes("-")) { + const customDateString = propertyId.split(";"); + return { + icon: , + 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: , + label: startDatePropertyDetail.label, + }; + } + case "target_date": + if (propertyId.includes("-")) { + const customDateString = propertyId.split(";"); + return { + icon: , + 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: , + label: targetDatePropertyDetail.label, + }; + } + default: + return undefined; + } + }; + + return { + propertyDefaultDetails, + propertyDetails, + }; +};