From 9a1cd92939716150b8d61999059d28947b4d8943 Mon Sep 17 00:00:00 2001 From: Aarya Paradkar Date: Tue, 30 Jun 2026 13:28:25 +1000 Subject: [PATCH] #871 - Amended as per new upstream changes and suggested changes --- package.json | 1 + .../service/environment-service.ts | 1 + src/main/event/main-event-service.ts | 5 ++ .../import/service/import-service.test.ts | 1 + .../import/service/openapi-importer.test.ts | 6 ++ src/main/import/service/openapi-importer.ts | 10 ++- src/main/import/service/postman-importer.ts | 12 ++++ .../persistence/service/info-files/v2-4-0.ts | 7 +- .../service/persistence-service.test.ts | 4 ++ .../service/persistence-service.ts | 2 - .../bodyTabs/InputTabs/InputTabs.tsx | 6 ++ .../InputTabs/tabs/DescriptionTab.tsx | 61 +++++++++++++++++ .../components/shared/MarkdownPreview.tsx | 19 ++++++ .../settings/CollectionSettingsModal.tsx | 8 ++- .../settings/FolderDescriptionModal.tsx | 67 +++++++++++++++++++ .../settings/GeneralTab/GeneralEditor.tsx | 29 +++++++- .../components/sidebar/CollectionDropdown.tsx | 21 +++++- .../Nav/Dropdown/FolderDropdown.tsx | 5 +- .../SidebarRequestList/Nav/NavFolder.tsx | 9 +++ .../SidebarRequestList/SidebarRequestList.tsx | 11 ++- src/renderer/components/ui/tabs.tsx | 19 +++++- src/renderer/components/ui/textarea.tsx | 24 +++++++ .../lib/markdown/render-markdown.test.ts | 15 +++++ src/renderer/lib/markdown/render-markdown.ts | 11 +++ .../services/event/renderer-event-service.ts | 1 + src/renderer/state/collectionStore.ts | 39 ++++++++++- .../state/interface/CollectionStateActions.ts | 15 ++++- src/renderer/styles/index.css | 15 ----- src/shim/event-service.ts | 6 ++ src/shim/objects/collection.ts | 3 +- src/shim/objects/folder.ts | 3 +- src/shim/objects/request.ts | 3 +- yarn.lock | 10 +++ 33 files changed, 413 insertions(+), 36 deletions(-) create mode 100644 src/renderer/components/mainWindow/bodyTabs/InputTabs/tabs/DescriptionTab.tsx create mode 100644 src/renderer/components/shared/MarkdownPreview.tsx create mode 100644 src/renderer/components/shared/settings/FolderDescriptionModal.tsx create mode 100644 src/renderer/components/ui/textarea.tsx create mode 100644 src/renderer/lib/markdown/render-markdown.test.ts create mode 100644 src/renderer/lib/markdown/render-markdown.ts diff --git a/package.json b/package.json index ba900bee..00d46043 100644 --- a/package.json +++ b/package.json @@ -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.53.0", "next-themes": "^0.4.6", diff --git a/src/main/environment/service/environment-service.ts b/src/main/environment/service/environment-service.ts index 077f5e44..424e4939 100644 --- a/src/main/environment/service/environment-service.ts +++ b/src/main/environment/service/environment-service.ts @@ -161,6 +161,7 @@ export class EnvironmentService implements Initializable { collections.push({ id: collection.id, title: collection.title, + description: collection.description, dirPath, }); } catch (e) { diff --git a/src/main/event/main-event-service.ts b/src/main/event/main-event-service.ts index 3dc1a95a..e5a24637 100644 --- a/src/main/event/main-event-service.ts +++ b/src/main/event/main-event-service.ts @@ -114,6 +114,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); } diff --git a/src/main/import/service/import-service.test.ts b/src/main/import/service/import-service.test.ts index 78fae813..a67685ac 100644 --- a/src/main/import/service/import-service.test.ts +++ b/src/main/import/service/import-service.test.ts @@ -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 }], diff --git a/src/main/import/service/openapi-importer.test.ts b/src/main/import/service/openapi-importer.test.ts index baf20e92..9b3e3f1d 100644 --- a/src/main/import/service/openapi-importer.test.ts +++ b/src/main/import/service/openapi-importer.test.ts @@ -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: { @@ -28,6 +29,7 @@ const OPEN_API_DOCUMENT = { get: { tags: ['Pets'], summary: 'List pets', + description: 'List all pets', security: [{ bearerAuth: [] }], parameters: [ { @@ -68,6 +70,7 @@ const OPEN_API_DOCUMENT = { }, }, }, + tags: [{ name: 'Pets', description: 'Pet operations' }], }; describe('OpenApiImporter', () => { @@ -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', diff --git a/src/main/import/service/openapi-importer.ts b/src/main/import/service/openapi-importer.ts index c7f230ef..4e71cc1f 100644 --- a/src/main/import/service/openapi-importer.ts +++ b/src/main/import/service/openapi-importer.ts @@ -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: {}, @@ -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); } @@ -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, @@ -124,17 +126,21 @@ export class OpenApiImporter implements CollectionImporter { private getOrCreateFolder( collection: TrufosCollection, foldersByTag: Map, - 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); diff --git a/src/main/import/service/postman-importer.ts b/src/main/import/service/postman-importer.ts index 327be99d..107daecf 100644 --- a/src/main/import/service/postman-importer.ts +++ b/src/main/import/service/postman-importer.ts @@ -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. @@ -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, @@ -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: [], }; @@ -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, diff --git a/src/main/persistence/service/info-files/v2-4-0.ts b/src/main/persistence/service/info-files/v2-4-0.ts index bca2061d..cf8d9af5 100644 --- a/src/main/persistence/service/info-files/v2-4-0.ts +++ b/src/main/persistence/service/info-files/v2-4-0.ts @@ -13,7 +13,7 @@ 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); @@ -21,6 +21,7 @@ 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; @@ -56,7 +57,9 @@ export type InfoFile = z.infer; export class InfoFileMigrator extends AbstractInfoFileMigrator { public readonly fromVersion = OLD_VERSION.toString(); - async migrate(old: OldInfoFile, type: TrufosObjectType, filePath: string): Promise { + async migrate(old: OldInfoFile, _type: TrufosObjectType, _filePath: string): Promise { + void _type; + void _filePath; return Object.assign(old, { version: VERSION.toString() }); } } diff --git a/src/main/persistence/service/persistence-service.test.ts b/src/main/persistence/service/persistence-service.test.ts index bdd8a66e..f8178092 100644 --- a/src/main/persistence/service/persistence-service.test.ts +++ b/src/main/persistence/service/persistence-service.test.ts @@ -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( @@ -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 () => { diff --git a/src/main/persistence/service/persistence-service.ts b/src/main/persistence/service/persistence-service.ts index 76356309..39092006 100644 --- a/src/main/persistence/service/persistence-service.ts +++ b/src/main/persistence/service/persistence-service.ts @@ -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; } diff --git a/src/renderer/components/mainWindow/bodyTabs/InputTabs/InputTabs.tsx b/src/renderer/components/mainWindow/bodyTabs/InputTabs/InputTabs.tsx index fdf5a005..7d081c38 100644 --- a/src/renderer/components/mainWindow/bodyTabs/InputTabs/InputTabs.tsx +++ b/src/renderer/components/mainWindow/bodyTabs/InputTabs/InputTabs.tsx @@ -3,6 +3,7 @@ import { useMemo } 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'; @@ -31,6 +32,7 @@ export function InputTabs(props: Readonly) { Body + Description {activeParamsCount === 0 ? 'Parameters' : `Parameters (${activeParamsCount})`} @@ -46,6 +48,10 @@ export function InputTabs(props: Readonly) { + + + + diff --git a/src/renderer/components/mainWindow/bodyTabs/InputTabs/tabs/DescriptionTab.tsx b/src/renderer/components/mainWindow/bodyTabs/InputTabs/tabs/DescriptionTab.tsx new file mode 100644 index 00000000..cce36682 --- /dev/null +++ b/src/renderer/components/mainWindow/bodyTabs/InputTabs/tabs/DescriptionTab.tsx @@ -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 | 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) => { + 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 ( +
+