@softwareone/spi-sv5-library 1.10.4 → 1.11.0

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.
Files changed (39) hide show
  1. package/dist/Button/Button.svelte +2 -1
  2. package/dist/Card/Card.svelte +1 -1
  3. package/dist/{AttachFile → Controls/AttachFile}/AttachFile.svelte +36 -21
  4. package/dist/{AttachFile → Controls/AttachFile}/AttachFile.svelte.d.ts +2 -1
  5. package/dist/{AttachFile → Controls/AttachFile}/Warnings.svelte +1 -1
  6. package/dist/Controls/Input/Input.svelte +2 -4
  7. package/dist/Controls/Input/Input.svelte.d.ts +2 -4
  8. package/dist/Controls/Label/Label.svelte +3 -8
  9. package/dist/Controls/Label/Label.svelte.d.ts +1 -7
  10. package/dist/Controls/Label/labelState.svelte.d.ts +7 -0
  11. package/dist/Controls/Label/labelState.svelte.js +1 -0
  12. package/dist/Controls/Select/Select.svelte +2 -5
  13. package/dist/Controls/Select/Select.svelte.d.ts +2 -5
  14. package/dist/Controls/TextArea/TextArea.svelte +2 -4
  15. package/dist/Controls/TextArea/TextArea.svelte.d.ts +2 -4
  16. package/dist/Controls/Toggle/Toggle.svelte +3 -5
  17. package/dist/Controls/Toggle/Toggle.svelte.d.ts +2 -4
  18. package/dist/Table/AdvancedFilter.svelte +385 -0
  19. package/dist/Table/AdvancedFilter.svelte.d.ts +29 -0
  20. package/dist/Table/Header.svelte +38 -5
  21. package/dist/Table/Table.svelte +78 -35
  22. package/dist/Table/Table.svelte.d.ts +2 -1
  23. package/dist/Table/consts.d.ts +6 -0
  24. package/dist/Table/consts.js +5 -0
  25. package/dist/Table/excel.js +11 -1
  26. package/dist/Table/index.d.ts +2 -1
  27. package/dist/Table/index.js +2 -1
  28. package/dist/Table/types.js +1 -1
  29. package/dist/Table/util.d.ts +2 -0
  30. package/dist/Table/util.js +21 -3
  31. package/dist/Tabs/Tabs.svelte +5 -2
  32. package/dist/Tabs/tabsState.svelte.d.ts +7 -5
  33. package/dist/Tabs/tabsState.svelte.js +3 -1
  34. package/dist/index.d.ts +3 -2
  35. package/dist/index.js +3 -2
  36. package/package.json +1 -1
  37. /package/dist/{AttachFile → Controls/AttachFile}/FileManager.svelte +0 -0
  38. /package/dist/{AttachFile → Controls/AttachFile}/FileManager.svelte.d.ts +0 -0
  39. /package/dist/{AttachFile → Controls/AttachFile}/Warnings.svelte.d.ts +0 -0
