@htlkg/components 0.0.1 → 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.
@@ -1,44 +1,169 @@
1
- import { ref, computed, type Ref, type ComputedRef } from 'vue';
1
+ import { ref, computed, unref, type Ref, type ComputedRef, type MaybeRef } from 'vue';
2
+
3
+ export interface SmartFilter {
4
+ category: string;
5
+ operator: string;
6
+ value: any;
7
+ }
2
8
 
3
9
  export interface UseTableOptions<T> {
4
- items: T[];
10
+ items: MaybeRef<T[]>; // Accept both arrays and refs/computed
5
11
  pageSize?: number;
6
12
  sortKey?: string;
7
13
  sortOrder?: 'asc' | 'desc';
14
+ idKey?: string; // Key to use for item identification (default: 'id')
8
15
  }
9
16
 
10
17
  export interface UseTableReturn<T> {
18
+ // Pagination
11
19
  currentPage: Ref<number>;
12
20
  pageSize: Ref<number>;
21
+ totalPages: ComputedRef<number>;
22
+ totalItems: ComputedRef<number>;
23
+ paginatedItems: ComputedRef<T[]>;
24
+
25
+ // Sorting
13
26
  sortKey: Ref<string>;
14
27
  sortOrder: Ref<'asc' | 'desc'>;
15
- selectedItems: Ref<T[]>;
16
- paginatedItems: ComputedRef<T[]>;
17
- totalPages: ComputedRef<number>;
18
28
  sortedItems: ComputedRef<T[]>;
29
+
30
+ // Selection
31
+ selectedItems: Ref<T[]>;
32
+ selectedItemIds: Ref<Set<string | number>>;
33
+ allSelected: ComputedRef<boolean>;
34
+ someSelected: ComputedRef<boolean>;
35
+
36
+ // Filtering
37
+ activeFilters: Ref<SmartFilter[]>;
38
+ filteredItems: ComputedRef<T[]>;
39
+
40
+ // Column visibility
41
+ hiddenColumns: Ref<number[]>;
42
+
43
+ // Reset state
44
+ resetSelected: Ref<boolean>;
45
+
46
+ // Methods - Pagination
19
47
  setPage: (page: number) => void;
20
48
  setPageSize: (size: number) => void;
49
+
50
+ // Methods - Sorting
21
51
  setSorting: (key: string, order?: 'asc' | 'desc') => void;
52
+ handleOrderBy: (event: { value: string; orderDirection: 'asc' | 'desc' }) => void;
53
+
54
+ // Methods - Selection
22
55
  selectItem: (item: T) => void;
23
56
  deselectItem: (item: T) => void;
24
57
  selectAll: () => void;
58
+ selectAllOnPage: () => void;
25
59
  clearSelection: () => void;
26
60
  isSelected: (item: T) => boolean;
61
+
62
+ // Methods - Filtering
63
+ applyFilters: (filters: SmartFilter[]) => void;
64
+ clearFilters: () => void;
65
+ removeFilter: (index: number) => void;
66
+
67
+ // Methods - Column visibility
68
+ toggleColumn: (index: number) => void;
69
+ showColumn: (index: number) => void;
70
+ hideColumn: (index: number) => void;
71
+
72
+ // Event handlers for uiTable
73
+ handlePageChange: (page: number) => void;
74
+ handlePageSizeChange: (size: number | string) => void;
75
+ handleSmartFiltersApplied: (filters: SmartFilter[]) => void;
76
+ handleSmartFiltersCleared: () => void;
77
+ handleSmartFilterDeleted: (index: number) => void;
78
+ handleColumnsVisibilityChanged: (event: { index: number; hidden: boolean }) => void;
79
+ handleModalAction: (event: { modal: string; action: string }) => void;
27
80
  }
28
81
 
