Co-authored-by: sealday <sealday@gmail.com> Reviewed-on: daoyoucloud/tachybase#1171
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import React, { useCallback, useMemo, useState } from 'react';
|
|
import { css, useCollectionManager_deprecated, useRecord, useRequest, useToken } from '@tachybase/client';
|
|
import { connect } from '@tachybase/schema';
|
|
import { CollectionsGraph, lodash } from '@tachybase/utils/client';
|
|
|
|
import { App, Col, Input, Row, Select, Space, Spin, Table, Tag } from 'antd';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
const excludeCollections = ['users', 'roles', 'applications'];
|
|
|
|
const useCollectionsGraph = ({ removed = [] }) => {
|
|
const { collections } = useCollectionManager_deprecated();
|
|
|
|
const findAddable = useCallback(
|
|
(name) => {
|
|
return CollectionsGraph.connectedNodes({
|
|
collections,
|
|
nodes: [name],
|
|
excludes: excludeCollections,
|
|
}).filter((name) => removed.includes(name));
|
|
},
|
|
[removed],
|
|
);
|
|
|
|
const findRemovable = useCallback(
|
|
(name) => {
|
|
return CollectionsGraph.connectedNodes({
|
|
collections,
|
|
nodes: [name],
|
|
excludes: excludeCollections,
|
|
direction: 'reverse',
|
|
}).filter((name) => !removed.includes(name));
|
|
},
|
|
[removed],
|
|
);
|
|
|
|
return {
|
|
findAddable,
|
|
findRemovable,
|
|
};
|
|
};
|
|
|
|
const useCollections = () => {
|
|
const record = useRecord();
|
|
const [selected, setSelected] = useState<any>([]);
|
|
|
|
const res1 = useRequest(
|
|
{
|
|
url: `applications/${record.name}/collectionBlacklist:list`,
|
|
params: {
|
|
paginate: false,
|
|
params: {
|
|
fields: ['name'],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
onSuccess(data) {
|
|
setSelected(data.data?.map((data) => data.name));
|
|
},
|
|
},
|
|
);
|
|
|
|
const res2 = useRequest<{
|
|
data: any[];
|
|
}>({
|
|
url: `collections`,
|
|
params: {
|
|
fields: ['name', 'title', 'hidden', 'category.name', 'category.color', 'category.sort'],
|
|
sort: 'sort',
|
|
paginate: false,
|
|
},
|
|
});
|
|
|
|
const res3 = useRequest<{
|
|
data: any[];
|
|
}>({
|
|
url: `collectionCategories`,
|
|
params: {
|
|
sort: 'sort',
|
|
paginate: false,
|
|
},
|
|
});
|
|
|
|
return {
|
|
loading: res1.loading || res2.loading || res3.loading,
|
|
collections: (res2.data?.data || []).filter((item) => !item.hidden && !excludeCollections.includes(item.name)),
|
|
removed: selected,
|
|
setSelected,
|
|
categories: (res3.data?.data || []).map((cat) => ({ label: cat.name, value: cat.name })),
|
|
};
|
|
};
|
|
|
|
const includes = (text: string, s: string | string[]) => {
|
|
const values = Array.isArray(s) ? s : [s];
|
|
for (const val of values) {
|
|
if (text.toLowerCase().includes(val)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const useRemovedDataSource = ({ collections, removed }) => {
|
|
const [filter, setFilter] = useState({ name: '', category: [] });
|
|
const dataSource = useMemo(() => {
|
|
return collections.filter((collection) => {
|
|
const { name, title, category = [] } = collection;
|
|
const results = [removed.includes(collection.name)];
|
|
if (filter.name) {
|
|
results.push(includes(name, filter.name) || includes(title, filter.name));
|
|
}
|
|
if (filter.category.length > 0) {
|
|
results.push(category.some((item) => includes(item.name, filter.category)));
|
|
}
|
|
return !results.includes(false);
|
|
});
|
|
}, [collections, removed, filter]);
|
|
const setNameFilter = useMemo(
|
|
() =>
|
|
lodash.debounce((name) => {
|
|
setFilter({
|
|
...filter,
|
|
name,
|
|
});
|
|
}, 300),
|
|
[],
|
|
);
|
|
return {
|
|
dataSource,
|
|
setNameFilter,
|
|
setCategoryFilter: (category) => {
|
|
setFilter({
|
|
...filter,
|
|
category,
|
|
});
|
|
},
|
|
};
|
|
};
|
|
|
|
const useAddedDataSource = ({ collections, removed }) => {
|
|
const [filter, setFilter] = useState({ name: '', category: [] });
|
|
const dataSource = collections.filter((collection) => {
|
|
const { name, title, category = [] } = collection;
|
|
const results = [!removed.includes(collection.name)];
|
|
if (filter.name) {
|
|
results.push(includes(name, filter.name) || includes(title, filter.name));
|
|
}
|
|
if (filter.category.length > 0) {
|
|
results.push(category.some((item) => includes(item.name, filter.category)));
|
|
}
|
|
return !results.includes(false);
|
|
});
|
|
const setNameFilter = useMemo(
|
|
() =>
|
|
lodash.debounce((name) => {
|
|
setFilter({
|
|
...filter,
|
|
name,
|
|
});
|
|
}, 300),
|
|
[],
|
|
);
|
|
return {
|
|
dataSource,
|
|
setNameFilter,
|
|
setCategoryFilter: (category) => {
|
|
setFilter({
|
|
...filter,
|
|
category,
|
|
});
|
|
},
|
|
};
|
|
};
|
|
|
|
export const TableTransfer = connect((props) => {
|
|
const { onChange } = props;
|
|
const { loading, collections, categories, removed, setSelected } = useCollections();
|
|
const [selectedRowKeys1, setSelectedRowKeys1] = useState([]);
|
|
const [selectedRowKeys2, setSelectedRowKeys2] = useState([]);
|
|
const { findAddable, findRemovable } = useCollectionsGraph({ removed });
|
|
const addedDataSource = useAddedDataSource({ collections, removed });
|
|
const removedDataSource = useRemovedDataSource({ collections, removed });
|
|
const { t } = useTranslation('multi-app-share-collection');
|
|
const { modal } = App.useApp();
|
|
const { token } = useToken();
|
|
const columns = useMemo(
|
|
() => [
|
|
{
|
|
title: t('Collection display name'),
|
|
dataIndex: 'title',
|
|
},
|
|
{
|
|
title: t('Collection name'),
|
|
dataIndex: 'name',
|
|
},
|
|
{
|
|
title: t('Collection category'),
|
|
dataIndex: 'category',
|
|
render: (categories) =>
|
|
categories.map((category) => (
|
|
<Tag key={category.name} color={category.color}>
|
|
{category.name}
|
|
</Tag>
|
|
)),
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
if (loading) {
|
|
return <Spin />;
|
|
}
|
|
return (
|
|
<div>
|
|
<Row
|
|
gutter={24}
|
|
className={css`
|
|
.ant-table-tbody > tr.ant-table-row:hover > td {
|
|
background: #e6f7ff;
|
|
cursor: pointer;
|
|
}
|
|
`}
|
|
>
|
|
<Col span={12}>
|
|
<div
|
|
className={css`
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: 100%;
|
|
margin-bottom: 8px;
|
|
`}
|
|
>
|
|
<strong style={{ fontSize: token.fontSizeLG, color: token.colorText }}>{t('Unshared collections')}</strong>
|
|
<Space.Compact style={{ width: 360 }}>
|
|
<Select
|
|
popupMatchSelectWidth={false}
|
|
onChange={(value) => {
|
|
removedDataSource.setCategoryFilter(value);
|
|
}}
|
|
mode={'multiple'}
|
|
style={{ width: '35%' }}
|
|
size={'middle'}
|
|
placeholder={t('All categories')}
|
|
options={categories}
|
|
allowClear
|
|
/>
|
|
<Input
|
|
onChange={(e) => removedDataSource.setNameFilter(e.target.value)}
|
|
style={{ width: '65%' }}
|
|
placeholder={t('Enter name or title...')}
|
|
allowClear
|
|
/>
|
|
</Space.Compact>
|
|
</div>
|
|
<Table
|
|
bordered
|
|
rowKey={'name'}
|
|
rowSelection={{
|
|
type: 'checkbox',
|
|
selectedRowKeys: selectedRowKeys1,
|
|
onChange(selectedRowKeys) {
|
|
const values = removed.filter((s) => !selectedRowKeys.includes(s));
|
|
setSelected(values);
|
|
onChange(values);
|
|
setSelectedRowKeys1([]);
|
|
},
|
|
}}
|
|
pagination={false}
|
|
size={'small'}
|
|
columns={columns}
|
|
// dataSource={collections.filter((collection) => removed.includes(collection.name))}
|
|
dataSource={removedDataSource.dataSource}
|
|
scroll={{ y: 'calc(100vh - 260px)' }}
|
|
onRow={({ name, disabled }: any) => ({
|
|
onClick: () => {
|
|
if (disabled) return;
|
|
const adding = findAddable(name);
|
|
const change = () => {
|
|
const values = removed.filter((s) => !adding.includes(s));
|
|
setSelected(values);
|
|
onChange(values);
|
|
};
|
|
if (adding.length === 1) {
|
|
return change();
|
|
}
|
|
modal.confirm({
|
|
title: t('Are you sure to add the following collections?'),
|
|
width: '60%',
|
|
content: (
|
|
<div>
|
|
<Table
|
|
size={'small'}
|
|
columns={columns}
|
|
dataSource={collections.filter((collection) => adding.includes(collection.name))}
|
|
pagination={false}
|
|
scroll={{ y: '60vh' }}
|
|
/>
|
|
</div>
|
|
),
|
|
onOk() {
|
|
change();
|
|
},
|
|
});
|
|
},
|
|
})}
|
|
/>
|
|
</Col>
|
|
<Col span={12}>
|
|
<div
|
|
className={css`
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: 100%;
|
|
margin-bottom: 8px;
|
|
`}
|
|
>
|
|
<strong style={{ fontSize: token.fontSizeLG, color: token.colorText }}>{t('Shared collections')}</strong>
|
|
<Space.Compact style={{ width: 360 }}>
|
|
<Select
|
|
popupMatchSelectWidth={false}
|
|
onChange={(value) => {
|
|
addedDataSource.setCategoryFilter(value);
|
|
}}
|
|
mode={'multiple'}
|
|
style={{ width: '35%' }}
|
|
size={'middle'}
|
|
placeholder={t('All categories')}
|
|
options={categories}
|
|
allowClear
|
|
/>
|
|
<Input
|
|
onChange={(e) => addedDataSource.setNameFilter(e.target.value)}
|
|
style={{ width: '65%' }}
|
|
placeholder={t('Enter name or title...')}
|
|
allowClear
|
|
/>
|
|
</Space.Compact>
|
|
</div>
|
|
<Table
|
|
bordered
|
|
rowKey={'name'}
|
|
rowSelection={{
|
|
type: 'checkbox',
|
|
selectedRowKeys: selectedRowKeys2,
|
|
onChange(selectedRowKeys) {
|
|
const values = lodash.uniq(removed.concat(selectedRowKeys));
|
|
setSelected(values);
|
|
onChange(values);
|
|
setSelectedRowKeys2([]);
|
|
},
|
|
}}
|
|
pagination={false}
|
|
size={'small'}
|
|
columns={columns}
|
|
dataSource={addedDataSource.dataSource}
|
|
// dataSource={collections.filter((collection) => !selected.includes(collection.name))}
|
|
scroll={{ y: 'calc(100vh - 260px)' }}
|
|
onRow={({ name }) => ({
|
|
onClick: () => {
|
|
const removing = findRemovable(name);
|
|
const change = () => {
|
|
removed.push(...removing);
|
|
const values = lodash.uniq([...removed]);
|
|
setSelected(values);
|
|
onChange(values);
|
|
};
|
|
if (removing.length === 1) {
|
|
return change();
|
|
}
|
|
modal.confirm({
|
|
title: t('Are you sure to remove the following collections?'),
|
|
width: '60%',
|
|
content: (
|
|
<div>
|
|
<Table
|
|
size={'small'}
|
|
columns={columns}
|
|
dataSource={collections.filter((collection) => removing.includes(collection.name))}
|
|
pagination={false}
|
|
scroll={{ y: '60vh' }}
|
|
/>
|
|
</div>
|
|
),
|
|
onOk() {
|
|
change();
|
|
},
|
|
});
|
|
},
|
|
})}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export default TableTransfer;
|