PluginProbe
The Innovative Form Builder – IvyForms / 0.2
The Innovative Form Builder – IvyForms v0.2
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / _components / table / IvyTable.vue

IvyTable.vue in The Innovative Form Builder – IvyForms 0.2, at frontend/src/views/_components/table/IvyTable.vue

302 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div class="ivyforms-table__container" style="position: relative">
3 <!-- Slot for custom header (when row is selected) -->
4 <div v-if="isCustomHeader" class="ivyforms-table__custom-header ivyforms-pt-2">
5 <slot name="header" />
6 </div>
7 <ElAutoResizer :class="{ loading: props.loading }">
8 <template #default="{ width, height }">
9 <ElTableV2
10 ref="tableV2Ref"
11 v-bind="$attrs"
12 :columns="formattedColumns"
13 :data="props.data"
14 :width="Math.max(width, props.minTableWidth)"
15 :height="computedTableHeight(height)"
16 class="ivyforms-table"
17 :fixed="props.fixed"
18 :row-class="getRowClass"
19 :h-scrollbar-size="20"
20 :v-scrollbar-size="10"
21 >
22 <!-- Slot for custom column headers with sorting -->
23 <template #header-cell="{ column }">
24 <div
25 :style="{
26 display: 'flex',
27 alignItems: 'center',
28 cursor: column.sortable ? 'pointer' : 'default',
29 }"
30 @click="emitSortChange(column)"
31 >
32 <component :is="column.headerRenderer()" v-if="column.headerRenderer" />
33
34 <div
35 v-else-if="column.comingSoon"
36 style="display: flex; align-items: center; gap: 8px"
37 >
38 <div style="opacity: 0.5; cursor: not-allowed">{{ column.title }}</div>
39 <ComingSoonBadge size="s" />
40 </div>
41 <span v-else>{{ column.title }}</span>
42
43 <!-- Sorting icons -->
44 <span v-if="column.sortable" style="margin-left: 5px">
45 <span v-if="sortColumn === column.key">
46 <span v-if="sortOrder === 'asc'">
47 <IvyIcon
48 size="d"
49 outer-size="20px"
50 :name="isDarkMode ? 'sort-asc-dark' : 'sort-asc'"
51 category="table"
52 />
53 </span>
54 <span v-else-if="sortOrder === 'desc'">
55 <IvyIcon
56 size="d"
57 outer-size="20px"
58 :name="isDarkMode ? 'sort-desc-dark' : 'sort-desc'"
59 category="table"
60 />
61 </span>
62 </span>
63 <span v-else>
64 <IvyIcon
65 size="d"
66 outer-size="20px"
67 name="arrow-up-down-2"
68 category="arrows"
69 type="outline"
70 color="var(--map-base-dusk-symbol-2)"
71 />
72 </span>
73 </span>
74 </div>
75 </template>
76
77 <!-- Slot for empty table/no filters match -->
78 <template #empty>
79 <slot name="empty" />
80 </template>
81 </ElTableV2>
82
83 <!-- Pagination -->
84 <slot v-if="pagination && data.length" name="pagination">
85 <IvyPagination :total="props.data.length" class="ivyforms-table__pagination" />
86 </slot>
87 </template>
88 </ElAutoResizer>
89 </div>
90 </template>
91
92 <script lang="ts" setup>
93 import { computed, ref, h, onUnmounted } from 'vue'
94 import type { TableColumn } from '@/views/_components/table/IvyTableColumn.ts'
95 import type { CellRenderProps } from '@/views/_components/table/IvyCellRendererProps.ts'
96
97 interface Props {
98 data: Array<{ [key: string]: string | number | boolean }>
99 columns?: TableColumn[]
100 loading?: boolean
101 pagination?: boolean
102 groupBy?: string
103 fixed?: boolean
104 isCustomHeader?: boolean
105 minTableWidth?: number
106 }
107
108 const props = withDefaults(defineProps<Props>(), {
109 loading: false,
110 pagination: false,
111 isCustomHeader: false,
112 columns: () => [],
113 groupBy: '',
114 minTableWidth: 600,
115 })
116
117 const emit = defineEmits<{
118 (e: 'sort-change', sortEvent: { column: string | null; order: 'asc' | 'desc' | null }): void
119 }>()
120
121 // Sort icons based on theme
122 const isDarkMode = ref(document.body.classList.contains('ivyforms-theme-dark'))
123
124 const observer = new MutationObserver(() => {
125 isDarkMode.value = document.body.classList.contains('ivyforms-theme-dark')
126 })
127
128 observer.observe(document.body, { attributes: true, attributeFilter: ['class'] })
129
130 onUnmounted(() => {
131 observer.disconnect()
132 })
133 // Adjust the height and width
134 const computedTableHeight = (parentHeight: number) => {
135 const rowHeight = 50
136 const numRows = props.data.length + 1
137 const maxTableHeight = numRows * rowHeight
138
139 // Set a minimum height for the table when there is no data - if there is only one row, it should be at least 550px
140 const minHeight = numRows > 1 ? 0 : 550
141 return Math.max(minHeight, Math.min(maxTableHeight, parentHeight))
142 }
143
144 // Format columns to use custom components inside headers and cells
145 const formattedColumns = computed(
146 () =>
147 props.columns
148 ?.filter((col) => col.visible)
149 .map((col) => ({
150 ...col,
151 sortable: col.sortable || false,
152 headerRenderer: col.headerRenderer || undefined,
153 cellRenderer:
154 col.cellRenderer || ((props: CellRenderProps) => h('span', {}, props.rowData[col.key])),
155 minWidth:
156 typeof col.minWidth === 'string' ? parseInt(col.minWidth, 10) || 100 : col.minWidth, // Ensure minWidth is a number
157 width: typeof col.width === 'string' ? parseInt(col.width, 10) || 100 : col.width, // Ensure width is a number
158 })) || [],
159 )
160
161 // Sorting state (only for UI purposes)
162 const sortColumn = ref<string | null>(null)
163 const sortOrder = ref<'asc' | 'desc' | null>(null)
164
165 // Emit sorting changes
166 const emitSortChange = (column) => {
167 if (!column.sortable) return
168
169 if (sortColumn.value === column.key) {
170 // Toggle order
171 sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
172 } else {
173 // New column selected, set ascending order
174 sortColumn.value = column.key
175 sortOrder.value = 'asc'
176 }
177
178 // Emit the sorting change event
179 const sortEvent = {
180 column: sortColumn.value,
181 order: sortOrder.value,
182 }
183 emit('sort-change', sortEvent)
184 }
185
186 // Custom row classes
187 const getRowClass = ({ rowData }: { rowData: Record<string, unknown> }) => {
188 const classes: string[] = []
189 if (rowData.status === 'unread') classes.push('ivyforms-unread-entry')
190 if (rowData.selected) classes.push('is-selected')
191 return classes.join(' ')
192 }
193 </script>
194
195 <style lang="scss">
196 .ivyforms-table__container {
197 display: flex;
198 flex-direction: column;
199 position: relative;
200
201 .ivyforms-table__custom-header {
202 position: absolute;
203 top: 0;
204 left: 0;
205 right: 0;
206 z-index: 10;
207 background: var(--map-ground-level-1-foreground);
208 border-bottom: 1px solid var(--map-divider);
209 }
210
211 .el-auto-resizer {
212 &.loading {
213 width: 100%;
214 height: 100%;
215 min-width: 0;
216 min-height: 0;
217 overflow: hidden;
218 }
219 }
220
221 .ivyforms-table {
222 // Header
223 .el-table-v2__header {
224 border-bottom: 1px solid var(--map-divider);
225 background: var(--map-ground-level-1-foreground);
226 transition: none;
227
228 // Columns
229 .el-table-v2__header-cell {
230 display: flex;
231 background: var(--map-ground-level-1-foreground);
232 color: var(--map-base-text-0);
233 text-align: center;
234 font-size: 14px;
235 font-style: normal;
236 font-weight: 400;
237 line-height: 20px;
238
239 // Sortable
240 &.is-sortable {
241 span {
242 margin-left: 0 !important;
243 }
244 .el-table-v2__sort-icon {
245 svg {
246 display: none;
247 }
248 }
249 }
250 }
251 }
252
253 // Body
254 .el-table-v2__body {
255 background: var(--map-ground-level-1-foreground);
256 transition: none;
257
258 // Rows
259 .el-table-v2__row {
260 display: flex;
261 height: 48px;
262 align-items: center;
263 flex-shrink: 0;
264 border-bottom: 1px solid var(--map-divider);
265 background: var(--map-ground-level-1-foreground);
266 color: var(--map-base-dusk-symbol-2);
267 transition: none;
268
269 &:hover,
270 &.hovered {
271 background: var(--map-hover);
272 }
273
274 // Selected
275 &.is-selected {
276 background: linear-gradient(
277 0deg,
278 var(--map-accent-amber-o05) 0%,
279 var(--map-accent-amber-o05) 100%
280 ),
281 var(--map-ground-level-1-foreground);
282 }
283 }
284 }
285 }
286 }
287
288 .ivyforms-table__pagination {
289 margin-top: 8px;
290 }
291
292 .el-skeleton {
293 &.ivyforms-skeleton {
294 margin: 1px;
295
296 .ivyforms-skeleton-item {
297 height: 48px;
298 }
299 }
300 }
301 </style>
302