fix: issue property dropdown data flow (#3425)

* dev: workspace states and estimates

* refactor issue dropdown logic to help work properly with issues on global level

* fix: project labels response change

* fix label type

* change store computed actions to computed functions from mobx-utils

* fix: state response change

* chore: project and workspace state change

* fix state and label types

* chore: state and label serializer change

* modify state and label types

* fix dropdown reset on project id change

* fix label sort order

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: Rahul R <rahulr@Rahuls-MacBook-Pro.local>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: Rahul R <rahul.ramesha@plane.so>
This commit is contained in:
rahulramesha 2024-01-22 17:07:32 +05:30 committed by GitHub
parent be62662bb1
commit b3ac9def8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 494 additions and 463 deletions

View File

@ -104,6 +104,7 @@ from .estimate import (
EstimateSerializer, EstimateSerializer,
EstimatePointSerializer, EstimatePointSerializer,
EstimateReadSerializer, EstimateReadSerializer,
WorkspaceEstimateSerializer,
) )
from .inbox import ( from .inbox import (

View File

@ -61,3 +61,18 @@ class EstimateReadSerializer(BaseSerializer):
"name", "name",
"description", "description",
] ]
class WorkspaceEstimateSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]

View File

@ -259,14 +259,17 @@ class IssuePropertySerializer(BaseSerializer):
class LabelSerializer(BaseSerializer): class LabelSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta: class Meta:
model = Label model = Label
fields = "__all__" fields = [
"parent",
"name",
"color",
"id",
"project_id",
"workspace_id",
"sort_order",
]
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
"project", "project",
@ -295,8 +298,12 @@ class IssueLabelSerializer(BaseSerializer):
class IssueRelationSerializer(BaseSerializer): class IssueRelationSerializer(BaseSerializer):
id = serializers.UUIDField(source="related_issue.id", read_only=True) id = serializers.UUIDField(source="related_issue.id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(source="related_issue.project_id", read_only=True) project_id = serializers.PrimaryKeyRelatedField(
sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True) source="related_issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(
source="related_issue.sequence_id", read_only=True
)
relation_type = serializers.CharField(read_only=True) relation_type = serializers.CharField(read_only=True)
class Meta: class Meta:
@ -315,8 +322,12 @@ class IssueRelationSerializer(BaseSerializer):
class RelatedIssueSerializer(BaseSerializer): class RelatedIssueSerializer(BaseSerializer):
id = serializers.UUIDField(source="issue.id", read_only=True) id = serializers.UUIDField(source="issue.id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True) project_id = serializers.PrimaryKeyRelatedField(
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True) source="issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(
source="issue.sequence_id", read_only=True
)
relation_type = serializers.CharField(read_only=True) relation_type = serializers.CharField(read_only=True)
class Meta: class Meta:

View File

@ -8,7 +8,17 @@ from plane.db.models import State
class StateSerializer(BaseSerializer): class StateSerializer(BaseSerializer):
class Meta: class Meta:
model = State model = State
fields = "__all__" fields = [
"id",
"project_id",
"workspace_id",
"name",
"color",
"group",
"default",
"description",
"sequence",
]
read_only_fields = [ read_only_fields = [
"workspace", "workspace",
"project", "project",

View File

@ -20,6 +20,8 @@ from plane.app.views import (
WorkspaceLabelsEndpoint, WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint, WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint, WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
) )
@ -207,4 +209,14 @@ urlpatterns = [
WorkspaceUserPropertiesEndpoint.as_view(), WorkspaceUserPropertiesEndpoint.as_view(),
name="workspace-user-filters", name="workspace-user-filters",
), ),
path(
"workspaces/<str:slug>/states/",
WorkspaceStatesEndpoint.as_view(),
name="workspace-state",
),
path(
"workspaces/<str:slug>/estimates/",
WorkspaceEstimatesEndpoint.as_view(),
name="workspace-estimate",
),
] ]

View File

@ -47,6 +47,8 @@ from .workspace import (
WorkspaceLabelsEndpoint, WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint, WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint, WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
) )
from .state import StateViewSet from .state import StateViewSet
from .view import ( from .view import (

View File

@ -83,6 +83,7 @@ from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from collections import defaultdict from collections import defaultdict
class IssueViewSet(WebhookMixin, BaseViewSet): class IssueViewSet(WebhookMixin, BaseViewSet):
def get_serializer_class(self): def get_serializer_class(self):
return ( return (
@ -821,7 +822,9 @@ class SubIssuesEndpoint(BaseAPIView):
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group")) updated_sub_issues = Issue.issue_objects.filter(
id__in=sub_issue_ids
).annotate(state_group=F("state__group"))
# Track the issue # Track the issue
_ = [ _ = [
@ -855,7 +858,6 @@ class SubIssuesEndpoint(BaseAPIView):
) )
class IssueLinkViewSet(BaseViewSet): class IssueLinkViewSet(BaseViewSet):
permission_classes = [ permission_classes = [
ProjectEntityPermission, ProjectEntityPermission,

View File

@ -9,9 +9,12 @@ from rest_framework.response import Response
from rest_framework import status from rest_framework import status
# Module imports # Module imports
from . import BaseViewSet from . import BaseViewSet, BaseAPIView
from plane.app.serializers import StateSerializer from plane.app.serializers import StateSerializer
from plane.app.permissions import ProjectEntityPermission from plane.app.permissions import (
ProjectEntityPermission,
WorkspaceEntityPermission,
)
from plane.db.models import State, Issue from plane.db.models import State, Issue
@ -22,9 +25,6 @@ class StateViewSet(BaseViewSet):
ProjectEntityPermission, ProjectEntityPermission,
] ]
def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
super() super()

View File

@ -46,10 +46,14 @@ from plane.app.serializers import (
WorkspaceMemberMeSerializer, WorkspaceMemberMeSerializer,
ProjectMemberRoleSerializer, ProjectMemberRoleSerializer,
WorkspaceUserPropertiesSerializer, WorkspaceUserPropertiesSerializer,
WorkspaceEstimateSerializer,
StateSerializer,
LabelSerializer,
) )
from plane.app.views.base import BaseAPIView from plane.app.views.base import BaseAPIView
from . import BaseViewSet from . import BaseViewSet
from plane.db.models import ( from plane.db.models import (
State,
User, User,
Workspace, Workspace,
WorkspaceMemberInvite, WorkspaceMemberInvite,
@ -67,6 +71,8 @@ from plane.db.models import (
CycleIssue, CycleIssue,
IssueReaction, IssueReaction,
WorkspaceUserProperties, WorkspaceUserProperties,
Estimate,
EstimatePoint,
) )
from plane.app.permissions import ( from plane.app.permissions import (
WorkSpaceBasePermission, WorkSpaceBasePermission,
@ -1358,7 +1364,9 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
.values("count") .values("count")
) )
.annotate( .annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@ -1441,10 +1449,46 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
labels = Label.objects.filter( labels = Label.objects.filter(
workspace__slug=slug, workspace__slug=slug,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
).values(
"parent", "name", "color", "id", "project_id", "workspace__slug"
) )
return Response(labels, status=status.HTTP_200_OK) serializer = LabelSerializer(labels, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
class WorkspaceStatesEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
states = State.objects.filter(
workspace__slug=slug,
project__project_projectmember__member=request.user,
)
serializer = StateSerializer(states, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
class WorkspaceEstimatesEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def get(self, request, slug):
estimate_ids = Project.objects.filter(
workspace__slug=slug, estimate__isnull=False
).values_list("estimate_id", flat=True)
estimates = Estimate.objects.filter(
pk__in=estimate_ids
).prefetch_related(
Prefetch(
"points",
queryset=EstimatePoint.objects.select_related(
"estimate", "workspace", "project"
),
)
)
serializer = WorkspaceEstimateSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class WorkspaceUserPropertiesEndpoint(BaseAPIView): class WorkspaceUserPropertiesEndpoint(BaseAPIView):

View File

@ -109,17 +109,10 @@ export type IssuePriorities = {
export interface IIssueLabel { export interface IIssueLabel {
id: string; id: string;
created_at: Date;
updated_at: Date;
name: string; name: string;
description: string;
color: string; color: string;
created_by: string; project_id: string;
updated_by: string; workspace_id: string;
project: string;
project_detail: IProjectLite;
workspace: string;
workspace_detail: IWorkspaceLite;
parent: string | null; parent: string | null;
sort_order: number; sort_order: number;
} }

View File

@ -5,20 +5,13 @@ export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "
export interface IState { export interface IState {
readonly id: string; readonly id: string;
color: string; color: string;
readonly created_at: Date;
readonly created_by: string;
default: boolean; default: boolean;
description: string; description: string;
group: TStateGroups; group: TStateGroups;
name: string; name: string;
project: string; project_id: string;
readonly project_detail: IProjectLite;
sequence: number; sequence: number;
readonly slug: string; workspace_id: string;
readonly updated_at: Date;
readonly updated_by: string;
workspace: string;
workspace_detail: IWorkspaceLite;
} }
export interface IStateLite { export interface IStateLite {

View File

@ -72,9 +72,7 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => { const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => {
// store hooks // store hooks
const { const { workspaceLabels, fetchWorkspaceLabels } = useLabel();
workspace: { workspaceLabels, fetchWorkspaceLabels },
} = useLabel();
useEffect(() => { useEffect(() => {
if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug); if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug);

View File

@ -158,17 +158,12 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const filteredOptions = const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch cycles of the project if not already present in the store const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
useEffect(() => {
if (!workspaceSlug) return;
if (!activeEstimate) fetchProjectEstimates(workspaceSlug, projectId);
}, [activeEstimate, fetchProjectEstimates, projectId, workspaceSlug]);
const selectedEstimate = value !== null ? getEstimatePointValue(value) : null;
const openDropdown = () => { const openDropdown = () => {
setIsOpen(true); setIsOpen(true);
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false); const closeDropdown = () => setIsOpen(false);

View File

@ -93,14 +93,10 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
}; };
if (multiple) comboboxProps.multiple = true; if (multiple) comboboxProps.multiple = true;
useEffect(() => {
if (!workspaceSlug) return;
if (!projectMemberIds) fetchProjectMembers(workspaceSlug, projectId);
}, [fetchProjectMembers, projectId, projectMemberIds, workspaceSlug]);
const openDropdown = () => { const openDropdown = () => {
setIsOpen(true); setIsOpen(true);
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false); const closeDropdown = () => setIsOpen(false);

View File

@ -142,17 +142,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
const filteredOptions = const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch states of the project if not already present in the store
useEffect(() => {
if (!workspaceSlug) return;
if (!statesList) fetchProjectStates(workspaceSlug, projectId);
}, [fetchProjectStates, projectId, statesList, workspaceSlug]);
const selectedState = getStateById(value); const selectedState = getStateById(value);
const openDropdown = () => { const openDropdown = () => {
setIsOpen(true); setIsOpen(true);
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsOpen(false); const closeDropdown = () => setIsOpen(false);

View File

@ -75,9 +75,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
} = useUser(); } = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { const {
project: { projectMemberIds }, project: { projectMemberIds },
} = useMember(); } = useMember();

View File

@ -45,9 +45,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },
} = useUser(); } = useUser();
const { const { workspaceLabels } = useLabel();
workspace: { workspaceLabels },
} = useLabel();
const { const {
workspace: { workspaceMemberIds }, workspace: { workspaceMemberIds },
} = useMember(); } = useMember();

View File

@ -77,9 +77,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
membership: { currentProjectRole }, membership: { currentProjectRole },
} = useUser(); } = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { const {
project: { projectMemberIds }, project: { projectMemberIds },

View File

@ -25,9 +25,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
} = useIssues(EIssuesStoreType.ARCHIVED); } = useIssues(EIssuesStoreType.ARCHIVED);
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { const {
project: { projectMemberIds }, project: { projectMemberIds },
} = useMember(); } = useMember();

View File

@ -22,9 +22,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
} = useIssues(EIssuesStoreType.DRAFT); } = useIssues(EIssuesStoreType.DRAFT);
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { const {
project: { projectMemberIds }, project: { projectMemberIds },
} = useMember(); } = useMember();

View File

@ -42,9 +42,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
} = useUser(); } = useUser();
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const activeLayout = issueFilters?.displayFilters?.layout; const activeLayout = issueFilters?.displayFilters?.layout;

View File

@ -49,9 +49,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const { currentProjectDetails } = useProject(); const { currentProjectDetails } = useProject();
const { projectViewIds, getViewById } = useProjectView(); const { projectViewIds, getViewById } = useProjectView();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { const {
project: { projectMemberIds }, project: { projectMemberIds },
} = useMember(); } = useMember();

View File

@ -24,9 +24,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props; const { workspaceSlug, projectId, issueId, disabled = false } = props;
// hooks // hooks
const { updateIssue } = useIssueDetail(); const { updateIssue } = useIssueDetail();
const { const { createLabel } = useLabel();
project: { createLabel },
} = useLabel();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const labelOperations: TLabelOperations = useMemo( const labelOperations: TLabelOperations = useMemo(

View File

@ -20,9 +20,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
const { const {
issue: { getIssueById }, issue: { getIssueById },
} = useIssueDetail(); } = useIssueDetail();
const { const { fetchProjectLabels, getProjectLabels } = useLabel();
project: { fetchProjectLabels, projectLabels },
} = useLabel();
// states // states
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null); const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
@ -30,10 +28,12 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
const projectLabels = getProjectLabels(projectId);
const fetchLabels = () => { const fetchLabels = () => {
setIsLoading(true); setIsLoading(true);
if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false)); if (!projectLabels && workspaceSlug && projectId)
fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
}; };
const options = (projectLabels ?? []).map((label) => ({ const options = (projectLabels ?? []).map((label) => ({

View File

@ -17,9 +17,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.ARCHIVED); } = useIssues(EIssuesStoreType.ARCHIVED);
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
// derived values // derived values
const userFilters = issueFilters?.filters; const userFilters = issueFilters?.filters;

View File

@ -21,9 +21,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE); } = useIssues(EIssuesStoreType.CYCLE);
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
// derived values // derived values
const userFilters = issueFilters?.filters; const userFilters = issueFilters?.filters;

View File

@ -16,9 +16,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.DRAFT); } = useIssues(EIssuesStoreType.DRAFT);
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
// derived values // derived values
const userFilters = issueFilters?.filters; const userFilters = issueFilters?.filters;

View File

@ -25,9 +25,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
const { const {
issuesFilter: { filters, updateFilters }, issuesFilter: { filters, updateFilters },
} = useIssues(EIssuesStoreType.GLOBAL); } = useIssues(EIssuesStoreType.GLOBAL);
const { const { workspaceLabels } = useLabel();
workspace: { workspaceLabels },
} = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView(); const { globalViewMap, updateGlobalView } = useGlobalView();
const { const {
membership: { currentWorkspaceRole }, membership: { currentWorkspaceRole },

View File

@ -20,9 +20,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.MODULE); } = useIssues(EIssuesStoreType.MODULE);
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
// derived values // derived values
const userFilters = issueFilters?.filters; const userFilters = issueFilters?.filters;

View File

@ -7,19 +7,20 @@ import { AppliedFiltersList } from "components/issues";
// types // types
import { IIssueFilterOptions } from "@plane/types"; import { IIssueFilterOptions } from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue"; import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties";
export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => { export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, userId } = router.query; const { workspaceSlug, userId } = router.query;
//swr hook for fetching issue properties
useWorskspaceIssueProperties(workspaceSlug);
// store hooks // store hooks
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROFILE); } = useIssues(EIssuesStoreType.PROFILE);
const { const { workspaceLabels } = useLabel();
workspace: { workspaceLabels },
} = useLabel();
// derived values // derived values
const userFilters = issueFilters?.filters; const userFilters = issueFilters?.filters;

View File

@ -19,9 +19,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
projectId: string; projectId: string;
}; };
// store hooks // store hooks
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT); } = useIssues(EIssuesStoreType.PROJECT);

