|
|
|
@ -4,6 +4,7 @@ import { Button, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select,
|
|
|
|
import { DndContext, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core';
|
|
|
|
import { DndContext, PointerSensor, useSensor, useSensors, DragEndEvent } from '@dnd-kit/core';
|
|
|
|
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
|
|
|
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
|
|
|
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
|
|
import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
|
|
|
|
|
|
import * as AntdIcons from "@ant-design/icons";
|
|
|
|
import { CSS } from '@dnd-kit/utilities';
|
|
|
|
import { CSS } from '@dnd-kit/utilities';
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
ApartmentOutlined,
|
|
|
|
ApartmentOutlined,
|
|
|
|
@ -39,6 +40,7 @@ import "./index.less";
|
|
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
const { Text } = Typography;
|
|
|
|
type TreePermission = SysPermission & { key: number; children?: TreePermission[] };
|
|
|
|
type TreePermission = SysPermission & { key: number; children?: TreePermission[] };
|
|
|
|
|
|
|
|
type MenuIconComponent = React.ElementType<{ "aria-hidden"?: string | boolean }>;
|
|
|
|
|
|
|
|
|
|
|
|
const legacyIconAliases: Record<string, string> = {
|
|
|
|
const legacyIconAliases: Record<string, string> = {
|
|
|
|
dashboard: "DashboardOutlined",
|
|
|
|
dashboard: "DashboardOutlined",
|
|
|
|
@ -53,11 +55,30 @@ const legacyIconAliases: Record<string, string> = {
|
|
|
|
setting: "SettingOutlined"
|
|
|
|
setting: "SettingOutlined"
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const menuIconComponents: Record<string, React.ComponentType<any>> = {
|
|
|
|
const commonMenuIconNames = [
|
|
|
|
|
|
|
|
"DashboardOutlined",
|
|
|
|
|
|
|
|
"VideoCameraOutlined",
|
|
|
|
|
|
|
|
"UserOutlined",
|
|
|
|
|
|
|
|
"TeamOutlined",
|
|
|
|
|
|
|
|
"SafetyCertificateOutlined",
|
|
|
|
|
|
|
|
"SettingOutlined",
|
|
|
|
|
|
|
|
"DesktopOutlined",
|
|
|
|
|
|
|
|
"ShopOutlined",
|
|
|
|
|
|
|
|
"ApartmentOutlined",
|
|
|
|
|
|
|
|
"BookOutlined",
|
|
|
|
|
|
|
|
"FolderOutlined",
|
|
|
|
|
|
|
|
"MenuOutlined",
|
|
|
|
|
|
|
|
"ClusterOutlined"
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fallbackMenuIconComponents: Record<string, MenuIconComponent> = {
|
|
|
|
ApartmentOutlined,
|
|
|
|
ApartmentOutlined,
|
|
|
|
BookOutlined,
|
|
|
|
BookOutlined,
|
|
|
|
|
|
|
|
ClusterOutlined,
|
|
|
|
DashboardOutlined,
|
|
|
|
DashboardOutlined,
|
|
|
|
DesktopOutlined,
|
|
|
|
DesktopOutlined,
|
|
|
|
|
|
|
|
FolderOutlined,
|
|
|
|
|
|
|
|
MenuOutlined,
|
|
|
|
SafetyCertificateOutlined,
|
|
|
|
SafetyCertificateOutlined,
|
|
|
|
SettingOutlined,
|
|
|
|
SettingOutlined,
|
|
|
|
ShopOutlined,
|
|
|
|
ShopOutlined,
|
|
|
|
@ -66,6 +87,13 @@ const menuIconComponents: Record<string, React.ComponentType<any>> = {
|
|
|
|
VideoCameraOutlined
|
|
|
|
VideoCameraOutlined
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const menuIconComponents = Object.entries(AntdIcons).reduce<Record<string, MenuIconComponent>>((icons, [name, component]) => {
|
|
|
|
|
|
|
|
if (name.endsWith("Outlined") && (typeof component === "function" || (typeof component === "object" && component !== null))) {
|
|
|
|
|
|
|
|
icons[name] = component as MenuIconComponent;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return icons;
|
|
|
|
|
|
|
|
}, {...fallbackMenuIconComponents});
|
|
|
|
|
|
|
|
|
|
|
|
interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
|
|
interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
|
|
|
|
'data-row-key': string;
|
|
|
|
'data-row-key': string;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -114,6 +142,7 @@ const DragHandle = () => {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const commonMenuIconOptions = commonMenuIconNames.filter((iconName) => menuIconComponents[iconName]);
|
|
|
|
const menuIconOptions = Object.keys(menuIconComponents).sort((left, right) => left.localeCompare(right));
|
|
|
|
const menuIconOptions = Object.keys(menuIconComponents).sort((left, right) => left.localeCompare(right));
|
|
|
|
|
|
|
|
|
|
|
|
function renderSelectableIcon(iconName?: string) {
|
|
|
|
function renderSelectableIcon(iconName?: string) {
|
|
|
|
@ -223,10 +252,18 @@ export default function Permissions() {
|
|
|
|
.map((permission) => ({ value: permission.permId, label: permission.name }));
|
|
|
|
.map((permission) => ({ value: permission.permId, label: permission.name }));
|
|
|
|
}, [data, currentPermType]);
|
|
|
|
}, [data, currentPermType]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const filteredCommonIconOptions = useMemo(() => {
|
|
|
|
|
|
|
|
const keyword = iconSearchKeyword.trim().toLowerCase();
|
|
|
|
|
|
|
|
if (!keyword) return commonMenuIconOptions;
|
|
|
|
|
|
|
|
return commonMenuIconOptions.filter((iconName) => iconName.toLowerCase().includes(keyword));
|
|
|
|
|
|
|
|
}, [iconSearchKeyword]);
|
|
|
|
|
|
|
|
|
|
|
|
const filteredIconOptions = useMemo(() => {
|
|
|
|
const filteredIconOptions = useMemo(() => {
|
|
|
|
const keyword = iconSearchKeyword.trim().toLowerCase();
|
|
|
|
const keyword = iconSearchKeyword.trim().toLowerCase();
|
|
|
|
if (!keyword) return menuIconOptions;
|
|
|
|
return menuIconOptions.filter((iconName) => {
|
|
|
|
return menuIconOptions.filter((iconName) => iconName.toLowerCase().includes(keyword));
|
|
|
|
const hitKeyword = keyword ? iconName.toLowerCase().includes(keyword) : true;
|
|
|
|
|
|
|
|
return hitKeyword && !commonMenuIconOptions.includes(iconName);
|
|
|
|
|
|
|
|
});
|
|
|
|
}, [iconSearchKeyword]);
|
|
|
|
}, [iconSearchKeyword]);
|
|
|
|
|
|
|
|
|
|
|
|
const visibleIconOptions = useMemo(() => filteredIconOptions.slice(0, visibleIconCount), [filteredIconOptions, visibleIconCount]);
|
|
|
|
const visibleIconOptions = useMemo(() => filteredIconOptions.slice(0, visibleIconCount), [filteredIconOptions, visibleIconCount]);
|
|
|
|
@ -254,6 +291,41 @@ export default function Permissions() {
|
|
|
|
setIconSearchKeyword("");
|
|
|
|
setIconSearchKeyword("");
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const renderIconPickerButton = (iconName: string) => {
|
|
|
|
|
|
|
|
const active = selectedIcon === iconName;
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
<Tooltip key={iconName} title={iconName}>
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
|
|
|
data-icon-name={iconName}
|
|
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
|
|
const nextValue = active ? undefined : iconName;
|
|
|
|
|
|
|
|
form.setFieldValue("icon", nextValue);
|
|
|
|
|
|
|
|
if (nextValue) {
|
|
|
|
|
|
|
|
setIconSearchKeyword("");
|
|
|
|
|
|
|
|
setIconPickerOpen(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
aria-label={iconName}
|
|
|
|
|
|
|
|
style={{
|
|
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
|
|
width: "100%",
|
|
|
|
|
|
|
|
borderRadius: 10,
|
|
|
|
|
|
|
|
border: active ? "1px solid #1677ff" : "1px solid transparent",
|
|
|
|
|
|
|
|
background: active ? "#e6f4ff" : "#fff",
|
|
|
|
|
|
|
|
color: active ? "#1677ff" : "inherit",
|
|
|
|
|
|
|
|
display: "inline-flex",
|
|
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
|
|
|
boxShadow: active ? "0 0 0 2px rgba(22,119,255,0.12)" : "none"
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{renderSelectableIcon(iconName)}
|
|
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onDragEnd = async ({ active, over }: DragEndEvent) => {
|
|
|
|
const onDragEnd = async ({ active, over }: DragEndEvent) => {
|
|
|
|
if (active.id !== over?.id) {
|
|
|
|
if (active.id !== over?.id) {
|
|
|
|
const activeId = Number(active.id);
|
|
|
|
const activeId = Number(active.id);
|
|
|
|
@ -606,43 +678,34 @@ export default function Permissions() {
|
|
|
|
setVisibleIconCount((count) => Math.min(count + 120, filteredIconOptions.length));
|
|
|
|
setVisibleIconCount((count) => Math.min(count + 120, filteredIconOptions.length));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}} style={{ maxHeight: 240, overflowY: "auto", border: "1px solid #f0f0f0", borderRadius: 8, padding: 12, background: "#fafafa" }}>
|
|
|
|
}} style={{ maxHeight: 240, overflowY: "auto", border: "1px solid #f0f0f0", borderRadius: 8, padding: 12, background: "#fafafa" }}>
|
|
|
|
|
|
|
|
{filteredCommonIconOptions.length ? <>
|
|
|
|
|
|
|
|
<Text type="secondary" style={{
|
|
|
|
|
|
|
|
display: "block",
|
|
|
|
|
|
|
|
marginBottom: 8,
|
|
|
|
|
|
|
|
fontSize: 12
|
|
|
|
|
|
|
|
}}>{t("permissionsExt.commonIcons")}</Text>
|
|
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
|
|
display: "grid",
|
|
|
|
|
|
|
|
gridTemplateColumns: "repeat(auto-fill, minmax(44px, 1fr))",
|
|
|
|
|
|
|
|
gap: 10,
|
|
|
|
|
|
|
|
marginBottom: 12
|
|
|
|
|
|
|
|
}}>
|
|
|
|
|
|
|
|
{filteredCommonIconOptions.map(renderIconPickerButton)}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</> : null}
|
|
|
|
|
|
|
|
{visibleIconOptions.length ? <Text type="secondary" style={{
|
|
|
|
|
|
|
|
display: "block",
|
|
|
|
|
|
|
|
marginBottom: 8,
|
|
|
|
|
|
|
|
fontSize: 12
|
|
|
|
|
|
|
|
}}>{t("permissionsExt.allIcons")}</Text> : null}
|
|
|
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(44px, 1fr))", gap: 10 }}>
|
|
|
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(44px, 1fr))", gap: 10 }}>
|
|
|
|
{visibleIconOptions.map((iconName) => {
|
|
|
|
{visibleIconOptions.map(renderIconPickerButton)}
|
|
|
|
const active = selectedIcon === iconName;
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
<Tooltip key={iconName} title={iconName}>
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
|
|
|
data-icon-name={iconName}
|
|
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
|
|
const nextValue = active ? undefined : iconName;
|
|
|
|
|
|
|
|
form.setFieldValue("icon", nextValue);
|
|
|
|
|
|
|
|
if (nextValue) {
|
|
|
|
|
|
|
|
setIconSearchKeyword("");
|
|
|
|
|
|
|
|
setIconPickerOpen(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
aria-label={iconName}
|
|
|
|
|
|
|
|
style={{
|
|
|
|
|
|
|
|
height: 44,
|
|
|
|
|
|
|
|
width: "100%",
|
|
|
|
|
|
|
|
borderRadius: 10,
|
|
|
|
|
|
|
|
border: active ? "1px solid #1677ff" : "1px solid transparent",
|
|
|
|
|
|
|
|
background: active ? "#e6f4ff" : "#fff",
|
|
|
|
|
|
|
|
color: active ? "#1677ff" : "inherit",
|
|
|
|
|
|
|
|
display: "inline-flex",
|
|
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
|
|
|
boxShadow: active ? "0 0 0 2px rgba(22,119,255,0.12)" : "none"
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{renderSelectableIcon(iconName)}
|
|
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{!filteredIconOptions.length ? <div style={{ padding: "16px 0", textAlign: "center", color: "#8c8c8c" }}>{t("permissionsExt.iconEmpty")}</div> : null}
|
|
|
|
{!filteredCommonIconOptions.length && !filteredIconOptions.length ? <div style={{
|
|
|
|
|
|
|
|
padding: "16px 0",
|
|
|
|
|
|
|
|
textAlign: "center",
|
|
|
|
|
|
|
|
color: "#8c8c8c"
|
|
|
|
|
|
|
|
}}>{t("permissionsExt.iconEmpty")}</div> : null}
|
|
|
|
{visibleIconOptions.length < filteredIconOptions.length ? <div style={{ paddingTop: 12, textAlign: "center", color: "#8c8c8c", fontSize: 12 }}>{t("permissionsExt.iconLoadingMore", { current: visibleIconOptions.length, total: filteredIconOptions.length })}</div> : null}
|
|
|
|
{visibleIconOptions.length < filteredIconOptions.length ? <div style={{ paddingTop: 12, textAlign: "center", color: "#8c8c8c", fontSize: 12 }}>{t("permissionsExt.iconLoadingMore", { current: visibleIconOptions.length, total: filteredIconOptions.length })}</div> : null}
|
|
|
|
</div></div> : null}
|
|
|
|
</div></div> : null}
|
|
|
|
</Space>
|
|
|
|
</Space>
|
|
|
|
|