snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React from "react";
// components
import { PageHead } from "@/components/core/page-title";
import { ProfileIssuesPage } from "@/components/profile/profile-issues";
import type { Route } from "./+types/page";
const ProfilePageHeader = {
assigned: "Profile - Assigned",
created: "Profile - Created",
subscribed: "Profile - Subscribed",
};
function isValidProfileViewId(viewId: string): viewId is keyof typeof ProfilePageHeader {
return viewId in ProfilePageHeader;
}
function ProfileIssuesTypePage({ params }: Route.ComponentProps) {
const { profileViewId } = params;
if (!isValidProfileViewId(profileViewId)) return null;
const header = ProfilePageHeader[profileViewId];
return (
<>
<PageHead title={header} />
<ProfileIssuesPage type={profileViewId} />
</>
);
}
export default ProfileIssuesTypePage;

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
// components
import { PageHead } from "@/components/core/page-title";
import { DownloadActivityButton } from "@/components/profile/activity/download-button";
import { WorkspaceActivityListPage } from "@/components/profile/activity/workspace-activity-list";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
const PER_PAGE = 100;
function ProfileActivityPage() {
// states
const [pageCount, setPageCount] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [resultsCount, setResultsCount] = useState(0);
// router
const { allowPermissions } = useUserPermissions();
//hooks
const { t } = useTranslation();
const updateTotalPages = (count: number) => setTotalPages(count);
const updateResultsCount = (count: number) => setResultsCount(count);
const handleLoadMore = () => setPageCount((prev) => prev + 1);
const activityPages: React.ReactNode[] = [];
for (let i = 0; i < pageCount; i++)
activityPages.push(
<WorkspaceActivityListPage
key={i}
cursor={`${PER_PAGE}:${i}:0`}
perPage={PER_PAGE}
updateResultsCount={updateResultsCount}
updateTotalPages={updateTotalPages}
/>
);
const canDownloadActivity = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
return (
<>
<PageHead title="Profile - Activity" />
<div className="flex h-full w-full flex-col overflow-hidden py-5">
<div className="flex items-center justify-between gap-2 px-5 md:px-9">
<h3 className="text-16 font-medium">{t("profile.stats.recent_activity.title")}</h3>
{canDownloadActivity && <DownloadActivityButton />}
</div>
<div className="vertical-scrollbar scrollbar-md flex h-full flex-col overflow-y-auto px-5 md:px-9">
{activityPages}
{pageCount < totalPages && resultsCount !== 0 && (
<div className="flex w-full items-center justify-center text-11">
<Button variant="secondary" onClick={handleLoadMore}>
{t("common.load_more")}
</Button>
</div>
)}
</div>
</div>
</>
);
}
export default observer(ProfileActivityPage);

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
// ui
import { observer } from "mobx-react";
import { useParams, useRouter } from "next/navigation";
import { ChevronDownOutline, RightSidePaneOutline, YourWorkOutline } from "@makeplane/propel/icons";
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { IUserProfileProjectSegregation } from "@plane/types";
import { Breadcrumbs, Header, CustomMenu } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import { ProfileIssuesFilter } from "@/components/profile/profile-issues-filter";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useUser, useUserPermissions } from "@/hooks/store/user";
import { Button } from "@plane/propel/button";
type TUserProfileHeader = {
userProjectsData: IUserProfileProjectSegregation | undefined;
type?: string | undefined;
showProfileIssuesFilter?: boolean;
};
export const UserProfileHeader = observer(function UserProfileHeader(props: TUserProfileHeader) {
const { userProjectsData, type = undefined, showProfileIssuesFilter } = props;
// router
const { workspaceSlug, userId } = useParams();
const router = useRouter();
// store hooks
const { toggleProfileSidebar, profileSidebarCollapsed } = useAppTheme();
const { data: currentUser } = useUser();
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { t } = useTranslation();
// derived values
const isAuthorized = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
if (!workspaceUserInfo) return null;
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
const userName = `${userProjectsData?.user_data?.first_name} ${userProjectsData?.user_data?.last_name}`;
const isCurrentUser = currentUser?.id === userId;
const breadcrumbLabel = isCurrentUser ? t("profile.page_label") : `${userName} ${t("profile.work")}`;
return (
<Header>
<Header.LeftItem>
<Breadcrumbs>
<Breadcrumbs.Item
component={
<BreadcrumbLink
label={breadcrumbLabel}
disableTooltip
icon={<YourWorkOutline className="h-4 w-4 text-tertiary" />}
/>
}
/>
</Breadcrumbs>
</Header.LeftItem>
<Header.RightItem>
<div className="hidden md:flex md:items-center">{showProfileIssuesFilter && <ProfileIssuesFilter />}</div>
<div className="flex gap-4 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-13 text-secondary"
placement="bottom-start"
customButton={
<div className="flex items-center gap-2 rounded-md border border-subtle px-2 py-1.5">
<span className="flex flex-grow justify-center text-13 text-secondary">{type}</span>
<ChevronDownOutline className="h-4 w-4 text-placeholder" />
</div>
}
customButtonClassName="flex flex-grow justify-center text-secondary text-13"
closeOnSelect
>
<></>
{tabsList.map((tab) => (
<CustomMenu.MenuItem
className="flex items-center gap-2"
key={tab.route}
onClick={() => router.push(`/${workspaceSlug}/profile/${userId}/${tab.route}`)}
>
<span className="w-full text-tertiary">{t(tab.i18n_label)}</span>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="shrink-0 md:hidden">
<Button
variant="ghost"
size="lg"
onClick={() => {
toggleProfileSidebar();
}}
appendIcon={
<RightSidePaneOutline className={!profileSidebarCollapsed ? "text-accent-primary" : "text-secondary"} />
}
></Button>
</div>
</div>
</Header.RightItem>
</Header>
);
});

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import { usePathname } from "next/navigation";
import { Outlet } from "react-router";
import useSWR from "swr";
// components
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AppHeader } from "@/components/core/app-header";
import { ContentWrapper } from "@/components/core/content-wrapper";
import { ProfileSidebar } from "@/components/profile/sidebar";
// constants
import { USER_PROFILE_PROJECT_SEGREGATION } from "@plane/constants";
// hooks
import { useUserPermissions } from "@/hooks/store/user";
import useSize from "@/hooks/use-window-size";
// local components
import { UserService } from "@/services/user.service";
import type { Route } from "./+types/layout";
import { UserProfileHeader } from "./header";
import { ProfileIssuesMobileHeader } from "./mobile-header";
import { ProfileNavbar } from "./navbar";
const userService = new UserService();
function UseProfileLayout({ params }: Route.ComponentProps) {
// router
const { workspaceSlug, userId } = params;
const pathname = usePathname();
// store hooks
const { allowPermissions } = useUserPermissions();
const { t } = useTranslation();
// derived values
const isAuthorized = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.WORKSPACE
);
const windowSize = useSize();
const isSmallerScreen = windowSize[0] >= 768;
const { data: userProjectsData } = useSWR(USER_PROFILE_PROJECT_SEGREGATION(workspaceSlug, userId), () =>
userService.getUserProfileProjectsSegregation(workspaceSlug, userId)
);
// derived values
const isAuthorizedPath =
pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed");
const isIssuesTab = pathname.includes("assigned") || pathname.includes("created") || pathname.includes("subscribed");
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
const currentTab = tabsList.find((tab) => pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}`);
return (
<>
{/* Passing the type prop from the current route value as we need the header as top most component.
TODO: We are depending on the route path to handle the mobile header type. If the path changes, this logic will break. */}
<div className="flex h-full w-full flex-col overflow-hidden md:flex-row">
<div className="flex h-full w-full flex-col overflow-hidden">
<AppHeader
header={
<UserProfileHeader
type={currentTab?.i18n_label}
userProjectsData={userProjectsData}
showProfileIssuesFilter={isIssuesTab}
/>
}
mobileHeader={isIssuesTab && <ProfileIssuesMobileHeader />}
/>
<ContentWrapper>
<div className="flex h-full w-full flex-row md:flex-col md:overflow-hidden">
<div className="flex w-full flex-col md:h-full md:overflow-hidden">
<ProfileNavbar isAuthorized={!!isAuthorized} />
{isAuthorized || !isAuthorizedPath ? (
<div className={`h-full w-full overflow-hidden`}>
<Outlet />
</div>
) : (
<div className="grid h-full w-full place-items-center text-secondary">
{t("you_do_not_have_the_permission_to_access_this_page")}
</div>
)}
</div>
{!isSmallerScreen && <ProfileSidebar userProjectsData={userProjectsData} />}
</div>
</ContentWrapper>
</div>
{isSmallerScreen && <ProfileSidebar userProjectsData={userProjectsData} />}
</div>
</>
);
}
export default observer(UseProfileLayout);

View File

@@ -0,0 +1,141 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane constants
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
// icons
import { ChevronDownOutline } from "@makeplane/propel/icons";
// types
import type {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
TIssueLayouts,
EIssueLayoutTypes,
} from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
// ui
import { CustomMenu } from "@plane/ui";
// components
import { DisplayFiltersSelection, FiltersDropdown } from "@/components/issues/issue-layouts/filters";
import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon";
// hooks
import { useIssues } from "@/hooks/store/use-issues";
export const ProfileIssuesMobileHeader = observer(function ProfileIssuesMobileHeader() {
// plane i18n
const { t } = useTranslation();
// router
const { workspaceSlug, userId } = useParams();
// store hook
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.PROFILE);
// derived values
const activeLayout = issueFilters?.displayFilters?.layout;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !userId) return;
updateFilters(
workspaceSlug.toString(),
undefined,
EIssueFilterType.DISPLAY_FILTERS,
{ layout: layout as EIssueLayoutTypes | undefined },
userId.toString()
);
},
[workspaceSlug, updateFilters, userId]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !userId) return;
updateFilters(
workspaceSlug.toString(),
undefined,
EIssueFilterType.DISPLAY_FILTERS,
updatedDisplayFilter,
userId.toString()
);
},
[workspaceSlug, updateFilters, userId]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !userId) return;
updateFilters(
workspaceSlug.toString(),
undefined,
EIssueFilterType.DISPLAY_PROPERTIES,
property,
userId.toString()
);
},
[workspaceSlug, updateFilters, userId]
);
return (
<div className="flex justify-evenly border-b border-subtle py-2 md:hidden">
<CustomMenu
maxHeight={"md"}
className="flex flex-grow justify-center text-13 text-secondary"
placement="bottom-start"
customButton={
<div className="flex-center flex text-13 text-secondary">
{t("common.layout")}
<ChevronDownOutline className="my-auto ml-2 h-4 w-4 text-secondary" />
</div>
}
customButtonClassName="flex flex-center text-secondary text-13"
closeOnSelect
>
{ISSUE_LAYOUTS.map((layout, index) => {
if (layout.key === "spreadsheet" || layout.key === "gantt_chart" || layout.key === "calendar") return;
return (
<CustomMenu.MenuItem
key={index}
onClick={() => {
handleLayoutChange(ISSUE_LAYOUTS[index].key);
}}
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-tertiary">{t(layout.i18n_title)}</div>
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
<div className="flex flex-grow items-center justify-center border-l border-subtle text-13 text-secondary">
<FiltersDropdown
title={t("common.display")}
placement="bottom-end"
menuButton={
<div className="flex-center flex text-13 text-secondary">
{t("common.display")}
<ChevronDownOutline className="ml-2 h-4 w-4 text-secondary" />
</div>
}
>
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.profile_issues.layoutOptions[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
/>
</FiltersDropdown>
</div>
</div>
);
});

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { PROFILE_VIEWER_TAB, PROFILE_ADMINS_TAB } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Header, EHeaderVariant } from "@plane/ui";
import { cn } from "@plane/utils";
type Props = {
isAuthorized: boolean;
};
export function ProfileNavbar(props: Props) {
const { isAuthorized } = props;
const { t } = useTranslation();
const { workspaceSlug, userId } = useParams();
const pathname = usePathname();
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
return (
<Header variant={EHeaderVariant.SECONDARY} showOnMobile={false}>
<div className="flex items-center overflow-x-scroll">
{tabsList.map((tab) => (
<Link key={tab.route} href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}>
<span
className={cn(
`flex border-b-2 p-4 text-13 font-medium whitespace-nowrap text-tertiary outline-none hover:text-primary ${
pathname === `/${workspaceSlug}/profile/${userId}${tab.selected}`
? "border-accent-strong text-accent-primary hover:text-accent-primary"
: "border-transparent"
}`
)}
>
{t(tab.i18n_label)}
</span>
</Link>
))}
</div>
</Header>
);
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import useSWR from "swr";
// plane imports
import { GROUP_CHOICES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import type { IUserStateDistribution, TStateGroups } from "@plane/types";
import { ContentWrapper } from "@plane/ui";
// components
import { PageHead } from "@/components/core/page-title";
import { ProfileActivity } from "@/components/profile/overview/activity";
import { ProfilePriorityDistribution } from "@/components/profile/overview/priority-distribution";
import { ProfileStateDistribution } from "@/components/profile/overview/state-distribution";
import { ProfileStats } from "@/components/profile/overview/stats";
import { ProfileWorkload } from "@/components/profile/overview/workload";
// constants
import { USER_PROFILE_DATA } from "@plane/constants";
// services
import { UserService } from "@/services/user.service";
import type { Route } from "./+types/page";
const userService = new UserService();
export default function ProfileOverviewPage({ params }: Route.ComponentProps) {
const { workspaceSlug, userId } = params;
const { t } = useTranslation();
const { data: userProfile } = useSWR(USER_PROFILE_DATA(workspaceSlug, userId), () =>
userService.getUserProfileData(workspaceSlug, userId)
);
const stateDistribution: IUserStateDistribution[] = Object.keys(GROUP_CHOICES).map((key) => {
const group = userProfile?.state_distribution.find((g) => g.state_group === key);
if (group) return group;
else return { state_group: key as TStateGroups, state_count: 0 };
});
return (
<>
<PageHead title={t("profile.page_label")} />
<ContentWrapper className="space-y-7">
<ProfileStats userProfile={userProfile} />
<ProfileWorkload stateDistribution={stateDistribution} />
<div className="grid grid-cols-1 items-stretch gap-5 xl:grid-cols-2">
<ProfilePriorityDistribution userProfile={userProfile} />
<ProfileStateDistribution stateDistribution={stateDistribution} userProfile={userProfile} />
</div>
<ProfileActivity />
</ContentWrapper>
</>
);
}