diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 42904a8fc..e777a93a6 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -192,7 +192,15 @@ class CycleViewSet(WebhookMixin, BaseViewSet): ) def list(self, request, slug, project_id): - queryset = self.get_queryset() + queryset = self.get_queryset().annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) cycle_view = request.GET.get("cycle_view", "all") # Update the order by @@ -223,6 +231,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "progress_snapshot", # meta fields "is_favorite", + "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -345,6 +354,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "external_id", "progress_snapshot", # meta fields + "total_issues", "is_favorite", "cancelled_issues", "completed_issues", diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index ee9718b59..881730d65 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -79,6 +79,15 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ), ) ) + .annotate( + total_issues=Count( + "issue_module", + filter=Q( + issue_module__issue__archived_at__isnull=True, + issue_module__issue__is_draft=False, + ), + ), + ) .annotate( completed_issues=Count( "issue_module__issue__state__group", @@ -214,6 +223,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): "external_source", "external_id", # computed fields + "total_issues", "is_favorite", "cancelled_issues", "completed_issues", diff --git a/apiserver/plane/db/management/commands/test_email.py b/apiserver/plane/db/management/commands/test_email.py new file mode 100644 index 000000000..d36a784d0 --- /dev/null +++ b/apiserver/plane/db/management/commands/test_email.py @@ -0,0 +1,61 @@ +from django.core.mail import EmailMultiAlternatives, get_connection +from django.core.management import BaseCommand, CommandError + +from plane.license.utils.instance_value import get_email_configuration + + +class Command(BaseCommand): + """Django command to pause execution until db is available""" + + def add_arguments(self, parser): + # Positional argument + parser.add_argument("to_email", type=str, help="receiver's email") + + def handle(self, *args, **options): + receiver_email = options.get("to_email") + + if not receiver_email: + raise CommandError("Reciever email is required") + + ( + EMAIL_HOST, + EMAIL_HOST_USER, + EMAIL_HOST_PASSWORD, + EMAIL_PORT, + EMAIL_USE_TLS, + EMAIL_FROM, + ) = get_email_configuration() + + connection = get_connection( + host=EMAIL_HOST, + port=int(EMAIL_PORT), + username=EMAIL_HOST_USER, + password=EMAIL_HOST_PASSWORD, + use_tls=EMAIL_USE_TLS == "1", + timeout=30, + ) + # Prepare email details + subject = "Email Notification from Plane" + message = ( + "This is a sample email notification sent from Plane application." + ) + + self.stdout.write(self.style.SUCCESS("Trying to send test email...")) + + # Send the email + try: + msg = EmailMultiAlternatives( + subject=subject, + body=message, + from_email=EMAIL_FROM, + to=[receiver_email], + connection=connection, + ) + msg.send() + self.stdout.write(self.style.SUCCESS("Email succesfully sent")) + except Exception as e: + self.stdout.write( + self.style.ERROR( + f"Error: Email could not be delivered due to {e}" + ) + ) diff --git a/packages/types/src/importer/index.d.ts b/packages/types/src/importer/index.d.ts index 877c07196..271d685b6 100644 --- a/packages/types/src/importer/index.d.ts +++ b/packages/types/src/importer/index.d.ts @@ -1,7 +1,7 @@ export * from "./github-importer"; export * from "./jira-importer"; -import { IProjectLite } from "../projects"; +import { IProjectLite } from "../project"; // types import { IUserLite } from "../users"; diff --git a/packages/types/src/inbox/inbox-types.d.ts b/packages/types/src/inbox/inbox-types.d.ts index 9db71c3ee..c3ec8461e 100644 --- a/packages/types/src/inbox/inbox-types.d.ts +++ b/packages/types/src/inbox/inbox-types.d.ts @@ -1,5 +1,5 @@ import { TIssue } from "../issues/base"; -import type { IProjectLite } from "../projects"; +import type { IProjectLite } from "../project"; export type TInboxIssueExtended = { completed_at: string | null; diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index eeec266b5..48d0c1448 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -2,10 +2,10 @@ export * from "./users"; export * from "./workspace"; export * from "./cycle"; export * from "./dashboard"; -export * from "./projects"; +export * from "./project"; export * from "./state"; export * from "./issues"; -export * from "./modules"; +export * from "./module"; export * from "./views"; export * from "./integration"; export * from "./pages"; diff --git a/packages/types/src/module/index.ts b/packages/types/src/module/index.ts new file mode 100644 index 000000000..783634662 --- /dev/null +++ b/packages/types/src/module/index.ts @@ -0,0 +1,2 @@ +export * from "./module_filters"; +export * from "./modules"; diff --git a/packages/types/src/module/module_filters.d.ts b/packages/types/src/module/module_filters.d.ts new file mode 100644 index 000000000..10d56c328 --- /dev/null +++ b/packages/types/src/module/module_filters.d.ts @@ -0,0 +1,32 @@ +export type TModuleOrderByOptions = + | "name" + | "-name" + | "progress" + | "-progress" + | "issues_length" + | "-issues_length" + | "target_date" + | "-target_date" + | "created_at" + | "-created_at"; + +export type TModuleLayoutOptions = "list" | "board" | "gantt"; + +export type TModuleDisplayFilters = { + favorites?: boolean; + layout?: TModuleLayoutOptions; + order_by?: TModuleOrderByOptions; +}; + +export type TModuleFilters = { + lead?: string[] | null; + members?: string[] | null; + start_date?: string[] | null; + status?: string[] | null; + target_date?: string[] | null; +}; + +export type TModuleStoredFilters = { + display_filters?: TModuleDisplayFilters; + filters?: TModuleFilters; +}; diff --git a/packages/types/src/modules.d.ts b/packages/types/src/module/modules.d.ts similarity index 100% rename from packages/types/src/modules.d.ts rename to packages/types/src/module/modules.d.ts diff --git a/packages/types/src/project/index.ts b/packages/types/src/project/index.ts new file mode 100644 index 000000000..ef7308bf7 --- /dev/null +++ b/packages/types/src/project/index.ts @@ -0,0 +1,2 @@ +export * from "./project_filters"; +export * from "./projects"; diff --git a/packages/types/src/project/project_filters.d.ts b/packages/types/src/project/project_filters.d.ts new file mode 100644 index 000000000..02ad09ee1 --- /dev/null +++ b/packages/types/src/project/project_filters.d.ts @@ -0,0 +1,25 @@ +export type TProjectOrderByOptions = + | "sort_order" + | "name" + | "-name" + | "created_at" + | "-created_at" + | "members_length" + | "-members_length"; + +export type TProjectDisplayFilters = { + my_projects?: boolean; + order_by?: TProjectOrderByOptions; +}; + +export type TProjectFilters = { + access?: string[] | null; + lead?: string[] | null; + members?: string[] | null; + created_at?: string[] | null; +}; + +export type TProjectStoredFilters = { + display_filters?: TProjectDisplayFilters; + filters?: TProjectFilters; +}; diff --git a/packages/types/src/projects.d.ts b/packages/types/src/project/projects.d.ts similarity index 99% rename from packages/types/src/projects.d.ts rename to packages/types/src/project/projects.d.ts index afae5199f..f310d9c66 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -7,7 +7,7 @@ import type { IWorkspace, IWorkspaceLite, TStateGroups, -} from "."; +} from ".."; export type TProjectLogoProps = { in_use: "emoji" | "icon"; diff --git a/packages/ui/src/emoji/emoji-icon-picker.tsx b/packages/ui/src/emoji/emoji-icon-picker.tsx index 42c367938..5bfcdbe17 100644 --- a/packages/ui/src/emoji/emoji-icon-picker.tsx +++ b/packages/ui/src/emoji/emoji-icon-picker.tsx @@ -103,7 +103,7 @@ export const CustomEmojiIconPicker: React.FC = (props) => { style={styles.popper} {...attributes.popper} className={cn( - "h-80 w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden", + "w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden", dropdownClassName )} > @@ -146,7 +146,7 @@ export const CustomEmojiIconPicker: React.FC = (props) => { }} /> - + { diff --git a/packages/ui/src/tooltip/tooltip.tsx b/packages/ui/src/tooltip/tooltip.tsx index 65d014efe..92bab8d04 100644 --- a/packages/ui/src/tooltip/tooltip.tsx +++ b/packages/ui/src/tooltip/tooltip.tsx @@ -29,6 +29,7 @@ interface ITooltipProps { className?: string; openDelay?: number; closeDelay?: number; + isMobile?: boolean; } export const Tooltip: React.FC = ({ @@ -40,6 +41,7 @@ export const Tooltip: React.FC = ({ className = "", openDelay = 200, closeDelay, + isMobile = false, }) => ( = ({ hoverCloseDelay={closeDelay} content={
{tooltipHeading &&
{tooltipHeading}
} {tooltipContent} diff --git a/web/components/api-token/modal/generated-token-details.tsx b/web/components/api-token/modal/generated-token-details.tsx index fcae6b249..d21caf36c 100644 --- a/web/components/api-token/modal/generated-token-details.tsx +++ b/web/components/api-token/modal/generated-token-details.tsx @@ -6,6 +6,8 @@ import { renderFormattedDate } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // types import { IApiToken } from "@plane/types"; +// hooks +import { usePlatformOS } from "hooks/use-platform-os"; type Props = { handleClose: () => void; @@ -14,7 +16,7 @@ type Props = { export const GeneratedTokenDetails: React.FC = (props) => { const { handleClose, tokenDetails } = props; - + const { isMobile } = usePlatformOS(); const copyApiToken = (token: string) => { copyTextToClipboard(token).then(() => setToast({ @@ -40,7 +42,7 @@ export const GeneratedTokenDetails: React.FC = (props) => { className="mt-4 flex w-full items-center justify-between rounded-md border-[0.5px] border-custom-border-200 px-3 py-2 text-sm font-medium outline-none" > {tokenDetails.token} - + diff --git a/web/components/api-token/token-list-item.tsx b/web/components/api-token/token-list-item.tsx index 88af9a0a2..3dd381d53 100644 --- a/web/components/api-token/token-list-item.tsx +++ b/web/components/api-token/token-list-item.tsx @@ -3,6 +3,7 @@ import { XCircle } from "lucide-react"; // components import { Tooltip } from "@plane/ui"; import { DeleteApiTokenModal } from "components/api-token"; +import { usePlatformOS } from "hooks/use-platform-os"; // ui // helpers import { renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper"; @@ -17,12 +18,14 @@ export const ApiTokenListItem: React.FC = (props) => { const { token } = props; // states const [deleteModalOpen, setDeleteModalOpen] = useState(false); + // hooks + const { isMobile } = usePlatformOS(); return ( <> setDeleteModalOpen(false)} tokenId={token.id} />
- +
{cycleDetails.assignee_ids.length > 0 && ( - +
{cycleDetails.assignee_ids.map((assigne_id) => { @@ -190,6 +193,7 @@ export const CyclesBoardCard: FC = observer((props) => {
diff --git a/web/components/cycles/cycles-view-header.tsx b/web/components/cycles/cycles-view-header.tsx index b0feede0e..f7ff3567c 100644 --- a/web/components/cycles/cycles-view-header.tsx +++ b/web/components/cycles/cycles-view-header.tsx @@ -5,6 +5,7 @@ import { ListFilter, Search, X } from "lucide-react"; // hooks import { useCycleFilter } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { usePlatformOS } from "hooks/use-platform-os"; // components import { CycleFiltersSelection } from "components/cycles"; import { FiltersDropdown } from "components/issues"; @@ -36,6 +37,7 @@ export const CyclesViewHeader: React.FC = observer((props) => { updateFilters, updateSearchQuery, } = useCycleFilter(); + const { isMobile } = usePlatformOS(); // outside click detector hook useOutsideClickDetector(inputRef, () => { if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false); @@ -62,7 +64,10 @@ export const CyclesViewHeader: React.FC = observer((props) => { const handleInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") { if (searchQuery && searchQuery.trim() !== "") updateSearchQuery(""); - else setIsSearchOpen(false); + else { + setIsSearchOpen(false); + inputRef.current?.blur(); + } } }; @@ -107,7 +112,7 @@ export const CyclesViewHeader: React.FC = observer((props) => { updateSearchQuery(e.target.value)} @@ -131,7 +136,7 @@ export const CyclesViewHeader: React.FC = observer((props) => {
{CYCLE_VIEW_LAYOUTS.map((layout) => ( - + + )} +
+ + updateSearchQuery(e.target.value)} + onKeyDown={handleInputKeyDown} + /> + {isSearchOpen && ( + )} +
+
+
+ {MODULE_VIEW_LAYOUTS.map((layout) => ( + + ))}
+ { + if (!projectId || val === displayFilters?.order_by) return; + updateDisplayFilters(projectId.toString(), { + order_by: val, + }); + }} + /> + } title="Filters" placement="bottom-end"> + { + if (!projectId) return; + updateDisplayFilters(projectId.toString(), val); + }} + handleFiltersUpdate={handleFilters} + memberIds={workspaceMemberIds ?? undefined} + /> + {canUserCreateModule && ( + )} +
+ + updateSearchQuery(e.target.value)} + onKeyDown={handleInputKeyDown} + /> + {isSearchOpen && ( + + )} +
)} + { + if (!workspaceSlug || val === displayFilters?.order_by) return; + updateDisplayFilters(workspaceSlug, { + order_by: val, + }); + }} + /> + } title="Filters" placement="bottom-end"> + { + if (!workspaceSlug) return; + updateDisplayFilters(workspaceSlug, val); + }} + memberIds={workspaceMemberIds ?? undefined} + /> + {isAuthorizedUser && ( )} diff --git a/web/components/inbox/sidebar/inbox-list-item.tsx b/web/components/inbox/sidebar/inbox-list-item.tsx index 2ba28404d..faaaa662a 100644 --- a/web/components/inbox/sidebar/inbox-list-item.tsx +++ b/web/components/inbox/sidebar/inbox-list-item.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/router"; // icons import { CalendarDays } from "lucide-react"; // hooks +import { usePlatformOS } from "hooks/use-platform-os"; // ui import { Tooltip, PriorityIcon } from "@plane/ui"; // helpers @@ -33,7 +34,7 @@ export const InboxIssueListItem: FC = observer((props) => { const { issue: { getIssueById }, } = useIssueDetail(); - + const { isMobile } = usePlatformOS(); const inboxIssueDetail = getInboxIssueByIssueId(inboxId, issueId); const issue = getIssueById(issueId); @@ -83,10 +84,10 @@ export const InboxIssueListItem: FC = observer((props) => {
- + - +
{renderFormattedDate(issue.created_at ?? "")} diff --git a/web/components/instance/sidebar-dropdown.tsx b/web/components/instance/sidebar-dropdown.tsx index 63ee1f2d1..a5a268af4 100644 --- a/web/components/instance/sidebar-dropdown.tsx +++ b/web/components/instance/sidebar-dropdown.tsx @@ -9,6 +9,7 @@ import { Menu, Transition } from "@headlessui/react"; // icons import { LogIn, LogOut, Settings, UserCog2 } from "lucide-react"; // hooks +import { usePlatformOS } from "hooks/use-platform-os"; import { Avatar, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; import { useApplication, useUser } from "hooks/store"; // ui @@ -34,7 +35,7 @@ export const InstanceSidebarDropdown = observer(() => { const { signOut, currentUser, currentUserSettings } = useUser(); // hooks const { setTheme } = useTheme(); - + const { isMobile } = usePlatformOS(); // redirect url for normal mode const redirectWorkspaceSlug = workspaceSlug || @@ -73,7 +74,7 @@ export const InstanceSidebarDropdown = observer(() => { {!sidebarCollapsed && (

