feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
|
||||
export interface IDocumentItemProps {
|
||||
id: string;
|
||||
title: string;
|
||||
selected?: boolean;
|
||||
onClick?: (id: string) => void;
|
||||
label?: ReactNode;
|
||||
tag?: ReactNode;
|
||||
}
|
||||
|
||||
const DocumentItem: React.FC<IDocumentItemProps> = props => {
|
||||
const { id, onClick, title, selected, tag, label } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
'w-full h-8 px-2 py-[6px] rounded-[8px] hover:coz-mg-primary cursor-pointer',
|
||||
'flex items-center',
|
||||
selected && 'coz-mg-primary',
|
||||
)}
|
||||
onClick={() => onClick?.(id)}
|
||||
>
|
||||
{label ? (
|
||||
<div className="w-full">{label}</div>
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text
|
||||
ellipsis={{ showTooltip: true }}
|
||||
className="w-full coz-fg-primary text-[14px] leading-[20px] grow truncate"
|
||||
>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
<div className="flex items-center shrink-0">{tag}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentItem;
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
import cls from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozInfoCircle } from '@coze-arch/coze-design/icons';
|
||||
import { Search, Tooltip } from '@coze-arch/coze-design';
|
||||
import { type ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
|
||||
import MergeOperation from '../assets/merge-operation.png';
|
||||
import MergeOperationEn from '../assets/merge-operation-en.png';
|
||||
import LevelOperation from '../assets/level-operation.png';
|
||||
import DeleteOperation from '../assets/delete-operation.png';
|
||||
import DeleteOperationEn from '../assets/delete-operation-en.png';
|
||||
import { SegmentTree } from './segment-tree';
|
||||
import DocumentItem from './document-item';
|
||||
interface IMenuItem {
|
||||
id: string;
|
||||
title: string;
|
||||
label?: ReactNode;
|
||||
tag?: ReactNode;
|
||||
tosUrl?: string;
|
||||
}
|
||||
|
||||
interface ISegmentMenuProps {
|
||||
list: IMenuItem[];
|
||||
onClick?: (id: string) => void;
|
||||
selectedID?: string;
|
||||
isSearchable?: boolean;
|
||||
treeVisible?: boolean;
|
||||
treeDisabled?: boolean;
|
||||
levelSegments?: ILevelSegment[];
|
||||
setLevelSegments?: (segments: ILevelSegment[]) => void;
|
||||
setSelectionIDs?: (ids: string[]) => void;
|
||||
}
|
||||
|
||||
const SegmentMenu: React.FC<ISegmentMenuProps> = props => {
|
||||
const {
|
||||
isSearchable,
|
||||
list,
|
||||
onClick,
|
||||
selectedID,
|
||||
levelSegments,
|
||||
setLevelSegments,
|
||||
setSelectionIDs,
|
||||
treeDisabled,
|
||||
treeVisible,
|
||||
} = props;
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col grow w-full h-full">
|
||||
{isSearchable ? (
|
||||
<Search
|
||||
value={searchValue}
|
||||
placeholder={I18n.t('datasets_placeholder_search')}
|
||||
onChange={setSearchValue}
|
||||
/>
|
||||
) : null}
|
||||
<div className="pl-2 h-6 mt-4 mb-1 flex items-center">
|
||||
<div className="coz-fg-secondary text-[12px] font-[400] leading-4 shrink-0">
|
||||
{/**文档列表 */}
|
||||
{I18n.t('knowledge_level_012')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col grow w-full">
|
||||
<div className="flex flex-col gap-1 h-[150px] grow !overflow-auto shrink-0">
|
||||
{list
|
||||
.filter(item => item.title.includes(searchValue))
|
||||
.map(document => {
|
||||
if (document.id !== '') {
|
||||
return (
|
||||
<DocumentItem
|
||||
key={document.id}
|
||||
id={document.id}
|
||||
selected={document.id === selectedID}
|
||||
onClick={onClick}
|
||||
title={document.title}
|
||||
tag={document.tag}
|
||||
label={document.label}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
|
||||
{levelSegments?.length && treeVisible ? (
|
||||
<>
|
||||
<div className="h-4 flex justify-center items-center px-[8px] mb-[8px]">
|
||||
<div
|
||||
className={cls(
|
||||
'border border-solid border-[0.5px] transition w-full',
|
||||
'coz-stroke-primary',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 !overflow-auto">
|
||||
<div className="w-full pl-2 h-6 items-center flex gap-[4px]">
|
||||
<div className="coz-fg-secondary text-[12px] font-[400] leading-4 shrink-0">
|
||||
{/**分段层级 */}
|
||||
{I18n.t('knowledge_level_adjust')}
|
||||
</div>
|
||||
{treeDisabled ? null : (
|
||||
<Tooltip
|
||||
style={{
|
||||
maxWidth: 602,
|
||||
}}
|
||||
position="left"
|
||||
content={
|
||||
<>
|
||||
<div className="coz-fg-plus text-[14px] font-[500] leading-[20px] mb-3">
|
||||
{I18n.t('knowledge_hierarchies_categories')}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col gap-1 justify-between w-[182px]">
|
||||
<span className="coz-fg-primary text-[12px] leading-[16px] font-[400]">
|
||||
{I18n.t('level_999')}
|
||||
</span>
|
||||
<img src={LevelOperation} className="w-[182px]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 justify-between w-[182px]">
|
||||
<span className="coz-fg-primary text-[12px] leading-[16px] font-[400]">
|
||||
{I18n.t('level_998')}
|
||||
</span>
|
||||
<img
|
||||
src={
|
||||
IS_OVERSEA ? MergeOperationEn : MergeOperation
|
||||
}
|
||||
className="w-[182px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 justify-between w-[182px]">
|
||||
<span className="coz-fg-primary text-[12px] leading-[16px] font-[400]">
|
||||
{I18n.t('level_997')}
|
||||
</span>
|
||||
<img
|
||||
src={
|
||||
IS_OVERSEA ? DeleteOperationEn : DeleteOperation
|
||||
}
|
||||
className="w-[182px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconCozInfoCircle className="coz-fg-secondary" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-[360px]">
|
||||
<SegmentTree
|
||||
segments={levelSegments}
|
||||
setLevelSegments={setLevelSegments}
|
||||
setSelectionIDs={setSelectionIDs}
|
||||
disabled={treeDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SegmentMenu;
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Tree, type NodeRendererProps, type CursorProps } from 'react-arborist';
|
||||
import { useState, type CSSProperties } from 'react';
|
||||
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import cls from 'classnames';
|
||||
import { type ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozArrowRight } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton, Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import {
|
||||
findDescendantIDs,
|
||||
getTreeNodes,
|
||||
handleDeleteNode,
|
||||
handleMergeNodes,
|
||||
handleTreeNodeMove,
|
||||
} from './utils/level-tree-op';
|
||||
import { useSegmentContextMenu } from './use-context-menu';
|
||||
import { type LevelDocumentTree } from './types';
|
||||
|
||||
interface ISegmentTreeProps {
|
||||
segments: ILevelSegment[];
|
||||
setLevelSegments?: (segments: ILevelSegment[]) => void;
|
||||
setSelectionIDs?: (ids: string[]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const SegmentTree: React.FC<ISegmentTreeProps> = ({
|
||||
segments,
|
||||
setLevelSegments,
|
||||
setSelectionIDs,
|
||||
disabled,
|
||||
}) => {
|
||||
/**
|
||||
* 选中功能
|
||||
*/
|
||||
const [selected, setSelected] = useState(new Set<string>());
|
||||
// 分片 id
|
||||
const [selectedThroughParent, setSelectedThroughParent] = useState(
|
||||
new Set<string>(),
|
||||
);
|
||||
const { ref, width, height } = useResizeObserver<HTMLDivElement>();
|
||||
|
||||
const onSelect = (node: LevelDocumentTree) => {
|
||||
setSelected(new Set([node.id]));
|
||||
setSelectedThroughParent(findDescendantIDs(node));
|
||||
setSelectionIDs?.([node.id, ...findDescendantIDs(node)]);
|
||||
};
|
||||
|
||||
/**
|
||||
* render
|
||||
*/
|
||||
const Node = ({
|
||||
node,
|
||||
style,
|
||||
dragHandle,
|
||||
}: NodeRendererProps<LevelDocumentTree>) => {
|
||||
const { isOpen, data } = node;
|
||||
const isLeaf = !data.children?.length;
|
||||
const expandIcon = (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="secondary"
|
||||
icon={
|
||||
<IconCozArrowRight
|
||||
className={cls(
|
||||
isOpen && 'rotate-90',
|
||||
'transition duration-150 ease-in-out',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
node.toggle();
|
||||
}}
|
||||
className={cls('bg-transparent ml-[4px] shrink-0')}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cls(
|
||||
'flex items-center gap-[4px]',
|
||||
'h-[32px] py-[4px] pr-[8px] mb-[2px]',
|
||||
'hover:coz-mg-primary cursor-pointer',
|
||||
'transition duration-150 ease-in-out',
|
||||
'rounded-[8px]',
|
||||
(selected.has(data.id) || selectedThroughParent.has(data.id)) &&
|
||||
'coz-mg-primary',
|
||||
)}
|
||||
onClick={() => {
|
||||
onSelect(data);
|
||||
}}
|
||||
onContextMenu={e => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
onContextMenu(e, node);
|
||||
}}
|
||||
style={style}
|
||||
ref={dragHandle}
|
||||
>
|
||||
{isLeaf ? <span className="w-6 ml-[4px] shrink-0" /> : expandIcon}
|
||||
<span
|
||||
className={cls('text-[14px] leading-[20px] coz-fg-primary truncate')}
|
||||
>
|
||||
{data.type !== 'image'
|
||||
? data.text.slice(0, 50)
|
||||
: I18n.t('knowledge_level_110')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* context menu
|
||||
*/
|
||||
const { popoverNode, onContextMenu } = useSegmentContextMenu({
|
||||
onMerge: node => {
|
||||
const { segments: newSegments, errMsg } = handleMergeNodes(
|
||||
node.id,
|
||||
Array.from(findDescendantIDs(node)),
|
||||
segments,
|
||||
);
|
||||
if (errMsg) {
|
||||
Toast.error(errMsg);
|
||||
}
|
||||
if (newSegments?.length) {
|
||||
setLevelSegments?.(newSegments);
|
||||
}
|
||||
},
|
||||
onDelete: node => {
|
||||
const newSegments = handleDeleteNode(
|
||||
[node.id, ...findDescendantIDs(node)],
|
||||
segments,
|
||||
);
|
||||
setLevelSegments?.(newSegments);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref} className="w-full h-full relative translate-z-0">
|
||||
<Tree
|
||||
data={getTreeNodes(segments)}
|
||||
disableDrag={disabled}
|
||||
disableDrop={disabled}
|
||||
onMove={({ dragIds, parentId, index }) => {
|
||||
const { segments: newSegments, errMsg } = handleTreeNodeMove(
|
||||
{ dragIDs: dragIds, parentID: parentId, dropIndex: index },
|
||||
segments,
|
||||
);
|
||||
if (errMsg) {
|
||||
Toast.error(errMsg);
|
||||
}
|
||||
if (newSegments?.length) {
|
||||
setLevelSegments?.(newSegments);
|
||||
}
|
||||
}}
|
||||
rowHeight={34}
|
||||
paddingTop={4}
|
||||
paddingBottom={4}
|
||||
width={width}
|
||||
height={height}
|
||||
renderCursor={Cursor}
|
||||
>
|
||||
{Node}
|
||||
</Tree>
|
||||
{popoverNode}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Cursor = ({ top, left, indent }: CursorProps) => {
|
||||
const placeholderStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
zIndex: 1,
|
||||
};
|
||||
const style: CSSProperties = {
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
top: `${top - 2}px`,
|
||||
left: `${left}px`,
|
||||
right: `${indent}px`,
|
||||
};
|
||||
return (
|
||||
<div style={{ ...placeholderStyle, ...style }}>
|
||||
<div className={cls('flex-1 h-[2px] coz-mg-hglt-plus')}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
|
||||
export type LevelDocumentTree = Omit<
|
||||
ILevelSegment,
|
||||
'children' | 'id' | 'parent'
|
||||
> & {
|
||||
id: string;
|
||||
parent: string;
|
||||
children: LevelDocumentTree[];
|
||||
};
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type NodeApi } from 'react-arborist';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useKnowledgeParams } from '@coze-data/knowledge-stores';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Menu, MenuSubMenu } from '@coze-arch/coze-design';
|
||||
|
||||
import { type LevelDocumentTree } from './types';
|
||||
|
||||
interface IUseSegmentContextMenuProps {
|
||||
onDelete: (node: LevelDocumentTree) => void;
|
||||
onMerge: (node: LevelDocumentTree) => void;
|
||||
}
|
||||
|
||||
export function useSegmentContextMenu({
|
||||
onDelete,
|
||||
onMerge,
|
||||
}: IUseSegmentContextMenuProps): {
|
||||
popoverNode: React.ReactNode;
|
||||
onContainerScroll: () => void;
|
||||
onContextMenu: (
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
treeNode: NodeApi<LevelDocumentTree>,
|
||||
) => void;
|
||||
} {
|
||||
const [treeNode, setTreeNode] = useState<NodeApi<LevelDocumentTree> | null>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
const params = useKnowledgeParams();
|
||||
|
||||
return {
|
||||
popoverNode: (
|
||||
<Menu
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
onClickOutSide={() => {
|
||||
setVisible(false);
|
||||
setTreeNode(null);
|
||||
}}
|
||||
trigger="custom"
|
||||
position="bottomLeft"
|
||||
render={
|
||||
<MenuSubMenu mode="menu">
|
||||
{treeNode && !treeNode.children?.length ? (
|
||||
<>
|
||||
<Menu.Item
|
||||
isMenu
|
||||
onClick={() => {
|
||||
onDelete(treeNode.data);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
{I18n.t('knowledge_level_028')}
|
||||
</Menu.Item>
|
||||
</>
|
||||
) : null}
|
||||
{treeNode && treeNode.children?.length ? (
|
||||
<>
|
||||
<Menu.Item
|
||||
isMenu
|
||||
onClick={() => {
|
||||
onMerge(treeNode.data);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
{I18n.t('knowledge_level_029')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
isMenu
|
||||
onClick={() => {
|
||||
onDelete(treeNode.data);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
{I18n.t('knowledge_level_028')}
|
||||
</Menu.Item>
|
||||
</>
|
||||
) : null}
|
||||
</MenuSubMenu>
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 0,
|
||||
width: 0,
|
||||
position: 'fixed',
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
),
|
||||
onContainerScroll: () => {
|
||||
if (visible) {
|
||||
setVisible(false);
|
||||
}
|
||||
},
|
||||
onContextMenu: (e, node: NodeApi<LevelDocumentTree>) => {
|
||||
e.preventDefault();
|
||||
setTreeNode(node);
|
||||
/** 在 project ide 里面,ide 容器设置了 contain: strict, 会导致 fixed position
|
||||
* 的偏移基础不对,所以这里需要减去 ide 容器的 left 和 top 值
|
||||
*/
|
||||
let clickX = e.pageX;
|
||||
let clickY = e.pageY;
|
||||
const ideDom = document.getElementById(
|
||||
`coze-project:///knowledge/${params.datasetID}`,
|
||||
);
|
||||
|
||||
if (ideDom) {
|
||||
const { left, top } = ideDom.getBoundingClientRect();
|
||||
clickX = clickX - left;
|
||||
clickY = clickY - top;
|
||||
}
|
||||
|
||||
setPosition({ left: clickX, top: clickY });
|
||||
setVisible(true);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { type ILevelSegment } from '@coze-data/knowledge-stores';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { type LevelDocumentTree } from '../types';
|
||||
|
||||
export const getTreeNodes = (
|
||||
segments: ILevelSegment[],
|
||||
): LevelDocumentTree[] => {
|
||||
const root = segments.find(f => f.parent === -1 && f.type === 'title');
|
||||
|
||||
if (!root) {
|
||||
return segments.map(item => ({
|
||||
...item,
|
||||
id: item.id.toString(),
|
||||
parent: item.parent?.toString(),
|
||||
children: [],
|
||||
}));
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
...root,
|
||||
id: root.id?.toString(),
|
||||
parent: root.parent?.toString(),
|
||||
children: getChildren(root, segments),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/** Segments to TreeNodes */
|
||||
const getChildren = (
|
||||
target: ILevelSegment,
|
||||
list: ILevelSegment[],
|
||||
): LevelDocumentTree[] =>
|
||||
(target.children ?? []).reduce<LevelDocumentTree[]>((acc, cur) => {
|
||||
const found = list.find(f => f.id === cur);
|
||||
if (found) {
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
...found,
|
||||
id: found.id?.toString(),
|
||||
parent: found.parent?.toString(),
|
||||
children: getChildren(found, list),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [...acc];
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**TreeNodes related */
|
||||
export const findDescendantIDs = (node: LevelDocumentTree) => {
|
||||
const ids = new Set<string>();
|
||||
const findChild = (item: LevelDocumentTree) => {
|
||||
if (!item || !item.id) {
|
||||
return;
|
||||
}
|
||||
const { children } = item;
|
||||
if (children && children.length) {
|
||||
children.forEach(child => {
|
||||
if (child && child.id) {
|
||||
ids.add(child.id);
|
||||
findChild(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
findChild(node);
|
||||
return ids;
|
||||
};
|
||||
|
||||
export const findTreeNodeByID = (
|
||||
nodes: LevelDocumentTree[],
|
||||
id: string,
|
||||
): LevelDocumentTree | null => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = findTreeNodeByID(node.children, id);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const handleTreeNodeMove = (
|
||||
positions: { dragIDs: string[]; parentID: string | null; dropIndex: number },
|
||||
segments: ILevelSegment[],
|
||||
): {
|
||||
segments: ILevelSegment[] | null;
|
||||
errMsg: string | null;
|
||||
} => {
|
||||
if (positions.parentID === null) {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_01'),
|
||||
};
|
||||
}
|
||||
const resSegments = cloneDeep(segments);
|
||||
for (const id of positions.dragIDs) {
|
||||
const dragSegmentIdx = resSegments.findIndex(
|
||||
segment => segment.id.toString() === id,
|
||||
);
|
||||
|
||||
if (dragSegmentIdx === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dragSegment = resSegments[dragSegmentIdx];
|
||||
const parentSegment = resSegments.find(
|
||||
segment => segment.id.toString() === positions.parentID,
|
||||
);
|
||||
|
||||
if (!parentSegment) {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_02'),
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是同一个 parent,且拖动的位置在当前位置之前,dropIndex 减 1
|
||||
const originalIndex = parentSegment.children.indexOf(dragSegment.id);
|
||||
const dropIndex =
|
||||
originalIndex < positions.dropIndex &&
|
||||
dragSegment.parent === parentSegment.id
|
||||
? positions.dropIndex - 1
|
||||
: positions.dropIndex;
|
||||
|
||||
if (dragSegment.parent !== parentSegment.id) {
|
||||
// Remove from old parent's children
|
||||
const oldParent = resSegments.find(s => s.id === dragSegment.parent);
|
||||
oldParent?.children.splice(oldParent.children.indexOf(dragSegment.id), 1);
|
||||
dragSegment.parent = parentSegment.id;
|
||||
}
|
||||
|
||||
// Reorder in parent's children
|
||||
parentSegment.children = parentSegment.children.filter(
|
||||
child => child !== dragSegment.id,
|
||||
);
|
||||
parentSegment.children.splice(dropIndex, 0, dragSegment.id);
|
||||
}
|
||||
|
||||
return {
|
||||
segments: resSegments,
|
||||
errMsg: null,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleDeleteNode = (ids: string[], segments: ILevelSegment[]) => {
|
||||
const resSegments = cloneDeep(segments);
|
||||
for (const id of ids) {
|
||||
const index = resSegments.findIndex(item => item.id.toString() === id);
|
||||
const parentSegment = resSegments.find(
|
||||
item => item.id === resSegments[index].parent,
|
||||
);
|
||||
if (parentSegment) {
|
||||
parentSegment.children = parentSegment.children.filter(
|
||||
item => item !== resSegments[index].id,
|
||||
);
|
||||
}
|
||||
resSegments.splice(index, 1);
|
||||
}
|
||||
return resSegments;
|
||||
};
|
||||
|
||||
export const handleMergeNodes = (
|
||||
id: string,
|
||||
descendants: string[],
|
||||
segments: ILevelSegment[],
|
||||
): {
|
||||
segments: ILevelSegment[] | null;
|
||||
errMsg: string | null;
|
||||
} => {
|
||||
const resSegments = cloneDeep(segments);
|
||||
const mergedSegment = resSegments.find(item => item.id.toString() === id);
|
||||
|
||||
if (!mergedSegment) {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_03'),
|
||||
};
|
||||
}
|
||||
|
||||
if (mergedSegment.parent === -1 && mergedSegment.type === 'title') {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_03'),
|
||||
};
|
||||
}
|
||||
|
||||
mergedSegment.children = [];
|
||||
mergedSegment.type = 'section-text';
|
||||
|
||||
for (const descendant of descendants) {
|
||||
const segmentToMerge = resSegments.find(
|
||||
item => item.id.toString() === descendant,
|
||||
);
|
||||
if (!segmentToMerge) {
|
||||
return {
|
||||
segments: null,
|
||||
errMsg: I18n.t('knowledge_hierarchies_categories_04'),
|
||||
};
|
||||
}
|
||||
|
||||
// 从原父节点的 children 中移除该节点
|
||||
const parentSegment = resSegments.find(
|
||||
item => item.id === segmentToMerge.parent,
|
||||
);
|
||||
if (parentSegment) {
|
||||
parentSegment.children = parentSegment.children.filter(
|
||||
childId => childId !== segmentToMerge.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (!['table', 'image', 'title'].includes(segmentToMerge?.type ?? '')) {
|
||||
// 合并文本内容并删除节点
|
||||
mergedSegment.text += segmentToMerge.text;
|
||||
const index = resSegments.findIndex(
|
||||
item => item.id === segmentToMerge.id,
|
||||
);
|
||||
if (index !== -1) {
|
||||
resSegments.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
// 非section-text类型的节点,将其移动到合并后节点的children中
|
||||
segmentToMerge.parent = mergedSegment.id;
|
||||
mergedSegment.children.push(segmentToMerge.id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
segments: resSegments,
|
||||
errMsg: null,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user