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,83 @@
/**
* 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";
import { useParams } from "react-router";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
// components
import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item";
// hooks
import { useProject } from "@/hooks/store/use-project";
// local imports
import { ArchiveRestoreProjectModal } from "../archive-restore-modal";
import { DeleteProjectModal } from "../delete-project-modal";
type Props = {
projectId: string;
};
export const GeneralProjectSettingsControlSection = observer(function GeneralProjectSettingsControlSection(
props: Props
) {
const { projectId } = props;
// states
const [selectProject, setSelectedProject] = useState<string | null>(null);
const [archiveProject, setArchiveProject] = useState<boolean>(false);
// params
const { workspaceSlug } = useParams();
// store hooks
const { currentProjectDetails } = useProject();
// translation
const { t } = useTranslation();
if (!currentProjectDetails) return null;
return (
<div className="mt-10">
{workspaceSlug && (
<ArchiveRestoreProjectModal
workspaceSlug={workspaceSlug}
projectId={projectId}
isOpen={archiveProject}
onClose={() => setArchiveProject(false)}
archive
/>
)}
<DeleteProjectModal
project={currentProjectDetails}
isOpen={Boolean(selectProject)}
onClose={() => setSelectedProject(null)}
/>
<div className="rounded-lg border border-subtle bg-layer-2">
{/* Project Selector */}
<SettingsBoxedControlItem
className="rounded-b-none border-0 border-b"
title={t("archive")}
description="Archiving a project will unlist your project from your side navigation although you will still be able to access it from your projects page. You can restore the project or delete it whenever you want."
control={
<Button variant="secondary" onClick={() => setArchiveProject(true)}>
{t("archive")}
</Button>
}
/>
{/* Format Selector */}
<SettingsBoxedControlItem
className="rounded-t-none border-0"
title={t("delete")}
description="When deleting a project, all of the data and resources within that project will be permanently removed and cannot be recovered."
control={
<Button variant="error-outline" onClick={() => setSelectedProject(currentProjectDetails.id ?? null)}>
{t("delete")}
</Button>
}
/>
</div>
</div>
);
});

View File

@@ -0,0 +1,150 @@
/**
* 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";
// plane imports
import { useTranslation } from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast";
import { Tooltip } from "@makeplane/propel/components/tooltip";
import type { IProject } from "@plane/types";
import { CyclesOutline, IntakeOutline, ModuleOutline, PagesOutline, ViewsOutline } from "@makeplane/propel/icons";
// components
import { SettingsBoxedControlItem } from "@/components/settings/boxed-control-item";
import { SettingsHeading } from "@/components/settings/heading";
// hooks
import { useProject } from "@/hooks/store/use-project";
// plane web imports
import { UpgradeBadge } from "@/components/workspace/upgrade-badge";
// local imports
import { ProjectFeatureToggle } from "./helper";
type Props = {
workspaceSlug: string;
projectId: string;
isAdmin: boolean;
};
const PROJECT_FEATURES_LIST = {
cycles: {
key: "cycles",
property: "cycle_view",
title: "Cycles",
description: "Timebox work as you see fit per project and change frequency from one period to the next.",
icon: <CyclesOutline className="h-5 w-5 flex-shrink-0 rotate-180 text-tertiary" />,
isPro: false,
isEnabled: true,
},
modules: {
key: "modules",
property: "module_view",
title: "Modules",
description: "Group work into sub-project-like set-ups with their own leads and assignees.",
icon: <ModuleOutline width={20} height={20} className="flex-shrink-0 text-tertiary" />,
isPro: false,
isEnabled: true,
},
views: {
key: "views",
property: "issue_views_view",
title: "Views",
description: "Save sorts, filters, and display options for later or share them.",
icon: <ViewsOutline className="h-5 w-5 flex-shrink-0 text-tertiary" />,
isPro: false,
isEnabled: true,
},
pages: {
key: "pages",
property: "page_view",
title: "Pages",
description: "Write anything like you write anything.",
icon: <PagesOutline className="h-5 w-5 flex-shrink-0 text-tertiary" />,
isPro: false,
isEnabled: true,
},
inbox: {
key: "intake",
property: "inbox_view",
title: "Intake",
description: "Consider and discuss work items before you add them to your project.",
icon: <IntakeOutline className="h-5 w-5 flex-shrink-0 text-tertiary" />,
isPro: false,
isEnabled: true,
},
};
export const ProjectFeaturesList = observer(function ProjectFeaturesList(props: Props) {
const { workspaceSlug, projectId, isAdmin } = props;
// store hooks
const { t } = useTranslation();
const { getProjectById, updateProject } = useProject();
// derived values
const currentProjectDetails = getProjectById(projectId);
const handleSubmit = (_featureKey: string, featureProperty: string) => {
if (!workspaceSlug || !projectId || !currentProjectDetails) return;
// making the request to update the project feature
const settingsPayload = {
[featureProperty]: !currentProjectDetails?.[featureProperty as keyof IProject],
};
const updateProjectPromise = updateProject(workspaceSlug, projectId, settingsPayload);
setPromiseToast(updateProjectPromise, {
loading: "Updating project feature...",
success: {
title: "Success!",
message: () => "Project feature updated successfully.",
},
error: {
title: "Error!",
message: () => "Something went wrong while updating project feature. Please try again.",
},
});
void updateProjectPromise.then(() => {
return undefined;
});
};
return (
<>
<div>
<SettingsHeading title={t("projects_and_issues")} description={t("projects_and_issues_description")} />
<div className="mt-6 flex flex-col gap-y-4">
{Object.entries(PROJECT_FEATURES_LIST).map(([featureItemKey, featureItem]) => (
<div key={featureItemKey}>
<SettingsBoxedControlItem
title={
<span className="flex items-center gap-2">
{t(featureItem.key)}
{featureItem.isPro && (
<Tooltip label="Pro feature">
<UpgradeBadge className="rounded-sm" />
</Tooltip>
)}
</span>
}
description={t(`${featureItem.key}_description`)}
control={
<ProjectFeatureToggle
workspaceSlug={workspaceSlug}
projectId={projectId}
featureItem={featureItem}
value={Boolean(currentProjectDetails?.[featureItem.property as keyof IProject])}
handleSubmit={handleSubmit}
disabled={!isAdmin}
/>
}
/>
{/* {currentProjectDetails?.[featureItem.property as keyof IProject] && (
<div className="pl-14">{featureItem.renderChildren?.(currentProjectDetails, workspaceSlug)}</div>
)} */}
</div>
))}
</div>
</div>
</>
);
});

