@ticatec/uniface-flexi-module 0.0.2
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/FLEXICRITERIASET_GUIDE.md +1559 -0
- package/FLEXICRITERIASET_GUIDE_CN.md +1133 -0
- package/FLEXIDATATABLE_GUIDE.md +1650 -0
- package/FLEXIDATATABLE_GUIDE_CN.md +1650 -0
- package/FLEXIFORM_GUIDE.md +1068 -0
- package/FLEXIFORM_GUIDE_CN.md +1068 -0
- package/FLEXI_CONTEXT_GUIDE_CN.md +172 -0
- package/MODULE_LOADER_CN.md +228 -0
- package/README.md +307 -0
- package/README_CN.md +51 -0
- package/SANDBOX_CN.md +201 -0
- package/dist/FlexiContext.d.ts +28 -0
- package/dist/FlexiContext.js +45 -0
- package/dist/ModuleLoader.d.ts +41 -0
- package/dist/ModuleLoader.js +55 -0
- package/dist/Sandbox.d.ts +33 -0
- package/dist/Sandbox.js +101 -0
- package/dist/criteria-panel/CriteriaFieldsPanel.svelte +26 -0
- package/dist/criteria-panel/CriteriaFieldsPanel.svelte.d.ts +22 -0
- package/dist/criteria-panel/components/CascadeSelectSearchField.svelte +10 -0
- package/dist/criteria-panel/components/CascadeSelectSearchField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/DateRangeField.svelte +11 -0
- package/dist/criteria-panel/components/DateRangeField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/DateSearchField.svelte +10 -0
- package/dist/criteria-panel/components/DateSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/DateTimeSearchField.svelte +10 -0
- package/dist/criteria-panel/components/DateTimeSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/InputOptionSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/NumberRangeField.svelte +11 -0
- package/dist/criteria-panel/components/NumberRangeField.svelte.d.ts +25 -0
- package/dist/criteria-panel/components/NumberSearchField.svelte +9 -0
- package/dist/criteria-panel/components/NumberSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/OptionMultiSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/OptionSelectSearchField.svelte +9 -0
- package/dist/criteria-panel/components/OptionSelectSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/SearchField.svelte +14 -0
- package/dist/criteria-panel/components/SearchField.svelte.d.ts +33 -0
- package/dist/criteria-panel/components/TextSearchField.svelte +9 -0
- package/dist/criteria-panel/components/TextSearchField.svelte.d.ts +24 -0
- package/dist/criteria-panel/components/UnknownCriteriaField.svelte +9 -0
- package/dist/criteria-panel/components/UnknownCriteriaField.svelte.d.ts +24 -0
- package/dist/criteria-panel/index.d.ts +6 -0
- package/dist/criteria-panel/index.js +6 -0
- package/dist/criteria-panel/lib/CriteriaComponentBuilder.d.ts +19 -0
- package/dist/criteria-panel/lib/CriteriaComponentBuilder.js +31 -0
- package/dist/criteria-panel/lib/CriteriaFieldBuilder.d.ts +1 -0
- package/dist/criteria-panel/lib/CriteriaFieldBuilder.js +127 -0
- package/dist/criteria-panel/lib/FlexiCriteriaField.d.ts +38 -0
- package/dist/criteria-panel/lib/FlexiCriteriaField.js +31 -0
- package/dist/criteria-panel/lib/FlexiCriteriaSet.d.ts +24 -0
- package/dist/criteria-panel/lib/FlexiCriteriaSet.js +48 -0
- package/dist/flexi-datatable/FlexiDataTable.d.ts +111 -0
- package/dist/flexi-datatable/FlexiDataTable.js +90 -0
- package/dist/flexi-datatable/index.d.ts +2 -0
- package/dist/flexi-datatable/index.js +2 -0
- package/dist/flexi-form/FlexiCompound.d.ts +34 -0
- package/dist/flexi-form/FlexiCompound.js +84 -0
- package/dist/flexi-form/FlexiFormDialog.svelte +24 -0
- package/dist/flexi-form/FlexiFormDialog.svelte.d.ts +21 -0
- package/dist/flexi-form/FlexiFormPage.svelte +26 -0
- package/dist/flexi-form/FlexiFormPage.svelte.d.ts +25 -0
- package/dist/flexi-form/Schema.d.ts +6 -0
- package/dist/flexi-form/Schema.js +1 -0
- package/dist/flexi-form/components/BreakLine.svelte +1 -0
- package/dist/flexi-form/components/BreakLine.svelte.d.ts +26 -0
- package/dist/flexi-form/components/CardTitleBar.svelte +18 -0
- package/dist/flexi-form/components/CardTitleBar.svelte.d.ts +22 -0
- package/dist/flexi-form/components/CascadeOptionSelectField.svelte +13 -0
- package/dist/flexi-form/components/CascadeOptionSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/CellFieldBuilder.d.ts +1 -0
- package/dist/flexi-form/components/CellFieldBuilder.js +178 -0
- package/dist/flexi-form/components/DateField.svelte +12 -0
- package/dist/flexi-form/components/DateField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/DateTimeField.svelte +13 -0
- package/dist/flexi-form/components/DateTimeField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/InputOptionSelectField.svelte +13 -0
- package/dist/flexi-form/components/InputOptionSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/MemoField.svelte +12 -0
- package/dist/flexi-form/components/MemoField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/NumberField.svelte +12 -0
- package/dist/flexi-form/components/NumberField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/OptionsMultiSelectField.svelte +13 -0
- package/dist/flexi-form/components/OptionsMultiSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/OptionsSelectField.svelte +13 -0
- package/dist/flexi-form/components/OptionsSelectField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/TextField.svelte +12 -0
- package/dist/flexi-form/components/TextField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/UnitNumberField.svelte +12 -0
- package/dist/flexi-form/components/UnitNumberField.svelte.d.ts +24 -0
- package/dist/flexi-form/components/UnknownTypeField.svelte +5 -0
- package/dist/flexi-form/components/UnknownTypeField.svelte.d.ts +18 -0
- package/dist/flexi-form/containers/FlexiPanel.svelte +13 -0
- package/dist/flexi-form/containers/FlexiPanel.svelte.d.ts +33 -0
- package/dist/flexi-form/flexi_card/FlexiCard.d.ts +64 -0
- package/dist/flexi-form/flexi_card/FlexiCard.js +66 -0
- package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte +57 -0
- package/dist/flexi-form/flexi_card/FlexiCardPanel.svelte.d.ts +22 -0
- package/dist/flexi-form/flexi_composite/FlexiComposite.d.ts +50 -0
- package/dist/flexi-form/flexi_composite/FlexiComposite.js +26 -0
- package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte +42 -0
- package/dist/flexi-form/flexi_composite/FlexiCompositePanel.svelte.d.ts +25 -0
- package/dist/flexi-form/flexi_composite/README.md +50 -0
- package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.d.ts +4 -0
- package/dist/flexi-form/flexi_datasheet/FlexiDataSheet.js +2 -0
- package/dist/flexi-form/flexi_field/FlexiField.d.ts +76 -0
- package/dist/flexi-form/flexi_field/FlexiField.js +128 -0
- package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte +35 -0
- package/dist/flexi-form/flexi_field/FlexiFieldCell.svelte.d.ts +25 -0
- package/dist/flexi-form/flexi_field/UnknownField.d.ts +3 -0
- package/dist/flexi-form/flexi_field/UnknownField.js +3 -0
- package/dist/flexi-form/flexi_form/FlexiForm.d.ts +127 -0
- package/dist/flexi-form/flexi_form/FlexiForm.js +160 -0
- package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte +57 -0
- package/dist/flexi-form/flexi_form/FlexiFormPanel.svelte.d.ts +25 -0
- package/dist/flexi-form/index.d.ts +11 -0
- package/dist/flexi-form/index.js +11 -0
- package/dist/flexi-form/lib/ComponentBuilder.d.ts +15 -0
- package/dist/flexi-form/lib/ComponentBuilder.js +31 -0
- package/dist/flexi-form/lib/index.d.ts +5 -0
- package/dist/flexi-form/lib/index.js +2 -0
- package/dist/flexi-form/lib/types.d.ts +7 -0
- package/dist/flexi-form/lib/types.js +6 -0
- package/dist/flexi-form/lib/utils.d.ts +10 -0
- package/dist/flexi-form/lib/utils.js +48 -0
- package/dist/i18n-res/i18nRes.d.ts +2 -0
- package/dist/i18n-res/i18nRes.js +8 -0
- package/dist/i18n-res/index.d.ts +2 -0
- package/dist/i18n-res/index.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/uniface-flexi-module.css +46 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +8 -0
- package/package.json +135 -0
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
# FlexiDataTable Complete Usage Guide
|
|
2
|
+
|
|
3
|
+
A comprehensive guide to using FlexiDataTable for building powerful, flexible data tables in Svelte applications.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Overview](#overview)
|
|
8
|
+
2. [Basic Usage](#basic-usage)
|
|
9
|
+
3. [Schema Structure](#schema-structure)
|
|
10
|
+
4. [Column Types and Features](#column-types-and-features)
|
|
11
|
+
5. [Advanced Table Features](#advanced-table-features)
|
|
12
|
+
6. [Dynamic Data Handling](#dynamic-data-handling)
|
|
13
|
+
7. [Events and Interactions](#events-and-interactions)
|
|
14
|
+
8. [Extending FlexiDataTable](#extending-flexidatatable)
|
|
15
|
+
9. [Integration Patterns](#integration-patterns)
|
|
16
|
+
10. [Best Practices](#best-practices)
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
FlexiDataTable is a powerful data table component that provides flexible column configuration, sorting, filtering, pagination, and extensive customization options. It's built on top of the uniface-element DataTable with enhanced schema-driven configuration.
|
|
21
|
+
|
|
22
|
+
### Key Features
|
|
23
|
+
|
|
24
|
+
- **Schema-Driven Configuration**: Define tables through JSON schemas
|
|
25
|
+
- **Flexible Column Types**: Data columns, indicator columns, and action columns
|
|
26
|
+
- **Custom Formatters**: Built-in and custom cell formatters
|
|
27
|
+
- **Sorting and Filtering**: Advanced data manipulation
|
|
28
|
+
- **Row Actions**: Configurable row-level actions
|
|
29
|
+
- **Responsive Design**: Adaptive layouts for different screen sizes
|
|
30
|
+
- **Extensible Architecture**: Custom renderers and formatters
|
|
31
|
+
|
|
32
|
+
## Basic Usage
|
|
33
|
+
|
|
34
|
+
### 1. Installation and Setup
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import FlexiDataTable from '@ticatec/uniface-flexi-form/flexi-datatable';
|
|
38
|
+
import DataTable from '@ticatec/uniface-element/DataTable';
|
|
39
|
+
import '@ticatec/uniface-flexi-form/uniface-flexi-form.css';
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Simple Data Table
|
|
43
|
+
|
|
44
|
+
```svelte
|
|
45
|
+
<!-- SimpleDataTable.svelte -->
|
|
46
|
+
<script lang="ts">
|
|
47
|
+
import DataTable from '@ticatec/uniface-element/DataTable';
|
|
48
|
+
import FlexiDataTable from '@ticatec/uniface-flexi-form/flexi-datatable';
|
|
49
|
+
|
|
50
|
+
// Sample data
|
|
51
|
+
let tableData = [
|
|
52
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com', age: 30, status: 'active' },
|
|
53
|
+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', age: 25, status: 'active' },
|
|
54
|
+
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', age: 35, status: 'inactive' }
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Table schema
|
|
58
|
+
const tableSchema = {
|
|
59
|
+
round: true,
|
|
60
|
+
indicatorColumn: {
|
|
61
|
+
width: 60,
|
|
62
|
+
displayNo: true,
|
|
63
|
+
selectable: true
|
|
64
|
+
},
|
|
65
|
+
actionsColumn: {
|
|
66
|
+
width: 120,
|
|
67
|
+
align: 'center',
|
|
68
|
+
getActions: 'getRowActions'
|
|
69
|
+
},
|
|
70
|
+
columns: [
|
|
71
|
+
{
|
|
72
|
+
text: 'Name',
|
|
73
|
+
field: 'name',
|
|
74
|
+
width: 150,
|
|
75
|
+
align: 'left',
|
|
76
|
+
resizable: true
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
text: 'Email',
|
|
80
|
+
field: 'email',
|
|
81
|
+
width: 200,
|
|
82
|
+
align: 'left',
|
|
83
|
+
resizable: true
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
text: 'Age',
|
|
87
|
+
field: 'age',
|
|
88
|
+
width: 80,
|
|
89
|
+
align: 'center',
|
|
90
|
+
formatter: 'formatAge'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
text: 'Status',
|
|
94
|
+
field: 'status',
|
|
95
|
+
width: 100,
|
|
96
|
+
align: 'center',
|
|
97
|
+
formatter: 'formatStatus'
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Create FlexiDataTable instance
|
|
103
|
+
class UserDataTable extends FlexiDataTable {
|
|
104
|
+
constructor(schema) {
|
|
105
|
+
super(schema);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
formatAge(value, row) {
|
|
109
|
+
return `${value} years`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
formatStatus(value, row) {
|
|
113
|
+
const statusClass = value === 'active' ? 'status-active' : 'status-inactive';
|
|
114
|
+
return `<span class="${statusClass}">${value.toUpperCase()}</span>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getRowActions(row) {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
text: 'Edit',
|
|
121
|
+
icon: 'edit',
|
|
122
|
+
handler: () => this.editUser(row)
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
text: 'Delete',
|
|
126
|
+
icon: 'delete',
|
|
127
|
+
handler: () => this.deleteUser(row),
|
|
128
|
+
confirm: 'Are you sure you want to delete this user?'
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
editUser(row) {
|
|
134
|
+
console.log('Editing user:', row);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
deleteUser(row) {
|
|
138
|
+
console.log('Deleting user:', row);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let dataTable = new UserDataTable(tableSchema);
|
|
143
|
+
|
|
144
|
+
onMount(async () => {
|
|
145
|
+
await dataTable.initialize();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
function handleRowSelect(event) {
|
|
149
|
+
console.log('Selected rows:', event.detail.selectedRows);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleSort(event) {
|
|
153
|
+
console.log('Sort changed:', event.detail);
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<div class="data-table-container">
|
|
158
|
+
<DataTable
|
|
159
|
+
columns={dataTable.columns}
|
|
160
|
+
data={tableData}
|
|
161
|
+
indicatorColumn={dataTable.indicatorColumn}
|
|
162
|
+
actionsColumn={dataTable.actionsColumn}
|
|
163
|
+
round={dataTable.round}
|
|
164
|
+
on:rowSelect={handleRowSelect}
|
|
165
|
+
on:sort={handleSort}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<style>
|
|
170
|
+
.data-table-container {
|
|
171
|
+
padding: 16px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
:global(.status-active) {
|
|
175
|
+
color: #28a745;
|
|
176
|
+
font-weight: bold;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
:global(.status-inactive) {
|
|
180
|
+
color: #dc3545;
|
|
181
|
+
font-weight: bold;
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Schema Structure
|
|
187
|
+
|
|
188
|
+
### FlexiDataTableSchema
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
interface FlexiDataTableSchema {
|
|
192
|
+
round?: boolean; // Rounded corners
|
|
193
|
+
indicatorColumn: IndicatorColumnSchema; // Row numbers/selection
|
|
194
|
+
actionsColumn?: ActionsColumnSchema; // Row actions
|
|
195
|
+
columns: Array<FlexiDataTableColumnSchema>; // Data columns
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### FlexiDataTableColumnSchema
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
interface FlexiDataTableColumnSchema {
|
|
203
|
+
text: string; // Column header text
|
|
204
|
+
field?: string; // Data field name
|
|
205
|
+
frozen?: boolean; // Freeze column
|
|
206
|
+
align?: 'left' | 'center' | 'right'; // Text alignment
|
|
207
|
+
width: number; // Column width
|
|
208
|
+
minWidth?: number; // Minimum width
|
|
209
|
+
warp?: boolean; // Text wrapping
|
|
210
|
+
formatter?: string; // Formatter function name
|
|
211
|
+
escapeHTML?: boolean; // HTML escaping
|
|
212
|
+
href?: string; // Link function name
|
|
213
|
+
hint?: string; // Tooltip function name
|
|
214
|
+
render?: string; // Custom renderer name
|
|
215
|
+
visible?: boolean; // Column visibility
|
|
216
|
+
resizable?: boolean; // Column resizing
|
|
217
|
+
compareFunction?: string; // Custom sort function name
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### IndicatorColumnSchema
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
interface IndicatorColumnSchema {
|
|
225
|
+
width: number; // Column width
|
|
226
|
+
displayNo?: boolean; // Show row numbers
|
|
227
|
+
selectable?: boolean; // Row selection
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### ActionsColumnSchema
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
interface ActionsColumnSchema {
|
|
235
|
+
width: number; // Column width
|
|
236
|
+
align?: 'left' | 'center'; // Alignment
|
|
237
|
+
getActions: string; // Action provider function name
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Column Types and Features
|
|
242
|
+
|
|
243
|
+
### Data Columns
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Basic data column
|
|
247
|
+
{
|
|
248
|
+
text: 'Product Name',
|
|
249
|
+
field: 'name',
|
|
250
|
+
width: 200,
|
|
251
|
+
align: 'left',
|
|
252
|
+
resizable: true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Formatted column
|
|
256
|
+
{
|
|
257
|
+
text: 'Price',
|
|
258
|
+
field: 'price',
|
|
259
|
+
width: 100,
|
|
260
|
+
align: 'right',
|
|
261
|
+
formatter: 'formatCurrency'
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Linked column
|
|
265
|
+
{
|
|
266
|
+
text: 'Website',
|
|
267
|
+
field: 'website',
|
|
268
|
+
width: 150,
|
|
269
|
+
href: 'buildWebsiteLink'
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Custom rendered column
|
|
273
|
+
{
|
|
274
|
+
text: 'Status',
|
|
275
|
+
field: 'status',
|
|
276
|
+
width: 120,
|
|
277
|
+
render: 'renderStatusBadge'
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Column Examples with Implementations
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
class ProductDataTable extends FlexiDataTable {
|
|
285
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
286
|
+
super(schema);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Currency formatter
|
|
290
|
+
formatCurrency(value: number, row: any): string {
|
|
291
|
+
return new Intl.NumberFormat('en-US', {
|
|
292
|
+
style: 'currency',
|
|
293
|
+
currency: 'USD'
|
|
294
|
+
}).format(value);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Date formatter
|
|
298
|
+
formatDate(value: string, row: any): string {
|
|
299
|
+
return new Date(value).toLocaleDateString();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Percentage formatter
|
|
303
|
+
formatPercentage(value: number, row: any): string {
|
|
304
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// File size formatter
|
|
308
|
+
formatFileSize(bytes: number, row: any): string {
|
|
309
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
310
|
+
if (bytes === 0) return '0 B';
|
|
311
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
312
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Link builder
|
|
316
|
+
buildWebsiteLink(value: string, row: any): string {
|
|
317
|
+
return value.startsWith('http') ? value : `https://${value}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Product link builder
|
|
321
|
+
buildProductLink(value: string, row: any): string {
|
|
322
|
+
return `/products/${row.id}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Tooltip provider
|
|
326
|
+
getProductHint(value: string, row: any): string {
|
|
327
|
+
return `Product ID: ${row.id}\\nCategory: ${row.category}\\nStock: ${row.stock}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Status badge renderer
|
|
331
|
+
renderStatusBadge(value: string, row: any): any {
|
|
332
|
+
// Return Svelte component configuration
|
|
333
|
+
return {
|
|
334
|
+
component: StatusBadge,
|
|
335
|
+
props: {
|
|
336
|
+
status: value,
|
|
337
|
+
size: 'small'
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Image renderer
|
|
343
|
+
renderProductImage(value: string, row: any): any {
|
|
344
|
+
return {
|
|
345
|
+
component: ProductImage,
|
|
346
|
+
props: {
|
|
347
|
+
src: value,
|
|
348
|
+
alt: row.name,
|
|
349
|
+
size: 'thumbnail'
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Custom sort function
|
|
355
|
+
compareProductNames(a: any, b: any): number {
|
|
356
|
+
// Custom sorting logic for product names
|
|
357
|
+
const nameA = a.name.toLowerCase();
|
|
358
|
+
const nameB = b.name.toLowerCase();
|
|
359
|
+
return this.compareStrings(nameA, nameB, { ignoreCase: true });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Numeric sort with null handling
|
|
363
|
+
compareStock(a: any, b: any): number {
|
|
364
|
+
const stockA = a.stock || 0;
|
|
365
|
+
const stockB = b.stock || 0;
|
|
366
|
+
return stockA - stockB;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Row actions
|
|
370
|
+
getRowActions(row: any): any[] {
|
|
371
|
+
const actions = [
|
|
372
|
+
{
|
|
373
|
+
text: 'View',
|
|
374
|
+
icon: 'eye',
|
|
375
|
+
handler: () => this.viewProduct(row)
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
if (row.status === 'active') {
|
|
380
|
+
actions.push({
|
|
381
|
+
text: 'Edit',
|
|
382
|
+
icon: 'edit',
|
|
383
|
+
handler: () => this.editProduct(row)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (row.stock === 0) {
|
|
388
|
+
actions.push({
|
|
389
|
+
text: 'Restock',
|
|
390
|
+
icon: 'plus',
|
|
391
|
+
handler: () => this.restockProduct(row)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
actions.push({
|
|
396
|
+
text: 'Delete',
|
|
397
|
+
icon: 'trash',
|
|
398
|
+
handler: () => this.deleteProduct(row),
|
|
399
|
+
confirm: 'Are you sure you want to delete this product?',
|
|
400
|
+
style: 'danger'
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return actions;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
viewProduct(row: any): void {
|
|
407
|
+
window.open(`/products/${row.id}`, '_blank');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
editProduct(row: any): void {
|
|
411
|
+
// Navigate to edit page or open modal
|
|
412
|
+
console.log('Editing product:', row);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
restockProduct(row: any): void {
|
|
416
|
+
// Open restock dialog
|
|
417
|
+
console.log('Restocking product:', row);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
deleteProduct(row: any): void {
|
|
421
|
+
// Perform delete operation
|
|
422
|
+
console.log('Deleting product:', row);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Advanced Table Features
|
|
428
|
+
|
|
429
|
+
### Frozen Columns
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
const tableSchemaWithFrozenColumns = {
|
|
433
|
+
round: true,
|
|
434
|
+
indicatorColumn: {
|
|
435
|
+
width: 60,
|
|
436
|
+
displayNo: true,
|
|
437
|
+
selectable: true
|
|
438
|
+
},
|
|
439
|
+
columns: [
|
|
440
|
+
{
|
|
441
|
+
text: 'ID',
|
|
442
|
+
field: 'id',
|
|
443
|
+
width: 80,
|
|
444
|
+
frozen: true, // This column stays fixed
|
|
445
|
+
align: 'center'
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
text: 'Name',
|
|
449
|
+
field: 'name',
|
|
450
|
+
width: 150,
|
|
451
|
+
frozen: true, // This column also stays fixed
|
|
452
|
+
align: 'left'
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
text: 'Description',
|
|
456
|
+
field: 'description',
|
|
457
|
+
width: 300,
|
|
458
|
+
align: 'left',
|
|
459
|
+
warp: true // Allow text wrapping
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
text: 'Price',
|
|
463
|
+
field: 'price',
|
|
464
|
+
width: 100,
|
|
465
|
+
align: 'right',
|
|
466
|
+
formatter: 'formatCurrency'
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
};
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Conditional Column Visibility
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
class ConditionalColumnsTable extends FlexiDataTable {
|
|
476
|
+
private userRole: string;
|
|
477
|
+
|
|
478
|
+
constructor(schema: FlexiDataTableSchema, userRole: string) {
|
|
479
|
+
super(schema);
|
|
480
|
+
this.userRole = userRole;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async initialize(): Promise<void> {
|
|
484
|
+
// Modify schema based on user role before initialization
|
|
485
|
+
this.applyRoleBasedVisibility();
|
|
486
|
+
await super.initialize();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private applyRoleBasedVisibility(): void {
|
|
490
|
+
this.schema.columns.forEach(column => {
|
|
491
|
+
switch (column.field) {
|
|
492
|
+
case 'salary':
|
|
493
|
+
column.visible = this.userRole === 'admin' || this.userRole === 'hr';
|
|
494
|
+
break;
|
|
495
|
+
case 'ssn':
|
|
496
|
+
column.visible = this.userRole === 'admin';
|
|
497
|
+
break;
|
|
498
|
+
case 'internalNotes':
|
|
499
|
+
column.visible = this.userRole !== 'guest';
|
|
500
|
+
break;
|
|
501
|
+
default:
|
|
502
|
+
column.visible = column.visible !== false;
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Dynamic Column Configuration
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
class DynamicColumnsTable extends FlexiDataTable {
|
|
513
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
514
|
+
super(schema);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
updateColumnConfiguration(newConfig: any): void {
|
|
518
|
+
// Update column widths
|
|
519
|
+
if (newConfig.columnWidths) {
|
|
520
|
+
this.columns.forEach((column, index) => {
|
|
521
|
+
if (newConfig.columnWidths[index]) {
|
|
522
|
+
column.width = newConfig.columnWidths[index];
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Update column visibility
|
|
528
|
+
if (newConfig.visibleColumns) {
|
|
529
|
+
this.columns.forEach((column, index) => {
|
|
530
|
+
column.visible = newConfig.visibleColumns.includes(index);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Update column order
|
|
535
|
+
if (newConfig.columnOrder) {
|
|
536
|
+
const reorderedColumns = newConfig.columnOrder.map(index => this.columns[index]);
|
|
537
|
+
this.#columns = reorderedColumns;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
getColumnConfiguration(): any {
|
|
542
|
+
return {
|
|
543
|
+
columnWidths: this.columns.map(col => col.width),
|
|
544
|
+
visibleColumns: this.columns
|
|
545
|
+
.map((col, index) => col.visible !== false ? index : null)
|
|
546
|
+
.filter(index => index !== null),
|
|
547
|
+
columnOrder: this.columns.map((_, index) => index)
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
saveColumnConfiguration(): void {
|
|
552
|
+
const config = this.getColumnConfiguration();
|
|
553
|
+
localStorage.setItem('table-config', JSON.stringify(config));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
loadColumnConfiguration(): void {
|
|
557
|
+
try {
|
|
558
|
+
const config = JSON.parse(localStorage.getItem('table-config') || '{}');
|
|
559
|
+
this.updateColumnConfiguration(config);
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error('Failed to load column configuration:', error);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Dynamic Data Handling
|
|
568
|
+
|
|
569
|
+
### Paginated Data Loading
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
class PaginatedDataTable extends FlexiDataTable {
|
|
573
|
+
private currentPage = 1;
|
|
574
|
+
private pageSize = 20;
|
|
575
|
+
private totalRecords = 0;
|
|
576
|
+
private loading = false;
|
|
577
|
+
private searchCriteria: any = {};
|
|
578
|
+
|
|
579
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
580
|
+
super(schema);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async loadData(page: number = 1, criteria: any = {}): Promise<any[]> {
|
|
584
|
+
this.loading = true;
|
|
585
|
+
this.currentPage = page;
|
|
586
|
+
this.searchCriteria = criteria;
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const queryParams = new URLSearchParams({
|
|
590
|
+
page: page.toString(),
|
|
591
|
+
limit: this.pageSize.toString(),
|
|
592
|
+
...this.flattenCriteria(criteria)
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const response = await fetch(`/api/data?${queryParams}`);
|
|
596
|
+
const result = await response.json();
|
|
597
|
+
|
|
598
|
+
this.totalRecords = result.total;
|
|
599
|
+
return result.data;
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.error('Failed to load data:', error);
|
|
602
|
+
return [];
|
|
603
|
+
} finally {
|
|
604
|
+
this.loading = false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async refreshData(): Promise<any[]> {
|
|
609
|
+
return this.loadData(this.currentPage, this.searchCriteria);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async nextPage(): Promise<any[]> {
|
|
613
|
+
if (this.hasNextPage()) {
|
|
614
|
+
return this.loadData(this.currentPage + 1, this.searchCriteria);
|
|
615
|
+
}
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async previousPage(): Promise<any[]> {
|
|
620
|
+
if (this.hasPreviousPage()) {
|
|
621
|
+
return this.loadData(this.currentPage - 1, this.searchCriteria);
|
|
622
|
+
}
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
hasNextPage(): boolean {
|
|
627
|
+
return this.currentPage * this.pageSize < this.totalRecords;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
hasPreviousPage(): boolean {
|
|
631
|
+
return this.currentPage > 1;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
getCurrentPageInfo(): any {
|
|
635
|
+
const startRecord = (this.currentPage - 1) * this.pageSize + 1;
|
|
636
|
+
const endRecord = Math.min(this.currentPage * this.pageSize, this.totalRecords);
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
currentPage: this.currentPage,
|
|
640
|
+
pageSize: this.pageSize,
|
|
641
|
+
totalRecords: this.totalRecords,
|
|
642
|
+
totalPages: Math.ceil(this.totalRecords / this.pageSize),
|
|
643
|
+
startRecord,
|
|
644
|
+
endRecord,
|
|
645
|
+
loading: this.loading
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private flattenCriteria(criteria: any): Record<string, string> {
|
|
650
|
+
const flattened: Record<string, string> = {};
|
|
651
|
+
for (const [key, value] of Object.entries(criteria)) {
|
|
652
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
653
|
+
flattened[key] = String(value);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return flattened;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Real-time Data Updates
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
class RealtimeDataTable extends FlexiDataTable {
|
|
665
|
+
private websocket: WebSocket | null = null;
|
|
666
|
+
private updateCallback: ((data: any[]) => void) | null = null;
|
|
667
|
+
|
|
668
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
669
|
+
super(schema);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
connectRealtime(wsUrl: string, onUpdate: (data: any[]) => void): void {
|
|
673
|
+
this.updateCallback = onUpdate;
|
|
674
|
+
this.websocket = new WebSocket(wsUrl);
|
|
675
|
+
|
|
676
|
+
this.websocket.onopen = () => {
|
|
677
|
+
console.log('WebSocket connected');
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
this.websocket.onmessage = (event) => {
|
|
681
|
+
try {
|
|
682
|
+
const message = JSON.parse(event.data);
|
|
683
|
+
this.handleRealtimeUpdate(message);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
console.error('Failed to parse WebSocket message:', error);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
this.websocket.onclose = () => {
|
|
690
|
+
console.log('WebSocket disconnected');
|
|
691
|
+
// Attempt to reconnect
|
|
692
|
+
setTimeout(() => this.connectRealtime(wsUrl, onUpdate), 5000);
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
this.websocket.onerror = (error) => {
|
|
696
|
+
console.error('WebSocket error:', error);
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private handleRealtimeUpdate(message: any): void {
|
|
701
|
+
switch (message.type) {
|
|
702
|
+
case 'data_updated':
|
|
703
|
+
this.updateCallback?.(message.data);
|
|
704
|
+
break;
|
|
705
|
+
case 'record_added':
|
|
706
|
+
this.handleRecordAdded(message.record);
|
|
707
|
+
break;
|
|
708
|
+
case 'record_updated':
|
|
709
|
+
this.handleRecordUpdated(message.record);
|
|
710
|
+
break;
|
|
711
|
+
case 'record_deleted':
|
|
712
|
+
this.handleRecordDeleted(message.recordId);
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private handleRecordAdded(record: any): void {
|
|
718
|
+
// Add record to current data and notify
|
|
719
|
+
console.log('New record added:', record);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private handleRecordUpdated(record: any): void {
|
|
723
|
+
// Update existing record and notify
|
|
724
|
+
console.log('Record updated:', record);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private handleRecordDeleted(recordId: string): void {
|
|
728
|
+
// Remove record from current data and notify
|
|
729
|
+
console.log('Record deleted:', recordId);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
disconnect(): void {
|
|
733
|
+
if (this.websocket) {
|
|
734
|
+
this.websocket.close();
|
|
735
|
+
this.websocket = null;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
## Events and Interactions
|
|
742
|
+
|
|
743
|
+
### Table Events
|
|
744
|
+
|
|
745
|
+
```svelte
|
|
746
|
+
<script>
|
|
747
|
+
function handleRowSelect(event) {
|
|
748
|
+
const { selectedRows, isAllSelected } = event.detail;
|
|
749
|
+
console.log('Selected rows:', selectedRows);
|
|
750
|
+
console.log('All selected:', isAllSelected);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function handleRowClick(event) {
|
|
754
|
+
const { row, rowIndex } = event.detail;
|
|
755
|
+
console.log('Row clicked:', row, 'at index:', rowIndex);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function handleRowDoubleClick(event) {
|
|
759
|
+
const { row, rowIndex } = event.detail;
|
|
760
|
+
console.log('Row double-clicked:', row);
|
|
761
|
+
// Open edit dialog or navigate to detail view
|
|
762
|
+
openEditDialog(row);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function handleSort(event) {
|
|
766
|
+
const { column, direction } = event.detail;
|
|
767
|
+
console.log('Sort changed:', column.field, direction);
|
|
768
|
+
|
|
769
|
+
// Perform sorting
|
|
770
|
+
if (direction === 'asc') {
|
|
771
|
+
tableData.sort((a, b) => a[column.field] > b[column.field] ? 1 : -1);
|
|
772
|
+
} else if (direction === 'desc') {
|
|
773
|
+
tableData.sort((a, b) => a[column.field] < b[column.field] ? 1 : -1);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Trigger reactivity
|
|
777
|
+
tableData = [...tableData];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function handleColumnResize(event) {
|
|
781
|
+
const { column, newWidth } = event.detail;
|
|
782
|
+
console.log('Column resized:', column.field, 'new width:', newWidth);
|
|
783
|
+
|
|
784
|
+
// Save column configuration
|
|
785
|
+
dataTable.saveColumnConfiguration();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function handlePageChange(event) {
|
|
789
|
+
const { page, pageSize } = event.detail;
|
|
790
|
+
console.log('Page changed:', page, 'size:', pageSize);
|
|
791
|
+
|
|
792
|
+
// Load new page data
|
|
793
|
+
loadPageData(page, pageSize);
|
|
794
|
+
}
|
|
795
|
+
</script>
|
|
796
|
+
|
|
797
|
+
<DataTable
|
|
798
|
+
columns={dataTable.columns}
|
|
799
|
+
data={tableData}
|
|
800
|
+
indicatorColumn={dataTable.indicatorColumn}
|
|
801
|
+
actionsColumn={dataTable.actionsColumn}
|
|
802
|
+
on:rowSelect={handleRowSelect}
|
|
803
|
+
on:rowClick={handleRowClick}
|
|
804
|
+
on:rowDoubleClick={handleRowDoubleClick}
|
|
805
|
+
on:sort={handleSort}
|
|
806
|
+
on:columnResize={handleColumnResize}
|
|
807
|
+
on:pageChange={handlePageChange}
|
|
808
|
+
/>
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Bulk Operations
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
class BulkOperationsTable extends FlexiDataTable {
|
|
815
|
+
private selectedRows: Set<any> = new Set();
|
|
816
|
+
|
|
817
|
+
constructor(schema: FlexiDataTableSchema) {
|
|
818
|
+
super(schema);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
handleRowSelection(selectedRows: any[]): void {
|
|
822
|
+
this.selectedRows = new Set(selectedRows);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
getBulkActions(): any[] {
|
|
826
|
+
if (this.selectedRows.size === 0) {
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return [
|
|
831
|
+
{
|
|
832
|
+
text: `Delete ${this.selectedRows.size} items`,
|
|
833
|
+
icon: 'trash',
|
|
834
|
+
handler: () => this.bulkDelete(),
|
|
835
|
+
confirm: `Are you sure you want to delete ${this.selectedRows.size} items?`,
|
|
836
|
+
style: 'danger'
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
text: `Export ${this.selectedRows.size} items`,
|
|
840
|
+
icon: 'download',
|
|
841
|
+
handler: () => this.bulkExport()
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
text: 'Update Status',
|
|
845
|
+
icon: 'edit',
|
|
846
|
+
handler: () => this.bulkUpdateStatus()
|
|
847
|
+
}
|
|
848
|
+
];
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async bulkDelete(): Promise<void> {
|
|
852
|
+
try {
|
|
853
|
+
const ids = Array.from(this.selectedRows).map(row => row.id);
|
|
854
|
+
await fetch('/api/bulk/delete', {
|
|
855
|
+
method: 'POST',
|
|
856
|
+
headers: { 'Content-Type': 'application/json' },
|
|
857
|
+
body: JSON.stringify({ ids })
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
console.log('Bulk delete successful');
|
|
861
|
+
this.selectedRows.clear();
|
|
862
|
+
} catch (error) {
|
|
863
|
+
console.error('Bulk delete failed:', error);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async bulkExport(): Promise<void> {
|
|
868
|
+
try {
|
|
869
|
+
const data = Array.from(this.selectedRows);
|
|
870
|
+
const csv = this.convertToCSV(data);
|
|
871
|
+
this.downloadCSV(csv, 'export.csv');
|
|
872
|
+
} catch (error) {
|
|
873
|
+
console.error('Bulk export failed:', error);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async bulkUpdateStatus(): Promise<void> {
|
|
878
|
+
const newStatus = prompt('Enter new status:');
|
|
879
|
+
if (!newStatus) return;
|
|
880
|
+
|
|
881
|
+
try {
|
|
882
|
+
const ids = Array.from(this.selectedRows).map(row => row.id);
|
|
883
|
+
await fetch('/api/bulk/update-status', {
|
|
884
|
+
method: 'POST',
|
|
885
|
+
headers: { 'Content-Type': 'application/json' },
|
|
886
|
+
body: JSON.stringify({ ids, status: newStatus })
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
console.log('Bulk status update successful');
|
|
890
|
+
} catch (error) {
|
|
891
|
+
console.error('Bulk status update failed:', error);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private convertToCSV(data: any[]): string {
|
|
896
|
+
if (data.length === 0) return '';
|
|
897
|
+
|
|
898
|
+
const headers = Object.keys(data[0]);
|
|
899
|
+
const csvContent = [
|
|
900
|
+
headers.join(','),
|
|
901
|
+
...data.map(row => headers.map(header => `"${row[header]}"`).join(','))
|
|
902
|
+
].join('\\n');
|
|
903
|
+
|
|
904
|
+
return csvContent;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private downloadCSV(csv: string, filename: string): void {
|
|
908
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
|
909
|
+
const url = window.URL.createObjectURL(blob);
|
|
910
|
+
const link = document.createElement('a');
|
|
911
|
+
link.href = url;
|
|
912
|
+
link.download = filename;
|
|
913
|
+
link.click();
|
|
914
|
+
window.URL.revokeObjectURL(url);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
## Extending FlexiDataTable
|
|
920
|
+
|
|
921
|
+
### Custom Cell Renderers
|
|
922
|
+
|
|
923
|
+
```svelte
|
|
924
|
+
<!-- StatusBadge.svelte -->
|
|
925
|
+
<script lang="ts">
|
|
926
|
+
export let status: string;
|
|
927
|
+
export let size: 'small' | 'medium' | 'large' = 'medium';
|
|
928
|
+
|
|
929
|
+
$: statusConfig = getStatusConfig(status);
|
|
930
|
+
|
|
931
|
+
function getStatusConfig(status: string) {
|
|
932
|
+
const configs = {
|
|
933
|
+
active: { color: '#28a745', background: '#d4edda', text: 'Active' },
|
|
934
|
+
inactive: { color: '#6c757d', background: '#e2e3e5', text: 'Inactive' },
|
|
935
|
+
pending: { color: '#ffc107', background: '#fff3cd', text: 'Pending' },
|
|
936
|
+
error: { color: '#dc3545', background: '#f8d7da', text: 'Error' }
|
|
937
|
+
};
|
|
938
|
+
return configs[status] || configs.inactive;
|
|
939
|
+
}
|
|
940
|
+
</script>
|
|
941
|
+
|
|
942
|
+
<span
|
|
943
|
+
class="status-badge {size}"
|
|
944
|
+
style="color: {statusConfig.color}; background-color: {statusConfig.background};"
|
|
945
|
+
>
|
|
946
|
+
{statusConfig.text}
|
|
947
|
+
</span>
|
|
948
|
+
|
|
949
|
+
<style>
|
|
950
|
+
.status-badge {
|
|
951
|
+
display: inline-block;
|
|
952
|
+
padding: 4px 8px;
|
|
953
|
+
border-radius: 12px;
|
|
954
|
+
font-weight: 600;
|
|
955
|
+
text-align: center;
|
|
956
|
+
white-space: nowrap;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.status-badge.small {
|
|
960
|
+
font-size: 0.75rem;
|
|
961
|
+
padding: 2px 6px;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
.status-badge.medium {
|
|
965
|
+
font-size: 0.875rem;
|
|
966
|
+
padding: 4px 8px;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
.status-badge.large {
|
|
970
|
+
font-size: 1rem;
|
|
971
|
+
padding: 6px 12px;
|
|
972
|
+
}
|
|
973
|
+
</style>
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
```svelte
|
|
977
|
+
<!-- ProgressBar.svelte -->
|
|
978
|
+
<script lang="ts">
|
|
979
|
+
export let value: number;
|
|
980
|
+
export let max: number = 100;
|
|
981
|
+
export let color: string = '#007bff';
|
|
982
|
+
export let showText: boolean = true;
|
|
983
|
+
|
|
984
|
+
$: percentage = Math.round((value / max) * 100);
|
|
985
|
+
</script>
|
|
986
|
+
|
|
987
|
+
<div class="progress-container">
|
|
988
|
+
<div class="progress-bar">
|
|
989
|
+
<div
|
|
990
|
+
class="progress-fill"
|
|
991
|
+
style="width: {percentage}%; background-color: {color};"
|
|
992
|
+
></div>
|
|
993
|
+
</div>
|
|
994
|
+
{#if showText}
|
|
995
|
+
<span class="progress-text">{percentage}%</span>
|
|
996
|
+
{/if}
|
|
997
|
+
</div>
|
|
998
|
+
|
|
999
|
+
<style>
|
|
1000
|
+
.progress-container {
|
|
1001
|
+
display: flex;
|
|
1002
|
+
align-items: center;
|
|
1003
|
+
gap: 8px;
|
|
1004
|
+
width: 100%;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.progress-bar {
|
|
1008
|
+
flex: 1;
|
|
1009
|
+
height: 8px;
|
|
1010
|
+
background-color: #e9ecef;
|
|
1011
|
+
border-radius: 4px;
|
|
1012
|
+
overflow: hidden;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.progress-fill {
|
|
1016
|
+
height: 100%;
|
|
1017
|
+
transition: width 0.3s ease;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.progress-text {
|
|
1021
|
+
font-size: 0.75rem;
|
|
1022
|
+
color: #6c757d;
|
|
1023
|
+
min-width: 35px;
|
|
1024
|
+
}
|
|
1025
|
+
</style>
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### Custom Table Classes
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
class AdvancedProductTable extends FlexiDataTable {
|
|
1032
|
+
private productCategories: any[] = [];
|
|
1033
|
+
private userPermissions: string[] = [];
|
|
1034
|
+
|
|
1035
|
+
constructor(schema: FlexiDataTableSchema, userPermissions: string[]) {
|
|
1036
|
+
super(schema);
|
|
1037
|
+
this.userPermissions = userPermissions;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async initialize(): Promise<void> {
|
|
1041
|
+
await this.loadProductCategories();
|
|
1042
|
+
this.setupPermissionBasedActions();
|
|
1043
|
+
await super.initialize();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private async loadProductCategories(): Promise<void> {
|
|
1047
|
+
try {
|
|
1048
|
+
const response = await fetch('/api/product-categories');
|
|
1049
|
+
this.productCategories = await response.json();
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
console.error('Failed to load product categories:', error);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
private setupPermissionBasedActions(): void {
|
|
1056
|
+
// Modify schema based on user permissions
|
|
1057
|
+
if (!this.userPermissions.includes('edit_products')) {
|
|
1058
|
+
// Remove edit-related columns or actions
|
|
1059
|
+
this.schema.columns = this.schema.columns.filter(col =>
|
|
1060
|
+
col.field !== 'editActions'
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Advanced formatters
|
|
1066
|
+
formatProductCategory(categoryId: string, row: any): string {
|
|
1067
|
+
const category = this.productCategories.find(cat => cat.id === categoryId);
|
|
1068
|
+
return category ? category.name : 'Unknown Category';
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
formatInventoryStatus(stock: number, row: any): string {
|
|
1072
|
+
if (stock === 0) {
|
|
1073
|
+
return '<span class="text-danger">Out of Stock</span>';
|
|
1074
|
+
} else if (stock < row.minimumStock) {
|
|
1075
|
+
return '<span class="text-warning">Low Stock</span>';
|
|
1076
|
+
} else {
|
|
1077
|
+
return '<span class="text-success">In Stock</span>';
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
formatPriceWithDiscount(price: number, row: any): string {
|
|
1082
|
+
if (row.discount > 0) {
|
|
1083
|
+
const discountedPrice = price * (1 - row.discount);
|
|
1084
|
+
return `
|
|
1085
|
+
<span class="original-price">$${price.toFixed(2)}</span>
|
|
1086
|
+
<span class="discounted-price">$${discountedPrice.toFixed(2)}</span>
|
|
1087
|
+
`;
|
|
1088
|
+
}
|
|
1089
|
+
return `$${price.toFixed(2)}`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Advanced renderers
|
|
1093
|
+
renderProductImage(imageUrl: string, row: any): any {
|
|
1094
|
+
return {
|
|
1095
|
+
component: ProductImage,
|
|
1096
|
+
props: {
|
|
1097
|
+
src: imageUrl,
|
|
1098
|
+
alt: row.name,
|
|
1099
|
+
size: 'thumbnail',
|
|
1100
|
+
lazy: true,
|
|
1101
|
+
fallback: '/images/product-placeholder.png'
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
renderStockLevel(stock: number, row: any): any {
|
|
1107
|
+
const percentage = Math.min((stock / row.maximumStock) * 100, 100);
|
|
1108
|
+
let color = '#28a745'; // Green
|
|
1109
|
+
|
|
1110
|
+
if (percentage < 20) {
|
|
1111
|
+
color = '#dc3545'; // Red
|
|
1112
|
+
} else if (percentage < 50) {
|
|
1113
|
+
color = '#ffc107'; // Yellow
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return {
|
|
1117
|
+
component: ProgressBar,
|
|
1118
|
+
props: {
|
|
1119
|
+
value: stock,
|
|
1120
|
+
max: row.maximumStock,
|
|
1121
|
+
color,
|
|
1122
|
+
showText: true
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
renderRating(rating: number, row: any): any {
|
|
1128
|
+
return {
|
|
1129
|
+
component: StarRating,
|
|
1130
|
+
props: {
|
|
1131
|
+
rating,
|
|
1132
|
+
maxRating: 5,
|
|
1133
|
+
readonly: true,
|
|
1134
|
+
size: 'small'
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Advanced actions
|
|
1140
|
+
getRowActions(row: any): any[] {
|
|
1141
|
+
const actions = [];
|
|
1142
|
+
|
|
1143
|
+
// Always available actions
|
|
1144
|
+
actions.push({
|
|
1145
|
+
text: 'View Details',
|
|
1146
|
+
icon: 'eye',
|
|
1147
|
+
handler: () => this.viewProductDetails(row)
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// Permission-based actions
|
|
1151
|
+
if (this.userPermissions.includes('edit_products')) {
|
|
1152
|
+
actions.push({
|
|
1153
|
+
text: 'Edit',
|
|
1154
|
+
icon: 'edit',
|
|
1155
|
+
handler: () => this.editProduct(row)
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (this.userPermissions.includes('manage_inventory')) {
|
|
1160
|
+
if (row.stock === 0) {
|
|
1161
|
+
actions.push({
|
|
1162
|
+
text: 'Restock',
|
|
1163
|
+
icon: 'plus-circle',
|
|
1164
|
+
handler: () => this.restockProduct(row)
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
actions.push({
|
|
1169
|
+
text: 'Adjust Stock',
|
|
1170
|
+
icon: 'warehouse',
|
|
1171
|
+
handler: () => this.adjustStock(row)
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (this.userPermissions.includes('delete_products')) {
|
|
1176
|
+
actions.push({
|
|
1177
|
+
text: 'Delete',
|
|
1178
|
+
icon: 'trash',
|
|
1179
|
+
handler: () => this.deleteProduct(row),
|
|
1180
|
+
confirm: 'Are you sure you want to delete this product?',
|
|
1181
|
+
style: 'danger'
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return actions;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Action implementations
|
|
1189
|
+
viewProductDetails(row: any): void {
|
|
1190
|
+
window.open(`/products/${row.id}`, '_blank');
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
editProduct(row: any): void {
|
|
1194
|
+
// Open edit modal or navigate to edit page
|
|
1195
|
+
const editEvent = new CustomEvent('edit-product', { detail: row });
|
|
1196
|
+
document.dispatchEvent(editEvent);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
restockProduct(row: any): void {
|
|
1200
|
+
// Open restock dialog
|
|
1201
|
+
const restockEvent = new CustomEvent('restock-product', { detail: row });
|
|
1202
|
+
document.dispatchEvent(restockEvent);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
adjustStock(row: any): void {
|
|
1206
|
+
// Open stock adjustment dialog
|
|
1207
|
+
const adjustEvent = new CustomEvent('adjust-stock', { detail: row });
|
|
1208
|
+
document.dispatchEvent(adjustEvent);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
deleteProduct(row: any): void {
|
|
1212
|
+
// Perform delete operation
|
|
1213
|
+
fetch(`/api/products/${row.id}`, { method: 'DELETE' })
|
|
1214
|
+
.then(() => {
|
|
1215
|
+
const deleteEvent = new CustomEvent('product-deleted', { detail: row });
|
|
1216
|
+
document.dispatchEvent(deleteEvent);
|
|
1217
|
+
})
|
|
1218
|
+
.catch(error => {
|
|
1219
|
+
console.error('Delete failed:', error);
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Advanced sorting
|
|
1224
|
+
compareProductNames(a: any, b: any): number {
|
|
1225
|
+
return this.compareStrings(a.name, b.name, { ignoreCase: true });
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
compareByCategory(a: any, b: any): number {
|
|
1229
|
+
const catA = this.formatProductCategory(a.categoryId, a);
|
|
1230
|
+
const catB = this.formatProductCategory(b.categoryId, b);
|
|
1231
|
+
return this.compareStrings(catA, catB);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
compareByStockLevel(a: any, b: any): number {
|
|
1235
|
+
const levelA = a.stock / a.maximumStock;
|
|
1236
|
+
const levelB = b.stock / b.maximumStock;
|
|
1237
|
+
return levelA - levelB;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
## Integration Patterns
|
|
1243
|
+
|
|
1244
|
+
### Table with Search Integration
|
|
1245
|
+
|
|
1246
|
+
```svelte
|
|
1247
|
+
<!-- ProductManagement.svelte -->
|
|
1248
|
+
<script lang="ts">
|
|
1249
|
+
import CriteriaPanel from '@ticatec/uniface-flexi-form/criteria-panel';
|
|
1250
|
+
import DataTable from '@ticatec/uniface-element/DataTable';
|
|
1251
|
+
import { AdvancedProductTable } from './AdvancedProductTable';
|
|
1252
|
+
|
|
1253
|
+
let searchCriteria = {};
|
|
1254
|
+
let tableData = [];
|
|
1255
|
+
let totalRecords = 0;
|
|
1256
|
+
let currentPage = 1;
|
|
1257
|
+
let pageSize = 20;
|
|
1258
|
+
let loading = false;
|
|
1259
|
+
|
|
1260
|
+
const searchSchema = {
|
|
1261
|
+
arrangement: 'horizontal',
|
|
1262
|
+
fields: [
|
|
1263
|
+
{
|
|
1264
|
+
type: 'text-search',
|
|
1265
|
+
name: 'name',
|
|
1266
|
+
label: 'Product Name',
|
|
1267
|
+
keys: { field: 'name' },
|
|
1268
|
+
size: 'x25'
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
type: 'option-select-search',
|
|
1272
|
+
name: 'category',
|
|
1273
|
+
label: 'Category',
|
|
1274
|
+
keys: { field: 'categoryId' },
|
|
1275
|
+
dictName: 'categories',
|
|
1276
|
+
size: 'x20'
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
type: 'number-range',
|
|
1280
|
+
name: 'price',
|
|
1281
|
+
label: 'Price Range',
|
|
1282
|
+
keys: { minField: 'priceMin', maxField: 'priceMax' },
|
|
1283
|
+
size: 'x25'
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
type: 'option-select-search',
|
|
1287
|
+
name: 'status',
|
|
1288
|
+
label: 'Status',
|
|
1289
|
+
keys: { field: 'status' },
|
|
1290
|
+
dictName: 'product-status',
|
|
1291
|
+
size: 'x15'
|
|
1292
|
+
}
|
|
1293
|
+
]
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
const tableSchema = {
|
|
1297
|
+
round: true,
|
|
1298
|
+
indicatorColumn: {
|
|
1299
|
+
width: 60,
|
|
1300
|
+
displayNo: true,
|
|
1301
|
+
selectable: true
|
|
1302
|
+
},
|
|
1303
|
+
actionsColumn: {
|
|
1304
|
+
width: 150,
|
|
1305
|
+
align: 'center',
|
|
1306
|
+
getActions: 'getRowActions'
|
|
1307
|
+
},
|
|
1308
|
+
columns: [
|
|
1309
|
+
{
|
|
1310
|
+
text: 'Image',
|
|
1311
|
+
field: 'imageUrl',
|
|
1312
|
+
width: 80,
|
|
1313
|
+
align: 'center',
|
|
1314
|
+
render: 'renderProductImage'
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
text: 'Name',
|
|
1318
|
+
field: 'name',
|
|
1319
|
+
width: 200,
|
|
1320
|
+
align: 'left',
|
|
1321
|
+
resizable: true,
|
|
1322
|
+
compareFunction: 'compareProductNames'
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
text: 'Category',
|
|
1326
|
+
field: 'categoryId',
|
|
1327
|
+
width: 150,
|
|
1328
|
+
formatter: 'formatProductCategory',
|
|
1329
|
+
compareFunction: 'compareByCategory'
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
text: 'Price',
|
|
1333
|
+
field: 'price',
|
|
1334
|
+
width: 120,
|
|
1335
|
+
align: 'right',
|
|
1336
|
+
formatter: 'formatPriceWithDiscount'
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
text: 'Stock',
|
|
1340
|
+
field: 'stock',
|
|
1341
|
+
width: 100,
|
|
1342
|
+
align: 'center',
|
|
1343
|
+
render: 'renderStockLevel',
|
|
1344
|
+
compareFunction: 'compareByStockLevel'
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
text: 'Rating',
|
|
1348
|
+
field: 'rating',
|
|
1349
|
+
width: 120,
|
|
1350
|
+
align: 'center',
|
|
1351
|
+
render: 'renderRating'
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
text: 'Status',
|
|
1355
|
+
field: 'status',
|
|
1356
|
+
width: 100,
|
|
1357
|
+
align: 'center',
|
|
1358
|
+
formatter: 'formatInventoryStatus'
|
|
1359
|
+
}
|
|
1360
|
+
]
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
let productTable = new AdvancedProductTable(tableSchema, ['edit_products', 'manage_inventory']);
|
|
1364
|
+
|
|
1365
|
+
onMount(async () => {
|
|
1366
|
+
await productTable.initialize();
|
|
1367
|
+
await loadData();
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
async function loadData() {
|
|
1371
|
+
loading = true;
|
|
1372
|
+
try {
|
|
1373
|
+
const queryParams = new URLSearchParams({
|
|
1374
|
+
page: currentPage.toString(),
|
|
1375
|
+
limit: pageSize.toString(),
|
|
1376
|
+
...flattenCriteria(searchCriteria)
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
const response = await fetch(`/api/products?${queryParams}`);
|
|
1380
|
+
const result = await response.json();
|
|
1381
|
+
|
|
1382
|
+
tableData = result.data;
|
|
1383
|
+
totalRecords = result.total;
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
console.error('Failed to load products:', error);
|
|
1386
|
+
} finally {
|
|
1387
|
+
loading = false;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function flattenCriteria(criteria) {
|
|
1392
|
+
const flattened = {};
|
|
1393
|
+
for (const [key, value] of Object.entries(criteria)) {
|
|
1394
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
1395
|
+
flattened[key] = value;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return flattened;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function handleCriteriaChange(event) {
|
|
1402
|
+
searchCriteria = event.detail.criteria;
|
|
1403
|
+
currentPage = 1;
|
|
1404
|
+
loadData();
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function handlePageChange(event) {
|
|
1408
|
+
currentPage = event.detail.page;
|
|
1409
|
+
pageSize = event.detail.pageSize;
|
|
1410
|
+
loadData();
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function handleSort(event) {
|
|
1414
|
+
console.log('Sort:', event.detail);
|
|
1415
|
+
loadData();
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Listen for custom events from table actions
|
|
1419
|
+
onMount(() => {
|
|
1420
|
+
document.addEventListener('edit-product', handleEditProduct);
|
|
1421
|
+
document.addEventListener('restock-product', handleRestockProduct);
|
|
1422
|
+
document.addEventListener('product-deleted', handleProductDeleted);
|
|
1423
|
+
|
|
1424
|
+
return () => {
|
|
1425
|
+
document.removeEventListener('edit-product', handleEditProduct);
|
|
1426
|
+
document.removeEventListener('restock-product', handleRestockProduct);
|
|
1427
|
+
document.removeEventListener('product-deleted', handleProductDeleted);
|
|
1428
|
+
};
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
function handleEditProduct(event) {
|
|
1432
|
+
console.log('Edit product:', event.detail);
|
|
1433
|
+
// Open edit modal or navigate to edit page
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function handleRestockProduct(event) {
|
|
1437
|
+
console.log('Restock product:', event.detail);
|
|
1438
|
+
// Open restock modal
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function handleProductDeleted(event) {
|
|
1442
|
+
console.log('Product deleted:', event.detail);
|
|
1443
|
+
// Refresh table data
|
|
1444
|
+
loadData();
|
|
1445
|
+
}
|
|
1446
|
+
</script>
|
|
1447
|
+
|
|
1448
|
+
<div class="product-management">
|
|
1449
|
+
<div class="search-section">
|
|
1450
|
+
<h3>Product Search</h3>
|
|
1451
|
+
<CriteriaPanel
|
|
1452
|
+
schema={searchSchema}
|
|
1453
|
+
bind:criteria={searchCriteria}
|
|
1454
|
+
on:change={handleCriteriaChange}
|
|
1455
|
+
/>
|
|
1456
|
+
</div>
|
|
1457
|
+
|
|
1458
|
+
<div class="table-section">
|
|
1459
|
+
<div class="table-header">
|
|
1460
|
+
<h3>Products ({totalRecords} items)</h3>
|
|
1461
|
+
{#if loading}
|
|
1462
|
+
<div class="loading">Loading...</div>
|
|
1463
|
+
{/if}
|
|
1464
|
+
</div>
|
|
1465
|
+
|
|
1466
|
+
<DataTable
|
|
1467
|
+
columns={productTable.columns}
|
|
1468
|
+
data={tableData}
|
|
1469
|
+
indicatorColumn={productTable.indicatorColumn}
|
|
1470
|
+
actionsColumn={productTable.actionsColumn}
|
|
1471
|
+
round={productTable.round}
|
|
1472
|
+
{totalRecords}
|
|
1473
|
+
{currentPage}
|
|
1474
|
+
{pageSize}
|
|
1475
|
+
on:pageChange={handlePageChange}
|
|
1476
|
+
on:sort={handleSort}
|
|
1477
|
+
/>
|
|
1478
|
+
</div>
|
|
1479
|
+
</div>
|
|
1480
|
+
|
|
1481
|
+
<style>
|
|
1482
|
+
.product-management {
|
|
1483
|
+
display: flex;
|
|
1484
|
+
flex-direction: column;
|
|
1485
|
+
gap: 24px;
|
|
1486
|
+
padding: 16px;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
.search-section {
|
|
1490
|
+
background: #f8f9fa;
|
|
1491
|
+
padding: 16px;
|
|
1492
|
+
border-radius: 8px;
|
|
1493
|
+
border: 1px solid #dee2e6;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
.table-section {
|
|
1497
|
+
flex: 1;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.table-header {
|
|
1501
|
+
display: flex;
|
|
1502
|
+
justify-content: space-between;
|
|
1503
|
+
align-items: center;
|
|
1504
|
+
margin-bottom: 16px;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.loading {
|
|
1508
|
+
color: #6c757d;
|
|
1509
|
+
font-style: italic;
|
|
1510
|
+
}
|
|
1511
|
+
</style>
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
## Best Practices
|
|
1515
|
+
|
|
1516
|
+
### 1. Performance Optimization
|
|
1517
|
+
|
|
1518
|
+
```typescript
|
|
1519
|
+
// Optimize large datasets with virtual scrolling
|
|
1520
|
+
class VirtualizedTable extends FlexiDataTable {
|
|
1521
|
+
private virtualItemHeight = 40;
|
|
1522
|
+
private visibleItemCount = 20;
|
|
1523
|
+
|
|
1524
|
+
getVirtualizedProps() {
|
|
1525
|
+
return {
|
|
1526
|
+
itemHeight: this.virtualItemHeight,
|
|
1527
|
+
visibleCount: this.visibleItemCount,
|
|
1528
|
+
overscan: 5
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Debounce search and filter operations
|
|
1534
|
+
class OptimizedTable extends FlexiDataTable {
|
|
1535
|
+
private searchDebounce = 300;
|
|
1536
|
+
|
|
1537
|
+
createDebouncedSearch(searchFn: Function) {
|
|
1538
|
+
let timeoutId: number;
|
|
1539
|
+
return (...args: any[]) => {
|
|
1540
|
+
clearTimeout(timeoutId);
|
|
1541
|
+
timeoutId = setTimeout(() => searchFn(...args), this.searchDebounce);
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
```
|
|
1546
|
+
|
|
1547
|
+
### 2. Accessibility
|
|
1548
|
+
|
|
1549
|
+
```typescript
|
|
1550
|
+
const accessibleTableSchema = {
|
|
1551
|
+
round: true,
|
|
1552
|
+
indicatorColumn: {
|
|
1553
|
+
width: 60,
|
|
1554
|
+
displayNo: true,
|
|
1555
|
+
selectable: true
|
|
1556
|
+
},
|
|
1557
|
+
columns: [
|
|
1558
|
+
{
|
|
1559
|
+
text: 'Product Name',
|
|
1560
|
+
field: 'name',
|
|
1561
|
+
width: 200,
|
|
1562
|
+
// Add ARIA attributes for screen readers
|
|
1563
|
+
'aria-label': 'Product name, sortable column',
|
|
1564
|
+
'aria-sort': 'none'
|
|
1565
|
+
}
|
|
1566
|
+
]
|
|
1567
|
+
};
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
### 3. Error Handling
|
|
1571
|
+
|
|
1572
|
+
```typescript
|
|
1573
|
+
class RobustDataTable extends FlexiDataTable {
|
|
1574
|
+
handleError(error: any, context: string): void {
|
|
1575
|
+
console.error(`Table error in ${context}:`, error);
|
|
1576
|
+
|
|
1577
|
+
// Show user-friendly error message
|
|
1578
|
+
this.showErrorMessage(`Failed to ${context}. Please try again.`);
|
|
1579
|
+
|
|
1580
|
+
// Log error for monitoring
|
|
1581
|
+
this.logError(error, context);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
private showErrorMessage(message: string): void {
|
|
1585
|
+
// Implement user notification
|
|
1586
|
+
console.log('User message:', message);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private logError(error: any, context: string): void {
|
|
1590
|
+
// Send error to monitoring service
|
|
1591
|
+
fetch('/api/errors', {
|
|
1592
|
+
method: 'POST',
|
|
1593
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1594
|
+
body: JSON.stringify({
|
|
1595
|
+
error: error.message,
|
|
1596
|
+
context,
|
|
1597
|
+
timestamp: new Date().toISOString()
|
|
1598
|
+
})
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
### 4. State Management
|
|
1605
|
+
|
|
1606
|
+
```typescript
|
|
1607
|
+
class StatefulDataTable extends FlexiDataTable {
|
|
1608
|
+
private state = {
|
|
1609
|
+
sortColumn: null,
|
|
1610
|
+
sortDirection: 'asc',
|
|
1611
|
+
selectedRows: [],
|
|
1612
|
+
columnWidths: {},
|
|
1613
|
+
hiddenColumns: []
|
|
1614
|
+
};
|
|
1615
|
+
|
|
1616
|
+
saveState(): void {
|
|
1617
|
+
localStorage.setItem('table-state', JSON.stringify(this.state));
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
loadState(): void {
|
|
1621
|
+
try {
|
|
1622
|
+
const saved = localStorage.getItem('table-state');
|
|
1623
|
+
if (saved) {
|
|
1624
|
+
this.state = { ...this.state, ...JSON.parse(saved) };
|
|
1625
|
+
this.applyState();
|
|
1626
|
+
}
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
console.error('Failed to load table state:', error);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
private applyState(): void {
|
|
1633
|
+
// Apply saved column widths
|
|
1634
|
+
this.columns.forEach((column, index) => {
|
|
1635
|
+
if (this.state.columnWidths[index]) {
|
|
1636
|
+
column.width = this.state.columnWidths[index];
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// Apply column visibility
|
|
1641
|
+
this.state.hiddenColumns.forEach(index => {
|
|
1642
|
+
if (this.columns[index]) {
|
|
1643
|
+
this.columns[index].visible = false;
|
|
1644
|
+
}
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
This comprehensive guide provides everything needed to effectively use and extend FlexiDataTable for building sophisticated data management interfaces. The examples show how to create powerful, interactive tables with advanced features like real-time updates, custom rendering, and complex data operations.
|