@baishuyun/ai-design 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +16 -14
- package/CHANGELOG.md +9 -0
- package/dist/ai-design.js +6255 -5314
- package/dist/ai-design.js.map +1 -1
- package/dist/ai-design.umd.cjs +67 -67
- package/dist/ai-design.umd.cjs.map +1 -1
- package/dist/src/components/agent-scroll-selector.d.ts +2 -1
- package/dist/src/components/agent-scroll-selector.d.ts.map +1 -1
- package/dist/src/components/bs-entry-select/app-group-folder.d.ts +6 -0
- package/dist/src/components/bs-entry-select/app-group-folder.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/bs-entry-select.d.ts +4 -0
- package/dist/src/components/bs-entry-select/bs-entry-select.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/build-tree.d.ts +9 -0
- package/dist/src/components/bs-entry-select/build-tree.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/entry-item.d.ts +8 -0
- package/dist/src/components/bs-entry-select/entry-item.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/entry-popover.d.ts +17 -0
- package/dist/src/components/bs-entry-select/entry-popover.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/index.d.ts +3 -0
- package/dist/src/components/bs-entry-select/index.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/other-app-section.d.ts +11 -0
- package/dist/src/components/bs-entry-select/other-app-section.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/scroll-slider.d.ts +10 -0
- package/dist/src/components/bs-entry-select/scroll-slider.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/search-input.d.ts +5 -0
- package/dist/src/components/bs-entry-select/search-input.d.ts.map +1 -0
- package/dist/src/components/bs-entry-select/tree-item-node.d.ts +9 -0
- package/dist/src/components/bs-entry-select/tree-item-node.d.ts.map +1 -0
- package/dist/src/components/{bs-entry-select.d.ts → bs-entry-select/types.d.ts} +12 -2
- package/dist/src/components/bs-entry-select/types.d.ts.map +1 -0
- package/dist/src/components/bs-icons.d.ts +2 -0
- package/dist/src/components/bs-icons.d.ts.map +1 -1
- package/dist/src/components/collapsible.d.ts +6 -0
- package/dist/src/components/collapsible.d.ts.map +1 -0
- package/dist/src/components/field-checker-list-msg.d.ts +19 -0
- package/dist/src/components/field-checker-list-msg.d.ts.map +1 -0
- package/dist/src/components/field-checker.d.ts +19 -0
- package/dist/src/components/field-checker.d.ts.map +1 -0
- package/dist/src/components/field-icon.d.ts +5 -0
- package/dist/src/components/field-icon.d.ts.map +1 -0
- package/dist/src/components/field-value-checker.d.ts +16 -0
- package/dist/src/components/field-value-checker.d.ts.map +1 -0
- package/dist/src/components/legacy-slot.d.ts.map +1 -1
- package/dist/src/components/office-container.d.ts +5 -0
- package/dist/src/components/office-container.d.ts.map +1 -1
- package/dist/src/components/subform-fields-value-checker.d.ts +22 -0
- package/dist/src/components/subform-fields-value-checker.d.ts.map +1 -0
- package/dist/src/const/ui.d.ts +3 -0
- package/dist/src/const/ui.d.ts.map +1 -1
- package/dist/src/hooks/use-column-appear.d.ts +22 -0
- package/dist/src/hooks/use-column-appear.d.ts.map +1 -0
- package/dist/src/hooks/use-resizable-width.d.ts +3 -1
- package/dist/src/hooks/use-resizable-width.d.ts.map +1 -1
- package/dist/src/index.d.ts +11 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/lib/utils.d.ts +3 -0
- package/dist/src/lib/utils.d.ts.map +1 -1
- package/dist/src/stories/BsEntrySelect.stories.d.ts +4 -0
- package/dist/src/stories/BsEntrySelect.stories.d.ts.map +1 -1
- package/dist/src/stories/OfficeContainer.stories.d.ts +5 -0
- package/dist/src/stories/OfficeContainer.stories.d.ts.map +1 -1
- package/dist/src/stories/collapsible.stories.d.ts +8 -0
- package/dist/src/stories/collapsible.stories.d.ts.map +1 -0
- package/dist/src/stories/field-checker-list-msg.stories.d.ts +11 -0
- package/dist/src/stories/field-checker-list-msg.stories.d.ts.map +1 -0
- package/dist/src/stories/field-checker.stories.d.ts +13 -0
- package/dist/src/stories/field-checker.stories.d.ts.map +1 -0
- package/dist/src/stories/field-icon.stories.d.ts +15 -0
- package/dist/src/stories/field-icon.stories.d.ts.map +1 -0
- package/dist/src/stories/field-value-checker.stories.d.ts +13 -0
- package/dist/src/stories/field-value-checker.stories.d.ts.map +1 -0
- package/dist/src/stories/subform-fields-value-checker.stories.d.ts +9 -0
- package/dist/src/stories/subform-fields-value-checker.stories.d.ts.map +1 -0
- package/package.json +7 -5
- package/src/components/agent-scroll-selector.tsx +10 -3
- package/src/components/bs-entry-select/app-group-folder.tsx +61 -0
- package/src/components/bs-entry-select/bs-entry-select.tsx +271 -0
- package/src/components/bs-entry-select/build-tree.ts +144 -0
- package/src/components/bs-entry-select/entry-item.tsx +43 -0
- package/src/components/bs-entry-select/entry-popover.tsx +97 -0
- package/src/components/bs-entry-select/index.ts +2 -0
- package/src/components/bs-entry-select/other-app-section.tsx +96 -0
- package/src/components/bs-entry-select/scroll-slider.tsx +455 -0
- package/src/components/bs-entry-select/search-input.tsx +32 -0
- package/src/components/bs-entry-select/tree-item-node.tsx +84 -0
- package/src/components/bs-entry-select/types.ts +30 -0
- package/src/components/bs-icons.tsx +36 -1
- package/src/components/card.tsx +1 -1
- package/src/components/collapsible.tsx +9 -0
- package/src/components/field-checker-list-msg.tsx +146 -0
- package/src/components/field-checker.tsx +182 -0
- package/src/components/field-icon.tsx +13 -0
- package/src/components/field-value-checker.tsx +118 -0
- package/src/components/office-container.tsx +111 -28
- package/src/components/subform-fields-value-checker.tsx +185 -0
- package/src/const/ui.ts +67 -0
- package/src/hooks/use-column-appear.ts +78 -0
- package/src/hooks/use-resizable-width.ts +10 -2
- package/src/index.ts +11 -2
- package/src/lib/utils.ts +67 -0
- package/src/stories/BsEntrySelect.stories.tsx +45 -2
- package/src/stories/OfficeContainer.stories.tsx +150 -1
- package/src/stories/collapsible.stories.tsx +47 -0
- package/src/stories/field-checker-list-msg.stories.tsx +95 -0
- package/src/stories/field-checker.stories.tsx +117 -0
- package/src/stories/field-icon.stories.tsx +70 -0
- package/src/stories/field-value-checker.stories.tsx +113 -0
- package/src/stories/subform-fields-value-checker.stories.tsx +98 -0
- package/dist/src/components/bs-entry-select.d.ts.map +0 -1
- package/src/components/bs-entry-select.tsx +0 -724
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils';
|
|
2
|
+
import { transCls } from '../../const/ui';
|
|
3
|
+
import { getDepthPadding, getEntryIconClass } from './build-tree';
|
|
4
|
+
import type { EntryTreeItem } from './types';
|
|
5
|
+
|
|
6
|
+
export function EntryItem({
|
|
7
|
+
item,
|
|
8
|
+
depth,
|
|
9
|
+
isSelected,
|
|
10
|
+
onClick,
|
|
11
|
+
}: {
|
|
12
|
+
item: EntryTreeItem;
|
|
13
|
+
depth: number;
|
|
14
|
+
isSelected: boolean;
|
|
15
|
+
onClick: () => void;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cn(
|
|
20
|
+
'flex items-center justify-between h-[34px] px-[10px] cursor-pointer rounded-[4px]',
|
|
21
|
+
getDepthPadding(depth),
|
|
22
|
+
'hover:bg-[#EFF0F6]',
|
|
23
|
+
isSelected && 'bg-[#EFF0F6]',
|
|
24
|
+
transCls,
|
|
25
|
+
)}
|
|
26
|
+
onClick={onClick}
|
|
27
|
+
>
|
|
28
|
+
<span className="flex items-center gap-[6px] min-w-0 flex-1">
|
|
29
|
+
<i
|
|
30
|
+
className={
|
|
31
|
+
item.entry
|
|
32
|
+
? getEntryIconClass(item.entry)
|
|
33
|
+
: 'node-icon icon-bsy-form icon-new-bsy-form-25'
|
|
34
|
+
}
|
|
35
|
+
style={{ fontSize: 16, lineHeight: 1 }}
|
|
36
|
+
/>
|
|
37
|
+
<span className="text-[14px] text-[#030303] truncate">
|
|
38
|
+
{item.name}
|
|
39
|
+
</span>
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { cn } from '../../lib/utils';
|
|
2
|
+
import { transCls } from '../../const/ui';
|
|
3
|
+
import { SearchInput } from './search-input';
|
|
4
|
+
import { TreeItemNode } from './tree-item-node';
|
|
5
|
+
import { AppGroupFolder } from './app-group-folder';
|
|
6
|
+
import { OtherAppSection } from './other-app-section';
|
|
7
|
+
import type { IAppDetail } from '@baishuyun/types';
|
|
8
|
+
import type { EntryTreeItem, BsEntrySelectChangeResult } from './types';
|
|
9
|
+
|
|
10
|
+
export function EntryPopover({
|
|
11
|
+
searchQuery,
|
|
12
|
+
onSearchChange,
|
|
13
|
+
currentAppTree,
|
|
14
|
+
currentAppId,
|
|
15
|
+
otherAppIds,
|
|
16
|
+
appDetails,
|
|
17
|
+
selectedEntryId,
|
|
18
|
+
hasCurrentApp,
|
|
19
|
+
hasOtherApps,
|
|
20
|
+
hasAnyContent,
|
|
21
|
+
hasSearch,
|
|
22
|
+
onSelect,
|
|
23
|
+
}: {
|
|
24
|
+
searchQuery: string;
|
|
25
|
+
onSearchChange: (v: string) => void;
|
|
26
|
+
currentAppTree: EntryTreeItem[];
|
|
27
|
+
currentAppId: string | undefined;
|
|
28
|
+
otherAppIds: string[];
|
|
29
|
+
appDetails: Record<string, IAppDetail>;
|
|
30
|
+
selectedEntryId: string | undefined;
|
|
31
|
+
hasCurrentApp: boolean;
|
|
32
|
+
hasOtherApps: boolean;
|
|
33
|
+
hasAnyContent: boolean;
|
|
34
|
+
hasSearch: boolean;
|
|
35
|
+
onSelect: (result: BsEntrySelectChangeResult) => void;
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={cn(
|
|
40
|
+
'bg-white rounded-[10px] shadow-[0px_0px_10px_0px_rgba(0,0,0,0.04)] p-[4px]',
|
|
41
|
+
'flex flex-col gap-[4px]',
|
|
42
|
+
transCls,
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<SearchInput value={searchQuery} onChange={onSearchChange} />
|
|
46
|
+
|
|
47
|
+
<div className="flex flex-col gap-[4px] max-h-[400px] overflow-y-auto">
|
|
48
|
+
{hasCurrentApp && (
|
|
49
|
+
<AppGroupFolder label="当前应用表单" searchQuery={searchQuery}>
|
|
50
|
+
{currentAppTree.length > 0 ? (
|
|
51
|
+
currentAppTree.map((node) => (
|
|
52
|
+
<TreeItemNode
|
|
53
|
+
key={node.id}
|
|
54
|
+
item={node}
|
|
55
|
+
selectedEntryId={selectedEntryId}
|
|
56
|
+
appId={currentAppId!}
|
|
57
|
+
section="current"
|
|
58
|
+
onSelect={onSelect}
|
|
59
|
+
/>
|
|
60
|
+
))
|
|
61
|
+
) : hasSearch ? (
|
|
62
|
+
<div className="text-[12px] text-[#999999] py-[8px] pl-[22px]">
|
|
63
|
+
无匹配结果
|
|
64
|
+
</div>
|
|
65
|
+
) : (
|
|
66
|
+
<div className="text-[12px] text-[#999999] py-[8px] pl-[22px]">
|
|
67
|
+
暂无表单
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</AppGroupFolder>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{hasOtherApps && (
|
|
74
|
+
<AppGroupFolder label="其他应用表单" searchQuery={searchQuery}>
|
|
75
|
+
{otherAppIds.map((appId) => (
|
|
76
|
+
<OtherAppSection
|
|
77
|
+
key={appId}
|
|
78
|
+
appId={appId}
|
|
79
|
+
appDetail={appDetails[appId]}
|
|
80
|
+
searchQuery={searchQuery}
|
|
81
|
+
selectedEntryId={selectedEntryId}
|
|
82
|
+
section="other"
|
|
83
|
+
onSelect={onSelect}
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
86
|
+
</AppGroupFolder>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{!hasAnyContent && (
|
|
90
|
+
<div className="text-[14px] text-[#999999] py-[12px] text-center">
|
|
91
|
+
暂无应用
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
import { transCls } from '../../const/ui';
|
|
4
|
+
import { buildEntryTreeFromAliasMap } from './build-tree';
|
|
5
|
+
import { TreeItemNode } from './tree-item-node';
|
|
6
|
+
import type { IAppDetail } from '@baishuyun/types';
|
|
7
|
+
import type { BsEntrySelectChangeResult } from './types';
|
|
8
|
+
import { SolidArrowDownIcon } from '../bs-icons';
|
|
9
|
+
|
|
10
|
+
const ChevronRightIcon = ({ size }: { size?: number }) => (
|
|
11
|
+
<SolidArrowDownIcon className="-rotate-90" size={size} />
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export function OtherAppSection({
|
|
15
|
+
appId,
|
|
16
|
+
appDetail,
|
|
17
|
+
searchQuery,
|
|
18
|
+
selectedEntryId,
|
|
19
|
+
section,
|
|
20
|
+
onSelect,
|
|
21
|
+
}: {
|
|
22
|
+
appId: string;
|
|
23
|
+
appDetail: IAppDetail;
|
|
24
|
+
searchQuery: string;
|
|
25
|
+
selectedEntryId: string | undefined;
|
|
26
|
+
section: 'current' | 'other';
|
|
27
|
+
onSelect: (result: BsEntrySelectChangeResult) => void;
|
|
28
|
+
}) {
|
|
29
|
+
const [expanded, setExpanded] = useState(true);
|
|
30
|
+
const hasSearch = searchQuery.trim().length > 0;
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (hasSearch) {
|
|
34
|
+
setExpanded(true);
|
|
35
|
+
}
|
|
36
|
+
}, [hasSearch]);
|
|
37
|
+
|
|
38
|
+
const tree = useMemo(() => {
|
|
39
|
+
return buildEntryTreeFromAliasMap(appDetail, searchQuery);
|
|
40
|
+
}, [appDetail, searchQuery]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div>
|
|
44
|
+
<div
|
|
45
|
+
className={cn(
|
|
46
|
+
'flex items-center gap-[6px] h-[32px] cursor-pointer rounded-[4px] pr-[10px]',
|
|
47
|
+
'hover:bg-[#EFF0F6]',
|
|
48
|
+
transCls,
|
|
49
|
+
)}
|
|
50
|
+
style={{ paddingLeft: 15 }}
|
|
51
|
+
onClick={() => setExpanded(!expanded)}
|
|
52
|
+
>
|
|
53
|
+
<span
|
|
54
|
+
className={cn(
|
|
55
|
+
'shrink-0 flex items-center justify-center w-[18px] h-[18px]',
|
|
56
|
+
transCls,
|
|
57
|
+
expanded && 'rotate-90',
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
<ChevronRightIcon size={18} />
|
|
61
|
+
</span>
|
|
62
|
+
<i
|
|
63
|
+
className="node-icon icon-bsy-form icon-new-bsy-form-0"
|
|
64
|
+
style={{ fontSize: 16, lineHeight: 1 }}
|
|
65
|
+
/>
|
|
66
|
+
<span className="text-[14px] text-[#030303] truncate">
|
|
67
|
+
{appDetail.name}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
{expanded && tree.length > 0 && (
|
|
71
|
+
<div className="flex flex-col" style={{ paddingLeft: 15 }}>
|
|
72
|
+
{tree.map((node) => (
|
|
73
|
+
<TreeItemNode
|
|
74
|
+
key={node.id}
|
|
75
|
+
item={node}
|
|
76
|
+
selectedEntryId={selectedEntryId}
|
|
77
|
+
appId={appId}
|
|
78
|
+
section={section}
|
|
79
|
+
onSelect={onSelect}
|
|
80
|
+
/>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
{expanded && tree.length === 0 && hasSearch && (
|
|
85
|
+
<div className="text-[12px] text-[#999999] py-[8px] pl-[32px]">
|
|
86
|
+
无匹配结果
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{expanded && tree.length === 0 && !hasSearch && (
|
|
90
|
+
<div className="text-[12px] text-[#999999] py-[8px] pl-[32px]">
|
|
91
|
+
暂无表单
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { createPortal } from 'react-dom';
|
|
9
|
+
import { cn } from '../../lib/utils';
|
|
10
|
+
import { transCls } from '../../const/ui';
|
|
11
|
+
import { IconBtn } from '../icon-btn';
|
|
12
|
+
import { LeftArrowIcon, SolidArrowDownIcon } from '../bs-icons';
|
|
13
|
+
import {
|
|
14
|
+
buildEntryTreeFromAliasMap,
|
|
15
|
+
getEntryIconClass,
|
|
16
|
+
filterTreeBySearch,
|
|
17
|
+
} from './build-tree';
|
|
18
|
+
import { SearchInput } from './search-input';
|
|
19
|
+
import { TreeItemNode } from './tree-item-node';
|
|
20
|
+
import type { IAppDetail } from '@baishuyun/types';
|
|
21
|
+
import type { EntryTreeItem, BsEntrySelectChangeResult } from './types';
|
|
22
|
+
|
|
23
|
+
const ChevronRightIcon = ({ size }: { size?: number }) => (
|
|
24
|
+
<SolidArrowDownIcon className="-rotate-90" size={size} />
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
interface FlatNode {
|
|
28
|
+
node: EntryTreeItem;
|
|
29
|
+
appId: string;
|
|
30
|
+
section: 'current' | 'other';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ScrollSlider({
|
|
34
|
+
appDetails,
|
|
35
|
+
currentAppId,
|
|
36
|
+
selectedEntryId,
|
|
37
|
+
onSelect,
|
|
38
|
+
onOverflowChange,
|
|
39
|
+
}: {
|
|
40
|
+
appDetails: Record<string, IAppDetail>;
|
|
41
|
+
currentAppId?: string;
|
|
42
|
+
selectedEntryId?: string;
|
|
43
|
+
onSelect: (result: BsEntrySelectChangeResult) => void;
|
|
44
|
+
onOverflowChange?: (hasOverflow: boolean) => void;
|
|
45
|
+
}) {
|
|
46
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
48
|
+
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
49
|
+
|
|
50
|
+
// Group popover state
|
|
51
|
+
const [activeGroup, setActiveGroup] = useState<FlatNode | null>(null);
|
|
52
|
+
const [groupSearchQuery, setGroupSearchQuery] = useState('');
|
|
53
|
+
const [popoverStyle, setPopoverStyle] = useState<React.CSSProperties>({});
|
|
54
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
const groupPillRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
|
56
|
+
const [groupPortalContainer, setGroupPortalContainer] = useState<HTMLElement | null>(null);
|
|
57
|
+
|
|
58
|
+
// Build first-level nodes from all apps (no search filter)
|
|
59
|
+
const flatNodes = useMemo<FlatNode[]>(() => {
|
|
60
|
+
const appIds = Object.keys(appDetails);
|
|
61
|
+
const currentFirst: FlatNode[] = [];
|
|
62
|
+
const otherNodes: FlatNode[] = [];
|
|
63
|
+
|
|
64
|
+
for (const appId of appIds) {
|
|
65
|
+
const detail = appDetails[appId];
|
|
66
|
+
const tree = buildEntryTreeFromAliasMap(detail, '');
|
|
67
|
+
const section = appId === currentAppId ? 'current' as const : 'other' as const;
|
|
68
|
+
const mapped = tree.map((node) => ({ node, appId, section }));
|
|
69
|
+
if (section === 'current') {
|
|
70
|
+
currentFirst.push(...mapped);
|
|
71
|
+
} else {
|
|
72
|
+
otherNodes.push(...mapped);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return [...currentFirst, ...otherNodes];
|
|
77
|
+
}, [appDetails, currentAppId]);
|
|
78
|
+
|
|
79
|
+
// Filtered tree for the active group popover
|
|
80
|
+
const filteredGroupTree = useMemo(() => {
|
|
81
|
+
if (!activeGroup) return [];
|
|
82
|
+
return filterTreeBySearch(activeGroup.node.children, groupSearchQuery);
|
|
83
|
+
}, [activeGroup, groupSearchQuery]);
|
|
84
|
+
|
|
85
|
+
const checkScroll = useCallback(() => {
|
|
86
|
+
const el = scrollRef.current;
|
|
87
|
+
if (!el) return;
|
|
88
|
+
const canLeft = el.scrollLeft > 0;
|
|
89
|
+
const canRight = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;
|
|
90
|
+
setCanScrollLeft(canLeft);
|
|
91
|
+
setCanScrollRight(canRight);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const hasOverflow = canScrollLeft || canScrollRight;
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
onOverflowChange?.(hasOverflow);
|
|
98
|
+
}, [hasOverflow, onOverflowChange]);
|
|
99
|
+
|
|
100
|
+
// Map from descendant entry ID to the group pill node ID
|
|
101
|
+
const descendantToGroupId = useMemo(() => {
|
|
102
|
+
const map = new Map<string, string>();
|
|
103
|
+
function collectDescendants(node: EntryTreeItem, groupId: string) {
|
|
104
|
+
if (node.isSelectable) {
|
|
105
|
+
map.set(node.id, groupId);
|
|
106
|
+
}
|
|
107
|
+
for (const child of node.children) {
|
|
108
|
+
collectDescendants(child, groupId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const { node } of flatNodes) {
|
|
112
|
+
if (!node.isSelectable) {
|
|
113
|
+
for (const child of node.children) {
|
|
114
|
+
collectDescendants(child, node.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return map;
|
|
119
|
+
}, [flatNodes]);
|
|
120
|
+
|
|
121
|
+
// Auto-scroll selected pill into view
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!selectedEntryId || !scrollRef.current) return;
|
|
124
|
+
// Try direct match first (leaf pill)
|
|
125
|
+
let targetId = selectedEntryId;
|
|
126
|
+
let pill = scrollRef.current.querySelector(
|
|
127
|
+
`[data-entry-id="${targetId}"]`,
|
|
128
|
+
) as HTMLElement | null;
|
|
129
|
+
// Fall back to ancestor group pill
|
|
130
|
+
if (!pill) {
|
|
131
|
+
targetId = descendantToGroupId.get(selectedEntryId) || '';
|
|
132
|
+
if (targetId) {
|
|
133
|
+
pill = scrollRef.current.querySelector(
|
|
134
|
+
`[data-entry-id="${targetId}"]`,
|
|
135
|
+
) as HTMLElement | null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (pill) {
|
|
139
|
+
pill.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
|
140
|
+
}
|
|
141
|
+
}, [selectedEntryId, descendantToGroupId]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
checkScroll();
|
|
145
|
+
const el = scrollRef.current;
|
|
146
|
+
if (!el) return;
|
|
147
|
+
|
|
148
|
+
const observer = new ResizeObserver(checkScroll);
|
|
149
|
+
observer.observe(el);
|
|
150
|
+
el.addEventListener('scroll', checkScroll, { passive: true });
|
|
151
|
+
return () => {
|
|
152
|
+
observer.disconnect();
|
|
153
|
+
el.removeEventListener('scroll', checkScroll);
|
|
154
|
+
};
|
|
155
|
+
}, [flatNodes, checkScroll]);
|
|
156
|
+
|
|
157
|
+
const scroll = useCallback((direction: 'left' | 'right') => {
|
|
158
|
+
const el = scrollRef.current;
|
|
159
|
+
if (!el) return;
|
|
160
|
+
el.scrollBy({
|
|
161
|
+
left: direction === 'left' ? -150 : 150,
|
|
162
|
+
behavior: 'smooth',
|
|
163
|
+
});
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const updatePopoverPosition = useCallback(() => {
|
|
167
|
+
if (!activeGroup) return;
|
|
168
|
+
const triggerEl = groupPillRefs.current.get(activeGroup.node.id);
|
|
169
|
+
if (!triggerEl) return;
|
|
170
|
+
const rect = triggerEl.getBoundingClientRect();
|
|
171
|
+
setPopoverStyle({
|
|
172
|
+
position: 'fixed',
|
|
173
|
+
top: rect.bottom + 4,
|
|
174
|
+
left: rect.left,
|
|
175
|
+
minWidth: Math.max(rect.width, 240),
|
|
176
|
+
zIndex: 9999,
|
|
177
|
+
});
|
|
178
|
+
}, [activeGroup]);
|
|
179
|
+
|
|
180
|
+
const closeGroupPopover = useCallback(() => {
|
|
181
|
+
setActiveGroup(null);
|
|
182
|
+
setGroupSearchQuery('');
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
const openGroupPopover = useCallback((flatNode: FlatNode, event: React.MouseEvent) => {
|
|
186
|
+
event.stopPropagation();
|
|
187
|
+
if (activeGroup?.node.id === flatNode.node.id) {
|
|
188
|
+
setActiveGroup(null);
|
|
189
|
+
setGroupSearchQuery('');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
setGroupSearchQuery('');
|
|
193
|
+
setActiveGroup(flatNode);
|
|
194
|
+
|
|
195
|
+
// Check Shadow DOM portal
|
|
196
|
+
const triggerEl = groupPillRefs.current.get(flatNode.node.id);
|
|
197
|
+
if (triggerEl) {
|
|
198
|
+
const rootNode = triggerEl.getRootNode();
|
|
199
|
+
if (rootNode instanceof ShadowRoot) {
|
|
200
|
+
const existing = rootNode.querySelector<HTMLElement>(
|
|
201
|
+
'[data-bs-entry-select-group-portal]',
|
|
202
|
+
);
|
|
203
|
+
if (existing) {
|
|
204
|
+
setGroupPortalContainer(existing);
|
|
205
|
+
} else {
|
|
206
|
+
const container = document.createElement('div');
|
|
207
|
+
container.setAttribute('data-bs-entry-select-group-portal', 'true');
|
|
208
|
+
rootNode.appendChild(container);
|
|
209
|
+
setGroupPortalContainer(container);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
setGroupPortalContainer(null);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Position the popover after the next frame
|
|
217
|
+
const groupId = flatNode.node.id;
|
|
218
|
+
requestAnimationFrame(() => {
|
|
219
|
+
const el = groupPillRefs.current.get(groupId);
|
|
220
|
+
if (el) {
|
|
221
|
+
const rect = el.getBoundingClientRect();
|
|
222
|
+
setPopoverStyle({
|
|
223
|
+
position: 'fixed',
|
|
224
|
+
top: rect.bottom + 4,
|
|
225
|
+
left: rect.left,
|
|
226
|
+
minWidth: Math.max(rect.width, 240),
|
|
227
|
+
zIndex: 9999,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}, [activeGroup]);
|
|
232
|
+
|
|
233
|
+
const handleGroupPopoverSelect = useCallback(
|
|
234
|
+
(result: BsEntrySelectChangeResult) => {
|
|
235
|
+
onSelect(result);
|
|
236
|
+
closeGroupPopover();
|
|
237
|
+
},
|
|
238
|
+
[onSelect, closeGroupPopover],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Click outside for group popover
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (!activeGroup) return;
|
|
244
|
+
|
|
245
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
246
|
+
const path = e.composedPath();
|
|
247
|
+
if (popoverRef.current && path.includes(popoverRef.current)) return;
|
|
248
|
+
const triggerEl = groupPillRefs.current.get(activeGroup.node.id);
|
|
249
|
+
if (triggerEl && path.includes(triggerEl)) return;
|
|
250
|
+
closeGroupPopover();
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const handleScroll = () => updatePopoverPosition();
|
|
254
|
+
|
|
255
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
256
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
257
|
+
window.addEventListener('resize', handleScroll);
|
|
258
|
+
|
|
259
|
+
return () => {
|
|
260
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
261
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
262
|
+
window.removeEventListener('resize', handleScroll);
|
|
263
|
+
};
|
|
264
|
+
}, [activeGroup, closeGroupPopover, updatePopoverPosition]);
|
|
265
|
+
|
|
266
|
+
// Keyboard escape
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
if (!activeGroup) return;
|
|
269
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
270
|
+
if (e.key === 'Escape') closeGroupPopover();
|
|
271
|
+
};
|
|
272
|
+
document.addEventListener('keydown', handleEscape);
|
|
273
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
274
|
+
}, [activeGroup, closeGroupPopover]);
|
|
275
|
+
|
|
276
|
+
if (flatNodes.length === 0) return null;
|
|
277
|
+
|
|
278
|
+
const hasGroupSearch = groupSearchQuery.trim().length > 0;
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div className="flex items-center flex-1 min-w-0">
|
|
282
|
+
{/* Left arrow: always in DOM, width transition */}
|
|
283
|
+
<span
|
|
284
|
+
className={cn(
|
|
285
|
+
transCls,
|
|
286
|
+
'inline-flex overflow-hidden',
|
|
287
|
+
canScrollLeft ? 'w-[26px] opacity-100 mr-2.5' : 'w-0 opacity-0 mr-0',
|
|
288
|
+
)}
|
|
289
|
+
>
|
|
290
|
+
<IconBtn
|
|
291
|
+
rounded
|
|
292
|
+
icon={<LeftArrowIcon />}
|
|
293
|
+
onClick={() => scroll('left')}
|
|
294
|
+
/>
|
|
295
|
+
</span>
|
|
296
|
+
|
|
297
|
+
<div
|
|
298
|
+
ref={scrollRef}
|
|
299
|
+
className="flex-1 overflow-x-auto scrollbar-hide flex gap-2.5 min-w-0"
|
|
300
|
+
>
|
|
301
|
+
{flatNodes.map((flatNode) => {
|
|
302
|
+
const { node, appId, section } = flatNode;
|
|
303
|
+
const isLeaf = node.isSelectable;
|
|
304
|
+
const isSelected = isLeaf && selectedEntryId === node.id;
|
|
305
|
+
const isGroupActive = activeGroup?.node.id === node.id;
|
|
306
|
+
|
|
307
|
+
const setPillRef = (el: HTMLButtonElement | null) => {
|
|
308
|
+
if (el) {
|
|
309
|
+
groupPillRefs.current.set(node.id, el);
|
|
310
|
+
} else {
|
|
311
|
+
groupPillRefs.current.delete(node.id);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (isLeaf) {
|
|
316
|
+
return (
|
|
317
|
+
<button
|
|
318
|
+
key={node.id}
|
|
319
|
+
data-entry-id={node.id}
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={() => {
|
|
322
|
+
if (node.entry) {
|
|
323
|
+
onSelect({ entry: node.entry, appId, section });
|
|
324
|
+
}
|
|
325
|
+
}}
|
|
326
|
+
className={cn(
|
|
327
|
+
'flex items-center gap-[6px] shrink-0 max-w-[240px]',
|
|
328
|
+
'px-[10px] py-[7px] rounded-[4px]',
|
|
329
|
+
transCls,
|
|
330
|
+
isSelected
|
|
331
|
+
? 'border border-[#F0F6FF] bg-[#F0F6FF] text-[#0265FF]'
|
|
332
|
+
: 'border border-[#E0E0E0] text-[#030303] hover:bg-[#F0F6FF]',
|
|
333
|
+
)}
|
|
334
|
+
>
|
|
335
|
+
<i
|
|
336
|
+
className={
|
|
337
|
+
node.entry
|
|
338
|
+
? getEntryIconClass(node.entry)
|
|
339
|
+
: 'node-icon icon-bsy-form icon-new-bsy-form-25'
|
|
340
|
+
}
|
|
341
|
+
style={{ fontSize: 16, lineHeight: 1 }}
|
|
342
|
+
/>
|
|
343
|
+
<span className="text-[14px] leading-[normal] truncate">
|
|
344
|
+
{node.name}
|
|
345
|
+
</span>
|
|
346
|
+
</button>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Group pill (has descendants)
|
|
351
|
+
return (
|
|
352
|
+
<button
|
|
353
|
+
key={node.id}
|
|
354
|
+
data-entry-id={node.id}
|
|
355
|
+
ref={setPillRef}
|
|
356
|
+
type="button"
|
|
357
|
+
onClick={(e) => openGroupPopover(flatNode, e)}
|
|
358
|
+
className={cn(
|
|
359
|
+
'flex items-center gap-[10px] shrink-0 max-w-[240px]',
|
|
360
|
+
'px-[10px] py-[7px] rounded-[4px]',
|
|
361
|
+
transCls,
|
|
362
|
+
isGroupActive
|
|
363
|
+
? 'border border-[#0265FF] text-[#030303]'
|
|
364
|
+
: 'border border-[#E0E0E0] text-[#030303] hover:bg-[#F0F6FF]',
|
|
365
|
+
)}
|
|
366
|
+
>
|
|
367
|
+
<span className="flex items-center gap-[6px] min-w-0 flex-1">
|
|
368
|
+
<i
|
|
369
|
+
className="icon-bsy-folder-close"
|
|
370
|
+
style={{ fontSize: 16, lineHeight: 1 }}
|
|
371
|
+
/>
|
|
372
|
+
<span className="text-[14px] leading-[normal] truncate">
|
|
373
|
+
{node.name}
|
|
374
|
+
</span>
|
|
375
|
+
</span>
|
|
376
|
+
<span
|
|
377
|
+
className={cn(
|
|
378
|
+
'shrink-0 flex items-center justify-center w-[16px] h-[16px] text-[#666666]',
|
|
379
|
+
transCls,
|
|
380
|
+
isGroupActive && 'rotate-90',
|
|
381
|
+
)}
|
|
382
|
+
>
|
|
383
|
+
<ChevronRightIcon size={16} />
|
|
384
|
+
</span>
|
|
385
|
+
</button>
|
|
386
|
+
);
|
|
387
|
+
})}
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
{/* Right arrow: always in DOM, width transition */}
|
|
391
|
+
<span
|
|
392
|
+
className={cn(
|
|
393
|
+
transCls,
|
|
394
|
+
'inline-flex overflow-hidden',
|
|
395
|
+
canScrollRight ? 'w-[26px] opacity-100 ml-2.5' : 'w-0 opacity-0 ml-0',
|
|
396
|
+
)}
|
|
397
|
+
>
|
|
398
|
+
<IconBtn
|
|
399
|
+
rounded
|
|
400
|
+
icon={
|
|
401
|
+
<span className="rotate-180 inline-flex">
|
|
402
|
+
<LeftArrowIcon />
|
|
403
|
+
</span>
|
|
404
|
+
}
|
|
405
|
+
onClick={() => scroll('right')}
|
|
406
|
+
/>
|
|
407
|
+
</span>
|
|
408
|
+
|
|
409
|
+
{/* Group subtree popover */}
|
|
410
|
+
{activeGroup &&
|
|
411
|
+
(() => {
|
|
412
|
+
const content = (
|
|
413
|
+
<div
|
|
414
|
+
ref={popoverRef}
|
|
415
|
+
style={popoverStyle}
|
|
416
|
+
className={cn(
|
|
417
|
+
'bg-white rounded-[10px] shadow-[0px_0px_10px_0px_rgba(0,0,0,0.04)] p-[4px]',
|
|
418
|
+
'flex flex-col gap-[4px]',
|
|
419
|
+
transCls,
|
|
420
|
+
)}
|
|
421
|
+
>
|
|
422
|
+
<SearchInput
|
|
423
|
+
value={groupSearchQuery}
|
|
424
|
+
onChange={setGroupSearchQuery}
|
|
425
|
+
/>
|
|
426
|
+
<div className="flex flex-col gap-[4px] max-h-[300px] overflow-y-auto">
|
|
427
|
+
{filteredGroupTree.length > 0 ? (
|
|
428
|
+
filteredGroupTree.map((child) => (
|
|
429
|
+
<TreeItemNode
|
|
430
|
+
key={child.id}
|
|
431
|
+
item={child}
|
|
432
|
+
selectedEntryId={selectedEntryId}
|
|
433
|
+
appId={activeGroup.appId}
|
|
434
|
+
section={activeGroup.section}
|
|
435
|
+
onSelect={handleGroupPopoverSelect}
|
|
436
|
+
/>
|
|
437
|
+
))
|
|
438
|
+
) : hasGroupSearch ? (
|
|
439
|
+
<div className="text-[12px] text-[#999999] py-[8px] text-center">
|
|
440
|
+
无匹配结果
|
|
441
|
+
</div>
|
|
442
|
+
) : (
|
|
443
|
+
<div className="text-[12px] text-[#999999] py-[8px] text-center">
|
|
444
|
+
暂无表单
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
return createPortal(content, groupPortalContainer || document.body);
|
|
452
|
+
})()}
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|