[WEB-5132] feat: add label-based grouping and filtering for initiatives (#4467)
This commit is contained in:
@@ -9,6 +9,8 @@ from plane.utils.filters.filterset import IssueFilterSet, UUIDInFilter, CharInFi
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.ee.models import Initiative
|
||||
from plane.utils.uuid import is_valid_uuid
|
||||
|
||||
|
||||
class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
team_project_id = filters.UUIDFilter(field_name="project_id")
|
||||
team_project_id__in = UUIDInFilter(field_name="project_id", lookup_expr="in")
|
||||
@@ -62,7 +64,6 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
return fields
|
||||
return []
|
||||
|
||||
|
||||
def filter_custom_property_value(self, queryset, name, value):
|
||||
"""Filter by custom property value (exact match)"""
|
||||
return self._filter_custom_property_value_with_lookup(queryset, "exact", value)
|
||||
@@ -113,7 +114,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
|
||||
def _filter_custom_property_value_with_lookup(self, queryset, lookup, value):
|
||||
"""Helper method to filter by custom property value with a specific lookup.
|
||||
|
||||
|
||||
This method handles the actual filtering logic for custom properties by:
|
||||
1. Getting the property_id from the request context (set by the filter backend)
|
||||
2. Joining with the IssuePropertyValue table
|
||||
@@ -136,12 +137,13 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
return Q()
|
||||
|
||||
# Get the property to determine its type
|
||||
property_obj = IssueProperty.objects.get(
|
||||
id=property_id
|
||||
)
|
||||
|
||||
property_obj = IssueProperty.objects.get(id=property_id)
|
||||
|
||||
# Build the filter based on the property type and lookup
|
||||
if property_obj.property_type in [PropertyTypeEnum.TEXT.value, PropertyTypeEnum.URL.value,]:
|
||||
if property_obj.property_type in [
|
||||
PropertyTypeEnum.TEXT.value,
|
||||
PropertyTypeEnum.URL.value,
|
||||
]:
|
||||
field_name = "value_text"
|
||||
elif property_obj.property_type == PropertyTypeEnum.DECIMAL:
|
||||
field_name = "value_decimal"
|
||||
@@ -171,7 +173,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}": value}
|
||||
**{f"properties__{field_name}": value},
|
||||
)
|
||||
elif lookup == "in" and property_obj.property_type in [
|
||||
PropertyTypeEnum.OPTION.value,
|
||||
@@ -182,7 +184,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__in": value}
|
||||
**{f"properties__{field_name}__in": value},
|
||||
)
|
||||
elif lookup == "gte" and property_obj.property_type in [
|
||||
PropertyTypeEnum.DECIMAL.value,
|
||||
@@ -193,7 +195,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__gte": value}
|
||||
**{f"properties__{field_name}__gte": value},
|
||||
)
|
||||
elif lookup == "gt" and property_obj.property_type in [
|
||||
PropertyTypeEnum.DECIMAL.value,
|
||||
@@ -204,7 +206,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__gt": value}
|
||||
**{f"properties__{field_name}__gt": value},
|
||||
)
|
||||
elif lookup == "lte" and property_obj.property_type in [
|
||||
PropertyTypeEnum.DECIMAL.value,
|
||||
@@ -215,7 +217,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__lte": value}
|
||||
**{f"properties__{field_name}__lte": value},
|
||||
)
|
||||
elif lookup == "lt" and property_obj.property_type in [
|
||||
PropertyTypeEnum.DECIMAL.value,
|
||||
@@ -226,7 +228,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__lt": value}
|
||||
**{f"properties__{field_name}__lt": value},
|
||||
)
|
||||
elif lookup == "icontains" and property_obj.property_type in [
|
||||
PropertyTypeEnum.TEXT.value,
|
||||
@@ -237,7 +239,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__icontains": value}
|
||||
**{f"properties__{field_name}__icontains": value},
|
||||
)
|
||||
elif lookup == "contains" and property_obj.property_type in [
|
||||
PropertyTypeEnum.TEXT.value,
|
||||
@@ -248,7 +250,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__contains": value}
|
||||
**{f"properties__{field_name}__contains": value},
|
||||
)
|
||||
elif lookup == "startswith" and property_obj.property_type in [
|
||||
PropertyTypeEnum.TEXT.value,
|
||||
@@ -259,7 +261,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__startswith": value}
|
||||
**{f"properties__{field_name}__startswith": value},
|
||||
)
|
||||
elif lookup == "endswith" and property_obj.property_type in [
|
||||
PropertyTypeEnum.TEXT.value,
|
||||
@@ -270,7 +272,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__endswith": value}
|
||||
**{f"properties__{field_name}__endswith": value},
|
||||
)
|
||||
elif lookup == "range" and property_obj.property_type in [
|
||||
PropertyTypeEnum.DECIMAL.value,
|
||||
@@ -283,7 +285,7 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
properties__property_id=property_id,
|
||||
properties__property__deleted_at__isnull=True,
|
||||
properties__deleted_at__isnull=True,
|
||||
**{f"properties__{field_name}__range": value}
|
||||
**{f"properties__{field_name}__range": value},
|
||||
)
|
||||
return Q()
|
||||
else:
|
||||
@@ -296,6 +298,8 @@ class ExtendedIssueFilterSet(IssueFilterSet):
|
||||
class InitiativeFilterSet(BaseFilterSet):
|
||||
lead = filters.UUIDFilter(field_name="lead")
|
||||
lead__in = UUIDInFilter(field_name="lead", lookup_expr="in")
|
||||
label_id = filters.UUIDFilter(method="filter_label_id")
|
||||
label_id__in = UUIDInFilter(method="filter_label_id_in", lookup_expr="in")
|
||||
|
||||
class Meta:
|
||||
model = Initiative
|
||||
@@ -304,3 +308,17 @@ class InitiativeFilterSet(BaseFilterSet):
|
||||
"end_date": ["exact", "gte", "gt", "lte", "lt", "range"],
|
||||
"state": ["exact", "in"],
|
||||
}
|
||||
|
||||
def filter_label_id(self, queryset, name, value):
|
||||
"""Filter by label IDs (in), excluding soft deleted labels"""
|
||||
return Q(
|
||||
initiative_label_associations__label_id=value,
|
||||
initiative_label_associations__deleted_at__isnull=True,
|
||||
)
|
||||
|
||||
def filter_label_id_in(self, queryset, name, value):
|
||||
"""Filter by label IDs (in), excluding soft deleted labels"""
|
||||
return Q(
|
||||
initiative_label_associations__label_id__in=value,
|
||||
initiative_label_associations__deleted_at__isnull=True,
|
||||
)
|
||||
|
||||
+1
-3
@@ -10,11 +10,9 @@ import { Popover, Transition } from "@headlessui/react";
|
||||
// plane imports
|
||||
import { getRandomLabelColor, LABEL_COLOR_OPTIONS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
import { Button, Input } from "@plane/ui";
|
||||
|
||||
// local imports
|
||||
import type { TInitiativeLabel } from "@/plane-web/types";
|
||||
|
||||
export type TInitiativeLabelOperationsCallbacks = {
|
||||
createLabel: (data: Partial<TInitiativeLabel>) => Promise<TInitiativeLabel | undefined>;
|
||||
updateLabel: (labelId: string, data: Partial<TInitiativeLabel>) => Promise<TInitiativeLabel | undefined>;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { AlertModalCore } from "@plane/ui";
|
||||
|
||||
// local imports
|
||||
import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -7,9 +7,9 @@ import { X } from "lucide-react";
|
||||
|
||||
// plane imports
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
import { CustomMenu, DragHandle } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types";
|
||||
|
||||
// local imports
|
||||
import { InitiativeLabelName } from "./initiative-label-name";
|
||||
|
||||
+1
-2
@@ -12,13 +12,12 @@ import { createRoot } from "react-dom/client";
|
||||
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import type { InstructionType } from "@plane/types";
|
||||
import type { InstructionType, TInitiativeLabel } from "@plane/types";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
import { DropIndicator } from "@plane/ui";
|
||||
|
||||
// local imports
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types/initiative";
|
||||
import { InitiativeLabelName } from "./initiative-label-name";
|
||||
import { getInitiativeCanDrop } from "./initiative-label-utils";
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import { EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Combobox } from "@plane/propel/combobox";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
// types
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types/initiative";
|
||||
|
||||
export type TInitiativeLabelDropdownProps = {
|
||||
value: string[];
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
// plane imports
|
||||
import type { TInitiativeLabel } from "@/plane-web/types/initiative";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
// components
|
||||
import type { TInitiativeLabelOperationsCallbacks } from "./create-update-initiative-label-inline";
|
||||
import { CreateUpdateInitiativeLabelInline } from "./create-update-initiative-label-inline";
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useParams } from "next/navigation";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import SettingsHeading from "@/components/settings/heading";
|
||||
@@ -18,7 +19,6 @@ import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
|
||||
// local imports
|
||||
import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types";
|
||||
import type { TInitiativeLabelOperationsCallbacks } from "./create-update-initiative-label-inline";
|
||||
import { CreateUpdateInitiativeLabelInline } from "./create-update-initiative-label-inline";
|
||||
import { DeleteInitiativeLabelModal } from "./delete-initiative-label-modal";
|
||||
|
||||
+1
-1
@@ -12,6 +12,7 @@ import { EUserPermissionsLevel, getRandomLabelColor } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
import { ComboDropDown } from "@plane/ui";
|
||||
|
||||
// local imports
|
||||
@@ -19,7 +20,6 @@ import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types";
|
||||
import { getInitiativeLabelsArray } from "./initiative-label-utils";
|
||||
|
||||
export interface IInitiativeLabelPropertyDropdownProps {
|
||||
|
||||
@@ -9,12 +9,12 @@ import { Tags } from "lucide-react";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
|
||||
// local imports
|
||||
import { cn } from "@plane/utils";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types";
|
||||
import { InitiativeLabelPropertyDropdown } from "./initiative-label-property-dropdown";
|
||||
|
||||
export interface IInitiativeLabelProperty {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { TDropTarget } from "@plane/types";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types/initiative";
|
||||
import type { TDropTarget, TInitiativeLabel } from "@plane/types";
|
||||
|
||||
/**
|
||||
* This provides a boolean to indicate if the label can be dropped onto the droptarget
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { cn } from "@plane/utils";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives";
|
||||
import type { TInitiativeLabel } from "@/plane-web/types/initiative";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
import { InitiativeLabelPropertyDropdown } from "./initiative-label-property-dropdown";
|
||||
import { getInitiativeLabelsArray } from "./initiative-label-utils";
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ const InitiativesFilterInstanceProvider = observer(
|
||||
}) => {
|
||||
const {
|
||||
initiativeFilters: { updateFilters },
|
||||
initiative: { getInitiativesLabels },
|
||||
} = useInitiatives();
|
||||
|
||||
const [filterInstance] = useState(() =>
|
||||
@@ -57,15 +58,18 @@ const InitiativesFilterInstanceProvider = observer(
|
||||
getUserDetails,
|
||||
} = useMember();
|
||||
|
||||
const labels = getInitiativesLabels(workspaceSlug);
|
||||
|
||||
const workspaceMembers = useMemo(() => {
|
||||
if (!workspaceMemberIds) return [];
|
||||
return workspaceMemberIds.map((memberId) => getUserDetails(memberId)).filter(Boolean) as IUserLite[];
|
||||
}, [getUserDetails, workspaceMemberIds]);
|
||||
|
||||
const { leadFilterConfig, startDateFilterConfig, endDateFilterConfig, statesFilterConfig } =
|
||||
const { leadFilterConfig, startDateFilterConfig, endDateFilterConfig, statesFilterConfig, labelsFilterConfig } =
|
||||
useInitiativesFilterConfigs({
|
||||
workspaceMembers,
|
||||
operatorConfigs,
|
||||
labels: Array.from(labels?.values() || []),
|
||||
});
|
||||
|
||||
// Register all filter configs
|
||||
@@ -74,6 +78,7 @@ const InitiativesFilterInstanceProvider = observer(
|
||||
startDateFilterConfig,
|
||||
endDateFilterConfig,
|
||||
statesFilterConfig,
|
||||
labelsFilterConfig,
|
||||
]);
|
||||
|
||||
const value = useMemo(
|
||||
|
||||
+21
-2
@@ -1,25 +1,28 @@
|
||||
import { useMemo } from "react";
|
||||
import { CalendarCheck2, CalendarClock, Users } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, Tags, Users } from "lucide-react";
|
||||
import { INITIATIVE_STATES } from "@plane/constants";
|
||||
import { InitiativeStateIcon } from "@plane/propel/icons";
|
||||
import type { IUserLite } from "@plane/types";
|
||||
import type { IUserLite, TInitiativeLabel } from "@plane/types";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import {
|
||||
getInitiativeLeadFilterConfig,
|
||||
getInitiativeStartDateFilterConfig,
|
||||
getInitiativeEndDateFilterConfig,
|
||||
getInitiativeStatesFilterConfig,
|
||||
getInitiativeLabelsFilterConfig,
|
||||
} from "@plane/utils";
|
||||
import type { TFiltersOperatorConfigs } from "@/ce/hooks/rich-filters/use-filters-operator-configs";
|
||||
|
||||
interface UseInitiativesFilterConfigsProps {
|
||||
workspaceMembers: IUserLite[];
|
||||
operatorConfigs: TFiltersOperatorConfigs;
|
||||
labels: TInitiativeLabel[];
|
||||
}
|
||||
|
||||
export const useInitiativesFilterConfigs = ({
|
||||
workspaceMembers,
|
||||
operatorConfigs,
|
||||
labels,
|
||||
}: UseInitiativesFilterConfigsProps) => {
|
||||
const leadFilterConfig = useMemo(
|
||||
() =>
|
||||
@@ -68,10 +71,26 @@ export const useInitiativesFilterConfigs = ({
|
||||
}),
|
||||
[operatorConfigs]
|
||||
);
|
||||
|
||||
const labelsFilterConfig = useMemo(
|
||||
() =>
|
||||
getInitiativeLabelsFilterConfig("label_id")({
|
||||
isEnabled: true,
|
||||
filterIcon: Tags,
|
||||
labels: labels,
|
||||
getOptionIcon: (color: string) => (
|
||||
<span className="flex flex-shrink-0 size-2.5 rounded-full" style={{ backgroundColor: color }} />
|
||||
),
|
||||
...operatorConfigs,
|
||||
}),
|
||||
[labels, operatorConfigs]
|
||||
);
|
||||
|
||||
return {
|
||||
leadFilterConfig,
|
||||
startDateFilterConfig,
|
||||
endDateFilterConfig,
|
||||
statesFilterConfig,
|
||||
labelsFilterConfig,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,6 +31,14 @@ export const getGroupList = (
|
||||
(a, b) =>
|
||||
INITIATIVE_STATES[a as TInitiativeStates].sortOrder - INITIATIVE_STATES[b as TInitiativeStates].sortOrder
|
||||
);
|
||||
case "label_ids":
|
||||
// Sort labels alphabetically, but "None" first
|
||||
sortedGroupIds = sortedGroupIds.sort((a, b) => {
|
||||
if (a === "None") return -1;
|
||||
if (b === "None") return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
sortedGroupIds = sortedGroupIds.sort((a) => {
|
||||
if (a === "none") return -1;
|
||||
@@ -80,6 +88,36 @@ export const getGroupList = (
|
||||
}
|
||||
}
|
||||
|
||||
if (groupBy === "label_ids") {
|
||||
const workspaceSlug = rootStore.router.workspaceSlug;
|
||||
const { getInitiativesLabels } = rootStore.initiativeStore;
|
||||
|
||||
if (!workspaceSlug) return groupList;
|
||||
|
||||
const labelsMap = getInitiativesLabels(workspaceSlug);
|
||||
|
||||
for (const groupId of sortedGroupIds) {
|
||||
if (groupId === "None") {
|
||||
groupList.push({
|
||||
id: groupId,
|
||||
name: "None",
|
||||
icon: <div className="h-3 w-3 rounded-full flex-shrink-0" style={{ backgroundColor: "#666" }} />,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = labelsMap?.get(groupId);
|
||||
|
||||
if (!label) continue;
|
||||
|
||||
groupList.push({
|
||||
id: groupId,
|
||||
name: label.name,
|
||||
icon: <div className="h-3 w-3 rounded-full flex-shrink-0" style={{ backgroundColor: label.color }} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return groupList;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const INITIATIVE_GROUP_BY_OPTIONS: {
|
||||
{ key: "lead", title: "Lead" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "state", title: "States" },
|
||||
{ key: "label_ids", title: "Labels" },
|
||||
{ key: undefined, title: "None" },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
|
||||
// services
|
||||
import type { TInitiativeLabel } from "@/plane-web/types";
|
||||
import type { TInitiativeLabel } from "@plane/types";
|
||||
// types
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
|
||||
@@ -4,7 +4,13 @@ import { computedFn } from "mobx-utils";
|
||||
|
||||
// plane imports
|
||||
import { E_FEATURE_FLAGS } from "@plane/constants";
|
||||
import type { TEpicStats, TLoader, TInitiativeGroupByOptions, TInitiativeOrderByOptions } from "@plane/types";
|
||||
import type {
|
||||
TEpicStats,
|
||||
TLoader,
|
||||
TInitiativeGroupByOptions,
|
||||
TInitiativeOrderByOptions,
|
||||
TInitiativeLabel,
|
||||
} from "@plane/types";
|
||||
import { convertToISODateString } from "@plane/utils";
|
||||
|
||||
// plane-web imports
|
||||
@@ -14,7 +20,6 @@ import type {
|
||||
TExternalInitiativeFilterExpression,
|
||||
TInitiative,
|
||||
TInitiativeAnalytics,
|
||||
TInitiativeLabel,
|
||||
TInitiativeReaction,
|
||||
TInitiativeStats,
|
||||
} from "@/plane-web/types/initiative";
|
||||
@@ -406,6 +411,10 @@ export class InitiativeStore implements IInitiativeStore {
|
||||
|
||||
if (!this.initiativesMap) this.initiativesMap = {};
|
||||
this.initiativesMap[response.id] = response;
|
||||
|
||||
if (this.filteredInitiativesMap) {
|
||||
this.filteredInitiativesMap[response.id] = response;
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchInitiativesStats(workspaceSlug);
|
||||
@@ -736,6 +745,26 @@ const getGroupedInitiativeIds = (initiatives: TInitiative[], key: TInitiativeGro
|
||||
|
||||
if (!key) return { [ALL_INITIATIVES]: initiatives.map((initiative) => initiative.id) };
|
||||
|
||||
if (key === "label_ids") {
|
||||
for (const initiative of initiatives) {
|
||||
const labelIds = initiative.label_ids;
|
||||
if (!labelIds || labelIds.length === 0) {
|
||||
if (!groupedInitiativeIds["None"]) {
|
||||
groupedInitiativeIds["None"] = [];
|
||||
}
|
||||
groupedInitiativeIds["None"].push(initiative.id);
|
||||
} else {
|
||||
labelIds.forEach((labelId) => {
|
||||
if (!groupedInitiativeIds[labelId]) {
|
||||
groupedInitiativeIds[labelId] = [];
|
||||
}
|
||||
groupedInitiativeIds[labelId].push(initiative.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
return groupedInitiativeIds;
|
||||
}
|
||||
|
||||
for (const initiative of initiatives) {
|
||||
const groupId = initiative[key] ?? "None";
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export type TInitiativeStoredFilters = {
|
||||
|
||||
// initiative rich filters
|
||||
|
||||
export type TInitiativeFilterKeys = "lead" | "start_date" | "end_date" | "state";
|
||||
export type TInitiativeFilterKeys = "lead" | "start_date" | "end_date" | "state" | "label_id";
|
||||
|
||||
export type TExternalInitiativeFilterOperator =
|
||||
| typeof CORE_OPERATORS.EXACT
|
||||
@@ -42,8 +42,8 @@ export type TExternalInitiativeFilterGroup =
|
||||
|
||||
export type TExternalInitiativeFilterExpressionData =
|
||||
| {
|
||||
[key in TInitiativeFilterKeys]?: TFilterValue;
|
||||
}
|
||||
[key in TInitiativeFilterKeys]?: TFilterValue;
|
||||
}
|
||||
| TExternalInitiativeFilterGroup;
|
||||
|
||||
export type TInternalInitiativeFilterExpression = TFilterExpression<TInitiativeFilterKeys>;
|
||||
|
||||
@@ -202,9 +202,3 @@ export type TInitiativeStats = {
|
||||
off_track_updates: number;
|
||||
};
|
||||
|
||||
export type TInitiativeLabel = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
sort_order: number;
|
||||
};
|
||||
|
||||
@@ -26,6 +26,13 @@ export type TInitiativeAttachment = {
|
||||
updated_by: string;
|
||||
};
|
||||
|
||||
export type TInitiativeLabel = {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
sort_order: number;
|
||||
};
|
||||
|
||||
export type TInitiativeAttachmentUploadResponse = TFileSignedURLResponse & {
|
||||
attachment: TInitiativeAttachment;
|
||||
};
|
||||
@@ -49,5 +56,5 @@ export type TInitiativeDisplayFilters = {
|
||||
order_by?: TInitiativeOrderByOptions;
|
||||
};
|
||||
|
||||
export type TInitiativeGroupByOptions = "lead" | "created_by" | "state" | undefined;
|
||||
export type TInitiativeGroupByOptions = "lead" | "created_by" | "state" | "label_ids" | undefined;
|
||||
export type TInitiativeOrderByOptions = "-updated_at" | "-created_at" | "sort_order";
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
// plane imports
|
||||
import { COLLECTION_OPERATOR, EQUALITY_OPERATOR, IUserLite, TInitiativeStates, TFilterProperty } from "@plane/types";
|
||||
import {
|
||||
COLLECTION_OPERATOR,
|
||||
EQUALITY_OPERATOR,
|
||||
IUserLite,
|
||||
TInitiativeStates,
|
||||
TFilterProperty,
|
||||
TInitiativeLabel,
|
||||
} from "@plane/types";
|
||||
// local imports
|
||||
import {
|
||||
createFilterConfig,
|
||||
@@ -24,6 +31,11 @@ export type TCreateInitiativeStatesFilterParams = TCreateFilterConfigParams &
|
||||
items: Array<{ key: TInitiativeStates; title: string; icon: React.FC<React.SVGAttributes<SVGElement>> }>;
|
||||
};
|
||||
|
||||
export type TCreateInitiativeLabelsFilterParams = TCreateFilterConfigParams &
|
||||
IFilterIconConfig<string> & {
|
||||
labels: TInitiativeLabel[];
|
||||
};
|
||||
|
||||
// ------------ Lead filter ------------
|
||||
|
||||
/**
|
||||
@@ -89,6 +101,44 @@ export const getInitiativeStatesFilterConfig =
|
||||
]),
|
||||
});
|
||||
|
||||
// ------------ Labels filter ------------
|
||||
|
||||
/**
|
||||
* Get the labels filter config for initiatives
|
||||
* @template K - The filter key
|
||||
* @param key - The filter key to use
|
||||
* @returns A function that takes parameters and returns the labels filter config
|
||||
*/
|
||||
export const getInitiativeLabelsFilterConfig =
|
||||
<P extends string>(key: P) =>
|
||||
(params: TCreateInitiativeLabelsFilterParams) =>
|
||||
createFilterConfig<P, string>({
|
||||
id: key,
|
||||
label: "Labels",
|
||||
icon: params.filterIcon,
|
||||
isEnabled: params.isEnabled,
|
||||
supportedOperatorConfigsMap: new Map([
|
||||
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
|
||||
getMultiSelectConfig<TInitiativeLabel, string, string>(
|
||||
{
|
||||
items: params.labels,
|
||||
getId: (label) => label.id,
|
||||
getLabel: (label) => label.name,
|
||||
getValue: (label) => label.id,
|
||||
getIconData: (label) => label.color,
|
||||
},
|
||||
{
|
||||
singleValueOperator: EQUALITY_OPERATOR.EXACT,
|
||||
...updatedParams,
|
||||
},
|
||||
{
|
||||
getOptionIcon: params.getOptionIcon,
|
||||
}
|
||||
)
|
||||
),
|
||||
]),
|
||||
});
|
||||
|
||||
// ------------ Date filters ------------
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user