11import { Button , Icon } from "@openbb/ui" ;
2- import { useCallback , useEffect , useRef , useState } from "react" ;
2+ import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
33import type { Layout , LayoutItem } from "react-grid-layout" ;
44import { ResponsiveGridLayout } from "react-grid-layout" ;
55import "react-grid-layout/css/styles.css" ;
@@ -11,6 +11,7 @@ import {
1111 getDashboard ,
1212 getDashboards ,
1313 updateDashboard ,
14+ updateWidget as updateWidgetApi ,
1415 type Dashboard as ApiDashboard ,
1516 type Widget as ApiWidget ,
1617} from "../../services/dashboardApi" ;
@@ -24,8 +25,10 @@ import {
2425 MarkdownWidget ,
2526 MetricWidget ,
2627 TableWidget ,
28+ TableSettingsModal ,
2729 WidgetMenuModal ,
2830} from "../widgets" ;
31+ import type { TableSettings } from "../widgets/TableSettingsModal" ;
2932import { NavigationBar } from "../widgets/NavigationBar" ;
3033import "./DashboardCanvas.css" ;
3134import { ParameterInput } from "./ParameterInput" ;
@@ -121,18 +124,38 @@ export function DashboardCanvas({ dashboardId: propId, activeTab: propActiveTab,
121124 // Determine active tab (use prop if provided, otherwise first tab)
122125 const activeTab = propActiveTab || ( tabs . length > 0 ? tabs [ 0 ] . id : "" ) ;
123126
124- // Filter widgets based on active tab
125- const filteredWidgets = tabs . length > 0
126- ? widgets . filter ( widget => widget . data ?. tabId === activeTab )
127- : widgets ;
128-
129- // Filter layout based on active tab
130- const filteredLayout = tabs . length > 0
131- ? layout . filter ( item => {
132- const widget = widgets . find ( w => w . id === item . i ) ;
133- return widget ?. data ?. tabId === activeTab ;
134- } )
135- : layout ;
127+ // Filter widgets based on active tab (with deduplication by widget.id)
128+ const filteredWidgets = useMemo ( ( ) => {
129+ const base = tabs . length > 0
130+ ? widgets . filter ( widget => widget . data ?. tabId === activeTab )
131+ : widgets ;
132+
133+ // Deduplicate by widget.id, keeping the first occurrence
134+ const seen = new Set < string > ( ) ;
135+ return base . filter ( widget => {
136+ if ( ! widget . id || seen . has ( widget . id ) ) return false ;
137+ seen . add ( widget . id ) ;
138+ return true ;
139+ } ) ;
140+ } , [ widgets , tabs , activeTab ] ) ;
141+
142+ // Filter layout based on active tab (with deduplication by item.i)
143+ const filteredLayout = useMemo ( ( ) => {
144+ const baseLayout = tabs . length > 0
145+ ? layout . filter ( item => {
146+ const widget = widgets . find ( w => w . id === item . i ) ;
147+ return widget ?. data ?. tabId === activeTab ;
148+ } )
149+ : layout ;
150+
151+ // Deduplicate by item.i, keeping the first occurrence
152+ const seen = new Set < string > ( ) ;
153+ return baseLayout . filter ( item => {
154+ if ( ! item . i || seen . has ( item . i ) ) return false ;
155+ seen . add ( item . i ) ;
156+ return true ;
157+ } ) ;
158+ } , [ layout , widgets , tabs , activeTab ] ) ;
136159 const [ loading , setLoading ] = useState ( true ) ;
137160 const [ error , setError ] = useState < string | null > ( null ) ;
138161 const [ gridWidth , setGridWidth ] = useState ( 1200 ) ;
@@ -161,6 +184,8 @@ export function DashboardCanvas({ dashboardId: propId, activeTab: propActiveTab,
161184 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
162185 const [ customWidgets , setCustomWidgets ] = useState < WidgetConfig [ ] > ( [ ] ) ;
163186 const [ groupConfigs , setGroupConfigs ] = useState < Group [ ] > ( [ ] ) ;
187+ const [ tableSettings , setTableSettings ] = useState < Record < string , TableSettings > > ( { } ) ;
188+ const [ openTableSettingsWidgetId , setOpenTableSettingsWidgetId ] = useState < string | null > ( null ) ;
164189
165190 useEffect ( ( ) => {
166191 if ( propId && propId !== activeId ) {
@@ -198,6 +223,18 @@ export function DashboardCanvas({ dashboardId: propId, activeTab: propActiveTab,
198223 h : widget . position . h ,
199224 } ) ) ;
200225 setLayout ( rawLayout ) ;
226+ // 初始化tableSettings
227+ const initialTableSettings : Record < string , TableSettings > = { } ;
228+ w . forEach ( widget => {
229+ const savedConfig = ( widget . data as Record < string , unknown > ) ?. tableConfig as Record < string , unknown > | undefined ;
230+ if ( savedConfig ) {
231+ initialTableSettings [ widget . id ] = {
232+ decimalPlaces : savedConfig . decimalPlaces as number || 2 ,
233+ visibleColumns : ( savedConfig . visibleColumns as string [ ] ) || [ ] ,
234+ } ;
235+ }
236+ } ) ;
237+ setTableSettings ( initialTableSettings ) ;
201238 } else {
202239 setError ( "Dashboard not found" ) ;
203240 }
@@ -217,6 +254,18 @@ export function DashboardCanvas({ dashboardId: propId, activeTab: propActiveTab,
217254 h : widget . position . h ,
218255 } ) ) ;
219256 setLayout ( rawLayout ) ;
257+ // 初始化tableSettings
258+ const initialTableSettings : Record < string , TableSettings > = { } ;
259+ w . forEach ( widget => {
260+ const savedConfig = ( widget . data as Record < string , unknown > ) ?. tableConfig as Record < string , unknown > | undefined ;
261+ if ( savedConfig ) {
262+ initialTableSettings [ widget . id ] = {
263+ decimalPlaces : savedConfig . decimalPlaces as number || 2 ,
264+ visibleColumns : ( savedConfig . visibleColumns as string [ ] ) || [ ] ,
265+ } ;
266+ }
267+ } ) ;
268+ setTableSettings ( initialTableSettings ) ;
220269 }
221270 }
222271 } catch ( e ) {
@@ -495,7 +544,7 @@ export function DashboardCanvas({ dashboardId: propId, activeTab: propActiveTab,
495544
496545 const handleLayoutChange = useCallback (
497546 ( newLayout : Layout , _layouts : any ) => {
498- const layoutItems = newLayout as LayoutItem [ ] ;
547+ const layoutItems = ( newLayout as LayoutItem [ ] ) . filter ( ( item ) => item . i ) ;
499548 setLayout ( ( prevLayout ) => {
500549 const newLayoutMap = new Map ( layoutItems . map ( ( item ) => [ item . i , item ] ) ) ;
501550 return prevLayout . map ( ( item ) => newLayoutMap . get ( item . i ) || item ) ;
@@ -830,7 +879,12 @@ export function DashboardCanvas({ dashboardId: propId, activeTab: propActiveTab,
830879 < div className = "py-1" >
831880 < button
832881 className = "widget-dropdown-item block px-4 py-2 text-sm w-full text-left"
833- onClick = { ( ) => setOpenMenuWidgetId ( null ) }
882+ onClick = { ( ) => {
883+ setOpenMenuWidgetId ( null ) ;
884+ if ( widget . type === "table" ) {
885+ setOpenTableSettingsWidgetId ( widget . id ) ;
886+ }
887+ } }
834888 >
835889 Settings
836890 </ button >
@@ -936,6 +990,7 @@ export function DashboardCanvas({ dashboardId: propId, activeTab: propActiveTab,
936990 error : widgetErrors [ widget . id ] ,
937991 } as any
938992 }
993+ settings = { tableSettings [ widget . id ] }
939994 mode = { widgetDebugModes [ widget . id ] ? "debug" : undefined }
940995 onUpdate = { ( params : Record < string , unknown > ) => {
941996 Object . entries ( params ) . forEach ( ( [ paramName , value ] ) => {
@@ -1211,6 +1266,79 @@ export function DashboardCanvas({ dashboardId: propId, activeTab: propActiveTab,
12111266 widgets = { [ ...widgetDefinitions , ...customWidgets ] }
12121267 loading = { ! widgetDefinitions . length && ! customWidgets . length }
12131268 />
1269+ { openTableSettingsWidgetId && ( ( ) => {
1270+ const widget = widgets . find ( w => w . id === openTableSettingsWidgetId ) ;
1271+ const columnDefs = ( ( ) => {
1272+ if ( ! widget ) return [ ] ;
1273+ const widgetAny = widget as unknown as Record < string , unknown > ;
1274+
1275+ // Check path 1: widget.tableConfig.columnsDefs
1276+ const tableConfig = widgetAny . tableConfig as Record < string , unknown > | undefined ;
1277+ if ( tableConfig && tableConfig . columnsDefs ) {
1278+ const columnsDefs = tableConfig . columnsDefs as Array < { field : string ; headerName ?: string } > ;
1279+ if ( columnsDefs && Array . isArray ( columnsDefs ) && columnsDefs . length > 0 ) {
1280+ return columnsDefs ;
1281+ }
1282+ }
1283+
1284+ // Check path 2: widget.data.data.table.columnsDefs
1285+ const widgetData = widgetAny . data as Record < string , unknown > | undefined ;
1286+ if ( widgetData ) {
1287+ const tableDataObj = widgetData . data as Record < string , unknown > | undefined ;
1288+ if ( tableDataObj ) {
1289+ const table = tableDataObj . table as Record < string , unknown > | undefined ;
1290+ if ( table && table . columnsDefs ) {
1291+ const columnsDefs = table . columnsDefs as Array < { field : string ; headerName ?: string } > ;
1292+ if ( columnsDefs && Array . isArray ( columnsDefs ) && columnsDefs . length > 0 ) {
1293+ return columnsDefs ;
1294+ }
1295+ }
1296+ }
1297+
1298+ // Check path 3: widget.data.columnsDefs
1299+ if ( widgetData . columnsDefs ) {
1300+ const columnsDefs = widgetData . columnsDefs as Array < { field : string ; headerName ?: string } > ;
1301+ if ( columnsDefs && Array . isArray ( columnsDefs ) && columnsDefs . length > 0 ) {
1302+ return columnsDefs ;
1303+ }
1304+ }
1305+ }
1306+
1307+ return [ ] ;
1308+ } ) ( ) ;
1309+ // 从widget.data中读取已保存的配置
1310+ const savedConfig = ( widget ?. data as Record < string , unknown > ) ?. tableConfig as Record < string , unknown > | undefined ;
1311+ const availableCols = columnDefs . map ( c => c . field ) ;
1312+ const currentSettings : TableSettings = {
1313+ decimalPlaces : savedConfig ?. decimalPlaces as number || tableSettings [ openTableSettingsWidgetId ] ?. decimalPlaces || 2 ,
1314+ visibleColumns : ( savedConfig ?. visibleColumns as string [ ] ) || tableSettings [ openTableSettingsWidgetId ] ?. visibleColumns || availableCols ,
1315+ } ;
1316+ return (
1317+ < TableSettingsModal
1318+ isOpen = { ! ! openTableSettingsWidgetId }
1319+ onClose = { ( ) => setOpenTableSettingsWidgetId ( null ) }
1320+ onSave = { ( settings ) => {
1321+ // 更新前端状态
1322+ setTableSettings ( ( prev ) => ( {
1323+ ...prev ,
1324+ [ openTableSettingsWidgetId ! ] : settings ,
1325+ } ) ) ;
1326+ // 调用API保存到后端
1327+ if ( activeId && widget ) {
1328+ const updatedData = {
1329+ ...widget . data ,
1330+ tableConfig : settings ,
1331+ } ;
1332+ updateWidgetApi ( activeId , widget . id , { data : updatedData } ) ;
1333+ }
1334+ // 刷新widget以应用新设置
1335+ handleRefreshWidget ( openTableSettingsWidgetId ! ) ;
1336+ } }
1337+ currentSettings = { currentSettings }
1338+ columnDefs = { columnDefs }
1339+ />
1340+ ) ;
1341+ } ) ( ) }
12141342 < input
12151343 ref = { fileInputRef }
12161344 type = "file"
0 commit comments