mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into develop
This commit is contained in:
commit
fb43cfffc2
@ -30,6 +30,11 @@ class CycleSerializer(BaseSerializer):
|
|||||||
model = Cycle
|
model = Cycle
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
"owned_by",
|
"owned_by",
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from lxml import html
|
||||||
|
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -43,7 +46,6 @@ class IssueSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
"workspace",
|
"workspace",
|
||||||
@ -53,6 +55,10 @@ class IssueSerializer(BaseSerializer):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
exclude = [
|
||||||
|
"description",
|
||||||
|
"description_stripped",
|
||||||
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if (
|
if (
|
||||||
@ -61,6 +67,15 @@ class IssueSerializer(BaseSerializer):
|
|||||||
and data.get("start_date", None) > data.get("target_date", None)
|
and data.get("start_date", None) > data.get("target_date", None)
|
||||||
):
|
):
|
||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if(data.get("description_html", None) is not None):
|
||||||
|
parsed = html.fromstring(data["description_html"])
|
||||||
|
parsed_str = html.tostring(parsed, encoding='unicode')
|
||||||
|
data["description_html"] = parsed_str
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
|
||||||
|
|
||||||
# Validate assignees are from project
|
# Validate assignees are from project
|
||||||
if data.get("assignees", []):
|
if data.get("assignees", []):
|
||||||
@ -292,7 +307,6 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IssueComment
|
model = IssueComment
|
||||||
fields = "__all__"
|
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
"workspace",
|
"workspace",
|
||||||
@ -303,6 +317,21 @@ class IssueCommentSerializer(BaseSerializer):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
exclude = [
|
||||||
|
"comment_stripped",
|
||||||
|
"comment_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
try:
|
||||||
|
if(data.get("comment_html", None) is not None):
|
||||||
|
parsed = html.fromstring(data["comment_html"])
|
||||||
|
parsed_str = html.tostring(parsed, encoding='unicode')
|
||||||
|
data["comment_html"] = parsed_str
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise serializers.ValidationError(f"Invalid HTML: {str(e)}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class IssueActivitySerializer(BaseSerializer):
|
class IssueActivitySerializer(BaseSerializer):
|
||||||
|
@ -21,6 +21,7 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id",
|
"id",
|
||||||
|
'emoji',
|
||||||
"workspace",
|
"workspace",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
@ -16,6 +16,11 @@ class StateSerializer(BaseSerializer):
|
|||||||
model = State
|
model = State
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
|
"id",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
"workspace",
|
"workspace",
|
||||||
"project",
|
"project",
|
||||||
]
|
]
|
||||||
|
@ -64,7 +64,7 @@ class StateAPIEndpoint(BaseAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if state.default:
|
if state.default:
|
||||||
return Response({"error": "Default state cannot be deleted"}, status=False)
|
return Response({"error": "Default state cannot be deleted"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Check for any issues in the state
|
# Check for any issues in the state
|
||||||
issue_exist = Issue.issue_objects.filter(state=state_id).exists()
|
issue_exist = Issue.issue_objects.filter(state=state_id).exists()
|
||||||
|
@ -77,7 +77,7 @@ class StateViewSet(BaseViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if state.default:
|
if state.default:
|
||||||
return Response({"error": "Default state cannot be deleted"}, status=False)
|
return Response({"error": "Default state cannot be deleted"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Check for any issues in the state
|
# Check for any issues in the state
|
||||||
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
issue_exist = Issue.issue_objects.filter(state=pk).exists()
|
||||||
|
@ -109,7 +109,7 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
|||||||
if webhook.secret_key:
|
if webhook.secret_key:
|
||||||
hmac_signature = hmac.new(
|
hmac_signature = hmac.new(
|
||||||
webhook.secret_key.encode("utf-8"),
|
webhook.secret_key.encode("utf-8"),
|
||||||
json.dumps(payload, sort_keys=True).encode("utf-8"),
|
json.dumps(payload).encode("utf-8"),
|
||||||
hashlib.sha256,
|
hashlib.sha256,
|
||||||
)
|
)
|
||||||
signature = hmac_signature.hexdigest()
|
signature = hmac_signature.hexdigest()
|
||||||
|
@ -290,7 +290,7 @@ CELERY_IMPORTS = (
|
|||||||
|
|
||||||
# Sentry Settings
|
# Sentry Settings
|
||||||
# Enable Sentry Settings
|
# Enable Sentry Settings
|
||||||
if bool(os.environ.get("SENTRY_DSN", False)):
|
if bool(os.environ.get("SENTRY_DSN", False)) and os.environ.get("SENTRY_DSN").startswith("https://"):
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn=os.environ.get("SENTRY_DSN", ""),
|
dsn=os.environ.get("SENTRY_DSN", ""),
|
||||||
integrations=[
|
integrations=[
|
||||||
|
@ -63,7 +63,7 @@ def date_filter(filter, date_term, queries):
|
|||||||
duration=int(digit),
|
duration=int(digit),
|
||||||
subsequent=date_query[1],
|
subsequent=date_query[1],
|
||||||
term=term,
|
term=term,
|
||||||
date_filter="created_at__date",
|
date_filter=date_term,
|
||||||
offset=date_query[2],
|
offset=date_query[2],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -38,3 +38,4 @@ beautifulsoup4==4.12.2
|
|||||||
dj-database-url==2.1.0
|
dj-database-url==2.1.0
|
||||||
posthog==3.0.2
|
posthog==3.0.2
|
||||||
cryptography==41.0.5
|
cryptography==41.0.5
|
||||||
|
lxml==4.9.3
|
||||||
|
@ -52,7 +52,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, peekCycle } = router.query;
|
const { workspaceSlug, projectId, peekCycle } = router.query;
|
||||||
|
|
||||||
const { cycle: cycleDetailsStore, trackEvent: { setTrackElement, postHogEventTracker } } = useMobxStore();
|
const {
|
||||||
|
cycle: cycleDetailsStore,
|
||||||
|
trackEvent: { setTrackElement, postHogEventTracker },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
|
const cycleDetails = cycleDetailsStore.cycle_details[cycleId] ?? undefined;
|
||||||
|
|
||||||
@ -70,31 +73,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const submitChanges = (data: Partial<ICycle>) => {
|
const submitChanges = (data: Partial<ICycle>) => {
|
||||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||||
|
|
||||||
mutate<ICycle>(CYCLE_DETAILS(cycleId as string), (prevData) => ({ ...(prevData as ICycle), ...data }), false);
|
cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data);
|
||||||
|
|
||||||
cycleService
|
|
||||||
.patchCycle(workspaceSlug as string, projectId as string, cycleId as string, data)
|
|
||||||
.then((res) => {
|
|
||||||
mutate(CYCLE_DETAILS(cycleId as string));
|
|
||||||
postHogEventTracker(
|
|
||||||
"CYCLE_UPDATE",
|
|
||||||
{
|
|
||||||
...res,
|
|
||||||
state: "SUCCESS"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
postHogEventTracker(
|
|
||||||
"CYCLE_UPDATE",
|
|
||||||
{
|
|
||||||
state: "FAILED"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
@ -304,10 +283,10 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
cycleDetails.total_issues === 0
|
cycleDetails.total_issues === 0
|
||||||
? "0 Issue"
|
? "0 Issue"
|
||||||
: cycleDetails.total_issues === cycleDetails.completed_issues
|
: cycleDetails.total_issues === cycleDetails.completed_issues
|
||||||
? cycleDetails.total_issues > 1
|
? cycleDetails.total_issues > 1
|
||||||
? `${cycleDetails.total_issues}`
|
? `${cycleDetails.total_issues}`
|
||||||
: `${cycleDetails.total_issues}`
|
: `${cycleDetails.total_issues}`
|
||||||
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -337,11 +316,12 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
</button>
|
</button>
|
||||||
{!isCompleted && (
|
{!isCompleted && (
|
||||||
<CustomMenu width="lg" placement="bottom-end" ellipsis>
|
<CustomMenu width="lg" placement="bottom-end" ellipsis>
|
||||||
<CustomMenu.MenuItem onClick={() => {
|
<CustomMenu.MenuItem
|
||||||
setTrackElement("CYCLE_PAGE_SIDEBAR");
|
onClick={() => {
|
||||||
setCycleDeleteModal(true)
|
setTrackElement("CYCLE_PAGE_SIDEBAR");
|
||||||
}
|
setCycleDeleteModal(true);
|
||||||
}>
|
}}
|
||||||
|
>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<Trash2 className="h-3 w-3" />
|
<Trash2 className="h-3 w-3" />
|
||||||
<span>Delete cycle</span>
|
<span>Delete cycle</span>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import Router, { useRouter } from "next/router";
|
import Router, { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -8,10 +8,10 @@ import { AlertTriangle, CheckCircle2, Clock, Copy, ExternalLink, Inbox, XCircle
|
|||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// components
|
// components
|
||||||
import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction } from "components/issues";
|
import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction, IssueUpdateStatus } from "components/issues";
|
||||||
import { InboxIssueActivity } from "components/inbox";
|
import { InboxIssueActivity } from "components/inbox";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
@ -31,7 +31,15 @@ export const InboxMainContent: React.FC = observer(() => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
||||||
|
|
||||||
const { inboxIssues: inboxIssuesStore, inboxIssueDetails: inboxIssueDetailsStore, user: userStore } = useMobxStore();
|
// states
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
|
|
||||||
|
const {
|
||||||
|
inboxIssues: inboxIssuesStore,
|
||||||
|
inboxIssueDetails: inboxIssueDetailsStore,
|
||||||
|
user: userStore,
|
||||||
|
projectState: { states },
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const user = userStore.currentUser;
|
const user = userStore.currentUser;
|
||||||
const userRole = userStore.currentProjectRole;
|
const userRole = userStore.currentProjectRole;
|
||||||
@ -55,6 +63,9 @@ export const InboxMainContent: React.FC = observer(() => {
|
|||||||
|
|
||||||
const issuesList = inboxId ? inboxIssuesStore.inboxIssues[inboxId.toString()] : undefined;
|
const issuesList = inboxId ? inboxIssuesStore.inboxIssues[inboxId.toString()] : undefined;
|
||||||
const issueDetails = inboxIssueId ? inboxIssueDetailsStore.issueDetails[inboxIssueId.toString()] : undefined;
|
const issueDetails = inboxIssueId ? inboxIssueDetailsStore.issueDetails[inboxIssueId.toString()] : undefined;
|
||||||
|
const currentIssueState = projectId
|
||||||
|
? states[projectId.toString()]?.find((s) => s.id === issueDetails?.state)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const submitChanges = useCallback(
|
const submitChanges = useCallback(
|
||||||
async (formData: Partial<IInboxIssue>) => {
|
async (formData: Partial<IInboxIssue>) => {
|
||||||
@ -217,8 +228,20 @@ export const InboxMainContent: React.FC = observer(() => {
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center mb-5">
|
||||||
|
{currentIssueState && (
|
||||||
|
<StateGroupIcon
|
||||||
|
className="h-4 w-4 mr-3"
|
||||||
|
stateGroup={currentIssueState.group}
|
||||||
|
color={currentIssueState.color}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issueDetails} />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IssueDescriptionForm
|
<IssueDescriptionForm
|
||||||
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
workspaceSlug={workspaceSlug as string}
|
workspaceSlug={workspaceSlug as string}
|
||||||
issue={{
|
issue={{
|
||||||
name: issueDetails.name,
|
name: issueDetails.name,
|
||||||
|
@ -26,14 +26,15 @@ export interface IssueDetailsProps {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
|
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
|
||||||
isAllowed: boolean;
|
isAllowed: boolean;
|
||||||
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
|
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||||
const { issue, handleFormSubmit, workspaceSlug, isAllowed } = props;
|
const { issue, handleFormSubmit, workspaceSlug, isAllowed, isSubmitting, setIsSubmitting } = props;
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
|
|
||||||
const { setShowAlert } = useReloadConfirmations();
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
@ -166,13 +167,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
|
||||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -15,6 +15,7 @@ export * from "./sidebar";
|
|||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./issue-reaction";
|
export * from "./issue-reaction";
|
||||||
export * from "./confirm-issue-discard";
|
export * from "./confirm-issue-discard";
|
||||||
|
export * from "./issue-update-status";
|
||||||
|
|
||||||
// draft issue
|
// draft issue
|
||||||
export * from "./draft-issue-form";
|
export * from "./draft-issue-form";
|
||||||
|
@ -224,7 +224,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
quickAddCallback={issueStore?.quickAddIssue}
|
quickAddCallback={issueStore?.quickAddIssue}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
disableIssueCreation={!enableIssueCreation}
|
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||||
isReadOnly={!enableInlineEditing || !isEditingAllowed}
|
isReadOnly={!enableInlineEditing || !isEditingAllowed}
|
||||||
currentStore={currentStore}
|
currentStore={currentStore}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
// components
|
// components
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
import { CreateUpdateIssueModal } from "components/issues/modal";
|
import { CreateUpdateIssueModal } from "components/issues/modal";
|
||||||
|
import { CreateUpdateDraftIssueModal } from "components/issues/draft-issue-modal";
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
// lucide icons
|
// lucide icons
|
||||||
import { Minimize2, Maximize2, Circle, Plus } from "lucide-react";
|
import { Minimize2, Maximize2, Circle, Plus } from "lucide-react";
|
||||||
@ -51,6 +52,8 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, moduleId, cycleId } = router.query;
|
const { workspaceSlug, projectId, moduleId, cycleId } = router.query;
|
||||||
|
|
||||||
|
const isDraftIssue = router.pathname.includes("draft-issue");
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const renderExistingIssueModal = moduleId || cycleId;
|
const renderExistingIssueModal = moduleId || cycleId;
|
||||||
@ -73,12 +76,21 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateIssueModal
|
{isDraftIssue ? (
|
||||||
isOpen={isOpen}
|
<CreateUpdateDraftIssueModal
|
||||||
handleClose={() => setIsOpen(false)}
|
isOpen={isOpen}
|
||||||
prePopulateData={issuePayload}
|
handleClose={() => setIsOpen(false)}
|
||||||
currentStore={currentStore}
|
prePopulateData={issuePayload}
|
||||||
/>
|
fieldsToShow={["all"]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CreateUpdateIssueModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
handleClose={() => setIsOpen(false)}
|
||||||
|
prePopulateData={issuePayload}
|
||||||
|
currentStore={currentStore}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{renderExistingIssueModal && (
|
{renderExistingIssueModal && (
|
||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
isOpen={openExistingIssueListModal}
|
isOpen={openExistingIssueListModal}
|
||||||
|
@ -148,7 +148,7 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
quickAddCallback={issueStore?.quickAddIssue}
|
quickAddCallback={issueStore?.quickAddIssue}
|
||||||
enableIssueQuickAdd={!!enableQuickAdd}
|
enableIssueQuickAdd={!!enableQuickAdd}
|
||||||
isReadonly={!enableInlineEditing || !isEditingAllowed}
|
isReadonly={!enableInlineEditing || !isEditingAllowed}
|
||||||
disableIssueCreation={!enableIssueCreation}
|
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||||
currentStore={currentStore}
|
currentStore={currentStore}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
/>
|
/>
|
||||||
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
// lucide icons
|
// lucide icons
|
||||||
import { CircleDashed, Plus } from "lucide-react";
|
import { CircleDashed, Plus } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
|
import { CreateUpdateDraftIssueModal } from "components/issues/draft-issue-modal";
|
||||||
import { CreateUpdateIssueModal } from "components/issues/modal";
|
import { CreateUpdateIssueModal } from "components/issues/modal";
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
import { CustomMenu } from "@plane/ui";
|
import { CustomMenu } from "@plane/ui";
|
||||||
@ -32,6 +33,8 @@ export const HeaderGroupByCard = observer(
|
|||||||
|
|
||||||
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
|
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
|
||||||
|
|
||||||
|
const isDraftIssue = router.pathname.includes("draft-issue");
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const renderExistingIssueModal = moduleId || cycleId;
|
const renderExistingIssueModal = moduleId || cycleId;
|
||||||
@ -90,12 +93,21 @@ export const HeaderGroupByCard = observer(
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<CreateUpdateIssueModal
|
{isDraftIssue ? (
|
||||||
isOpen={isOpen}
|
<CreateUpdateDraftIssueModal
|
||||||
handleClose={() => setIsOpen(false)}
|
isOpen={isOpen}
|
||||||
currentStore={currentStore}
|
handleClose={() => setIsOpen(false)}
|
||||||
prePopulateData={issuePayload}
|
prePopulateData={issuePayload}
|
||||||
/>
|
fieldsToShow={["all"]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CreateUpdateIssueModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
handleClose={() => setIsOpen(false)}
|
||||||
|
currentStore={currentStore}
|
||||||
|
prePopulateData={issuePayload}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{renderExistingIssueModal && (
|
{renderExistingIssueModal && (
|
||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
|
@ -26,16 +26,27 @@ interface IPeekOverviewIssueDetails {
|
|||||||
issueUpdate: (issue: Partial<IIssue>) => void;
|
issueUpdate: (issue: Partial<IIssue>) => void;
|
||||||
issueReactionCreate: (reaction: string) => void;
|
issueReactionCreate: (reaction: string) => void;
|
||||||
issueReactionRemove: (reaction: string) => void;
|
issueReactionRemove: (reaction: string) => void;
|
||||||
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
|
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) => {
|
||||||
const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props;
|
const {
|
||||||
|
workspaceSlug,
|
||||||
|
issue,
|
||||||
|
issueReactions,
|
||||||
|
user,
|
||||||
|
issueUpdate,
|
||||||
|
issueReactionCreate,
|
||||||
|
issueReactionRemove,
|
||||||
|
isSubmitting,
|
||||||
|
setIsSubmitting,
|
||||||
|
} = props;
|
||||||
// store
|
// store
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
const { currentProjectRole } = userStore;
|
const { currentProjectRole } = userStore;
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
// hooks
|
// hooks
|
||||||
const { setShowAlert } = useReloadConfirmations();
|
const { setShowAlert } = useReloadConfirmations();
|
||||||
@ -172,13 +183,6 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
className={`absolute right-5 bottom-5 text-xs text-custom-text-200 border border-custom-border-400 rounded-xl w-[6.5rem] py-1 z-10 flex items-center justify-center ${
|
|
||||||
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isSubmitting === "submitting" ? "Saving..." : "Saved"}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
issueReactions={issueReactions}
|
issueReactions={issueReactions}
|
||||||
|
@ -8,8 +8,7 @@ import { PeekOverviewIssueDetails } from "./issue-detail";
|
|||||||
import { PeekOverviewProperties } from "./properties";
|
import { PeekOverviewProperties } from "./properties";
|
||||||
import { IssueComment } from "./activity";
|
import { IssueComment } from "./activity";
|
||||||
import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
import { Button, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Spinner } from "@plane/ui";
|
||||||
import { DeleteIssueModal } from "../delete-issue-modal";
|
import { DeleteIssueModal, DeleteArchivedIssueModal, IssueUpdateStatus } from "components/issues/";
|
||||||
import { DeleteArchivedIssueModal } from "../delete-archived-issue-modal";
|
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
// hooks
|
// hooks
|
||||||
@ -93,6 +92,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
|
|
||||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
|
|
||||||
const updateRoutePeekId = () => {
|
const updateRoutePeekId = () => {
|
||||||
if (issueId != peekIssueId) {
|
if (issueId != peekIssueId) {
|
||||||
@ -216,33 +216,35 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-x-4">
|
||||||
<div className="flex items-center gap-4">
|
<IssueUpdateStatus isSubmitting={isSubmitting} />
|
||||||
{issue?.created_by !== user?.id &&
|
<div className="flex items-center gap-4">
|
||||||
!issue?.assignees.includes(user?.id ?? "") &&
|
{issue?.created_by !== user?.id &&
|
||||||
!router.pathname.includes("[archivedIssueId]") && (
|
!issue?.assignees.includes(user?.id ?? "") &&
|
||||||
<Button
|
!router.pathname.includes("[archivedIssueId]") && (
|
||||||
size="sm"
|
<Button
|
||||||
prependIcon={<Bell className="h-3 w-3" />}
|
size="sm"
|
||||||
variant="outline-primary"
|
prependIcon={<Bell className="h-3 w-3" />}
|
||||||
className="hover:!bg-custom-primary-100/20"
|
variant="outline-primary"
|
||||||
onClick={() =>
|
className="hover:!bg-custom-primary-100/20"
|
||||||
issueSubscription && issueSubscription.subscribed
|
onClick={() =>
|
||||||
? issueSubscriptionRemove()
|
issueSubscription && issueSubscription.subscribed
|
||||||
: issueSubscriptionCreate()
|
? issueSubscriptionRemove()
|
||||||
}
|
: issueSubscriptionCreate()
|
||||||
>
|
}
|
||||||
{issueSubscription && issueSubscription.subscribed ? "Unsubscribe" : "Subscribe"}
|
>
|
||||||
</Button>
|
{issueSubscription && issueSubscription.subscribed ? "Unsubscribe" : "Subscribe"}
|
||||||
)}
|
</Button>
|
||||||
<button onClick={handleCopyText}>
|
)}
|
||||||
<Link2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200 -rotate-45" />
|
<button onClick={handleCopyText}>
|
||||||
</button>
|
<Link2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200 -rotate-45" />
|
||||||
{!disableUserActions && (
|
|
||||||
<button onClick={() => setDeleteIssueModal(true)}>
|
|
||||||
<Trash2 className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
{!disableUserActions && (
|
||||||
|
<button onClick={() => setDeleteIssueModal(true)}>
|
||||||
|
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -261,6 +263,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
<div className="absolute top-0 left-0 h-full min-h-full w-full z-[9] flex items-center justify-center bg-custom-background-100 opacity-60" />
|
<div className="absolute top-0 left-0 h-full min-h-full w-full z-[9] flex items-center justify-center bg-custom-background-100 opacity-60" />
|
||||||
)}
|
)}
|
||||||
<PeekOverviewIssueDetails
|
<PeekOverviewIssueDetails
|
||||||
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueUpdate={issueUpdate}
|
issueUpdate={issueUpdate}
|
||||||
@ -295,6 +299,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
<div className="relative w-full h-full space-y-6 p-4 py-5 overflow-auto">
|
<div className="relative w-full h-full space-y-6 p-4 py-5 overflow-auto">
|
||||||
<div className={isArchived ? "pointer-events-none" : ""}>
|
<div className={isArchived ? "pointer-events-none" : ""}>
|
||||||
<PeekOverviewIssueDetails
|
<PeekOverviewIssueDetails
|
||||||
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueReactions={issueReactions}
|
issueReactions={issueReactions}
|
||||||
|
32
web/components/issues/issue-update-status.tsx
Normal file
32
web/components/issues/issue-update-status.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { RefreshCw } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
|
issueDetail?: IIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueUpdateStatus: React.FC<Props> = (props) => {
|
||||||
|
const { isSubmitting, issueDetail } = props;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{issueDetail && (
|
||||||
|
<h4 className="text-lg text-custom-text-300 font-medium mr-4">
|
||||||
|
{issueDetail.project_detail?.identifier}-{issueDetail.sequence_id}
|
||||||
|
</h4>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`flex transition-all duration-300 items-center gap-x-2 ${
|
||||||
|
isSubmitting === "saved" ? "fadeOut" : "fadeIn"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSubmitting !== "submitted" && isSubmitting !== "saved" && (
|
||||||
|
<RefreshCw className="h-4 w-4 stroke-custom-text-300" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-custom-text-300">{isSubmitting === "submitting" ? "Saving..." : "Saved"}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -2,6 +2,7 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
import { MinusCircle } from "lucide-react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
@ -16,12 +17,12 @@ import {
|
|||||||
IssueAttachments,
|
IssueAttachments,
|
||||||
IssueDescriptionForm,
|
IssueDescriptionForm,
|
||||||
IssueReaction,
|
IssueReaction,
|
||||||
|
IssueUpdateStatus,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
|
import { useState } from "react";
|
||||||
import { SubIssuesRoot } from "./sub-issues";
|
import { SubIssuesRoot } from "./sub-issues";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, LayersIcon } from "@plane/ui";
|
import { CustomMenu, LayersIcon, StateGroupIcon } from "@plane/ui";
|
||||||
// icons
|
|
||||||
import { MinusCircle } from "lucide-react";
|
|
||||||
// types
|
// types
|
||||||
import { IIssue, IIssueComment } from "types";
|
import { IIssue, IIssueComment } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
@ -41,15 +42,25 @@ const issueCommentService = new IssueCommentService();
|
|||||||
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
const { issueDetails, submitChanges, uneditable = false } = props;
|
const { issueDetails, submitChanges, uneditable = false } = props;
|
||||||
|
|
||||||
|
// states
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const { user: userStore, project: projectStore } = useMobxStore();
|
const {
|
||||||
|
user: userStore,
|
||||||
|
project: projectStore,
|
||||||
|
projectState: { states },
|
||||||
|
} = useMobxStore();
|
||||||
const user = userStore.currentUser ?? undefined;
|
const user = userStore.currentUser ?? undefined;
|
||||||
const userRole = userStore.currentProjectRole;
|
const userRole = userStore.currentProjectRole;
|
||||||
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined;
|
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : undefined;
|
||||||
|
const currentIssueState = projectId
|
||||||
|
? states[projectId.toString()]?.find((s) => s.id === issueDetails.state)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const { data: siblingIssues } = useSWR(
|
const { data: siblingIssues } = useSWR(
|
||||||
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
|
workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null,
|
||||||
@ -165,7 +176,19 @@ export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<div className="flex items-center mb-5">
|
||||||
|
{currentIssueState && (
|
||||||
|
<StateGroupIcon
|
||||||
|
className="h-4 w-4 mr-3"
|
||||||
|
stateGroup={currentIssueState.group}
|
||||||
|
color={currentIssueState.color}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issueDetails} />
|
||||||
|
</div>
|
||||||
<IssueDescriptionForm
|
<IssueDescriptionForm
|
||||||
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
workspaceSlug={workspaceSlug as string}
|
workspaceSlug={workspaceSlug as string}
|
||||||
issue={issueDetails}
|
issue={issueDetails}
|
||||||
handleFormSubmit={submitChanges}
|
handleFormSubmit={submitChanges}
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react";
|
import { Bell, CalendarDays, LinkIcon, Plus, Signal, Tag, Trash2, Triangle, User2 } from "lucide-react";
|
||||||
import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, UserGroupIcon } from "@plane/ui";
|
import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
@ -80,12 +80,15 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const [linkModal, setLinkModal] = useState(false);
|
const [linkModal, setLinkModal] = useState(false);
|
||||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<linkDetails | null>(null);
|
||||||
|
|
||||||
const { user: userStore } = useMobxStore();
|
const {
|
||||||
|
user: userStore,
|
||||||
|
projectState: { states },
|
||||||
|
} = useMobxStore();
|
||||||
const user = userStore.currentUser;
|
const user = userStore.currentUser;
|
||||||
const userRole = userStore.currentProjectRole;
|
const userRole = userStore.currentProjectRole;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId, inboxIssueId } = router.query;
|
||||||
|
|
||||||
const { isEstimateActive } = useEstimateOption();
|
const { isEstimateActive } = useEstimateOption();
|
||||||
|
|
||||||
@ -248,6 +251,10 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
|
|
||||||
|
const currentIssueState = projectId
|
||||||
|
? states[projectId.toString()]?.find((s) => s.id === issueDetail?.state)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LinkModal
|
<LinkModal
|
||||||
@ -266,9 +273,20 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
<div className="h-full w-full flex flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-5 pb-3">
|
<div className="flex items-center justify-between px-5 pb-3">
|
||||||
<h4 className="text-sm font-medium">
|
<div className="flex items-center gap-x-2">
|
||||||
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
{currentIssueState ? (
|
||||||
</h4>
|
<StateGroupIcon
|
||||||
|
className="h-4 w-4"
|
||||||
|
stateGroup={currentIssueState.group}
|
||||||
|
color={currentIssueState.color}
|
||||||
|
/>
|
||||||
|
) : inboxIssueId ? (
|
||||||
|
<StateGroupIcon className="h-4 w-4" stateGroup="backlog" color="#ff7700" />
|
||||||
|
) : null}
|
||||||
|
<h4 className="text-lg text-custom-text-300 font-medium">
|
||||||
|
{issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{issueDetail?.created_by !== user?.id &&
|
{issueDetail?.created_by !== user?.id &&
|
||||||
!issueDetail?.assignees.includes(user?.id ?? "") &&
|
!issueDetail?.assignees.includes(user?.id ?? "") &&
|
||||||
|
@ -75,20 +75,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const submitChanges = (data: Partial<IModule>) => {
|
const submitChanges = (data: Partial<IModule>) => {
|
||||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||||
|
moduleStore.updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, data);
|
||||||
mutate<IModule>(
|
|
||||||
MODULE_DETAILS(moduleId as string),
|
|
||||||
(prevData) => ({
|
|
||||||
...(prevData as IModule),
|
|
||||||
...data,
|
|
||||||
}),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
moduleService
|
|
||||||
.patchModule(workspaceSlug as string, projectId as string, moduleId as string, data)
|
|
||||||
.then(() => mutate(MODULE_DETAILS(moduleId as string)))
|
|
||||||
.catch((e) => console.log(e));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateLink = async (formData: ModuleLink) => {
|
const handleCreateLink = async (formData: ModuleLink) => {
|
||||||
|
@ -33,6 +33,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
const {
|
const {
|
||||||
user: { currentUser, currentProjectMemberInfo, currentProjectRole, leaveProject },
|
user: { currentUser, currentProjectMemberInfo, currentProjectRole, leaveProject },
|
||||||
projectMember: { removeMemberFromProject, updateMember },
|
projectMember: { removeMemberFromProject, updateMember },
|
||||||
|
project: { fetchProjects },
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// hooks
|
// hooks
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
@ -46,7 +47,11 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
if (memberDetails.id === currentUser?.id) {
|
if (memberDetails.id === currentUser?.id) {
|
||||||
await leaveProject(workspaceSlug.toString(), projectId.toString())
|
await leaveProject(workspaceSlug.toString(), projectId.toString())
|
||||||
.then(() => router.push(`/${workspaceSlug}/projects`))
|
.then(async () => {
|
||||||
|
await fetchProjects(workspaceSlug.toString());
|
||||||
|
|
||||||
|
router.push(`/${workspaceSlug}/projects`);
|
||||||
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
@ -174,7 +179,7 @@ export const ProjectMemberListItem: React.FC<Props> = observer((props) => {
|
|||||||
onClick={() => setRemoveMemberModal(true)}
|
onClick={() => setRemoveMemberModal(true)}
|
||||||
className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
|
className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto"
|
||||||
>
|
>
|
||||||
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
|
<XCircle className="h-3.5 w-3.5 text-red-500" strokeWidth={2} />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
@ -284,7 +284,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
|||||||
<CustomMenu.MenuItem onClick={handleLeaveProject}>
|
<CustomMenu.MenuItem onClick={handleLeaveProject}>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
|
<LogOut className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||||
<span>Leave Project</span>
|
<span>Leave project</span>
|
||||||
</div>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
|
@ -243,7 +243,7 @@ export const WorkspaceMembersListItem: FC<Props> = observer((props) => {
|
|||||||
: "opacity-0 pointer-events-none"
|
: "opacity-0 pointer-events-none"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<XCircle className="h-3.5 w-3.5 text-custom-text-400" strokeWidth={2} />
|
<XCircle className="h-3.5 w-3.5 text-red-500" strokeWidth={2} />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ export class ProjectDraftIssuesStore extends IssueBaseStore implements IProjectD
|
|||||||
//viewData
|
//viewData
|
||||||
viewFlags = {
|
viewFlags = {
|
||||||
enableQuickAdd: false,
|
enableQuickAdd: false,
|
||||||
enableIssueCreation: false,
|
enableIssueCreation: true,
|
||||||
enableInlineEditing: false,
|
enableInlineEditing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user