View File

@ -23,9 +23,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
const { const {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT_VIEW); } = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { viewMap, updateView } = useProjectView(); const { viewMap, updateView } = useProjectView();
// derived values // derived values

View File

@ -71,10 +71,10 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const member = useMember(); const member = useMember();
const project = useProject(); const project = useProject();
const projectLabel = useLabel(); const label = useLabel();
const projectState = useProjectState(); const projectState = useProjectState();
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member); const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
if (!list) return null; if (!list) return null;

View File

@ -208,17 +208,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
const member = useMember(); const member = useMember();
const project = useProject(); const project = useProject();
const projectLabel = useLabel(); const label = useLabel();
const projectState = useProjectState(); const projectState = useProjectState();
const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member); const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
const subGroupByList = getGroupByColumns( const subGroupByList = getGroupByColumns(sub_group_by as GroupByColumnTypes, project, label, projectState, member);
sub_group_by as GroupByColumnTypes,
project,
projectLabel,
projectState,
member
);
if (!groupByList || !subGroupByList) return null; if (!groupByList || !subGroupByList) return null;

View File

@ -59,10 +59,10 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
// store hooks // store hooks
const member = useMember(); const member = useMember();
const project = useProject(); const project = useProject();
const projectLabel = useLabel(); const label = useLabel();
const projectState = useProjectState(); const projectState = useProjectState();
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member, true); const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true);
if (!list) return null; if (!list) return null;

View File

