@deckbuilderkit/view-rest 0.0.0-stage → 0.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.
@@ -0,0 +1,262 @@
1
+ import { useEffect, useSyncExternalStore } from 'react';
2
+ import { draftToSpec, emptyDraft, type HttpRequestDraft } from '@deckbuilderkit/editor';
3
+ import type { HttpResponseInfo } from '@deckbuilderkit/server-core/protocol';
4
+ import { type ApiError, errorOf } from '@deckbuilderkit/ui';
5
+ import { restApi as api } from '../client';
6
+ import type { RestTabsFile, SavedRequest } from '../settings';
7
+
8
+
9
+ /** A REST tab as the tabs store keeps it (no Authorization value). */
10
+ export interface RestTab {
11
+ id: string;
12
+ draft: HttpRequestDraft;
13
+ savedId?: string;
14
+ /** Saved request's name, or an imported request's name. */
15
+ title?: string;
16
+ }
17
+
18
+ /** Per-tab state that lives only for the app's lifetime. */
19
+ export interface TabRuntime {
20
+ auth: string;
21
+ /** Set when the tab came from an import or `openRestTab`; `auth` tells the auth field to accept a value. */
22
+ imported?: { name: string; auth: boolean };
23
+ response?: HttpResponseInfo;
24
+ error?: ApiError;
25
+ sending: boolean;
26
+ savedPath?: string;
27
+ }
28
+
29
+ interface Snapshot {
30
+ tabs: RestTab[];
31
+ active: string;
32
+ runtime: Record<string, TabRuntime>;
33
+ /** The last open attempt found every tab with unsaved work at the cap. */
34
+ full?: boolean;
35
+ }
36
+
37
+ /** Saved requests as the REST screen last loaded them; until known, a tab with a saved id counts as dirty. */
38
+ let savedList: SavedRequest[] | undefined;
39
+
40
+ const MAX_TABS = 30;
41
+ const SAVE_DELAY_MS = 500;
42
+ /** The server refuses bodies over 1 MB; large drafts lose their body in the stored copy first. */
43
+ const MAX_FILE_CHARS = 950_000;
44
+
45
+ const newId = () => crypto.randomUUID();
46
+ const blankRuntime = (): TabRuntime => ({ auth: '', sending: false });
47
+ const freshTab = (): RestTab => ({ id: newId(), draft: emptyDraft() });
48
+
49
+ let snap: Snapshot = (() => {
50
+ const tab = freshTab();
51
+ return { tabs: [tab], active: tab.id, runtime: {} };
52
+ })();
53
+ let loaded = false;
54
+ let loading: Promise<void> | undefined;
55
+ /** Tabs opened before the stored ones arrived; they are kept after the stored ones. */
56
+ let touched = false;
57
+ let saveTimer: ReturnType<typeof setTimeout> | undefined;
58
+ const controllers = new Map<string, AbortController>();
59
+ const listeners = new Set<() => void>();
60
+
61
+ const emit = () => listeners.forEach((l) => l());
62
+
63
+ function set(next: Partial<Snapshot>, persist = true) {
64
+ snap = { ...snap, ...next };
65
+ if (persist) {
66
+ touched = true;
67
+ scheduleSave();
68
+ }
69
+ emit();
70
+ }
71
+
72
+ function fileOf(): RestTabsFile {
73
+ const tabs = snap.tabs.map((t) => ({ ...t, draft: { ...t.draft } }));
74
+ let size = JSON.stringify(tabs).length;
75
+ for (const tab of [...tabs].sort((a, b) => b.draft.body.length - a.draft.body.length)) {
76
+ if (size <= MAX_FILE_CHARS) break;
77
+ size -= tab.draft.body.length;
78
+ tab.draft.body = '';
79
+ }
80
+ return { version: 1, tabs, active: snap.active };
81
+ }
82
+
83
+ function scheduleSave() {
84
+ if (!loaded) return;
85
+ clearTimeout(saveTimer);
86
+ saveTimer = setTimeout(() => void api.saveRestTabs(fileOf()).catch(() => {}), SAVE_DELAY_MS);
87
+ }
88
+
89
+ function ensureLoaded() {
90
+ loading ??= api
91
+ .restTabs()
92
+ .catch(() => undefined)
93
+ .then((file) => {
94
+ const stored = file?.tabs ?? [];
95
+ loaded = true;
96
+ if (!stored.length) {
97
+ if (touched) scheduleSave();
98
+ return;
99
+ }
100
+ if (!touched) {
101
+ const active = stored.some((t) => t.id === file?.active) ? file!.active! : stored[0]!.id;
102
+ set({ tabs: stored.slice(0, MAX_TABS), active }, false);
103
+ return;
104
+ }
105
+ const ids = new Set(snap.tabs.map((t) => t.id));
106
+ const blank = (t: RestTab) => !t.savedId && !t.title && !isNonEmpty(t.draft);
107
+ const opened = snap.tabs.filter((t) => !blank(t) || t.id === snap.active);
108
+ const merged = [...stored.filter((t) => !ids.has(t.id)), ...opened];
109
+ while (merged.length > MAX_TABS) {
110
+ const clean = merged.findIndex((t) => t.id !== snap.active && !dirtyNow(t));
111
+ merged.splice(clean < 0 ? 0 : clean, 1);
112
+ }
113
+ set({ tabs: merged });
114
+ });
115
+ return loading;
116
+ }
117
+
118
+ const sameDraft = (a: HttpRequestDraft, b: HttpRequestDraft) => JSON.stringify(a) === JSON.stringify(b);
119
+ const isNonEmpty = (draft: HttpRequestDraft) => !sameDraft(draft, emptyDraft());
120
+
121
+ /** Whether `tab` differs from its saved request, or is unsaved and not empty. */
122
+ export function isDirty(tab: RestTab, saved: SavedRequest[] | undefined): boolean {
123
+ if (!tab.savedId) return isNonEmpty(tab.draft);
124
+ const record = saved?.find((r) => r.id === tab.savedId);
125
+ return record ? !sameDraft(tab.draft, record.request) : false;
126
+ }
127
+
128
+ export const runtimeOf = (id: string): TabRuntime => snap.runtime[id] ?? blankRuntime();
129
+
130
+ function setRuntime(id: string, patch: Partial<TabRuntime>) {
131
+ set({ runtime: { ...snap.runtime, [id]: { ...runtimeOf(id), ...patch } } }, false);
132
+ }
133
+
134
+ const dirtyNow = (tab: RestTab) => (tab.savedId && !savedList ? true : isDirty(tab, savedList));
135
+
136
+ /** Adds `tab` as the active one; at the cap the oldest clean, inactive tab goes. False when every tab has unsaved work. */
137
+ function append(tab: RestTab, runtime?: Partial<TabRuntime>): boolean {
138
+ void ensureLoaded();
139
+ let tabs = snap.tabs;
140
+ if (tabs.length >= MAX_TABS) {
141
+ const drop = tabs.find((t) => t.id !== snap.active && !dirtyNow(t) && !runtimeOf(t.id).sending);
142
+ if (!drop) {
143
+ set({ full: true }, false);
144
+ return false;
145
+ }
146
+ controllers.get(drop.id)?.abort();
147
+ controllers.delete(drop.id);
148
+ tabs = tabs.filter((t) => t.id !== drop.id);
149
+ }
150
+ tabs = [...tabs, tab];
151
+ const keep = new Set(tabs.map((t) => t.id));
152
+ const rt = Object.fromEntries(Object.entries(snap.runtime).filter(([id]) => keep.has(id)));
153
+ if (runtime) rt[tab.id] = { ...blankRuntime(), ...runtime };
154
+ set({ tabs, active: tab.id, runtime: rt, full: false });
155
+ return true;
156
+ }
157
+
158
+ /**
159
+ * Opens `draft` in a new unsaved REST tab and makes it active; `auth` stays in memory only.
160
+ * Works whether the REST screen is shown or not; the caller then shows it (`go('rest')`).
161
+ */
162
+ export function openRestTab(draft: HttpRequestDraft, auth?: string, name?: string): string | null {
163
+ const id = newId();
164
+ const added = append({ id, draft, ...(name ? { title: name } : {}) }, { auth: auth ?? '', imported: { name: name ?? '', auth: auth !== undefined } });
165
+ return added ? id : null;
166
+ }
167
+
168
+ export const restTabs = {
169
+ /** The REST screen passes its saved list so the cap knows which tabs are clean. */
170
+ setSaved(list: SavedRequest[] | undefined) {
171
+ savedList = list;
172
+ },
173
+ dismissFull() {
174
+ if (snap.full) set({ full: false }, false);
175
+ },
176
+ newTab() {
177
+ append(freshTab());
178
+ },
179
+ /** Focuses the tab of saved request `item`, or opens one. */
180
+ openSaved(item: SavedRequest) {
181
+ const tab = snap.tabs.find((t) => t.savedId === item.id);
182
+ if (tab) set({ active: tab.id });
183
+ else append({ id: newId(), draft: item.request, savedId: item.id, title: item.name });
184
+ },
185
+ activate(id: string) {
186
+ if (snap.tabs.some((t) => t.id === id)) set({ active: id });
187
+ },
188
+ close(id: string) {
189
+ const index = snap.tabs.findIndex((t) => t.id === id);
190
+ if (index < 0) return;
191
+ controllers.get(id)?.abort();
192
+ controllers.delete(id);
193
+ let tabs = snap.tabs.filter((t) => t.id !== id);
194
+ if (!tabs.length) tabs = [freshTab()];
195
+ const active = snap.active === id ? tabs[Math.min(index, tabs.length - 1)]!.id : snap.active;
196
+ const { [id]: _, ...runtime } = snap.runtime;
197
+ set({ tabs, active, runtime, full: false });
198
+ },
199
+ update(id: string, patch: Partial<Omit<RestTab, 'id'>>) {
200
+ set({ tabs: snap.tabs.map((t) => (t.id === id ? { ...t, ...patch } : t)) });
201
+ },
202
+ /** Replaces tab `id`'s content with an imported request. */
203
+ loadImported(id: string, draft: HttpRequestDraft, name: string, auth?: string) {
204
+ set({ tabs: snap.tabs.map((t) => (t.id === id ? { id, draft, ...(name ? { title: name } : {}) } : t)) });
205
+ setRuntime(id, { auth: auth ?? '', imported: { name, auth: auth !== undefined }, response: undefined, error: undefined });
206
+ },
207
+ /** Marks tab `id` as showing saved `record`; its Authorization value now lives in the secret store. */
208
+ markSaved(id: string, record: SavedRequest) {
209
+ set({ tabs: snap.tabs.map((t) => (t.id === id ? { id, draft: t.draft, savedId: record.id, title: record.name } : t)) });
210
+ setRuntime(id, { auth: '', imported: undefined });
211
+ },
212
+ /** Saved request `savedId` was renamed or deleted (`record` undefined). */
213
+ savedChanged(savedId: string, record?: SavedRequest) {
214
+ set({
215
+ tabs: snap.tabs.map((t) => {
216
+ if (t.savedId !== savedId) return t;
217
+ if (record) return { ...t, title: record.name };
218
+ const { savedId: _, title: __, ...rest } = t;
219
+ return rest;
220
+ }),
221
+ });
222
+ },
223
+ setRuntime,
224
+ /** Sends tab `id` with REST environment `environment` (id); `extraHeaders` (e.g. a typed Authorization value) go after the draft's headers. */
225
+ async send(id: string, extraHeaders?: Array<[string, string]>, environment?: string) {
226
+ const tab = snap.tabs.find((t) => t.id === id);
227
+ if (!tab || runtimeOf(id).sending) return;
228
+ const abort = new AbortController();
229
+ controllers.set(id, abort);
230
+ setRuntime(id, { sending: true, error: undefined, savedPath: undefined });
231
+ try {
232
+ const spec = draftToSpec(tab.draft);
233
+ if (extraHeaders) spec.headers.push(...extraHeaders);
234
+ const response = await api.send(spec, tab.savedId, abort.signal, { environment: environment || undefined, draft: tab.draft });
235
+ if (!abort.signal.aborted) setRuntime(id, { response });
236
+ } catch (err) {
237
+ if (!abort.signal.aborted) setRuntime(id, { response: undefined, error: errorOf(err) });
238
+ } finally {
239
+ if (controllers.get(id) === abort) {
240
+ controllers.delete(id);
241
+ if (snap.tabs.some((t) => t.id === id)) setRuntime(id, { sending: false });
242
+ }
243
+ }
244
+ },
245
+ cancel(id: string) {
246
+ controllers.get(id)?.abort();
247
+ controllers.delete(id);
248
+ setRuntime(id, { sending: false });
249
+ },
250
+ };
251
+
252
+ const subscribe = (l: () => void) => {
253
+ listeners.add(l);
254
+ return () => listeners.delete(l);
255
+ };
256
+ const getSnapshot = () => snap;
257
+
258
+ /** The open tabs, the active id and per-tab runtime state; loads the stored tabs on first use. */
259
+ export function useRestTabs(): Snapshot {
260
+ useEffect(() => void ensureLoaded(), []);
261
+ return useSyncExternalStore(subscribe, getSnapshot);
262
+ }
package/src/rest.css ADDED
@@ -0,0 +1,36 @@
1
+ /* REST screen: fixed-width list of saved requests, then the editor and response split. */
2
+ .rest-body { display: flex; flex: 1; min-height: 0; }
3
+ .modal:has(.rest-env) { width: min(820px, 100%); }
4
+ .rest-env { display: flex; gap: var(--space-12); min-height: 260px; }
5
+ .rest-env-list { width: 180px; flex-shrink: 0; display: flex; flex-direction: column; gap: var(--space-4); padding-right: var(--space-8); border-right: 1px solid var(--border-soft); overflow-y: auto; }
6
+ .rest-env-vars { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-8); }
7
+ .rest-env-table { width: 100%; border-collapse: collapse; }
8
+ .rest-env-table th { text-align: left; font-weight: 500; font-size: var(--fs-xs-plus); color: var(--text-2); padding: var(--space-2) var(--space-4); }
9
+ .rest-env-table td { padding: var(--space-2) var(--space-4); vertical-align: middle; }
10
+ .rest-env-table td input:not([type='checkbox']) { width: 100%; }
11
+ .rest-history { width: 300px; flex-shrink: 0; display: flex; flex-direction: column; gap: var(--space-6); padding: var(--space-8); overflow: hidden; border-left: 1px solid var(--border-soft); }
12
+ .rest-history-head { display: flex; align-items: center; gap: var(--space-6); }
13
+ .rest-history-head .spacer { flex: 1; }
14
+ .rest-history-search { width: 100%; }
15
+ .rest-history-list { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-2); }
16
+ .rest-history-item { display: grid; grid-template-columns: 44px 1fr auto; gap: var(--space-2) var(--space-6); padding: 5px var(--space-6); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--text); text-align: left; cursor: pointer; }
17
+ .rest-history-item:hover { background: var(--hover); }
18
+ .rest-history-url { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-mono); font-size: var(--fs-xs-plus); }
19
+ .rest-history-status { font-family: var(--font-mono); font-size: var(--fs-xs); }
20
+ .rest-history-status.err { color: var(--err); }
21
+ .rest-history-meta { grid-column: 2 / 4; font-size: var(--fs-xs); color: var(--text-3); }
22
+ .rest-list { width: 220px; flex-shrink: 0; display: flex; flex-direction: column; gap: var(--space-4); padding: var(--space-8); overflow-y: auto; border-right: 1px solid var(--border-soft); }
23
+ .rest-list-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-4); }
24
+ .rest-item { position: relative; display: flex; align-items: center; border-radius: var(--radius-md); }
25
+ .rest-item:hover, .rest-item.active { background: var(--hover); }
26
+ .rest-item.drop-target { box-shadow: inset 0 2px 0 var(--accent); }
27
+ .rest-item-actions { position: absolute; right: 0; top: 0; bottom: 0; display: none; align-items: center; padding-left: var(--space-12); border-radius: 0 var(--radius-md) var(--radius-md) 0; background: linear-gradient(to right, transparent, var(--hover) 12px); }
28
+ .rest-item:hover .rest-item-actions, .rest-item:focus-within .rest-item-actions { display: flex; }
29
+ .rest-item-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--space-8); padding: var(--space-6); border: 0; background: transparent; color: var(--text); text-align: left; cursor: pointer; }
30
+ .rest-item-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
31
+ .rest-method { flex-shrink: 0; width: 44px; font-family: var(--font-mono); font-size: var(--fs-xs); font-weight: 600; color: var(--text-3); }
32
+ .rest-pane { display: flex; flex-direction: column; gap: var(--space-10); min-width: 0; min-height: 0; height: 100%; padding: var(--space-10) 14px; overflow: auto; }
33
+ .rest-auth { max-width: 420px; }
34
+ .rest-body > :last-child { flex: 1; min-width: 0; }
35
+ .rest-main { display: flex; flex-direction: column; min-height: 0; }
36
+ .rest-main > :last-child { flex: 1; min-height: 0; }
package/src/server.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { basename } from 'node:path';
2
+ import { pickFile, saveFileAs, writeFileAtomic } from '@deckbuilderkit/platform';
3
+ import { defineServerView, invalid, isRecord, type Routes } from '@deckbuilderkit/server-core';
4
+ import { fromPlatform, recordRoutes, type RecordStore, type RestHistory, restTabsRoutes } from '@deckbuilderkit/server-mods';
5
+
6
+ /** The part of the server settings the REST view owns; the app's `Settings` extends it. */
7
+ export interface RestServerSettings {
8
+ /** REST screen: split fractions of the editor and the response, active environment, history size. */
9
+ rest?: RestState;
10
+ }
11
+
12
+ export interface RestState {
13
+ sizes?: { main: [number, number] };
14
+ /** Id of the active REST environment; '' or absent = none. */
15
+ environment?: string;
16
+ /** Entries kept in the REST request history; 0 turns history off. */
17
+ historySize?: number;
18
+ }
19
+
20
+ const validPair = (v: unknown): [number, number] | null =>
21
+ Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === 'number' && n >= 0 && n <= 1) && Math.abs(v[0] + v[1] - 1) <= 0.01
22
+ ? [v[0], v[1]]
23
+ : null;
24
+
25
+ /** A copy of `raw` when it is a well-formed REST screen state, else null. */
26
+ export function validRest(raw: unknown): RestState | null {
27
+ if (!isRecord(raw)) return null;
28
+ const out: RestState = {};
29
+ if (raw.sizes !== undefined) {
30
+ if (!isRecord(raw.sizes)) return null;
31
+ const main = validPair(raw.sizes.main);
32
+ if (!main) return null;
33
+ out.sizes = { main };
34
+ }
35
+ if (raw.environment !== undefined) {
36
+ if (typeof raw.environment !== 'string' || (raw.environment !== '' && !/^[\w-]{1,64}$/.test(raw.environment))) return null;
37
+ out.environment = raw.environment;
38
+ }
39
+ if (raw.historySize !== undefined) {
40
+ if (!Number.isInteger(raw.historySize) || (raw.historySize as number) < 0 || (raw.historySize as number) > 5000) return null;
41
+ out.historySize = raw.historySize as number;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ /** `rest.historySize` (default 100; 0 = no REST history). */
47
+ export const restHistorySizeOf = (s: RestServerSettings) => {
48
+ const n = s.rest?.historySize;
49
+ return typeof n === 'number' && Number.isInteger(n) && n >= 0 ? n : 100;
50
+ };
51
+
52
+ const MAX_IMPORT_BYTES = 1_000_000;
53
+ const HTTP_FILE_FILTERS = [
54
+ { name: 'HTTP', patterns: ['*.http', '*.rest'] },
55
+ { name: 'All files', patterns: ['*.*'] },
56
+ ];
57
+
58
+ /** A saved request record; the store itself belongs to the app (its `/api/http/send` reads the saved Authorization value). */
59
+ interface SavedRequestRecord {
60
+ name: string;
61
+ request: unknown;
62
+ auth?: unknown;
63
+ }
64
+
65
+ /**
66
+ * Server half of the REST view: saved requests, open tabs, request history and the `rest` settings group.
67
+ * Stores come from the app (`ctx.stores`): `requests` (a `RecordStore`) and `restHistory` (a `RestHistory`, whose `append` the app's
68
+ * `httpRoutes` `onSend` calls). `/api/http/*` and the environments stay app-mounted (the Explorer's curl panel uses them too).
69
+ */
70
+ export const restServerView = () =>
71
+ defineServerView<RestServerSettings>({
72
+ id: 'rest',
73
+ routes: (ctx): Routes => {
74
+ const requests = ctx.stores.requests as RecordStore<SavedRequestRecord>;
75
+ const history = ctx.stores.restHistory as RestHistory;
76
+ return {
77
+ ...recordRoutes(requests, '/api/requests'),
78
+ // The saved Authorization value, only for Copy as curl.
79
+ 'GET /api/requests/auth': async ({ query }) => {
80
+ const id = query.get('id');
81
+ if (!id) throw invalid('badRequest', 'Invalid request.');
82
+ await requests.get(id);
83
+ return { value: ((await requests.secret(id, 'auth')) as string | undefined) ?? null };
84
+ },
85
+ ...restTabsRoutes({ dir: ctx.dataDir }),
86
+ // The target comes from the save dialog on this side; the client never names a path.
87
+ 'POST /api/requests/save-curl': async ({ json }) => {
88
+ const { text } = await json<{ text?: unknown }>();
89
+ if (typeof text !== 'string' || text.length > MAX_IMPORT_BYTES) throw invalid('badRequest', 'Invalid request.');
90
+ const path = await saveFileAs({ suggestedName: 'request.curl', filters: [{ name: 'cURL', patterns: ['*.curl', '*.sh'] }, { name: 'All files', patterns: ['*.*'] }] }).catch(fromPlatform);
91
+ if (!path) return { path: null };
92
+ await writeFileAtomic(path, `${text}\n`).catch(fromPlatform);
93
+ return { path };
94
+ },
95
+ // The file's text goes to the client for parsing; its path never does.
96
+ 'POST /api/requests/import-file': async () => {
97
+ const path = await pickFile({ filters: HTTP_FILE_FILTERS }).catch(fromPlatform);
98
+ if (!path) return { text: null };
99
+ const file = Bun.file(path);
100
+ if (file.size > MAX_IMPORT_BYTES) throw invalid('importTooLarge', 'The file is too large to import.');
101
+ return { name: basename(path), text: await file.text() };
102
+ },
103
+ 'GET /api/rest/history': async ({ query }) => history.list({ q: query.get('q') ?? undefined }),
104
+ 'DELETE /api/rest/history': async () => {
105
+ await history.clear();
106
+ return {};
107
+ },
108
+ };
109
+ },
110
+ settings: { validate: { rest: validRest } },
111
+ });
@@ -0,0 +1,48 @@
1
+ import type { HttpRequestDraft } from '@deckbuilderkit/editor';
2
+ import type { SectionPath, SettingDef } from '@deckbuilderkit/ui';
3
+
4
+ /** The stored REST screen state. */
5
+ export interface RestSettings {
6
+ sizes?: { main: [number, number] };
7
+ /** Id of the active REST environment; '' or absent = none. */
8
+ environment?: string;
9
+ /** Entries kept in the REST request history; 0 turns history off. */
10
+ historySize?: number;
11
+ }
12
+
13
+ /** The part of the app settings the REST view reads and writes; the app's `Settings` extends it. */
14
+ export interface RestViewSettings {
15
+ /** State of the REST screen; the server validates it. */
16
+ rest?: RestSettings;
17
+ }
18
+
19
+ export interface SavedRequest {
20
+ id: string;
21
+ name: string;
22
+ request: HttpRequestDraft;
23
+ auth?: { hasSecret: true };
24
+ }
25
+
26
+ export interface RestTabsFile {
27
+ version: 1;
28
+ tabs: Array<{ id: string; draft: HttpRequestDraft; savedId?: string; title?: string }>;
29
+ active?: string;
30
+ }
31
+
32
+ const rest: SectionPath = ['rest'];
33
+
34
+ export const restSettingDefs: SettingDef<RestViewSettings>[] = [
35
+ {
36
+ id: 'rest.historySize',
37
+ kind: 'number',
38
+ section: rest,
39
+ order: 10,
40
+ label: 'settings.rest.historySize',
41
+ description: 'settings.rest.historySize.hint',
42
+ default: 100,
43
+ min: 0,
44
+ max: 5000,
45
+ get: (s) => s.rest?.historySize ?? 100,
46
+ set: (s, historySize) => ({ rest: { ...s.rest, historySize } }),
47
+ },
48
+ ];
package/src/view.tsx ADDED
@@ -0,0 +1,28 @@
1
+ import { lazy } from 'react';
2
+ import { defineView } from '@deckbuilderkit/ui';
3
+ import { bindRestClient, type RestClient } from './client';
4
+ import { restCommands } from './commands';
5
+ import { en, ptBR } from './i18n';
6
+ import { restSettingDefs, type RestViewSettings } from './settings';
7
+
8
+ const RestScreen = lazy(() => import('./RestScreen').then((m) => ({ default: m.RestScreen })));
9
+
10
+ export interface RestViewOptions {
11
+ /** i18n key of the nav section title. */
12
+ group: string;
13
+ order: number;
14
+ /** The server calls the screen makes. */
15
+ client: RestClient;
16
+ }
17
+
18
+ export function restView({ group, order, client }: RestViewOptions) {
19
+ bindRestClient(client);
20
+ return defineView<'rest', RestViewSettings, 'nav.rest'>({
21
+ id: 'rest',
22
+ nav: { group, icon: 'send', label: 'nav.rest', order },
23
+ render: ({ settings, onSave }) => <RestScreen settings={settings} onSave={onSave} />,
24
+ commands: restCommands,
25
+ settings: { defs: restSettingDefs },
26
+ messages: { en, 'pt-BR': ptBR },
27
+ });
28
+ }
package/src/web.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { type RestClient } from './client';
2
+ export { type RestMessageKey } from './i18n';
3
+ export { type RestSettings, type RestTabsFile, type RestViewSettings, type SavedRequest } from './settings';
4
+ export { restView, type RestViewOptions } from './view';