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
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:
@@ -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>
|
||||
);
|
||||
});
|
||||
150
apps/web/core/components/project/settings/features-list.tsx
Normal file
150
apps/web/core/components/project/settings/features-list.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
46
apps/web/core/components/project/settings/helper.tsx
Normal file
46
apps/web/core/components/project/settings/helper.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
194
apps/web/core/components/project/settings/member-columns.tsx
Normal file
194
apps/web/core/components/project/settings/member-columns.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user