@warkypublic/svelix 0.1.35 → 0.1.37
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/Boxer/Boxer.svelte +192 -45
- package/dist/components/Boxer/Boxer.svelte.d.ts +1 -0
- package/dist/components/Boxer/BoxerBaseAdapter.d.ts +31 -0
- package/dist/components/Boxer/BoxerBaseAdapter.js +83 -0
- package/dist/components/Boxer/BoxerResolveSpecAdapter.d.ts +5 -14
- package/dist/components/Boxer/BoxerResolveSpecAdapter.js +6 -49
- package/dist/components/Boxer/BoxerRestHeaderSpecAdapter.d.ts +5 -14
- package/dist/components/Boxer/BoxerRestHeaderSpecAdapter.js +6 -49
- package/dist/components/Boxer/index.d.ts +1 -0
- package/dist/components/Boxer/index.js +1 -0
- package/dist/components/Boxer/store.d.ts +10 -1
- package/dist/components/Boxer/store.js +135 -43
- package/dist/components/Boxer/types.d.ts +17 -10
- package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +10 -7
- package/dist/components/GlobalStateStore/GlobalStateStore.js +21 -6
- package/dist/components/GlobalStateStore/GlobalStateStore.utils.d.ts +1 -1
- package/dist/components/GlobalStateStore/GlobalStateStore.utils.js +3 -1
- package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +25 -9
- package/dist/components/Gridler/components/Gridler.svelte +27 -10
- package/dist/components/Gridler/components/GridlerCanvas.svelte +30 -61
- package/dist/components/Gridler/components/GridlerCanvas.svelte.d.ts +1 -0
- package/dist/components/Gridler/components/GridlerFull.svelte +70 -26
- package/dist/components/Gridler/renderers/ImageRenderer.js +14 -4
- package/dist/components/Gridler/renderers/MarkdownRenderer.d.ts +3 -2
- package/dist/components/Gridler/renderers/MarkdownRenderer.js +12 -3
- package/dist/components/Gridler/renderers/PercentageRenderer.js +11 -6
- package/dist/components/Gridler/renderers/index.js +12 -4
- package/dist/components/Gridler/renderers/shared.d.ts +5 -0
- package/dist/components/Gridler/renderers/shared.js +15 -5
- package/dist/components/Gridler/types.d.ts +16 -0
- package/dist/components/Screenshot/Screenshot.util.js +5 -4
- package/package.json +30 -30
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as Boxer } from './Boxer.svelte';
|
|
2
2
|
export { default as BoxerTarget } from './BoxerTarget.svelte';
|
|
3
|
+
export { BoxerBaseAdapter } from './BoxerBaseAdapter.js';
|
|
3
4
|
export { BoxerResolveSpecAdapter } from './BoxerResolveSpecAdapter.js';
|
|
4
5
|
export { BoxerRestHeaderSpecAdapter } from './BoxerRestHeaderSpecAdapter.js';
|
|
5
6
|
export { createBoxerStore } from './store';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as Boxer } from './Boxer.svelte';
|
|
2
2
|
export { default as BoxerTarget } from './BoxerTarget.svelte';
|
|
3
|
+
export { BoxerBaseAdapter } from './BoxerBaseAdapter.js';
|
|
3
4
|
export { BoxerResolveSpecAdapter } from './BoxerResolveSpecAdapter.js';
|
|
4
5
|
export { BoxerRestHeaderSpecAdapter } from './BoxerRestHeaderSpecAdapter.js';
|
|
5
6
|
export { createBoxerStore } from './store';
|
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
import type { BoxerItem, BoxerProps } from './types';
|
|
2
|
-
export interface BoxerStoreState extends BoxerProps {
|
|
2
|
+
export interface BoxerStoreState extends Omit<BoxerProps, 'error'> {
|
|
3
3
|
boxerData: Array<BoxerItem>;
|
|
4
|
+
/** Runtime fetch error, distinct from the `error` prop used for form validation. */
|
|
5
|
+
error: string | null;
|
|
4
6
|
hasMore: boolean;
|
|
5
7
|
input: string;
|
|
6
8
|
isFetching: boolean;
|
|
7
9
|
opened: boolean;
|
|
8
10
|
page: number;
|
|
9
11
|
search: string;
|
|
12
|
+
selectedItems: Array<BoxerItem>;
|
|
10
13
|
selectedOptionIndex: number;
|
|
11
14
|
total: number;
|
|
12
15
|
}
|
|
13
16
|
export declare function createBoxerStore(initialProps: BoxerProps): {
|
|
17
|
+
addSelectedItem: (item: BoxerItem) => void;
|
|
18
|
+
cancel: () => void;
|
|
19
|
+
clearSelectedItems: () => void;
|
|
14
20
|
fetchData: (search?: string, reset?: boolean) => Promise<void>;
|
|
15
21
|
fetchMoreOnBottomReached: (target: HTMLDivElement) => void;
|
|
16
22
|
loadMore: () => Promise<void>;
|
|
23
|
+
refetch: () => Promise<void>;
|
|
24
|
+
removeSelectedItem: (value: unknown) => void;
|
|
17
25
|
set: (this: void, value: BoxerStoreState) => void;
|
|
18
26
|
setInput: (input: string) => void;
|
|
19
27
|
setOpened: (opened: boolean) => void;
|
|
20
28
|
setSearch: (search: string) => void;
|
|
29
|
+
setSelectedItems: (items: Array<BoxerItem>) => void;
|
|
21
30
|
setSelectedOptionIndex: (index: number) => void;
|
|
22
31
|
subscribe: (this: void, run: import("svelte/store").Subscriber<BoxerStoreState>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
|
|
23
32
|
update: (this: void, updater: import("svelte/store").Updater<BoxerStoreState>) => void;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { writable } from 'svelte/store';
|
|
1
|
+
import { get, writable } from 'svelte/store';
|
|
2
|
+
function valueKey(v) {
|
|
3
|
+
return typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);
|
|
4
|
+
}
|
|
5
|
+
function matchesLocal(item, needle, columns) {
|
|
6
|
+
const lower = needle.toLowerCase();
|
|
7
|
+
return columns.some((col) => String(item[col] ?? '').toLowerCase().includes(lower));
|
|
8
|
+
}
|
|
2
9
|
export function createBoxerStore(initialProps) {
|
|
3
|
-
const { data = [], dataSource = 'local', pageSize = 50, ...rest } = initialProps;
|
|
10
|
+
const { data = [], dataSource = 'local', error: _propError, pageSize = 50, ...rest } = initialProps;
|
|
11
|
+
void _propError;
|
|
4
12
|
const initial = {
|
|
5
13
|
...rest,
|
|
6
14
|
boxerData: data,
|
|
7
15
|
data,
|
|
8
16
|
dataSource,
|
|
17
|
+
error: null,
|
|
9
18
|
hasMore: dataSource === 'server',
|
|
10
19
|
input: '',
|
|
11
20
|
isFetching: false,
|
|
@@ -13,74 +22,117 @@ export function createBoxerStore(initialProps) {
|
|
|
13
22
|
page: 0,
|
|
14
23
|
pageSize,
|
|
15
24
|
search: '',
|
|
25
|
+
selectedItems: [],
|
|
16
26
|
selectedOptionIndex: -1,
|
|
17
27
|
total: data.length,
|
|
18
28
|
};
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return current;
|
|
24
|
-
}
|
|
25
|
-
let lastResetSearch;
|
|
29
|
+
const store = writable(initial);
|
|
30
|
+
const { subscribe, update, set } = store;
|
|
31
|
+
let requestCounter = 0;
|
|
32
|
+
let activeController = null;
|
|
26
33
|
async function fetchData(search, reset) {
|
|
27
|
-
const state =
|
|
34
|
+
const state = get(store);
|
|
35
|
+
// Local mode — synchronous filter
|
|
28
36
|
if (state.dataSource === 'local' || !state.onAPICall) {
|
|
29
37
|
const localData = state.data ?? [];
|
|
30
38
|
if (!search) {
|
|
31
|
-
update((s) => ({
|
|
39
|
+
update((s) => ({
|
|
40
|
+
...s,
|
|
41
|
+
boxerData: localData,
|
|
42
|
+
error: null,
|
|
43
|
+
hasMore: false,
|
|
44
|
+
total: localData.length,
|
|
45
|
+
}));
|
|
32
46
|
return;
|
|
33
47
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
48
|
+
const cols = state.searchColumns && state.searchColumns.length > 0
|
|
49
|
+
? state.searchColumns
|
|
50
|
+
: ['label'];
|
|
51
|
+
const filtered = localData.filter((item) => matchesLocal(item, search, cols));
|
|
52
|
+
update((s) => ({
|
|
53
|
+
...s,
|
|
54
|
+
boxerData: filtered,
|
|
55
|
+
error: null,
|
|
56
|
+
hasMore: false,
|
|
57
|
+
total: filtered.length,
|
|
58
|
+
}));
|
|
36
59
|
return;
|
|
37
60
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
61
|
+
// Server mode — cancel in-flight, issue new request
|
|
62
|
+
const onAPICall = state.onAPICall;
|
|
63
|
+
activeController?.abort();
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
activeController = controller;
|
|
66
|
+
const thisRequestId = ++requestCounter;
|
|
67
|
+
const currentPage = reset ? 0 : state.page;
|
|
68
|
+
update((s) => ({ ...s, error: null, isFetching: true }));
|
|
69
|
+
try {
|
|
70
|
+
const result = await onAPICall({
|
|
71
|
+
page: currentPage,
|
|
72
|
+
pageSize: state.pageSize ?? 50,
|
|
73
|
+
search,
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
});
|
|
76
|
+
if (thisRequestId !== requestCounter)
|
|
77
|
+
return;
|
|
78
|
+
update((s) => {
|
|
79
|
+
const boxerData = reset ? result.data : [...(s.boxerData ?? []), ...result.data];
|
|
80
|
+
return {
|
|
81
|
+
...s,
|
|
82
|
+
boxerData,
|
|
83
|
+
error: null,
|
|
84
|
+
hasMore: boxerData.length < result.total,
|
|
85
|
+
isFetching: false,
|
|
86
|
+
page: reset ? 0 : s.page,
|
|
87
|
+
total: result.total,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
// Aborted by a newer fetch — newer fetch owns the state now.
|
|
93
|
+
if (controller.signal.aborted)
|
|
41
94
|
return;
|
|
42
|
-
|
|
43
|
-
if (reset && search === lastResetSearch && state.boxerData.length > 0)
|
|
95
|
+
if (thisRequestId !== requestCounter)
|
|
44
96
|
return;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const boxerData = reset ? result.data : [...(s.boxerData ?? []), ...result.data];
|
|
53
|
-
return {
|
|
54
|
-
...s,
|
|
55
|
-
boxerData,
|
|
56
|
-
hasMore: boxerData.length < result.total,
|
|
57
|
-
isFetching: false,
|
|
58
|
-
page: reset ? 0 : s.page,
|
|
59
|
-
total: result.total,
|
|
60
|
-
};
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
console.error('Boxer fetchData error:', error);
|
|
65
|
-
update((s) => ({ ...s, isFetching: false }));
|
|
66
|
-
}
|
|
97
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
+
console.error('Boxer fetchData error:', err);
|
|
99
|
+
update((s) => ({ ...s, error: message, isFetching: false }));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (activeController === controller)
|
|
103
|
+
activeController = null;
|
|
67
104
|
}
|
|
68
105
|
}
|
|
69
106
|
async function loadMore() {
|
|
70
|
-
const state =
|
|
107
|
+
const state = get(store);
|
|
71
108
|
if (!state.hasMore || state.isFetching)
|
|
72
109
|
return;
|
|
110
|
+
const prevPage = state.page;
|
|
73
111
|
update((s) => ({ ...s, page: s.page + 1 }));
|
|
74
112
|
await fetchData(state.search);
|
|
113
|
+
// Revert the page bump if the fetch errored (so next retry fetches the same page).
|
|
114
|
+
const after = get(store);
|
|
115
|
+
if (after.error && after.page === prevPage + 1) {
|
|
116
|
+
update((s) => ({ ...s, page: prevPage }));
|
|
117
|
+
}
|
|
75
118
|
}
|
|
76
119
|
function fetchMoreOnBottomReached(target) {
|
|
77
|
-
const state =
|
|
120
|
+
const state = get(store);
|
|
78
121
|
if (!state.hasMore || state.isFetching)
|
|
79
122
|
return;
|
|
80
123
|
const pct = (target.scrollTop + target.clientHeight) / target.scrollHeight;
|
|
81
124
|
if (pct > 0.8)
|
|
82
125
|
loadMore();
|
|
83
126
|
}
|
|
127
|
+
function refetch() {
|
|
128
|
+
const state = get(store);
|
|
129
|
+
return fetchData(state.search, true);
|
|
130
|
+
}
|
|
131
|
+
function cancel() {
|
|
132
|
+
activeController?.abort();
|
|
133
|
+
activeController = null;
|
|
134
|
+
update((s) => (s.isFetching ? { ...s, isFetching: false } : s));
|
|
135
|
+
}
|
|
84
136
|
function setOpened(opened) {
|
|
85
137
|
update((s) => ({ ...s, opened }));
|
|
86
138
|
}
|
|
@@ -93,17 +145,57 @@ export function createBoxerStore(initialProps) {
|
|
|
93
145
|
function setSelectedOptionIndex(index) {
|
|
94
146
|
update((s) => ({ ...s, selectedOptionIndex: index }));
|
|
95
147
|
}
|
|
148
|
+
function addSelectedItem(item) {
|
|
149
|
+
update((s) => {
|
|
150
|
+
const key = valueKey(item.value);
|
|
151
|
+
if (s.selectedItems.some((i) => valueKey(i.value) === key))
|
|
152
|
+
return s;
|
|
153
|
+
return { ...s, selectedItems: [...s.selectedItems, item] };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function removeSelectedItem(value) {
|
|
157
|
+
update((s) => {
|
|
158
|
+
const key = valueKey(value);
|
|
159
|
+
const next = s.selectedItems.filter((i) => valueKey(i.value) !== key);
|
|
160
|
+
if (next.length === s.selectedItems.length)
|
|
161
|
+
return s;
|
|
162
|
+
return { ...s, selectedItems: next };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function setSelectedItems(items) {
|
|
166
|
+
update((s) => ({ ...s, selectedItems: items }));
|
|
167
|
+
}
|
|
168
|
+
function clearSelectedItems() {
|
|
169
|
+
update((s) => (s.selectedItems.length === 0 ? s : { ...s, selectedItems: [] }));
|
|
170
|
+
}
|
|
96
171
|
function updateProps(props) {
|
|
97
|
-
update((s) =>
|
|
172
|
+
update((s) => {
|
|
173
|
+
const merged = { ...s, ...props };
|
|
174
|
+
// Keep boxerData consistent with data in local mode.
|
|
175
|
+
if ((props.data !== undefined || props.dataSource !== undefined) &&
|
|
176
|
+
merged.dataSource === 'local') {
|
|
177
|
+
const localData = merged.data ?? [];
|
|
178
|
+
merged.boxerData = localData;
|
|
179
|
+
merged.total = localData.length;
|
|
180
|
+
merged.hasMore = false;
|
|
181
|
+
}
|
|
182
|
+
return merged;
|
|
183
|
+
});
|
|
98
184
|
}
|
|
99
185
|
return {
|
|
186
|
+
addSelectedItem,
|
|
187
|
+
cancel,
|
|
188
|
+
clearSelectedItems,
|
|
100
189
|
fetchData,
|
|
101
190
|
fetchMoreOnBottomReached,
|
|
102
191
|
loadMore,
|
|
192
|
+
refetch,
|
|
193
|
+
removeSelectedItem,
|
|
103
194
|
set,
|
|
104
195
|
setInput,
|
|
105
196
|
setOpened,
|
|
106
197
|
setSearch,
|
|
198
|
+
setSelectedItems,
|
|
107
199
|
setSelectedOptionIndex,
|
|
108
200
|
subscribe,
|
|
109
201
|
update,
|
|
@@ -24,15 +24,19 @@ export interface BoxerAdapterConfig {
|
|
|
24
24
|
/** Custom row-to-BoxerItem mapping. When omitted, `labelField` and `valueField` are mapped automatically. */
|
|
25
25
|
mapItem?: (row: any) => BoxerItem;
|
|
26
26
|
}
|
|
27
|
+
export interface BoxerFetchParams {
|
|
28
|
+
page: number;
|
|
29
|
+
pageSize: number;
|
|
30
|
+
search?: string;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
27
33
|
export interface BoxerServerAdapter {
|
|
28
|
-
fetch(params: {
|
|
29
|
-
page: number;
|
|
30
|
-
pageSize: number;
|
|
31
|
-
search?: string;
|
|
32
|
-
}): Promise<{
|
|
34
|
+
fetch(params: BoxerFetchParams): Promise<{
|
|
33
35
|
data: BoxerItem[];
|
|
34
36
|
total: number;
|
|
35
37
|
}>;
|
|
38
|
+
/** Resolve BoxerItems for a list of selected values. Used to display labels for preloaded values not in the current page. */
|
|
39
|
+
resolveByValue?(values: any[], signal?: AbortSignal): Promise<BoxerItem[]>;
|
|
36
40
|
}
|
|
37
41
|
export type BoxerItem = {
|
|
38
42
|
[key: string]: any;
|
|
@@ -45,6 +49,8 @@ export interface BoxerProps {
|
|
|
45
49
|
clearable?: boolean;
|
|
46
50
|
data?: Array<BoxerItem>;
|
|
47
51
|
dataSource?: BoxerDataSource;
|
|
52
|
+
/** Debounce interval in ms for search-triggered fetches. Defaults to `300`. */
|
|
53
|
+
debounceMs?: number;
|
|
48
54
|
disablePortal?: boolean;
|
|
49
55
|
disabled?: boolean;
|
|
50
56
|
error?: string;
|
|
@@ -54,21 +60,21 @@ export interface BoxerProps {
|
|
|
54
60
|
mah?: number;
|
|
55
61
|
multiSelect?: boolean;
|
|
56
62
|
name?: string;
|
|
57
|
-
onAPICall?: (params: {
|
|
58
|
-
page: number;
|
|
59
|
-
pageSize: number;
|
|
60
|
-
search?: string;
|
|
61
|
-
}) => Promise<{
|
|
63
|
+
onAPICall?: (params: BoxerFetchParams) => Promise<{
|
|
62
64
|
data: Array<BoxerItem>;
|
|
63
65
|
total: number;
|
|
64
66
|
}>;
|
|
65
67
|
onBufferChange?: (buffer: Array<BoxerItem> | BoxerItem | null) => void;
|
|
66
68
|
onChange?: (value: any | Array<any>) => void;
|
|
69
|
+
/** Resolve labels for preloaded values that may not be in the current page (server-mode initial values). */
|
|
70
|
+
onResolveValues?: (values: any[], signal?: AbortSignal) => Promise<Array<BoxerItem>>;
|
|
67
71
|
openOnClear?: boolean;
|
|
68
72
|
pageSize?: number;
|
|
69
73
|
placeholder?: string;
|
|
70
74
|
rightSection?: Snippet;
|
|
71
75
|
searchable?: boolean;
|
|
76
|
+
/** Columns to search against in `local` mode. Falls back to `['label']`. */
|
|
77
|
+
searchColumns?: string[];
|
|
72
78
|
selectFirst?: boolean;
|
|
73
79
|
showAll?: boolean;
|
|
74
80
|
value?: any | Array<any>;
|
|
@@ -79,5 +85,6 @@ export interface BoxerRef {
|
|
|
79
85
|
focus: () => void;
|
|
80
86
|
getValue: () => any | Array<any>;
|
|
81
87
|
open: () => void;
|
|
88
|
+
refetch: () => void;
|
|
82
89
|
setValue: (value: any | Array<any>) => void;
|
|
83
90
|
}
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
isDark = nextIsDark;
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
if (!container) return;
|
|
39
|
+
editor = monaco.editor.create(container, {
|
|
39
40
|
value: '',
|
|
40
41
|
language: lang,
|
|
41
42
|
readOnly: readonly,
|
|
@@ -43,15 +44,16 @@
|
|
|
43
44
|
automaticLayout: true,
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
const createdEditor = editor;
|
|
48
|
+
createdEditor.onDidChangeModelContent(() => {
|
|
47
49
|
if (changeGuard) return;
|
|
48
|
-
const content =
|
|
50
|
+
const content = createdEditor.getValue();
|
|
49
51
|
onChange?.(new Blob([content]));
|
|
50
52
|
});
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
createdEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
|
53
55
|
if (!onSave) return;
|
|
54
|
-
onSave(new Blob([
|
|
56
|
+
onSave(new Blob([createdEditor.getValue()]));
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
function doInsertText(text: string): void {
|
|
@@ -74,10 +76,11 @@
|
|
|
74
76
|
// Sync incoming Blob → editor text
|
|
75
77
|
$effect(() => {
|
|
76
78
|
if (!editor || !value) return;
|
|
79
|
+
const currentEditor = editor;
|
|
77
80
|
blobToString(value).then((text) => {
|
|
78
|
-
if (
|
|
81
|
+
if (currentEditor.getValue() !== text) {
|
|
79
82
|
changeGuard = true;
|
|
80
|
-
|
|
83
|
+
currentEditor.setValue(text);
|
|
81
84
|
changeGuard = false;
|
|
82
85
|
}
|
|
83
86
|
});
|
|
@@ -108,6 +108,7 @@ const computeIsLoggedIn = (session) => {
|
|
|
108
108
|
}
|
|
109
109
|
return !isSessionExpired(session);
|
|
110
110
|
};
|
|
111
|
+
const VALIDATION_FAILURE_BACKOFF_MS = 5000;
|
|
111
112
|
const createGlobalStateStore = () => {
|
|
112
113
|
const initialState = createInitialState();
|
|
113
114
|
let isStorageInitialized = false;
|
|
@@ -115,9 +116,14 @@ const createGlobalStateStore = () => {
|
|
|
115
116
|
let operationLock = Promise.resolve();
|
|
116
117
|
let hasAutoValidated = false;
|
|
117
118
|
let validationScheduled = false;
|
|
119
|
+
let lastValidationFailureAt = 0;
|
|
118
120
|
const scheduleValidation = () => {
|
|
119
121
|
if (validationScheduled)
|
|
120
122
|
return;
|
|
123
|
+
if (lastValidationFailureAt &&
|
|
124
|
+
Date.now() - lastValidationFailureAt < VALIDATION_FAILURE_BACKOFF_MS) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
121
127
|
validationScheduled = true;
|
|
122
128
|
waitForInitialization()
|
|
123
129
|
.then(() => withOperationLock(() => fetchDataInternal()))
|
|
@@ -131,9 +137,10 @@ const createGlobalStateStore = () => {
|
|
|
131
137
|
const before = get(store);
|
|
132
138
|
store.update((current) => {
|
|
133
139
|
const nextPartial = typeof partial === "function" ? partial(current) : partial;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
if (replace) {
|
|
141
|
+
return { ...actions, ...nextPartial };
|
|
142
|
+
}
|
|
143
|
+
return { ...current, ...nextPartial };
|
|
137
144
|
});
|
|
138
145
|
const after = get(store);
|
|
139
146
|
const handlerJustRegistered = !hasAutoValidated &&
|
|
@@ -260,6 +267,7 @@ const createGlobalStateStore = () => {
|
|
|
260
267
|
}
|
|
261
268
|
catch (e) {
|
|
262
269
|
const error = `Load Exception: ${String(e)}`;
|
|
270
|
+
lastValidationFailureAt = Date.now();
|
|
263
271
|
setGlobalState((state) => ({
|
|
264
272
|
session: {
|
|
265
273
|
...state.session,
|
|
@@ -268,7 +276,9 @@ const createGlobalStateStore = () => {
|
|
|
268
276
|
loading: false,
|
|
269
277
|
},
|
|
270
278
|
}));
|
|
279
|
+
return;
|
|
271
280
|
}
|
|
281
|
+
lastValidationFailureAt = 0;
|
|
272
282
|
};
|
|
273
283
|
const actions = {
|
|
274
284
|
fetchData: async (url) => {
|
|
@@ -345,10 +355,12 @@ const createGlobalStateStore = () => {
|
|
|
345
355
|
return withOperationLock(async () => {
|
|
346
356
|
const previousState = getState();
|
|
347
357
|
try {
|
|
358
|
+
const blank = createInitialState();
|
|
348
359
|
setGlobalState((state) => ({
|
|
349
|
-
...
|
|
360
|
+
...blank,
|
|
361
|
+
initialized: true,
|
|
350
362
|
session: {
|
|
351
|
-
...
|
|
363
|
+
...blank.session,
|
|
352
364
|
apiURL: state.session.apiURL,
|
|
353
365
|
expiryDate: undefined,
|
|
354
366
|
loading: true,
|
|
@@ -485,6 +497,7 @@ const createGlobalStateStore = () => {
|
|
|
485
497
|
isStorageInitialized = true;
|
|
486
498
|
initializationPromise = null;
|
|
487
499
|
});
|
|
500
|
+
let lastPersistedState;
|
|
488
501
|
store.subscribe((state) => {
|
|
489
502
|
if (!isStorageInitialized) {
|
|
490
503
|
return;
|
|
@@ -495,9 +508,11 @@ const createGlobalStateStore = () => {
|
|
|
495
508
|
typeof state.onFetchSession === "function") {
|
|
496
509
|
scheduleValidation();
|
|
497
510
|
}
|
|
498
|
-
|
|
511
|
+
const nextPersisted = toPersistedState(state);
|
|
512
|
+
saveStorage(nextPersisted, lastPersistedState).catch((e) => {
|
|
499
513
|
console.error("Error saving storage:", e);
|
|
500
514
|
});
|
|
515
|
+
lastPersistedState = nextPersisted;
|
|
501
516
|
});
|
|
502
517
|
return {
|
|
503
518
|
getState,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { PersistedGlobalState } from './GlobalStateStore.types';
|
|
2
2
|
declare function loadStorage(): Promise<Partial<PersistedGlobalState>>;
|
|
3
|
-
declare function saveStorage(state: PersistedGlobalState): Promise<void>;
|
|
3
|
+
declare function saveStorage(state: PersistedGlobalState, previous?: Partial<PersistedGlobalState>): Promise<void>;
|
|
4
4
|
export { loadStorage, saveStorage };
|
|
@@ -75,8 +75,10 @@ async function loadStorage() {
|
|
|
75
75
|
}
|
|
76
76
|
return result;
|
|
77
77
|
}
|
|
78
|
-
async function saveStorage(state) {
|
|
78
|
+
async function saveStorage(state, previous) {
|
|
79
79
|
for (const key of PERSIST_KEYS) {
|
|
80
|
+
if (previous && previous[key] === state[key])
|
|
81
|
+
continue;
|
|
80
82
|
const storageKey = `${STORAGE_KEY}:${key}`;
|
|
81
83
|
const filtered = filterState(state[key], key);
|
|
82
84
|
const serialized = JSON.stringify(filtered);
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
let lastFetchTime = 0;
|
|
41
41
|
let fetchInProgress = false;
|
|
42
42
|
let mounted = false;
|
|
43
|
+
let storeInitialized = $state(GetGlobalState().initialized);
|
|
44
|
+
let lastHandlers: {
|
|
45
|
+
onFetchSession?: Props['onFetchSession'];
|
|
46
|
+
onLogin?: Props['onLogin'];
|
|
47
|
+
onLogout?: Props['onLogout'];
|
|
48
|
+
} = {};
|
|
43
49
|
|
|
44
50
|
const throttledFetch = async (url?: string): Promise<void> => {
|
|
45
51
|
const now = Date.now();
|
|
@@ -67,30 +73,40 @@
|
|
|
67
73
|
};
|
|
68
74
|
|
|
69
75
|
$effect(() => {
|
|
76
|
+
const unsubscribe = GlobalStateStore.subscribe((state) => {
|
|
77
|
+
storeInitialized = state.initialized;
|
|
78
|
+
});
|
|
79
|
+
return unsubscribe;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
$effect(() => {
|
|
83
|
+
if (!storeInitialized) return;
|
|
70
84
|
if (apiURL) {
|
|
71
85
|
GlobalStateStore.getState().setApiURL(apiURL);
|
|
72
86
|
}
|
|
73
87
|
});
|
|
74
88
|
|
|
75
89
|
$effect(() => {
|
|
90
|
+
if (!storeInitialized) return;
|
|
76
91
|
if (program) {
|
|
77
92
|
GlobalStateStore.getState().setProgram(program);
|
|
78
93
|
}
|
|
79
94
|
});
|
|
80
95
|
|
|
81
96
|
$effect(() => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
GlobalStateStore.setState({ onLogout });
|
|
97
|
+
if (
|
|
98
|
+
onFetchSession === lastHandlers.onFetchSession &&
|
|
99
|
+
onLogin === lastHandlers.onLogin &&
|
|
100
|
+
onLogout === lastHandlers.onLogout
|
|
101
|
+
) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
lastHandlers = { onFetchSession, onLogin, onLogout };
|
|
105
|
+
GlobalStateStore.setState({ onFetchSession, onLogin, onLogout });
|
|
91
106
|
});
|
|
92
107
|
|
|
93
108
|
$effect(() => {
|
|
109
|
+
if (!storeInitialized) return;
|
|
94
110
|
if (!mounted) {
|
|
95
111
|
mounted = true;
|
|
96
112
|
if (autoFetch && fetchOnMount) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
import type { Snippet } from "svelte";
|
|
4
|
-
import { untrack } from "svelte";
|
|
5
4
|
import type {
|
|
6
5
|
GridlerProps,
|
|
7
6
|
GridlerColumn,
|
|
@@ -74,7 +73,7 @@
|
|
|
74
73
|
sortOrder,
|
|
75
74
|
onFilterChange: _onFilterChange,
|
|
76
75
|
filters: _filters,
|
|
77
|
-
selectedItems,
|
|
76
|
+
selectedItems: _selectedItems,
|
|
78
77
|
onSelectedItemsChange: _onSelectedItemsChange,
|
|
79
78
|
getRowData,
|
|
80
79
|
settings,
|
|
@@ -123,8 +122,13 @@
|
|
|
123
122
|
|
|
124
123
|
// ── Selection / editing state ────────────────────────────────────────────────
|
|
125
124
|
|
|
126
|
-
let currentSelection = $state<Selection>(
|
|
125
|
+
let currentSelection = $state<Selection>({ type: "none" });
|
|
127
126
|
let focusedCell = $state<Item | null>(null);
|
|
127
|
+
|
|
128
|
+
$effect(() => {
|
|
129
|
+
if (!selection || selection.type === "none") return;
|
|
130
|
+
currentSelection = selection;
|
|
131
|
+
});
|
|
128
132
|
let isEditing = $state(false);
|
|
129
133
|
let editingCell = $state<Item | null>(null);
|
|
130
134
|
|
|
@@ -211,10 +215,14 @@
|
|
|
211
215
|
}
|
|
212
216
|
|
|
213
217
|
if (lines.length > 0) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
try {
|
|
219
|
+
await navigator.clipboard.writeText(lines.join("\n"));
|
|
220
|
+
canvasComponent?.setAnnouncement(
|
|
221
|
+
`Copied ${lines.length} row${lines.length > 1 ? "s" : ""} to clipboard`,
|
|
222
|
+
);
|
|
223
|
+
} catch {
|
|
224
|
+
canvasComponent?.setAnnouncement("Clipboard access denied");
|
|
225
|
+
}
|
|
218
226
|
}
|
|
219
227
|
}
|
|
220
228
|
|
|
@@ -222,7 +230,13 @@
|
|
|
222
230
|
if (resolvedReadonly) return;
|
|
223
231
|
const fc = canvasComponent?.getFocusedCell() ?? focusedCell;
|
|
224
232
|
if (!fc) return;
|
|
225
|
-
|
|
233
|
+
let text: string;
|
|
234
|
+
try {
|
|
235
|
+
text = await navigator.clipboard.readText();
|
|
236
|
+
} catch {
|
|
237
|
+
canvasComponent?.setAnnouncement("Clipboard access denied");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
226
240
|
const pastedRows = text.split("\n").map((line) => line.split("\t"));
|
|
227
241
|
const [startCol, startRow] = fc;
|
|
228
242
|
|
|
@@ -413,6 +427,7 @@
|
|
|
413
427
|
width={typeof width === "number" ? `${width}px` : (width ?? "100%")}
|
|
414
428
|
height={typeof height === "number" ? `${height}px` : (height ?? "400px")}
|
|
415
429
|
{mergedTheme}
|
|
430
|
+
{isDarkMode}
|
|
416
431
|
{headerHeight}
|
|
417
432
|
{rowHeight}
|
|
418
433
|
readonly={resolvedReadonly}
|
|
@@ -460,8 +475,10 @@
|
|
|
460
475
|
onRowContextMenu={(row) => {
|
|
461
476
|
onRowContextMenu?.(row);
|
|
462
477
|
}}
|
|
463
|
-
onColumnResized={(col,
|
|
464
|
-
|
|
478
|
+
onColumnResized={(col, newWidth) => {
|
|
479
|
+
columns[col].width = newWidth;
|
|
480
|
+
columns[col].grow = 0;
|
|
481
|
+
onGridEvent?.("column_resized", undefined, columns[col], undefined, { width: newWidth });
|
|
465
482
|
}}
|
|
466
483
|
onGridResize={(width, height) => {
|
|
467
484
|
onGridEvent?.("resize", undefined, undefined, undefined, { width, height });
|