From e2524799d2abe2ab99d3b78fe7870e679dfa01f5 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:39:09 +0530 Subject: [PATCH 1/6] chore: html validation (#2970) * chore: changed api serializers * chore: state status code * chore: removed sorted keys --- apiserver/plane/api/serializers/cycle.py | 5 ++++ apiserver/plane/api/serializers/issue.py | 33 ++++++++++++++++++++-- apiserver/plane/api/serializers/project.py | 1 + apiserver/plane/api/serializers/state.py | 5 ++++ apiserver/plane/api/views/state.py | 2 +- apiserver/plane/app/views/state.py | 2 +- apiserver/plane/bgtasks/webhook_task.py | 2 +- apiserver/plane/utils/issue_filters.py | 2 +- apiserver/requirements/base.txt | 1 + 9 files changed, 47 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index 5895a1bfc..eaff8181a 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -30,6 +30,11 @@ class CycleSerializer(BaseSerializer): model = Cycle fields = "__all__" read_only_fields = [ + "id", + "created_at", + "updated_at", + "created_by", + "updated_by", "workspace", "project", "owned_by", diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 10b3a4f85..ab61ae523 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -1,3 +1,6 @@ +from lxml import html + + # Django imports from django.utils import timezone @@ -43,7 +46,6 @@ class IssueSerializer(BaseSerializer): class Meta: model = Issue - fields = "__all__" read_only_fields = [ "id", "workspace", @@ -53,6 +55,10 @@ class IssueSerializer(BaseSerializer): "created_at", "updated_at", ] + exclude = [ + "description", + "description_stripped", + ] def validate(self, data): if ( @@ -61,6 +67,15 @@ class IssueSerializer(BaseSerializer): and data.get("start_date", None) > data.get("target_date", None) ): 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 if data.get("assignees", []): @@ -292,7 +307,6 @@ class IssueCommentSerializer(BaseSerializer): class Meta: model = IssueComment - fields = "__all__" read_only_fields = [ "id", "workspace", @@ -303,6 +317,21 @@ class IssueCommentSerializer(BaseSerializer): "created_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): diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index 932597799..c394a080d 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -21,6 +21,7 @@ class ProjectSerializer(BaseSerializer): fields = "__all__" read_only_fields = [ "id", + 'emoji', "workspace", "created_at", "updated_at", diff --git a/apiserver/plane/api/serializers/state.py b/apiserver/plane/api/serializers/state.py index 4c7f05ab8..9d08193d8 100644 --- a/apiserver/plane/api/serializers/state.py +++ b/apiserver/plane/api/serializers/state.py @@ -16,6 +16,11 @@ class StateSerializer(BaseSerializer): model = State fields = "__all__" read_only_fields = [ + "id", + "created_by", + "updated_by", + "created_at", + "updated_at", "workspace", "project", ] diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index 679c12964..3d2861778 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -64,7 +64,7 @@ class StateAPIEndpoint(BaseAPIView): ) 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 issue_exist = Issue.issue_objects.filter(state=state_id).exists() diff --git a/apiserver/plane/app/views/state.py b/apiserver/plane/app/views/state.py index 5867edb68..f7226ba6e 100644 --- a/apiserver/plane/app/views/state.py +++ b/apiserver/plane/app/views/state.py @@ -77,7 +77,7 @@ class StateViewSet(BaseViewSet): ) 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 issue_exist = Issue.issue_objects.filter(state=pk).exists() diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index f5ee96256..3681f002d 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -109,7 +109,7 @@ def webhook_task(self, webhook, slug, event, event_data, action): if webhook.secret_key: hmac_signature = hmac.new( webhook.secret_key.encode("utf-8"), - json.dumps(payload, sort_keys=True).encode("utf-8"), + json.dumps(payload).encode("utf-8"), hashlib.sha256, ) signature = hmac_signature.hexdigest() diff --git a/apiserver/plane/utils/issue_filters.py b/apiserver/plane/utils/issue_filters.py index 75437fbee..2da24092a 100644 --- a/apiserver/plane/utils/issue_filters.py +++ b/apiserver/plane/utils/issue_filters.py @@ -63,7 +63,7 @@ def date_filter(filter, date_term, queries): duration=int(digit), subsequent=date_query[1], term=term, - date_filter="created_at__date", + date_filter=date_term, offset=date_query[2], ) else: diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 5342da85d..b6059bcd5 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -38,3 +38,4 @@ beautifulsoup4==4.12.2 dj-database-url==2.1.0 posthog==3.0.2 cryptography==41.0.5 +lxml==4.9.3 From 2cf847e8a70288d56d916eb8c91683e56a5d2012 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:40:29 +0530 Subject: [PATCH 2/6] chore: module and cycle sidebar date mutation fix (#2986) --- web/components/cycles/sidebar.tsx | 50 +++++++++--------------------- web/components/modules/sidebar.tsx | 15 +-------- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 924faf455..890a09f49 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -52,7 +52,10 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const router = useRouter(); 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; @@ -70,31 +73,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !cycleId) return; - mutate(CYCLE_DETAILS(cycleId as string), (prevData) => ({ ...(prevData as ICycle), ...data }), false); - - 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" - } - ); - } - ); + cycleDetailsStore.patchCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), data); }; const handleCopyText = () => { @@ -304,10 +283,10 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { cycleDetails.total_issues === 0 ? "0 Issue" : cycleDetails.total_issues === cycleDetails.completed_issues - ? cycleDetails.total_issues > 1 - ? `${cycleDetails.total_issues}` - : `${cycleDetails.total_issues}` - : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; + ? cycleDetails.total_issues > 1 + ? `${cycleDetails.total_issues}` + : `${cycleDetails.total_issues}` + : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; return ( <> @@ -337,11 +316,12 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { {!isCompleted && ( - { - setTrackElement("CYCLE_PAGE_SIDEBAR"); - setCycleDeleteModal(true) - } - }> + { + setTrackElement("CYCLE_PAGE_SIDEBAR"); + setCycleDeleteModal(true); + }} + > Delete cycle diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index a525124ee..f8813b80b 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -75,20 +75,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const submitChanges = (data: Partial) => { if (!workspaceSlug || !projectId || !moduleId) return; - - mutate( - 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)); + moduleStore.updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, data); }; const handleCreateLink = async (formData: ModuleLink) => { From 7b12d54d8335c71c056027cfec5f2eae697652e5 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:41:31 +0530 Subject: [PATCH 3/6] chore: draft issue layout and permission validation (#2982) * chore: create draft issue option added in draft issue layout and permission validation added * chore: create draft issue option added in draft issue list layout and permission validation added --- .../issue-layouts/kanban/base-kanban-root.tsx | 2 +- .../kanban/headers/group-by-card.tsx | 24 ++++++++++++++----- .../issue-layouts/list/base-list-root.tsx | 2 +- .../list/headers/group-by-card.tsx | 24 ++++++++++++++----- .../project-issues/draft/issue.store.ts | 2 +- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 66d33ad16..e5d279809 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -224,7 +224,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas isDragStarted={isDragStarted} quickAddCallback={issueStore?.quickAddIssue} viewId={viewId} - disableIssueCreation={!enableIssueCreation} + disableIssueCreation={!enableIssueCreation || !isEditingAllowed} isReadOnly={!enableInlineEditing || !isEditingAllowed} currentStore={currentStore} addIssuesToView={addIssuesToView} diff --git a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx index f58001402..8fc2c6e17 100644 --- a/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/kanban/headers/group-by-card.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; // components import { CustomMenu } from "@plane/ui"; import { CreateUpdateIssueModal } from "components/issues/modal"; +import { CreateUpdateDraftIssueModal } from "components/issues/draft-issue-modal"; import { ExistingIssuesListModal } from "components/core"; // lucide icons import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; @@ -51,6 +52,8 @@ export const HeaderGroupByCard: FC = observer((props) => { const router = useRouter(); const { workspaceSlug, projectId, moduleId, cycleId } = router.query; + const isDraftIssue = router.pathname.includes("draft-issue"); + const { setToastAlert } = useToast(); const renderExistingIssueModal = moduleId || cycleId; @@ -73,12 +76,21 @@ export const HeaderGroupByCard: FC = observer((props) => { return ( <> - setIsOpen(false)} - prePopulateData={issuePayload} - currentStore={currentStore} - /> + {isDraftIssue ? ( + setIsOpen(false)} + prePopulateData={issuePayload} + fieldsToShow={["all"]} + /> + ) : ( + setIsOpen(false)} + prePopulateData={issuePayload} + currentStore={currentStore} + /> + )} {renderExistingIssueModal && ( { quickAddCallback={issueStore?.quickAddIssue} enableIssueQuickAdd={!!enableQuickAdd} isReadonly={!enableInlineEditing || !isEditingAllowed} - disableIssueCreation={!enableIssueCreation} + disableIssueCreation={!enableIssueCreation || !isEditingAllowed} currentStore={currentStore} addIssuesToView={addIssuesToView} /> diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index c49d33d1e..24dbf435d 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; // components +import { CreateUpdateDraftIssueModal } from "components/issues/draft-issue-modal"; import { CreateUpdateIssueModal } from "components/issues/modal"; import { ExistingIssuesListModal } from "components/core"; import { CustomMenu } from "@plane/ui"; @@ -32,6 +33,8 @@ export const HeaderGroupByCard = observer( const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false); + const isDraftIssue = router.pathname.includes("draft-issue"); + const { setToastAlert } = useToast(); const renderExistingIssueModal = moduleId || cycleId; @@ -90,12 +93,21 @@ export const HeaderGroupByCard = observer( ))} - setIsOpen(false)} - currentStore={currentStore} - prePopulateData={issuePayload} - /> + {isDraftIssue ? ( + setIsOpen(false)} + prePopulateData={issuePayload} + fieldsToShow={["all"]} + /> + ) : ( + setIsOpen(false)} + currentStore={currentStore} + prePopulateData={issuePayload} + /> + )} {renderExistingIssueModal && ( Date: Tue, 5 Dec 2023 13:42:16 +0530 Subject: [PATCH 4/6] fix: sentry dsn error (#2981) --- apiserver/plane/settings/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 3eca1dee8..fff6b9e90 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -290,7 +290,7 @@ CELERY_IMPORTS = ( # 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( dsn=os.environ.get("SENTRY_DSN", ""), integrations=[ From fd73f18d951ed857db90a23e4a6b9600c709e19c Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:44:01 +0530 Subject: [PATCH 5/6] fix: leave project mutation (#2976) --- web/components/project/member-list-item.tsx | 9 +++++++-- web/components/project/sidebar-list-item.tsx | 2 +- web/components/workspace/settings/members-list-item.tsx | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index 576accee9..74ff74450 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -33,6 +33,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { const { user: { currentUser, currentProjectMemberInfo, currentProjectRole, leaveProject }, projectMember: { removeMemberFromProject, updateMember }, + project: { fetchProjects }, } = useMobxStore(); // hooks const { setToastAlert } = useToast(); @@ -46,7 +47,11 @@ export const ProjectMemberListItem: React.FC = observer((props) => { if (memberDetails.id === currentUser?.id) { await leaveProject(workspaceSlug.toString(), projectId.toString()) - .then(() => router.push(`/${workspaceSlug}/projects`)) + .then(async () => { + await fetchProjects(workspaceSlug.toString()); + + router.push(`/${workspaceSlug}/projects`); + }) .catch((err) => setToastAlert({ type: "error", @@ -174,7 +179,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { onClick={() => setRemoveMemberModal(true)} className="opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto" > - + )} diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index 488ae571a..5ad757e5f 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -284,7 +284,7 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
- Leave Project + Leave project
)} diff --git a/web/components/workspace/settings/members-list-item.tsx b/web/components/workspace/settings/members-list-item.tsx index 7536e78c9..751fc14e1 100644 --- a/web/components/workspace/settings/members-list-item.tsx +++ b/web/components/workspace/settings/members-list-item.tsx @@ -243,7 +243,7 @@ export const WorkspaceMembersListItem: FC = observer((props) => { : "opacity-0 pointer-events-none" } > - + From effad33f67621a987a08aa17bbdbadeb275c6111 Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:40:29 +0530 Subject: [PATCH 6/6] chore: issue update status in peekview & detail (#2985) --- web/components/inbox/main-content.tsx | 31 ++++++++-- web/components/issues/description-form.tsx | 12 +--- web/components/issues/index.ts | 1 + .../issue-peek-overview/issue-detail.tsx | 22 ++++--- .../issues/issue-peek-overview/view.tsx | 62 ++++++++++--------- web/components/issues/issue-update-status.tsx | 32 ++++++++++ web/components/issues/main-content.tsx | 31 ++++++++-- web/components/issues/sidebar.tsx | 30 +++++++-- 8 files changed, 161 insertions(+), 60 deletions(-) create mode 100644 web/components/issues/issue-update-status.tsx diff --git a/web/components/inbox/main-content.tsx b/web/components/inbox/main-content.tsx index 5d7863196..193f59263 100644 --- a/web/components/inbox/main-content.tsx +++ b/web/components/inbox/main-content.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import Router, { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; @@ -8,10 +8,10 @@ import { AlertTriangle, CheckCircle2, Clock, Copy, ExternalLink, Inbox, XCircle // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction } from "components/issues"; +import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction, IssueUpdateStatus } from "components/issues"; import { InboxIssueActivity } from "components/inbox"; // ui -import { Loader } from "@plane/ui"; +import { Loader, StateGroupIcon } from "@plane/ui"; // helpers import { renderShortDateWithYearFormat } from "helpers/date-time.helper"; // types @@ -31,7 +31,15 @@ export const InboxMainContent: React.FC = observer(() => { const router = useRouter(); 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 userRole = userStore.currentProjectRole; @@ -55,6 +63,9 @@ export const InboxMainContent: React.FC = observer(() => { const issuesList = inboxId ? inboxIssuesStore.inboxIssues[inboxId.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( async (formData: Partial) => { @@ -217,8 +228,20 @@ export const InboxMainContent: React.FC = observer(() => { ) : null} +
+ {currentIssueState && ( + + )} + +
setIsSubmitting(value)} + isSubmitting={isSubmitting} workspaceSlug={workspaceSlug as string} issue={{ name: issueDetails.name, diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 43600533b..ef26e22d8 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -26,14 +26,15 @@ export interface IssueDetailsProps { workspaceSlug: string; handleFormSubmit: (value: IssueDescriptionFormValues) => Promise; isAllowed: boolean; + isSubmitting: "submitting" | "submitted" | "saved"; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } const fileService = new FileService(); export const IssueDescriptionForm: FC = (props) => { - const { issue, handleFormSubmit, workspaceSlug, isAllowed } = props; + const { issue, handleFormSubmit, workspaceSlug, isAllowed, isSubmitting, setIsSubmitting } = props; // states - const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [characterLimit, setCharacterLimit] = useState(false); const { setShowAlert } = useReloadConfirmations(); @@ -166,13 +167,6 @@ export const IssueDescriptionForm: FC = (props) => { /> )} /> -
- {isSubmitting === "submitting" ? "Saving..." : "Saved"} -
); diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 0cf3c8bda..f8f0ba003 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -15,6 +15,7 @@ export * from "./sidebar"; export * from "./label"; export * from "./issue-reaction"; export * from "./confirm-issue-discard"; +export * from "./issue-update-status"; // draft issue export * from "./draft-issue-form"; diff --git a/web/components/issues/issue-peek-overview/issue-detail.tsx b/web/components/issues/issue-peek-overview/issue-detail.tsx index fafdccb71..3e90c8b8d 100644 --- a/web/components/issues/issue-peek-overview/issue-detail.tsx +++ b/web/components/issues/issue-peek-overview/issue-detail.tsx @@ -26,16 +26,27 @@ interface IPeekOverviewIssueDetails { issueUpdate: (issue: Partial) => void; issueReactionCreate: (reaction: string) => void; issueReactionRemove: (reaction: string) => void; + isSubmitting: "submitting" | "submitted" | "saved"; + setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } export const PeekOverviewIssueDetails: FC = (props) => { - const { workspaceSlug, issue, issueReactions, user, issueUpdate, issueReactionCreate, issueReactionRemove } = props; + const { + workspaceSlug, + issue, + issueReactions, + user, + issueUpdate, + issueReactionCreate, + issueReactionRemove, + isSubmitting, + setIsSubmitting, + } = props; // store const { user: userStore } = useMobxStore(); const { currentProjectRole } = userStore; const isAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; // states - const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const [characterLimit, setCharacterLimit] = useState(false); // hooks const { setShowAlert } = useReloadConfirmations(); @@ -172,13 +183,6 @@ export const PeekOverviewIssueDetails: FC = (props) = /> )} /> -
- {isSubmitting === "submitting" ? "Saving..." : "Saved"} -
= observer((props) => { const [peekMode, setPeekMode] = useState("side-peek"); const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); const updateRoutePeekId = () => { if (issueId != peekIssueId) { @@ -216,33 +216,35 @@ export const IssueView: FC = observer((props) => { )} - -
- {issue?.created_by !== user?.id && - !issue?.assignees.includes(user?.id ?? "") && - !router.pathname.includes("[archivedIssueId]") && ( - - )} - - {!disableUserActions && ( - + )} + - )} + {!disableUserActions && ( + + )} +
@@ -261,6 +263,8 @@ export const IssueView: FC = observer((props) => {
)} setIsSubmitting(value)} + isSubmitting={isSubmitting} workspaceSlug={workspaceSlug} issue={issue} issueUpdate={issueUpdate} @@ -295,6 +299,8 @@ export const IssueView: FC = observer((props) => {
setIsSubmitting(value)} + isSubmitting={isSubmitting} workspaceSlug={workspaceSlug} issue={issue} issueReactions={issueReactions} diff --git a/web/components/issues/issue-update-status.tsx b/web/components/issues/issue-update-status.tsx new file mode 100644 index 000000000..e6852936e --- /dev/null +++ b/web/components/issues/issue-update-status.tsx @@ -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) => { + const { isSubmitting, issueDetail } = props; + return ( + <> + {issueDetail && ( +

+ {issueDetail.project_detail?.identifier}-{issueDetail.sequence_id} +

+ )} +
+ {isSubmitting !== "submitted" && isSubmitting !== "saved" && ( + + )} + {isSubmitting === "submitting" ? "Saving..." : "Saved"} +
+ + ); +}; diff --git a/web/components/issues/main-content.tsx b/web/components/issues/main-content.tsx index 7ea0e7cfe..bd18cb73b 100644 --- a/web/components/issues/main-content.tsx +++ b/web/components/issues/main-content.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import useSWR, { mutate } from "swr"; +import { MinusCircle } from "lucide-react"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // services @@ -16,12 +17,12 @@ import { IssueAttachments, IssueDescriptionForm, IssueReaction, + IssueUpdateStatus, } from "components/issues"; +import { useState } from "react"; import { SubIssuesRoot } from "./sub-issues"; // ui -import { CustomMenu, LayersIcon } from "@plane/ui"; -// icons -import { MinusCircle } from "lucide-react"; +import { CustomMenu, LayersIcon, StateGroupIcon } from "@plane/ui"; // types import { IIssue, IIssueComment } from "types"; // fetch-keys @@ -41,15 +42,25 @@ const issueCommentService = new IssueCommentService(); export const IssueMainContent: React.FC = observer((props) => { const { issueDetails, submitChanges, uneditable = false } = props; + // states + const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved"); + const router = useRouter(); const { workspaceSlug, projectId, issueId } = router.query; const { setToastAlert } = useToast(); - const { user: userStore, project: projectStore } = useMobxStore(); + const { + user: userStore, + project: projectStore, + projectState: { states }, + } = useMobxStore(); const user = userStore.currentUser ?? undefined; const userRole = userStore.currentProjectRole; 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( workspaceSlug && projectId && issueDetails?.parent ? SUB_ISSUES(issueDetails.parent) : null, @@ -165,7 +176,19 @@ export const IssueMainContent: React.FC = observer((props) => {
) : null} +
+ {currentIssueState && ( + + )} + +
setIsSubmitting(value)} + isSubmitting={isSubmitting} workspaceSlug={workspaceSlug as string} issue={issueDetails} handleFormSubmit={submitChanges} diff --git a/web/components/issues/sidebar.tsx b/web/components/issues/sidebar.tsx index d6681cb4a..b0315304d 100644 --- a/web/components/issues/sidebar.tsx +++ b/web/components/issues/sidebar.tsx @@ -33,7 +33,7 @@ import { import { CustomDatePicker } from "components/ui"; // icons 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 import { copyTextToClipboard } from "helpers/string.helper"; // types @@ -80,12 +80,15 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const [linkModal, setLinkModal] = useState(false); const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null); - const { user: userStore } = useMobxStore(); + const { + user: userStore, + projectState: { states }, + } = useMobxStore(); const user = userStore.currentUser; const userRole = userStore.currentProjectRole; const router = useRouter(); - const { workspaceSlug, projectId, issueId } = router.query; + const { workspaceSlug, projectId, issueId, inboxIssueId } = router.query; const { isEstimateActive } = useEstimateOption(); @@ -248,6 +251,10 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const isAllowed = !!userRole && userRole >= EUserWorkspaceRoles.MEMBER; + const currentIssueState = projectId + ? states[projectId.toString()]?.find((s) => s.id === issueDetail?.state) + : undefined; + return ( <> = observer((props) => { )}
-

- {issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id} -

+
+ {currentIssueState ? ( + + ) : inboxIssueId ? ( + + ) : null} +

+ {issueDetail?.project_detail?.identifier}-{issueDetail?.sequence_id} +

+
{issueDetail?.created_by !== user?.id && !issueDetail?.assignees.includes(user?.id ?? "") &&