@alaarab/ogrid-vue-vuetify 2.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/dist/esm/ColumnChooser/ColumnChooser.js +74 -0
- package/dist/esm/ColumnHeaderFilter/ColumnHeaderFilter.js +210 -0
- package/dist/esm/ColumnHeaderFilter/MultiSelectFilterPopover.js +68 -0
- package/dist/esm/ColumnHeaderFilter/PeopleFilterPopover.js +82 -0
- package/dist/esm/ColumnHeaderFilter/TextFilterPopover.js +40 -0
- package/dist/esm/ColumnHeaderFilter/index.js +1 -0
- package/dist/esm/DataGridTable/DataGridTable.js +467 -0
- package/dist/esm/DataGridTable/GridContextMenu.js +55 -0
- package/dist/esm/DataGridTable/InlineCellEditor.js +124 -0
- package/dist/esm/DataGridTable/StatusBar.js +45 -0
- package/dist/esm/OGrid/OGrid.js +95 -0
- package/dist/esm/PaginationControls/PaginationControls.js +131 -0
- package/dist/esm/index.js +8 -0
- package/dist/types/ColumnChooser/ColumnChooser.d.ts +36 -0
- package/dist/types/ColumnHeaderFilter/ColumnHeaderFilter.d.ts +177 -0
- package/dist/types/ColumnHeaderFilter/MultiSelectFilterPopover.d.ts +88 -0
- package/dist/types/ColumnHeaderFilter/PeopleFilterPopover.d.ts +66 -0
- package/dist/types/ColumnHeaderFilter/TextFilterPopover.d.ts +38 -0
- package/dist/types/ColumnHeaderFilter/index.d.ts +2 -0
- package/dist/types/DataGridTable/DataGridTable.d.ts +15 -0
- package/dist/types/DataGridTable/GridContextMenu.d.ts +116 -0
- package/dist/types/DataGridTable/InlineCellEditor.d.ts +72 -0
- package/dist/types/DataGridTable/StatusBar.d.ts +80 -0
- package/dist/types/OGrid/OGrid.d.ts +15 -0
- package/dist/types/PaginationControls/PaginationControls.d.ts +74 -0
- package/dist/types/index.d.ts +9 -0
- package/package.json +40 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineComponent, ref, computed, h } from 'vue';
|
|
2
|
+
import { VBtn, VMenu, VList, VListItem, VDivider } from 'vuetify/components';
|
|
3
|
+
import { useColumnChooserState } from '@alaarab/ogrid-vue';
|
|
4
|
+
export const ColumnChooser = defineComponent({
|
|
5
|
+
name: 'ColumnChooser',
|
|
6
|
+
props: {
|
|
7
|
+
columns: { type: Array, required: true },
|
|
8
|
+
visibleColumns: { type: Object, required: true },
|
|
9
|
+
onVisibilityChange: { type: Function, required: true },
|
|
10
|
+
},
|
|
11
|
+
setup(props) {
|
|
12
|
+
const menuOpen = ref(false);
|
|
13
|
+
const columnsRef = computed(() => props.columns);
|
|
14
|
+
const visibleColumnsRef = computed(() => props.visibleColumns);
|
|
15
|
+
const state = useColumnChooserState({
|
|
16
|
+
columns: columnsRef,
|
|
17
|
+
visibleColumns: visibleColumnsRef,
|
|
18
|
+
onVisibilityChange: props.onVisibilityChange,
|
|
19
|
+
});
|
|
20
|
+
return () => {
|
|
21
|
+
return h(VMenu, {
|
|
22
|
+
modelValue: menuOpen.value,
|
|
23
|
+
'onUpdate:modelValue': (v) => { menuOpen.value = v; },
|
|
24
|
+
closeOnContentClick: false,
|
|
25
|
+
location: 'bottom end',
|
|
26
|
+
}, {
|
|
27
|
+
activator: ({ props: activatorProps }) => h(VBtn, {
|
|
28
|
+
...activatorProps,
|
|
29
|
+
variant: 'outlined',
|
|
30
|
+
size: 'small',
|
|
31
|
+
prependIcon: 'mdi-view-column',
|
|
32
|
+
appendIcon: menuOpen.value ? 'mdi-chevron-up' : 'mdi-chevron-down',
|
|
33
|
+
}, () => `Column Visibility (${state.visibleCount.value} of ${state.totalCount.value})`),
|
|
34
|
+
default: () => h('div', { style: { minWidth: '220px' } }, [
|
|
35
|
+
// Header
|
|
36
|
+
h('div', {
|
|
37
|
+
style: {
|
|
38
|
+
padding: '8px 12px',
|
|
39
|
+
borderBottom: '1px solid rgba(0,0,0,0.12)',
|
|
40
|
+
backgroundColor: 'rgba(0,0,0,0.04)',
|
|
41
|
+
fontWeight: '600',
|
|
42
|
+
fontSize: '0.875rem',
|
|
43
|
+
},
|
|
44
|
+
}, `Select Columns (${state.visibleCount.value} of ${state.totalCount.value})`),
|
|
45
|
+
// Column list
|
|
46
|
+
h(VList, { density: 'compact', style: { maxHeight: '320px', overflowY: 'auto' } }, () => props.columns.map((column) => h(VListItem, { key: column.columnId, style: { minHeight: '32px' } }, () => h('label', {
|
|
47
|
+
style: { display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', width: '100%' },
|
|
48
|
+
}, [
|
|
49
|
+
h('input', {
|
|
50
|
+
type: 'checkbox',
|
|
51
|
+
checked: props.visibleColumns.has(column.columnId),
|
|
52
|
+
onChange: (e) => state.handleCheckboxChange(column.columnId)(e.target.checked),
|
|
53
|
+
}),
|
|
54
|
+
h('span', { style: { fontSize: '0.875rem' } }, column.name),
|
|
55
|
+
])))),
|
|
56
|
+
// Footer with actions
|
|
57
|
+
h(VDivider),
|
|
58
|
+
h('div', {
|
|
59
|
+
style: {
|
|
60
|
+
display: 'flex',
|
|
61
|
+
justifyContent: 'flex-end',
|
|
62
|
+
gap: '8px',
|
|
63
|
+
padding: '8px 12px',
|
|
64
|
+
backgroundColor: 'rgba(0,0,0,0.04)',
|
|
65
|
+
},
|
|
66
|
+
}, [
|
|
67
|
+
h(VBtn, { size: 'small', variant: 'text', onClick: state.handleClearAll }, () => 'Clear All'),
|
|
68
|
+
h(VBtn, { size: 'small', variant: 'flat', color: 'primary', onClick: state.handleSelectAll }, () => 'Select All'),
|
|
69
|
+
]),
|
|
70
|
+
]),
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue';
|
|
2
|
+
import { VBtn, VIcon, VMenu, VTooltip } from 'vuetify/components';
|
|
3
|
+
import { useColumnHeaderFilterState, } from '@alaarab/ogrid-vue';
|
|
4
|
+
import { TextFilterPopover } from './TextFilterPopover';
|
|
5
|
+
import { MultiSelectFilterPopover } from './MultiSelectFilterPopover';
|
|
6
|
+
import { PeopleFilterPopover } from './PeopleFilterPopover';
|
|
7
|
+
// Vuetify component types don't align with h() overloads; cast to Component
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
9
|
+
const _VBtn = VBtn;
|
|
10
|
+
const _VIcon = VIcon;
|
|
11
|
+
const _VMenu = VMenu;
|
|
12
|
+
const _VTooltip = VTooltip;
|
|
13
|
+
export const ColumnHeaderFilter = defineComponent({
|
|
14
|
+
name: 'ColumnHeaderFilter',
|
|
15
|
+
props: {
|
|
16
|
+
columnKey: { type: String, required: true },
|
|
17
|
+
columnName: { type: String, required: true },
|
|
18
|
+
filterType: { type: String, required: true },
|
|
19
|
+
isSorted: { type: Boolean, default: false },
|
|
20
|
+
isSortedDescending: { type: Boolean, default: false },
|
|
21
|
+
onSort: { type: Function, default: undefined },
|
|
22
|
+
selectedValues: { type: Array, default: undefined },
|
|
23
|
+
onFilterChange: { type: Function, default: undefined },
|
|
24
|
+
options: { type: Array, default: () => [] },
|
|
25
|
+
isLoadingOptions: { type: Boolean, default: false },
|
|
26
|
+
textValue: { type: String, default: '' },
|
|
27
|
+
onTextChange: { type: Function, default: undefined },
|
|
28
|
+
selectedUser: { type: Object, default: undefined },
|
|
29
|
+
onUserChange: { type: Function, default: undefined },
|
|
30
|
+
peopleSearch: { type: Function, default: undefined },
|
|
31
|
+
dateValue: { type: Object, default: undefined },
|
|
32
|
+
onDateChange: { type: Function, default: undefined },
|
|
33
|
+
},
|
|
34
|
+
setup(props) {
|
|
35
|
+
const state = useColumnHeaderFilterState({
|
|
36
|
+
filterType: props.filterType,
|
|
37
|
+
isSorted: props.isSorted,
|
|
38
|
+
isSortedDescending: props.isSortedDescending,
|
|
39
|
+
onSort: props.onSort,
|
|
40
|
+
selectedValues: props.selectedValues,
|
|
41
|
+
onFilterChange: props.onFilterChange,
|
|
42
|
+
options: props.options,
|
|
43
|
+
isLoadingOptions: props.isLoadingOptions,
|
|
44
|
+
textValue: props.textValue,
|
|
45
|
+
onTextChange: props.onTextChange,
|
|
46
|
+
selectedUser: props.selectedUser,
|
|
47
|
+
onUserChange: props.onUserChange,
|
|
48
|
+
peopleSearch: props.peopleSearch,
|
|
49
|
+
dateValue: props.dateValue,
|
|
50
|
+
onDateChange: props.onDateChange,
|
|
51
|
+
});
|
|
52
|
+
const renderPopoverContent = () => {
|
|
53
|
+
if (props.filterType === 'multiSelect') {
|
|
54
|
+
return h(MultiSelectFilterPopover, {
|
|
55
|
+
searchText: state.searchText.value,
|
|
56
|
+
onSearchChange: state.setSearchText,
|
|
57
|
+
options: props.options ?? [],
|
|
58
|
+
filteredOptions: state.filteredOptions.value,
|
|
59
|
+
selected: state.tempSelected.value,
|
|
60
|
+
onOptionToggle: state.handlers.handleCheckboxChange,
|
|
61
|
+
onSelectAll: state.handlers.handleSelectAll,
|
|
62
|
+
onClearSelection: state.handlers.handleClearSelection,
|
|
63
|
+
onApply: state.handlers.handleApplyMultiSelect,
|
|
64
|
+
isLoading: props.isLoadingOptions,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (props.filterType === 'text') {
|
|
68
|
+
return h(TextFilterPopover, {
|
|
69
|
+
value: state.tempTextValue.value ?? '',
|
|
70
|
+
onValueChange: state.setTempTextValue,
|
|
71
|
+
onApply: state.handlers.handleTextApply,
|
|
72
|
+
onClear: state.handlers.handleTextClear,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (props.filterType === 'people') {
|
|
76
|
+
return h(PeopleFilterPopover, {
|
|
77
|
+
selectedUser: props.selectedUser,
|
|
78
|
+
searchText: state.peopleSearchText.value,
|
|
79
|
+
onSearchChange: state.setPeopleSearchText,
|
|
80
|
+
suggestions: state.peopleSuggestions.value,
|
|
81
|
+
isLoading: state.isPeopleLoading.value,
|
|
82
|
+
onUserSelect: state.handlers.handleUserSelect,
|
|
83
|
+
onClearUser: state.handlers.handleClearUser,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (props.filterType === 'date') {
|
|
87
|
+
return h('div', { style: { padding: '12px', display: 'flex', flexDirection: 'column', gap: '8px' } }, [
|
|
88
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [
|
|
89
|
+
h('span', { style: { minWidth: '36px', fontSize: '0.75rem' } }, 'From:'),
|
|
90
|
+
h('input', {
|
|
91
|
+
type: 'date',
|
|
92
|
+
value: state.tempDateFrom.value ?? '',
|
|
93
|
+
onInput: (e) => state.setTempDateFrom(e.target.value),
|
|
94
|
+
style: { flex: '1', padding: '4px 6px' },
|
|
95
|
+
}),
|
|
96
|
+
]),
|
|
97
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } }, [
|
|
98
|
+
h('span', { style: { minWidth: '36px', fontSize: '0.75rem' } }, 'To:'),
|
|
99
|
+
h('input', {
|
|
100
|
+
type: 'date',
|
|
101
|
+
value: state.tempDateTo.value ?? '',
|
|
102
|
+
onInput: (e) => state.setTempDateTo(e.target.value),
|
|
103
|
+
style: { flex: '1', padding: '4px 6px' },
|
|
104
|
+
}),
|
|
105
|
+
]),
|
|
106
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' } }, [
|
|
107
|
+
h('button', {
|
|
108
|
+
onClick: state.handlers.handleDateClear,
|
|
109
|
+
disabled: !state.tempDateFrom.value && !state.tempDateTo.value,
|
|
110
|
+
style: { padding: '4px 12px', cursor: 'pointer' },
|
|
111
|
+
}, 'Clear'),
|
|
112
|
+
h('button', {
|
|
113
|
+
onClick: state.handlers.handleDateApply,
|
|
114
|
+
style: { padding: '4px 12px', cursor: 'pointer' },
|
|
115
|
+
}, 'Apply'),
|
|
116
|
+
]),
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
return () => {
|
|
122
|
+
return h('div', {
|
|
123
|
+
ref: (el) => { state.headerRef.value = el; },
|
|
124
|
+
style: { display: 'flex', alignItems: 'center', width: '100%', minWidth: '0' },
|
|
125
|
+
}, [
|
|
126
|
+
// Column name with tooltip
|
|
127
|
+
h('div', { style: { flex: '1', minWidth: '0', overflow: 'hidden' } }, h(_VTooltip, { text: props.columnName, location: 'top' }, {
|
|
128
|
+
activator: ({ props: tipProps }) => h('span', {
|
|
129
|
+
...tipProps,
|
|
130
|
+
'data-header-label': '',
|
|
131
|
+
style: {
|
|
132
|
+
fontWeight: '600',
|
|
133
|
+
fontSize: '0.875rem',
|
|
134
|
+
lineHeight: '1.4',
|
|
135
|
+
whiteSpace: 'nowrap',
|
|
136
|
+
overflow: 'hidden',
|
|
137
|
+
textOverflow: 'ellipsis',
|
|
138
|
+
display: 'block',
|
|
139
|
+
},
|
|
140
|
+
}, props.columnName),
|
|
141
|
+
})),
|
|
142
|
+
// Sort + filter buttons
|
|
143
|
+
h('div', { style: { display: 'flex', alignItems: 'center', marginLeft: '4px', flexShrink: '0' } }, [
|
|
144
|
+
// Sort button
|
|
145
|
+
...(props.onSort ? [
|
|
146
|
+
h(_VBtn, {
|
|
147
|
+
icon: true,
|
|
148
|
+
size: 'x-small',
|
|
149
|
+
variant: 'text',
|
|
150
|
+
color: props.isSorted ? 'primary' : undefined,
|
|
151
|
+
'aria-label': `Sort by ${props.columnName}`,
|
|
152
|
+
title: props.isSorted ? (props.isSortedDescending ? 'Sorted descending' : 'Sorted ascending') : 'Sort',
|
|
153
|
+
onClick: state.handlers.handleSortClick,
|
|
154
|
+
}, () => h(_VIcon, { size: '16' }, () => props.isSorted
|
|
155
|
+
? (props.isSortedDescending ? 'mdi-arrow-down' : 'mdi-arrow-up')
|
|
156
|
+
: 'mdi-swap-vertical')),
|
|
157
|
+
] : []),
|
|
158
|
+
// Filter icon + menu
|
|
159
|
+
...(props.filterType !== 'none' ? [
|
|
160
|
+
h(_VMenu, {
|
|
161
|
+
modelValue: state.isFilterOpen.value,
|
|
162
|
+
'onUpdate:modelValue': (v) => state.setFilterOpen(v),
|
|
163
|
+
closeOnContentClick: false,
|
|
164
|
+
location: 'bottom start',
|
|
165
|
+
}, {
|
|
166
|
+
activator: ({ props: menuProps }) => h('div', { style: { position: 'relative' } }, [
|
|
167
|
+
h(_VBtn, {
|
|
168
|
+
...menuProps,
|
|
169
|
+
icon: true,
|
|
170
|
+
size: 'x-small',
|
|
171
|
+
variant: 'text',
|
|
172
|
+
color: state.hasActiveFilter.value || state.isFilterOpen.value ? 'primary' : undefined,
|
|
173
|
+
'aria-label': `Filter ${props.columnName}`,
|
|
174
|
+
title: `Filter ${props.columnName}`,
|
|
175
|
+
}, () => h(_VIcon, { size: '16' }, () => 'mdi-filter-variant')),
|
|
176
|
+
...(state.hasActiveFilter.value ? [
|
|
177
|
+
h('div', {
|
|
178
|
+
style: {
|
|
179
|
+
position: 'absolute',
|
|
180
|
+
top: '2px',
|
|
181
|
+
right: '2px',
|
|
182
|
+
width: '6px',
|
|
183
|
+
height: '6px',
|
|
184
|
+
borderRadius: '50%',
|
|
185
|
+
backgroundColor: 'rgb(var(--v-theme-primary))',
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
] : []),
|
|
189
|
+
]),
|
|
190
|
+
default: () => h('div', {
|
|
191
|
+
ref: (el) => { state.popoverRef.value = el; },
|
|
192
|
+
onClick: (e) => e.stopPropagation(),
|
|
193
|
+
}, [
|
|
194
|
+
h('div', {
|
|
195
|
+
style: {
|
|
196
|
+
borderBottom: '1px solid rgba(0,0,0,0.12)',
|
|
197
|
+
padding: '8px 12px',
|
|
198
|
+
fontWeight: '600',
|
|
199
|
+
fontSize: '0.875rem',
|
|
200
|
+
},
|
|
201
|
+
}, `Filter: ${props.columnName}`),
|
|
202
|
+
renderPopoverContent(),
|
|
203
|
+
]),
|
|
204
|
+
}),
|
|
205
|
+
] : []),
|
|
206
|
+
]),
|
|
207
|
+
]);
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue';
|
|
2
|
+
import { VBtn, VTextField, VCheckbox, VProgressCircular, VDivider } from 'vuetify/components';
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
+
const _VBtn = VBtn;
|
|
5
|
+
const _VTextField = VTextField;
|
|
6
|
+
const _VCheckbox = VCheckbox;
|
|
7
|
+
const _VProgressCircular = VProgressCircular;
|
|
8
|
+
const _VDivider = VDivider;
|
|
9
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
10
|
+
export const MultiSelectFilterPopover = defineComponent({
|
|
11
|
+
name: 'MultiSelectFilterPopover',
|
|
12
|
+
props: {
|
|
13
|
+
searchText: { type: String, required: true },
|
|
14
|
+
onSearchChange: { type: Function, required: true },
|
|
15
|
+
options: { type: Array, required: true },
|
|
16
|
+
filteredOptions: { type: Array, required: true },
|
|
17
|
+
selected: { type: Object, required: true },
|
|
18
|
+
onOptionToggle: { type: Function, required: true },
|
|
19
|
+
onSelectAll: { type: Function, required: true },
|
|
20
|
+
onClearSelection: { type: Function, required: true },
|
|
21
|
+
onApply: { type: Function, required: true },
|
|
22
|
+
isLoading: { type: Boolean, default: false },
|
|
23
|
+
},
|
|
24
|
+
setup(props) {
|
|
25
|
+
return () => h('div', { style: { width: '280px' } }, [
|
|
26
|
+
// Search
|
|
27
|
+
h('div', { style: { padding: '12px 12px 4px' } }, [
|
|
28
|
+
h(_VTextField, {
|
|
29
|
+
modelValue: props.searchText,
|
|
30
|
+
'onUpdate:modelValue': (v) => props.onSearchChange(v),
|
|
31
|
+
placeholder: 'Search...',
|
|
32
|
+
density: 'compact',
|
|
33
|
+
variant: 'outlined',
|
|
34
|
+
hideDetails: true,
|
|
35
|
+
autocomplete: 'off',
|
|
36
|
+
prependInnerIcon: 'mdi-magnify',
|
|
37
|
+
onKeydown: (e) => e.stopPropagation(),
|
|
38
|
+
}),
|
|
39
|
+
h('span', {
|
|
40
|
+
style: { display: 'block', marginTop: '4px', fontSize: '0.75rem', color: 'rgba(0,0,0,0.6)' },
|
|
41
|
+
}, `${props.filteredOptions.length} of ${props.options.length} options`),
|
|
42
|
+
]),
|
|
43
|
+
// Select all / clear
|
|
44
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', padding: '4px 12px' } }, [
|
|
45
|
+
h(_VBtn, { size: 'small', variant: 'text', onClick: props.onSelectAll }, () => `Select All (${props.filteredOptions.length})`),
|
|
46
|
+
h(_VBtn, { size: 'small', variant: 'text', onClick: props.onClearSelection }, () => 'Clear'),
|
|
47
|
+
]),
|
|
48
|
+
// Options list
|
|
49
|
+
h('div', { style: { maxHeight: '240px', overflowY: 'auto', padding: '0 4px' } }, props.isLoading
|
|
50
|
+
? h('div', { style: { display: 'flex', justifyContent: 'center', padding: '16px 0' } }, h(_VProgressCircular, { size: 24, indeterminate: true }))
|
|
51
|
+
: props.filteredOptions.length === 0
|
|
52
|
+
? h('div', { style: { padding: '16px 0', textAlign: 'center', fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, 'No options found')
|
|
53
|
+
: props.filteredOptions.map((option) => h('div', { key: option, style: { display: 'flex', alignItems: 'center', minHeight: '32px' } }, h(_VCheckbox, {
|
|
54
|
+
modelValue: props.selected.has(option),
|
|
55
|
+
label: option,
|
|
56
|
+
density: 'compact',
|
|
57
|
+
hideDetails: true,
|
|
58
|
+
'onUpdate:modelValue': (checked) => props.onOptionToggle(option, checked),
|
|
59
|
+
})))),
|
|
60
|
+
// Footer
|
|
61
|
+
h(_VDivider),
|
|
62
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', padding: '8px 12px' } }, [
|
|
63
|
+
h(_VBtn, { size: 'small', variant: 'text', onClick: props.onClearSelection }, () => 'Clear'),
|
|
64
|
+
h(_VBtn, { size: 'small', variant: 'flat', color: 'primary', onClick: props.onApply }, () => 'Apply'),
|
|
65
|
+
]),
|
|
66
|
+
]);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue';
|
|
2
|
+
import { VBtn, VTextField, VProgressCircular, VAvatar, VIcon, VDivider } from 'vuetify/components';
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
+
const _VBtn = VBtn;
|
|
5
|
+
const _VTextField = VTextField;
|
|
6
|
+
const _VProgressCircular = VProgressCircular;
|
|
7
|
+
const _VAvatar = VAvatar;
|
|
8
|
+
const _VIcon = VIcon;
|
|
9
|
+
const _VDivider = VDivider;
|
|
10
|
+
export const PeopleFilterPopover = defineComponent({
|
|
11
|
+
name: 'PeopleFilterPopover',
|
|
12
|
+
props: {
|
|
13
|
+
selectedUser: { type: Object, default: undefined },
|
|
14
|
+
searchText: { type: String, required: true },
|
|
15
|
+
onSearchChange: { type: Function, required: true },
|
|
16
|
+
suggestions: { type: Array, required: true },
|
|
17
|
+
isLoading: { type: Boolean, default: false },
|
|
18
|
+
onUserSelect: { type: Function, required: true },
|
|
19
|
+
onClearUser: { type: Function, required: true },
|
|
20
|
+
},
|
|
21
|
+
setup(props) {
|
|
22
|
+
return () => h('div', { style: { width: '300px' } }, [
|
|
23
|
+
// Selected user display
|
|
24
|
+
...(props.selectedUser ? [
|
|
25
|
+
h('div', { style: { padding: '12px 12px 8px', borderBottom: '1px solid rgba(0,0,0,0.12)' } }, [
|
|
26
|
+
h('span', { style: { fontSize: '0.75rem', color: 'rgba(0,0,0,0.6)' } }, 'Currently filtered by:'),
|
|
27
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '4px' } }, [
|
|
28
|
+
h(_VAvatar, { size: 32, image: props.selectedUser.photo }, () => props.selectedUser.displayName?.[0] ?? ''),
|
|
29
|
+
h('div', { style: { flex: '1', minWidth: '0' } }, [
|
|
30
|
+
h('div', { style: { fontSize: '0.875rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, props.selectedUser.displayName),
|
|
31
|
+
h('div', { style: { fontSize: '0.75rem', color: 'rgba(0,0,0,0.6)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, props.selectedUser.email),
|
|
32
|
+
]),
|
|
33
|
+
h(_VBtn, {
|
|
34
|
+
icon: true,
|
|
35
|
+
size: 'x-small',
|
|
36
|
+
variant: 'text',
|
|
37
|
+
'aria-label': 'Remove filter',
|
|
38
|
+
onClick: props.onClearUser,
|
|
39
|
+
}, () => h(_VIcon, { size: '16' }, () => 'mdi-close')),
|
|
40
|
+
]),
|
|
41
|
+
]),
|
|
42
|
+
] : []),
|
|
43
|
+
// Search input
|
|
44
|
+
h('div', { style: { padding: '12px 12px 4px' } }, h(_VTextField, {
|
|
45
|
+
modelValue: props.searchText,
|
|
46
|
+
'onUpdate:modelValue': (v) => props.onSearchChange(v),
|
|
47
|
+
placeholder: 'Search for a person...',
|
|
48
|
+
density: 'compact',
|
|
49
|
+
variant: 'outlined',
|
|
50
|
+
hideDetails: true,
|
|
51
|
+
autocomplete: 'off',
|
|
52
|
+
prependInnerIcon: 'mdi-magnify',
|
|
53
|
+
onKeydown: (e) => e.stopPropagation(),
|
|
54
|
+
})),
|
|
55
|
+
// Suggestions list
|
|
56
|
+
h('div', { style: { maxHeight: '240px', overflowY: 'auto' } }, props.isLoading && props.searchText.trim()
|
|
57
|
+
? h('div', { style: { display: 'flex', justifyContent: 'center', padding: '16px 0' } }, h(_VProgressCircular, { size: 24, indeterminate: true }))
|
|
58
|
+
: props.suggestions.length === 0 && props.searchText.trim()
|
|
59
|
+
? h('div', { style: { padding: '16px 0', textAlign: 'center', fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, 'No results found')
|
|
60
|
+
: props.searchText.trim()
|
|
61
|
+
? props.suggestions.map((user) => h('div', {
|
|
62
|
+
key: user.id || user.email || user.displayName,
|
|
63
|
+
style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '8px 12px', cursor: 'pointer' },
|
|
64
|
+
onClick: () => props.onUserSelect(user),
|
|
65
|
+
onMouseenter: (e) => { e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.04)'; },
|
|
66
|
+
onMouseleave: (e) => { e.currentTarget.style.backgroundColor = ''; },
|
|
67
|
+
}, [
|
|
68
|
+
h(_VAvatar, { size: 32, image: user.photo }, () => user.displayName?.[0] ?? ''),
|
|
69
|
+
h('div', { style: { flex: '1', minWidth: '0' } }, [
|
|
70
|
+
h('div', { style: { fontSize: '0.875rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, user.displayName),
|
|
71
|
+
h('div', { style: { fontSize: '0.75rem', color: 'rgba(0,0,0,0.6)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, user.email),
|
|
72
|
+
]),
|
|
73
|
+
]))
|
|
74
|
+
: h('div', { style: { padding: '16px 0', textAlign: 'center', fontSize: '0.875rem', color: 'rgba(0,0,0,0.6)' } }, 'Type to search...')),
|
|
75
|
+
// Clear filter button
|
|
76
|
+
...(props.selectedUser ? [
|
|
77
|
+
h(_VDivider),
|
|
78
|
+
h('div', { style: { padding: '8px 12px' } }, h(_VBtn, { size: 'small', variant: 'text', block: true, onClick: props.onClearUser }, () => 'Clear Filter')),
|
|
79
|
+
] : []),
|
|
80
|
+
]);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineComponent, h } from 'vue';
|
|
2
|
+
import { VBtn, VTextField } from 'vuetify/components';
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
4
|
+
const _VBtn = VBtn;
|
|
5
|
+
const _VTextField = VTextField;
|
|
6
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
7
|
+
export const TextFilterPopover = defineComponent({
|
|
8
|
+
name: 'TextFilterPopover',
|
|
9
|
+
props: {
|
|
10
|
+
value: { type: String, required: true },
|
|
11
|
+
onValueChange: { type: Function, required: true },
|
|
12
|
+
onApply: { type: Function, required: true },
|
|
13
|
+
onClear: { type: Function, required: true },
|
|
14
|
+
},
|
|
15
|
+
setup(props) {
|
|
16
|
+
return () => h('div', { style: { width: '260px' } }, [
|
|
17
|
+
h('div', { style: { padding: '12px' } }, h(_VTextField, {
|
|
18
|
+
modelValue: props.value,
|
|
19
|
+
'onUpdate:modelValue': (v) => props.onValueChange(v),
|
|
20
|
+
placeholder: 'Enter search term...',
|
|
21
|
+
density: 'compact',
|
|
22
|
+
variant: 'outlined',
|
|
23
|
+
hideDetails: true,
|
|
24
|
+
autocomplete: 'off',
|
|
25
|
+
prependInnerIcon: 'mdi-magnify',
|
|
26
|
+
onKeydown: (e) => {
|
|
27
|
+
e.stopPropagation();
|
|
28
|
+
if (e.key === 'Enter') {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
props.onApply();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
})),
|
|
34
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', padding: '0 12px 12px' } }, [
|
|
35
|
+
h(_VBtn, { size: 'small', variant: 'text', disabled: !props.value, onClick: props.onClear }, () => 'Clear'),
|
|
36
|
+
h(_VBtn, { size: 'small', variant: 'flat', color: 'primary', onClick: props.onApply }, () => 'Apply'),
|
|
37
|
+
]),
|
|
38
|
+
]);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ColumnHeaderFilter } from './ColumnHeaderFilter';
|