chore: model access statement
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Checkbox, Empty, Input } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import List from './list';
|
||||
import SelectedList from './selected-list';
|
||||
|
||||
const PanelWrapper = styled.div<{ $maxHeight?: number; $leftWidth?: number }>`
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--ant-border-radius);
|
||||
overflow-y: auto;
|
||||
max-height: ${({ $maxHeight }) =>
|
||||
$maxHeight ? `${$maxHeight + 2}px` : 'auto'};
|
||||
`;
|
||||
|
||||
const Left = styled.div`
|
||||
padding: 0;
|
||||
`;
|
||||
const Right = styled.div``;
|
||||
|
||||
const Header = styled.div`
|
||||
padding: 8px 12px 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--ant-color-split);
|
||||
background-color: var(--ant-color-fill-alter);
|
||||
`;
|
||||
|
||||
interface SelectPanelProps {
|
||||
searchPlaceholder?: string;
|
||||
height?: number;
|
||||
leftWidth?: number;
|
||||
options: Array<{ key: string; title: string }>;
|
||||
selectedKeys: string[];
|
||||
onSelectChange: (selectedKeys: string[]) => void;
|
||||
}
|
||||
|
||||
const SelectPanel: React.FC<SelectPanelProps> = ({
|
||||
height = 300,
|
||||
leftWidth = 260,
|
||||
options,
|
||||
selectedKeys,
|
||||
searchPlaceholder,
|
||||
onSelectChange
|
||||
}) => {
|
||||
const [indeterminate, setIndeterminate] = React.useState(false);
|
||||
const [checkAll, setCheckAll] = React.useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const showOptions = useMemo(() => {
|
||||
return options.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
}, [options, searchText]);
|
||||
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchText(e.target.value);
|
||||
};
|
||||
|
||||
const handleOnUnselect = (
|
||||
key: string,
|
||||
newSelectedKeys: { key: string; title: string }[]
|
||||
) => {
|
||||
onSelectChange(newSelectedKeys.map((item) => item.key));
|
||||
};
|
||||
|
||||
const handleCheckAllChange = (e: any) => {
|
||||
const checked = e.target.checked;
|
||||
setCheckAll(checked);
|
||||
setIndeterminate(false);
|
||||
if (checked) {
|
||||
const allKeys = options
|
||||
.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
.map((item) => item.key);
|
||||
onSelectChange(Array.from(new Set([...selectedKeys, ...allKeys])));
|
||||
} else {
|
||||
const filteredKeys = options
|
||||
.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
.map((item) => item.key);
|
||||
const newSelectedKeys = selectedKeys.filter(
|
||||
(key) => !filteredKeys.includes(key)
|
||||
);
|
||||
onSelectChange(newSelectedKeys);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCheckStatus = (newSelectedKeys: string[]) => {
|
||||
if (options.length === 0) {
|
||||
setIndeterminate(false);
|
||||
setCheckAll(false);
|
||||
return;
|
||||
}
|
||||
const filteredOptions = options.filter((item) =>
|
||||
item.title.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
const filteredKeys = filteredOptions.map((item) => item.key);
|
||||
const selectedFilteredKeys = newSelectedKeys.filter((key) =>
|
||||
filteredKeys.includes(key)
|
||||
);
|
||||
setIndeterminate(
|
||||
selectedFilteredKeys.length > 0 &&
|
||||
selectedFilteredKeys.length < filteredKeys.length
|
||||
);
|
||||
setCheckAll(selectedFilteredKeys.length === filteredKeys.length);
|
||||
};
|
||||
|
||||
const handleSelectChange = (newSelectedKeys: string[]) => {
|
||||
onSelectChange(newSelectedKeys);
|
||||
updateCheckStatus(newSelectedKeys);
|
||||
};
|
||||
|
||||
const handleClearSelection = () => {
|
||||
onSelectChange([]);
|
||||
setCheckAll(false);
|
||||
setIndeterminate(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateCheckStatus(selectedKeys);
|
||||
}, [selectedKeys, options]);
|
||||
|
||||
const renderRight = () => {
|
||||
return (
|
||||
<Right>
|
||||
<Header>
|
||||
<span>({selectedKeys.length}) selected</span>
|
||||
<Button type="text" size="small" onClick={handleClearSelection}>
|
||||
Clear
|
||||
</Button>
|
||||
</Header>
|
||||
<SelectedList
|
||||
maxHeight={height - 40}
|
||||
selectedList={options.filter((item) =>
|
||||
selectedKeys.includes(item.key)
|
||||
)}
|
||||
onUnselect={handleOnUnselect}
|
||||
/>
|
||||
</Right>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PanelWrapper $maxHeight={height} $leftWidth={leftWidth}>
|
||||
<Left>
|
||||
<Header>
|
||||
<span
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
color: 'var(--ant-color-text-tertiary)'
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checkAll}
|
||||
indeterminate={indeterminate}
|
||||
onChange={handleCheckAllChange}
|
||||
></Checkbox>
|
||||
<span> {selectedKeys.length} selected</span>
|
||||
</span>
|
||||
<Input
|
||||
prefix={
|
||||
<SearchOutlined
|
||||
style={{ color: 'var(--ant-color-text-quaternary)' }}
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
allowClear
|
||||
placeholder={searchPlaceholder}
|
||||
style={{
|
||||
width: 300,
|
||||
height: 32,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'var(--ant-color-bg-container) !important'
|
||||
}}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</Header>
|
||||
{showOptions.length > 0 ? (
|
||||
<List
|
||||
maxHeight={height - 40}
|
||||
dataList={showOptions}
|
||||
selectedKeys={selectedKeys}
|
||||
onSelectChange={handleSelectChange}
|
||||
/>
|
||||
) : (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="No models found"
|
||||
></Empty>
|
||||
)}
|
||||
</Left>
|
||||
</PanelWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectPanel;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { OverlayScroller } from '@/components/overlay-scroller';
|
||||
import { Checkbox } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import AutoTooltip from '../../../components/auto-tooltip';
|
||||
|
||||
interface ListProps {
|
||||
maxHeight?: number;
|
||||
dataList: Array<{ key: string; title: string }>;
|
||||
selectedKeys: string[];
|
||||
renderTitle?: (item: { key: string; title: string }) => React.ReactNode;
|
||||
onSelectChange: (selectedKeys: string[]) => void;
|
||||
}
|
||||
|
||||
const UL = styled.ul`
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const LI = styled.li<{ selected: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
gap: 8px;
|
||||
&:hover {
|
||||
background-color: var(--ant-control-item-bg-hover);
|
||||
}
|
||||
`;
|
||||
|
||||
const List: React.FC<ListProps> = ({
|
||||
maxHeight,
|
||||
dataList,
|
||||
selectedKeys,
|
||||
onSelectChange,
|
||||
renderTitle
|
||||
}) => {
|
||||
const handleClickItem = (item: { key: string; title: string }) => {
|
||||
const itemKey = item.key;
|
||||
const newSelectedKeys = selectedKeys.includes(itemKey)
|
||||
? selectedKeys.filter((key) => key !== itemKey)
|
||||
: [...selectedKeys, itemKey];
|
||||
onSelectChange(newSelectedKeys);
|
||||
};
|
||||
|
||||
return (
|
||||
<OverlayScroller style={{ paddingInline: 0 }} maxHeight={maxHeight}>
|
||||
<UL>
|
||||
{dataList.map((item) => (
|
||||
<LI
|
||||
key={item.key}
|
||||
selected={selectedKeys.includes(item.key)}
|
||||
onClick={() => handleClickItem(item)}
|
||||
>
|
||||
<Checkbox checked={selectedKeys.includes(item.key)}></Checkbox>
|
||||
{renderTitle ? (
|
||||
renderTitle(item)
|
||||
) : (
|
||||
<AutoTooltip ghost>{item.title}</AutoTooltip>
|
||||
)}
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
</OverlayScroller>
|
||||
);
|
||||
};
|
||||
|
||||
export default List;
|
||||
@@ -0,0 +1,60 @@
|
||||
import AutoTooltip from '@/components/auto-tooltip';
|
||||
import { OverlayScroller } from '@/components/overlay-scroller';
|
||||
import { Tag } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const TagInner = styled(Tag)`
|
||||
border-radius: 12px;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
`;
|
||||
|
||||
interface SelectedProps {
|
||||
maxHeight?: number;
|
||||
selectedList: { key: string; title: string }[];
|
||||
onUnselect: (
|
||||
key: string,
|
||||
newSelectedKeys: { key: string; title: string }[]
|
||||
) => void;
|
||||
}
|
||||
|
||||
const SelectedList: React.FC<SelectedProps> = ({
|
||||
maxHeight,
|
||||
selectedList,
|
||||
onUnselect
|
||||
}) => {
|
||||
const handleOnUnselect = (key: string) => {
|
||||
const newSelectedKeys = selectedList.filter((item) => item.key !== key);
|
||||
onUnselect(key, newSelectedKeys);
|
||||
};
|
||||
|
||||
return (
|
||||
<OverlayScroller maxHeight={maxHeight}>
|
||||
<Content>
|
||||
{selectedList.map((item) => (
|
||||
<AutoTooltip
|
||||
title={item.title}
|
||||
key={item.key}
|
||||
maxWidth={200}
|
||||
onClose={(e) => {
|
||||
e.preventDefault();
|
||||
handleOnUnselect(item.key);
|
||||
}}
|
||||
closable
|
||||
>
|
||||
{item.title}
|
||||
</AutoTooltip>
|
||||
))}
|
||||
</Content>
|
||||
</OverlayScroller>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectedList;
|
||||
@@ -1,20 +1,51 @@
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { Pagination, Transfer, TransferProps } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import { Transfer, TransferProps } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type TransferKey = string | number | bigint;
|
||||
|
||||
const PaginationWrapper = styled.div`
|
||||
padding: 4px 16px;
|
||||
`;
|
||||
|
||||
const TransferWrap = styled.div`
|
||||
.ant-transfer-list {
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
height: 300px;
|
||||
.ant-transfer-list-header-dropdown {
|
||||
display: none;
|
||||
}
|
||||
.ant-input-outlined {
|
||||
height: 32px;
|
||||
padding-block: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.ant-transfer-operation {
|
||||
margin: 0 16px;
|
||||
gap: 12px;
|
||||
.ant-btn-icon-only {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
.ant-transfer-list-content {
|
||||
&::-webkit-scrollbar {
|
||||
width: var(--scrollbar-size);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-scrollbar-thumb);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.ant-transfer-list-content-item {
|
||||
&:hover {
|
||||
background-color: var(--ant-control-item-bg-hover);
|
||||
@@ -39,38 +70,28 @@ interface TransferInnerProps extends TransferProps {
|
||||
dataSource?: Array<{ key: TransferKey; title: string }>;
|
||||
targetKeys?: TransferKey[];
|
||||
}
|
||||
|
||||
const TransferInner: React.FC<TransferInnerProps> = (props) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const { onPageChange, total, perPage = 30 } = props;
|
||||
|
||||
const handleOnPageChange = (page: number, perPage?: number) => {
|
||||
setPage(page);
|
||||
onPageChange?.(page, perPage);
|
||||
};
|
||||
|
||||
const renderFooter = (TransferProps: any, { direction }: any) => {
|
||||
if (direction === 'left' && total && total > perPage!) {
|
||||
const renderAllLabels = (info: {
|
||||
selectedCount: number;
|
||||
totalCount: number;
|
||||
}) => {
|
||||
if (info.selectedCount) {
|
||||
return (
|
||||
<PaginationWrapper>
|
||||
<Pagination
|
||||
simple={{ readOnly: true }}
|
||||
size="small"
|
||||
total={total}
|
||||
onChange={handleOnPageChange}
|
||||
pageSize={perPage}
|
||||
current={page}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
</PaginationWrapper>
|
||||
<span style={{ color: 'var(--ant-color-text-secondary)' }}>
|
||||
{info.selectedCount} selected
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<TransferWrap>
|
||||
<Transfer
|
||||
{...props}
|
||||
selectAllLabels={
|
||||
props.selectAllLabels || [renderAllLabels, renderAllLabels]
|
||||
}
|
||||
selectionsIcon={
|
||||
<MoreOutlined style={{ fontSize: 14, marginBottom: 3 }} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user