@baishuyun/ai-design 2.0.37 → 2.1.0-alpha.0
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/.storybook/main.ts +31 -31
- package/.storybook/preview.ts +16 -16
- package/CHANGELOG.md +512 -492
- package/components.json +22 -22
- package/package.json +9 -6
- package/scripts/strip-layers.cjs +41 -41
- package/src/components/abs-fullscreen-gradient-bg.tsx +35 -35
- package/src/components/agent-helper/agent-category-select-menu/index.tsx +57 -57
- package/src/components/agent-helper/agent-entry-card/index.tsx +94 -94
- package/src/components/agent-helper/agent-helper-card/index.tsx +179 -179
- package/src/components/agent-helper/agent-helper-card-list/index.tsx +59 -59
- package/src/components/agent-helper/agent-helper-main-layout/index.tsx +60 -60
- package/src/components/agent-helper/agent-sort-select/index.tsx +89 -89
- package/src/components/agent-helper/agent-tag/index.tsx +29 -29
- package/src/components/agent-helper/app-select/index.tsx +559 -559
- package/src/components/agent-helper/condition-group/index.tsx +75 -75
- package/src/components/agent-helper/condition-input/index.tsx +233 -233
- package/src/components/agent-helper/condition-item/index.tsx +237 -237
- package/src/components/agent-helper/condition-quantifier-select/index.tsx +142 -142
- package/src/components/agent-helper/condition-trigger-description/index.tsx +49 -49
- package/src/components/agent-helper/date-picker/calendar-panel.tsx +164 -164
- package/src/components/agent-helper/date-picker/date-input.tsx +91 -91
- package/src/components/agent-helper/date-picker/date-picker.tsx +327 -327
- package/src/components/agent-helper/date-picker/footer-panel.tsx +44 -44
- package/src/components/agent-helper/date-picker/index.ts +3 -3
- package/src/components/agent-helper/date-picker/month-panel.tsx +82 -82
- package/src/components/agent-helper/date-picker/time-panel.tsx +146 -146
- package/src/components/agent-helper/date-picker/types.ts +37 -37
- package/src/components/agent-helper/date-picker/utils.ts +121 -121
- package/src/components/agent-helper/date-picker/year-panel.tsx +86 -86
- package/src/components/agent-helper/exec-log-filter/index.tsx +232 -232
- package/src/components/agent-helper/field-edit-preference-select/index.tsx +291 -291
- package/src/components/agent-helper/field-select/index.tsx +466 -466
- package/src/components/agent-helper/field-value-input/china-address-data.ts +4117 -4117
- package/src/components/agent-helper/field-value-input/display.tsx +71 -71
- package/src/components/agent-helper/field-value-input/icons.tsx +38 -38
- package/src/components/agent-helper/field-value-input/index.tsx +144 -144
- package/src/components/agent-helper/field-value-input/inputs.tsx +247 -247
- package/src/components/agent-helper/field-value-input/picker-inputs.tsx +1141 -1141
- package/src/components/agent-helper/flow-node-action-trigger-select/index.tsx +57 -57
- package/src/components/agent-helper/form-action-trigger-select/index.tsx +60 -60
- package/src/components/agent-helper/form-divider/index.tsx +44 -44
- package/src/components/agent-helper/form-select/index.tsx +732 -732
- package/src/components/agent-helper/monthdays-select/index.tsx +378 -378
- package/src/components/agent-helper/offset-preference-select/index.tsx +268 -268
- package/src/components/agent-helper/offset-type-select/index.tsx +66 -66
- package/src/components/agent-helper/offset-unit-select/index.tsx +54 -54
- package/src/components/agent-helper/placeholder-card/index.tsx +75 -75
- package/src/components/agent-helper/prompt-input/index.tsx +646 -640
- package/src/components/agent-helper/relational-operator-select/index.tsx +179 -179
- package/src/components/agent-helper/repeat-by-type-select/index.tsx +53 -53
- package/src/components/agent-helper/repeat-preference-select/index.tsx +162 -162
- package/src/components/agent-helper/repeat-preference-type-select/index.tsx +58 -58
- package/src/components/agent-helper/search-input/index.tsx +35 -35
- package/src/components/agent-helper/table/index.tsx +114 -114
- package/src/components/agent-helper/trigger-select/index.tsx +121 -121
- package/src/components/agent-helper/trigger-time-preference-select/index.tsx +189 -189
- package/src/components/agent-helper/trigger-time-source-type-select/index.tsx +53 -53
- package/src/components/agent-helper/weekdays-select/index.tsx +230 -230
- package/src/components/agent-scroll-selector.tsx +193 -193
- package/src/components/agent-select.tsx +154 -154
- package/src/components/attachment-part-group.tsx +35 -35
- package/src/components/attachment-part.tsx +296 -296
- package/src/components/attachments-previewer.tsx +272 -272
- package/src/components/base-button.tsx +49 -49
- package/src/components/border-animation.tsx +107 -107
- package/src/components/border-color-animation.tsx +66 -66
- package/src/components/bot-avatar-name.tsx +36 -36
- package/src/components/bot-icon.tsx +23 -23
- package/src/components/bs-entry-select/app-group-folder.tsx +61 -61
- package/src/components/bs-entry-select/bs-entry-select.tsx +296 -296
- package/src/components/bs-entry-select/build-tree.ts +265 -265
- package/src/components/bs-entry-select/entry-item.tsx +45 -45
- package/src/components/bs-entry-select/entry-popover.tsx +99 -99
- package/src/components/bs-entry-select/index.ts +7 -7
- package/src/components/bs-entry-select/other-app-section.tsx +96 -96
- package/src/components/bs-entry-select/scroll-slider.tsx +872 -872
- package/src/components/bs-entry-select/search-input.tsx +4 -4
- package/src/components/bs-entry-select/tree-item-node.tsx +84 -84
- package/src/components/bs-entry-select/types.ts +46 -46
- package/src/components/bs-icons.tsx +1362 -1362
- package/src/components/card-field.tsx +23 -23
- package/src/components/card.tsx +36 -36
- package/src/components/chat-area-header.tsx +65 -65
- package/src/components/check-card.tsx +77 -77
- package/src/components/circle-checker.tsx +66 -66
- package/src/components/collapsible-txt-msg.tsx +75 -75
- package/src/components/collapsible.tsx +9 -9
- package/src/components/confirm-dialog.tsx +139 -139
- package/src/components/date-range-picker.tsx +91 -91
- package/src/components/fake-bot-msg.tsx +51 -51
- package/src/components/field-checker-list-msg.tsx +146 -146
- package/src/components/field-checker.tsx +185 -185
- package/src/components/field-icon.tsx +13 -13
- package/src/components/field-value-checker.tsx +116 -116
- package/src/components/fields-design-info-table.tsx +160 -160
- package/src/components/fields-generating-indicator.tsx +95 -95
- package/src/components/fields-portal.tsx +18 -18
- package/src/components/fields-previewer.tsx +216 -216
- package/src/components/font-icon.tsx +65 -65
- package/src/components/form-info-editor.tsx +263 -263
- package/src/components/formula-tag.tsx +32 -32
- package/src/components/generate-animation.tsx +56 -56
- package/src/components/icon-btn.tsx +43 -43
- package/src/components/icon-card.tsx +67 -67
- package/src/components/icons/calendar-icon.tsx +5 -5
- package/src/components/icons/clear-all-icon.tsx +5 -5
- package/src/components/icons/empty-agent-icon.tsx +93 -93
- package/src/components/icons/empty-table-icon.tsx +104 -104
- package/src/components/icons/grids-icon.tsx +5 -5
- package/src/components/img-part.tsx +27 -27
- package/src/components/legacy-slot.tsx +110 -110
- package/src/components/line-checker.tsx +168 -168
- package/src/components/linear-gradient-border-btn.tsx +48 -48
- package/src/components/linear-gradient-color-bg-animation.tsx +103 -103
- package/src/components/memoized-markdown.tsx +32 -32
- package/src/components/message-content.tsx +21 -21
- package/src/components/number-part.tsx +46 -46
- package/src/components/office-container.tsx +289 -265
- package/src/components/option-item.tsx +27 -27
- package/src/components/output-config-editor.tsx +478 -478
- package/src/components/previewer-header.tsx +270 -270
- package/src/components/primary-confirm-btn.tsx +58 -58
- package/src/components/primary-entry-btn.tsx +48 -48
- package/src/components/radio-card.tsx +63 -63
- package/src/components/scroll-to-bottom-btn.tsx +28 -28
- package/src/components/split-line.tsx +9 -9
- package/src/components/square-checker.tsx +87 -87
- package/src/components/subform-fields-value-checker.tsx +184 -184
- package/src/components/tab-radio-group.tsx +83 -83
- package/src/components/tooltip.tsx +53 -53
- package/src/components/ui/button.tsx +51 -51
- package/src/components/ui/calendar.tsx +57 -57
- package/src/components/ui/dialog.tsx +104 -104
- package/src/components/ui/drawer.tsx +279 -279
- package/src/components/ui/form.tsx +202 -202
- package/src/components/ui/icons.tsx +1267 -1267
- package/src/components/ui/input.tsx +28 -28
- package/src/components/ui/label.tsx +30 -30
- package/src/components/ui/popover.tsx +85 -85
- package/src/components/ui/select.tsx +236 -236
- package/src/components/ui/spinner.tsx +17 -17
- package/src/components/ui/switch.tsx +33 -33
- package/src/components/ui/tabs.tsx +62 -62
- package/src/components/ui/textarea.tsx +28 -28
- package/src/components/ui/tooltip.tsx +53 -53
- package/src/components/user-txt-msg-bubble.tsx +47 -47
- package/src/components/warning-msg.tsx +27 -27
- package/src/const/ui.ts +119 -119
- package/src/css.d.ts +4 -4
- package/src/hooks/use-column-appear.ts +78 -78
- package/src/hooks/use-portal-container.ts +27 -27
- package/src/hooks/use-resizable-width.ts +129 -129
- package/src/hooks/use-select-popover-position.ts +74 -74
- package/src/hooks/use-shadow.ts +127 -127
- package/src/index.ts +226 -226
- package/src/lib/utils.ts +138 -138
- package/src/stories/AbsFullscreenGradientBg.stories.tsx +36 -36
- package/src/stories/AgentCategorySelectMenu.stories.tsx +33 -33
- package/src/stories/AgentEntryCard.stories.tsx +90 -90
- package/src/stories/AgentHelperCard.stories.tsx +105 -105
- package/src/stories/AgentHelperCardList.stories.tsx +120 -120
- package/src/stories/AgentHelperMainLayout.stories.tsx +71 -71
- package/src/stories/AgentScrollSelector.stories.tsx +95 -95
- package/src/stories/AgentSelect.stories.tsx +134 -134
- package/src/stories/AgentTag.stories.tsx +44 -44
- package/src/stories/AppSelect.stories.tsx +250 -250
- package/src/stories/AttachmentPart.stories.tsx +122 -122
- package/src/stories/AttachmentPartGroup.stories.tsx +152 -152
- package/src/stories/AttachmentsPreviewer.stories.tsx +306 -306
- package/src/stories/BaseButton.stories.tsx +33 -33
- package/src/stories/BorderAnimation.stories.tsx +116 -116
- package/src/stories/BorderColorAnimation.stories.tsx +63 -63
- package/src/stories/BotAvatarAndName.stories.ts +24 -24
- package/src/stories/BotIcon.stories.ts +22 -22
- package/src/stories/BsConditionInput.stories.tsx +460 -460
- package/src/stories/BsEntrySelect.stories.tsx +343 -343
- package/src/stories/BsTooltip.stories.tsx +32 -32
- package/src/stories/Card.stories.tsx +28 -28
- package/src/stories/CardField.stories.tsx +87 -87
- package/src/stories/ChatAreaHeader.stories.ts +26 -26
- package/src/stories/CheckCard.stories.tsx +114 -114
- package/src/stories/CircleChecker.stories.tsx +89 -89
- package/src/stories/CollapsibleTextMsg.stories.ts +44 -44
- package/src/stories/ConditionGroup.stories.tsx +216 -216
- package/src/stories/ConditionInput.stories.tsx +502 -502
- package/src/stories/ConditionItem.stories.tsx +163 -163
- package/src/stories/ConditonQuantifierSelect.stories.tsx +54 -54
- package/src/stories/ConditonTriggerDescription.stories.tsx +61 -61
- package/src/stories/ConfirmDialog.stories.tsx +123 -123
- package/src/stories/DataTable.stories.tsx +191 -191
- package/src/stories/DatePicker.stories.tsx +174 -174
- package/src/stories/ExecLogFilter.stories.tsx +145 -145
- package/src/stories/FakeBotMsg.stories.tsx +36 -36
- package/src/stories/FieldEditPreferenceSelect.stories.tsx +161 -161
- package/src/stories/FieldSelect.stories.tsx +228 -228
- package/src/stories/FieldValueInput.stories.tsx +442 -442
- package/src/stories/FlowNodeActionTriggerSelect.stories.tsx +80 -80
- package/src/stories/Form.stories.tsx +302 -302
- package/src/stories/FormActionTriggerSelect.stories.tsx +80 -80
- package/src/stories/FormDivider.stories.tsx +31 -31
- package/src/stories/FormInfoEditor.stories.tsx +173 -173
- package/src/stories/FormSelect.stories.tsx +222 -222
- package/src/stories/FormulaTag.stories.tsx +68 -68
- package/src/stories/GenerateAnimation.stories.ts +20 -20
- package/src/stories/IconBtn.stories.tsx +32 -32
- package/src/stories/IconCard.stories.tsx +35 -35
- package/src/stories/IconLib.tsx +45 -45
- package/src/stories/Icons.stories.ts +22 -22
- package/src/stories/ImgMsgPart.stories.tsx +42 -42
- package/src/stories/Input.stories.tsx +102 -102
- package/src/stories/Label.stories.tsx +66 -66
- package/src/stories/LegacySlot.stories.tsx +56 -56
- package/src/stories/LineChecker.stories.tsx +133 -133
- package/src/stories/LinearGradientBorderBtn.stories.tsx +86 -86
- package/src/stories/LinearGradientColorBgAnimation.stories.tsx +60 -60
- package/src/stories/MemoizedMarkdown.stories.tsx +102 -102
- package/src/stories/MessageContent.stories.tsx +36 -36
- package/src/stories/MonthdaysSelect.stories.tsx +78 -78
- package/src/stories/NumbersMsgPart.stories.tsx +42 -42
- package/src/stories/OfficeContainer.stories.tsx +287 -287
- package/src/stories/OffsetPreferenceSelect.stories.tsx +118 -118
- package/src/stories/OffsetTypeSelect.stories.tsx +75 -75
- package/src/stories/OffsetUnitSelect.stories.tsx +61 -61
- package/src/stories/OptionItem.stories.tsx +77 -77
- package/src/stories/OutputConfigEditor.stories.tsx +96 -96
- package/src/stories/PlaceholderCard.stories.tsx +35 -35
- package/src/stories/PreviewerHeader.stories.tsx +91 -91
- package/src/stories/PrimaryConfirmBtn.stories.tsx +60 -60
- package/src/stories/PrimaryEntryBtn.stories.tsx +74 -74
- package/src/stories/PromptInput.stories.tsx +228 -228
- package/src/stories/RadioCard.stories.tsx +103 -103
- package/src/stories/RelationalOperatorSelect.stories.tsx +61 -61
- package/src/stories/RepeatByTypeSelect.stories.tsx +58 -58
- package/src/stories/RepeatPreferenceSelect.stories.tsx +107 -107
- package/src/stories/RepeatPreferenceTypeSelect.stories.tsx +58 -58
- package/src/stories/ScrollToBottom.stories.tsx +24 -24
- package/src/stories/SearchInput.stories.tsx +50 -50
- package/src/stories/Select.stories.tsx +128 -128
- package/src/stories/SplitLine.stories.tsx +40 -40
- package/src/stories/SquareChecker.stories.tsx +96 -96
- package/src/stories/TabRadioGroup.stories.tsx +84 -84
- package/src/stories/Textarea.stories.tsx +67 -67
- package/src/stories/TriggerSelect.stories.tsx +48 -48
- package/src/stories/TriggerTimePreferenceSelect.stories.tsx +106 -106
- package/src/stories/TriggerTimeSourceTypeSelect.stories.tsx +68 -68
- package/src/stories/UserTextMsgBubble.stories.ts +24 -24
- package/src/stories/WarningMessage.stories.tsx +23 -23
- package/src/stories/WeekdaysSelect.stories.tsx +66 -66
- package/src/stories/assets/demo.css +539 -539
- package/src/stories/assets/demo_index.html +7988 -7988
- package/src/stories/assets/iconfont.css +1377 -1377
- package/src/stories/assets/iconfont.json +2382 -2382
- package/src/stories/assets/iconfont.svg +697 -697
- package/src/stories/collapsible.stories.tsx +47 -47
- package/src/stories/drawer.stories.tsx +196 -196
- package/src/stories/field-checker-list-msg.stories.tsx +95 -95
- package/src/stories/field-checker.stories.tsx +117 -117
- package/src/stories/field-icon.stories.tsx +70 -70
- package/src/stories/field-value-checker.stories.tsx +113 -113
- package/src/stories/fields-design-info-table.stories.tsx +203 -203
- package/src/stories/fields-generating.stories.tsx +41 -41
- package/src/stories/fields-previewer.stories.tsx +77 -77
- package/src/stories/subform-fields-value-checker.stories.tsx +98 -98
- package/src/style.css +226 -226
- package/src/types.ts +8 -8
- package/style.css +3652 -3652
- package/tsconfig.json +13 -13
- package/vite.config.ts +44 -44
|
@@ -1,559 +1,559 @@
|
|
|
1
|
-
import { useState, useCallback, useMemo, useRef } from 'react';
|
|
2
|
-
import { cn } from '../../../lib/utils';
|
|
3
|
-
import { transCls } from '../../../const/ui';
|
|
4
|
-
import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
|
|
5
|
-
import { SquareChecker } from '../../square-checker';
|
|
6
|
-
import type { IAppGroup, IAppGroupItem } from '@baishuyun/types';
|
|
7
|
-
import { AppIcon } from '../../font-icon';
|
|
8
|
-
import { SearchIcon } from '../../bs-icons';
|
|
9
|
-
|
|
10
|
-
// ── Tree Node Type ────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
export interface AppTreeNode {
|
|
13
|
-
id: string;
|
|
14
|
-
label: string;
|
|
15
|
-
type: 'group' | 'app' | 'item';
|
|
16
|
-
color?: string;
|
|
17
|
-
children?: AppTreeNode[];
|
|
18
|
-
/** Original app data (for leaf app nodes) */
|
|
19
|
-
data?: IAppGroupItem;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ── Converter: IAppGroup[] → AppTreeNode[] ────────────────────────
|
|
23
|
-
|
|
24
|
-
export function groupsToAppTreeNodes(groups: IAppGroup[]): AppTreeNode[] {
|
|
25
|
-
return groups.map((group) => ({
|
|
26
|
-
id: group._id,
|
|
27
|
-
label: group.name,
|
|
28
|
-
type: 'group' as const,
|
|
29
|
-
children: group.apps.map((app) => ({
|
|
30
|
-
id: app._id,
|
|
31
|
-
label: app.appname,
|
|
32
|
-
type: 'app' as const,
|
|
33
|
-
color: String(app.style?.color || '#666666'),
|
|
34
|
-
data: app,
|
|
35
|
-
})),
|
|
36
|
-
}));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// ── Inline SVG Icons ──────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
/** Folder/group icon (16x16) */
|
|
42
|
-
const FolderIcon = () => (
|
|
43
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
|
44
|
-
<path d="M13.9004 15H2.65039C2.25257 15 1.87104 14.842 1.58973 14.5607C1.30843 14.2794 1.15039 13.8978 1.15039 13.5V4.5C1.15039 4.10218 1.30843 3.72064 1.58973 3.43934C1.87104 3.15804 2.25257 3 2.65039 3H7.15039L8.65039 4.5H13.9004C14.0974 4.5 14.2924 4.5388 14.4744 4.61418C14.6564 4.68956 14.8218 4.80005 14.9611 4.93934C15.1003 5.07863 15.2108 5.24399 15.2862 5.42597C15.3616 5.60796 15.4004 5.80302 15.4004 6H2.65039V13.5L4.25539 7.5H16.0754C16.1927 7.49595 16.3093 7.51947 16.4158 7.56867C16.5223 7.61788 16.6158 7.69139 16.6888 7.78331C16.7617 7.87523 16.8121 7.98298 16.8358 8.0979C16.8595 8.21282 16.856 8.33171 16.8254 8.445L15.3704 13.875C15.2865 14.1999 15.0961 14.4873 14.8296 14.6913C14.5631 14.8952 14.2359 15.0039 13.9004 15Z" fill="#6F81BD"/>
|
|
45
|
-
</svg>
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
/** App grid icon (16x16), colorized */
|
|
49
|
-
// const AppGridIcon = ({ color }: { color?: string }) => {
|
|
50
|
-
// const c = color || '#666666';
|
|
51
|
-
// return (
|
|
52
|
-
// <svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="shrink-0">
|
|
53
|
-
// <rect x="1.5" y="1.5" width="5" height="5" rx="1" fill={c} opacity="0.2" />
|
|
54
|
-
// <rect x="1.5" y="1.5" width="5" height="5" rx="1" stroke={c} strokeWidth="1.2" />
|
|
55
|
-
// <rect x="9.5" y="1.5" width="5" height="5" rx="1" fill={c} opacity="0.2" />
|
|
56
|
-
// <rect x="9.5" y="1.5" width="5" height="5" rx="1" stroke={c} strokeWidth="1.2" />
|
|
57
|
-
// <rect x="1.5" y="9.5" width="5" height="5" rx="1" fill={c} opacity="0.2" />
|
|
58
|
-
// <rect x="1.5" y="9.5" width="5" height="5" rx="1" stroke={c} strokeWidth="1.2" />
|
|
59
|
-
// <rect x="9.5" y="9.5" width="5" height="5" rx="1" fill={c} opacity="0.2" />
|
|
60
|
-
// <rect x="9.5" y="9.5" width="5" height="5" rx="1" stroke={c} strokeWidth="1.2" />
|
|
61
|
-
// </svg>
|
|
62
|
-
// );
|
|
63
|
-
// };
|
|
64
|
-
|
|
65
|
-
/** Generic item icon (16x16), colorized — used for leaf items */
|
|
66
|
-
// const ItemIcon = ({ color }: { color?: string }) => (
|
|
67
|
-
// <AppGridIcon color={color} />
|
|
68
|
-
// );
|
|
69
|
-
|
|
70
|
-
/** Collapse/expand toggle (18x18) — shown on expandable parent nodes */
|
|
71
|
-
const ToggleIcon = ({ expanded }: { expanded: boolean }) => (
|
|
72
|
-
<svg
|
|
73
|
-
width="18" height="18" viewBox="0 0 18 18" fill="none"
|
|
74
|
-
className={cn('shrink-0', transCls)}
|
|
75
|
-
style={{ transform: expanded ? 'rotate(0deg)' : 'rotate(-90deg)' }}
|
|
76
|
-
>
|
|
77
|
-
<path d="M6 8L9 11L12 8" stroke="#666666" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
78
|
-
</svg>
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
/** Close X icon (12x12) for chip dismiss */
|
|
82
|
-
const CloseIcon = () => (
|
|
83
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className="shrink-0">
|
|
84
|
-
<path d="M3 3L9 9M9 3L3 9" stroke="#999999" strokeWidth="1.2" strokeLinecap="round" />
|
|
85
|
-
</svg>
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
/** More dots icon (12x12) for overflow chip */
|
|
89
|
-
const MoreIcon = () => (
|
|
90
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className="shrink-0">
|
|
91
|
-
<circle cx="2" cy="6" r="1" fill="#999999" />
|
|
92
|
-
<circle cx="6" cy="6" r="1" fill="#999999" />
|
|
93
|
-
<circle cx="10" cy="6" r="1" fill="#999999" />
|
|
94
|
-
</svg>
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
/** Dropdown chevron in trigger — matches Figma trigger icon */
|
|
98
|
-
const DropdownChevron = ({ open }: { open: boolean }) => (
|
|
99
|
-
<svg
|
|
100
|
-
width="16" height="16" viewBox="0 0 16 16" fill="none"
|
|
101
|
-
className={cn('shrink-0', transCls)}
|
|
102
|
-
style={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
|
103
|
-
>
|
|
104
|
-
<path d="M4 6L8 10L12 6" stroke="#666666" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
105
|
-
</svg>
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// ── Types ─────────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
export const ALL_APPS_VALUE = '__all__';
|
|
111
|
-
|
|
112
|
-
export interface AppSelectProps {
|
|
113
|
-
/** Tree nodes to display. Use groupsToAppTreeNodes() to convert IAppGroup[] */
|
|
114
|
-
nodes: AppTreeNode[];
|
|
115
|
-
/** Selection mode */
|
|
116
|
-
mode?: 'single' | 'multiple';
|
|
117
|
-
/** Controlled value — selected node id(s) */
|
|
118
|
-
value?: string | string[];
|
|
119
|
-
/** Uncontrolled default value */
|
|
120
|
-
defaultValue?: string | string[];
|
|
121
|
-
/** Change handler — returns selected id(s) */
|
|
122
|
-
onChange?: (value: string | string[]) => void;
|
|
123
|
-
/** Placeholder when no selection */
|
|
124
|
-
placeholder?: string;
|
|
125
|
-
/** Search placeholder */
|
|
126
|
-
searchPlaceholder?: string;
|
|
127
|
-
/** Disable the control */
|
|
128
|
-
disabled?: boolean;
|
|
129
|
-
/** Max visible chips in multi-select trigger (default 4) */
|
|
130
|
-
maxVisibleChips?: number;
|
|
131
|
-
/** Additional className */
|
|
132
|
-
className?: string;
|
|
133
|
-
/** Show a "全部应用" (All Apps) item at the top. Only effective in single mode. */
|
|
134
|
-
showAll?: boolean;
|
|
135
|
-
/** Allow selecting an empty value as the first option. Only effective in single mode. */
|
|
136
|
-
allowEmpty?: boolean;
|
|
137
|
-
/** Label for the empty option. Default "无". */
|
|
138
|
-
emptyLabel?: string;
|
|
139
|
-
popoverContentOnly?: boolean; // for testing: render only the popover content without the trigger
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ── Helpers ───────────────────────────────────────────────────────
|
|
143
|
-
|
|
144
|
-
/** Flatten tree into a map of id → node */
|
|
145
|
-
function flattenNodes(nodes: AppTreeNode[]): Map<string, AppTreeNode> {
|
|
146
|
-
const map = new Map<string, AppTreeNode>();
|
|
147
|
-
const walk = (list: AppTreeNode[]) => {
|
|
148
|
-
for (const n of list) {
|
|
149
|
-
map.set(n.id, n);
|
|
150
|
-
if (n.children) walk(n.children);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
walk(nodes);
|
|
154
|
-
return map;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── Component ─────────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
export const AppSelect = ({
|
|
160
|
-
nodes,
|
|
161
|
-
mode = 'single',
|
|
162
|
-
popoverContentOnly,
|
|
163
|
-
showAll = false,
|
|
164
|
-
allowEmpty = false,
|
|
165
|
-
emptyLabel = '无',
|
|
166
|
-
value: controlledValue,
|
|
167
|
-
defaultValue,
|
|
168
|
-
onChange,
|
|
169
|
-
placeholder = '请选择',
|
|
170
|
-
searchPlaceholder = '输入应用名称来搜索',
|
|
171
|
-
disabled = false,
|
|
172
|
-
maxVisibleChips = 4,
|
|
173
|
-
className,
|
|
174
|
-
}: AppSelectProps) => {
|
|
175
|
-
const [open, setOpen] = useState(false);
|
|
176
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
177
|
-
const [internalValue, setInternalValue] = useState<string | string[]>(
|
|
178
|
-
defaultValue ?? (mode === 'multiple' ? [] : '')
|
|
179
|
-
);
|
|
180
|
-
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
181
|
-
const [triggerWidth, setTriggerWidth] = useState<number>(0);
|
|
182
|
-
const triggerBtnRef = useRef<HTMLButtonElement | null>(null);
|
|
183
|
-
|
|
184
|
-
// Expanded node IDs (only nodes with children can be expanded)
|
|
185
|
-
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => {
|
|
186
|
-
// Start with root nodes expanded
|
|
187
|
-
const s = new Set<string>();
|
|
188
|
-
const initExpand = (list: AppTreeNode[], depth: number) => {
|
|
189
|
-
for (const n of list) {
|
|
190
|
-
if (n.children && n.children.length > 0 && depth < 2) {
|
|
191
|
-
s.add(n.id);
|
|
192
|
-
initExpand(n.children, depth + 1);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
initExpand(nodes, 0);
|
|
197
|
-
return s;
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Controlled vs uncontrolled value
|
|
201
|
-
const resolvedValue = controlledValue !== undefined ? controlledValue : internalValue;
|
|
202
|
-
|
|
203
|
-
// Flat lookup
|
|
204
|
-
const nodeMap = useMemo(() => flattenNodes(nodes), [nodes]);
|
|
205
|
-
|
|
206
|
-
// Selected ids as Set
|
|
207
|
-
const selectedIds = useMemo(() => {
|
|
208
|
-
if (mode === 'multiple') {
|
|
209
|
-
return new Set(Array.isArray(resolvedValue) ? resolvedValue : []);
|
|
210
|
-
}
|
|
211
|
-
return new Set(typeof resolvedValue === 'string' && resolvedValue ? [resolvedValue] : []);
|
|
212
|
-
}, [resolvedValue, mode]);
|
|
213
|
-
|
|
214
|
-
// Selected labels for trigger
|
|
215
|
-
const selectedLabels = useMemo(() => {
|
|
216
|
-
const ids = Array.isArray(resolvedValue) ? resolvedValue : (resolvedValue ? [resolvedValue] : []);
|
|
217
|
-
return ids.map((id) => nodeMap.get(id)?.label ?? id);
|
|
218
|
-
}, [resolvedValue, nodeMap]);
|
|
219
|
-
|
|
220
|
-
const selectedAppLabel =
|
|
221
|
-
mode === 'single' && selectedLabels.length === 1
|
|
222
|
-
? selectedLabels[0] === ALL_APPS_VALUE
|
|
223
|
-
? '全部应用'
|
|
224
|
-
: selectedLabels[0]
|
|
225
|
-
: null;
|
|
226
|
-
|
|
227
|
-
// ── Tree filter by search ──────────────────────────────────────
|
|
228
|
-
|
|
229
|
-
const filteredNodes = useMemo(() => {
|
|
230
|
-
if (!searchQuery.trim()) return nodes;
|
|
231
|
-
const q = searchQuery.toLowerCase();
|
|
232
|
-
|
|
233
|
-
const filterTree = (list: AppTreeNode[]): AppTreeNode[] => {
|
|
234
|
-
const result: AppTreeNode[] = [];
|
|
235
|
-
for (const node of list) {
|
|
236
|
-
const labelMatch = node?.label?.toLowerCase().includes(q);
|
|
237
|
-
const filteredChildren = node.children ? filterTree(node.children) : [];
|
|
238
|
-
if (labelMatch || filteredChildren.length > 0) {
|
|
239
|
-
result.push({ ...node, children: filteredChildren.length > 0 ? filteredChildren : node.children });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return result;
|
|
243
|
-
};
|
|
244
|
-
return filterTree(nodes);
|
|
245
|
-
}, [nodes, searchQuery]);
|
|
246
|
-
|
|
247
|
-
// ── Shadow DOM portal ref ──────────────────────────────────────
|
|
248
|
-
|
|
249
|
-
const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
|
|
250
|
-
triggerBtnRef.current = node;
|
|
251
|
-
if (node) setTriggerWidth(node.offsetWidth);
|
|
252
|
-
if (!node) { setPortalContainer(null); return; }
|
|
253
|
-
const rootNode = node.getRootNode();
|
|
254
|
-
if (rootNode instanceof ShadowRoot) {
|
|
255
|
-
const existing = rootNode.querySelector<HTMLElement>('[data-bs-app-select-portal]');
|
|
256
|
-
if (existing) { setPortalContainer(existing); return; }
|
|
257
|
-
const container = document.createElement('div');
|
|
258
|
-
container.setAttribute('data-bs-app-select-portal', 'true');
|
|
259
|
-
rootNode.appendChild(container);
|
|
260
|
-
setPortalContainer(container);
|
|
261
|
-
} else {
|
|
262
|
-
setPortalContainer(null);
|
|
263
|
-
}
|
|
264
|
-
}, []);
|
|
265
|
-
|
|
266
|
-
// ── Handlers ────────────────────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
const updateValue = useCallback(
|
|
269
|
-
(newValue: string | string[]) => {
|
|
270
|
-
if (controlledValue === undefined) setInternalValue(newValue);
|
|
271
|
-
onChange?.(newValue);
|
|
272
|
-
},
|
|
273
|
-
[controlledValue, onChange]
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
const toggleExpanded = useCallback((nodeId: string) => {
|
|
277
|
-
setExpandedIds((prev) => {
|
|
278
|
-
const next = new Set(prev);
|
|
279
|
-
if (next.has(nodeId)) next.delete(nodeId);
|
|
280
|
-
else next.add(nodeId);
|
|
281
|
-
return next;
|
|
282
|
-
});
|
|
283
|
-
}, []);
|
|
284
|
-
|
|
285
|
-
const handleSelect = useCallback(
|
|
286
|
-
(nodeId: string, node: AppTreeNode) => {
|
|
287
|
-
// Groups toggle expand; never selectable
|
|
288
|
-
if (node.type === 'group' || (node.children && node.children.length > 0)) {
|
|
289
|
-
toggleExpanded(nodeId);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Leaf node selection
|
|
294
|
-
if (mode === 'single') {
|
|
295
|
-
updateValue(nodeId);
|
|
296
|
-
setOpen(false);
|
|
297
|
-
} else {
|
|
298
|
-
const currentIds = Array.isArray(resolvedValue) ? [...resolvedValue] : [];
|
|
299
|
-
const idx = currentIds.indexOf(nodeId);
|
|
300
|
-
if (idx >= 0) {
|
|
301
|
-
currentIds.splice(idx, 1);
|
|
302
|
-
} else {
|
|
303
|
-
currentIds.push(nodeId);
|
|
304
|
-
}
|
|
305
|
-
updateValue(currentIds);
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
[mode, resolvedValue, updateValue, toggleExpanded]
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
const handleRemoveChip = useCallback(
|
|
312
|
-
(nodeId: string, e: React.MouseEvent) => {
|
|
313
|
-
e.stopPropagation();
|
|
314
|
-
if (mode === 'multiple' && Array.isArray(resolvedValue)) {
|
|
315
|
-
updateValue(resolvedValue.filter((id) => id !== nodeId));
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
[mode, resolvedValue, updateValue]
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
const handleOpenChange = useCallback(
|
|
322
|
-
(nextOpen: boolean) => {
|
|
323
|
-
if (disabled) return;
|
|
324
|
-
setOpen(nextOpen);
|
|
325
|
-
if (nextOpen && triggerBtnRef.current) {
|
|
326
|
-
setTriggerWidth(triggerBtnRef.current.offsetWidth);
|
|
327
|
-
}
|
|
328
|
-
if (!nextOpen) setSearchQuery('');
|
|
329
|
-
},
|
|
330
|
-
[disabled]
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
// ── Chip visibility ─────────────────────────────────────────────
|
|
334
|
-
|
|
335
|
-
const multiSelectedLabels = mode === 'multiple' ? selectedLabels : [];
|
|
336
|
-
const visibleChips = multiSelectedLabels.slice(0, maxVisibleChips);
|
|
337
|
-
const overflowCount = multiSelectedLabels.length - visibleChips.length;
|
|
338
|
-
|
|
339
|
-
// ── Tree row rendering ──────────────────────────────────────────
|
|
340
|
-
|
|
341
|
-
const renderNodeRow = useCallback(
|
|
342
|
-
(node: AppTreeNode, depth: number) => {
|
|
343
|
-
const hasChildren = node.children && node.children.length > 0;
|
|
344
|
-
const isExpanded = expandedIds.has(node.id);
|
|
345
|
-
const isSelected = selectedIds.has(node.id);
|
|
346
|
-
const leftPad = depth * 22 + 10; // 10, 32, 54, 78, 102...
|
|
347
|
-
|
|
348
|
-
return (
|
|
349
|
-
<div key={node.id}>
|
|
350
|
-
<button
|
|
351
|
-
type="button"
|
|
352
|
-
onClick={() => handleSelect(node.id, node)}
|
|
353
|
-
className={cn(
|
|
354
|
-
'flex items-center gap-[6px] w-full h-[34px] rounded-[4px] text-left',
|
|
355
|
-
'pr-[10px]',
|
|
356
|
-
transCls,
|
|
357
|
-
isSelected
|
|
358
|
-
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
359
|
-
: 'text-[#030303] hover:bg-[#EFF0F6]'
|
|
360
|
-
)}
|
|
361
|
-
style={{ paddingLeft: `${leftPad}px`, paddingTop: '10px', paddingBottom: '10px' }}
|
|
362
|
-
>
|
|
363
|
-
{/* Left side: toggle icon for expandable nodes */}
|
|
364
|
-
{hasChildren && <ToggleIcon expanded={isExpanded} />}
|
|
365
|
-
|
|
366
|
-
{/* Node icon */}
|
|
367
|
-
{node.type === 'group' && !hasChildren && <FolderIcon />}
|
|
368
|
-
{node.type === 'group' && hasChildren && <FolderIcon />}
|
|
369
|
-
{node.type === 'app' && <AppIcon name={node.data?.style.icon!} />}
|
|
370
|
-
{/* {node.type === 'item' && <ItemIcon color={isSelected ? '#0265FF' : node.color} />} */}
|
|
371
|
-
|
|
372
|
-
{/* Label */}
|
|
373
|
-
<span className="text-[14px] truncate leading-[normal] flex-1 min-w-0">
|
|
374
|
-
{node.label}
|
|
375
|
-
</span>
|
|
376
|
-
|
|
377
|
-
{/* SquareChecker for multi-select on leaf nodes — always visible */}
|
|
378
|
-
{mode === 'multiple' && !hasChildren && node.type !== 'group' && (
|
|
379
|
-
<span onClick={(e) => e.stopPropagation()} className='grid place-items-center'>
|
|
380
|
-
<SquareChecker
|
|
381
|
-
checked={isSelected}
|
|
382
|
-
onCheckedChange={() => handleSelect(node.id, node)}
|
|
383
|
-
className="shrink-0"
|
|
384
|
-
/>
|
|
385
|
-
</span>
|
|
386
|
-
)}
|
|
387
|
-
</button>
|
|
388
|
-
|
|
389
|
-
{/* Children (when expanded) */}
|
|
390
|
-
{hasChildren && isExpanded && (
|
|
391
|
-
<div>
|
|
392
|
-
{node.children!.map((child) => renderNodeRow(child, depth + 1))}
|
|
393
|
-
</div>
|
|
394
|
-
)}
|
|
395
|
-
</div>
|
|
396
|
-
);
|
|
397
|
-
},
|
|
398
|
-
[expandedIds, selectedIds, mode, handleSelect]
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
// ── Render ──────────────────────────────────────────────────────
|
|
402
|
-
|
|
403
|
-
const hasMultiSelection = mode === 'multiple' && multiSelectedLabels.length > 0;
|
|
404
|
-
|
|
405
|
-
const popoverContent = (
|
|
406
|
-
<div className="flex flex-col gap-[4px]">
|
|
407
|
-
{/* Search */}
|
|
408
|
-
<div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
|
|
409
|
-
<SearchIcon />
|
|
410
|
-
<input
|
|
411
|
-
type="text"
|
|
412
|
-
value={searchQuery}
|
|
413
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
414
|
-
placeholder={searchPlaceholder}
|
|
415
|
-
className="flex-1 outline-none text-[14px] placeholder:text-[#999999] bg-transparent min-w-0 text-[#030303]"
|
|
416
|
-
/>
|
|
417
|
-
</div>
|
|
418
|
-
|
|
419
|
-
{/* Empty option */}
|
|
420
|
-
{allowEmpty && mode === 'single' && !searchQuery.trim() && (
|
|
421
|
-
<button
|
|
422
|
-
type="button"
|
|
423
|
-
onClick={() => {
|
|
424
|
-
updateValue('');
|
|
425
|
-
setOpen(false);
|
|
426
|
-
}}
|
|
427
|
-
className={cn(
|
|
428
|
-
'flex items-center w-full h-[34px] rounded-[4px] text-left',
|
|
429
|
-
'px-[10px]',
|
|
430
|
-
transCls,
|
|
431
|
-
'text-[#030303] hover:bg-[#EFF0F6]'
|
|
432
|
-
)}
|
|
433
|
-
>
|
|
434
|
-
<span className="text-[14px] leading-[normal]">{emptyLabel}</span>
|
|
435
|
-
</button>
|
|
436
|
-
)}
|
|
437
|
-
|
|
438
|
-
{/* "全部应用" item */}
|
|
439
|
-
{showAll && mode === 'single' && !searchQuery.trim() && (
|
|
440
|
-
<button
|
|
441
|
-
type="button"
|
|
442
|
-
onClick={() => {
|
|
443
|
-
updateValue(ALL_APPS_VALUE);
|
|
444
|
-
setOpen(false);
|
|
445
|
-
}}
|
|
446
|
-
className={cn(
|
|
447
|
-
'flex items-center w-full h-[34px] rounded-[4px] text-left',
|
|
448
|
-
'px-[10px]',
|
|
449
|
-
transCls,
|
|
450
|
-
selectedIds.has(ALL_APPS_VALUE)
|
|
451
|
-
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
452
|
-
: 'text-[#030303] hover:bg-[#EFF0F6]'
|
|
453
|
-
)}
|
|
454
|
-
>
|
|
455
|
-
<span className="text-[14px] leading-[normal]">全部应用</span>
|
|
456
|
-
</button>
|
|
457
|
-
)}
|
|
458
|
-
|
|
459
|
-
{/* Tree */}
|
|
460
|
-
<div className="max-h-[400px] overflow-y-auto">
|
|
461
|
-
{filteredNodes.length > 0 ? (
|
|
462
|
-
filteredNodes.map((node) => renderNodeRow(node, 0))
|
|
463
|
-
) : (
|
|
464
|
-
<div className="flex items-center justify-center h-[60px] text-[12px] text-[#999999]">
|
|
465
|
-
{searchQuery.trim() ? '未找到相关应用' : '暂无应用'}
|
|
466
|
-
</div>
|
|
467
|
-
)}
|
|
468
|
-
</div>
|
|
469
|
-
</div>
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
if (popoverContentOnly) {
|
|
473
|
-
return popoverContent;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return (
|
|
477
|
-
<div className={cn('relative', className)}>
|
|
478
|
-
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
479
|
-
<PopoverTrigger asChild>
|
|
480
|
-
<button
|
|
481
|
-
ref={setTriggerRef}
|
|
482
|
-
type="button"
|
|
483
|
-
disabled={disabled}
|
|
484
|
-
className={cn(
|
|
485
|
-
'flex items-center justify-between w-full h-[34px] gap-2.5',
|
|
486
|
-
'bg-white border border-[#E0E0E0] rounded-[4px]',
|
|
487
|
-
'py-[7px]',
|
|
488
|
-
hasMultiSelection ? 'pl-[4px] pr-[10px]' : 'px-[10px]',
|
|
489
|
-
'text-left cursor-pointer',
|
|
490
|
-
'hover:border-[#0265FF]',
|
|
491
|
-
disabled && 'opacity-50 cursor-not-allowed hover:border-[#E0E0E0]',
|
|
492
|
-
open && 'border-[#0265FF]',
|
|
493
|
-
transCls
|
|
494
|
-
)}
|
|
495
|
-
// style={{ minHeight: '34px' }}
|
|
496
|
-
>
|
|
497
|
-
{hasMultiSelection ? (
|
|
498
|
-
<>
|
|
499
|
-
<div className="flex items-center gap-[4px] min-w-0 flex-1 overflow-x-auto scrollbar-hide">
|
|
500
|
-
{visibleChips.map((label, i) => (
|
|
501
|
-
<span
|
|
502
|
-
key={i}
|
|
503
|
-
className="flex items-center gap-[4px] bg-[#EEEEEE] rounded-[2px] h-[26px] px-[6px] shrink-0 max-w-[160px]"
|
|
504
|
-
onClick={(e) => {
|
|
505
|
-
// stop propagation to prevent opening popover when clicking the chip, but still allow it when clicking the close icon
|
|
506
|
-
e.stopPropagation();
|
|
507
|
-
const ids = Array.isArray(resolvedValue) ? resolvedValue : [];
|
|
508
|
-
if (ids[i]) handleRemoveChip(ids[i], e);
|
|
509
|
-
}}
|
|
510
|
-
>
|
|
511
|
-
<span className="text-[14px] text-[#030303] truncate leading-[normal]">
|
|
512
|
-
{label}
|
|
513
|
-
</span>
|
|
514
|
-
<span
|
|
515
|
-
className="cursor-pointer flex items-center shrink-0 hover:bg-[#EFF0F6]"
|
|
516
|
-
>
|
|
517
|
-
<CloseIcon />
|
|
518
|
-
</span>
|
|
519
|
-
</span>
|
|
520
|
-
))}
|
|
521
|
-
{overflowCount > 0 && (
|
|
522
|
-
<span className="flex items-center gap-[4px] bg-[#EEEEEE] rounded-[2px] h-[26px] px-[6px] shrink-0">
|
|
523
|
-
<span className="text-[14px] text-[#030303] leading-[normal]">
|
|
524
|
-
+{overflowCount}
|
|
525
|
-
</span>
|
|
526
|
-
<MoreIcon />
|
|
527
|
-
</span>
|
|
528
|
-
)}
|
|
529
|
-
</div>
|
|
530
|
-
<DropdownChevron open={open} />
|
|
531
|
-
</>
|
|
532
|
-
) : (
|
|
533
|
-
<>
|
|
534
|
-
<span
|
|
535
|
-
className={cn(
|
|
536
|
-
'text-[14px] leading-[normal] truncate',
|
|
537
|
-
selectedAppLabel ? 'text-[#030303]' : 'text-[#999999]'
|
|
538
|
-
)}
|
|
539
|
-
>
|
|
540
|
-
{selectedAppLabel ?? (allowEmpty ? emptyLabel : placeholder)}
|
|
541
|
-
</span>
|
|
542
|
-
<DropdownChevron open={open} />
|
|
543
|
-
</>
|
|
544
|
-
)}
|
|
545
|
-
</button>
|
|
546
|
-
</PopoverTrigger>
|
|
547
|
-
<PopoverContent
|
|
548
|
-
align="start"
|
|
549
|
-
sideOffset={4}
|
|
550
|
-
portalContainer={portalContainer}
|
|
551
|
-
className="!border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]"
|
|
552
|
-
style={{ minWidth: triggerWidth || undefined }}
|
|
553
|
-
>
|
|
554
|
-
{popoverContent}
|
|
555
|
-
</PopoverContent>
|
|
556
|
-
</Popover>
|
|
557
|
-
</div>
|
|
558
|
-
);
|
|
559
|
-
};
|
|
1
|
+
import { useState, useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { cn } from '../../../lib/utils';
|
|
3
|
+
import { transCls } from '../../../const/ui';
|
|
4
|
+
import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
|
|
5
|
+
import { SquareChecker } from '../../square-checker';
|
|
6
|
+
import type { IAppGroup, IAppGroupItem } from '@baishuyun/types';
|
|
7
|
+
import { AppIcon } from '../../font-icon';
|
|
8
|
+
import { SearchIcon } from '../../bs-icons';
|
|
9
|
+
|
|
10
|
+
// ── Tree Node Type ────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface AppTreeNode {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
type: 'group' | 'app' | 'item';
|
|
16
|
+
color?: string;
|
|
17
|
+
children?: AppTreeNode[];
|
|
18
|
+
/** Original app data (for leaf app nodes) */
|
|
19
|
+
data?: IAppGroupItem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Converter: IAppGroup[] → AppTreeNode[] ────────────────────────
|
|
23
|
+
|
|
24
|
+
export function groupsToAppTreeNodes(groups: IAppGroup[]): AppTreeNode[] {
|
|
25
|
+
return groups.map((group) => ({
|
|
26
|
+
id: group._id,
|
|
27
|
+
label: group.name,
|
|
28
|
+
type: 'group' as const,
|
|
29
|
+
children: group.apps.map((app) => ({
|
|
30
|
+
id: app._id,
|
|
31
|
+
label: app.appname,
|
|
32
|
+
type: 'app' as const,
|
|
33
|
+
color: String(app.style?.color || '#666666'),
|
|
34
|
+
data: app,
|
|
35
|
+
})),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Inline SVG Icons ──────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/** Folder/group icon (16x16) */
|
|
42
|
+
const FolderIcon = () => (
|
|
43
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
|
|
44
|
+
<path d="M13.9004 15H2.65039C2.25257 15 1.87104 14.842 1.58973 14.5607C1.30843 14.2794 1.15039 13.8978 1.15039 13.5V4.5C1.15039 4.10218 1.30843 3.72064 1.58973 3.43934C1.87104 3.15804 2.25257 3 2.65039 3H7.15039L8.65039 4.5H13.9004C14.0974 4.5 14.2924 4.5388 14.4744 4.61418C14.6564 4.68956 14.8218 4.80005 14.9611 4.93934C15.1003 5.07863 15.2108 5.24399 15.2862 5.42597C15.3616 5.60796 15.4004 5.80302 15.4004 6H2.65039V13.5L4.25539 7.5H16.0754C16.1927 7.49595 16.3093 7.51947 16.4158 7.56867C16.5223 7.61788 16.6158 7.69139 16.6888 7.78331C16.7617 7.87523 16.8121 7.98298 16.8358 8.0979C16.8595 8.21282 16.856 8.33171 16.8254 8.445L15.3704 13.875C15.2865 14.1999 15.0961 14.4873 14.8296 14.6913C14.5631 14.8952 14.2359 15.0039 13.9004 15Z" fill="#6F81BD"/>
|
|
45
|
+
</svg>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** App grid icon (16x16), colorized */
|
|
49
|
+
// const AppGridIcon = ({ color }: { color?: string }) => {
|
|
50
|
+
// const c = color || '#666666';
|
|
51
|
+
// return (
|
|
52
|
+
// <svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="shrink-0">
|
|
53
|
+
// <rect x="1.5" y="1.5" width="5" height="5" rx="1" fill={c} opacity="0.2" />
|
|
54
|
+
// <rect x="1.5" y="1.5" width="5" height="5" rx="1" stroke={c} strokeWidth="1.2" />
|
|
55
|
+
// <rect x="9.5" y="1.5" width="5" height="5" rx="1" fill={c} opacity="0.2" />
|
|
56
|
+
// <rect x="9.5" y="1.5" width="5" height="5" rx="1" stroke={c} strokeWidth="1.2" />
|
|
57
|
+
// <rect x="1.5" y="9.5" width="5" height="5" rx="1" fill={c} opacity="0.2" />
|
|
58
|
+
// <rect x="1.5" y="9.5" width="5" height="5" rx="1" stroke={c} strokeWidth="1.2" />
|
|
59
|
+
// <rect x="9.5" y="9.5" width="5" height="5" rx="1" fill={c} opacity="0.2" />
|
|
60
|
+
// <rect x="9.5" y="9.5" width="5" height="5" rx="1" stroke={c} strokeWidth="1.2" />
|
|
61
|
+
// </svg>
|
|
62
|
+
// );
|
|
63
|
+
// };
|
|
64
|
+
|
|
65
|
+
/** Generic item icon (16x16), colorized — used for leaf items */
|
|
66
|
+
// const ItemIcon = ({ color }: { color?: string }) => (
|
|
67
|
+
// <AppGridIcon color={color} />
|
|
68
|
+
// );
|
|
69
|
+
|
|
70
|
+
/** Collapse/expand toggle (18x18) — shown on expandable parent nodes */
|
|
71
|
+
const ToggleIcon = ({ expanded }: { expanded: boolean }) => (
|
|
72
|
+
<svg
|
|
73
|
+
width="18" height="18" viewBox="0 0 18 18" fill="none"
|
|
74
|
+
className={cn('shrink-0', transCls)}
|
|
75
|
+
style={{ transform: expanded ? 'rotate(0deg)' : 'rotate(-90deg)' }}
|
|
76
|
+
>
|
|
77
|
+
<path d="M6 8L9 11L12 8" stroke="#666666" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
78
|
+
</svg>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/** Close X icon (12x12) for chip dismiss */
|
|
82
|
+
const CloseIcon = () => (
|
|
83
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className="shrink-0">
|
|
84
|
+
<path d="M3 3L9 9M9 3L3 9" stroke="#999999" strokeWidth="1.2" strokeLinecap="round" />
|
|
85
|
+
</svg>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
/** More dots icon (12x12) for overflow chip */
|
|
89
|
+
const MoreIcon = () => (
|
|
90
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className="shrink-0">
|
|
91
|
+
<circle cx="2" cy="6" r="1" fill="#999999" />
|
|
92
|
+
<circle cx="6" cy="6" r="1" fill="#999999" />
|
|
93
|
+
<circle cx="10" cy="6" r="1" fill="#999999" />
|
|
94
|
+
</svg>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/** Dropdown chevron in trigger — matches Figma trigger icon */
|
|
98
|
+
const DropdownChevron = ({ open }: { open: boolean }) => (
|
|
99
|
+
<svg
|
|
100
|
+
width="16" height="16" viewBox="0 0 16 16" fill="none"
|
|
101
|
+
className={cn('shrink-0', transCls)}
|
|
102
|
+
style={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
|
103
|
+
>
|
|
104
|
+
<path d="M4 6L8 10L12 6" stroke="#666666" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
105
|
+
</svg>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// ── Types ─────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export const ALL_APPS_VALUE = '__all__';
|
|
111
|
+
|
|
112
|
+
export interface AppSelectProps {
|
|
113
|
+
/** Tree nodes to display. Use groupsToAppTreeNodes() to convert IAppGroup[] */
|
|
114
|
+
nodes: AppTreeNode[];
|
|
115
|
+
/** Selection mode */
|
|
116
|
+
mode?: 'single' | 'multiple';
|
|
117
|
+
/** Controlled value — selected node id(s) */
|
|
118
|
+
value?: string | string[];
|
|
119
|
+
/** Uncontrolled default value */
|
|
120
|
+
defaultValue?: string | string[];
|
|
121
|
+
/** Change handler — returns selected id(s) */
|
|
122
|
+
onChange?: (value: string | string[]) => void;
|
|
123
|
+
/** Placeholder when no selection */
|
|
124
|
+
placeholder?: string;
|
|
125
|
+
/** Search placeholder */
|
|
126
|
+
searchPlaceholder?: string;
|
|
127
|
+
/** Disable the control */
|
|
128
|
+
disabled?: boolean;
|
|
129
|
+
/** Max visible chips in multi-select trigger (default 4) */
|
|
130
|
+
maxVisibleChips?: number;
|
|
131
|
+
/** Additional className */
|
|
132
|
+
className?: string;
|
|
133
|
+
/** Show a "全部应用" (All Apps) item at the top. Only effective in single mode. */
|
|
134
|
+
showAll?: boolean;
|
|
135
|
+
/** Allow selecting an empty value as the first option. Only effective in single mode. */
|
|
136
|
+
allowEmpty?: boolean;
|
|
137
|
+
/** Label for the empty option. Default "无". */
|
|
138
|
+
emptyLabel?: string;
|
|
139
|
+
popoverContentOnly?: boolean; // for testing: render only the popover content without the trigger
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/** Flatten tree into a map of id → node */
|
|
145
|
+
function flattenNodes(nodes: AppTreeNode[]): Map<string, AppTreeNode> {
|
|
146
|
+
const map = new Map<string, AppTreeNode>();
|
|
147
|
+
const walk = (list: AppTreeNode[]) => {
|
|
148
|
+
for (const n of list) {
|
|
149
|
+
map.set(n.id, n);
|
|
150
|
+
if (n.children) walk(n.children);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
walk(nodes);
|
|
154
|
+
return map;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Component ─────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export const AppSelect = ({
|
|
160
|
+
nodes,
|
|
161
|
+
mode = 'single',
|
|
162
|
+
popoverContentOnly,
|
|
163
|
+
showAll = false,
|
|
164
|
+
allowEmpty = false,
|
|
165
|
+
emptyLabel = '无',
|
|
166
|
+
value: controlledValue,
|
|
167
|
+
defaultValue,
|
|
168
|
+
onChange,
|
|
169
|
+
placeholder = '请选择',
|
|
170
|
+
searchPlaceholder = '输入应用名称来搜索',
|
|
171
|
+
disabled = false,
|
|
172
|
+
maxVisibleChips = 4,
|
|
173
|
+
className,
|
|
174
|
+
}: AppSelectProps) => {
|
|
175
|
+
const [open, setOpen] = useState(false);
|
|
176
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
177
|
+
const [internalValue, setInternalValue] = useState<string | string[]>(
|
|
178
|
+
defaultValue ?? (mode === 'multiple' ? [] : '')
|
|
179
|
+
);
|
|
180
|
+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
|
|
181
|
+
const [triggerWidth, setTriggerWidth] = useState<number>(0);
|
|
182
|
+
const triggerBtnRef = useRef<HTMLButtonElement | null>(null);
|
|
183
|
+
|
|
184
|
+
// Expanded node IDs (only nodes with children can be expanded)
|
|
185
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => {
|
|
186
|
+
// Start with root nodes expanded
|
|
187
|
+
const s = new Set<string>();
|
|
188
|
+
const initExpand = (list: AppTreeNode[], depth: number) => {
|
|
189
|
+
for (const n of list) {
|
|
190
|
+
if (n.children && n.children.length > 0 && depth < 2) {
|
|
191
|
+
s.add(n.id);
|
|
192
|
+
initExpand(n.children, depth + 1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
initExpand(nodes, 0);
|
|
197
|
+
return s;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Controlled vs uncontrolled value
|
|
201
|
+
const resolvedValue = controlledValue !== undefined ? controlledValue : internalValue;
|
|
202
|
+
|
|
203
|
+
// Flat lookup
|
|
204
|
+
const nodeMap = useMemo(() => flattenNodes(nodes), [nodes]);
|
|
205
|
+
|
|
206
|
+
// Selected ids as Set
|
|
207
|
+
const selectedIds = useMemo(() => {
|
|
208
|
+
if (mode === 'multiple') {
|
|
209
|
+
return new Set(Array.isArray(resolvedValue) ? resolvedValue : []);
|
|
210
|
+
}
|
|
211
|
+
return new Set(typeof resolvedValue === 'string' && resolvedValue ? [resolvedValue] : []);
|
|
212
|
+
}, [resolvedValue, mode]);
|
|
213
|
+
|
|
214
|
+
// Selected labels for trigger
|
|
215
|
+
const selectedLabels = useMemo(() => {
|
|
216
|
+
const ids = Array.isArray(resolvedValue) ? resolvedValue : (resolvedValue ? [resolvedValue] : []);
|
|
217
|
+
return ids.map((id) => nodeMap.get(id)?.label ?? id);
|
|
218
|
+
}, [resolvedValue, nodeMap]);
|
|
219
|
+
|
|
220
|
+
const selectedAppLabel =
|
|
221
|
+
mode === 'single' && selectedLabels.length === 1
|
|
222
|
+
? selectedLabels[0] === ALL_APPS_VALUE
|
|
223
|
+
? '全部应用'
|
|
224
|
+
: selectedLabels[0]
|
|
225
|
+
: null;
|
|
226
|
+
|
|
227
|
+
// ── Tree filter by search ──────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
const filteredNodes = useMemo(() => {
|
|
230
|
+
if (!searchQuery.trim()) return nodes;
|
|
231
|
+
const q = searchQuery.toLowerCase();
|
|
232
|
+
|
|
233
|
+
const filterTree = (list: AppTreeNode[]): AppTreeNode[] => {
|
|
234
|
+
const result: AppTreeNode[] = [];
|
|
235
|
+
for (const node of list) {
|
|
236
|
+
const labelMatch = node?.label?.toLowerCase().includes(q);
|
|
237
|
+
const filteredChildren = node.children ? filterTree(node.children) : [];
|
|
238
|
+
if (labelMatch || filteredChildren.length > 0) {
|
|
239
|
+
result.push({ ...node, children: filteredChildren.length > 0 ? filteredChildren : node.children });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
};
|
|
244
|
+
return filterTree(nodes);
|
|
245
|
+
}, [nodes, searchQuery]);
|
|
246
|
+
|
|
247
|
+
// ── Shadow DOM portal ref ──────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
|
|
250
|
+
triggerBtnRef.current = node;
|
|
251
|
+
if (node) setTriggerWidth(node.offsetWidth);
|
|
252
|
+
if (!node) { setPortalContainer(null); return; }
|
|
253
|
+
const rootNode = node.getRootNode();
|
|
254
|
+
if (rootNode instanceof ShadowRoot) {
|
|
255
|
+
const existing = rootNode.querySelector<HTMLElement>('[data-bs-app-select-portal]');
|
|
256
|
+
if (existing) { setPortalContainer(existing); return; }
|
|
257
|
+
const container = document.createElement('div');
|
|
258
|
+
container.setAttribute('data-bs-app-select-portal', 'true');
|
|
259
|
+
rootNode.appendChild(container);
|
|
260
|
+
setPortalContainer(container);
|
|
261
|
+
} else {
|
|
262
|
+
setPortalContainer(null);
|
|
263
|
+
}
|
|
264
|
+
}, []);
|
|
265
|
+
|
|
266
|
+
// ── Handlers ────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
const updateValue = useCallback(
|
|
269
|
+
(newValue: string | string[]) => {
|
|
270
|
+
if (controlledValue === undefined) setInternalValue(newValue);
|
|
271
|
+
onChange?.(newValue);
|
|
272
|
+
},
|
|
273
|
+
[controlledValue, onChange]
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const toggleExpanded = useCallback((nodeId: string) => {
|
|
277
|
+
setExpandedIds((prev) => {
|
|
278
|
+
const next = new Set(prev);
|
|
279
|
+
if (next.has(nodeId)) next.delete(nodeId);
|
|
280
|
+
else next.add(nodeId);
|
|
281
|
+
return next;
|
|
282
|
+
});
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
const handleSelect = useCallback(
|
|
286
|
+
(nodeId: string, node: AppTreeNode) => {
|
|
287
|
+
// Groups toggle expand; never selectable
|
|
288
|
+
if (node.type === 'group' || (node.children && node.children.length > 0)) {
|
|
289
|
+
toggleExpanded(nodeId);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Leaf node selection
|
|
294
|
+
if (mode === 'single') {
|
|
295
|
+
updateValue(nodeId);
|
|
296
|
+
setOpen(false);
|
|
297
|
+
} else {
|
|
298
|
+
const currentIds = Array.isArray(resolvedValue) ? [...resolvedValue] : [];
|
|
299
|
+
const idx = currentIds.indexOf(nodeId);
|
|
300
|
+
if (idx >= 0) {
|
|
301
|
+
currentIds.splice(idx, 1);
|
|
302
|
+
} else {
|
|
303
|
+
currentIds.push(nodeId);
|
|
304
|
+
}
|
|
305
|
+
updateValue(currentIds);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
[mode, resolvedValue, updateValue, toggleExpanded]
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const handleRemoveChip = useCallback(
|
|
312
|
+
(nodeId: string, e: React.MouseEvent) => {
|
|
313
|
+
e.stopPropagation();
|
|
314
|
+
if (mode === 'multiple' && Array.isArray(resolvedValue)) {
|
|
315
|
+
updateValue(resolvedValue.filter((id) => id !== nodeId));
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
[mode, resolvedValue, updateValue]
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const handleOpenChange = useCallback(
|
|
322
|
+
(nextOpen: boolean) => {
|
|
323
|
+
if (disabled) return;
|
|
324
|
+
setOpen(nextOpen);
|
|
325
|
+
if (nextOpen && triggerBtnRef.current) {
|
|
326
|
+
setTriggerWidth(triggerBtnRef.current.offsetWidth);
|
|
327
|
+
}
|
|
328
|
+
if (!nextOpen) setSearchQuery('');
|
|
329
|
+
},
|
|
330
|
+
[disabled]
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// ── Chip visibility ─────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
const multiSelectedLabels = mode === 'multiple' ? selectedLabels : [];
|
|
336
|
+
const visibleChips = multiSelectedLabels.slice(0, maxVisibleChips);
|
|
337
|
+
const overflowCount = multiSelectedLabels.length - visibleChips.length;
|
|
338
|
+
|
|
339
|
+
// ── Tree row rendering ──────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
const renderNodeRow = useCallback(
|
|
342
|
+
(node: AppTreeNode, depth: number) => {
|
|
343
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
344
|
+
const isExpanded = expandedIds.has(node.id);
|
|
345
|
+
const isSelected = selectedIds.has(node.id);
|
|
346
|
+
const leftPad = depth * 22 + 10; // 10, 32, 54, 78, 102...
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div key={node.id}>
|
|
350
|
+
<button
|
|
351
|
+
type="button"
|
|
352
|
+
onClick={() => handleSelect(node.id, node)}
|
|
353
|
+
className={cn(
|
|
354
|
+
'flex items-center gap-[6px] w-full h-[34px] rounded-[4px] text-left',
|
|
355
|
+
'pr-[10px]',
|
|
356
|
+
transCls,
|
|
357
|
+
isSelected
|
|
358
|
+
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
359
|
+
: 'text-[#030303] hover:bg-[#EFF0F6]'
|
|
360
|
+
)}
|
|
361
|
+
style={{ paddingLeft: `${leftPad}px`, paddingTop: '10px', paddingBottom: '10px' }}
|
|
362
|
+
>
|
|
363
|
+
{/* Left side: toggle icon for expandable nodes */}
|
|
364
|
+
{hasChildren && <ToggleIcon expanded={isExpanded} />}
|
|
365
|
+
|
|
366
|
+
{/* Node icon */}
|
|
367
|
+
{node.type === 'group' && !hasChildren && <FolderIcon />}
|
|
368
|
+
{node.type === 'group' && hasChildren && <FolderIcon />}
|
|
369
|
+
{node.type === 'app' && <AppIcon name={node.data?.style.icon!} />}
|
|
370
|
+
{/* {node.type === 'item' && <ItemIcon color={isSelected ? '#0265FF' : node.color} />} */}
|
|
371
|
+
|
|
372
|
+
{/* Label */}
|
|
373
|
+
<span className="text-[14px] truncate leading-[normal] flex-1 min-w-0">
|
|
374
|
+
{node.label}
|
|
375
|
+
</span>
|
|
376
|
+
|
|
377
|
+
{/* SquareChecker for multi-select on leaf nodes — always visible */}
|
|
378
|
+
{mode === 'multiple' && !hasChildren && node.type !== 'group' && (
|
|
379
|
+
<span onClick={(e) => e.stopPropagation()} className='grid place-items-center'>
|
|
380
|
+
<SquareChecker
|
|
381
|
+
checked={isSelected}
|
|
382
|
+
onCheckedChange={() => handleSelect(node.id, node)}
|
|
383
|
+
className="shrink-0"
|
|
384
|
+
/>
|
|
385
|
+
</span>
|
|
386
|
+
)}
|
|
387
|
+
</button>
|
|
388
|
+
|
|
389
|
+
{/* Children (when expanded) */}
|
|
390
|
+
{hasChildren && isExpanded && (
|
|
391
|
+
<div>
|
|
392
|
+
{node.children!.map((child) => renderNodeRow(child, depth + 1))}
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
[expandedIds, selectedIds, mode, handleSelect]
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
// ── Render ──────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
const hasMultiSelection = mode === 'multiple' && multiSelectedLabels.length > 0;
|
|
404
|
+
|
|
405
|
+
const popoverContent = (
|
|
406
|
+
<div className="flex flex-col gap-[4px]">
|
|
407
|
+
{/* Search */}
|
|
408
|
+
<div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
|
|
409
|
+
<SearchIcon />
|
|
410
|
+
<input
|
|
411
|
+
type="text"
|
|
412
|
+
value={searchQuery}
|
|
413
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
414
|
+
placeholder={searchPlaceholder}
|
|
415
|
+
className="flex-1 outline-none text-[14px] placeholder:text-[#999999] bg-transparent min-w-0 text-[#030303]"
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{/* Empty option */}
|
|
420
|
+
{allowEmpty && mode === 'single' && !searchQuery.trim() && (
|
|
421
|
+
<button
|
|
422
|
+
type="button"
|
|
423
|
+
onClick={() => {
|
|
424
|
+
updateValue('');
|
|
425
|
+
setOpen(false);
|
|
426
|
+
}}
|
|
427
|
+
className={cn(
|
|
428
|
+
'flex items-center w-full h-[34px] rounded-[4px] text-left',
|
|
429
|
+
'px-[10px]',
|
|
430
|
+
transCls,
|
|
431
|
+
'text-[#030303] hover:bg-[#EFF0F6]'
|
|
432
|
+
)}
|
|
433
|
+
>
|
|
434
|
+
<span className="text-[14px] leading-[normal]">{emptyLabel}</span>
|
|
435
|
+
</button>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{/* "全部应用" item */}
|
|
439
|
+
{showAll && mode === 'single' && !searchQuery.trim() && (
|
|
440
|
+
<button
|
|
441
|
+
type="button"
|
|
442
|
+
onClick={() => {
|
|
443
|
+
updateValue(ALL_APPS_VALUE);
|
|
444
|
+
setOpen(false);
|
|
445
|
+
}}
|
|
446
|
+
className={cn(
|
|
447
|
+
'flex items-center w-full h-[34px] rounded-[4px] text-left',
|
|
448
|
+
'px-[10px]',
|
|
449
|
+
transCls,
|
|
450
|
+
selectedIds.has(ALL_APPS_VALUE)
|
|
451
|
+
? 'bg-[#F0F6FF] text-[#0265FF]'
|
|
452
|
+
: 'text-[#030303] hover:bg-[#EFF0F6]'
|
|
453
|
+
)}
|
|
454
|
+
>
|
|
455
|
+
<span className="text-[14px] leading-[normal]">全部应用</span>
|
|
456
|
+
</button>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
{/* Tree */}
|
|
460
|
+
<div className="max-h-[400px] overflow-y-auto">
|
|
461
|
+
{filteredNodes.length > 0 ? (
|
|
462
|
+
filteredNodes.map((node) => renderNodeRow(node, 0))
|
|
463
|
+
) : (
|
|
464
|
+
<div className="flex items-center justify-center h-[60px] text-[12px] text-[#999999]">
|
|
465
|
+
{searchQuery.trim() ? '未找到相关应用' : '暂无应用'}
|
|
466
|
+
</div>
|
|
467
|
+
)}
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
if (popoverContentOnly) {
|
|
473
|
+
return popoverContent;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<div className={cn('relative', className)}>
|
|
478
|
+
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
479
|
+
<PopoverTrigger asChild>
|
|
480
|
+
<button
|
|
481
|
+
ref={setTriggerRef}
|
|
482
|
+
type="button"
|
|
483
|
+
disabled={disabled}
|
|
484
|
+
className={cn(
|
|
485
|
+
'flex items-center justify-between w-full h-[34px] gap-2.5',
|
|
486
|
+
'bg-white border border-[#E0E0E0] rounded-[4px]',
|
|
487
|
+
'py-[7px]',
|
|
488
|
+
hasMultiSelection ? 'pl-[4px] pr-[10px]' : 'px-[10px]',
|
|
489
|
+
'text-left cursor-pointer',
|
|
490
|
+
'hover:border-[#0265FF]',
|
|
491
|
+
disabled && 'opacity-50 cursor-not-allowed hover:border-[#E0E0E0]',
|
|
492
|
+
open && 'border-[#0265FF]',
|
|
493
|
+
transCls
|
|
494
|
+
)}
|
|
495
|
+
// style={{ minHeight: '34px' }}
|
|
496
|
+
>
|
|
497
|
+
{hasMultiSelection ? (
|
|
498
|
+
<>
|
|
499
|
+
<div className="flex items-center gap-[4px] min-w-0 flex-1 overflow-x-auto scrollbar-hide">
|
|
500
|
+
{visibleChips.map((label, i) => (
|
|
501
|
+
<span
|
|
502
|
+
key={i}
|
|
503
|
+
className="flex items-center gap-[4px] bg-[#EEEEEE] rounded-[2px] h-[26px] px-[6px] shrink-0 max-w-[160px]"
|
|
504
|
+
onClick={(e) => {
|
|
505
|
+
// stop propagation to prevent opening popover when clicking the chip, but still allow it when clicking the close icon
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
const ids = Array.isArray(resolvedValue) ? resolvedValue : [];
|
|
508
|
+
if (ids[i]) handleRemoveChip(ids[i], e);
|
|
509
|
+
}}
|
|
510
|
+
>
|
|
511
|
+
<span className="text-[14px] text-[#030303] truncate leading-[normal]">
|
|
512
|
+
{label}
|
|
513
|
+
</span>
|
|
514
|
+
<span
|
|
515
|
+
className="cursor-pointer flex items-center shrink-0 hover:bg-[#EFF0F6]"
|
|
516
|
+
>
|
|
517
|
+
<CloseIcon />
|
|
518
|
+
</span>
|
|
519
|
+
</span>
|
|
520
|
+
))}
|
|
521
|
+
{overflowCount > 0 && (
|
|
522
|
+
<span className="flex items-center gap-[4px] bg-[#EEEEEE] rounded-[2px] h-[26px] px-[6px] shrink-0">
|
|
523
|
+
<span className="text-[14px] text-[#030303] leading-[normal]">
|
|
524
|
+
+{overflowCount}
|
|
525
|
+
</span>
|
|
526
|
+
<MoreIcon />
|
|
527
|
+
</span>
|
|
528
|
+
)}
|
|
529
|
+
</div>
|
|
530
|
+
<DropdownChevron open={open} />
|
|
531
|
+
</>
|
|
532
|
+
) : (
|
|
533
|
+
<>
|
|
534
|
+
<span
|
|
535
|
+
className={cn(
|
|
536
|
+
'text-[14px] leading-[normal] truncate',
|
|
537
|
+
selectedAppLabel ? 'text-[#030303]' : 'text-[#999999]'
|
|
538
|
+
)}
|
|
539
|
+
>
|
|
540
|
+
{selectedAppLabel ?? (allowEmpty ? emptyLabel : placeholder)}
|
|
541
|
+
</span>
|
|
542
|
+
<DropdownChevron open={open} />
|
|
543
|
+
</>
|
|
544
|
+
)}
|
|
545
|
+
</button>
|
|
546
|
+
</PopoverTrigger>
|
|
547
|
+
<PopoverContent
|
|
548
|
+
align="start"
|
|
549
|
+
sideOffset={4}
|
|
550
|
+
portalContainer={portalContainer}
|
|
551
|
+
className="!border-0 rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]"
|
|
552
|
+
style={{ minWidth: triggerWidth || undefined }}
|
|
553
|
+
>
|
|
554
|
+
{popoverContent}
|
|
555
|
+
</PopoverContent>
|
|
556
|
+
</Popover>
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
};
|