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", |     "@fortawesome/free-solid-svg-icons": "^7.0.1", | ||||||
|     "@push.rocks/smarti18n": "^1.0.4", |     "@push.rocks/smarti18n": "^1.0.4", | ||||||
|     "@push.rocks/smartpromise": "^4.2.0", |     "@push.rocks/smartpromise": "^4.2.0", | ||||||
|     "@push.rocks/smartstring": "^4.0.15", |     "@push.rocks/smartstring": "^4.1.0", | ||||||
|     "@tiptap/core": "^2.23.0", |     "@tiptap/core": "^2.23.0", | ||||||
|     "@tiptap/extension-link": "^2.23.0", |     "@tiptap/extension-link": "^2.23.0", | ||||||
|     "@tiptap/extension-text-align": "^2.23.0", |     "@tiptap/extension-text-align": "^2.23.0", | ||||||
| @@ -33,11 +33,11 @@ | |||||||
|     "@tiptap/starter-kit": "^2.23.0", |     "@tiptap/starter-kit": "^2.23.0", | ||||||
|     "@tsclass/tsclass": "^9.2.0", |     "@tsclass/tsclass": "^9.2.0", | ||||||
|     "@webcontainer/api": "1.2.0", |     "@webcontainer/api": "1.2.0", | ||||||
|     "apexcharts": "^5.3.4", |     "apexcharts": "^5.3.5", | ||||||
|     "highlight.js": "11.11.1", |     "highlight.js": "11.11.1", | ||||||
|     "ibantools": "^4.5.1", |     "ibantools": "^4.5.1", | ||||||
|     "lucide": "^0.542.0", |     "lucide": "^0.544.0", | ||||||
|     "monaco-editor": "^0.52.2", |     "monaco-editor": "^0.53.0", | ||||||
|     "pdfjs-dist": "^4.10.38", |     "pdfjs-dist": "^4.10.38", | ||||||
|     "xterm": "^5.3.0", |     "xterm": "^5.3.0", | ||||||
|     "xterm-addon-fit": "^0.8.0" |     "xterm-addon-fit": "^0.8.0" | ||||||
| @@ -45,7 +45,7 @@ | |||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@git.zone/tsbuild": "^2.6.8", |     "@git.zone/tsbuild": "^2.6.8", | ||||||
|     "@git.zone/tsbundle": "^2.5.1", |     "@git.zone/tsbundle": "^2.5.1", | ||||||
|     "@git.zone/tstest": "^2.3.6", |     "@git.zone/tstest": "^2.3.8", | ||||||
|     "@git.zone/tswatch": "^2.2.1", |     "@git.zone/tswatch": "^2.2.1", | ||||||
|     "@push.rocks/projectinfo": "^5.0.2", |     "@push.rocks/projectinfo": "^5.0.2", | ||||||
|     "@push.rocks/tapbundle": "^6.0.3", |     "@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 { DeesInputPhone } from './dees-input-phone.js'; | ||||||
| import { DeesInputTypelist } from './dees-input-typelist.js'; | import { DeesInputTypelist } from './dees-input-typelist.js'; | ||||||
| import { DeesFormSubmit } from './dees-form-submit.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'; | import { demoFunc } from './dees-form.demo.js'; | ||||||
|  |  | ||||||
| // Unified set for form input types | // Unified set for form input types | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { type ITableAction } from './dees-table.js'; | 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'; | import { html, css, cssManager } from '@design.estate/dees-element'; | ||||||
| 
 | 
 | ||||||
