Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6be2b3b8c5 | ||
|
|
d3fe57664a | ||
|
|
64fcb532fc | ||
|
|
f7586ba046 | ||
|
|
8288e1363c | ||
|
|
2bb0a89679 | ||
|
|
4761d4e0e9 | ||
|
|
ef5a09deba | ||
|
|
75965fc86e | ||
|
|
7e95b8f160 | ||
|
|
78c7a016e3 | ||
|
|
66de428987 | ||
|
|
e975abff21 | ||
|
|
f68e6023c3 | ||
|
|
67414983da | ||
|
|
ba27cc57bd | ||
|
|
37ddc64b83 |
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -47,6 +47,8 @@ from .workspace import (
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceProjectMemberEndpoint,
|
||||
WorkspaceUserPropertiesEndpoint,
|
||||
WorkspaceStatesEndpoint,
|
||||
WorkspaceEstimatesEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import (
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+2
-3
@@ -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,
|
||||
}),
|
||||
];
|
||||
|
||||
+12
-8
@@ -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 &&
|
||||
|
||||
+4
-3
@@ -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";
|
||||
|
||||
|
||||
Vendored
+2
-9
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+2
-9
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
+1
-3
@@ -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;
|
||||
|
||||
+1
-3
@@ -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;
|
||||
|
||||
+4
-3
@@ -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);
|
||||
|
||||
+1
-3
@@ -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;
|
||||
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -29,7 +29,7 @@ export class RouterStore implements IRouterStore {
|
||||
// observables
|
||||
query: observable,
|
||||
// actions
|
||||
setQuery: action,
|
||||
setQuery: action.bound,
|
||||
//computed
|
||||
workspaceSlug: computed,
|
||||
projectId: computed,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user