mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-478]: implemented cycle filters in display properties for list, kanban, and spreadsheet layouts (#3702)
* chore: implemented the modules and cycle filter in the display properties * typo: added placeholders for module and cycle select in spreadsheet view * feat: created workspace modules and cycles endpoints in appi server and implemented in application * ui: UI changes in the spreadsheet module and cycle dropdown and added cursor navigation for cycle via arrow keys * format: formatted api sever * chore: module select logic updated * chore: updated module updated handler in all-properties and spreadsheet column * chore: updated url names for workspace modules and cycles * fix: validated members availability in the modules list member tooltip --------- Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
This commit is contained in:
parent
56f4df4cb5
commit
ac6e710623
@ -22,6 +22,8 @@ from plane.app.views import (
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
WorkspaceModulesEndpoint,
|
||||
WorkspaceCyclesEndpoint,
|
||||
)
|
||||
|
||||
|
||||
@ -219,4 +221,14 @@ urlpatterns = [
|
||||
WorkspaceEstimatesEndpoint.as_view(),
|
||||
name="workspace-estimate",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/modules/",
|
||||
WorkspaceModulesEndpoint.as_view(),
|
||||
name="workspace-modules",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/cycles/",
|
||||
WorkspaceCyclesEndpoint.as_view(),
|
||||
name="workspace-cycles",
|
||||
),
|
||||
]
|
||||
|
@ -49,6 +49,8 @@ from .workspace import (
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
WorkspaceModulesEndpoint,
|
||||
WorkspaceCyclesEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import (
|
||||
|
@ -22,6 +22,7 @@ from django.db.models import (
|
||||
When,
|
||||
Max,
|
||||
IntegerField,
|
||||
Sum,
|
||||
)
|
||||
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
|
||||
from django.db.models.fields import DateField
|
||||
@ -73,6 +74,9 @@ from plane.db.models import (
|
||||
WorkspaceUserProperties,
|
||||
Estimate,
|
||||
EstimatePoint,
|
||||
Module,
|
||||
ModuleLink,
|
||||
Cycle,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
WorkSpaceBasePermission,
|
||||
@ -85,6 +89,12 @@ from plane.app.permissions import (
|
||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.event_tracking_task import workspace_invite_event
|
||||
from plane.app.serializers.module import (
|
||||
ModuleSerializer,
|
||||
)
|
||||
from plane.app.serializers.cycle import (
|
||||
CycleSerializer,
|
||||
)
|
||||
|
||||
|
||||
class WorkSpaceViewSet(BaseViewSet):
|
||||
@ -1490,6 +1500,192 @@ class WorkspaceEstimatesEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceModulesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceViewerPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
modules = (
|
||||
Module.objects.filter(workspace__slug=slug)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("lead")
|
||||
.prefetch_related("members")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"link_module",
|
||||
queryset=ModuleLink.objects.select_related(
|
||||
"module", "created_by"
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_module",
|
||||
filter=Q(
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="completed",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cancelled_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="cancelled",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="started",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
unstarted_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="unstarted",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_issues=Count(
|
||||
"issue_module__issue__state__group",
|
||||
filter=Q(
|
||||
issue_module__issue__state__group="backlog",
|
||||
issue_module__issue__archived_at__isnull=True,
|
||||
issue_module__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||
)
|
||||
|
||||
serializer = ModuleSerializer(modules, many=True).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceViewerPermission,
|
||||
]
|
||||
|
||||
def get(self, request, slug):
|
||||
cycles = (
|
||||
Cycle.objects.filter(workspace__slug=slug)
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
.select_related("owned_by")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
filter=Q(
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cancelled_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="cancelled",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
unstarted_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="unstarted",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_issues=Count(
|
||||
"issue_cycle__issue__state__group",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="backlog",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
||||
)
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="completed",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_estimates=Sum(
|
||||
"issue_cycle__issue__estimate_point",
|
||||
filter=Q(
|
||||
issue_cycle__issue__state__group="started",
|
||||
issue_cycle__issue__archived_at__isnull=True,
|
||||
issue_cycle__issue__is_draft=False,
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||
.distinct()
|
||||
)
|
||||
serializer = CycleSerializer(cycles, many=True).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkspaceViewerPermission,
|
||||
|
6
packages/types/src/view-props.d.ts
vendored
6
packages/types/src/view-props.d.ts
vendored
@ -30,6 +30,10 @@ export type TIssueOrderByOptions =
|
||||
| "-assignees__first_name"
|
||||
| "labels__name"
|
||||
| "-labels__name"
|
||||
| "modules__name"
|
||||
| "-modules__name"
|
||||
| "cycle__name"
|
||||
| "-cycle__name"
|
||||
| "target_date"
|
||||
| "-target_date"
|
||||
| "estimate_point"
|
||||
@ -109,6 +113,8 @@ export interface IIssueDisplayProperties {
|
||||
estimate?: boolean;
|
||||
created_on?: boolean;
|
||||
updated_on?: boolean;
|
||||
modules?: boolean;
|
||||
cycle?: boolean;
|
||||
}
|
||||
|
||||
export type TIssueKanbanFilters = {
|
||||
|
@ -86,7 +86,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
|
||||
const cycleIds = (getProjectCycleIds(projectId) ?? [])?.filter((cycleId) => {
|
||||
const cycleDetails = getCycleById(cycleId);
|
||||
return cycleDetails?.status.toLowerCase() != "completed" ? true : false;
|
||||
return cycleDetails?.status ? (cycleDetails?.status.toLowerCase() != "completed" ? true : false) : true;
|
||||
});
|
||||
|
||||
const options: DropdownOptions = cycleIds?.map((cycleId) => {
|
||||
@ -172,7 +172,10 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
|
||||
className={cn(
|
||||
"clickable block h-full w-full outline-none hover:bg-custom-background-80",
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
@ -182,7 +185,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn(
|
||||
"block h-full max-w-full outline-none",
|
||||
"clickable block h-full max-w-full outline-none hover:bg-custom-background-80",
|
||||
{
|
||||
"cursor-not-allowed text-custom-text-200": disabled,
|
||||
"cursor-pointer": !disabled,
|
||||
|
@ -278,7 +278,10 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
|
||||
className={cn(
|
||||
"clickable block h-full w-full outline-none hover:bg-custom-background-80",
|
||||
buttonContainerClassName
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
{button}
|
||||
@ -288,7 +291,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={cn(
|
||||
"clickable block h-full max-w-full outline-none",
|
||||
"clickable block h-full max-w-full outline-none hover:bg-custom-background-80",
|
||||
{
|
||||
"cursor-not-allowed text-custom-text-200": disabled,
|
||||
"cursor-pointer": !disabled,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import xor from "lodash/xor";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
@ -36,16 +37,22 @@ export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props)
|
||||
if (!issue || !issue.module_ids) return;
|
||||
|
||||
setIsUpdating(true);
|
||||
const updatedModuleIds = xor(issue.module_ids, moduleIds);
|
||||
const modulesToAdd: string[] = [];
|
||||
const modulesToRemove: string[] = [];
|
||||
|
||||
if (moduleIds.length === 0)
|
||||
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, issue.module_ids);
|
||||
else if (moduleIds.length > issue.module_ids.length) {
|
||||
const newModuleIds = moduleIds.filter((m) => !issue.module_ids?.includes(m));
|
||||
await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, newModuleIds);
|
||||
} else if (moduleIds.length < issue.module_ids.length) {
|
||||
const removedModuleIds = issue.module_ids.filter((m) => !moduleIds.includes(m));
|
||||
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, removedModuleIds);
|
||||
for (const moduleId of updatedModuleIds) {
|
||||
if (issue.module_ids.includes(moduleId)) {
|
||||
modulesToRemove.push(moduleId);
|
||||
} else {
|
||||
modulesToAdd.push(moduleId);
|
||||
}
|
||||
}
|
||||
if (modulesToRemove.length > 0)
|
||||
await issueOperations.removeModulesFromIssue?.(workspaceSlug, projectId, issueId, modulesToRemove);
|
||||
|
||||
if (modulesToAdd.length > 0)
|
||||
await issueOperations.addModulesToIssue?.(workspaceSlug, projectId, issueId, modulesToAdd);
|
||||
|
||||
setIsUpdating(false);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
// components
|
||||
import { FilterHeader } from "../helpers/filter-header";
|
||||
// types
|
||||
@ -14,10 +14,19 @@ type Props = {
|
||||
};
|
||||
|
||||
export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
|
||||
const router = useRouter();
|
||||
const { moduleId, cycleId } = router.query;
|
||||
const { displayProperties, handleUpdate } = props;
|
||||
|
||||
const [previewEnabled, setPreviewEnabled] = React.useState(true);
|
||||
|
||||
const handleDisplayPropertyVisibility = (key: keyof IIssueDisplayProperties): boolean => {
|
||||
const visibility = true;
|
||||
if (key === "modules" && moduleId) return false;
|
||||
if (key === "cycle" && cycleId) return false;
|
||||
return visibility;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterHeader
|
||||
@ -27,24 +36,27 @@ export const FilterDisplayProperties: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
{previewEnabled && (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
{ISSUE_DISPLAY_PROPERTIES.map((displayProperty) => (
|
||||
<button
|
||||
key={displayProperty.key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-0.5 text-xs transition-all ${
|
||||
displayProperties?.[displayProperty.key]
|
||||
? "border-custom-primary-100 bg-custom-primary-100 text-white"
|
||||
: "border-custom-border-200 hover:bg-custom-background-80"
|
||||
}`}
|
||||
onClick={() =>
|
||||
handleUpdate({
|
||||
[displayProperty.key]: !displayProperties?.[displayProperty.key],
|
||||
})
|
||||
}
|
||||
>
|
||||
{displayProperty.title}
|
||||
</button>
|
||||
))}
|
||||
{ISSUE_DISPLAY_PROPERTIES.map(
|
||||
(displayProperty) =>
|
||||
handleDisplayPropertyVisibility(displayProperty?.key) && (
|
||||
<button
|
||||
key={displayProperty.key}
|
||||
type="button"
|
||||
className={`rounded border px-2 py-0.5 text-xs transition-all ${
|
||||
displayProperties?.[displayProperty.key]
|
||||
? "border-custom-primary-100 bg-custom-primary-100 text-white"
|
||||
: "border-custom-border-200 hover:bg-custom-background-80"
|
||||
}`}
|
||||
onClick={() =>
|
||||
handleUpdate({
|
||||
[displayProperty.key]: !displayProperties?.[displayProperty.key],
|
||||
})
|
||||
}
|
||||
>
|
||||
{displayProperty.title}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -10,6 +10,7 @@ import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
|
||||
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
|
||||
import { IDraftIssuesFilter, IDraftIssues } from "store/issue/draft";
|
||||
import { IArchivedIssuesFilter, IArchivedIssues } from "store/issue/archived";
|
||||
import { EIssueActions } from "../types";
|
||||
// components
|
||||
import { IQuickActionProps } from "./list-view-types";
|
||||
// constants
|
||||
@ -18,12 +19,6 @@ import { TCreateModalStoreTypes } from "constants/issue";
|
||||
// hooks
|
||||
import { useIssues, useUser } from "hooks/store";
|
||||
|
||||
enum EIssueActions {
|
||||
UPDATE = "update",
|
||||
DELETE = "delete",
|
||||
REMOVE = "remove",
|
||||
}
|
||||
|
||||
interface IBaseListRoot {
|
||||
issuesFilter:
|
||||
| IProjectIssuesFilter
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
|
||||
import xor from "lodash/xor";
|
||||
// hooks
|
||||
import { useEventTracker, useEstimate, useLabel, useApplication } from "hooks/store";
|
||||
import { useEventTracker, useEstimate, useLabel, useIssues } from "hooks/store";
|
||||
// components
|
||||
import { IssuePropertyLabels } from "../properties/labels";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
@ -12,6 +14,8 @@ import {
|
||||
EstimateDropdown,
|
||||
PriorityDropdown,
|
||||
ProjectMemberDropdown,
|
||||
ModuleDropdown,
|
||||
CycleDropdown,
|
||||
StateDropdown,
|
||||
} from "components/dropdowns";
|
||||
// helpers
|
||||
@ -20,6 +24,7 @@ import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types";
|
||||
// constants
|
||||
import { ISSUE_UPDATED } from "constants/event-tracker";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
export interface IIssueProperties {
|
||||
issue: TIssue;
|
||||
@ -36,12 +41,39 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
const { labelMap } = useLabel();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const {
|
||||
router: { workspaceSlug },
|
||||
} = useApplication();
|
||||
issues: { addModulesToIssue, removeModulesFromIssue },
|
||||
} = useIssues(EIssuesStoreType.MODULE);
|
||||
const {
|
||||
issues: { addIssueToCycle, removeIssueFromCycle },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId, moduleId } = router.query;
|
||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||
const currentLayout = `${activeLayout} layout`;
|
||||
|
||||
const issueOperations = useMemo(
|
||||
() => ({
|
||||
addModulesToIssue: async (moduleIds: string[]) => {
|
||||
if (!workspaceSlug || !issue.project_id || !issue.id) return;
|
||||
await addModulesToIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, moduleIds);
|
||||
},
|
||||
removeModulesFromIssue: async (moduleIds: string[]) => {
|
||||
if (!workspaceSlug || !issue.project_id || !issue.id) return;
|
||||
await removeModulesFromIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, moduleIds);
|
||||
},
|
||||
addIssueToCycle: async (cycleId: string) => {
|
||||
if (!workspaceSlug || !issue.project_id || !issue.id) return;
|
||||
await addIssueToCycle?.(workspaceSlug.toString(), issue.project_id, cycleId, [issue.id]);
|
||||
},
|
||||
removeIssueFromCycle: async (cycleId: string) => {
|
||||
if (!workspaceSlug || !issue.project_id || !issue.id) return;
|
||||
await removeIssueFromCycle?.(workspaceSlug.toString(), issue.project_id, cycleId, issue.id);
|
||||
},
|
||||
}),
|
||||
[workspaceSlug, issue, addModulesToIssue, removeModulesFromIssue, addIssueToCycle, removeIssueFromCycle]
|
||||
);
|
||||
|
||||
const handleState = (stateId: string) => {
|
||||
handleIssues({ ...issue, state_id: stateId }).then(() => {
|
||||
captureIssueEvent({
|
||||
@ -98,6 +130,45 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleModule = useCallback(
|
||||
(moduleIds: string[] | null) => {
|
||||
if (!issue || !issue.module_ids || !moduleIds) return;
|
||||
|
||||
const updatedModuleIds = xor(issue.module_ids, moduleIds);
|
||||
const modulesToAdd: string[] = [];
|
||||
const modulesToRemove: string[] = [];
|
||||
for (const moduleId of updatedModuleIds)
|
||||
if (issue.module_ids.includes(moduleId)) modulesToRemove.push(moduleId);
|
||||
else modulesToAdd.push(moduleId);
|
||||
if (modulesToAdd.length > 0) issueOperations.addModulesToIssue(modulesToAdd);
|
||||
if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove);
|
||||
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||
path: router.asPath,
|
||||
updates: { changed_property: "module_ids", change_details: { module_ids: moduleIds } },
|
||||
});
|
||||
},
|
||||
[issueOperations, captureIssueEvent, currentLayout, router, issue]
|
||||
);
|
||||
|
||||
const handleCycle = useCallback(
|
||||
(cycleId: string | null) => {
|
||||
if (!issue || issue.cycle_id === cycleId) return;
|
||||
if (cycleId) issueOperations.addIssueToCycle?.(cycleId);
|
||||
else issueOperations.removeIssueFromCycle?.(issue.cycle_id ?? "");
|
||||
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_UPDATED,
|
||||
payload: { ...issue, state: "SUCCESS", element: currentLayout },
|
||||
path: router.asPath,
|
||||
updates: { changed_property: "cycle", change_details: { cycle_id: cycleId } },
|
||||
});
|
||||
},
|
||||
[issue, issueOperations, captureIssueEvent, currentLayout, router.asPath]
|
||||
);
|
||||
|
||||
const handleStartDate = (date: Date | null) => {
|
||||
handleIssues({ ...issue, start_date: date ? renderFormattedPayloadDate(date) : null }).then(() => {
|
||||
captureIssueEvent({
|
||||
@ -249,6 +320,40 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
{/* modules */}
|
||||
{moduleId === undefined && (
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="modules">
|
||||
<div className="h-5">
|
||||
<ModuleDropdown
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.module_ids ?? []}
|
||||
onChange={handleModule}
|
||||
disabled={isReadOnly}
|
||||
multiple
|
||||
buttonVariant="border-with-text"
|
||||
showCount={true}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
)}
|
||||
|
||||
{/* cycles */}
|
||||
{cycleId === undefined && (
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="cycle">
|
||||
<div className="h-5">
|
||||
<CycleDropdown
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.cycle_id}
|
||||
onChange={handleCycle}
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-with-text"
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
)}
|
||||
|
||||
{/* estimates */}
|
||||
{areEstimatesEnabledForCurrentProject && (
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
|
||||
|
@ -0,0 +1,66 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues } from "hooks/store";
|
||||
// components
|
||||
import { CycleDropdown } from "components/dropdowns";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetCycleColumn: React.FC<Props> = observer((props) => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// props
|
||||
const { issue, disabled, onClose } = props;
|
||||
// hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const {
|
||||
issues: { addIssueToCycle, removeIssueFromCycle },
|
||||
} = useIssues(EIssuesStoreType.CYCLE);
|
||||
|
||||
const handleCycle = useCallback(
|
||||
async (cycleId: string | null) => {
|
||||
console.log("cycleId", cycleId);
|
||||
if (!workspaceSlug || !issue || issue.cycle_id === cycleId) return;
|
||||
if (cycleId) await addIssueToCycle(workspaceSlug.toString(), issue.project_id, cycleId, [issue.id]);
|
||||
else await removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, issue.cycle_id ?? "", issue.id);
|
||||
captureIssueEvent({
|
||||
eventName: "Issue updated",
|
||||
payload: {
|
||||
...issue,
|
||||
cycle_id: cycleId,
|
||||
element: "Spreadsheet layout",
|
||||
},
|
||||
updates: { changed_property: "cycle", change_details: { cycle_id: cycleId } },
|
||||
path: router.asPath,
|
||||
});
|
||||
},
|
||||
[workspaceSlug, issue, addIssueToCycle, removeIssueFromCycle, captureIssueEvent, router.asPath]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<CycleDropdown
|
||||
projectId={issue.project_id}
|
||||
value={issue.cycle_id}
|
||||
onChange={handleCycle}
|
||||
disabled={disabled}
|
||||
placeholder="Select cycle"
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonContainerClassName="w-full relative flex items-center p-2"
|
||||
buttonClassName="relative border-[0.5px] border-custom-border-400 h-4.5"
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -9,4 +9,6 @@ export * from "./priority-column";
|
||||
export * from "./start-date-column";
|
||||
export * from "./state-column";
|
||||
export * from "./sub-issue-column";
|
||||
export * from "./updated-on-column";
|
||||
export * from "./updated-on-column";
|
||||
export * from "./module-column";
|
||||
export * from "./cycle-column";
|
||||
|
@ -0,0 +1,79 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import xor from "lodash/xor";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues } from "hooks/store";
|
||||
// components
|
||||
import { ModuleDropdown } from "components/dropdowns";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetModuleColumn: React.FC<Props> = observer((props) => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// props
|
||||
const { issue, disabled, onClose } = props;
|
||||
// hooks
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const {
|
||||
issues: { addModulesToIssue, removeModulesFromIssue },
|
||||
} = useIssues(EIssuesStoreType.MODULE);
|
||||
|
||||
const handleModule = useCallback(
|
||||
async (moduleIds: string[] | null) => {
|
||||
if (!workspaceSlug || !issue || !issue.module_ids || !moduleIds) return;
|
||||
|
||||
const updatedModuleIds = xor(issue.module_ids, moduleIds);
|
||||
const modulesToAdd: string[] = [];
|
||||
const modulesToRemove: string[] = [];
|
||||
for (const moduleId of updatedModuleIds)
|
||||
if (issue.module_ids.includes(moduleId)) modulesToRemove.push(moduleId);
|
||||
else modulesToAdd.push(moduleId);
|
||||
if (modulesToAdd.length > 0)
|
||||
addModulesToIssue(workspaceSlug.toString(), issue.project_id, issue.id, modulesToAdd);
|
||||
if (modulesToRemove.length > 0)
|
||||
removeModulesFromIssue(workspaceSlug.toString(), issue.project_id, issue.id, modulesToRemove);
|
||||
|
||||
captureIssueEvent({
|
||||
eventName: "Issue updated",
|
||||
payload: {
|
||||
...issue,
|
||||
module_ids: moduleIds,
|
||||
element: "Spreadsheet layout",
|
||||
},
|
||||
updates: { changed_property: "module_ids", change_details: { module_ids: moduleIds } },
|
||||
path: router.asPath,
|
||||
});
|
||||
},
|
||||
[workspaceSlug, issue, addModulesToIssue, removeModulesFromIssue, captureIssueEvent, router.asPath]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<ModuleDropdown
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.module_ids ?? []}
|
||||
onChange={handleModule}
|
||||
disabled={disabled}
|
||||
placeholder="Select modules"
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonContainerClassName="w-full relative flex items-center p-2"
|
||||
buttonClassName="relative border-[0.5px] border-custom-border-400 h-4.5"
|
||||
onClose={onClose}
|
||||
multiple
|
||||
showCount={true}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -242,7 +242,10 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
>
|
||||
<div className="w-full overflow-hidden">
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
|
||||
<div className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100" tabIndex={-1}>
|
||||
<div
|
||||
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{issueDetail.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -251,15 +254,15 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
</td>
|
||||
{/* Rest of the columns */}
|
||||
{SPREADSHEET_PROPERTY_LIST.map((property) => (
|
||||
<IssueColumn
|
||||
displayProperties={displayProperties}
|
||||
issueDetail={issueDetail}
|
||||
disableUserActions={disableUserActions}
|
||||
property={property}
|
||||
handleIssues={handleIssues}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
/>
|
||||
))}
|
||||
<IssueColumn
|
||||
displayProperties={displayProperties}
|
||||
issueDetail={issueDetail}
|
||||
disableUserActions={disableUserActions}
|
||||
property={property}
|
||||
handleIssues={handleIssues}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -207,7 +207,7 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 relative flex items-center gap-3">
|
||||
<Tooltip tooltipContent={`${moduleDetails.member_ids.length} Members`}>
|
||||
<Tooltip tooltipContent={`${moduleDetails?.member_ids?.length || 0} Members`}>
|
||||
<div className="flex w-10 cursor-default items-center justify-center gap-1">
|
||||
{moduleDetails.member_ids.length > 0 ? (
|
||||
<AvatarGroup showTooltip={false}>
|
||||
|
@ -116,6 +116,8 @@ export const ISSUE_DISPLAY_PROPERTIES: {
|
||||
{ key: "attachment_count", title: "Attachment Count" },
|
||||
{ key: "link", title: "Link" },
|
||||
{ key: "estimate", title: "Estimate" },
|
||||
{ key: "modules", title: "Modules" },
|
||||
{ key: "cycle", title: "Cycle" },
|
||||
];
|
||||
|
||||
export const ISSUE_EXTRA_OPTIONS: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { IIssueDisplayProperties, TIssue, TIssueOrderByOptions } from "@plane/types";
|
||||
import { LayersIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
||||
import { LayersIcon, DoubleCircleIcon, UserGroupIcon, DiceIcon, ContrastIcon } from "@plane/ui";
|
||||
import { CalendarDays, Link2, Signal, Tag, Triangle, Paperclip, CalendarClock, CalendarCheck } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import { ISvgIcons } from "@plane/ui/src/icons/type";
|
||||
@ -10,6 +10,8 @@ import {
|
||||
SpreadsheetDueDateColumn,
|
||||
SpreadsheetEstimateColumn,
|
||||
SpreadsheetLabelColumn,
|
||||
SpreadsheetModuleColumn,
|
||||
SpreadsheetCycleColumn,
|
||||
SpreadsheetLinkColumn,
|
||||
SpreadsheetPriorityColumn,
|
||||
SpreadsheetStartDateColumn,
|
||||
@ -79,6 +81,24 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
icon: Tag,
|
||||
Column: SpreadsheetLabelColumn,
|
||||
},
|
||||
modules: {
|
||||
title: "Modules",
|
||||
ascendingOrderKey: "modules__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-modules__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: DiceIcon,
|
||||
Column: SpreadsheetModuleColumn,
|
||||
},
|
||||
cycle: {
|
||||
title: "Cycle",
|
||||
ascendingOrderKey: "cycle__name",
|
||||
ascendingOrderTitle: "A",
|
||||
descendingOrderKey: "-cycle__name",
|
||||
descendingOrderTitle: "Z",
|
||||
icon: ContrastIcon,
|
||||
Column: SpreadsheetCycleColumn,
|
||||
},
|
||||
priority: {
|
||||
title: "Priority",
|
||||
ascendingOrderKey: "priority",
|
||||
@ -149,6 +169,8 @@ export const SPREADSHEET_PROPERTY_LIST: (keyof IIssueDisplayProperties)[] = [
|
||||
"priority",
|
||||
"assignee",
|
||||
"labels",
|
||||
"modules",
|
||||
"cycle",
|
||||
"start_date",
|
||||
"due_date",
|
||||
"estimate",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import useSWR from "swr";
|
||||
import { useEstimate, useLabel, useProjectState } from "./store";
|
||||
import { useCycle, useEstimate, useLabel, useModule, useProjectState } from "./store";
|
||||
|
||||
export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => {
|
||||
const { fetchWorkspaceLabels } = useLabel();
|
||||
@ -8,6 +8,22 @@ export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | u
|
||||
|
||||
const { fetchWorkspaceEstimates } = useEstimate();
|
||||
|
||||
const { fetchWorkspaceModules } = useModule();
|
||||
|
||||
const { fetchWorkspaceCycles } = useCycle();
|
||||
|
||||
// fetch workspace Modules
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_MODULES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceModules(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
// fetch workspace Cycles
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_CYCLES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null
|
||||
);
|
||||
|
||||
// fetch workspace labels
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// services
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { CycleDateCheckData, ICycle, TIssue, TIssueMap } from "@plane/types";
|
||||
import type { CycleDateCheckData, ICycle, TIssue } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
@ -10,6 +10,14 @@ export class CycleService extends APIService {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getWorkspaceCycles(workspaceSlug: string): Promise<ICycle[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/cycles/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async createCycle(workspaceSlug: string, projectId: string, data: any): Promise<ICycle> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/`, data)
|
||||
.then((response) => response?.data)
|
||||
|
@ -9,6 +9,14 @@ export class ModuleService extends APIService {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getWorkspaceModules(workspaceSlug: string): Promise<IModule[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/modules/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getModules(workspaceSlug: string, projectId: string): Promise<IModule[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`)
|
||||
.then((response) => response?.data)
|
||||
|
@ -33,6 +33,7 @@ export interface ICycleStore {
|
||||
// actions
|
||||
validateDate: (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) => Promise<any>;
|
||||
// fetch
|
||||
fetchWorkspaceCycles: (workspaceSlug: string) => Promise<ICycle[]>;
|
||||
fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
||||
fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise<undefined | ICycle[]>;
|
||||
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise<ICycle>;
|
||||
@ -79,6 +80,7 @@ export class CycleStore implements ICycleStore {
|
||||
currentProjectDraftCycleIds: computed,
|
||||
currentProjectActiveCycleId: computed,
|
||||
// actions
|
||||
fetchWorkspaceCycles: action,
|
||||
fetchAllCycles: action,
|
||||
fetchActiveCycle: action,
|
||||
fetchCycleDetails: action,
|
||||
@ -218,6 +220,22 @@ export class CycleStore implements ICycleStore {
|
||||
validateDate = async (workspaceSlug: string, projectId: string, payload: CycleDateCheckData) =>
|
||||
await this.cycleService.cycleDateCheck(workspaceSlug, projectId, payload);
|
||||
|
||||
/**
|
||||
* @description fetch all cycles
|
||||
* @param workspaceSlug
|
||||
* @returns ICycle[]
|
||||
*/
|
||||
fetchWorkspaceCycles = async (workspaceSlug: string) =>
|
||||
await this.cycleService.getWorkspaceCycles(workspaceSlug).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((cycle) => {
|
||||
set(this.cycleMap, [cycle.id], { ...this.cycleMap[cycle.id], ...cycle });
|
||||
set(this.fetchedMap, cycle.project_id, true);
|
||||
});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description fetches all cycles for a project
|
||||
* @param workspaceSlug
|
||||
@ -337,7 +355,6 @@ export class CycleStore implements ICycleStore {
|
||||
*/
|
||||
addCycleToFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||
const currentCycle = this.getCycleById(cycleId);
|
||||
const currentActiveCycle = this.getActiveCycleById(cycleId);
|
||||
try {
|
||||
runInAction(() => {
|
||||
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], true);
|
||||
@ -362,7 +379,6 @@ export class CycleStore implements ICycleStore {
|
||||
*/
|
||||
removeCycleFromFavorites = async (workspaceSlug: string, projectId: string, cycleId: string) => {
|
||||
const currentCycle = this.getCycleById(cycleId);
|
||||
const currentActiveCycle = this.getActiveCycleById(cycleId);
|
||||
try {
|
||||
runInAction(() => {
|
||||
if (currentCycle) set(this.cycleMap, [cycleId, "is_favorite"], false);
|
||||
|
@ -191,6 +191,8 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
||||
key: displayProperties?.key ?? true,
|
||||
created_on: displayProperties?.created_on ?? true,
|
||||
updated_on: displayProperties?.updated_on ?? true,
|
||||
modules: displayProperties?.modules ?? true,
|
||||
cycle: displayProperties?.cycle ?? true,
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -170,7 +170,7 @@ export class IssueHelperStore implements TIssueHelperStore {
|
||||
* @returns string | string[] of sortable fields to be used for sorting
|
||||
*/
|
||||
populateIssueDataForSorting(
|
||||
dataType: "state_id" | "label_ids" | "assignee_ids",
|
||||
dataType: "state_id" | "label_ids" | "assignee_ids" | "module_ids" | "cycle_id",
|
||||
dataIds: string | string[] | null | undefined,
|
||||
order?: "asc" | "desc"
|
||||
) {
|
||||
@ -205,6 +205,22 @@ export class IssueHelperStore implements TIssueHelperStore {
|
||||
if (member && member.first_name) dataValues.push(member.first_name.toLocaleLowerCase());
|
||||
}
|
||||
break;
|
||||
case "module_ids":
|
||||
const moduleMap = this.rootStore?.moduleMap;
|
||||
if (!moduleMap) break;
|
||||
for (const dataId of dataIdsArray) {
|
||||
const _module = moduleMap[dataId];
|
||||
if (_module && _module.name) dataValues.push(_module.name.toLocaleLowerCase());
|
||||
}
|
||||
break;
|
||||
case "cycle_id":
|
||||
const cycleMap = this.rootStore?.cycleMap;
|
||||
if (!cycleMap) break;
|
||||
for (const dataId of dataIdsArray) {
|
||||
const cycle = cycleMap[dataId];
|
||||
if (cycle && cycle.name) dataValues.push(cycle.name.toLocaleLowerCase());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return isDataIdsArray ? (order ? orderBy(dataValues, undefined, [order]) : dataValues) : dataValues[0];
|
||||
@ -313,6 +329,36 @@ export class IssueHelperStore implements TIssueHelperStore {
|
||||
["asc", "desc"]
|
||||
);
|
||||
|
||||
case "modules__name":
|
||||
return orderBy(array, [
|
||||
this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "asc"),
|
||||
]);
|
||||
case "-modules__name":
|
||||
return orderBy(
|
||||
array,
|
||||
[
|
||||
this.getSortOrderToFilterEmptyValues.bind(null, "module_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("module_ids", issue["module_ids"], "desc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
);
|
||||
|
||||
case "cycle__name":
|
||||
return orderBy(array, [
|
||||
this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "asc"),
|
||||
]);
|
||||
case "-cycle__name":
|
||||
return orderBy(
|
||||
array,
|
||||
[
|
||||
this.getSortOrderToFilterEmptyValues.bind(null, "cycle_id"), //preferring sorting based on empty values to always keep the empty values below
|
||||
(issue) => this.populateIssueDataForSorting("cycle_id", issue["cycle_id"], "desc"),
|
||||
],
|
||||
["asc", "desc"]
|
||||
);
|
||||
|
||||
case "assignees__first_name":
|
||||
return orderBy(array, [
|
||||
this.getSortOrderToFilterEmptyValues.bind(null, "assignee_ids"), //preferring sorting based on empty values to always keep the empty values below
|
||||
|
@ -4,7 +4,7 @@ import isEmpty from "lodash/isEmpty";
|
||||
import { RootStore } from "../root.store";
|
||||
import { IStateStore, StateStore } from "../state.store";
|
||||
// issues data store
|
||||
import { IIssueLabel, IProject, IState, IUserLite } from "@plane/types";
|
||||
import { ICycle, IIssueLabel, IModule, IProject, IState, IUserLite } from "@plane/types";
|
||||
import { IIssueStore, IssueStore } from "./issue.store";
|
||||
import { IIssueDetail, IssueDetail } from "./issue-details/root.store";
|
||||
import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace";
|
||||
@ -39,6 +39,8 @@ export interface IIssueRootStore {
|
||||
workSpaceMemberRolesMap: Record<string, IWorkspaceMembership> | undefined;
|
||||
memberMap: Record<string, IUserLite> | undefined;
|
||||
projectMap: Record<string, IProject> | undefined;
|
||||
moduleMap: Record<string, IModule> | undefined;
|
||||
cycleMap: Record<string, ICycle> | undefined;
|
||||
|
||||
rootStore: RootStore;
|
||||
|
||||
@ -91,6 +93,8 @@ export class IssueRootStore implements IIssueRootStore {
|
||||
workSpaceMemberRolesMap: Record<string, IWorkspaceMembership> | undefined = undefined;
|
||||
memberMap: Record<string, IUserLite> | undefined = undefined;
|
||||
projectMap: Record<string, IProject> | undefined = undefined;
|
||||
moduleMap: Record<string, IModule> | undefined = undefined;
|
||||
cycleMap: Record<string, ICycle> | undefined = undefined;
|
||||
|
||||
rootStore: RootStore;
|
||||
|
||||
@ -142,6 +146,8 @@ export class IssueRootStore implements IIssueRootStore {
|
||||
memberMap: observable,
|
||||
workSpaceMemberRolesMap: observable,
|
||||
projectMap: observable,
|
||||
moduleMap: observable,
|
||||
cycleMap: observable,
|
||||
});
|
||||
|
||||
this.rootStore = rootStore;
|
||||
@ -163,6 +169,8 @@ export class IssueRootStore implements IIssueRootStore {
|
||||
if (!isEmpty(rootStore?.memberRoot?.memberMap)) this.memberMap = rootStore?.memberRoot?.memberMap || undefined;
|
||||
if (!isEmpty(rootStore?.projectRoot?.project?.projectMap))
|
||||
this.projectMap = rootStore?.projectRoot?.project?.projectMap;
|
||||
if (!isEmpty(rootStore?.module?.moduleMap)) this.moduleMap = rootStore?.module?.moduleMap;
|
||||
if (!isEmpty(rootStore?.cycle?.cycleMap)) this.cycleMap = rootStore?.cycle?.cycleMap;
|
||||
});
|
||||
|
||||
this.issues = new IssueStore();
|
||||
|
@ -22,6 +22,7 @@ export interface IModuleStore {
|
||||
getProjectModuleIds: (projectId: string) => string[] | null;
|
||||
// actions
|
||||
// fetch
|
||||
fetchWorkspaceModules: (workspaceSlug: string) => Promise<IModule[]>;
|
||||
fetchModules: (workspaceSlug: string, projectId: string) => Promise<undefined | IModule[]>;
|
||||
fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<IModule>;
|
||||
// crud
|
||||
@ -73,6 +74,7 @@ export class ModulesStore implements IModuleStore {
|
||||
// computed
|
||||
projectModuleIds: computed,
|
||||
// actions
|
||||
fetchWorkspaceModules: action,
|
||||
fetchModules: action,
|
||||
fetchModuleDetails: action,
|
||||
createModule: action,
|
||||
@ -125,6 +127,21 @@ export class ModulesStore implements IModuleStore {
|
||||
return projectModuleIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description fetch all modules
|
||||
* @param workspaceSlug
|
||||
* @returns IModule[]
|
||||
*/
|
||||
fetchWorkspaceModules = async (workspaceSlug: string) =>
|
||||
await this.moduleService.getWorkspaceModules(workspaceSlug).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((module) => {
|
||||
set(this.moduleMap, [module.id], { ...this.moduleMap[module.id], ...module });
|
||||
});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description fetch all modules
|
||||
* @param workspaceSlug
|
||||
|
Loading…
Reference in New Issue
Block a user