View File

@@ -0,0 +1,46 @@
/**
* 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 { ChevronRightOutline } from "@makeplane/propel/icons";
import { EPillVariant, Pill, EPillSize } from "@plane/propel/pill";
import { Switch } from "@makeplane/propel/components/switch";
import { joinUrlPath } from "@plane/utils";
type Props = {
workspaceSlug: string;
projectId: string;
featureItem: any;
value: boolean;
handleSubmit: (featureKey: string, featureProperty: string) => void;
disabled?: boolean;
};
export function ProjectFeatureToggle(props: Props) {
const { workspaceSlug, projectId, featureItem, value, handleSubmit, disabled } = props;
return featureItem?.href ? (
<Link href={joinUrlPath(workspaceSlug, "settings", "projects", projectId, "features", featureItem?.href)}>
<div className="flex items-center gap-2">
<Pill
variant={value ? EPillVariant.PRIMARY : EPillVariant.DEFAULT}
size={EPillSize.SM}
className="rounded-lg border-none"
>
{value ? "Enabled" : "Disabled"}
</Pill>
<ChevronRightOutline className="h-4 w-4 text-tertiary" />
</div>
</Link>
) : (
<Switch
size="sm"
checked={value}
onCheckedChange={() => handleSubmit(featureItem?.key, featureItem?.property)}
disabled={disabled}
aria-label="Toggle project feature"
/>
);
}

View File

@@ -0,0 +1,194 @@
/**
* 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 Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { CircleMinus } from "lucide-react";
import { Disclosure } from "@headlessui/react";
// plane imports
import { ROLE, EUserPermissions } from "@plane/constants";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { EUserProjectRoles, IUser, IWorkspaceMember, TProjectMembership } from "@plane/types";
import { CustomMenu, CustomSelect } from "@plane/ui";
import { getFileURL } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useUser, useUserPermissions } from "@/hooks/store/user";
export interface RowData extends Pick<TProjectMembership, "original_role"> {
member: IWorkspaceMember;
}
type NameProps = {
rowData: RowData;
workspaceSlug: string;
isAdmin: boolean;
currentUser: IUser | undefined;
setRemoveMemberModal: (rowData: RowData) => void;
};
type AccountTypeProps = {
rowData: RowData;
currentProjectRole: EUserPermissions | undefined;
workspaceSlug: string;
projectId: string;
};
export function NameColumn(props: NameProps) {
const { rowData, workspaceSlug, isAdmin, currentUser, setRemoveMemberModal } = props;
// derived values
const { avatar_url, display_name, email, first_name, id, last_name } = rowData.member;
return (
<Disclosure>
{({}) => (
<div className="group relative">
<div className="flex w-72 items-center gap-2">
<div className="flex flex-1 items-center gap-x-2 gap-y-2">
{avatar_url && avatar_url.trim() !== "" ? (
<Link href={`/${workspaceSlug}/profile/${id}`}>
<span className="relative flex size-6 items-center justify-center rounded-full text-on-color capitalize">
<img
src={getFileURL(avatar_url)}
className="absolute top-0 left-0 h-full w-full rounded-full object-cover"
alt={display_name || email}
/>
</span>
</Link>
) : (
<Link href={`/${workspaceSlug}/profile/${id}`}>
<span className="relative flex size-6 items-center justify-center rounded-full bg-layer-3 text-11 text-on-color capitalize">
{(email ?? display_name ?? "?")[0]}
</span>
</Link>
)}
{first_name} {last_name}
</div>
{(isAdmin || id === currentUser?.id) && (
<CustomMenu
ellipsis
buttonClassName="p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
optionsClassName="p-1.5"
placement="bottom-end"
>
<CustomMenu.MenuItem>
<div
className="flex cursor-pointer items-center gap-x-1 font-medium text-danger-primary"
onClick={() => setRemoveMemberModal(rowData)}
>
<CircleMinus className="size-3.5 flex-shrink-0" />
{rowData.member?.id === currentUser?.id ? "Leave " : "Remove "}
</div>
</CustomMenu.MenuItem>
</CustomMenu>
)}
</div>
</div>
)}
</Disclosure>
);
}
export const AccountTypeColumn = observer(function AccountTypeColumn(props: AccountTypeProps) {
const { rowData, projectId, workspaceSlug } = props;
// store hooks
const {
project: { updateMemberRole },
workspace: { getWorkspaceMemberDetails },
} = useMember();
const { data: currentUser } = useUser();
const { getProjectRoleByWorkspaceSlugAndProjectId } = useUserPermissions();
// form info
const {
control,
formState: { errors },
} = useForm();
// derived values
const roleLabel = ROLE[rowData.original_role ?? EUserPermissions.GUEST];
const isCurrentUser = currentUser?.id === rowData.member.id;
const isRowDataWorkspaceAdmin = [EUserPermissions.ADMIN].includes(
Number(getWorkspaceMemberDetails(rowData.member.id)?.role) ?? EUserPermissions.GUEST
);
const isCurrentUserWorkspaceAdmin = currentUser
? [EUserPermissions.ADMIN].includes(
Number(getWorkspaceMemberDetails(currentUser.id)?.role) ?? EUserPermissions.GUEST
)
: false;
const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
const isCurrentUserProjectAdmin = currentProjectRole
? ![EUserPermissions.MEMBER, EUserPermissions.GUEST].includes(Number(currentProjectRole) ?? EUserPermissions.GUEST)
: false;
// logic
// Workspace admin can change his own role
// Project admin can change any role except his own and workspace admin's role
const isRoleEditable =
(isCurrentUserWorkspaceAdmin && isCurrentUser) ||
(isCurrentUserProjectAdmin && !isRowDataWorkspaceAdmin && !isCurrentUser);
const checkCurrentOptionWorkspaceRole = (value: string) => {
const currentMemberWorkspaceRole = getWorkspaceMemberDetails(value)?.role as EUserPermissions | undefined;
if (!value || !currentMemberWorkspaceRole) return ROLE;
const isGuest = [EUserPermissions.GUEST].includes(currentMemberWorkspaceRole);
return Object.fromEntries(
Object.entries(ROLE).filter(([key]) => !isGuest || parseInt(key) === EUserPermissions.GUEST)
);
};
return (
<>
{isRoleEditable ? (
<Controller
name="role"
control={control}
rules={{ required: "Role is required." }}
render={() => (
<CustomSelect
value={rowData.original_role}
onChange={async (value: EUserProjectRoles) => {
if (!workspaceSlug) return;
await updateMemberRole(workspaceSlug.toString(), projectId.toString(), rowData.member.id, value).catch(
(err) => {
console.log(err, "err");
const error = err.error;
const errorString = Array.isArray(error) ? error[0] : error;
setToast({
type: TOAST_TYPE.ERROR,
title: "You can’t change this role yet.",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
}
);
}}
label={
<div className="flex">
<span>{roleLabel}</span>
</div>
}
buttonClassName={`!px-0 !justify-start hover:bg-surface-1 ${errors.role ? "border-danger-strong" : "border-none"}`}
className="w-32 rounded-md p-0"
input
>
{Object.entries(checkCurrentOptionWorkspaceRole(rowData.member.id)).map(([key, label]) => (
<CustomSelect.Option key={key} value={key}>
{label}
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
) : (
<div className="flex w-32">
<span>{roleLabel}</span>
</div>
)}
</>
);
});