@pathscale/ui 2.12.0 → 3.1.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/auth-field-group/AuthFieldGroup.css +11 -3
- package/dist/components/button/Button.css +4 -4
- package/dist/components/card/Card.generated.d.ts +21 -2
- package/dist/components/card/index.d.ts +1 -1
- package/dist/components/checkbox/Checkbox.generated.d.ts +23 -2
- package/dist/components/checkbox/Checkbox.generated.js +14 -6
- package/dist/components/connection-settings/ConnectionSettings.css +76 -0
- package/dist/components/connection-settings/ConnectionSettings.generated.d.ts +45 -0
- package/dist/components/connection-settings/ConnectionSettings.generated.js +300 -0
- package/dist/components/connection-settings/ConnectionSettings.recipe.d.ts +54 -0
- package/dist/components/connection-settings/ConnectionSettings.recipe.js +111 -0
- package/dist/components/connection-settings/index.d.ts +2 -0
- package/dist/components/connection-settings/index.js +1 -0
- package/dist/components/dialog/Dialog.generated.js +17 -79
- package/dist/components/drawer/Drawer.a11y.d.ts +1 -3
- package/dist/components/drawer/Drawer.a11y.js +2 -30
- package/dist/components/drawer/Drawer.generated.js +17 -40
- package/dist/components/grid/Grid.generated.js +3 -3
- package/dist/components/input/Input.css +5 -3
- package/dist/components/input/Input.generated.js +6 -3
- package/dist/components/password-field/PasswordField.generated.d.ts +9 -1
- package/dist/components/password-field/PasswordField.generated.js +2 -2
- package/dist/components/password-field/PasswordField.interactions.d.ts +2 -2
- package/dist/components/password-field/PasswordField.interactions.js +2 -2
- package/dist/components/popover/Popover.generated.js +14 -11
- package/dist/components/radio/Radio.generated.d.ts +26 -2
- package/dist/components/radio/Radio.generated.js +9 -5
- package/dist/components/select/Select.generated.js +5 -0
- package/dist/components/slider/Slider.generated.d.ts +14 -1
- package/dist/components/slider/Slider.generated.js +3 -2
- package/dist/components/switch/Switch.css +0 -3
- package/dist/components/switch/Switch.generated.d.ts +23 -2
- package/dist/components/switch/Switch.generated.js +21 -44
- package/dist/components/switch/Switch.recipe.d.ts +61 -38
- package/dist/components/switch/Switch.recipe.js +88 -68
- package/dist/components/text/Text.generated.js +2 -6
- package/dist/components/text/Text.recipe.d.ts +17 -5
- package/dist/components/text/Text.recipe.js +7 -13
- package/dist/hooks/connection/createConnectionSettings.d.ts +128 -0
- package/dist/hooks/connection/createConnectionSettings.js +211 -0
- package/dist/hooks/connection/index.d.ts +2 -0
- package/dist/hooks/connection/index.js +1 -0
- package/dist/hooks/data/createMutation.d.ts +7 -6
- package/dist/hooks/data/createMutation.js +12 -3
- package/dist/hooks/data/createQuery.d.ts +31 -6
- package/dist/hooks/data/createQuery.js +21 -4
- package/dist/hooks/data/createQuery.test.js +41 -14
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/layouts.manifest.json +1 -1
- package/dist/lib/focus.d.ts +31 -0
- package/dist/lib/focus.js +39 -0
- package/dist/lib/overlay.d.ts +94 -0
- package/dist/lib/overlay.js +72 -0
- package/dist/purge-manifest.json +14427 -15269
- package/dist/styles/base/index.css +42 -0
- package/package.json +3 -3
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Accessor } from "solid-js";
|
|
2
|
+
/**
|
|
3
|
+
* Where an application points itself, and how that survives a reload.
|
|
4
|
+
*
|
|
5
|
+
* Every property in this family ships a "connection settings" page: a toggle,
|
|
6
|
+
* one or more backend URLs, an app id, and a save that reconfigures the
|
|
7
|
+
* transport. Six of them had written it separately, in two different shapes and
|
|
8
|
+
* with different bugs, which is what this replaces.
|
|
9
|
+
*
|
|
10
|
+
* The storage model is per-endpoint rather than global. Four sites had a single
|
|
11
|
+
* `useCustomUrl` covering every backend at once; two had already grown a flag
|
|
12
|
+
* per backend, because pointing the API at a local instance while leaving auth
|
|
13
|
+
* on production is the ordinary case. Per-endpoint is the shape that covers
|
|
14
|
+
* both, and {@link ConnectionSettingsStore.setUseCustom} flips them together
|
|
15
|
+
* for a page that only wants one switch.
|
|
16
|
+
*/
|
|
17
|
+
export interface ConnectionEndpoint {
|
|
18
|
+
/** Stable key. Names the value in storage and in {@link ConnectionSettingsStore.urls}. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** The address used whenever this endpoint's override is off. */
|
|
21
|
+
fallback: string;
|
|
22
|
+
/**
|
|
23
|
+
* Reject an address before it is saved. Return a message to refuse it, or
|
|
24
|
+
* nothing to accept.
|
|
25
|
+
*
|
|
26
|
+
* Defaults to {@link isAbsoluteUrl}: the value has to parse as an absolute
|
|
27
|
+
* URL. That is deliberately weak, because this hook does not know what a
|
|
28
|
+
* given endpoint speaks. An endpoint that knows should say so -- a WebSocket
|
|
29
|
+
* transport handed `http://…` fails at connect time, long after the page
|
|
30
|
+
* that could have explained it has gone.
|
|
31
|
+
*/
|
|
32
|
+
validate?: (url: string) => string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/** What {@link ConnectionSettingsStore.apply} hands to `onApply`. */
|
|
35
|
+
export interface ConnectionSettingsApplied {
|
|
36
|
+
/** Resolved per endpoint, overrides taken into account. Never empty. */
|
|
37
|
+
urls: Readonly<Record<string, string>>;
|
|
38
|
+
appPublicId: string;
|
|
39
|
+
/** Which endpoints the resolved address came from an override for. */
|
|
40
|
+
overrides: Readonly<Record<string, boolean>>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a bracketed host is an IPv6 literal.
|
|
44
|
+
*
|
|
45
|
+
* `https://[1:2:3]` is not an address, and a character class of hex digits and
|
|
46
|
+
* colons said it was. This is the shape the URL Standard describes: eight
|
|
47
|
+
* 16-bit groups, or fewer with exactly one `::` standing for the run of zeroes
|
|
48
|
+
* it elides, optionally ending in a dotted IPv4 form.
|
|
49
|
+
*
|
|
50
|
+
* Written out rather than folded into one pattern because the `::` rule is a
|
|
51
|
+
* count, not a shape: each half has to parse, and together they have to leave
|
|
52
|
+
* at least one group for the `::` to stand for.
|
|
53
|
+
*/
|
|
54
|
+
export declare const isIpv6Literal: (host: string) => boolean;
|
|
55
|
+
export declare const isAbsoluteUrl: (url: string) => string | undefined;
|
|
56
|
+
export interface ConnectionSettingsOptions {
|
|
57
|
+
/** `localStorage` key. Namespace it per application; two apps on one origin would collide. */
|
|
58
|
+
storageKey: string;
|
|
59
|
+
endpoints: readonly ConnectionEndpoint[];
|
|
60
|
+
/** The application's own id, when it has one. Stored alongside the URLs. */
|
|
61
|
+
appPublicId?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Run after a successful save or reset, with everything that was applied.
|
|
64
|
+
*
|
|
65
|
+
* This is where an application reconfigures its transport. It is awaited, so
|
|
66
|
+
* `isApplying` covers the reconnect rather than just the write.
|
|
67
|
+
*
|
|
68
|
+
* It receives the whole applied state, not only the URLs: a site that stores
|
|
69
|
+
* an app id here reconfigures its identity from the same call, and passing
|
|
70
|
+
* the URLs alone left it reaching back into the store it had just handed to
|
|
71
|
+
* this hook.
|
|
72
|
+
*/
|
|
73
|
+
onApply?: (applied: ConnectionSettingsApplied) => void | Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
export interface ConnectionSettingsState {
|
|
76
|
+
/** Per endpoint: is the override in use. Absent means no. */
|
|
77
|
+
overrides: Record<string, boolean>;
|
|
78
|
+
/** Per endpoint: the address to use when its override is on. */
|
|
79
|
+
urls: Record<string, string>;
|
|
80
|
+
appPublicId: string;
|
|
81
|
+
}
|
|
82
|
+
export interface ConnectionSettingsStore {
|
|
83
|
+
/** What is stored, override flags included. Read this to populate a form. */
|
|
84
|
+
readonly state: ConnectionSettingsState;
|
|
85
|
+
/**
|
|
86
|
+
* The address for each endpoint after overrides are applied. This is what a
|
|
87
|
+
* transport should read; it never contains an empty string.
|
|
88
|
+
*/
|
|
89
|
+
readonly urls: Readonly<Record<string, string>>;
|
|
90
|
+
/** The address each endpoint falls back to, by name. What "not overridden" means. */
|
|
91
|
+
readonly fallbacks: Readonly<Record<string, string>>;
|
|
92
|
+
/** True while `onApply` is in flight. */
|
|
93
|
+
readonly isApplying: boolean;
|
|
94
|
+
/** Nothing has been overridden and the app id is untouched. */
|
|
95
|
+
readonly isAtDefaults: boolean;
|
|
96
|
+
/** True when this endpoint is overridden. */
|
|
97
|
+
isOverridden(name: string): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Why this address cannot be saved for this endpoint, or `undefined` if it
|
|
100
|
+
* can. Empty is always refused; beyond that the endpoint's own `validate`
|
|
101
|
+
* decides, defaulting to {@link isAbsoluteUrl}.
|
|
102
|
+
*
|
|
103
|
+
* A settings page is the last place able to explain a bad address. Saved
|
|
104
|
+
* unchecked, the value survives a failed reconnect and is still there on the
|
|
105
|
+
* next launch, with nothing on screen saying why nothing connects.
|
|
106
|
+
*/
|
|
107
|
+
validate(name: string, url: string): string | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Set an endpoint's override.
|
|
110
|
+
*
|
|
111
|
+
* Returns the reason it was refused, or `undefined` when it was stored. A
|
|
112
|
+
* value written here becomes an active, persisted override, so it has to
|
|
113
|
+
* pass the same check {@link ConnectionSettingsStore.validate} applies;
|
|
114
|
+
* an invalid one is not stored at all. Keep it as a draft in the UI instead.
|
|
115
|
+
*/
|
|
116
|
+
setUrl(name: string, url: string): string | undefined;
|
|
117
|
+
setOverride(name: string, on: boolean): void;
|
|
118
|
+
/** Flip every endpoint together, for a page with one switch. */
|
|
119
|
+
setUseCustom(on: boolean): void;
|
|
120
|
+
setAppPublicId(id: string): void;
|
|
121
|
+
/** Drop every override and forget the stored copy. Does not apply. */
|
|
122
|
+
reset(): void;
|
|
123
|
+
/** Persist, then hand the resolved addresses to `onApply`. */
|
|
124
|
+
apply(): Promise<void>;
|
|
125
|
+
/** For a component that wants to track the state rather than read it once. */
|
|
126
|
+
state$: Accessor<ConnectionSettingsState>;
|
|
127
|
+
}
|
|
128
|
+
export declare const createConnectionSettings: (options: ConnectionSettingsOptions) => ConnectionSettingsStore;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { createEffect, createRoot, createSignal } from "solid-js";
|
|
2
|
+
const ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:\/\/(\[[^\]\s]*\]|[^\s/?#:]+)(?::(\d{1,5}))?(?:[/?#]\S*)?$/i;
|
|
3
|
+
const IPV6_GROUP = "[0-9a-f]{1,4}";
|
|
4
|
+
const IPV4_TAIL = "(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)";
|
|
5
|
+
const IPV6_FULL = new RegExp(`^(?:${IPV6_GROUP}:){6}(?:${IPV6_GROUP}:${IPV6_GROUP}|${IPV4_TAIL})$`, "i");
|
|
6
|
+
const IPV6_PIECES = new RegExp(`^(?:${IPV6_GROUP}(?::${IPV6_GROUP})*)?$`, "i");
|
|
7
|
+
const isIpv6Literal = (host)=>{
|
|
8
|
+
if (IPV6_FULL.test(host)) return true;
|
|
9
|
+
const halves = host.split("::");
|
|
10
|
+
if (2 !== halves.length) return false;
|
|
11
|
+
const [head, tail] = halves;
|
|
12
|
+
const embeddedV4 = new RegExp(`(?:^|:)(${IPV4_TAIL})$`, "i").exec(tail);
|
|
13
|
+
const groups = embeddedV4 ? tail.slice(0, tail.length - embeddedV4[1].length).replace(/:$/, "") : tail;
|
|
14
|
+
if (!IPV6_PIECES.test(head)) return false;
|
|
15
|
+
if (!IPV6_PIECES.test(groups)) return false;
|
|
16
|
+
const count = (part)=>"" === part ? 0 : part.split(":").length;
|
|
17
|
+
return count(head) + count(groups) + (embeddedV4 ? 2 : 0) <= 7;
|
|
18
|
+
};
|
|
19
|
+
const isAbsoluteUrl = (url)=>{
|
|
20
|
+
const match = ABSOLUTE_URL.exec(url);
|
|
21
|
+
if (!match) return `${url} is not an absolute address (expected scheme://host)`;
|
|
22
|
+
const [, host, port] = match;
|
|
23
|
+
if (host.startsWith("[") && !isIpv6Literal(host.slice(1, -1))) return `${url} does not contain a valid IPv6 address`;
|
|
24
|
+
if (void 0 !== port && Number(port) > 65535) return `${url} has a port outside 0-65535`;
|
|
25
|
+
};
|
|
26
|
+
const isRecord = (value)=>"object" == typeof value && null !== value && !Array.isArray(value);
|
|
27
|
+
const createConnectionSettings = (options)=>{
|
|
28
|
+
const { storageKey, endpoints, appPublicId = "", onApply } = options;
|
|
29
|
+
const fallbacks = Object.freeze(Object.fromEntries(endpoints.map((e)=>[
|
|
30
|
+
e.name,
|
|
31
|
+
e.fallback
|
|
32
|
+
])));
|
|
33
|
+
const defaults = ()=>({
|
|
34
|
+
overrides: {},
|
|
35
|
+
urls: Object.fromEntries(endpoints.map((e)=>[
|
|
36
|
+
e.name,
|
|
37
|
+
e.fallback
|
|
38
|
+
])),
|
|
39
|
+
appPublicId
|
|
40
|
+
});
|
|
41
|
+
const read = ()=>{
|
|
42
|
+
const base = defaults();
|
|
43
|
+
try {
|
|
44
|
+
if ("u" < typeof localStorage) return base;
|
|
45
|
+
const raw = localStorage.getItem(storageKey);
|
|
46
|
+
if (!raw) return base;
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (!isRecord(parsed)) return base;
|
|
49
|
+
const overrides = {};
|
|
50
|
+
const urls = {
|
|
51
|
+
...base.urls
|
|
52
|
+
};
|
|
53
|
+
const storedOverrides = isRecord(parsed.overrides) ? parsed.overrides : {};
|
|
54
|
+
const storedUrls = isRecord(parsed.urls) ? parsed.urls : {};
|
|
55
|
+
for (const endpoint of endpoints){
|
|
56
|
+
const url = storedUrls[endpoint.name];
|
|
57
|
+
const usable = "string" == typeof url && "" !== url && void 0 === (endpoint.validate ?? isAbsoluteUrl)(url);
|
|
58
|
+
if (usable) urls[endpoint.name] = url;
|
|
59
|
+
if (usable && true === storedOverrides[endpoint.name]) overrides[endpoint.name] = true;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
overrides,
|
|
63
|
+
urls,
|
|
64
|
+
appPublicId: "string" == typeof parsed.appPublicId ? parsed.appPublicId : base.appPublicId
|
|
65
|
+
};
|
|
66
|
+
} catch {
|
|
67
|
+
return base;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const [state, setState, isApplying, setIsApplying] = createRoot(()=>{
|
|
71
|
+
const [s, setS] = createSignal(read());
|
|
72
|
+
const [a, setA] = createSignal(false);
|
|
73
|
+
return [
|
|
74
|
+
s,
|
|
75
|
+
setS,
|
|
76
|
+
a,
|
|
77
|
+
setA
|
|
78
|
+
];
|
|
79
|
+
});
|
|
80
|
+
const atDefaults = (value)=>value.appPublicId === appPublicId && endpoints.every((e)=>true !== value.overrides[e.name] && value.urls[e.name] === e.fallback);
|
|
81
|
+
const write = (value)=>{
|
|
82
|
+
try {
|
|
83
|
+
if ("u" < typeof localStorage) return;
|
|
84
|
+
if (atDefaults(value)) localStorage.removeItem(storageKey);
|
|
85
|
+
else localStorage.setItem(storageKey, JSON.stringify(value));
|
|
86
|
+
} catch {}
|
|
87
|
+
};
|
|
88
|
+
createRoot(()=>{
|
|
89
|
+
createEffect(()=>state(), (value)=>{
|
|
90
|
+
write(value);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
let committed = state();
|
|
94
|
+
let inFlight = 0;
|
|
95
|
+
let queue = Promise.resolve();
|
|
96
|
+
const update = (change)=>{
|
|
97
|
+
committed = change(committed);
|
|
98
|
+
setState(committed);
|
|
99
|
+
};
|
|
100
|
+
const resolveFrom = (current)=>Object.fromEntries(endpoints.map((e)=>{
|
|
101
|
+
const override = true === current.overrides[e.name] ? current.urls[e.name] : void 0;
|
|
102
|
+
return [
|
|
103
|
+
e.name,
|
|
104
|
+
override && "" !== override ? override : e.fallback
|
|
105
|
+
];
|
|
106
|
+
}));
|
|
107
|
+
let resolvedFor;
|
|
108
|
+
let resolvedValue = Object.freeze({});
|
|
109
|
+
const resolved = ()=>{
|
|
110
|
+
const current = state();
|
|
111
|
+
if (resolvedFor !== current) {
|
|
112
|
+
resolvedFor = current;
|
|
113
|
+
resolvedValue = Object.freeze(resolveFrom(current));
|
|
114
|
+
}
|
|
115
|
+
return resolvedValue;
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
get state () {
|
|
119
|
+
return state();
|
|
120
|
+
},
|
|
121
|
+
get urls () {
|
|
122
|
+
return resolved();
|
|
123
|
+
},
|
|
124
|
+
validate (name, url) {
|
|
125
|
+
const endpoint = endpoints.find((e)=>e.name === name);
|
|
126
|
+
if (!endpoint) return `${name} is not a configured endpoint`;
|
|
127
|
+
if ("" === url) return "an address is required";
|
|
128
|
+
return (endpoint.validate ?? isAbsoluteUrl)(url);
|
|
129
|
+
},
|
|
130
|
+
get fallbacks () {
|
|
131
|
+
return fallbacks;
|
|
132
|
+
},
|
|
133
|
+
get isApplying () {
|
|
134
|
+
return isApplying();
|
|
135
|
+
},
|
|
136
|
+
get isAtDefaults () {
|
|
137
|
+
state();
|
|
138
|
+
return atDefaults(committed);
|
|
139
|
+
},
|
|
140
|
+
state$: state,
|
|
141
|
+
isOverridden (name) {
|
|
142
|
+
return true === state().overrides[name];
|
|
143
|
+
},
|
|
144
|
+
setUrl (name, url) {
|
|
145
|
+
if ("" !== url) {
|
|
146
|
+
const endpoint = endpoints.find((e)=>e.name === name);
|
|
147
|
+
const problem = endpoint ? (endpoint.validate ?? isAbsoluteUrl)(url) : `${name} is not a configured endpoint`;
|
|
148
|
+
if (problem) return problem;
|
|
149
|
+
}
|
|
150
|
+
update((c)=>({
|
|
151
|
+
...c,
|
|
152
|
+
urls: {
|
|
153
|
+
...c.urls,
|
|
154
|
+
[name]: url
|
|
155
|
+
}
|
|
156
|
+
}));
|
|
157
|
+
},
|
|
158
|
+
setOverride (name, on) {
|
|
159
|
+
update((c)=>({
|
|
160
|
+
...c,
|
|
161
|
+
overrides: {
|
|
162
|
+
...c.overrides,
|
|
163
|
+
[name]: on
|
|
164
|
+
}
|
|
165
|
+
}));
|
|
166
|
+
},
|
|
167
|
+
setUseCustom (on) {
|
|
168
|
+
update((c)=>({
|
|
169
|
+
...c,
|
|
170
|
+
overrides: Object.fromEntries(endpoints.map((e)=>[
|
|
171
|
+
e.name,
|
|
172
|
+
on
|
|
173
|
+
]))
|
|
174
|
+
}));
|
|
175
|
+
},
|
|
176
|
+
setAppPublicId (id) {
|
|
177
|
+
update((c)=>({
|
|
178
|
+
...c,
|
|
179
|
+
appPublicId: id
|
|
180
|
+
}));
|
|
181
|
+
},
|
|
182
|
+
reset () {
|
|
183
|
+
committed = defaults();
|
|
184
|
+
setState(committed);
|
|
185
|
+
},
|
|
186
|
+
async apply () {
|
|
187
|
+
inFlight += 1;
|
|
188
|
+
setIsApplying(true);
|
|
189
|
+
const current = committed;
|
|
190
|
+
const reconnect = ()=>{
|
|
191
|
+
write(current);
|
|
192
|
+
return onApply?.({
|
|
193
|
+
urls: resolveFrom(current),
|
|
194
|
+
appPublicId: current.appPublicId,
|
|
195
|
+
overrides: {
|
|
196
|
+
...current.overrides
|
|
197
|
+
}
|
|
198
|
+
}) ?? void 0;
|
|
199
|
+
};
|
|
200
|
+
const run = 1 === inFlight ? (async ()=>reconnect())() : queue.then(reconnect).then(()=>void 0);
|
|
201
|
+
queue = run.catch(()=>void 0);
|
|
202
|
+
try {
|
|
203
|
+
await run;
|
|
204
|
+
} finally{
|
|
205
|
+
inFlight -= 1;
|
|
206
|
+
if (0 === inFlight) setIsApplying(false);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
export { createConnectionSettings, isAbsoluteUrl, isIpv6Literal };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createConnectionSettings, isAbsoluteUrl } from "./createConnectionSettings.js";
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { Accessor } from "solid-js";
|
|
2
1
|
/**
|
|
3
2
|
* A write, without a query library. The companion to `createQuery`.
|
|
4
3
|
*
|
|
5
4
|
* Replaces `useMutation`. The same rule applies as there: reading this never
|
|
6
|
-
* suspends and never throws. `mutate` reports failure through `error
|
|
5
|
+
* suspends and never throws. `mutate` reports failure through `error`;
|
|
7
6
|
* `mutateAsync` rejects, for a caller that wants to await and handle it.
|
|
8
7
|
*/
|
|
9
8
|
export interface CreateMutationOptions<TArgs extends unknown[], TResult> {
|
|
@@ -13,15 +12,17 @@ export interface CreateMutationOptions<TArgs extends unknown[], TResult> {
|
|
|
13
12
|
/** Runs after success or failure, like TanStack's `onSettled`. */
|
|
14
13
|
onSettled?: () => void | Promise<void>;
|
|
15
14
|
}
|
|
15
|
+
/** Read as properties, for the same reason as `QueryResult`. */
|
|
16
16
|
export interface MutationResult<TArgs extends unknown[], TResult> {
|
|
17
|
-
/** Fire and forget. Failure lands on `error
|
|
17
|
+
/** Fire and forget. Failure lands on `error` rather than as a rejection. */
|
|
18
18
|
mutate: (...args: TArgs) => void;
|
|
19
19
|
/** Fire and await. Rejects on failure. */
|
|
20
20
|
mutateAsync: (...args: TArgs) => Promise<TResult>;
|
|
21
|
-
isPending:
|
|
22
|
-
error:
|
|
21
|
+
readonly isPending: boolean;
|
|
22
|
+
readonly error: unknown;
|
|
23
|
+
readonly isError: boolean;
|
|
23
24
|
/** The last successful result. */
|
|
24
|
-
data:
|
|
25
|
+
readonly data: TResult | undefined;
|
|
25
26
|
/** Clear `error` and `data`. */
|
|
26
27
|
reset: () => void;
|
|
27
28
|
}
|
|
@@ -29,9 +29,18 @@ const createMutation = (options)=>{
|
|
|
29
29
|
mutateAsync(...args).catch(()=>{});
|
|
30
30
|
},
|
|
31
31
|
mutateAsync,
|
|
32
|
-
isPending
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
get isPending () {
|
|
33
|
+
return isPending();
|
|
34
|
+
},
|
|
35
|
+
get error () {
|
|
36
|
+
return error();
|
|
37
|
+
},
|
|
38
|
+
get isError () {
|
|
39
|
+
return void 0 !== error();
|
|
40
|
+
},
|
|
41
|
+
get data () {
|
|
42
|
+
return data();
|
|
43
|
+
},
|
|
35
44
|
reset: ()=>{
|
|
36
45
|
setError(void 0);
|
|
37
46
|
setData(()=>void 0);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Accessor } from "solid-js";
|
|
2
1
|
/**
|
|
3
2
|
* Asynchronous reads, without a query library.
|
|
4
3
|
*
|
|
@@ -16,7 +15,7 @@ import type { Accessor } from "solid-js";
|
|
|
16
15
|
* had nothing to print. That is how a support-chat button that had not
|
|
17
16
|
* connected replaced an entire application with a blank error page.
|
|
18
17
|
*
|
|
19
|
-
* Here, `data
|
|
18
|
+
* Here, `data` is `undefined` until there is data, `isLoading` is true only
|
|
20
19
|
* while a fetch is actually in flight, and neither ever throws. A caller that
|
|
21
20
|
* wants to suspend can do so explicitly; a caller that forgets cannot take the
|
|
22
21
|
* page down.
|
|
@@ -32,15 +31,41 @@ export interface CreateQueryOptions<T> {
|
|
|
32
31
|
/** Held back while false. Default true. */
|
|
33
32
|
enabled?: boolean;
|
|
34
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Read as properties, not as accessors.
|
|
36
|
+
*
|
|
37
|
+
* `solid-query` returned a store, so every consumer of it is written
|
|
38
|
+
* `query.data`, `query.isLoading`, `query.isError` -- roughly three thousand
|
|
39
|
+
* such reads across these applications. Exposing accessors here would mean
|
|
40
|
+
* editing all of them to add `()`, turning a migration of the ~300 files that
|
|
41
|
+
* *define* queries into one that touches every file that reads one.
|
|
42
|
+
*
|
|
43
|
+
* These are getters over signals, so a read inside a tracked scope still
|
|
44
|
+
* subscribes exactly as an accessor call would.
|
|
45
|
+
*/
|
|
35
46
|
export interface QueryResult<T> {
|
|
36
47
|
/** The last value read, or `undefined` before the first one arrives. */
|
|
37
|
-
data:
|
|
48
|
+
readonly data: T | undefined;
|
|
38
49
|
/** The last failure, cleared by the next successful read. */
|
|
39
|
-
error:
|
|
50
|
+
readonly error: unknown;
|
|
40
51
|
/** True only while a fetch is in flight. Never true for a disabled query. */
|
|
41
|
-
isLoading:
|
|
52
|
+
readonly isLoading: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Alias of `isLoading`, for call sites carried over from `solid-query`.
|
|
55
|
+
*
|
|
56
|
+
* Note the deliberate difference in meaning. There, `isPending` was "has no
|
|
57
|
+
* data", which stayed true forever for a query held back by `enabled: false`
|
|
58
|
+
* -- so `if (isPending) return <Spinner/>` spun for the life of the page.
|
|
59
|
+
* Here it means "a fetch is in flight", so a disabled query reads as not
|
|
60
|
+
* pending and its consumer renders instead of hanging.
|
|
61
|
+
*/
|
|
62
|
+
readonly isPending: boolean;
|
|
63
|
+
/** True when the last read failed. */
|
|
64
|
+
readonly isError: boolean;
|
|
65
|
+
/** True when a value has arrived and the last read did not fail. */
|
|
66
|
+
readonly isSuccess: boolean;
|
|
42
67
|
/** True once a value has arrived at least once. */
|
|
43
|
-
isReady:
|
|
68
|
+
readonly isReady: boolean;
|
|
44
69
|
/** Read again now, regardless of `enabled`. */
|
|
45
70
|
refetch: () => Promise<void>;
|
|
46
71
|
}
|
|
@@ -45,10 +45,27 @@ const createQuery = (options)=>{
|
|
|
45
45
|
generation++;
|
|
46
46
|
});
|
|
47
47
|
return {
|
|
48
|
-
data
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
get data () {
|
|
49
|
+
return data();
|
|
50
|
+
},
|
|
51
|
+
get error () {
|
|
52
|
+
return error();
|
|
53
|
+
},
|
|
54
|
+
get isLoading () {
|
|
55
|
+
return isLoading();
|
|
56
|
+
},
|
|
57
|
+
get isPending () {
|
|
58
|
+
return isLoading();
|
|
59
|
+
},
|
|
60
|
+
get isError () {
|
|
61
|
+
return void 0 !== error();
|
|
62
|
+
},
|
|
63
|
+
get isSuccess () {
|
|
64
|
+
return isReady() && void 0 === error();
|
|
65
|
+
},
|
|
66
|
+
get isReady () {
|
|
67
|
+
return isReady();
|
|
68
|
+
},
|
|
52
69
|
refetch: run
|
|
53
70
|
};
|
|
54
71
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { createRoot, flush } from "solid-js";
|
|
2
|
+
import { createRenderEffect, createRoot, flush } from "solid-js";
|
|
3
3
|
import { createMutation } from "./createMutation.js";
|
|
4
4
|
import { createQuery, invalidateQueries } from "./createQuery.js";
|
|
5
5
|
const tick = ()=>new Promise((resolve)=>setTimeout(resolve, 0));
|
|
@@ -17,9 +17,9 @@ describe("createQuery", ()=>{
|
|
|
17
17
|
return "value";
|
|
18
18
|
}
|
|
19
19
|
}));
|
|
20
|
-
expect(q.isLoading
|
|
21
|
-
expect(q.data
|
|
22
|
-
expect(q.isReady
|
|
20
|
+
expect(q.isLoading).toBe(false);
|
|
21
|
+
expect(q.data).toBeUndefined();
|
|
22
|
+
expect(q.isReady).toBe(false);
|
|
23
23
|
return d;
|
|
24
24
|
});
|
|
25
25
|
await tick();
|
|
@@ -37,15 +37,15 @@ describe("createQuery", ()=>{
|
|
|
37
37
|
}));
|
|
38
38
|
flush();
|
|
39
39
|
const whileLoading = {
|
|
40
|
-
loading: q.isLoading
|
|
41
|
-
ready: q.isReady
|
|
40
|
+
loading: q.isLoading,
|
|
41
|
+
ready: q.isReady
|
|
42
42
|
};
|
|
43
43
|
resolveFetch?.("hello");
|
|
44
44
|
await tick();
|
|
45
45
|
return {
|
|
46
46
|
whileLoading,
|
|
47
|
-
data: q.data
|
|
48
|
-
ready: q.isReady
|
|
47
|
+
data: q.data,
|
|
48
|
+
ready: q.isReady,
|
|
49
49
|
dispose
|
|
50
50
|
};
|
|
51
51
|
});
|
|
@@ -57,7 +57,7 @@ describe("createQuery", ()=>{
|
|
|
57
57
|
expect(result.ready).toBe(true);
|
|
58
58
|
result.dispose();
|
|
59
59
|
});
|
|
60
|
-
test("a failure lands on error
|
|
60
|
+
test("a failure lands on error rather than being thrown", async ()=>{
|
|
61
61
|
const result = await createRoot(async (dispose)=>{
|
|
62
62
|
const q = createQuery(()=>({
|
|
63
63
|
key: [
|
|
@@ -70,8 +70,8 @@ describe("createQuery", ()=>{
|
|
|
70
70
|
flush();
|
|
71
71
|
await tick();
|
|
72
72
|
return {
|
|
73
|
-
error: q.error
|
|
74
|
-
loading: q.isLoading
|
|
73
|
+
error: q.error,
|
|
74
|
+
loading: q.isLoading,
|
|
75
75
|
dispose
|
|
76
76
|
};
|
|
77
77
|
});
|
|
@@ -134,6 +134,33 @@ describe("createQuery", ()=>{
|
|
|
134
134
|
expect(calls).toBe(1);
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
|
+
describe("property reads stay reactive", ()=>{
|
|
138
|
+
test("a tracked scope re-runs when data lands", async ()=>{
|
|
139
|
+
let resolveFetch;
|
|
140
|
+
const seen = [];
|
|
141
|
+
const result = await createRoot(async (dispose)=>{
|
|
142
|
+
const q = createQuery(()=>({
|
|
143
|
+
key: [
|
|
144
|
+
"reactive"
|
|
145
|
+
],
|
|
146
|
+
fetcher: ()=>new Promise((r)=>resolveFetch = r)
|
|
147
|
+
}));
|
|
148
|
+
createRenderEffect(()=>q.data, (value)=>{
|
|
149
|
+
seen.push(value);
|
|
150
|
+
});
|
|
151
|
+
flush();
|
|
152
|
+
resolveFetch?.("arrived");
|
|
153
|
+
await tick();
|
|
154
|
+
flush();
|
|
155
|
+
return {
|
|
156
|
+
dispose
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
expect(seen[0]).toBeUndefined();
|
|
160
|
+
expect(seen.at(-1)).toBe("arrived");
|
|
161
|
+
result.dispose();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
137
164
|
describe("createMutation", ()=>{
|
|
138
165
|
test("mutate reports failure without an unhandled rejection", async ()=>{
|
|
139
166
|
const result = await createRoot(async (dispose)=>{
|
|
@@ -145,8 +172,8 @@ describe("createMutation", ()=>{
|
|
|
145
172
|
m.mutate();
|
|
146
173
|
await tick();
|
|
147
174
|
return {
|
|
148
|
-
error: m.error
|
|
149
|
-
pending: m.isPending
|
|
175
|
+
error: m.error,
|
|
176
|
+
pending: m.isPending,
|
|
150
177
|
dispose
|
|
151
178
|
};
|
|
152
179
|
});
|
|
@@ -166,7 +193,7 @@ describe("createMutation", ()=>{
|
|
|
166
193
|
const value = await m.mutateAsync("app");
|
|
167
194
|
return {
|
|
168
195
|
value,
|
|
169
|
-
data: m.data
|
|
196
|
+
data: m.data,
|
|
170
197
|
dispose
|
|
171
198
|
};
|
|
172
199
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type { ButtonProps } from "./components/button";
|
|
|
20
20
|
export { default as Button } from "./components/button";
|
|
21
21
|
export { PanelToggle, type PanelToggleProps, type PanelToggleSide, } from "./components/panel-toggle";
|
|
22
22
|
export { type CalendarDayHoverHandler, type CalendarDaySelectHandler, type CalendarProps, type CalendarSelectionMode, type CalendarWeekdayFormat, default as Calendar, } from "./components/calendar";
|
|
23
|
-
export type { CardElevation, CardMaterial, CardProps, CardSectionProps, } from "./components/card";
|
|
23
|
+
export type { CardElevation, CardMaterial, CardState, CardProps, CardSectionProps, } from "./components/card";
|
|
24
24
|
export { Card as CardRoot, CardBody, CardFooter, CardHeader, default as Card, } from "./components/card";
|
|
25
25
|
export { default as ChatBubble } from "./components/chatbubble";
|
|
26
26
|
export { default as Checkbox } from "./components/checkbox";
|
|
@@ -123,6 +123,10 @@ export type { AnyFormApi, CreateFormOptions, FormApi, UseFieldResult, } from "./
|
|
|
123
123
|
export { createForm, FormContext, getFirstFieldError, useField, useFormContext, } from "./hooks/form";
|
|
124
124
|
export type { CreateMutationOptions, CreateQueryOptions, MutationResult, QueryResult, } from "./hooks/data";
|
|
125
125
|
export { createMutation, createQuery, invalidateQueries, } from "./hooks/data";
|
|
126
|
+
export type { ConnectionEndpoint, ConnectionSettingsApplied, ConnectionSettingsOptions, ConnectionSettingsState, ConnectionSettingsStore, } from "./hooks/connection";
|
|
127
|
+
export { createConnectionSettings, isAbsoluteUrl } from "./hooks/connection";
|
|
128
|
+
export type { ConnectionSettingsEndpointLabel, ConnectionSettingsLabels, ConnectionSettingsProps, } from "./components/connection-settings";
|
|
129
|
+
export { ConnectionSettings } from "./components/connection-settings";
|
|
126
130
|
export { useDesktop } from "./hooks/layout";
|
|
127
131
|
export type { UseAnchoredOverlayPositionOptions } from "./hooks/table";
|
|
128
132
|
export { useAnchoredOverlayPosition } from "./hooks/table";
|
package/dist/index.js
CHANGED
|
@@ -73,6 +73,8 @@ export { TooltipArrow, TooltipContent, TooltipTrigger, default as Tooltip } from
|
|
|
73
73
|
export { FLAVORS, SIZES, SPACES, STATES, VARIANTS, isInvalid, resolveState } from "./components/vocabulary.js";
|
|
74
74
|
export { FormContext, createForm, getFirstFieldError, useField, useFormContext } from "./hooks/form/index.js";
|
|
75
75
|
export { createMutation, createQuery, invalidateQueries } from "./hooks/data/index.js";
|
|
76
|
+
export { createConnectionSettings, isAbsoluteUrl } from "./hooks/connection/index.js";
|
|
77
|
+
export { ConnectionSettings } from "./components/connection-settings/index.js";
|
|
76
78
|
export { useDesktop } from "./hooks/layout/index.js";
|
|
77
79
|
export { useAnchoredOverlayPosition } from "./hooks/table/index.js";
|
|
78
80
|
export { evaluatePasswordRules, matchPasswordConfirmation } from "./passwordRules.js";
|