Compare commits

...
Author SHA1 Message Date
NarayanBavisetti 6be2b3b8c5 Merge branch 'chore/project-entities' of github.com:makeplane/plane into chore/project-entities 2024-01-22 14:44:09 +05:30
NarayanBavisetti d3fe57664a chore: state and label serializer change 2024-01-22 14:43:47 +05:30
Rahul R 64fcb532fc Merge branch 'chore/project-entities' of github.com:makeplane/plane into chore/project-entities 2024-01-22 14:12:49 +05:30
Rahul R f7586ba046 fix state and label types 2024-01-22 14:11:03 +05:30
NarayanBavisetti 8288e1363c chore: project and workspace state change 2024-01-22 13:48:54 +05:30
Rahul R 2bb0a89679 Merge branch 'chore/project-entities' of github.com:makeplane/plane into chore/project-entities 2024-01-22 13:30:35 +05:30
NarayanBavisetti 4761d4e0e9 fix: state response change 2024-01-22 13:29:31 +05:30
Rahul R ef5a09deba change store computed actions to computed functions from mobx-utils 2024-01-22 13:09:31 +05:30
Rahul R 75965fc86e fix label type 2024-01-22 12:47:05 +05:30
NarayanBavisetti 7e95b8f160 fix: project labels response change 2024-01-22 12:24:50 +05:30
Rahul R 78c7a016e3 refactor issue dropdown logic to help work properly with issues on global level 2024-01-22 11:54:04 +05:30
Rahul R 66de428987 Merge branch 'develop' of github.com:makeplane/plane into chore/project-entities 2024-01-19 15:38:53 +05:30
e975abff21 Improvement: High Performance MobX Integration for Pages ✈︎ (#3397)
* fix: removed parameters `workspace`, `project` & `id` from the patch calls

* feat: modified components to work with new pages hooks

* feat: modified stores

* feat: modified initial component

* feat: component implementation changes

* feat: store implementation

* refactor pages store

* feat: updated page store to perform async operations faster

* fix: added types for archive and restore pages

* feat: implemented archive and restore pages

* fix: page creating twice when form submit

* feat: updated create-page-modal

* feat: updated page form and delete page modal

* fix: create page modal not updating isSubmitted prop

* feat: list items and list view refactored for pages

* feat: refactored project-page-store for inserting computed pagesids

* chore: renamed project pages hook

* feat: added favourite pages implementation

* fix: implemented store for archived pages

* fix: project page store for recent pages

* fix: issue suggestions breaking pages

* fix: issue embeds and suggestions breaking

* feat: implemented page store and project page store in page editor

* chore: lock file changes

* fix: modified page details header to catch mobx updates instead of swr calls

* fix: modified usePage hook to fetch page details when reloaded directly on page

* fix: fixed deleting pages

* fix: removed render on props changed

* feat: implemented page store inside page details

* fix: role change in pages archives

* fix: rerending of pages on tab change

* fix: reimplementation of peek overview inside pages

* chore: typo fixes

* fix: issue suggestion widget selecting wrong issues on click

* feat: added labels in pages

* fix: deepsource errors fixed

* fix: build errors

* fix: review comments

* fix: removed swr hooks from the `usePage` store hook and refactored `issueEmbed` hook

* fix: resolved reviewed comments

---------

Co-authored-by: Rahul R <[email protected]>
2024-01-19 15:18:47 +05:30
Bavisetti NarayanandGitHub f68e6023c3 fix: user profile issues (#3409) 2024-01-19 15:17:49 +05:30
67414983da fix: update global issues filter and enable profile issues (#3410)
* fix all issues and fix profile issues

* minor comments update

* minor change nullish check logic

* update nullish check logic

---------

Co-authored-by: Rahul R <[email protected]>
2024-01-19 15:06:19 +05:30
pablohashescobar ba27cc57bd dev: workspace states and estimates 2024-01-19 13:51:12 +05:30
Anmol Singh BhatiaandGitHub 37ddc64b83 chore: issue filter loader improvement (#3406) 2024-01-18 17:26:13 +05:30
125 changed files with 1536 additions and 1637 deletions
@@ -104,6 +104,7 @@ from .estimate import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
WorkspaceEstimateSerializer,
)
from .inbox import (
@@ -61,3 +61,18 @@ class EstimateReadSerializer(BaseSerializer):
"name",
"description",
]
class WorkspaceEstimateSerializer(BaseSerializer):
points = EstimatePointSerializer(read_only=True, many=True)
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = [
"points",
"name",
"description",
]
+21 -10
View File
@@ -259,14 +259,17 @@ class IssuePropertySerializer(BaseSerializer):
class LabelSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
class Meta:
model = Label
fields = "__all__"
fields = [
"parent",
"name",
"color",
"id",
"project_id",
"workspace_id",
"sort_order",
]
read_only_fields = [
"workspace",
"project",
@@ -295,8 +298,12 @@ class IssueLabelSerializer(BaseSerializer):
class IssueRelationSerializer(BaseSerializer):
id = serializers.UUIDField(source="related_issue.id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(source="related_issue.project_id", read_only=True)
sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(
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)
class Meta:
@@ -315,8 +322,12 @@ class IssueRelationSerializer(BaseSerializer):
class RelatedIssueSerializer(BaseSerializer):
id = serializers.UUIDField(source="issue.id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField(
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)
class Meta:
+11 -1
View File
@@ -8,7 +8,17 @@ from plane.db.models import State
class StateSerializer(BaseSerializer):
class Meta:
model = State
fields = "__all__"
fields = [
"id",
"project_id",
"workspace_id",
"name",
"color",
"group",
"default",
"description",
"sequence",
]
read_only_fields = [
"workspace",
"project",
+12
View File
@@ -20,6 +20,8 @@ from plane.app.views import (
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
)
@@ -207,4 +209,14 @@ urlpatterns = [
WorkspaceUserPropertiesEndpoint.as_view(),
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",
),
]
+2
View File
@@ -47,6 +47,8 @@ from .workspace import (
WorkspaceLabelsEndpoint,
WorkspaceProjectMemberEndpoint,
WorkspaceUserPropertiesEndpoint,
WorkspaceStatesEndpoint,
WorkspaceEstimatesEndpoint,
)
from .state import StateViewSet
from .view import (
+5 -3
View File
@@ -83,6 +83,7 @@ from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
from collections import defaultdict
class IssueViewSet(WebhookMixin, BaseViewSet):
def get_serializer_class(self):
return (
@@ -821,7 +822,9 @@ class SubIssuesEndpoint(BaseAPIView):
_ = 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
_ = [
@@ -836,7 +839,7 @@ class SubIssuesEndpoint(BaseAPIView):
)
for sub_issue_id in sub_issue_ids
]
# create's a dict with state group name with their respective issue id's
result = defaultdict(list)
for sub_issue in updated_sub_issues:
@@ -853,7 +856,6 @@ class SubIssuesEndpoint(BaseAPIView):
},
status=status.HTTP_200_OK,
)
class IssueLinkViewSet(BaseViewSet):
+10 -21
View File
@@ -1,5 +1,5 @@
# Python imports
from datetime import timedelta, date, datetime
from datetime import date, datetime, timedelta
# Django imports
from django.db import connection
@@ -7,30 +7,19 @@ from django.db.models import Exists, OuterRef, Q
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.app.permissions import ProjectEntityPermission
from plane.db.models import (
Page,
PageFavorite,
Issue,
IssueAssignee,
IssueActivity,
PageLog,
ProjectMember,
)
from plane.app.serializers import (
PageSerializer,
PageFavoriteSerializer,
PageLogSerializer,
IssueLiteSerializer,
SubPageSerializer,
)
from plane.app.serializers import (IssueLiteSerializer, PageFavoriteSerializer,
PageLogSerializer, PageSerializer,
SubPageSerializer)
from plane.db.models import (Issue, IssueActivity, IssueAssignee, Page,
PageFavorite, PageLog, ProjectMember)
# Module imports
from .base import BaseAPIView, BaseViewSet
def unarchive_archive_page_and_descendants(page_id, archived_at):
@@ -175,7 +164,7 @@ class PageViewSet(BaseViewSet):
project_id=project_id,
member=request.user,
is_active=True,
role__gt=20,
role__gte=20,
).exists()
or request.user.id != page.owned_by_id
):
+5 -5
View File
@@ -9,9 +9,12 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from . import BaseViewSet
from . import BaseViewSet, BaseAPIView
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
@@ -22,9 +25,6 @@ class StateViewSet(BaseViewSet):
ProjectEntityPermission,
]
def perform_create(self, serializer):
serializer.save(project_id=self.kwargs.get("project_id"))
def get_queryset(self):
return self.filter_queryset(
super()
+59 -21
View File
@@ -41,15 +41,19 @@ from plane.app.serializers import (
ProjectMemberSerializer,
WorkspaceThemeSerializer,
IssueActivitySerializer,
IssueLiteSerializer,
IssueSerializer,
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
ProjectMemberRoleSerializer,
WorkspaceUserPropertiesSerializer,
WorkspaceEstimateSerializer,
StateSerializer,
LabelSerializer,
)
from plane.app.views.base import BaseAPIView
from . import BaseViewSet
from plane.db.models import (
State,
User,
Workspace,
WorkspaceMemberInvite,
@@ -67,6 +71,8 @@ from plane.db.models import (
CycleIssue,
IssueReaction,
WorkspaceUserProperties,
Estimate,
EstimatePoint,
)
from plane.app.permissions import (
WorkSpaceBasePermission,
@@ -1339,23 +1345,10 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
project__project_projectmember__member=request.user,
)
.filter(**filters)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.select_related("project", "workspace", "state", "parent")
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.order_by("-created_at")
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
@@ -1370,6 +1363,15 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.order_by("created_at")
).distinct()
# Priority Ordering
@@ -1432,7 +1434,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(
issues = IssueSerializer(
issue_queryset, many=True, fields=fields if fields else None
).data
return Response(issues, status=status.HTTP_200_OK)
@@ -1447,10 +1449,46 @@ class WorkspaceLabelsEndpoint(BaseAPIView):
labels = Label.objects.filter(
workspace__slug=slug,
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):
+11 -13
View File
@@ -7,19 +7,17 @@ from . import ProjectBaseModel
def get_default_filters():
return (
{
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
)
return {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
}
def get_default_display_filters():
@@ -32,6 +32,7 @@
"@plane/editor-core": "*",
"@plane/editor-extensions": "*",
"@plane/ui": "*",
"@tippyjs/react": "^4.2.6",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-placeholder": "^2.1.13",
"@tiptap/pm": "^2.1.13",
@@ -18,7 +18,7 @@ import {
type IPageRenderer = {
documentDetails: DocumentDetails;
updatePageTitle: (title: string) => Promise<void>;
updatePageTitle: (title: string) => void;
editor: Editor;
onActionCompleteHandler: (action: {
title: string;
@@ -30,18 +30,6 @@ type IPageRenderer = {
readonly: boolean;
};
const debounce = (func: (...args: any[]) => void, wait: number) => {
let timeout: NodeJS.Timeout | null = null;
return function executedFunction(...args: any[]) {
const later = () => {
if (timeout) clearTimeout(timeout);
func(...args);
};
if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
export const PageRenderer = (props: IPageRenderer) => {
const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
@@ -64,11 +52,9 @@ export const PageRenderer = (props: IPageRenderer) => {
const { getFloatingProps } = useInteractions([dismiss]);
const debouncedUpdatePageTitle = debounce(updatePageTitle, 300);
const handlePageTitleChange = (title: string) => {
setPagetitle(title);
debouncedUpdatePageTitle(title);
updatePageTitle(title);
};
const [cleanup, setcleanup] = useState(() => () => {});
@@ -26,7 +26,7 @@ export const DocumentEditorExtensions = (
.focus()
.insertContentAt(
range,
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>"
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>\n"
)
.run();
},
@@ -24,7 +24,7 @@ export const IssueSuggestions = (suggestions: any[]) => {
title: suggestion.name,
priority: suggestion.priority.toString(),
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
state: suggestion.state_detail.name,
state: suggestion.state_detail && suggestion.state_detail.name ? suggestion.state_detail.name : "Todo",
command: ({ editor, range }) => {
editor
.chain()
@@ -9,6 +9,8 @@ export const IssueEmbedSuggestions = Extension.create({
addOptions() {
return {
suggestion: {
char: "#issue_",
allowSpaces: true,
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
props.command({ editor, range });
},
@@ -18,11 +20,8 @@ export const IssueEmbedSuggestions = Extension.create({
addProseMirrorPlugins() {
return [
Suggestion({
char: "#issue_",
pluginKey: new PluginKey("issue-embed-suggestions"),
editor: this.editor,
allowSpaces: true,
...this.options.suggestion,
}),
];
@@ -53,7 +53,7 @@ const IssueSuggestionList = ({
const commandListContainer = useRef<HTMLDivElement>(null);
useEffect(() => {
let newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
const newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
let totalLength = 0;
sections.forEach((section) => {
newDisplayedItems[section] = items.filter((item) => item.state === section).slice(0, 5);
@@ -65,8 +65,8 @@ const IssueSuggestionList = ({
}, [items]);
const selectItem = useCallback(
(index: number) => {
const item = displayedItems[currentSection][index];
(section: string, index: number) => {
const item = displayedItems[section][index];
if (item) {
command(item);
}
@@ -87,6 +87,7 @@ const IssueSuggestionList = ({
setSelectedIndex(
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
);
e.stopPropagation();
return true;
}
if (e.key === "ArrowDown") {
@@ -101,10 +102,12 @@ const IssueSuggestionList = ({
[currentSection]: [...prevItems[currentSection], ...nextItems],
}));
}
e.stopPropagation();
return true;
}
if (e.key === "Enter") {
selectItem(selectedIndex);
selectItem(currentSection, selectedIndex);
e.stopPropagation();
return true;
}
if (e.key === "Tab") {
@@ -112,6 +115,7 @@ const IssueSuggestionList = ({
const nextSectionIndex = (currentSectionIndex + 1) % sections.length;
setCurrentSection(sections[nextSectionIndex]);
setSelectedIndex(0);
e.stopPropagation();
return true;
}
return false;
@@ -172,7 +176,7 @@ const IssueSuggestionList = ({
}
)}
key={item.identifier}
onClick={() => selectItem(index)}
onClick={() => selectItem(section, index)}
>
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
<PriorityIcon priority={item.priority} />
@@ -195,7 +199,7 @@ export const IssueListRenderer = () => {
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
component = new ReactRenderer(IssueSuggestionList, {
props,
// @ts-ignore
@@ -210,10 +214,10 @@ export const IssueListRenderer = () => {
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "right",
placement: "bottom-start",
});
},
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
component?.updateProps(props);
popup &&
@@ -15,8 +15,7 @@ export const IssueWidgetCard = (props) => {
setIssueDetails(issue);
setLoading(0);
})
.catch((error) => {
console.log(error);
.catch(() => {
setLoading(-1);
});
}, []);
@@ -30,7 +29,9 @@ export const IssueWidgetCard = (props) => {
{loading == 0 ? (
<div
onClick={completeIssueEmbedAction}
className="w-full cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs"
className={`${
props.selected ? "border-custom-primary-200 border-[2px]" : ""
} w-full cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs`}
>
<h5 className="text-xs text-custom-text-300">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
@@ -16,7 +16,7 @@ interface IDocumentEditor {
// document info
documentDetails: DocumentDetails;
value: string;
rerenderOnPropsChange: {
rerenderOnPropsChange?: {
id: string;
description_html: string;
};
@@ -39,7 +39,7 @@ interface IDocumentEditor {
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any;
updatePageTitle: (title: string) => Promise<void>;
updatePageTitle: (title: string) => void;
debouncedUpdatesEnabled?: boolean;
isSubmitting: "submitting" | "submitted" | "saved";
+2 -9
View File
@@ -109,17 +109,10 @@ export type IssuePriorities = {
export interface IIssueLabel {
id: string;
created_at: Date;
updated_at: Date;
name: string;
description: string;
color: string;
created_by: string;
updated_by: string;
project: string;
project_detail: IProjectLite;
workspace: string;
workspace_detail: IWorkspaceLite;
project_id: string;
workspace__slug: string;
parent: string | null;
sort_order: number;
}
+2 -9
View File
@@ -5,20 +5,13 @@ export type TStateGroups = "backlog" | "unstarted" | "started" | "completed" | "
export interface IState {
readonly id: string;
color: string;
readonly created_at: Date;
readonly created_by: string;
default: boolean;
description: string;
group: TStateGroups;
name: string;
project: string;
readonly project_detail: IProjectLite;
project_id: string;
sequence: number;
readonly slug: string;
readonly updated_at: Date;
readonly updated_by: string;
workspace: string;
workspace_detail: IWorkspaceLite;
workspace__slug: string;
}
export interface IStateLite {
+1 -3
View File
@@ -72,9 +72,7 @@ const UserLink = ({ activity }: { activity: IIssueActivity }) => {
const LabelPill = observer(({ labelId, workspaceSlug }: { labelId: string; workspaceSlug: string }) => {
// store hooks
const {
workspace: { workspaceLabels, fetchWorkspaceLabels },
} = useLabel();
const { workspaceLabels, fetchWorkspaceLabels } = useLabel();
useEffect(() => {
if (!workspaceLabels) fetchWorkspaceLabels(workspaceSlug);
+3 -8
View File
@@ -158,17 +158,12 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
const filteredOptions =
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
// fetch cycles of the project if not already present in the store
useEffect(() => {
if (!workspaceSlug) return;
if (!activeEstimate) fetchProjectEstimates(workspaceSlug, projectId);
}, [activeEstimate, fetchProjectEstimates, projectId, workspaceSlug]);
const selectedEstimate = value !== null ? getEstimatePointValue(value) : null;
const selectedEstimate = value !== null ? getEstimatePointValue(value, projectId) : null;
const openDropdown = () => {
setIsOpen(true);
if (!activeEstimate && workspaceSlug) fetchProjectEstimates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
@@ -93,14 +93,10 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
};
if (multiple) comboboxProps.multiple = true;
useEffect(() => {
if (!workspaceSlug) return;
if (!projectMemberIds) fetchProjectMembers(workspaceSlug, projectId);
}, [fetchProjectMembers, projectId, projectMemberIds, workspaceSlug]);
const openDropdown = () => {
setIsOpen(true);
if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
+1 -6
View File
@@ -142,17 +142,12 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
const filteredOptions =
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 openDropdown = () => {
setIsOpen(true);
if (!statesList && workspaceSlug) fetchProjectStates(workspaceSlug, projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsOpen(false);
+1 -3
View File
@@ -75,9 +75,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
} = useUser();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
+1 -3
View File
@@ -45,9 +45,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
const {
membership: { currentWorkspaceRole },
} = useUser();
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
const {
workspace: { workspaceMemberIds },
} = useMember();
+1 -3
View File
@@ -77,9 +77,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
membership: { currentProjectRole },
} = useUser();
const { currentProjectDetails } = useProject();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
const {
project: { projectMemberIds },
+2 -13
View File
@@ -1,23 +1,17 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { FileText, Plus } from "lucide-react";
// hooks
import { useApplication, useProject } from "hooks/store";
// services
import { PageService } from "services/page.service";
import { useApplication, usePage, useProject } from "hooks/store";
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helpers
import { renderEmoji } from "helpers/emoji.helper";
// fetch-keys
import { PAGE_DETAILS } from "constants/fetch-keys";
export interface IPagesHeaderProps {
showButton?: boolean;
}
const pageService = new PageService();
export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
const { showButton = false } = props;
@@ -28,12 +22,7 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
const { commandPalette: commandPaletteStore } = useApplication();
const { currentProjectDetails } = useProject();
const { data: pageDetails } = useSWR(
workspaceSlug && currentProjectDetails?.id && pageId ? PAGE_DETAILS(pageId as string) : null,
workspaceSlug && currentProjectDetails?.id
? () => pageService.getPageDetails(workspaceSlug as string, currentProjectDetails.id, pageId as string)
: null
);
const pageDetails = usePage(pageId as string);
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
@@ -25,9 +25,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
} = useIssues(EIssuesStoreType.ARCHIVED);
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
@@ -22,9 +22,7 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
} = useIssues(EIssuesStoreType.DRAFT);
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
+1 -3
View File
@@ -42,9 +42,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
} = useUser();
const { currentProjectDetails } = useProject();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const activeLayout = issueFilters?.displayFilters?.layout;
@@ -49,9 +49,7 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => {
const { currentProjectDetails } = useProject();
const { projectViewIds, getViewById } = useProjectView();
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
@@ -24,9 +24,7 @@ export const IssueLabel: FC<TIssueLabel> = observer((props) => {
const { workspaceSlug, projectId, issueId, disabled = false } = props;
// hooks
const { updateIssue } = useIssueDetail();
const {
project: { createLabel },
} = useLabel();
const { createLabel } = useLabel();
const { setToastAlert } = useToast();
const labelOperations: TLabelOperations = useMemo(
@@ -20,9 +20,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
const {
issue: { getIssueById },
} = useIssueDetail();
const {
project: { fetchProjectLabels, projectLabels },
} = useLabel();
const { fetchProjectLabels, getProjectLabels } = useLabel();
// states
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | 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 issue = getIssueById(issueId);
const projectLabels = getProjectLabels(projectId);
const fetchLabels = () => {
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) => ({
@@ -17,9 +17,7 @@ export const ArchivedIssueAppliedFiltersRoot: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.ARCHIVED);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;
@@ -21,9 +21,7 @@ export const CycleAppliedFiltersRoot: React.FC = observer(() => {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.CYCLE);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;
@@ -16,9 +16,7 @@ export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.DRAFT);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;
@@ -25,9 +25,7 @@ export const GlobalViewsAppliedFiltersRoot = observer((props: Props) => {
const {
issuesFilter: { filters, updateFilters },
} = useIssues(EIssuesStoreType.GLOBAL);
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
const { globalViewMap, updateGlobalView } = useGlobalView();
const {
membership: { currentWorkspaceRole },
@@ -20,9 +20,7 @@ export const ModuleAppliedFiltersRoot: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.MODULE);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;
@@ -7,19 +7,20 @@ import { AppliedFiltersList } from "components/issues";
// types
import { IIssueFilterOptions } from "@plane/types";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties";
export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, userId } = router.query;
//swr hook for fetching issue properties
useWorskspaceIssueProperties(workspaceSlug);
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROFILE);
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
// derived values
const userFilters = issueFilters?.filters;
@@ -19,9 +19,7 @@ export const ProjectAppliedFiltersRoot: React.FC = observer(() => {
projectId: string;
};
// store hooks
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT);
@@ -23,9 +23,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
const { viewMap, updateView } = useProjectView();
// derived values
@@ -71,10 +71,10 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const member = useMember();
const project = useProject();
const projectLabel = useLabel();
const label = useLabel();
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;
@@ -208,17 +208,11 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
const member = useMember();
const project = useProject();
const projectLabel = useLabel();
const label = useLabel();
const projectState = useProjectState();
const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, projectLabel, projectState, member);
const subGroupByList = getGroupByColumns(
sub_group_by as GroupByColumnTypes,
project,
projectLabel,
projectState,
member
);
const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member);
const subGroupByList = getGroupByColumns(sub_group_by as GroupByColumnTypes, project, label, projectState, member);
if (!groupByList || !subGroupByList) return null;
@@ -59,10 +59,10 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
// store hooks
const member = useMember();
const project = useProject();
const projectLabel = useLabel();
const label = useLabel();
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;
@@ -53,13 +53,15 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
const {
router: { workspaceSlug },
} = useApplication();
const {
project: { fetchProjectLabels, projectLabels: storeLabels },
} = useLabel();
const { fetchProjectLabels, getProjectLabels } = useLabel();
const fetchLabels = () => {
setIsLoading(true);
if (workspaceSlug && projectId) fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
const storeLabels = getProjectLabels(projectId);
const openDropDown = () => {
if (!storeLabels && workspaceSlug && projectId) {
setIsLoading(true);
fetchProjectLabels(workspaceSlug, projectId).then(() => setIsLoading(false));
}
};
const { styles, attributes } = usePopper(referenceElement, popperElement, {
@@ -182,7 +184,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
? "cursor-pointer"
: "cursor-pointer hover:bg-custom-background-80"
} ${buttonClassName}`}
onClick={() => !storeLabels && fetchLabels()}
onClick={openDropDown}
>
{label}
{!hideDropdownArrow && !disabled && <ChevronDown className="h-3 w-3" aria-hidden="true" />}
@@ -4,25 +4,25 @@ import { observer } from "mobx-react-lite";
import useSWR from "swr";
// hooks
import { useGlobalView, useIssues, useUser } from "hooks/store";
import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties";
// components
import { GlobalViewsAppliedFiltersRoot } from "components/issues";
import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues";
import { SpreadsheetView } from "components/issues/issue-layouts";
import { AllIssueQuickActions } from "components/issues/issue-layouts/quick-action-dropdowns";
// ui
import { Spinner } from "@plane/ui";
// types
import { TIssue, IIssueDisplayFilterOptions, TStaticViewTypes, TUnGroupedIssues } from "@plane/types";
import { TIssue, IIssueDisplayFilterOptions } from "@plane/types";
import { EIssueActions } from "../types";
import { EUserProjectRoles } from "constants/project";
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
export const AllIssueLayoutRoot: React.FC = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, globalViewId } = router.query;
//swr hook for fetching issue properties
useWorskspaceIssueProperties(workspaceSlug);
// store
const {
issuesFilter: { filters, fetchFilters, updateFilters },
@@ -48,11 +48,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
if (workspaceSlug && globalViewId) {
await fetchAllGlobalViews(workspaceSlug.toString());
await fetchFilters(workspaceSlug.toString(), globalViewId.toString());
await fetchIssues(
workspaceSlug.toString(),
globalViewId.toString(),
groupedIssueIds ? "mutation" : "init-loader"
);
await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader");
}
}
);
@@ -138,6 +134,9 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
)}
</>
)}
{/* peek overview */}
<IssuePeekOverview />
</div>
);
});
@@ -1,17 +1,17 @@
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { ISSUE_PRIORITIES } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper";
import { ILabelRootStore } from "store/label";
import { IMemberRootStore } from "store/member";
import { IProjectStore } from "store/project/project.store";
import { IStateStore } from "store/state.store";
import { GroupByColumnTypes, IGroupByColumn } from "@plane/types";
import { STATE_GROUPS } from "constants/state";
import { ILabelStore } from "store/label.store";
export const getGroupByColumns = (
groupBy: GroupByColumnTypes | null,
project: IProjectStore,
projectLabel: ILabelRootStore,
label: ILabelStore,
projectState: IStateStore,
member: IMemberRootStore,
includeNone?: boolean
@@ -26,7 +26,7 @@ export const getGroupByColumns = (
case "priority":
return getPriorityColumns();
case "labels":
return getLabelsColumns(projectLabel) as any;
return getLabelsColumns(label) as any;
case "assignees":
return getAssigneeColumns(member) as any;
case "created_by":
@@ -97,10 +97,8 @@ const getPriorityColumns = () => {
}));
};
const getLabelsColumns = (projectLabel: ILabelRootStore) => {
const {
project: { projectLabels },
} = projectLabel;
const getLabelsColumns = (label: ILabelStore) => {
const { projectLabels } = label;
if (!projectLabels) return;
+9 -9
View File
@@ -325,7 +325,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
{envConfig?.has_openai_configured && (
<GptAssistantPopover
isOpen={gptAssistantModal}
projectId={projectId}
projectId={watch("project_id")}
handleClose={() => {
setGptAssistantModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
@@ -389,7 +389,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
onChange(stateId);
handleFormChange();
}}
projectId={projectId}
projectId={watch("project_id")}
buttonVariant="border-with-text"
tabIndex={6}
/>
@@ -419,7 +419,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ProjectMemberDropdown
projectId={projectId}
projectId={watch("project_id")}
value={value}
onChange={(assigneeIds) => {
onChange(assigneeIds);
@@ -446,7 +446,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
onChange(labelIds);
handleFormChange();
}}
projectId={projectId}
projectId={watch("project_id")}
tabIndex={9}
/>
</div>
@@ -497,7 +497,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
render={({ field: { value, onChange } }) => (
<div className="h-7">
<CycleDropdown
projectId={projectId}
projectId={watch("project_id")}
onChange={(cycleId) => {
onChange(cycleId);
handleFormChange();
@@ -517,7 +517,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
render={({ field: { value, onChange } }) => (
<div className="h-7">
<ModuleDropdown
projectId={projectId}
projectId={watch("project_id")}
value={value}
onChange={(moduleId) => {
onChange(moduleId);
@@ -530,7 +530,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
)}
/>
)}
{areEstimatesEnabledForProject(projectId) && (
{areEstimatesEnabledForProject(watch("project_id")) && (
<Controller
control={control}
name="estimate_point"
@@ -542,7 +542,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
onChange(estimatePoint);
handleFormChange();
}}
projectId={projectId}
projectId={watch("project_id")}
buttonVariant="border-with-text"
tabIndex={14}
/>
@@ -609,7 +609,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
handleFormChange();
setSelectedParentIssue(issue);
}}
projectId={projectId}
projectId={watch("project_id")}
/>
)}
/>
+1 -8
View File
@@ -23,14 +23,7 @@ export interface IssuesModalProps {
}
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
const {
data,
isOpen,
onClose,
onSubmit,
withDraftIssueWrapper = true,
storeType = EIssuesStoreType.PROJECT,
} = props;
const { data, isOpen, onClose, onSubmit, withDraftIssueWrapper = true, storeType = EIssuesStoreType.PROJECT } = props;
// states
const [changesMade, setChangesMade] = useState<Partial<TIssue> | null>(null);
const [createMore, setCreateMore] = useState(false);
+2 -4
View File
@@ -35,12 +35,9 @@ export type TIssuePeekOperations = {
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const { is_archived = false, onIssueUpdate } = props;
// hooks
const {
project: {},
} = useMember();
const { setToastAlert } = useToast();
const {
membership: { currentProjectRole },
membership: { currentWorkspaceAllProjectsRole },
} = useUser();
const {
issues: { removeIssue: removeArchivedIssue },
@@ -198,6 +195,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
const issue = getIssueById(peekIssue.issueId) || undefined;
const currentProjectRole = currentWorkspaceAllProjectsRole?.[peekIssue?.projectId];
// Check if issue is editable, based on user role
const is_editable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
const isLoading = !issue || loader ? true : false;
+2 -8
View File
@@ -29,9 +29,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
project: { getProjectLabels, fetchProjectLabels },
} = useLabel();
const { getProjectLabels, fetchProjectLabels } = useLabel();
// states
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
@@ -50,13 +48,9 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const filteredOptions =
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 = () => {
setIsDropdownOpen(true);
if (!projectLabels && workspaceSlug && projectId) fetchProjectLabels(workspaceSlug.toString(), projectId);
if (referenceElement) referenceElement.focus();
};
const closeDropdown = () => setIsDropdownOpen(false);
@@ -21,7 +21,7 @@ export const ViewEstimateSelect: React.FC<Props> = observer((props) => {
const { issue, onChange, tooltipPosition = "top", customButton = false, disabled } = props;
const { areEstimatesEnabledForCurrentProject, activeEstimateDetails, getEstimatePointValue } = useEstimate();
const estimateValue = getEstimatePointValue(issue.estimate_point);
const estimateValue = getEstimatePointValue(issue.estimate_point, issue.project_id);
const estimateLabels = (
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue} position={tooltipPosition}>
+1 -3
View File
@@ -34,9 +34,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug } = router.query;
// store hooks
const {
project: { createLabel },
} = useLabel();
const { createLabel } = useLabel();
// form info
const {
formState: { errors, isSubmitting },
@@ -34,9 +34,7 @@ export const CreateUpdateLabelInline = observer(
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { createLabel, updateLabel },
} = useLabel();
const { createLabel, updateLabel } = useLabel();
// toast alert
const { setToastAlert } = useToast();
// form info
+1 -3
View File
@@ -25,9 +25,7 @@ export const DeleteLabelModal: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { deleteLabel },
} = useLabel();
const { deleteLabel } = useLabel();
// states
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// hooks
+1 -3
View File
@@ -25,9 +25,7 @@ export const LabelsListModal: React.FC<Props> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { projectLabels, fetchProjectLabels, updateLabel },
} = useLabel();
const { projectLabels, fetchProjectLabels, updateLabel } = useLabel();
// api call to fetch project details
useSWR(
@@ -28,9 +28,7 @@ export const ProjectSettingLabelItem: React.FC<Props> = (props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { updateLabel },
} = useLabel();
const { updateLabel } = useLabel();
const removeFromGroup = (label: IIssueLabel) => {
if (!workspaceSlug || !projectId) return;
@@ -41,9 +41,7 @@ export const ProjectSettingsLabelList: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const {
project: { projectLabels, updateLabelPosition, projectLabelsTree },
} = useLabel();
const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel();
// portal
const renderDraggable = useDraggableInPortal();
+25 -101
View File
@@ -2,132 +2,56 @@ import React, { FC } from "react";
import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import { useApplication, usePage, useWorkspace } from "hooks/store";
import useToast from "hooks/use-toast";
import { useApplication } from "hooks/store";
// components
import { PageForm } from "./page-form";
// types
import { IPage } from "@plane/types";
import { useProjectPages } from "hooks/store/use-project-page";
import { IPageStore } from "store/page.store";
type Props = {
data?: IPage | null;
// data?: IPage | null;
pageStore?: IPageStore;
handleClose: () => void;
isOpen: boolean;
projectId: string;
};
export const CreateUpdatePageModal: FC<Props> = (props) => {
const { isOpen, handleClose, data, projectId } = props;
const { isOpen, handleClose, projectId, pageStore } = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
const { createPage } = useProjectPages();
// store hooks
const {
eventTracker: { postHogEventTracker },
} = useApplication();
const { currentWorkspace } = useWorkspace();
const { createPage, updatePage } = usePage();
// toast alert
const { setToastAlert } = useToast();
const onClose = () => {
handleClose();
};
const createProjectPage = async (payload: IPage) => {
if (!workspaceSlug) return;
// await createPage(workspaceSlug.toString(), projectId, payload)
// .then((res) => {
// router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res.id}`);
// onClose();
// setToastAlert({
// type: "success",
// title: "Success!",
// message: "Page created successfully.",
// });
// postHogEventTracker(
// "PAGE_CREATED",
// {
// ...res,
// state: "SUCCESS",
// },
// {
// isGrouping: true,
// groupType: "Workspace_metrics",
// groupId: currentWorkspace?.id!,
// }
// );
// })
// .catch((err) => {
// setToastAlert({
// type: "error",
// title: "Error!",
// message: err.detail ?? "Page could not be created. Please try again.",
// });
// postHogEventTracker(
// "PAGE_CREATED",
// {
// state: "FAILED",
// },
// {
// isGrouping: true,
// groupType: "Workspace_metrics",
// groupId: currentWorkspace?.id!,
// }
// );
// });
};
const updateProjectPage = async (payload: IPage) => {
if (!data || !workspaceSlug) return;
// await updatePage(workspaceSlug.toString(), projectId, data.id, payload)
// .then((res) => {
// onClose();
// setToastAlert({
// type: "success",
// title: "Success!",
// message: "Page updated successfully.",
// });
// postHogEventTracker(
// "PAGE_UPDATED",
// {
// ...res,
// state: "SUCCESS",
// },
// {
// isGrouping: true,
// groupType: "Workspace_metrics",
// groupId: currentWorkspace?.id!,
// }
// );
// })
// .catch((err) => {
// setToastAlert({
// type: "error",
// title: "Error!",
// message: err.detail ?? "Page could not be updated. Please try again.",
// });
// postHogEventTracker(
// "PAGE_UPDATED",
// {
// state: "FAILED",
// },
// {
// isGrouping: true,
// groupType: "Workspace_metrics",
// groupId: currentWorkspace?.id!,
// }
// );
// });
await createPage(workspaceSlug.toString(), projectId, payload);
};
const handleFormSubmit = async (formData: IPage) => {
if (!workspaceSlug || !projectId) return;
if (!data) await createProjectPage(formData);
else await updateProjectPage(formData);
try {
if (pageStore) {
if (pageStore.name !== formData.name) {
await pageStore.updateName(formData.name);
}
if (pageStore.access !== formData.access) {
formData.access === 1 ? await pageStore.makePrivate() : await pageStore.makePublic();
}
} else {
await createProjectPage(formData);
}
handleClose();
} catch (error) {
console.log(error);
}
};
return (
@@ -157,7 +81,7 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-2xl">
<PageForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} data={data} />
<PageForm handleFormSubmit={handleFormSubmit} handleClose={handleClose} pageStore={pageStore} />
</Dialog.Panel>
</Transition.Child>
</div>
+16 -8
View File
@@ -9,37 +9,45 @@ import useToast from "hooks/use-toast";
// ui
import { Button } from "@plane/ui";
// types
import type { IPage } from "@plane/types";
import { useProjectPages } from "hooks/store/use-project-page";
type TConfirmPageDeletionProps = {
data?: IPage | null;
pageId: string;
isOpen: boolean;
onClose: () => void;
};
export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((props) => {
const { data, isOpen, onClose } = props;
const { pageId, isOpen, onClose } = props;
// states
const [isDeleting, setIsDeleting] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { deletePage } = usePage();
const { deletePage } = useProjectPages();
const pageStore = usePage(pageId);
// toast alert
const { setToastAlert } = useToast();
if (!pageStore) return null;
const { name } = pageStore;
const handleClose = () => {
setIsDeleting(false);
onClose();
};
const handleDelete = async () => {
if (!data || !workspaceSlug || !projectId) return;
if (!pageId || !workspaceSlug || !projectId) return;
setIsDeleting(true);
await deletePage(workspaceSlug.toString(), data.project, data.id)
// Delete Page will only delete the page from the archive page map, at this point only archived pages can be deleted
await deletePage(workspaceSlug.toString(), projectId as string, pageId)
.then(() => {
handleClose();
setToastAlert({
@@ -99,8 +107,8 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
<div className="mt-2">
<p className="text-sm text-custom-text-200">
Are you sure you want to delete page-{" "}
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? The Page
will be deleted permanently. This action cannot be undone.
<span className="break-words font-medium text-custom-text-100">{name}</span>? The Page will be
deleted permanently. This action cannot be undone.
</p>
</div>
</div>
+9 -8
View File
@@ -5,11 +5,12 @@ import { Button, Input, Tooltip } from "@plane/ui";
import { IPage } from "@plane/types";
// constants
import { PAGE_ACCESS_SPECIFIERS } from "constants/page";
import { IPageStore } from "store/page.store";
type Props = {
handleFormSubmit: (values: IPage) => Promise<void>;
handleClose: () => void;
data?: IPage | null;
pageStore?: IPageStore;
};
const defaultValues = {
@@ -19,24 +20,24 @@ const defaultValues = {
};
export const PageForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, data } = props;
const { handleFormSubmit, handleClose, pageStore } = props;
const {
formState: { errors, isSubmitting },
handleSubmit,
control,
} = useForm<IPage>({
defaultValues: { ...defaultValues, ...data },
defaultValues: pageStore
? { name: pageStore.name, description: pageStore.description, access: pageStore.access }
: defaultValues,
});
const handleCreateUpdatePage = async (formData: IPage) => {
await handleFormSubmit(formData);
};
const handleCreateUpdatePage = (formData: IPage) => handleFormSubmit(formData);
return (
<form onSubmit={handleSubmit(handleCreateUpdatePage)}>
<div className="space-y-4">
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{data ? "Update" : "Create"} Page</h3>
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{pageStore ? "Update" : "Create"} Page</h3>
<div className="space-y-3">
<div>
<Controller
@@ -104,7 +105,7 @@ export const PageForm: React.FC<Props> = (props) => {
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
{data ? (isSubmitting ? "Updating..." : "Update page") : isSubmitting ? "Creating..." : "Create Page"}
{pageStore ? (isSubmitting ? "Updating..." : "Update page") : isSubmitting ? "Creating..." : "Create Page"}
</Button>
</div>
</div>
@@ -1,17 +1,17 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { usePage } from "hooks/store";
// components
import { PagesListView } from "components/pages/pages-list";
// ui
import { Loader } from "@plane/ui";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
export const AllPagesList: FC = observer(() => {
// store
const { projectPageIds } = usePage();
const pageStores = useProjectPages();
// subscribing to the projectPageStore
const { projectPageIds } = pageStores;
if (!projectPageIds)
if (!projectPageIds) {
return (
<Loader className="space-y-4">
<Loader.Item height="40px" />
@@ -19,6 +19,6 @@ export const AllPagesList: FC = observer(() => {
<Loader.Item height="40px" />
</Loader>
);
}
return <PagesListView pageIds={projectPageIds} />;
});
@@ -3,14 +3,15 @@ import { observer } from "mobx-react-lite";
// components
import { PagesListView } from "components/pages/pages-list";
// hooks
import { usePage } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
export const ArchivedPagesList: FC = observer(() => {
const { archivedProjectPageIds } = usePage();
const projectPageStore = useProjectPages();
const { archivedPageIds } = projectPageStore;
if (!archivedProjectPageIds)
if (!archivedPageIds)
return (
<Loader className="space-y-4">
<Loader.Item height="40px" />
@@ -19,5 +20,5 @@ export const ArchivedPagesList: FC = observer(() => {
</Loader>
);
return <PagesListView pageIds={archivedProjectPageIds} />;
return <PagesListView pageIds={archivedPageIds} />;
});
@@ -3,12 +3,13 @@ import { observer } from "mobx-react-lite";
// components
import { PagesListView } from "components/pages/pages-list";
// hooks
import { usePage } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
export const FavoritePagesList: FC = observer(() => {
const { favoriteProjectPageIds } = usePage();
const projectPageStore = useProjectPages();
const { favoriteProjectPageIds } = projectPageStore;
if (!favoriteProjectPageIds)
return (
+77 -106
View File
@@ -13,10 +13,6 @@ import {
Star,
Trash2,
} from "lucide-react";
// hooks
import { useMember, usePage, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
// helpers
import { copyUrlToClipboard } from "helpers/string.helper";
import { renderFormattedTime, renderFormattedDate } from "helpers/date-time.helper";
// ui
@@ -25,142 +21,120 @@ import { CustomMenu, Tooltip } from "@plane/ui";
import { CreateUpdatePageModal, DeletePageModal } from "components/pages";
// constants
import { EUserProjectRoles } from "constants/project";
import { useRouter } from "next/router";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
import { useMember, usePage, useUser } from "hooks/store";
import { IIssueLabel } from "@plane/types";
export interface IPagesListItem {
workspaceSlug: string;
projectId: string;
pageId: string;
projectId: string;
}
export const PagesListItem: FC<IPagesListItem> = observer((props) => {
const { workspaceSlug, projectId, pageId } = props;
export const PagesListItem: FC<IPagesListItem> = observer(({ pageId, projectId }: IPagesListItem) => {
const projectPageStore = useProjectPages();
// Now, I am observing only the projectPages, out of the projectPageStore.
const { archivePage, restorePage } = projectPageStore;
const pageStore = usePage(pageId);
// states
const router = useRouter();
const { workspaceSlug } = router.query;
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
const [deletePageModal, setDeletePageModal] = useState(false);
// store hooks
const {
currentUser,
membership: { currentProjectRole },
} = useUser();
const {
getArchivedPageById,
getUnArchivedPageById,
archivePage,
removeFromFavorites,
addToFavorites,
makePrivate,
makePublic,
restorePage,
} = usePage();
const {
project: { getProjectMemberDetails },
} = useMember();
// toast alert
const { setToastAlert } = useToast();
// derived values
const pageDetails = getUnArchivedPageById(pageId) ?? getArchivedPageById(pageId);
const handleCopyUrl = (e: any) => {
if (!pageStore) return null;
const {
archived_at,
label_details,
access,
is_favorite,
owned_by,
name,
created_at,
updated_at,
makePublic,
makePrivate,
addToFavorites,
removeFromFavorites,
} = pageStore;
const handleCopyUrl = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${pageId}`).then(() => {
setToastAlert({
type: "success",
title: "Link Copied!",
message: "Page link copied to clipboard.",
});
});
await copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/pages/${pageId}`);
};
const handleAddToFavorites = (e: any) => {
const handleAddToFavorites = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
addToFavorites();
};
const handleRemoveFromFavorites = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
addToFavorites(workspaceSlug, projectId, pageId)
.then(() => {
setToastAlert({
type: "success",
title: "Success!",
message: "Successfully added the page to favorites.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't add the page to favorites. Please try again.",
});
});
removeFromFavorites();
};
const handleRemoveFromFavorites = (e: any) => {
const handleMakePublic = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
removeFromFavorites(workspaceSlug, projectId, pageId)
.then(() => {
setToastAlert({
type: "success",
title: "Success!",
message: "Successfully removed the page from favorites.",
});
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Couldn't remove the page from favorites. Please try again.",
});
});
makePublic();
};
const handleMakePublic = (e: any) => {
const handleMakePrivate = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
makePublic(workspaceSlug, projectId, pageId);
makePrivate();
};
const handleMakePrivate = (e: any) => {
const handleArchivePage = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
makePrivate(workspaceSlug, projectId, pageId);
await archivePage(workspaceSlug as string, projectId as string, pageId as string);
};
const handleArchivePage = (e: any) => {
const handleRestorePage = async (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
archivePage(workspaceSlug, projectId, pageId);
await restorePage(workspaceSlug as string, projectId as string, pageId as string);
};
const handleRestorePage = (e: any) => {
e.preventDefault();
e.stopPropagation();
restorePage(workspaceSlug, projectId, pageId);
};
const handleDeletePage = (e: any) => {
const handleDeletePage = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
setDeletePageModal(true);
};
const handleEditPage = (e: any) => {
const handleEditPage = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
e.stopPropagation();
setCreateUpdatePageModal(true);
};
if (!pageDetails) return null;
const ownerDetails = getProjectMemberDetails(pageDetails.owned_by);
const isCurrentUserOwner = pageDetails.owned_by === currentUser?.id;
const ownerDetails = getProjectMemberDetails(owned_by);
const isCurrentUserOwner = owned_by === currentUser?.id;
const userCanEdit =
isCurrentUserOwner ||
@@ -173,22 +147,21 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
return (
<>
<CreateUpdatePageModal
pageStore={pageStore}
isOpen={createUpdatePageModal}
handleClose={() => setCreateUpdatePageModal(false)}
data={pageDetails}
projectId={projectId}
/>
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} data={pageDetails} />
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} pageId={pageId} />
<li>
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${pageDetails.id}`}>
<Link href={`/${workspaceSlug}/projects/${projectId}/pages/${pageId}`}>
<div className="relative rounded p-4 text-custom-text-200 hover:bg-custom-background-80">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 overflow-hidden">
<FileText className="h-4 w-4 shrink-0" />
<p className="mr-2 truncate text-sm text-custom-text-100">{pageDetails.name}</p>
{/* FIXME: replace any with proper type */}
{pageDetails.label_details.length > 0 &&
pageDetails.label_details.map((label: any) => (
<p className="mr-2 truncate text-sm text-custom-text-100">{name}</p>
{label_details.length > 0 &&
label_details.map((label: IIssueLabel) => (
<div
key={label.id}
className="group flex items-center gap-1 rounded-2xl border border-custom-border-200 px-2 py-0.5 text-xs"
@@ -207,26 +180,26 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
))}
</div>
<div className="flex items-center gap-2.5">
{pageDetails.archived_at ? (
{archived_at ? (
<Tooltip
tooltipContent={`Archived at ${renderFormattedTime(
pageDetails.archived_at
)} on ${renderFormattedDate(pageDetails.archived_at)}`}
tooltipContent={`Archived at ${renderFormattedTime(archived_at)} on ${renderFormattedDate(
archived_at
)}`}
>
<p className="text-sm text-custom-text-200">{renderFormattedTime(pageDetails.archived_at)}</p>
<p className="text-sm text-custom-text-200">{renderFormattedTime(archived_at)}</p>
</Tooltip>
) : (
<Tooltip
tooltipContent={`Last updated at ${renderFormattedTime(
pageDetails.updated_at
)} on ${renderFormattedDate(pageDetails.updated_at)}`}
tooltipContent={`Last updated at ${renderFormattedTime(updated_at)} on ${renderFormattedDate(
updated_at
)}`}
>
<p className="text-sm text-custom-text-200">{renderFormattedTime(pageDetails.updated_at)}</p>
<p className="text-sm text-custom-text-200">{renderFormattedTime(updated_at)}</p>
</Tooltip>
)}
{isEditingAllowed && (
<Tooltip tooltipContent={`${pageDetails.is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
{pageDetails.is_favorite ? (
<Tooltip tooltipContent={`${is_favorite ? "Remove from favorites" : "Mark as favorite"}`}>
{is_favorite ? (
<button type="button" onClick={handleRemoveFromFavorites}>
<Star className="h-3.5 w-3.5 fill-orange-400 text-orange-400" />
</button>
@@ -240,12 +213,10 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
{userCanChangeAccess && (
<Tooltip
tooltipContent={`${
pageDetails.access
? "This page is only visible to you"
: "This page can be viewed by anyone in the project"
access ? "This page is only visible to you" : "This page can be viewed by anyone in the project"
}`}
>
{pageDetails.access ? (
{access ? (
<button type="button" onClick={handleMakePublic}>
<Lock className="h-3.5 w-3.5" />
</button>
@@ -259,13 +230,13 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
<Tooltip
position="top-right"
tooltipContent={`Created by ${ownerDetails?.member.display_name} on ${renderFormattedDate(
pageDetails.created_at
created_at
)}`}
>
<AlertCircle className="h-3.5 w-3.5" />
</Tooltip>
<CustomMenu width="auto" placement="bottom-end" className="!-m-1" verticalEllipsis>
{pageDetails.archived_at ? (
{archived_at ? (
<>
{userCanArchive && (
<CustomMenu.MenuItem onClick={handleRestorePage}>
+12 -14
View File
@@ -1,11 +1,9 @@
import { FC } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
import { useApplication, useUser } from "hooks/store";
// components
import { PagesListItem } from "./list-item";
import { NewEmptyState } from "components/common/new-empty-state";
// ui
import { Loader } from "@plane/ui";
@@ -13,14 +11,17 @@ import { Loader } from "@plane/ui";
import emptyPage from "public/empty-state/empty_page.png";
// constants
import { EUserProjectRoles } from "constants/project";
import { PagesListItem } from "./list-item";
type IPagesListView = {
pageIds: string[];
};
export const PagesListView: FC<IPagesListView> = observer((props) => {
const { pageIds } = props;
export const PagesListView: FC<IPagesListView> = (props) => {
const { pageIds: projectPageIds } = props;
// store hooks
// trace(true);
const {
commandPalette: { toggleCreatePageModal },
} = useApplication();
@@ -31,21 +32,18 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// here we are only observing the projectPageStore, so that we can re-render the component when the projectPageStore changes
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
return (
<>
{pageIds && workspaceSlug && projectId ? (
{projectPageIds && workspaceSlug && projectId ? (
<div className="h-full space-y-4 overflow-y-auto">
{pageIds.length > 0 ? (
{projectPageIds.length > 0 ? (
<ul role="list" className="divide-y divide-custom-border-200">
{pageIds.map((pageId) => (
<PagesListItem
key={pageId}
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
pageId={pageId}
/>
{projectPageIds.map((pageId: string) => (
<PagesListItem key={pageId} pageId={pageId} projectId={projectId.toString()} />
))}
</ul>
) : (
@@ -77,4 +75,4 @@ export const PagesListView: FC<IPagesListView> = observer((props) => {
)}
</>
);
});
};
@@ -1,14 +1,15 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { usePage } from "hooks/store";
// components
import { PagesListView } from "components/pages/pages-list";
// ui
import { Loader } from "@plane/ui";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
export const PrivatePagesList: FC = observer(() => {
const { privateProjectPageIds } = usePage();
const projectPageStore = useProjectPages();
const { privateProjectPageIds } = projectPageStore;
if (!privateProjectPageIds)
return (
@@ -2,7 +2,7 @@ import React, { FC } from "react";
import { observer } from "mobx-react-lite";
import { Plus } from "lucide-react";
// hooks
import { useApplication, usePage, useUser } from "hooks/store";
import { useApplication, useUser } from "hooks/store";
// components
import { PagesListView } from "components/pages/pages-list";
import { NewEmptyState } from "components/common/new-empty-state";
@@ -14,6 +14,7 @@ import emptyPage from "public/empty-state/empty_page.png";
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// constants
import { EUserProjectRoles } from "constants/project";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
export const RecentPagesList: FC = observer(() => {
// store hooks
@@ -21,7 +22,7 @@ export const RecentPagesList: FC = observer(() => {
const {
membership: { currentProjectRole },
} = useUser();
const { recentProjectPages } = usePage();
const { recentProjectPages } = useProjectPages();
// FIXME: replace any with proper type
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
@@ -3,12 +3,13 @@ import { observer } from "mobx-react-lite";
// components
import { PagesListView } from "components/pages/pages-list";
// hooks
import { usePage } from "hooks/store";
// ui
import { Loader } from "@plane/ui";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
export const SharedPagesList: FC = observer(() => {
const { publicProjectPageIds } = usePage();
const projectPageStore = useProjectPages();
const { publicProjectPageIds } = projectPageStore;
if (!publicProjectPageIds)
return (
@@ -18,9 +18,7 @@ export const ProfileIssuesFilter = observer(() => {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROFILE);
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
// derived values
const states = undefined;
const members = undefined;
+4 -2
View File
@@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
// components
import { ProfileIssuesListLayout } from "components/issues/issue-layouts/list/roots/profile-issues-root";
import { ProfileIssuesKanBanLayout } from "components/issues/issue-layouts/kanban/roots/profile-issues-root";
import { ProfileIssuesAppliedFiltersRoot } from "components/issues";
import { IssuePeekOverview, ProfileIssuesAppliedFiltersRoot } from "components/issues";
import { Spinner } from "@plane/ui";
// hooks
import { useIssues } from "hooks/store";
@@ -34,7 +34,7 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
async () => {
if (workspaceSlug && userId) {
await fetchFilters(workspaceSlug, userId);
await fetchIssues(workspaceSlug, userId, groupedIssueIds ? "mutation" : "init-loader", undefined, type);
await fetchIssues(workspaceSlug, undefined, groupedIssueIds ? "mutation" : "init-loader", userId, type);
}
}
);
@@ -57,6 +57,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => {
<ProfileIssuesKanBanLayout />
) : null}
</div>
{/* peek overview */}
<IssuePeekOverview />
</>
)}
</>
+1 -1
View File
@@ -42,7 +42,7 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
setIsDeleteLoading(true);
await deleteState(workspaceSlug.toString(), data.project, data.id)
await deleteState(workspaceSlug.toString(), data.project_id, data.id)
.then(() => {
postHogEventTracker("STATE_DELETE", {
state: "SUCCESS",
+1 -3
View File
@@ -28,9 +28,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
// store hooks
const { projectStates } = useProjectState();
const {
project: { projectLabels },
} = useLabel();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
+1 -3
View File
@@ -27,9 +27,7 @@ const defaultValues: Partial<IWorkspaceView> = {
export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
const { handleFormSubmit, handleClose, data, preLoadedData } = props;
// store hooks
const {
workspace: { workspaceLabels },
} = useLabel();
const { workspaceLabels } = useLabel();
const {
workspace: { workspaceMemberIds },
} = useMember();
-6
View File
@@ -1,7 +1 @@
export const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
export const isNil = (value: any) => {
if (value === undefined || value === null) return true;
return false;
};
+3 -3
View File
@@ -2,10 +2,10 @@ import { useContext } from "react";
// mobx store
import { StoreContext } from "contexts/store-context";
// types
import { ILabelRootStore } from "store/label";
import { ILabelStore } from "store/label.store";
export const useLabel = (): ILabelRootStore => {
export const useLabel = (): ILabelStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useLabel must be used within StoreProvider");
return context.labelRoot;
return context.label;
};
+14 -4
View File
@@ -1,11 +1,21 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "contexts/store-context";
// types
import { IPageStore } from "store/page.store";
export const usePage = (): IPageStore => {
export const usePage = (pageId: string) => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("usePage must be used within StoreProvider");
return context.page;
const { projectPageMap, projectArchivedPageMap } = context.projectPages;
const { projectId, workspaceSlug } = context.app.router;
if (!projectId || !workspaceSlug) throw new Error("usePage must be used within ProjectProvider");
if (projectPageMap[projectId] && projectPageMap[projectId][pageId]) {
return projectPageMap[projectId][pageId];
} else if (projectArchivedPageMap[projectId] && projectArchivedPageMap[projectId][pageId]) {
return projectArchivedPageMap[projectId][pageId];
} else {
return;
}
};
+2 -1
View File
@@ -1,8 +1,9 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "contexts/store-context";
import { IProjectPageStore } from "store/project-page.store";
export const useProjectPages = () => {
export const useProjectPages = (): IProjectPageStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("useProjectPublish must be used within StoreProvider");
return context.projectPages;
@@ -0,0 +1,11 @@
import { useContext } from "react";
// mobx store
import { StoreContext } from "contexts/store-context";
// types
import { IProjectPageStore } from "store/project-page.store";
export const useProjectPages = (): IProjectPageStore => {
const context = useContext(StoreContext);
if (context === undefined) throw new Error("usePage must be used within StoreProvider");
return context.projectPages;
};
+48
View File
@@ -0,0 +1,48 @@
import { TIssue } from "@plane/types";
import { PROJECT_ISSUES_LIST, STATES_LIST } from "constants/fetch-keys";
import { EIssuesStoreType } from "constants/issue";
import { StoreContext } from "contexts/store-context";
import { autorun, toJS } from "mobx";
import { useContext } from "react";
import { IssueService } from "services/issue";
import useSWR from "swr";
import { useIssueDetail, useIssues, useMember, useProject, useProjectState } from "./store";
const issueService = new IssueService();
export const useIssueEmbeds = () => {
const workspaceSlug = useContext(StoreContext).app.router.workspaceSlug;
const projectId = useContext(StoreContext).app.router.projectId;
const { getProjectById } = useProject();
const { setPeekIssue } = useIssueDetail();
const { getStateById } = useProjectState();
const { getUserDetails } = useMember();
const { data: issuesResponse } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
);
const issues = Object.values(issuesResponse ?? {});
const issuesWithStateAndProject = issues.map((issue) => ({
...issue,
state_detail: toJS(getStateById(issue.state_id)),
project_detail: toJS(getProjectById(issue.project_id)),
assignee_details: issue.assignee_ids.map((assigneeid) => toJS(getUserDetails(assigneeid))),
}));
const fetchIssue = async (issueId: string) => issuesWithStateAndProject.find((issue) => issue.id === issueId);
const issueWidgetClickAction = (issueId: string) => {
if (!workspaceSlug || !projectId) return;
setPeekIssue({ workspaceSlug, projectId: projectId, issueId });
};
return {
issues: issuesWithStateAndProject,
fetchIssue,
issueWidgetClickAction,
};
};
@@ -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
);
};
+1 -3
View File
@@ -45,9 +45,7 @@ export const ProjectAuthWrapper: FC<IProjectAuthWrapper> = observer((props) => {
project: { fetchProjectMembers },
} = useMember();
const { fetchProjectStates } = useProjectState();
const {
project: { fetchProjectLabels },
} = useLabel();
const { fetchProjectLabels } = useLabel();
const { fetchProjectEstimates } = useEstimate();
// router
const router = useRouter();
@@ -20,9 +20,6 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
const {
workspace: { fetchWorkspaceMembers },
} = useMember();
const {
workspace: { fetchWorkspaceLabels },
} = useLabel();
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@@ -41,11 +38,6 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : 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
useSWR(
workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null,
+3 -2
View File
@@ -26,8 +26,8 @@
"@plane/document-editor": "*",
"@plane/lite-text-editor": "*",
"@plane/rich-text-editor": "*",
"@plane/ui": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@popperjs/core": "^2.11.8",
"@sentry/nextjs": "^7.85.0",
"axios": "^1.1.3",
@@ -39,7 +39,8 @@
"lodash": "^4.17.21",
"lucide-react": "^0.294.0",
"mobx": "^6.10.0",
"mobx-react-lite": "^4.0.3",
"mobx-react": "^9.1.0",
"mobx-utils": "^6.0.8",
"next": "^14.0.3",
"next-pwa": "^5.6.0",
"next-themes": "^0.2.1",
@@ -1,52 +1,50 @@
import React, { useEffect, useRef, useState, ReactElement, useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import useSWR, { MutatorOptions } from "swr";
import { Controller, useForm } from "react-hook-form";
import { Sparkle } from "lucide-react";
import debounce from "lodash/debounce";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { useRouter } from "next/router";
import { ReactElement, useEffect, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// hooks
import { useApplication, useUser } from "hooks/store";
import useToast from "hooks/use-toast";
import { useApplication, useIssues, usePage, useUser } from "hooks/store";
import useReloadConfirmations from "hooks/use-reload-confirmation";
import useToast from "hooks/use-toast";
// services
import { PageService } from "services/page.service";
import { FileService } from "services/file.service";
import { IssueService } from "services/issue";
// layouts
import { AppLayout } from "layouts/app-layout";
// components
import { GptAssistantPopover } from "components/core";
import { PageDetailsHeader } from "components/headers/page-details";
import { EmptyState } from "components/common";
// ui
import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor";
import { Spinner } from "@plane/ui";
// assets
import emptyPage from "public/empty-state/page.svg";
// helpers
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
// types
import { IPage } from "@plane/types";
import { NextPageWithLayout } from "lib/types";
import { IPage, TIssue } from "@plane/types";
// fetch-keys
import { PAGE_DETAILS, PROJECT_ISSUES_LIST } from "constants/fetch-keys";
// constants
import { EUserProjectRoles } from "constants/project";
import { useProjectPages } from "hooks/store/use-project-specific-pages";
import { useIssueEmbeds } from "hooks/use-issue-embeds";
import { IssuePeekOverview } from "components/issues";
import { PROJECT_ISSUES_LIST } from "constants/fetch-keys";
import { IssueService } from "services/issue";
import { EIssuesStoreType } from "constants/issue";
// services
const fileService = new FileService();
const pageService = new PageService();
const issueService = new IssueService();
const PageDetailsPage: NextPageWithLayout = observer(() => {
// states
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
const [gptModalOpen, setGptModal] = useState(false);
// refs
const editorRef = useRef<any>(null);
// router
const router = useRouter();
const { workspaceSlug, projectId, pageId } = router.query;
// store hooks
const {
@@ -59,18 +57,82 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
// toast alert
const { setToastAlert } = useToast();
//TODO:fix reload confirmations, with mobx
const { setShowAlert } = useReloadConfirmations();
const { handleSubmit, setValue, watch, getValues, control, reset } = useForm<IPage>({
defaultValues: { name: "", description_html: "" },
});
const { data: issuesResponse } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null,
workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null
const {
archivePage: archivePageAction,
restorePage: restorePageAction,
createPage: createPageAction,
projectPageMap,
projectArchivedPageMap,
fetchProjectPages,
fetchArchivedProjectPages,
} = useProjectPages();
useSWR(
workspaceSlug && projectId ? `ALL_PAGES_LIST_${projectId}` : null,
workspaceSlug && projectId && !projectPageMap[projectId as string] && !projectArchivedPageMap[projectId as string]
? () => fetchProjectPages(workspaceSlug.toString(), projectId.toString())
: null
);
// fetching archived pages from API
useSWR(
workspaceSlug && projectId ? `ALL_ARCHIVED_PAGES_LIST_${projectId}` : null,
workspaceSlug && projectId && !projectArchivedPageMap[projectId as string] && !projectPageMap[projectId as string]
? () => fetchArchivedProjectPages(workspaceSlug.toString(), projectId.toString())
: null
);
const issues = Object.values(issuesResponse ?? {});
const { issues, fetchIssue, issueWidgetClickAction } = useIssueEmbeds();
const pageStore = usePage(pageId as string);
useEffect(
() => () => {
if (pageStore) {
pageStore.cleanup();
}
},
[pageStore]
);
if (!pageStore) {
return (
<div className="grid h-full w-full place-items-center">
<Spinner />
</div>
);
}
// We need to get the values of title and description from the page store but we don't have to subscribe to those values
const pageTitle = pageStore?.name;
const pageDescription = pageStore?.description_html;
const {
lockPage: lockPageAction,
unlockPage: unlockPageAction,
updateName: updateNameAction,
updateDescription: updateDescriptionAction,
id: pageIdMobx,
isSubmitting,
setIsSubmitting,
owned_by,
is_locked,
archived_at,
created_at,
created_by,
updated_at,
updated_by,
} = pageStore;
const updatePage = async (formData: IPage) => {
if (!workspaceSlug || !projectId || !pageId) return;
await updateDescriptionAction(formData.description_html);
};
const handleAiAssistance = async (response: string) => {
if (!workspaceSlug || !projectId || !pageId) return;
@@ -78,47 +140,7 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
const newDescription = `${watch("description_html")}<p>${response}</p>`;
setValue("description_html", newDescription);
editorRef.current?.setEditorValue(newDescription);
pageService
.patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), {
description_html: newDescription,
})
.then(() => {
mutatePageDetails((prevData) => ({ ...prevData, description_html: newDescription } as IPage), false);
});
};
// =================== Fetching Page Details ======================
const {
data: pageDetails,
mutate: mutatePageDetails,
error,
} = useSWR(
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId.toString()) : null,
workspaceSlug && projectId && pageId
? () => pageService.getPageDetails(workspaceSlug.toString(), projectId.toString(), pageId.toString())
: null,
{
revalidateOnFocus: false,
}
);
const fetchIssue = async (issueId: string) => {
const issue = await issueService.retrieve(workspaceSlug as string, projectId as string, issueId as string);
return issue as TIssue;
};
const issueWidgetClickAction = (issueId: string) => {
const url = new URL(router.asPath, window.location.origin);
const params = new URLSearchParams(url.search);
if (params.has("peekIssueId")) {
params.set("peekIssueId", issueId);
} else {
params.append("peekIssueId", issueId);
}
// Replace the current URL with the new one
router.replace(`${url.pathname}?${params.toString()}`, undefined, { shallow: true });
updateDescriptionAction(newDescription);
};
const actionCompleteAlert = ({
@@ -137,122 +159,14 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
});
};
useEffect(() => {
if (isSubmitting === "submitted") {
setShowAlert(false);
setTimeout(async () => {
setIsSubmitting("saved");
}, 2000);
} else if (isSubmitting === "submitting") {
setShowAlert(true);
}
}, [isSubmitting, setShowAlert]);
// adding pageDetails.description_html to dependency array causes
// editor rerendering on every save
useEffect(() => {
if (pageDetails?.description_html) {
setLocalIssueDescription({ id: pageId as string, description_html: pageDetails.description_html });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageDetails?.description_html]); // TODO: Verify the exhaustive-deps warning
function createObjectFromArray(keys: string[], options: any): any {
return keys.reduce((obj, key) => {
if (options[key] !== undefined) {
obj[key] = options[key];
}
return obj;
}, {} as { [key: string]: any });
}
const mutatePageDetailsHelper = (
serverMutatorFn: Promise<any>,
dataToMutate: Partial<IPage>,
formDataValues: Array<keyof IPage>,
onErrorAction: () => void
) => {
const commonSwrOptions: MutatorOptions = {
revalidate: false,
populateCache: false,
rollbackOnError: () => {
onErrorAction();
return true;
},
};
const formData = getValues();
const formDataMutationObject = createObjectFromArray(formDataValues, formData);
mutatePageDetails(async () => serverMutatorFn, {
optimisticData: (prevData) => {
if (!prevData) return;
return {
...prevData,
description_html: formData["description_html"],
...formDataMutationObject,
...dataToMutate,
};
},
...commonSwrOptions,
});
};
useEffect(() => {
mutatePageDetails(undefined, {
revalidate: true,
populateCache: true,
rollbackOnError: () => {
actionCompleteAlert({
title: `Page could not be updated`,
message: `Sorry, page could not be updated, please try again later`,
type: "error",
});
return true;
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updatePage = async (formData: IPage) => {
const updatePageTitle = (title: string) => {
if (!workspaceSlug || !projectId || !pageId) return;
formData.name = pageDetails?.name as string;
if (!formData?.name || formData?.name.length === 0) return;
try {
await pageService.patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), formData);
} catch (error) {
actionCompleteAlert({
title: `Page could not be updated`,
message: `Sorry, page could not be updated, please try again later`,
type: "error",
});
}
};
const updatePageTitle = async (title: string) => {
if (!workspaceSlug || !projectId || !pageId) return;
mutatePageDetailsHelper(
pageService.patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), { name: title }),
{
name: title,
},
[],
() =>
actionCompleteAlert({
title: `Page Title could not be updated`,
message: `Sorry, page title could not be updated, please try again later`,
type: "error",
})
);
updateNameAction(title);
};
const createPage = async (payload: Partial<IPage>) => {
if (!workspaceSlug || !projectId) return;
await pageService.createPage(workspaceSlug.toString(), projectId.toString(), payload);
await createPageAction(workspaceSlug as string, projectId as string, payload);
};
// ================ Page Menu Actions ==================
@@ -260,121 +174,84 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
const currentPageValues = getValues();
if (!currentPageValues?.description_html) {
currentPageValues.description_html = pageDetails?.description_html as string;
// TODO: We need to get latest data the above variable will give us stale data
currentPageValues.description_html = pageDescription as string;
}
const formData: Partial<IPage> = {
name: "Copy of " + pageDetails?.name,
name: "Copy of " + pageTitle,
description_html: currentPageValues.description_html,
};
await createPage(formData);
try {
await createPage(formData);
} catch (error) {
actionCompleteAlert({
title: `Page could not be duplicated`,
message: `Sorry, page could not be duplicated, please try again later`,
type: "error",
});
}
};
const archivePage = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
mutatePageDetailsHelper(
pageService.archivePage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
{
archived_at: renderFormattedPayloadDate(new Date()),
},
["description_html"],
() =>
actionCompleteAlert({
title: `Page could not be Archived`,
message: `Sorry, page could not be Archived, please try again later`,
type: "error",
})
);
try {
await archivePageAction(workspaceSlug as string, projectId as string, pageId as string);
} catch (error) {
actionCompleteAlert({
title: `Page could not be archived`,
message: `Sorry, page could not be archived, please try again later`,
type: "error",
});
}
};
const unArchivePage = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
mutatePageDetailsHelper(
pageService.restorePage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
{
archived_at: null,
},
["description_html"],
() =>
actionCompleteAlert({
title: `Page could not be Restored`,
message: `Sorry, page could not be Restored, please try again later`,
type: "error",
})
);
try {
await restorePageAction(workspaceSlug as string, projectId as string, pageId as string);
} catch (error) {
actionCompleteAlert({
title: `Page could not be restored`,
message: `Sorry, page could not be restored, please try again later`,
type: "error",
});
}
};
// ========================= Page Lock ==========================
const lockPage = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
mutatePageDetailsHelper(
pageService.lockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
{
is_locked: true,
},
["description_html"],
() =>
actionCompleteAlert({
title: `Page cannot be Locked`,
message: `Sorry, page cannot be Locked, please try again later`,
type: "error",
})
);
try {
await lockPageAction();
} catch (error) {
actionCompleteAlert({
title: `Page could not be locked`,
message: `Sorry, page could not be locked, please try again later`,
type: "error",
});
}
};
const unlockPage = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
mutatePageDetailsHelper(
pageService.unlockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString()),
{
is_locked: false,
},
["description_html"],
() =>
actionCompleteAlert({
title: `Page could not be Unlocked`,
message: `Sorry, page could not be Unlocked, please try again later`,
type: "error",
})
);
try {
await unlockPageAction();
} catch (error) {
actionCompleteAlert({
title: `Page could not be unlocked`,
message: `Sorry, page could not be unlocked, please try again later`,
type: "error",
});
}
};
const [localPageDescription, setLocalIssueDescription] = useState({
id: pageId as string,
description_html: "",
});
// ADDING updatePage TO DEPENDENCY ARRAY PRODUCES ADVERSE EFFECTS
// TODO: Verify the exhaustive-deps warning
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedFormSave = useCallback(
debounce(async () => {
handleSubmit(updatePage)().finally(() => setIsSubmitting("submitted"));
}, 1500),
[handleSubmit, pageDetails]
);
if (error)
return (
<EmptyState
image={emptyPage}
title="Page does not exist"
description="The page you are looking for does not exist or has been deleted."
primaryButton={{
text: "View other pages",
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/pages`),
}}
/>
);
const isPageReadOnly =
pageDetails?.is_locked ||
pageDetails?.archived_at ||
is_locked ||
archived_at ||
(currentProjectRole && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(currentProjectRole));
const isCurrentUserOwner = pageDetails?.owned_by === currentUser?.id;
const isCurrentUserOwner = owned_by === currentUser?.id;
const userCanDuplicate =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
@@ -382,144 +259,132 @@ const PageDetailsPage: NextPageWithLayout = observer(() => {
const userCanLock =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
return (
<>
{pageDetails && issuesResponse ? (
<div className="flex h-full flex-col justify-between">
<div className="h-full w-full overflow-hidden">
{isPageReadOnly ? (
<DocumentReadOnlyEditorWithRef
onActionCompleteHandler={actionCompleteAlert}
ref={editorRef}
value={localPageDescription.description_html}
rerenderOnPropsChange={localPageDescription}
customClassName={"tracking-tight w-full px-0"}
borderOnFocus={false}
noBorder
documentDetails={{
title: pageDetails.name,
created_by: pageDetails.created_by,
created_on: pageDetails.created_at,
last_updated_at: pageDetails.updated_at,
last_updated_by: pageDetails.updated_by,
}}
pageLockConfig={
userCanLock && !pageDetails.archived_at
? { action: unlockPage, is_locked: pageDetails.is_locked }
: undefined
}
pageDuplicationConfig={
userCanDuplicate && !pageDetails.archived_at ? { action: duplicate_page } : undefined
}
pageArchiveConfig={
userCanArchive
? {
action: pageDetails.archived_at ? unArchivePage : archivePage,
is_archived: pageDetails.archived_at ? true : false,
archived_at: pageDetails.archived_at ? new Date(pageDetails.archived_at) : undefined,
}
: undefined
}
embedConfig={{
issueEmbedConfig: {
issues: issues,
fetchIssue: fetchIssue,
clickAction: issueWidgetClickAction,
},
}}
/>
) : (
<div className="relative h-full w-full overflow-hidden">
<Controller
name="description_html"
control={control}
render={({ field: { onChange } }) => (
<DocumentEditorWithRef
isSubmitting={isSubmitting}
documentDetails={{
title: pageDetails.name,
created_by: pageDetails.created_by,
created_on: pageDetails.created_at,
last_updated_at: pageDetails.updated_at,
last_updated_by: pageDetails.updated_by,
}}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
setShouldShowAlert={setShowAlert}
deleteFile={fileService.deleteImage}
restoreFile={fileService.restoreImage}
cancelUploadImage={fileService.cancelUpload}
ref={editorRef}
debouncedUpdatesEnabled={false}
setIsSubmitting={setIsSubmitting}
updatePageTitle={updatePageTitle}
value={localPageDescription.description_html}
rerenderOnPropsChange={localPageDescription}
onActionCompleteHandler={actionCompleteAlert}
customClassName="tracking-tight self-center px-0 h-full w-full"
onChange={(_description_json: Object, description_html: string) => {
setShowAlert(true);
onChange(description_html);
setIsSubmitting("submitting");
debouncedFormSave();
}}
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
pageArchiveConfig={
userCanArchive
? {
is_archived: pageDetails.archived_at ? true : false,
action: pageDetails.archived_at ? unArchivePage : archivePage,
}
: undefined
}
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
embedConfig={{
issueEmbedConfig: {
issues: issues,
fetchIssue: fetchIssue,
clickAction: issueWidgetClickAction,
},
}}
/>
)}
return pageIdMobx && issues ? (
<div className="flex h-full flex-col justify-between">
<div className="h-full w-full overflow-hidden">
{isPageReadOnly ? (
<DocumentReadOnlyEditorWithRef
onActionCompleteHandler={actionCompleteAlert}
ref={editorRef}
value={pageDescription}
customClassName={"tracking-tight w-full px-0"}
borderOnFocus={false}
noBorder
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
pageLockConfig={userCanLock && !archived_at ? { action: unlockPage, is_locked: is_locked } : undefined}
pageDuplicationConfig={userCanDuplicate && !archived_at ? { action: duplicate_page } : undefined}
pageArchiveConfig={
userCanArchive
? {
action: archived_at ? unArchivePage : archivePage,
is_archived: archived_at ? true : false,
archived_at: archived_at ? new Date(archived_at) : undefined,
}
: undefined
}
embedConfig={{
issueEmbedConfig: {
issues: issues,
fetchIssue: fetchIssue,
clickAction: issueWidgetClickAction,
},
}}
/>
) : (
<div className="relative h-full w-full overflow-hidden">
<Controller
name="description_html"
control={control}
render={({ field: { onChange } }) => (
<DocumentEditorWithRef
isSubmitting={isSubmitting}
documentDetails={{
title: pageTitle,
created_by: created_by,
created_on: created_at,
last_updated_at: updated_at,
last_updated_by: updated_by,
}}
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
value={pageDescription}
setShouldShowAlert={setShowAlert}
deleteFile={fileService.deleteImage}
restoreFile={fileService.restoreImage}
cancelUploadImage={fileService.cancelUpload}
ref={editorRef}
debouncedUpdatesEnabled={false}
setIsSubmitting={setIsSubmitting}
updatePageTitle={updatePageTitle}
onActionCompleteHandler={actionCompleteAlert}
customClassName="tracking-tight self-center px-0 h-full w-full"
onChange={(_description_json: Object, description_html: string) => {
setShowAlert(true);
onChange(description_html);
handleSubmit(updatePage)();
}}
duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined}
pageArchiveConfig={
userCanArchive
? {
is_archived: archived_at ? true : false,
action: archived_at ? unArchivePage : archivePage,
}
: undefined
}
pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined}
embedConfig={{
issueEmbedConfig: {
issues: issues,
fetchIssue: fetchIssue,
clickAction: issueWidgetClickAction,
},
}}
/>
)}
/>
{projectId && envConfig?.has_openai_configured && (
<div className="absolute right-[68px] top-2.5">
<GptAssistantPopover
isOpen={gptModalOpen}
projectId={projectId.toString()}
handleClose={() => {
setGptModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptModal((prevData) => !prevData)}
>
<Sparkle className="h-4 w-4" />
AI
</button>
}
className="!min-w-[38rem]"
/>
{projectId && envConfig?.has_openai_configured && (
<div className="absolute right-[68px] top-2.5">
<GptAssistantPopover
isOpen={gptModalOpen}
projectId={projectId.toString()}
handleClose={() => {
setGptModal((prevData) => !prevData);
// this is done so that the title do not reset after gpt popover closed
reset(getValues());
}}
onResponse={(response) => {
handleAiAssistance(response);
}}
placement="top-end"
button={
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs hover:bg-custom-background-90"
onClick={() => setGptModal((prevData) => !prevData)}
>
<Sparkle className="h-4 w-4" />
AI
</button>
}
className="!min-w-[38rem]"
/>
</div>
)}
</div>
)}
</div>
</div>
) : (
<div className="grid h-full w-full place-items-center">
<Spinner />
</div>
)}
</>
)}
<IssuePeekOverview />
</div>
</div>
) : (
<div className="grid h-full w-full place-items-center">
<Spinner />
</div>
);
});
@@ -5,7 +5,7 @@ import { Tab } from "@headlessui/react";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// hooks
import { usePage, useUser } from "hooks/store";
import { useUser } from "hooks/store";
import useLocalStorage from "hooks/use-local-storage";
import useUserAuth from "hooks/use-user-auth";
// layouts
@@ -17,6 +17,7 @@ import { PagesHeader } from "components/headers";
import { NextPageWithLayout } from "lib/types";
// constants
import { PAGE_TABS_LIST } from "constants/page";
import { useProjectPages } from "hooks/store/use-project-page";
const AllPagesList = dynamic<any>(() => import("components/pages").then((a) => a.AllPagesList), {
ssr: false,
@@ -45,8 +46,9 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => {
// states
const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false);
// store
const { fetchProjectPages, fetchArchivedProjectPages } = usePage();
const { currentUser, currentUserLoader } = useUser();
const { fetchProjectPages, fetchArchivedProjectPages } = useProjectPages();
// hooks
const {} = useUserAuth({ user: currentUser, isLoading: currentUserLoader });
// local storage
@@ -61,4 +61,12 @@ export class ProjectEstimateService extends APIService {
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;
});
}
}
@@ -65,4 +65,12 @@ export class ProjectStateService extends APIService {
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;
});
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ export class EventTrackerStore implements IEventTrackerStore {
constructor(_rootStore: RootStore) {
makeObservable(this, {
trackElement: observable,
setTrackElement: action,
setTrackElement: action.bound,
postHogEventTracker: action,
});
this.rootStore = _rootStore;
+1 -1
View File
@@ -29,7 +29,7 @@ export class RouterStore implements IRouterStore {
// observables
query: observable,
// actions
setQuery: action,
setQuery: action.bound,
//computed
workspaceSlug: computed,
projectId: computed,
+7 -9
View File
@@ -1,4 +1,5 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import { isFuture, isPast } from "date-fns";
import set from "lodash/set";
import sortBy from "lodash/sortBy";
@@ -74,10 +75,6 @@ export class CycleStore implements ICycleStore {
currentProjectIncompleteCycleIds: computed,
currentProjectDraftCycleIds: computed,
currentProjectActiveCycleId: computed,
// computed actions
getCycleById: action,
getActiveCycleById: action,
getProjectCycleIds: action,
// actions
fetchAllCycles: action,
fetchActiveCycle: action,
@@ -184,28 +181,29 @@ export class CycleStore implements ICycleStore {
* @param cycleId
* @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
* @param cycleId
* @returns
*/
getActiveCycleById = (cycleId: string): ICycle | null =>
this.activeCycleIdMap?.[cycleId] && this.cycleMap?.[cycleId] ? this.cycleMap?.[cycleId] : null;
getActiveCycleById = computedFn((cycleId: string): ICycle | null =>
this.activeCycleIdMap?.[cycleId] && this.cycleMap?.[cycleId] ? this.cycleMap?.[cycleId] : null
);
/**
* @description returns list of cycle ids of the project id passed as argument
* @param projectId
*/
getProjectCycleIds = (projectId: string): string[] | null => {
getProjectCycleIds = computedFn((projectId: string): string[] | null => {
if (!this.fetchedMap[projectId]) return null;
let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project === projectId);
cycles = sortBy(cycles, [(c) => !c.is_favorite, (c) => c.name.toLowerCase()]);
const cycleIds = cycles.map((c) => c.id);
return cycleIds || null;
};
});
/**
* @description validates cycle dates
+3 -4
View File
@@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import set from "lodash/set";
// services
import { DashboardService } from "services/dashboard.service";
@@ -74,8 +75,6 @@ export class DashboardStore implements IDashboardStore {
widgetStats: observable,
// computed
homeDashboardWidgets: computed,
// computed actions
getWidgetDetails: action,
// fetch actions
fetchHomeDashboardWidgets: action,
fetchWidgetStats: action,
@@ -109,11 +108,11 @@ export class DashboardStore implements IDashboardStore {
* @param widgetId
* @returns widget details
*/
getWidgetDetails = (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => {
getWidgetDetails = computedFn((workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => {
const widgets = this.widgetDetails?.[workspaceSlug]?.[dashboardId];
if (!widgets) return undefined;
return widgets.find((widget) => widget.key === widgetKey);
};
});
/**
* @description fetch home dashboard details and widgets
+58 -33
View File
@@ -5,21 +5,25 @@ import { ProjectEstimateService } from "services/project";
// types
import { RootStore } from "store/root.store";
import { IEstimate, IEstimateFormData } from "@plane/types";
import { computedFn } from "mobx-utils";
export interface IEstimateStore {
//Loaders
fetchedMap: Record<string, boolean>;
// observables
estimates: Record<string, IEstimate[] | null>;
estimateMap: Record<string, IEstimate>;
// computed
areEstimatesEnabledForCurrentProject: boolean;
projectEstimates: IEstimate[] | null;
activeEstimateDetails: IEstimate | null;
// computed actions
areEstimatesEnabledForProject: (projectId: string) => boolean;
getEstimatePointValue: (estimateKey: number | null) => string;
getEstimatePointValue: (estimateKey: number | null, projectId?: string) => string;
getProjectEstimateById: (estimateId: string) => IEstimate | null;
getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null;
// fetch actions
fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<IEstimate[]>;
fetchWorskpaceEstimates: (workspaceSlug: string) => Promise<IEstimate[]>;
// crud actions
createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise<IEstimate>;
updateEstimate: (
@@ -33,7 +37,9 @@ export interface IEstimateStore {
export class EstimateStore implements IEstimateStore {
// observables
estimates: Record<string, IEstimate[] | null> = {};
estimateMap: Record<string, IEstimate> = {};
//loaders
fetchedMap: Record<string, boolean> = {};
// root store
rootStore;
// services
@@ -42,18 +48,15 @@ export class EstimateStore implements IEstimateStore {
constructor(_rootStore: RootStore) {
makeObservable(this, {
// observables
estimates: observable,
estimateMap: observable,
fetchedMap: observable,
// computed
areEstimatesEnabledForCurrentProject: computed,
projectEstimates: computed,
activeEstimateDetails: computed,
// computed actions
areEstimatesEnabledForProject: action,
getProjectEstimateById: action,
getEstimatePointValue: action,
getProjectActiveEstimateDetails: action,
// actions
fetchProjectEstimates: action,
fetchWorskpaceEstimates: action,
createEstimate: action,
updateEstimate: action,
deleteEstimate: action,
@@ -79,8 +82,9 @@ export class EstimateStore implements IEstimateStore {
*/
get projectEstimates() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return null;
return this.estimates?.[projectId] || null;
const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
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() {
const currentProjectDetails = this.rootStore.projectRoot.project.currentProjectDetails;
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
* @param projectId
*/
areEstimatesEnabledForProject = (projectId: string) => {
areEstimatesEnabledForProject = computedFn((projectId: string) => {
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId);
if (!projectDetails) return false;
return Boolean(projectDetails.estimate) ?? false;
};
});
/**
* @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";
const activeEstimate = this.activeEstimateDetails;
const activeEstimate = projectId ? this.getProjectActiveEstimateDetails(projectId) : this.activeEstimateDetails;
return activeEstimate?.points?.find((point) => point.key === estimateKey)?.value || "None";
};
});
/**
* @description returns the estimate details for the given estimate id
* @param estimateId
*/
getProjectEstimateById = (estimateId: string) => {
getProjectEstimateById = computedFn((estimateId: string) => {
if (!this.projectEstimates) return null;
const estimateInfo = this.projectEstimates?.find((estimate) => estimate.id === estimateId) || null;
const estimateInfo = this.estimateMap?.[estimateId] || null;
return estimateInfo;
};
});
/**
* @description returns the estimate details for the given estimate id
* @param projectId
*/
getProjectActiveEstimateDetails = (projectId: string) => {
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId);
if (!projectDetails || !projectDetails?.estimate) return null;
return this.projectEstimates?.find((estimate) => estimate.id === projectDetails?.estimate) || null;
};
getProjectActiveEstimateDetails = computedFn((projectId: string) => {
const projectDetails = this.rootStore.projectRoot.project?.getProjectById(projectId);
const worksapceSlug = this.rootStore.app.router.workspaceSlug || "";
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
@@ -139,7 +145,26 @@ export class EstimateStore implements IEstimateStore {
fetchProjectEstimates = async (workspaceSlug: string, projectId: string) =>
await this.estimateService.getEstimatesList(workspaceSlug, projectId).then((response) => {
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;
});
@@ -157,7 +182,7 @@ export class EstimateStore implements IEstimateStore {
points: response.estimate_points,
};
runInAction(() => {
set(this.estimates, projectId, [responseEstimate, ...(this.estimates?.[projectId] || [])]);
set(this.estimateMap, [responseEstimate.id], responseEstimate);
});
return response.estimate;
});
@@ -171,11 +196,12 @@ export class EstimateStore implements IEstimateStore {
*/
updateEstimate = async (workspaceSlug: string, projectId: string, estimateId: string, data: IEstimateFormData) =>
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(() => {
set(this.estimates, projectId, updatedEstimates);
set(this.estimateMap, estimateId, {
...this.estimateMap[estimateId],
...data.estimate,
points: [...data.estimate_points],
});
});
return response;
});
@@ -188,9 +214,8 @@ export class EstimateStore implements IEstimateStore {
*/
deleteEstimate = async (workspaceSlug: string, projectId: string, estimateId: string) =>
await this.estimateService.deleteEstimate(workspaceSlug, projectId, estimateId).then(() => {
const updatedEstimates = (this.estimates?.[projectId] ?? []).filter((estimate) => estimate.id !== estimateId);
runInAction(() => {
set(this.estimates, projectId, updatedEstimates);
delete this.estimateMap[estimateId];
});
});
}
+4 -6
View File
@@ -1,4 +1,5 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { computedFn } from "mobx-utils";
import { set } from "lodash";
// services
import { WorkspaceService } from "services/workspace.service";
@@ -37,9 +38,6 @@ export class GlobalViewStore implements IGlobalViewStore {
globalViewMap: observable,
// computed
currentWorkspaceViews: computed,
// computed actions
getSearchedViews: action,
getViewDetailsById: action,
// actions
fetchAllGlobalViews: action,
fetchGlobalViewDetails: action,
@@ -73,7 +71,7 @@ export class GlobalViewStore implements IGlobalViewStore {
* @param searchQuery
* @returns
*/
getSearchedViews = (searchQuery: string) => {
getSearchedViews = computedFn((searchQuery: string) => {
const currentWorkspaceDetails = this.rootStore.workspaceRoot.currentWorkspace;
if (!currentWorkspaceDetails) return null;
@@ -84,13 +82,13 @@ export class GlobalViewStore implements IGlobalViewStore {
this.globalViewMap[viewId]?.name?.toLowerCase().includes(searchQuery.toLowerCase())
) ?? null
);
};
});
/**
* @description returns view details for given 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
+3 -4
View File
@@ -1,4 +1,5 @@
import { observable, action, makeObservable, runInAction, computed } from "mobx";
import { computedFn } from "mobx-utils";
import { set } from "lodash";
// services
import { InboxService } from "services/inbox.service";
@@ -43,8 +44,6 @@ export class InboxStore implements IInboxStore {
inboxDetails: observable,
// computed
isInboxEnabled: computed,
// computed actions
getInboxId: action,
// actions
fetchInboxesList: action,
});
@@ -69,11 +68,11 @@ export class InboxStore implements IInboxStore {
/**
* Returns the inbox Id belongs to a specific project
*/
getInboxId = (projectId: string) => {
getInboxId = computedFn((projectId: string) => {
const projectDetails = this.rootStore.projectRoot.project.getProjectById(projectId);
if (!projectDetails || !projectDetails.inbox_view) return null;
return this.inboxesList[projectId]?.[0]?.id ?? null;
};
});
/**
* Fetches the inboxes list belongs to a specific project
+4 -3
View File
@@ -1,4 +1,5 @@
import { observable, action, makeObservable, runInAction, autorun, computed } from "mobx";
import { computedFn } from "mobx-utils";
import { set } from "lodash";
// services
import { InboxService } from "services/inbox.service";
@@ -61,8 +62,6 @@ export class InboxIssuesStore implements IInboxIssuesStore {
issueMap: observable,
// computed
currentInboxIssueIds: computed,
// computed actions
getIssueById: action,
// fetch actions
fetchIssues: action,
fetchIssueDetails: action,
@@ -99,7 +98,9 @@ export class InboxIssuesStore implements IInboxIssuesStore {
/**
* 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
+9 -2
View File
@@ -1,6 +1,8 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import isEmpty from "lodash/isEmpty";
import set from "lodash/set";
import pickBy from "lodash/pickBy";
import isArray from "lodash/isArray";
// base class
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
// helpers
@@ -148,8 +150,13 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
set(this.filters, [projectId, "filters", _key], updatedFilters[_key as keyof IIssueFilterOptions]);
});
});
this.rootIssueStore.archivedIssues.fetchIssues(workspaceSlug, projectId, "mutation");
const appliedFilters = _filters.filters || {};
const filteredFilters = pickBy(appliedFilters, (value) => value && isArray(value) && value.length > 0);
this.rootIssueStore.archivedIssues.fetchIssues(
workspaceSlug,
projectId,
isEmpty(filteredFilters) ? "init-loader" : "mutation"
);
this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, {
filters: _filters.filters,
});
+10 -1
View File
@@ -1,6 +1,8 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import isEmpty from "lodash/isEmpty";
import set from "lodash/set";
import pickBy from "lodash/pickBy";
import isArray from "lodash/isArray";
// base class
import { IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
// helpers
@@ -158,7 +160,14 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
});
});
this.rootIssueStore.cycleIssues.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
const appliedFilters = _filters.filters || {};
const filteredFilters = pickBy(appliedFilters, (value) => value && isArray(value) && value.length > 0);
this.rootIssueStore.cycleIssues.fetchIssues(
workspaceSlug,
projectId,
isEmpty(filteredFilters) ? "init-loader" : "mutation",
cycleId
);
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
filters: _filters.filters,
});

Some files were not shown because too many files have changed in this diff Show More