@@ -21,6 +21,8 @@ const ModelProperty = {
2121 TABLE_HTML : "table_html" ,
2222 SORT_COLUMN : "sort_column" ,
2323 SORT_ASCENDING : "sort_ascending" ,
24+ ERROR_MESSAGE : "error_message" ,
25+ ORDERABLE_COLUMNS : "orderable_columns" ,
2426} ;
2527
2628const Event = {
@@ -40,7 +42,17 @@ function render({ model, el }) {
4042 // Main container with a unique class for CSS scoping
4143 el . classList . add ( "bigframes-widget" ) ;
4244
43- // Structure
45+ // Add error message container at the top
46+ const errorContainer = document . createElement ( "div" ) ;
47+ errorContainer . classList . add ( "error-message" ) ;
48+ errorContainer . style . display = "none" ;
49+ errorContainer . style . color = "red" ;
50+ errorContainer . style . padding = "8px" ;
51+ errorContainer . style . marginBottom = "8px" ;
52+ errorContainer . style . border = "1px solid red" ;
53+ errorContainer . style . borderRadius = "4px" ;
54+ errorContainer . style . backgroundColor = "#ffebee" ;
55+
4456 const tableContainer = document . createElement ( "div" ) ;
4557 const footer = document . createElement ( "div" ) ;
4658
@@ -121,44 +133,65 @@ function render({ model, el }) {
121133 }
122134 }
123135
124- /** Updates the HTML in the table container and refreshes button states. */
125136 function handleTableHTMLChange ( ) {
126- // Note: Using innerHTML is safe here because the content is generated
127- // by a trusted backend (DataFrame.to_html).
128137 tableContainer . innerHTML = model . get ( ModelProperty . TABLE_HTML ) ;
129138
139+ // Get sortable columns from backend
140+ const sortableColumns = model . get ( ModelProperty . ORDERABLE_COLUMNS ) ;
141+ const currentSortColumn = model . get ( ModelProperty . SORT_COLUMN ) ;
142+ const currentSortAscending = model . get ( ModelProperty . SORT_ASCENDING ) ;
143+
130144 // Add click handlers to column headers for sorting
131145 const headers = tableContainer . querySelectorAll ( "th" ) ;
132146 headers . forEach ( ( header ) => {
133- const columnName = header . textContent . trim ( ) ;
134- if ( columnName ) {
147+ const columnName = header . querySelector ( "div" ) . textContent . trim ( ) ;
148+
149+ // Only add sorting UI for sortable columns
150+ if ( columnName && sortableColumns . includes ( columnName ) ) {
135151 header . style . cursor = "pointer" ;
136- header . addEventListener ( Event . CLICK , ( ) => {
137- const currentSortColumn = model . get ( ModelProperty . SORT_COLUMN ) ;
138- const currentSortAscending = model . get ( ModelProperty . SORT_ASCENDING ) ;
139152
153+ // Determine sort indicator
154+ let indicator = " ●" ; // Default: unsorted (dot)
155+ if ( currentSortColumn === columnName ) {
156+ indicator = currentSortAscending ? " ▲" : " ▼" ;
157+ }
158+ header . textContent = columnName + indicator ;
159+
160+ // Add click handler for three-state toggle
161+ header . addEventListener ( Event . CLICK , ( ) => {
140162 if ( currentSortColumn === columnName ) {
141- // Toggle sort direction
142- model . set ( ModelProperty . SORT_ASCENDING , ! currentSortAscending ) ;
163+ if ( currentSortAscending ) {
164+ // Currently ascending → switch to descending
165+ model . set ( ModelProperty . SORT_ASCENDING , false ) ;
166+ } else {
167+ // Currently descending → clear sort (back to unsorted)
168+ model . set ( ModelProperty . SORT_COLUMN , "" ) ;
169+ model . set ( ModelProperty . SORT_ASCENDING , true ) ;
170+ }
143171 } else {
144- // New column, default to ascending
172+ // Not currently sorted → sort ascending
145173 model . set ( ModelProperty . SORT_COLUMN , columnName ) ;
146174 model . set ( ModelProperty . SORT_ASCENDING , true ) ;
147175 }
148176 model . save_changes ( ) ;
149177 } ) ;
150-
151- // Add visual indicator for sorted column
152- if ( model . get ( ModelProperty . SORT_COLUMN ) === columnName ) {
153- const arrow = model . get ( ModelProperty . SORT_ASCENDING ) ? " ▲" : " ▼" ;
154- header . textContent = columnName + arrow ;
155- }
156178 }
157179 } ) ;
158180
159181 updateButtonStates ( ) ;
160182 }
161183
184+ // Add error message handler
185+ function handleErrorMessageChange ( ) {
186+ const errorMsg = model . get ( ModelProperty . ERROR_MESSAGE ) ;
187+ if ( errorMsg ) {
188+ errorContainer . textContent = errorMsg ;
189+ errorContainer . style . display = "block" ;
190+ } else {
191+ errorContainer . style . display = "none" ;
192+ }
193+ }
194+
162195 // Add event listeners
163196 prevPage . addEventListener ( Event . CLICK , ( ) => handlePageChange ( - 1 ) ) ;
164197 nextPage . addEventListener ( Event . CLICK , ( ) => handlePageChange ( 1 ) ) ;
@@ -170,6 +203,7 @@ function render({ model, el }) {
170203 } ) ;
171204 model . on ( Event . CHANGE_TABLE_HTML , handleTableHTMLChange ) ;
172205 model . on ( `change:${ ModelProperty . ROW_COUNT } ` , updateButtonStates ) ;
206+ model . on ( `change:${ ModelProperty . ERROR_MESSAGE } ` , handleErrorMessageChange ) ;
173207 model . on ( `change:_initial_load_complete` , ( val ) => {
174208 if ( val ) {
175209 updateButtonStates ( ) ;
@@ -188,11 +222,13 @@ function render({ model, el }) {
188222 footer . appendChild ( paginationContainer ) ;
189223 footer . appendChild ( pageSizeContainer ) ;
190224
225+ el . appendChild ( errorContainer ) ;
191226 el . appendChild ( tableContainer ) ;
192227 el . appendChild ( footer ) ;
193228
194229 // Initial render
195230 handleTableHTMLChange ( ) ;
231+ handleErrorMessageChange ( ) ;
196232}
197233
198234export default { render } ;
0 commit comments