@htlkg/components 0.0.2 → 0.0.3
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/README.md +52 -0
- package/dist/composables/index.js +196 -4
- package/dist/composables/index.js.map +1 -1
- package/package.json +7 -4
- package/src/composables/composables.md +109 -0
- package/src/composables/index.ts +17 -0
- package/src/composables/usePageContext.ts +171 -0
- package/src/composables/useTable.ts +26 -5
- package/src/data/DataTable.vue +553 -0
- package/src/data/Table/Table.vue +295 -0
- package/src/data/columnHelpers.ts +334 -0
- package/src/data/data.md +106 -0
- package/src/data/index.ts +20 -0
- package/src/domain/domain.md +102 -0
- package/src/forms/forms.md +89 -0
- package/src/index.ts +4 -3
- package/src/navigation/navigation.md +80 -0
- package/src/overlays/overlays.md +86 -0
- package/src/stores/stores.md +82 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue';
|
|
3
|
+
import {
|
|
4
|
+
type TableItemType,
|
|
5
|
+
type UiDropdownItemType,
|
|
6
|
+
type UiModalInterface,
|
|
7
|
+
type UiTableInterface,
|
|
8
|
+
uiTable
|
|
9
|
+
} from '@hotelinking/ui';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
// Data
|
|
13
|
+
items: TableItemType[];
|
|
14
|
+
header: UiTableInterface['header'];
|
|
15
|
+
|
|
16
|
+
// State
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
currentPage?: number;
|
|
19
|
+
totalPages?: number;
|
|
20
|
+
totalItems?: number;
|
|
21
|
+
pageSize?: number;
|
|
22
|
+
orderedBy?: string;
|
|
23
|
+
orderDirection?: 'asc' | 'desc';
|
|
24
|
+
|
|
25
|
+
// Selection
|
|
26
|
+
actions?: Array<{ name: string; id: string }>;
|
|
27
|
+
selectAllItemsModal?: UiModalInterface;
|
|
28
|
+
resetSelected?: boolean;
|
|
29
|
+
|
|
30
|
+
// Filters (now integrated in uiTable)
|
|
31
|
+
smartFilterCategories?: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
componentType: 'uiInput' | 'uiSelect';
|
|
35
|
+
defaultProps: any;
|
|
36
|
+
}>;
|
|
37
|
+
filterLiterals?: {
|
|
38
|
+
filters: string;
|
|
39
|
+
contains: string;
|
|
40
|
+
is: string;
|
|
41
|
+
and: string;
|
|
42
|
+
or: string;
|
|
43
|
+
deleteAll: string;
|
|
44
|
+
filter: string;
|
|
45
|
+
};
|
|
46
|
+
filters?: {
|
|
47
|
+
logicOperator: string;
|
|
48
|
+
filters: Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
label: string;
|
|
51
|
+
type: 'uiInput' | 'uiSelect';
|
|
52
|
+
value: string | undefined;
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Additional features
|
|
57
|
+
tableActionsDropdown?: { items: UiDropdownItemType[]; loading?: boolean };
|
|
58
|
+
tableActionButtons?: Array<{
|
|
59
|
+
text: string;
|
|
60
|
+
id: string;
|
|
61
|
+
color?: string;
|
|
62
|
+
size?: string;
|
|
63
|
+
disabled?: boolean;
|
|
64
|
+
icon?: any;
|
|
65
|
+
loading?: boolean;
|
|
66
|
+
}>;
|
|
67
|
+
noResults?: {
|
|
68
|
+
title: string;
|
|
69
|
+
message: string;
|
|
70
|
+
actions?: Array<{ action: string; text: string }>;
|
|
71
|
+
items?: UiDropdownItemType[];
|
|
72
|
+
select?: UiDropdownItemType;
|
|
73
|
+
};
|
|
74
|
+
hiddenColumns?: number[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
78
|
+
loading: false,
|
|
79
|
+
currentPage: 1,
|
|
80
|
+
totalPages: 1,
|
|
81
|
+
totalItems: 0,
|
|
82
|
+
pageSize: 10,
|
|
83
|
+
orderedBy: '',
|
|
84
|
+
orderDirection: 'asc',
|
|
85
|
+
resetSelected: false,
|
|
86
|
+
filterLiterals: () => ({
|
|
87
|
+
filters: 'Smart Filters',
|
|
88
|
+
contains: 'contains',
|
|
89
|
+
is: 'is',
|
|
90
|
+
and: 'and',
|
|
91
|
+
or: 'or',
|
|
92
|
+
deleteAll: 'Delete All',
|
|
93
|
+
filter: 'Filter'
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const emit = defineEmits<{
|
|
98
|
+
'update:currentPage': [page: number];
|
|
99
|
+
'update:pageSize': [size: number];
|
|
100
|
+
'update:selected': [items: any[]];
|
|
101
|
+
'update:orderedBy': [key: string];
|
|
102
|
+
'update:orderDirection': [direction: 'asc' | 'desc'];
|
|
103
|
+
'update:resetSelected': [value: boolean];
|
|
104
|
+
'table-action': [data: { action: string; items: Array<string | number> }];
|
|
105
|
+
'table-action-selected': [item: any];
|
|
106
|
+
'table-action-button-clicked': [data: { id: string; text: string }];
|
|
107
|
+
'smart-filters-sent': [filters: any];
|
|
108
|
+
'smart-filters-cleared': [];
|
|
109
|
+
'smart-filter-deleted': [index: number];
|
|
110
|
+
'custom-emit': [data: any];
|
|
111
|
+
'modal-action': [data: { modal: string; action: string }];
|
|
112
|
+
'select-all-items': [];
|
|
113
|
+
'deselect-all-items': [];
|
|
114
|
+
'no-results-action': [action: string];
|
|
115
|
+
'no-results-option-selected': [item: any];
|
|
116
|
+
'columns-visibility-changed': [data: { index: number; hidden: boolean }];
|
|
117
|
+
'order-by': [data: { value: string; orderDirection: 'asc' | 'desc' }];
|
|
118
|
+
'change-page': [page: number];
|
|
119
|
+
'change-page-size': [size: number | string];
|
|
120
|
+
}>();
|
|
121
|
+
|
|
122
|
+
// State management
|
|
123
|
+
const internalHiddenColumns = ref<number[]>(props.hiddenColumns || []);
|
|
124
|
+
const selectedItemIds = ref<Set<string | number>>(new Set());
|
|
125
|
+
|
|
126
|
+
// Page size options for uiTable
|
|
127
|
+
const pageSizeOptions = computed(() => [
|
|
128
|
+
{ name: '10', value: '10', active: props.pageSize === 10 },
|
|
129
|
+
{ name: '25', value: '25', active: props.pageSize === 25 },
|
|
130
|
+
{ name: '50', value: '50', active: props.pageSize === 50 },
|
|
131
|
+
{ name: '100', value: '100', active: props.pageSize === 100 }
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// Event handlers
|
|
135
|
+
function handleOrderBy(event: { value: string; orderDirection: 'asc' | 'desc' }) {
|
|
136
|
+
emit('update:orderedBy', event.value);
|
|
137
|
+
emit('update:orderDirection', event.orderDirection);
|
|
138
|
+
emit('order-by', event);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function handleChangePage(page: number) {
|
|
142
|
+
emit('update:currentPage', page);
|
|
143
|
+
emit('change-page', page);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleChangePageSize(size: number | string) {
|
|
147
|
+
const numSize = typeof size === 'string' ? Number(size) : size;
|
|
148
|
+
emit('update:pageSize', numSize);
|
|
149
|
+
emit('change-page-size', size);
|
|
150
|
+
emit('update:currentPage', 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleTableAction(data: { action: string; items: Array<string | number> }) {
|
|
154
|
+
selectedItemIds.value = new Set(data.items);
|
|
155
|
+
updateSelectedItems();
|
|
156
|
+
emit('table-action', data);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resetSelectedItems() {
|
|
160
|
+
selectedItemIds.value.clear();
|
|
161
|
+
updateSelectedItems();
|
|
162
|
+
emit('update:resetSelected', true);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handleTableActionSelected(item: any) {
|
|
166
|
+
emit('table-action-selected', item);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function handleTableActionButtonClicked(data: { id: string; text: string }) {
|
|
170
|
+
emit('table-action-button-clicked', data);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function handleSmartFiltersSent(filters: any) {
|
|
174
|
+
emit('smart-filters-sent', filters);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handleSmartFiltersCleared() {
|
|
178
|
+
emit('smart-filters-cleared');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handleSmartFilterDeleted(index: number) {
|
|
182
|
+
emit('smart-filter-deleted', index);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function handleCustomEmit(data: any) {
|
|
186
|
+
emit('custom-emit', data);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function handleColumnsVisibilityChanged(event: { index: number; hidden: boolean }) {
|
|
190
|
+
if (event.hidden) {
|
|
191
|
+
if (!internalHiddenColumns.value.includes(event.index)) {
|
|
192
|
+
internalHiddenColumns.value.push(event.index);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
const index = internalHiddenColumns.value.indexOf(event.index);
|
|
196
|
+
if (index > -1) {
|
|
197
|
+
internalHiddenColumns.value.splice(index, 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
emit('columns-visibility-changed', event);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function handleModalAction(data: { modal: string; action: string }) {
|
|
204
|
+
emit('modal-action', data);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function handleSelectAllItems() {
|
|
208
|
+
selectedItemIds.value = new Set(props.items.map(item => item.id));
|
|
209
|
+
updateSelectedItems();
|
|
210
|
+
emit('select-all-items');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleDeselectAllItems() {
|
|
214
|
+
selectedItemIds.value.clear();
|
|
215
|
+
updateSelectedItems();
|
|
216
|
+
emit('deselect-all-items');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function handleNoResultsAction(action: string) {
|
|
220
|
+
emit('no-results-action', action);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function handleNoResultsOptionSelected(item: any) {
|
|
224
|
+
emit('no-results-option-selected', item);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function handleSelectedItemsDeleted() {
|
|
228
|
+
emit('update:resetSelected', false);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function updateSelectedItems() {
|
|
232
|
+
const selected = props.items.filter(item => item.id && selectedItemIds.value.has(item.id));
|
|
233
|
+
emit('update:selected', selected);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Expose methods for parent components
|
|
237
|
+
defineExpose({
|
|
238
|
+
clearSelection: () => {
|
|
239
|
+
emit('update:resetSelected', true);
|
|
240
|
+
selectedItemIds.value.clear();
|
|
241
|
+
updateSelectedItems();
|
|
242
|
+
},
|
|
243
|
+
getHiddenColumns: () => internalHiddenColumns.value,
|
|
244
|
+
getSelectedItems: () => Array.from(selectedItemIds.value),
|
|
245
|
+
getSelectedItemsData: () => props.items.filter(item => item.id && selectedItemIds.value.has(item.id))
|
|
246
|
+
});
|
|
247
|
+
</script>
|
|
248
|
+
|
|
249
|
+
<template>
|
|
250
|
+
<uiTable
|
|
251
|
+
:loading="loading"
|
|
252
|
+
:header="header"
|
|
253
|
+
:items="items"
|
|
254
|
+
:ordered-by="orderedBy"
|
|
255
|
+
:order-direction="orderDirection"
|
|
256
|
+
:actions="(actions as any)"
|
|
257
|
+
:hidden-columns="internalHiddenColumns"
|
|
258
|
+
:reset-selected="resetSelected"
|
|
259
|
+
:select-all-items-modal="(selectAllItemsModal as any)"
|
|
260
|
+
:smart-filter-categories="smartFilterCategories"
|
|
261
|
+
:filter-literals="filterLiterals"
|
|
262
|
+
:filters="filters"
|
|
263
|
+
:table-actions-dropdown="tableActionsDropdown"
|
|
264
|
+
:table-action-buttons="tableActionButtons"
|
|
265
|
+
:pagination-current="currentPage"
|
|
266
|
+
:pagination-total="totalPages"
|
|
267
|
+
:pagination-total-items="totalItems"
|
|
268
|
+
:page-size-options="pageSizeOptions"
|
|
269
|
+
:current-page-size="pageSize"
|
|
270
|
+
:no-results="noResults"
|
|
271
|
+
@order-by="handleOrderBy"
|
|
272
|
+
@table-action="handleTableAction"
|
|
273
|
+
@table-action-selected="handleTableActionSelected"
|
|
274
|
+
@table-action-button-clicked="handleTableActionButtonClicked"
|
|
275
|
+
@smart-filters-sent="handleSmartFiltersSent"
|
|
276
|
+
@smart-filters-cleared="handleSmartFiltersCleared"
|
|
277
|
+
@smart-filter-deleted="handleSmartFilterDeleted"
|
|
278
|
+
@change-page="handleChangePage"
|
|
279
|
+
@change-page-size="handleChangePageSize"
|
|
280
|
+
@custom-emit="handleCustomEmit"
|
|
281
|
+
@columns-visibility-changed="handleColumnsVisibilityChanged"
|
|
282
|
+
@selected-items-deleted="handleSelectedItemsDeleted"
|
|
283
|
+
@modal-action="handleModalAction"
|
|
284
|
+
@select-all-items="handleSelectAllItems"
|
|
285
|
+
@deselect-all-items="handleDeselectAllItems"
|
|
286
|
+
@no-results-action="handleNoResultsAction"
|
|
287
|
+
@no-results-option-selected="handleNoResultsOptionSelected"
|
|
288
|
+
/>
|
|
289
|
+
</template>
|
|
290
|
+
|
|
291
|
+
<style scoped>
|
|
292
|
+
.table-component {
|
|
293
|
+
width: 100%;
|
|
294
|
+
}
|
|
295
|
+
</style>
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column Definition Helpers
|
|
3
|
+
*
|
|
4
|
+
* Pre-built column definitions for common patterns, reducing boilerplate
|
|
5
|
+
* when defining DataTable columns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DataTableColumn } from "./DataTable.vue";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Date formatting options
|
|
12
|
+
*/
|
|
13
|
+
export interface DateFormatOptions {
|
|
14
|
+
format?: "short" | "medium" | "long" | "full";
|
|
15
|
+
locale?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tag color function type
|
|
20
|
+
*/
|
|
21
|
+
export type TagColorFn<T> = (item: T) => string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pre-built column helpers
|
|
25
|
+
*/
|
|
26
|
+
export const columns = {
|
|
27
|
+
/**
|
|
28
|
+
* Simple text column
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* columns.text('name', 'Name')
|
|
32
|
+
*/
|
|
33
|
+
text: <T = any>(key: string, label: string, options?: { sortable?: boolean; width?: string }): DataTableColumn<T> => ({
|
|
34
|
+
key,
|
|
35
|
+
label,
|
|
36
|
+
type: "text",
|
|
37
|
+
sortable: options?.sortable ?? true,
|
|
38
|
+
width: options?.width,
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Number column with locale formatting
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* columns.number('amount', 'Amount')
|
|
46
|
+
* columns.number('price', 'Price', { prefix: '$', decimals: 2 })
|
|
47
|
+
*/
|
|
48
|
+
number: <T = any>(
|
|
49
|
+
key: string,
|
|
50
|
+
label: string,
|
|
51
|
+
options?: { prefix?: string; suffix?: string; decimals?: number; sortable?: boolean }
|
|
52
|
+
): DataTableColumn<T> => ({
|
|
53
|
+
key,
|
|
54
|
+
label,
|
|
55
|
+
type: "number",
|
|
56
|
+
sortable: options?.sortable ?? true,
|
|
57
|
+
render: (item: any) => {
|
|
58
|
+
const value = item[key];
|
|
59
|
+
if (value === null || value === undefined) return "";
|
|
60
|
+
|
|
61
|
+
const formatted =
|
|
62
|
+
options?.decimals !== undefined
|
|
63
|
+
? Number(value).toLocaleString(undefined, {
|
|
64
|
+
minimumFractionDigits: options.decimals,
|
|
65
|
+
maximumFractionDigits: options.decimals,
|
|
66
|
+
})
|
|
67
|
+
: Number(value).toLocaleString();
|
|
68
|
+
|
|
69
|
+
return `${options?.prefix ?? ""}${formatted}${options?.suffix ?? ""}`;
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Date column with formatting
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* columns.date('createdAt', 'Created')
|
|
78
|
+
* columns.date('updatedAt', 'Updated', { format: 'long' })
|
|
79
|
+
*/
|
|
80
|
+
date: <T = any>(key: string, label: string, options?: DateFormatOptions & { sortable?: boolean }): DataTableColumn<T> => ({
|
|
81
|
+
key,
|
|
82
|
+
label,
|
|
83
|
+
type: "date",
|
|
84
|
+
sortable: options?.sortable ?? true,
|
|
85
|
+
render: (item: any) => {
|
|
86
|
+
const value = item[key];
|
|
87
|
+
if (!value) return "";
|
|
88
|
+
|
|
89
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
90
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
91
|
+
|
|
92
|
+
const formatOptions: Intl.DateTimeFormatOptions = {
|
|
93
|
+
short: { dateStyle: "short" },
|
|
94
|
+
medium: { dateStyle: "medium" },
|
|
95
|
+
long: { dateStyle: "long" },
|
|
96
|
+
full: { dateStyle: "full" },
|
|
97
|
+
}[options?.format ?? "medium"] as Intl.DateTimeFormatOptions;
|
|
98
|
+
|
|
99
|
+
return date.toLocaleDateString(options?.locale, formatOptions);
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* DateTime column with formatting
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* columns.dateTime('createdAt', 'Created At')
|
|
108
|
+
*/
|
|
109
|
+
dateTime: <T = any>(key: string, label: string, options?: { sortable?: boolean; locale?: string }): DataTableColumn<T> => ({
|
|
110
|
+
key,
|
|
111
|
+
label,
|
|
112
|
+
type: "date",
|
|
113
|
+
sortable: options?.sortable ?? true,
|
|
114
|
+
render: (item: any) => {
|
|
115
|
+
const value = item[key];
|
|
116
|
+
if (!value) return "";
|
|
117
|
+
|
|
118
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
119
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
120
|
+
|
|
121
|
+
return date.toLocaleString(options?.locale, {
|
|
122
|
+
dateStyle: "medium",
|
|
123
|
+
timeStyle: "short",
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Tag column with optional color function
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* columns.tag('status', 'Status')
|
|
133
|
+
* columns.tag('status', 'Status', (item) => item.active ? 'green' : 'red')
|
|
134
|
+
*/
|
|
135
|
+
tag: <T = any>(key: string, label: string, colorFn?: TagColorFn<T>, options?: { sortable?: boolean }): DataTableColumn<T> => ({
|
|
136
|
+
key,
|
|
137
|
+
label,
|
|
138
|
+
type: "tag",
|
|
139
|
+
sortable: options?.sortable ?? true,
|
|
140
|
+
render: (item: T) => {
|
|
141
|
+
const value = (item as any)[key];
|
|
142
|
+
return {
|
|
143
|
+
content: value,
|
|
144
|
+
color: colorFn?.(item) ?? "gray",
|
|
145
|
+
type: "tag",
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Badge column (numeric badge style)
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* columns.badge('count', 'Count')
|
|
155
|
+
* columns.badge('brandCount', 'Brands')
|
|
156
|
+
*/
|
|
157
|
+
badge: <T = any>(key: string, label: string, options?: { sortable?: boolean }): DataTableColumn<T> => ({
|
|
158
|
+
key,
|
|
159
|
+
label,
|
|
160
|
+
type: "badge",
|
|
161
|
+
sortable: options?.sortable ?? true,
|
|
162
|
+
render: (item: any) => ({
|
|
163
|
+
content: item[key],
|
|
164
|
+
type: "badge",
|
|
165
|
+
}),
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Boolean column rendered as tag
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* columns.boolean('active', 'Active')
|
|
173
|
+
* columns.boolean('verified', 'Verified', { trueLabel: 'Yes', falseLabel: 'No' })
|
|
174
|
+
*/
|
|
175
|
+
boolean: <T = any>(
|
|
176
|
+
key: string,
|
|
177
|
+
label: string,
|
|
178
|
+
options?: { trueLabel?: string; falseLabel?: string; trueColor?: string; falseColor?: string; sortable?: boolean }
|
|
179
|
+
): DataTableColumn<T> => ({
|
|
180
|
+
key,
|
|
181
|
+
label,
|
|
182
|
+
type: "tag",
|
|
183
|
+
sortable: options?.sortable ?? true,
|
|
184
|
+
render: (item: any) => {
|
|
185
|
+
const value = Boolean(item[key]);
|
|
186
|
+
return {
|
|
187
|
+
content: value ? (options?.trueLabel ?? "Yes") : (options?.falseLabel ?? "No"),
|
|
188
|
+
color: value ? (options?.trueColor ?? "green") : (options?.falseColor ?? "red"),
|
|
189
|
+
type: "tag",
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Email column (renders as link)
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* columns.email('email', 'Email')
|
|
199
|
+
*/
|
|
200
|
+
email: <T = any>(key: string, label: string, options?: { sortable?: boolean }): DataTableColumn<T> => ({
|
|
201
|
+
key,
|
|
202
|
+
label,
|
|
203
|
+
type: "text",
|
|
204
|
+
sortable: options?.sortable ?? true,
|
|
205
|
+
render: (item: any) => ({
|
|
206
|
+
content: item[key],
|
|
207
|
+
type: "link",
|
|
208
|
+
href: `mailto:${item[key]}`,
|
|
209
|
+
}),
|
|
210
|
+
}),
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Actions column (row actions)
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* columns.actions(['view', 'edit', 'delete'])
|
|
217
|
+
*/
|
|
218
|
+
actions: <T = any>(actions: string[]): DataTableColumn<T> => ({
|
|
219
|
+
key: "_actions",
|
|
220
|
+
label: "",
|
|
221
|
+
sortable: false,
|
|
222
|
+
render: () => actions,
|
|
223
|
+
}),
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Custom render column
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* columns.custom('fullName', 'Name', (item) => `${item.firstName} ${item.lastName}`)
|
|
230
|
+
*/
|
|
231
|
+
custom: <T = any>(
|
|
232
|
+
key: string,
|
|
233
|
+
label: string,
|
|
234
|
+
render: (item: T, index: number) => any,
|
|
235
|
+
options?: { sortable?: boolean; width?: string }
|
|
236
|
+
): DataTableColumn<T> => ({
|
|
237
|
+
key,
|
|
238
|
+
label,
|
|
239
|
+
sortable: options?.sortable ?? false,
|
|
240
|
+
width: options?.width,
|
|
241
|
+
render,
|
|
242
|
+
}),
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Truncated text column
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* columns.truncate('description', 'Description', 50)
|
|
249
|
+
*/
|
|
250
|
+
truncate: <T = any>(key: string, label: string, maxLength: number, options?: { sortable?: boolean }): DataTableColumn<T> => ({
|
|
251
|
+
key,
|
|
252
|
+
label,
|
|
253
|
+
type: "text",
|
|
254
|
+
sortable: options?.sortable ?? true,
|
|
255
|
+
render: (item: any) => {
|
|
256
|
+
const value = String(item[key] ?? "");
|
|
257
|
+
if (value.length <= maxLength) return value;
|
|
258
|
+
return `${value.substring(0, maxLength)}...`;
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Image column
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* columns.image('avatar', 'Avatar', { size: 32 })
|
|
267
|
+
*/
|
|
268
|
+
image: <T = any>(key: string, label: string, options?: { size?: number; fallback?: string }): DataTableColumn<T> => ({
|
|
269
|
+
key,
|
|
270
|
+
label,
|
|
271
|
+
sortable: false,
|
|
272
|
+
render: (item: any) => ({
|
|
273
|
+
content: item[key] || options?.fallback,
|
|
274
|
+
type: "image",
|
|
275
|
+
size: options?.size ?? 32,
|
|
276
|
+
}),
|
|
277
|
+
}),
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Progress column
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* columns.progress('completion', 'Progress')
|
|
284
|
+
*/
|
|
285
|
+
progress: <T = any>(key: string, label: string, options?: { max?: number; sortable?: boolean }): DataTableColumn<T> => ({
|
|
286
|
+
key,
|
|
287
|
+
label,
|
|
288
|
+
type: "number",
|
|
289
|
+
sortable: options?.sortable ?? true,
|
|
290
|
+
render: (item: any) => ({
|
|
291
|
+
content: item[key],
|
|
292
|
+
type: "progress",
|
|
293
|
+
max: options?.max ?? 100,
|
|
294
|
+
}),
|
|
295
|
+
}),
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Status color mappings for common status values
|
|
300
|
+
*/
|
|
301
|
+
export const statusColors: Record<string, string> = {
|
|
302
|
+
active: "green",
|
|
303
|
+
inactive: "gray",
|
|
304
|
+
pending: "yellow",
|
|
305
|
+
approved: "green",
|
|
306
|
+
rejected: "red",
|
|
307
|
+
draft: "gray",
|
|
308
|
+
published: "green",
|
|
309
|
+
archived: "gray",
|
|
310
|
+
error: "red",
|
|
311
|
+
success: "green",
|
|
312
|
+
warning: "yellow",
|
|
313
|
+
info: "blue",
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get color for a status value
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* columns.tag('status', 'Status', (item) => getStatusColor(item.status))
|
|
321
|
+
*/
|
|
322
|
+
export function getStatusColor(status: string): string {
|
|
323
|
+
return statusColors[status?.toLowerCase()] ?? "gray";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Create a status color function for a specific key
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* columns.tag('status', 'Status', createStatusColorFn('status'))
|
|
331
|
+
*/
|
|
332
|
+
export function createStatusColorFn<T>(key: keyof T): TagColorFn<T> {
|
|
333
|
+
return (item: T) => getStatusColor(String(item[key]));
|
|
334
|
+
}
|