diff --git a/packages/types/src/issues/issue_reaction.d.ts b/packages/types/src/issues/issue_reaction.d.ts index a4eaee0a8..7fba8cd9c 100644 --- a/packages/types/src/issues/issue_reaction.d.ts +++ b/packages/types/src/issues/issue_reaction.d.ts @@ -1,7 +1,7 @@ export type TIssueReaction = { - actor_id: string; + actor: string; id: string; - issue_id: string; + issue: string; reaction: string; }; diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index d1623dddf..b94faf436 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -114,7 +114,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { type="button" onClick={(e) => { e.stopPropagation(); - openDropdown(); + isOpen ? closeDropdown() : openDropdown(); if (menuButtonOnClick) menuButtonOnClick(); }} className={customButtonClassName} @@ -132,7 +132,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { type="button" onClick={(e) => { e.stopPropagation(); - openDropdown(); + isOpen ? closeDropdown() : openDropdown(); if (menuButtonOnClick) menuButtonOnClick(); }} disabled={disabled} @@ -158,7 +158,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { } ${buttonClassName}`} onClick={(e) => { e.stopPropagation(); - openDropdown(); + isOpen ? closeDropdown() : openDropdown(); if (menuButtonOnClick) menuButtonOnClick(); }} tabIndex={customButtonTabIndex} diff --git a/web/components/command-palette/command-modal.tsx b/web/components/command-palette/command-modal.tsx index cffd3ff11..60c4fcc04 100644 --- a/web/components/command-palette/command-modal.tsx +++ b/web/components/command-palette/command-modal.tsx @@ -4,9 +4,19 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Dialog, Transition } from "@headlessui/react"; +// icons import { FolderPlus, Search, Settings } from "lucide-react"; // hooks +import { useApplication, useEventTracker, useProject } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; +import useDebounce from "hooks/use-debounce"; +// services +import { IssueService } from "services/issue"; +import { WorkspaceService } from "services/workspace.service"; +// ui import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; +// components +import { EmptyState } from "components/empty-state"; import { CommandPaletteThemeActions, ChangeIssueAssignee, @@ -18,18 +28,13 @@ import { CommandPaletteWorkspaceSettingsActions, CommandPaletteSearchResults, } from "components/command-palette"; -import { ISSUE_DETAILS } from "constants/fetch-keys"; -import { useApplication, useEventTracker, useProject } from "hooks/store"; -import { usePlatformOS } from "hooks/use-platform-os"; -// services -import useDebounce from "hooks/use-debounce"; -import { IssueService } from "services/issue"; -import { WorkspaceService } from "services/workspace.service"; // types import { IWorkspaceSearchResults } from "@plane/types"; // fetch-keys +// constants +import { EmptyStateType } from "constants/empty-state"; +import { ISSUE_DETAILS } from "constants/fetch-keys"; -// services const workspaceService = new WorkspaceService(); const issueService = new IssueService(); @@ -244,7 +249,9 @@ export const CommandModal: React.FC = observer(() => { )} {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
No results found.
+
+ +
)} {(isLoading || isSearching) && ( diff --git a/web/components/command-palette/command-palette.tsx b/web/components/command-palette/command-palette.tsx index ab2743afd..0d02614ae 100644 --- a/web/components/command-palette/command-palette.tsx +++ b/web/components/command-palette/command-palette.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, FC } from "react"; +import React, { useCallback, useEffect, FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; @@ -23,24 +23,29 @@ import { EIssuesStoreType } from "constants/issue"; import { copyTextToClipboard } from "helpers/string.helper"; import { useApplication, useEventTracker, useIssues, useUser } from "hooks/store"; import { IssueService } from "services/issue"; +import { EUserProjectRoles } from "constants/project"; +import { EUserWorkspaceRoles } from "constants/workspace"; // services const issueService = new IssueService(); export const CommandPalette: FC = observer(() => { + // router const router = useRouter(); const { workspaceSlug, projectId, issueId, cycleId, moduleId } = router.query; - + // store hooks const { commandPalette, theme: { toggleSidebar }, } = useApplication(); const { setTrackElement } = useEventTracker(); - const { currentUser } = useUser(); + const { + currentUser, + membership: { currentWorkspaceRole, currentProjectRole }, + } = useUser(); const { issues: { removeIssue }, } = useIssues(EIssuesStoreType.PROJECT); - const { toggleCommandPaletteModal, isCreateIssueModalOpen, @@ -91,6 +96,105 @@ export const CommandPalette: FC = observer(() => { }); }, [issueId]); + // auth + const canPerformProjectCreateActions = useCallback( + (showToast: boolean = true) => { + const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + if (!isAllowed && showToast) + setToast({ + type: TOAST_TYPE.ERROR, + title: "You don't have permission to perform this action.", + }); + + return isAllowed; + }, + [currentProjectRole] + ); + const canPerformWorkspaceCreateActions = useCallback( + (showToast: boolean = true) => { + const isAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; + console.log("currentWorkspaceRole", currentWorkspaceRole); + console.log("isAllowed", isAllowed); + if (!isAllowed && showToast) + setToast({ + type: TOAST_TYPE.ERROR, + title: "You don't have permission to perform this action.", + }); + return isAllowed; + }, + [currentWorkspaceRole] + ); + + const shortcutsList: { + global: Record void }>; + workspace: Record void }>; + project: Record void }>; + } = useMemo( + () => ({ + global: { + c: { + title: "Create a new issue", + description: "Create a new issue in the current project", + action: () => toggleCreateIssueModal(true), + }, + h: { + title: "Show shortcuts", + description: "Show all the available shortcuts", + action: () => toggleShortcutModal(true), + }, + }, + workspace: { + p: { + title: "Create a new project", + description: "Create a new project in the current workspace", + action: () => toggleCreateProjectModal(true), + }, + }, + project: { + d: { + title: "Create a new page", + description: "Create a new page in the current project", + action: () => toggleCreatePageModal(true), + }, + m: { + title: "Create a new module", + description: "Create a new module in the current project", + action: () => toggleCreateModuleModal(true), + }, + q: { + title: "Create a new cycle", + description: "Create a new cycle in the current project", + action: () => toggleCreateCycleModal(true), + }, + v: { + title: "Create a new view", + description: "Create a new view in the current project", + action: () => toggleCreateViewModal(true), + }, + backspace: { + title: "Bulk delete issues", + description: "Bulk delete issues in the current project", + action: () => toggleBulkDeleteIssueModal(true), + }, + delete: { + title: "Bulk delete issues", + description: "Bulk delete issues in the current project", + action: () => toggleBulkDeleteIssueModal(true), + }, + }, + }), + [ + toggleBulkDeleteIssueModal, + toggleCreateCycleModal, + toggleCreateIssueModal, + toggleCreateModuleModal, + toggleCreatePageModal, + toggleCreateProjectModal, + toggleCreateViewModal, + toggleShortcutModal, + ] + ); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { const { key, ctrlKey, metaKey, altKey } = e; @@ -102,7 +206,7 @@ export const CommandPalette: FC = observer(() => { if ( e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement || - (e.target as Element).classList?.contains("ProseMirror") + (e.target as Element)?.classList?.contains("ProseMirror") ) return; @@ -119,42 +223,37 @@ export const CommandPalette: FC = observer(() => { } } else if (!isAnyModalOpen) { setTrackElement("Shortcut key"); - if (keyPressed === "c") { - toggleCreateIssueModal(true); - } else if (keyPressed === "p") { - toggleCreateProjectModal(true); - } else if (keyPressed === "h") { - toggleShortcutModal(true); - } else if (keyPressed === "v" && workspaceSlug && projectId) { - toggleCreateViewModal(true); - } else if (keyPressed === "d" && workspaceSlug && projectId) { - toggleCreatePageModal(true); - } else if (keyPressed === "q" && workspaceSlug && projectId) { - toggleCreateCycleModal(true); - } else if (keyPressed === "m" && workspaceSlug && projectId) { - toggleCreateModuleModal(true); - } else if (keyPressed === "backspace" || keyPressed === "delete") { + if (Object.keys(shortcutsList.global).includes(keyPressed)) shortcutsList.global[keyPressed].action(); + // workspace authorized actions + else if ( + Object.keys(shortcutsList.workspace).includes(keyPressed) && + workspaceSlug && + canPerformWorkspaceCreateActions() + ) + shortcutsList.workspace[keyPressed].action(); + // project authorized actions + else if ( + Object.keys(shortcutsList.project).includes(keyPressed) && + projectId && + canPerformProjectCreateActions() + ) { e.preventDefault(); - toggleBulkDeleteIssueModal(true); + // actions that can be performed only inside a project + shortcutsList.project[keyPressed].action(); } } }, [ + canPerformProjectCreateActions, + canPerformWorkspaceCreateActions, copyIssueUrlToClipboard, - toggleCreateProjectModal, - toggleCreateViewModal, - toggleCreatePageModal, - toggleShortcutModal, - toggleCreateCycleModal, - toggleCreateModuleModal, - toggleBulkDeleteIssueModal, + isAnyModalOpen, + projectId, + setTrackElement, + shortcutsList, toggleCommandPaletteModal, toggleSidebar, - toggleCreateIssueModal, - projectId, workspaceSlug, - isAnyModalOpen, - setTrackElement, ] ); @@ -169,18 +268,11 @@ export const CommandPalette: FC = observer(() => { return ( <> - { - toggleShortcutModal(false); - }} - /> + toggleShortcutModal(false)} /> {workspaceSlug && ( { - toggleCreateProjectModal(false); - }} + onClose={() => toggleCreateProjectModal(false)} workspaceSlug={workspaceSlug.toString()} /> )} @@ -194,9 +286,7 @@ export const CommandPalette: FC = observer(() => { /> { - toggleCreateModuleModal(false); - }} + onClose={() => toggleCreateModuleModal(false)} workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} /> @@ -236,9 +326,7 @@ export const CommandPalette: FC = observer(() => { { - toggleBulkDeleteIssueModal(false); - }} + onClose={() => toggleBulkDeleteIssueModal(false)} user={currentUser} /> diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index 94d665fa7..05b98176c 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -5,22 +5,22 @@ import { SubmitHandler, useForm } from "react-hook-form"; import useSWR from "swr"; import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import { Search } from "lucide-react"; -import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; - -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; -import { EIssuesStoreType } from "constants/issue"; -import { useIssues, useProject } from "hooks/store"; import { IssueService } from "services/issue"; // ui +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // icons +import { Search } from "lucide-react"; // types import { IUser, TIssue } from "@plane/types"; -// fetch keys // store hooks +import { useIssues, useProject } from "hooks/store"; // components import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item"; +import { EmptyState } from "components/empty-state"; // constants +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import { EIssuesStoreType } from "constants/issue"; +import { EmptyStateType } from "constants/empty-state"; type FormInput = { delete_issue_ids: string[]; @@ -178,12 +178,15 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { ) : ( -
- -

- No issues found. Create a new issue with{" "} -
C
. -

+
+
)} diff --git a/web/components/core/modals/existing-issues-list-modal.tsx b/web/components/core/modals/existing-issues-list-modal.tsx index 79f134b31..b3f81b6ee 100644 --- a/web/components/core/modals/existing-issues-list-modal.tsx +++ b/web/components/core/modals/existing-issues-list-modal.tsx @@ -2,12 +2,14 @@ import React, { useEffect, useState } from "react"; import { Combobox, Dialog, Transition } from "@headlessui/react"; import { Rocket, Search, X } from "lucide-react"; // services -import { Button, LayersIcon, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; - +import { ProjectService } from "services/project"; +// hooks import useDebounce from "hooks/use-debounce"; import { usePlatformOS } from "hooks/use-platform-os"; -import { ProjectService } from "services/project"; +// components +import { IssueSearchModalEmptyState } from "./issue-search-modal-empty-state"; // ui +import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // types import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types"; @@ -40,7 +42,7 @@ export const ExistingIssuesListModal: React.FC = (props) => { const [isSearching, setIsSearching] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); - const { isMobile } = usePlatformOS(); + const { isMobile } = usePlatformOS(); const debouncedSearchTerm: string = useDebounce(searchTerm, 500); const handleClose = () => { @@ -192,15 +194,12 @@ export const ExistingIssuesListModal: React.FC = (props) => { )} - {!isSearching && issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
- -

- No issues found. Create a new issue with{" "} -
C
. -

-
- )} + {isSearching ? ( diff --git a/web/components/core/modals/index.ts b/web/components/core/modals/index.ts index cf72365f5..a95c22114 100644 --- a/web/components/core/modals/index.ts +++ b/web/components/core/modals/index.ts @@ -4,3 +4,4 @@ export * from "./gpt-assistant-popover"; export * from "./link-modal"; export * from "./user-image-upload-modal"; export * from "./workspace-image-upload-modal"; +export * from "./issue-search-modal-empty-state"; diff --git a/web/components/core/modals/issue-search-modal-empty-state.tsx b/web/components/core/modals/issue-search-modal-empty-state.tsx new file mode 100644 index 000000000..00dcc03bb --- /dev/null +++ b/web/components/core/modals/issue-search-modal-empty-state.tsx @@ -0,0 +1,36 @@ +import React from "react"; +// components +import { EmptyState } from "components/empty-state"; +// types +import { ISearchIssueResponse } from "@plane/types"; +// constants +import { EmptyStateType } from "constants/empty-state"; + +interface EmptyStateProps { + issues: ISearchIssueResponse[]; + searchTerm: string; + debouncedSearchTerm: string; + isSearching: boolean; +} + +export const IssueSearchModalEmptyState: React.FC = ({ + issues, + searchTerm, + debouncedSearchTerm, + isSearching, +}) => { + const renderEmptyState = (type: EmptyStateType) => ( +
+ +
+ ); + + const emptyState = + issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching + ? renderEmptyState(EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE) + : issues.length === 0 + ? renderEmptyState(EmptyStateType.ISSUE_RELATION_EMPTY_STATE) + : null; + + return emptyState; +}; diff --git a/web/components/cycles/board/root.tsx b/web/components/cycles/board/root.tsx index 26154becf..e9fde3428 100644 --- a/web/components/cycles/board/root.tsx +++ b/web/components/cycles/board/root.tsx @@ -22,12 +22,14 @@ export const CyclesBoard: FC = observer((props) => {
- + {cycleIds.length > 0 && ( + + )} {completedCycleIds.length !== 0 && ( diff --git a/web/components/cycles/cycle-peek-overview.tsx b/web/components/cycles/cycle-peek-overview.tsx index b7e778c10..b15e89ab2 100644 --- a/web/components/cycles/cycle-peek-overview.tsx +++ b/web/components/cycles/cycle-peek-overview.tsx @@ -38,7 +38,7 @@ export const CyclePeekOverview: React.FC = observer(({ projectId, workspa {peekCycle && (
= observer((props) => { useOutsideClickDetector(inputRef, () => { if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false); }); + // derived values + const activeLayout = currentProjectDisplayFilters?.layout ?? "list"; const handleFilters = useCallback( (key: keyof TCycleFilters, value: string | string[]) => { @@ -140,9 +142,7 @@ export const CyclesViewHeader: React.FC = observer((props) => { diff --git a/web/components/cycles/form.tsx b/web/components/cycles/form.tsx index c711a9a41..7ac5c4fde 100644 --- a/web/components/cycles/form.tsx +++ b/web/components/cycles/form.tsx @@ -6,6 +6,7 @@ import { DateRangeDropdown, ProjectDropdown } from "components/dropdowns"; // ui // helpers import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import { ICycle } from "@plane/types"; @@ -66,6 +67,7 @@ export const CycleForm: React.FC = (props) => { setActiveProject(val); }} buttonVariant="background-with-text" + renderCondition={(project) => shouldRenderProject(project)} tabIndex={7} /> )} diff --git a/web/components/dropdowns/project.tsx b/web/components/dropdowns/project.tsx index 719b89802..ce490583a 100644 --- a/web/components/dropdowns/project.tsx +++ b/web/components/dropdowns/project.tsx @@ -15,6 +15,7 @@ import { ProjectLogo } from "components/project"; // types import { BUTTON_VARIANTS_WITH_TEXT } from "./constants"; import { TDropdownProps } from "./types"; +import { IProject } from "@plane/types"; // constants type Props = TDropdownProps & { @@ -23,6 +24,7 @@ type Props = TDropdownProps & { dropdownArrowClassName?: string; onChange: (val: string) => void; onClose?: () => void; + renderCondition?: (project: IProject) => boolean; value: string | null; }; @@ -41,6 +43,7 @@ export const ProjectDropdown: React.FC = observer((props) => { onClose, placeholder = "Project", placement, + renderCondition, showTooltip = false, tabIndex, value, @@ -71,7 +74,7 @@ export const ProjectDropdown: React.FC = observer((props) => { const options = joinedProjectIds?.map((projectId) => { const projectDetails = getProjectById(projectId); - + if (renderCondition && projectDetails && !renderCondition(projectDetails)) return; return { value: projectId, query: `${projectDetails?.name}`, @@ -89,7 +92,7 @@ export const ProjectDropdown: React.FC = observer((props) => { }); const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase())); const selectedProject = value ? getProjectById(value) : null; @@ -205,24 +208,27 @@ export const ProjectDropdown: React.FC = observer((props) => {
{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 && } - - )} - - )) + filteredOptions.map((option) => { + if (!option) return; + return ( + + `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

) diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index e718c065a..783025679 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -16,7 +16,7 @@ import { cn } from "helpers/common.helper"; export type EmptyStateProps = { type: EmptyStateType; size?: "sm" | "md" | "lg"; - layout?: "widget-simple" | "screen-detailed" | "screen-simple"; + layout?: "screen-detailed" | "screen-simple"; additionalPath?: string; primaryButtonOnClick?: () => void; primaryButtonLink?: string; @@ -149,6 +149,28 @@ export const EmptyState: React.FC = (props) => {
)} + {layout === "screen-simple" && ( +
+
+ {key +
+ {description ? ( + <> +

{title}

+

{description}

+ + ) : ( +

{title}

+ )} +
+ )} ); }; diff --git a/web/components/inbox/modals/select-duplicate.tsx b/web/components/inbox/modals/select-duplicate.tsx index 321628f53..b34dd4ee7 100644 --- a/web/components/inbox/modals/select-duplicate.tsx +++ b/web/components/inbox/modals/select-duplicate.tsx @@ -2,15 +2,19 @@ import React, { useEffect, useState } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { Combobox, Dialog, Transition } from "@headlessui/react"; +// hooks +import { useProject, useProjectState } from "hooks/store"; // icons import { Search } from "lucide-react"; +// components +import { EmptyState } from "components/empty-state"; // ui -import { Button, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; -// fetch-keys -import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; -import { useProject, useProjectState } from "hooks/store"; +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; // services import { IssueService } from "services/issue"; +// constants +import { PROJECT_ISSUES_LIST } from "constants/fetch-keys"; +import { EmptyStateType } from "constants/empty-state"; type Props = { isOpen: boolean; @@ -158,12 +162,15 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { ) : ( -
- -

