From e20681bb6cf4df52eb0e370e4f4f17c58c87f6ab Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:52:59 +0530 Subject: [PATCH 01/16] [INFRA-7] feat: selective build (#3806) * test 1 * wip * wip * build for changed-files/release/master --- .github/workflows/build-branch.yml | 45 ++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index 9d5931c6b26..44bae0efa60 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -23,6 +23,10 @@ jobs: gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }} gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }} gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }} + build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed }} + build_space: ${{ steps.changed_files.outputs.space_any_changed }} + build_backend: ${{ steps.changed_files.outputs.backend_any_changed }} + build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }} steps: - id: set_env_variables @@ -41,7 +45,36 @@ jobs: fi echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT + - id: checkout_files + name: Checkout Files + uses: actions/checkout@v4 + + - name: Get changed files + id: changed_files + uses: tj-actions/changed-files@v42 + with: + files_yaml: | + frontend: + - web/** + - packages/** + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - 'turbo.json' + space: + - space/** + - packages/** + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - 'turbo.json' + backend: + - apiserver/** + proxy: + - nginx/** + branch_build_push_frontend: + if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -77,7 +110,7 @@ jobs: endpoint: ${{ env.BUILDX_ENDPOINT }} - name: Check out the repo - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4 - name: Build and Push Frontend to Docker Container Registry uses: docker/build-push-action@v5.1.0 @@ -93,6 +126,7 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_space: + if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -128,7 +162,7 @@ jobs: endpoint: ${{ env.BUILDX_ENDPOINT }} - name: Check out the repo - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4 - name: Build and Push Space to Docker Hub uses: docker/build-push-action@v5.1.0 @@ -144,6 +178,7 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_backend: + if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -179,7 +214,7 @@ jobs: endpoint: ${{ env.BUILDX_ENDPOINT }} - name: Check out the repo - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4 - name: Build and Push Backend to Docker Hub uses: docker/build-push-action@v5.1.0 @@ -194,8 +229,8 @@ jobs: DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} - branch_build_push_proxy: + if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -231,7 +266,7 @@ jobs: endpoint: ${{ env.BUILDX_ENDPOINT }} - name: Check out the repo - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4 - name: Build and Push Plane-Proxy to Docker Hub uses: docker/build-push-action@v5.1.0 From 58e0170cac00893e8c8a9c8f424a554e9385f77e Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:53:56 +0530 Subject: [PATCH 02/16] chore: posthog key validation (#3766) --- apiserver/plane/app/views/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/app/views/config.py b/apiserver/plane/app/views/config.py index 29b4bbf8bb5..b2a27252cba 100644 --- a/apiserver/plane/app/views/config.py +++ b/apiserver/plane/app/views/config.py @@ -66,15 +66,15 @@ def get(self, request): }, { "key": "SLACK_CLIENT_ID", - "default": os.environ.get("SLACK_CLIENT_ID", "1"), + "default": os.environ.get("SLACK_CLIENT_ID", None), }, { "key": "POSTHOG_API_KEY", - "default": os.environ.get("POSTHOG_API_KEY", "1"), + "default": os.environ.get("POSTHOG_API_KEY", None), }, { "key": "POSTHOG_HOST", - "default": os.environ.get("POSTHOG_HOST", "1"), + "default": os.environ.get("POSTHOG_HOST", None), }, { "key": "UNSPLASH_ACCESS_KEY", @@ -181,11 +181,11 @@ def get(self, request): }, { "key": "POSTHOG_API_KEY", - "default": os.environ.get("POSTHOG_API_KEY", "1"), + "default": os.environ.get("POSTHOG_API_KEY", None), }, { "key": "POSTHOG_HOST", - "default": os.environ.get("POSTHOG_HOST", "1"), + "default": os.environ.get("POSTHOG_HOST", None), }, { "key": "UNSPLASH_ACCESS_KEY", From aba170dbdec13aba980626b139a10d3e6ae70b2e Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:55:00 +0530 Subject: [PATCH 03/16] chore: sequence id search in the issue search (#3807) --- apiserver/plane/utils/issue_search.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/utils/issue_search.py b/apiserver/plane/utils/issue_search.py index d38b1f4c32a..3b6dea332ec 100644 --- a/apiserver/plane/utils/issue_search.py +++ b/apiserver/plane/utils/issue_search.py @@ -9,11 +9,11 @@ def search_issues(query, queryset): - fields = ["name", "sequence_id"] + fields = ["name", "sequence_id", "project__identifier"] q = Q() for field in fields: if field == "sequence_id" and len(query) <= 20: - sequences = re.findall(r"[A-Za-z0-9]{1,12}-\d+", query) + sequences = re.findall(r"\b\d+\b", query) for sequence_id in sequences: q |= Q(**{"sequence_id": sequence_id}) else: From c858b760544035e34a95efeafa9d2b922518fefd Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:56:40 +0530 Subject: [PATCH 04/16] chore: onboarding workspace name valdiation error indicator added (#3808) --- web/components/onboarding/user-details.tsx | 63 +++++++++++----------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/web/components/onboarding/user-details.tsx b/web/components/onboarding/user-details.tsx index cd129c74c5e..a29df3c9422 100644 --- a/web/components/onboarding/user-details.tsx +++ b/web/components/onboarding/user-details.tsx @@ -62,6 +62,7 @@ export const UserDetails: React.FC = observer((props) => { formState: { errors, isSubmitting, isValid }, } = useForm({ defaultValues, + mode: "onChange", }); const onSubmit = async (formData: IUser) => { @@ -164,36 +165,38 @@ export const UserDetails: React.FC = observer((props) => { )} -
- ( - { - setUserName(event.target.value); - onChange(event); - }} - ref={ref} - hasError={Boolean(errors.first_name)} - placeholder="Enter your full name..." - className="w-full border-onboarding-border-100 focus:border-custom-primary-100" - maxLength={24} - /> - )} - /> +
+
+ ( + { + setUserName(event.target.value); + onChange(event); + }} + ref={ref} + hasError={Boolean(errors.first_name)} + placeholder="Enter your full name..." + className="w-full border-onboarding-border-100 focus:border-custom-primary-100" + /> + )} + /> +
+ {errors.first_name && {errors.first_name.message}}
From 34d6b135f2be363e368f251e0453892a89b0ea73 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Tue, 27 Feb 2024 16:58:46 +0530 Subject: [PATCH 05/16] [WEB-581] fix: issue editing functionality enhancement in Create/Edit modal (#3809) * chore: draft issue update request * chore: changed the serializer * chore: handled issue description in issue modal, inbox issues mutation and draft issue mutaion and changed the endpoints * chore: handled draft toggle in make a issue payload in issues * chore: handled issue labels in the inbox issues --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/app/views/issue.py | 11 +- web/components/inbox/inbox-issue-actions.tsx | 15 +- .../issues/issue-detail/inbox/root.tsx | 4 +- .../issues/issue-detail/inbox/sidebar.tsx | 4 + .../issues/issue-detail/label/root.tsx | 31 ++-- .../quick-action-dropdowns/project-issue.tsx | 5 +- .../roots/draft-issue-layout-root.tsx | 2 +- web/components/issues/issue-modal/form.tsx | 169 ++++++++++-------- web/components/issues/issue-modal/modal.tsx | 40 ++++- web/components/issues/peek-overview/root.tsx | 11 +- .../archived-issues/[archivedIssueId].tsx | 2 +- web/services/issue/issue_draft.service.ts | 6 +- web/store/inbox/inbox_issue.store.ts | 44 +---- web/store/issue/draft/issue.store.ts | 4 +- web/store/issue/issue-details/issue.store.ts | 50 +++++- web/store/issue/issue-details/root.store.ts | 8 +- 16 files changed, 250 insertions(+), 156 deletions(-) diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 25c42dc5b35..66cc3caf728 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -2310,17 +2310,10 @@ def partial_update(self, request, slug, project_id, pk): status=status.HTTP_404_NOT_FOUND, ) - serializer = IssueSerializer(issue, data=request.data, partial=True) + serializer = IssueCreateSerializer(issue, data=request.data, partial=True) if serializer.is_valid(): - if request.data.get( - "is_draft" - ) is not None and not request.data.get("is_draft"): - serializer.save( - created_at=timezone.now(), updated_at=timezone.now() - ) - else: - serializer.save() + serializer.save() issue_activity.delay( type="issue_draft.activity.updated", requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), diff --git a/web/components/inbox/inbox-issue-actions.tsx b/web/components/inbox/inbox-issue-actions.tsx index 691ce36c7fa..8a3bb42614d 100644 --- a/web/components/inbox/inbox-issue-actions.tsx +++ b/web/components/inbox/inbox-issue-actions.tsx @@ -92,7 +92,7 @@ export const InboxIssueActionsHeader: FC = observer((p id: inboxIssueId, state: "SUCCESS", element: "Inbox page", - } + }, }); router.push({ pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`, @@ -269,12 +269,17 @@ export const InboxIssueActionsHeader: FC = observer((p { if (!date) return; setDate(date) }} + onSelect={(date) => { + if (!date) return; + setDate(date); + }} mode="single" className="border border-custom-border-200 rounded-md p-3" - disabled={[{ - before: tomorrow, - }]} + disabled={[ + { + before: tomorrow, + }, + ]} />
diff --git a/web/components/issues/issue-detail/label/root.tsx b/web/components/issues/issue-detail/label/root.tsx index 2ef9bec6e63..94f9b451f28 100644 --- a/web/components/issues/issue-detail/label/root.tsx +++ b/web/components/issues/issue-detail/label/root.tsx @@ -13,6 +13,8 @@ export type TIssueLabel = { projectId: string; issueId: string; disabled: boolean; + isInboxIssue?: boolean; + onLabelUpdate?: (labelIds: string[]) => void; }; export type TLabelOperations = { @@ -21,7 +23,7 @@ export type TLabelOperations = { }; export const IssueLabel: FC = observer((props) => { - const { workspaceSlug, projectId, issueId, disabled = false } = props; + const { workspaceSlug, projectId, issueId, disabled = false, isInboxIssue = false, onLabelUpdate } = props; // hooks const { updateIssue } = useIssueDetail(); const { createLabel } = useLabel(); @@ -31,12 +33,14 @@ export const IssueLabel: FC = observer((props) => { () => ({ updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { - await updateIssue(workspaceSlug, projectId, issueId, data); - setToastAlert({ - title: "Issue updated successfully", - type: "success", - message: "Issue updated successfully", - }); + if (onLabelUpdate) onLabelUpdate(data.label_ids || []); + else await updateIssue(workspaceSlug, projectId, issueId, data); + if (!isInboxIssue) + setToastAlert({ + title: "Issue updated successfully", + type: "success", + message: "Issue updated successfully", + }); } catch (error) { setToastAlert({ title: "Issue update failed", @@ -48,11 +52,12 @@ export const IssueLabel: FC = observer((props) => { createLabel: async (workspaceSlug: string, projectId: string, data: Partial) => { try { const labelResponse = await createLabel(workspaceSlug, projectId, data); - setToastAlert({ - title: "Label created successfully", - type: "success", - message: "Label created successfully", - }); + if (!isInboxIssue) + setToastAlert({ + title: "Label created successfully", + type: "success", + message: "Label created successfully", + }); return labelResponse; } catch (error) { setToastAlert({ @@ -64,7 +69,7 @@ export const IssueLabel: FC = observer((props) => { } }, }), - [updateIssue, createLabel, setToastAlert] + [updateIssue, createLabel, setToastAlert, onLabelUpdate] ); return ( diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 1d6d88f2575..b1272430137 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -49,16 +49,17 @@ export const ProjectIssueQuickActions: React.FC = (props) => ); }; + const isDraftIssue = router?.asPath?.includes("draft-issues") || false; + const duplicateIssuePayload = omit( { ...issue, name: `${issue.name} (copy)`, + is_draft: isDraftIssue ? false : issue.is_draft, }, ["id"] ); - const isDraftIssue = router?.asPath?.includes("draft-issues") || false; - return ( <> { ) : null} {/* issue peek overview */} - + )} diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index cfb4b912c54..2327eb96181 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -27,7 +27,7 @@ import { StateDropdown, } from "components/dropdowns"; // ui -import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; +import { Button, CustomMenu, Input, Loader, ToggleSwitch } from "@plane/ui"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // types @@ -162,6 +162,10 @@ export const IssueFormRoot: FC = observer((props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectId]); + useEffect(() => { + if (data?.description_html) setValue("description_html", data?.description_html); + }, [data?.description_html]); + const issueName = watch("name"); const handleFormSubmit = async (formData: Partial, is_draft_issue = false) => { @@ -365,80 +369,105 @@ export const IssueFormRoot: FC = observer((props) => { )} />
-
- {issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && ( - - )} - {envConfig?.has_openai_configured && ( - { - setGptAssistantModal((prevData) => !prevData); - // this is done so that the title do not reset after gpt popover closed - reset(getValues()); - }} - onResponse={(response) => { - handleAiAssistance(response); - }} - placement="top-end" - button={ + {data?.description_html === undefined ? ( + + +
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+ ) : ( + +
+ {issueName && issueName.trim() !== "" && envConfig?.has_openai_configured && ( - } - /> - )} -
- ( - { - onChange(description_html); - handleFormChange(); - }} - mentionHighlights={mentionHighlights} - mentionSuggestions={mentionSuggestions} - // tabIndex={2} + )} + {envConfig?.has_openai_configured && ( + { + setGptAssistantModal((prevData) => !prevData); + // this is done so that the title do not reset after gpt popover closed + reset(getValues()); + }} + onResponse={(response) => { + handleAiAssistance(response); + }} + placement="top-end" + button={ + + } + /> + )} +
+ ( + { + onChange(description_html); + handleFormChange(); + }} + mentionHighlights={mentionHighlights} + mentionSuggestions={mentionSuggestions} + // tabIndex={2} + /> + )} /> - )} - /> + + )}
= observer((prop const [changesMade, setChangesMade] = useState | null>(null); const [createMore, setCreateMore] = useState(false); const [activeProjectId, setActiveProjectId] = useState(null); + const [description, setDescription] = useState(undefined); // store hooks const { captureIssueEvent } = useEventTracker(); const { @@ -53,7 +63,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const { issues: cycleIssues } = useIssues(EIssuesStoreType.CYCLE); const { issues: viewIssues } = useIssues(EIssuesStoreType.PROJECT_VIEW); const { issues: profileIssues } = useIssues(EIssuesStoreType.PROFILE); - const { issues: draftIssueStore } = useIssues(EIssuesStoreType.DRAFT); + const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); + const { fetchIssue } = useIssueDetail(); // store mapping based on current store const issueStores = { [EIssuesStoreType.PROJECT]: { @@ -86,7 +97,20 @@ export const CreateUpdateIssueModal: React.FC = observer((prop // current store details const { store: currentIssueStore, viewId } = issueStores[storeType]; + const fetchIssueDetail = async (issueId: string | undefined) => { + if (!workspaceSlug || !projectId) return; + if (issueId === undefined) { + setDescription("

"); + return; + } + const response = await fetchIssue(workspaceSlug, projectId, issueId, isDraft ? "DRAFT" : "DEFAULT"); + if (response) setDescription(response?.description_html || "

"); + }; + useEffect(() => { + // fetching issue details + if (isOpen) fetchIssueDetail(data?.id); + // if modal is closed, reset active project to null // and return to avoid activeProjectId being set to some other project if (!isOpen) { @@ -105,6 +129,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop // in the url. This has the least priority. if (workspaceProjectIds && workspaceProjectIds.length > 0 && !activeProjectId) setActiveProjectId(projectId ?? workspaceProjectIds?.[0]); + + // clearing up the description state when we leave the component + return () => setDescription(undefined); }, [data, projectId, workspaceProjectIds, isOpen, activeProjectId]); const addIssueToCycle = async (issue: TIssue, cycleId: string) => { @@ -142,7 +169,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop try { const response = is_draft_issue - ? await draftIssueStore.createIssue(workspaceSlug, payload.project_id, payload) + ? await draftIssues.createIssue(workspaceSlug, payload.project_id, payload) : await currentIssueStore.createIssue(workspaceSlug, payload.project_id, payload, viewId); if (!response) throw new Error(); @@ -183,7 +210,10 @@ export const CreateUpdateIssueModal: React.FC = observer((prop if (!workspaceSlug || !payload.project_id || !data?.id) return; try { - await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); + isDraft + ? await draftIssues.updateIssue(workspaceSlug, payload.project_id, data.id, payload) + : await currentIssueStore.updateIssue(workspaceSlug, payload.project_id, data.id, payload, viewId); + setToastAlert({ type: "success", title: "Success!", @@ -261,6 +291,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop changesMade={changesMade} data={{ ...data, + description_html: description, cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId : null, module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId] : null, }} @@ -276,6 +307,7 @@ export const CreateUpdateIssueModal: React.FC = observer((prop = observer((props) => { - const { is_archived = false } = props; + const { is_archived = false, is_draft = false } = props; // hooks const { setToastAlert } = useToast(); // router @@ -72,7 +73,12 @@ export const IssuePeekOverview: FC = observer((props) => { () => ({ fetch: async (workspaceSlug: string, projectId: string, issueId: string) => { try { - await fetchIssue(workspaceSlug, projectId, issueId, is_archived); + await fetchIssue( + workspaceSlug, + projectId, + issueId, + is_archived ? "ARCHIVED" : is_draft ? "DRAFT" : "DEFAULT" + ); } catch (error) { console.error("Error fetching the parent issue"); } @@ -302,6 +308,7 @@ export const IssuePeekOverview: FC = observer((props) => { }), [ is_archived, + is_draft, fetchIssue, updateIssue, removeIssue, diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index 1538d240f5a..17c002c3c6e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -42,7 +42,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { ? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}` : null, workspaceSlug && projectId && archivedIssueId - ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), true) + ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString(), "ARCHIVED") : null ); diff --git a/web/services/issue/issue_draft.service.ts b/web/services/issue/issue_draft.service.ts index b4eb995b0a9..a93bda776db 100644 --- a/web/services/issue/issue_draft.service.ts +++ b/web/services/issue/issue_draft.service.ts @@ -42,8 +42,10 @@ export class IssueDraftService extends APIService { }); } - async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`) + async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, { + params: queries, + }) .then((response) => response?.data) .catch((error) => { throw error?.response; diff --git a/web/store/inbox/inbox_issue.store.ts b/web/store/inbox/inbox_issue.store.ts index 2fedb73dc1b..4f980357fdb 100644 --- a/web/store/inbox/inbox_issue.store.ts +++ b/web/store/inbox/inbox_issue.store.ts @@ -53,7 +53,7 @@ export interface IInboxIssue { inboxId: string, inboxIssueId: string, data: Partial - ) => Promise; + ) => Promise; removeInboxIssue: (workspaceSlug: string, projectId: string, inboxId: string, issueId: string) => Promise; updateInboxIssueStatus: ( workspaceSlug: string, @@ -61,7 +61,7 @@ export interface IInboxIssue { inboxId: string, inboxIssueId: string, data: TInboxDetailedStatus - ) => Promise; + ) => Promise; } export class InboxIssue implements IInboxIssue { @@ -215,22 +215,9 @@ export class InboxIssue implements IInboxIssue { issue: data, }); - runInAction(() => { - const { ["issue_inbox"]: issueInboxDetail, ...issue } = response; - this.rootStore.inbox.rootStore.issue.issues.updateIssue(issue.id, issue); - const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0]; - set(this.inboxIssueMap, [inboxId, response.id], inboxIssue); - }); - - runInAction(() => { - update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => { - if (inboxIssueIds.includes(response.id)) return inboxIssueIds; - return uniq(concat(inboxIssueIds, response.id)); - }); - }); + this.rootStore.inbox.rootStore.issue.issues.updateIssue(inboxIssueId, data); await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); - return response as any; } catch (error) { throw error; } @@ -238,7 +225,7 @@ export class InboxIssue implements IInboxIssue { removeInboxIssue = async (workspaceSlug: string, projectId: string, inboxId: string, inboxIssueId: string) => { try { - const response = await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId); + await this.inboxIssueService.removeInboxIssue(workspaceSlug, projectId, inboxId, inboxIssueId); runInAction(() => { pull(this.inboxIssues[inboxId], inboxIssueId); @@ -248,7 +235,6 @@ export class InboxIssue implements IInboxIssue { }); await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); - return response as any; } catch (error) { throw error; } @@ -262,34 +248,18 @@ export class InboxIssue implements IInboxIssue { data: TInboxDetailedStatus ) => { try { - const response = await this.inboxIssueService.updateInboxIssueStatus( - workspaceSlug, - projectId, - inboxId, - inboxIssueId, - data - ); + await this.inboxIssueService.updateInboxIssueStatus(workspaceSlug, projectId, inboxId, inboxIssueId, data); const pendingStatus = -2; runInAction(() => { - const { ["issue_inbox"]: issueInboxDetail, ...issue } = response; - this.rootStore.inbox.rootStore.issue.issues.addIssue([issue]); - const { ["id"]: omittedId, ...inboxIssue } = issueInboxDetail[0]; - set(this.inboxIssueMap, [inboxId, response.id], inboxIssue); + set(this.inboxIssueMap, [inboxId, inboxIssueId, "status"], data.status); + update(this.rootStore.inbox.inbox.inboxMap, [inboxId, "pending_issue_count"], (count: number = 0) => data.status === pendingStatus ? count + 1 : count - 1 ); }); - runInAction(() => { - update(this.inboxIssues, inboxId, (inboxIssueIds: string[] = []) => { - if (inboxIssueIds.includes(response.id)) return inboxIssueIds; - return uniq(concat(inboxIssueIds, response.id)); - }); - }); - await this.rootStore.issue.issueDetail.fetchActivities(workspaceSlug, projectId, inboxIssueId); - return response as any; } catch (error) { throw error; } diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index ee6d785ecbf..824bbb3c154 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -141,7 +141,9 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { try { - await this.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data); + + this.rootStore.issues.updateIssue(issueId, data); if (data.hasOwnProperty("is_draft") && data?.is_draft === false) { runInAction(() => { diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index 8731bf478bd..46b96c27e26 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -1,6 +1,6 @@ import { makeObservable } from "mobx"; // services -import { IssueArchiveService, IssueService } from "services/issue"; +import { IssueArchiveService, IssueDraftService, IssueService } from "services/issue"; // types import { TIssue } from "@plane/types"; import { computedFn } from "mobx-utils"; @@ -8,7 +8,12 @@ import { IIssueDetail } from "./root.store"; export interface IIssueStoreActions { // actions - fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise; + fetchIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + issueType?: "DEFAULT" | "DRAFT" | "ARCHIVED" + ) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; @@ -34,6 +39,7 @@ export class IssueStore implements IIssueStore { // services issueService; issueArchiveService; + issueDraftService; constructor(rootStore: IIssueDetail) { makeObservable(this, {}); @@ -42,6 +48,7 @@ export class IssueStore implements IIssueStore { // services this.issueService = new IssueService(); this.issueArchiveService = new IssueArchiveService(); + this.issueDraftService = new IssueDraftService(); } // helper methods @@ -51,21 +58,54 @@ export class IssueStore implements IIssueStore { }); // actions - fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => { + fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, issueType = "DEFAULT") => { try { const query = { expand: "issue_reactions,issue_attachment,issue_link,parent", }; let issue: TIssue; + let issuePayload: TIssue; - if (isArchived) + if (issueType === "ARCHIVED") issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query); + else if (issueType === "DRAFT") + issue = await this.issueDraftService.getDraftIssueById(workspaceSlug, projectId, issueId, query); else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query); if (!issue) throw new Error("Issue not found"); - this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue], true); + issuePayload = { + id: issue?.id, + sequence_id: issue?.sequence_id, + name: issue?.name, + description_html: issue?.description_html, + sort_order: issue?.sort_order, + state_id: issue?.state_id, + priority: issue?.priority, + label_ids: issue?.label_ids, + assignee_ids: issue?.assignee_ids, + estimate_point: issue?.estimate_point, + sub_issues_count: issue?.sub_issues_count, + attachment_count: issue?.attachment_count, + link_count: issue?.link_count, + project_id: issue?.project_id, + parent_id: issue?.parent_id, + cycle_id: issue?.cycle_id, + module_ids: issue?.module_ids, + created_at: issue?.created_at, + updated_at: issue?.updated_at, + start_date: issue?.start_date, + target_date: issue?.target_date, + completed_at: issue?.completed_at, + archived_at: issue?.archived_at, + created_by: issue?.created_by, + updated_by: issue?.updated_by, + is_draft: issue?.is_draft, + is_subscribed: issue?.is_subscribed, + }; + + this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload], true); // store handlers from issue detail // parent diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index 4c2d6add1e5..daaae749e53 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -140,8 +140,12 @@ export class IssueDetail implements IIssueDetail { toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value); // issue - fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => - this.issue.fetchIssue(workspaceSlug, projectId, issueId, isArchived); + fetchIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + issueType: "DEFAULT" | "ARCHIVED" | "DRAFT" = "DEFAULT" + ) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueType); updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => this.issue.updateIssue(workspaceSlug, projectId, issueId, data); removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => From 002b2505f33c3f4a1351464789b3b3ac4f412242 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:35:32 +0530 Subject: [PATCH 06/16] [WEB-583] fix: issue modal description on workspace level (#3814) * fix: issue modal description on workspace level * fix: issue modal description on workspace level --- web/components/issues/issue-modal/modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index cefd1f283f9..4c2833efa26 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -98,8 +98,8 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const { store: currentIssueStore, viewId } = issueStores[storeType]; const fetchIssueDetail = async (issueId: string | undefined) => { - if (!workspaceSlug || !projectId) return; - if (issueId === undefined) { + if (!workspaceSlug) return; + if (!projectId || issueId === undefined) { setDescription("

"); return; } From bd142989b4f0bf41c8c6d413486e0f258345a9d0 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:17:35 +0530 Subject: [PATCH 07/16] [WEB-590] chore: project member active (#3820) * chore: project member active filter * chore: updated active filter --- apiserver/plane/api/views/cycle.py | 10 ++++- apiserver/plane/api/views/issue.py | 21 ++++++---- apiserver/plane/api/views/module.py | 5 ++- apiserver/plane/api/views/state.py | 5 ++- apiserver/plane/app/views/cycle.py | 10 ++++- .../plane/app/views/integration/slack.py | 5 ++- apiserver/plane/app/views/issue.py | 42 +++++++++++++++---- apiserver/plane/app/views/module.py | 5 ++- apiserver/plane/app/views/page.py | 5 ++- apiserver/plane/app/views/search.py | 10 ++++- apiserver/plane/app/views/state.py | 5 ++- apiserver/plane/app/views/view.py | 10 ++++- apiserver/plane/app/views/workspace.py | 11 +++++ apiserver/plane/bgtasks/export_task.py | 1 + 14 files changed, 116 insertions(+), 29 deletions(-) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 6f66c373ec6..84931f46be9 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -45,7 +45,10 @@ def get_queryset(self): return ( Cycle.objects.filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("project") .select_related("workspace") .select_related("owned_by") @@ -390,7 +393,10 @@ def get_queryset(self): ) .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .filter(cycle_id=self.kwargs.get("cycle_id")) .select_related("project") .select_related("workspace") diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 50269fe0727..0905ae1f7d1 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -352,7 +352,10 @@ def get_queryset(self): return ( Label.objects.filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("project") .select_related("workspace") .select_related("parent") @@ -481,7 +484,10 @@ def get_queryset(self): IssueLink.objects.filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(issue_id=self.kwargs.get("issue_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .order_by(self.kwargs.get("order_by", "-created_at")) .distinct() ) @@ -607,11 +613,11 @@ def get_queryset(self): ) .filter(project_id=self.kwargs.get("project_id")) .filter(issue_id=self.kwargs.get("issue_id")) - .filter(project__project_projectmember__member=self.request.user) - .select_related("project") - .select_related("workspace") - .select_related("issue") - .select_related("actor") + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) + .select_related("workspace", "project", "issue", "actor") .annotate( is_member=Exists( ProjectMember.objects.filter( @@ -784,6 +790,7 @@ def get(self, request, slug, project_id, issue_id, pk=None): .filter( ~Q(field__in=["comment", "vote", "reaction", "draft"]), project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, ) .select_related("actor", "workspace", "issue", "project") ).order_by(request.GET.get("order_by", "created_at")) diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index d509a53c79d..2e5bb85e2b7 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -273,7 +273,10 @@ def get_queryset(self): .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(module_id=self.kwargs.get("module_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("project") .select_related("workspace") .select_related("module") diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/api/views/state.py index dedc15ccdca..ec10f9babe1 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/api/views/state.py @@ -24,7 +24,10 @@ def get_queryset(self): return ( State.objects.filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .filter(~Q(name="Triage")) .select_related("project") .select_related("workspace") diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index 866396655fc..85e1e9f2e84 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -85,7 +85,10 @@ def get_queryset(self): .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("project", "workspace", "owned_by") .prefetch_related( Prefetch( @@ -689,7 +692,10 @@ def get_queryset(self): ) .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .filter(cycle_id=self.kwargs.get("cycle_id")) .select_related("project") .select_related("workspace") diff --git a/apiserver/plane/app/views/integration/slack.py b/apiserver/plane/app/views/integration/slack.py index 410e6b332c3..c22ee3e52bd 100644 --- a/apiserver/plane/app/views/integration/slack.py +++ b/apiserver/plane/app/views/integration/slack.py @@ -36,7 +36,10 @@ def get_queryset(self): workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"), ) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) ) def create(self, request, slug, project_id, workspace_integration_id): diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 66cc3caf728..6d50a2aba28 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -773,7 +773,10 @@ class WorkSpaceIssuesEndpoint(BaseAPIView): def get(self, request, slug): issues = ( Issue.issue_objects.filter(workspace__slug=slug) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .order_by("-created_at") ) serializer = IssueSerializer(issues, many=True) @@ -796,6 +799,7 @@ def get(self, request, slug, project_id, issue_id): .filter( ~Q(field__in=["comment", "vote", "reaction", "draft"]), project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, workspace__slug=slug, ) .filter(**filters) @@ -805,6 +809,7 @@ def get(self, request, slug, project_id, issue_id): IssueComment.objects.filter(issue_id=issue_id) .filter( project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, workspace__slug=slug, ) .filter(**filters) @@ -856,7 +861,10 @@ def get_queryset(self): .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(issue_id=self.kwargs.get("issue_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("project") .select_related("workspace") .select_related("issue") @@ -1018,7 +1026,10 @@ def get_queryset(self): .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("project") .select_related("workspace") .select_related("parent") @@ -1231,7 +1242,10 @@ def get_queryset(self): .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(issue_id=self.kwargs.get("issue_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .order_by("-created_at") .distinct() ) @@ -1692,7 +1706,10 @@ def get_queryset(self): .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(issue_id=self.kwargs.get("issue_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .order_by("-created_at") .distinct() ) @@ -1776,7 +1793,10 @@ def get_queryset(self): .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(issue_id=self.kwargs.get("issue_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .order_by("-created_at") .distinct() ) @@ -1845,7 +1865,10 @@ def get_queryset(self): .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(comment_id=self.kwargs.get("comment_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .order_by("-created_at") .distinct() ) @@ -1915,7 +1938,10 @@ def get_queryset(self): .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(issue_id=self.kwargs.get("issue_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("project") .select_related("workspace") .select_related("issue") diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py index 5ac244dda36..3b52db64f9e 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -673,7 +673,10 @@ def get_queryset(self): .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) .filter(module_id=self.kwargs.get("module_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .order_by("-created_at") .distinct() ) diff --git a/apiserver/plane/app/views/page.py b/apiserver/plane/app/views/page.py index 1d8ff1fbb15..7ecf22fa847 100644 --- a/apiserver/plane/app/views/page.py +++ b/apiserver/plane/app/views/page.py @@ -60,7 +60,10 @@ def get_queryset(self): .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .filter(parent__isnull=True) .filter(Q(owned_by=self.request.user) | Q(access=0)) .select_related("project") diff --git a/apiserver/plane/app/views/search.py b/apiserver/plane/app/views/search.py index ccef3d18f12..a2ed1c015ad 100644 --- a/apiserver/plane/app/views/search.py +++ b/apiserver/plane/app/views/search.py @@ -48,8 +48,8 @@ def filter_projects(self, query, slug, project_id, workspace_search): return ( Project.objects.filter( q, - Q(project_projectmember__member=self.request.user) - | Q(network=2), + project_projectmember__member=self.request.user, + project_projectmember__is_active=True, workspace__slug=slug, ) .distinct() @@ -71,6 +71,7 @@ def filter_issues(self, query, slug, project_id, workspace_search): issues = Issue.issue_objects.filter( q, project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, workspace__slug=slug, ) @@ -95,6 +96,7 @@ def filter_cycles(self, query, slug, project_id, workspace_search): cycles = Cycle.objects.filter( q, project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, workspace__slug=slug, ) @@ -118,6 +120,7 @@ def filter_modules(self, query, slug, project_id, workspace_search): modules = Module.objects.filter( q, project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, workspace__slug=slug, ) @@ -141,6 +144,7 @@ def filter_pages(self, query, slug, project_id, workspace_search): pages = Page.objects.filter( q, project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, workspace__slug=slug, ) @@ -164,6 +168,7 @@ def filter_views(self, query, slug, project_id, workspace_search): issue_views = IssueView.objects.filter( q, project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, workspace__slug=slug, ) @@ -236,6 +241,7 @@ def get(self, request, slug, project_id): issues = Issue.issue_objects.filter( workspace__slug=slug, project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, ) if workspace_search == "false": diff --git a/apiserver/plane/app/views/state.py b/apiserver/plane/app/views/state.py index 242061e1878..34b3d1dcc01 100644 --- a/apiserver/plane/app/views/state.py +++ b/apiserver/plane/app/views/state.py @@ -31,7 +31,10 @@ def get_queryset(self): .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .filter(~Q(name="Triage")) .select_related("project") .select_related("workspace") diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view.py index 97a0f036f61..ade445fae80 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view.py @@ -86,6 +86,10 @@ def get_queryset(self): .values("count") ) .filter(workspace__slug=self.kwargs.get("slug")) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") .annotate(cycle_id=F("issue_cycle__cycle_id")) @@ -163,7 +167,6 @@ def list(self, request, slug): issue_queryset = ( self.get_queryset() .filter(**filters) - .filter(project__project_projectmember__member=self.request.user) .annotate(cycle_id=F("issue_cycle__cycle_id")) ) @@ -284,7 +287,10 @@ def get_queryset(self): .get_queryset() .filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) - .filter(project__project_projectmember__member=self.request.user) + .filter( + project__project_projectmember__member=self.request.user, + project__project_projectmember__is_active=True, + ) .select_related("project") .select_related("workspace") .annotate(is_favorite=Exists(subquery)) diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index 6677b4c4bda..47de86a1c17 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -1086,6 +1086,7 @@ def get(self, request, slug, user_id): workspace__slug=slug, assignees__in=[user_id], project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True ) .filter(**filters) .annotate(state_group=F("state__group")) @@ -1101,6 +1102,7 @@ def get(self, request, slug, user_id): workspace__slug=slug, assignees__in=[user_id], project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True ) .filter(**filters) .values("priority") @@ -1123,6 +1125,7 @@ def get(self, request, slug, user_id): Issue.issue_objects.filter( workspace__slug=slug, project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, created_by_id=user_id, ) .filter(**filters) @@ -1134,6 +1137,7 @@ def get(self, request, slug, user_id): workspace__slug=slug, assignees__in=[user_id], project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, ) .filter(**filters) .count() @@ -1145,6 +1149,7 @@ def get(self, request, slug, user_id): workspace__slug=slug, assignees__in=[user_id], project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, ) .filter(**filters) .count() @@ -1156,6 +1161,7 @@ def get(self, request, slug, user_id): assignees__in=[user_id], state__group="completed", project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True ) .filter(**filters) .count() @@ -1166,6 +1172,7 @@ def get(self, request, slug, user_id): workspace__slug=slug, subscriber_id=user_id, project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True ) .filter(**filters) .count() @@ -1215,6 +1222,7 @@ def get(self, request, slug, user_id): ~Q(field__in=["comment", "vote", "reaction", "draft"]), workspace__slug=slug, project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True, actor=user_id, ).select_related("actor", "workspace", "issue", "project") @@ -1355,6 +1363,7 @@ def get(self, request, slug, user_id): | Q(issue_subscribers__subscriber_id=user_id), workspace__slug=slug, project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True ) .filter(**filters) .select_related("workspace", "project", "state", "parent") @@ -1486,6 +1495,7 @@ def get(self, request, slug): labels = Label.objects.filter( workspace__slug=slug, project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True ) serializer = LabelSerializer(labels, many=True).data return Response(serializer, status=status.HTTP_200_OK) @@ -1500,6 +1510,7 @@ def get(self, request, slug): states = State.objects.filter( workspace__slug=slug, project__project_projectmember__member=request.user, + project__project_projectmember__is_active=True ) serializer = StateSerializer(states, many=True).data return Response(serializer, status=status.HTTP_200_OK) diff --git a/apiserver/plane/bgtasks/export_task.py b/apiserver/plane/bgtasks/export_task.py index b99e4b1d944..d8522e7697f 100644 --- a/apiserver/plane/bgtasks/export_task.py +++ b/apiserver/plane/bgtasks/export_task.py @@ -292,6 +292,7 @@ def issue_export_task( workspace__id=workspace_id, project_id__in=project_ids, project__project_projectmember__member=exporter_instance.initiated_by_id, + project__project_projectmember__is_active=True ) .select_related( "project", "workspace", "state", "parent", "created_by" From 6c70d3854adf719a7985eddb824fb82a54444361 Mon Sep 17 00:00:00 2001 From: rahulramesha <71900764+rahulramesha@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:18:11 +0530 Subject: [PATCH 08/16] [WEB-575] chore: safely re-enable SWR (#3805) * safley enable swr and make sure to minimalize re renders * resolve build errors * fix dropdowns updation by adding observer --- .../core/modals/bulk-delete-issues-modal.tsx | 6 +- web/components/dropdowns/cycle.tsx | 277 ------------------ .../dropdowns/cycle/cycle-options.tsx | 162 ++++++++++ web/components/dropdowns/cycle/index.tsx | 149 ++++++++++ web/components/dropdowns/member/index.ts | 2 - web/components/dropdowns/member/index.tsx | 156 ++++++++++ .../dropdowns/member/member-options.tsx | 142 +++++++++ .../dropdowns/member/project-member.tsx | 261 ----------------- .../dropdowns/member/workspace-member.tsx | 238 --------------- .../{module.tsx => module/index.tsx} | 144 +-------- .../dropdowns/module/module-options.tsx | 163 +++++++++++ web/components/issues/draft-issue-form.tsx | 4 +- .../issues/issue-detail/inbox/sidebar.tsx | 4 +- .../issues/issue-detail/sidebar.tsx | 10 +- .../issue-layouts/calendar/issue-blocks.tsx | 4 +- .../issues/issue-layouts/gantt/blocks.tsx | 6 +- .../issues/issue-layouts/kanban/block.tsx | 6 +- .../issue-layouts/kanban/roots/cycle-root.tsx | 17 +- .../issues/issue-layouts/list/block.tsx | 8 +- .../issue-layouts/list/roots/cycle-root.tsx | 17 +- .../properties/all-properties.tsx | 4 +- .../roots/all-issue-layout-root.tsx | 17 +- .../roots/archived-issue-layout-root.tsx | 3 +- .../issue-layouts/roots/cycle-layout-root.tsx | 3 +- .../roots/draft-issue-layout-root.tsx | 3 +- .../roots/module-layout-root.tsx | 3 +- .../roots/project-layout-root.tsx | 24 +- .../roots/project-view-layout-root.tsx | 3 +- .../spreadsheet/columns/assignee-column.tsx | 4 +- .../issue-layouts/spreadsheet/issue-row.tsx | 4 +- .../spreadsheet/roots/cycle-root.tsx | 4 +- web/components/issues/issue-modal/form.tsx | 4 +- web/components/issues/issue-modal/modal.tsx | 1 - .../issues/peek-overview/properties.tsx | 10 +- .../issues/sub-issues/properties.tsx | 4 +- web/components/labels/index.ts | 1 - web/components/labels/labels-list-modal.tsx | 157 ---------- web/components/modules/form.tsx | 6 +- web/components/modules/sidebar.tsx | 6 +- web/components/profile/profile-issues.tsx | 3 +- .../project/create-project-modal.tsx | 4 +- web/constants/swr-config.ts | 4 +- web/hooks/use-workspace-issue-properties.ts | 15 +- web/layouts/app-layout/layout.tsx | 2 +- web/layouts/auth-layout/project-wrapper.tsx | 21 +- web/layouts/auth-layout/workspace-wrapper.tsx | 12 +- web/store/cycle.store.ts | 8 + web/store/module.store.ts | 8 + web/store/project/project.store.ts | 11 + 49 files changed, 952 insertions(+), 1173 deletions(-) delete mode 100644 web/components/dropdowns/cycle.tsx create mode 100644 web/components/dropdowns/cycle/cycle-options.tsx create mode 100644 web/components/dropdowns/cycle/index.tsx delete mode 100644 web/components/dropdowns/member/index.ts create mode 100644 web/components/dropdowns/member/index.tsx create mode 100644 web/components/dropdowns/member/member-options.tsx delete mode 100644 web/components/dropdowns/member/project-member.tsx delete mode 100644 web/components/dropdowns/member/workspace-member.tsx rename web/components/dropdowns/{module.tsx => module/index.tsx} (61%) create mode 100644 web/components/dropdowns/module/module-options.tsx delete mode 100644 web/components/labels/labels-list-modal.tsx diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index f5eab83efa3..39be2872b59 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -49,8 +49,10 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { const [query, setQuery] = useState(""); // fetching project issues. const { data: issues } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null, - workspaceSlug && projectId ? () => issueService.getIssues(workspaceSlug as string, projectId as string) : null + workspaceSlug && projectId && isOpen ? PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string) : null, + workspaceSlug && projectId && isOpen + ? () => issueService.getIssues(workspaceSlug as string, projectId as string) + : null ); const { setToastAlert } = useToast(); diff --git a/web/components/dropdowns/cycle.tsx b/web/components/dropdowns/cycle.tsx deleted file mode 100644 index d1f552a99af..00000000000 --- a/web/components/dropdowns/cycle.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; -import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search } from "lucide-react"; -// hooks -import { useApplication, useCycle } from "hooks/store"; -import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// components -import { DropdownButton } from "./buttons"; -// icons -import { ContrastIcon, CycleGroupIcon } from "@plane/ui"; -// helpers -import { cn } from "helpers/common.helper"; -// types -import { TDropdownProps } from "./types"; -import { TCycleGroups } from "@plane/types"; -// constants -import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; - -type Props = TDropdownProps & { - button?: ReactNode; - dropdownArrow?: boolean; - dropdownArrowClassName?: string; - onChange: (val: string | null) => void; - onClose?: () => void; - projectId: string; - value: string | null; -}; - -type DropdownOptions = - | { - value: string | null; - query: string; - content: JSX.Element; - }[] - | undefined; - -export const CycleDropdown: React.FC = observer((props) => { - const { - button, - buttonClassName, - buttonContainerClassName, - buttonVariant, - className = "", - disabled = false, - dropdownArrow = false, - dropdownArrowClassName = "", - hideIcon = false, - onChange, - onClose, - placeholder = "Cycle", - placement, - projectId, - showTooltip = false, - tabIndex, - value, - } = props; - // states - const [query, setQuery] = useState(""); - const [isOpen, setIsOpen] = useState(false); - // refs - const dropdownRef = useRef(null); - const inputRef = useRef(null); - // popper-js refs - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // popper-js init - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - // store hooks - const { - router: { workspaceSlug }, - } = useApplication(); - const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle(); - - const cycleIds = (getProjectCycleIds(projectId) ?? [])?.filter((cycleId) => { - const cycleDetails = getCycleById(cycleId); - return cycleDetails?.status ? (cycleDetails?.status.toLowerCase() != "completed" ? true : false) : true; - }); - - const options: DropdownOptions = cycleIds?.map((cycleId) => { - const cycleDetails = getCycleById(cycleId); - const cycleStatus = cycleDetails?.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft"; - - return { - value: cycleId, - query: `${cycleDetails?.name}`, - content: ( -
- - {cycleDetails?.name} -
- ), - }; - }); - options?.unshift({ - value: null, - query: "No cycle", - content: ( -
- - No cycle -
- ), - }); - - const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - - const selectedCycle = value ? getCycleById(value) : null; - - const onOpen = () => { - if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug, projectId); - }; - - const handleClose = () => { - if (!isOpen) return; - setIsOpen(false); - onClose && onClose(); - }; - - const toggleDropdown = () => { - if (!isOpen) onOpen(); - setIsOpen((prevIsOpen) => !prevIsOpen); - }; - - const dropdownOnChange = (val: string | null) => { - onChange(val); - handleClose(); - }; - - const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); - - const handleOnClick = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - toggleDropdown(); - }; - - const searchInputKeyDown = (e: React.KeyboardEvent) => { - if (query !== "" && e.key === "Escape") { - e.stopPropagation(); - setQuery(""); - } - }; - - useOutsideClickDetector(dropdownRef, handleClose); - - useEffect(() => { - if (isOpen && inputRef.current) { - inputRef.current.focus(); - } - }, [isOpen]); - - return ( - - - {button ? ( - - ) : ( - - )} - - {isOpen && ( - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - onKeyDown={searchInputKeyDown} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( -

No matches found

- ) - ) : ( -

Loading...

- )} -
-
-
- )} -
- ); -}); diff --git a/web/components/dropdowns/cycle/cycle-options.tsx b/web/components/dropdowns/cycle/cycle-options.tsx new file mode 100644 index 00000000000..e691569b7a9 --- /dev/null +++ b/web/components/dropdowns/cycle/cycle-options.tsx @@ -0,0 +1,162 @@ +import { useEffect, useRef, useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { observer } from "mobx-react"; +//components +import { ContrastIcon, CycleGroupIcon } from "@plane/ui"; +//store +import { useApplication, useCycle } from "hooks/store"; +//hooks +import { usePopper } from "react-popper"; +//icon +import { Check, Search } from "lucide-react"; +//types +import { Placement } from "@popperjs/core"; +import { TCycleGroups } from "@plane/types"; + +type DropdownOptions = + | { + value: string | null; + query: string; + content: JSX.Element; + }[] + | undefined; + +interface Props { + projectId: string; + referenceElement: HTMLButtonElement | null; + placement: Placement | undefined; + isOpen: boolean; +} + +export const CycleOptions = observer((props: any) => { + const { projectId, isOpen, referenceElement, placement } = props; + + //state hooks + const [query, setQuery] = useState(""); + const [popperElement, setPopperElement] = useState(null); + const inputRef = useRef(null); + + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle(); + + useEffect(() => { + if (isOpen) { + onOpen(); + inputRef.current && inputRef.current.focus(); + } + }, [isOpen]); + + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const cycleIds = (getProjectCycleIds(projectId) ?? [])?.filter((cycleId) => { + const cycleDetails = getCycleById(cycleId); + return cycleDetails?.status ? (cycleDetails?.status.toLowerCase() != "completed" ? true : false) : true; + }); + + const onOpen = () => { + if (workspaceSlug && !cycleIds) fetchAllCycles(workspaceSlug, projectId); + }; + + const searchInputKeyDown = (e: React.KeyboardEvent) => { + if (query !== "" && e.key === "Escape") { + e.stopPropagation(); + setQuery(""); + } + }; + + const options: DropdownOptions = cycleIds?.map((cycleId) => { + const cycleDetails = getCycleById(cycleId); + const cycleStatus = cycleDetails?.status ? (cycleDetails.status.toLocaleLowerCase() as TCycleGroups) : "draft"; + + return { + value: cycleId, + query: `${cycleDetails?.name}`, + content: ( +
+ + {cycleDetails?.name} +
+ ), + }; + }); + options?.unshift({ + value: null, + query: "No cycle", + content: ( +
+ + No cycle +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + return ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + onKeyDown={searchInputKeyDown} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matches found

+ ) + ) : ( +

Loading...

+ )} +
+
+
+ ); +}); diff --git a/web/components/dropdowns/cycle/index.tsx b/web/components/dropdowns/cycle/index.tsx new file mode 100644 index 00000000000..465eb3e2a9e --- /dev/null +++ b/web/components/dropdowns/cycle/index.tsx @@ -0,0 +1,149 @@ +import { Fragment, ReactNode, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { ChevronDown } from "lucide-react"; +// hooks +import { useCycle } from "hooks/store"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// components +import { DropdownButton } from "../buttons"; +// icons +import { ContrastIcon } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TDropdownProps } from "../types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; +import { CycleOptions } from "./cycle-options"; + +type Props = TDropdownProps & { + button?: ReactNode; + dropdownArrow?: boolean; + dropdownArrowClassName?: string; + onChange: (val: string | null) => void; + onClose?: () => void; + projectId: string; + value: string | null; +}; + +export const CycleDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + dropdownArrowClassName = "", + hideIcon = false, + onChange, + onClose, + placeholder = "Cycle", + placement, + projectId, + showTooltip = false, + tabIndex, + value, + } = props; + // states + + const [isOpen, setIsOpen] = useState(false); + const { getCycleNameById } = useCycle(); + // refs + const dropdownRef = useRef(null); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + + const selectedName = value ? getCycleNameById(value) : null; + + const handleClose = () => { + if (!isOpen) return; + setIsOpen(false); + onClose && onClose(); + }; + + const toggleDropdown = () => { + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string | null) => { + onChange(val); + handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); + + return ( + + + {button ? ( + + ) : ( + + )} + + {isOpen && ( + + )} + + ); +}); diff --git a/web/components/dropdowns/member/index.ts b/web/components/dropdowns/member/index.ts deleted file mode 100644 index a9f7e09c8cd..00000000000 --- a/web/components/dropdowns/member/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./project-member"; -export * from "./workspace-member"; diff --git a/web/components/dropdowns/member/index.tsx b/web/components/dropdowns/member/index.tsx new file mode 100644 index 00000000000..332f2227ad9 --- /dev/null +++ b/web/components/dropdowns/member/index.tsx @@ -0,0 +1,156 @@ +import { Fragment, useRef, useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Combobox } from "@headlessui/react"; +import { ChevronDown } from "lucide-react"; +// hooks +import { useMember } from "hooks/store"; +import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// components +import { ButtonAvatars } from "./avatar"; +import { DropdownButton } from "../buttons"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { MemberDropdownProps } from "./types"; +// constants +import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; +import { MemberOptions } from "./member-options"; + +type Props = { + projectId?: string; + onClose?: () => void; +} & MemberDropdownProps; + +export const MemberDropdown: React.FC = observer((props) => { + const { + button, + buttonClassName, + buttonContainerClassName, + buttonVariant, + className = "", + disabled = false, + dropdownArrow = false, + dropdownArrowClassName = "", + hideIcon = false, + multiple, + onChange, + onClose, + placeholder = "Members", + placement, + projectId, + showTooltip = false, + tabIndex, + value, + } = props; + // states + const [isOpen, setIsOpen] = useState(false); + // refs + const dropdownRef = useRef(null); + // popper-js refs + const [referenceElement, setReferenceElement] = useState(null); + + const { getUserDetails } = useMember(); + + const comboboxProps: any = { + value, + onChange, + disabled, + }; + if (multiple) comboboxProps.multiple = true; + + const handleClose = () => { + if (!isOpen) return; + setIsOpen(false); + onClose && onClose(); + }; + + const toggleDropdown = () => { + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const dropdownOnChange = (val: string & string[]) => { + onChange(val); + if (!multiple) handleClose(); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(dropdownRef, handleClose); + + return ( + + + {button ? ( + + ) : ( + + )} + + {isOpen && ( + + )} + + ); +}); diff --git a/web/components/dropdowns/member/member-options.tsx b/web/components/dropdowns/member/member-options.tsx new file mode 100644 index 00000000000..46a0b9cbad4 --- /dev/null +++ b/web/components/dropdowns/member/member-options.tsx @@ -0,0 +1,142 @@ +import { useEffect, useRef, useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { observer } from "mobx-react"; +//components +import { Avatar } from "@plane/ui"; +//store +import { useApplication, useMember, useUser } from "hooks/store"; +//hooks +import { usePopper } from "react-popper"; +//icon +import { Check, Search } from "lucide-react"; +//types +import { Placement } from "@popperjs/core"; + +interface Props { + projectId?: string; + referenceElement: HTMLButtonElement | null; + placement: Placement | undefined; + isOpen: boolean; +} + +export const MemberOptions = observer((props: Props) => { + const { projectId, referenceElement, placement, isOpen } = props; + + const [query, setQuery] = useState(""); + const [popperElement, setPopperElement] = useState(null); + const inputRef = useRef(null); + + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { + getUserDetails, + project: { getProjectMemberIds, fetchProjectMembers }, + workspace: { workspaceMemberIds }, + } = useMember(); + const { currentUser } = useUser(); + + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + useEffect(() => { + if (isOpen) { + onOpen(); + inputRef.current && inputRef.current.focus(); + } + }, [isOpen]); + + const memberIds = projectId ? getProjectMemberIds(projectId) : workspaceMemberIds; + const onOpen = () => { + if (!memberIds && workspaceSlug && projectId) fetchProjectMembers(workspaceSlug, projectId); + }; + + const searchInputKeyDown = (e: React.KeyboardEvent) => { + if (query !== "" && e.key === "Escape") { + e.stopPropagation(); + setQuery(""); + } + }; + + const options = memberIds?.map((userId) => { + const userDetails = getUserDetails(userId); + + return { + value: userId, + query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, + content: ( +
+ + {currentUser?.id === userId ? "You" : userDetails?.display_name} +
+ ), + }; + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + return ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + onKeyDown={searchInputKeyDown} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+ ); +}); diff --git a/web/components/dropdowns/member/project-member.tsx b/web/components/dropdowns/member/project-member.tsx deleted file mode 100644 index db870253588..00000000000 --- a/web/components/dropdowns/member/project-member.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { Fragment, useEffect, useRef, useState } from "react"; -import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search } from "lucide-react"; -// hooks -import { useApplication, useMember, useUser } from "hooks/store"; -import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// components -import { ButtonAvatars } from "./avatar"; -import { DropdownButton } from "../buttons"; -// icons -import { Avatar } from "@plane/ui"; -// helpers -import { cn } from "helpers/common.helper"; -// types -import { MemberDropdownProps } from "./types"; -// constants -import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; - -type Props = { - projectId: string; - onClose?: () => void; -} & MemberDropdownProps; - -export const ProjectMemberDropdown: React.FC = observer((props) => { - const { - button, - buttonClassName, - buttonContainerClassName, - buttonVariant, - className = "", - disabled = false, - dropdownArrow = false, - dropdownArrowClassName = "", - hideIcon = false, - multiple, - onChange, - onClose, - placeholder = "Members", - placement, - projectId, - showTooltip = false, - tabIndex, - value, - } = props; - // states - const [query, setQuery] = useState(""); - const [isOpen, setIsOpen] = useState(false); - // refs - const dropdownRef = useRef(null); - const inputRef = useRef(null); - // popper-js refs - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // popper-js init - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - // store hooks - const { - router: { workspaceSlug }, - } = useApplication(); - const { currentUser } = useUser(); - const { - getUserDetails, - project: { getProjectMemberIds, fetchProjectMembers }, - } = useMember(); - const projectMemberIds = getProjectMemberIds(projectId); - - const options = projectMemberIds?.map((userId) => { - const userDetails = getUserDetails(userId); - - return { - value: userId, - query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, - content: ( -
- - {currentUser?.id === userId ? "You" : userDetails?.display_name} -
- ), - }; - }); - - const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - - const comboboxProps: any = { - value, - onChange, - disabled, - }; - if (multiple) comboboxProps.multiple = true; - - const onOpen = () => { - if (!projectMemberIds && workspaceSlug) fetchProjectMembers(workspaceSlug, projectId); - }; - - const handleClose = () => { - if (!isOpen) return; - setIsOpen(false); - onClose && onClose(); - }; - - const toggleDropdown = () => { - if (!isOpen) onOpen(); - setIsOpen((prevIsOpen) => !prevIsOpen); - }; - - const dropdownOnChange = (val: string & string[]) => { - onChange(val); - if (!multiple) handleClose(); - }; - - const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); - - const handleOnClick = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - toggleDropdown(); - }; - - const searchInputKeyDown = (e: React.KeyboardEvent) => { - if (query !== "" && e.key === "Escape") { - e.stopPropagation(); - setQuery(""); - } - }; - - useOutsideClickDetector(dropdownRef, handleClose); - - useEffect(() => { - if (isOpen && inputRef.current) { - inputRef.current.focus(); - } - }, [isOpen]); - - return ( - - - {button ? ( - - ) : ( - - )} - - {isOpen && ( - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - onKeyDown={searchInputKeyDown} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( -

No matching results

- ) - ) : ( -

Loading...

- )} -
-
-
- )} -
- ); -}); diff --git a/web/components/dropdowns/member/workspace-member.tsx b/web/components/dropdowns/member/workspace-member.tsx deleted file mode 100644 index e7a679750f9..00000000000 --- a/web/components/dropdowns/member/workspace-member.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { Fragment, useEffect, useRef, useState } from "react"; -import { observer } from "mobx-react-lite"; -import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search } from "lucide-react"; -// hooks -import { useMember, useUser } from "hooks/store"; -import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// components -import { ButtonAvatars } from "./avatar"; -import { DropdownButton } from "../buttons"; -// icons -import { Avatar } from "@plane/ui"; -// helpers -import { cn } from "helpers/common.helper"; -// types -import { MemberDropdownProps } from "./types"; -// constants -import { BUTTON_VARIANTS_WITH_TEXT } from "../constants"; - -export const WorkspaceMemberDropdown: React.FC = observer((props) => { - const { - button, - buttonClassName, - buttonContainerClassName, - buttonVariant, - className = "", - disabled = false, - dropdownArrow = false, - dropdownArrowClassName = "", - hideIcon = false, - multiple, - onChange, - onClose, - placeholder = "Members", - placement, - showTooltip = false, - tabIndex, - value, - } = props; - // states - const [query, setQuery] = useState(""); - const [isOpen, setIsOpen] = useState(false); - // refs - const dropdownRef = useRef(null); - const inputRef = useRef(null); - // popper-js refs - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // popper-js init - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - // store hooks - const { currentUser } = useUser(); - const { - getUserDetails, - workspace: { workspaceMemberIds }, - } = useMember(); - - const options = workspaceMemberIds?.map((userId) => { - const userDetails = getUserDetails(userId); - - return { - value: userId, - query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, - content: ( -
- - {currentUser?.id === userId ? "You" : userDetails?.display_name} -
- ), - }; - }); - - const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - - const comboboxProps: any = { - value, - onChange, - disabled, - }; - if (multiple) comboboxProps.multiple = true; - - const handleClose = () => { - if (!isOpen) return; - setIsOpen(false); - onClose && onClose(); - }; - - const toggleDropdown = () => { - setIsOpen((prevIsOpen) => !prevIsOpen); - }; - - const dropdownOnChange = (val: string & string[]) => { - onChange(val); - if (!multiple) handleClose(); - }; - - const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); - - const handleOnClick = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - toggleDropdown(); - }; - - useOutsideClickDetector(dropdownRef, handleClose); - - useEffect(() => { - if (isOpen && inputRef.current) { - inputRef.current.focus(); - } - }, [isOpen]); - - return ( - - - {button ? ( - - ) : ( - - )} - - {isOpen && ( - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( -

No matching results

- ) - ) : ( -

Loading...

- )} -
-
-
- )} -
- ); -}); diff --git a/web/components/dropdowns/module.tsx b/web/components/dropdowns/module/index.tsx similarity index 61% rename from web/components/dropdowns/module.tsx rename to web/components/dropdowns/module/index.tsx index cee0ed9f319..ee2d746d96b 100644 --- a/web/components/dropdowns/module.tsx +++ b/web/components/dropdowns/module/index.tsx @@ -1,22 +1,22 @@ import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react-lite"; import { Combobox } from "@headlessui/react"; -import { usePopper } from "react-popper"; -import { Check, ChevronDown, Search, X } from "lucide-react"; +import { ChevronDown, X } from "lucide-react"; // hooks -import { useApplication, useModule } from "hooks/store"; +import { useModule } from "hooks/store"; import { useDropdownKeyDown } from "hooks/use-dropdown-key-down"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // components -import { DropdownButton } from "./buttons"; +import { DropdownButton } from "../buttons"; // icons import { DiceIcon, Tooltip } from "@plane/ui"; // helpers import { cn } from "helpers/common.helper"; // types -import { TDropdownProps } from "./types"; +import { TDropdownProps } from "../types"; // constants -import { BUTTON_VARIANTS_WITHOUT_TEXT } from "./constants"; +import { BUTTON_VARIANTS_WITHOUT_TEXT } from "../constants"; +import { ModuleOptions } from "./module-options"; type Props = TDropdownProps & { button?: ReactNode; @@ -38,14 +38,6 @@ type Props = TDropdownProps & { } ); -type DropdownOptions = - | { - value: string | null; - query: string; - content: JSX.Element; - }[] - | undefined; - type ButtonContentProps = { disabled: boolean; dropdownArrow: boolean; @@ -166,64 +158,14 @@ export const ModuleDropdown: React.FC = observer((props) => { value, } = props; // states - const [query, setQuery] = useState(""); const [isOpen, setIsOpen] = useState(false); // refs const dropdownRef = useRef(null); const inputRef = useRef(null); // popper-js refs const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // popper-js init - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement: placement ?? "bottom-start", - modifiers: [ - { - name: "preventOverflow", - options: { - padding: 12, - }, - }, - ], - }); - // store hooks - const { - router: { workspaceSlug }, - } = useApplication(); - const { getProjectModuleIds, fetchModules, getModuleById } = useModule(); - const moduleIds = getProjectModuleIds(projectId); - - const options: DropdownOptions = moduleIds?.map((moduleId) => { - const moduleDetails = getModuleById(moduleId); - return { - value: moduleId, - query: `${moduleDetails?.name}`, - content: ( -
- - {moduleDetails?.name} -
- ), - }; - }); - if (!multiple) - options?.unshift({ - value: null, - query: "No module", - content: ( -
- - No module -
- ), - }); - const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); - - const onOpen = () => { - if (!moduleIds && workspaceSlug) fetchModules(workspaceSlug, projectId); - }; + const { getModuleNameById } = useModule(); const handleClose = () => { if (!isOpen) return; @@ -232,7 +174,6 @@ export const ModuleDropdown: React.FC = observer((props) => { }; const toggleDropdown = () => { - if (!isOpen) onOpen(); setIsOpen((prevIsOpen) => !prevIsOpen); }; @@ -249,13 +190,6 @@ export const ModuleDropdown: React.FC = observer((props) => { toggleDropdown(); }; - const searchInputKeyDown = (e: React.KeyboardEvent) => { - if (query !== "" && e.key === "Escape") { - e.stopPropagation(); - setQuery(""); - } - }; - useOutsideClickDetector(dropdownRef, handleClose); const comboboxProps: any = { @@ -314,7 +248,7 @@ export const ModuleDropdown: React.FC = observer((props) => { tooltipContent={ Array.isArray(value) ? `${value - .map((moduleId) => getModuleById(moduleId)?.name) + .map((moduleId) => getModuleNameById(moduleId)) .toString() .replaceAll(",", ", ")}` : "" @@ -339,61 +273,13 @@ export const ModuleDropdown: React.FC = observer((props) => { )} {isOpen && ( - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - onKeyDown={searchInputKeyDown} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - cn( - "w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none", - { - "bg-custom-background-80": active, - "text-custom-text-100": selected, - "text-custom-text-200": !selected, - } - ) - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( -

No matching results

- ) - ) : ( -

Loading...

- )} -
-
-
+ )} ); diff --git a/web/components/dropdowns/module/module-options.tsx b/web/components/dropdowns/module/module-options.tsx new file mode 100644 index 00000000000..e7d205b126d --- /dev/null +++ b/web/components/dropdowns/module/module-options.tsx @@ -0,0 +1,163 @@ +import { useEffect, useRef, useState } from "react"; +import { Combobox } from "@headlessui/react"; +import { observer } from "mobx-react"; +//components +import { DiceIcon } from "@plane/ui"; +//store +import { useApplication, useModule } from "hooks/store"; +//hooks +import { usePopper } from "react-popper"; +import { cn } from "helpers/common.helper"; +//icon +import { Check, Search } from "lucide-react"; +//types +import { Placement } from "@popperjs/core"; + +type DropdownOptions = + | { + value: string | null; + query: string; + content: JSX.Element; + }[] + | undefined; + +interface Props { + projectId: string; + referenceElement: HTMLButtonElement | null; + placement: Placement | undefined; + isOpen: boolean; + multiple: boolean; +} + +export const ModuleOptions = observer((props: Props) => { + const { projectId, isOpen, referenceElement, placement, multiple } = props; + + const [query, setQuery] = useState(""); + const [popperElement, setPopperElement] = useState(null); + const inputRef = useRef(null); + + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getProjectModuleIds, fetchModules, getModuleById } = useModule(); + + useEffect(() => { + if (isOpen) { + onOpen(); + inputRef.current && inputRef.current.focus(); + } + }, [isOpen]); + + // popper-js init + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const moduleIds = getProjectModuleIds(projectId); + + const onOpen = () => { + if (workspaceSlug && !moduleIds) fetchModules(workspaceSlug, projectId); + }; + + const searchInputKeyDown = (e: React.KeyboardEvent) => { + if (query !== "" && e.key === "Escape") { + e.stopPropagation(); + setQuery(""); + } + }; + + const options: DropdownOptions = moduleIds?.map((moduleId) => { + const moduleDetails = getModuleById(moduleId); + return { + value: moduleId, + query: `${moduleDetails?.name}`, + content: ( +
+ + {moduleDetails?.name} +
+ ), + }; + }); + if (!multiple) + options?.unshift({ + value: null, + query: "No module", + content: ( +
+ + No module +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + + return ( + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + onKeyDown={searchInputKeyDown} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + cn( + "w-full truncate flex items-center justify-between gap-2 rounded px-1 py-1.5 cursor-pointer select-none", + { + "bg-custom-background-80": active, + "text-custom-text-100": selected, + "text-custom-text-200": !selected, + } + ) + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( +

No matching results

+ ) + ) : ( +

Loading...

+ )} +
+
+
+ ); +}); diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx index cfd6370fad2..b08d1d31d83 100644 --- a/web/components/issues/draft-issue-form.tsx +++ b/web/components/issues/draft-issue-form.tsx @@ -21,10 +21,10 @@ import { CycleDropdown, DateDropdown, EstimateDropdown, + MemberDropdown, ModuleDropdown, PriorityDropdown, ProjectDropdown, - ProjectMemberDropdown, StateDropdown, } from "components/dropdowns"; // ui @@ -474,7 +474,7 @@ export const DraftIssueForm: FC = observer((props) => { name="assignee_ids" render={({ field: { value, onChange } }) => (
- = observer((props) => { Assignees
- issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })} disabled={!is_editable} diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index 0db0ed29f0f..76a14c6db00 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -27,13 +27,7 @@ import { IssueLabel, } from "components/issues"; import { IssueSubscription } from "./subscription"; -import { - DateDropdown, - EstimateDropdown, - PriorityDropdown, - ProjectMemberDropdown, - StateDropdown, -} from "components/dropdowns"; +import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // icons import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, UserGroupIcon } from "@plane/ui"; // helpers @@ -161,7 +155,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { Assignees
- issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })} disabled={!is_editable} diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index 9f353230293..b5d0c434609 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -26,7 +26,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { const { router: { workspaceSlug, projectId }, } = useApplication(); - const { getProjectById } = useProject(); + const { getProjectIdentifierById } = useProject(); const { getProjectStates } = useProjectState(); const { peekIssue, setPeekIssue } = useIssueDetail(); // states @@ -108,7 +108,7 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { }} />
- {getProjectById(issue?.project_id)?.identifier}-{issue.sequence_id} + {getProjectIdentifierById(issue?.project_id)}-{issue.sequence_id}
{issue.name}
diff --git a/web/components/issues/issue-layouts/gantt/blocks.tsx b/web/components/issues/issue-layouts/gantt/blocks.tsx index d668b8a44dc..209d876ac56 100644 --- a/web/components/issues/issue-layouts/gantt/blocks.tsx +++ b/web/components/issues/issue-layouts/gantt/blocks.tsx @@ -66,7 +66,7 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => { const { issueId } = props; // store hooks const { getStateById } = useProjectState(); - const { getProjectById } = useProject(); + const { getProjectIdentifierById } = useProject(); const { router: { workspaceSlug }, } = useApplication(); @@ -76,7 +76,7 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => { } = useIssueDetail(); // derived values const issueDetails = getIssueById(issueId); - const projectDetails = issueDetails && getProjectById(issueDetails?.project_id); + const projectIdentifier = issueDetails && getProjectIdentifierById(issueDetails?.project_id); const stateDetails = issueDetails && getStateById(issueDetails?.state_id); const handleIssuePeekOverview = () => @@ -95,7 +95,7 @@ export const IssueGanttSidebarBlock: React.FC = observer((props) => {
{stateDetails && }
- {projectDetails?.identifier} {issueDetails?.sequence_id} + {projectIdentifier} {issueDetails?.sequence_id}
{issueDetails?.name} diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 0dc9aa9083d..8446e7328c3 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -42,9 +42,9 @@ interface IssueDetailsBlockProps { const KanbanIssueDetailsBlock: React.FC = observer((props: IssueDetailsBlockProps) => { const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props; // hooks - const { getProjectById } = useProject(); + const { getProjectIdentifierById } = useProject(); const { - router: { workspaceSlug, projectId }, + router: { workspaceSlug }, } = useApplication(); const { setPeekIssue } = useIssueDetail(); @@ -64,7 +64,7 @@ const KanbanIssueDetailsBlock: React.FC = observer((prop
- {getProjectById(issue.project_id)?.identifier}-{issue.sequence_id} + {getProjectIdentifierById(issue.project_id)}-{issue.sequence_id}
{quickActions(issue)}
diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index 2b311f6eb95..f496a51209d 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks @@ -46,7 +46,15 @@ export const CycleKanBanLayout: React.FC = observer(() => { const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; - const canEditIssueProperties = () => !isCompletedCycle; + const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]); + + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !cycleId) throw new Error(); + return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); + }, + [issues?.addIssueToCycle, workspaceSlug, projectId, cycleId] + ); return ( { QuickActions={CycleIssueQuickActions} viewId={cycleId?.toString() ?? ""} storeType={EIssuesStoreType.CYCLE} - addIssuesToView={(issueIds: string[]) => { - if (!workspaceSlug || !projectId || !cycleId) throw new Error(); - return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); - }} + addIssuesToView={addIssuesToView} canEditPropertiesBasedOnProject={canEditIssueProperties} isCompletedCycle={isCompletedCycle} /> diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 1bbd574ed70..cc04ed71620 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -24,9 +24,9 @@ export const IssueBlock: React.FC = observer((props: IssueBlock const { issuesMap, issueId, handleIssues, quickActions, displayProperties, canEditProperties } = props; // hooks const { - router: { workspaceSlug, projectId }, + router: { workspaceSlug }, } = useApplication(); - const { getProjectById } = useProject(); + const { getProjectIdentifierById } = useProject(); const { peekIssue, setPeekIssue } = useIssueDetail(); const updateIssue = async (issueToUpdate: TIssue) => { @@ -45,7 +45,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock if (!issue) return null; const canEditIssueProperties = canEditProperties(issue.project_id); - const projectDetails = getProjectById(issue.project_id); + const projectIdentifier = getProjectIdentifierById(issue.project_id); return (
= observer((props: IssueBlock > {displayProperties && displayProperties?.key && (
- {projectDetails?.identifier}-{issue.sequence_id} + {projectIdentifier}-{issue.sequence_id}
)} diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index e30c207b649..d7ea6690437 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks @@ -44,7 +44,15 @@ export const CycleListLayout: React.FC = observer(() => { const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; - const canEditIssueProperties = () => !isCompletedCycle; + const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]); + + const addIssuesToView = useCallback( + (issueIds: string[]) => { + if (!workspaceSlug || !projectId || !cycleId) throw new Error(); + return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); + }, + [issues?.addIssueToCycle, workspaceSlug, projectId, cycleId] + ); return ( { issueActions={issueActions} viewId={cycleId?.toString()} storeType={EIssuesStoreType.CYCLE} - addIssuesToView={(issueIds: string[]) => { - if (!workspaceSlug || !projectId || !cycleId) throw new Error(); - return issues.addIssueToCycle(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), issueIds); - }} + addIssuesToView={addIssuesToView} canEditPropertiesBasedOnProject={canEditIssueProperties} isCompletedCycle={isCompletedCycle} /> diff --git a/web/components/issues/issue-layouts/properties/all-properties.tsx b/web/components/issues/issue-layouts/properties/all-properties.tsx index 8c16d3e2418..ce97f1afa47 100644 --- a/web/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/components/issues/issue-layouts/properties/all-properties.tsx @@ -13,7 +13,7 @@ import { DateDropdown, EstimateDropdown, PriorityDropdown, - ProjectMemberDropdown, + MemberDropdown, ModuleDropdown, CycleDropdown, StateDropdown, @@ -313,7 +313,7 @@ export const IssueProperties: React.FC = observer((props) => { {/* assignee */}
- { } }; - useSWR(workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS${workspaceSlug}` : null, async () => { - if (workspaceSlug) { - await fetchAllGlobalViews(workspaceSlug.toString()); - } - }); + useSWR( + workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS_${workspaceSlug}` : null, + async () => { + if (workspaceSlug) { + await fetchAllGlobalViews(workspaceSlug.toString()); + } + }, + { revalidateIfStale: false, revalidateOnFocus: false } + ); useSWR( workspaceSlug && globalViewId ? `WORKSPACE_GLOBAL_VIEW_ISSUES_${workspaceSlug}_${globalViewId}` : null, @@ -103,7 +107,8 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader"); routerFilterParams(); } - } + }, + { revalidateIfStale: false, revalidateOnFocus: false } ); const canEditProperties = useCallback( diff --git a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx index 5f049d4c39c..7db9a1e3b77 100644 --- a/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx @@ -33,7 +33,8 @@ export const ArchivedIssueLayoutRoot: React.FC = observer(() => { issues?.groupedIssueIds ? "mutation" : "init-loader" ); } - } + }, + { revalidateIfStale: false, revalidateOnFocus: false } ); if (issues?.loader === "init-loader" || !issues?.groupedIssueIds) { diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index bc9c8c3975a..759495284be 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -47,7 +47,8 @@ export const CycleLayoutRoot: React.FC = observer(() => { cycleId.toString() ); } - } + }, + { revalidateIfStale: false, revalidateOnFocus: false } ); const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; diff --git a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx index 6297ea0cd41..02b666ceb86 100644 --- a/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/draft-issue-layout-root.tsx @@ -33,7 +33,8 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => { issues?.groupedIssueIds ? "mutation" : "init-loader" ); } - } + }, + { revalidateIfStale: false, revalidateOnFocus: false } ); const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined; diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index b7978c7bccf..14505c65aa5 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -43,7 +43,8 @@ export const ModuleLayoutRoot: React.FC = observer(() => { moduleId.toString() ); } - } + }, + { revalidateIfStale: false, revalidateOnFocus: false } ); const userFilters = issuesFilter?.issueFilters?.filters; diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index 75bb4bfadf5..cae73610efa 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -29,16 +29,20 @@ export const ProjectLayoutRoot: FC = observer(() => { // hooks const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null, async () => { - if (workspaceSlug && projectId) { - await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); - await issues?.fetchIssues( - workspaceSlug.toString(), - projectId.toString(), - issues?.groupedIssueIds ? "mutation" : "init-loader" - ); - } - }); + useSWR( + workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null, + async () => { + if (workspaceSlug && projectId) { + await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString()); + await issues?.fetchIssues( + workspaceSlug.toString(), + projectId.toString(), + issues?.groupedIssueIds ? "mutation" : "init-loader" + ); + } + }, + { revalidateIfStale: false, revalidateOnFocus: false } + ); const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index 2329e52df79..fa942b7f604 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -41,7 +41,8 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => { viewId.toString() ); } - } + }, + { revalidateIfStale: false, revalidateOnFocus: false } ); const issueActions = useMemo( diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx index b9450141b97..c4b8ea0ef75 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx @@ -1,7 +1,7 @@ import React from "react"; import { observer } from "mobx-react-lite"; // components -import { ProjectMemberDropdown } from "components/dropdowns"; +import { MemberDropdown } from "components/dropdowns"; // types import { TIssue } from "@plane/types"; @@ -17,7 +17,7 @@ export const SpreadsheetAssigneeColumn: React.FC = observer((props: Props return (
- { onChange( diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index b241a51686f..41734a86741 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -142,7 +142,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { const router = useRouter(); const { workspaceSlug } = router.query; //hooks - const { getProjectById } = useProject(); + const { getProjectIdentifierById } = useProject(); const { peekIssue, setPeekIssue } = useIssueDetail(); // states const [isMenuActive, setIsMenuActive] = useState(false); @@ -212,7 +212,7 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { isMenuActive ? "opacity-0" : "opacity-100" }`} > - {getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id} + {getProjectIdentifierById(issueDetail.project_id)}-{issueDetail.sequence_id} {canEditProperties(issueDetail.project_id) && ( diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index 7f92bd74ca6..0ac5db0aa93 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // mobx store @@ -39,7 +39,7 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => { const isCompletedCycle = cycleId && currentProjectCompletedCycleIds ? currentProjectCompletedCycleIds.includes(cycleId.toString()) : false; - const canEditIssueProperties = () => !isCompletedCycle; + const canEditIssueProperties = useCallback(() => !isCompletedCycle, [isCompletedCycle]); return ( = observer((props) => { name="assignee_ids" render={({ field: { value, onChange } }) => (
- { diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 4c2833efa26..84a625d201b 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -54,7 +54,6 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const { router: { workspaceSlug, projectId, cycleId, moduleId, viewId: projectViewId }, } = useApplication(); - const { currentWorkspace } = useWorkspace(); const { workspaceProjectIds } = useProject(); const { fetchCycleDetails } = useCycle(); const { fetchModuleDetails } = useModule(); diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index 2b428a57bbb..2588dafecae 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -14,13 +14,7 @@ import { TIssueOperations, IssueRelationSelect, } from "components/issues"; -import { - DateDropdown, - EstimateDropdown, - PriorityDropdown, - ProjectMemberDropdown, - StateDropdown, -} from "components/dropdowns"; +import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // components import { renderFormattedPayloadDate } from "helpers/date-time.helper"; // helpers @@ -87,7 +81,7 @@ export const PeekOverviewProperties: FC = observer((pro Assignees
- issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })} disabled={disabled} diff --git a/web/components/issues/sub-issues/properties.tsx b/web/components/issues/sub-issues/properties.tsx index 3a205aea285..03c9d8902d1 100644 --- a/web/components/issues/sub-issues/properties.tsx +++ b/web/components/issues/sub-issues/properties.tsx @@ -2,7 +2,7 @@ import React from "react"; // hooks import { useIssueDetail } from "hooks/store"; // components -import { PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns"; +import { PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns"; // types import { TSubIssueOperations } from "./root"; @@ -62,7 +62,7 @@ export const IssueProperty: React.FC = (props) => {
- diff --git a/web/components/labels/index.ts b/web/components/labels/index.ts index a22251c64d7..9195f8649b2 100644 --- a/web/components/labels/index.ts +++ b/web/components/labels/index.ts @@ -1,7 +1,6 @@ export * from "./create-label-modal"; export * from "./create-update-label-inline"; export * from "./delete-label-modal"; -export * from "./labels-list-modal"; export * from "./project-setting-label-group"; export * from "./project-setting-label-item"; export * from "./project-setting-label-list"; diff --git a/web/components/labels/labels-list-modal.tsx b/web/components/labels/labels-list-modal.tsx deleted file mode 100644 index 82920b9bae5..00000000000 --- a/web/components/labels/labels-list-modal.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useState } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import useSWR from "swr"; -import { Combobox, Dialog, Transition } from "@headlessui/react"; -import { Search } from "lucide-react"; -// hooks -import { useLabel } from "hooks/store"; -// icons -import { LayerStackIcon } from "@plane/ui"; -// types -import { IIssueLabel } from "@plane/types"; - -type Props = { - isOpen: boolean; - handleClose: () => void; - parent: IIssueLabel | undefined; -}; - -export const LabelsListModal: React.FC = observer((props) => { - const { isOpen, handleClose, parent } = props; - // states - const [query, setQuery] = useState(""); - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - // store hooks - const { projectLabels, fetchProjectLabels, updateLabel } = useLabel(); - - // api call to fetch project details - useSWR( - workspaceSlug && projectId ? "PROJECT_LABELS" : null, - workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null - ); - - // derived values - const filteredLabels: IIssueLabel[] = - query === "" - ? projectLabels ?? [] - : projectLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())) ?? []; - - const handleModalClose = () => { - handleClose(); - setQuery(""); - }; - - const addChildLabel = async (label: IIssueLabel) => { - if (!workspaceSlug || !projectId) return; - - await updateLabel(workspaceSlug.toString(), projectId.toString(), label.id, { - parent: parent?.id!, - }); - }; - - return ( - setQuery("")} appear> - - -
- - -
- - - -
-
- - - {filteredLabels.length > 0 && ( -
  • - {query === "" && ( -

    Labels

    - )} -
      - {filteredLabels.map((label) => { - const children = projectLabels?.filter((l) => l.parent === label.id); - - if ( - (label.parent === "" || label.parent === null) && // issue does not have any other parent - label.id !== parent?.id && // issue is not itself - children?.length === 0 // issue doesn't have any other children - ) - return ( - - `flex w-full cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-custom-text-200 ${ - active ? "bg-custom-background-80 text-custom-text-100" : "" - }` - } - onClick={() => { - addChildLabel(label); - }} - > - - {label.name} - - ); - })} -
    -
  • - )} -
    - - {query !== "" && filteredLabels.length === 0 && ( -
    -
    - )} -
    -
    -
    -
    -
    -
    - ); -}); diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index 19147635796..1dde2b85d0a 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; // components import { ModuleStatusSelect } from "components/modules"; -import { DateRangeDropdown, ProjectDropdown, ProjectMemberDropdown } from "components/dropdowns"; +import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "components/dropdowns"; // ui import { Button, Input, TextArea } from "@plane/ui"; // helpers @@ -175,7 +175,7 @@ export const ModuleForm: React.FC = (props) => { name="lead_id" render={({ field: { value, onChange } }) => (
    - = (props) => { name="member_ids" render={({ field: { value, onChange } }) => (
    - = observer((props) => { name="lead_id" render={({ field: { value } }) => (
    - { submitChanges({ lead_id: val }); @@ -409,7 +409,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { name="member_ids" render={({ field: { value } }) => (
    - { submitChanges({ member_ids: val }); diff --git a/web/components/profile/profile-issues.tsx b/web/components/profile/profile-issues.tsx index e7b61b4d22f..7e501764a4b 100644 --- a/web/components/profile/profile-issues.tsx +++ b/web/components/profile/profile-issues.tsx @@ -51,7 +51,8 @@ export const ProfileIssuesPage = observer((props: IProfileIssuesPage) => { await fetchFilters(workspaceSlug, userId); await fetchIssues(workspaceSlug, undefined, groupedIssueIds ? "mutation" : "init-loader", userId, type); } - } + }, + { revalidateIfStale: false, revalidateOnFocus: false } ); const isLightMode = resolvedTheme ? resolvedTheme === "light" : currentUser?.theme.theme === "light"; diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 83589795e19..49a42a0a3a3 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -11,7 +11,7 @@ import { Button, CustomSelect, Input, TextArea } from "@plane/ui"; // components import { ImagePickerPopover } from "components/core"; import EmojiIconPicker from "components/emoji-icon-picker"; -import { WorkspaceMemberDropdown } from "components/dropdowns"; +import { MemberDropdown } from "components/dropdowns"; // helpers import { getRandomEmoji, renderEmoji } from "helpers/emoji.helper"; // constants @@ -383,7 +383,7 @@ export const CreateProjectModal: FC = observer((props) => { control={control} render={({ field: { value, onChange } }) => (
    - fetchWorkspaceModules(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceModules(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetch workspace Cycles useSWR( workspaceSlug ? `WORKSPACE_CYCLES_${workspaceSlug}` : null, - workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetch workspace labels useSWR( workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null, - workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetch workspace states useSWR( workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null, - workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetch workspace estimates useSWR( workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null, - workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); }; diff --git a/web/layouts/app-layout/layout.tsx b/web/layouts/app-layout/layout.tsx index d0bd7849a7e..2a788e7617e 100644 --- a/web/layouts/app-layout/layout.tsx +++ b/web/layouts/app-layout/layout.tsx @@ -33,7 +33,7 @@ export const AppLayout: FC = observer((props) => { // await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader"); } }, - { revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true } + { revalidateIfStale: false, revalidateOnFocus: false } ); return ( diff --git a/web/layouts/auth-layout/project-wrapper.tsx b/web/layouts/auth-layout/project-wrapper.tsx index b9fd22f2d17..bdd2da8b524 100644 --- a/web/layouts/auth-layout/project-wrapper.tsx +++ b/web/layouts/auth-layout/project-wrapper.tsx @@ -66,37 +66,44 @@ export const ProjectAuthWrapper: FC = observer((props) => { // fetching project labels useSWR( workspaceSlug && projectId ? `PROJECT_LABELS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchProjectLabels(workspaceSlug.toString(), projectId.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project members useSWR( workspaceSlug && projectId ? `PROJECT_MEMBERS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchProjectMembers(workspaceSlug.toString(), projectId.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project states useSWR( workspaceSlug && projectId ? `PROJECT_STATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchProjectStates(workspaceSlug.toString(), projectId.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project estimates useSWR( workspaceSlug && projectId ? `PROJECT_ESTIMATES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchProjectEstimates(workspaceSlug.toString(), projectId.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project cycles useSWR( workspaceSlug && projectId ? `PROJECT_ALL_CYCLES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchAllCycles(workspaceSlug.toString(), projectId.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project modules useSWR( workspaceSlug && projectId ? `PROJECT_MODULES_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchModules(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchModules(workspaceSlug.toString(), projectId.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project views useSWR( workspaceSlug && projectId ? `PROJECT_VIEWS_${workspaceSlug}_${projectId}` : null, - workspaceSlug && projectId ? () => fetchViews(workspaceSlug.toString(), projectId.toString()) : null + workspaceSlug && projectId ? () => fetchViews(workspaceSlug.toString(), projectId.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching project inboxes if inbox is enabled in project settings useSWR( diff --git a/web/layouts/auth-layout/workspace-wrapper.tsx b/web/layouts/auth-layout/workspace-wrapper.tsx index 9f402cb5710..ba64983012e 100644 --- a/web/layouts/auth-layout/workspace-wrapper.tsx +++ b/web/layouts/auth-layout/workspace-wrapper.tsx @@ -26,22 +26,26 @@ export const WorkspaceAuthWrapper: FC = observer((props) // fetching user workspace information useSWR( workspaceSlug ? `WORKSPACE_MEMBERS_ME_${workspaceSlug}` : null, - workspaceSlug ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null + workspaceSlug ? () => membership.fetchUserWorkspaceInfo(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetching workspace projects useSWR( workspaceSlug ? `WORKSPACE_PROJECTS_${workspaceSlug}` : null, - workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchProjects(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetch workspace members useSWR( workspaceSlug ? `WORKSPACE_MEMBERS_${workspaceSlug}` : null, - workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null + workspaceSlug ? () => fetchWorkspaceMembers(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // fetch workspace user projects role useSWR( workspaceSlug ? `WORKSPACE_PROJECTS_ROLE_${workspaceSlug}` : null, - workspaceSlug ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) : null + workspaceSlug ? () => membership.fetchUserWorkspaceProjectsRole(workspaceSlug.toString()) : null, + { revalidateIfStale: false, revalidateOnFocus: false } ); // while data is being loaded diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index a970eee8222..ee4842539b9 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -28,6 +28,7 @@ export interface ICycleStore { currentProjectActiveCycleId: string | null; // computed actions getCycleById: (cycleId: string) => ICycle | null; + getCycleNameById: (cycleId: string) => string | undefined; getActiveCycleById: (cycleId: string) => ICycle | null; getProjectCycleIds: (projectId: string) => string[] | null; // actions @@ -189,6 +190,13 @@ export class CycleStore implements ICycleStore { */ getCycleById = computedFn((cycleId: string): ICycle | null => this.cycleMap?.[cycleId] ?? null); + /** + * @description returns cycle name by cycle id + * @param cycleId + * @returns + */ + getCycleNameById = computedFn((cycleId: string): string => this.cycleMap?.[cycleId]?.name); + /** * @description returns active cycle details by cycle id * @param cycleId diff --git a/web/store/module.store.ts b/web/store/module.store.ts index cd6b7100aab..2b4522cd09a 100644 --- a/web/store/module.store.ts +++ b/web/store/module.store.ts @@ -19,6 +19,7 @@ export interface IModuleStore { projectModuleIds: string[] | null; // computed actions getModuleById: (moduleId: string) => IModule | null; + getModuleNameById: (moduleId: string) => string; getProjectModuleIds: (projectId: string) => string[] | null; // actions // fetch @@ -114,6 +115,13 @@ export class ModulesStore implements IModuleStore { */ getModuleById = computedFn((moduleId: string) => this.moduleMap?.[moduleId] || null); + /** + * @description get module by id + * @param moduleId + * @returns IModule | null + */ + getModuleNameById = computedFn((moduleId: string) => this.moduleMap?.[moduleId]?.name); + /** * @description returns list of module ids of the project id passed as argument * @param projectId diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index a47d459f8f5..c9aa826feae 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -24,6 +24,7 @@ export interface IProjectStore { // actions setSearchQuery: (query: string) => void; getProjectById: (projectId: string) => IProject | null; + getProjectIdentifierById: (projectId: string) => string; // fetch actions fetchProjects: (workspaceSlug: string) => Promise; fetchProjectDetails: (workspaceSlug: string, projectId: string) => Promise; @@ -210,6 +211,16 @@ export class ProjectStore implements IProjectStore { return projectInfo; }); + /** + * Returns project identifier using project id + * @param projectId + * @returns string + */ + getProjectIdentifierById = computedFn((projectId: string) => { + const projectInfo = this.projectMap?.[projectId]; + return projectInfo?.identifier; + }); + /** * Adds project to favorites and updates project favorite status in the store * @param workspaceSlug From 7e46cbcb52d61d2419b8a6bc24713df6e1054356 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Wed, 28 Feb 2024 15:19:11 +0530 Subject: [PATCH 09/16] [WEB-127] fix: issue with command palette outside click detection. (#3812) --- .../command-palette/command-modal.tsx | 418 +++++++++--------- 1 file changed, 210 insertions(+), 208 deletions(-) diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index bd489f4c4c3..b52976aa8c8 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -154,237 +154,239 @@ export const CommandModal: React.FC = observer(() => {
    -
    - - -
    - { - if (value.toLowerCase().includes(search.toLowerCase())) return 1; - return 0; - }} - onKeyDown={(e) => { - // when search is empty and page is undefined - // when user tries to close the modal with esc - if (e.key === "Escape" && !page && !searchTerm) closePalette(); +
    +
    + + +
    + { + if (value.toLowerCase().includes(search.toLowerCase())) return 1; + return 0; + }} + onKeyDown={(e) => { + // when search is empty and page is undefined + // when user tries to close the modal with esc + if (e.key === "Escape" && !page && !searchTerm) closePalette(); - // Escape goes to previous page - // Backspace goes to previous page when search is empty - if (e.key === "Escape" || (e.key === "Backspace" && !searchTerm)) { - e.preventDefault(); - setPages((pages) => pages.slice(0, -1)); - setPlaceholder("Type a command or search..."); - } - }} - > -
    pages.slice(0, -1)); + setPlaceholder("Type a command or search..."); + } + }} > - {issueDetails && ( -
    - {projectDetails?.identifier}-{issueDetails.sequence_id} {issueDetails.name} -
    - )} - {projectId && ( - -
    - - setIsWorkspaceLevel((prevData) => !prevData)} - /> +
    + {issueDetails && ( +
    + {projectDetails?.identifier}-{issueDetails.sequence_id} {issueDetails.name}
    - - )} -
    -
    -
    - - - {searchTerm !== "" && ( -
    - Search results for{" "} - - {'"'} - {searchTerm} - {'"'} - {" "} - in {!projectId || isWorkspaceLevel ? "workspace" : "project"}: -
    - )} + )} + {projectId && ( + +
    + + setIsWorkspaceLevel((prevData) => !prevData)} + /> +
    +
    + )} +
    +
    +
    - {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
    No results found.
    - )} + + {searchTerm !== "" && ( +
    + Search results for{" "} + + {'"'} + {searchTerm} + {'"'} + {" "} + in {!projectId || isWorkspaceLevel ? "workspace" : "project"}: +
    + )} - {(isLoading || isSearching) && ( - - - - - - - - - )} + {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( +
    No results found.
    + )} - {debouncedSearchTerm !== "" && ( - - )} + {(isLoading || isSearching) && ( + + + + + + + + + )} - {!page && ( - <> - {/* issue actions */} - {issueId && ( - setPages(newPages)} - setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)} - setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)} - /> - )} - - { - closePalette(); - setTrackElement("Command Palette"); - toggleCreateIssueModal(true); - }} - className="focus:bg-custom-background-80" - > -
    - - Create new issue -
    - C -
    -
    + {debouncedSearchTerm !== "" && ( + + )} - {workspaceSlug && ( - + {!page && ( + <> + {/* issue actions */} + {issueId && ( + setPages(newPages)} + setPlaceholder={(newPlaceholder) => setPlaceholder(newPlaceholder)} + setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)} + /> + )} + { closePalette(); - setTrackElement("Command palette"); - toggleCreateProjectModal(true); + setTrackElement("Command Palette"); + toggleCreateIssueModal(true); }} - className="focus:outline-none" + className="focus:bg-custom-background-80" >
    - - Create new project + + Create new issue
    - P + C
    - )} - {/* project actions */} - {projectId && } + {workspaceSlug && ( + + { + closePalette(); + setTrackElement("Command palette"); + toggleCreateProjectModal(true); + }} + className="focus:outline-none" + > +
    + + Create new project +
    + P +
    +
    + )} - - { - setPlaceholder("Search workspace settings..."); - setSearchTerm(""); - setPages([...pages, "settings"]); - }} - className="focus:outline-none" - > -
    - - Search settings... -
    -
    -
    - - -
    - - Create new workspace -
    -
    - { - setPlaceholder("Change interface theme..."); - setSearchTerm(""); - setPages([...pages, "change-interface-theme"]); - }} - className="focus:outline-none" - > -
    - - Change interface theme... -
    -
    -
    + {/* project actions */} + {projectId && } + + + { + setPlaceholder("Search workspace settings..."); + setSearchTerm(""); + setPages([...pages, "settings"]); + }} + className="focus:outline-none" + > +
    + + Search settings... +
    +
    +
    + + +
    + + Create new workspace +
    +
    + { + setPlaceholder("Change interface theme..."); + setSearchTerm(""); + setPages([...pages, "change-interface-theme"]); + }} + className="focus:outline-none" + > +
    + + Change interface theme... +
    +
    +
    - {/* help options */} - - - )} + {/* help options */} + + + )} - {/* workspace settings actions */} - {page === "settings" && workspaceSlug && ( - - )} + {/* workspace settings actions */} + {page === "settings" && workspaceSlug && ( + + )} - {/* issue details page actions */} - {page === "change-issue-state" && issueDetails && ( - - )} - {page === "change-issue-priority" && issueDetails && ( - - )} - {page === "change-issue-assignee" && issueDetails && ( - - )} + {/* issue details page actions */} + {page === "change-issue-state" && issueDetails && ( + + )} + {page === "change-issue-priority" && issueDetails && ( + + )} + {page === "change-issue-assignee" && issueDetails && ( + + )} - {/* theme actions */} - {page === "change-interface-theme" && ( - { - closePalette(); - setPages((pages) => pages.slice(0, -1)); - }} - /> - )} -
    - -
    - - + {/* theme actions */} + {page === "change-interface-theme" && ( + { + closePalette(); + setPages((pages) => pages.slice(0, -1)); + }} + /> + )} + +
    +
    +
    +
    +
    From 895ff03cf2c110ddea114c14f32f7c6d3c322fc1 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:19:54 +0530 Subject: [PATCH 10/16] chore: issue comment edit validation (#3817) --- .../issue-activity/comments/comment-card.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/web/components/issues/issue-detail/issue-activity/comments/comment-card.tsx b/web/components/issues/issue-detail/issue-activity/comments/comment-card.tsx index 2000721ee18..722fd80a513 100644 --- a/web/components/issues/issue-detail/issue-activity/comments/comment-card.tsx +++ b/web/components/issues/issue-detail/issue-activity/comments/comment-card.tsx @@ -14,6 +14,8 @@ import { FileService } from "services/file.service"; // types import { TIssueComment } from "@plane/types"; import { TActivityOperations } from "../root"; +// helpers +import { isEmptyHtmlString } from "helpers/string.helper"; const fileService = new FileService(); @@ -67,6 +69,12 @@ export const IssueCommentCard: FC = (props) => { isEditing && setFocus("comment_html"); }, [isEditing, setFocus]); + const isEmpty = + watch("comment_html") === "" || + watch("comment_html")?.trim() === "" || + watch("comment_html") === "

    " || + isEmptyHtmlString(watch("comment_html") ?? ""); + if (!comment || !currentUser) return <>; return ( = (props) => { > <>
    -
    +
    { + if (e.key === "Enter" && !e.shiftKey && !isEmpty) { + handleSubmit(onEnter)(e); + } + }} + > = (props) => { - -
    -
    - - -
    -
    - - - ); -}; diff --git a/web/components/issues/draft-issue-form.tsx b/web/components/issues/draft-issue-form.tsx deleted file mode 100644 index cfd6370fad2..00000000000 --- a/web/components/issues/draft-issue-form.tsx +++ /dev/null @@ -1,668 +0,0 @@ -import React, { FC, useState, useEffect, useRef } from "react"; -import { useRouter } from "next/router"; -import { Controller, useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; -import { Sparkle, X } from "lucide-react"; -// hooks -import { useApplication, useEstimate, useMention, useProject, useWorkspace } from "hooks/store"; -import useToast from "hooks/use-toast"; -import useLocalStorage from "hooks/use-local-storage"; -// services -import { AIService } from "services/ai.service"; -import { FileService } from "services/file.service"; -// components -import { GptAssistantPopover } from "components/core"; -import { ParentIssuesListModal } from "components/issues"; -import { IssueLabelSelect } from "components/issues/select"; -import { CreateStateModal } from "components/states"; -import { CreateLabelModal } from "components/labels"; -import { RichTextEditorWithRef } from "@plane/rich-text-editor"; -import { - CycleDropdown, - DateDropdown, - EstimateDropdown, - ModuleDropdown, - PriorityDropdown, - ProjectDropdown, - ProjectMemberDropdown, - StateDropdown, -} from "components/dropdowns"; -// ui -import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui"; -// helpers -import { renderFormattedPayloadDate } from "helpers/date-time.helper"; -// types -import type { IUser, TIssue, ISearchIssueResponse } from "@plane/types"; - -const aiService = new AIService(); -const fileService = new FileService(); - -const defaultValues: Partial = { - project_id: "", - name: "", - description_html: "

    ", - estimate_point: null, - state_id: "", - parent_id: null, - priority: "none", - assignee_ids: [], - label_ids: [], - start_date: undefined, - target_date: undefined, -}; - -interface IssueFormProps { - handleFormSubmit: ( - formData: Partial, - action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" - ) => Promise; - data?: Partial | null; - isOpen: boolean; - prePopulatedData?: Partial | null; - projectId: string; - setActiveProject: React.Dispatch>; - createMore: boolean; - setCreateMore: React.Dispatch>; - handleClose: () => void; - handleDiscard: () => void; - status: boolean; - user: IUser | undefined; - fieldsToShow: ( - | "project" - | "name" - | "description" - | "state" - | "priority" - | "assignee" - | "label" - | "startDate" - | "dueDate" - | "estimate" - | "parent" - | "all" - )[]; -} - -export const DraftIssueForm: FC = observer((props) => { - const { - handleFormSubmit, - data, - isOpen, - prePopulatedData, - projectId, - setActiveProject, - createMore, - setCreateMore, - status, - fieldsToShow, - handleDiscard, - } = props; - // states - const [stateModal, setStateModal] = useState(false); - const [labelModal, setLabelModal] = useState(false); - const [parentIssueListModalOpen, setParentIssueListModalOpen] = useState(false); - const [selectedParentIssue, setSelectedParentIssue] = useState(null); - const [gptAssistantModal, setGptAssistantModal] = useState(false); - const [iAmFeelingLucky, setIAmFeelingLucky] = useState(false); - // store hooks - const { areEstimatesEnabledForProject } = useEstimate(); - const { mentionHighlights, mentionSuggestions } = useMention(); - // hooks - const { setValue: setLocalStorageValue } = useLocalStorage("draftedIssue", {}); - const { setToastAlert } = useToast(); - // refs - const editorRef = useRef(null); - // router - const router = useRouter(); - const { workspaceSlug } = router.query; - const workspaceStore = useWorkspace(); - const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; - - // store - const { - config: { envConfig }, - } = useApplication(); - const { getProjectById } = useProject(); - // form info - const { - formState: { errors, isSubmitting }, - handleSubmit, - reset, - watch, - control, - getValues, - setValue, - setFocus, - } = useForm({ - defaultValues: prePopulatedData ?? defaultValues, - reValidateMode: "onChange", - }); - - const issueName = watch("name"); - - const payload: Partial = { - name: watch("name"), - description_html: watch("description_html"), - state_id: watch("state_id"), - priority: watch("priority"), - assignee_ids: watch("assignee_ids"), - label_ids: watch("label_ids"), - start_date: watch("start_date"), - target_date: watch("target_date"), - project_id: watch("project_id"), - parent_id: watch("parent_id"), - cycle_id: watch("cycle_id"), - module_ids: watch("module_ids"), - }; - - useEffect(() => { - if (!isOpen || data) return; - - setLocalStorageValue( - JSON.stringify({ - ...payload, - }) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(payload), isOpen, data]); - - // const onClose = () => { - // handleClose(); - // }; - - // const onClose = () => { - // handleClose(); - // }; - - const handleCreateUpdateIssue = async ( - formData: Partial, - action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" - ) => { - await handleFormSubmit( - { - ...(data ?? {}), - ...formData, - // is_draft: action === "createDraft" || action === "updateDraft", - }, - action - ); - // TODO: check_with_backend - - setGptAssistantModal(false); - - reset({ - ...defaultValues, - project_id: projectId, - description_html: "

    ", - }); - editorRef?.current?.clearEditor(); - }; - - const handleAiAssistance = async (response: string) => { - if (!workspaceSlug || !projectId) return; - - // setValue("description", {}); - setValue("description_html", `${watch("description_html")}

    ${response}

    `); - editorRef.current?.setEditorValue(`${watch("description_html")}`); - }; - - const handleAutoGenerateDescription = async () => { - if (!workspaceSlug || !projectId) return; - - setIAmFeelingLucky(true); - - aiService - .createGptTask(workspaceSlug as string, projectId as string, { - prompt: issueName, - task: "Generate a proper description for this issue.", - }) - .then((res) => { - if (res.response === "") - setToastAlert({ - type: "error", - title: "Error!", - message: - "Issue title isn't informative enough to generate the description. Please try with a different title.", - }); - else handleAiAssistance(res.response_html); - }) - .catch((err) => { - const error = err?.data?.error; - - if (err.status === 429) - setToastAlert({ - type: "error", - title: "Error!", - message: error || "You have reached the maximum number of requests of 50 requests per month per user.", - }); - else - setToastAlert({ - type: "error", - title: "Error!", - message: error || "Some error occurred. Please try again.", - }); - }) - .finally(() => setIAmFeelingLucky(false)); - }; - - useEffect(() => { - setFocus("name"); - }, [setFocus]); - - // update projectId in form when projectId changes - useEffect(() => { - reset({ - ...getValues(), - project_id: projectId, - }); - }, [getValues, projectId, reset]); - - const startDate = watch("start_date"); - const targetDate = watch("target_date"); - - const minDate = startDate ? new Date(startDate) : null; - minDate?.setDate(minDate.getDate()); - - const maxDate = targetDate ? new Date(targetDate) : null; - maxDate?.setDate(maxDate.getDate()); - - const projectDetails = getProjectById(projectId); - - return ( - <> - {projectId && ( - <> - setStateModal(false)} projectId={projectId} /> - setLabelModal(false)} - projectId={projectId} - onSuccess={(response) => setValue("label_ids", [...watch("label_ids"), response.id])} - /> - - )} - - handleCreateUpdateIssue(formData, data ? "convertToNewIssue" : "createDraft") - )} - > -
    -
    - {(fieldsToShow.includes("all") || fieldsToShow.includes("project")) && ( - ( -
    - { - onChange(val); - setActiveProject(val); - }} - buttonVariant="border-with-text" - /> -
    - )} - /> - )} -

    - {status ? "Update" : "Create"} issue -

    -
    - {watch("parent_id") && - (fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && - selectedParentIssue && ( -
    -
    - - - {selectedParentIssue.project__identifier}-{selectedParentIssue.sequence_id} - - {selectedParentIssue.name.substring(0, 50)} - { - setValue("parent_id", null); - setSelectedParentIssue(null); - }} - /> -
    -
    - )} -
    -
    - {(fieldsToShow.includes("all") || fieldsToShow.includes("name")) && ( -
    - ( - - )} - /> -
    - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("description")) && ( -
    -
    - {issueName && issueName !== "" && ( - - )} - {envConfig?.has_openai_configured && ( - { - setGptAssistantModal((prevData) => !prevData); - // this is done so that the title do not reset after gpt popover closed - reset(getValues()); - }} - onResponse={(response) => { - handleAiAssistance(response); - }} - button={ - - } - className=" !min-w-[38rem]" - placement="top-end" - /> - )} -
    - ( - { - onChange(description_html); - }} - mentionHighlights={mentionHighlights} - mentionSuggestions={mentionSuggestions} - /> - )} - /> -
    - )} -
    - {(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && ( - ( -
    - -
    - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && ( - ( -
    - -
    - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && ( - ( -
    - 0 ? "transparent-without-text" : "border-with-text"} - buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""} - placeholder="Assignees" - multiple - /> -
    - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && ( - ( -
    - -
    - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && ( - ( -
    - onChange(date ? renderFormattedPayloadDate(date) : null)} - buttonVariant="border-with-text" - placeholder="Start date" - maxDate={maxDate ?? undefined} - /> -
    - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && ( - ( -
    - onChange(date ? renderFormattedPayloadDate(date) : null)} - buttonVariant="border-with-text" - placeholder="Due date" - minDate={minDate ?? undefined} - /> -
    - )} - /> - )} - {projectDetails?.cycle_view && ( - ( -
    - onChange(cycleId)} - value={value} - buttonVariant="border-with-text" - /> -
    - )} - /> - )} - - {projectDetails?.module_view && workspaceSlug && ( - ( -
    - -
    - )} - /> - )} - - {(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) && - areEstimatesEnabledForProject(projectId) && ( - ( -
    - -
    - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( - ( - setParentIssueListModalOpen(false)} - onChange={(issue) => { - onChange(issue.id); - setSelectedParentIssue(issue); - }} - projectId={projectId} - /> - )} - /> - )} - {(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && ( - - {watch("parent_id") ? ( - <> - setParentIssueListModalOpen(true)}> - Change parent issue - - setValue("parent_id", null)}> - Remove parent issue - - - ) : ( - setParentIssueListModalOpen(true)}> - Select Parent Issue - - )} - - )} -
    -
    -
    -
    -
    -
    setCreateMore((prevData) => !prevData)} - > - Create more - {}} size="md" /> -
    -
    - - - -
    -
    - - - ); -}); diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx deleted file mode 100644 index 40a79798e06..00000000000 --- a/web/components/issues/draft-issue-modal.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import { mutate } from "swr"; -import { Dialog, Transition } from "@headlessui/react"; -// services -import { IssueService } from "services/issue"; -import { ModuleService } from "services/module.service"; -// hooks -import useToast from "hooks/use-toast"; -import useLocalStorage from "hooks/use-local-storage"; -import { useIssues, useProject, useUser } from "hooks/store"; -// components -import { DraftIssueForm } from "components/issues"; -// types -import type { TIssue } from "@plane/types"; -import { EIssuesStoreType } from "constants/issue"; -// fetch-keys -import { PROJECT_ISSUES_DETAILS, USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys"; - -interface IssuesModalProps { - data?: TIssue | null; - handleClose: () => void; - isOpen: boolean; - isUpdatingSingleIssue?: boolean; - prePopulateData?: Partial; - fieldsToShow?: ( - | "project" - | "name" - | "description" - | "state" - | "priority" - | "assignee" - | "label" - | "startDate" - | "dueDate" - | "estimate" - | "parent" - | "all" - )[]; - onSubmit?: (data: Partial) => Promise | void; -} - -// services -const issueService = new IssueService(); -const moduleService = new ModuleService(); - -export const CreateUpdateDraftIssueModal: React.FC = observer((props) => { - const { - data, - handleClose, - isOpen, - isUpdatingSingleIssue = false, - prePopulateData: prePopulateDataProps, - fieldsToShow = ["all"], - onSubmit, - } = props; - - // states - const [createMore, setCreateMore] = useState(false); - const [activeProject, setActiveProject] = useState(null); - const [prePopulateData, setPreloadedData] = useState | undefined>(undefined); - // router - const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; - // store - const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); - const { currentUser } = useUser(); - const { workspaceProjectIds: workspaceProjects } = useProject(); - // derived values - const projects = workspaceProjects; - - const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {}); - - const { setToastAlert } = useToast(); - - const onClose = () => { - handleClose(); - setActiveProject(null); - }; - - const onDiscard = () => { - clearDraftIssueLocalStorage(); - onClose(); - }; - - useEffect(() => { - setPreloadedData(prePopulateDataProps ?? {}); - - if (cycleId && !prePopulateDataProps?.cycle_id) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - cycle: cycleId.toString(), - })); - } - if (moduleId && !prePopulateDataProps?.module_ids) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - module: moduleId.toString(), - })); - } - if ( - (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && - !prePopulateDataProps?.assignee_ids - ) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - assignees: prePopulateDataProps?.assignee_ids ?? [currentUser?.id ?? ""], - })); - } - }, [prePopulateDataProps, cycleId, moduleId, router.asPath, currentUser?.id]); - - useEffect(() => { - setPreloadedData(prePopulateDataProps ?? {}); - - if (cycleId && !prePopulateDataProps?.cycle_id) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - cycle: cycleId.toString(), - })); - } - if (moduleId && !prePopulateDataProps?.module_ids) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - module: moduleId.toString(), - })); - } - if ( - (router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && - !prePopulateDataProps?.assignee_ids - ) { - setPreloadedData((prevData) => ({ - ...(prevData ?? {}), - ...prePopulateDataProps, - assignees: prePopulateDataProps?.assignee_ids ?? [currentUser?.id ?? ""], - })); - } - }, [prePopulateDataProps, cycleId, moduleId, router.asPath, currentUser?.id]); - - useEffect(() => { - // if modal is closed, reset active project to null - // and return to avoid activeProject being set to some other project - if (!isOpen) { - setActiveProject(null); - return; - } - - // if data is present, set active project to the project of the - // issue. This has more priority than the project in the url. - if (data && data.project_id) return setActiveProject(data.project_id); - - if (prePopulateData && prePopulateData.project_id && !activeProject) - return setActiveProject(prePopulateData.project_id); - - if (prePopulateData && prePopulateData.project_id && !activeProject) - return setActiveProject(prePopulateData.project_id); - - // if data is not present, set active project to the project - // in the url. This has the least priority. - if (projects && projects.length > 0 && !activeProject) - setActiveProject(projects?.find((id) => id === projectId) ?? projects?.[0] ?? null); - }, [activeProject, data, projectId, projects, isOpen, prePopulateData]); - - const createDraftIssue = async (payload: Partial) => { - if (!workspaceSlug || !activeProject || !currentUser) return; - - await draftIssues - .createIssue(workspaceSlug as string, activeProject ?? "", payload) - .then(async () => { - await draftIssues.fetchIssues(workspaceSlug as string, activeProject ?? "", "mutation"); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - - if (payload.assignee_ids?.some((assignee) => assignee === currentUser?.id)) - mutate(USER_ISSUE(workspaceSlug.toString())); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be created. Please try again.", - }); - }); - - if (!createMore) onClose(); - }; - - const updateDraftIssue = async (payload: Partial) => { - await draftIssues - .updateIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload) - .then(() => { - if (isUpdatingSingleIssue) { - mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...payload } as TIssue), false); - } else { - if (payload.parent_id) mutate(SUB_ISSUES(payload.parent_id.toString())); - } - - // if (!payload.is_draft) { // TODO: check_with_backend - // if (payload.cycle_id && payload.cycle_id !== "") addIssueToCycle(res.id, payload.cycle_id); - // if (payload.module_id && payload.module_id !== "") addIssueToModule(res.id, payload.module_id); - // } - - if (!createMore) onClose(); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue updated successfully.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be updated. Please try again.", - }); - }); - }; - - const addIssueToCycle = async (issueId: string, cycleId: string) => { - if (!workspaceSlug || !activeProject) return; - - await issueService.addIssueToCycle(workspaceSlug as string, activeProject ?? "", cycleId, { - issues: [issueId], - }); - }; - - const addIssueToModule = async (issueId: string, moduleIds: string[]) => { - if (!workspaceSlug || !activeProject) return; - - await moduleService.addModulesToIssue(workspaceSlug as string, activeProject ?? "", issueId as string, { - modules: moduleIds, - }); - }; - - const createIssue = async (payload: Partial) => { - if (!workspaceSlug || !activeProject) return; - - await issueService - .createIssue(workspaceSlug.toString(), activeProject, payload) - .then(async (res) => { - if (payload.cycle_id && payload.cycle_id !== "") await addIssueToCycle(res.id, payload.cycle_id); - if (payload.module_ids && payload.module_ids.length > 0) await addIssueToModule(res.id, payload.module_ids); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - - if (!createMore) onClose(); - - if (payload.assignee_ids?.some((assignee) => assignee === currentUser?.id)) - mutate(USER_ISSUE(workspaceSlug as string)); - - if (payload.parent_id && payload.parent_id !== "") mutate(SUB_ISSUES(payload.parent_id)); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Issue could not be created. Please try again.", - }); - }); - }; - - const handleFormSubmit = async ( - formData: Partial, - action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" - ) => { - if (!workspaceSlug || !activeProject) return; - - const payload: Partial = { - ...formData, - // description: formData.description ?? "", - description_html: formData.description_html ?? "

    ", - }; - - if (action === "createDraft") await createDraftIssue(payload); - else if (action === "updateDraft" || action === "convertToNewIssue") await updateDraftIssue(payload); - else if (action === "createNewIssue") await createIssue(payload); - - clearDraftIssueLocalStorage(); - - if (onSubmit) await onSubmit(payload); - }; - - if (!projects || projects.length === 0) return null; - - return ( - <> - - - -
    - - -
    -
    - - - - - -
    -
    -
    -
    - - ); -}); diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 3904049e9c4..cab935a5ddd 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -14,10 +14,5 @@ export * from "./issue-detail"; export * from "./peek-overview"; -// draft issue -export * from "./draft-issue-form"; -export * from "./draft-issue-modal"; -export * from "./delete-draft-issue-modal"; - // archived issue export * from "./delete-archived-issue-modal"; 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 ec9742bafd5..440b379b860 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,7 +3,7 @@ import { useRouter } from "next/router"; // components import { CustomMenu } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; -import { CreateUpdateIssueModal, CreateUpdateDraftIssueModal } from "components/issues"; +import { CreateUpdateIssueModal } from "components/issues"; // lucide icons import { Minimize2, Maximize2, Circle, Plus } from "lucide-react"; // hooks 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 5a6b3c462d0..8d9164b3767 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 @@ -2,7 +2,7 @@ import { useRouter } from "next/router"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; // components -import { CreateUpdateIssueModal, CreateUpdateDraftIssueModal } from "components/issues"; +import { CreateUpdateIssueModal } from "components/issues"; import { ExistingIssuesListModal } from "components/core"; import { CustomMenu } from "@plane/ui"; // mobx diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx index 83b46baf6ef..88fbf105461 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/cycle-column.tsx @@ -30,7 +30,6 @@ export const SpreadsheetCycleColumn: React.FC = observer((props) => { const handleCycle = useCallback( async (cycleId: string | null) => { - console.log("cycleId", cycleId); if (!workspaceSlug || !issue || issue.cycle_id === cycleId) return; if (cycleId) await addIssueToCycle(workspaceSlug.toString(), issue.project_id, cycleId, [issue.id]); else await removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, issue.cycle_id ?? "", issue.id); diff --git a/web/components/issues/issue-modal/modal.tsx b/web/components/issues/issue-modal/modal.tsx index 4c2833efa26..691f7da1175 100644 --- a/web/components/issues/issue-modal/modal.tsx +++ b/web/components/issues/issue-modal/modal.tsx @@ -93,7 +93,9 @@ export const CreateUpdateIssueModal: React.FC = observer((prop // toast alert const { setToastAlert } = useToast(); // local storage - const { setValue: setLocalStorageDraftIssue } = useLocalStorage("draftedIssue", {}); + const { storedValue: localStorageDraftIssues, setValue: setLocalStorageDraftIssue } = useLocalStorage< + Record> + >("draftedIssue", {}); // current store details const { store: currentIssueStore, viewId } = issueStores[storeType]; @@ -154,9 +156,14 @@ export const CreateUpdateIssueModal: React.FC = observer((prop const handleClose = (saveDraftIssueInLocalStorage?: boolean) => { if (changesMade && saveDraftIssueInLocalStorage) { - const draftIssue = JSON.stringify(changesMade); - setLocalStorageDraftIssue(draftIssue); + // updating the current edited issue data in the local storage + let draftIssues = localStorageDraftIssues ? localStorageDraftIssues : {}; + if (workspaceSlug) { + draftIssues = { ...draftIssues, [workspaceSlug]: changesMade }; + setLocalStorageDraftIssue(draftIssues); + } } + setActiveProjectId(null); onClose(); }; diff --git a/web/components/workspace/sidebar-quick-action.tsx b/web/components/workspace/sidebar-quick-action.tsx index 28b57f0f5ed..38d3e6b6aa4 100644 --- a/web/components/workspace/sidebar-quick-action.tsx +++ b/web/components/workspace/sidebar-quick-action.tsx @@ -5,23 +5,29 @@ import { ChevronUp, PenSquare, Search } from "lucide-react"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // components -import { CreateUpdateDraftIssueModal } from "components/issues"; +import { CreateUpdateIssueModal } from "components/issues"; // constants import { EUserWorkspaceRoles } from "constants/workspace"; import { EIssuesStoreType } from "constants/issue"; +// types +import { TIssue } from "@plane/types"; export const WorkspaceSidebarQuickAction = observer(() => { // states const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); - const { theme: themeStore, commandPalette: commandPaletteStore } = useApplication(); + const { + router: { workspaceSlug }, + theme: themeStore, + commandPalette: commandPaletteStore, + } = useApplication(); const { setTrackElement } = useEventTracker(); const { joinedProjectIds } = useProject(); const { membership: { currentWorkspaceRole }, } = useUser(); - const { storedValue, clearValue } = useLocalStorage("draftedIssue", JSON.stringify({})); + const { storedValue, setValue } = useLocalStorage>>("draftedIssue", {}); //useState control for displaying draft issue button instead of group hover const [isDraftButtonOpen, setIsDraftButtonOpen] = useState(false); @@ -45,18 +51,25 @@ export const WorkspaceSidebarQuickAction = observer(() => { setIsDraftButtonOpen(false); }, 300); }; + + const workspaceDraftIssue = workspaceSlug ? storedValue?.[workspaceSlug] ?? undefined : undefined; + + const removeWorkspaceDraftIssue = () => { + const draftIssues = storedValue ?? {}; + if (workspaceSlug && draftIssues[workspaceSlug]) delete draftIssues[workspaceSlug]; + setValue(draftIssues); + }; + return ( <> - setIsDraftIssueModalOpen(false)} - prePopulateData={storedValue ? JSON.parse(storedValue) : {}} - onSubmit={() => { - localStorage.removeItem("draftedIssue"); - clearValue(); - }} - fieldsToShow={["all"]} + onClose={() => setIsDraftIssueModalOpen(false)} + data={workspaceDraftIssue ?? {}} + // storeType={storeType} + isDraft={true} /> +
    { {!isSidebarCollapsed && New Issue} - {!disabled && storedValue && Object.keys(JSON.parse(storedValue)).length > 0 && ( + {!disabled && workspaceDraftIssue && ( <>
    Date: Wed, 28 Feb 2024 15:35:04 +0530 Subject: [PATCH 14/16] fix: default assignee for feature and bug report --- .github/ISSUE_TEMPLATE/--bug-report.yaml | 4 ++-- .github/ISSUE_TEMPLATE/--feature-request.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/--bug-report.yaml b/.github/ISSUE_TEMPLATE/--bug-report.yaml index 5d19be11cc8..3adaa42309c 100644 --- a/.github/ISSUE_TEMPLATE/--bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/--bug-report.yaml @@ -2,7 +2,7 @@ name: Bug report description: Create a bug report to help us improve Plane title: "[bug]: " labels: [🐛bug] -assignees: [srinivaspendem, pushya-plane] +assignees: [srinivaspendem, pushya22] body: - type: markdown attributes: @@ -45,7 +45,7 @@ body: - Deploy preview validations: required: true - type: dropdown +- type: dropdown id: browser attributes: label: Browser diff --git a/.github/ISSUE_TEMPLATE/--feature-request.yaml b/.github/ISSUE_TEMPLATE/--feature-request.yaml index 941fbef87c5..ff9cdd23839 100644 --- a/.github/ISSUE_TEMPLATE/--feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/--feature-request.yaml @@ -2,7 +2,7 @@ name: Feature request description: Suggest a feature to improve Plane title: "[feature]: " labels: [✨feature] -assignees: [srinivaspendem, pushya-plane] +assignees: [srinivaspendem, pushya22] body: - type: markdown attributes: From 6dd785b5dd6e8785406cee543c16f51a4c7c9d1f Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Wed, 28 Feb 2024 15:50:28 +0530 Subject: [PATCH 15/16] chore: added parameters `TARGET_REPO_A` & `TARGET_REPO_B` for sync branch workflow (#3816) --- .github/workflows/create-sync-pr.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/create-sync-pr.yml b/.github/workflows/create-sync-pr.yml index 47a85f3ba8c..5e64434bf6c 100644 --- a/.github/workflows/create-sync-pr.yml +++ b/.github/workflows/create-sync-pr.yml @@ -5,7 +5,7 @@ on: push: branches: - preview - + env: SOURCE_BRANCH_NAME: ${{ github.ref_name }} @@ -21,7 +21,7 @@ jobs: with: persist-credentials: false fetch-depth: 0 - + - name: Setup GH CLI run: | type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) @@ -31,14 +31,26 @@ jobs: sudo apt update sudo apt install gh -y - - name: Push Changes to Target Repo + - name: Push Changes to Target Repo A env: GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} run: | - TARGET_REPO="${{ secrets.SYNC_TARGET_REPO_NAME }}" + TARGET_REPO="${{ secrets.TARGET_REPO_A }}" TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}" SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" git checkout $SOURCE_BRANCH - git remote add target-origin "https://$GH_TOKEN@github.com/$TARGET_REPO.git" - git push target-origin $SOURCE_BRANCH:$TARGET_BRANCH + git remote add target-origin-a "https://$GH_TOKEN@github.com/$TARGET_REPO.git" + git push target-origin-a $SOURCE_BRANCH:$TARGET_BRANCH + + - name: Push Changes to Target Repo B + env: + GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} + run: | + TARGET_REPO="${{ secrets.TARGET_REPO_B }}" + TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}" + SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" + + git remote add target-origin-b "https://$GH_TOKEN@github.com/$TARGET_REPO.git" + git push target-origin-b $SOURCE_BRANCH:$TARGET_BRANCH + From 16d7b3c7d1cf644a76334b83bd2f8d17976afdfd Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Wed, 28 Feb 2024 15:56:39 +0530 Subject: [PATCH 16/16] fix: sync action variable naming --- .github/workflows/create-sync-pr.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/create-sync-pr.yml b/.github/workflows/create-sync-pr.yml index 5e64434bf6c..8644f04f02e 100644 --- a/.github/workflows/create-sync-pr.yml +++ b/.github/workflows/create-sync-pr.yml @@ -2,10 +2,10 @@ name: Create Sync Action on: workflow_dispatch: - push: + push: branches: - preview - + env: SOURCE_BRANCH_NAME: ${{ github.ref_name }} @@ -17,11 +17,11 @@ jobs: contents: read steps: - name: Checkout Code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.1 with: persist-credentials: false fetch-depth: 0 - + - name: Setup GH CLI run: | type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y) @@ -36,7 +36,7 @@ jobs: GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} run: | TARGET_REPO="${{ secrets.TARGET_REPO_A }}" - TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}" + TARGET_BRANCH="${{ secrets.TARGET_REPO_A_BRANCH_NAME }}" SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" git checkout $SOURCE_BRANCH @@ -48,9 +48,8 @@ jobs: GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} run: | TARGET_REPO="${{ secrets.TARGET_REPO_B }}" - TARGET_BRANCH="${{ secrets.SYNC_TARGET_BRANCH_NAME }}" + TARGET_BRANCH="${{ secrets.TARGET_REPO_B_BRANCH_NAME }}" SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}" git remote add target-origin-b "https://$GH_TOKEN@github.com/$TARGET_REPO.git" git push target-origin-b $SOURCE_BRANCH:$TARGET_BRANCH -