@@ -0,0 +1,385 @@
1
+ <script lang="ts" generics="T">
2
+ import { page } from '$app/state';
3
+
4
+ import { Button, Input, Select, type SelectOption } from '../index.js';
5
+ import type { Column } from './adapter/index.js';
6
+ import { Operator } from './consts.js';
7
+ import { type Filter } from './types.js';
8
+
9
+ interface Props {
10
+ columns: Column<T, unknown>[];
11
+ onfilterschange?: (filters: Filter[]) => void;
12
+ }
13
+
14
+ let { columns, onfilterschange }: Props = $props();
15
+
16
+ let isOpen = $state(false);
17
+ let editingFilters = $state<Filter[]>([]);
18
+ const operatorOptions: Operator[] = Object.values(Operator);
19
+
20
+ const filters = $derived.by<Filter[]>(() => {
21
+ const filterParams = page.url.searchParams.getAll('filter');
22
+ return filterParams
23
+ .filter((param) => param.trim())
24
+ .map(parseFilterParam)
25
+ .filter((filter): filter is Filter => filter !== null);
26
+ });
27
+
28
+ const hasActiveFilters = $derived(filters.length > 0);
29
+ const columnOptions = $derived<SelectOption[]>(
30
+ columns
31
+ .filter((column) => !column.columnDef.meta?.hideColumnFilter)
32
+ .map((column) => ({
33
+ value: column.id,
34
+ label: (column.columnDef.header as string) || column.id
35
+ }))
36
+ );
37
+
38
+ const addFilterRow = () => {
39
+ editingFilters = [...editingFilters, { column: '', value: '', operator: Operator.And }];
40
+ };
41
+
42
+ const removeFilterRow = (index: number) => {
43
+ const remainingFilters = editingFilters.filter((_, filterIndex) => filterIndex !== index);
44
+
45
+ editingFilters =
46
+ remainingFilters.length === 1
47
+ ? [{ ...remainingFilters[0], operator: undefined }]
48
+ : remainingFilters;
49
+ };
50
+
51
+ const applyFilters = () => {
52
+ const validFilters = getValidFilters();
53
+ handleFilters(validFilters);
54
+ };
55
+
56
+ const clearAllFilters = () => {
57
+ editingFilters = [];
58
+ handleFilters(editingFilters);
59
+ };
60
+
61
+ const handleFilters = (filters: Filter[]) => {
62
+ onfilterschange?.(filters);
63
+ isOpen = false;
64
+ };
65
+
66
+ const toggleModal = () => {
67
+ isOpen = !isOpen;
68
+ isOpen ? initializeEditingFilters() : clearFilters();
69
+ };
70
+
71
+ const initializeEditingFilters = () => {
72
+ if (!editingFilters.length) {
73
+ editingFilters = filters.length
74
+ ? filters.map((filter) => ({ ...filter }))
75
+ : [{ column: '', value: '', operator: undefined }];
76
+ }
77
+ };
78
+
79
+ const clearFilters = () => {
80
+ const validFilters = getValidFilters();
81
+ if (validFilters.length === 0 && filters.length > 0) {
82
+ clearAllFilters();
83
+ }
84
+ };
85
+
86
+ const getValidFilters = () => {
87
+ return editingFilters.filter((filter) => filter.column && filter.value.trim());
88
+ };
89
+
90
+ const parseFilterParam = (filterValue: string) => {
91
+ const parts = filterValue.split(':');
92
+ if (parts.length === 3) return parseSimpleFilter(parts);
93
+ if (parts.length === 4) return parseOperatorFilter(parts);
94
+ return null;
95
+ };
96
+
97
+ const parseSimpleFilter = (parts: string[]): Filter | null => {
98
+ const [column, operator, value] = parts;
99
+ return isValidFilter(column, operator, value) ? { column, value, operator: undefined } : null;
100
+ };
101
+
102
+ const parseOperatorFilter = (parts: string[]): Filter | null => {
103
+ const [operator, column, eq, value] = parts;
104
+ return isValidFilter(column, eq, value) && isValidOperator(operator)
105
+ ? { column, value, operator }
106
+ : null;
107
+ };
108
+
109
+ const isValidFilter = (column: string, operator: string, value: string) => {
110
+ return operator === 'eq' && !!column && !!value;
111
+ };
112
+
113
+ const isValidOperator = (operator: string): operator is Operator => {
114
+ return operatorOptions.includes(operator as Operator);
115
+ };
116
+ </script>
117
+
118
+ <div class="filter-container">
119
+ <button
120
+ type="button"
121
+ class={['filter-button', hasActiveFilters && 'filter-button--active']}
122
+ onclick={toggleModal}
123
+ >
124
+ <span class="material-icons">filter_list</span>
125
+ {#if hasActiveFilters}
126
+ <strong>{filters.length} {filters.length === 1 ? 'Filter' : 'Filters'}</strong>
127
+ {:else}
128
+ <p>Filters</p>
129
+ {/if}
130
+ </button>
131
+
132
+ {#if isOpen}
133
+ <button
134
+ type="button"
135
+ class="filter-overlay"
136
+ onclick={toggleModal}
137
+ aria-label="Close filter modal"
138
+ ></button>
139
+
140
+ <div
141
+ class="filter-modal"
142
+ role="dialog"
143
+ tabindex="-1"
144
+ aria-modal="true"
145
+ aria-labelledby="filter-dialog-title"
146
+ >
147
+ <div class="filter-header">
148
+ <h2 id="filter-dialog-title" class="filter-title">Filters</h2>
149
+ <button type="button" onclick={toggleModal} class="filter-close-button" aria-label="Close">
150
+ <span class="material-icons">close</span>
151
+ </button>
152
+ </div>
153
+
154
+ <div class="filter-body">
155
+ {#each editingFilters as filter, index (index)}
156
+ <div class="filter-row">
157
+ {#if index > 0}
158
+ <div class="filter-operator">
159
+ <Select
160
+ options={operatorOptions}
161
+ disableValidationColor
162
+ bind:value={filter.operator}
163
+ hideClearButton
164
+ />
165
+ </div>
166
+ {:else}
167
+ <div class="filter-operator"></div>
168
+ {/if}
169
+
170
+ <div class="filter-column">
171
+ <Select
172
+ options={columnOptions}
173
+ disableValidationColor
174
+ bind:value={filter.column}
175
+ placeholder="Select column"
176
+ searchable
177
+ />
178
+ </div>
179
+
180
+ <div class="filter-value">
181
+ <Input
182
+ id="filter-value-{index}"
183
+ disableValidationColor
184
+ type="text"
185
+ bind:value={filter.value}
186
+ placeholder="Enter value"
187
+ />
188
+ </div>
189
+
190
+ <button
191
+ type="button"
192
+ onclick={() => removeFilterRow(index)}
193
+ class="filter-delete-button"
194
+ aria-label="Remove filter"
195
+ >
196
+ <span class="material-icons-outlined">delete</span>
197
+ </button>
198
+ </div>
199
+ {/each}
200
+
201
+ <div class="filter-actions">
202
+ <Button variant="outline-none" type="button" onclick={addFilterRow}>Add condition</Button>
203
+ {#if editingFilters.length}
204
+ <Button variant="outline-none" type="button" onclick={clearAllFilters}>
205
+ Reset filters
206
+ </Button>
207
+ {/if}
208
+ </div>
209
+ </div>
210
+
211
+ {#if editingFilters.length}
212
+ <div class="filter-footer">
213
+ <Button
214
+ onclick={applyFilters}
215
+ disabled={editingFilters.every((filter) => !filter.column || !filter.value.trim())}
216
+ >
217
+ Apply Filters
218
+ </Button>
219
+ </div>
220
+ {/if}
221
+ </div>
222
+ {/if}
223
+ </div>
224
+
225
+ <style>
226
+ .filter-container {
227
+ --color-primary: #472aff;
228
+ --color-primary-light: #eaecff;
229
+ --color-white: #fff;
230
+ --color-text-primary: #25282d;
231
+ --color-text-dark: #111827;
232
+ --color-text-secondary: #6b7280;
233
+ --color-text-hover: #374151;
234
+ --color-bg-hover: #f4f6f8;
235
+ --color-border: #e5e7eb;
236
+ --spacing-xs: 4px;
237
+ --spacing-sm: 8px;
238
+ --spacing-md: 12px;
239
+ --spacing-lg: 16px;
240
+ --spacing-xl: 24px;
241
+ --font-size-sm: 14px;
242
+ --font-size-md: 18px;
243
+ --font-size-icon: 20px;
244
+ --border-radius-sm: 8px;
245
+ --border-radius-circle: 50%;
246
+ --button-size: 40px;
247
+ --delete-button-size: 48px;
248
+ --modal-width: 640px;
249
+ --operator-column-width: 80px;
250
+ --transition-speed: 0.2s;
251
+
252
+ position: relative;
253
+ }
254
+
255
+ .filter-button {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: var(--spacing-sm);
259
+ padding: var(--spacing-sm) var(--spacing-lg);
260
+ border: none;
261
+ border-radius: var(--border-radius-sm);
262
+ background: var(--color-white);
263
+ font-size: var(--font-size-sm);
264
+ color: var(--color-text-primary);
265
+ cursor: pointer;
266
+ transition: background-color var(--transition-speed) ease-in-out;
267
+ }
268
+
269
+ .filter-button:hover {
270
+ background: var(--color-bg-hover);
271
+ }
272
+
273
+ .filter-button--active {
274
+ background: var(--color-primary-light);
275
+ color: var(--color-primary);
276
+ }
277
+
278
+ .filter-overlay {
279
+ position: fixed;
280
+ inset: 0;
281
+ z-index: 9;
282
+ background: transparent;
283
+ border: none;
284
+ cursor: default;
285
+ }
286
+
287
+ .filter-modal {
288
+ position: absolute;
289
+ top: calc(100% + var(--spacing-sm));
290
+ right: 0;
291
+ z-index: 10;
292
+ width: var(--modal-width);
293
+ padding: var(--spacing-xl);
294
+ border: 1px solid var(--color-border);
295
+ border-radius: var(--border-radius-sm);
296
+ background: var(--color-white);
297
+ box-shadow:
298
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
299
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
300
+ }
301
+
302
+ .filter-header {
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: space-between;
306
+ margin-bottom: var(--spacing-xl);
307
+ }
308
+
309
+ .filter-title {
310
+ font-size: var(--font-size-md);
311
+ font-weight: 600;
312
+ color: var(--color-text-dark);
313
+ }
314
+
315
+ .filter-close-button {
316
+ display: flex;
317
+ align-items: center;
318
+ padding: var(--spacing-xs);
319
+ border: none;
320
+ background: transparent;
321
+ color: var(--color-text-secondary);
322
+ cursor: pointer;
323
+ transition: color var(--transition-speed) ease-in-out;
324
+ }
325
+
326
+ .filter-close-button:hover {
327
+ color: var(--color-text-hover);
328
+ }
329
+
330
+ .filter-body {
331
+ display: flex;
332
+ flex-direction: column;
333
+ gap: var(--spacing-lg);
334
+ }
335
+
336
+ .filter-row {
337
+ display: grid;
338
+ grid-template-columns: var(--operator-column-width) 1fr 1fr var(--delete-button-size);
339
+ align-items: end;
340
+ gap: var(--spacing-md);
341
+ }
342
+
343
+ .filter-operator,
344
+ .filter-column,
345
+ .filter-value {
346
+ display: flex;
347
+ }
348
+
349
+ .filter-delete-button {
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ padding: var(--spacing-sm);
354
+ border: none;
355
+ background: transparent;
356
+ color: var(--color-text-primary);
357
+ border-radius: var(--border-radius-circle);
358
+ width: var(--button-size);
359
+ height: var(--button-size);
360
+ cursor: pointer;
361
+ transition: background-color var(--transition-speed) ease-in-out;
362
+ }
363
+
364
+ .filter-delete-button:hover {
365
+ background: var(--color-bg-hover);
366
+ }
367
+
368
+ .filter-actions {
369
+ display: flex;
370
+ align-items: center;
371
+ justify-content: space-between;
372
+ margin-top: var(--spacing-sm);
373
+ }
374
+
375
+ .filter-footer {
376
+ display: flex;
377
+ justify-content: flex-end;
378
+ margin-top: var(--spacing-lg);
379
+ }
380
+
381
+ .material-icons,
382
+ .material-icons-outlined {
383
+ font-size: var(--font-size-icon);
384
+ }
385
+ </style>
@@ -0,0 +1,29 @@
1
+ import type { Column } from './adapter/index.js';
2
+ import { type Filter } from './types.js';
3
+ declare function $$render<T>(): {
4
+ props: {
5
+ columns: Column<T, unknown>[];
6
+ onfilterschange?: (filters: Filter[]) => void;
7
+ };
8
+ exports: {};
9
+ bindings: "";
10
+ slots: {};
11
+ events: {};
12
+ };
13
+ declare class __sveltets_Render<T> {
14
+ props(): ReturnType<typeof $$render<T>>['props'];
15
+ events(): ReturnType<typeof $$render<T>>['events'];
16
+ slots(): ReturnType<typeof $$render<T>>['slots'];
17
+ bindings(): "";
18
+ exports(): {};
19
+ }
20
+ interface $$IsomorphicComponent {
21
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
22
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
23
+ } & ReturnType<__sveltets_Render<T>['exports']>;
24
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
25
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
26
+ }
27
+ declare const AdvancedFilter: $$IsomorphicComponent;
28
+ type AdvancedFilter<T> = InstanceType<typeof AdvancedFilter<T>>;
29
+ export default AdvancedFilter;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts" generics="T extends RowData">
2
+ import { page } from '$app/state';
3
+
2
4
  import { Search } from '../index.js';
3
5
  import { FlexRender, type HeaderGroup, type RowData } from './adapter/index.js';
4
6
 
@@ -7,13 +9,24 @@
7
9
  enableColumnSearch: boolean;
8
10
  }
9
11
 
12
+ let { headerGroups, enableColumnSearch }: Props = $props();
13
+
10
14
  const getSortIcon: Record<string, string> = {
11
15
  asc: 'arrow_upward',
12
16
  desc: 'arrow_downward',
13
17
  false: 'swap_vert'
14
18
  };
15
19
 
16
- let { headerGroups, enableColumnSearch }: Props = $props();
20
+ const isColumnFiltered = (columnId: string): boolean => {
21
+ const filterParams = page.url.searchParams.getAll('filter');
22
+ return filterParams.some((filterValue) => {
23
+ const parts = filterValue.split(':');
24
+ return (
25
+ (parts.length === 3 && parts[0] === columnId) ||
26
+ (parts.length === 4 && parts[1] === columnId)
27
+ );
28
+ });
29
+ };
17
30
  </script>
18
31
 
19
32
  <thead class="container">
@@ -27,9 +40,11 @@
27
40
  {@const hideColumnFilter = header.column.columnDef.meta?.hideColumnFilter}
28
41
  {@const justifyContent = alignColumn ?? 'left'}
29
42
  {@const isSorted = header.column.getIsSorted().toString() !== 'false'}
43
+ {@const isFiltered = isColumnFiltered(header.column.id)}
44
+ {@const isActive = isSorted || isFiltered}
30
45
  <th
31
46
  colSpan={header.colSpan}
32
- class={['table-header-cell', className, isSorted && 'table-header-cell--sorted']}
47
+ class={['table-header-cell', className, isActive && 'table-header-cell--active']}
33
48
  style:width={columnWidth}
34
49
  style={columnStyle}
35
50
  >
@@ -61,6 +76,12 @@
61
76
  >
62
77
  </div>
63
78
  {/if}
79
+
80
+ {#if isFiltered}
81
+ <div class="table-header-filtered-icon">
82
+ <span class="material-icons-outlined">filter_alt</span>
83
+ </div>
84
+ {/if}
64
85
  </button>
65
86
 
66
87
  {#if enableColumnSearch && !hideColumnFilter}
@@ -82,6 +103,7 @@
82
103
  <style>
83
104
  .container {
84
105
  --color-primary: #472aff;
106
+ --color-primary-light: #eaecff;
85
107
  --color-black: #000000;
86
108
  --color-gray-100: #f3f4f6;
87
109
  --color-gray-200: #e5e7eb;
@@ -112,8 +134,8 @@
112
134
  background: var(--color-gray-100);
113
135
  }
114
136
 
115
- .table-header-cell--sorted {
116
- background: var(--color-gray-200);
137
+ .table-header-cell--active {
138
+ background: var(--color-primary-light);
117
139
  color: var(--color-primary);
118
140
  }
119
141
 
@@ -158,7 +180,18 @@
158
180
  margin-top: 4px;
159
181
  }
160
182
 
161
- .table-header-sort-icon .material-icons {
183
+ .table-header-sort-icon:has(+ .table-header-filtered-icon) {
184
+ right: 0;
185
+ }
186
+
187
+ .table-header-filtered-icon {
188
+ position: absolute;
189
+ right: -13px;
190
+ margin-top: 4px;
191
+ }
192
+
193
+ .table-header-sort-icon .material-icons,
194
+ .material-icons-outlined {
162
195
  font-size: var(--font-size-sm);
163
196
  }
164
197
 
@@ -1,4 +1,6 @@
1
1
  <script lang="ts" generics="T">
2
+ import { goto } from '$app/navigation';
3
+ import { page } from '$app/state';
2
4
  import { type Snippet } from 'svelte';
3
5
 
4
6
  import { Button, Search } from '../index.js';
@@ -18,6 +20,7 @@
18
20
  type Updater,
19
21
  type VisibilityState
20
22
  } from './adapter/index.js';
23
+ import AdvancedFilter from './AdvancedFilter.svelte';
21
24
  import Body from './Body.svelte';
22
25
  import ColumnVisibilityDropdown from './ColumnVisibilityDropdown.svelte';
23
26
  import { DEFAULT_MIN_PAGE_LIMIT, DEFAULT_PAGE_LIMIT } from './consts.js';
@@ -27,8 +30,8 @@
27
30
  import Footer from './Footer.svelte';
28
31
  import Header from './Header.svelte';
29
32
  import Skeleton from './Skeleton.svelte';
30
- import type { Pagination } from './types.js';
31
- import { createCheckedColumn } from './util.js';
33
+ import { type Filter, type Pagination } from './types.js';
34
+ import { createCheckedColumn, getPage, getPageLimit } from './util.js';
32
35
 
33
36
  interface Props {
34
37
  columns: ColumnDef<T, any>[];
@@ -40,6 +43,7 @@
40
43
  enableGlobalSearch?: boolean;
41
44
  enableColumnSearch?: boolean;
42
45
  enableColumnVisibility?: boolean;
46
+ enableAdvancedFilter?: boolean;
43
47
  manualPagination?: boolean;
44
48
  pagination?: Pagination;
45
49
  excelSetting?: ExcelSetting;
@@ -59,6 +63,7 @@
59
63
  enableGlobalSearch = true,
60
64
  enableColumnSearch = false,
61
65
  enableColumnVisibility = true,
66
+ enableAdvancedFilter = false,
62
67
  manualPagination = false,
63
68
  pagination,
64
69
  excelSetting = {
@@ -71,33 +76,6 @@
71
76
  bulkActions
72
77
  }: Props = $props();
73
78
 
74
- const getInitialColumnVisibility = (): VisibilityState => {
75
- const visibility: VisibilityState = {};
76
- columns.forEach((column) => {
77
- const columnId =
78
- column.id ?? ('accessorKey' in column ? String(column.accessorKey) : undefined);
79
- if (columnId && column.meta?.isVisible === false) {
80
- visibility[columnId] = false;
81
- }
82
- });
83
- return visibility;
84
- };
85
-
86
- const getInitialPaginationState = (): PaginationState => {
87
- if (!manualPagination) {
88
- return {
89
- pageIndex: 0,
90
- pageSize: minPageSize ? DEFAULT_MIN_PAGE_LIMIT : DEFAULT_PAGE_LIMIT
91
- };
92
- }
93
-
94
- const pageSize = DEFAULT_PAGE_LIMIT;
95
- const pageIndex = 0;
96
-
97
- return { pageIndex, pageSize };
98
- };
99
-
100
- const tableManualPagination = manualPagination ? setPaginationTableContext() : undefined;
101
79
  let globalFilter = $state<string>('');
102
80
  let sorting = $state<SortingState>([]);
103
81
  let columnFilters = $state<ColumnFiltersState>([]);
@@ -106,6 +84,8 @@
106
84
  let columnVisibility = $state<VisibilityState>(getInitialColumnVisibility());
107
85
  let paginationState = $state<PaginationState>(getInitialPaginationState());
108
86
  let tableData = $derived(data);
87
+ const tableManualPagination = manualPagination ? setPaginationTableContext() : undefined;
88
+ const hasData = $derived(data.length > 0);
109
89
 
110
90
  const table = createSvelteTable({
111
91
  get data() {
@@ -172,7 +152,37 @@
172
152
  sortDescFirst: false
173
153
  });
174
154
 
175
- const changeManualPagination = (updatedPaginationState: PaginationState) => {
155
+ const hasFilteredRows = $derived(table.getRowModel().rows.length > 0);
156
+ const selectedRows = $derived(table.getSelectedRowModel().rows);
157
+
158
+ function getInitialColumnVisibility(): VisibilityState {
159
+ const visibility: VisibilityState = {};
160
+ columns.forEach((column) => {
161
+ const columnId =
162
+ column.id ?? ('accessorKey' in column ? String(column.accessorKey) : undefined);
163
+ if (columnId && column.meta?.isVisible === false) {
164
+ visibility[columnId] = false;
165
+ }
166
+ });
167
+ return visibility;
168
+ }
169
+
170
+ function getInitialPaginationState(): PaginationState {
171
+ if (!manualPagination) {
172
+ return {
173
+ pageIndex: 0,
174
+ pageSize: minPageSize ? DEFAULT_MIN_PAGE_LIMIT : DEFAULT_PAGE_LIMIT
175
+ };
176
+ }
177
+
178
+ const currentPage = getPage(page.url.searchParams);
179
+ const pageSize = getPageLimit(page.url.searchParams);
180
+ const pageIndex = Math.max(0, currentPage - 1);
181
+
182
+ return { pageIndex, pageSize };
183
+ }
184
+
185
+ function changeManualPagination(updatedPaginationState: PaginationState) {
176
186
  const pageSizeChanged = updatedPaginationState.pageSize !== paginationState.pageSize;
177
187
 
178
188
  if (pageSizeChanged) {
@@ -182,11 +192,30 @@
182
192
  const currentPageNumber = updatedPaginationState.pageIndex + 1;
183
193
  onpagechange?.(updatedPaginationState.pageSize, currentPageNumber);
184
194
  }
195
+ }
196
+
197
+ const changeFilters = (filters: Filter[]) => {
198
+ const searchParams = new URLSearchParams(page.url.searchParams);
199
+ searchParams.delete('filter');
200
+
201
+ filters.forEach((filter) => {
202
+ const filterValue = buildFilterValue(filter);
203
+ searchParams.append('filter', filterValue);
204
+ });
205
+
206
+ const url = buildUrl(searchParams);
207
+ goto(url);
185
208
  };
186
209
 
187
- const hasData = $derived(data.length > 0);
188
- const hasFilteredRows = $derived(table.getRowModel().rows.length > 0);
189
- const selectedRows = $derived(table.getSelectedRowModel().rows);
210
+ const buildFilterValue = (filter: Filter) => {
211
+ const operator = filter.operator ? `${filter.operator}:` : '';
212
+ return `${operator}${filter.column}:eq:${filter.value}`;
213
+ };
214
+
215
+ const buildUrl = (searchParams: URLSearchParams) => {
216
+ const queryString = searchParams.toString();
217
+ return queryString ? `${page.url.pathname}?${queryString}` : page.url.pathname;
218
+ };
190
219
 
191
220
  $effect(() => {
192
221
  if (manualPagination && pagination) {
@@ -216,8 +245,16 @@
216
245
  ]}
217
246
  >
218
247
  {#if enableGlobalSearch}
219
- <div class="table-search">
220
- <Search bind:value={globalFilter} />
248
+ <div class="table-search-container">
249
+ <div class="table-search">
250
+ <Search bind:value={globalFilter} />
251
+ </div>
252
+ {#if enableAdvancedFilter}
253
+ <AdvancedFilter
254
+ columns={table.getAllLeafColumns().filter((column) => column.getCanHide())}
255
+ onfilterschange={changeFilters}
256
+ />
257
+ {/if}
221
258
  </div>
222
259
  {/if}
223
260
 
@@ -303,6 +340,12 @@
303
340
  width: var(--search-width);
304
341
  }
305
342
 
343
+ .table-search-container {
344
+ display: flex;
345
+ align-items: center;
346
+ gap: var(--spacing-md);
347
+ }
348
+
306
349
  .table-actions {
307
350
  display: flex;
308
351
  gap: var(--spacing-md);
@@ -1,7 +1,7 @@
1
1
  import { type Snippet } from 'svelte';
2
2
  import { type ColumnDef } from './adapter/index.js';
3
3
  import type { ExcelSetting } from './excel-setting.js';
4
- import type { Pagination } from './types.js';
4
+ import { type Pagination } from './types.js';
5
5
  declare function $$render<T>(): {
6
6
  props: {
7
7
  columns: ColumnDef<T, any>[];
@@ -13,6 +13,7 @@ declare function $$render<T>(): {
13
13
  enableGlobalSearch?: boolean;
14
14
  enableColumnSearch?: boolean;
15
15
  enableColumnVisibility?: boolean;
16
+ enableAdvancedFilter?: boolean;
16
17
  manualPagination?: boolean;
17
18
  pagination?: Pagination;
18
19
  excelSetting?: ExcelSetting;