@softwareone/spi-sv5-library 1.16.0 → 1.17.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.
- package/dist/components/clipboard/Clipboard.svelte +74 -0
- package/dist/components/clipboard/Clipboard.svelte.d.ts +9 -0
- package/dist/components/controls/select/Select.svelte +1 -1
- package/dist/components/header/Header.svelte +2 -2
- package/dist/components/header/Header.svelte.d.ts +2 -2
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/menu/Menu.svelte +10 -9
- package/dist/components/menu/Menu.svelte.d.ts +2 -2
- package/dist/components/menu/utils.d.ts +1 -0
- package/dist/components/menu/utils.js +9 -4
- package/dist/components/table/Table.svelte +68 -8
- package/dist/components/table/Table.svelte.d.ts +2 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/local-storage.d.ts +6 -0
- package/dist/utils/local-storage.js +26 -0
- package/dist/utils/url.d.ts +1 -2
- package/dist/utils/url.js +6 -14
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Input, addToast } from '../../index.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
label?: string;
|
|
6
|
+
notification?: string;
|
|
7
|
+
iconOnly?: boolean;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { label, notification = label, iconOnly = false, value }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const copyToClipboard = async (value: string, notification?: string) => {
|
|
14
|
+
await navigator.clipboard.writeText(value);
|
|
15
|
+
addToast({
|
|
16
|
+
message: `${notification || 'Item'} copied to clipboard.`,
|
|
17
|
+
type: 'neutral'
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class={['clipboard', label && 'clipboard-labeled']}>
|
|
23
|
+
{#if label}
|
|
24
|
+
<span class="clipboard-label">{label}</span>
|
|
25
|
+
{/if}
|
|
26
|
+
<div class={[!iconOnly && 'clipboard-row-with-input']}>
|
|
27
|
+
{#if !iconOnly}
|
|
28
|
+
<div class="clipboard-input">
|
|
29
|
+
<Input
|
|
30
|
+
placeholder={label}
|
|
31
|
+
aria-label={label}
|
|
32
|
+
{value}
|
|
33
|
+
readonly
|
|
34
|
+
disableValidationColor
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
{/if}
|
|
38
|
+
<Button
|
|
39
|
+
variant="outline"
|
|
40
|
+
aria-label={notification ?? label ?? 'Copy to clipboard'}
|
|
41
|
+
onclick={() => copyToClipboard(value, notification)}
|
|
42
|
+
>
|
|
43
|
+
<span class="material-icons" style:font-size="var(--spi-font-size-sm)"
|
|
44
|
+
>content_copy</span
|
|
45
|
+
>
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.clipboard {
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.clipboard-labeled {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: var(--spi-size-2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.clipboard-label {
|
|
62
|
+
font-size: var(--spi-font-size-sm);
|
|
63
|
+
font-weight: var(--spi-font-weight-medium);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.clipboard-row-with-input {
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: var(--spi-size-2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.clipboard-input {
|
|
72
|
+
flex: 1;
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { AnnouncementItem, MenuItem } from '../../index.js';
|
|
2
|
+
import type { AnnouncementItem, MainMenu, MenuItem } from '../../index.js';
|
|
3
3
|
import Announcement from '../announcement/Announcement.svelte';
|
|
4
4
|
import Menu from '../menu/Menu.svelte';
|
|
5
5
|
import Waffle from '../waffle/Waffle.svelte';
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
userName?: string;
|
|
18
18
|
hideLoader?: boolean;
|
|
19
19
|
hideAnnouncement?: boolean;
|
|
20
|
-
menuItems?:
|
|
20
|
+
menuItems?: MainMenu[];
|
|
21
21
|
waffleItems?: WaffleItem[];
|
|
22
22
|
profileMenuItems?: MenuItem[];
|
|
23
23
|
announcementItems?: AnnouncementItem[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnnouncementItem, MenuItem } from '../../index.js';
|
|
1
|
+
import type { AnnouncementItem, MainMenu, MenuItem } from '../../index.js';
|
|
2
2
|
import type { WaffleItem } from '../waffle/types.js';
|
|
3
3
|
interface Props {
|
|
4
4
|
title?: string;
|
|
@@ -9,7 +9,7 @@ interface Props {
|
|
|
9
9
|
userName?: string;
|
|
10
10
|
hideLoader?: boolean;
|
|
11
11
|
hideAnnouncement?: boolean;
|
|
12
|
-
menuItems?:
|
|
12
|
+
menuItems?: MainMenu[];
|
|
13
13
|
waffleItems?: WaffleItem[];
|
|
14
14
|
profileMenuItems?: MenuItem[];
|
|
15
15
|
announcementItems?: AnnouncementItem[];
|
|
@@ -3,6 +3,7 @@ export { default as Avatar } from './avatar/Avatar.svelte';
|
|
|
3
3
|
export { default as Button } from './button/Button.svelte';
|
|
4
4
|
export { default as Card } from './card/Card.svelte';
|
|
5
5
|
export { default as Chips } from './chips/Chips.svelte';
|
|
6
|
+
export { default as Clipboard } from './clipboard/Clipboard.svelte';
|
|
6
7
|
export { default as ConfirmationModal } from './confirmation/Confirmation.svelte';
|
|
7
8
|
export { default as AttachFile } from './controls/attach-file/AttachFile.svelte';
|
|
8
9
|
export { default as Input } from './controls/input/Input.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Avatar } from './avatar/Avatar.svelte';
|
|
|
3
3
|
export { default as Button } from './button/Button.svelte';
|
|
4
4
|
export { default as Card } from './card/Card.svelte';
|
|
5
5
|
export { default as Chips } from './chips/Chips.svelte';
|
|
6
|
+
export { default as Clipboard } from './clipboard/Clipboard.svelte';
|
|
6
7
|
export { default as ConfirmationModal } from './confirmation/Confirmation.svelte';
|
|
7
8
|
export { default as AttachFile } from './controls/attach-file/AttachFile.svelte';
|
|
8
9
|
export { default as Input } from './controls/input/Input.svelte';
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
2
3
|
import { fade } from 'svelte/transition';
|
|
3
4
|
|
|
4
|
-
import { existRoute,
|
|
5
|
+
import { existRoute, getSectionPrefix } from '../../utils/index.js';
|
|
5
6
|
import MainMenuItem from './MenuItem.svelte';
|
|
6
|
-
import type {
|
|
7
|
+
import type { MainMenu } from './types.js';
|
|
8
|
+
import { hasMenuItems } from './utils.js';
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
9
|
-
menuItems:
|
|
11
|
+
menuItems: MainMenu[];
|
|
10
12
|
showMenu: boolean;
|
|
11
13
|
}
|
|
12
14
|
|
|
@@ -15,14 +17,13 @@
|
|
|
15
17
|
let activeItem = $state('');
|
|
16
18
|
|
|
17
19
|
const setActiveMenuItem = () => {
|
|
18
|
-
activeItem =
|
|
19
|
-
menuItems.find((menuItem: MenuItem) => isActiveMenuItem(menuItem.url))?.text || '';
|
|
20
|
+
activeItem = menuItems.find((menu) => isActiveMenuItem(menu))?.text || '';
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
const isActiveMenuItem = (
|
|
23
|
-
const pathname =
|
|
24
|
-
const
|
|
25
|
-
return existRoute(
|
|
23
|
+
const isActiveMenuItem = (menu: MainMenu): boolean => {
|
|
24
|
+
const pathname = page.url.pathname;
|
|
25
|
+
const routeToMatch = hasMenuItems(menu.children) ? getSectionPrefix(menu.url) : menu.url;
|
|
26
|
+
return existRoute(routeToMatch, pathname);
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
const onHandleMenu = () => {
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { page } from '$app/state';
|
|
2
|
+
import { existRoute, getSectionPrefix } from '../../utils/index.js';
|
|
3
|
+
export const hasMenuItems = (children) => {
|
|
4
|
+
return Boolean(children?.some((child) => child.menuItems.length));
|
|
5
|
+
};
|
|
2
6
|
export const getSubMenuItemsFromMenu = (items, excludedRoutes) => {
|
|
3
|
-
const pathname =
|
|
7
|
+
const pathname = page.url.pathname;
|
|
4
8
|
if (excludedRoutes?.includes(pathname))
|
|
5
9
|
return [];
|
|
6
|
-
const firstPathSegment = getFirstPathSegment(pathname);
|
|
7
10
|
const matchedMenu = items.find((menu) => {
|
|
8
|
-
|
|
11
|
+
if (!hasMenuItems(menu.children))
|
|
12
|
+
return false;
|
|
13
|
+
return existRoute(getSectionPrefix(menu.url), pathname);
|
|
9
14
|
});
|
|
10
15
|
return matchedMenu?.children ?? [];
|
|
11
16
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object">
|
|
2
|
+
import { browser } from '$app/environment';
|
|
2
3
|
import { goto } from '$app/navigation';
|
|
3
4
|
import { page } from '$app/state';
|
|
4
|
-
import { type Snippet } from 'svelte';
|
|
5
|
+
import { onMount, type Snippet } from 'svelte';
|
|
5
6
|
|
|
6
7
|
import { Button, Search } from '../../index.js';
|
|
8
|
+
import { LocalStorage } from '../../utils/local-storage.js';
|
|
7
9
|
import {
|
|
8
10
|
createSvelteTable,
|
|
9
11
|
getCoreRowModel,
|
|
@@ -40,7 +42,8 @@
|
|
|
40
42
|
createCheckedColumn,
|
|
41
43
|
isCursorPagination,
|
|
42
44
|
isValidPage,
|
|
43
|
-
isValidPageSize
|
|
45
|
+
isValidPageSize,
|
|
46
|
+
sanitizeFilters
|
|
44
47
|
} from './utils.js';
|
|
45
48
|
|
|
46
49
|
interface BaseProps<T> {
|
|
@@ -54,6 +57,8 @@
|
|
|
54
57
|
enableColumnSearch?: boolean;
|
|
55
58
|
enableColumnVisibility?: boolean;
|
|
56
59
|
enableAdvancedFilter?: boolean;
|
|
60
|
+
stickyFilter?: boolean;
|
|
61
|
+
stickyFilterKey?: string;
|
|
57
62
|
serverSide?: boolean;
|
|
58
63
|
pagination?: Pagination;
|
|
59
64
|
initialPage?: number;
|
|
@@ -101,6 +106,8 @@
|
|
|
101
106
|
enableColumnSearch = false,
|
|
102
107
|
enableColumnVisibility = true,
|
|
103
108
|
enableAdvancedFilter = false,
|
|
109
|
+
stickyFilter = false,
|
|
110
|
+
stickyFilterKey,
|
|
104
111
|
serverSide = false,
|
|
105
112
|
pagination,
|
|
106
113
|
initialPage,
|
|
@@ -118,6 +125,7 @@
|
|
|
118
125
|
}: Props<T> = $props();
|
|
119
126
|
|
|
120
127
|
const tableManualPagination = serverSide ? setPaginationTableContext() : undefined;
|
|
128
|
+
const STICKY_FILTER_STORAGE_PREFIX = 'spi-table-sticky-filters';
|
|
121
129
|
|
|
122
130
|
let globalFilter = $state<string>('');
|
|
123
131
|
let sorting = $state<SortingState>([]);
|
|
@@ -272,17 +280,58 @@
|
|
|
272
280
|
rowSelection = {};
|
|
273
281
|
};
|
|
274
282
|
|
|
283
|
+
const getStickyFilterStorageKey = () => {
|
|
284
|
+
const key = stickyFilterKey?.trim() ? stickyFilterKey : page.url.pathname;
|
|
285
|
+
return `${STICKY_FILTER_STORAGE_PREFIX}:${key}`;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const isStickyFilterActive = () => browser && stickyFilter;
|
|
289
|
+
|
|
290
|
+
const readStickyFilterValues = (): string[] =>
|
|
291
|
+
isStickyFilterActive()
|
|
292
|
+
? (LocalStorage.getItem<string[]>(getStickyFilterStorageKey()) ?? [])
|
|
293
|
+
: [];
|
|
294
|
+
|
|
295
|
+
const writeStickyFilterValues = (filters: string[]) => {
|
|
296
|
+
if (!isStickyFilterActive()) return;
|
|
297
|
+
|
|
298
|
+
const storageKey = getStickyFilterStorageKey();
|
|
299
|
+
if (!filters.length) {
|
|
300
|
+
LocalStorage.removeItem(storageKey);
|
|
301
|
+
} else {
|
|
302
|
+
LocalStorage.setItem(storageKey, filters);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const restoreStickyFilters = () => {
|
|
307
|
+
if (!isStickyFilterActive() || !enableAdvancedFilter) return;
|
|
308
|
+
|
|
309
|
+
const currentFilters = page.url.searchParams.getAll('filter');
|
|
310
|
+
if (currentFilters.length) {
|
|
311
|
+
writeStickyFilterValues(sanitizeFilters(currentFilters).map(buildFilterValue));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const storedFilters = sanitizeFilters(readStickyFilterValues()).map(buildFilterValue);
|
|
316
|
+
if (!storedFilters.length) return;
|
|
317
|
+
|
|
318
|
+
const searchParams = new URLSearchParams(page.url.searchParams);
|
|
319
|
+
storedFilters.forEach((filterValue) => {
|
|
320
|
+
searchParams.append('filter', filterValue);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
goto(buildUrl(searchParams), { replaceState: true, noScroll: true, keepFocus: true });
|
|
324
|
+
};
|
|
325
|
+
|
|
275
326
|
const changeFilters = (filters: Filter[]) => {
|
|
276
327
|
const searchParams = new URLSearchParams(page.url.searchParams);
|
|
277
328
|
searchParams.delete('filter');
|
|
278
329
|
|
|
279
|
-
filters.
|
|
280
|
-
|
|
281
|
-
searchParams.append('filter', filterValue);
|
|
282
|
-
});
|
|
330
|
+
const filterValues = filters.map(buildFilterValue);
|
|
331
|
+
filterValues.forEach((filterValue) => searchParams.append('filter', filterValue));
|
|
283
332
|
|
|
284
|
-
|
|
285
|
-
goto(
|
|
333
|
+
writeStickyFilterValues(filterValues);
|
|
334
|
+
goto(buildUrl(searchParams));
|
|
286
335
|
};
|
|
287
336
|
|
|
288
337
|
const buildFilterValue = (filter: Filter) => {
|
|
@@ -295,6 +344,13 @@
|
|
|
295
344
|
return queryString ? `${page.url.pathname}?${queryString}` : page.url.pathname;
|
|
296
345
|
};
|
|
297
346
|
|
|
347
|
+
$effect(() => {
|
|
348
|
+
columnFilters = sanitizeFilters(page.url.searchParams.getAll('filter')).map((filter) => ({
|
|
349
|
+
id: filter.column,
|
|
350
|
+
value: filter.value
|
|
351
|
+
}));
|
|
352
|
+
});
|
|
353
|
+
|
|
298
354
|
$effect(() => {
|
|
299
355
|
if (serverSide && pagination) {
|
|
300
356
|
$tableManualPagination = { ...pagination };
|
|
@@ -310,6 +366,10 @@
|
|
|
310
366
|
if (!enableChecked) return;
|
|
311
367
|
onclearselection?.(clearSelection);
|
|
312
368
|
});
|
|
369
|
+
|
|
370
|
+
onMount(() => {
|
|
371
|
+
restoreStickyFilters();
|
|
372
|
+
});
|
|
313
373
|
</script>
|
|
314
374
|
|
|
315
375
|
<section class="table-container">
|
|
@@ -13,6 +13,8 @@ interface BaseProps<T> {
|
|
|
13
13
|
enableColumnSearch?: boolean;
|
|
14
14
|
enableColumnVisibility?: boolean;
|
|
15
15
|
enableAdvancedFilter?: boolean;
|
|
16
|
+
stickyFilter?: boolean;
|
|
17
|
+
stickyFilterKey?: string;
|
|
16
18
|
serverSide?: boolean;
|
|
17
19
|
pagination?: Pagination;
|
|
18
20
|
initialPage?: number;
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const LocalStorage = {
|
|
2
|
+
getItem: (key) => {
|
|
3
|
+
const item = localStorage.getItem(key);
|
|
4
|
+
if (item === null)
|
|
5
|
+
return null;
|
|
6
|
+
const cachedItem = JSON.parse(item);
|
|
7
|
+
if (cachedItem.expiresAt && Date.now() > cachedItem.expiresAt) {
|
|
8
|
+
localStorage.removeItem(key);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return cachedItem.value;
|
|
12
|
+
},
|
|
13
|
+
setItem: (key, value, expiresInMs) => {
|
|
14
|
+
const stored = {
|
|
15
|
+
value,
|
|
16
|
+
expiresAt: expiresInMs ? Date.now() + expiresInMs : undefined
|
|
17
|
+
};
|
|
18
|
+
localStorage.setItem(key, JSON.stringify(stored));
|
|
19
|
+
},
|
|
20
|
+
removeItem: (key) => {
|
|
21
|
+
localStorage.removeItem(key);
|
|
22
|
+
},
|
|
23
|
+
clear: () => {
|
|
24
|
+
localStorage.clear();
|
|
25
|
+
}
|
|
26
|
+
};
|
package/dist/utils/url.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
export declare const getFirstPathSegment: (pathname: string) => string;
|
|
1
|
+
export declare const getSectionPrefix: (url: string) => string;
|
|
3
2
|
export declare const existRoute: (url: string, pathname: string) => boolean;
|
package/dist/utils/url.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const pathname = page.url.pathname;
|
|
6
|
-
const withoutBase = base && pathname.startsWith(base) ? pathname.slice(base.length) : pathname;
|
|
7
|
-
return normalizePathname(withoutBase);
|
|
8
|
-
};
|
|
9
|
-
export const getFirstPathSegment = (pathname) => {
|
|
10
|
-
const matchedPath = /^\/[^/]+/.exec(pathname);
|
|
11
|
-
return matchedPath?.[0] ?? '/';
|
|
1
|
+
export const getSectionPrefix = (url) => {
|
|
2
|
+
const segments = url.split('/');
|
|
3
|
+
segments.pop();
|
|
4
|
+
return segments.join('/') || '/';
|
|
12
5
|
};
|
|
13
6
|
export const existRoute = (url, pathname) => {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
return regex.test(normalizedPathname);
|
|
7
|
+
const regex = new RegExp(String.raw `^${url}(?![\w-])`);
|
|
8
|
+
return regex.test(pathname);
|
|
17
9
|
};
|