- No issues found. Create a new issue with{" "} -
C
. -

+
+
)} diff --git a/web/components/issues/attachment/attachments-list.tsx b/web/components/issues/attachment/attachments-list.tsx index 0f834c1a4..aed1f7922 100644 --- a/web/components/issues/attachment/attachments-list.tsx +++ b/web/components/issues/attachment/attachments-list.tsx @@ -21,23 +21,21 @@ export const IssueAttachmentsList: FC = observer((props) const { attachment: { getAttachmentsByIssueId }, } = useIssueDetail(); - + // derived values const issueAttachments = getAttachmentsByIssueId(issueId); if (!issueAttachments) return <>; return ( <> - {issueAttachments && - issueAttachments.length > 0 && - issueAttachments.map((attachmentId) => ( - - ))} + {issueAttachments?.map((attachmentId) => ( + + ))} ); }); diff --git a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx index 096f9e778..85f0ea0e9 100644 --- a/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx +++ b/web/components/issues/attachment/delete-attachment-confirmation-modal.tsx @@ -69,7 +69,7 @@ export const IssueAttachmentDeleteModal: FC = (props) => {
- Delete Attachment + Delete attachment

@@ -94,7 +94,7 @@ export const IssueAttachmentDeleteModal: FC = (props) => { }} disabled={loader} > - {loader ? "Deleting..." : "Delete"} + {loader ? "Deleting" : "Delete"}

