@lucashw68/nsdb 1.0.0-rc.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/CHANGELOG.md +34 -0
- package/GET_STARTED.md +709 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/cli/index.js +83 -0
- package/helpers/args.js +22 -0
- package/helpers/config.js +142 -0
- package/helpers/generated.js +48 -0
- package/helpers/io.js +39 -0
- package/helpers/metadata.js +19 -0
- package/helpers/names.js +16 -0
- package/helpers/relations.js +101 -0
- package/helpers/shell.js +15 -0
- package/helpers/tables.js +79 -0
- package/helpers/ts.js +37 -0
- package/module.ts +151 -0
- package/nsdb.config.example.mjs +39 -0
- package/nsdb.config.example.ts +42 -0
- package/package.json +114 -0
- package/runtime/components/Form/NsdbRelationSelect.vue +258 -0
- package/runtime/components/NsdbForm.vue +865 -0
- package/runtime/components/NsdbList.vue +961 -0
- package/runtime/composables/useNsdbProfile.ts +119 -0
- package/runtime/composables/useNsdbSchemas.ts +176 -0
- package/runtime/composables/useSupabaseApi.ts +177 -0
- package/runtime/composables/useSupabaseApiStorage.ts +337 -0
- package/runtime/composables/useSupabaseModels.ts +412 -0
- package/runtime/query.ts +126 -0
- package/runtime/stores/createDbStore.ts +439 -0
- package/runtime/stores/createSingletonDbStore.ts +67 -0
- package/runtime/utils/dataFreshness.ts +47 -0
- package/runtime/utils/storage.ts +41 -0
- package/scripts/clear.js +64 -0
- package/scripts/generate-composables.js +100 -0
- package/scripts/generate-enums.js +106 -0
- package/scripts/generate-metadata.js +165 -0
- package/scripts/generate-models.js +164 -0
- package/scripts/generate-schemas.js +443 -0
- package/scripts/generate-stores.js +90 -0
- package/scripts/generate-types.js +196 -0
- package/scripts/init.js +225 -0
- package/templates/model.template.ts +48 -0
- package/templates/schema.template.ts +13 -0
- package/templates/useNsdbModel.template.ts +9 -0
- package/types/config.ts +50 -0
- package/types/entities.ts +66 -0
- package/types/index.ts +14 -0
- package/types/list.ts +78 -0
- package/types/model.ts +57 -0
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, computed, onBeforeUnmount, onMounted, useId } from 'vue'
|
|
3
|
+
import { useSupabaseUser } from '#imports'
|
|
4
|
+
import { useNsdbModel } from '#build/nsdb/registry'
|
|
5
|
+
import * as nsdbSchemas from '#build/nsdb/schemas'
|
|
6
|
+
import type { Column, NsdbTableClasses, OrderDirection, SortState, WhereClause } from '@lucashw68/nsdb/types/list'
|
|
7
|
+
|
|
8
|
+
const defaultClasses: NsdbTableClasses = {
|
|
9
|
+
wrapper: 'w-full space-y-3',
|
|
10
|
+
headerWrapper: 'flex items-center justify-between',
|
|
11
|
+
headerTitle: 'text-lg font-semibold capitalize mb-2',
|
|
12
|
+
headerSubtitle: 'text-sm opacity-70',
|
|
13
|
+
toolbar: 'flex flex-col md:flex-row md:items-center gap-2',
|
|
14
|
+
searchInput: 'border text-black rounded px-3 py-2 w-full md:max-w-xs text-sm',
|
|
15
|
+
error: 'text-sm text-red-600',
|
|
16
|
+
tableContainer: 'border w-full overflow-x-auto',
|
|
17
|
+
table: 'nsdb-table w-full text-sm',
|
|
18
|
+
thead: 'bg-gray-50',
|
|
19
|
+
theadRow: 'border-2 border-white',
|
|
20
|
+
th: 'text-left text-black px-4 py-2 font-bold hover:cursor-pointer hover:text-purple-500 hover:bg-gray-200 hover:rounded-lg',
|
|
21
|
+
actionsTh: 'text-black',
|
|
22
|
+
loadingCell: 'px-4 py-6 text-center',
|
|
23
|
+
emptyCell: 'px-4 py-6 text-center',
|
|
24
|
+
bodyRow: 'border-t hover:bg-gray-400 hover:cursor-pointer',
|
|
25
|
+
td: 'py-2 px-4 text-center border-2 border-white',
|
|
26
|
+
actionsTd: 'flex items-center gap-2 py-2 px-4 justify-center',
|
|
27
|
+
deleteButton: 'flex items-center rounded-full hover:bg-gray-600 px-2 py-1',
|
|
28
|
+
footer: 'w-full flex flex-col md:flex-row md:justify-between md:items-center gap-2 mt-2',
|
|
29
|
+
pagination: 'flex flex-wrap items-center gap-2',
|
|
30
|
+
pageButton: 'px-3 py-1 rounded border text-sm',
|
|
31
|
+
pageButtonActive: 'font-bold',
|
|
32
|
+
pageButtonDisabled: 'opacity-40 cursor-not-allowed',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const props = defineProps<{
|
|
36
|
+
model: string
|
|
37
|
+
columns?: Column[]
|
|
38
|
+
pageSize?: number
|
|
39
|
+
query?: {
|
|
40
|
+
select?: string
|
|
41
|
+
where?: WhereClause
|
|
42
|
+
orderBy?: string
|
|
43
|
+
orderDirection?: OrderDirection
|
|
44
|
+
orderForeignTable?: string
|
|
45
|
+
limit?: number
|
|
46
|
+
offset?: number
|
|
47
|
+
search?: string
|
|
48
|
+
searchColumns?: string[]
|
|
49
|
+
}
|
|
50
|
+
filters?: WhereClause
|
|
51
|
+
sortBy?: string
|
|
52
|
+
sortDirection?: OrderDirection
|
|
53
|
+
classes?: Partial<NsdbTableClasses>
|
|
54
|
+
unstyled?: boolean
|
|
55
|
+
variant?: 'table' | 'cards'
|
|
56
|
+
pageWindow?: number
|
|
57
|
+
showFirstLast?: boolean
|
|
58
|
+
showPageNumbers?: boolean
|
|
59
|
+
searchable?: boolean
|
|
60
|
+
search?: string
|
|
61
|
+
searchColumns?: string[]
|
|
62
|
+
searchPlaceholder?: string
|
|
63
|
+
searchDebounceMs?: number
|
|
64
|
+
store?: boolean
|
|
65
|
+
}>()
|
|
66
|
+
|
|
67
|
+
const loading = ref(false)
|
|
68
|
+
const error = ref<string | null>(null)
|
|
69
|
+
|
|
70
|
+
type NsdbListModel = {
|
|
71
|
+
primaryKey?: string
|
|
72
|
+
items: { value?: Array<Record<string, any>> }
|
|
73
|
+
totalCount?: { value?: number | null }
|
|
74
|
+
fetch: (query?: Record<string, any>) => Promise<Array<Record<string, any>>>
|
|
75
|
+
remove?: (id: string | number) => Promise<void> | void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nsdbModel = computed(() =>
|
|
79
|
+
useNsdbModel(props.model, { store: props.store ?? false }) as unknown as NsdbListModel
|
|
80
|
+
)
|
|
81
|
+
const supabaseUser = useSupabaseUser()
|
|
82
|
+
const rows = computed(() => nsdbModel.value.items.value ?? [])
|
|
83
|
+
const totalCount = computed<number | null>(() => {
|
|
84
|
+
return (nsdbModel.value as any)?.totalCount?.value ?? null
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const currentPage = ref(1)
|
|
88
|
+
|
|
89
|
+
const pageSize = computed<number | undefined>(() => {
|
|
90
|
+
if (props.pageSize != null) return props.pageSize
|
|
91
|
+
return undefined
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const effectiveLimit = computed<number>(() => {
|
|
95
|
+
const baseQuery = props.query ?? {}
|
|
96
|
+
return pageSize.value ?? baseQuery.limit ?? 100
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const effectiveOffset = computed<number>(() => {
|
|
100
|
+
const baseQuery = props.query ?? {}
|
|
101
|
+
return baseQuery.offset ?? (currentPage.value - 1) * effectiveLimit.value
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const totalPages = computed<number | null>(() => {
|
|
105
|
+
if (!pageSize.value) return null
|
|
106
|
+
if (totalCount.value == null) return null
|
|
107
|
+
return Math.max(1, Math.ceil(totalCount.value / pageSize.value))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const classes = computed<NsdbTableClasses>(() => {
|
|
111
|
+
if (props.unstyled) {
|
|
112
|
+
return Object.keys(defaultClasses).reduce((acc, key) => {
|
|
113
|
+
acc[key as keyof NsdbTableClasses] = ''
|
|
114
|
+
return acc
|
|
115
|
+
}, {} as NsdbTableClasses)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
...defaultClasses,
|
|
120
|
+
...(props.classes ?? {}),
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const canGoPrev = computed(() => currentPage.value > 1)
|
|
125
|
+
|
|
126
|
+
const canGoNext = computed(() => {
|
|
127
|
+
if (totalPages.value != null) return currentPage.value < totalPages.value
|
|
128
|
+
if (!pageSize.value) return rows.value.length > 0
|
|
129
|
+
return rows.value.length === pageSize.value
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const pageWindow = computed(() => props.pageWindow ?? 2)
|
|
133
|
+
const showFirstLast = computed(() => props.showFirstLast ?? true)
|
|
134
|
+
const showPageNumbers = computed(() => props.showPageNumbers ?? true)
|
|
135
|
+
const searchTerm = ref(props.search ?? '')
|
|
136
|
+
const debouncedSearchTerm = ref(props.search ?? '')
|
|
137
|
+
let searchDebounceTimeout: ReturnType<typeof setTimeout> | null = null
|
|
138
|
+
let loadSequence = 0
|
|
139
|
+
let isMounted = false
|
|
140
|
+
const deletingRows = ref(new Set<string | number>())
|
|
141
|
+
const searchInputId = `nsdb-list-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}-search`
|
|
142
|
+
|
|
143
|
+
const pageItems = computed<(number | '...')[]>(() => {
|
|
144
|
+
if (!showPageNumbers.value) return []
|
|
145
|
+
if (totalPages.value == null) return []
|
|
146
|
+
|
|
147
|
+
const tp = totalPages.value
|
|
148
|
+
const cp = currentPage.value
|
|
149
|
+
const w = Math.max(0, pageWindow.value)
|
|
150
|
+
|
|
151
|
+
if (tp <= 1) return [1]
|
|
152
|
+
if (tp <= 2 + 2 * w + 2) {
|
|
153
|
+
return Array.from({ length: tp }, (_, i) => i + 1)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const items: (number | '...')[] = []
|
|
157
|
+
const start = Math.max(2, cp - w)
|
|
158
|
+
const end = Math.min(tp - 1, cp + w)
|
|
159
|
+
|
|
160
|
+
items.push(1)
|
|
161
|
+
|
|
162
|
+
if (start > 2) items.push('...')
|
|
163
|
+
for (let p = start; p <= end; p++) items.push(p)
|
|
164
|
+
if (end < tp - 1) items.push('...')
|
|
165
|
+
|
|
166
|
+
items.push(tp)
|
|
167
|
+
return items
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const effectiveColumns = computed<Column[]>(() => {
|
|
171
|
+
if (props.columns && props.columns.length > 0) {
|
|
172
|
+
props.columns.forEach(column => {
|
|
173
|
+
if (typeof column.key !== 'string') {
|
|
174
|
+
console.warn('[NsdbList] column.key should be a string, got:', column.key)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
return props.columns
|
|
178
|
+
}
|
|
179
|
+
const schema = modelSchema.value
|
|
180
|
+
if (schema) {
|
|
181
|
+
return Object.entries(schema)
|
|
182
|
+
.filter(([, field]) => field?.selectable !== false && !field?.hidden && !field?.serverOnly)
|
|
183
|
+
.map(([key, field]) => ({ key, label: field?.label ?? key }))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const first = rows.value[0]
|
|
187
|
+
if (!first) return []
|
|
188
|
+
return Object.keys(first).map(key => ({ key, label: key }))
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
function toPascalCase(value: string) {
|
|
192
|
+
return value
|
|
193
|
+
.split(/[^a-zA-Z0-9]/)
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
196
|
+
.join('')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const modelSchema = computed<Record<string, any> | null>(() => {
|
|
200
|
+
const schemaExportName = `${toPascalCase(props.model)}Schema`
|
|
201
|
+
return (nsdbSchemas as Record<string, any>)[schemaExportName] ?? null
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
function isTextSearchColumn(columnKey: string, warn = false) {
|
|
205
|
+
const schema = modelSchema.value
|
|
206
|
+
if (!schema) return true
|
|
207
|
+
if (columnKey.includes('.')) return true
|
|
208
|
+
|
|
209
|
+
const field = schema[columnKey]
|
|
210
|
+
if (!field) return true
|
|
211
|
+
|
|
212
|
+
const isTextField = field.type === 'text' || field.type === 'textarea'
|
|
213
|
+
if (!isTextField && warn) {
|
|
214
|
+
console.warn(
|
|
215
|
+
`[NsdbList] "${columnKey}" ignored from searchColumns on "${props.model}" because its schema type is "${field.type}". Use filters for non-text columns.`
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return isTextField
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isSortableColumn(columnKey: string) {
|
|
223
|
+
if (columnKey.includes('.')) {
|
|
224
|
+
console.warn(
|
|
225
|
+
`[NsdbList] "${columnKey}" cannot be sorted automatically because it is a relation path. Configure query.orderBy/orderForeignTable manually if needed.`
|
|
226
|
+
)
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const effectiveSearchColumns = computed(() => {
|
|
234
|
+
if (props.searchColumns?.length) {
|
|
235
|
+
return props.searchColumns.filter(column => isTextSearchColumn(column, true))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (props.query?.searchColumns?.length) {
|
|
239
|
+
return props.query.searchColumns.filter(column => isTextSearchColumn(column, true))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return effectiveColumns.value
|
|
243
|
+
.map(column => column.key)
|
|
244
|
+
.filter(columnKey => !columnKey.includes('.'))
|
|
245
|
+
.filter(column => isTextSearchColumn(column))
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const effectiveSearch = computed(() => {
|
|
249
|
+
const localSearch = debouncedSearchTerm.value.trim()
|
|
250
|
+
if (localSearch) return localSearch
|
|
251
|
+
return props.query?.search ?? ''
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
function setSearchTerm(value: string) {
|
|
255
|
+
searchTerm.value = value
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const sortState = ref<SortState>({
|
|
259
|
+
key: props.sortBy ?? null,
|
|
260
|
+
direction: props.sortBy ? props.sortDirection ?? 'asc' : null,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
async function setSort(key: string | null, direction: OrderDirection | null = 'asc') {
|
|
264
|
+
if (key && !isSortableColumn(key)) return
|
|
265
|
+
|
|
266
|
+
sortState.value = { key, direction: key ? direction : null }
|
|
267
|
+
currentPage.value = 1
|
|
268
|
+
await load()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function sortAriaValue(column: Column) {
|
|
272
|
+
if (sortState.value.key !== column.key || !sortState.value.direction) return 'none'
|
|
273
|
+
return sortState.value.direction === 'asc' ? 'ascending' : 'descending'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function toggleSort(column: Column) {
|
|
277
|
+
if (sortState.value.key !== column.key) {
|
|
278
|
+
await setSort(column.key, 'asc')
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (sortState.value.direction === 'asc') {
|
|
283
|
+
await setSort(column.key, 'desc')
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await setSort(null, null)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getDeep(row: any, path: unknown) {
|
|
291
|
+
if (!row || path == null) return null
|
|
292
|
+
|
|
293
|
+
if (Array.isArray(path)) {
|
|
294
|
+
let current: any = row
|
|
295
|
+
for (const part of path) {
|
|
296
|
+
if (current == null) return null
|
|
297
|
+
current = current[part as keyof typeof current]
|
|
298
|
+
}
|
|
299
|
+
return current
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (typeof path !== 'string') {
|
|
303
|
+
console.warn('[NsdbList.getDeep] path is not a string:', path, 'typeof =', typeof path)
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const segments = path.split('.')
|
|
308
|
+
let current: any = row
|
|
309
|
+
for (const segment of segments) {
|
|
310
|
+
if (current == null) return null
|
|
311
|
+
current = current[segment as keyof typeof current]
|
|
312
|
+
}
|
|
313
|
+
return current
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const effectiveWhere = computed<WhereClause | undefined>(() => {
|
|
317
|
+
const mergedWhere = {
|
|
318
|
+
...(props.query?.where ?? {}),
|
|
319
|
+
...(props.filters ?? {}),
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return Object.keys(mergedWhere).length > 0 ? mergedWhere : undefined
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
const serverQuery = computed(() => {
|
|
326
|
+
const baseQuery = props.query ?? {}
|
|
327
|
+
|
|
328
|
+
// Requête unique consommée par le modèle: recherche, filtres, tri et pagination restent côté Supabase.
|
|
329
|
+
return {
|
|
330
|
+
...baseQuery,
|
|
331
|
+
where: effectiveWhere.value,
|
|
332
|
+
orderBy: sortState.value.key ?? props.sortBy ?? baseQuery.orderBy,
|
|
333
|
+
orderDirection: sortState.value.direction ?? props.sortDirection ?? baseQuery.orderDirection,
|
|
334
|
+
limit: effectiveLimit.value,
|
|
335
|
+
offset: effectiveOffset.value,
|
|
336
|
+
search: effectiveSearch.value || undefined,
|
|
337
|
+
searchColumns: effectiveSearchColumns.value,
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const displayRows = computed(() => rows.value)
|
|
342
|
+
const hasRows = computed(() => displayRows.value.length > 0)
|
|
343
|
+
const isEmpty = computed(() => !loading.value && !error.value && !hasRows.value)
|
|
344
|
+
const emptyMessage = computed(() => effectiveSearch.value ? 'Aucun résultat pour cette recherche' : 'Aucune donnée')
|
|
345
|
+
const paginationSlotProps = computed(() => ({
|
|
346
|
+
currentPage: currentPage.value,
|
|
347
|
+
pageSize: effectiveLimit.value,
|
|
348
|
+
limit: effectiveLimit.value,
|
|
349
|
+
offset: effectiveOffset.value,
|
|
350
|
+
totalCount: totalCount.value,
|
|
351
|
+
totalPages: totalPages.value,
|
|
352
|
+
canGoPrev: canGoPrev.value,
|
|
353
|
+
canGoNext: canGoNext.value,
|
|
354
|
+
pageItems: pageItems.value,
|
|
355
|
+
showFirstLast: showFirstLast.value,
|
|
356
|
+
showPageNumbers: showPageNumbers.value,
|
|
357
|
+
goToPage,
|
|
358
|
+
firstPage: handleFirstPage,
|
|
359
|
+
lastPage: handleLastPage,
|
|
360
|
+
prevPage: handlePrevPage,
|
|
361
|
+
nextPage: handleNextPage,
|
|
362
|
+
}))
|
|
363
|
+
|
|
364
|
+
async function load() {
|
|
365
|
+
const requestId = ++loadSequence
|
|
366
|
+
loading.value = true
|
|
367
|
+
error.value = null
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
if (typeof (nsdbModel.value as any).fetch === 'function') {
|
|
371
|
+
await (nsdbModel.value as any).fetch(serverQuery.value)
|
|
372
|
+
} else {
|
|
373
|
+
console.warn('[NsdbList] No fetch() found on nsdbModel for', props.model)
|
|
374
|
+
}
|
|
375
|
+
} catch (e: any) {
|
|
376
|
+
if (requestId !== loadSequence) return
|
|
377
|
+
error.value = e?.message ?? 'Erreur de chargement'
|
|
378
|
+
} finally {
|
|
379
|
+
if (requestId === loadSequence) loading.value = false
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
defineExpose({ refresh: load })
|
|
384
|
+
|
|
385
|
+
async function goToPage(page: number) {
|
|
386
|
+
const tp = totalPages.value
|
|
387
|
+
let target = page
|
|
388
|
+
|
|
389
|
+
if (target < 1) target = 1
|
|
390
|
+
if (tp != null && target > tp) target = tp
|
|
391
|
+
|
|
392
|
+
if (target === currentPage.value) return
|
|
393
|
+
currentPage.value = target
|
|
394
|
+
await load()
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function handlePrevPage() {
|
|
398
|
+
if (!canGoPrev.value) return
|
|
399
|
+
await goToPage(currentPage.value - 1)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function handleNextPage() {
|
|
403
|
+
if (!canGoNext.value) return
|
|
404
|
+
await goToPage(currentPage.value + 1)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function handleFirstPage() {
|
|
408
|
+
await goToPage(1)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function handleLastPage() {
|
|
412
|
+
if (totalPages.value == null) return
|
|
413
|
+
await goToPage(totalPages.value)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
watch(
|
|
417
|
+
() => [
|
|
418
|
+
props.model,
|
|
419
|
+
props.pageSize,
|
|
420
|
+
props.query,
|
|
421
|
+
props.filters,
|
|
422
|
+
props.searchColumns,
|
|
423
|
+
],
|
|
424
|
+
() => {
|
|
425
|
+
currentPage.value = 1
|
|
426
|
+
if (isMounted) void load()
|
|
427
|
+
},
|
|
428
|
+
{ deep: true }
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
watch(
|
|
432
|
+
() => [props.sortBy, props.sortDirection] as const,
|
|
433
|
+
([sortBy, sortDirection]) => {
|
|
434
|
+
sortState.value = {
|
|
435
|
+
key: sortBy ?? null,
|
|
436
|
+
direction: sortBy ? sortDirection ?? 'asc' : null,
|
|
437
|
+
}
|
|
438
|
+
currentPage.value = 1
|
|
439
|
+
if (isMounted) void load()
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
watch(
|
|
444
|
+
() => props.search,
|
|
445
|
+
(searchValue) => {
|
|
446
|
+
searchTerm.value = searchValue ?? ''
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
watch(
|
|
451
|
+
() => supabaseUser.value?.id ?? null,
|
|
452
|
+
() => {
|
|
453
|
+
// Quarantine rendered rows synchronously across logout/account switches.
|
|
454
|
+
const modelItems = nsdbModel.value.items
|
|
455
|
+
if (modelItems && 'value' in modelItems) modelItems.value = []
|
|
456
|
+
currentPage.value = 1
|
|
457
|
+
if (isMounted) void load()
|
|
458
|
+
},
|
|
459
|
+
{ flush: 'sync' },
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
watch(
|
|
463
|
+
searchTerm,
|
|
464
|
+
(searchValue) => {
|
|
465
|
+
if (searchDebounceTimeout) clearTimeout(searchDebounceTimeout)
|
|
466
|
+
|
|
467
|
+
searchDebounceTimeout = setTimeout(() => {
|
|
468
|
+
debouncedSearchTerm.value = searchValue
|
|
469
|
+
currentPage.value = 1
|
|
470
|
+
if (isMounted) void load()
|
|
471
|
+
}, props.searchDebounceMs ?? 300)
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
onMounted(() => {
|
|
476
|
+
isMounted = true
|
|
477
|
+
void load()
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
onBeforeUnmount(() => {
|
|
481
|
+
isMounted = false
|
|
482
|
+
if (searchDebounceTimeout) clearTimeout(searchDebounceTimeout)
|
|
483
|
+
loadSequence++
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
async function handleDelete(row: any) {
|
|
487
|
+
const id = row?.[nsdbModel.value.primaryKey ?? 'id']
|
|
488
|
+
if (id == null) return
|
|
489
|
+
if (deletingRows.value.has(id)) return
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
deletingRows.value.add(id)
|
|
493
|
+
if (typeof (nsdbModel.value as any).remove === 'function') {
|
|
494
|
+
await (nsdbModel.value as any).remove(id)
|
|
495
|
+
} else {
|
|
496
|
+
console.warn('[NsdbList] No remove() method found on nsdbModel for', props.model)
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (totalPages.value != null && currentPage.value > totalPages.value) {
|
|
501
|
+
currentPage.value = totalPages.value
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
await load()
|
|
505
|
+
} catch (e: any) {
|
|
506
|
+
console.error('[NsdbList] Error while deleting row:', e)
|
|
507
|
+
error.value = e?.message ?? 'Erreur de suppression'
|
|
508
|
+
} finally {
|
|
509
|
+
deletingRows.value.delete(id)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function displayValue(value: unknown) {
|
|
514
|
+
if (value == null || value === '') return 'Inconnu'
|
|
515
|
+
if (Array.isArray(value)) return value.length === 0 ? '—' : `${value.length} élément${value.length > 1 ? 's' : ''}`
|
|
516
|
+
if (typeof value === 'object') {
|
|
517
|
+
const record = value as Record<string, unknown>
|
|
518
|
+
for (const key of ['label', 'name', 'title']) {
|
|
519
|
+
if (record[key] != null) return String(record[key])
|
|
520
|
+
}
|
|
521
|
+
return '1 élément'
|
|
522
|
+
}
|
|
523
|
+
if (typeof value === 'boolean') return value ? 'Oui' : 'Non'
|
|
524
|
+
return String(value)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function rowKey(row: Record<string, any>) {
|
|
528
|
+
return row?.[nsdbModel.value.primaryKey ?? 'id'] ?? JSON.stringify(row)
|
|
529
|
+
}
|
|
530
|
+
</script>
|
|
531
|
+
|
|
532
|
+
<template>
|
|
533
|
+
<slot
|
|
534
|
+
:model="props.model"
|
|
535
|
+
:rows="displayRows"
|
|
536
|
+
:raw-rows="rows"
|
|
537
|
+
:has-rows="hasRows"
|
|
538
|
+
:is-empty="isEmpty"
|
|
539
|
+
:columns="effectiveColumns"
|
|
540
|
+
:loading="loading"
|
|
541
|
+
:error="error"
|
|
542
|
+
:query="serverQuery"
|
|
543
|
+
:filters="effectiveWhere"
|
|
544
|
+
:sort-state="sortState"
|
|
545
|
+
:set-sort="setSort"
|
|
546
|
+
:current-page="currentPage"
|
|
547
|
+
:page-size="effectiveLimit"
|
|
548
|
+
:limit="effectiveLimit"
|
|
549
|
+
:offset="effectiveOffset"
|
|
550
|
+
:total-count="totalCount"
|
|
551
|
+
:total-pages="totalPages"
|
|
552
|
+
:can-go-prev="canGoPrev"
|
|
553
|
+
:can-go-next="canGoNext"
|
|
554
|
+
:page-items="pageItems"
|
|
555
|
+
:show-first-last="showFirstLast"
|
|
556
|
+
:show-page-numbers="showPageNumbers"
|
|
557
|
+
:pagination="paginationSlotProps"
|
|
558
|
+
:go-to-page="goToPage"
|
|
559
|
+
:first-page="handleFirstPage"
|
|
560
|
+
:last-page="handleLastPage"
|
|
561
|
+
:prev-page="handlePrevPage"
|
|
562
|
+
:next-page="handleNextPage"
|
|
563
|
+
:delete-row="handleDelete"
|
|
564
|
+
:search="searchTerm"
|
|
565
|
+
:search-columns="effectiveSearchColumns"
|
|
566
|
+
:set-search="setSearchTerm"
|
|
567
|
+
:refresh="load"
|
|
568
|
+
>
|
|
569
|
+
<div :class="classes.wrapper" :aria-busy="loading ? 'true' : 'false'">
|
|
570
|
+
<div :class="classes.headerWrapper">
|
|
571
|
+
<slot
|
|
572
|
+
name="header"
|
|
573
|
+
:model="props.model"
|
|
574
|
+
:rows="rows"
|
|
575
|
+
:has-rows="hasRows"
|
|
576
|
+
:is-empty="isEmpty"
|
|
577
|
+
:loading="loading"
|
|
578
|
+
:error="error"
|
|
579
|
+
:query="serverQuery"
|
|
580
|
+
:filters="effectiveWhere"
|
|
581
|
+
:sort-state="sortState"
|
|
582
|
+
:columns="effectiveColumns"
|
|
583
|
+
:current-page="currentPage"
|
|
584
|
+
:total-count="totalCount"
|
|
585
|
+
:total-pages="totalPages"
|
|
586
|
+
>
|
|
587
|
+
<div>
|
|
588
|
+
<h3 :class="classes.headerTitle">
|
|
589
|
+
{{ props.model }}
|
|
590
|
+
</h3>
|
|
591
|
+
<div v-if="totalCount != null" :class="classes.headerSubtitle">
|
|
592
|
+
{{ totalCount }} éléments
|
|
593
|
+
<span v-if="totalPages"> - page {{ currentPage }} / {{ totalPages }}</span>
|
|
594
|
+
</div>
|
|
595
|
+
<div v-else-if="rows.length" :class="classes.headerSubtitle">
|
|
596
|
+
{{ rows.length }} éléments (page {{ currentPage }})
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
</slot>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
<slot
|
|
603
|
+
name="toolbar"
|
|
604
|
+
:search="searchTerm"
|
|
605
|
+
:search-columns="effectiveSearchColumns"
|
|
606
|
+
:set-search="setSearchTerm"
|
|
607
|
+
:query="serverQuery"
|
|
608
|
+
:filters="effectiveWhere"
|
|
609
|
+
:sort-state="sortState"
|
|
610
|
+
:set-sort="setSort"
|
|
611
|
+
:refresh="load"
|
|
612
|
+
>
|
|
613
|
+
<div v-if="props.searchable" :class="classes.toolbar">
|
|
614
|
+
<label :for="searchInputId" class="sr-only">Rechercher dans {{ props.model }}</label>
|
|
615
|
+
<input
|
|
616
|
+
:id="searchInputId"
|
|
617
|
+
v-model="searchTerm"
|
|
618
|
+
type="search"
|
|
619
|
+
:class="classes.searchInput"
|
|
620
|
+
:placeholder="props.searchPlaceholder ?? 'Rechercher...'"
|
|
621
|
+
:disabled="loading"
|
|
622
|
+
/>
|
|
623
|
+
</div>
|
|
624
|
+
</slot>
|
|
625
|
+
|
|
626
|
+
<slot name="error" v-if="error" :error="error">
|
|
627
|
+
<div :class="classes.error" role="alert" aria-live="polite">
|
|
628
|
+
{{ error }}
|
|
629
|
+
</div>
|
|
630
|
+
</slot>
|
|
631
|
+
|
|
632
|
+
<div v-if="props.variant === 'cards'">
|
|
633
|
+
<template v-if="loading">
|
|
634
|
+
<slot name="loading" :columns="effectiveColumns">
|
|
635
|
+
<div :class="classes.loadingCell">Chargement...</div>
|
|
636
|
+
</slot>
|
|
637
|
+
</template>
|
|
638
|
+
|
|
639
|
+
<template v-else-if="!error && displayRows.length === 0">
|
|
640
|
+
<slot
|
|
641
|
+
name="empty"
|
|
642
|
+
:model="props.model"
|
|
643
|
+
:rows="displayRows"
|
|
644
|
+
:raw-rows="rows"
|
|
645
|
+
:columns="effectiveColumns"
|
|
646
|
+
:loading="loading"
|
|
647
|
+
:error="error"
|
|
648
|
+
:query="serverQuery"
|
|
649
|
+
:filters="effectiveWhere"
|
|
650
|
+
:search="searchTerm"
|
|
651
|
+
:search-columns="effectiveSearchColumns"
|
|
652
|
+
:refresh="load"
|
|
653
|
+
>
|
|
654
|
+
<div :class="classes.emptyCell">
|
|
655
|
+
{{ emptyMessage }}
|
|
656
|
+
</div>
|
|
657
|
+
</slot>
|
|
658
|
+
</template>
|
|
659
|
+
|
|
660
|
+
<template v-else>
|
|
661
|
+
<slot name="cards" :rows="displayRows" :columns="effectiveColumns" :query="serverQuery">
|
|
662
|
+
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
663
|
+
<div
|
|
664
|
+
v-for="row in displayRows"
|
|
665
|
+
:key="rowKey(row)"
|
|
666
|
+
class="border rounded-lg p-4 shadow-sm bg-white"
|
|
667
|
+
>
|
|
668
|
+
<slot name="card" :row="row" :columns="effectiveColumns">
|
|
669
|
+
<div
|
|
670
|
+
v-for="column in effectiveColumns"
|
|
671
|
+
:key="column.key"
|
|
672
|
+
class="text-sm text-black mb-1"
|
|
673
|
+
>
|
|
674
|
+
<span class="font-semibold mr-1">{{ column.label }}:</span>
|
|
675
|
+
<span>
|
|
676
|
+
{{
|
|
677
|
+
column.format
|
|
678
|
+
? column.format(getDeep(row, column.key), row)
|
|
679
|
+
: displayValue(getDeep(row, column.key))
|
|
680
|
+
}}
|
|
681
|
+
</span>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<button
|
|
685
|
+
type="button"
|
|
686
|
+
class="mt-2 text-xs text-red-500 underline"
|
|
687
|
+
@click="handleDelete(row)"
|
|
688
|
+
>
|
|
689
|
+
Supprimer
|
|
690
|
+
</button>
|
|
691
|
+
</slot>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
</slot>
|
|
695
|
+
</template>
|
|
696
|
+
</div>
|
|
697
|
+
|
|
698
|
+
<div v-else :class="classes.tableContainer">
|
|
699
|
+
<table :class="classes.table">
|
|
700
|
+
<thead :class="classes.thead">
|
|
701
|
+
<slot name="thead" :columns="effectiveColumns">
|
|
702
|
+
<tr :class="classes.theadRow">
|
|
703
|
+
<th
|
|
704
|
+
v-for="column in effectiveColumns"
|
|
705
|
+
:key="column.key"
|
|
706
|
+
:class="classes.th"
|
|
707
|
+
:aria-sort="sortAriaValue(column)"
|
|
708
|
+
>
|
|
709
|
+
<slot
|
|
710
|
+
name="th"
|
|
711
|
+
:column="column"
|
|
712
|
+
:sort-key="sortState.key"
|
|
713
|
+
:sort-direction="sortState.direction"
|
|
714
|
+
:sort-state="sortState"
|
|
715
|
+
:set-sort="setSort"
|
|
716
|
+
:toggle-sort="() => toggleSort(column)"
|
|
717
|
+
>
|
|
718
|
+
<button
|
|
719
|
+
type="button"
|
|
720
|
+
class="w-full text-left"
|
|
721
|
+
:aria-label="`Trier par ${column.label}`"
|
|
722
|
+
@click="toggleSort(column)"
|
|
723
|
+
>
|
|
724
|
+
{{ column.label }}
|
|
725
|
+
<span v-if="sortState.key === column.key" aria-hidden="true">
|
|
726
|
+
<span v-if="sortState.direction === 'asc'">▲</span>
|
|
727
|
+
<span v-else-if="sortState.direction === 'desc'">▼</span>
|
|
728
|
+
</span>
|
|
729
|
+
</button>
|
|
730
|
+
</slot>
|
|
731
|
+
</th>
|
|
732
|
+
|
|
733
|
+
<th :class="classes.actionsTh">Actions</th>
|
|
734
|
+
</tr>
|
|
735
|
+
</slot>
|
|
736
|
+
</thead>
|
|
737
|
+
|
|
738
|
+
<tbody>
|
|
739
|
+
<template v-if="loading">
|
|
740
|
+
<slot name="loading" :columns="effectiveColumns">
|
|
741
|
+
<tr>
|
|
742
|
+
<td :colspan="effectiveColumns.length + 1" :class="classes.loadingCell">
|
|
743
|
+
Chargement...
|
|
744
|
+
</td>
|
|
745
|
+
</tr>
|
|
746
|
+
</slot>
|
|
747
|
+
</template>
|
|
748
|
+
|
|
749
|
+
<template v-else-if="!error && displayRows.length === 0">
|
|
750
|
+
<slot
|
|
751
|
+
name="empty"
|
|
752
|
+
:model="props.model"
|
|
753
|
+
:rows="displayRows"
|
|
754
|
+
:raw-rows="rows"
|
|
755
|
+
:columns="effectiveColumns"
|
|
756
|
+
:loading="loading"
|
|
757
|
+
:error="error"
|
|
758
|
+
:query="serverQuery"
|
|
759
|
+
:filters="effectiveWhere"
|
|
760
|
+
:search="searchTerm"
|
|
761
|
+
:search-columns="effectiveSearchColumns"
|
|
762
|
+
:refresh="load"
|
|
763
|
+
>
|
|
764
|
+
<tr>
|
|
765
|
+
<td :colspan="effectiveColumns.length + 1" :class="classes.emptyCell">
|
|
766
|
+
{{ emptyMessage }}
|
|
767
|
+
</td>
|
|
768
|
+
</tr>
|
|
769
|
+
</slot>
|
|
770
|
+
</template>
|
|
771
|
+
|
|
772
|
+
<template v-else>
|
|
773
|
+
<slot name="body" :rows="displayRows" :columns="effectiveColumns" :query="serverQuery">
|
|
774
|
+
<tr
|
|
775
|
+
v-for="row in displayRows"
|
|
776
|
+
:key="rowKey(row)"
|
|
777
|
+
:class="classes.bodyRow"
|
|
778
|
+
>
|
|
779
|
+
<td
|
|
780
|
+
v-for="column in effectiveColumns"
|
|
781
|
+
:key="column.key"
|
|
782
|
+
:class="classes.td"
|
|
783
|
+
>
|
|
784
|
+
<slot
|
|
785
|
+
name="cell"
|
|
786
|
+
:row="row"
|
|
787
|
+
:column="column"
|
|
788
|
+
:value="
|
|
789
|
+
column.format
|
|
790
|
+
? column.format(getDeep(row, column.key), row)
|
|
791
|
+
: displayValue(getDeep(row, column.key))
|
|
792
|
+
"
|
|
793
|
+
>
|
|
794
|
+
{{
|
|
795
|
+
column.format
|
|
796
|
+
? column.format(getDeep(row, column.key), row)
|
|
797
|
+
: displayValue(getDeep(row, column.key))
|
|
798
|
+
}}
|
|
799
|
+
</slot>
|
|
800
|
+
</td>
|
|
801
|
+
|
|
802
|
+
<td :class="classes.actionsTd">
|
|
803
|
+
<button
|
|
804
|
+
type="button"
|
|
805
|
+
aria-label="Supprimer la ligne"
|
|
806
|
+
:disabled="deletingRows.has(row?.[nsdbModel.primaryKey ?? 'id'])"
|
|
807
|
+
@click="handleDelete(row)"
|
|
808
|
+
:class="classes.deleteButton"
|
|
809
|
+
>
|
|
810
|
+
<svg
|
|
811
|
+
aria-hidden="true"
|
|
812
|
+
class="w-4 h-4 text-red-500"
|
|
813
|
+
viewBox="0 0 24 24"
|
|
814
|
+
fill="none"
|
|
815
|
+
stroke="currentColor"
|
|
816
|
+
stroke-width="2"
|
|
817
|
+
>
|
|
818
|
+
<path d="M4 7h16M9 7V4h6v3m-9 0 1 13h10l1-13M10 11v5m4-5v5" />
|
|
819
|
+
</svg>
|
|
820
|
+
</button>
|
|
821
|
+
</td>
|
|
822
|
+
</tr>
|
|
823
|
+
</slot>
|
|
824
|
+
</template>
|
|
825
|
+
</tbody>
|
|
826
|
+
</table>
|
|
827
|
+
</div>
|
|
828
|
+
|
|
829
|
+
<div :class="classes.footer">
|
|
830
|
+
<slot
|
|
831
|
+
name="footer"
|
|
832
|
+
:rows="displayRows"
|
|
833
|
+
:columns="effectiveColumns"
|
|
834
|
+
:model="props.model"
|
|
835
|
+
:query="serverQuery"
|
|
836
|
+
:filters="effectiveWhere"
|
|
837
|
+
:sort-state="sortState"
|
|
838
|
+
:set-sort="setSort"
|
|
839
|
+
:current-page="currentPage"
|
|
840
|
+
:page-size="effectiveLimit"
|
|
841
|
+
:limit="effectiveLimit"
|
|
842
|
+
:offset="effectiveOffset"
|
|
843
|
+
:total-count="totalCount"
|
|
844
|
+
:total-pages="totalPages"
|
|
845
|
+
:can-go-prev="canGoPrev"
|
|
846
|
+
:can-go-next="canGoNext"
|
|
847
|
+
:go-to-page="goToPage"
|
|
848
|
+
:first-page="handleFirstPage"
|
|
849
|
+
:last-page="handleLastPage"
|
|
850
|
+
:prev-page="handlePrevPage"
|
|
851
|
+
:next-page="handleNextPage"
|
|
852
|
+
:page-items="pageItems"
|
|
853
|
+
:show-first-last="showFirstLast"
|
|
854
|
+
:show-page-numbers="showPageNumbers"
|
|
855
|
+
:pagination="paginationSlotProps"
|
|
856
|
+
>
|
|
857
|
+
<div class="text-sm opacity-70">
|
|
858
|
+
Page {{ currentPage }}
|
|
859
|
+
<span v-if="totalPages"> / {{ totalPages }}</span>
|
|
860
|
+
<span v-if="totalCount != null"> - {{ totalCount }} éléments</span>
|
|
861
|
+
</div>
|
|
862
|
+
|
|
863
|
+
<slot
|
|
864
|
+
name="pagination"
|
|
865
|
+
:rows="displayRows"
|
|
866
|
+
:model="props.model"
|
|
867
|
+
:loading="loading"
|
|
868
|
+
:current-page="currentPage"
|
|
869
|
+
:page-size="effectiveLimit"
|
|
870
|
+
:limit="effectiveLimit"
|
|
871
|
+
:offset="effectiveOffset"
|
|
872
|
+
:total-count="totalCount"
|
|
873
|
+
:total-pages="totalPages"
|
|
874
|
+
:can-go-prev="canGoPrev"
|
|
875
|
+
:can-go-next="canGoNext"
|
|
876
|
+
:page-items="pageItems"
|
|
877
|
+
:show-first-last="showFirstLast"
|
|
878
|
+
:show-page-numbers="showPageNumbers"
|
|
879
|
+
:go-to-page="goToPage"
|
|
880
|
+
:first-page="handleFirstPage"
|
|
881
|
+
:last-page="handleLastPage"
|
|
882
|
+
:prev-page="handlePrevPage"
|
|
883
|
+
:next-page="handleNextPage"
|
|
884
|
+
:pagination="paginationSlotProps"
|
|
885
|
+
>
|
|
886
|
+
<div :class="classes.pagination">
|
|
887
|
+
<button
|
|
888
|
+
v-if="showFirstLast && totalPages"
|
|
889
|
+
type="button"
|
|
890
|
+
:class="[classes.pageButton, (!canGoPrev || loading) && classes.pageButtonDisabled]"
|
|
891
|
+
:disabled="!canGoPrev || loading"
|
|
892
|
+
@click="handleFirstPage"
|
|
893
|
+
>
|
|
894
|
+
Première
|
|
895
|
+
</button>
|
|
896
|
+
|
|
897
|
+
<button
|
|
898
|
+
type="button"
|
|
899
|
+
:class="[classes.pageButton, (!canGoPrev || loading) && classes.pageButtonDisabled]"
|
|
900
|
+
:disabled="!canGoPrev || loading"
|
|
901
|
+
@click="handlePrevPage"
|
|
902
|
+
>
|
|
903
|
+
Précédent
|
|
904
|
+
</button>
|
|
905
|
+
|
|
906
|
+
<template v-if="showPageNumbers && totalPages">
|
|
907
|
+
<template v-for="it in pageItems" :key="String(it) + '-' + currentPage">
|
|
908
|
+
<span v-if="it === '...'" class="px-2 opacity-60">...</span>
|
|
909
|
+
<button
|
|
910
|
+
v-else
|
|
911
|
+
type="button"
|
|
912
|
+
:class="[
|
|
913
|
+
classes.pageButton,
|
|
914
|
+
it === currentPage && classes.pageButtonActive,
|
|
915
|
+
loading && classes.pageButtonDisabled
|
|
916
|
+
]"
|
|
917
|
+
:disabled="loading"
|
|
918
|
+
@click="goToPage(it)"
|
|
919
|
+
>
|
|
920
|
+
{{ it }}
|
|
921
|
+
</button>
|
|
922
|
+
</template>
|
|
923
|
+
</template>
|
|
924
|
+
|
|
925
|
+
<button
|
|
926
|
+
type="button"
|
|
927
|
+
:class="[classes.pageButton, (!canGoNext || loading) && classes.pageButtonDisabled]"
|
|
928
|
+
:disabled="!canGoNext || loading"
|
|
929
|
+
@click="handleNextPage"
|
|
930
|
+
>
|
|
931
|
+
Suivant
|
|
932
|
+
</button>
|
|
933
|
+
|
|
934
|
+
<button
|
|
935
|
+
v-if="showFirstLast && totalPages"
|
|
936
|
+
type="button"
|
|
937
|
+
:class="[classes.pageButton, (!canGoNext || loading) && classes.pageButtonDisabled]"
|
|
938
|
+
:disabled="!canGoNext || loading"
|
|
939
|
+
@click="handleLastPage"
|
|
940
|
+
>
|
|
941
|
+
Dernière
|
|
942
|
+
</button>
|
|
943
|
+
</div>
|
|
944
|
+
</slot>
|
|
945
|
+
</slot>
|
|
946
|
+
</div>
|
|
947
|
+
</div>
|
|
948
|
+
</slot>
|
|
949
|
+
</template>
|
|
950
|
+
|
|
951
|
+
<style scoped>
|
|
952
|
+
.nsdb-table {
|
|
953
|
+
width: 100% !important;
|
|
954
|
+
max-width: 100%;
|
|
955
|
+
table-layout: auto;
|
|
956
|
+
width: -webkit-fill-available;
|
|
957
|
+
width: -moz-available;
|
|
958
|
+
width: stretch;
|
|
959
|
+
max-width: 100%;
|
|
960
|
+
}
|
|
961
|
+
</style>
|