Merge branch 'develop' of github.com:makeplane/plane into develop

This commit is contained in:
NarayanBavisetti 2023-12-05 14:54:59 +05:30
commit fb43cfffc2
28 changed files with 273 additions and 135 deletions

View File

@ -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",

View File

@ -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):

View File

@ -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",

View File

@ -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",
] ]

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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=[

View File

@ -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:

View File

@ -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

View File

@ -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>

View File

@ -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,

View File

@ -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>
); );

View File

@ -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";

View File

@ -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}

View File

@ -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}

View File

@ -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}
/> />

View File

@ -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

View File

@ -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}

View File

@ -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}

View 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>
</>
);
};

View File

@ -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}

View File

@ -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 ?? "") &&

View File

@ -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) => {

View File

@ -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>
)} )}

View File

@ -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>
)} )}

View File

@ -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>

View File

@ -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,
}; };