diff --git a/web/components/issues/attachment/root.tsx b/web/components/issues/attachment/root.tsx index 3cf6b162a..715e9f840 100644 --- a/web/components/issues/attachment/root.tsx +++ b/web/components/issues/attachment/root.tsx @@ -101,7 +101,7 @@ export const IssueAttachmentRoot: FC = (props) => { return (

Attachments

-
+
= observer((props) => { const reactionUsers = (reactionIds?.[reaction] || []) .map((reactionId) => { const reactionDetails = getReactionById(reactionId); - return reactionDetails ? getUserDetails(reactionDetails.actor_id)?.display_name : null; + return reactionDetails ? getUserDetails(reactionDetails.actor)?.display_name : null; }) .filter((displayName): displayName is string => !!displayName); diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 1a49794c6..8def7cc85 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -62,12 +62,14 @@ export const CycleEmptyState: React.FC = observer((props) => { const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {}); - const emptyStateType = isCompletedCycleSnapshotAvailable + const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status.toLowerCase() === "completed"; + + const emptyStateType = isCompletedAndEmpty ? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES : isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; - const additionalPath = isCompletedCycleSnapshotAvailable ? undefined : activeLayout ?? "list"; + const additionalPath = isCompletedAndEmpty ? undefined : activeLayout ?? "list"; const emptyStateSize = isEmptyFilters ? "lg" : "sm"; return ( @@ -86,7 +88,7 @@ export const CycleEmptyState: React.FC = observer((props) => { additionalPath={additionalPath} size={emptyStateSize} primaryButtonOnClick={ - !isCompletedCycleSnapshotAvailable && !isEmptyFilters + !isCompletedAndEmpty && !isEmptyFilters ? () => { setTrackElement("Cycle issue empty state"); toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); @@ -94,9 +96,7 @@ export const CycleEmptyState: React.FC = observer((props) => { : undefined } secondaryButtonOnClick={ - !isCompletedCycleSnapshotAvailable && isEmptyFilters - ? handleClearAllFilters - : () => setCycleIssuesListModal(true) + !isCompletedAndEmpty && isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true) } />
diff --git a/web/components/issues/issue-modal/draft-issue-layout.tsx b/web/components/issues/issue-modal/draft-issue-layout.tsx index 785ccb0bb..71bf1fae0 100644 --- a/web/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/components/issues/issue-modal/draft-issue-layout.tsx @@ -55,7 +55,10 @@ export const DraftIssueLayout: React.FC = observer((props) => { const handleCreateDraftIssue = async () => { if (!changesMade || !workspaceSlug || !projectId) return; - const payload = { ...changesMade }; + const payload = { + ...changesMade, + name: changesMade.name?.trim() === "" ? "Untitled" : changesMade.name?.trim(), + }; await issueDraftService .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index a588a49df..b18c2c82b 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -30,6 +30,7 @@ import { FileService } from "services/file.service"; // ui // helpers import { getChangedIssuefields } from "helpers/issue.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import type { TIssue, ISearchIssueResponse } from "@plane/types"; @@ -304,7 +305,7 @@ export const IssueFormRoot: FC = observer((props) => { handleFormChange(); }} buttonVariant="border-with-text" - // TODO: update tabIndex logic + renderCondition={(project) => shouldRenderProject(project)} tabIndex={getTabIndex("project_id")} />
diff --git a/web/components/issues/parent-issues-list-modal.tsx b/web/components/issues/parent-issues-list-modal.tsx index 7f5f4984b..cdf642711 100644 --- a/web/components/issues/parent-issues-list-modal.tsx +++ b/web/components/issues/parent-issues-list-modal.tsx @@ -3,14 +3,16 @@ import { useRouter } from "next/router"; // headless ui import { Combobox, Dialog, Transition } from "@headlessui/react"; // services -import { Rocket, Search } from "lucide-react"; -import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; -import useDebounce from "hooks/use-debounce"; import { ProjectService } from "services/project"; // hooks +import useDebounce from "hooks/use-debounce"; import { usePlatformOS } from "hooks/use-platform-os"; +// components +import { IssueSearchModalEmptyState } from "components/core"; // ui +import { Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // icons +import { Rocket, Search } from "lucide-react"; // types import { ISearchIssueResponse } from "@plane/types"; @@ -151,15 +153,12 @@ export const ParentIssuesListModal: React.FC = ({ )} - {!isSearching && issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && ( -
- -

- No issues found. Create a new issue with{" "} -
C
. -

-
- )} + {isSearching ? ( diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx index 7e2142eb0..0570f53f1 100644 --- a/web/components/issues/peek-overview/header.tsx +++ b/web/components/issues/peek-overview/header.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; +import Link from "next/link"; import { observer } from "mobx-react"; -import { useRouter } from "next/router"; import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react"; // ui import { @@ -74,8 +74,6 @@ export const IssuePeekOverviewHeader: FC = observer((pr handleRestoreIssue, isSubmitting, } = props; - // router - const router = useRouter(); // store hooks const { currentUser } = useUser(); const { @@ -101,10 +99,6 @@ export const IssuePeekOverviewHeader: FC = observer((pr }); }); }; - const redirectToIssueDetail = () => { - router.push({ pathname: `/${issueLink}` }); - removeRoutePeekId(); - }; // auth const isArchivingAllowed = !isArchived && !disabled; const isInArchivableGroup = @@ -122,9 +116,9 @@ export const IssuePeekOverviewHeader: FC = observer((pr - + {currentMode && (
= (props) => { + const { disabled, issueId, projectId, workspaceSlug } = props; + // store hooks + const { captureIssueEvent } = useEventTracker(); + const { + attachment: { createAttachment, removeAttachment }, + } = useIssueDetail(); + + const handleAttachmentOperations: TAttachmentOperations = useMemo( + () => ({ + create: async (data: FormData) => { + try { + const attachmentUploadPromise = createAttachment(workspaceSlug, projectId, issueId, data); + setPromiseToast(attachmentUploadPromise, { + loading: "Uploading attachment...", + success: { + title: "Attachment uploaded", + message: () => "The attachment has been successfully uploaded", + }, + error: { + title: "Attachment not uploaded", + message: () => "The attachment could not be uploaded", + }, + }); + + const res = await attachmentUploadPromise; + captureIssueEvent({ + eventName: "Issue attachment added", + payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, + updates: { + changed_property: "attachment", + change_details: res.id, + }, + }); + } catch (error) { + captureIssueEvent({ + eventName: "Issue attachment added", + payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, + }); + } + }, + remove: async (attachmentId: string) => { + try { + if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields"); + await removeAttachment(workspaceSlug, projectId, issueId, attachmentId); + setToast({ + message: "The attachment has been successfully removed", + type: TOAST_TYPE.SUCCESS, + title: "Attachment removed", + }); + captureIssueEvent({ + eventName: "Issue attachment deleted", + payload: { id: issueId, state: "SUCCESS", element: "Issue detail page" }, + updates: { + changed_property: "attachment", + change_details: "", + }, + }); + } catch (error) { + captureIssueEvent({ + eventName: "Issue attachment deleted", + payload: { id: issueId, state: "FAILED", element: "Issue detail page" }, + updates: { + changed_property: "attachment", + change_details: "", + }, + }); + setToast({ + message: "The Attachment could not be removed", + type: TOAST_TYPE.ERROR, + title: "Attachment not removed", + }); + } + }, + }), + [workspaceSlug, projectId, issueId, captureIssueEvent, createAttachment, removeAttachment] + ); + + return ( +
+
Attachments
+
+ + +
+
+ ); +}; diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index 59b1c1609..2abcec2ff 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -55,7 +55,7 @@ export const PeekOverviewIssueDetails: FC = observer( : undefined; return ( - <> +
{projectDetails?.identifier}-{issue?.sequence_id} @@ -89,6 +89,6 @@ export const PeekOverviewIssueDetails: FC = observer( currentUser={currentUser} /> )} - +
); }); diff --git a/web/components/issues/peek-overview/properties.tsx b/web/components/issues/peek-overview/properties.tsx index d6fe76310..8b7aaf0b8 100644 --- a/web/components/issues/peek-overview/properties.tsx +++ b/web/components/issues/peek-overview/properties.tsx @@ -61,7 +61,7 @@ export const PeekOverviewProperties: FC = observer((pro maxDate?.setDate(maxDate.getDate()); return ( -
+
Properties
{/* TODO: render properties using a common component */}
diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index 47890c95c..4109f3feb 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -11,13 +11,15 @@ import { PeekOverviewProperties, TIssueOperations, ArchiveIssueModal, + PeekOverviewIssueAttachments, } from "components/issues"; // hooks -import { useIssueDetail } from "hooks/store"; +import { useIssueDetail, useUser } from "hooks/store"; import useKeypress from "hooks/use-keypress"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // store hooks import { IssueActivity } from "../issue-detail/issue-activity"; +import { SubIssuesRoot } from "../sub-issues"; interface IIssueView { workspaceSlug: string; @@ -37,6 +39,7 @@ export const IssueView: FC = observer((props) => { // ref const issuePeekOverviewRef = useRef(null); // store hooks + const { currentUser } = useUser(); const { setPeekIssue, isAnyModalOpen, @@ -147,7 +150,7 @@ export const IssueView: FC = observer((props) => { issue && ( <> {["side-peek", "modal"].includes(peekMode) ? ( -
+
= observer((props) => { setIsSubmitting={(value) => setIsSubmitting(value)} /> + {currentUser && ( + + )} + + + = observer((props) => {
) : ( -
+
-
+
= observer((props) => { setIsSubmitting={(value) => setIsSubmitting(value)} /> + {currentUser && ( + + )} + + +
diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index 170bf622f..781a91a3d 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -1,6 +1,6 @@ import React from "react"; import { observer } from "mobx-react-lite"; -import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; +import { ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } from "lucide-react"; // components import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; import { useIssueDetail, useProject, useProjectState } from "hooks/store"; @@ -11,6 +11,7 @@ import { IssueProperty } from "./properties"; // ui // types import { TSubIssueOperations } from "./root"; +import { cn } from "helpers/common.helper"; // import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root"; export interface ISubIssues { @@ -90,11 +91,12 @@ export const IssueListItem: React.FC = observer((props) => { setSubIssueHelpers(parentIssueId, "issue_visibility", issueId); }} > - {subIssueHelpers.issue_visibility.includes(issue.id) ? ( - - ) : ( - - )} +
)} diff --git a/web/components/issues/sub-issues/root.tsx b/web/components/issues/sub-issues/root.tsx index ed46a40f5..c3655286e 100644 --- a/web/components/issues/sub-issues/root.tsx +++ b/web/components/issues/sub-issues/root.tsx @@ -1,9 +1,9 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -import { Plus, ChevronRight, ChevronDown, Loader } from "lucide-react"; +import { Plus, ChevronRight, Loader, Pencil } from "lucide-react"; // hooks -import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +import { CircularProgressIndicator, CustomMenu, LayersIcon, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { copyTextToClipboard } from "helpers/string.helper"; @@ -11,7 +11,7 @@ import { useEventTracker, useIssueDetail } from "hooks/store"; // components import { IUser, TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; -import { ProgressBar } from "./progressbar"; +import { cn } from "helpers/common.helper"; // ui // helpers // types @@ -53,6 +53,10 @@ export const SubIssuesRoot: FC = observer((props) => { updateSubIssue, removeSubIssue, deleteSubIssue, + isCreateIssueModalOpen, + toggleCreateIssueModal, + isSubIssuesModalOpen, + toggleSubIssuesModal, } = useIssueDetail(); const { setTrackElement, captureIssueEvent } = useEventTracker(); // state @@ -310,55 +314,81 @@ export const SubIssuesRoot: FC = observer((props) => { <> {subIssues && subIssues?.length > 0 ? ( <> -
-
-
- {subIssueHelpers.preview_loader.includes(parentIssueId) ? ( - - ) : subIssueHelpers.issue_visibility.includes(parentIssueId) ? ( - - ) : ( - - )} +
+
+ +
+ + + {subIssuesDistribution?.completed?.length ?? 0}/{subIssues.length} Done +
-
Sub-issues
-
({subIssues?.length || 0})
-
- -
-
{!disabled && ( -
-
+ + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + placement="bottom-end" + noBorder + noChevron + > + { - setTrackElement("Issue detail add sub-issue"); + setTrackElement("Issue detail nested sub-issue"); handleIssueCrudState("create", parentIssueId, null); + toggleCreateIssueModal(true); }} > - Add sub-issue -
-
+ + Create new +
+ + { - setTrackElement("Issue detail add sub-issue"); + setTrackElement("Issue detail nested sub-issue"); handleIssueCrudState("existing", parentIssueId, null); + toggleSubIssuesModal(true); }} > - Add an existing issue -
-
+
+ + Add existing +
+ + )}
@@ -379,62 +409,74 @@ export const SubIssuesRoot: FC = observer((props) => { ) : ( !disabled && (
-
No Sub-Issues yet
-
- - - Add sub-issue - - } - buttonClassName="whitespace-nowrap" - placement="bottom-end" - noBorder - noChevron +
No sub-issues yet
+ + + Add sub-issue + + } + buttonClassName="whitespace-nowrap" + placement="bottom-end" + noBorder + noChevron + > + { + setTrackElement("Issue detail nested sub-issue"); + handleIssueCrudState("create", parentIssueId, null); + toggleCreateIssueModal(true); + }} > - { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("create", parentIssueId, null); - }} - > - Create new - - { - setTrackElement("Issue detail nested sub-issue"); - handleIssueCrudState("existing", parentIssueId, null); - }} - > - Add an existing issue - - -
+
+ + Create new +
+ + { + setTrackElement("Issue detail nested sub-issue"); + handleIssueCrudState("existing", parentIssueId, null); + toggleSubIssuesModal(true); + }} + > +
+ + Add existing +
+
+
) )} {/* issue create, add from existing , update and delete modals */} - {issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && ( + {issueCrudState?.create?.toggle && issueCrudState?.create?.parentIssueId && isCreateIssueModalOpen && ( handleIssueCrudState("create", null, null)} + onClose={() => { + handleIssueCrudState("create", null, null); + toggleCreateIssueModal(false); + }} onSubmit={async (_issue: TIssue) => { await subIssueOperations.addSubIssue(workspaceSlug, projectId, parentIssueId, [_issue.id]); }} /> )} - {issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && ( + {issueCrudState?.existing?.toggle && issueCrudState?.existing?.parentIssueId && isSubIssuesModalOpen && ( handleIssueCrudState("existing", null, null)} + handleClose={() => { + handleIssueCrudState("existing", null, null); + toggleSubIssuesModal(false); + }} searchParams={{ sub_issue: true, issue_id: issueCrudState?.existing?.parentIssueId }} handleOnSubmit={(_issue) => subIssueOperations.addSubIssue( diff --git a/web/components/modules/form.tsx b/web/components/modules/form.tsx index 8ce9f3bd2..5ff1d690d 100644 --- a/web/components/modules/form.tsx +++ b/web/components/modules/form.tsx @@ -7,6 +7,7 @@ import { ModuleStatusSelect } from "components/modules"; // ui // helpers import { getDate, renderFormattedPayloadDate } from "helpers/date-time.helper"; +import { shouldRenderProject } from "helpers/project.helper"; // types import { IModule } from "@plane/types"; @@ -78,6 +79,7 @@ export const ModuleForm: React.FC = (props) => { setActiveProject(val); }} buttonVariant="border-with-text" + renderCondition={(project) => shouldRenderProject(project)} tabIndex={10} />
diff --git a/web/components/modules/module-peek-overview.tsx b/web/components/modules/module-peek-overview.tsx index 5590d0390..76d35b93e 100644 --- a/web/components/modules/module-peek-overview.tsx +++ b/web/components/modules/module-peek-overview.tsx @@ -39,7 +39,7 @@ export const ModulePeekOverview: React.FC = observer(({ projectId, worksp {peekModule && (
{ // states @@ -59,6 +60,16 @@ export const NotificationPopover = observer(() => { if (selectedNotificationForSnooze === null) setIsActive(false); }); + const currentTabEmptyState = snoozed + ? EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE + : archived + ? EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE + : selectedTab === "created" + ? EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE + : selectedTab === "watching" + ? EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE + : EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE; + return ( <> { /> <> - +
) : (
- +
) ) : ( diff --git a/web/components/project/create-project-modal.tsx b/web/components/project/create-project-modal.tsx index 93f3d065a..5e8cf1895 100644 --- a/web/components/project/create-project-modal.tsx +++ b/web/components/project/create-project-modal.tsx @@ -1,15 +1,8 @@ import { useEffect, Fragment, FC, useState } from "react"; -import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// ui -import { setToast, TOAST_TYPE } from "@plane/ui"; // components import { CreateProjectForm } from "./create-project-form"; import { ProjectFeatureUpdate } from "./project-feature-update"; -// constants -import { EUserWorkspaceRoles } from "constants/workspace"; -// hooks -import { useUser } from "hooks/store"; type Props = { isOpen: boolean; @@ -23,32 +16,11 @@ enum EProjectCreationSteps { FEATURE_SELECTION = "FEATURE_SELECTION", } -interface IIsGuestCondition { - onClose: () => void; -} - -const IsGuestCondition: FC = ({ onClose }) => { - useEffect(() => { - onClose(); - setToast({ - title: "Error", - type: TOAST_TYPE.ERROR, - message: "You don't have permission to create project.", - }); - }, [onClose]); - - return null; -}; - -export const CreateProjectModal: FC = observer((props) => { +export const CreateProjectModal: FC = (props) => { const { isOpen, onClose, setToFavorite = false, workspaceSlug } = props; // states const [currentStep, setCurrentStep] = useState(EProjectCreationSteps.CREATE_PROJECT); const [createdProjectId, setCreatedProjectId] = useState(null); - // hooks - const { - membership: { currentWorkspaceRole }, - } = useUser(); useEffect(() => { if (isOpen) { @@ -57,9 +29,6 @@ export const CreateProjectModal: FC = observer((props) => { } }, [isOpen]); - if (currentWorkspaceRole && isOpen) - if (currentWorkspaceRole < EUserWorkspaceRoles.MEMBER) return ; - const handleNextStep = (projectId: string) => { if (!projectId) return; setCreatedProjectId(projectId); @@ -111,4 +80,4 @@ export const CreateProjectModal: FC = observer((props) => { ); -}); +}; diff --git a/web/components/workspace/sidebar-menu.tsx b/web/components/workspace/sidebar-menu.tsx index 69860dea2..bc955a7c2 100644 --- a/web/components/workspace/sidebar-menu.tsx +++ b/web/components/workspace/sidebar-menu.tsx @@ -55,10 +55,11 @@ export const WorkspaceSidebarMenu = observer(() => { isMobile={isMobile} >
{ { })} /> } -

{!themeStore?.sidebarCollapsed && link.label}

+ {!themeStore?.sidebarCollapsed &&

{link.label}

} {!themeStore?.sidebarCollapsed && link.key === "active-cycles" && ( )} diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index dd6d76ef3..587f58cee 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -59,7 +59,6 @@ export enum EmptyStateType { PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues", VIEWS_EMPTY_SEARCH = "views-empty-search", PROJECTS_EMPTY_SEARCH = "projects-empty-search", - COMMANDK_EMPTY_SEARCH = "commandK-empty-search", MEMBERS_EMPTY_SEARCH = "members-empty-search", PROJECT_MODULE_ISSUES = "project-module-issues", PROJECT_MODULE = "project-module", @@ -71,6 +70,18 @@ export enum EmptyStateType { PROJECT_PAGE_SHARED = "project-page-shared", PROJECT_PAGE_ARCHIVED = "project-page-archived", PROJECT_PAGE_RECENT = "project-page-recent", + + COMMAND_K_SEARCH_EMPTY_STATE = "command-k-search-empty-state", + ISSUE_RELATION_SEARCH_EMPTY_STATE = "issue-relation-search-empty-state", + ISSUE_RELATION_EMPTY_STATE = "issue-relation-empty-state", + ISSUE_COMMENT_EMPTY_STATE = "issue-comment-empty-state", + + NOTIFICATION_MY_ISSUE_EMPTY_STATE = "notification-my-issues-empty-state", + NOTIFICATION_CREATED_EMPTY_STATE = "notification-created-empty-state", + NOTIFICATION_SUBSCRIBED_EMPTY_STATE = "notification-subscribed-empty-state", + NOTIFICATION_ARCHIVED_EMPTY_STATE = "notification-archived-empty-state", + NOTIFICATION_SNOOZED_EMPTY_STATE = "notification-snoozed-empty-state", + NOTIFICATION_UNREAD_EMPTY_STATE = "notification-unread-empty-state", } const emptyStateDetails = { @@ -384,11 +395,6 @@ const emptyStateDetails = { description: "No projects detected with the matching criteria. Create a new project instead.", path: "/empty-state/search/project", }, - [EmptyStateType.COMMANDK_EMPTY_SEARCH]: { - key: EmptyStateType.COMMANDK_EMPTY_SEARCH, - title: "No results found. ", - path: "/empty-state/search/search", - }, [EmptyStateType.MEMBERS_EMPTY_SEARCH]: { key: EmptyStateType.MEMBERS_EMPTY_SEARCH, title: "No matching members", @@ -504,6 +510,66 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, + + [EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE]: { + key: EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE, + title: "No results found", + path: "/empty-state/search/search", + }, + [EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE]: { + key: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE, + title: "No maching issues found", + path: "/empty-state/search/search", + }, + [EmptyStateType.ISSUE_RELATION_EMPTY_STATE]: { + key: EmptyStateType.ISSUE_RELATION_EMPTY_STATE, + title: "No issues found", + path: "/empty-state/search/issues", + }, + [EmptyStateType.ISSUE_COMMENT_EMPTY_STATE]: { + key: EmptyStateType.ISSUE_COMMENT_EMPTY_STATE, + title: "No comments yet", + description: "Comments can be used as a discussion and follow-up space for the issues", + path: "/empty-state/search/comments", + }, + + [EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE, + title: "No issues assigned", + description: "Updates for issues assigned to you can be \n seen here", + path: "/empty-state/search/notification", + }, + + [EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE, + title: "No updates to issues", + description: "Updates to issues created by you can be \n seen here", + path: "/empty-state/search/notification", + }, + [EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE, + title: "No updates to issues", + description: "Updates to any issue you are \n subscribed to can be seen here", + path: "/empty-state/search/notification", + }, + [EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE, + title: "No unread notifications", + description: "Congratulations, you are up-to-date \n with everything happening in the issues \n you care about", + path: "/empty-state/search/notification", + }, + [EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE, + title: "No snoozed notifications yet", + description: "Any notification you snooze for later will \n be available here to act upon", + path: "/empty-state/search/snooze", + }, + [EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE]: { + key: EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE, + title: "No archived notifications yet", + description: "Any notification you archive will be \n available here to help you focus", + path: "/empty-state/search/archive", + }, } as const; export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index ba0d52742..fbed0ba5b 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -3,6 +3,8 @@ import sortBy from "lodash/sortBy"; import { satisfiesDateFilter } from "helpers/filter.helper"; // types import { IProject, TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types"; +// constants +import { EUserProjectRoles } from "constants/project"; /** * Updates the sort order of the project. @@ -51,6 +53,14 @@ export const orderJoinedProjects = ( export const projectIdentifierSanitizer = (identifier: string): string => identifier.replace(/[^ÇŞĞIİÖÜA-Za-z0-9]/g, ""); +/** + * @description Checks if the project should be rendered or not based on the user role + * @param {IProject} project + * @returns {boolean} + */ +export const shouldRenderProject = (project: IProject): boolean => + !!project.member_role && project.member_role >= EUserProjectRoles.MEMBER; + /** * @description filters projects based on the filter * @param {IProject} project diff --git a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx index 87029724e..dbef69895 100644 --- a/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx +++ b/web/pages/[workspaceSlug]/profile/[userId]/activity.tsx @@ -54,12 +54,12 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => { currentUser?.id === userId && !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; return ( -
-
+
+

Recent activity

{canDownloadActivity && }
-
+
{activityPages} {pageCount < totalPages && resultsCount !== 0 && (
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index fa9008d2f..651d03898 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -47,7 +47,7 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; // selected display filters const cycleTab = currentProjectDisplayFilters?.active_tab; - const cycleLayout = currentProjectDisplayFilters?.layout; + const cycleLayout = currentProjectDisplayFilters?.layout ?? "list"; const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => { if (!projectId) return; @@ -120,14 +120,12 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { - {cycleTab && cycleLayout && ( - - )} + diff --git a/web/public/empty-state/search/archive-dark.webp b/web/public/empty-state/search/archive-dark.webp new file mode 100644 index 000000000..d586be888 Binary files /dev/null and b/web/public/empty-state/search/archive-dark.webp differ diff --git a/web/public/empty-state/search/archive-light.webp b/web/public/empty-state/search/archive-light.webp new file mode 100644 index 000000000..5a79179e1 Binary files /dev/null and b/web/public/empty-state/search/archive-light.webp differ diff --git a/web/public/empty-state/search/comments-dark.webp b/web/public/empty-state/search/comments-dark.webp new file mode 100644 index 000000000..d06ba7e42 Binary files /dev/null and b/web/public/empty-state/search/comments-dark.webp differ diff --git a/web/public/empty-state/search/comments-light.webp b/web/public/empty-state/search/comments-light.webp new file mode 100644 index 000000000..5a66067d6 Binary files /dev/null and b/web/public/empty-state/search/comments-light.webp differ diff --git a/web/public/empty-state/search/issue-dark.webp b/web/public/empty-state/search/issue-dark.webp new file mode 100644 index 000000000..dcd00aa94 Binary files /dev/null and b/web/public/empty-state/search/issue-dark.webp differ diff --git a/web/public/empty-state/search/issues-light.webp b/web/public/empty-state/search/issues-light.webp new file mode 100644 index 000000000..7703fdede Binary files /dev/null and b/web/public/empty-state/search/issues-light.webp differ diff --git a/web/public/empty-state/search/notification-dark.webp b/web/public/empty-state/search/notification-dark.webp new file mode 100644 index 000000000..cb299d112 Binary files /dev/null and b/web/public/empty-state/search/notification-dark.webp differ diff --git a/web/public/empty-state/search/notification-light.webp b/web/public/empty-state/search/notification-light.webp new file mode 100644 index 000000000..55c4ffac7 Binary files /dev/null and b/web/public/empty-state/search/notification-light.webp differ diff --git a/web/public/empty-state/search/search-dark.webp b/web/public/empty-state/search/search-dark.webp new file mode 100644 index 000000000..92abc616b Binary files /dev/null and b/web/public/empty-state/search/search-dark.webp differ diff --git a/web/public/empty-state/search/search-light.webp b/web/public/empty-state/search/search-light.webp new file mode 100644 index 000000000..807ed2615 Binary files /dev/null and b/web/public/empty-state/search/search-light.webp differ diff --git a/web/public/empty-state/search/snooze-dark.webp b/web/public/empty-state/search/snooze-dark.webp new file mode 100644 index 000000000..296e797e8 Binary files /dev/null and b/web/public/empty-state/search/snooze-dark.webp differ diff --git a/web/public/empty-state/search/snooze-light.webp b/web/public/empty-state/search/snooze-light.webp new file mode 100644 index 000000000..897ff3598 Binary files /dev/null and b/web/public/empty-state/search/snooze-light.webp differ diff --git a/web/store/issue/issue-details/reaction.store.ts b/web/store/issue/issue-details/reaction.store.ts index a32ba6eca..e873e4c8c 100644 --- a/web/store/issue/issue-details/reaction.store.ts +++ b/web/store/issue/issue-details/reaction.store.ts @@ -84,7 +84,7 @@ export class IssueReactionStore implements IIssueReactionStore { if (reactions?.[reaction]) reactions?.[reaction].map((reactionId) => { const currentReaction = this.getReactionById(reactionId); - if (currentReaction && currentReaction.actor_id === userId) _userReactions.push(currentReaction); + if (currentReaction && currentReaction.actor === userId) _userReactions.push(currentReaction); }); }); @@ -151,7 +151,7 @@ export class IssueReactionStore implements IIssueReactionStore { ) => { try { const userReactions = this.reactionsByUser(issueId, userId); - const currentReaction = find(userReactions, { actor_id: userId, reaction: reaction }); + const currentReaction = find(userReactions, { actor: userId, reaction: reaction }); if (currentReaction && currentReaction.id) { runInAction(() => { diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index be77efcd1..c58e1af42 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -44,20 +44,24 @@ export interface IIssueDetail IIssueCommentReactionStoreActions { // observables peekIssue: TPeekIssue | undefined; + isCreateIssueModalOpen: boolean; isIssueLinkModalOpen: boolean; isParentIssueModalOpen: boolean; isDeleteIssueModalOpen: boolean; isArchiveIssueModalOpen: boolean; isRelationModalOpen: TIssueRelationTypes | null; + isSubIssuesModalOpen: boolean; // computed isAnyModalOpen: boolean; // actions setPeekIssue: (peekIssue: TPeekIssue | undefined) => void; + toggleCreateIssueModal: (value: boolean) => void; toggleIssueLinkModal: (value: boolean) => void; toggleParentIssueModal: (value: boolean) => void; toggleDeleteIssueModal: (value: boolean) => void; toggleArchiveIssueModal: (value: boolean) => void; toggleRelationModal: (value: TIssueRelationTypes | null) => void; + toggleSubIssuesModal: (value: boolean) => void; // store rootIssueStore: IIssueRootStore; issue: IIssueStore; @@ -75,11 +79,13 @@ export interface IIssueDetail export class IssueDetail implements IIssueDetail { // observables peekIssue: TPeekIssue | undefined = undefined; + isCreateIssueModalOpen: boolean = false; isIssueLinkModalOpen: boolean = false; isParentIssueModalOpen: boolean = false; isDeleteIssueModalOpen: boolean = false; isArchiveIssueModalOpen: boolean = false; isRelationModalOpen: TIssueRelationTypes | null = null; + isSubIssuesModalOpen: boolean = false; // store rootIssueStore: IIssueRootStore; issue: IIssueStore; @@ -97,20 +103,24 @@ export class IssueDetail implements IIssueDetail { makeObservable(this, { // observables peekIssue: observable, + isCreateIssueModalOpen: observable, isIssueLinkModalOpen: observable.ref, isParentIssueModalOpen: observable.ref, isDeleteIssueModalOpen: observable.ref, isArchiveIssueModalOpen: observable.ref, isRelationModalOpen: observable.ref, + isSubIssuesModalOpen: observable.ref, // computed isAnyModalOpen: computed, // action setPeekIssue: action, + toggleCreateIssueModal: action, toggleIssueLinkModal: action, toggleParentIssueModal: action, toggleDeleteIssueModal: action, toggleArchiveIssueModal: action, toggleRelationModal: action, + toggleSubIssuesModal: action, }); // store @@ -130,21 +140,25 @@ export class IssueDetail implements IIssueDetail { // computed get isAnyModalOpen() { return ( + this.isCreateIssueModalOpen || this.isIssueLinkModalOpen || this.isParentIssueModalOpen || this.isDeleteIssueModalOpen || this.isArchiveIssueModalOpen || - Boolean(this.isRelationModalOpen) + Boolean(this.isRelationModalOpen) || + this.isSubIssuesModalOpen ); } // actions setPeekIssue = (peekIssue: TPeekIssue | undefined) => (this.peekIssue = peekIssue); + toggleCreateIssueModal = (value: boolean) => (this.isCreateIssueModalOpen = value); toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value); toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value); toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value); toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value); toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value); + toggleSubIssuesModal = (value: boolean) => (this.isSubIssuesModalOpen = value); // issue fetchIssue = async (