runeforge 0.0.33 → 0.0.35
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.
|
@@ -64,21 +64,39 @@
|
|
|
64
64
|
let currentPage = $state(pagination?.page ?? 1);
|
|
65
65
|
let lastKnownPage = pagination?.page ?? 1;
|
|
66
66
|
|
|
67
|
+
const serverFilterSampleSize = 5;
|
|
68
|
+
|
|
69
|
+
// Server mode: there's no way to know every value a column can take without
|
|
70
|
+
// querying the whole (server-owned) dataset, so this is deliberately just a
|
|
71
|
+
// cosmetic hint — up to `serverFilterSampleSize` distinct values found on
|
|
72
|
+
// the current page, not an exhaustive list. Boolean is the one exception:
|
|
73
|
+
// its two states are always known, so both show up regardless of what the
|
|
74
|
+
// current page happens to contain.
|
|
67
75
|
const distinctValues = $derived(
|
|
68
76
|
pagination
|
|
69
|
-
?
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
? {
|
|
78
|
+
...Object.fromEntries(
|
|
79
|
+
Object.entries(
|
|
80
|
+
distinctEntries(
|
|
81
|
+
data,
|
|
82
|
+
columns.filter((c) => isFilterable(c) && c.type !== 'boolean')
|
|
83
|
+
)
|
|
84
|
+
).map(([attribute, entries]) => [attribute, entries.slice(0, serverFilterSampleSize)])
|
|
85
|
+
),
|
|
86
|
+
...Object.fromEntries(
|
|
87
|
+
columns
|
|
88
|
+
.filter((c) => isFilterable(c) && c.type === 'boolean')
|
|
89
|
+
.map((c) => [
|
|
90
|
+
c.attribute,
|
|
91
|
+
[
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
{ key: 'true', label: c.formatter?.(true as any, {} as T), row: {} as T },
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
{ key: 'false', label: c.formatter?.(false as any, {} as T), row: {} as T }
|
|
96
|
+
]
|
|
97
|
+
])
|
|
98
|
+
)
|
|
99
|
+
}
|
|
82
100
|
: distinctEntries(data, columns)
|
|
83
101
|
);
|
|
84
102
|
|
|
@@ -1,95 +1,98 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import Button from '../form/Button.svelte';
|
|
3
|
+
import { getStrings } from '../../i18n/context.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const strings = getStrings();
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
let {
|
|
8
|
+
page = $bindable(1),
|
|
9
|
+
totalPages,
|
|
10
|
+
pageStart,
|
|
11
|
+
pageSize,
|
|
12
|
+
total
|
|
13
|
+
}: {
|
|
14
|
+
page?: number;
|
|
15
|
+
totalPages: number;
|
|
16
|
+
pageStart: number;
|
|
17
|
+
pageSize: number;
|
|
18
|
+
total: number;
|
|
19
|
+
} = $props();
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
function prev() {
|
|
22
|
+
if (page > 1) page--;
|
|
23
|
+
}
|
|
24
|
+
function next() {
|
|
25
|
+
if (page < totalPages) page++;
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
function buildPages(current: number, total: number): number[] {
|
|
29
|
+
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
// Local, immediately-discarded dedup set for this one calculation, not
|
|
32
|
+
// reactive state — a plain Set is correct here.
|
|
33
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
34
|
+
const visible = new Set<number>();
|
|
35
|
+
visible.add(1);
|
|
36
|
+
visible.add(total);
|
|
37
|
+
for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
|
|
38
|
+
visible.add(i);
|
|
39
|
+
}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const sorted = Array.from(visible).sort((a, b) => a - b);
|
|
42
|
+
const result: number[] = [];
|
|
43
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
44
|
+
if (i > 0 && sorted[i] - sorted[i - 1] > 1) result.push(0);
|
|
45
|
+
result.push(sorted[i]);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
const pages = $derived(buildPages(page, totalPages));
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
function handlePageInput(e: KeyboardEvent) {
|
|
53
|
+
if (e.key !== 'Enter') return;
|
|
54
|
+
const val = parseInt((e.currentTarget as HTMLInputElement).value);
|
|
55
|
+
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
|
56
|
+
page = val;
|
|
57
|
+
} else {
|
|
58
|
+
(e.currentTarget as HTMLInputElement).value = String(page);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
function resetInput(e: FocusEvent) {
|
|
63
|
+
(e.currentTarget as HTMLInputElement).value = String(page);
|
|
64
|
+
}
|
|
62
65
|
</script>
|
|
63
66
|
|
|
64
67
|
{#if totalPages > 1}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
<div class="flex items-center justify-between">
|
|
69
|
+
<span class="text-sm text-base-content/60">
|
|
70
|
+
{strings.showing(pageStart + 1, Math.min(pageStart + pageSize, total), total)}
|
|
71
|
+
</span>
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
<div class="join">
|
|
74
|
+
<Button class="join-item btn-sm" disabled={page === 1} onclick={prev}>«</Button>
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
{#each pages as p, i (i)}
|
|
77
|
+
{#if p === 0}
|
|
78
|
+
<Button class="join-item btn-sm" disabled>…</Button>
|
|
79
|
+
{:else if p === page}
|
|
80
|
+
<input
|
|
81
|
+
type="number"
|
|
82
|
+
class="join-item btn btn-sm w-12 text-center appearance-none"
|
|
83
|
+
style="background-color: var(--color-base-100);"
|
|
84
|
+
min="1"
|
|
85
|
+
max={totalPages}
|
|
86
|
+
value={page}
|
|
87
|
+
onkeydown={handlePageInput}
|
|
88
|
+
onblur={resetInput}
|
|
89
|
+
/>
|
|
90
|
+
{:else}
|
|
91
|
+
<Button class="join-item btn-sm" onclick={() => (page = p)}>{p}</Button>
|
|
92
|
+
{/if}
|
|
93
|
+
{/each}
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
<Button class="join-item btn-sm" disabled={page === totalPages} onclick={next}>»</Button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
95
98
|
{/if}
|
|
@@ -33,9 +33,13 @@ export class SortState {
|
|
|
33
33
|
const col = columns.find((c) => c.attribute === attribute);
|
|
34
34
|
const factor = direction === 'asc' ? 1 : -1;
|
|
35
35
|
if (col?.formatter) {
|
|
36
|
-
return [...rows].sort((a, b) => factor *
|
|
36
|
+
return [...rows].sort((a, b) => factor *
|
|
37
|
+
cellRenderedText(a.row, col).localeCompare(cellRenderedText(b.row, col), undefined, {
|
|
38
|
+
numeric: true
|
|
39
|
+
}));
|
|
37
40
|
}
|
|
38
|
-
return [...rows].sort((a, b) => factor *
|
|
41
|
+
return [...rows].sort((a, b) => factor *
|
|
42
|
+
compare(a.row[attribute], b.row[attribute]));
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
export class FilterState {
|
|
@@ -58,9 +62,9 @@ export class FilterState {
|
|
|
58
62
|
return this.dateRanges.get(attribute) ?? { from: '', to: '' };
|
|
59
63
|
}
|
|
60
64
|
hasActive(attribute) {
|
|
61
|
-
return (this.text.get(attribute)?.length ?? 0) > 0
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
return ((this.text.get(attribute)?.length ?? 0) > 0 ||
|
|
66
|
+
(this.values.get(attribute)?.size ?? 0) > 0 ||
|
|
67
|
+
!!(this.dateRanges.get(attribute)?.from || this.dateRanges.get(attribute)?.to));
|
|
64
68
|
}
|
|
65
69
|
isChecked(attribute, value) {
|
|
66
70
|
return this.values.get(attribute)?.has(value) ?? false;
|
|
@@ -101,11 +105,16 @@ export class FilterState {
|
|
|
101
105
|
const range = this.dateRanges.get(col.attribute);
|
|
102
106
|
if (range?.from || range?.to) {
|
|
103
107
|
const raw = row[col.attribute];
|
|
108
|
+
// Local, immediately-discarded timestamps for a one-off comparison,
|
|
109
|
+
// not reactive state — a plain Date is correct here.
|
|
110
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
104
111
|
const ts = raw ? new Date(raw).getTime() : NaN;
|
|
105
112
|
if (isNaN(ts))
|
|
106
113
|
return false;
|
|
114
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
107
115
|
if (range.from && ts < new Date(range.from).getTime())
|
|
108
116
|
return false;
|
|
117
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
109
118
|
if (range.to && ts > new Date(range.to + 'T23:59:59.999').getTime())
|
|
110
119
|
return false;
|
|
111
120
|
}
|
|
@@ -126,6 +135,6 @@ export function snapshotFilter(filter) {
|
|
|
126
135
|
return {
|
|
127
136
|
text: Object.fromEntries(filter.text),
|
|
128
137
|
values: Object.fromEntries([...filter.values].map(([k, set]) => [k, [...set]])),
|
|
129
|
-
dateRanges: Object.fromEntries(filter.dateRanges)
|
|
138
|
+
dateRanges: Object.fromEntries(filter.dateRanges)
|
|
130
139
|
};
|
|
131
140
|
}
|