@ -53,13 +53,15 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
const { const {
router: { workspaceSlug }, router: { workspaceSlug },
} = useApplication(); } = useApplication();
const { const { fetchProjectLabels, getProjectLabels } = useLabel();
project: { fetchProjectLabels, projectLabels: storeLabels },
} = useLabel();
const fetchLabels = () => { const storeLabels = getProjectLabels(projectId);
const openDropDown = () => {
if (!storeLabels && workspaceSlug && projectId) {
setIsLoading(true); setIsLoading(true);
if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false)); fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
}
}; };
const { styles, attributes } = usePopper(referenceElement, popperElement, { const { styles, attributes } = usePopper(referenceElement, popperElement, {
@ -182,7 +184,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
? "cursor-pointer" ? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80" : "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`} } ${buttonClassName}`}
onClick={() => !storeLabels && fetchLabels()} onClick={openDropDown}
> >
{label} {label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />} {!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}

View File

@ -4,6 +4,7 @@ import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
import { useGlobalView, useIssues, useUser } from "hooks/store"; import { useGlobalView, useIssues, useUser } from "hooks/store";
import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties";
// components // components
import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues"; import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues";
import { SpreadsheetView } from "components/issues/issue-layouts"; import { SpreadsheetView } from "components/issues/issue-layouts";
@ -20,7 +21,8 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug, globalViewId } = router.query; const { workspaceSlug, globalViewId } = router.query;
//swr hook for fetching issue properties
useWorskspaceIssueProperties(workspaceSlug);
// store // store
const { const {
issuesFilter: { filters, fetchFilters, updateFilters }, issuesFilter: { filters, fetchFilters, updateFilters },

View File

@ -1,17 +1,17 @@
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui"; import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { ISSUE_PRIORITIES } from "constants/issue"; import { ISSUE_PRIORITIES } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper"; import { renderEmoji } from "helpers/emoji.helper";
import { ILabelRootStore } from "store/label";
import { IMemberRootStore } from "store/member"; import { IMemberRootStore } from "store/member";
import { IProjectStore } from "store/project/project.store"; import { IProjectStore } from "store/project/project.store";
import { IStateStore } from "store/state.store"; import { IStateStore } from "store/state.store";
import { GroupByColumnTypes, IGroupByColumn } from "@plane/types"; import { GroupByColumnTypes, IGroupByColumn } from "@plane/types";
import { STATE_GROUPS } from "constants/state"; import { STATE_GROUPS } from "constants/state";
import { ILabelStore } from "store/label.store";
export const getGroupByColumns = ( export const getGroupByColumns = (
groupBy: GroupByColumnTypes | null, groupBy: GroupByColumnTypes | null,
project: IProjectStore, project: IProjectStore,
projectLabel: ILabelRootStore, label: ILabelStore,
projectState: IStateStore, projectState: IStateStore,
member: IMemberRootStore, member: IMemberRootStore,
includeNone?: boolean includeNone?: boolean
@ -26,7 +26,7 @@ export const getGroupByColumns = (
case "priority": case "priority":
return getPriorityColumns(); return getPriorityColumns();
case "labels": case "labels":
return getLabelsColumns(projectLabel) as any; return getLabelsColumns(label) as any;
case "assignees": case "assignees":
return getAssigneeColumns(member) as any; return getAssigneeColumns(member) as any;
case "created_by": case "created_by":
@ -97,10 +97,8 @@ const getPriorityColumns = () => {
})); }));
}; };
const getLabelsColumns = (projectLabel: ILabelRootStore) => { const getLabelsColumns = (label: ILabelStore) => {
const { const { projectLabels } = label;
project: { projectLabels },
} = projectLabel;
if (!projectLabels) return; if (!projectLabels) return;

View File

@ -64,7 +64,15 @@ const aiService = new AIService();
const fileService = new FileService(); const fileService = new FileService();
export const IssueFormRoot: FC<IssueFormProps> = observer((props) => { export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const { data, onChange, onClose, onSubmit, projectId, isCreateMoreToggleEnabled, onCreateMoreToggleChange } = props; const {
data,
onChange,
onClose,
onSubmit,
projectId: defaultProjectId,
isCreateMoreToggleEnabled,
onCreateMoreToggleChange,
} = props;
// states // states
const [labelModal, setLabelModal] = useState(false); const [labelModal, setLabelModal] = useState(false);
const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false);
@ -99,10 +107,30 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
getValues, getValues,
setValue, setValue,
} = useForm<TIssue>({ } = useForm<TIssue>({
defaultValues: { ...defaultValues, project_id: projectId, ...data }, defaultValues: { ...defaultValues, project_id: defaultProjectId, ...data },
reValidateMode: "onChange", reValidateMode: "onChange",
}); });
const projectId = watch("project_id");
//reset few fields on projectId change
useEffect(() => {
if (isDirty) {
const formData = getValues();
reset({
...defaultValues,
project_id: projectId,
name: formData.name,
description_html: formData.description_html,
priority: formData.priority,
start_date: formData.start_date,
target_date: formData.target_date,
parent_id: formData.parent_id,
});
}
}, [projectId]);
const issueName = watch("name"); const issueName = watch("name");
const handleFormSubmit = async (formData: Partial<TIssue>) => { const handleFormSubmit = async (formData: Partial<TIssue>) => {
@ -130,7 +158,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
setIAmFeelingLucky(true); setIAmFeelingLucky(true);
aiService aiService
.createGptTask(workspaceSlug.toString(), projectId.toString(), { .createGptTask(workspaceSlug.toString(), projectId, {
prompt: issueName, prompt: issueName,
task: "Generate a proper description for this issue.", task: "Generate a proper description for this issue.",
}) })

View File

@ -23,14 +23,7 @@ export interface IssuesModalProps {
} }
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => { export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
const { const { data, isOpen, onClose, onSubmit, withDraftIssueWrapper = true, storeType = EIssuesStoreType.PROJECT } = props;
data,
isOpen,
onClose,
onSubmit,
withDraftIssueWrapper = true,
storeType = EIssuesStoreType.PROJECT,
} = props;
// states // states
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null); const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
const [createMore, setCreateMore] = useState(false); const [createMore, setCreateMore] = useState(false);

View File

@ -29,9 +29,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store hooks // store hooks
const { const { getProjectLabels, fetchProjectLabels } = useLabel();
project: { getProjectLabels, fetchProjectLabels },
} = useLabel();
// states // states
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null); const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
@ -50,13 +48,9 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const filteredOptions = const filteredOptions =
query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())); query === "" ? projectLabels : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase()));
useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUE_LABELS_${projectId.toUpperCase()}` : null,
workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId) : null
);
const openDropdown = () => { const openDropdown = () => {
setIsDropdownOpen(true); setIsDropdownOpen(true);
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
if (referenceElement) referenceElement.focus(); if (referenceElement) referenceElement.focus();
}; };
const closeDropdown = () => setIsDropdownOpen(false); const closeDropdown = () => setIsDropdownOpen(false);

View File

@ -21,7 +21,7 @@ export const ViewEstimateSelect: React.FC<Props> = observer((props) => {
const { issue, onChange, tooltipPosition = "top", customButton = false, disabled } = props; const { issue, onChange, tooltipPosition = "top", customButton = false, disabled } = props;
const { areEstimatesEnabledForCurrentProject, activeEstimateDetails, getEstimatePointValue } = useEstimate(); const { areEstimatesEnabledForCurrentProject, activeEstimateDetails, getEstimatePointValue } = useEstimate();
const estimateValue = getEstimatePointValue(issue.estimate_point); const estimateValue = getEstimatePointValue(issue.estimate_point, issue.project_id);
const estimateLabels = ( const estimateLabels = (
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}> <Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>

View File

@ -34,9 +34,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
// store hooks // store hooks
const { const { createLabel } = useLabel();
project: { createLabel },
} = useLabel();
// form info // form info
const { const {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },

View File

@ -34,9 +34,7 @@ export const CreateUpdateLabelInline = observer(
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { const { createLabel, updateLabel } = useLabel();
project: { createLabel, updateLabel },
} = useLabel();
// toast alert // toast alert
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
// form info // form info

View File

@ -25,9 +25,7 @@ export const DeleteLabelModal: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { const { deleteLabel } = useLabel();
project: { deleteLabel },
} = useLabel();
// states // states
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// hooks // hooks

View File

@ -25,9 +25,7 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { const { projectLabels, fetchProjectLabels, updateLabel } = useLabel();
project: { projectLabels, fetchProjectLabels, updateLabel },
} = useLabel();
// api call to fetch project details // api call to fetch project details
useSWR( useSWR(

View File

@ -28,9 +28,7 @@ export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { const { updateLabel } = useLabel();
project: { updateLabel },
} = useLabel();
const removeFromGroup = (label: IIssueLabel) => { const removeFromGroup = (label: IIssueLabel) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;

View File

@ -41,9 +41,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
// store hooks // store hooks
const { const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
project: { projectLabels, updateLabelPosition, projectLabelsTree },
} = useLabel();
// portal // portal
const renderDraggable = useDraggableInPortal(); const renderDraggable = useDraggableInPortal();

View File

@ -18,9 +18,7 @@ export const ProfileIssuesFilter = observer(() => {
issuesFilter: { issueFilters, updateFilters }, issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROFILE); } = useIssues(EIssuesStoreType.PROFILE);
const { const { workspaceLabels } = useLabel();
workspace: { workspaceLabels },
} = useLabel();
// derived values // derived values
const states = undefined; const states = undefined;
const members = undefined; const members = undefined;

View File

@ -42,7 +42,7 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
setIsDeleteLoading(true); setIsDeleteLoading(true);
await deleteState(workspaceSlug.toString(), data.project, data.id) await deleteState(workspaceSlug.toString(), data.project_id, data.id)
.then(() => { .then(() => {
postHogEventTracker("STATE_DELETE", { postHogEventTracker("STATE_DELETE", {
state: "SUCCESS", state: "SUCCESS",

View File

@ -28,9 +28,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
const { handleFormSubmit, handleClose, data, preLoadedData } = props; const { handleFormSubmit, handleClose, data, preLoadedData } = props;
// store hooks // store hooks
const { projectStates } = useProjectState(); const { projectStates } = useProjectState();
const { const { projectLabels } = useLabel();
project: { projectLabels },
} = useLabel();
const { const {
project: { projectMemberIds }, project: { projectMemberIds },
} = useMember(); } = useMember();

View File

@ -27,9 +27,7 @@ const defaultValues: Partial<IWorkspaceView> = {
export const WorkspaceViewForm: React.FC<Props> = observer((props) => { export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
const { handleFormSubmit, handleClose, data, preLoadedData } = props; const { handleFormSubmit, handleClose, data, preLoadedData } = props;
// store hooks // store hooks
const { const { workspaceLabels } = useLabel();
workspace: { workspaceLabels },
} = useLabel();
const { const {
workspace: { workspaceMemberIds }, workspace: { workspaceMemberIds },
} = useMember(); } = useMember();

View File

@ -2,10 +2,10 @@ import { useContext } from "react";
// mobx store // mobx store
import { StoreContext } from "contexts/store-context"; import { StoreContext } from "contexts/store-context";
// types // types
import { ILabelRootStore } from "store/label"; import { ILabelStore } from "store/label.store";
export const useLabel = (): ILabelRootStore => { export const useLabel = (): ILabelStore => {
const context = useContext(StoreContext); const context = useContext(StoreContext);
if (context === undefined) throw new Error("useLabel must be used within StoreProvider"); if (context === undefined) throw new Error("useLabel must be used within StoreProvider");
return context.labelRoot; return context.label;
}; };

View File

@ -0,0 +1,28 @@
import useSWR from "swr";
import { useEstimate, useLabel, useProjectState } from "./store";
export const useWorskspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => {
const { fetchWorkspaceLabels } = useLabel();
const { fetchWorkspaceStates } = useProjectState();
const { fetchWorskpaceEstimates } = useEstimate();
// fetch workspace labels
useSWR(
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null
);
// fetch workspace states
useSWR(
workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null
);
// fetch workspace estimates
useSWR(
workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorskpaceEstimates(workspaceSlug.toString()) : null
);
};

View File

@ -45,9 +45,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
project: { fetchProjectMembers }, project: { fetchProjectMembers },
} = useMember(); } = useMember();
const { fetchProjectStates } = useProjectState(); const { fetchProjectStates } = useProjectState();
const { const { fetchProjectLabels } = useLabel();
project: { fetchProjectLabels },
} = useLabel();
const { fetchProjectEstimates } = useEstimate(); const { fetchProjectEstimates } = useEstimate();
// router // router
const router = useRouter(); const router = useRouter();

View File

@ -20,9 +20,6 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
const { const {
workspace: { fetchWorkspaceMembers }, workspace: { fetchWorkspaceMembers },
} = useMember(); } = useMember();
const {
workspace: { fetchWorkspaceLabels },
} = useLabel();
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;
@ -41,11 +38,6 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null
); );
// fetch workspace labels
useSWR(
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null
);
// fetch workspace user projects role // fetch workspace user projects role
useSWR( useSWR(
workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null, workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,

View File

@ -40,6 +40,7 @@
"lucide-react": "^0.294.0", "lucide-react": "^0.294.0",
"mobx": "^6.10.0", "mobx": "^6.10.0",
"mobx-react": "^9.1.0", "mobx-react": "^9.1.0",
"mobx-utils": "^6.0.8",
"next": "^14.0.3", "next": "^14.0.3",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",

View File

@ -61,4 +61,12 @@ export class ProjectEstimateService extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async getWorkspaceEstimatesList(workspaceSlug: string): Promise<IEstimate[]> {
return this.get(`/api/workspaces/${workspaceSlug}/estimates/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
} }

View File

