torch-glare 2.3.0 → 2.4.1
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/README.md +8 -0
- package/apps/lib/components/ActionButton.tsx +7 -12
- package/apps/lib/components/ActionsGroup.tsx +4 -15
- package/apps/lib/components/AlertDialog.tsx +173 -191
- package/apps/lib/components/Avatar.tsx +13 -16
- package/apps/lib/components/Badge.tsx +7 -3
- package/apps/lib/components/BadgeField.tsx +76 -71
- package/apps/lib/components/Breadcrumb.tsx +49 -85
- package/apps/lib/components/Button.tsx +20 -19
- package/apps/lib/components/ButtonGroup.tsx +31 -41
- package/apps/lib/components/Calendar.tsx +51 -29
- package/apps/lib/components/Card.tsx +2 -13
- package/apps/lib/components/ChartBlockTool.ts +17 -52
- package/apps/lib/components/Checkbox.tsx +9 -16
- package/apps/lib/components/ContextMenu.tsx +110 -106
- package/apps/lib/components/CountBadge.tsx +4 -6
- package/apps/lib/components/DataTable.tsx +196 -216
- package/apps/lib/components/DataViews/DataViewRadio.tsx +10 -8
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +21 -55
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +1 -3
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +5 -12
- package/apps/lib/components/DataViews/FilterPanel.tsx +173 -150
- package/apps/lib/components/DataViews/HeaderSearch.tsx +1 -5
- package/apps/lib/components/DataViews/InboxView.tsx +22 -68
- package/apps/lib/components/DataViews/InboxViewCard.tsx +68 -77
- package/apps/lib/components/DataViews/KanbanView.tsx +8 -25
- package/apps/lib/components/DataViews/PanelControls.tsx +3 -13
- package/apps/lib/components/DataViews/SettingsPanel.tsx +83 -89
- package/apps/lib/components/DataViews/TableView.tsx +8 -28
- package/apps/lib/components/DataViews/TreeView.tsx +12 -40
- package/apps/lib/components/DataViews/badgeAdapter.ts +32 -28
- package/apps/lib/components/DataViews/fieldRenderers.tsx +106 -141
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +87 -0
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +39 -32
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +15 -15
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +69 -58
- package/apps/lib/components/DataViews/index.ts +26 -26
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +12 -16
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +28 -31
- package/apps/lib/components/DataViews/types.ts +111 -114
- package/apps/lib/components/DatePicker.tsx +198 -191
- package/apps/lib/components/Dialog.tsx +92 -105
- package/apps/lib/components/Divider.tsx +21 -24
- package/apps/lib/components/Drawer.tsx +79 -139
- package/apps/lib/components/DropdownMenu.tsx +144 -181
- package/apps/lib/components/FieldHint.tsx +4 -11
- package/apps/lib/components/Form.tsx +146 -155
- package/apps/lib/components/FormStepper.tsx +9 -39
- package/apps/lib/components/HeaderBar.tsx +119 -0
- package/apps/lib/components/ImageAttachment.tsx +111 -73
- package/apps/lib/components/InnerLabelField.tsx +20 -16
- package/apps/lib/components/Input.tsx +171 -172
- package/apps/lib/components/InputField.tsx +44 -45
- package/apps/lib/components/InputOTP.tsx +68 -69
- package/apps/lib/components/Label.tsx +31 -35
- package/apps/lib/components/LabelField.tsx +4 -5
- package/apps/lib/components/LabeledCheckBox.tsx +8 -12
- package/apps/lib/components/LabeledRadio.tsx +7 -5
- package/apps/lib/components/LinkButton.tsx +2 -9
- package/apps/lib/components/LoginButton.tsx +4 -9
- package/apps/lib/components/PasswordLevel.tsx +5 -5
- package/apps/lib/components/Popover.tsx +15 -33
- package/apps/lib/components/ProfileMenu.tsx +67 -72
- package/apps/lib/components/Radio.tsx +1 -5
- package/apps/lib/components/RadioCard.tsx +9 -28
- package/apps/lib/components/ScrollArea.tsx +45 -43
- package/apps/lib/components/SearchField.tsx +49 -39
- package/apps/lib/components/SearchableSelect.tsx +46 -52
- package/apps/lib/components/SearchableTable.tsx +199 -219
- package/apps/lib/components/SearchableTree.tsx +469 -0
- package/apps/lib/components/SearchableTreeDialog.tsx +493 -0
- package/apps/lib/components/SectionBlock.tsx +4 -16
- package/apps/lib/components/Select.tsx +53 -80
- package/apps/lib/components/SimpleSelect.tsx +196 -187
- package/apps/lib/components/Skeleton.tsx +9 -12
- package/apps/lib/components/SlideDatePicker.tsx +215 -166
- package/apps/lib/components/SpinLoading.tsx +33 -49
- package/apps/lib/components/Stepper.tsx +13 -56
- package/apps/lib/components/Switch.tsx +8 -10
- package/apps/lib/components/TabFormItem.tsx +3 -12
- package/apps/lib/components/TabSwitch.tsx +7 -34
- package/apps/lib/components/Table.tsx +60 -75
- package/apps/lib/components/TableDnDWrapper.ts +15 -48
- package/apps/lib/components/TextEditor.tsx +93 -104
- package/apps/lib/components/Textarea.tsx +8 -12
- package/apps/lib/components/Timeline.tsx +21 -41
- package/apps/lib/components/Toast.tsx +83 -83
- package/apps/lib/components/Toggle.tsx +112 -114
- package/apps/lib/components/ToggleButton.tsx +1 -1
- package/apps/lib/components/Tooltip.tsx +24 -19
- package/apps/lib/components/TransparentLabel.tsx +61 -62
- package/apps/lib/components/TreeDropDown.tsx +86 -52
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +15 -48
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +16 -21
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +9 -20
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +3 -3
- package/apps/lib/components/TreeFolder/icons.tsx +14 -17
- package/apps/lib/components/TreeFolder/index.ts +15 -8
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +47 -52
- package/apps/lib/components/TreeFolder/types.ts +32 -32
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +117 -123
- package/apps/lib/hooks/useActiveTreeItem.ts +52 -55
- package/apps/lib/hooks/useClickOutside.ts +21 -20
- package/apps/lib/hooks/useDataViewsState.ts +86 -80
- package/apps/lib/hooks/useIsMobile.ts +12 -12
- package/apps/lib/hooks/useResize.ts +54 -65
- package/apps/lib/hooks/useTagSelection.ts +201 -201
- package/apps/lib/layouts/CNLayout.tsx +181 -140
- package/apps/lib/layouts/DataViewCard.tsx +4 -18
- package/apps/lib/layouts/FieldSection.tsx +2 -6
- package/apps/lib/layouts/TreeSubLayout.tsx +24 -38
- package/apps/lib/providers/ThemeProvider.tsx +8 -23
- package/apps/lib/registry.json +1004 -0
- package/apps/lib/tsconfig.json +9 -0
- package/apps/lib/utils/cn.ts +1 -1
- package/apps/lib/utils/dataViews/columnUtils.ts +46 -44
- package/apps/lib/utils/dataViews/fieldUtils.ts +70 -71
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +129 -122
- package/apps/lib/utils/dataViews/pathUtils.ts +74 -67
- package/apps/lib/utils/dataViews/rangeUtils.ts +121 -112
- package/apps/lib/utils/dataViews/treeUtils.ts +204 -211
- package/apps/lib/utils/dateFormat.ts +64 -54
- package/apps/lib/utils/markdownParser.ts +2 -7
- package/apps/lib/utils/resize.ts +21 -25
- package/apps/lib/utils/types.ts +13 -13
- package/dist/bin/index.js +8 -2
- package/dist/bin/index.js.map +1 -1
- package/dist/src/shared/getAvailableFiles.d.ts.map +1 -1
- package/dist/src/shared/getAvailableFiles.js +6 -1
- package/dist/src/shared/getAvailableFiles.js.map +1 -1
- package/dist/src/shared/loadRegistry.d.ts +7 -0
- package/dist/src/shared/loadRegistry.d.ts.map +1 -0
- package/dist/src/shared/loadRegistry.js +31 -0
- package/dist/src/shared/loadRegistry.js.map +1 -0
- package/dist/src/shared/resolveInstallPlan.d.ts +12 -0
- package/dist/src/shared/resolveInstallPlan.d.ts.map +1 -0
- package/dist/src/shared/resolveInstallPlan.js +39 -0
- package/dist/src/shared/resolveInstallPlan.js.map +1 -0
- package/dist/src/types/main.d.ts +24 -0
- package/dist/src/types/main.d.ts.map +1 -1
- package/docs/README.md +10 -2
- package/docs/components/action-button.md +30 -24
- package/docs/components/actions-group.md +26 -20
- package/docs/components/alert-dialog.md +45 -45
- package/docs/components/avatar.md +6 -2
- package/docs/components/badge-field.md +6 -2
- package/docs/components/badge.md +7 -1
- package/docs/components/breadcrumb.md +15 -40
- package/docs/components/button-group.md +13 -10
- package/docs/components/button.md +58 -33
- package/docs/components/calendar.md +26 -19
- package/docs/components/card.md +19 -16
- package/docs/components/chart-block-tool.md +128 -0
- package/docs/components/checkbox.md +16 -8
- package/docs/components/cn-layout.md +7 -1
- package/docs/components/context-menu.md +47 -24
- package/docs/components/count-badge.md +7 -1
- package/docs/components/data-table.md +45 -33
- package/docs/components/data-views-config-panel.md +18 -14
- package/docs/components/data-views-layout.md +20 -17
- package/docs/components/date-picker.md +30 -23
- package/docs/components/dialog.md +43 -42
- package/docs/components/divider.md +10 -4
- package/docs/components/drawer.md +8 -33
- package/docs/components/dropdown-menu.md +51 -32
- package/docs/components/field-hint.md +37 -25
- package/docs/components/field-section.md +9 -3
- package/docs/components/form-stepper.md +6 -0
- package/docs/components/form.md +17 -20
- package/docs/components/header-bar.md +187 -0
- package/docs/components/image-attachment.md +33 -41
- package/docs/components/inbox-view.md +16 -5
- package/docs/components/inner-label-field.md +34 -24
- package/docs/components/input-field.md +18 -10
- package/docs/components/input-otp.md +14 -11
- package/docs/components/input.md +22 -16
- package/docs/components/kanban-view.md +15 -5
- package/docs/components/label-field.md +32 -22
- package/docs/components/label.md +43 -32
- package/docs/components/labeled-check-box.md +16 -8
- package/docs/components/labeled-radio.md +18 -11
- package/docs/components/link-button.md +7 -1
- package/docs/components/login-button.md +8 -2
- package/docs/components/password-level.md +37 -24
- package/docs/components/popover.md +26 -23
- package/docs/components/profile-menu.md +38 -27
- package/docs/components/radio-card.md +16 -9
- package/docs/components/radio.md +16 -8
- package/docs/components/scroll-area.md +6 -2
- package/docs/components/search-field.md +14 -6
- package/docs/components/searchable-select.md +19 -14
- package/docs/components/searchable-table.md +60 -41
- package/docs/components/searchable-tree-dialog.md +190 -0
- package/docs/components/searchable-tree.md +200 -0
- package/docs/components/section-block.md +130 -4
- package/docs/components/select.md +23 -18
- package/docs/components/simple-select.md +14 -10
- package/docs/components/skeleton.md +43 -33
- package/docs/components/slide-date-picker.md +37 -29
- package/docs/components/spin-loading.md +32 -23
- package/docs/components/stepper.md +6 -0
- package/docs/components/switch.md +14 -6
- package/docs/components/tab-form-item.md +7 -1
- package/docs/components/tab-switch.md +13 -6
- package/docs/components/table-dnd-wrapper.md +105 -0
- package/docs/components/table-view.md +15 -5
- package/docs/components/table.md +28 -28
- package/docs/components/text-editor.md +19 -13
- package/docs/components/textarea.md +15 -7
- package/docs/components/timeline.md +6 -0
- package/docs/components/toast.md +35 -26
- package/docs/components/toggle-button.md +24 -21
- package/docs/components/toggle.md +23 -15
- package/docs/components/tooltip.md +42 -30
- package/docs/components/transparent-label.md +29 -20
- package/docs/components/tree-drop-down.md +31 -23
- package/docs/components/tree-sub-layout.md +6 -2
- package/docs/components/tree-view.md +16 -5
- package/docs/explanation/architecture.md +54 -0
- package/docs/explanation/design-system.md +52 -0
- package/docs/how-to/data-views-from-backend-response.md +7 -4
- package/docs/how-to/guides.md +24 -18
- package/docs/migration/changelog.md +38 -0
- package/docs/reference/cli.md +87 -0
- package/docs/reference/components.md +11 -12
- package/docs/reference/hooks.md +8 -8
- package/docs/reference/providers.md +22 -22
- package/docs/reference/tailwind-plugins.md +2 -2
- package/docs/reference/theme.md +85 -0
- package/docs/reference/types.md +4 -4
- package/docs/reference/utilities.md +19 -26
- package/docs/tutorials/building-first-form.md +20 -13
- package/docs/tutorials/component-composition.md +34 -50
- package/docs/tutorials/getting-started.md +2 -2
- package/docs/tutorials/theming-basics.md +13 -10
- package/package.json +31 -3
- package/dist/src/commands/block.d.ts +0 -5
- package/dist/src/commands/block.d.ts.map +0 -1
- package/dist/src/commands/block.js +0 -255
- package/dist/src/commands/block.js.map +0 -1
- package/dist/src/shared/configureFonts.d.ts +0 -6
- package/dist/src/shared/configureFonts.d.ts.map +0 -1
- package/dist/src/shared/configureFonts.js +0 -106
- package/dist/src/shared/configureFonts.js.map +0 -1
- package/dist/src/shared/configureGlobalCss.d.ts +0 -6
- package/dist/src/shared/configureGlobalCss.d.ts.map +0 -1
- package/dist/src/shared/configureGlobalCss.js +0 -154
- package/dist/src/shared/configureGlobalCss.js.map +0 -1
- package/dist/src/shared/configureTailwind.d.ts +0 -7
- package/dist/src/shared/configureTailwind.d.ts.map +0 -1
- package/dist/src/shared/configureTailwind.js +0 -163
- package/dist/src/shared/configureTailwind.js.map +0 -1
- package/dist/src/shared/detectFramework.d.ts +0 -23
- package/dist/src/shared/detectFramework.d.ts.map +0 -1
- package/dist/src/shared/detectFramework.js +0 -119
- package/dist/src/shared/detectFramework.js.map +0 -1
- package/dist/src/shared/installBaseUtils.d.ts +0 -5
- package/dist/src/shared/installBaseUtils.d.ts.map +0 -1
- package/dist/src/shared/installBaseUtils.js +0 -79
- package/dist/src/shared/installBaseUtils.js.map +0 -1
- package/dist/src/shared/resolveAliases.d.ts +0 -24
- package/dist/src/shared/resolveAliases.d.ts.map +0 -1
- package/dist/src/shared/resolveAliases.js +0 -98
- package/dist/src/shared/resolveAliases.js.map +0 -1
|
@@ -1,41 +1,38 @@
|
|
|
1
|
-
import type { DynamicRecord, TreeConfig } from "../../components/DataViews/types"
|
|
2
|
-
import { getByPath, setByPath } from "./pathUtils"
|
|
1
|
+
import type { DynamicRecord, TreeConfig } from "../../components/DataViews/types";
|
|
2
|
+
import { getByPath, setByPath } from "./pathUtils";
|
|
3
3
|
|
|
4
4
|
export type TreeNode = {
|
|
5
|
-
id: string
|
|
6
|
-
record: DynamicRecord
|
|
7
|
-
children: TreeNode[]
|
|
8
|
-
depth: number
|
|
9
|
-
parent: TreeNode | null
|
|
10
|
-
}
|
|
5
|
+
id: string;
|
|
6
|
+
record: DynamicRecord;
|
|
7
|
+
children: TreeNode[];
|
|
8
|
+
depth: number;
|
|
9
|
+
parent: TreeNode | null;
|
|
10
|
+
};
|
|
11
11
|
|
|
12
12
|
export type ResolvedTreeConfig = {
|
|
13
|
-
childrenField?: string
|
|
14
|
-
parentField?: string
|
|
15
|
-
idField: string
|
|
16
|
-
orderField?: string
|
|
17
|
-
nodeLabel?: string
|
|
18
|
-
defaultExpanded: "all" | "roots" | "none"
|
|
19
|
-
}
|
|
13
|
+
childrenField?: string;
|
|
14
|
+
parentField?: string;
|
|
15
|
+
idField: string;
|
|
16
|
+
orderField?: string;
|
|
17
|
+
nodeLabel?: string;
|
|
18
|
+
defaultExpanded: "all" | "roots" | "none";
|
|
19
|
+
};
|
|
20
20
|
|
|
21
|
-
const CHILDREN_PATTERNS = ["children", "items", "kids", "subItems", "nodes"]
|
|
22
|
-
const PARENT_PATTERNS
|
|
23
|
-
const ID_PATTERNS
|
|
21
|
+
const CHILDREN_PATTERNS = ["children", "items", "kids", "subItems", "nodes"];
|
|
22
|
+
const PARENT_PATTERNS = ["parentId", "parent_id", "parent", "managerId", "manager"];
|
|
23
|
+
const ID_PATTERNS = ["id", "_id", "uuid", "key"];
|
|
24
24
|
|
|
25
|
-
export function autoDetectTreeShape(
|
|
26
|
-
data
|
|
27
|
-
config: TreeConfig,
|
|
28
|
-
): ResolvedTreeConfig {
|
|
29
|
-
const sample = data.find((r) => r != null && typeof r === "object") ?? {}
|
|
25
|
+
export function autoDetectTreeShape(data: DynamicRecord[], config: TreeConfig): ResolvedTreeConfig {
|
|
26
|
+
const sample = data.find((r) => r != null && typeof r === "object") ?? {};
|
|
30
27
|
|
|
31
|
-
const idField = config.idField ?? pickKey(sample, ID_PATTERNS) ?? "id"
|
|
28
|
+
const idField = config.idField ?? pickKey(sample, ID_PATTERNS) ?? "id";
|
|
32
29
|
|
|
33
|
-
let childrenField = config.childrenField
|
|
34
|
-
let parentField = config.parentField
|
|
30
|
+
let childrenField = config.childrenField;
|
|
31
|
+
let parentField = config.parentField;
|
|
35
32
|
|
|
36
33
|
if (!childrenField && !parentField) {
|
|
37
|
-
childrenField = pickArrayKey(sample, CHILDREN_PATTERNS)
|
|
38
|
-
if (!childrenField) parentField = pickKey(sample, PARENT_PATTERNS)
|
|
34
|
+
childrenField = pickArrayKey(sample, CHILDREN_PATTERNS);
|
|
35
|
+
if (!childrenField) parentField = pickKey(sample, PARENT_PATTERNS);
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
return {
|
|
@@ -45,98 +42,94 @@ export function autoDetectTreeShape(
|
|
|
45
42
|
orderField: config.orderField,
|
|
46
43
|
nodeLabel: config.nodeLabel,
|
|
47
44
|
defaultExpanded: config.defaultExpanded ?? "roots",
|
|
48
|
-
}
|
|
45
|
+
};
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
function pickKey(record: DynamicRecord, candidates: string[]): string | undefined {
|
|
52
49
|
for (const key of candidates) {
|
|
53
|
-
if (key in record) return key
|
|
50
|
+
if (key in record) return key;
|
|
54
51
|
}
|
|
55
|
-
return undefined
|
|
52
|
+
return undefined;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
function pickArrayKey(record: DynamicRecord, candidates: string[]): string | undefined {
|
|
59
56
|
for (const key of candidates) {
|
|
60
|
-
if (key in record && Array.isArray(record[key])) return key
|
|
57
|
+
if (key in record && Array.isArray(record[key])) return key;
|
|
61
58
|
}
|
|
62
|
-
return undefined
|
|
59
|
+
return undefined;
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
export function buildTree(
|
|
66
|
-
data
|
|
67
|
-
config
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
if (config.childrenField) return buildFromNested(data, config)
|
|
71
|
-
if (config.parentField) return buildFromFlat(data, config)
|
|
72
|
-
return data.map((record, idx) => makeNode(record, config, idx, null, 0))
|
|
62
|
+
export function buildTree(data: DynamicRecord[], config: ResolvedTreeConfig): TreeNode[] {
|
|
63
|
+
if (!data || data.length === 0) return [];
|
|
64
|
+
if (config.childrenField) return buildFromNested(data, config);
|
|
65
|
+
if (config.parentField) return buildFromFlat(data, config);
|
|
66
|
+
return data.map((record, idx) => makeNode(record, config, idx, null, 0));
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
function buildFromNested(
|
|
76
|
-
|
|
77
|
-
config: ResolvedTreeConfig,
|
|
78
|
-
): TreeNode[] {
|
|
79
|
-
const seen = new Set<string>()
|
|
69
|
+
function buildFromNested(data: DynamicRecord[], config: ResolvedTreeConfig): TreeNode[] {
|
|
70
|
+
const seen = new Set<string>();
|
|
80
71
|
|
|
81
|
-
const visit = (
|
|
82
|
-
|
|
72
|
+
const visit = (
|
|
73
|
+
record: DynamicRecord,
|
|
74
|
+
depth: number,
|
|
75
|
+
parent: TreeNode | null,
|
|
76
|
+
idx: number,
|
|
77
|
+
): TreeNode => {
|
|
78
|
+
const node = makeNode(record, config, idx, parent, depth);
|
|
83
79
|
if (seen.has(node.id)) {
|
|
84
|
-
return { ...node, children: [] }
|
|
80
|
+
return { ...node, children: [] };
|
|
85
81
|
}
|
|
86
|
-
seen.add(node.id)
|
|
82
|
+
seen.add(node.id);
|
|
87
83
|
|
|
88
|
-
const rawChildren = getByPath(record, config.childrenField)
|
|
84
|
+
const rawChildren = getByPath(record, config.childrenField);
|
|
89
85
|
if (Array.isArray(rawChildren)) {
|
|
90
|
-
node.children = rawChildren.map((child, ci) => visit(child, depth + 1, node, ci))
|
|
86
|
+
node.children = rawChildren.map((child, ci) => visit(child, depth + 1, node, ci));
|
|
91
87
|
}
|
|
92
|
-
return node
|
|
93
|
-
}
|
|
88
|
+
return node;
|
|
89
|
+
};
|
|
94
90
|
|
|
95
|
-
return data.map((record, idx) => visit(record, 0, null, idx))
|
|
91
|
+
return data.map((record, idx) => visit(record, 0, null, idx));
|
|
96
92
|
}
|
|
97
93
|
|
|
98
|
-
function buildFromFlat(
|
|
99
|
-
|
|
100
|
-
config: ResolvedTreeConfig,
|
|
101
|
-
): TreeNode[] {
|
|
102
|
-
const byId = new Map<string, TreeNode>()
|
|
94
|
+
function buildFromFlat(data: DynamicRecord[], config: ResolvedTreeConfig): TreeNode[] {
|
|
95
|
+
const byId = new Map<string, TreeNode>();
|
|
103
96
|
for (let i = 0; i < data.length; i++) {
|
|
104
|
-
const record = data[i]
|
|
105
|
-
const id = String(getByPath(record, config.idField) ?? `__row${i}`)
|
|
97
|
+
const record = data[i];
|
|
98
|
+
const id = String(getByPath(record, config.idField) ?? `__row${i}`);
|
|
106
99
|
byId.set(id, {
|
|
107
100
|
id,
|
|
108
101
|
record,
|
|
109
102
|
children: [],
|
|
110
103
|
depth: 0,
|
|
111
104
|
parent: null,
|
|
112
|
-
})
|
|
105
|
+
});
|
|
113
106
|
}
|
|
114
107
|
|
|
115
|
-
const roots: TreeNode[] = []
|
|
108
|
+
const roots: TreeNode[] = [];
|
|
116
109
|
for (const node of byId.values()) {
|
|
117
|
-
const parentId = getByPath(node.record, config.parentField)
|
|
110
|
+
const parentId = getByPath(node.record, config.parentField);
|
|
118
111
|
if (parentId == null) {
|
|
119
|
-
roots.push(node)
|
|
120
|
-
continue
|
|
112
|
+
roots.push(node);
|
|
113
|
+
continue;
|
|
121
114
|
}
|
|
122
|
-
const parent = byId.get(String(parentId))
|
|
115
|
+
const parent = byId.get(String(parentId));
|
|
123
116
|
if (!parent || parent === node) {
|
|
124
|
-
roots.push(node)
|
|
125
|
-
continue
|
|
117
|
+
roots.push(node);
|
|
118
|
+
continue;
|
|
126
119
|
}
|
|
127
|
-
node.parent = parent
|
|
128
|
-
parent.children.push(node)
|
|
120
|
+
node.parent = parent;
|
|
121
|
+
parent.children.push(node);
|
|
129
122
|
}
|
|
130
123
|
|
|
131
124
|
const setDepth = (n: TreeNode, d: number, guard = new Set<TreeNode>()) => {
|
|
132
|
-
if (guard.has(n)) return
|
|
133
|
-
guard.add(n)
|
|
134
|
-
n.depth = d
|
|
135
|
-
for (const c of n.children) setDepth(c, d + 1, guard)
|
|
136
|
-
}
|
|
137
|
-
for (const r of roots) setDepth(r, 0)
|
|
138
|
-
|
|
139
|
-
return roots
|
|
125
|
+
if (guard.has(n)) return;
|
|
126
|
+
guard.add(n);
|
|
127
|
+
n.depth = d;
|
|
128
|
+
for (const c of n.children) setDepth(c, d + 1, guard);
|
|
129
|
+
};
|
|
130
|
+
for (const r of roots) setDepth(r, 0);
|
|
131
|
+
|
|
132
|
+
return roots;
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
function makeNode(
|
|
@@ -146,80 +139,80 @@ function makeNode(
|
|
|
146
139
|
parent: TreeNode | null,
|
|
147
140
|
depth: number,
|
|
148
141
|
): TreeNode {
|
|
149
|
-
const idValue = getByPath(record, config.idField)
|
|
150
|
-
const id = idValue != null ? String(idValue) : `__row${idx}
|
|
151
|
-
return { id, record, children: [], depth, parent }
|
|
142
|
+
const idValue = getByPath(record, config.idField);
|
|
143
|
+
const id = idValue != null ? String(idValue) : `__row${idx}`;
|
|
144
|
+
return { id, record, children: [], depth, parent };
|
|
152
145
|
}
|
|
153
146
|
|
|
154
147
|
export function findNodeById(roots: TreeNode[], id: string): TreeNode | null {
|
|
155
148
|
for (const root of roots) {
|
|
156
|
-
if (root.id === id) return root
|
|
157
|
-
const inChild = findNodeById(root.children, id)
|
|
158
|
-
if (inChild) return inChild
|
|
149
|
+
if (root.id === id) return root;
|
|
150
|
+
const inChild = findNodeById(root.children, id);
|
|
151
|
+
if (inChild) return inChild;
|
|
159
152
|
}
|
|
160
|
-
return null
|
|
153
|
+
return null;
|
|
161
154
|
}
|
|
162
155
|
|
|
163
156
|
export function* walk(node: TreeNode): Generator<TreeNode> {
|
|
164
|
-
yield node
|
|
165
|
-
for (const c of node.children) yield* walk(c)
|
|
157
|
+
yield node;
|
|
158
|
+
for (const c of node.children) yield* walk(c);
|
|
166
159
|
}
|
|
167
160
|
|
|
168
161
|
export function flatten(node: TreeNode): DynamicRecord[] {
|
|
169
|
-
const out: DynamicRecord[] = []
|
|
170
|
-
for (const n of walk(node)) out.push(n.record)
|
|
171
|
-
return out
|
|
162
|
+
const out: DynamicRecord[] = [];
|
|
163
|
+
for (const n of walk(node)) out.push(n.record);
|
|
164
|
+
return out;
|
|
172
165
|
}
|
|
173
166
|
|
|
174
167
|
export function descendantIds(node: TreeNode): Set<string> {
|
|
175
|
-
const out = new Set<string>()
|
|
176
|
-
for (const n of walk(node)) out.add(n.id)
|
|
177
|
-
return out
|
|
168
|
+
const out = new Set<string>();
|
|
169
|
+
for (const n of walk(node)) out.add(n.id);
|
|
170
|
+
return out;
|
|
178
171
|
}
|
|
179
172
|
|
|
180
173
|
export function allIds(roots: TreeNode[]): Set<string> {
|
|
181
|
-
const out = new Set<string>()
|
|
182
|
-
for (const r of roots) for (const n of walk(r)) out.add(n.id)
|
|
183
|
-
return out
|
|
174
|
+
const out = new Set<string>();
|
|
175
|
+
for (const r of roots) for (const n of walk(r)) out.add(n.id);
|
|
176
|
+
return out;
|
|
184
177
|
}
|
|
185
178
|
|
|
186
179
|
export function initialExpansion(roots: TreeNode[], mode: "all" | "roots" | "none"): Set<string> {
|
|
187
|
-
if (mode === "none") return new Set()
|
|
188
|
-
if (mode === "roots") return new Set(roots.map((r) => r.id))
|
|
189
|
-
return allIds(roots)
|
|
180
|
+
if (mode === "none") return new Set();
|
|
181
|
+
if (mode === "roots") return new Set(roots.map((r) => r.id));
|
|
182
|
+
return allIds(roots);
|
|
190
183
|
}
|
|
191
184
|
|
|
192
185
|
export function flattenAll(data: DynamicRecord[], childrenField: string): DynamicRecord[] {
|
|
193
|
-
const out: DynamicRecord[] = []
|
|
186
|
+
const out: DynamicRecord[] = [];
|
|
194
187
|
const dfs = (record: DynamicRecord) => {
|
|
195
|
-
if (record == null || typeof record !== "object") return
|
|
196
|
-
const { [childrenField]: kids, ...rest } = record
|
|
197
|
-
out.push(rest as DynamicRecord)
|
|
198
|
-
if (Array.isArray(kids)) for (const k of kids) dfs(k)
|
|
199
|
-
}
|
|
200
|
-
for (const r of data) dfs(r)
|
|
201
|
-
return out
|
|
188
|
+
if (record == null || typeof record !== "object") return;
|
|
189
|
+
const { [childrenField]: kids, ...rest } = record;
|
|
190
|
+
out.push(rest as DynamicRecord);
|
|
191
|
+
if (Array.isArray(kids)) for (const k of kids) dfs(k);
|
|
192
|
+
};
|
|
193
|
+
for (const r of data) dfs(r);
|
|
194
|
+
return out;
|
|
202
195
|
}
|
|
203
196
|
|
|
204
197
|
export type MoveArgs = {
|
|
205
|
-
dragIds: string[]
|
|
206
|
-
parentId: string | null
|
|
207
|
-
index: number
|
|
208
|
-
}
|
|
198
|
+
dragIds: string[];
|
|
199
|
+
parentId: string | null;
|
|
200
|
+
index: number;
|
|
201
|
+
};
|
|
209
202
|
|
|
210
203
|
export function applyMove(
|
|
211
204
|
data: DynamicRecord[],
|
|
212
205
|
config: ResolvedTreeConfig,
|
|
213
206
|
args: MoveArgs,
|
|
214
207
|
): DynamicRecord[] {
|
|
215
|
-
if (config.childrenField) return applyMoveNested(data, config, args)
|
|
216
|
-
if (config.parentField) return applyMoveFlat(data, config, args)
|
|
217
|
-
return applyMoveTopLevel(data, config, args)
|
|
208
|
+
if (config.childrenField) return applyMoveNested(data, config, args);
|
|
209
|
+
if (config.parentField) return applyMoveFlat(data, config, args);
|
|
210
|
+
return applyMoveTopLevel(data, config, args);
|
|
218
211
|
}
|
|
219
212
|
|
|
220
213
|
function recordId(record: DynamicRecord, idField: string, fallbackIdx: number): string {
|
|
221
|
-
const v = getByPath(record, idField)
|
|
222
|
-
return v != null ? String(v) : `__row${fallbackIdx}
|
|
214
|
+
const v = getByPath(record, idField);
|
|
215
|
+
return v != null ? String(v) : `__row${fallbackIdx}`;
|
|
223
216
|
}
|
|
224
217
|
|
|
225
218
|
function applyMoveTopLevel(
|
|
@@ -227,16 +220,16 @@ function applyMoveTopLevel(
|
|
|
227
220
|
config: ResolvedTreeConfig,
|
|
228
221
|
args: MoveArgs,
|
|
229
222
|
): DynamicRecord[] {
|
|
230
|
-
const ids = new Set(args.dragIds)
|
|
231
|
-
const dragged: DynamicRecord[] = []
|
|
232
|
-
const remaining: DynamicRecord[] = []
|
|
223
|
+
const ids = new Set(args.dragIds);
|
|
224
|
+
const dragged: DynamicRecord[] = [];
|
|
225
|
+
const remaining: DynamicRecord[] = [];
|
|
233
226
|
data.forEach((rec, i) => {
|
|
234
|
-
if (ids.has(recordId(rec, config.idField, i))) dragged.push(rec)
|
|
235
|
-
else remaining.push(rec)
|
|
236
|
-
})
|
|
237
|
-
const target = args.parentId == null ? remaining : remaining
|
|
238
|
-
const insertAt = Math.max(0, Math.min(args.index, target.length))
|
|
239
|
-
return [...target.slice(0, insertAt), ...dragged, ...target.slice(insertAt)]
|
|
227
|
+
if (ids.has(recordId(rec, config.idField, i))) dragged.push(rec);
|
|
228
|
+
else remaining.push(rec);
|
|
229
|
+
});
|
|
230
|
+
const target = args.parentId == null ? remaining : remaining;
|
|
231
|
+
const insertAt = Math.max(0, Math.min(args.index, target.length));
|
|
232
|
+
return [...target.slice(0, insertAt), ...dragged, ...target.slice(insertAt)];
|
|
240
233
|
}
|
|
241
234
|
|
|
242
235
|
function applyMoveFlat(
|
|
@@ -244,63 +237,63 @@ function applyMoveFlat(
|
|
|
244
237
|
config: ResolvedTreeConfig,
|
|
245
238
|
args: MoveArgs,
|
|
246
239
|
): DynamicRecord[] {
|
|
247
|
-
const idField = config.idField
|
|
248
|
-
const parentField = config.parentField
|
|
249
|
-
const orderField = config.orderField
|
|
240
|
+
const idField = config.idField;
|
|
241
|
+
const parentField = config.parentField!;
|
|
242
|
+
const orderField = config.orderField;
|
|
250
243
|
|
|
251
|
-
const dragSet = new Set(args.dragIds)
|
|
244
|
+
const dragSet = new Set(args.dragIds);
|
|
252
245
|
|
|
253
246
|
if (args.parentId) {
|
|
254
247
|
for (const d of data) {
|
|
255
|
-
const id = String(getByPath(d, idField) ?? "")
|
|
256
|
-
if (!dragSet.has(id)) continue
|
|
257
|
-
let cur: string | null = args.parentId
|
|
248
|
+
const id = String(getByPath(d, idField) ?? "");
|
|
249
|
+
if (!dragSet.has(id)) continue;
|
|
250
|
+
let cur: string | null = args.parentId;
|
|
258
251
|
while (cur) {
|
|
259
|
-
if (cur === id) return data
|
|
260
|
-
const search = cur
|
|
261
|
-
const parentRec = data.find((r) => String(getByPath(r, idField) ?? "") === search)
|
|
262
|
-
if (!parentRec) break
|
|
263
|
-
const next = getByPath(parentRec, parentField)
|
|
264
|
-
cur = next == null ? null : String(next)
|
|
252
|
+
if (cur === id) return data;
|
|
253
|
+
const search = cur;
|
|
254
|
+
const parentRec = data.find((r) => String(getByPath(r, idField) ?? "") === search);
|
|
255
|
+
if (!parentRec) break;
|
|
256
|
+
const next = getByPath(parentRec, parentField);
|
|
257
|
+
cur = next == null ? null : String(next);
|
|
265
258
|
}
|
|
266
259
|
}
|
|
267
260
|
}
|
|
268
261
|
|
|
269
262
|
const updatedParent: DynamicRecord[] = data.map((rec) => {
|
|
270
|
-
const id = String(getByPath(rec, idField) ?? "")
|
|
271
|
-
if (!dragSet.has(id)) return rec
|
|
272
|
-
return setByPath(rec, parentField, args.parentId ?? null)
|
|
273
|
-
})
|
|
263
|
+
const id = String(getByPath(rec, idField) ?? "");
|
|
264
|
+
if (!dragSet.has(id)) return rec;
|
|
265
|
+
return setByPath(rec, parentField, args.parentId ?? null);
|
|
266
|
+
});
|
|
274
267
|
|
|
275
|
-
if (!orderField) return updatedParent
|
|
268
|
+
if (!orderField) return updatedParent;
|
|
276
269
|
|
|
277
270
|
const siblingsAfter = updatedParent.filter((rec) => {
|
|
278
|
-
const p = getByPath(rec, parentField)
|
|
279
|
-
const norm = p == null ? null : String(p)
|
|
280
|
-
return norm === (args.parentId ?? null)
|
|
281
|
-
})
|
|
271
|
+
const p = getByPath(rec, parentField);
|
|
272
|
+
const norm = p == null ? null : String(p);
|
|
273
|
+
return norm === (args.parentId ?? null);
|
|
274
|
+
});
|
|
282
275
|
|
|
283
|
-
const dragged: DynamicRecord[] = []
|
|
284
|
-
const others: DynamicRecord[] = []
|
|
276
|
+
const dragged: DynamicRecord[] = [];
|
|
277
|
+
const others: DynamicRecord[] = [];
|
|
285
278
|
siblingsAfter.forEach((rec) => {
|
|
286
|
-
const id = String(getByPath(rec, idField) ?? "")
|
|
287
|
-
if (dragSet.has(id)) dragged.push(rec)
|
|
288
|
-
else others.push(rec)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
const insertAt = Math.max(0, Math.min(args.index, others.length))
|
|
292
|
-
const newOrder = [...others.slice(0, insertAt), ...dragged, ...others.slice(insertAt)]
|
|
293
|
-
const orderById = new Map<string, number>()
|
|
279
|
+
const id = String(getByPath(rec, idField) ?? "");
|
|
280
|
+
if (dragSet.has(id)) dragged.push(rec);
|
|
281
|
+
else others.push(rec);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const insertAt = Math.max(0, Math.min(args.index, others.length));
|
|
285
|
+
const newOrder = [...others.slice(0, insertAt), ...dragged, ...others.slice(insertAt)];
|
|
286
|
+
const orderById = new Map<string, number>();
|
|
294
287
|
newOrder.forEach((rec, i) => {
|
|
295
|
-
const id = String(getByPath(rec, idField) ?? "")
|
|
296
|
-
orderById.set(id, i)
|
|
297
|
-
})
|
|
288
|
+
const id = String(getByPath(rec, idField) ?? "");
|
|
289
|
+
orderById.set(id, i);
|
|
290
|
+
});
|
|
298
291
|
|
|
299
292
|
return updatedParent.map((rec) => {
|
|
300
|
-
const id = String(getByPath(rec, idField) ?? "")
|
|
301
|
-
if (!orderById.has(id)) return rec
|
|
302
|
-
return setByPath(rec, orderField, orderById.get(id)!)
|
|
303
|
-
})
|
|
293
|
+
const id = String(getByPath(rec, idField) ?? "");
|
|
294
|
+
if (!orderById.has(id)) return rec;
|
|
295
|
+
return setByPath(rec, orderField, orderById.get(id)!);
|
|
296
|
+
});
|
|
304
297
|
}
|
|
305
298
|
|
|
306
299
|
function applyMoveNested(
|
|
@@ -308,69 +301,69 @@ function applyMoveNested(
|
|
|
308
301
|
config: ResolvedTreeConfig,
|
|
309
302
|
args: MoveArgs,
|
|
310
303
|
): DynamicRecord[] {
|
|
311
|
-
const childrenField = config.childrenField
|
|
312
|
-
const idField = config.idField
|
|
313
|
-
const dragSet = new Set(args.dragIds)
|
|
304
|
+
const childrenField = config.childrenField!;
|
|
305
|
+
const idField = config.idField;
|
|
306
|
+
const dragSet = new Set(args.dragIds);
|
|
314
307
|
|
|
315
308
|
const isAncestorOfDrag = (record: DynamicRecord, ancestors: DynamicRecord[]): boolean => {
|
|
316
|
-
const id = String(getByPath(record, idField) ?? "")
|
|
309
|
+
const id = String(getByPath(record, idField) ?? "");
|
|
317
310
|
if (dragSet.has(id)) {
|
|
318
311
|
for (const a of ancestors) {
|
|
319
|
-
const aid = String(getByPath(a, idField) ?? "")
|
|
320
|
-
if (aid === args.parentId) return true
|
|
312
|
+
const aid = String(getByPath(a, idField) ?? "");
|
|
313
|
+
if (aid === args.parentId) return true;
|
|
321
314
|
}
|
|
322
315
|
}
|
|
323
|
-
const kids = getByPath(record, childrenField)
|
|
316
|
+
const kids = getByPath(record, childrenField);
|
|
324
317
|
if (Array.isArray(kids)) {
|
|
325
318
|
for (const k of kids) {
|
|
326
|
-
if (isAncestorOfDrag(k, [...ancestors, record])) return true
|
|
319
|
+
if (isAncestorOfDrag(k, [...ancestors, record])) return true;
|
|
327
320
|
}
|
|
328
321
|
}
|
|
329
|
-
return false
|
|
330
|
-
}
|
|
331
|
-
for (const r of data) if (isAncestorOfDrag(r, [])) return data
|
|
322
|
+
return false;
|
|
323
|
+
};
|
|
324
|
+
for (const r of data) if (isAncestorOfDrag(r, [])) return data;
|
|
332
325
|
|
|
333
|
-
const extracted: DynamicRecord[] = []
|
|
326
|
+
const extracted: DynamicRecord[] = [];
|
|
334
327
|
const extract = (records: DynamicRecord[]): DynamicRecord[] =>
|
|
335
328
|
records
|
|
336
329
|
.map((rec) => {
|
|
337
|
-
const id = String(getByPath(rec, idField) ?? "")
|
|
338
|
-
const kids = getByPath(rec, childrenField)
|
|
339
|
-
const newKids = Array.isArray(kids) ? extract(kids) : kids
|
|
340
|
-
const next = Array.isArray(kids) ? setByPath(rec, childrenField, newKids) : rec
|
|
330
|
+
const id = String(getByPath(rec, idField) ?? "");
|
|
331
|
+
const kids = getByPath(rec, childrenField);
|
|
332
|
+
const newKids = Array.isArray(kids) ? extract(kids) : kids;
|
|
333
|
+
const next = Array.isArray(kids) ? setByPath(rec, childrenField, newKids) : rec;
|
|
341
334
|
if (dragSet.has(id)) {
|
|
342
|
-
extracted.push(next)
|
|
343
|
-
return null
|
|
335
|
+
extracted.push(next);
|
|
336
|
+
return null;
|
|
344
337
|
}
|
|
345
|
-
return next
|
|
338
|
+
return next;
|
|
346
339
|
})
|
|
347
|
-
.filter((r): r is DynamicRecord => r !== null)
|
|
340
|
+
.filter((r): r is DynamicRecord => r !== null);
|
|
348
341
|
|
|
349
|
-
|
|
342
|
+
const pruned = extract(data);
|
|
350
343
|
|
|
351
344
|
const insert = (records: DynamicRecord[]): DynamicRecord[] =>
|
|
352
345
|
records.map((rec) => {
|
|
353
|
-
const id = String(getByPath(rec, idField) ?? "")
|
|
346
|
+
const id = String(getByPath(rec, idField) ?? "");
|
|
354
347
|
if (id === args.parentId) {
|
|
355
|
-
const kids = getByPath(rec, childrenField)
|
|
356
|
-
const arr = Array.isArray(kids) ? [...kids] : []
|
|
357
|
-
const at = Math.max(0, Math.min(args.index, arr.length))
|
|
358
|
-
arr.splice(at, 0, ...extracted)
|
|
359
|
-
return setByPath(rec, childrenField, arr)
|
|
348
|
+
const kids = getByPath(rec, childrenField);
|
|
349
|
+
const arr = Array.isArray(kids) ? [...kids] : [];
|
|
350
|
+
const at = Math.max(0, Math.min(args.index, arr.length));
|
|
351
|
+
arr.splice(at, 0, ...extracted);
|
|
352
|
+
return setByPath(rec, childrenField, arr);
|
|
360
353
|
}
|
|
361
|
-
const kids = getByPath(rec, childrenField)
|
|
354
|
+
const kids = getByPath(rec, childrenField);
|
|
362
355
|
if (Array.isArray(kids)) {
|
|
363
|
-
return setByPath(rec, childrenField, insert(kids))
|
|
356
|
+
return setByPath(rec, childrenField, insert(kids));
|
|
364
357
|
}
|
|
365
|
-
return rec
|
|
366
|
-
})
|
|
358
|
+
return rec;
|
|
359
|
+
});
|
|
367
360
|
|
|
368
361
|
if (args.parentId == null) {
|
|
369
|
-
const at = Math.max(0, Math.min(args.index, pruned.length))
|
|
370
|
-
return [...pruned.slice(0, at), ...extracted, ...pruned.slice(at)]
|
|
362
|
+
const at = Math.max(0, Math.min(args.index, pruned.length));
|
|
363
|
+
return [...pruned.slice(0, at), ...extracted, ...pruned.slice(at)];
|
|
371
364
|
}
|
|
372
365
|
|
|
373
|
-
return insert(pruned)
|
|
366
|
+
return insert(pruned);
|
|
374
367
|
}
|
|
375
368
|
|
|
376
369
|
export function pruneTree(
|
|
@@ -378,26 +371,26 @@ export function pruneTree(
|
|
|
378
371
|
predicate: (record: DynamicRecord) => boolean,
|
|
379
372
|
): TreeNode[] {
|
|
380
373
|
const visit = (node: TreeNode, parent: TreeNode | null): TreeNode | null => {
|
|
381
|
-
const newChildren: TreeNode[] = []
|
|
374
|
+
const newChildren: TreeNode[] = [];
|
|
382
375
|
for (const c of node.children) {
|
|
383
|
-
const next = visit(c, node)
|
|
384
|
-
if (next) newChildren.push(next)
|
|
376
|
+
const next = visit(c, node);
|
|
377
|
+
if (next) newChildren.push(next);
|
|
385
378
|
}
|
|
386
|
-
const selfMatches = predicate(node.record)
|
|
387
|
-
if (!selfMatches && newChildren.length === 0) return null
|
|
379
|
+
const selfMatches = predicate(node.record);
|
|
380
|
+
if (!selfMatches && newChildren.length === 0) return null;
|
|
388
381
|
return {
|
|
389
382
|
id: node.id,
|
|
390
383
|
record: node.record,
|
|
391
384
|
depth: node.depth,
|
|
392
385
|
parent,
|
|
393
386
|
children: newChildren,
|
|
394
|
-
}
|
|
395
|
-
}
|
|
387
|
+
};
|
|
388
|
+
};
|
|
396
389
|
|
|
397
|
-
const out: TreeNode[] = []
|
|
390
|
+
const out: TreeNode[] = [];
|
|
398
391
|
for (const r of roots) {
|
|
399
|
-
const next = visit(r, null)
|
|
400
|
-
if (next) out.push(next)
|
|
392
|
+
const next = visit(r, null);
|
|
393
|
+
if (next) out.push(next);
|
|
401
394
|
}
|
|
402
|
-
return out
|
|
395
|
+
return out;
|
|
403
396
|
}
|