@deckbuilderkit/view-rest 0.0.0-stage → 0.1.1

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/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # @deckbuilderkit/view-rest
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ REST
4
+
5
+ - Id: `rest`.
6
+ - `src/view.tsx`: web entry (built with `defineView`).
7
+ - `src/server.ts`: server entry (built with `defineServerView`; validates the view's settings and serves its routes).
8
+ - `src/i18n.ts`: messages (en, pt-BR) and the view's typed `t`.
9
+ - `src/settings.ts`: setting types and schema.
10
+
11
+ Settings: REST client view: requests, environments, saved requests and history.
12
+ owns the `rest` state (panel sizes, active environment, history size). No Settings cards.
13
+ Links and capabilities: none.
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@deckbuilderkit/view-rest",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/adrianogpena/deckbuilder.git",
8
+ "directory": "packages/view-rest"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "!src/**/*.test.ts"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ "./web": "./src/web.ts",
20
+ "./server": "./src/server.ts",
21
+ "./styles.css": "./src/rest.css"
22
+ },
23
+ "dependencies": {
24
+ "@deckbuilderkit/editor": "0.1.1",
25
+ "@deckbuilderkit/platform": "0.1.1",
26
+ "@deckbuilderkit/server-core": "0.1.1",
27
+ "@deckbuilderkit/server-mods": "0.1.1",
28
+ "@deckbuilderkit/ui": "0.1.1"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "^18.3.1",
32
+ "react-dom": "^18.3.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^18.3.12",
36
+ "react": "^18.3.1",
37
+ "react-dom": "^18.3.1",
38
+ "@types/react-dom": "^18.3.1"
39
+ }
40
+ }
@@ -0,0 +1,309 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { draftToSpec, hasAuthHeader, type HttpRequestDraft, type ImportedRequest, RequestEditor, ResponseView, toCurl } from '@deckbuilderkit/editor';
3
+ import type { HttpResponseInfo, RestEnvVar } from '@deckbuilderkit/server-core/protocol';
4
+ import { type ApiError, ErrorBanner, errorOf, Icon, PageHead, SplitView, useAsync, useCommand, useDebouncedSave, useDialogs, useViewRequest } from '@deckbuilderkit/ui';
5
+ import { restCommands } from './commands';
6
+ import { restApi as api } from './client';
7
+ import type { RestSettings, RestViewSettings, SavedRequest } from './settings';
8
+ import { t } from './i18n';
9
+ import './rest.css';
10
+ import { ImportDialog, requestLabel } from './rest/ImportDialog';
11
+ import { CurlDialog } from './rest/CurlDialog';
12
+ import { HistoryPanel } from './rest/HistoryPanel';
13
+ import { EnvironmentsDialog } from './rest/EnvironmentsDialog';
14
+ import { RestTabBar } from './rest/RestTabBar';
15
+ import { openRestTab, restTabs, runtimeOf, useRestTabs } from './rest/tabs-store';
16
+
17
+ const NO_VARS: RestEnvVar[] = [];
18
+ const MAX_NAME = 80;
19
+
20
+ /** `name`, or `name (2)`, `name (3)`… when taken; the result stays within the name limit. */
21
+ function uniqueName(name: string, taken: Set<string>): string {
22
+ const base = name.trim().slice(0, MAX_NAME) || 'Request';
23
+ let candidate = base;
24
+ for (let n = 2; taken.has(candidate); n++) {
25
+ const suffix = ` (${n})`;
26
+ candidate = base.slice(0, MAX_NAME - suffix.length) + suffix;
27
+ }
28
+ return candidate;
29
+ }
30
+
31
+ const DEFAULT_MAIN: [number, number] = [0.5, 0.5];
32
+
33
+ export function RestScreen({ settings, onSave }: { settings: RestViewSettings; onSave: (patch: Partial<RestViewSettings>) => void }) {
34
+ const saved = useAsync(() => api.requests(), []);
35
+ const { confirm, prompt, element } = useDialogs();
36
+ const [state, setState] = useState<RestSettings>(settings.rest ?? {});
37
+ const { tabs, active, full } = useRestTabs();
38
+ useEffect(() => restTabs.setSaved(saved.data), [saved.data]);
39
+ const tab = tabs.find((t) => t.id === active) ?? tabs[0]!;
40
+ const draft = tab.draft;
41
+ const loaded = tab.savedId ? saved.data?.find((r) => r.id === tab.savedId) : undefined;
42
+ const { auth, imported, response, error, sending, savedPath } = runtimeOf(tab.id);
43
+ const [importing, setImporting] = useState(false);
44
+ const [curl, setCurl] = useState<string>();
45
+ const [actionError, setActionError] = useState<ApiError>();
46
+ const { request: incoming, take: takeIncoming } = useViewRequest<ImportedRequest>('rest');
47
+ useEffect(() => {
48
+ if (!incoming) return;
49
+ const request = takeIncoming();
50
+ if (request && openRestTab(request.draft, request.auth, request.name) === null) setActionError({ message: t('rest.tab.full') } as ApiError);
51
+ }, [incoming]);
52
+ const [dragId, setDragId] = useState<string | null>(null);
53
+ const [dragOver, setDragOver] = useState<string | null>(null);
54
+
55
+ const environments = useAsync(() => api.environments(), []);
56
+ const [managing, setManaging] = useState(false);
57
+ /** Active environment id ('' = none); only sent while it still exists. */
58
+ const environment = settings.rest?.environment ?? '';
59
+ const activeEnv = environments.data?.find((e) => e.id === environment);
60
+ const setEnvironment = (id: string) => onSave({ rest: { ...settings.rest, environment: id } });
61
+
62
+ const [historyOpen, setHistoryOpen] = useState(false);
63
+ const [historyTick, setHistoryTick] = useState(0);
64
+ const historyOpenRef = useRef(historyOpen);
65
+ historyOpenRef.current = historyOpen;
66
+
67
+ const persist = useDebouncedSave((next: RestSettings) => onSave({ rest: { ...settings.rest, sizes: next.sizes } }));
68
+ const sizes = state.sizes?.main ?? DEFAULT_MAIN;
69
+
70
+ const setDraft = (next: HttpRequestDraft) => restTabs.update(tab.id, { draft: next });
71
+ const setAuth = (value: string) => restTabs.setRuntime(tab.id, { auth: value });
72
+ const send = () =>
73
+ void restTabs
74
+ .send(tab.id, auth && !hasAuthHeader(draft.headers) ? [['Authorization', auth]] : undefined, environments.data ? activeEnv?.id : environment || undefined)
75
+ .then(() => historyOpenRef.current && setHistoryTick((n) => n + 1));
76
+ const cancel = () => restTabs.cancel(tab.id);
77
+
78
+ const loadImported = (item: ImportedRequest) => restTabs.loadImported(tab.id, item.draft, item.name ?? '', item.auth);
79
+
80
+ const saveImported = async (items: ImportedRequest[]) => {
81
+ const taken = new Set((saved.data ?? []).map((r) => r.name));
82
+ try {
83
+ for (const item of items) {
84
+ const name = uniqueName(requestLabel(item), taken);
85
+ taken.add(name);
86
+ await api.createRequest({ name, request: item.draft, ...(item.auth ? { auth: item.auth } : {}) });
87
+ }
88
+ } finally {
89
+ saved.reload();
90
+ }
91
+ };
92
+
93
+ const save = async (asNew: boolean) => {
94
+ const { id, savedId } = tab;
95
+ setActionError(undefined);
96
+ try {
97
+ let name = loaded?.name ?? tab.title;
98
+ if (asNew || !savedId || !name) {
99
+ name =
100
+ (await prompt({
101
+ title: t('rest.saveAsTitle'),
102
+ label: t('rest.name'),
103
+ value: name ?? imported?.name ?? '',
104
+ validate: (v) => (v.trim() ? null : t('error.badRequestName')),
105
+ })) ?? undefined;
106
+ if (!name) return;
107
+ }
108
+ // Switching a saved request from Basic/Bearer to None clears its secret ('' removes it).
109
+ const cleared = !auth && !draft.authKind && !!loaded?.request.authKind && !asNew;
110
+ const input = { name, request: draft, ...(auth ? { auth } : cleared ? { auth: '' } : {}) };
111
+ const record = asNew || !savedId ? await api.createRequest(input) : await api.updateRequest(savedId, input);
112
+ restTabs.markSaved(id, record);
113
+ saved.reload();
114
+ } catch (err) {
115
+ setActionError(errorOf(err));
116
+ }
117
+ };
118
+ useCommand(restCommands()[0]!, () => void save(false));
119
+
120
+ const remove = async (item: SavedRequest) => {
121
+ if (!(await confirm({ title: t('rest.deleteTitle', { name: item.name }), okLabel: t('rest.delete'), danger: true }))) return;
122
+ setActionError(undefined);
123
+ try {
124
+ await api.deleteRequest(item.id);
125
+ restTabs.savedChanged(item.id);
126
+ saved.reload();
127
+ } catch (err) {
128
+ setActionError(errorOf(err));
129
+ }
130
+ };
131
+
132
+ const rename = async (item: SavedRequest) => {
133
+ const name = await prompt({
134
+ title: t('rest.renameTitle'),
135
+ label: t('rest.name'),
136
+ value: item.name,
137
+ validate: (v) => (v.trim() ? null : t('error.badRequestName')),
138
+ });
139
+ if (!name || name.trim() === item.name) return;
140
+ setActionError(undefined);
141
+ try {
142
+ const record = await api.updateRequest(item.id, { name, request: item.request });
143
+ restTabs.savedChanged(item.id, record);
144
+ saved.reload();
145
+ } catch (err) {
146
+ setActionError(errorOf(err));
147
+ }
148
+ };
149
+
150
+ const moveTo = async (fromId: string, toId: string) => {
151
+ const ids = (saved.data ?? []).map((r) => r.id);
152
+ const from = ids.indexOf(fromId);
153
+ const to = ids.indexOf(toId);
154
+ if (from < 0 || to < 0 || from === to) return;
155
+ ids.splice(to, 0, ...ids.splice(from, 1));
156
+ setActionError(undefined);
157
+ try {
158
+ await api.reorderRequests(ids);
159
+ } catch (err) {
160
+ setActionError(errorOf(err));
161
+ } finally {
162
+ saved.reload();
163
+ }
164
+ };
165
+
166
+ const showCurl = async () => {
167
+ const spec = draftToSpec(draft);
168
+ let value = auth || undefined;
169
+ if (!value && tab.savedId && loaded?.auth?.hasSecret && !hasAuthHeader(draft.headers)) {
170
+ setActionError(undefined);
171
+ try {
172
+ value = (await api.requestAuth(tab.savedId)).value ?? undefined;
173
+ } catch (err) {
174
+ setActionError(errorOf(err));
175
+ return;
176
+ }
177
+ }
178
+ setCurl(toCurl(spec, value));
179
+ };
180
+
181
+ const saveResponse = async (info: HttpResponseInfo) => {
182
+ const id = tab.id;
183
+ setActionError(undefined);
184
+ try {
185
+ const { path } = await api.httpSave(info.id);
186
+ if (path) restTabs.setRuntime(id, { savedPath: path });
187
+ } catch (err) {
188
+ setActionError(errorOf(err));
189
+ }
190
+ };
191
+
192
+ return (
193
+ <div className="content screen-edges">
194
+ <PageHead title={t('nav.rest')}>
195
+ <div className="row">
196
+ <button className="btn btn-sm" onClick={() => setImporting(true)}>{t('rest.import')}</button>
197
+ <button className="btn btn-sm" onClick={() => void showCurl()}>{t('rest.code')}</button>
198
+ <button className="btn btn-sm" onClick={() => save(false)}>{t('rest.save')}</button>
199
+ <button className="btn btn-sm" onClick={() => save(true)}>{t('rest.saveAs')}</button>
200
+ </div>
201
+ </PageHead>
202
+ {actionError && <ErrorBanner error={actionError} compact />}
203
+ {full && <p className="muted" role="status">{t('rest.tab.full')}</p>}
204
+ <div className="rest-body">
205
+ <aside className="rest-list">
206
+ <div className="rest-list-head">
207
+ <span className="mono-label">{t('rest.saved')}</span>
208
+ <button className="btn btn-sm" onClick={restTabs.newTab}><Icon name="plus" /> {t('rest.new')}</button>
209
+ </div>
210
+ {saved.error && <ErrorBanner error={saved.error} onRetry={saved.reload} compact />}
211
+ {saved.data?.length === 0 && <p className="muted">{t('rest.none')}</p>}
212
+ {saved.data?.map((item) => (
213
+ <div
214
+ key={item.id}
215
+ className={`rest-item${item.id === tab.savedId ? ' active' : ''}${item.id === dragOver ? ' drop-target' : ''}`}
216
+ draggable
217
+ onDragStart={(e) => {
218
+ e.dataTransfer.effectAllowed = 'move';
219
+ e.dataTransfer.setData('text/plain', item.id);
220
+ setDragId(item.id);
221
+ }}
222
+ onDragOver={(e) => {
223
+ if (dragId === null) return;
224
+ e.preventDefault();
225
+ setDragOver(item.id);
226
+ }}
227
+ onDrop={(e) => {
228
+ e.preventDefault();
229
+ if (dragId !== null) void moveTo(dragId, item.id);
230
+ setDragId(null);
231
+ setDragOver(null);
232
+ }}
233
+ onDragEnd={() => {
234
+ setDragId(null);
235
+ setDragOver(null);
236
+ }}
237
+ >
238
+ <button className="rest-item-main" onClick={() => restTabs.openSaved(item)} title={item.request.url}>
239
+ <span className="rest-method">{item.request.method}</span>
240
+ <span className="rest-item-name">{item.name}</span>
241
+ </button>
242
+ <span className="rest-item-actions">
243
+ <button className="btn btn-ghost btn-sm btn-icon" onClick={() => rename(item)} aria-label={t('rest.rename')} title={t('rest.rename')}>
244
+ <Icon name="edit" />
245
+ </button>
246
+ <button className="btn btn-ghost btn-sm btn-icon" onClick={() => remove(item)} aria-label={t('rest.delete')} title={t('rest.delete')}>
247
+ <Icon name="trash" />
248
+ </button>
249
+ </span>
250
+ </div>
251
+ ))}
252
+ </aside>
253
+ <div className="rest-main">
254
+ <RestTabBar
255
+ tabs={tabs}
256
+ active={tab.id}
257
+ saved={saved.data}
258
+ env={{ environments: environments.data, environment, onEnvironment: setEnvironment, onManage: () => setManaging(true), history: historyOpen, onHistory: () => setHistoryOpen((v) => !v) }}
259
+ />
260
+ <SplitView
261
+ direction="column"
262
+ sizes={[sizes[0], sizes[1]]}
263
+ defaultSizes={DEFAULT_MAIN}
264
+ onResize={(s) => {
265
+ const next = { ...state, sizes: { main: [s[0], s[1]] as [number, number] } };
266
+ setState(next);
267
+ persist(next);
268
+ }}
269
+ >
270
+ <div key="request" className="rest-pane">
271
+ <RequestEditor
272
+ value={draft}
273
+ onChange={setDraft}
274
+ onSend={send}
275
+ onCancel={cancel}
276
+ sending={sending}
277
+ variables={activeEnv?.vars ?? NO_VARS}
278
+ auth={{
279
+ value: auth,
280
+ onChange: setAuth,
281
+ disabled: !tab.savedId && !imported?.auth,
282
+ placeholder: loaded?.auth?.hasSecret ? t('rest.authSaved') : undefined,
283
+ hasSaved: !!loaded?.auth?.hasSecret,
284
+ hint: tab.savedId ? t('rest.authHint') : imported?.auth ? t('rest.authImported') : t('rest.authNeedsSave'),
285
+ }}
286
+ />
287
+ </div>
288
+ <div key="response" className="rest-pane">
289
+ {savedPath && <p className="muted">{t('rest.responseSaved', { path: savedPath })}</p>}
290
+ <ResponseView response={response} error={error} sending={sending} onSave={response ? () => saveResponse(response) : undefined} />
291
+ </div>
292
+ </SplitView>
293
+ </div>
294
+ {historyOpen && <HistoryPanel refresh={historyTick} confirm={confirm} onClose={() => setHistoryOpen(false)} />}
295
+ </div>
296
+ {importing && <ImportDialog onLoad={loadImported} onSaveMany={saveImported} onClose={() => setImporting(false)} />}
297
+ {managing && (
298
+ <EnvironmentsDialog
299
+ environments={environments.data}
300
+ onChanged={environments.reload}
301
+ onDeleted={(id) => id === environment && setEnvironment('')}
302
+ onClose={() => setManaging(false)}
303
+ />
304
+ )}
305
+ {curl !== undefined && <CurlDialog curl={curl} onClose={() => setCurl(undefined)} />}
306
+ {element}
307
+ </div>
308
+ );
309
+ }
package/src/client.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { HttpClient } from '@deckbuilderkit/editor';
2
+ import type { RestEnvironment, RestEnvironmentInput, RestHistoryEntry } from '@deckbuilderkit/server-core/protocol';
3
+ import type { RestTabsFile, SavedRequest } from './settings';
4
+
5
+ /** The server calls the REST view makes (the sending and the environment list come from `HttpClient`); the app builds it from its api and passes it to `restView`. */
6
+ export interface RestClient extends HttpClient {
7
+ requests: () => Promise<SavedRequest[]>;
8
+ createRequest: (input: Record<string, unknown>) => Promise<SavedRequest>;
9
+ updateRequest: (id: string, input: Record<string, unknown>) => Promise<SavedRequest>;
10
+ deleteRequest: (id: string) => Promise<unknown>;
11
+ reorderRequests: (ids: string[]) => Promise<unknown>;
12
+ /** The saved Authorization value of request `id`, for Copy as curl. */
13
+ requestAuth: (id: string) => Promise<{ value: string | null }>;
14
+ /** Picks a file on the server side; `text` is null when the dialog was cancelled. */
15
+ importRequestsFile: () => Promise<{ name?: string; text: string | null }>;
16
+ saveCurl: (text: string) => Promise<{ path: string | null }>;
17
+ /** Saves the body of the sent response `id`; `path` is null when the dialog was cancelled. */
18
+ httpSave: (id: string) => Promise<{ path: string | null; bytes?: number }>;
19
+ restTabs: () => Promise<RestTabsFile>;
20
+ saveRestTabs: (file: RestTabsFile) => Promise<unknown>;
21
+ createRestEnvironment: (input: RestEnvironmentInput) => Promise<RestEnvironment>;
22
+ updateRestEnvironment: (id: string, input: RestEnvironmentInput) => Promise<RestEnvironment>;
23
+ deleteRestEnvironment: (id: string) => Promise<unknown>;
24
+ /** Sent requests, newest first; `q` matches method and URL. */
25
+ restHistory: (q?: string) => Promise<RestHistoryEntry[]>;
26
+ clearRestHistory: () => Promise<unknown>;
27
+ }
28
+
29
+ let current: RestClient | undefined;
30
+
31
+ /** Set by `restView`; the tabs store and the dialogs call the server through `restApi`. */
32
+ export const bindRestClient = (client: RestClient) => {
33
+ current = client;
34
+ };
35
+
36
+ export const restApi: RestClient = new Proxy({} as RestClient, {
37
+ get(_, key) {
38
+ if (!current) throw new Error('The REST client is not bound');
39
+ return current[key as keyof RestClient];
40
+ },
41
+ });
@@ -0,0 +1,4 @@
1
+ import type { CommandDef } from '@deckbuilderkit/ui';
2
+ import { t } from './i18n';
3
+
4
+ export const restCommands = (): CommandDef[] => [{ id: 'rest.save', scope: 'rest', label: t('keys.rest.save'), keys: ['Ctrl+S'], inInput: true }];
package/src/i18n.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { createT, type Messages } from '@deckbuilderkit/ui';
2
+
3
+ export const en = {
4
+ 'nav.rest': 'REST',
5
+ 'settings.section.rest': 'REST',
6
+ 'settings.rest.historySize': 'Request history size',
7
+ 'settings.rest.historySize.hint': 'Number of requests kept in the REST history. 0 turns history off.',
8
+ 'rest.saved': 'Saved requests',
9
+ 'rest.new': 'New',
10
+ 'rest.none': 'No saved requests yet.',
11
+ 'rest.save': 'Save',
12
+ 'rest.saveAs': 'Save as…',
13
+ 'rest.saveAsTitle': 'Save request',
14
+ 'rest.name': 'Name',
15
+ 'rest.delete': 'Delete',
16
+ 'rest.rename': 'Rename',
17
+ 'rest.renameTitle': 'Rename request',
18
+ 'rest.deleteTitle': 'Delete "{name}"?',
19
+ 'rest.authHint': 'Sent as the Authorization header, only to the origin of the saved URL.',
20
+ 'rest.authNeedsSave': 'Save the request first to keep an Authorization value.',
21
+ 'rest.authSaved': 'Saved',
22
+ 'rest.responseSaved': 'Saved to {path}',
23
+ 'rest.authImported': 'Imported value. Save as… keeps it.',
24
+ 'rest.tab.untitled': 'New request',
25
+ 'rest.tab.new': 'New tab',
26
+ 'rest.tab.close': 'Close tab',
27
+ 'rest.tab.unsaved': 'Unsaved changes',
28
+ 'rest.tab.full': 'All 30 tabs have unsaved changes. Save or close one to open another.',
29
+ 'rest.import': 'Import',
30
+ 'rest.code': 'Code',
31
+ 'rest.code.title': 'cURL',
32
+ 'rest.code.copy': 'Copy',
33
+ 'rest.code.copied': 'Copied',
34
+ 'rest.code.save': 'Save as file',
35
+ 'rest.code.saved': 'Saved to {path}',
36
+ 'rest.import.title': 'Import requests',
37
+ 'rest.import.paste': 'Paste a cURL command or .http content',
38
+ 'rest.import.openFile': 'Open .http file…',
39
+ 'rest.import.parse': 'Parse',
40
+ 'rest.import.close': 'Close',
41
+ 'rest.import.nothing': 'No request found in this text.',
42
+ 'rest.import.loaded': 'Loaded "{name}" into the editor.',
43
+ 'rest.import.found': '{count} requests found',
44
+ 'rest.import.saveSelected': 'Save selected',
45
+ 'rest.import.warnings': 'Warnings',
46
+ 'rest.import.warn.unknownFlag': 'Ignored the flag {arg}.',
47
+ 'rest.import.warn.missingValue': 'The flag {arg} has no value.',
48
+ 'rest.import.warn.dataFile': 'Data from a file is not read ({arg}); the text was kept as typed.',
49
+ 'rest.import.warn.cookieFile': 'Cookie files are not read ({arg}).',
50
+ 'rest.import.warn.badHeader': 'Skipped a line that is not a header: {arg}',
51
+ 'rest.import.warn.badMethod': 'The method {arg} is not supported; using GET or POST.',
52
+ 'rest.import.warn.extraArgument': 'Ignored the extra argument {arg}.',
53
+ 'rest.import.warn.noUrl': 'No URL found.',
54
+ 'rest.import.warn.bodyFile': 'A body read from a file is not imported ({arg}).',
55
+ 'rest.env.select': 'Environment',
56
+ 'rest.env.none': 'No environment',
57
+ 'rest.env.manage': 'Manage environments…',
58
+ 'rest.env.title': 'Environments',
59
+ 'rest.env.list': 'Environments',
60
+ 'rest.env.empty': 'No environments yet.',
61
+ 'rest.env.name': 'Name',
62
+ 'rest.env.addTitle': 'New environment',
63
+ 'rest.env.renameTitle': 'Rename environment',
64
+ 'rest.env.deleteTitle': 'Delete environment "{name}"?',
65
+ 'rest.env.discard': 'Discard unsaved variable changes?',
66
+ 'rest.env.discardOk': 'Discard',
67
+ 'rest.env.saved': 'Saved "{name}".',
68
+ 'rest.env.hint': 'Use {{name}} in the URL, params, headers, auth and body. Secret values are stored encrypted and never shown again.',
69
+ 'rest.env.var.enabled': 'On',
70
+ 'rest.env.var.name': 'Variable',
71
+ 'rest.env.var.value': 'Value',
72
+ 'rest.env.var.secret': 'Secret',
73
+ 'rest.env.var.set': 'Set',
74
+ 'rest.env.var.notSet': 'Not set',
75
+ 'rest.env.var.replace': 'Replace',
76
+ 'rest.env.var.add': 'Add variable',
77
+ 'rest.env.var.remove': 'Remove variable',
78
+ 'rest.history.toggle': 'History',
79
+ 'rest.history.title': 'History',
80
+ 'rest.history.search': 'Search history',
81
+ 'rest.history.empty': 'No requests sent yet.',
82
+ 'rest.history.clear': 'Clear',
83
+ 'rest.history.clearTitle': 'Clear the request history?',
84
+ 'rest.history.close': 'Close history',
85
+ 'rest.import.warn.unresolvedVar': 'The variable {arg} is not defined.',
86
+ 'keys.rest.save': 'Save request',
87
+ 'error.badEnvironmentName': 'The name must have 1 to 80 characters.',
88
+ 'error.badRequestName': 'The name must have 1 to 80 characters.',
89
+ };
90
+
91
+ export const ptBR: Messages<typeof en> = {
92
+ 'nav.rest': 'REST',
93
+ 'settings.section.rest': 'REST',
94
+ 'settings.rest.historySize': 'Tamanho do histórico de requisições',
95
+ 'settings.rest.historySize.hint': 'Número de requisições mantidas no histórico REST. 0 desativa o histórico.',
96
+ 'rest.saved': 'Requisições salvas',
97
+ 'rest.new': 'Nova',
98
+ 'rest.none': 'Nenhuma requisição salva.',
99
+ 'rest.save': 'Salvar',
100
+ 'rest.saveAs': 'Salvar como…',
101
+ 'rest.saveAsTitle': 'Salvar requisição',
102
+ 'rest.name': 'Nome',
103
+ 'rest.delete': 'Excluir',
104
+ 'rest.rename': 'Renomear',
105
+ 'rest.renameTitle': 'Renomear requisição',
106
+ 'rest.deleteTitle': 'Excluir "{name}"?',
107
+ 'rest.authHint': 'Enviado como cabeçalho Authorization, só para a origem da URL salva.',
108
+ 'rest.authNeedsSave': 'Salve a requisição primeiro para guardar um valor de Authorization.',
109
+ 'rest.authSaved': 'Salvo',
110
+ 'rest.responseSaved': 'Salvo em {path}',
111
+ 'rest.authImported': 'Valor importado. Salvar como… o mantém.',
112
+ 'rest.tab.untitled': 'Nova requisição',
113
+ 'rest.tab.new': 'Nova aba',
114
+ 'rest.tab.close': 'Fechar aba',
115
+ 'rest.tab.unsaved': 'Alterações não salvas',
116
+ 'rest.tab.full': 'As 30 abas têm alterações não salvas. Salve ou feche uma para abrir outra.',
117
+ 'rest.import': 'Importar',
118
+ 'rest.code': 'Código',
119
+ 'rest.code.title': 'cURL',
120
+ 'rest.code.copy': 'Copiar',
121
+ 'rest.code.copied': 'Copiado',
122
+ 'rest.code.save': 'Salvar como arquivo',
123
+ 'rest.code.saved': 'Salvo em {path}',
124
+ 'rest.import.title': 'Importar requisições',
125
+ 'rest.import.paste': 'Cole um comando cURL ou conteúdo .http',
126
+ 'rest.import.openFile': 'Abrir arquivo .http…',
127
+ 'rest.import.parse': 'Ler',
128
+ 'rest.import.close': 'Fechar',
129
+ 'rest.import.nothing': 'Nenhuma requisição encontrada neste texto.',
130
+ 'rest.import.loaded': '"{name}" carregada no editor.',
131
+ 'rest.import.found': '{count} requisições encontradas',
132
+ 'rest.import.saveSelected': 'Salvar selecionadas',
133
+ 'rest.import.warnings': 'Avisos',
134
+ 'rest.import.warn.unknownFlag': 'A opção {arg} foi ignorada.',
135
+ 'rest.import.warn.missingValue': 'A opção {arg} não tem valor.',
136
+ 'rest.import.warn.dataFile': 'Dados de arquivo não são lidos ({arg}); o texto foi mantido como está.',
137
+ 'rest.import.warn.cookieFile': 'Arquivos de cookie não são lidos ({arg}).',
138
+ 'rest.import.warn.badHeader': 'Linha ignorada, não é um cabeçalho: {arg}',
139
+ 'rest.import.warn.badMethod': 'O método {arg} não é suportado; usando GET ou POST.',
140
+ 'rest.import.warn.extraArgument': 'O argumento extra {arg} foi ignorado.',
141
+ 'rest.import.warn.noUrl': 'Nenhuma URL encontrada.',
142
+ 'rest.import.warn.bodyFile': 'Corpo lido de arquivo não é importado ({arg}).',
143
+ 'rest.env.select': 'Ambiente',
144
+ 'rest.env.none': 'Sem ambiente',
145
+ 'rest.env.manage': 'Gerenciar ambientes…',
146
+ 'rest.env.title': 'Ambientes',
147
+ 'rest.env.list': 'Ambientes',
148
+ 'rest.env.empty': 'Nenhum ambiente ainda.',
149
+ 'rest.env.name': 'Nome',
150
+ 'rest.env.addTitle': 'Novo ambiente',
151
+ 'rest.env.renameTitle': 'Renomear ambiente',
152
+ 'rest.env.deleteTitle': 'Excluir o ambiente "{name}"?',
153
+ 'rest.env.discard': 'Descartar as alterações não salvas nas variáveis?',
154
+ 'rest.env.discardOk': 'Descartar',
155
+ 'rest.env.saved': '"{name}" salvo.',
156
+ 'rest.env.hint': 'Use {{name}} na URL, parâmetros, cabeçalhos, autenticação e corpo. Valores secretos são guardados criptografados e nunca mais exibidos.',
157
+ 'rest.env.var.enabled': 'Ativa',
158
+ 'rest.env.var.name': 'Variável',
159
+ 'rest.env.var.value': 'Valor',
160
+ 'rest.env.var.secret': 'Secreta',
161
+ 'rest.env.var.set': 'Definido',
162
+ 'rest.env.var.notSet': 'Não definido',
163
+ 'rest.env.var.replace': 'Substituir',
164
+ 'rest.env.var.add': 'Adicionar variável',
165
+ 'rest.env.var.remove': 'Remover variável',
166
+ 'rest.history.toggle': 'Histórico',
167
+ 'rest.history.title': 'Histórico',
168
+ 'rest.history.search': 'Pesquisar histórico',
169
+ 'rest.history.empty': 'Nenhuma requisição enviada ainda.',
170
+ 'rest.history.clear': 'Limpar',
171
+ 'rest.history.clearTitle': 'Limpar o histórico de requisições?',
172
+ 'rest.history.close': 'Fechar histórico',
173
+ 'rest.import.warn.unresolvedVar': 'A variável {arg} não está definida.',
174
+ 'keys.rest.save': 'Salvar requisição',
175
+ 'error.badEnvironmentName': 'O nome deve ter de 1 a 80 caracteres.',
176
+ 'error.badRequestName': 'O nome deve ter de 1 a 80 caracteres.',
177
+ };
178
+
179
+ export type RestMessageKey = keyof typeof en;
180
+ export const t = createT<RestMessageKey>();
@@ -0,0 +1,40 @@
1
+ import { useState } from 'react';
2
+ import { CodeEditor } from '@deckbuilderkit/editor';
3
+ import { type ApiError, copyText, ErrorBanner, errorOf, Modal } from '@deckbuilderkit/ui';
4
+ import { restApi as api } from '../client';
5
+ import { t } from '../i18n';
6
+
7
+ export function CurlDialog({ curl, onClose }: { curl: string; onClose: () => void }) {
8
+ const [copied, setCopied] = useState(false);
9
+ const [savedPath, setSavedPath] = useState<string>();
10
+ const [error, setError] = useState<ApiError>();
11
+
12
+ const copy = async () => {
13
+ setCopied(await copyText(curl));
14
+ };
15
+ const save = async () => {
16
+ setError(undefined);
17
+ try {
18
+ const { path } = await api.saveCurl(curl);
19
+ if (path) setSavedPath(path);
20
+ } catch (err) {
21
+ setError(errorOf(err));
22
+ }
23
+ };
24
+
25
+ const footer = (
26
+ <>
27
+ <button className="btn" onClick={onClose}>{t('rest.import.close')}</button>
28
+ <button className="btn" onClick={save}>{t('rest.code.save')}</button>
29
+ <button className="btn btn-primary" onClick={copy}>{copied ? t('rest.code.copied') : t('rest.code.copy')}</button>
30
+ </>
31
+ );
32
+
33
+ return (
34
+ <Modal title={t('rest.code.title')} onClose={onClose} footer={footer}>
35
+ <CodeEditor value={curl} lang="shell" readOnly height={260} wrap />
36
+ {savedPath && <p className="muted">{t('rest.code.saved', { path: savedPath })}</p>}
37
+ {error && <ErrorBanner error={error} compact />}
38
+ </Modal>
39
+ );
40
+ }