Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"electron-squirrel-startup": "^1.0.1",
"immer": "^11.1.8",
"lucide-react": "1.22.0",
"marked": "^18.0.5",
"mime-types": "^3.0.2",
"monaco-editor": "^0.55.1",
"next-themes": "^0.4.6",
Expand Down
1 change: 1 addition & 0 deletions src/main/environment/service/environment-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export class EnvironmentService implements Initializable {
collections.push({
id: collection.id,
title: collection.title,
description: collection.description,
dirPath,
});
} catch (e) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/event/main-event-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export class MainEventService implements IEventService {
return await persistenceService.saveRequest(request, textBody);
}

async saveCollection(collection: Collection) {
await persistenceService.saveCollection(collection);
return collection;
}

async saveScript(request: TrufosRequest, type: ScriptType, script: string) {
return await persistenceService.saveScript(request, type, script);
}
Expand Down
1 change: 1 addition & 0 deletions src/main/import/service/import-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('ImportService', () => {
const firstChild = childrenLevel1[0] as TrufosRequest;
expect(firstChild.type).toBe('request');
expect(firstChild.title).toBe('200 ok');
expect(firstChild.description).toContain('This is H1');
expect(firstChild.url).toEqual({
base: 'http://echo.getpostman.com/status/200',
query: [{ key: 'key', value: 'value', isActive: true }],
Expand Down
6 changes: 6 additions & 0 deletions src/main/import/service/openapi-importer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const OPEN_API_DOCUMENT = {
info: {
title: 'Petstore API',
version: '1.0.0',
description: 'Petstore API description',
},
servers: [{ url: 'https://api.example.com/v1' }],
components: {
Expand All @@ -28,6 +29,7 @@ const OPEN_API_DOCUMENT = {
get: {
tags: ['Pets'],
summary: 'List pets',
description: 'List all pets',
security: [{ bearerAuth: [] }],
parameters: [
{
Expand Down Expand Up @@ -68,6 +70,7 @@ const OPEN_API_DOCUMENT = {
},
},
},
tags: [{ name: 'Pets', description: 'Pet operations' }],
};

describe('OpenApiImporter', () => {
Expand All @@ -81,15 +84,18 @@ describe('OpenApiImporter', () => {

expect(result.type).toBe('collection');
expect(result.title).toBe('Petstore API');
expect(result.description).toBe('Petstore API description');
expect(result.children.length).toBe(2);

const folder = result.children[0] as Folder;
expect(folder.type).toBe('folder');
expect(folder.title).toBe('Pets');
expect(folder.description).toBe('Pet operations');
expect(folder.children.length).toBe(2);

const listPets = folder.children[0] as TrufosRequest;
expect(listPets.title).toBe('List pets');
expect(listPets.description).toBe('List all pets');
expect(listPets.method).toBe('GET');
expect(listPets.url).toEqual({
base: 'https://api.example.com/v1/pets',
Expand Down
10 changes: 8 additions & 2 deletions src/main/import/service/openapi-importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class OpenApiImporter implements CollectionImporter {
type: 'collection',
lastModified: Date.now(),
title: document.info.title,
description: document.info.description,
dirPath: '',
children: [],
variables: {},
Expand Down Expand Up @@ -78,7 +79,7 @@ export class OpenApiImporter implements CollectionImporter {
continue;
}

const folder = this.getOrCreateFolder(collection, foldersByTag, firstTag);
const folder = this.getOrCreateFolder(collection, foldersByTag, firstTag, document);
request.parentId = folder.id;
folder.children.push(request);
}
Expand Down Expand Up @@ -110,6 +111,7 @@ export class OpenApiImporter implements CollectionImporter {
lastModified: Date.now(),
title:
operation.summary || operation.operationId || `${method.toUpperCase()} ${pathTemplate}`,
description: operation.description,
url: {
...parseUrl(this.joinUrl(baseUrl, pathTemplate)),
query,
Expand All @@ -124,17 +126,21 @@ export class OpenApiImporter implements CollectionImporter {
private getOrCreateFolder(
collection: TrufosCollection,
foldersByTag: Map<string, TrufosFolder>,
tag: string
tag: string,
document: OpenApiDocument
) {
let folder = foldersByTag.get(tag);
if (folder != null) return folder;

const tagDescription = document.tags?.find((candidate) => candidate.name === tag)?.description;

folder = {
id: randomUUID(),
parentId: collection.id,
type: 'folder',
lastModified: Date.now(),
title: tag,
description: tagDescription,
children: [],
};
foldersByTag.set(tag, folder);
Expand Down
12 changes: 12 additions & 0 deletions src/main/import/service/postman-importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ import {

const DEFAULT_MIME_TYPE = 'text/plain';

function extractDescription(description: unknown): string | undefined {
if (typeof description === 'string') return description;
if (description != null && typeof description === 'object' && 'content' in description) {
const content = (description as { content?: unknown }).content;
if (typeof content === 'string') return content;
}
return undefined;
}

/**
* An importer for Postman collections. It will import the collection and all of its variables,
* folders and requests. It imports using the DFS algorithm.
Expand Down Expand Up @@ -55,6 +64,7 @@ export class PostmanImporter implements CollectionImporter {
type: 'collection',
lastModified: Date.now(),
title: postmanCollection.name,
description: extractDescription((postmanCollection as { description?: unknown }).description),
dirPath: '', // must be set after import
children: [],
variables,
Expand Down Expand Up @@ -83,6 +93,7 @@ export class PostmanImporter implements CollectionImporter {
type: 'folder',
lastModified: Date.now(),
title: postmanFolder.name,
description: extractDescription((postmanFolder as { description?: unknown }).description),
children: [],
};

Expand Down Expand Up @@ -118,6 +129,7 @@ export class PostmanImporter implements CollectionImporter {
type: 'request',
lastModified: Date.now(),
title: item.name,
description: extractDescription((item as { description?: unknown }).description),
url: parseUrl(request.url.toString()),
headers: request.headers.all().map((header) => ({
key: header.key,
Expand Down
7 changes: 5 additions & 2 deletions src/main/persistence/service/info-files/v2-4-0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import { ClientCertificate } from 'shim/objects/collection';
import { SemVer } from 'main/util/semver';
import { AbstractInfoFileMigrator } from './migrator';
import { InfoFile as OldInfoFile, VERSION as OLD_VERSION } from './v2-3-0';
import z from 'zod';
import { z } from 'zod';

export const VERSION = new SemVer(2, 4, 0);

export const InfoFileBase = z.object({
id: z.string(),
version: z.literal(VERSION.string),
title: z.string(),
description: z.string().optional(),
});
export type InfoFileBase = z.infer<typeof InfoFileBase>;

Expand Down Expand Up @@ -56,7 +57,9 @@ export type InfoFile = z.infer<typeof InfoFile>;
export class InfoFileMigrator extends AbstractInfoFileMigrator<OldInfoFile, InfoFile> {
public readonly fromVersion = OLD_VERSION.toString();

async migrate(old: OldInfoFile, type: TrufosObjectType, filePath: string): Promise<InfoFile> {
async migrate(old: OldInfoFile, _type: TrufosObjectType, _filePath: string): Promise<InfoFile> {
void _type;
void _filePath;
return Object.assign(old, { version: VERSION.toString() });
}
}
4 changes: 4 additions & 0 deletions src/main/persistence/service/persistence-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ describe('PersistenceService', () => {
collection.children.push(request);

await persistenceService.saveCollection(collection, true);
await writeFile(path.join(collection.dirPath, request.title, TEXT_BODY_FILE_NAME), 'body text');
const oldInfo = JSON.parse(
await readFile(
path.join(
Expand Down Expand Up @@ -300,6 +301,9 @@ describe('PersistenceService', () => {
) as RequestInfoFile;
expect(newInfo.method).toBe(request.method);
expect(newInfo).not.toEqual(oldInfo);
expect(await readFile(path.join(collection.dirPath, request.title, TEXT_BODY_FILE_NAME), 'utf-8')).toBe(
'body text'
);
});

it('saveCollection() should save the metadata of the collection', async () => {
Expand Down
2 changes: 0 additions & 2 deletions src/main/persistence/service/persistence-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,6 @@ export class PersistenceService {
body.type = RequestBodyType.TEXT; // enforce type
delete body.text; // only present once, if imported collection
await fs.writeFile(path.join(dirPath, TEXT_BODY_FILE_NAME), textBody);
} else if (await exists(path.join(dirPath, TEXT_BODY_FILE_NAME))) {
await fs.unlink(path.join(dirPath, TEXT_BODY_FILE_NAME));
}
return request;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
import { selectRequest, useCollectionStore } from '@/state/collectionStore';
import { HeaderTab } from '@/components/mainWindow/bodyTabs/InputTabs/tabs/HeaderTab/HeaderTab';
import { BodyTab } from '@/components/mainWindow/bodyTabs/InputTabs/tabs/BodyTab';
import { DescriptionTab } from '@/components/mainWindow/bodyTabs/InputTabs/tabs/DescriptionTab';
import { ParamsTab } from '@/components/mainWindow/bodyTabs/InputTabs/tabs/ParamsTab';
import { AuthorizationTab } from '@/components/mainWindow/bodyTabs/InputTabs/tabs/AuthorizationTab/AuthorizationTab';
import { ScriptTab } from '@/components/mainWindow/bodyTabs/InputTabs/tabs/ScriptTab';
Expand Down Expand Up @@ -64,6 +65,7 @@ export function InputTabs(props: Readonly<InputTabsProps>) {
>
<TabsList>
<TabsTrigger value="body">Body</TabsTrigger>
<TabsTrigger value="description">Description</TabsTrigger>
<TabsTrigger value="queryParams">
{activeParamsCount === 0 ? 'Parameters' : `Parameters (${activeParamsCount})`}
</TabsTrigger>
Expand All @@ -79,6 +81,10 @@ export function InputTabs(props: Readonly<InputTabsProps>) {
<BodyTab />
</TabsContent>

<TabsContent value="description">
<DescriptionTab />
</TabsContent>

<TabsContent value="queryParams">
<ParamsTab />
</TabsContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useCallback, useEffect, useRef, useState, type ChangeEvent } from 'react';
import { MarkdownPreview } from '@/components/shared/MarkdownPreview';
import { selectRequest, useCollectionActions, useCollectionStore } from '@/state/collectionStore';
import { RendererEventService } from '@/services/event/renderer-event-service';
import { Textarea } from '@/components/ui/textarea';

const DEBOUNCE_MS = 500;

export function DescriptionTab() {
const request = useCollectionStore(selectRequest);
const { updateRequest } = useCollectionActions();
const [value, setValue] = useState(request?.description ?? '');
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const requestRef = useRef(request);
const valueRef = useRef(value);

requestRef.current = request;
valueRef.current = value;

useEffect(() => {
setValue(request?.description ?? '');
}, [request?.id, request?.description]);

useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);

const handleChange = useCallback(
(event: ChangeEvent<HTMLTextAreaElement>) => {
const next = event.target.value;
setValue(next);
updateRequest({ description: next });

if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
const current = requestRef.current;
if (current) {
void RendererEventService.instance.saveRequest({ ...current, description: valueRef.current });
}
}, DEBOUNCE_MS);
},
[updateRequest]
);

if (request == null) return null;

return (
<div className="flex h-full flex-col gap-4 p-4">
<Textarea
value={value}
onChange={handleChange}
placeholder="Describe what this request does, gotchas, auth requirements, examples..."
/>
<div className="bg-muted/10 border-border flex-1 overflow-y-auto rounded border p-4">
<MarkdownPreview content={value} />
</div>
</div>
);
}
19 changes: 19 additions & 0 deletions src/renderer/components/shared/MarkdownPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { cn } from '@/lib/utils';
import { renderMarkdown } from '@/lib/markdown/render-markdown';

export interface MarkdownPreviewProps {
content: string;
className?: string;
}

export function MarkdownPreview({ content, className }: MarkdownPreviewProps) {
return (
<div
className={cn(
'space-y-1 text-sm leading-6 [&>p]:mb-2 [&>p:last-child]:mb-0 [&>ul]:mb-2 [&>ol]:mb-2 [&>blockquote]:mb-2 [&>pre]:mb-2',
className
)}
dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface CollectionSettingsModalProps {
*/
type CollectionDraft = {
title: string;
description: string;
variables: VariableObjectWithKey[];
environments: EnvironmentMap;
selectedEnvironment?: string;
Expand All @@ -51,16 +52,18 @@ type CollectionDraft = {
export const CollectionSettingsModal = ({ isOpen, onClose }: CollectionSettingsModalProps) => {
const { setVariables } = useVariableActions();
const { setEnvironments, selectEnvironment } = useEnvironmentActions();
const { setClientCertificate, renameCollection } = useCollectionActions();
const { setClientCertificate, renameCollection, updateCollection } = useCollectionActions();

const variables = useVariableStore(selectVariables);
const environments = useEnvironmentStore(selectEnvironments);
const selectedEnvironment = useEnvironmentStore(selectSelectedEnvironment);
const storedCertificate = useCollectionStore((s) => s.collection?.clientCertificate ?? null);
const collectionTitle = useCollectionStore((s) => s.collection?.title ?? '');
const collectionDescription = useCollectionStore((s) => s.collection?.description ?? '');

const buildDraft = (): CollectionDraft => ({
title: collectionTitle,
description: collectionDescription,
variables: variableMapToArray(variables),
environments,
selectedEnvironment,
Expand Down Expand Up @@ -99,6 +102,7 @@ export const CollectionSettingsModal = ({ isOpen, onClose }: CollectionSettingsM
await setEnvironments(draft.environments);
await selectEnvironment(draft.selectedEnvironment);
await setClientCertificate(draft.clientCertificate);
await updateCollection({ description: draft.description.trim() === '' ? undefined : draft.description });

// rename must run last
const newTitle = draft.title.trim();
Expand Down Expand Up @@ -144,6 +148,8 @@ export const CollectionSettingsModal = ({ isOpen, onClose }: CollectionSettingsM
<GeneralEditor
name={draft.title}
onNameChange={(title) => update({ title })}
description={draft.description}
onDescriptionChange={(description) => update({ description })}
onCloseCollection={onClose}
/>
</div>
Expand Down
Loading