| interface ITableDemoData { | interface ITableDemoData { | ||||||
| @@ -427,6 +427,46 @@ export const demoFunc = () => html` | |||||||
|           dataName="items" |           dataName="items" | ||||||
|         ></dees-table> |         ></dees-table> | ||||||
|       </div> |       </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> | ||||||
|   </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 { demoFunc } from './dees-table.demo.js'; | ||||||
| import { cssGeistFontFamily } from './00fonts.js'; | import { cssGeistFontFamily } from '../00fonts.js'; | ||||||
| import { | import { | ||||||
|   customElement, |   customElement, | ||||||
|   html, |   html, | ||||||
| @@ -12,10 +12,10 @@ import { | |||||||
|   directives, |   directives, | ||||||
| } from '@design.estate/dees-element'; | } 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 * as domtools from '@design.estate/dees-domtools'; | ||||||
| import { type TIconKey } from './dees-icon.js'; | import { type TIconKey } from '../dees-icon.js'; | ||||||
| 
 | 
 | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
| @@ -63,6 +63,21 @@ export interface ITableActionDataArg<T> { | |||||||
|   table: DeesTable<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; | export type TDisplayFunction<T = any> = (itemArg: T) => object; | ||||||
| 
 | 
 | ||||||
| // the table implementation
 | // the table implementation
 | ||||||
| @@ -134,6 +149,24 @@ export class DeesTable<T> extends DeesElement { | |||||||
|   }) |   }) | ||||||
|   public dataActions: ITableAction<T>[] = []; |   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({ |   @property({ | ||||||
|     attribute: false, |     attribute: false, | ||||||
|   }) |   }) | ||||||
| @@ -180,6 +213,12 @@ export class DeesTable<T> extends DeesElement { | |||||||
| 
 | 
 | ||||||
|   public dataChangeSubject = new domtools.plugins.smartrx.rxjs.Subject(); |   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() { |   constructor() { | ||||||
|     super(); |     super(); | ||||||
|   } |   } | ||||||
| @@ -544,6 +583,11 @@ export class DeesTable<T> extends DeesElement { | |||||||
|   ]; |   ]; | ||||||
| 
 | 
 | ||||||
|   public render(): TemplateResult { |   public render(): TemplateResult { | ||||||
|  |     const usingColumns = Array.isArray(this.columns) && this.columns.length > 0; | ||||||
|  |     const effectiveColumns: Column<T>[] = usingColumns | ||||||
|  |       ? this.computeEffectiveColumns() | ||||||
|  |       : this.computeColumnsFromDisplayFunction(); | ||||||
|  | 
 | ||||||
|     return html` |     return html` | ||||||
|       <div class="mainbox"> |       <div class="mainbox"> | ||||||
|         <!-- the heading part --> |         <!-- the heading part --> | ||||||
| @@ -609,32 +653,35 @@ export class DeesTable<T> extends DeesElement { | |||||||
|         <!-- the actual table --> |         <!-- the actual table --> | ||||||
|         <style></style> |         <style></style> | ||||||
|         ${this.data.length > 0 |         ${this.data.length > 0 | ||||||
|           ? (() => { |           ? html` | ||||||
|               // Only pick up the keys from the first transformed data object
 |               <table> | ||||||
|               // as all data objects are assumed to have the same structure
 |                 <thead> | ||||||
|               const firstTransformedItem = this.displayFunction(this.data[0]); |                   <tr> | ||||||
|               const headings: string[] = Object.keys(firstTransformedItem); |                     ${effectiveColumns | ||||||
|               return html` |                       .filter((c) => !c.hidden) | ||||||
|                 <table> |                       .map((col) => { | ||||||
|                   <thead> |                         const isSortable = !!col.sortable; | ||||||
|                     <tr> |                         const ariaSort = this.getAriaSort(col); | ||||||
|                       ${headings.map( |                         return html` | ||||||
|                         (headingArg) => html` |                           <th | ||||||
|                           <th>${headingArg}</th> |                             role="columnheader" | ||||||
|                         ` |                             aria-sort=${ariaSort} | ||||||
|                       )} |                             style="${isSortable ? 'cursor: pointer;' : ''}" | ||||||
|                       ${(() => { |                             @click=${() => (isSortable ? this.toggleSort(col) : null)} | ||||||
|                         if (this.dataActions && this.dataActions.length > 0) { |                           > | ||||||
|                           return html` |                             ${col.header ?? (col.key as any)} | ||||||
|                             <th>Actions</th> |                             ${this.renderSortIndicator(col)} | ||||||
|                           `;
 |                           </th>`;
 | ||||||
|                         } |                       })} | ||||||
|                       })()} |                     ${(() => { | ||||||
|                     </tr> |                       if (this.dataActions && this.dataActions.length > 0) { | ||||||
|                   </thead> |                         return html` <th>Actions</th> `; | ||||||
|                   <tbody> |                       } | ||||||
|                   ${this.data.map((itemArg) => { |                     })()} | ||||||
|                     const transformedItem = this.displayFunction(itemArg); |                   </tr> | ||||||
|  |                 </thead> | ||||||
|  |                 <tbody> | ||||||
|  |                   ${this.getViewData(effectiveColumns).map((itemArg, rowIndex) => { | ||||||
|                     const getTr = (elementArg: HTMLElement): HTMLElement => { |                     const getTr = (elementArg: HTMLElement): HTMLElement => { | ||||||
|                       if (elementArg.tagName === 'TR') { |                       if (elementArg.tagName === 'TR') { | ||||||
|                         return elementArg; |                         return elementArg; | ||||||
| @@ -651,8 +698,6 @@ export class DeesTable<T> extends DeesElement { | |||||||
|                           eventArg.preventDefault(); |                           eventArg.preventDefault(); | ||||||
|                           eventArg.stopPropagation(); |                           eventArg.stopPropagation(); | ||||||
|                           const realTarget = getTr(eventArg.target as HTMLElement); |                           const realTarget = getTr(eventArg.target as HTMLElement); | ||||||
|                           console.log('dragenter'); |  | ||||||
|                           console.log(realTarget); |  | ||||||
|                           setTimeout(() => { |                           setTimeout(() => { | ||||||
|                             realTarget.classList.add('hasAttachment'); |                             realTarget.classList.add('hasAttachment'); | ||||||
|                           }, 0); |                           }, 0); | ||||||
| @@ -702,29 +747,31 @@ export class DeesTable<T> extends DeesElement { | |||||||
|                         }} |                         }} | ||||||
|                         class="${itemArg === this.selectedDataRow ? 'selected' : ''}" |                         class="${itemArg === this.selectedDataRow ? 'selected' : ''}" | ||||||
|                       > |                       > | ||||||
|                         ${headings.map( |                         ${effectiveColumns | ||||||
|                           (headingArg) => html` |                           .filter((c) => !c.hidden) | ||||||
|                             <td |                           .map((col, colIndex) => { | ||||||
|                               @dblclick=${(e: Event) => { |                             const value = this.getCellValue(itemArg, col); | ||||||
|                                 if (this.editableFields.includes(headingArg)) { |                             const content = col.renderer | ||||||
|                                   this.handleCellEditing(e, itemArg, headingArg); |                               ? col.renderer(value, itemArg, { rowIndex, colIndex, column: col }) | ||||||
|                                 } else { |                               : value; | ||||||
|                                   const wantedAction = this.dataActions.find((actionArg) => |                             const editKey = String(col.key); | ||||||
|  |                             return html` | ||||||
|  |                               <td | ||||||
|  |                                 @dblclick=${(e: Event) => { | ||||||
|  |                                   const dblAction = this.dataActions.find((actionArg) => | ||||||
|                                     actionArg.type.includes('doubleClick') |                                     actionArg.type.includes('doubleClick') | ||||||
|                                   ); |                                   ); | ||||||
|                                   if (wantedAction) { |                                   if (this.editableFields.includes(editKey)) { | ||||||
|                                     wantedAction.actionFunc({ |                                     this.handleCellEditing(e, itemArg, editKey); | ||||||
|                                       item: itemArg, |                                   } else if (dblAction) { | ||||||
|                                       table: this, |                                     dblAction.actionFunc({ item: itemArg, table: this }); | ||||||
|                                     }); |  | ||||||
|                                   } |                                   } | ||||||
|                                 } |                                 }} | ||||||
|                               }} |                               > | ||||||
|                             > |                                 <div class="innerCellContainer">${content}</div> | ||||||
|                               <div class="innerCellContainer">${transformedItem[headingArg]}</div> |                               </td> | ||||||
|                             </td> |                             `;
 | ||||||
|                           ` |                           })} | ||||||
|                         )} |  | ||||||
|                         ${(() => { |                         ${(() => { | ||||||
|                           if (this.dataActions && this.dataActions.length > 0) { |                           if (this.dataActions && this.dataActions.length > 0) { | ||||||
|                             return html` |                             return html` | ||||||
| @@ -732,36 +779,30 @@ export class DeesTable<T> extends DeesElement { | |||||||
|                                 <div class="actionsContainer"> |                                 <div class="actionsContainer"> | ||||||
|                                   ${this.getActionsForType('inRow').map( |                                   ${this.getActionsForType('inRow').map( | ||||||
|                                     (actionArg) => html` |                                     (actionArg) => html` | ||||||
|                                         <div |                                       <div | ||||||
|                                           class="action" |                                         class="action" | ||||||
|                                           @click=${() => |                                         @click=${() => | ||||||
|                                             actionArg.actionFunc({ |                                           actionArg.actionFunc({ | ||||||
|                                               item: itemArg, |                                             item: itemArg, | ||||||
|                                               table: this, |                                             table: this, | ||||||
|                                             })} |                                           })} | ||||||
|                                         > |                                       > | ||||||
|                                           ${actionArg.iconName |                                         ${actionArg.iconName | ||||||
|                                             ? html` |                                           ? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> ` | ||||||
|                                                 <dees-icon |                                           : actionArg.name} | ||||||
|                                                   .icon=${actionArg.iconName} |                                       </div> | ||||||
|                                                 ></dees-icon> |                                     ` | ||||||
|                                               ` |                                   )} | ||||||
|                                             : actionArg.name} |  | ||||||
|                                         </div> |  | ||||||
|                                       ` |  | ||||||
|                                     )} |  | ||||||
|                                 </div> |                                 </div> | ||||||
|                               </td> |                               </td> | ||||||
|                             `;
 |                             `;
 | ||||||
|                           } |                           } | ||||||
|                         })()} |                         })()} | ||||||
|                       </tr> |                       </tr>`;
 | ||||||
|                     `;
 |  | ||||||
|                   })} |                   })} | ||||||
|                   </tbody> |                 </tbody> | ||||||
|                 </table> |               </table> | ||||||
|               `;
 |             ` | ||||||