Instance admin

- +
diff --git a/web/components/instance/sidebar-menu.tsx b/web/components/instance/sidebar-menu.tsx index 782cc90d9..e6719895f 100644 --- a/web/components/instance/sidebar-menu.tsx +++ b/web/components/instance/sidebar-menu.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react"; // hooks +import { usePlatformOS } from "hooks/use-platform-os"; import { Tooltip } from "@plane/ui"; import { useApplication } from "hooks/store"; // ui @@ -46,6 +47,7 @@ export const InstanceAdminSidebarMenu = () => { } = useApplication(); // router const router = useRouter(); + const { isMobile } = usePlatformOS(); return (
@@ -55,7 +57,7 @@ export const InstanceAdminSidebarMenu = () => { return (
- +
= observer(({ integration }) } = useUser(); const isUserAdmin = currentWorkspaceRole === 20; - + const { isMobile } = usePlatformOS(); const { startAuth, isConnecting: isInstalling } = useIntegrationPopup({ provider: integration.provider, github_app_name: envConfig?.github_app_name || "", @@ -129,6 +130,7 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) {workspaceIntegrations ? ( isInstalled ? ( @@ -147,6 +149,7 @@ export const SingleIntegrationCard: React.FC = observer(({ integration }) ) : ( diff --git a/web/components/issues/attachment/attachment-detail.tsx b/web/components/issues/attachment/attachment-detail.tsx index 8ff2b9305..c1be0f355 100644 --- a/web/components/issues/attachment/attachment-detail.tsx +++ b/web/components/issues/attachment/attachment-detail.tsx @@ -2,6 +2,7 @@ import { FC, useState } from "react"; import Link from "next/link"; import { AlertCircle, X } from "lucide-react"; // hooks +import { usePlatformOS } from "hooks/use-platform-os"; // ui import { Tooltip } from "@plane/ui"; // components @@ -34,7 +35,7 @@ export const IssueAttachmentsDetail: FC = (props) => { } = useIssueDetail(); // states const [attachmentDeleteModal, setAttachmentDeleteModal] = useState(false); - + const { isMobile } = usePlatformOS(); const attachment = attachmentId && getAttachmentById(attachmentId); if (!attachment) return <>; @@ -56,10 +57,11 @@ export const IssueAttachmentsDetail: FC = (props) => {
{getFileIcon(getFileExtension(attachment.asset))}
- + {truncateText(`${getFileName(attachment.attributes.name)}`, 10)} = (pr } = useIssueDetail(); const activity = getActivityById(activityId); - + const { isMobile } = usePlatformOS(); if (!activity) return <>; return (
= (pr {children} {calculateTimeAgo(activity.created_at)} diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx index 49f813ec6..c622079e2 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/issue-link.tsx @@ -2,6 +2,7 @@ import { FC } from "react"; // hooks import { Tooltip } from "@plane/ui"; import { useIssueDetail } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; // ui type TIssueLink = { @@ -14,12 +15,12 @@ export const IssueLink: FC = (props) => { const { activity: { getActivityById }, } = useIssueDetail(); - + const { isMobile } = usePlatformOS(); const activity = getActivityById(activityId); if (!activity) return <>; return ( - + = (props) => { +export const LabelList: FC = observer((props) => { const { workspaceSlug, projectId, issueId, labelOperations, disabled } = props; // hooks const { @@ -40,4 +41,4 @@ export const LabelList: FC = (props) => { ))} ); -}; +}); diff --git a/web/components/issues/issue-detail/links/link-detail.tsx b/web/components/issues/issue-detail/links/link-detail.tsx index 4504329f0..2772dc0c7 100644 --- a/web/components/issues/issue-detail/links/link-detail.tsx +++ b/web/components/issues/issue-detail/links/link-detail.tsx @@ -9,6 +9,7 @@ import { ExternalLinkIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; import { calculateTimeAgo } from "helpers/date-time.helper"; import { copyTextToClipboard } from "helpers/string.helper"; import { useIssueDetail, useMember } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; import { IssueLinkCreateUpdateModal, TLinkOperationsModal } from "./create-update-link-modal"; export type TIssueLinkDetail = { @@ -33,7 +34,7 @@ export const IssueLinkDetail: FC = (props) => { toggleIssueLinkModalStore(modalToggle); setIsIssueLinkModalOpen(modalToggle); }; - + const { isMobile } = usePlatformOS(); const linkDetail = getLinkById(linkId); if (!linkDetail) return <>; @@ -64,7 +65,7 @@ export const IssueLinkDetail: FC = (props) => { - + {linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url} diff --git a/web/components/issues/issue-detail/module-select.tsx b/web/components/issues/issue-detail/module-select.tsx index f157ede86..91cda67d6 100644 --- a/web/components/issues/issue-detail/module-select.tsx +++ b/web/components/issues/issue-detail/module-select.tsx @@ -71,7 +71,6 @@ export const IssueModuleSelect: React.FC = observer((props) hideIcon dropdownArrow dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" - showTooltip multiple />
diff --git a/web/components/issues/issue-detail/parent-select.tsx b/web/components/issues/issue-detail/parent-select.tsx index 0b6501027..60cb06664 100644 --- a/web/components/issues/issue-detail/parent-select.tsx +++ b/web/components/issues/issue-detail/parent-select.tsx @@ -3,6 +3,7 @@ import { observer } from "mobx-react-lite"; import Link from "next/link"; import { Pencil, X } from "lucide-react"; // hooks +import { usePlatformOS } from "hooks/use-platform-os"; // components import { Tooltip } from "@plane/ui"; import { ParentIssuesListModal } from "components/issues"; @@ -35,7 +36,7 @@ export const IssueParentSelect: React.FC = observer((props) const parentIssue = issue?.parent_id ? getIssueById(issue.parent_id) : undefined; const parentIssueProjectDetails = parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined; - + const { isMobile } = usePlatformOS(); const handleParentIssue = async (_issueId: string | null = null) => { try { await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId }); @@ -73,7 +74,7 @@ export const IssueParentSelect: React.FC = observer((props) > {issue.parent_id && parentIssue ? (
- + = observer((props) {!disabled && ( - + { e.preventDefault(); diff --git a/web/components/issues/issue-detail/reactions/issue-comment.tsx b/web/components/issues/issue-detail/reactions/issue-comment.tsx index 97c63a017..e26befe1b 100644 --- a/web/components/issues/issue-detail/reactions/issue-comment.tsx +++ b/web/components/issues/issue-detail/reactions/issue-comment.tsx @@ -1,10 +1,11 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; // components -import { TOAST_TYPE, setToast } from "@plane/ui"; +import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; import { renderEmoji } from "helpers/emoji.helper"; -import { useIssueDetail } from "hooks/store"; -// ui +import { useIssueDetail, useMember } from "hooks/store"; +// helper +import { formatTextList } from "helpers/issue.helper"; // types import { IUser } from "@plane/types"; import { ReactionSelector } from "./reaction-selector"; @@ -21,10 +22,11 @@ export const IssueCommentReaction: FC = observer((props) // hooks const { - commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser }, + commentReaction: { getCommentReactionsByCommentId, commentReactionsByUser, getCommentReactionById }, createCommentReaction, removeCommentReaction, } = useIssueDetail(); + const { getUserDetails } = useMember(); const reactionIds = getCommentReactionsByCommentId(commentId); const userReactions = commentReactionsByUser(commentId, currentUser.id).map((r) => r.reaction); @@ -73,6 +75,17 @@ export const IssueCommentReaction: FC = observer((props) [workspaceSlug, projectId, commentId, currentUser, createCommentReaction, removeCommentReaction, userReactions] ); + const getReactionUsers = (reaction: string): string => { + const reactionUsers = (reactionIds?.[reaction] || []) + .map((reactionId) => { + const reactionDetails = getCommentReactionById(reactionId); + return reactionDetails ? getUserDetails(reactionDetails.actor)?.display_name : null; + }) + .filter((displayName): displayName is string => !!displayName); + const formattedUsers = formatTextList(reactionUsers); + return formattedUsers; + }; + return (
= observer((props) (reaction) => reactionIds[reaction]?.length > 0 && ( <> - + + + ) )} diff --git a/web/components/issues/issue-detail/reactions/issue.tsx b/web/components/issues/issue-detail/reactions/issue.tsx index 6f5610634..c21f92139 100644 --- a/web/components/issues/issue-detail/reactions/issue.tsx +++ b/web/components/issues/issue-detail/reactions/issue.tsx @@ -1,10 +1,12 @@ import { FC, useMemo } from "react"; import { observer } from "mobx-react-lite"; -// components -import { TOAST_TYPE, setToast } from "@plane/ui"; -import { renderEmoji } from "helpers/emoji.helper"; -import { useIssueDetail } from "hooks/store"; +// hooks +import { useIssueDetail, useMember } from "hooks/store"; // ui +import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; +import { formatTextList } from "helpers/issue.helper"; // types import { IUser } from "@plane/types"; import { ReactionSelector } from "./reaction-selector"; @@ -20,10 +22,11 @@ export const IssueReaction: FC = observer((props) => { const { workspaceSlug, projectId, issueId, currentUser } = props; // hooks const { - reaction: { getReactionsByIssueId, reactionsByUser }, + reaction: { getReactionsByIssueId, reactionsByUser, getReactionById }, createReaction, removeReaction, } = useIssueDetail(); + const { getUserDetails } = useMember(); const reactionIds = getReactionsByIssueId(issueId); const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction); @@ -72,6 +75,18 @@ export const IssueReaction: FC = observer((props) => { [workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, userReactions] ); + const getReactionUsers = (reaction: string): string => { + const reactionUsers = (reactionIds?.[reaction] || []) + .map((reactionId) => { + const reactionDetails = getReactionById(reactionId); + return reactionDetails ? getUserDetails(reactionDetails.actor_id)?.display_name : null; + }) + .filter((displayName): displayName is string => !!displayName); + + const formattedUsers = formatTextList(reactionUsers); + return formattedUsers; + }; + return (
@@ -81,19 +96,21 @@ export const IssueReaction: FC = observer((props) => { (reaction) => reactionIds[reaction]?.length > 0 && ( <> - + + + ) )} diff --git a/web/components/issues/issue-detail/relation-select.tsx b/web/components/issues/issue-detail/relation-select.tsx index 0fd0902c6..d609d1ddd 100644 --- a/web/components/issues/issue-detail/relation-select.tsx +++ b/web/components/issues/issue-detail/relation-select.tsx @@ -7,6 +7,7 @@ import { RelatedIcon, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; import { cn } from "helpers/common.helper"; import { useIssueDetail, useIssues, useProject } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; // components // ui // helpers @@ -59,7 +60,7 @@ export const IssueRelationSelect: React.FC = observer((pro toggleRelationModal, } = useIssueDetail(); const { issueMap } = useIssues(); - + const { isMobile } = usePlatformOS(); const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey); const onSubmit = async (data: ISearchIssueResponse[]) => { @@ -124,7 +125,7 @@ export const IssueRelationSelect: React.FC = observer((pro key={relationIssueId} className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${issueRelationObject[relationKey].className}`} > - + = observer((pro {!disabled && ( - + { e.preventDefault(); diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index b1bd22ee8..c58851704 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -378,7 +378,7 @@ export const IssueDetailRoot: FC = observer((props) => { />
= observer((props) => { issue: { getIssueById }, } = useIssueDetail(); const { getStateById } = useProjectState(); - + const { isMobile } = usePlatformOS(); const issue = getIssueById(issueId); if (!issue) return <>; @@ -138,7 +139,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { )}
- +
diff --git a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx index fdaed4b9b..24a197c76 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/date.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/date.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite"; // icons import { X } from "lucide-react"; // helpers -import { DATE_FILTER_OPTIONS } from "constants/filters"; +import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters"; import { renderFormattedDate } from "helpers/date-time.helper"; import { capitalizeFirstLetter } from "helpers/string.helper"; // constants @@ -18,7 +18,7 @@ export const AppliedDateFilters: React.FC = observer((props) => { const getDateLabel = (value: string): string => { let dateLabel = ""; - const dateDetails = DATE_FILTER_OPTIONS.find((d) => d.value === value); + const dateDetails = DATE_AFTER_FILTER_OPTIONS.find((d) => d.value === value); if (dateDetails) dateLabel = dateDetails.name; else { diff --git a/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx b/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx index 87def7e29..3c47eb286 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/start-date.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite"; import { DateFilterModal } from "components/core"; import { FilterHeader, FilterOption } from "components/issues"; // constants -import { DATE_FILTER_OPTIONS } from "constants/filters"; +import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters"; type Props = { appliedFilters: string[] | null; @@ -21,7 +21,9 @@ export const FilterStartDate: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase())); + const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) => + d.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); return ( <> diff --git a/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx b/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx index 9e0ce18a7..83a526351 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/target-date.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite"; import { DateFilterModal } from "components/core"; import { FilterHeader, FilterOption } from "components/issues"; // constants -import { DATE_FILTER_OPTIONS } from "constants/filters"; +import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters"; type Props = { appliedFilters: string[] | null; @@ -21,7 +21,9 @@ export const FilterTargetDate: React.FC = observer((props) => { const appliedFiltersCount = appliedFilters?.length ?? 0; - const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase())); + const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) => + d.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); return ( <> diff --git a/web/components/issues/issue-layouts/filters/header/layout-selection.tsx b/web/components/issues/issue-layouts/filters/header/layout-selection.tsx index a69ead577..2b8df8edf 100644 --- a/web/components/issues/issue-layouts/filters/header/layout-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/layout-selection.tsx @@ -5,7 +5,8 @@ import { Tooltip } from "@plane/ui"; // types import { ISSUE_LAYOUTS } from "constants/issue"; import { TIssueLayouts } from "@plane/types"; -// constants +// hooks +import { usePlatformOS } from "hooks/use-platform-os"; type Props = { layouts: TIssueLayouts[]; @@ -15,11 +16,12 @@ type Props = { export const LayoutSelection: React.FC = (props) => { const { layouts, onChange, selectedLayout } = props; + const { isMobile } = usePlatformOS(); return (
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => ( - + {isArchivingAllowed && ( )} {!disabled && ( - + diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index c3ac1495a..47890c95c 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -93,7 +93,6 @@ export const IssueView: FC = observer((props) => { isOpen={isDeleteIssueModalOpen} handleClose={() => { toggleDeleteIssueModal(false); - removeRoutePeekId(); }} data={issue} onSubmit={() => issueOperations.remove(workspaceSlug, projectId, issueId)} diff --git a/web/components/issues/sub-issues/issue-list-item.tsx b/web/components/issues/sub-issues/issue-list-item.tsx index 5d7d19730..170bf622f 100644 --- a/web/components/issues/sub-issues/issue-list-item.tsx +++ b/web/components/issues/sub-issues/issue-list-item.tsx @@ -4,6 +4,7 @@ import { ChevronDown, ChevronRight, X, Pencil, Trash, Link as LinkIcon, Loader } // components import { ControlLink, CustomMenu, Tooltip } from "@plane/ui"; import { useIssueDetail, useProject, useProjectState } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; import { TIssue } from "@plane/types"; import { IssueList } from "./issues-list"; import { IssueProperty } from "./properties"; @@ -46,7 +47,7 @@ export const IssueListItem: React.FC = observer((props) => { } = useIssueDetail(); const project = useProject(); const { getProjectStates } = useProjectState(); - + const { isMobile } = usePlatformOS(); const issue = getIssueById(issueId); const projectDetail = (issue && issue.project_id && project.getProjectById(issue.project_id)) || undefined; const currentIssueStateDetail = @@ -117,7 +118,7 @@ export const IssueListItem: React.FC = observer((props) => { onClick={() => handleIssuePeekOverview(issue)} className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100" > - + {issue.name} diff --git a/web/components/modules/applied-filters/date.tsx b/web/components/modules/applied-filters/date.tsx new file mode 100644 index 000000000..42494bdbd --- /dev/null +++ b/web/components/modules/applied-filters/date.tsx @@ -0,0 +1,56 @@ +import { observer } from "mobx-react-lite"; +// icons +import { X } from "lucide-react"; +// helpers +import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters"; +import { renderFormattedDate } from "helpers/date-time.helper"; +import { capitalizeFirstLetter } from "helpers/string.helper"; +// constants + +type Props = { + editable: boolean | undefined; + handleRemove: (val: string) => void; + values: string[]; +}; + +export const AppliedDateFilters: React.FC = observer((props) => { + const { editable, handleRemove, values } = props; + + const getDateLabel = (value: string): string => { + let dateLabel = ""; + + const dateDetails = DATE_AFTER_FILTER_OPTIONS.find((d) => d.value === value); + + if (dateDetails) dateLabel = dateDetails.name; + else { + const dateParts = value.split(";"); + + if (dateParts.length === 2) { + const [date, time] = dateParts; + + dateLabel = `${capitalizeFirstLetter(time)} ${renderFormattedDate(date)}`; + } + } + + return dateLabel; + }; + + return ( + <> + {values.map((date) => ( +
+ {getDateLabel(date)} + {editable && ( + + )} +
+ ))} + + ); +}); diff --git a/web/components/modules/applied-filters/index.ts b/web/components/modules/applied-filters/index.ts new file mode 100644 index 000000000..cf34b6e69 --- /dev/null +++ b/web/components/modules/applied-filters/index.ts @@ -0,0 +1,4 @@ +export * from "./date"; +export * from "./members"; +export * from "./root"; +export * from "./status"; diff --git a/web/components/modules/applied-filters/members.tsx b/web/components/modules/applied-filters/members.tsx new file mode 100644 index 000000000..88f18ee0c --- /dev/null +++ b/web/components/modules/applied-filters/members.tsx @@ -0,0 +1,46 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// ui +import { Avatar } from "@plane/ui"; +// types +import { useMember } from "hooks/store"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; + editable: boolean | undefined; +}; + +export const AppliedMembersFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + // store hooks + const { + workspace: { getWorkspaceMemberDetails }, + } = useMember(); + + return ( + <> + {values.map((memberId) => { + const memberDetails = getWorkspaceMemberDetails(memberId)?.member; + + if (!memberDetails) return null; + + return ( +
+ + {memberDetails.display_name} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/components/modules/applied-filters/root.tsx b/web/components/modules/applied-filters/root.tsx new file mode 100644 index 000000000..2969ea715 --- /dev/null +++ b/web/components/modules/applied-filters/root.tsx @@ -0,0 +1,88 @@ +import { X } from "lucide-react"; +// components +import { AppliedDateFilters, AppliedMembersFilters, AppliedStatusFilters } from "components/modules"; +// helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +// types +import { TModuleFilters } from "@plane/types"; + +type Props = { + appliedFilters: TModuleFilters; + handleClearAllFilters: () => void; + handleRemoveFilter: (key: keyof TModuleFilters, value: string | null) => void; + alwaysAllowEditing?: boolean; +}; + +const MEMBERS_FILTERS = ["lead", "members"]; +const DATE_FILTERS = ["start_date", "target_date"]; + +export const ModuleAppliedFiltersList: React.FC = (props) => { + const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props; + + if (!appliedFilters) return null; + if (Object.keys(appliedFilters).length === 0) return null; + + const isEditingAllowed = alwaysAllowEditing; + + return ( +
+ {Object.entries(appliedFilters).map(([key, value]) => { + const filterKey = key as keyof TModuleFilters; + + if (!value) return; + if (Array.isArray(value) && value.length === 0) return; + + return ( +
+
+ {replaceUnderscoreIfSnakeCase(filterKey)} + {filterKey === "status" && ( + handleRemoveFilter("status", val)} + values={value} + /> + )} + {DATE_FILTERS.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} + values={value} + /> + )} + {MEMBERS_FILTERS.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} + values={value} + /> + )} + {isEditingAllowed && ( + + )} +
+
+ ); + })} + {isEditingAllowed && ( + + )} +
+ ); +}; diff --git a/web/components/modules/applied-filters/status.tsx b/web/components/modules/applied-filters/status.tsx new file mode 100644 index 000000000..ed5426cde --- /dev/null +++ b/web/components/modules/applied-filters/status.tsx @@ -0,0 +1,41 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// ui +import { ModuleStatusIcon } from "@plane/ui"; +// constants +import { MODULE_STATUS } from "constants/module"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; + editable: boolean | undefined; +}; + +export const AppliedStatusFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + + return ( + <> + {values.map((status) => { + const statusDetails = MODULE_STATUS?.find((s) => s.value === status); + if (!statusDetails) return null; + + return ( +
+ + {statusDetails.label} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/components/modules/dropdowns/filters/index.ts b/web/components/modules/dropdowns/filters/index.ts new file mode 100644 index 000000000..786fc5cec --- /dev/null +++ b/web/components/modules/dropdowns/filters/index.ts @@ -0,0 +1,6 @@ +export * from "./lead"; +export * from "./members"; +export * from "./root"; +export * from "./start-date"; +export * from "./status"; +export * from "./target-date"; diff --git a/web/components/modules/dropdowns/filters/lead.tsx b/web/components/modules/dropdowns/filters/lead.tsx new file mode 100644 index 000000000..ffd4f8a2e --- /dev/null +++ b/web/components/modules/dropdowns/filters/lead.tsx @@ -0,0 +1,96 @@ +import { useMemo, useState } from "react"; +import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; +// hooks +import { useMember } from "hooks/store"; +// components +import { FilterHeader, FilterOption } from "components/issues"; +// ui +import { Avatar, Loader } from "@plane/ui"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + memberIds: string[] | undefined; + searchQuery: string; +}; + +export const FilterLead: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + }, [appliedFilters, getUserDetails, memberIds, , searchQuery]); + + const handleViewToggle = () => { + if (!sortedOptions) return; + + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {sortedOptions ? ( + sortedOptions.length > 0 ? ( + <> + {sortedOptions.slice(0, itemsToRender).map((memberId) => { + const member = getUserDetails(memberId); + + if (!member) return null; + return ( + handleUpdate(member.id)} + icon={} + title={member.display_name} + /> + ); + })} + {sortedOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}); diff --git a/web/components/modules/dropdowns/filters/members.tsx b/web/components/modules/dropdowns/filters/members.tsx new file mode 100644 index 000000000..0d2737227 --- /dev/null +++ b/web/components/modules/dropdowns/filters/members.tsx @@ -0,0 +1,97 @@ +import { useMemo, useState } from "react"; +import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; +// hooks +import { useMember } from "hooks/store"; +// components +import { FilterHeader, FilterOption } from "components/issues"; +// ui +import { Avatar, Loader } from "@plane/ui"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + memberIds: string[] | undefined; + searchQuery: string; +}; + +export const FilterMembers: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); + + const handleViewToggle = () => { + if (!sortedOptions) return; + + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {sortedOptions ? ( + sortedOptions.length > 0 ? ( + <> + {sortedOptions.slice(0, itemsToRender).map((memberId) => { + const member = getUserDetails(memberId); + + if (!member) return null; + return ( + handleUpdate(member.id)} + icon={} + title={member.display_name} + /> + ); + })} + {sortedOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}); diff --git a/web/components/modules/dropdowns/filters/root.tsx b/web/components/modules/dropdowns/filters/root.tsx new file mode 100644 index 000000000..30841a43a --- /dev/null +++ b/web/components/modules/dropdowns/filters/root.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Search, X } from "lucide-react"; +// components +import { FilterLead, FilterMembers, FilterStartDate, FilterStatus, FilterTargetDate } from "components/modules"; +import { FilterOption } from "components/issues"; +// types +import { TModuleDisplayFilters, TModuleFilters } from "@plane/types"; +import { TModuleStatus } from "@plane/ui"; + +type Props = { + displayFilters: TModuleDisplayFilters; + filters: TModuleFilters; + handleDisplayFiltersUpdate: (updatedDisplayProperties: Partial) => void; + handleFiltersUpdate: (key: keyof TModuleFilters, value: string | string[]) => void; + memberIds?: string[] | undefined; +}; + +export const ModuleFiltersSelection: React.FC = observer((props) => { + const { displayFilters, filters, handleDisplayFiltersUpdate, handleFiltersUpdate, memberIds } = props; + // states + const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); + + return ( +
+
+
+ + setFiltersSearchQuery(e.target.value)} + autoFocus + /> + {filtersSearchQuery !== "" && ( + + )} +
+
+
+
+ + handleDisplayFiltersUpdate({ + favorites: !displayFilters.favorites, + }) + } + title="Favorites" + /> +
+ + {/* status */} +
+ handleFiltersUpdate("status", val)} + searchQuery={filtersSearchQuery} + /> +
+ + {/* lead */} +
+ handleFiltersUpdate("lead", val)} + searchQuery={filtersSearchQuery} + memberIds={memberIds} + /> +
+ + {/* members */} +
+ handleFiltersUpdate("members", val)} + searchQuery={filtersSearchQuery} + memberIds={memberIds} + /> +
+ + {/* start date */} +
+ handleFiltersUpdate("start_date", val)} + searchQuery={filtersSearchQuery} + /> +
+ + {/* target date */} +
+ handleFiltersUpdate("target_date", val)} + searchQuery={filtersSearchQuery} + /> +
+
+
+ ); +}); diff --git a/web/components/modules/dropdowns/filters/start-date.tsx b/web/components/modules/dropdowns/filters/start-date.tsx new file mode 100644 index 000000000..3c47eb286 --- /dev/null +++ b/web/components/modules/dropdowns/filters/start-date.tsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; + +// components +import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; +// constants +import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string | string[]) => void; + searchQuery: string; +}; + +export const FilterStartDate: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) => + d.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( + <> + {isDateFilterModalOpen && ( + setIsDateFilterModalOpen(false)} + isOpen={isDateFilterModalOpen} + onSelect={(val) => handleUpdate(val)} + title="Start date" + /> + )} + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + <> + {filteredOptions.map((option) => ( + handleUpdate(option.value)} + title={option.name} + multiple + /> + ))} + setIsDateFilterModalOpen(true)} title="Custom" multiple /> + + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/modules/dropdowns/filters/status.tsx b/web/components/modules/dropdowns/filters/status.tsx new file mode 100644 index 000000000..f73db2554 --- /dev/null +++ b/web/components/modules/dropdowns/filters/status.tsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { FilterHeader, FilterOption } from "components/issues"; +// ui +import { ModuleStatusIcon } from "@plane/ui"; +// types +import { TModuleStatus } from "@plane/types"; +// constants +import { MODULE_STATUS } from "constants/module"; + +type Props = { + appliedFilters: TModuleStatus[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterStatus: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + // states + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + const filteredOptions = MODULE_STATUS.filter((p) => p.value.includes(searchQuery.toLowerCase())); + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + filteredOptions.map((status) => ( + handleUpdate(status.value)} + icon={} + title={status.label} + /> + )) + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/modules/dropdowns/filters/target-date.tsx b/web/components/modules/dropdowns/filters/target-date.tsx new file mode 100644 index 000000000..d563dbe92 --- /dev/null +++ b/web/components/modules/dropdowns/filters/target-date.tsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; + +// components +import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; +// constants +import { DATE_AFTER_FILTER_OPTIONS } from "constants/filters"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string | string[]) => void; + searchQuery: string; +}; + +export const FilterTargetDate: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = DATE_AFTER_FILTER_OPTIONS.filter((d) => + d.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( + <> + {isDateFilterModalOpen && ( + setIsDateFilterModalOpen(false)} + isOpen={isDateFilterModalOpen} + onSelect={(val) => handleUpdate(val)} + title="Due date" + /> + )} + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + <> + {filteredOptions.map((option) => ( + handleUpdate(option.value)} + title={option.name} + multiple + /> + ))} + setIsDateFilterModalOpen(true)} title="Custom" multiple /> + + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/modules/dropdowns/index.ts b/web/components/modules/dropdowns/index.ts new file mode 100644 index 000000000..f6c42552f --- /dev/null +++ b/web/components/modules/dropdowns/index.ts @@ -0,0 +1,2 @@ +export * from "./filters"; +export * from "./order-by"; diff --git a/web/components/modules/dropdowns/order-by.tsx b/web/components/modules/dropdowns/order-by.tsx new file mode 100644 index 000000000..a611d1ead --- /dev/null +++ b/web/components/modules/dropdowns/order-by.tsx @@ -0,0 +1,70 @@ +import { ArrowDownWideNarrow, Check, ChevronDown } from "lucide-react"; +// ui +import { CustomMenu, getButtonStyling } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TModuleOrderByOptions } from "@plane/types"; +// constants +import { MODULE_ORDER_BY_OPTIONS } from "constants/module"; + +type Props = { + onChange: (value: TModuleOrderByOptions) => void; + value: TModuleOrderByOptions | undefined; +}; + +export const ModuleOrderByDropdown: React.FC = (props) => { + const { onChange, value } = props; + + const orderByDetails = MODULE_ORDER_BY_OPTIONS.find((option) => value?.includes(option.key)); + + const isDescending = value?.[0] === "-"; + + return ( + + + {orderByDetails?.label} + +
+ } + placement="bottom-end" + maxHeight="lg" + closeOnSelect + > + {MODULE_ORDER_BY_OPTIONS.map((option) => ( + { + if (isDescending) onChange(`-${option.key}` as TModuleOrderByOptions); + else onChange(option.key); + }} + > + {option.label} + {value?.includes(option.key) && } + + ))} +
+ { + if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions); + }} + > + Ascending + {!isDescending && } + + { + if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions); + }} + > + Descending + {isDescending && } + + + ); +}; diff --git a/web/components/modules/gantt-chart/blocks.tsx b/web/components/modules/gantt-chart/blocks.tsx index 073283df4..60af5d048 100644 --- a/web/components/modules/gantt-chart/blocks.tsx +++ b/web/components/modules/gantt-chart/blocks.tsx @@ -1,6 +1,8 @@ +import Link from "next/link"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; // hooks +import { usePlatformOS } from "hooks/use-platform-os"; // ui import { Tooltip, ModuleStatusIcon } from "@plane/ui"; // helpers @@ -24,6 +26,8 @@ export const ModuleGanttBlock: React.FC = observer((props) => { const { getModuleById } = useModule(); // derived values const moduleDetails = getModuleById(moduleId); + // hooks + const { isMobile } = usePlatformOS(); return (
= observer((props) => { >
{moduleDetails?.name}
@@ -54,8 +59,6 @@ export const ModuleGanttBlock: React.FC = observer((props) => { export const ModuleGanttSidebarBlock: React.FC = observer((props) => { const { moduleId } = props; - // router - const router = useRouter(); // store hooks const { router: { workspaceSlug }, @@ -65,14 +68,12 @@ export const ModuleGanttSidebarBlock: React.FC = observer((props) => { const moduleDetails = getModuleById(moduleId); return ( -
- router.push(`/${workspaceSlug}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`) - } + href={`/${workspaceSlug}/projects/${moduleDetails?.project_id}/modules/${moduleDetails?.id}`} >
{moduleDetails?.name}
-
+ ); }); diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx index 0a9b433c5..6c680e1ca 100644 --- a/web/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/components/modules/gantt-chart/modules-list-layout.tsx @@ -11,10 +11,12 @@ import { IModule } from "@plane/types"; export const ModulesListGanttChartView: React.FC = observer(() => { // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; // store const { currentProjectDetails } = useProject(); - const { projectModuleIds, moduleMap, updateModuleDetails } = useModule(); + const { getFilteredModuleIds, moduleMap, updateModuleDetails } = useModule(); + // derived values + const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined; const handleModuleUpdate = async (module: IModule, data: IBlockUpdateData) => { if (!workspaceSlug || !module) return; @@ -44,7 +46,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => { } blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)} blockToRender={(data: IModule) => } diff --git a/web/components/modules/index.ts b/web/components/modules/index.ts index c87ea79d2..7bda973fa 100644 --- a/web/components/modules/index.ts +++ b/web/components/modules/index.ts @@ -1,3 +1,5 @@ +export * from "./applied-filters"; +export * from "./dropdowns"; export * from "./select"; export * from "./sidebar-select"; export * from "./delete-module-modal"; diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 4873e009c..8ef3fe024 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -12,6 +12,7 @@ import { EUserProjectRoles } from "constants/project"; import { renderFormattedDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; import { useEventTracker, useMember, useModule, useUser } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; // components // ui // helpers @@ -39,7 +40,7 @@ export const ModuleCardItem: React.FC = observer((props) => { // derived values const moduleDetails = getModuleById(moduleId); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - + const { isMobile } = usePlatformOS(); const handleAddToFavorites = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); @@ -179,7 +180,7 @@ export const ModuleCardItem: React.FC = observer((props) => {
- + {moduleDetails.name}
@@ -208,7 +209,7 @@ export const ModuleCardItem: React.FC = observer((props) => { {issueCount ?? "0 Issue"}
{moduleDetails.member_ids?.length > 0 && ( - +
{moduleDetails.member_ids.map((member_id) => { @@ -222,6 +223,7 @@ export const ModuleCardItem: React.FC = observer((props) => {
diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 7fe25b918..0b28712b0 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -21,6 +21,7 @@ import { EUserProjectRoles } from "constants/project"; import { renderFormattedDate } from "helpers/date-time.helper"; import { copyUrlToClipboard } from "helpers/string.helper"; import { useModule, useUser, useEventTracker, useMember } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; // components // ui // helpers @@ -48,7 +49,7 @@ export const ModuleListItem: React.FC = observer((props) => { // derived values const moduleDetails = getModuleById(moduleId); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - + const { isMobile } = usePlatformOS(); const handleAddToFavorites = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); @@ -194,7 +195,7 @@ export const ModuleListItem: React.FC = observer((props) => { )} - + {moduleDetails.name}
@@ -227,7 +228,7 @@ export const ModuleListItem: React.FC = observer((props) => {
- +
{moduleDetails.member_ids.length > 0 ? ( diff --git a/web/components/modules/modules-list-view.tsx b/web/components/modules/modules-list-view.tsx index 78b4a6571..2998843e1 100644 --- a/web/components/modules/modules-list-view.tsx +++ b/web/components/modules/modules-list-view.tsx @@ -1,13 +1,16 @@ +import Image from "next/image"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks -import { useApplication, useEventTracker, useModule } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; +import { useApplication, useEventTracker, useModule, useModuleFilter } from "hooks/store"; // components import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "components/modules"; import { EmptyState } from "components/empty-state"; // ui import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; +// assets +import NameFilterImage from "public/empty-state/module/name-filter.svg"; +import AllFiltersImage from "public/empty-state/module/all-filters.svg"; // constants import { EmptyStateType } from "constants/empty-state"; @@ -18,29 +21,48 @@ export const ModulesListView: React.FC = observer(() => { // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); + const { getFilteredModuleIds, loader } = useModule(); + const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter(); + // derived values + const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined; - const { projectModuleIds, loader } = useModule(); - - const { storedValue: modulesView } = useLocalStorage("modules_view", "grid"); - - if (loader || !projectModuleIds) + if (loader || !filteredModuleIds) return ( <> - {modulesView === "list" && } - {modulesView === "grid" && } - {modulesView === "gantt_chart" && } + {displayFilters?.layout === "list" && } + {displayFilters?.layout === "board" && } + {displayFilters?.layout === "gantt" && } ); + if (filteredModuleIds.length === 0) + return ( +
+
+ No matching modules +
No matching modules
+

+ {searchQuery.trim() === "" + ? "Remove the filters to see all modules" + : "Remove the search criteria to see all modules"} +

+
+
+ ); + return ( <> - {projectModuleIds.length > 0 ? ( + {filteredModuleIds.length > 0 ? ( <> - {modulesView === "list" && ( + {displayFilters?.layout === "list" && (
- {projectModuleIds.map((moduleId) => ( + {filteredModuleIds.map((moduleId) => ( ))}
@@ -51,7 +73,7 @@ export const ModulesListView: React.FC = observer(() => {
)} - {modulesView === "grid" && ( + {displayFilters?.layout === "board" && (
{ : "lg:grid-cols-2 xl:grid-cols-3 3xl:grid-cols-4" } auto-rows-max transition-all vertical-scrollbar scrollbar-lg`} > - {projectModuleIds.map((moduleId) => ( + {filteredModuleIds.map((moduleId) => ( ))}
@@ -72,7 +94,7 @@ export const ModulesListView: React.FC = observer(() => {
)} - {modulesView === "gantt_chart" && } + {displayFilters?.layout === "gantt" && } ) : ( = (props) => { } = props; // store hooks const { captureEvent } = useEventTracker(); - + const { isMobile } = usePlatformOS(); const router = useRouter(); const { workspaceSlug } = router.query; // states @@ -369,7 +370,7 @@ export const NotificationCard: React.FC = (props) => { }, }, ].map((item) => ( - + ))} - + = (props) => } = props; // store hooks const { captureEvent } = useEventTracker(); + // hooks + const { isMobile } = usePlatformOS(); const notificationTabs: Array<{ label: string; @@ -84,7 +87,7 @@ export const NotificationHeader: React.FC = (props) =>
- + - + diff --git a/web/components/notifications/notification-popover.tsx b/web/components/notifications/notification-popover.tsx index 7d675214d..a8ad10285 100644 --- a/web/components/notifications/notification-popover.tsx +++ b/web/components/notifications/notification-popover.tsx @@ -11,6 +11,7 @@ import { getNumberCount } from "helpers/string.helper"; import { useApplication } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useUserNotification from "hooks/use-user-notifications"; +import { usePlatformOS } from "hooks/use-platform-os"; // components // images import emptyNotification from "public/empty-state/notification.svg"; @@ -23,6 +24,8 @@ export const NotificationPopover = observer(() => { const { theme: themeStore } = useApplication(); // refs const notificationPopoverRef = React.useRef(null); + // hooks + const { isMobile } = usePlatformOS(); const { notifications, @@ -67,7 +70,7 @@ export const NotificationPopover = observer(() => { /> <> - +
{project.assigned_issues > 0 && ( - +
void; + values: string[]; + editable: boolean | undefined; +}; + +export const AppliedAccessFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + + return ( + <> + {values.map((status) => { + const accessDetails = NETWORK_CHOICES.find((s) => `${s.key}` === status); + return ( +
+ {accessDetails?.label} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/components/project/applied-filters/date.tsx b/web/components/project/applied-filters/date.tsx new file mode 100644 index 000000000..aab0cf98a --- /dev/null +++ b/web/components/project/applied-filters/date.tsx @@ -0,0 +1,55 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +import { capitalizeFirstLetter } from "helpers/string.helper"; +// constants +import { DATE_BEFORE_FILTER_OPTIONS } from "constants/filters"; + +type Props = { + editable: boolean | undefined; + handleRemove: (val: string) => void; + values: string[]; +}; + +export const AppliedDateFilters: React.FC = observer((props) => { + const { editable, handleRemove, values } = props; + + const getDateLabel = (value: string): string => { + let dateLabel = ""; + + const dateDetails = DATE_BEFORE_FILTER_OPTIONS.find((d) => d.value === value); + + if (dateDetails) dateLabel = dateDetails.name; + else { + const dateParts = value.split(";"); + + if (dateParts.length === 2) { + const [date, time] = dateParts; + + dateLabel = `${capitalizeFirstLetter(time)} ${renderFormattedDate(date)}`; + } + } + + return dateLabel; + }; + + return ( + <> + {values.map((date) => ( +
+ {getDateLabel(date)} + {editable && ( + + )} +
+ ))} + + ); +}); diff --git a/web/components/project/applied-filters/index.ts b/web/components/project/applied-filters/index.ts new file mode 100644 index 000000000..818aa6134 --- /dev/null +++ b/web/components/project/applied-filters/index.ts @@ -0,0 +1,4 @@ +export * from "./access"; +export * from "./date"; +export * from "./members"; +export * from "./root"; diff --git a/web/components/project/applied-filters/members.tsx b/web/components/project/applied-filters/members.tsx new file mode 100644 index 000000000..88f18ee0c --- /dev/null +++ b/web/components/project/applied-filters/members.tsx @@ -0,0 +1,46 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// ui +import { Avatar } from "@plane/ui"; +// types +import { useMember } from "hooks/store"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; + editable: boolean | undefined; +}; + +export const AppliedMembersFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + // store hooks + const { + workspace: { getWorkspaceMemberDetails }, + } = useMember(); + + return ( + <> + {values.map((memberId) => { + const memberDetails = getWorkspaceMemberDetails(memberId)?.member; + + if (!memberDetails) return null; + + return ( +
+ + {memberDetails.display_name} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/components/project/applied-filters/root.tsx b/web/components/project/applied-filters/root.tsx new file mode 100644 index 000000000..6e1cbf6a7 --- /dev/null +++ b/web/components/project/applied-filters/root.tsx @@ -0,0 +1,113 @@ +import { X } from "lucide-react"; +// components +import { AppliedAccessFilters, AppliedDateFilters, AppliedMembersFilters } from "components/project"; +// ui +import { Tooltip } from "@plane/ui"; +// helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +// types +import { TProjectFilters } from "@plane/types"; + +type Props = { + appliedFilters: TProjectFilters; + handleClearAllFilters: () => void; + handleRemoveFilter: (key: keyof TProjectFilters, value: string | null) => void; + alwaysAllowEditing?: boolean; + filteredProjects: number; + totalProjects: number; +}; + +const MEMBERS_FILTERS = ["lead", "members"]; +const DATE_FILTERS = ["created_at"]; + +export const ProjectAppliedFiltersList: React.FC = (props) => { + const { + appliedFilters, + handleClearAllFilters, + handleRemoveFilter, + alwaysAllowEditing, + filteredProjects, + totalProjects, + } = props; + + if (!appliedFilters) return null; + if (Object.keys(appliedFilters).length === 0) return null; + + const isEditingAllowed = alwaysAllowEditing; + + return ( +
+
+ {Object.entries(appliedFilters).map(([key, value]) => { + const filterKey = key as keyof TProjectFilters; + + if (!value) return; + if (Array.isArray(value) && value.length === 0) return; + + return ( +
+
+ {replaceUnderscoreIfSnakeCase(filterKey)} + {filterKey === "access" && ( + handleRemoveFilter("access", val)} + values={value} + /> + )} + {DATE_FILTERS.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} + values={value} + /> + )} + {MEMBERS_FILTERS.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} + values={value} + /> + )} + {isEditingAllowed && ( + + )} +
+
+ ); + })} + {isEditingAllowed && ( + + )} +
+ + {filteredProjects} of{" "} + {totalProjects} projects match the applied filters. +

+ } + > + + {filteredProjects}/{totalProjects} + +
+
+ ); +}; diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index df63dfb73..3f23ed9a2 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -1,10 +1,14 @@ +import Image from "next/image"; import { observer } from "mobx-react-lite"; // hooks -import { useApplication, useEventTracker, useProject } from "hooks/store"; +import { useApplication, useEventTracker, useProject, useProjectFilter } from "hooks/store"; // components import { EmptyState } from "components/empty-state"; import { ProjectCard } from "components/project"; import { ProjectsLoader } from "components/ui"; +// assets +import AllFiltersImage from "public/empty-state/project/all-filters.svg"; +import NameFilterImage from "public/empty-state/project/name-filter.svg"; // constants import { EmptyStateType } from "constants/empty-state"; @@ -12,38 +16,49 @@ export const ProjectCardList = observer(() => { // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); + const { workspaceProjectIds, filteredProjectIds, getProjectById } = useProject(); + const { searchQuery } = useProjectFilter(); - const { workspaceProjectIds, searchedProjects, getProjectById } = useProject(); + if (!filteredProjectIds) return ; - if (!workspaceProjectIds) return ; + if (workspaceProjectIds?.length === 0) + return ( + { + setTrackElement("Project empty state"); + commandPaletteStore.toggleCreateProjectModal(true); + }} + /> + ); + if (filteredProjectIds.length === 0) + return ( +
+
+ No matching projects +
No matching projects
+

+ {searchQuery.trim() === "" + ? "Remove the filters to see all projects" + : "No projects detected with the matching\ncriteria. Create a new project instead"} +

+
+
+ ); return ( - <> - {workspaceProjectIds.length > 0 ? ( -
- {searchedProjects.length == 0 ? ( -
No matching projects
- ) : ( -
- {searchedProjects.map((projectId) => { - const projectDetails = getProjectById(projectId); - - if (!projectDetails) return; - - return ; - })} -
- )} -
- ) : ( - { - setTrackElement("Project empty state"); - commandPaletteStore.toggleCreateProjectModal(true); - }} - /> - )} - +
+
+ {filteredProjectIds.map((projectId) => { + const projectDetails = getProjectById(projectId); + if (!projectDetails) return; + return ; + })} +
+
); }); diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 08aec43fa..976e7c896 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -2,36 +2,43 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import Link from "next/link"; import { useRouter } from "next/router"; -import { LinkIcon, Lock, Pencil, Star } from "lucide-react"; +import { Check, LinkIcon, Lock, Pencil, Star } from "lucide-react"; // ui import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui"; // components import { DeleteProjectModal, JoinProjectModal, ProjectLogo } from "components/project"; // helpers -import { copyTextToClipboard } from "helpers/string.helper"; +import { copyUrlToClipboard } from "helpers/string.helper"; +import { renderFormattedDate } from "helpers/date-time.helper"; // hooks import { useProject } from "hooks/store"; // types import type { IProject } from "@plane/types"; -import { EUserProjectRoles } from "constants/project"; +// hooks +import { usePlatformOS } from "hooks/use-platform-os"; // constants +import { EUserProjectRoles } from "constants/project"; -export type ProjectCardProps = { +type Props = { project: IProject; }; -export const ProjectCard: React.FC = observer((props) => { +export const ProjectCard: React.FC = observer((props) => { const { project } = props; - // router - const router = useRouter(); - const { workspaceSlug } = router.query; // states const [deleteProjectModalOpen, setDeleteProjectModal] = useState(false); const [joinProjectModalOpen, setJoinProjectModal] = useState(false); + // router + const router = useRouter(); + const { workspaceSlug } = router.query; // store hooks const { addProjectToFavorites, removeProjectFromFavorites } = useProject(); - + // hooks + const { isMobile } = usePlatformOS(); project.member_role; + // derived values + const projectMembersIds = project.members?.map((member) => member.member_id); + // auth const isOwner = project.member_role === EUserProjectRoles.ADMIN; const isMember = project.member_role === EUserProjectRoles.MEMBER; @@ -53,7 +60,7 @@ export const ProjectCard: React.FC = observer((props) => { }; const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !project) return; + if (!workspaceSlug) return; const removeFromFavoritePromise = removeProjectFromFavorites(workspaceSlug.toString(), project.id); setPromiseToast(removeFromFavoritePromise, { @@ -69,23 +76,18 @@ export const ProjectCard: React.FC = observer((props) => { }); }; - const handleCopyText = () => { - const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${project.id}/issues`).then(() => { + const handleCopyText = () => + copyUrlToClipboard(`${workspaceSlug}/projects/${project.id}/issues`).then(() => setToast({ type: TOAST_TYPE.SUCCESS, title: "Link Copied!", message: "Project link copied to clipboard.", - }); - }); - }; - - const projectMembersIds = project.members?.map((member) => member.member_id); + }) + ); return ( <> - {/* Delete Project Modal */} + {/* Delete Project Modal */} = observer((props) => { {/* Join Project Modal */} {workspaceSlug && ( setJoinProjectModal(false)} /> )} - - {/* Card Information */} -
{ - if (project.is_member) router.push(`/${workspaceSlug?.toString()}/projects/${project.id}/issues`); - else setJoinProjectModal(true); + { + if (!project.is_member) { + e.preventDefault(); + e.stopPropagation(); + setJoinProjectModal(true); + } }} - className="flex cursor-pointer flex-col rounded border border-custom-border-200 bg-custom-background-100" + className="flex flex-col rounded border border-custom-border-200 bg-custom-background-100" >
@@ -121,12 +125,10 @@ export const ProjectCard: React.FC = observer((props) => { className="absolute left-0 top-0 h-full w-full rounded-t object-cover" /> -
+
-
- - - +
+
@@ -152,15 +154,10 @@ export const ProjectCard: React.FC = observer((props) => {
-

{project.description}

+

+ {project.description && project.description.trim() !== "" + ? project.description + : `Created on ${renderFormattedDate(project.created_at)}`} +

0 ? `${project.members.length} Members` : "No Member" @@ -197,19 +199,24 @@ export const ProjectCard: React.FC = observer((props) => { No Member Yet )} - {(isOwner || isMember) && ( - { - e.stopPropagation(); - }} - href={`/${workspaceSlug}/projects/${project.id}/settings`} - > - - - )} - - {!project.is_member ? ( + {project.is_member && + (isOwner || isMember ? ( + { + e.stopPropagation(); + }} + href={`/${workspaceSlug}/projects/${project.id}/settings`} + > + + + ) : ( + + + Joined + + ))} + {!project.is_member && (
- ) : null} + )}
-
+ ); }); diff --git a/web/components/project/create-project-form.tsx b/web/components/project/create-project-form.tsx index 509cf310c..694bd8185 100644 --- a/web/components/project/create-project-form.tsx +++ b/web/components/project/create-project-form.tsx @@ -27,6 +27,7 @@ import { cn } from "helpers/common.helper"; import { projectIdentifierSanitizer } from "helpers/project.helper"; // hooks import { useEventTracker, useProject } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; // types import { IProject } from "@plane/types"; @@ -71,7 +72,7 @@ export const CreateProjectForm: FC = observer((props) => { defaultValues, reValidateMode: "onChange", }); - + const { isMobile } = usePlatformOS(); const handleAddToFavorites = (projectId: string) => { if (!workspaceSlug) return; @@ -283,6 +284,7 @@ export const CreateProjectForm: FC = observer((props) => { )} /> void; + searchQuery: string; +}; + +export const FilterAccess: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + // states + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + const filteredOptions = NETWORK_CHOICES.filter((a) => a.label.includes(searchQuery.toLowerCase())); + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + filteredOptions.map((access) => ( + handleUpdate(`${access.key}`)} + icon={} + title={access.label} + /> + )) + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/project/dropdowns/filters/created-at.tsx b/web/components/project/dropdowns/filters/created-at.tsx new file mode 100644 index 000000000..3867ab148 --- /dev/null +++ b/web/components/project/dropdowns/filters/created-at.tsx @@ -0,0 +1,64 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; +// constants +import { DATE_BEFORE_FILTER_OPTIONS } from "constants/filters"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string | string[]) => void; + searchQuery: string; +}; + +export const FilterCreatedDate: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = DATE_BEFORE_FILTER_OPTIONS.filter((d) => + d.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return ( + <> + {isDateFilterModalOpen && ( + setIsDateFilterModalOpen(false)} + isOpen={isDateFilterModalOpen} + onSelect={(val) => handleUpdate(val)} + title="Created date" + /> + )} + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + <> + {filteredOptions.map((option) => ( + handleUpdate(option.value)} + title={option.name} + multiple + /> + ))} + setIsDateFilterModalOpen(true)} title="Custom" multiple /> + + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/project/dropdowns/filters/index.ts b/web/components/project/dropdowns/filters/index.ts new file mode 100644 index 000000000..c04162e57 --- /dev/null +++ b/web/components/project/dropdowns/filters/index.ts @@ -0,0 +1,5 @@ +export * from "./access"; +export * from "./created-at"; +export * from "./lead"; +export * from "./members"; +export * from "./root"; diff --git a/web/components/project/dropdowns/filters/lead.tsx b/web/components/project/dropdowns/filters/lead.tsx new file mode 100644 index 000000000..02c257b9b --- /dev/null +++ b/web/components/project/dropdowns/filters/lead.tsx @@ -0,0 +1,97 @@ +import { useMemo, useState } from "react"; +import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; +// hooks +import { useMember } from "hooks/store"; +// components +import { FilterHeader, FilterOption } from "components/issues"; +// ui +import { Avatar, Loader } from "@plane/ui"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + memberIds: string[] | undefined; + searchQuery: string; +}; + +export const FilterLead: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); + + const handleViewToggle = () => { + if (!sortedOptions) return; + + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {sortedOptions ? ( + sortedOptions.length > 0 ? ( + <> + {sortedOptions.slice(0, itemsToRender).map((memberId) => { + const member = getUserDetails(memberId); + + if (!member) return null; + return ( + handleUpdate(member.id)} + icon={} + title={member.display_name} + /> + ); + })} + {sortedOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}); diff --git a/web/components/project/dropdowns/filters/members.tsx b/web/components/project/dropdowns/filters/members.tsx new file mode 100644 index 000000000..0d2737227 --- /dev/null +++ b/web/components/project/dropdowns/filters/members.tsx @@ -0,0 +1,97 @@ +import { useMemo, useState } from "react"; +import { observer } from "mobx-react-lite"; +import sortBy from "lodash/sortBy"; +// hooks +import { useMember } from "hooks/store"; +// components +import { FilterHeader, FilterOption } from "components/issues"; +// ui +import { Avatar, Loader } from "@plane/ui"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + memberIds: string[] | undefined; + searchQuery: string; +}; + +export const FilterMembers: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); + + const handleViewToggle = () => { + if (!sortedOptions) return; + + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); + }; + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {sortedOptions ? ( + sortedOptions.length > 0 ? ( + <> + {sortedOptions.slice(0, itemsToRender).map((memberId) => { + const member = getUserDetails(memberId); + + if (!member) return null; + return ( + handleUpdate(member.id)} + icon={} + title={member.display_name} + /> + ); + })} + {sortedOptions.length > 5 && ( + + )} + + ) : ( +

No matches found

+ ) + ) : ( + + + + + + )} +
+ )} + + ); +}); diff --git a/web/components/project/dropdowns/filters/root.tsx b/web/components/project/dropdowns/filters/root.tsx new file mode 100644 index 000000000..e79fc8418 --- /dev/null +++ b/web/components/project/dropdowns/filters/root.tsx @@ -0,0 +1,96 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Search, X } from "lucide-react"; +// components +import { FilterAccess, FilterCreatedDate, FilterLead, FilterMembers } from "components/project"; +// types +import { TProjectDisplayFilters, TProjectFilters } from "@plane/types"; +import { FilterOption } from "components/issues"; + +type Props = { + displayFilters: TProjectDisplayFilters; + filters: TProjectFilters; + handleFiltersUpdate: (key: keyof TProjectFilters, value: string | string[]) => void; + handleDisplayFiltersUpdate: (updatedDisplayProperties: Partial) => void; + memberIds?: string[] | undefined; +}; + +export const ProjectFiltersSelection: React.FC = observer((props) => { + const { displayFilters, filters, handleFiltersUpdate, handleDisplayFiltersUpdate, memberIds } = props; + // states + const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); + + return ( +
+
+
+ + setFiltersSearchQuery(e.target.value)} + autoFocus + /> + {filtersSearchQuery !== "" && ( + + )} +
+
+
+
+ + handleDisplayFiltersUpdate({ + my_projects: !displayFilters.my_projects, + }) + } + title="My projects" + /> +
+ + {/* access */} +
+ handleFiltersUpdate("access", val)} + searchQuery={filtersSearchQuery} + /> +
+ + {/* lead */} +
+ handleFiltersUpdate("lead", val)} + searchQuery={filtersSearchQuery} + memberIds={memberIds} + /> +
+ + {/* members */} +
+ handleFiltersUpdate("members", val)} + searchQuery={filtersSearchQuery} + memberIds={memberIds} + /> +
+ + {/* created date */} +
+ handleFiltersUpdate("created_at", val)} + searchQuery={filtersSearchQuery} + /> +
+
+
+ ); +}); diff --git a/web/components/project/dropdowns/index.ts b/web/components/project/dropdowns/index.ts new file mode 100644 index 000000000..f6c42552f --- /dev/null +++ b/web/components/project/dropdowns/index.ts @@ -0,0 +1,2 @@ +export * from "./filters"; +export * from "./order-by"; diff --git a/web/components/project/dropdowns/order-by.tsx b/web/components/project/dropdowns/order-by.tsx new file mode 100644 index 000000000..ceb61997e --- /dev/null +++ b/web/components/project/dropdowns/order-by.tsx @@ -0,0 +1,74 @@ +import { ArrowDownWideNarrow, Check, ChevronDown } from "lucide-react"; +// ui +import { CustomMenu, getButtonStyling } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TProjectOrderByOptions } from "@plane/types"; +// constants +import { PROJECT_ORDER_BY_OPTIONS } from "constants/project"; + +type Props = { + onChange: (value: TProjectOrderByOptions) => void; + value: TProjectOrderByOptions | undefined; +}; + +const DISABLED_ORDERING_OPTIONS = ["sort_order"]; + +export const ProjectOrderByDropdown: React.FC = (props) => { + const { onChange, value } = props; + + const orderByDetails = PROJECT_ORDER_BY_OPTIONS.find((option) => value?.includes(option.key)); + + const isDescending = value?.[0] === "-"; + const isOrderingDisabled = !!value && DISABLED_ORDERING_OPTIONS.includes(value); + + return ( + + + {orderByDetails?.label} + +
+ } + placement="bottom-end" + closeOnSelect + > + {PROJECT_ORDER_BY_OPTIONS.map((option) => ( + { + if (isDescending) onChange(`-${option.key}` as TProjectOrderByOptions); + else onChange(option.key); + }} + > + {option.label} + {value?.includes(option.key) && } + + ))} +
+ { + if (isDescending) onChange(value.slice(1) as TProjectOrderByOptions); + }} + disabled={isOrderingDisabled} + > + Ascending + {!isOrderingDisabled && !isDescending && } + + { + if (!isDescending) onChange(`-${value}` as TProjectOrderByOptions); + }} + disabled={isOrderingDisabled} + > + Descending + {!isOrderingDisabled && isDescending && } + + + ); +}; diff --git a/web/components/project/index.ts b/web/components/project/index.ts index 6dedb63d4..db51bc284 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -1,3 +1,5 @@ +export * from "./applied-filters"; +export * from "./dropdowns"; export * from "./publish-project"; export * from "./settings"; export * from "./card-list"; diff --git a/web/components/project/member-list-item.tsx b/web/components/project/member-list-item.tsx index b2c847e58..72312cbee 100644 --- a/web/components/project/member-list-item.tsx +++ b/web/components/project/member-list-item.tsx @@ -16,6 +16,7 @@ import { ROLE } from "constants/workspace"; import { useEventTracker, useMember, useProject, useUser } from "hooks/store"; // helpers import { getUserRole } from "helpers/user.helper"; +import { usePlatformOS } from "hooks/use-platform-os"; type Props = { userId: string; @@ -38,7 +39,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { project: { removeMemberFromProject, getProjectMemberDetails, updateMember }, } = useMember(); const { captureEvent } = useEventTracker(); - + const { isMobile } = usePlatformOS(); // derived values const isAdmin = currentProjectRole === EUserProjectRoles.ADMIN; const userDetails = getProjectMemberDetails(userId); @@ -191,7 +192,7 @@ export const ProjectMemberListItem: React.FC = observer((props) => { })} {(isAdmin || userDetails.member?.id === currentUser?.id) && ( - + )} - + = observer((props) => { = observer((props) => { // store hooks const { captureProjectStateEvent, setTrackElement } = useEventTracker(); const { createState, updateState } = useProjectState(); + const { isMobile } = usePlatformOS(); // form info const { handleSubmit, @@ -240,7 +242,7 @@ export const CreateUpdateStateInline: React.FC = observer((props) => { name="group" control={control} render={({ field: { value, onChange } }) => ( - +
= observer((props) => { // store hooks const { setTrackElement } = useEventTracker(); const { markStateAsDefault, moveStatePosition } = useProjectState(); + const { isMobile } = usePlatformOS(); // derived values const groupStates = statesList.filter((s) => s.group === state.group); const groupLength = groupStates.length; @@ -109,11 +111,11 @@ export const StatesListItem: React.FC = observer((props) => { disabled={state.default || groupLength === 1} > {state.default ? ( - + ) : groupLength === 1 ? ( - + ) : ( diff --git a/web/components/ui/labels-list.tsx b/web/components/ui/labels-list.tsx index fddea8478..8ebc19158 100644 --- a/web/components/ui/labels-list.tsx +++ b/web/components/ui/labels-list.tsx @@ -3,6 +3,8 @@ import { FC } from "react"; import { Tooltip } from "@plane/ui"; // types import { IIssueLabel } from "@plane/types"; +// hooks +import { usePlatformOS } from "hooks/use-platform-os"; type IssueLabelsListProps = { labels?: (IIssueLabel | undefined)[]; @@ -12,11 +14,12 @@ type IssueLabelsListProps = { export const IssueLabelsList: FC = (props) => { const { labels } = props; + const { isMobile } = usePlatformOS(); return ( <> {labels && ( <> - l?.name).join(", ")}> + l?.name).join(", ")} isMobile={isMobile}>
{`${labels.length} Labels`} diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx index dfef477c8..641c5a48f 100644 --- a/web/components/views/view-list-item.tsx +++ b/web/components/views/view-list-item.tsx @@ -74,6 +74,7 @@ export const ProjectViewListItem: React.FC = observer((props) => { }); }; + // @ts-expect-error key types are not compatible const totalFilters = calculateTotalFilters(view.filters ?? {}); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; diff --git a/web/components/web-hooks/form/secret-key.tsx b/web/components/web-hooks/form/secret-key.tsx index 92edb7ec6..d6bb7e9c2 100644 --- a/web/components/web-hooks/form/secret-key.tsx +++ b/web/components/web-hooks/form/secret-key.tsx @@ -10,6 +10,7 @@ import { csvDownload } from "helpers/download.helper"; import { copyTextToClipboard } from "helpers/string.helper"; // hooks import { useEventTracker, useWebhook, useWorkspace } from "hooks/store"; +import { usePlatformOS } from "hooks/use-platform-os"; // types import { IWebhook } from "@plane/types"; // utils @@ -34,6 +35,7 @@ export const WebhookSecretKey: FC = observer((props) => { const { currentWebhook, regenerateSecretKey, webhookSecretKey } = useWebhook(); const { captureEvent } = useEventTracker(); + const { isMobile } = usePlatformOS(); const handleCopySecretKey = () => { if (!webhookSecretKey) return; @@ -114,7 +116,7 @@ export const WebhookSecretKey: FC = observer((props) => { {webhookSecretKey && (
{SECRET_KEY_OPTIONS.map((option) => ( - + diff --git a/web/components/workspace/help-section.tsx b/web/components/workspace/help-section.tsx index 210bbbd3a..2e113414d 100644 --- a/web/components/workspace/help-section.tsx +++ b/web/components/workspace/help-section.tsx @@ -10,6 +10,7 @@ import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // hooks import { useApplication } from "hooks/store"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; +import { usePlatformOS } from "hooks/use-platform-os"; // assets import packageJson from "package.json"; @@ -41,6 +42,7 @@ export const WorkspaceHelpSection: React.FC = observe theme: { sidebarCollapsed, toggleSidebar }, commandPalette: { toggleShortcutModal }, } = useApplication(); + const { isMobile } = usePlatformOS(); // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // refs @@ -69,7 +71,7 @@ export const WorkspaceHelpSection: React.FC = observe
)}
- + - + - +