29
82
  export function useTable<T extends Record<string, any>>(
30
83
  options: UseTableOptions<T>
31
84
  ): UseTableReturn<T> {
85
+ const idKey = options.idKey ?? 'id';
86
+
87
+ // Create a reactive reference to items with safety check
88
+ const items = computed(() => {
89
+ try {
90
+ const unwrapped = unref(options.items);
91
+ // Ensure we always return an array
92
+ if (!unwrapped) return [];
93
+ if (!Array.isArray(unwrapped)) {
94
+ console.warn('useTable: items is not an array', unwrapped);
95
+ return [];
96
+ }
97
+ return unwrapped;
98
+ } catch (error) {
99
+ console.error('useTable: Error unwrapping items', error);
100
+ return [];
101
+ }
102
+ });
103
+
104
+ // State - Pagination
32
105
  const currentPage = ref(1);
33
106
  const pageSize = ref(options.pageSize ?? 10);
107
+
108
+ // State - Sorting
34
109
  const sortKey = ref(options.sortKey ?? '');
35
110
  const sortOrder = ref<'asc' | 'desc'>(options.sortOrder ?? 'asc');
36
- const selectedItems = ref<T[]>([]);
111
+
112
+ // State - Selection
113
+ const selectedItems = ref<T[]>([]) as Ref<T[]>;
114
+ const selectedItemIds = ref<Set<string | number>>(new Set());
115
+ const resetSelected = ref(false);
116
+
117
+ // State - Filtering
118
+ const activeFilters = ref<SmartFilter[]>([]);
119
+
120
+ // State - Column visibility
121
+ const hiddenColumns = ref<number[]>([]);
122
+
123
+ // Computed - Filtering
124
+ const filteredItems = computed(() => {
125
+ if (activeFilters.value.length === 0) return items.value;
126
+
127
+ return items.value.filter(item => {
128
+ // All filters must pass (AND logic by default)
129
+ return activeFilters.value.every(filter => {
130
+ const { category, operator, value } = filter;
131
+
132
+ if (value === undefined || value === null || value === '') return true;
133
+
134
+ const itemValue = item[category];
135
+
136
+ // Handle different operators
137
+ switch (operator) {
138
+ case 'contains':
139
+ return String(itemValue).toLowerCase().includes(String(value).toLowerCase());
140
+ case 'is':
141
+ case '=':
142
+ return itemValue === value;
143
+ case '>':
144
+ case 'greater':
145
+ return Number(itemValue) > Number(value);
146
+ case '<':
147
+ case 'less':
148
+ return Number(itemValue) < Number(value);
149
+ case '>=':
150
+ case 'greaterOrEqual':
151
+ return Number(itemValue) >= Number(value);
152
+ case '<=':
153
+ case 'lessOrEqual':
154
+ return Number(itemValue) <= Number(value);
155
+ default:
156
+ return String(itemValue).toLowerCase().includes(String(value).toLowerCase());
157
+ }
158
+ });
159
+ });
160
+ });
37
161
 
162
+ // Computed - Sorting
38
163
  const sortedItems = computed(() => {
39
- if (!sortKey.value) return options.items;
164
+ if (!sortKey.value) return filteredItems.value;
40
165
 
41
- return [...options.items].sort((a, b) => {
166
+ return [...filteredItems.value].sort((a, b) => {
42
167
  const aVal = a[sortKey.value];
43
168
  const bVal = b[sortKey.value];
44
169
  const order = sortOrder.value === 'asc' ? 1 : -1;
@@ -47,10 +172,16 @@ export function useTable<T extends Record<string, any>>(
47
172
  if (aVal == null) return 1;
48
173
  if (bVal == null) return -1;
49
174
 
175
+ // Handle different types
176
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
177
+ return aVal.localeCompare(bVal) * order;
178
+ }
179
+
50
180
  return aVal > bVal ? order : -order;
51
181
  });
52
182
  });
53
183
 
184
+ // Computed - Pagination
54
185
  const paginatedItems = computed(() => {
55
186
  const start = (currentPage.value - 1) * pageSize.value;
56
187
  const end = start + pageSize.value;
@@ -58,9 +189,22 @@ export function useTable<T extends Record<string, any>>(
58
189
  });
59
190
 
60
191
  const totalPages = computed(() =>
61
- Math.ceil(options.items.length / pageSize.value)
192
+ Math.ceil(sortedItems.value.length / pageSize.value)
62
193
  );
63
194
 
195
+ const totalItems = computed(() => sortedItems.value.length);
196
+
197
+ // Computed - Selection
198
+ const allSelected = computed(() => {
199
+ if (items.value.length === 0) return false;
200
+ return selectedItemIds.value.size === items.value.length;
201
+ });
202
+
203
+ const someSelected = computed(() => {
204
+ return selectedItemIds.value.size > 0 && !allSelected.value;
205
+ });
206
+
207
+ // Methods - Pagination
64
208
  function setPage(page: number) {
65
209
  if (page >= 1 && page <= totalPages.value) {
66
210
  currentPage.value = page;
@@ -72,6 +216,16 @@ export function useTable<T extends Record<string, any>>(
72
216
  currentPage.value = 1; // Reset to first page
73
217
  }
74
218
 
219
+ function handlePageChange(page: number) {
220
+ setPage(page);
221
+ }
222
+
223
+ function handlePageSizeChange(size: number | string) {
224
+ const numSize = typeof size === 'string' ? parseInt(size) : size;
225
+ setPageSize(numSize);
226
+ }
227
+
228
+ // Methods - Sorting
75
229
  function setSorting(key: string, order: 'asc' | 'desc' = 'asc') {
76
230
  if (sortKey.value === key) {
77
231
  // Toggle order if same key
@@ -82,53 +236,200 @@ export function useTable<T extends Record<string, any>>(
82
236
  }
83
237
  }
84
238
 
239
+ function handleOrderBy(event: { value: string; orderDirection: 'asc' | 'desc' }) {
240
+ sortKey.value = event.value;
241
+ sortOrder.value = event.orderDirection;
242
+ }
243
+
244
+ // Methods - Selection
245
+ function getItemId(item: T): string | number {
246
+ return item[idKey];
247
+ }
248
+
85
249
  function selectItem(item: T) {
86
- if (!isSelected(item)) {
87
- selectedItems.value.push(item);
250
+ const id = getItemId(item);
251
+ if (!selectedItemIds.value.has(id)) {
252
+ selectedItemIds.value.add(id);
253
+ (selectedItems.value as T[]).push(item);
88
254
  }
89
255
  }
90
256
 
91
257
  function deselectItem(item: T) {
92
- // Use deep comparison for objects
93
- const index = selectedItems.value.findIndex(i =>
94
- JSON.stringify(i) === JSON.stringify(item)
95
- );
96
- if (index !== -1) {
97
- selectedItems.value.splice(index, 1);
258
+ const id = getItemId(item);
259
+ if (selectedItemIds.value.has(id)) {
260
+ selectedItemIds.value.delete(id);
261
+ const index = (selectedItems.value as T[]).findIndex(i => getItemId(i) === id);
262
+ if (index !== -1) {
263
+ (selectedItems.value as T[]).splice(index, 1);
264
+ }
98
265
  }
99
266
  }
100
267
 
101
268
  function selectAll() {
102
- selectedItems.value = [...options.items];
269
+ selectedItemIds.value.clear();
270
+ (selectedItems.value as T[]).length = 0;
271
+
272
+ items.value.forEach(item => {
273
+ const id = getItemId(item);
274
+ selectedItemIds.value.add(id);
275
+ (selectedItems.value as T[]).push(item);
276
+ });
277
+ }
278
+
279
+ function selectAllOnPage() {
280
+ paginatedItems.value.forEach(item => {
281
+ selectItem(item);
282
+ });
103
283
  }
104
284
 
105
285
  function clearSelection() {
106
- selectedItems.value = [];
286
+ selectedItemIds.value.clear();
287
+ (selectedItems.value as T[]).length = 0;
288
+ resetSelected.value = true;
289
+ setTimeout(() => {
290
+ resetSelected.value = false;
291
+ }, 100);
107
292
  }
108
293
 
109
294
  function isSelected(item: T): boolean {
110
- // Use deep comparison for objects
111
- return selectedItems.value.some(i =>
112
- JSON.stringify(i) === JSON.stringify(item)
113
- );
295
+ const id = getItemId(item);
296
+ return selectedItemIds.value.has(id);
297
+ }
298
+
299
+ // Methods - Filtering
300
+ function applyFilters(filters: SmartFilter[]) {
301
+ activeFilters.value = filters;
302
+ currentPage.value = 1; // Reset to first page when filters change
303
+ }
304
+
305
+ function clearFilters() {
306
+ activeFilters.value = [];
307
+ currentPage.value = 1;
308
+ }
309
+
310
+ function removeFilter(index: number) {
311
+ if (index >= 0 && index < activeFilters.value.length) {
312
+ activeFilters.value.splice(index, 1);
313
+ currentPage.value = 1;
314
+ }
315
+ }
316
+
317
+ function handleSmartFiltersApplied(filters: SmartFilter[]) {
318
+ applyFilters(filters);
319
+ }
320
+
321
+ function handleSmartFiltersCleared() {
322
+ clearFilters();
323
+ }
324
+
325
+ function handleSmartFilterDeleted(index: number) {
326
+ removeFilter(index);
327
+ }
328
+
329
+ // Methods - Column visibility
330
+ function toggleColumn(index: number) {
331
+ const hiddenIndex = hiddenColumns.value.indexOf(index);
332
+ if (hiddenIndex > -1) {
333
+ hiddenColumns.value.splice(hiddenIndex, 1);
334
+ } else {
335
+ hiddenColumns.value.push(index);
336
+ }
337
+ }
338
+
339
+ function showColumn(index: number) {
340
+ const hiddenIndex = hiddenColumns.value.indexOf(index);
341
+ if (hiddenIndex > -1) {
342
+ hiddenColumns.value.splice(hiddenIndex, 1);
343
+ }
344
+ }
345
+
346
+ function hideColumn(index: number) {
347
+ if (!hiddenColumns.value.includes(index)) {
348
+ hiddenColumns.value.push(index);
349
+ }
350
+ }
351
+
352
+ function handleColumnsVisibilityChanged(event: { index: number; hidden: boolean }) {
353
+ if (event.hidden) {
354
+ hideColumn(event.index);
355
+ } else {
356
+ showColumn(event.index);
357
+ }
358
+ }
359
+
360
+ // Methods - Modal actions
361
+ function handleModalAction(event: { modal: string; action: string }) {
362
+ if (event.modal === 'selectAllItemsModal' || event.modal.includes('selectAll')) {
363
+ if (event.action === 'selectAll') {
364
+ selectAll();
365
+ } else if (event.action === 'close') {
366
+ selectAllOnPage();
367
+ }
368
+ }
114
369
  }
115
370
 
116
371
  return {
372
+ // Pagination
117
373
  currentPage,
118
374
  pageSize,
375
+ totalPages,
376
+ totalItems,
377
+ paginatedItems,
378
+
379
+ // Sorting
119
380
  sortKey,
120
381
  sortOrder,
121
- selectedItems,
122
- paginatedItems,
123
- totalPages,
124
382
  sortedItems,
383
+
384
+ // Selection
385
+ selectedItems,
386
+ selectedItemIds,
387
+ allSelected,
388
+ someSelected,
389
+
390
+ // Filtering
391
+ activeFilters,
392
+ filteredItems,
393
+
394
+ // Column visibility
395
+ hiddenColumns,
396
+
397
+ // Reset state
398
+ resetSelected,
399
+
400
+ // Methods - Pagination
125
401
  setPage,
126
402
  setPageSize,
403
+ handlePageChange,
404
+ handlePageSizeChange,
405
+
406
+ // Methods - Sorting
127
407
  setSorting,
408
+ handleOrderBy,
409
+
410
+ // Methods - Selection
128
411
  selectItem,
129
412
  deselectItem,
130
413
  selectAll,
414
+ selectAllOnPage,
131
415
  clearSelection,
132
- isSelected
416
+ isSelected,
417
+
418
+ // Methods - Filtering
419
+ applyFilters,
420
+ clearFilters,
421
+ removeFilter,
422
+ handleSmartFiltersApplied,
423
+ handleSmartFiltersCleared,
424
+ handleSmartFilterDeleted,
425
+
426
+ // Methods - Column visibility
427
+ toggleColumn,
428
+ showColumn,
429
+ hideColumn,
430
+ handleColumnsVisibilityChanged,
431
+
432
+ // Methods - Modal actions
433
+ handleModalAction,
133
434
  };
134
435
  }
@@ -21,13 +21,13 @@ const allItems = ref([
21
21
  { id: 15, name: 'Maria Garcia', email: 'maria@example.com', role: 'User', status: 'Inactive', department: 'Marketing' },
22
22
  ]);
23
23
 
24
- // Table columns
25
- const columns = [
26
- { name: 'Name', value: 'name' },
27
- { name: 'Email', value: 'email' },
28
- { name: 'Role', value: 'role' },
29
- { name: 'Status', value: 'status' },
30
- { name: 'Department', value: 'department' }
24
+ // Table header
25
+ const header = [
26
+ { name: 'Name', value: 'name', tooltip: 'User full name' },
27
+ { name: 'Email', value: 'email', tooltip: 'User email address' },
28
+ { name: 'Role', value: 'role', tooltip: 'User role in the system' },
29
+ { name: 'Status', value: 'status', tooltip: 'Account status' },
30
+ { name: 'Department', value: 'department', tooltip: 'User department' }
31
31
  ];
32
32
 
33
33
  // State
@@ -164,11 +164,27 @@ const filteredItems = computed(() => {
164
164
  return items;
165
165
  });
166
166
 
167
- // Computed: Paginated items
167
+ // Computed: Paginated items formatted for uiTable
168
168
  const paginatedItems = computed(() => {
169
169
  const start = (currentPage.value - 1) * itemsPerPage.value;
170
170
  const end = start + itemsPerPage.value;
171
- return filteredItems.value.slice(start, end);
171
+ const items = filteredItems.value.slice(start, end);
172
+
173
+ // Format items for uiTable
174
+ return items.map(item => ({
175
+ id: item.id,
176
+ row: [
177
+ item.name,
178
+ item.email,
179
+ item.role,
180
+ {
181
+ content: item.status,
182
+ color: item.status === 'Active' ? 'green' as const : 'gray' as const,
183
+ type: 'tag' as const
184
+ },
185
+ item.department
186
+ ]
187
+ }));
172
188
  });
173
189
 
174
190
  // Computed: Total pages
@@ -261,7 +277,7 @@ const handleItemsPerPageChanged = (size: number) => {
261
277
  <!-- Table Component -->
262
278
  <Table
263
279
  :items="paginatedItems"
264
- :columns="columns"
280
+ :header="header"
265
281
  :loading="loading"
266
282
  :actions="actions"
267
283
  :select-all-items-modal="selectAllItemsModal"
@@ -8,16 +8,10 @@ import {
8
8
  uiTable
9
9
  } from '@hotelinking/ui';
10
10
 
11
- export interface Column {
12
- name: string;
13
- value: string;
14
- tooltip?: string;
15
- }
16
-
17
11
  interface Props {
18
12
  // Data
19
- items: any[];
20
- columns: Column[];
13
+ items: TableItemType[];
14
+ header: UiTableInterface['header'];
21
15
 
22
16
  // State
23
17
  loading?: boolean;
@@ -129,32 +123,6 @@ const emit = defineEmits<{
129
123
  const internalHiddenColumns = ref<number[]>(props.hiddenColumns || []);
130
124
  const selectedItemIds = ref<Set<string | number>>(new Set());
131
125
 
132
- // Convert columns to uiTable header format
133
- const tableHeader = computed<UiTableInterface['header']>(() =>
134
- props.columns.map(col => ({
135
- name: col.name,
136
- value: col.value,
137
- tooltip: col.tooltip
138
- }))
139
- );
140
-
141
- // Convert items to uiTable format
142
- const tableItems = computed<TableItemType[]>(() =>
143
- props.items.map((item, index) => ({
144
- id: item.id || index,
145
- disabled: item.disabled || false,
146
- row: props.columns.map(col => {
147
- const key = col.value || col.name.toLowerCase();
148
- const value = item[key];
149
- // Return the value as-is if it's already formatted for uiTable
150
- if (typeof value === 'object' && value !== null && 'type' in value) {
151
- return value;
152
- }
153
- return String(value ?? '');
154
- })
155
- }))
156
- );
157
-
158
126
  // Page size options for uiTable
159
127
  const pageSizeOptions = computed(() => [
160
128
  { name: '10', value: '10', active: props.pageSize === 10 },
@@ -186,7 +154,12 @@ function handleTableAction(data: { action: string; items: Array<string | number>
186
154
  selectedItemIds.value = new Set(data.items);
187
155
  updateSelectedItems();
188
156
  emit('table-action', data);
189
- resetSelectedItems();
157
+ }
158
+
159
+ function resetSelectedItems() {
160
+ selectedItemIds.value.clear();
161
+ updateSelectedItems();
162
+ emit('update:resetSelected', true);
190
163
  }
191
164
 
192
165
  function handleTableActionSelected(item: any) {
@@ -256,7 +229,7 @@ function handleSelectedItemsDeleted() {
256
229
  }
257
230
 
258
231
  function updateSelectedItems() {
259
- const selected = props.items.filter(item => selectedItemIds.value.has(item.id));
232
+ const selected = props.items.filter(item => item.id && selectedItemIds.value.has(item.id));
260
233
  emit('update:selected', selected);
261
234
  }
262
235
 
@@ -268,15 +241,16 @@ defineExpose({
268
241
  updateSelectedItems();
269
242
  },
270
243
  getHiddenColumns: () => internalHiddenColumns.value,
271
- getSelectedItems: () => Array.from(selectedItemIds.value)
244
+ getSelectedItems: () => Array.from(selectedItemIds.value),
245
+ getSelectedItemsData: () => props.items.filter(item => item.id && selectedItemIds.value.has(item.id))
272
246
  });
273
247
  </script>
274
248
 
275
249
  <template>
276
250
  <uiTable
277
251
  :loading="loading"
278
- :header="tableHeader"
279
- :items="tableItems"
252
+ :header="header"
253
+ :items="items"
280
254
  :ordered-by="orderedBy"
281
255
  :order-direction="orderDirection"
282
256
  :actions="(actions as any)"