feat: implement DeesTable component with schema-first columns API, data actions, and customizable styles
- Added DeesTable class extending DeesElement - Introduced properties for headings, data, actions, and columns - Implemented rendering logic for table headers, rows, and cells - Added support for sorting, searching, and context menus - Included customizable styles for table layout and appearance - Integrated editable fields and drag-and-drop file handling - Enhanced accessibility with ARIA attributes for sorting
This commit is contained in:
		
							
								
								
									
										1
									
								
								.serena/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.serena/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /cache | ||||
							
								
								
									
										67
									
								
								.serena/project.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								.serena/project.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| # language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby) | ||||
| #  * For C, use cpp | ||||
| #  * For JavaScript, use typescript | ||||
| # Special requirements: | ||||
| #  * csharp: Requires the presence of a .sln file in the project folder. | ||||
| language: typescript | ||||
|  | ||||
| # whether to use the project's gitignore file to ignore files | ||||
| # Added on 2025-04-07 | ||||
| ignore_all_files_in_gitignore: true | ||||
| # list of additional paths to ignore | ||||
| # same syntax as gitignore, so you can use * and ** | ||||
| # Was previously called `ignored_dirs`, please update your config if you are using that. | ||||
| # Added (renamed) on 2025-04-07 | ||||
| ignored_paths: [] | ||||
|  | ||||
| # whether the project is in read-only mode | ||||
| # If set to true, all editing tools will be disabled and attempts to use them will result in an error | ||||
| # Added on 2025-04-18 | ||||
| read_only: false | ||||
|  | ||||
| # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. | ||||
| # Below is the complete list of tools for convenience. | ||||
| # To make sure you have the latest list of tools, and to view their descriptions,  | ||||
| # execute `uv run scripts/print_tool_overview.py`. | ||||
| # | ||||
| #  * `activate_project`: Activates a project by name. | ||||
| #  * `check_onboarding_performed`: Checks whether project onboarding was already performed. | ||||
| #  * `create_text_file`: Creates/overwrites a file in the project directory. | ||||
| #  * `delete_lines`: Deletes a range of lines within a file. | ||||
| #  * `delete_memory`: Deletes a memory from Serena's project-specific memory store. | ||||
| #  * `execute_shell_command`: Executes a shell command. | ||||
| #  * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. | ||||
| #  * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). | ||||
| #  * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). | ||||
| #  * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. | ||||
| #  * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. | ||||
| #  * `initial_instructions`: Gets the initial instructions for the current project. | ||||
| #     Should only be used in settings where the system prompt cannot be set, | ||||
| #     e.g. in clients you have no control over, like Claude Desktop. | ||||
| #  * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. | ||||
| #  * `insert_at_line`: Inserts content at a given line in a file. | ||||
| #  * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. | ||||
| #  * `list_dir`: Lists files and directories in the given directory (optionally with recursion). | ||||
| #  * `list_memories`: Lists memories in Serena's project-specific memory store. | ||||
| #  * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). | ||||
| #  * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). | ||||
| #  * `read_file`: Reads a file within the project directory. | ||||
| #  * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. | ||||
| #  * `remove_project`: Removes a project from the Serena configuration. | ||||
| #  * `replace_lines`: Replaces a range of lines within a file with new content. | ||||
| #  * `replace_symbol_body`: Replaces the full definition of a symbol. | ||||
| #  * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. | ||||
| #  * `search_for_pattern`: Performs a search for a pattern in the project. | ||||
| #  * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. | ||||
| #  * `switch_modes`: Activates modes by providing a list of their names | ||||
| #  * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. | ||||
| #  * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. | ||||
| #  * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. | ||||
| #  * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. | ||||
| excluded_tools: [] | ||||
|  | ||||
| # initial prompt for the project. It will always be given to the LLM upon activating the project | ||||
| # (contrary to the memories, which are loaded on demand). | ||||
| initial_prompt: "" | ||||
|  | ||||
| project_name: "dees-catalog" | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @@ -24,7 +24,7 @@ | ||||
|     "@fortawesome/free-solid-svg-icons": "^7.0.1", | ||||
|     "@push.rocks/smarti18n": "^1.0.4", | ||||
|     "@push.rocks/smartpromise": "^4.2.0", | ||||
|     "@push.rocks/smartstring": "^4.0.15", | ||||
|     "@push.rocks/smartstring": "^4.1.0", | ||||
|     "@tiptap/core": "^2.23.0", | ||||
|     "@tiptap/extension-link": "^2.23.0", | ||||
|     "@tiptap/extension-text-align": "^2.23.0", | ||||
| @@ -33,11 +33,11 @@ | ||||
|     "@tiptap/starter-kit": "^2.23.0", | ||||
|     "@tsclass/tsclass": "^9.2.0", | ||||
|     "@webcontainer/api": "1.2.0", | ||||
|     "apexcharts": "^5.3.4", | ||||
|     "apexcharts": "^5.3.5", | ||||
|     "highlight.js": "11.11.1", | ||||
|     "ibantools": "^4.5.1", | ||||
|     "lucide": "^0.542.0", | ||||
|     "monaco-editor": "^0.52.2", | ||||
|     "lucide": "^0.544.0", | ||||
|     "monaco-editor": "^0.53.0", | ||||
|     "pdfjs-dist": "^4.10.38", | ||||
|     "xterm": "^5.3.0", | ||||
|     "xterm-addon-fit": "^0.8.0" | ||||
| @@ -45,7 +45,7 @@ | ||||
|   "devDependencies": { | ||||
|     "@git.zone/tsbuild": "^2.6.8", | ||||
|     "@git.zone/tsbundle": "^2.5.1", | ||||
|     "@git.zone/tstest": "^2.3.6", | ||||
|     "@git.zone/tstest": "^2.3.8", | ||||
|     "@git.zone/tswatch": "^2.2.1", | ||||
|     "@push.rocks/projectinfo": "^5.0.2", | ||||
|     "@push.rocks/tapbundle": "^6.0.3", | ||||
|   | ||||
							
								
								
									
										583
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										583
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2
									
								
								pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								pnpm-workspace.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| onlyBuiltDependencies: | ||||
|   - puppeteer | ||||
| @@ -20,7 +20,7 @@ import { DeesInputMultitoggle } from './dees-input-multitoggle.js'; | ||||
| import { DeesInputPhone } from './dees-input-phone.js'; | ||||
| import { DeesInputTypelist } from './dees-input-typelist.js'; | ||||
| import { DeesFormSubmit } from './dees-form-submit.js'; | ||||
| import { DeesTable } from './dees-table.js'; | ||||
| import { DeesTable } from './dees-table/dees-table.js'; | ||||
| import { demoFunc } from './dees-form.demo.js'; | ||||
|  | ||||
| // Unified set for form input types | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { type ITableAction } from './dees-table.js'; | ||||
| import * as plugins from './00plugins.js'; | ||||
| import * as plugins from '../00plugins.js'; | ||||
| import { html, css, cssManager } from '@design.estate/dees-element'; | ||||
| 
 | ||||
| interface ITableDemoData { | ||||
| @@ -427,6 +427,46 @@ export const demoFunc = () => html` | ||||
|           dataName="items" | ||||
|         ></dees-table> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="demo-section"> | ||||
|         <h2 class="demo-title">Schema-First Columns (New)</h2> | ||||
|         <p class="demo-description">Defines columns explicitly and renders via schema. No displayFunction needed.</p> | ||||
|         <dees-table | ||||
|           heading1="Users (Schema-First)" | ||||
|           heading2="Columns define rendering and order" | ||||
|           .columns=${[ | ||||
|             { key: 'name', header: 'Name', sortable: true }, | ||||
|             { key: 'email', header: 'Email', renderer: (v: string) => html`<dees-badge>${v}</dees-badge>` }, | ||||
|             { key: 'joinedAt', header: 'Joined', renderer: (v: string) => new Date(v).toLocaleDateString() }, | ||||
|           ]} | ||||
|           .data=${[ | ||||
|             { name: 'Alice', email: 'alice@example.com', joinedAt: '2022-08-01' }, | ||||
|             { name: 'Bob', email: 'bob@example.com', joinedAt: '2021-12-11' }, | ||||
|             { name: 'Carol', email: 'carol@example.com', joinedAt: '2023-03-22' }, | ||||
|           ]} | ||||
|           dataName="users" | ||||
|         ></dees-table> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="demo-section"> | ||||
|         <h2 class="demo-title">Partial Schema + Augment (New)</h2> | ||||
|         <p class="demo-description">Provides only the important columns; the rest are merged in from displayFunction.</p> | ||||
|         <dees-table | ||||
|           heading1="Users (Partial + Augment)" | ||||
|           heading2="Missing columns are derived" | ||||
|           .columns=${[ | ||||
|             { key: 'name', header: 'Name', sortable: true }, | ||||
|           ]} | ||||
|           .displayFunction=${(u: any) => ({ name: u.name, email: u.email, role: u.role })} | ||||
|           .augmentFromDisplayFunction=${true} | ||||
|           .data=${[ | ||||
|             { name: 'Erin', email: 'erin@example.com', role: 'Admin' }, | ||||
|             { name: 'Finn', email: 'finn@example.com', role: 'User' }, | ||||
|             { name: 'Gina', email: 'gina@example.com', role: 'User' }, | ||||
|           ]} | ||||
|           dataName="users" | ||||
|         ></dees-table> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| `;
 | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from './00plugins.js'; | ||||
| import * as plugins from '../00plugins.js'; | ||||
| import { demoFunc } from './dees-table.demo.js'; | ||||
| import { cssGeistFontFamily } from './00fonts.js'; | ||||
| import { cssGeistFontFamily } from '../00fonts.js'; | ||||
| import { | ||||
|   customElement, | ||||
|   html, | ||||
| @@ -12,10 +12,10 @@ import { | ||||
|   directives, | ||||
| } from '@design.estate/dees-element'; | ||||
| 
 | ||||
| import { DeesContextmenu } from './dees-contextmenu.js'; | ||||
| import { DeesContextmenu } from '../dees-contextmenu.js'; | ||||
| 
 | ||||
| import * as domtools from '@design.estate/dees-domtools'; | ||||
| import { type TIconKey } from './dees-icon.js'; | ||||
| import { type TIconKey } from '../dees-icon.js'; | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
| @@ -63,6 +63,21 @@ export interface ITableActionDataArg<T> { | ||||
|   table: DeesTable<T>; | ||||
| } | ||||
| 
 | ||||
| // schema-first columns API (Phase 1)
 | ||||
| export interface Column<T = any> { | ||||
|   /** key in the raw item or a computed key name */ | ||||
|   key: keyof T | string; | ||||
|   /** header label or template; defaults to key */ | ||||
|   header?: string | TemplateResult; | ||||
|   /** compute the cell value when not reading directly by key */ | ||||
|   value?: (row: T) => any; | ||||
|   /** optional cell renderer */ | ||||
|   renderer?: (value: any, row: T, ctx: { rowIndex: number; colIndex: number; column: Column<T> }) => TemplateResult | string; | ||||
|   /** reserved for future phases; present to sketch intent */ | ||||
|   sortable?: boolean; | ||||
|   hidden?: boolean; | ||||
| } | ||||
| 
 | ||||
| export type TDisplayFunction<T = any> = (itemArg: T) => object; | ||||
| 
 | ||||
| // the table implementation
 | ||||
| @@ -134,6 +149,24 @@ export class DeesTable<T> extends DeesElement { | ||||
|   }) | ||||
|   public dataActions: ITableAction<T>[] = []; | ||||
| 
 | ||||
|   // schema-first columns API
 | ||||
|   @property({ attribute: false }) | ||||
|   public columns: Column<T>[] = []; | ||||
| 
 | ||||
|   /** | ||||
|    * Stable row identity for selection and updates. If provided as a function, | ||||
|    * it is only usable as a property (not via attribute). | ||||
|    */ | ||||
|   @property({ attribute: false }) | ||||
|   public rowKey?: keyof T | ((row: T) => string); | ||||
| 
 | ||||
|   /** | ||||
|    * When true and columns are provided, merge any missing columns discovered | ||||
|    * via displayFunction into the effective schema. | ||||
|    */ | ||||
|   @property({ type: Boolean }) | ||||
|   public augmentFromDisplayFunction: boolean = false; | ||||
| 
 | ||||
|   @property({ | ||||
|     attribute: false, | ||||
|   }) | ||||
| @@ -180,6 +213,12 @@ export class DeesTable<T> extends DeesElement { | ||||
| 
 | ||||
|   public dataChangeSubject = new domtools.plugins.smartrx.rxjs.Subject(); | ||||
| 
 | ||||
|   // simple client-side sorting (Phase 1)
 | ||||
|   @property({ attribute: false }) | ||||
|   private sortKey?: string; | ||||
|   @property({ attribute: false }) | ||||
|   private sortDir: 'asc' | 'desc' | null = null; | ||||
| 
 | ||||
|   constructor() { | ||||
|     super(); | ||||
|   } | ||||
| @@ -544,6 +583,11 @@ export class DeesTable<T> extends DeesElement { | ||||
|   ]; | ||||
| 
 | ||||
|   public render(): TemplateResult { | ||||
|     const usingColumns = Array.isArray(this.columns) && this.columns.length > 0; | ||||
|     const effectiveColumns: Column<T>[] = usingColumns | ||||
|       ? this.computeEffectiveColumns() | ||||
|       : this.computeColumnsFromDisplayFunction(); | ||||
| 
 | ||||
|     return html` | ||||
|       <div class="mainbox"> | ||||
|         <!-- the heading part --> | ||||
| @@ -609,32 +653,35 @@ export class DeesTable<T> extends DeesElement { | ||||
|         <!-- the actual table --> | ||||
|         <style></style> | ||||
|         ${this.data.length > 0 | ||||
|           ? (() => { | ||||
|               // Only pick up the keys from the first transformed data object
 | ||||
|               // as all data objects are assumed to have the same structure
 | ||||
|               const firstTransformedItem = this.displayFunction(this.data[0]); | ||||
|               const headings: string[] = Object.keys(firstTransformedItem); | ||||
|               return html` | ||||
|                 <table> | ||||
|                   <thead> | ||||
|                     <tr> | ||||
|                       ${headings.map( | ||||
|                         (headingArg) => html` | ||||
|                           <th>${headingArg}</th> | ||||
|                         ` | ||||
|                       )} | ||||
|                       ${(() => { | ||||
|                         if (this.dataActions && this.dataActions.length > 0) { | ||||
|                           return html` | ||||
|                             <th>Actions</th> | ||||
|                           `;
 | ||||
|                         } | ||||
|                       })()} | ||||
|                     </tr> | ||||
|                   </thead> | ||||
|                   <tbody> | ||||
|                   ${this.data.map((itemArg) => { | ||||
|                     const transformedItem = this.displayFunction(itemArg); | ||||
|           ? html` | ||||
|               <table> | ||||
|                 <thead> | ||||
|                   <tr> | ||||
|                     ${effectiveColumns | ||||
|                       .filter((c) => !c.hidden) | ||||
|                       .map((col) => { | ||||
|                         const isSortable = !!col.sortable; | ||||
|                         const ariaSort = this.getAriaSort(col); | ||||
|                         return html` | ||||
|                           <th | ||||
|                             role="columnheader" | ||||
|                             aria-sort=${ariaSort} | ||||
|                             style="${isSortable ? 'cursor: pointer;' : ''}" | ||||
|                             @click=${() => (isSortable ? this.toggleSort(col) : null)} | ||||
|                           > | ||||
|                             ${col.header ?? (col.key as any)} | ||||
|                             ${this.renderSortIndicator(col)} | ||||
|                           </th>`;
 | ||||
|                       })} | ||||
|                     ${(() => { | ||||
|                       if (this.dataActions && this.dataActions.length > 0) { | ||||
|                         return html` <th>Actions</th> `; | ||||
|                       } | ||||
|                     })()} | ||||
|                   </tr> | ||||
|                 </thead> | ||||
|                 <tbody> | ||||
|                   ${this.getViewData(effectiveColumns).map((itemArg, rowIndex) => { | ||||
|                     const getTr = (elementArg: HTMLElement): HTMLElement => { | ||||
|                       if (elementArg.tagName === 'TR') { | ||||
|                         return elementArg; | ||||
| @@ -651,8 +698,6 @@ export class DeesTable<T> extends DeesElement { | ||||
|                           eventArg.preventDefault(); | ||||
|                           eventArg.stopPropagation(); | ||||
|                           const realTarget = getTr(eventArg.target as HTMLElement); | ||||
|                           console.log('dragenter'); | ||||
|                           console.log(realTarget); | ||||
|                           setTimeout(() => { | ||||
|                             realTarget.classList.add('hasAttachment'); | ||||
|                           }, 0); | ||||
| @@ -702,29 +747,31 @@ export class DeesTable<T> extends DeesElement { | ||||
|                         }} | ||||
|                         class="${itemArg === this.selectedDataRow ? 'selected' : ''}" | ||||
|                       > | ||||
|                         ${headings.map( | ||||
|                           (headingArg) => html` | ||||
|                             <td | ||||
|                               @dblclick=${(e: Event) => { | ||||
|                                 if (this.editableFields.includes(headingArg)) { | ||||
|                                   this.handleCellEditing(e, itemArg, headingArg); | ||||
|                                 } else { | ||||
|                                   const wantedAction = this.dataActions.find((actionArg) => | ||||
|                         ${effectiveColumns | ||||
|                           .filter((c) => !c.hidden) | ||||
|                           .map((col, colIndex) => { | ||||
|                             const value = this.getCellValue(itemArg, col); | ||||
|                             const content = col.renderer | ||||
|                               ? col.renderer(value, itemArg, { rowIndex, colIndex, column: col }) | ||||
|                               : value; | ||||
|                             const editKey = String(col.key); | ||||
|                             return html` | ||||
|                               <td | ||||
|                                 @dblclick=${(e: Event) => { | ||||
|                                   const dblAction = this.dataActions.find((actionArg) => | ||||
|                                     actionArg.type.includes('doubleClick') | ||||
|                                   ); | ||||
|                                   if (wantedAction) { | ||||
|                                     wantedAction.actionFunc({ | ||||
|                                       item: itemArg, | ||||
|                                       table: this, | ||||
|                                     }); | ||||
|                                   if (this.editableFields.includes(editKey)) { | ||||
|                                     this.handleCellEditing(e, itemArg, editKey); | ||||
|                                   } else if (dblAction) { | ||||
|                                     dblAction.actionFunc({ item: itemArg, table: this }); | ||||
|                                   } | ||||
|                                 } | ||||
|                               }} | ||||
|                             > | ||||
|                               <div class="innerCellContainer">${transformedItem[headingArg]}</div> | ||||
|                             </td> | ||||
|                           ` | ||||
|                         )} | ||||
|                                 }} | ||||
|                               > | ||||
|                                 <div class="innerCellContainer">${content}</div> | ||||
|                               </td> | ||||
|                             `;
 | ||||
|                           })} | ||||
|                         ${(() => { | ||||
|                           if (this.dataActions && this.dataActions.length > 0) { | ||||
|                             return html` | ||||
| @@ -732,36 +779,30 @@ export class DeesTable<T> extends DeesElement { | ||||
|                                 <div class="actionsContainer"> | ||||
|                                   ${this.getActionsForType('inRow').map( | ||||
|                                     (actionArg) => html` | ||||
|                                         <div | ||||
|                                           class="action" | ||||
|                                           @click=${() => | ||||
|                                             actionArg.actionFunc({ | ||||
|                                               item: itemArg, | ||||
|                                               table: this, | ||||
|                                             })} | ||||
|                                         > | ||||
|                                           ${actionArg.iconName | ||||
|                                             ? html` | ||||
|                                                 <dees-icon | ||||
|                                                   .icon=${actionArg.iconName} | ||||
|                                                 ></dees-icon> | ||||
|                                               ` | ||||
|                                             : actionArg.name} | ||||
|                                         </div> | ||||
|                                       ` | ||||
|                                     )} | ||||
|                                       <div | ||||
|                                         class="action" | ||||
|                                         @click=${() => | ||||
|                                           actionArg.actionFunc({ | ||||
|                                             item: itemArg, | ||||
|                                             table: this, | ||||
|                                           })} | ||||
|                                       > | ||||
|                                         ${actionArg.iconName | ||||
|                                           ? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> ` | ||||
|                                           : actionArg.name} | ||||
|                                       </div> | ||||
|                                     ` | ||||
|                                   )} | ||||
|                                 </div> | ||||
|                               </td> | ||||
|                             `;
 | ||||
|                           } | ||||
|                         })()} | ||||
|                       </tr> | ||||
|                     `;
 | ||||
|                       </tr>`;
 | ||||
|                   })} | ||||
|                   </tbody> | ||||
|                 </table> | ||||
|               `;
 | ||||
|             })() | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             ` | ||||
|           : html` <div class="noDataSet">No data set!</div> `} | ||||
|         <div class="footer"> | ||||
|           <div class="tableStatistics"> | ||||
| @@ -869,6 +910,87 @@ export class DeesTable<T> extends DeesElement { | ||||
|     table.style.tableLayout = 'fixed'; | ||||
|   } | ||||
| 
 | ||||
|   private computeColumnsFromDisplayFunction(): Column<T>[] { | ||||
|     if (!this.data || this.data.length === 0) return []; | ||||
|     const firstTransformedItem = this.displayFunction(this.data[0]); | ||||
|     const keys: string[] = Object.keys(firstTransformedItem); | ||||
|     return keys.map((key) => ({ | ||||
|       key, | ||||
|       header: key, | ||||
|       value: (row: T) => this.displayFunction(row)[key], | ||||
|     })); | ||||
|   } | ||||
| 
 | ||||
|   private computeEffectiveColumns(): Column<T>[] { | ||||
|     const base = (this.columns || []).slice(); | ||||
|     if (!this.augmentFromDisplayFunction) return base; | ||||
|     const fromDisplay = this.computeColumnsFromDisplayFunction(); | ||||
|     const existingKeys = new Set(base.map((c) => String(c.key))); | ||||
|     for (const col of fromDisplay) { | ||||
|       if (!existingKeys.has(String(col.key))) { | ||||
|         base.push(col); | ||||
|       } | ||||
|     } | ||||
|     return base; | ||||
|   } | ||||
| 
 | ||||
|   private getCellValue(row: T, col: Column<T>): any { | ||||
|     return col.value ? col.value(row) : (row as any)[col.key as any]; | ||||
|   } | ||||
| 
 | ||||
|   private getViewData(effectiveColumns: Column<T>[]): T[] { | ||||
|     if (!this.sortKey || !this.sortDir) return this.data; | ||||
|     const col = effectiveColumns.find((c) => String(c.key) === this.sortKey); | ||||
|     if (!col) return this.data; | ||||
|     const arr = this.data.slice(); | ||||
|     const dir = this.sortDir === 'asc' ? 1 : -1; | ||||
|     arr.sort((a, b) => { | ||||
|       const va = this.getCellValue(a, col); | ||||
|       const vb = this.getCellValue(b, col); | ||||
|       if (va == null && vb == null) return 0; | ||||
|       if (va == null) return -1 * dir; | ||||
|       if (vb == null) return 1 * dir; | ||||
|       if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir; | ||||
|       const sa = String(va).toLowerCase(); | ||||
|       const sb = String(vb).toLowerCase(); | ||||
|       if (sa < sb) return -1 * dir; | ||||
|       if (sa > sb) return 1 * dir; | ||||
|       return 0; | ||||
|     }); | ||||
|     return arr; | ||||
|   } | ||||
| 
 | ||||
|   private toggleSort(col: Column<T>) { | ||||
|     const key = String(col.key); | ||||
|     if (this.sortKey !== key) { | ||||
|       this.sortKey = key; | ||||
|       this.sortDir = 'asc'; | ||||
|     } else { | ||||
|       if (this.sortDir === 'asc') this.sortDir = 'desc'; | ||||
|       else if (this.sortDir === 'desc') { | ||||
|         this.sortDir = null; | ||||
|         this.sortKey = undefined; | ||||
|       } else this.sortDir = 'asc'; | ||||
|     } | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent('sortChange', { | ||||
|         detail: { key: this.sortKey, dir: this.sortDir }, | ||||
|         bubbles: true, | ||||
|       }) | ||||
|     ); | ||||
|     this.requestUpdate(); | ||||
|   } | ||||
| 
 | ||||
|   private getAriaSort(col: Column<T>): 'none' | 'ascending' | 'descending' { | ||||
|     if (String(col.key) !== this.sortKey || !this.sortDir) return 'none'; | ||||
|     return this.sortDir === 'asc' ? 'ascending' : 'descending'; | ||||
|   } | ||||
| 
 | ||||
|   private renderSortIndicator(col: Column<T>) { | ||||
|     if (String(col.key) !== this.sortKey || !this.sortDir) return html``; | ||||
|     return html`<span style="margin-left:6px; opacity:0.7;">${this.sortDir === 'asc' ? '▲' : '▼'}</span>`; | ||||
|   } | ||||
| 
 | ||||
|   getActionsForType(typeArg: ITableAction['type'][0]) { | ||||
|     const actions: ITableAction[] = []; | ||||
|     for (const action of this.dataActions) { | ||||
| @@ -884,7 +1006,7 @@ export class DeesTable<T> extends DeesElement { | ||||
|     const originalColor = target.style.color; | ||||
|     target.style.color = 'transparent'; | ||||
|     const transformedItem = this.displayFunction(itemArg); | ||||
|     const initialValue = (transformedItem[key] as unknown as string) || ''; | ||||
|     const initialValue = ((transformedItem as any)[key] ?? (itemArg as any)[key] ?? '') as string; | ||||
|     // Create an input element
 | ||||
|     const input = document.createElement('input'); | ||||
|     input.type = 'text'; | ||||
| @@ -57,7 +57,7 @@ export * from './dees-speechbubble.js'; | ||||
| export * from './dees-spinner.js'; | ||||
| export * from './dees-statsgrid.js'; | ||||
| export * from './dees-stepper.js'; | ||||
| export * from './dees-table.js'; | ||||
| export * from './dees-table/dees-table.js'; | ||||
| export * from './dees-terminal.js'; | ||||
| export * from './dees-toast.js'; | ||||
| export * from './dees-updater.js'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user