|             })() |  | ||||||
|           : html` <div class="noDataSet">No data set!</div> `} |           : html` <div class="noDataSet">No data set!</div> `} | ||||||
|         <div class="footer"> |         <div class="footer"> | ||||||
|           <div class="tableStatistics"> |           <div class="tableStatistics"> | ||||||
| @@ -869,6 +910,87 @@ export class DeesTable<T> extends DeesElement { | |||||||
|     table.style.tableLayout = 'fixed'; |     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]) { |   getActionsForType(typeArg: ITableAction['type'][0]) { | ||||||
|     const actions: ITableAction[] = []; |     const actions: ITableAction[] = []; | ||||||
|     for (const action of this.dataActions) { |     for (const action of this.dataActions) { | ||||||
| @@ -884,7 +1006,7 @@ export class DeesTable<T> extends DeesElement { | |||||||
|     const originalColor = target.style.color; |     const originalColor = target.style.color; | ||||||
|     target.style.color = 'transparent'; |     target.style.color = 'transparent'; | ||||||
|     const transformedItem = this.displayFunction(itemArg); |     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
 |     // Create an input element
 | ||||||
|     const input = document.createElement('input'); |     const input = document.createElement('input'); | ||||||
|     input.type = 'text'; |     input.type = 'text'; | ||||||
| @@ -57,7 +57,7 @@ export * from './dees-speechbubble.js'; | |||||||
| export * from './dees-spinner.js'; | export * from './dees-spinner.js'; | ||||||
| export * from './dees-statsgrid.js'; | export * from './dees-statsgrid.js'; | ||||||
| export * from './dees-stepper.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-terminal.js'; | ||||||
| export * from './dees-toast.js'; | export * from './dees-toast.js'; | ||||||
| export * from './dees-updater.js'; | export * from './dees-updater.js'; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user