@ -65,4 +65,12 @@ export class ProjectStateService extends APIService {
throw error?.response; throw error?.response;
}); });
} }
async getWorkspaceStates(workspaceSlug: string): Promise<IState[]> {
return this.get(`/api/workspaces/${workspaceSlug}/states/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
} }

View File

@ -19,7 +19,7 @@ export class EventTrackerStore implements IEventTrackerStore {
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
trackElement: observable, trackElement: observable,
setTrackElement: action, setTrackElement: action.bound,
postHogEventTracker: action, postHogEventTracker: action,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;

View File

@ -29,7 +29,7 @@ export class RouterStore implements IRouterStore {
// observables // observables
query: observable, query: observable,
// actions // actions
setQuery: action, setQuery: action.bound,
//computed //computed
workspaceSlug: computed, workspaceSlug: computed,
projectId: computed, projectId: computed,

View File

@ -1,4 +1,5 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import { isFuture, isPast } from "date-fns"; import { isFuture, isPast } from "date-fns";
import set from "lodash/set"; import set from "lodash/set";
import sortBy from "lodash/sortBy"; import sortBy from "lodash/sortBy";
@ -74,10 +75,6 @@ export class CycleStore implements ICycleStore {
currentProjectIncompleteCycleIds: computed, currentProjectIncompleteCycleIds: computed,
currentProjectDraftCycleIds: computed, currentProjectDraftCycleIds: computed,
currentProjectActiveCycleId: computed, currentProjectActiveCycleId: computed,
// computed actions
getCycleById: action,
getActiveCycleById: action,
getProjectCycleIds: action,
// actions // actions
fetchAllCycles: action, fetchAllCycles: action,
fetchActiveCycle: action, fetchActiveCycle: action,
@ -184,28 +181,29 @@ export class CycleStore implements ICycleStore {
* @param cycleId * @param cycleId
* @returns * @returns
*/ */
getCycleById = (cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null; getCycleById = computedFn((cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null);
/** /**
* @description returns active cycle details by cycle id * @description returns active cycle details by cycle id
* @param cycleId * @param cycleId
* @returns * @returns
*/ */
getActiveCycleById = (cycleId: string): ICycle | null => getActiveCycleById = computedFn((cycleId: string): ICycle | null =>
this.activeCycleIdMap?.[cycleId] && this.cycleMap?.[cycleId] ? this.cycleMap?.[cycleId] : null; this.activeCycleIdMap?.[cycleId] && this.cycleMap?.[cycleId] ? this.cycleMap?.[cycleId] : null
);
/** /**
* @description returns list of cycle ids of the project id passed as argument * @description returns list of cycle ids of the project id passed as argument
* @param projectId * @param projectId
*/ */
getProjectCycleIds = (projectId: string): string[] | null => { getProjectCycleIds = computedFn((projectId: string): string[] | null => {
if (!this.fetchedMap[projectId]) return null; if (!this.fetchedMap[projectId]) return null;
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId); let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId);
cycles = sortBy(cycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]); cycles = sortBy(cycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
const cycleIds = cycles.map((c) => c.id); const cycleIds = cycles.map((c) => c.id);
return cycleIds || null; return cycleIds || null;
}; });
/** /**
* @description validates cycle dates * @description validates cycle dates

View File

@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set"; import set from "lodash/set";
// services // services
import { DashboardService } from "services/dashboard.service"; import { DashboardService } from "services/dashboard.service";
@ -74,8 +75,6 @@ export class DashboardStore implements IDashboardStore {
widgetStats: observable, widgetStats: observable,
// computed // computed
homeDashboardWidgets: computed, homeDashboardWidgets: computed,
// computed actions
getWidgetDetails: action,
// fetch actions // fetch actions
fetchHomeDashboardWidgets: action, fetchHomeDashboardWidgets: action,
fetchWidgetStats: action, fetchWidgetStats: action,
@ -109,11 +108,11 @@ export class DashboardStore implements IDashboardStore {
* @param widgetId * @param widgetId
* @returns widget details * @returns widget details
*/ */
getWidgetDetails = (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => { getWidgetDetails = computedFn((workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => {
const widgets = this.widgetDetails?.[workspaceSlug]?.[dashboardId]; const widgets = this.widgetDetails?.[workspaceSlug]?.[dashboardId];
if (!widgets) return undefined; if (!widgets) return undefined;
return widgets.find((widget) => widget.key === widgetKey); return widgets.find((widget) => widget.key === widgetKey);
}; });
/** /**
* @description fetch home dashboard details and widgets * @description fetch home dashboard details and widgets

View File

@ -5,21 +5,25 @@ import { ProjectEstimateService } from "services/project";
// types // types
import { RootStore } from "store/root.store"; import { RootStore } from "store/root.store";
import { IEstimate, IEstimateFormData } from "@plane/types"; import { IEstimate, IEstimateFormData } from "@plane/types";
import { computedFn } from "mobx-utils";
export interface IEstimateStore { export interface IEstimateStore {
//Loaders
fetchedMap: Record<string, boolean>;
// observables // observables
estimates: Record<string, IEstimate[] | null>; estimateMap: Record<string, IEstimate>;
// computed // computed
areEstimatesEnabledForCurrentProject: boolean; areEstimatesEnabledForCurrentProject: boolean;
projectEstimates: IEstimate[] | null; projectEstimates: IEstimate[] | null;
activeEstimateDetails: IEstimate | null; activeEstimateDetails: IEstimate | null;
// computed actions // computed actions
areEstimatesEnabledForProject: (projectId: string) => boolean; areEstimatesEnabledForProject: (projectId: string) => boolean;
getEstimatePointValue: (estimateKey: number | null) => string; getEstimatePointValue: (estimateKey: number | null, projectId?: string) => string;
getProjectEstimateById: (estimateId: string) => IEstimate | null; getProjectEstimateById: (estimateId: string) => IEstimate | null;
getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null; getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null;
// fetch actions // fetch actions
fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<IEstimate[]>; fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<IEstimate[]>;
fetchWorskpaceEstimates: (workspaceSlug: string) => Promise<IEstimate[]>;
// crud actions // crud actions
createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise<IEstimate>; createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise<IEstimate>;
updateEstimate: ( updateEstimate: (
@ -33,7 +37,9 @@ export interface IEstimateStore {
export class EstimateStore implements IEstimateStore { export class EstimateStore implements IEstimateStore {
// observables // observables
estimates: Record<string, IEstimate[] | null> = {}; estimateMap: Record<string, IEstimate> = {};
//loaders
fetchedMap: Record<string, boolean> = {};
// root store // root store
rootStore; rootStore;
// services // services
@ -42,18 +48,15 @@ export class EstimateStore implements IEstimateStore {
constructor(_rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
// observables // observables
estimates: observable, estimateMap: observable,
fetchedMap: observable,
// computed // computed
areEstimatesEnabledForCurrentProject: computed, areEstimatesEnabledForCurrentProject: computed,
projectEstimates: computed, projectEstimates: computed,
activeEstimateDetails: computed, activeEstimateDetails: computed,
// computed actions
areEstimatesEnabledForProject: action,
getProjectEstimateById: action,
getEstimatePointValue: action,
getProjectActiveEstimateDetails: action,
// actions // actions
fetchProjectEstimates: action, fetchProjectEstimates: action,
fetchWorskpaceEstimates: action,
createEstimate: action, createEstimate: action,
updateEstimate: action, updateEstimate: action,
deleteEstimate: action, deleteEstimate: action,
@ -79,8 +82,9 @@ export class EstimateStore implements IEstimateStore {
*/ */
get projectEstimates() { get projectEstimates() {
const projectId = this.rootStore.app.router.projectId; const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null; const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
return this.estimates?.[projectId] || null; if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return null;
return Object.values(this.estimateMap).filter((estimate) => estimate.project === projectId);
} }
/** /**
@ -89,47 +93,49 @@ export class EstimateStore implements IEstimateStore {
get activeEstimateDetails() { get activeEstimateDetails() {
const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails; const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails;
if (!currentProjectDetails || !currentProjectDetails?.estimate) return null; if (!currentProjectDetails || !currentProjectDetails?.estimate) return null;
return this.projectEstimates?.find((estimate) => estimate.id === currentProjectDetails?.estimate) || null; return this.estimateMap?.[currentProjectDetails?.estimate || ""] || null;
} }
/** /**
* @description returns true if estimates are enabled for a project using project id * @description returns true if estimates are enabled for a project using project id
* @param projectId * @param projectId
*/ */
areEstimatesEnabledForProject = (projectId: string) => { areEstimatesEnabledForProject = computedFn((projectId: string) => {
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId); const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId);
if (!projectDetails) return false; if (!projectDetails) return false;
return Boolean(projectDetails.estimate) ?? false; return Boolean(projectDetails.estimate) ?? false;
}; });
/** /**
* @description returns the point value for the given estimate key to display in the UI * @description returns the point value for the given estimate key to display in the UI
*/ */
getEstimatePointValue = (estimateKey: number | null) => { getEstimatePointValue = computedFn((estimateKey: number | null, projectId?: string) => {
if (estimateKey === null) return "None"; if (estimateKey === null) return "None";
const activeEstimate = this.activeEstimateDetails; const activeEstimate = projectId ? this.getProjectActiveEstimateDetails(projectId) : this.activeEstimateDetails;
return activeEstimate?.points?.find((point) => point.key === estimateKey)?.value || "None"; return activeEstimate?.points?.find((point) => point.key === estimateKey)?.value || "None";
}; });
/** /**
* @description returns the estimate details for the given estimate id * @description returns the estimate details for the given estimate id
* @param estimateId * @param estimateId
*/ */
getProjectEstimateById = (estimateId: string) => { getProjectEstimateById = computedFn((estimateId: string) => {
if (!this.projectEstimates) return null; if (!this.projectEstimates) return null;
const estimateInfo = this.projectEstimates?.find((estimate) => estimate.id === estimateId) || null; const estimateInfo = this.estimateMap?.[estimateId] || null;
return estimateInfo; return estimateInfo;
}; });
/** /**
* @description returns the estimate details for the given estimate id * @description returns the estimate details for the given estimate id
* @param projectId * @param projectId
*/ */
getProjectActiveEstimateDetails = (projectId: string) => { getProjectActiveEstimateDetails = computedFn((projectId: string) => {
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId); const projectDetails = this.rootStore.projectRoot.project?.getProjectById(projectId);
if (!projectDetails || !projectDetails?.estimate) return null; const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
return this.projectEstimates?.find((estimate) => estimate.id === projectDetails?.estimate) || null; if (!projectDetails || !projectDetails?.estimate || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug]))
}; return null;
return this.estimateMap?.[projectDetails?.estimate || ""] || null;
});
/** /**
* @description fetches the list of estimates for the given project * @description fetches the list of estimates for the given project
@ -139,7 +145,26 @@ export class EstimateStore implements IEstimateStore {
fetchProjectEstimates = async (workspaceSlug: string, projectId: string) => fetchProjectEstimates = async (workspaceSlug: string, projectId: string) =>
await this.estimateService.getEstimatesList(workspaceSlug, projectId).then((response) => { await this.estimateService.getEstimatesList(workspaceSlug, projectId).then((response) => {
runInAction(() => { runInAction(() => {
set(this.estimates, projectId, response); response.forEach((estimate) => {
set(this.estimateMap, estimate.id, estimate);
});
this.fetchedMap[projectId] = true;
});
return response;
});
/**
* @description fetches the list of estimates for the given project
* @param workspaceSlug
* @param projectId
*/
fetchWorskpaceEstimates = async (workspaceSlug: string) =>
await this.estimateService.getWorkspaceEstimatesList(workspaceSlug).then((response) => {
runInAction(() => {
response.forEach((estimate) => {
set(this.estimateMap, estimate.id, estimate);
});
this.fetchedMap[workspaceSlug] = true;
}); });
return response; return response;
}); });
@ -157,7 +182,7 @@ export class EstimateStore implements IEstimateStore {
points: response.estimate_points, points: response.estimate_points,
}; };
runInAction(() => { runInAction(() => {
set(this.estimates, projectId, [responseEstimate, ...(this.estimates?.[projectId] || [])]); set(this.estimateMap, [responseEstimate.id], responseEstimate);
}); });
return response.estimate; return response.estimate;
}); });
@ -171,11 +196,12 @@ export class EstimateStore implements IEstimateStore {
*/ */
updateEstimate = async (workspaceSlug: string, projectId: string, estimateId: string, data: IEstimateFormData) => updateEstimate = async (workspaceSlug: string, projectId: string, estimateId: string, data: IEstimateFormData) =>
await this.estimateService.patchEstimate(workspaceSlug, projectId, estimateId, data).then((response) => { await this.estimateService.patchEstimate(workspaceSlug, projectId, estimateId, data).then((response) => {
const updatedEstimates = (this.estimates?.[projectId] ?? []).map((estimate) =>
estimate.id === estimateId ? { ...estimate, ...data.estimate, points: [...data.estimate_points] } : estimate
);
runInAction(() => { runInAction(() => {
set(this.estimates, projectId, updatedEstimates); set(this.estimateMap, estimateId, {
...this.estimateMap[estimateId],
...data.estimate,
points: [...data.estimate_points],
});
}); });
return response; return response;
}); });
@ -188,9 +214,8 @@ export class EstimateStore implements IEstimateStore {
*/ */
deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) => deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) =>
await this.estimateService.deleteEstimate(workspaceSlug, projectId, estimateId).then(() => { await this.estimateService.deleteEstimate(workspaceSlug, projectId, estimateId).then(() => {
const updatedEstimates = (this.estimates?.[projectId] ?? []).filter((estimate) => estimate.id !== estimateId);
runInAction(() => { runInAction(() => {
set(this.estimates, projectId, updatedEstimates); delete this.estimateMap[estimateId];
}); });
}); });
} }

View File

@ -1,4 +1,5 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx"; import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { computedFn } from "mobx-utils";
import { set } from "lodash"; import { set } from "lodash";
// services // services
import { WorkspaceService } from "services/workspace.service"; import { WorkspaceService } from "services/workspace.service";
@ -37,9 +38,6 @@ export class GlobalViewStore implements IGlobalViewStore {
globalViewMap: observable, globalViewMap: observable,
// computed // computed
currentWorkspaceViews: computed, currentWorkspaceViews: computed,
// computed actions
getSearchedViews: action,
getViewDetailsById: action,
// actions // actions
fetchAllGlobalViews: action, fetchAllGlobalViews: action,
fetchGlobalViewDetails: action, fetchGlobalViewDetails: action,
@ -73,7 +71,7 @@ export class GlobalViewStore implements IGlobalViewStore {
* @param searchQuery * @param searchQuery
* @returns * @returns
*/ */
getSearchedViews = (searchQuery: string) => { getSearchedViews = computedFn((searchQuery: string) => {
const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace; const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
if (!currentWorkspaceDetails) return null; if (!currentWorkspaceDetails) return null;
@ -84,13 +82,13 @@ export class GlobalViewStore implements IGlobalViewStore {
this.globalViewMap[viewId]?.name?.toLowerCase().includes(searchQuery.toLowerCase()) this.globalViewMap[viewId]?.name?.toLowerCase().includes(searchQuery.toLowerCase())
) ?? null ) ?? null
); );
}; });
/** /**
* @description returns view details for given viewId * @description returns view details for given viewId
* @param viewId * @param viewId
*/ */
getViewDetailsById = (viewId: string): IWorkspaceView | null => this.globalViewMap[viewId] ?? null; getViewDetailsById = computedFn((viewId: string): IWorkspaceView | null => this.globalViewMap[viewId] ?? null);
/** /**
* @description fetch all global views for given workspace * @description fetch all global views for given workspace

View File

@ -1,4 +1,5 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx"; import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { computedFn } from "mobx-utils";
import { set } from "lodash"; import { set } from "lodash";
// services // services
import { InboxService } from "services/inbox.service"; import { InboxService } from "services/inbox.service";
@ -43,8 +44,6 @@ export class InboxStore implements IInboxStore {
inboxDetails: observable, inboxDetails: observable,
// computed // computed
isInboxEnabled: computed, isInboxEnabled: computed,
// computed actions
getInboxId: action,
// actions // actions
fetchInboxesList: action, fetchInboxesList: action,
}); });
@ -69,11 +68,11 @@ export class InboxStore implements IInboxStore {
/** /**
* Returns the inbox Id belongs to a specific project * Returns the inbox Id belongs to a specific project
*/ */
getInboxId = (projectId: string) => { getInboxId = computedFn((projectId: string) => {
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId); const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId);
if (!projectDetails || !projectDetails.inbox_view) return null; if (!projectDetails || !projectDetails.inbox_view) return null;
return this.inboxesList[projectId]?.[0]?.id ?? null; return this.inboxesList[projectId]?.[0]?.id ?? null;
}; });
/** /**
* Fetches the inboxes list belongs to a specific project * Fetches the inboxes list belongs to a specific project

View File

@ -1,4 +1,5 @@
import { observable, action, makeObservable, runInAction, autorun, computed } from "mobx"; import { observable, action, makeObservable, runInAction, autorun, computed } from "mobx";
import { computedFn } from "mobx-utils";
import { set } from "lodash"; import { set } from "lodash";
// services // services
import { InboxService } from "services/inbox.service"; import { InboxService } from "services/inbox.service";
@ -61,8 +62,6 @@ export class InboxIssuesStore implements IInboxIssuesStore {
issueMap: observable, issueMap: observable,
// computed // computed
currentInboxIssueIds: computed, currentInboxIssueIds: computed,
// computed actions
getIssueById: action,
// fetch actions // fetch actions
fetchIssues: action, fetchIssues: action,
fetchIssueDetails: action, fetchIssueDetails: action,
@ -99,7 +98,9 @@ export class InboxIssuesStore implements IInboxIssuesStore {
/** /**
* Returns the issue details belongs to a specific inbox issue * Returns the issue details belongs to a specific inbox issue
*/ */
getIssueById = (inboxId: string, issueId: string): IInboxIssue | null => this.issueMap?.[inboxId]?.[issueId] ?? null; getIssueById = computedFn(
(inboxId: string, issueId: string): IInboxIssue | null => this.issueMap?.[inboxId]?.[issueId] ?? null
);
/** /**
* Fetches issues of a specific inbox and adds it to the store * Fetches issues of a specific inbox and adds it to the store

View File

@ -2,6 +2,7 @@ import set from "lodash/set";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
// store // store
import { action, makeObservable, observable, runInAction } from "mobx"; import { action, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
// types // types
import { TIssue } from "@plane/types"; import { TIssue } from "@plane/types";
@ -80,17 +81,17 @@ export class IssueStore implements IIssueStore {
* @param {string} issueId * @param {string} issueId
* @returns {TIssue | undefined} * @returns {TIssue | undefined}
*/ */
getIssueById = (issueId: string) => { getIssueById = computedFn((issueId: string) => {
if (!issueId || isEmpty(this.issuesMap) || !this.issuesMap[issueId]) return undefined; if (!issueId || isEmpty(this.issuesMap) || !this.issuesMap[issueId]) return undefined;
return this.issuesMap[issueId]; return this.issuesMap[issueId];
}; });
/** /**
* @description This method will return the issues from the issuesMap * @description This method will return the issues from the issuesMap
* @param {string[]} issueIds * @param {string[]} issueIds
* @returns {Record<string, TIssue> | undefined} * @returns {Record<string, TIssue> | undefined}
*/ */
getIssuesByIds = (issueIds: string[]) => { getIssuesByIds = computedFn((issueIds: string[]) => {
if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return undefined; if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return undefined;
const filteredIssues: { [key: string]: TIssue } = {}; const filteredIssues: { [key: string]: TIssue } = {};
Object.values(this.issuesMap).forEach((issue) => { Object.values(this.issuesMap).forEach((issue) => {
@ -99,5 +100,5 @@ export class IssueStore implements IIssueStore {
} }
}); });
return isEmpty(filteredIssues) ? undefined : filteredIssues; return isEmpty(filteredIssues) ? undefined : filteredIssues;
}; });
} }

View File

@ -142,7 +142,7 @@ export class IssueRootStore implements IIssueRootStore {
if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId; if (rootStore.app.router.userId) this.userId = rootStore.app.router.userId;
if (!isEmpty(rootStore?.state?.stateMap)) this.states = Object.keys(rootStore?.state?.stateMap); if (!isEmpty(rootStore?.state?.stateMap)) this.states = Object.keys(rootStore?.state?.stateMap);
if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates; if (!isEmpty(rootStore?.state?.projectStates)) this.stateDetails = rootStore?.state?.projectStates;
if (!isEmpty(rootStore?.labelRoot?.labelMap)) this.labels = Object.keys(rootStore?.labelRoot?.labelMap); if (!isEmpty(rootStore?.label?.labelMap)) this.labels = Object.keys(rootStore?.label?.labelMap);
if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap)) if (!isEmpty(rootStore?.memberRoot?.workspace?.workspaceMemberMap))
this.members = Object.keys(rootStore?.memberRoot?.workspace?.workspaceMemberMap); this.members = Object.keys(rootStore?.memberRoot?.workspace?.workspaceMemberMap);
if (!isEmpty(rootStore?.projectRoot?.project?.projectMap)) if (!isEmpty(rootStore?.projectRoot?.project?.projectMap))

View File

@ -1,5 +1,7 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set"; import set from "lodash/set";
import sortBy from "lodash/sortBy";
// services // services
import { IssueLabelService } from "services/issue"; import { IssueLabelService } from "services/issue";
// helpers // helpers
@ -7,17 +9,21 @@ import { buildTree } from "helpers/array.helper";
// types // types
import { RootStore } from "store/root.store"; import { RootStore } from "store/root.store";
import { IIssueLabel, IIssueLabelTree } from "@plane/types"; import { IIssueLabel, IIssueLabelTree } from "@plane/types";
import { ILabelRootStore } from "store/label";
export interface IProjectLabelStore { export interface ILabelStore {
//Loaders //Loaders
fetchedMap: Record<string, boolean>; fetchedMap: Record<string, boolean>;
//Observable
labelMap: Record<string, IIssueLabel>;
// computed // computed
projectLabels: IIssueLabel[] | undefined; projectLabels: IIssueLabel[] | undefined;
projectLabelsTree: IIssueLabelTree[] | undefined; projectLabelsTree: IIssueLabelTree[] | undefined;
workspaceLabels: IIssueLabel[] | undefined;
//computed actions //computed actions
getProjectLabels: (projectId: string) => IIssueLabel[] | undefined; getProjectLabels: (projectId: string | null) => IIssueLabel[] | undefined;
getLabelById: (labelId: string) => IIssueLabel | null;
// fetch actions // fetch actions
fetchWorkspaceLabels: (workspaceSlug: string) => Promise<IIssueLabel[]>;
fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise<IIssueLabel[]>; fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise<IIssueLabel[]>;
// crud actions // crud actions
createLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => Promise<IIssueLabel>; createLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => Promise<IIssueLabel>;
@ -39,7 +45,7 @@ export interface IProjectLabelStore {
deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise<void>; deleteLabel: (workspaceSlug: string, projectId: string, labelId: string) => Promise<void>;
} }
export class ProjectLabelStore implements IProjectLabelStore { export class LabelStore implements ILabelStore {
// root store // root store
rootStore; rootStore;
// root store labelMap // root store labelMap
@ -49,15 +55,13 @@ export class ProjectLabelStore implements IProjectLabelStore {
// services // services
issueLabelService; issueLabelService;
constructor(_labelRoot: ILabelRootStore, _rootStore: RootStore) { constructor(_rootStore: RootStore) {
makeObservable(this, { makeObservable(this, {
labelMap: observable, labelMap: observable,
fetchedMap: observable, fetchedMap: observable,
// computed // computed
projectLabels: computed, projectLabels: computed,
projectLabelsTree: computed, projectLabelsTree: computed,
// actions
getProjectLabels: action,
fetchProjectLabels: action, fetchProjectLabels: action,
createLabel: action, createLabel: action,
@ -68,18 +72,34 @@ export class ProjectLabelStore implements IProjectLabelStore {
// root store // root store
this.rootStore = _rootStore; this.rootStore = _rootStore;
this.labelMap = _labelRoot?.labelMap;
// services // services
this.issueLabelService = new IssueLabelService(); this.issueLabelService = new IssueLabelService();
} }
/**
* Returns the labelMap belongs to a specific workspace
*/
get workspaceLabels() {
const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
if (!currentWorkspaceDetails || !this.fetchedMap[worksapceSlug]) return;
return sortBy(
Object.values(this.labelMap).filter((label) => label.workspace_id === currentWorkspaceDetails.id),
"sort_order"
);
}
/** /**
* Returns the labelMap belonging to the current project * Returns the labelMap belonging to the current project
*/ */
get projectLabels() { get projectLabels() {
const projectId = this.rootStore.app.router.projectId; const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId] || !this.labelMap) return; const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
return Object.values(this.labelMap ?? {}).filter((label) => label.project === projectId); if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return;
return sortBy(
Object.values(this.labelMap).filter((label) => label.project_id === projectId),
"sort_order"
);
} }
/** /**
@ -90,10 +110,20 @@ export class ProjectLabelStore implements IProjectLabelStore {
return buildTree(this.projectLabels); return buildTree(this.projectLabels);
} }
getProjectLabels = (projectId: string) => { getProjectLabels = computedFn((projectId: string | null) => {
if (!this.fetchedMap[projectId] || !this.labelMap) return; const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
return Object.values(this.labelMap ?? {}).filter((label) => label.project === projectId); if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return;
}; return sortBy(
Object.values(this.labelMap).filter((label) => label.project_id === projectId),
"sort_order"
);
});
/**
* get label info from the map of labels in the store using label id
* @param labelId
*/
getLabelById = computedFn((labelId: string): IIssueLabel | null => this.labelMap?.[labelId] || null);
/** /**
* Fetches all the labelMap belongs to a specific project * Fetches all the labelMap belongs to a specific project
@ -112,6 +142,23 @@ export class ProjectLabelStore implements IProjectLabelStore {
return response; return response;
}); });
/**
* Fetches all the labelMap belongs to a specific project
* @param workspaceSlug
* @param projectId
* @returns Promise<IIssueLabel[]>
*/
fetchWorkspaceLabels = async (workspaceSlug: string) =>
await this.issueLabelService.getWorkspaceIssueLabels(workspaceSlug).then((response) => {
runInAction(() => {
response.forEach((label) => {
set(this.labelMap, [label.id], label);
});
set(this.fetchedMap, workspaceSlug, true);
});
return response;
});
/** /**
* Creates a new label for a specific project and add it to the store * Creates a new label for a specific project and add it to the store
* @param workspaceSlug * @param workspaceSlug

View File

@ -1,42 +0,0 @@
import { observable, makeObservable, action } from "mobx";
import { RootStore } from "../root.store";
// types
import { IIssueLabel } from "@plane/types";
import { IProjectLabelStore, ProjectLabelStore } from "./project-label.store";
import { IWorkspaceLabelStore, WorkspaceLabelStore } from "./workspace-label.store";
export interface ILabelRootStore {
// observables
labelMap: Record<string, IIssueLabel>;
// computed actions
getLabelById: (labelId: string) => IIssueLabel | null;
// sub-stores
project: IProjectLabelStore;
workspace: IWorkspaceLabelStore;
}
export class LabelRootStore implements ILabelRootStore {
// observables
labelMap: Record<string, IIssueLabel> = {};
// sub-stores
project: IProjectLabelStore;
workspace: IWorkspaceLabelStore;
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
labelMap: observable,
// computed actions
getLabelById: action,
});
// sub-stores
this.project = new ProjectLabelStore(this, _rootStore);
this.workspace = new WorkspaceLabelStore(this, _rootStore);
}
/**
* get label info from the map of labels in the store using label id
* @param labelId
*/
getLabelById = (labelId: string): IIssueLabel | null => this.labelMap?.[labelId] || null;
}

View File

@ -1,64 +0,0 @@
import { action, computed, makeObservable, runInAction } from "mobx";
import { set } from "lodash";
// services
import { IssueLabelService } from "services/issue";
// types
import { RootStore } from "store/root.store";
import { IIssueLabel } from "@plane/types";
import { ILabelRootStore } from "store/label";
export interface IWorkspaceLabelStore {
// computed
workspaceLabels: IIssueLabel[] | undefined;
// fetch actions
fetchWorkspaceLabels: (workspaceSlug: string) => Promise<IIssueLabel[]>;
}
export class WorkspaceLabelStore implements IWorkspaceLabelStore {
// root store
rootStore;
// root store labelMap
labelMap: Record<string, IIssueLabel> = {};
// services
issueLabelService;
constructor(_labelRoot: ILabelRootStore, _rootStore: RootStore) {
makeObservable(this, {
// computed
workspaceLabels: computed,
// actions
fetchWorkspaceLabels: action,
});
// root store
this.rootStore = _rootStore;
this.labelMap = _labelRoot?.labelMap;
// services
this.issueLabelService = new IssueLabelService();
}
/**
* Returns the labelMap belongs to a specific workspace
*/
get workspaceLabels() {
const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
if (!currentWorkspaceDetails) return;
return Object.values(this.labelMap).filter((label) => label.workspace === currentWorkspaceDetails.id);
}
/**
* Fetches all the labelMap belongs to a specific project
* @param workspaceSlug
* @param projectId
* @returns Promise<IIssueLabel[]>
*/
fetchWorkspaceLabels = async (workspaceSlug: string) =>
await this.issueLabelService.getWorkspaceIssueLabels(workspaceSlug).then((response) => {
runInAction(() => {
response.forEach((label) => {
set(this.labelMap, [label.id], label);
});
});
return response;
});
}

View File

@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set"; import set from "lodash/set";
import sortBy from "lodash/sortBy"; import sortBy from "lodash/sortBy";
// services // services
@ -64,9 +65,6 @@ export class ProjectMemberStore implements IProjectMemberStore {
projectMemberMap: observable, projectMemberMap: observable,
// computed // computed
projectMemberIds: computed, projectMemberIds: computed,
// computed actions
getProjectMemberDetails: action,
getProjectMemberIds: action,
// actions // actions
fetchProjectMembers: action, fetchProjectMembers: action,
bulkAddMembersToProject: action, bulkAddMembersToProject: action,
@ -101,7 +99,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
* @description get the details of a project member * @description get the details of a project member
* @param userId * @param userId
*/ */
getProjectMemberDetails = (userId: string) => { getProjectMemberDetails = computedFn((userId: string) => {
const projectId = this.routerStore.projectId; const projectId = this.routerStore.projectId;
if (!projectId) return null; if (!projectId) return null;
const projectMember = this.projectMemberMap?.[projectId]?.[userId]; const projectMember = this.projectMemberMap?.[projectId]?.[userId];
@ -113,13 +111,13 @@ export class ProjectMemberStore implements IProjectMemberStore {
member: this.memberRoot?.memberMap?.[projectMember.member], member: this.memberRoot?.memberMap?.[projectMember.member],
}; };
return memberDetails; return memberDetails;
}; });
/** /**
* @description get the list of all the user ids of all the members of a project using projectId * @description get the list of all the user ids of all the members of a project using projectId
* @param projectId * @param projectId
*/ */
getProjectMemberIds = (projectId: string): string[] | null => { getProjectMemberIds = computedFn((projectId: string): string[] | null => {
if (!this.projectMemberMap?.[projectId]) return null; if (!this.projectMemberMap?.[projectId]) return null;
let members = Object.values(this.projectMemberMap?.[projectId]); let members = Object.values(this.projectMemberMap?.[projectId]);
members = sortBy(members, [ members = sortBy(members, [
@ -128,7 +126,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
]); ]);
const memberIds = members.map((m) => m.member); const memberIds = members.map((m) => m.member);
return memberIds; return memberIds;
}; });
/** /**
* @description fetch the list of all the members of a project * @description fetch the list of all the members of a project

View File

@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set"; import set from "lodash/set";
import sortBy from "lodash/sortBy"; import sortBy from "lodash/sortBy";
// services // services
@ -67,11 +68,6 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
// computed // computed
workspaceMemberIds: computed, workspaceMemberIds: computed,
workspaceMemberInvitationIds: computed, workspaceMemberInvitationIds: computed,
// computed actions
getSearchedWorkspaceMemberIds: action,
getSearchedWorkspaceInvitationIds: action,
getWorkspaceMemberDetails: action,
getWorkspaceInvitationDetails: action,
// actions // actions
fetchWorkspaceMembers: action, fetchWorkspaceMembers: action,
updateMember: action, updateMember: action,
@ -114,7 +110,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
* @description get the list of all the user ids that match the search query of all the members of the current workspace * @description get the list of all the user ids that match the search query of all the members of the current workspace
* @param searchQuery * @param searchQuery
*/ */
getSearchedWorkspaceMemberIds = (searchQuery: string) => { getSearchedWorkspaceMemberIds = computedFn((searchQuery: string) => {
const workspaceSlug = this.routerStore.workspaceSlug; const workspaceSlug = this.routerStore.workspaceSlug;
if (!workspaceSlug) return null; if (!workspaceSlug) return null;
const workspaceMemberIds = this.workspaceMemberIds; const workspaceMemberIds = this.workspaceMemberIds;
@ -128,13 +124,13 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
return memberSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase()); return memberSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase());
}); });
return searchedWorkspaceMemberIds; return searchedWorkspaceMemberIds;
}; });
/** /**
* @description get the list of all the invitation ids that match the search query of all the member invitations of the current workspace * @description get the list of all the invitation ids that match the search query of all the member invitations of the current workspace
* @param searchQuery * @param searchQuery
*/ */
getSearchedWorkspaceInvitationIds = (searchQuery: string) => { getSearchedWorkspaceInvitationIds = computedFn((searchQuery: string) => {
const workspaceSlug = this.routerStore.workspaceSlug; const workspaceSlug = this.routerStore.workspaceSlug;
if (!workspaceSlug) return null; if (!workspaceSlug) return null;
const workspaceMemberInvitationIds = this.workspaceMemberInvitationIds; const workspaceMemberInvitationIds = this.workspaceMemberInvitationIds;
@ -146,13 +142,13 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
return invitationSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase()); return invitationSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase());
}); });
return searchedWorkspaceMemberInvitationIds; return searchedWorkspaceMemberInvitationIds;
}; });
/** /**
* @description get the details of a workspace member * @description get the details of a workspace member
* @param userId * @param userId
*/ */
getWorkspaceMemberDetails = (userId: string) => { getWorkspaceMemberDetails = computedFn((userId: string) => {
const workspaceSlug = this.routerStore.workspaceSlug; const workspaceSlug = this.routerStore.workspaceSlug;
if (!workspaceSlug) return null; if (!workspaceSlug) return null;
const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[userId]; const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[userId];
@ -164,14 +160,14 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
member: this.memberRoot?.memberMap?.[workspaceMember.member], member: this.memberRoot?.memberMap?.[workspaceMember.member],
}; };
return memberDetails; return memberDetails;
}; });
/** /**
* @description get the details of a workspace member invitation * @description get the details of a workspace member invitation
* @param workspaceSlug * @param workspaceSlug
* @param memberId * @param memberId
*/ */
getWorkspaceInvitationDetails = (invitationId: string) => { getWorkspaceInvitationDetails = computedFn((invitationId: string) => {
const workspaceSlug = this.routerStore.workspaceSlug; const workspaceSlug = this.routerStore.workspaceSlug;
if (!workspaceSlug) return null; if (!workspaceSlug) return null;
const invitationsList = this.workspaceMemberInvitations?.[workspaceSlug]; const invitationsList = this.workspaceMemberInvitations?.[workspaceSlug];
@ -179,7 +175,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
const invitation = invitationsList.find((inv) => inv.id === invitationId); const invitation = invitationsList.find((inv) => inv.id === invitationId);
return invitation ?? null; return invitation ?? null;
}; });
/** /**
* @description fetch all the members of a workspace * @description fetch all the members of a workspace

View File

@ -1,4 +1,5 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set"; import set from "lodash/set";
import sortBy from "lodash/sortBy"; import sortBy from "lodash/sortBy";
// services // services
@ -68,9 +69,6 @@ export class ModulesStore implements IModuleStore {
fetchedMap: observable, fetchedMap: observable,
// computed // computed
projectModuleIds: computed, projectModuleIds: computed,
// computed actions
getModuleById: action,
getProjectModuleIds: action,
// actions // actions
fetchModules: action, fetchModules: action,
fetchModuleDetails: action, fetchModuleDetails: action,
@ -109,20 +107,20 @@ export class ModulesStore implements IModuleStore {
* @param moduleId * @param moduleId
* @returns IModule | null * @returns IModule | null
*/ */
getModuleById = (moduleId: string) => this.moduleMap?.[moduleId] || null; getModuleById = computedFn((moduleId: string) => this.moduleMap?.[moduleId] || null);
/** /**
* @description returns list of module ids of the project id passed as argument * @description returns list of module ids of the project id passed as argument
* @param projectId * @param projectId
*/ */
getProjectModuleIds = (projectId: string) => { getProjectModuleIds = computedFn((projectId: string) => {
if (!this.fetchedMap[projectId]) return null; if (!this.fetchedMap[projectId]) return null;
let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId); let projectModules = Object.values(this.moduleMap).filter((m) => m.project === projectId);
projectModules = sortBy(projectModules, [(m) => !m.is_favorite, (m) => m.name.toLowerCase()]); projectModules = sortBy(projectModules, [(m) => !m.is_favorite, (m) => m.name.toLowerCase()]);
const projectModuleIds = projectModules.map((m) => m.id); const projectModuleIds = projectModules.map((m) => m.id);
return projectModuleIds; return projectModuleIds;
}; });
/** /**
* @description fetch all modules * @description fetch all modules

View File

@ -1,5 +1,6 @@
import { set } from "lodash"; import { set } from "lodash";
import { observable, action, makeObservable, runInAction, computed } from "mobx"; import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { computedFn } from "mobx-utils";
// services // services
import { ViewService } from "services/view.service"; import { ViewService } from "services/view.service";
import { RootStore } from "store/root.store"; import { RootStore } from "store/root.store";
@ -49,8 +50,6 @@ export class ProjectViewStore implements IProjectViewStore {
fetchedMap: observable, fetchedMap: observable,
// computed // computed
projectViewIds: computed, projectViewIds: computed,
// computed actions
getViewById: action,
// fetch actions // fetch actions
fetchViews: action, fetchViews: action,
fetchViewDetails: action, fetchViewDetails: action,
@ -81,7 +80,7 @@ export class ProjectViewStore implements IProjectViewStore {
/** /**
* Returns view details by id * Returns view details by id
*/ */
getViewById = (viewId: string) => this.viewMap?.[viewId] ?? null; getViewById = computedFn((viewId: string) => this.viewMap?.[viewId] ?? null);
/** /**
* Fetches views for current project * Fetches views for current project

View File

@ -1,4 +1,5 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx"; import { observable, action, computed, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set"; import set from "lodash/set";
// types // types
import { RootStore } from "../root.store"; import { RootStore } from "../root.store";
@ -6,7 +7,6 @@ import { IProject } from "@plane/types";
// services // services
import { IssueLabelService, IssueService } from "services/issue"; import { IssueLabelService, IssueService } from "services/issue";
import { ProjectService, ProjectStateService } from "services/project"; import { ProjectService, ProjectStateService } from "services/project";
export interface IProjectStore { export interface IProjectStore {
// observables // observables
searchQuery: string; searchQuery: string;
@ -64,7 +64,7 @@ export class ProjectStore implements IProjectStore {
joinedProjectIds: computed, joinedProjectIds: computed,
favoriteProjectIds: computed, favoriteProjectIds: computed,
// actions // actions
setSearchQuery: action, setSearchQuery: action.bound,
// fetch actions // fetch actions
fetchProjects: action, fetchProjects: action,
fetchProjectDetails: action, fetchProjectDetails: action,
@ -199,10 +199,10 @@ export class ProjectStore implements IProjectStore {
* @param projectId * @param projectId
* @returns IProject | null * @returns IProject | null
*/ */
getProjectById = (projectId: string) => { getProjectById = computedFn((projectId: string) => {
const projectInfo = this.projectMap[projectId] || null; const projectInfo = this.projectMap[projectId] || null;
return projectInfo; return projectInfo;
}; });
/** /**
* Adds project to favorites and updates project favorite status in the store * Adds project to favorites and updates project favorite status in the store

View File

@ -9,7 +9,6 @@ import { IUserRootStore, UserRootStore } from "./user";
import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace";
import { IssueRootStore, IIssueRootStore } from "./issue/root.store"; import { IssueRootStore, IIssueRootStore } from "./issue/root.store";
import { IStateStore, StateStore } from "./state.store"; import { IStateStore, StateStore } from "./state.store";
import { ILabelRootStore, LabelRootStore } from "./label";
import { IMemberRootStore, MemberRootStore } from "./member"; import { IMemberRootStore, MemberRootStore } from "./member";
import { IInboxRootStore, InboxRootStore } from "./inbox"; import { IInboxRootStore, InboxRootStore } from "./inbox";
import { IEstimateStore, EstimateStore } from "./estimate.store"; import { IEstimateStore, EstimateStore } from "./estimate.store";
@ -17,6 +16,7 @@ import { GlobalViewStore, IGlobalViewStore } from "./global-view.store";
import { IMentionStore, MentionStore } from "./mention.store"; import { IMentionStore, MentionStore } from "./mention.store";
import { DashboardStore, IDashboardStore } from "./dashboard.store"; import { DashboardStore, IDashboardStore } from "./dashboard.store";
import { IProjectPageStore, ProjectPageStore } from "./project-page.store"; import { IProjectPageStore, ProjectPageStore } from "./project-page.store";
import { ILabelStore, LabelStore } from "./label.store";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@ -25,7 +25,6 @@ export class RootStore {
user: IUserRootStore; user: IUserRootStore;
workspaceRoot: IWorkspaceRootStore; workspaceRoot: IWorkspaceRootStore;
projectRoot: IProjectRootStore; projectRoot: IProjectRootStore;
labelRoot: ILabelRootStore;
memberRoot: IMemberRootStore; memberRoot: IMemberRootStore;
inboxRoot: IInboxRootStore; inboxRoot: IInboxRootStore;
cycle: ICycleStore; cycle: ICycleStore;
@ -34,6 +33,7 @@ export class RootStore {
globalView: IGlobalViewStore; globalView: IGlobalViewStore;
issue: IIssueRootStore; issue: IIssueRootStore;
state: IStateStore; state: IStateStore;
label: ILabelStore;
estimate: IEstimateStore; estimate: IEstimateStore;
mention: IMentionStore; mention: IMentionStore;
dashboard: IDashboardStore; dashboard: IDashboardStore;
@ -44,7 +44,6 @@ export class RootStore {
this.user = new UserRootStore(this); this.user = new UserRootStore(this);
this.workspaceRoot = new WorkspaceRootStore(this); this.workspaceRoot = new WorkspaceRootStore(this);
this.projectRoot = new ProjectRootStore(this); this.projectRoot = new ProjectRootStore(this);
this.labelRoot = new LabelRootStore(this);
this.memberRoot = new MemberRootStore(this); this.memberRoot = new MemberRootStore(this);
this.inboxRoot = new InboxRootStore(this); this.inboxRoot = new InboxRootStore(this);
// independent stores // independent stores
@ -54,6 +53,7 @@ export class RootStore {
this.globalView = new GlobalViewStore(this); this.globalView = new GlobalViewStore(this);
this.issue = new IssueRootStore(this); this.issue = new IssueRootStore(this);
this.state = new StateStore(this); this.state = new StateStore(this);
this.label = new LabelStore(this);
this.estimate = new EstimateStore(this); this.estimate = new EstimateStore(this);
this.mention = new MentionStore(this); this.mention = new MentionStore(this);
this.projectPages = new ProjectPageStore(this); this.projectPages = new ProjectPageStore(this);

View File

@ -1,4 +1,5 @@
import { makeObservable, observable, computed, action, runInAction } from "mobx"; import { makeObservable, observable, computed, action, runInAction } from "mobx";
import { computedFn } from "mobx-utils"
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
import set from "lodash/set"; import set from "lodash/set";
// store // store
@ -21,6 +22,7 @@ export interface IStateStore {
getProjectStates: (projectId: string) => IState[] | undefined; getProjectStates: (projectId: string) => IState[] | undefined;
// fetch actions // fetch actions
fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<IState[]>; fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise<IState[]>;
fetchWorkspaceStates: (workspaceSlug: string) => Promise<IState[]>;
// crud actions // crud actions
createState: (workspaceSlug: string, projectId: string, data: Partial<IState>) => Promise<IState>; createState: (workspaceSlug: string, projectId: string, data: Partial<IState>) => Promise<IState>;
updateState: ( updateState: (
@ -55,9 +57,6 @@ export class StateStore implements IStateStore {
// computed // computed
projectStates: computed, projectStates: computed,
groupedProjectStates: computed, groupedProjectStates: computed,
// computed actions
getStateById: action,
getProjectStates: action,
// fetch action // fetch action
fetchProjectStates: action, fetchProjectStates: action,
// CRUD actions // CRUD actions
@ -76,9 +75,10 @@ export class StateStore implements IStateStore {
* Returns the stateMap belongs to a specific project * Returns the stateMap belongs to a specific project
*/ */
get projectStates() { get projectStates() {
const projectId = this.router.query?.projectId?.toString(); const projectId = this.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return; const worksapceSlug = this.router.workspaceSlug || "";
return Object.values(this.stateMap).filter((state) => state.project === this.router.query.projectId); if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return;
return Object.values(this.stateMap).filter((state) => state.project_id === projectId);
} }
/** /**
@ -93,20 +93,21 @@ export class StateStore implements IStateStore {
* @description returns state details using state id * @description returns state details using state id
* @param stateId * @param stateId
*/ */
getStateById = (stateId: string) => { getStateById = computedFn((stateId: string) => {
if (!this.stateMap) return; if (!this.stateMap) return;
return this.stateMap[stateId] ?? undefined; return this.stateMap[stateId] ?? undefined;
}; });
/** /**
* Returns the stateMap belongs to a project by projectId * Returns the stateMap belongs to a project by projectId
* @param projectId * @param projectId
* @returns IState[] * @returns IState[]
*/ */
getProjectStates = (projectId: string) => { getProjectStates = computedFn((projectId: string) => {
if (!projectId || !this.fetchedMap[projectId]) return; const worksapceSlug = this.router.workspaceSlug || "";
return Object.values(this.stateMap).filter((state) => state.project === projectId); if (!projectId || !(this.fetchedMap[projectId] || this.fetchedMap[worksapceSlug])) return;
}; return Object.values(this.stateMap).filter((state) => state.project_id === projectId);
});
/** /**
* fetches the stateMap of a project * fetches the stateMap of a project
@ -125,6 +126,22 @@ export class StateStore implements IStateStore {
return statesResponse; return statesResponse;
}; };
/**
* fetches the stateMap of all the states in workspace
* @param workspaceSlug
* @returns
*/
fetchWorkspaceStates = async (workspaceSlug: string) => {
const statesResponse = await this.stateService.getWorkspaceStates(workspaceSlug);
runInAction(() => {
statesResponse.forEach((state) => {
set(this.stateMap, [state.id], state);
});
set(this.fetchedMap, workspaceSlug, true);
});
return statesResponse;
};
/** /**
* creates a new state in a project and adds it to the store * creates a new state in a project and adds it to the store
* @param workspaceSlug * @param workspaceSlug
@ -191,7 +208,7 @@ export class StateStore implements IStateStore {
markStateAsDefault = async (workspaceSlug: string, projectId: string, stateId: string) => { markStateAsDefault = async (workspaceSlug: string, projectId: string, stateId: string) => {
const originalStates = this.stateMap; const originalStates = this.stateMap;
const currentDefaultState = Object.values(this.stateMap).find( const currentDefaultState = Object.values(this.stateMap).find(
(state) => state.project === projectId && state.default (state) => state.project_id === projectId && state.default
); );
try { try {
runInAction(() => { runInAction(() => {

View File

@ -1,5 +1,6 @@
// mobx // mobx
import { action, observable, makeObservable, runInAction } from "mobx"; import { action, observable, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import { APITokenService } from "services/api_token.service"; import { APITokenService } from "services/api_token.service";
import { RootStore } from "../root.store"; import { RootStore } from "../root.store";
// types // types
@ -30,8 +31,6 @@ export class ApiTokenStore implements IApiTokenStore {
makeObservable(this, { makeObservable(this, {
// observables // observables
apiTokens: observable, apiTokens: observable,
// computed actions
getApiTokenById: action,
// fetch actions // fetch actions
fetchApiTokens: action, fetchApiTokens: action,
fetchApiTokenDetails: action, fetchApiTokenDetails: action,
@ -49,10 +48,10 @@ export class ApiTokenStore implements IApiTokenStore {
* get API token by id * get API token by id
* @param apiTokenId * @param apiTokenId
*/ */
getApiTokenById = (apiTokenId: string) => { getApiTokenById = computedFn((apiTokenId: string) => {
if (!this.apiTokens) return null; if (!this.apiTokens) return null;
return this.apiTokens[apiTokenId] || null; return this.apiTokens[apiTokenId] || null;
}; });
/** /**
* fetch all the API tokens for a workspace * fetch all the API tokens for a workspace

View File

@ -1,5 +1,6 @@
// mobx // mobx
import { action, observable, makeObservable, computed, runInAction } from "mobx"; import { action, observable, makeObservable, computed, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import { IWebhook } from "@plane/types"; import { IWebhook } from "@plane/types";
import { WebhookService } from "services/webhook.service"; import { WebhookService } from "services/webhook.service";
import { RootStore } from "../root.store"; import { RootStore } from "../root.store";
@ -46,8 +47,6 @@ export class WebhookStore implements IWebhookStore {
webhookSecretKey: observable.ref, webhookSecretKey: observable.ref,
// computed // computed
currentWebhook: computed, currentWebhook: computed,
// computed actions
getWebhookById: action,
// fetch actions // fetch actions
fetchWebhooks: action, fetchWebhooks: action,
fetchWebhookById: action, fetchWebhookById: action,
@ -80,7 +79,7 @@ export class WebhookStore implements IWebhookStore {
* get webhook info from the object of webhooks in the store using webhook id * get webhook info from the object of webhooks in the store using webhook id
* @param webhookId * @param webhookId
*/ */
getWebhookById = (webhookId: string) => this.webhooks?.[webhookId] || null; getWebhookById = computedFn((webhookId: string) => this.webhooks?.[webhookId] || null);
/** /**
* fetch all the webhooks for a workspace * fetch all the webhooks for a workspace

View File

@ -2406,6 +2406,13 @@
lodash.merge "^4.6.2" lodash.merge "^4.6.2"
postcss-selector-parser "6.0.10" postcss-selector-parser "6.0.10"
"@tippyjs/react@^4.2.6":
version "4.2.6"
resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.6.tgz#971677a599bf663f20bb1c60a62b9555b749cc71"
integrity sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==
dependencies:
tippy.js "^6.3.1"
"@tiptap/core@^2.1.13": "@tiptap/core@^2.1.13":
version "2.1.13" version "2.1.13"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.13.tgz#e21f566e81688c826c6f26d2940886734189e193" resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.1.13.tgz#e21f566e81688c826c6f26d2940886734189e193"
@ -6617,16 +6624,6 @@ mkdirp@^0.5.5:
dependencies: dependencies:
minimist "^1.2.6" minimist "^1.2.6"
mobx-devtools-mst@^0.9.30:
version "0.9.30"
resolved "https://registry.yarnpkg.com/mobx-devtools-mst/-/mobx-devtools-mst-0.9.30.tgz#0d1cad8b3d97e1f3f94bb9afb701cd9c8b5b164d"
integrity sha512-6fIYeFG4xT4syIeKddmK55zQbc3ZZZr/272/cCbfaAAM5YiuFdteGZGUgdsz8wxf/mGxWZbFOM3WmASAnpwrbw==
mobx-react-devtools@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/mobx-react-devtools/-/mobx-react-devtools-6.1.1.tgz#a462b944085cf11ff96fc937d12bf31dab4c8984"
integrity sha512-nc5IXLdEUFLn3wZal65KF3/JFEFd+mbH4KTz/IG5BOPyw7jo8z29w/8qm7+wiCyqVfUIgJ1gL4+HVKmcXIOgqA==
mobx-react-lite@^4.0.3, mobx-react-lite@^4.0.4: mobx-react-lite@^4.0.3, mobx-react-lite@^4.0.4:
version "4.0.5" version "4.0.5"
resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.5.tgz#e2cb98f813e118917bcc463638f5bf6ea053a67b" resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-4.0.5.tgz#e2cb98f813e118917bcc463638f5bf6ea053a67b"
@ -6641,10 +6638,10 @@ mobx-react@^9.1.0:
dependencies: dependencies:
mobx-react-lite "^4.0.4" mobx-react-lite "^4.0.4"
mobx-state-tree@^5.4.0: mobx-utils@^6.0.8:
version "5.4.0" version "6.0.8"
resolved "https://registry.yarnpkg.com/mobx-state-tree/-/mobx-state-tree-5.4.0.tgz#d41b7fd90b8d4b063bc32526758417f1100751df" resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-6.0.8.tgz#843e222c7694050c2e42842682fd24a84fdb7024"
integrity sha512-2VuUhAqFklxgGqFNqaZUXYYSQINo8C2SUEP9YfCQrwatHWHqJLlEC7Xb+5WChkev7fubzn3aVuby26Q6h+JeBg== integrity sha512-fPNt0vJnHwbQx9MojJFEnJLfM3EMGTtpy4/qOOW6xueh1mPofMajrbYAUvByMYAvCJnpy1A5L0t+ZVB5niKO4g==
mobx@^6.10.0: mobx@^6.10.0:
version "6.12.0" version "6.12.0"
@ -8540,7 +8537,7 @@ tinycolor2@^1.4.1:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
tippy.js@^6.3.7: tippy.js@^6.3.1, tippy.js@^6.3.7:
version "6.3.7" version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==