diff --git a/.gitignore b/.gitignore index 68c412619ab..4fa9e3e157b 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,9 @@ devenv.local.nix # pre-commit .pre-commit-config.yaml + +# AI Coding Assistants +.claude/ +.cursor/ +.aider* +.windsurfrules diff --git a/package.json b/package.json index ce10b9f5e2c..eff22ddf253 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Hoppscotch (support@hoppscotch.io)", "private": true, "license": "MIT", - "packageManager": "pnpm@10.28.1", + "packageManager": "pnpm@10.29.3", "scripts": { "preinstall": "npx only-allow pnpm", "prepare": "husky", @@ -24,8 +24,8 @@ "./packages/*" ], "devDependencies": { - "@commitlint/cli": "20.2.0", - "@commitlint/config-conventional": "20.3.1", + "@commitlint/cli": "20.4.1", + "@commitlint/config-conventional": "20.4.1", "@hoppscotch/ui": "0.2.5", "@types/node": "24.10.1", "cross-env": "10.1.0", @@ -39,15 +39,15 @@ "apiconnect-wsdl": "2.0.36", "body-parser": "2.2.1", "cross-spawn": "7.0.6", - "execa@0.10.0": "2.0.0", + "execa@<2.0.0": "2.0.0", "form-data": "4.0.4", "glob@<11.1.0": "11.1.0", - "hono@4.10.6": "4.11.4", - "jws@<3.2.3": "3.2.3", - "nodemailer@<7.0.12": "8.0.0", - "qs@6.14.0": "6.14.1", + "hono@4.11.4": "4.11.7", + "lodash@4.17.21": "4.17.23", + "nodemailer@<7.0.11": "7.0.11", + "qs@6.14.1": "6.14.2", "subscriptions-transport-ws>ws": "7.5.10", - "vue": "3.5.27", + "vue": "3.5.28", "ws": "8.17.1" }, "onlyBuiltDependencies": [ diff --git a/packages/hoppscotch-agent/package.json b/packages/hoppscotch-agent/package.json index fe240b3b0e4..a7f47df9ecd 100644 --- a/packages/hoppscotch-agent/package.json +++ b/packages/hoppscotch-agent/package.json @@ -20,26 +20,26 @@ "@hoppscotch/ui": "0.2.5", "@tauri-apps/api": "2.1.1", "@tauri-apps/plugin-shell": "2.3.3", - "@vueuse/core": "14.1.0", - "axios": "1.13.2", + "@vueuse/core": "14.2.1", + "axios": "1.13.5", "fp-ts": "2.16.11", - "lodash-es": "4.17.22", - "vue": "3.5.27" + "lodash-es": "4.17.23", + "vue": "3.5.28" }, "devDependencies": { - "@iconify-json/lucide": "1.2.86", + "@iconify-json/lucide": "1.2.91", "@tauri-apps/cli": "2.9.3", "@types/lodash-es": "4.17.12", "@types/node": "24.10.1", - "@typescript-eslint/eslint-plugin": "8.53.1", - "@typescript-eslint/parser": "8.53.1", - "@vitejs/plugin-vue": "6.0.3", - "@vue/eslint-config-typescript": "14.6.0", - "autoprefixer": "10.4.23", + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@vitejs/plugin-vue": "6.0.4", + "@vue/eslint-config-typescript": "14.7.0", + "autoprefixer": "10.4.24", "cross-env": "10.1.0", "eslint": "9.39.2", "eslint-plugin-prettier": "5.5.5", - "eslint-plugin-vue": "10.6.2", + "eslint-plugin-vue": "10.8.0", "globals": "16.5.0", "postcss": "8.5.6", "tailwindcss": "3.4.16", diff --git a/packages/hoppscotch-backend/package.json b/packages/hoppscotch-backend/package.json index 30447f6079b..54bfbe93d83 100644 --- a/packages/hoppscotch-backend/package.json +++ b/packages/hoppscotch-backend/package.json @@ -1,6 +1,6 @@ { "name": "hoppscotch-backend", - "version": "2026.1.1", + "version": "2026.2.0", "description": "", "author": "", "private": true, @@ -31,32 +31,32 @@ "do-test": "pnpm run test" }, "dependencies": { - "@apollo/server": "5.2.0", + "@apollo/server": "5.4.0", "@as-integrations/express5": "1.1.2", "@nestjs-modules/mailer": "2.0.2", - "@nestjs/apollo": "13.2.3", - "@nestjs/common": "11.1.12", - "@nestjs/config": "4.0.2", - "@nestjs/core": "11.1.12", - "@nestjs/graphql": "13.2.3", + "@nestjs/apollo": "13.2.4", + "@nestjs/common": "11.1.13", + "@nestjs/config": "4.0.3", + "@nestjs/core": "11.1.13", + "@nestjs/graphql": "13.2.4", "@nestjs/jwt": "11.0.2", "@nestjs/passport": "11.0.0", - "@nestjs/platform-express": "11.1.12", - "@nestjs/schedule": "6.1.0", - "@nestjs/swagger": "11.2.5", + "@nestjs/platform-express": "11.1.13", + "@nestjs/schedule": "6.1.1", + "@nestjs/swagger": "11.2.6", "@nestjs/terminus": "11.0.0", "@nestjs/throttler": "6.5.0", - "@prisma/adapter-pg": "7.2.0", - "@prisma/client": "7.2.0", + "@prisma/adapter-pg": "7.4.0", + "@prisma/client": "7.4.0", "argon2": "0.44.0", "bcrypt": "6.0.0", "class-transformer": "0.5.1", "class-validator": "0.14.3", "cookie": "1.1.1", "cookie-parser": "1.4.7", - "dotenv": "17.2.3", + "dotenv": "17.3.1", "express": "5.2.1", - "express-session": "1.18.2", + "express-session": "1.19.0", "fp-ts": "2.16.11", "graphql": "16.12.0", "graphql-query-complexity": "1.1.0", @@ -65,49 +65,49 @@ "handlebars": "4.7.8", "io-ts": "2.2.22", "morgan": "1.10.1", - "nodemailer": "8.0.0", + "nodemailer": "8.0.1", "passport": "0.7.0", "passport-github2": "0.1.12", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.1", "passport-local": "1.0.0", "passport-microsoft": "2.1.0", - "pg": "8.17.1", - "posthog-node": "5.23.0", - "prisma": "7.2.0", + "pg": "8.18.0", + "posthog-node": "5.24.15", + "prisma": "7.4.0", "reflect-metadata": "0.2.2", - "rimraf": "6.1.2", + "rimraf": "6.1.3", "rxjs": "7.8.2" }, "devDependencies": { "@eslint/eslintrc": "3.3.3", - "@eslint/js": "9.39.2", + "@eslint/js": "10.0.1", "@nestjs/cli": "11.0.16", "@nestjs/schematics": "11.0.9", - "@nestjs/testing": "11.1.12", + "@nestjs/testing": "11.1.13", "@relmify/jest-fp-ts": "2.1.1", "@types/bcrypt": "6.0.0", "@types/cookie-parser": "1.4.10", "@types/express": "5.0.6", "@types/jest": "30.0.0", - "@types/node": "25.0.9", - "@types/nodemailer": "7.0.5", + "@types/node": "25.2.3", + "@types/nodemailer": "7.0.10", "@types/passport-github2": "1.2.9", "@types/passport-google-oauth20": "2.0.17", "@types/passport-jwt": "4.0.1", "@types/passport-microsoft": "2.1.1", "@types/pg": "8.16.0", "@types/supertest": "6.0.3", - "@typescript-eslint/eslint-plugin": "8.53.1", - "@typescript-eslint/parser": "8.53.1", + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", "cross-env": "10.1.0", - "eslint": "9.39.2", + "eslint": "10.0.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.5", - "globals": "17.0.0", + "globals": "17.3.0", "jest": "30.2.0", "jest-mock-extended": "4.0.0", - "prettier": "3.8.0", + "prettier": "3.8.1", "source-map-support": "0.5.21", "supertest": "7.2.2", "ts-jest": "29.4.6", diff --git a/packages/hoppscotch-backend/prisma/migrations/20251207122817_add_slug_to_published_docs/migration.sql b/packages/hoppscotch-backend/prisma/migrations/20251207122817_add_slug_to_published_docs/migration.sql new file mode 100644 index 00000000000..03ae84ca306 --- /dev/null +++ b/packages/hoppscotch-backend/prisma/migrations/20251207122817_add_slug_to_published_docs/migration.sql @@ -0,0 +1,31 @@ +-- Step 1: Add slug column as nullable first +ALTER TABLE "PublishedDocs" ADD COLUMN "slug" TEXT; + +-- Step 2: For backward compatibility, set slug = id for existing records +UPDATE "PublishedDocs" SET "slug" = "id" WHERE "slug" IS NULL; + +-- Step 3: Handle duplicates - for multiple published docs with same collection and version +-- Keep the latest one (most recent), delete all older ones +-- delete old duplicates are safe, as multiple published docs with same collection and version is not expected behavior till v2025.11.x +WITH ranked_docs AS ( + SELECT + id, + "collectionID", + version, + "createdOn", + ROW_NUMBER() OVER (PARTITION BY "collectionID", version ORDER BY "createdOn" DESC) as rn + FROM "PublishedDocs" +) +DELETE FROM "PublishedDocs" +WHERE id IN ( + SELECT id FROM ranked_docs WHERE rn > 1 +); + +-- Step 4: Now make slug NOT NULL +ALTER TABLE "PublishedDocs" ALTER COLUMN "slug" SET NOT NULL; + +-- CreateIndex +CREATE INDEX "PublishedDocs_collectionID_idx" ON "PublishedDocs"("collectionID"); + +-- CreateIndex +CREATE UNIQUE INDEX "PublishedDocs_slug_version_key" ON "PublishedDocs"("slug", "version"); diff --git a/packages/hoppscotch-backend/prisma/migrations/20260209063744_published_doc_environment/migration.sql b/packages/hoppscotch-backend/prisma/migrations/20260209063744_published_doc_environment/migration.sql new file mode 100644 index 00000000000..7c9d0aa02e2 --- /dev/null +++ b/packages/hoppscotch-backend/prisma/migrations/20260209063744_published_doc_environment/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "PublishedDocs" ADD COLUMN "environmentID" TEXT, +ADD COLUMN "environmentName" TEXT, +ADD COLUMN "environmentVariables" JSONB; diff --git a/packages/hoppscotch-backend/prisma/schema.prisma b/packages/hoppscotch-backend/prisma/schema.prisma index 3fb074bb061..26a3b263059 100644 --- a/packages/hoppscotch-backend/prisma/schema.prisma +++ b/packages/hoppscotch-backend/prisma/schema.prisma @@ -297,18 +297,25 @@ model MockServerActivity { } model PublishedDocs { - id String @id @default(cuid()) - title String - collectionID String - creatorUid String - version String - autoSync Boolean - documentTree Json? // Optional if autoSync is true - workspaceType WorkspaceType - workspaceID String - metadata Json? - createdOn DateTime @default(now()) @db.Timestamptz(3) - updatedOn DateTime @updatedAt @db.Timestamptz(3) + id String @id @default(cuid()) + slug String + title String + collectionID String + creatorUid String + version String + autoSync Boolean + documentTree Json? // Optional if autoSync is true + workspaceType WorkspaceType + workspaceID String + environmentID String? + environmentName String? + environmentVariables Json? + metadata Json? + createdOn DateTime @default(now()) @db.Timestamptz(3) + updatedOn DateTime @updatedAt @db.Timestamptz(3) + + @@unique([slug, version]) + @@index([collectionID]) } enum WorkspaceType { diff --git a/packages/hoppscotch-backend/src/admin/admin.service.ts b/packages/hoppscotch-backend/src/admin/admin.service.ts index d8192dcd2ae..40330c2598e 100644 --- a/packages/hoppscotch-backend/src/admin/admin.service.ts +++ b/packages/hoppscotch-backend/src/admin/admin.service.ts @@ -220,12 +220,30 @@ export class AdminService { * @param cursorID team id * @param take number of items to fetch * @returns an array of teams + * @deprecated use fetchAllTeamsV2 instead */ async fetchAllTeams(cursorID: string, take: number) { const allTeams = await this.teamService.fetchAllTeams(cursorID, take); return allTeams; } + /** + * Fetch all the teams in the infra. + * @param searchString search on team name or ID + * @param paginationOption pagination options + * @returns an array of teams + */ + async fetchAllTeamsV2( + searchString: string, + paginationOption: OffsetPaginationArgs, + ) { + const allTeams = await this.teamService.fetchAllTeamsV2( + searchString, + paginationOption, + ); + return allTeams; + } + /** * Fetch the count of all the members in a team. * @param teamID team id diff --git a/packages/hoppscotch-backend/src/admin/infra.resolver.ts b/packages/hoppscotch-backend/src/admin/infra.resolver.ts index c3ce53e4d1f..b822fb1398d 100644 --- a/packages/hoppscotch-backend/src/admin/infra.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/infra.resolver.ts @@ -120,12 +120,32 @@ export class InfraResolver { @ResolveField(() => [Team], { description: 'Returns a list of all the teams in the infra', + deprecationReason: 'Use allTeamsV2 instead', }) async allTeams(@Args() args: PaginationArgs): Promise { const teams = await this.adminService.fetchAllTeams(args.cursor, args.take); return teams; } + @ResolveField(() => [Team], { + description: 'Returns a list of all the teams in the infra', + }) + async allTeamsV2( + @Args({ + name: 'searchString', + nullable: true, + description: 'Search on team name or ID', + }) + searchString: string, + @Args() paginationOption: OffsetPaginationArgs, + ): Promise { + const teams = await this.adminService.fetchAllTeamsV2( + searchString, + paginationOption, + ); + return teams; + } + @ResolveField(() => Team, { description: 'Returns a team info by ID when requested by Admin', }) diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 70046f997b9..c3a64aff625 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -783,6 +783,18 @@ export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED = export const INFRA_CONFIG_OPERATION_NOT_ALLOWED = 'infra_config/operation_not_allowed'; +/** + * Error message for when the onboarding status fetch fails + * (InfraConfigService) + */ +export const INFRA_CONFIG_FETCH_FAILED = 'infra_config/fetch_failed' as const; + +/** + * Onboarding has already been completed and cannot be re-run + * (OnboardingController) + */ +export const ONBOARDING_CANNOT_BE_RERUN = 'onboarding/cannot_be_rerun' as const; + /** * Error message for when the database table does not exist * (InfraConfigService) @@ -961,6 +973,13 @@ export const PUBLISHED_DOCS_UPDATE_FAILED = 'published_docs/update_failed'; */ export const PUBLISHED_DOCS_DELETION_FAILED = 'published_docs/deletion_failed'; +/** + * Published Docs invalid environment + * (PublishedDocsService) + */ +export const PUBLISHED_DOCS_INVALID_ENVIRONMENT = + 'published_docs/invalid_environment'; + /** * Published Docs not found * (PublishedDocsService) diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts index e0042682c6f..3a1b908ee76 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts @@ -7,6 +7,7 @@ import { InfraConfigEnum } from 'src/types/InfraConfig'; import { AUTH_PROVIDER_NOT_SPECIFIED, DATABASE_TABLE_NOT_EXIST, + INFRA_CONFIG_FETCH_FAILED, INFRA_CONFIG_INVALID_INPUT, INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_RESET_FAILED, @@ -512,13 +513,17 @@ export class InfraConfigService implements OnModuleInit, OnModuleDestroy { * @returns GetOnboardingStatusResponse */ async getOnboardingStatus() { - const configMap = await this.getInfraConfigsMap(); - const usersCount = await this.userService.getUsersCount(); - - return E.right({ - onboardingCompleted: configMap.ONBOARDING_COMPLETED === 'true', - canReRunOnboarding: usersCount === 0, - } as GetOnboardingStatusResponse); + try { + const configMap = await this.getInfraConfigsMap(); + const usersCount = await this.userService.getUsersCount(); + + return E.right({ + onboardingCompleted: configMap.ONBOARDING_COMPLETED === 'true', + canReRunOnboarding: usersCount === 0, + } as GetOnboardingStatusResponse); + } catch { + return E.left(INFRA_CONFIG_FETCH_FAILED); + } } /** diff --git a/packages/hoppscotch-backend/src/infra-config/onboarding.controller.ts b/packages/hoppscotch-backend/src/infra-config/onboarding.controller.ts index 1741e77d371..f0c2ef636da 100644 --- a/packages/hoppscotch-backend/src/infra-config/onboarding.controller.ts +++ b/packages/hoppscotch-backend/src/infra-config/onboarding.controller.ts @@ -11,6 +11,7 @@ import { InfraConfigService } from './infra-config.service'; import { RESTError } from 'src/types/RESTError'; import { throwHTTPErr } from 'src/utils'; import * as E from 'fp-ts/Either'; +import { ONBOARDING_CANNOT_BE_RERUN } from 'src/errors'; import { GetOnboardingConfigResponse, GetOnboardingStatusResponse, @@ -60,6 +61,24 @@ export class OnboardingController { type: SaveOnboardingConfigResponse, }) async updateOnboardingConfig(@Body() dto: SaveOnboardingConfigRequest) { + const onboardingStatus = + await this.infraConfigService.getOnboardingStatus(); + + if (E.isLeft(onboardingStatus)) + throwHTTPErr({ + message: onboardingStatus.left, + statusCode: HttpStatus.UNPROCESSABLE_ENTITY, + }); + + if ( + onboardingStatus.right.onboardingCompleted && + !onboardingStatus.right.canReRunOnboarding + ) + throwHTTPErr({ + message: ONBOARDING_CANNOT_BE_RERUN, + statusCode: HttpStatus.BAD_REQUEST, + }); + const updateConfigResult = await this.infraConfigService.updateOnboardingConfig(dto); diff --git a/packages/hoppscotch-backend/src/published-docs/input-type.args.ts b/packages/hoppscotch-backend/src/published-docs/input-type.args.ts index 29f22cf871f..af3b9681b44 100644 --- a/packages/hoppscotch-backend/src/published-docs/input-type.args.ts +++ b/packages/hoppscotch-backend/src/published-docs/input-type.args.ts @@ -51,6 +51,15 @@ export class CreatePublishedDocsArgs { description: 'Metadata associated with the published document', }) metadata: string; + + @Field({ + name: 'environmentID', + description: + 'ID of the environment to associate with the published document', + nullable: true, + }) + @IsOptional() + environmentID?: string; } @InputType() @@ -60,6 +69,7 @@ export class UpdatePublishedDocsArgs { description: 'Title of the published document', nullable: true, }) + @IsOptional() title?: string; @Field({ @@ -80,6 +90,7 @@ export class UpdatePublishedDocsArgs { 'Whether the published document should auto-sync with the source', nullable: true, }) + @IsOptional() autoSync?: boolean; @Field({ @@ -87,5 +98,15 @@ export class UpdatePublishedDocsArgs { description: 'Metadata associated with the published document', nullable: true, }) + @IsOptional() metadata?: string; + + @Field({ + name: 'environmentID', + description: + 'ID of the environment to associate with the published document. Pass null to remove the environment.', + nullable: true, + }) + @IsOptional() + environmentID?: string; } diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.controller.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.controller.ts index c61e80c0993..25b07789451 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.controller.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.controller.ts @@ -2,14 +2,12 @@ import { Controller, Get, Param, - Query, HttpCode, HttpStatus, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { PublishedDocsService } from './published-docs.service'; -import { GetPublishedDocsQueryDto } from './published-docs.dto'; import * as E from 'fp-ts/Either'; import { throwHTTPErr } from 'src/utils'; import { PublishedDocs } from './published-docs.model'; @@ -21,12 +19,12 @@ import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.gua export class PublishedDocsController { constructor(private readonly publishedDocsService: PublishedDocsService) {} - @Get(':docId') + @Get(':slug') @HttpCode(HttpStatus.OK) @ApiOperation({ - summary: 'Get published documentation', + summary: 'Get latest published documentation by slug', description: - 'Returns published collection documentation in API-doc JSON format for unauthenticated users', + 'Returns the latest version of published collection documentation by slug for unauthenticated users.', }) @ApiResponse({ status: 200, @@ -37,13 +35,42 @@ export class PublishedDocsController { status: 404, description: 'Published documentation not found', }) - async getPublishedDocs( - @Param('docId') docId: string, - @Query() query: GetPublishedDocsQueryDto, + async getPublishedDocsBySlugLatest(@Param('slug') slug: string) { + const result = await this.publishedDocsService.getPublishedDocBySlugPublic( + slug, + null, + ); + + if (E.isLeft(result)) { + throwHTTPErr({ message: result.left, statusCode: HttpStatus.NOT_FOUND }); + } + + return result.right; + } + + @Get(':slug/:version') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Get published documentation by slug and version', + description: + 'Returns published collection documentation by slug and version for unauthenticated users.', + }) + @ApiResponse({ + status: 200, + description: 'Successfully retrieved published documentation', + type: () => PublishedDocs, + }) + @ApiResponse({ + status: 404, + description: 'Published documentation not found', + }) + async getPublishedDocsBySlug( + @Param('slug') slug: string, + @Param('version') version: string, ) { - const result = await this.publishedDocsService.getPublishedDocByIDPublic( - docId, - query, + const result = await this.publishedDocsService.getPublishedDocBySlugPublic( + slug, + version, ); if (E.isLeft(result)) { diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.dto.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.dto.ts deleted file mode 100644 index baca56320e8..00000000000 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IsEnum, IsOptional } from 'class-validator'; -import { ApiPropertyOptional } from '@nestjs/swagger'; - -export enum TreeLevel { - FULL = 'full', - FIRST_LEVEL = 'first_level', -} - -export class GetPublishedDocsQueryDto { - @ApiPropertyOptional({ - description: 'Specifies whether to return full tree or only first level', - enum: TreeLevel, - default: TreeLevel.FULL, - required: false, - }) - @IsOptional() - @IsEnum(TreeLevel) - tree?: TreeLevel = TreeLevel.FULL; -} diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.model.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.model.ts index 97c1c02cd87..08ee1b90753 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.model.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.model.ts @@ -1,5 +1,69 @@ import { ObjectType, Field, ID } from '@nestjs/graphql'; import { ApiProperty } from '@nestjs/swagger'; +import { Expose, Type } from 'class-transformer'; + +@ObjectType() +export class PublishedDocsVersion { + @Field(() => ID, { + description: 'ID of the published document version', + }) + @ApiProperty({ + description: 'ID of the published document version', + example: 'doc_12345', + }) + @Expose() + id: string; + + @Field(() => String, { + description: 'Slug of the published document', + }) + @ApiProperty({ + description: 'Slug of the published document', + example: 'abc-123-uuid', + }) + @Expose() + slug: string; + + @Field(() => String, { + description: 'Version string', + }) + @ApiProperty({ + description: 'Version string', + example: '1.0.0', + }) + @Expose() + version: string; + + @Field(() => String, { + description: 'Title of the API documentation', + }) + @ApiProperty({ + description: 'Title of the API documentation', + example: 'API Documentation v1.0', + }) + @Expose() + title: string; + + @Field(() => Boolean, { + description: 'Indicates if the documentation is set to auto-sync', + }) + @ApiProperty({ + description: 'Indicates if the documentation is set to auto-sync', + example: true, + }) + @Expose() + autoSync: boolean; + + @Field(() => String, { + description: 'URL where the published API documentation can be accessed', + }) + @ApiProperty({ + description: 'URL where the published API documentation can be accessed', + example: 'https://docs.example.com/api/v1.0', + }) + @Expose() + url: string; +} @ObjectType() export class PublishedDocs { @@ -10,13 +74,27 @@ export class PublishedDocs { description: 'ID of the published API documentation', example: 'doc_12345', }) + @Expose() id: string; + @Field(() => ID, { + description: + 'Slug of the published API documentation (unique with version)', + }) + @ApiProperty({ + description: + 'Slug of the published API documentation (unique with version)', + example: 'my-api-docs', + }) + @Expose() + slug: string; + @Field({ description: 'Title of the published API documentation' }) @ApiProperty({ description: 'Title of the published API documentation', example: 'My API Documentation', }) + @Expose() title: string; @Field({ @@ -26,6 +104,7 @@ export class PublishedDocs { description: 'URL where the published API documentation can be accessed', example: 'https://docs.example.com/api', }) + @Expose() url: string; @Field({ description: 'Version of the published API documentation' }) @@ -33,6 +112,7 @@ export class PublishedDocs { description: 'Version of the published API documentation', example: '1.0.0', }) + @Expose() version: string; @Field({ description: 'Indicates if the documentation is set to auto-sync' }) @@ -40,6 +120,7 @@ export class PublishedDocs { description: 'Indicates if the documentation is set to auto-sync', example: true, }) + @Expose() autoSync: boolean; @Field({ @@ -50,6 +131,7 @@ export class PublishedDocs { example: '{"id": "string", "name": "string", "folders": [], "requests": [], "data": "string"}', }) + @Expose() documentTree: string; @Field({ @@ -79,13 +161,42 @@ export class PublishedDocs { description: 'Metadata of the documentation', example: '{"author": "John Doe", "tags": ["api", "rest"]}', }) + @Expose() metadata: string; + @Field({ + description: 'Name of the environment associated with the documentation', + nullable: true, + }) + @ApiProperty({ + description: 'Name of the environment associated with the documentation', + example: 'Production', + nullable: true, + }) + @Expose() + environmentName?: string; + + @Field({ + description: + 'Stringified JSON of the environment variables associated with the documentation', + nullable: true, + }) + @ApiProperty({ + description: + 'Stringified JSON of the environment variables associated with the documentation', + example: + '[{"key":"base_url","secret":false,"currentValue":"","initialValue":"http://hoppscotch.com"}]', + nullable: true, + }) + @Expose() + environmentVariables?: string; + @Field({ description: 'Timestamp when the documentation was created' }) @ApiProperty({ description: 'Timestamp when the documentation was created', example: '2024-01-01T00:00:00.000Z', }) + @Expose() createdOn: Date; @Field({ description: 'Timestamp when the documentation was last updated' }) @@ -93,7 +204,20 @@ export class PublishedDocs { description: 'Timestamp when the documentation was last updated', example: '2024-01-15T12:30:00.000Z', }) + @Expose() updatedOn: Date; + + @Field(() => [PublishedDocsVersion], { + description: 'All available versions of this published documentation', + nullable: true, + }) + @ApiProperty({ + description: 'All available versions of this published documentation', + type: [PublishedDocsVersion], + }) + @Expose() + @Type(() => PublishedDocsVersion) + versions?: PublishedDocsVersion[]; } @ObjectType() diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.resolver.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.resolver.ts index 11e72ec2c88..ecb263236f7 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.resolver.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.resolver.ts @@ -9,7 +9,11 @@ import { Query, } from '@nestjs/graphql'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; -import { PublishedDocs, PublishedDocsCollection } from './published-docs.model'; +import { + PublishedDocs, + PublishedDocsCollection, + PublishedDocsVersion, +} from './published-docs.model'; import { GqlAuthGuard } from 'src/guards/gql-auth.guard'; import { GqlUser } from 'src/decorators/gql-user.decorator'; import { @@ -60,6 +64,20 @@ export class PublishedDocsResolver { return collection.right; } + @ResolveField(() => [PublishedDocsVersion], { + description: 'Returns all versions of the published document (same slug)', + }) + async versions( + @Parent() publishedDocs: PublishedDocs, + ): Promise { + const versions = await this.publishedDocsService.getPublishedDocsVersions( + publishedDocs.slug, + ); + + if (E.isLeft(versions)) throwErr(versions.left); + return versions.right; + } + // Queries @Query(() => PublishedDocs, { diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts index e532eb61945..5d7769f4c89 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.service.spec.ts @@ -4,6 +4,7 @@ import { PUBLISHED_DOCS_CREATION_FAILED, PUBLISHED_DOCS_DELETION_FAILED, PUBLISHED_DOCS_INVALID_COLLECTION, + PUBLISHED_DOCS_INVALID_ENVIRONMENT, PUBLISHED_DOCS_NOT_FOUND, PUBLISHED_DOCS_UPDATE_FAILED, TEAM_INVALID_ID, @@ -21,7 +22,6 @@ import { UpdatePublishedDocsArgs, } from './input-type.args'; import { TeamAccessRole } from 'src/team/team.model'; -import { TreeLevel } from './published-docs.dto'; import { ConfigService } from '@nestjs/config'; const mockPrisma = mockDeep(); @@ -53,6 +53,7 @@ const user: User = { const userPublishedDoc: DBPublishedDocs = { id: 'pub_doc_1', + slug: 'slug-collection-1', title: 'User API Documentation', version: '1.0.0', autoSync: true, @@ -62,12 +63,16 @@ const userPublishedDoc: DBPublishedDocs = { collectionID: 'collection_1', creatorUid: user.uid, metadata: {}, + environmentID: null, + environmentName: null, + environmentVariables: null, createdOn: currentTime, updatedOn: currentTime, }; const userPublishedDocCasted: PublishedDocs = { id: userPublishedDoc.id, + slug: userPublishedDoc.slug, title: userPublishedDoc.title, version: userPublishedDoc.version, autoSync: userPublishedDoc.autoSync, @@ -75,13 +80,16 @@ const userPublishedDocCasted: PublishedDocs = { workspaceType: userPublishedDoc.workspaceType, workspaceID: userPublishedDoc.workspaceID, metadata: JSON.stringify(userPublishedDoc.metadata), + environmentName: null, + environmentVariables: null, createdOn: userPublishedDoc.createdOn, updatedOn: userPublishedDoc.updatedOn, - url: `${mockConfigService.get('VITE_BASE_URL')}/view/${userPublishedDoc.id}/${userPublishedDoc.version}`, + url: `${mockConfigService.get('VITE_BASE_URL')}/view/${userPublishedDoc.slug}/${userPublishedDoc.version}`, }; const teamPublishedDoc: DBPublishedDocs = { id: 'pub_doc_2', + slug: 'slug-team-collection-1', title: 'Team API Documentation', version: '1.0.0', autoSync: true, @@ -91,12 +99,16 @@ const teamPublishedDoc: DBPublishedDocs = { collectionID: 'team_collection_1', creatorUid: user.uid, metadata: {}, + environmentID: null, + environmentName: null, + environmentVariables: null, createdOn: currentTime, updatedOn: currentTime, }; const teamPublishedDocCasted: PublishedDocs = { id: teamPublishedDoc.id, + slug: teamPublishedDoc.slug, title: teamPublishedDoc.title, version: teamPublishedDoc.version, autoSync: teamPublishedDoc.autoSync, @@ -104,9 +116,11 @@ const teamPublishedDocCasted: PublishedDocs = { workspaceType: teamPublishedDoc.workspaceType, workspaceID: teamPublishedDoc.workspaceID, metadata: JSON.stringify(teamPublishedDoc.metadata), + environmentName: null, + environmentVariables: null, createdOn: teamPublishedDoc.createdOn, updatedOn: teamPublishedDoc.updatedOn, - url: `${mockConfigService.get('VITE_BASE_URL')}/view/${teamPublishedDoc.id}/${teamPublishedDoc.version}`, + url: `${mockConfigService.get('VITE_BASE_URL')}/view/${teamPublishedDoc.slug}/${teamPublishedDoc.version}`, }; beforeEach(() => { @@ -597,6 +611,10 @@ describe('updatePublishedDoc', () => { test('should successfully update a published document with valid inputs', async () => { mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(userPublishedDoc); + // autoSync switching from true → false requires exporting collection snapshot + mockUserCollectionService.exportUserCollectionToJSONObject.mockResolvedValueOnce( + E.right({} as any), + ); mockPrisma.publishedDocs.update.mockResolvedValueOnce({ ...userPublishedDoc, title: updateArgs.title, @@ -658,6 +676,10 @@ describe('updatePublishedDoc', () => { test('should successfully update team published document when user has OWNER role', async () => { mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(teamPublishedDoc); mockPrisma.team.findFirst.mockResolvedValueOnce({ id: 'team_1' } as any); + // autoSync switching from true → false requires exporting collection snapshot + mockTeamCollectionService.exportCollectionToJSONObject.mockResolvedValueOnce( + E.right({} as any), + ); mockPrisma.publishedDocs.update.mockResolvedValueOnce({ ...teamPublishedDoc, title: updateArgs.title, @@ -675,6 +697,10 @@ describe('updatePublishedDoc', () => { test('should successfully update team published document when user has EDITOR role', async () => { mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(teamPublishedDoc); mockPrisma.team.findFirst.mockResolvedValueOnce({ id: 'team_1' } as any); + // autoSync switching from true → false requires exporting collection snapshot + mockTeamCollectionService.exportCollectionToJSONObject.mockResolvedValueOnce( + E.right({} as any), + ); mockPrisma.publishedDocs.update.mockResolvedValueOnce({ ...teamPublishedDoc, title: updateArgs.title, @@ -979,8 +1005,111 @@ describe('checkPublishedDocsAccess', () => { }); }); -describe('getPublishedDocByIDPublic', () => { - test('should return collection data when autoSync is enabled for user workspace', async () => { +describe('getPublishedDocsVersions', () => { + test('should return all versions for a given slug ordered by autoSync and createdOn', async () => { + const mockDbRecords = [ + { + id: 'pub_doc_1', + slug: 'slug-collection-1', + version: '1.0.0', + title: 'API Docs v1', + autoSync: true, + collectionID: 'coll_1', + creatorUid: 'user_1', + workspaceType: 'USER' as any, + workspaceID: 'workspace_1', + documentTree: { folders: [] }, + metadata: { description: 'v1' }, + environmentID: null, + environmentName: null, + environmentVariables: null, + createdOn: new Date(), + updatedOn: new Date(), + }, + { + id: 'pub_doc_2', + slug: 'slug-collection-1', + version: '2.0.0', + title: 'API Docs v2', + autoSync: true, + collectionID: 'coll_1', + creatorUid: 'user_1', + workspaceType: 'USER' as any, + workspaceID: 'workspace_1', + documentTree: { folders: [] }, + metadata: { description: 'v2' }, + environmentID: null, + environmentName: null, + environmentVariables: null, + createdOn: new Date(), + updatedOn: new Date(), + }, + { + id: 'pub_doc_3', + slug: 'slug-collection-1', + version: '3.0.0', + title: 'API Docs v3', + autoSync: false, + collectionID: 'coll_1', + creatorUid: 'user_1', + workspaceType: 'USER' as any, + workspaceID: 'workspace_1', + documentTree: { folders: [] }, + metadata: { description: 'v3' }, + environmentID: null, + environmentName: null, + environmentVariables: null, + createdOn: new Date(), + updatedOn: new Date(), + }, + ]; + + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce( + mockDbRecords as any, + ); + + const result = + await publishedDocsService.getPublishedDocsVersions('slug-collection-1'); + + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + // cast() adds versions array, stringifies documentTree/metadata, and adds url + expect(result.right).toHaveLength(3); + expect(result.right[0]).toMatchObject({ + id: 'pub_doc_1', + slug: 'slug-collection-1', + version: '1.0.0', + title: 'API Docs v1', + autoSync: true, + }); + expect(result.right[0].url).toContain('/view/slug-collection-1/1.0.0'); + expect(result.right[0].versions).toEqual([]); + } + }); + + test('should return PUBLISHED_DOCS_NOT_FOUND when no versions found', async () => { + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([]); + + const result = + await publishedDocsService.getPublishedDocsVersions('non-existent-slug'); + + expect(result).toEqualLeft(PUBLISHED_DOCS_NOT_FOUND); + }); + + test('should query with correct orderBy clause for autoSync priority', async () => { + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([]); + + await publishedDocsService.getPublishedDocsVersions('test-slug'); + + expect(mockPrisma.publishedDocs.findMany).toHaveBeenCalledWith({ + where: { slug: 'test-slug' }, + orderBy: [{ autoSync: 'desc' }, { createdOn: 'desc' }], + }); + }); +}); + +describe('getPublishedDocBySlugPublic', () => { + test('should return published document by slug and version with autoSync enabled', async () => { const collectionData = { id: 'collection_1', name: 'Test Collection', @@ -992,95 +1121,855 @@ describe('getPublishedDocByIDPublic', () => { ...userPublishedDoc, autoSync: true, }); + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + { + id: userPublishedDoc.id, + slug: userPublishedDoc.slug, + version: userPublishedDoc.version, + title: userPublishedDoc.title, + autoSync: userPublishedDoc.autoSync, + }, + ] as any); mockUserCollectionService.exportUserCollectionToJSONObject.mockResolvedValueOnce( E.right(collectionData as any), ); - const result = await publishedDocsService.getPublishedDocByIDPublic( + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'slug-collection-1', + '1.0.0', + ); + + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.slug).toBe('slug-collection-1'); + expect(result.right.version).toBe('1.0.0'); + expect(result.right.documentTree).toBe(JSON.stringify(collectionData)); + } + }); + + test('should return published document with stored documentTree when autoSync is false', async () => { + const storedDocTree = { folders: [], requests: [] }; + + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce({ + ...userPublishedDoc, + autoSync: false, + documentTree: storedDocTree, + }); + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + { + id: userPublishedDoc.id, + slug: userPublishedDoc.slug, + version: userPublishedDoc.version, + title: userPublishedDoc.title, + autoSync: false, + }, + ] as any); + + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'slug-collection-1', + '1.0.0', + ); + + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.documentTree).toBe(JSON.stringify(storedDocTree)); + } + }); + + test('should throw PUBLISHED_DOCS_NOT_FOUND when slug and version combination not found', async () => { + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([]); + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(null); + + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'non-existent-slug', + '1.0.0', + ); + + expect(result).toEqualLeft(PUBLISHED_DOCS_NOT_FOUND); + }); + + test('should use unique constraint slug_version for lookup', async () => { + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + { + id: 'v1', + slug: 'test-slug', + version: '2.0.0', + title: 'V1', + autoSync: true, + }, + ] as any); + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(null); + + await publishedDocsService.getPublishedDocBySlugPublic( + 'test-slug', + '2.0.0', + ); + + expect(mockPrisma.publishedDocs.findUnique).toHaveBeenCalledWith({ + where: { + slug_version: { + slug: 'test-slug', + version: '2.0.0', + }, + }, + }); + }); + + test('should fetch all versions for the slug', async () => { + const allVersions = [ + { + id: 'v1', + slug: 'test-slug', + version: '1.0.0', + title: 'V1', + autoSync: true, + }, + { + id: 'v2', + slug: 'test-slug', + version: '2.0.0', + title: 'V2', + autoSync: true, + }, + ]; + + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce({ + ...userPublishedDoc, + autoSync: false, + }); + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce(allVersions as any); + + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'test-slug', + '1.0.0', + ); + + expect(E.isRight(result)).toBe(true); + }); + + test('should use first version as default when no version specified and versions exist', async () => { + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + { + id: 'v1', + slug: 'test-slug', + version: 'CURRENT', + title: 'V1', + autoSync: true, + }, + ] as any); + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce({ + ...userPublishedDoc, + version: 'CURRENT', + autoSync: false, + }); + + await publishedDocsService.getPublishedDocBySlugPublic('test-slug', null); + + expect(mockPrisma.publishedDocs.findUnique).toHaveBeenCalledWith({ + where: { + slug_version: { + slug: 'test-slug', + version: 'CURRENT', + }, + }, + }); + }); +}); + +describe('createPublishedDoc - slug generation and race conditions', () => { + const createArgs: CreatePublishedDocsArgs = { + title: 'New API Documentation', + version: '1.0.0', + autoSync: true, + workspaceType: WorkspaceType.USER, + workspaceID: user.uid, + collectionID: 'collection_1', + metadata: '{}', + }; + + test('should generate new slug for first version of a collection', async () => { + mockPrisma.userCollection.findUnique.mockResolvedValueOnce({ + id: 'collection_1', + userUid: user.uid, + } as any); + // No existing docs for this collection + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockPrisma.publishedDocs.create.mockResolvedValueOnce({ + ...userPublishedDoc, + slug: expect.any(String), + }); + + const result = await publishedDocsService.createPublishedDoc( + createArgs, + user, + ); + + expect(E.isRight(result)).toBe(true); + expect(mockPrisma.publishedDocs.findFirst).toHaveBeenCalledWith({ + where: { + collectionID: 'collection_1', + workspaceType: WorkspaceType.USER, + workspaceID: user.uid, + }, + orderBy: { + createdOn: 'asc', + }, + }); + }); + + test('should reuse existing slug for subsequent versions of same collection', async () => { + const existingSlug = 'existing-slug-abc'; + const existingDoc = { + ...userPublishedDoc, + slug: existingSlug, + version: '1.0.0', + }; + + mockPrisma.userCollection.findUnique.mockResolvedValueOnce({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(existingDoc); + mockPrisma.publishedDocs.create.mockResolvedValueOnce({ + ...userPublishedDoc, + slug: existingSlug, + version: '2.0.0', + }); + + const result = await publishedDocsService.createPublishedDoc( + { ...createArgs, version: '2.0.0' }, + user, + ); + + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.slug).toBe(existingSlug); + } + }); + + test('should retry on race condition (P2002 error) up to 3 times', async () => { + const uniqueConstraintError = { + code: 'P2002', + meta: { target: ['slug', 'version'] }, + }; + + mockPrisma.userCollection.findUnique.mockResolvedValue({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValue(null); + + // First two attempts fail with P2002, third succeeds + mockPrisma.publishedDocs.create + .mockRejectedValueOnce(uniqueConstraintError) + .mockRejectedValueOnce(uniqueConstraintError) + .mockResolvedValueOnce(userPublishedDoc); + + const result = await publishedDocsService.createPublishedDoc( + createArgs, + user, + ); + + expect(E.isRight(result)).toBe(true); + expect(mockPrisma.publishedDocs.create).toHaveBeenCalledTimes(3); + }); + + test('should fail after max retries (3 attempts)', async () => { + const uniqueConstraintError = { + code: 'P2002', + meta: { target: ['slug', 'version'] }, + }; + + mockPrisma.userCollection.findUnique.mockResolvedValue({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValue(null); + + // All attempts fail with P2002 + mockPrisma.publishedDocs.create.mockRejectedValue(uniqueConstraintError); + + const result = await publishedDocsService.createPublishedDoc( + createArgs, + user, + ); + + expect(result).toEqualLeft(PUBLISHED_DOCS_CREATION_FAILED); + expect(mockPrisma.publishedDocs.create).toHaveBeenCalledTimes(3); + }); + + test('should not retry on non-P2002 errors', async () => { + const otherError = new Error('Database connection failed'); + + mockPrisma.userCollection.findUnique.mockResolvedValueOnce({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockPrisma.publishedDocs.create.mockRejectedValueOnce(otherError); + + const result = await publishedDocsService.createPublishedDoc( + createArgs, + user, + ); + + expect(result).toEqualLeft(PUBLISHED_DOCS_CREATION_FAILED); + expect(mockPrisma.publishedDocs.create).toHaveBeenCalledTimes(1); + }); + + test('should store null documentTree when autoSync is true', async () => { + mockPrisma.userCollection.findUnique.mockResolvedValueOnce({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockPrisma.publishedDocs.create.mockResolvedValueOnce(userPublishedDoc); + + await publishedDocsService.createPublishedDoc( + { ...createArgs, autoSync: true }, + user, + ); + + expect(mockPrisma.publishedDocs.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + documentTree: null, + }), + }), + ); + }); + + test('should fetch and store documentTree when autoSync is false', async () => { + const collectionData = { folders: [], requests: [] }; + + mockPrisma.userCollection.findUnique.mockResolvedValueOnce({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockUserCollectionService.exportUserCollectionToJSONObject.mockResolvedValueOnce( + E.right(collectionData as any), + ); + mockPrisma.publishedDocs.create.mockResolvedValueOnce({ + ...userPublishedDoc, + documentTree: collectionData, + }); + + await publishedDocsService.createPublishedDoc( + { ...createArgs, autoSync: false }, + user, + ); + + expect( + mockUserCollectionService.exportUserCollectionToJSONObject, + ).toHaveBeenCalledWith(user.uid, 'collection_1'); + }); +}); + +describe('createPublishedDoc - environment support', () => { + const createArgs: CreatePublishedDocsArgs = { + title: 'New API Documentation', + version: '1.0.0', + autoSync: true, + workspaceType: WorkspaceType.USER, + workspaceID: user.uid, + collectionID: 'collection_1', + metadata: '{}', + }; + + test('should create published doc with environment for user workspace', async () => { + const envData = { + id: 'env_1', + userUid: user.uid, + name: 'Production', + variables: [{ key: 'BASE_URL', value: 'https://api.example.com' }], + isGlobal: false, + }; + + mockPrisma.userCollection.findUnique.mockResolvedValueOnce({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(envData as any); + mockPrisma.publishedDocs.create.mockResolvedValueOnce({ + ...userPublishedDoc, + environmentID: 'env_1', + environmentName: 'Production', + environmentVariables: envData.variables, + }); + + const result = await publishedDocsService.createPublishedDoc( + { ...createArgs, environmentID: 'env_1' }, + user, + ); + + expect(E.isRight(result)).toBe(true); + expect(mockPrisma.publishedDocs.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + environmentID: 'env_1', + environmentName: 'Production', + environmentVariables: envData.variables, + }), + }), + ); + }); + + test('should create published doc with environment for team workspace', async () => { + const teamArgs: CreatePublishedDocsArgs = { + ...createArgs, + workspaceType: WorkspaceType.TEAM, + workspaceID: 'team_1', + collectionID: 'team_collection_1', + environmentID: 'team_env_1', + }; + const envData = { + id: 'team_env_1', + teamID: 'team_1', + name: 'Staging', + variables: [{ key: 'API_KEY', value: 'abc123' }], + }; + + mockPrisma.team.findFirst.mockResolvedValueOnce({ id: 'team_1' } as any); + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({ + id: 'team_collection_1', + teamID: 'team_1', + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(envData as any); + mockPrisma.publishedDocs.create.mockResolvedValueOnce({ + ...teamPublishedDoc, + environmentID: 'team_env_1', + environmentName: 'Staging', + environmentVariables: envData.variables, + }); + + const result = await publishedDocsService.createPublishedDoc( + teamArgs, + user, + ); + + expect(E.isRight(result)).toBe(true); + expect(mockPrisma.publishedDocs.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + environmentID: 'team_env_1', + environmentName: 'Staging', + environmentVariables: envData.variables, + }), + }), + ); + }); + + test('should return error when user environment ID is invalid', async () => { + mockPrisma.userCollection.findUnique.mockResolvedValueOnce({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + + const result = await publishedDocsService.createPublishedDoc( + { ...createArgs, environmentID: 'invalid_env' }, + user, + ); + + expect(result).toEqualLeft(PUBLISHED_DOCS_INVALID_ENVIRONMENT); + }); + + test('should return error when team environment ID is invalid', async () => { + const teamArgs: CreatePublishedDocsArgs = { + ...createArgs, + workspaceType: WorkspaceType.TEAM, + workspaceID: 'team_1', + collectionID: 'team_collection_1', + environmentID: 'invalid_env', + }; + + mockPrisma.team.findFirst.mockResolvedValueOnce({ id: 'team_1' } as any); + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({ + id: 'team_collection_1', + teamID: 'team_1', + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(null); + + const result = await publishedDocsService.createPublishedDoc( + teamArgs, + user, + ); + + expect(result).toEqualLeft(PUBLISHED_DOCS_INVALID_ENVIRONMENT); + }); + + test('should create published doc without environment when environmentID is not provided', async () => { + mockPrisma.userCollection.findUnique.mockResolvedValueOnce({ + id: 'collection_1', + userUid: user.uid, + } as any); + mockPrisma.publishedDocs.findFirst.mockResolvedValueOnce(null); + mockPrisma.publishedDocs.create.mockResolvedValueOnce(userPublishedDoc); + + const result = await publishedDocsService.createPublishedDoc( + createArgs, + user, + ); + + expect(E.isRight(result)).toBe(true); + expect(mockPrisma.publishedDocs.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + environmentID: null, + environmentName: null, + environmentVariables: null, + }), + }), + ); + }); +}); + +describe('updatePublishedDoc - environment support', () => { + test('should update published doc with new environment', async () => { + const envData = { + id: 'env_2', + userUid: user.uid, + name: 'Staging', + variables: [{ key: 'API_URL', value: 'https://staging.example.com' }], + isGlobal: false, + }; + + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(userPublishedDoc); + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(envData as any); + mockPrisma.publishedDocs.update.mockResolvedValueOnce({ + ...userPublishedDoc, + environmentID: 'env_2', + environmentName: 'Staging', + environmentVariables: envData.variables, + }); + + const result = await publishedDocsService.updatePublishedDoc( userPublishedDoc.id, - { tree: TreeLevel.FULL }, + { environmentID: 'env_2' }, + user, + ); + + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.environmentName).toBe('Staging'); + expect(result.right.environmentVariables).toBe( + JSON.stringify(envData.variables), + ); + } + }); + + test('should remove environment when environmentID is set to null', async () => { + const docWithEnv = { + ...userPublishedDoc, + environmentID: 'env_1', + environmentName: 'Production', + environmentVariables: [ + { key: 'BASE_URL', value: 'https://api.example.com' }, + ], + }; + + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(docWithEnv); + mockPrisma.publishedDocs.update.mockResolvedValueOnce({ + ...docWithEnv, + environmentID: null, + environmentName: null, + environmentVariables: null, + }); + + const result = await publishedDocsService.updatePublishedDoc( + docWithEnv.id, + { environmentID: null }, + user, ); - expect(result).toMatchObject( - E.right({ - ...userPublishedDocCasted, - documentTree: JSON.stringify(collectionData), + expect(E.isRight(result)).toBe(true); + expect(mockPrisma.publishedDocs.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + environmentID: null, + environmentName: null, + environmentVariables: null, + }), }), ); }); - test('should return collection data when autoSync is enabled for team workspace', async () => { + test('should return error when updating with invalid environment ID', async () => { + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(userPublishedDoc); + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + + const result = await publishedDocsService.updatePublishedDoc( + userPublishedDoc.id, + { environmentID: 'invalid_env' }, + user, + ); + + expect(result).toEqualLeft(PUBLISHED_DOCS_INVALID_ENVIRONMENT); + }); + + test('should not change environment when environmentID is not provided in update args', async () => { + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(userPublishedDoc); + mockPrisma.publishedDocs.update.mockResolvedValueOnce(userPublishedDoc); + + await publishedDocsService.updatePublishedDoc( + userPublishedDoc.id, + { title: 'Updated Title' }, + user, + ); + + expect(mockPrisma.publishedDocs.update).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + environmentID: undefined, + environmentName: undefined, + environmentVariables: undefined, + }), + }), + ); + }); + + test('should update environment for team published doc', async () => { + const envData = { + id: 'team_env_1', + teamID: 'team_1', + name: 'Team Staging', + variables: [{ key: 'TOKEN', value: 'xyz789' }], + }; + + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(teamPublishedDoc); + mockPrisma.team.findFirst.mockResolvedValueOnce({ id: 'team_1' } as any); + mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(envData as any); + mockPrisma.publishedDocs.update.mockResolvedValueOnce({ + ...teamPublishedDoc, + environmentID: 'team_env_1', + environmentName: 'Team Staging', + environmentVariables: envData.variables, + }); + + const result = await publishedDocsService.updatePublishedDoc( + teamPublishedDoc.id, + { environmentID: 'team_env_1' }, + user, + ); + + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.environmentName).toBe('Team Staging'); + } + }); +}); + +describe('getPublishedDocBySlugPublic - environment support', () => { + test('should re-fetch environment when autoSync is true and environmentID is set', async () => { const collectionData = { - id: 'team_collection_1', - name: 'Team Test Collection', + id: 'collection_1', + name: 'Test Collection', folders: [], requests: [], }; - - mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce({ - ...teamPublishedDoc, + const envData = { + id: 'env_1', + userUid: user.uid, + name: 'Updated Env Name', + variables: [{ key: 'BASE_URL', value: 'https://updated.example.com' }], + isGlobal: false, + }; + const docWithEnv = { + ...userPublishedDoc, autoSync: true, - }); - mockTeamCollectionService.exportCollectionToJSONObject.mockResolvedValueOnce( + environmentID: 'env_1', + environmentName: 'Old Env Name', + environmentVariables: [ + { key: 'BASE_URL', value: 'https://old.example.com' }, + ], + }; + + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + docWithEnv, + ] as any); + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(docWithEnv); + mockUserCollectionService.exportUserCollectionToJSONObject.mockResolvedValueOnce( E.right(collectionData as any), ); + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(envData as any); - const result = await publishedDocsService.getPublishedDocByIDPublic( - teamPublishedDoc.id, - { tree: TreeLevel.FULL }, + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'slug-collection-1', + '1.0.0', ); - expect(result).toMatchObject( - E.right({ - ...teamPublishedDocCasted, - documentTree: JSON.stringify(collectionData), - }), + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.environmentName).toBe('Updated Env Name'); + expect(result.right.environmentVariables).toBe( + JSON.stringify(envData.variables), + ); + } + }); + + test('should not re-fetch environment when autoSync is false', async () => { + const docWithEnv = { + ...userPublishedDoc, + autoSync: false, + environmentID: 'env_1', + environmentName: 'Production', + environmentVariables: [ + { key: 'BASE_URL', value: 'https://api.example.com' }, + ], + }; + + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + docWithEnv, + ] as any); + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(docWithEnv); + + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'slug-collection-1', + '1.0.0', ); + + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.environmentName).toBe('Production'); + expect(result.right.environmentVariables).toBe( + JSON.stringify(docWithEnv.environmentVariables), + ); + } + // Should not attempt to fetch environment + expect(mockPrisma.userEnvironment.findFirst).not.toHaveBeenCalled(); + expect(mockPrisma.teamEnvironment.findFirst).not.toHaveBeenCalled(); }); - test('should throw PUBLISHED_DOCS_NOT_FOUND when document ID is invalid', async () => { - mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(null); + test('should not re-fetch environment when autoSync is true but no environmentID', async () => { + const collectionData = { + id: 'collection_1', + name: 'Test Collection', + folders: [], + requests: [], + }; - const result = await publishedDocsService.getPublishedDocByIDPublic( - 'invalid_id', - { tree: TreeLevel.FULL }, + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + userPublishedDoc, + ] as any); + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(userPublishedDoc); + mockUserCollectionService.exportUserCollectionToJSONObject.mockResolvedValueOnce( + E.right(collectionData as any), ); - expect(result).toEqualLeft(PUBLISHED_DOCS_NOT_FOUND); + + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'slug-collection-1', + '1.0.0', + ); + + expect(E.isRight(result)).toBe(true); + expect(mockPrisma.userEnvironment.findFirst).not.toHaveBeenCalled(); + expect(mockPrisma.teamEnvironment.findFirst).not.toHaveBeenCalled(); }); - test('should call exportUserCollectionToJSONObject with correct parameters', async () => { - mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce({ + test('should return error when re-fetch of environment fails', async () => { + const collectionData = { + id: 'collection_1', + name: 'Test Collection', + folders: [], + requests: [], + }; + const docWithEnv = { ...userPublishedDoc, autoSync: true, - }); + environmentID: 'env_deleted', + environmentName: 'Deleted Env', + environmentVariables: [{ key: 'OLD', value: 'data' }], + }; + + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + docWithEnv, + ] as any); + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(docWithEnv); mockUserCollectionService.exportUserCollectionToJSONObject.mockResolvedValueOnce( - E.right({} as any), + E.right(collectionData as any), ); + // Environment not found — fetchEnvironment returns Left + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); - await publishedDocsService.getPublishedDocByIDPublic(userPublishedDoc.id, { - tree: TreeLevel.FULL, - } as any); + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'slug-collection-1', + '1.0.0', + ); - expect( - mockUserCollectionService.exportUserCollectionToJSONObject, - ).toHaveBeenCalledWith(user.uid, 'collection_1', true); + expect(result).toEqualLeft(PUBLISHED_DOCS_INVALID_ENVIRONMENT); }); - test('should call exportCollectionToJSONObject with correct parameters', async () => { + test('should return null environment fields when no environment is associated', async () => { + const storedDocTree = { folders: [], requests: [] }; + + mockPrisma.publishedDocs.findMany.mockResolvedValueOnce([ + userPublishedDoc, + ] as any); mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce({ - ...teamPublishedDoc, - autoSync: true, + ...userPublishedDoc, + autoSync: false, + documentTree: storedDocTree, }); - mockTeamCollectionService.exportCollectionToJSONObject.mockResolvedValueOnce( - E.right({} as any), + + const result = await publishedDocsService.getPublishedDocBySlugPublic( + 'slug-collection-1', + '1.0.0', ); - await publishedDocsService.getPublishedDocByIDPublic(teamPublishedDoc.id, { - tree: TreeLevel.FULL, - }); + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.environmentName).toBeNull(); + expect(result.right.environmentVariables).toBeNull(); + } + }); +}); - expect( - mockTeamCollectionService.exportCollectionToJSONObject, - ).toHaveBeenCalledWith('team_1', 'team_collection_1', true); +describe('cast - environment stringification', () => { + test('should stringify environmentVariables in cast output', () => { + const docWithEnv: DBPublishedDocs = { + ...userPublishedDoc, + environmentID: 'env_1', + environmentName: 'Production', + environmentVariables: [ + { key: 'BASE_URL', value: 'https://api.example.com' }, + ], + }; + + // Access private cast via getPublishedDocByID which calls cast internally + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(docWithEnv); + + return publishedDocsService + .getPublishedDocByID(docWithEnv.id, user) + .then((result) => { + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.environmentName).toBe('Production'); + expect(typeof result.right.environmentVariables).toBe('string'); + expect(result.right.environmentVariables).toBe( + JSON.stringify([ + { key: 'BASE_URL', value: 'https://api.example.com' }, + ]), + ); + } + }); + }); + + test('should return null environmentVariables when not set', () => { + mockPrisma.publishedDocs.findUnique.mockResolvedValueOnce(userPublishedDoc); + + return publishedDocsService + .getPublishedDocByID(userPublishedDoc.id, user) + .then((result) => { + expect(E.isRight(result)).toBe(true); + if (E.isRight(result)) { + expect(result.right.environmentName).toBeNull(); + expect(result.right.environmentVariables).toBeNull(); + } + }); }); }); diff --git a/packages/hoppscotch-backend/src/published-docs/published-docs.service.ts b/packages/hoppscotch-backend/src/published-docs/published-docs.service.ts index 43e5001f642..4b090c4de7c 100644 --- a/packages/hoppscotch-backend/src/published-docs/published-docs.service.ts +++ b/packages/hoppscotch-backend/src/published-docs/published-docs.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@nestjs/common'; +import * as crypto from 'crypto'; import { CreatePublishedDocsArgs, UpdatePublishedDocsArgs, @@ -12,6 +13,7 @@ import { PUBLISHED_DOCS_CREATION_FAILED, PUBLISHED_DOCS_DELETION_FAILED, PUBLISHED_DOCS_INVALID_COLLECTION, + PUBLISHED_DOCS_INVALID_ENVIRONMENT, PUBLISHED_DOCS_NOT_FOUND, PUBLISHED_DOCS_UPDATE_FAILED, TEAM_INVALID_COLL_ID, @@ -19,13 +21,16 @@ import { USER_COLL_NOT_FOUND, } from 'src/errors'; import * as E from 'fp-ts/Either'; -import { PublishedDocs } from './published-docs.model'; +import { PublishedDocs, PublishedDocsVersion } from './published-docs.model'; import { OffsetPaginationArgs } from 'src/types/input-types.args'; import { stringToJson } from 'src/utils'; import { UserCollectionService } from 'src/user-collection/user-collection.service'; import { TeamCollectionService } from 'src/team-collection/team-collection.service'; -import { GetPublishedDocsQueryDto, TreeLevel } from './published-docs.dto'; import { ConfigService } from '@nestjs/config'; +import { PrismaError } from 'src/prisma/prisma-error-codes'; +import { CollectionFolder } from 'src/types/CollectionFolder'; +import { plainToInstance } from 'class-transformer'; +import { JsonValue } from '@prisma/client/runtime/client'; @Injectable() export class PublishedDocsService { @@ -36,18 +41,83 @@ export class PublishedDocsService { private readonly configService: ConfigService, ) {} + /** + * Get or generate slug for a collection + * - For existing published docs with the same collectionID, reuse the slug + * - For new collections, generate a new UUID-based slug + */ + private async getOrGenerateSlug( + collectionID: string, + workspaceType: WorkspaceType, + workspaceID: string, + ): Promise { + // Check if there's already a published doc for this collection + const existingDoc = await this.prisma.publishedDocs.findFirst({ + where: { + collectionID, + workspaceType, + workspaceID, + }, + orderBy: { + createdOn: 'asc', // Get the oldest one + }, + }); + + // If exists, reuse its slug + if (existingDoc) { + return existingDoc.slug; + } + + // Otherwise, generate a new slug using crypto.randomUUID() + return crypto.randomUUID(); + } + /** * Cast database PublishedDocs to GraphQL PublishedDocs */ - private cast(doc: DbPublishedDocs): PublishedDocs { + private cast( + doc: DbPublishedDocs, + versions: PublishedDocsVersion[] = [], + ): PublishedDocs { return { ...doc, + versions, documentTree: JSON.stringify(doc.documentTree), metadata: JSON.stringify(doc.metadata), - url: `${this.configService.get('VITE_BASE_URL')}/view/${doc.id}/${doc.version}`, + environmentName: doc.environmentName ?? null, + environmentVariables: doc.environmentVariables + ? JSON.stringify(doc.environmentVariables) + : null, + url: `${this.configService.get('VITE_BASE_URL')}/view/${doc.slug}/${doc.version}`, }; } + /** + * Fetch environment by ID based on workspace type + * Returns the environment name and variables, or an error if not found + */ + private async fetchEnvironment( + environmentID: string, + workspaceType: WorkspaceType, + workspaceID: string, + ): Promise> { + if (workspaceType === WorkspaceType.TEAM) { + const env = await this.prisma.teamEnvironment.findFirst({ + where: { id: environmentID, teamID: workspaceID }, + }); + if (!env) return E.left(PUBLISHED_DOCS_INVALID_ENVIRONMENT); + return E.right({ name: env.name, variables: env.variables }); + } else if (workspaceType === WorkspaceType.USER) { + const env = await this.prisma.userEnvironment.findFirst({ + where: { id: environmentID, userUid: workspaceID }, + }); + if (!env) return E.left(PUBLISHED_DOCS_INVALID_ENVIRONMENT); + return E.right({ name: env.name ?? '', variables: env.variables }); + } + + return E.left(PUBLISHED_DOCS_INVALID_ENVIRONMENT); + } + /** * Check if user has access to a team with specific roles */ @@ -195,6 +265,21 @@ export class PublishedDocsService { return E.left(PUBLISHED_DOCS_INVALID_COLLECTION); } + /** + * (Field resolver) + * Get all versions of a published document by slug + */ + async getPublishedDocsVersions(slug: string) { + const allVersions = await this.prisma.publishedDocs.findMany({ + where: { slug }, + orderBy: [{ autoSync: 'desc' }, { createdOn: 'desc' }], + }); + + if (allVersions.length === 0) return E.left(PUBLISHED_DOCS_NOT_FOUND); + + return E.right(allVersions.map((doc) => this.cast(doc))); + } + /** * Get a published document by ID */ @@ -215,19 +300,29 @@ export class PublishedDocsService { } /** - * Get a published document by ID for public access (unauthenticated) - * @param id - The ID of the published document - * @param query - Query parameters specifying tree level + * Get a published document by slug and version for public access (unauthenticated) + * @param slug - The slug of the published document + * @param version - The version of the published document */ - async getPublishedDocByIDPublic( - id: string, - query: GetPublishedDocsQueryDto, + async getPublishedDocBySlugPublic( + slug: string, + version: string | null, ): Promise> { + const allVersions = await this.getPublishedDocsVersions(slug); + if (E.isLeft(allVersions)) return E.left(allVersions.left); + const publishedDocs = await this.prisma.publishedDocs.findUnique({ - where: { id }, + where: { + slug_version: { + slug, + version: version ? version : allVersions.right[0].version, // If version is not specified, get the latest version + }, + }, }); if (!publishedDocs) return E.left(PUBLISHED_DOCS_NOT_FOUND); + let docToReturn = publishedDocs; + // if autoSync is enabled, fetch from the collection directly if (publishedDocs.autoSync) { const collectionResult = @@ -235,12 +330,10 @@ export class PublishedDocsService { ? await this.userCollectionService.exportUserCollectionToJSONObject( publishedDocs.creatorUid, publishedDocs.collectionID, - query.tree === TreeLevel.FULL, ) : await this.teamCollectionService.exportCollectionToJSONObject( publishedDocs.workspaceID, publishedDocs.collectionID, - query.tree === TreeLevel.FULL, ); if (E.isLeft(collectionResult)) { @@ -258,15 +351,44 @@ export class PublishedDocsService { return E.left(collectionResult.left); } - return E.right( - this.cast({ - ...publishedDocs, - documentTree: JSON.parse(JSON.stringify(collectionResult.right)), - }), - ); + // Re-fetch environment if environmentID is set + let environmentName = publishedDocs.environmentName; + let environmentVariables = publishedDocs.environmentVariables; + + if (publishedDocs.environmentID) { + const workspaceID = + publishedDocs.workspaceType === WorkspaceType.USER + ? publishedDocs.creatorUid + : publishedDocs.workspaceID; + + const envResult = await this.fetchEnvironment( + publishedDocs.environmentID, + publishedDocs.workspaceType as WorkspaceType, + workspaceID, + ); + if (E.isLeft(envResult)) return E.left(envResult.left); + + if (E.isRight(envResult) && envResult.right) { + environmentName = envResult.right.name; + environmentVariables = envResult.right.variables; + } + } + + docToReturn = { + ...publishedDocs, + documentTree: collectionResult.right as unknown as JsonValue, + environmentName, + environmentVariables, + }; } - return E.right(this.cast(publishedDocs)); + return E.right( + plainToInstance( + PublishedDocs, + this.cast(docToReturn, allVersions.right), + { excludeExtraneousValues: true, enableCircularCheck: true }, + ), + ); } /** @@ -281,7 +403,7 @@ export class PublishedDocsService { if (docsToDelete.length > 0) { const idsToDelete = docsToDelete.map((doc) => doc.id); - this.prisma.publishedDocs.deleteMany({ + await this.prisma.publishedDocs.deleteMany({ where: { id: { in: idsToDelete } }, }); } @@ -383,7 +505,11 @@ export class PublishedDocsService { * @param args - Arguments for creating the published document * @param user - The user creating the published document */ - async createPublishedDoc(args: CreatePublishedDocsArgs, user: User) { + async createPublishedDoc( + args: CreatePublishedDocsArgs, + user: User, + retryCount: number = 0, + ): Promise> { try { // Validate workspace type and ID const workspaceValidation = await this.validateWorkspace(user, { @@ -408,25 +534,87 @@ export class PublishedDocsService { const metadata = stringToJson(args.metadata); if (E.isLeft(metadata)) return E.left(metadata.left); - // Create published document + // Get or generate slug for this collection + const workspaceID = + args.workspaceType === WorkspaceType.TEAM ? args.workspaceID : user.uid; + + // Get or generate slug + const slug = await this.getOrGenerateSlug( + args.collectionID, + args.workspaceType, + workspaceID, + ); + + let documentTree: CollectionFolder | null = null; + // If autoSync is disabled, fetch the latest collection data for snapshot + if (!args.autoSync) { + const collectionResult = + args.workspaceType === WorkspaceType.USER + ? await this.userCollectionService.exportUserCollectionToJSONObject( + user.uid, + args.collectionID, + ) + : await this.teamCollectionService.exportCollectionToJSONObject( + args.workspaceID, + args.collectionID, + ); + + if (E.isLeft(collectionResult)) { + return E.left(collectionResult.left); + } + + documentTree = collectionResult.right; + } + + // Fetch environment if environmentID is provided + let environmentName: string | null = null; + let environmentVariables: JsonValue | null = null; + + if (args.environmentID) { + const envResult = await this.fetchEnvironment( + args.environmentID, + args.workspaceType, + workspaceID, + ); + if (E.isLeft(envResult)) return E.left(envResult.left); + if (envResult.right) { + environmentName = envResult.right.name; + environmentVariables = envResult.right.variables; + } + } + + // Attempt to create the published document const newPublishedDoc = await this.prisma.publishedDocs.create({ data: { title: args.title, + slug: slug, collectionID: args.collectionID, creatorUid: user.uid, version: args.version, autoSync: args.autoSync, workspaceType: args.workspaceType, - workspaceID: - args.workspaceType === WorkspaceType.TEAM - ? args.workspaceID - : user.uid, + workspaceID: workspaceID, + documentTree: documentTree as unknown as JsonValue, metadata: metadata.right, + environmentID: args.environmentID ?? null, + environmentName, + environmentVariables, }, }); return E.right(this.cast(newPublishedDoc)); } catch (error) { + // Check if it's a unique constraint violation on [slug, version] + // Allow up to 3 total attempts (initial + 2 retries) + const maxRetries = 2; + if ( + error.code === PrismaError.UNIQUE_CONSTRAINT_VIOLATION && + retryCount < maxRetries + ) { + // Race condition detected: retry with fresh slug generation + return this.createPublishedDoc(args, user, retryCount + 1); + } + console.error('Error creating published document:', error); return E.left(PUBLISHED_DOCS_CREATION_FAILED); } @@ -464,6 +652,59 @@ export class PublishedDocsService { if (E.isLeft(metadata)) return E.left(metadata.left); } + // Determine documentTree based on autoSync value + let documentTree: CollectionFolder | null | undefined = undefined; // undefined = no change + + if (args.autoSync === true) { + // autoSync enabled → clear documentTree (will be generated dynamically) + documentTree = null; + } else if (args.autoSync === false && publishedDocs.autoSync === true) { + // Switching from autoSync true → false: generate a snapshot of the collection + const collectionResult = + publishedDocs.workspaceType === WorkspaceType.USER + ? await this.userCollectionService.exportUserCollectionToJSONObject( + publishedDocs.creatorUid, + publishedDocs.collectionID, + ) + : await this.teamCollectionService.exportCollectionToJSONObject( + publishedDocs.workspaceID, + publishedDocs.collectionID, + ); + + if (E.isLeft(collectionResult)) { + return E.left(collectionResult.left); + } + + documentTree = collectionResult.right; + } + + // Handle environment update if environmentID is provided + let environmentName: string | null | undefined = undefined; // undefined = no change + let environmentVariables: JsonValue | undefined = undefined; + let environmentID: string | null | undefined = undefined; + + if (args.environmentID !== undefined) { + if (args.environmentID === null) { + // Explicitly removing environment + environmentID = null; + environmentName = null; + environmentVariables = null; + } else { + // Fetch environment data + const envResult = await this.fetchEnvironment( + args.environmentID, + publishedDocs.workspaceType as WorkspaceType, + publishedDocs.workspaceID, + ); + if (E.isLeft(envResult)) return E.left(envResult.left); + if (envResult.right) { + environmentID = args.environmentID; + environmentName = envResult.right.name; + environmentVariables = envResult.right.variables; + } + } + } + // Update published document const updatedPublishedDoc = await this.prisma.publishedDocs.update({ where: { id }, @@ -471,8 +712,20 @@ export class PublishedDocsService { title: args.title, version: args.version, autoSync: args.autoSync, + documentTree: + documentTree !== undefined + ? (documentTree as unknown as JsonValue) + : undefined, metadata: metadata && E.isRight(metadata) ? metadata.right : undefined, + environmentID: + environmentID !== undefined ? environmentID : undefined, + environmentName: + environmentName !== undefined ? environmentName : undefined, + environmentVariables: + environmentVariables !== undefined + ? environmentVariables + : undefined, }, }); diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts index 92c6cf9a99f..8488da37b7d 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts @@ -893,7 +893,10 @@ describe('deleteCollection', () => { .spyOn(teamCollectionService, 'getCollection') .mockResolvedValueOnce(E.right(rootTeamCollection)); jest - .spyOn(teamCollectionService as any, 'deleteCollectionAndUpdateSiblingsOrderIndex') + .spyOn( + teamCollectionService as any, + 'deleteCollectionAndUpdateSiblingsOrderIndex', + ) .mockResolvedValueOnce(E.left(TEAM_COL_REORDERING_FAILED)); const result = await teamCollectionService.deleteCollection( @@ -1667,23 +1670,6 @@ describe('FIX: updateMany queries now include teamID filter for root collections mockReset(mockPrisma); }); - const team2: Team = { - id: 'team_2', - name: 'Team 2', - }; - - // Team 2's root collection - should NOT be affected by Team 1's operations - const team2RootCollection: DBTeamCollection = { - id: 'team2-root-coll', - orderIndex: 1, - parentID: null, - title: 'Team 2 Root Collection', - teamID: team2.id, - data: {}, - createdOn: currentTime, - updatedOn: currentTime, - }; - test('FIX: deleteCollection - updateMany now correctly filters by teamID for root collections', async () => { /** * Scenario: Team 1 deletes a root collection @@ -1717,7 +1703,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections await teamCollectionService.deleteCollection(team1RootToDelete.id); // Get the updateMany call from the transaction - const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.teamCollection.updateMany.mock.calls[0][0]; // FIX VERIFICATION: The query now correctly includes teamID // This ensures only Team 1's root collections are affected @@ -1769,7 +1756,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections ); // Get the actual updateMany call arguments - const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.teamCollection.updateMany.mock.calls[0][0]; // FIX VERIFICATION: The query now correctly includes teamID expect(updateManyCall.where).toEqual({ @@ -1830,7 +1818,8 @@ describe('FIX: updateMany queries now include teamID filter for root collections ); // Get the actual updateMany call arguments - const updateManyCall = mockPrisma.teamCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.teamCollection.updateMany.mock.calls[0][0]; // FIX VERIFICATION: The query now correctly includes teamID expect(updateManyCall.where).toEqual({ diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts index b11b392e839..ba9258de8e8 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts @@ -106,35 +106,32 @@ export class TeamCollectionService { * * @param teamID The Team ID * @param collectionID The Collection ID - * @param withChildren Whether to include child collections and their requests * @returns A JSON string containing all the contents of a collection */ async exportCollectionToJSONObject( teamID: string, collectionID: string, - withChildren: boolean = true, ): Promise | E.Left> { const collection = await this.getCollection(collectionID); if (E.isLeft(collection)) return E.left(TEAM_INVALID_COLL_ID); const childrenCollectionObjects = []; - if (withChildren) { - const childrenCollection = await this.prisma.teamCollection.findMany({ - where: { - teamID, - parentID: collectionID, - }, - orderBy: { - orderIndex: 'asc', - }, - }); - for (const coll of childrenCollection) { - const result = await this.exportCollectionToJSONObject(teamID, coll.id); - if (E.isLeft(result)) return E.left(result.left); + const childrenCollection = await this.prisma.teamCollection.findMany({ + where: { + teamID, + parentID: collectionID, + }, + orderBy: { + orderIndex: 'asc', + }, + }); - childrenCollectionObjects.push(result.right); - } + for (const coll of childrenCollection) { + const result = await this.exportCollectionToJSONObject(teamID, coll.id); + if (E.isLeft(result)) return E.left(result.left); + + childrenCollectionObjects.push(result.right); } const requests = await this.prisma.teamRequest.findMany({ @@ -219,7 +216,11 @@ export class TeamCollectionService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + teamID, + parentID, + ); // Get the last order index const lastEntry = await tx.teamCollection.findFirst({ @@ -400,15 +401,18 @@ export class TeamCollectionService { * @param collectionID The collection ID * @returns An Either of the Collection details */ - async getCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) { + async getCollection( + collectionID: string, + tx: Prisma.TransactionClient | null = null, + ) { try { - const teamCollection = await (tx || this.prisma).teamCollection.findUniqueOrThrow( - { - where: { - id: collectionID, - }, + const teamCollection = await ( + tx || this.prisma + ).teamCollection.findUniqueOrThrow({ + where: { + id: collectionID, }, - ); + }); return E.right(teamCollection); } catch (error) { return E.left(TEAM_COLL_NOT_FOUND); @@ -472,7 +476,11 @@ export class TeamCollectionService { teamCollection = await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + teamID, + parentID, + ); // fetch last collection const lastCollection = await tx.teamCollection.findFirst({ @@ -555,15 +563,19 @@ export class TeamCollectionService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.teamID, collection.parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + collection.teamID, + collection.parentID, + ); const deletedCollection = await tx.teamCollection.delete({ where: { id: collection.id }, }); - + // if collection is deleted, update siblings orderIndexes // if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if (deletedCollection) { + if (deletedCollection) { // update siblings orderIndexes await tx.teamCollection.updateMany({ where: { @@ -626,7 +638,7 @@ export class TeamCollectionService { ); return E.right(true); - } + } /** * Change parentID of TeamCollection's @@ -641,11 +653,10 @@ export class TeamCollectionService { newParentID: string | null, ) { // fetch last collection - const lastCollectionUnderNewParent = - await tx.teamCollection.findFirst({ - where: { teamID: collection.teamID, parentID: newParentID }, - orderBy: { orderIndex: 'desc' }, - }); + const lastCollectionUnderNewParent = await tx.teamCollection.findFirst({ + where: { teamID: collection.teamID, parentID: newParentID }, + orderBy: { orderIndex: 'desc' }, + }); // decrement orderIndex of all next sibling collections from original collection await tx.teamCollection.updateMany({ @@ -739,7 +750,11 @@ export class TeamCollectionService { const collection = await this.getCollection(collectionID, tx); if (E.isLeft(collection)) return E.left(collection.left); // lock the rows of the collection and its siblings - await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.right.teamID, collection.right.parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + collection.right.teamID, + collection.right.parentID, + ); // destCollectionID == null i.e move collection to root if (!destCollectionID) { if (!collection.right.parentID) { @@ -747,7 +762,7 @@ export class TeamCollectionService { // Throw error if collection is already a root collection return E.left(TEAM_COL_ALREADY_ROOT); } - + // Change parent from child to root i.e child collection becomes a root collection // Move child collection into root and update orderIndexes for root teamCollections const updatedCollection = await this.changeParentAndUpdateOrderIndex( @@ -755,31 +770,32 @@ export class TeamCollectionService { collection.right, null, ); - if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left); - + if (E.isLeft(updatedCollection)) + return E.left(updatedCollection.left); + this.pubsub.publish( `team_coll/${collection.right.teamID}/coll_moved`, updatedCollection.right, ); - + return E.right(updatedCollection.right); } - + // destCollectionID != null i.e move into another collection if (collectionID === destCollectionID) { // Throw error if collectionID and destCollectionID are the same return E.left(TEAM_COLL_DEST_SAME); } - + // Get collection details of destCollectionID const destCollection = await this.getCollection(destCollectionID, tx); if (E.isLeft(destCollection)) return E.left(TEAM_COLL_NOT_FOUND); - + // Check if collection and destCollection belong to the same user account if (collection.right.teamID !== destCollection.right.teamID) { return E.left(TEAM_COLL_NOT_SAME_TEAM); } - + // Check if collection is present on the parent tree for destCollection const checkIfParent = await this.isParent( collection.right, @@ -791,8 +807,12 @@ export class TeamCollectionService { } // lock the rows of the destination collection and its siblings - await this.prisma.lockTeamCollectionByTeamAndParent(tx, destCollection.right.teamID, destCollection.right.parentID); - + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + destCollection.right.teamID, + destCollection.right.parentID, + ); + // Change parent from null to teamCollection i.e collection becomes a child collection // Move root/child collection into another child collection and update orderIndexes of the previous parent const updatedCollection = await this.changeParentAndUpdateOrderIndex( @@ -810,11 +830,7 @@ export class TeamCollectionService { return E.right(updatedCollection.right); }); } catch (error) { - - console.error( - 'Error from TeamCollectionService.moveCollection', - error, - ); + console.error('Error from TeamCollectionService.moveCollection', error); return E.left(TEAM_COL_REORDERING_FAILED); } } @@ -826,7 +842,11 @@ export class TeamCollectionService { * @param teamID The Team ID (required when collectionID is null for root collections) * @returns Number of collections */ - getCollectionCount(collectionID: string, teamID: string, tx: Prisma.TransactionClient | null = null): Promise { + getCollectionCount( + collectionID: string, + teamID: string, + tx: Prisma.TransactionClient | null = null, + ): Promise { return (tx || this.prisma).teamCollection.count({ where: { parentID: collectionID, teamID: teamID }, }); @@ -870,7 +890,7 @@ export class TeamCollectionService { // if collection is found, update orderIndexes of siblings // if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(collectionInTx) { + if (collectionInTx) { // Step 1: Decrement orderIndex of all items that come after collection.orderIndex till end of list of items await tx.teamCollection.updateMany({ where: { @@ -884,7 +904,7 @@ export class TeamCollectionService { orderIndex: { decrement: 1 }, }, }); - + // Step 2: Update orderIndex of collection to length of list await tx.teamCollection.update({ where: { id: collection.right.id }, @@ -897,7 +917,6 @@ export class TeamCollectionService { }, }); } - } catch (error) { throw new ConflictException(error); } @@ -930,7 +949,11 @@ export class TeamCollectionService { await this.prisma.$transaction(async (tx) => { try { // Step 0: lock the rows - await this.prisma.lockTeamCollectionByTeamAndParent(tx, collection.right.teamID, collection.right.parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + collection.right.teamID, + collection.right.parentID, + ); const collectionInTx = await tx.teamCollection.findFirst({ where: { id: collectionID }, @@ -943,10 +966,10 @@ export class TeamCollectionService { // if collection and subsequentCollection are found, update orderIndexes of siblings // if collection or subsequentCollection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(collectionInTx && subsequentCollectionInTx) { + if (collectionInTx && subsequentCollectionInTx) { // Step 1: Determine if we are moving collection up or down the list const isMovingUp = - subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex; + subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex; // Step 2: Update OrderIndex of items in list depending on moving up or down const updateFrom = isMovingUp @@ -1493,7 +1516,11 @@ export class TeamCollectionService { try { await this.prisma.$transaction(async (tx) => { - await this.prisma.lockTeamCollectionByTeamAndParent(tx, teamID, parentID); + await this.prisma.lockTeamCollectionByTeamAndParent( + tx, + teamID, + parentID, + ); const collections = await tx.teamCollection.findMany({ where: { teamID, parentID }, diff --git a/packages/hoppscotch-backend/src/team-request/team-request.service.ts b/packages/hoppscotch-backend/src/team-request/team-request.service.ts index 941223c24d3..a0ff29ea51a 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.service.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.service.ts @@ -120,7 +120,9 @@ export class TeamRequestService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamRequestByCollections(tx, dbTeamReq.teamID, [dbTeamReq.collectionID]); + await this.prisma.lockTeamRequestByCollections(tx, dbTeamReq.teamID, [ + dbTeamReq.collectionID, + ]); const deletedTeamRequest = await tx.teamRequest.delete({ where: { id: requestID }, @@ -128,7 +130,7 @@ export class TeamRequestService { // if request is deleted, update orderIndexes of siblings // if request was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(deletedTeamRequest) { + if (deletedTeamRequest) { await tx.teamRequest.updateMany({ where: { collectionID: dbTeamReq.collectionID, @@ -181,7 +183,9 @@ export class TeamRequestService { dbTeamRequest = await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockTeamRequestByCollections(tx, teamID, [collectionID]); + await this.prisma.lockTeamRequestByCollections(tx, teamID, [ + collectionID, + ]); // fetch last team request const lastTeamRequest = await tx.teamRequest.findFirst({ @@ -385,7 +389,10 @@ export class TeamRequestService { * A helper function to get the number of requests in a collection * @param collectionID Collection ID to fetch */ - private async getRequestsCountInCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) { + private async getRequestsCountInCollection( + collectionID: string, + tx: Prisma.TransactionClient | null = null, + ) { return (tx || this.prisma).teamRequest.count({ where: { collectionID }, }); @@ -409,7 +416,10 @@ export class TeamRequestService { E.Left | E.Right >(async (tx) => { // lock the rows - await this.prisma.lockTeamRequestByCollections(tx, request.teamID, [srcCollID, destCollID]); + await this.prisma.lockTeamRequestByCollections(tx, request.teamID, [ + srcCollID, + destCollID, + ]); request = await tx.teamRequest.findUnique({ where: { id: request.id }, @@ -422,22 +432,24 @@ export class TeamRequestService { // if request is found in transaction, update orderIndexes of siblings // if request was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(request) { + if (request) { const isSameCollection = srcCollID === destCollID; const isMovingUp = nextRequest?.orderIndex < request.orderIndex; // false, if nextRequest is null - + const nextReqOrderIndex = nextRequest?.orderIndex; const reqCountInDestColl = nextRequest ? undefined : await this.getRequestsCountInCollection(destCollID, tx); - + // Updating order indexes of other requests in collection(s) if (isSameCollection) { const updateFrom = isMovingUp ? nextReqOrderIndex : request.orderIndex + 1; - const updateTo = isMovingUp ? request.orderIndex : nextReqOrderIndex; - + const updateTo = isMovingUp + ? request.orderIndex + : nextReqOrderIndex; + await tx.teamRequest.updateMany({ where: { collectionID: srcCollID, @@ -455,7 +467,7 @@ export class TeamRequestService { }, data: { orderIndex: { decrement: 1 } }, }); - + if (nextRequest) { await tx.teamRequest.updateMany({ where: { @@ -466,20 +478,21 @@ export class TeamRequestService { }); } } - + // Updating order index of the request let adjust: number; - if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; + if (isSameCollection) + adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; else adjust = nextRequest ? 0 : 1; - + const newOrderIndex = (nextReqOrderIndex ?? reqCountInDestColl) + adjust; - + const updatedRequest = await tx.teamRequest.update({ where: { id: request.id }, data: { orderIndex: newOrderIndex, collectionID: destCollID }, }); - + return E.right(updatedRequest); } }); @@ -539,7 +552,9 @@ export class TeamRequestService { try { await this.prisma.$transaction(async (tx) => { // lock the rows - await this.prisma.lockTeamRequestByCollections(tx, teamID, [collectionID]); + await this.prisma.lockTeamRequestByCollections(tx, teamID, [ + collectionID, + ]); const teamRequests = await tx.teamRequest.findMany({ where: { teamID, collectionID }, orderBy, diff --git a/packages/hoppscotch-backend/src/team/team.service.spec.ts b/packages/hoppscotch-backend/src/team/team.service.spec.ts index 6126405e558..0ab92145bda 100644 --- a/packages/hoppscotch-backend/src/team/team.service.spec.ts +++ b/packages/hoppscotch-backend/src/team/team.service.spec.ts @@ -962,6 +962,86 @@ describe('fetchAllTeams', () => { }); }); +describe('fetchAllTeamsV2', () => { + test('should return teams with offset pagination when no search string', async () => { + mockPrisma.team.findMany.mockResolvedValueOnce(teams); + + const result = await teamService.fetchAllTeamsV2('', { + skip: 0, + take: 20, + }); + + expect(result).toEqual(teams); + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ + skip: 0, + take: 20, + where: undefined, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + }); + + test('should search by name and id when search string is provided', async () => { + mockPrisma.team.findMany.mockResolvedValueOnce([teams[0]]); + + const result = await teamService.fetchAllTeamsV2('team', { + skip: 0, + take: 20, + }); + + expect(result).toEqual([teams[0]]); + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ + skip: 0, + take: 20, + where: { + OR: [ + { name: { contains: 'team', mode: 'insensitive' } }, + { id: { contains: 'team', mode: 'insensitive' } }, + ], + }, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + }); + + test('should apply skip for pagination', async () => { + mockPrisma.team.findMany.mockResolvedValueOnce(teams); + + const result = await teamService.fetchAllTeamsV2('', { + skip: 20, + take: 20, + }); + + expect(result).toEqual(teams); + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ + skip: 20, + take: 20, + where: undefined, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + }); + + test('should return empty array when no teams match', async () => { + mockPrisma.team.findMany.mockResolvedValueOnce([]); + + const result = await teamService.fetchAllTeamsV2('nonexistent', { + skip: 0, + take: 20, + }); + + expect(result).toEqual([]); + expect(mockPrisma.team.findMany).toHaveBeenCalledWith({ + skip: 0, + take: 20, + where: { + OR: [ + { name: { contains: 'nonexistent', mode: 'insensitive' } }, + { id: { contains: 'nonexistent', mode: 'insensitive' } }, + ], + }, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + }); +}); + describe('getCountOfMembersInTeam', () => { test('should resolve right and return a total team member count ', async () => { mockPrisma.teamMember.count.mockResolvedValueOnce(2); diff --git a/packages/hoppscotch-backend/src/team/team.service.ts b/packages/hoppscotch-backend/src/team/team.service.ts index bad4bd1ab28..225c5fe5ce9 100644 --- a/packages/hoppscotch-backend/src/team/team.service.ts +++ b/packages/hoppscotch-backend/src/team/team.service.ts @@ -23,6 +23,7 @@ import * as T from 'fp-ts/Task'; import * as A from 'fp-ts/Array'; import { isValidLength, throwErr } from 'src/utils'; import { AuthUser } from '../types/AuthUser'; +import { OffsetPaginationArgs } from 'src/types/input-types.args'; @Injectable() export class TeamService implements UserDataHandler, OnModuleInit { @@ -522,6 +523,7 @@ export class TeamService implements UserDataHandler, OnModuleInit { * @param cursorID string of teamID or undefined * @param take number of items to query * @returns an array of `Team` object + * @deprecated use fetchAllTeamsV2 instead */ async fetchAllTeams(cursorID: string, take: number) { const options = { @@ -534,6 +536,32 @@ export class TeamService implements UserDataHandler, OnModuleInit { return fetchedTeams; } + /** + * Fetch all the teams in the `Team` table with offset pagination and search + * @param searchString search on team name or ID + * @param paginationOption pagination options + * @returns an array of `Team` object + */ + async fetchAllTeamsV2( + searchString: string, + paginationOption: OffsetPaginationArgs, + ) { + const fetchedTeams = await this.prisma.team.findMany({ + skip: paginationOption.skip, + take: paginationOption.take, + where: searchString + ? { + OR: [ + { name: { contains: searchString, mode: 'insensitive' } }, + { id: { contains: searchString, mode: 'insensitive' } }, + ], + } + : undefined, + orderBy: [{ name: 'asc' }, { id: 'asc' }], + }); + return fetchedTeams; + } + /** * Fetch list of all the Teams in the DB * diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts index 504bef06337..e3870d9a803 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts @@ -132,6 +132,7 @@ export class UserCollectionResolver { }) @UseGuards(GqlAuthGuard) async userCollection( + @GqlUser() user: AuthUser, @Args({ type: () => ID, name: 'userCollectionID', @@ -139,8 +140,10 @@ export class UserCollectionResolver { }) userCollectionID: string, ) { - const userCollection = - await this.userCollectionService.getUserCollection(userCollectionID); + const userCollection = await this.userCollectionService.getUserCollection( + userCollectionID, + user.uid, + ); if (E.isLeft(userCollection)) throwErr(userCollection.left); return { diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts index 007d8b7e724..22f32234f1b 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts @@ -653,6 +653,7 @@ describe('getUserCollection', () => { const result = await userCollectionService.getUserCollection( rootRESTUserCollection.id, + user.uid, ); expect(result).toEqualRight(rootRESTUserCollection); }); @@ -661,7 +662,20 @@ describe('getUserCollection', () => { 'NotFoundError', ); - const result = await userCollectionService.getUserCollection('123'); + const result = await userCollectionService.getUserCollection( + '123', + user.uid, + ); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); + }); + test('should throw USER_COLL_NOT_FOUND when collectionID belongs to a different user', async () => { + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + const result = await userCollectionService.getUserCollection( + rootRESTUserCollection.id, + 'another-user', + ); expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); }); @@ -729,16 +743,11 @@ describe('createUserCollection', () => { expect(result).toEqualLeft(USER_COLL_SHORT_TITLE); }); - test('should throw USER_NOT_OWNER when user is not the owner of the collection', async () => { + test('should throw USER_COLLECTION_CREATION_FAILED when user is not the owner of the parent collection', async () => { mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma)); jest .spyOn(userCollectionService, 'getUserCollection') - .mockResolvedValueOnce( - E.right({ - ...rootRESTUserCollection, - userUid: 'other-user-uid', - }), - ); + .mockResolvedValueOnce(E.left(USER_COLL_NOT_FOUND)); const result = await userCollectionService.createUserCollection( user, @@ -1041,23 +1050,26 @@ describe('deleteUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); - test('should throw USER_NOT_OWNER when collectionID is invalid ', async () => { - // getUserCollection - mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( - rootRESTUserCollection, + test('should throw USER_COLL_NOT_FOUND when collectionID belongs to a different user', async () => { + // getUserCollection (userUid is now part of the where clause, so it rejects for wrong user) + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', ); const result = await userCollectionService.deleteUserCollection( rootRESTUserCollection.id, 'op09', ); - expect(result).toEqualLeft(USER_NOT_OWNER); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); test('should throw USER_COLL_REORDERING_FAILED when removeCollectionAndUpdateSiblingsOrderIndex fails', async () => { jest .spyOn(userCollectionService, 'getUserCollection') .mockResolvedValueOnce(E.right(rootRESTUserCollection)); jest - .spyOn(userCollectionService as any, 'removeCollectionAndUpdateSiblingsOrderIndex') + .spyOn( + userCollectionService as any, + 'removeCollectionAndUpdateSiblingsOrderIndex', + ) .mockResolvedValueOnce(E.left(USER_COLL_REORDERING_FAILED)); const result = await userCollectionService.deleteUserCollection( @@ -1115,11 +1127,11 @@ describe('moveUserCollection', () => { expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); - test('should throw USER_NOT_OWNER if user is not owner of collection', async () => { + test('should throw USER_COLL_NOT_FOUND if user is not owner of collection', async () => { mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma)); - // getUserCollection - mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( - rootRESTUserCollection, + // getUserCollection (userUid is now part of the where clause, so it rejects for wrong user) + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', ); const result = await userCollectionService.moveUserCollection( @@ -1127,7 +1139,7 @@ describe('moveUserCollection', () => { '009', 'op09', ); - expect(result).toEqualLeft(USER_NOT_OWNER); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); test('should throw USER_COLL_DEST_SAME if userCollectionID and destCollectionID is the same', async () => { @@ -1183,24 +1195,23 @@ describe('moveUserCollection', () => { expect(result).toEqualLeft(USER_COLL_NOT_SAME_TYPE); }); - test('should throw USER_COLL_NOT_SAME_USER if userCollectionID and destCollectionID are not from the same user', async () => { + test('should throw USER_COLL_NOT_FOUND if destCollectionID belongs to a different user', async () => { mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma)); - // getUserCollection + // getUserCollection for source collection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( rootRESTUserCollection, ); - // getUserCollection for destCollection - mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({ - ...childRESTUserCollection_2, - userUid: 'differentUserUid', - }); + // getUserCollection for destCollection (userUid is now part of the where clause, so it rejects for wrong user) + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); const result = await userCollectionService.moveUserCollection( rootRESTUserCollection.id, childRESTUserCollection_2.id, user.uid, ); - expect(result).toEqualLeft(USER_COLL_NOT_SAME_USER); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); test('should throw USER_COLL_IS_PARENT_COLL if userCollectionID is parent of destCollectionID ', async () => { @@ -1416,10 +1427,10 @@ describe('updateUserCollectionOrder', () => { expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); - test('should throw USER_NOT_OWNER if userUID is of a different user', async () => { - // getUserCollection; - mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( - childRESTUserCollectionList[4], + test('should throw USER_COLL_NOT_FOUND if userUID is of a different user', async () => { + // getUserCollection (userUid is now part of the where clause, so it rejects for wrong user) + mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', ); const result = await userCollectionService.updateUserCollectionOrder( @@ -1427,7 +1438,7 @@ describe('updateUserCollectionOrder', () => { null, 'op09', ); - expect(result).toEqualLeft(USER_NOT_OWNER); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); test('should successfully move the child user-collection to the end of the list', async () => { @@ -1803,10 +1814,13 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Alice's delete only affected Alice's collections - const aliceDeleteCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const aliceDeleteCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(aliceDeleteCall.where.userUid).toBe(alice.uid); expect(aliceDeleteCall.where.parentID).toBe(null); - expect(aliceDeleteCall.where.orderIndex).toEqual({ gt: aliceCollection2.orderIndex }); + expect(aliceDeleteCall.where.orderIndex).toEqual({ + gt: aliceCollection2.orderIndex, + }); // Reset mocks for Bob's operation mockReset(mockPrisma); @@ -1831,7 +1845,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Bob's reorder only affected Bob's collections - const bobReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const bobReorderCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(bobReorderCall.where.userUid).toBe(bob.uid); expect(bobReorderCall.where.parentID).toBe(null); }); @@ -1873,7 +1888,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Alice's operation is scoped to Alice - const aliceReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const aliceReorderCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(aliceReorderCall.where.userUid).toBe(alice.uid); expect(aliceReorderCall.where.parentID).toBe(null); @@ -1903,7 +1919,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Bob's operation is scoped to Bob - const bobReorderCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const bobReorderCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(bobReorderCall.where.userUid).toBe(bob.uid); expect(bobReorderCall.where.parentID).toBe(null); }); @@ -1937,7 +1954,9 @@ describe('FIX: updateMany queries now include userUid filter for root collection .mockResolvedValueOnce(E.right(aliceChildCollection)); mockPrisma.$transaction.mockImplementation(async (fn) => fn(mockPrisma)); - mockPrisma.userCollection.findFirst.mockResolvedValueOnce(aliceCollection3); // Last root + mockPrisma.userCollection.findFirst.mockResolvedValueOnce( + aliceCollection3, + ); // Last root mockPrisma.userCollection.update.mockResolvedValueOnce({ ...aliceChildCollection, parentID: null, @@ -1952,7 +1971,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Alice's move-to-root only affects Alice's collections - const aliceMoveCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const aliceMoveCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(aliceMoveCall.where.userUid).toBe(alice.uid); expect(aliceMoveCall.where.parentID).toBe(aliceChildCollection.parentID); @@ -1976,10 +1996,13 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); // Verify Bob's delete only affects Bob's collections - const bobDeleteCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const bobDeleteCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; expect(bobDeleteCall.where.userUid).toBe(bob.uid); expect(bobDeleteCall.where.parentID).toBe(null); - expect(bobDeleteCall.where.orderIndex).toEqual({ gt: bobCollection2.orderIndex }); + expect(bobDeleteCall.where.orderIndex).toEqual({ + gt: bobCollection2.orderIndex, + }); }); }); @@ -2016,7 +2039,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection ); expect(mockPrisma.userCollection.updateMany).toHaveBeenCalled(); - const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; // FIXED: The where clause now includes userUid to prevent cross-user data corruption expect(updateManyCall.where).toEqual({ @@ -2061,7 +2085,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection user.uid, ); - const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; // FIXED: Now includes userUid - only affects current user's root collections expect(updateManyCall.where).toEqual({ @@ -2117,7 +2142,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection user.uid, ); - const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; // FIXED: Now includes userUid - only affects current user's root collections expect(updateManyCall.where).toEqual({ @@ -2159,7 +2185,8 @@ describe('FIX: updateMany queries now include userUid filter for root collection user.uid, ); - const updateManyCall = mockPrisma.userCollection.updateMany.mock.calls[0][0]; + const updateManyCall = + mockPrisma.userCollection.updateMany.mock.calls[0][0]; // FIXED: Now includes userUid - only affects current user's root collections expect(updateManyCall.where).toEqual({ diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts index 66dc179d38c..ad399092a88 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts @@ -175,11 +175,17 @@ export class UserCollectionService { * @param collectionID The collection ID * @returns An Either of the Collection details */ - async getUserCollection(collectionID: string, tx: Prisma.TransactionClient | null = null) { + async getUserCollection( + collectionID: string, + userUid: string, + tx: Prisma.TransactionClient | null = null, + ) { try { - const userCollection = await (tx || this.prisma).userCollection.findUniqueOrThrow( - { where: { id: collectionID } }, - ); + const userCollection = await ( + tx || this.prisma + ).userCollection.findUniqueOrThrow({ + where: { id: collectionID, userUid }, + }); return E.right(userCollection); } catch (error) { return E.left(USER_COLL_NOT_FOUND); @@ -212,21 +218,19 @@ export class UserCollectionService { data = jsonReq.right; } - - let userCollection: UserCollection = null; try { userCollection = await this.prisma.$transaction(async (tx) => { try { // If creating a child collection if (parentID !== null) { - const parentCollection = await this.getUserCollection(parentID, tx); + const parentCollection = await this.getUserCollection( + parentID, + user.uid, + tx, + ); if (E.isLeft(parentCollection)) throw Error(parentCollection.left); - // Check to see if parentUserCollectionID belongs to this User - if (parentCollection.right.userUid !== user.uid) - throw Error(USER_NOT_OWNER); - // Check to see if parent collection is of the same type of new collection being created if (parentCollection.right.type !== type) throw Error(USER_COLL_NOT_SAME_TYPE); @@ -234,7 +238,6 @@ export class UserCollectionService { // lock the rows await this.prisma.lockUserCollectionByParent(tx, user.uid, parentID); - // fetch last user collection const lastUserCollection = await tx.userCollection.findFirst({ where: { userUid: user.uid, parentID }, @@ -391,12 +394,9 @@ export class UserCollectionService { */ async deleteUserCollection(collectionID: string, userID: string) { // Get collection details of collectionID - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userID); if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND); - // Check to see is the collection belongs to the user - if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER); - // Delete all child collections and requests in the collection const isDeleted = await this.removeCollectionAndUpdateSiblingsOrderIndex( collection.right, @@ -427,11 +427,10 @@ export class UserCollectionService { newParentID: string | null, ) { // fetch last collection - const lastCollectionUnderNewParent = - await tx.userCollection.findFirst({ - where: { userUid: collection.userUid, parentID: newParentID }, - orderBy: { orderIndex: 'desc' }, - }); + const lastCollectionUnderNewParent = await tx.userCollection.findFirst({ + where: { userUid: collection.userUid, parentID: newParentID }, + orderBy: { orderIndex: 'desc' }, + }); // update collection's parentID and orderIndex const updatedCollection = await tx.userCollection.update({ @@ -483,6 +482,7 @@ export class UserCollectionService { // Get collection details of collection one step above in the tree i.e the parent collection const parentCollection = await this.getUserCollection( destCollection.parentID, + destCollection.userUid, tx, ); if (E.isLeft(parentCollection)) { @@ -514,7 +514,11 @@ export class UserCollectionService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockUserCollectionByParent(tx, userID, collection.parentID); + await this.prisma.lockUserCollectionByParent( + tx, + userID, + collection.parentID, + ); const deletedCollection = await tx.userCollection.delete({ where: { id: collection.id }, @@ -577,12 +581,13 @@ export class UserCollectionService { try { return await this.prisma.$transaction(async (tx) => { // Get collection details of collectionID - const collection = await this.getUserCollection(userCollectionID, tx); + const collection = await this.getUserCollection( + userCollectionID, + userID, + tx, + ); if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND); - - // Check to see is the collection belongs to the user - if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER); - + // destCollectionID == null i.e move collection to root if (!destCollectionID) { if (!collection.right.parentID) { @@ -590,7 +595,7 @@ export class UserCollectionService { // Throw error if collection is already a root collection return E.left(USER_COLL_ALREADY_ROOT); } - + // Change parent from child to root i.e child collection becomes a root collection // Move child collection into root and update orderIndexes for child userCollections const updatedCollection = await this.changeParentAndUpdateOrderIndex( @@ -598,36 +603,36 @@ export class UserCollectionService { collection.right, null, ); - if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left); - + if (E.isLeft(updatedCollection)) + return E.left(updatedCollection.left); + this.pubsub.publish( `user_coll/${collection.right.userUid}/moved`, this.cast(updatedCollection.right), ); - + return E.right(this.cast(updatedCollection.right)); } - + // destCollectionID != null i.e move into another collection if (userCollectionID === destCollectionID) { // Throw error if collectionID and destCollectionID are the same return E.left(USER_COLL_DEST_SAME); } - + // Get collection details of destCollectionID - const destCollection = await this.getUserCollection(destCollectionID, tx); + const destCollection = await this.getUserCollection( + destCollectionID, + userID, + tx, + ); if (E.isLeft(destCollection)) return E.left(USER_COLL_NOT_FOUND); - + // Check if collection and destCollection belong to the same collection type if (collection.right.type !== destCollection.right.type) { return E.left(USER_COLL_NOT_SAME_TYPE); } - - // Check if collection and destCollection belong to the same user account - if (collection.right.userUid !== destCollection.right.userUid) { - return E.left(USER_COLL_NOT_SAME_USER); - } - + // Check if collection is present on the parent tree for destCollection const checkIfParent = await this.isParent( collection.right, @@ -637,7 +642,7 @@ export class UserCollectionService { if (O.isNone(checkIfParent)) { return E.left(USER_COLL_IS_PARENT_COLL); } - + // Change parent from null to teamCollection i.e collection becomes a child collection // Move root/child collection into another child collection and update orderIndexes of the previous parent const updatedCollection = await this.changeParentAndUpdateOrderIndex( @@ -646,16 +651,15 @@ export class UserCollectionService { destCollection.right.id, ); if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left); - + this.pubsub.publish( `user_coll/${collection.right.userUid}/moved`, this.cast(updatedCollection.right), ); - + return E.right(this.cast(updatedCollection.right)); }); } catch (error) { - console.error( 'Error from UserCollectionService.moveUserCollection', error, @@ -671,7 +675,11 @@ export class UserCollectionService { * @param userUid The User UID * @returns Number of collections */ - getCollectionCount(collectionID: string, userUid: string, tx: Prisma.TransactionClient | null = null): Promise { + getCollectionCount( + collectionID: string, + userUid: string, + tx: Prisma.TransactionClient | null = null, + ): Promise { return (tx || this.prisma).userCollection.count({ where: { parentID: collectionID, @@ -698,19 +706,20 @@ export class UserCollectionService { return E.left(USER_COLL_SAME_NEXT_COLL); // Get collection details of collectionID - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userID); if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND); - // Check to see is the collection belongs to the user - if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER); - if (!nextCollectionID) { // nextCollectionID == null i.e move collection to the end of the list try { await this.prisma.$transaction(async (tx) => { try { // Step 0: lock the rows - await this.prisma.lockUserCollectionByParent(tx, userID, collection.right.parentID); + await this.prisma.lockUserCollectionByParent( + tx, + userID, + collection.right.parentID, + ); const collectionInTx = await tx.userCollection.findFirst({ where: { id: collectionID }, @@ -719,7 +728,7 @@ export class UserCollectionService { // if collection is found, update orderIndexes of siblings // if collection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(collectionInTx) { + if (collectionInTx) { // Step 1: Decrement orderIndex of all items that come after collection.orderIndex till end of list of items await tx.userCollection.updateMany({ where: { @@ -729,7 +738,7 @@ export class UserCollectionService { }, data: { orderIndex: { decrement: 1 } }, }); - + // Step 2: Update orderIndex of collection to length of list await tx.userCollection.update({ where: { id: collection.right.id }, @@ -742,7 +751,6 @@ export class UserCollectionService { }, }); } - } catch (error) { throw new ConflictException(error); } @@ -764,7 +772,10 @@ export class UserCollectionService { // nextCollectionID != null i.e move to a certain position // Get collection details of nextCollectionID - const subsequentCollection = await this.getUserCollection(nextCollectionID); + const subsequentCollection = await this.getUserCollection( + nextCollectionID, + userID, + ); if (E.isLeft(subsequentCollection)) return E.left(USER_COLL_NOT_FOUND); if (collection.right.userUid !== subsequentCollection.right.userUid) @@ -779,7 +790,11 @@ export class UserCollectionService { await this.prisma.$transaction(async (tx) => { try { // Step 0: lock the rows - await this.prisma.lockUserCollectionByParent(tx, userID, subsequentCollection.right.parentID); + await this.prisma.lockUserCollectionByParent( + tx, + userID, + subsequentCollection.right.parentID, + ); // subsequentCollectionInTx and subsequentCollection are same, just to make sure, orderIndex value is concrete const collectionInTx = await tx.userCollection.findFirst({ @@ -793,20 +808,20 @@ export class UserCollectionService { // if collection and subsequentCollection are found, update orderIndexes of siblings // if collection or subsequentCollection was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(collectionInTx && subsequentCollectionInTx) { + if (collectionInTx && subsequentCollectionInTx) { // Step 1: Determine if we are moving collection up or down the list const isMovingUp = subsequentCollectionInTx.orderIndex < collectionInTx.orderIndex; - + // Step 2: Update OrderIndex of items in list depending on moving up or down const updateFrom = isMovingUp ? subsequentCollectionInTx.orderIndex : collectionInTx.orderIndex + 1; - + const updateTo = isMovingUp ? collectionInTx.orderIndex - 1 : subsequentCollectionInTx.orderIndex - 1; - + await tx.userCollection.updateMany({ where: { userUid: collection.right.userUid, @@ -817,7 +832,7 @@ export class UserCollectionService { orderIndex: isMovingUp ? { increment: 1 } : { decrement: 1 }, }, }); - + // Step 3: Update OrderIndex of collection await tx.userCollection.update({ where: { id: collection.right.id }, @@ -828,7 +843,6 @@ export class UserCollectionService { }, }); } - } catch (error) { throw new ConflictException(error); } @@ -853,41 +867,38 @@ export class UserCollectionService { * * @param userUID The User UID * @param collectionID The Collection ID - * @param withChildren Whether to include child collections and their requests * @returns A JSON string containing all the contents of a collection */ async exportUserCollectionToJSONObject( userUID: string, collectionID: string, - withChildren: boolean = true, ): Promise | E.Right> { // Get Collection details - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userUID); if (E.isLeft(collection)) return E.left(collection.left); const childrenCollectionObjects: CollectionFolder[] = []; - if (withChildren) { - // Get all child collections whose parentID === collectionID - const childCollectionList = await this.prisma.userCollection.findMany({ - where: { - parentID: collectionID, - userUid: userUID, - }, - orderBy: { - orderIndex: 'asc', - }, - }); - // Create a list of child collection and request data ready for export - for (const coll of childCollectionList) { - const result = await this.exportUserCollectionToJSONObject( - userUID, - coll.id, - ); - if (E.isLeft(result)) return E.left(result.left); + // Get all child collections whose parentID === collectionID + const childCollectionList = await this.prisma.userCollection.findMany({ + where: { + parentID: collectionID, + userUid: userUID, + }, + orderBy: { + orderIndex: 'asc', + }, + }); - childrenCollectionObjects.push(result.right); - } + // Create a list of child collection and request data ready for export + for (const coll of childCollectionList) { + const result = await this.exportUserCollectionToJSONObject( + userUID, + coll.id, + ); + if (E.isLeft(result)) return E.left(result.left); + + childrenCollectionObjects.push(result.right); } // Fetch all child requests that belong to collectionID @@ -958,7 +969,10 @@ export class UserCollectionService { // If collectionID is not null, return JSON stringified data for specific collection if (collectionID) { // Get Details of collection - const parentCollection = await this.getUserCollection(collectionID); + const parentCollection = await this.getUserCollection( + collectionID, + userUID, + ); if (E.isLeft(parentCollection)) return E.left(parentCollection.left); if (parentCollection.right.type !== reqType) @@ -1086,13 +1100,12 @@ export class UserCollectionService { // Check to see if destCollectionID belongs to this User if (destCollectionID) { - const parentCollection = await this.getUserCollection(destCollectionID); + const parentCollection = await this.getUserCollection( + destCollectionID, + userID, + ); if (E.isLeft(parentCollection)) return E.left(parentCollection.left); - // Check to see if parentUserCollectionID belongs to this User - if (parentCollection.right.userUid !== userID) - return E.left(USER_NOT_OWNER); - // Check to see if parent collection is of the same type of new collection being created if (parentCollection.right.type !== reqType) return E.left(USER_COLL_NOT_SAME_TYPE); @@ -1104,7 +1117,11 @@ export class UserCollectionService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockUserCollectionByParent(tx, userID, destCollectionID); + await this.prisma.lockUserCollectionByParent( + tx, + userID, + destCollectionID, + ); // Get the last order index const lastCollection = await tx.userCollection.findFirst({ @@ -1150,6 +1167,7 @@ export class UserCollectionService { if (isCollectionDuplication) { const duplicatedCollectionData = await this.fetchCollectionData( userCollections[0].id, + userID, ); if (E.isRight(duplicatedCollectionData)) { this.pubsub.publish( @@ -1236,10 +1254,9 @@ export class UserCollectionService { userID: string, reqType: DBReqType, ) { - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userID); if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND); - if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER); if (collection.right.type !== reqType) return E.left(USER_COLL_NOT_SAME_TYPE); @@ -1275,8 +1292,9 @@ export class UserCollectionService { */ private async fetchCollectionData( collectionID: string, + userID: string, ): Promise | E.Right> { - const collection = await this.getUserCollection(collectionID); + const collection = await this.getUserCollection(collectionID, userID); if (E.isLeft(collection)) return E.left(collection.left); const { id, title, data, type, parentID, userUid } = collection.right; @@ -1294,7 +1312,7 @@ export class UserCollectionService { ]); const childCollectionDataList = await Promise.all( - childCollections.map(({ id }) => this.fetchCollectionData(id)), + childCollections.map(({ id }) => this.fetchCollectionData(id, userID)), ); const failedChildData = childCollectionDataList.find(E.isLeft); diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index 875780dd042..e0559a20cd3 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -80,6 +80,7 @@ export class UserEnvironmentsResolver { }) @UseGuards(GqlAuthGuard) async updateUserEnvironment( + @GqlUser() user: User, @Args({ name: 'id', description: 'ID of the user environment', @@ -103,6 +104,7 @@ export class UserEnvironmentsResolver { id, name, variables, + user, ); if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); return userEnvironment.right; diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts index 5eff98e0ef3..d98981b8174 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts @@ -9,10 +9,24 @@ import { USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME, } from '../errors'; import { PubSubService } from '../pubsub/pubsub.service'; +import { User } from '../user/user.model'; const mockPrisma = mockDeep(); const mockPubSub = mockDeep(); +const mockUser: User = { + uid: 'abc123', + displayName: 'Test User', + email: 'support@example.com', + photoURL: 'https://example.com/profile.jpg', + isAdmin: false, + lastLoggedOn: new Date(), + lastActiveOn: new Date(), + createdOn: new Date(), + currentRESTSession: JSON.stringify({}), + currentGQLSession: JSON.stringify({}), +}; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const userEnvironmentsService = new UserEnvironmentsService( @@ -301,6 +315,7 @@ describe('UserEnvironmentsService', () => { 'abc123', 'test', '[{}]', + mockUser, ), ).toEqualRight(result); }); @@ -327,6 +342,7 @@ describe('UserEnvironmentsService', () => { 'abc123', null, '[{}]', + mockUser, ), ).toEqualRight(result); }); @@ -341,6 +357,7 @@ describe('UserEnvironmentsService', () => { 'abc123', 'test', '[{}]', + mockUser, ), ).toEqualLeft(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); }); @@ -366,6 +383,7 @@ describe('UserEnvironmentsService', () => { 'abc123', 'test', '[{}]', + mockUser, ); return expect(mockPubSub.publish).toHaveBeenCalledWith( @@ -395,6 +413,7 @@ describe('UserEnvironmentsService', () => { 'abc123', null, '[{}]', + mockUser, ); return expect(mockPubSub.publish).toHaveBeenCalledWith( diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts index 7754209bb3e..b1214d06502 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -14,6 +14,7 @@ import { USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME, } from '../errors'; import { stringToJson } from '../utils'; +import { User } from '../user/user.model'; @Injectable() export class UserEnvironmentsService { @@ -128,14 +129,20 @@ export class UserEnvironmentsService { * @param id environment id * @param name environments name * @param variables environment variables + * @param user User object for authorization * @returns an Either of `UserEnvironment` or error */ - async updateUserEnvironment(id: string, name: string, variables: string) { + async updateUserEnvironment( + id: string, + name: string, + variables: string, + user: User, + ) { const envVariables = stringToJson(variables); if (E.isLeft(envVariables)) return E.left(envVariables.left); try { const updatedEnvironment = await this.prisma.userEnvironment.update({ - where: { id: id }, + where: { id: id, userUid: user.uid }, data: { name: name, variables: envVariables.right, @@ -179,6 +186,7 @@ export class UserEnvironmentsService { const deletedEnvironment = await this.prisma.userEnvironment.delete({ where: { id: id, + userUid: uid, }, }); @@ -238,7 +246,7 @@ export class UserEnvironmentsService { if (env.id === id) { try { const updatedEnvironment = await this.prisma.userEnvironment.update({ - where: { id: id }, + where: { id: id, userUid: uid }, data: { variables: [], }, diff --git a/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts b/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts index ff604517037..a4e6f9d7a62 100644 --- a/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-request/user-request.service.spec.ts @@ -849,6 +849,7 @@ describe('UserRequestService', () => { destCollID, userRequests[0], userRequests[1], + user, ); expect(result).resolves.toEqualRight(true); diff --git a/packages/hoppscotch-backend/src/user-request/user-request.service.ts b/packages/hoppscotch-backend/src/user-request/user-request.service.ts index 2f45c60a1d2..27f069a0393 100644 --- a/packages/hoppscotch-backend/src/user-request/user-request.service.ts +++ b/packages/hoppscotch-backend/src/user-request/user-request.service.ts @@ -8,7 +8,6 @@ import { UserRequest as DbUserRequest, } from 'src/generated/prisma/client'; import { - USER_COLLECTION_NOT_FOUND, USER_REQUEST_CREATION_FAILED, USER_REQUEST_INVALID_TYPE, USER_REQUEST_NOT_FOUND, @@ -99,7 +98,10 @@ export class UserRequestService { * @param user User who owns the collection * @returns Number of requests in the collection */ - getRequestsCountInCollection(collectionID: string, tx: Prisma.TransactionClient | null = null): Promise { + getRequestsCountInCollection( + collectionID: string, + tx: Prisma.TransactionClient | null = null, + ): Promise { return (tx || this.prisma).userRequest.count({ where: { collectionID }, }); @@ -124,13 +126,12 @@ export class UserRequestService { const jsonRequest = stringToJson(request); if (E.isLeft(jsonRequest)) return E.left(jsonRequest.left); - const collection = - await this.userCollectionService.getUserCollection(collectionID); + const collection = await this.userCollectionService.getUserCollection( + collectionID, + user.uid, + ); if (E.isLeft(collection)) return E.left(collection.left); - if (collection.right.userUid !== user.uid) - return E.left(USER_COLLECTION_NOT_FOUND); - if (collection.right.type !== ReqType[type]) return E.left(USER_REQUEST_INVALID_TYPE); @@ -139,7 +140,9 @@ export class UserRequestService { newRequest = await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockUserRequestByCollections(tx, user.uid, [collectionID]); + await this.prisma.lockUserRequestByCollections(tx, user.uid, [ + collectionID, + ]); // fetch last user request const lastUserRequest = await tx.userRequest.findFirst({ @@ -235,13 +238,15 @@ export class UserRequestService { await this.prisma.$transaction(async (tx) => { try { // lock the rows - await this.prisma.lockUserRequestByCollections(tx, user.uid, [request.collectionID]); + await this.prisma.lockUserRequestByCollections(tx, user.uid, [ + request.collectionID, + ]); const deletedRequest = await tx.userRequest.delete({ where: { id } }); // if request is found, update orderIndexes of siblings // if request was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(deletedRequest) { + if (deletedRequest) { await tx.userRequest.updateMany({ where: { collectionID: request.collectionID, @@ -298,6 +303,7 @@ export class UserRequestService { destCollID, dbRequest, dbNextRequest, + user, ); if (E.isLeft(isTypeValidate)) return E.left(isTypeValidate.left); @@ -332,10 +338,11 @@ export class UserRequestService { destCollID, request, nextRequest, + user: AuthUser, ) { const collections = await Promise.all([ - this.userCollectionService.getUserCollection(srcCollID), - this.userCollectionService.getUserCollection(destCollID), + this.userCollectionService.getUserCollection(srcCollID, user.uid), + this.userCollectionService.getUserCollection(destCollID, user.uid), ]); const srcColl = collections[0]; @@ -416,7 +423,10 @@ export class UserRequestService { E.Left | E.Right >(async (tx) => { // lock the rows - await this.prisma.lockUserRequestByCollections(tx, request.userUid, [srcCollID, destCollID]); + await this.prisma.lockUserRequestByCollections(tx, request.userUid, [ + srcCollID, + destCollID, + ]); request = await tx.userRequest.findUnique({ where: { id: request.id }, @@ -429,22 +439,24 @@ export class UserRequestService { // Check again if request is found in transaction, update orderIndexes of siblings // if request was deleted before the transaction started (race condition), do not update siblings orderIndexes - if(request) { + if (request) { const isSameCollection = srcCollID === destCollID; const isMovingUp = nextRequest?.orderIndex < request.orderIndex; // false, if nextRequest is null - + const nextReqOrderIndex = nextRequest?.orderIndex; const reqCountInDestColl = nextRequest ? undefined : await this.getRequestsCountInCollection(destCollID); - + // Updating order indexes of other requests in collection(s) if (isSameCollection) { const updateFrom = isMovingUp ? nextReqOrderIndex : request.orderIndex + 1; - const updateTo = isMovingUp ? request.orderIndex : nextReqOrderIndex; - + const updateTo = isMovingUp + ? request.orderIndex + : nextReqOrderIndex; + await tx.userRequest.updateMany({ where: { collectionID: srcCollID, @@ -462,7 +474,7 @@ export class UserRequestService { }, data: { orderIndex: { decrement: 1 } }, }); - + if (nextRequest) { await tx.userRequest.updateMany({ where: { @@ -473,15 +485,16 @@ export class UserRequestService { }); } } - + // Updating order index of the request let adjust: number; - if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; + if (isSameCollection) + adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; else adjust = nextRequest ? 0 : 1; - + const newOrderIndex = (nextReqOrderIndex ?? reqCountInDestColl) + adjust; - + const updatedRequest = await tx.userRequest.update({ where: { id: request.id }, data: { orderIndex: newOrderIndex, collectionID: destCollID }, @@ -520,7 +533,9 @@ export class UserRequestService { try { await this.prisma.$transaction(async (tx) => { - await this.prisma.lockUserRequestByCollections(tx, userUid, [collectionID]); + await this.prisma.lockUserRequestByCollections(tx, userUid, [ + collectionID, + ]); const userRequests = await tx.userRequest.findMany({ where: { userUid, collectionID }, diff --git a/packages/hoppscotch-cli/package.json b/packages/hoppscotch-cli/package.json index 849e73fa068..a8bf5d2af20 100644 --- a/packages/hoppscotch-cli/package.json +++ b/packages/hoppscotch-cli/package.json @@ -1,6 +1,6 @@ { "name": "@hoppscotch/cli", - "version": "0.30.2", + "version": "0.30.3", "description": "A CLI to run Hoppscotch test scripts in CI environments.", "homepage": "https://hoppscotch.io", "type": "module", @@ -42,16 +42,16 @@ "private": false, "dependencies": { "aws4fetch": "1.0.20", - "axios": "1.13.2", + "axios": "1.13.5", "axios-cookiejar-support": "6.0.5", "chalk": "5.6.2", - "commander": "14.0.2", + "commander": "14.0.3", "isolated-vm": "6.0.2", "js-md5": "0.8.3", "jsonc-parser": "3.3.1", - "lodash-es": "4.17.22", + "lodash-es": "4.17.23", "papaparse": "5.5.3", - "qs": "6.14.1", + "qs": "6.15.0", "tough-cookie": "6.0.0", "verzod": "0.4.0", "xmlbuilder2": "4.0.3", @@ -65,11 +65,11 @@ "@types/papaparse": "5.5.2", "@types/qs": "6.14.0", "fp-ts": "2.16.11", - "prettier": "3.8.0", + "prettier": "3.8.1", "qs": "6.11.2", - "semver": "7.7.3", + "semver": "7.7.4", "tsup": "8.5.1", "typescript": "5.9.3", - "vitest": "4.0.17" + "vitest": "4.0.18" } } diff --git a/packages/hoppscotch-common/assets/themes/tippy-themes.scss b/packages/hoppscotch-common/assets/themes/tippy-themes.scss index 7fe6ae224d1..30adb24175e 100644 --- a/packages/hoppscotch-common/assets/themes/tippy-themes.scss +++ b/packages/hoppscotch-common/assets/themes/tippy-themes.scss @@ -16,6 +16,25 @@ @apply flex-col; } + // Constrained tooltip styles to prevent overflow beyond the viewport + // when environment variable values are very long (fixes #5876) + .env-tooltip-constrained { + @apply flex-col; + @apply w-full; + @apply box-border; + @apply overflow-hidden; + + .env-tooltip-value { + @apply break-words; + @apply break-all; + @apply whitespace-pre-wrap; + @apply overflow-hidden; + @apply inline-block; + @apply max-w-full; + @apply align-top; + } + } + .tippy-content { @apply flex; @apply text-tiny; diff --git a/packages/hoppscotch-common/locales/cs.json b/packages/hoppscotch-common/locales/cs.json index 2ce89f2221a..9f7e33e633d 100644 --- a/packages/hoppscotch-common/locales/cs.json +++ b/packages/hoppscotch-common/locales/cs.json @@ -1,221 +1,380 @@ { "action": { - "add": "Add", + "add": "Přidat", "autoscroll": "Autoscroll", "cancel": "Zrušit", "choose_file": "Vyberte soubor", + "choose_workspace": "Vyberte pracovní prostor", + "choose_collection": "Vyberte kolekci", + "select_workspace": "Vyberte pracovní prostor", "clear": "Vymazat", "clear_all": "Vymazat vše", - "clear_history": "Clear all History", - "close": "Close", + "clear_cache": "Vymazat mezipaměť", + "clear_history": "Vymazat celou historii", + "clear_unpinned": "Vymazat nepřipnuté", + "clear_response": "Vymazat odpověď", + "close": "Zavřít", + "confirm": "Potvrdit", "connect": "Připojit", - "connecting": "Connecting", + "connecting": "Připojování", "copy": "Kopírovat", - "create": "Create", + "create": "Vytvořit", "delete": "Vymazat", "disconnect": "Odpojit", - "dismiss": "Zavrhnout", - "dont_save": "Don't save", + "dismiss": "Skrýt", + "done": "Hotovo", + "dont_save": "Neukládat", "download_file": "Stáhnout soubor", - "drag_to_reorder": "Drag to reorder", + "download_test_report": "Stáhnout test report", + "drag_to_reorder": "Přetáhněte pro změnu pořadí", "duplicate": "Duplikovat", "edit": "Upravit", "filter": "Filtrovat", "go_back": "Přejít zpět", "go_forward": "Přejít vpřed", - "group_by": "Group by", - "hide_secret": "Hide secret", + "group_by": "Seskupit podle", + "hide_secret": "Skrýt tajný údaj", "label": "Označení", "learn_more": "Další informace", - "download_here": "Download here", + "download_here": "Stáhnout zde", "less": "Méně", "more": "Více", "new": "Nový", "no": "Ne", + "open": "Otevřít", "open_workspace": "Otevřít pracovní prostor", "paste": "Vložit", "prettify": "Prettify", "properties": "Vlastnosti", + "register": "Registrovat", "remove": "Odstranit", + "remove_instance": "Odebrat instanci", "rename": "Přejmenovat", "restore": "Obnovit", + "retry": "Zkusit znovu", "save": "Uložit", "save_as_example": "Uložit jako příklad", - "scroll_to_bottom": "Scroll to bottom", - "scroll_to_top": "Scroll to top", + "add_example": "Přidat příklad", + "invalid_request": "Neplatná data požadavku", + "scroll_to_bottom": "Posunout dolů", + "scroll_to_top": "Posunout nahoru", "search": "Vyhledávání", - "send": "Poslat", + "send": "Odeslat", "share": "Sdílet", - "show_secret": "Show secret", - "start": "Start", - "starting": "Starting", - "stop": "Stop", - "to_close": "to close", - "to_navigate": "to navigate", - "to_select": "to select", + "show_secret": "Zobrazit tajný údaj", + "sort": "Seřadit", + "start": "Spustit", + "starting": "Spouštění", + "stop": "Zastavit", + "to_close": "pro zavření", + "to_navigate": "pro navigaci", + "to_select": "pro výběr", "turn_off": "Vypnout", "turn_on": "Zapnout", - "undo": "vrátit", - "yes": "Ano" + "undo": "Vrátit zpět", + "unpublish": "Zrušit publikování", + "yes": "Ano", + "verify": "Ověřit", + "enable": "Povolit", + "disable": "Zakázat", + "assign": "Přiřadit" + }, + "activity_logs": { + "ACTIVITY_LOG_DELETE": "Protokol aktivit byl smazán", + "WORKSPACE_CREATE": "Vytvořen nový pracovní prostor {name}", + "WORKSPACE_RENAME": "Přejmenován pracovní prostor z {old_name} na {new_name}", + "WORKSPACE_USER_ADD": "{user} byl přidán do pracovního prostoru jako {role}", + "WORKSPACE_USER_INVITE": "{user} byl pozván uživatelem {inviteeEmail} jako {role}", + "WORKSPACE_USER_INVITE_REVOKE": "Odvoláno pozvání {inviteeEmail} jako {inviteeRole}", + "WORKSPACE_USER_INVITE_ACCEPT": "{inviteeEmail} přijal pozvání jako {inviteeRole}", + "WORKSPACE_USER_REMOVE": "{user} byl odebrán z pracovního prostoru", + "WORKSPACE_USER_ROLE_UPDATE": "Role {user} byla aktualizována z {old_role} na {new_role}", + "COLLECTION_CREATE": "Vytvořena nová kolekce {title}", + "COLLECTION_RENAME": "Přejmenována kolekce z {old_title} na {new_title}", + "COLLECTION_IMPORT": "Importováno {count} kolekcí", + "COLLECTION_DELETE": "Smazána kolekce {title}", + "COLLECTION_DUPLICATE": "Duplikována kolekce {parentTitle}", + "REQUEST_CREATE": "Vytvořen nový požadavek {title}", + "REQUEST_RENAME": "Přejmenován požadavek z {old_title} na {new_title}", + "REQUEST_DELETE": "Smazán požadavek {title}" }, "add": { "new": "Přidat nový", "star": "Přidat hvězdičku" }, + "agent": { + "registration_instruction": "Chcete-li pokračovat, zaregistrujte si Hoppscotch Agenta u svého webového klienta.", + "enter_otp_instruction": "Zadejte prosím ověřovací kód vygenerovaný agentem Hoppscotch a dokončete registraci", + "otp_label": "Ověřovací kód", + "processing": "Zpracovává se váš požadavek...", + "not_running_title": "Agent nebyl zjištěn", + "registration_title": "Registrace agenta", + "verify_ssl_certs": "Ověřit SSL certifikáty", + "ca_certs": "Certifikáty CA", + "client_certs": "Klientské certifikáty", + "use_http_proxy": "Použít HTTP proxy", + "proxy_capabilities": "Hoppscotch Agent podporuje HTTP/HTTPS/SOCKS proxy spolu s NTLM a Basic Auth v těchto proxy. V samotném URL uveďte uživatelské jméno a heslo pro ověření proxy.", + "add_cert_file": "Přidat soubor certifikátu", + "add_client_cert": "Přidat klientský certifikát", + "add_key_file": "Přidat soubor klíče", + "domain": "Doména", + "cert": "Certifikát", + "key": "Klíč", + "pfx_or_pkcs": "PFX/PKCS12", + "pfx_or_pkcs_file": "Soubor PFX/PKCS12", + "add_pfx_or_pkcs_file": "Přidat soubor PFX/PKCS12" + }, "app": { - "chat_with_us": "Popovídejte si s námi", + "additional_links": { + "macOS": "macOS", + "windows": "Windows", + "linux": "Linux", + "web_app": "Webová aplikace", + "cli": "CLI" + }, + "chat_with_us": "Napište nám", "contact_us": "Kontaktujte nás", - "cookies": "Cookies", + "cookies": "Soubory cookie", "copy": "Kopírovat", - "copy_interface_type": "Copy interface type", - "copy_user_id": "Copy User Auth Token", - "developer_option": "Developer options", - "developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.", + "copy_interface_type": "Kopírovat typ rozhraní", + "copy_user_id": "Kopírovat auth token uživatele", + "developer_option": "Možnosti vývojáře", + "developer_option_description": "Nástroje pro vývojáře, které pomáhají s vývojem a údržbou Hoppscotch.", "discord": "Discord", "documentation": "Dokumentace", "github": "GitHub", - "help": "Nápověda, zpětná vazba a dokumentace", + "help": "Nápověda a zpětná vazba", "home": "Domov", "invite": "Pozvat", - "invite_description": "V Hoppscotch jsme navrhli jednoduché a intuitivní rozhraní pro vytváření a správu vašich API. Hoppscotch je nástroj, který vám pomáhá vytvářet, testovat, dokumentovat a sdílet vaše API.", - "invite_your_friends": "Pozvi své přátele", + "invite_description": "Hoppscotch je open-source ekosystém pro vývoj API. Navrhli jsme jednoduché a intuitivní rozhraní pro vytváření a správu API. Hoppscotch vám pomůže API vytvářet, testovat, dokumentovat a sdílet.", + "invite_your_friends": "Pozvěte své přátele", "join_discord_community": "Připojte se k naší komunitě Discord", "keyboard_shortcuts": "Klávesové zkratky", "name": "Hoppscotch", - "new_version_found": "Nalezena nová verze. Aktualizujte aktualizací.", - "open_in_hoppscotch": "Open in Hoppscotch", + "new_version_found": "Nalezena nová verze. Obnovte stránku pro aktualizaci.", + "open_in_hoppscotch": "Otevřít v Hoppscotch", "options": "Možnosti", + "powered_by": "Powered by Hoppscotch", "proxy_privacy_policy": "Zásady ochrany osobních údajů proxy", "reload": "Znovu načíst", - "search": "Vyhledávání", - "share": "Podíl", + "search": "Vyhledávání a příkazy", + "share": "Sdílet", "shortcuts": "Klávesové zkratky", - "social_description": "Follow us on social media to stay updated with the latest news, updates and releases.", - "social_links": "Social links", - "spotlight": "Reflektor", + "social_description": "Sledujte nás na sociálních sítích, ať máte přehled o novinkách, aktualizacích a vydáních.", + "social_links": "Odkazy na sociální sítě", + "spotlight": "Spotlight", "status": "Stav", - "status_description": "Check the status of the website", + "status_description": "Zkontrolujte stav služby", "terms_and_privacy": "Podmínky a soukromí", "twitter": "Twitter", "type_a_command_search": "Zadejte příkaz nebo hledejte…", - "we_use_cookies": "Používáme cookies", - "updated_text": "Hoppscotch has been updated to v{version} 🎉", + "we_use_cookies": "Používáme soubory cookie", + "updated_text": "Hoppscotch byl aktualizován na v{version} 🎉", "whats_new": "Co je nového?", - "see_whats_new": "See what’s new", - "wiki": "Wiki" + "see_whats_new": "Podívejte se, co je nového", + "wiki": "Wiki", + "collapse_sidebar": "Sbalit postranní panel", + "continue_to_dashboard": "Pokračovat na dashboard", + "expand_sidebar": "Rozbalit postranní panel", + "no_name": "Bez názvu", + "open_navigation": "Otevřít navigaci", + "read_documentation": "Přečtěte si dokumentaci", + "default": "výchozí: {value}" }, "auth": { - "account_exists": "Účet existuje s různými pověřeními - Přihlaste se a propojte oba účty", + "account_deactivated": "Váš účet byl deaktivován. Chcete-li získat přístup, kontaktujte správce.", + "account_exists": "Účet existuje s jinými přihlašovacími údaji. Přihlaste se a propojte oba účty.", "all_sign_in_options": "Všechny možnosti přihlášení", - "continue_with_auth_provider": "Continue with {provider}", - "continue_with_email": "Pokračujte e -mailem", - "continue_with_github": "Pokračujte na GitHubu", - "continue_with_github_enterprise": "Continue with GitHub Enterprise", - "continue_with_google": "Pokračovat s Google", - "continue_with_microsoft": "Continue with Microsoft", - "email": "E-mailem", - "logged_out": "Odhlásit", + "continue_with_auth_provider": "Pokračovat s {provider}", + "continue_with_email": "Pokračovat e-mailem", + "continue_with_github": "Pokračovat s GitHubem", + "continue_with_github_enterprise": "Pokračovat s GitHub Enterprise", + "continue_with_google": "Pokračovat s Googlem", + "continue_with_microsoft": "Pokračovat s Microsoftem", + "email": "E-mail", + "logged_out": "Odhlášeno", "login": "Přihlásit se", - "login_success": "Úspěšně přihlášen", + "login_success": "Úspěšně přihlášeno", "login_to_hoppscotch": "Přihlaste se do Hoppscotch", "logout": "Odhlásit se", "re_enter_email": "Znovu zadat e-mail", - "send_magic_link": "Pošlete kouzelný odkaz", + "send_magic_link": "Odeslat magic link", "sync": "Synchronizace", - "we_sent_magic_link": "Poslali jsme vám kouzelný odkaz!", - "we_sent_magic_link_description": "Zkontrolujte svou doručenou poštu - odeslali jsme e -mail na adresu {email}. Obsahuje kouzelný odkaz, který vás přihlásí." + "we_sent_magic_link": "Poslali jsme vám magic link!", + "we_sent_magic_link_description": "Zkontrolujte svou doručenou poštu - odeslali jsme e-mail na adresu {email}. Obsahuje magic link, který vás přihlásí." }, "authorization": { - "generate_token": "Generovat token", - "graphql_headers": "Authorization Headers are sent as part of the payload to connection_init", + "generate_token": "Vygenerovat token", + "refresh_token": "Obnovit token", + "graphql_headers": "Autorizační hlavičky se odesílají jako součást payloadu do connection_init", "include_in_url": "Zahrnout do adresy URL", - "inherited_from": "Inherited from {auth} from Parent Collection {collection} ", - "learn": "Zjistěte jak", + "inherited_from": "Autorizace {auth} zděděna z nadřazené kolekce {collection}", + "learn": "Zjistěte více", "oauth": { - "redirect_auth_server_returned_error": "Auth Server returned an error state", - "redirect_auth_token_request_failed": "Request to get the auth token failed", - "redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token", - "redirect_invalid_state": "Invalid State value present in the redirect", - "redirect_no_auth_code": "No Authorization Code present in the redirect", - "redirect_no_client_id": "No Client ID defined", - "redirect_no_client_secret": "No Client Secret Defined", - "redirect_no_code_verifier": "No Code Verifier Defined", - "redirect_no_token_endpoint": "No Token Endpoint Defined", - "something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect", - "something_went_wrong_on_token_generation": "Something went wrong on token generation", - "token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed", - "grant_type": "Grant Type", - "grant_type_auth_code": "Authorization Code", - "token_fetched_successfully": "Token fetched successfully", - "token_fetch_failed": "Failed to fetch token", - "validation_failed": "Validation Failed, please check the form fields", - "label_authorization_endpoint": "Authorization Endpoint", + "redirect_auth_server_returned_error": "Autorizační server vrátil chybový stav", + "redirect_auth_token_request_failed": "Požadavek na získání Access Tokenu selhal", + "redirect_auth_token_request_invalid_response": "Při požadavku na získání Access Tokenu přišla z Token Endpointu neplatná odpověď", + "redirect_invalid_state": "V přesměrování je neplatná hodnota state", + "redirect_no_auth_code": "V přesměrování chybí autorizační kód", + "redirect_no_client_id": "Není definováno Client ID", + "redirect_no_client_secret": "Není definován Client Secret", + "redirect_no_code_verifier": "Není definován Code Verifier", + "redirect_no_token_endpoint": "Není definován Token Endpoint", + "something_went_wrong_on_oauth_redirect": "Při OAuth přesměrování se něco pokazilo", + "something_went_wrong_on_token_generation": "Při generování tokenu se něco pokazilo", + "token_generation_oidc_discovery_failed": "Generování tokenu selhalo: nepodařilo se OIDC Discovery", + "grant_type": "Typ grantu", + "grant_type_auth_code": "Autorizační kód", + "token_fetched_successfully": "Token byl úspěšně získán", + "token_fetch_failed": "Token se nepodařilo získat", + "validation_failed": "Validace selhala, zkontrolujte prosím pole formuláře", + "no_refresh_token_present": "Chybí Refresh Token. Spusťte znovu proces generování tokenu", + "refresh_token_request_failed": "Požadavek na obnovení tokenu se nezdařil", + "token_refreshed_successfully": "Token byl úspěšně obnoven", + "label_authorization_endpoint": "Autorizační endpoint", "label_client_id": "Client ID", "label_client_secret": "Client Secret", "label_code_challenge": "Code Challenge", - "label_code_challenge_method": "Code Challenge Method", + "label_code_challenge_method": "Metoda Code Challenge", "label_code_verifier": "Code Verifier", "label_scopes": "Scopes", "label_token_endpoint": "Token Endpoint", - "label_use_pkce": "Use PKCE", - "label_implicit": "Implicit", - "label_password": "Password", - "label_username": "Username", - "label_auth_code": "Authorization Code", - "label_client_credentials": "Client Credentials" - }, - "pass_key_by": "Pass by", - "pass_by_query_params_label": "Query Parameters", - "pass_by_headers_label": "Headers", + "label_use_pkce": "Použít PKCE", + "label_implicit": "Implicitní", + "label_password": "Heslo", + "label_username": "Uživatelské jméno", + "label_auth_code": "Autorizační kód", + "label_client_credentials": "Client Credentials", + "label_send_as": "Ověření klienta", + "label_send_in_body": "Odeslat přihlašovací údaje v těle požadavku", + "label_send_as_basic_auth": "Odeslat přihlašovací údaje jako Basic Auth", + "enter_value": "Zadejte hodnotu", + "auth_request": "Auth Request", + "token_request": "Token Request", + "refresh_request": "Refresh Request", + "send_in": "Odeslat v" + }, + "pass_key_by": "Předat přes", + "pass_by_query_params_label": "Query parametry", + "pass_by_headers_label": "Hlavičky", "password": "Heslo", - "save_to_inherit": "Please save this request in any collection to inherit the authorization", - "token": "Žeton", + "save_to_inherit": "Uložte prosím tento požadavek do některé kolekce, aby bylo možné dědit autorizaci", + "token": "Token", + "access_token": "Access Token", + "client_token": "Client Token", + "client_secret": "Client Secret", + "timestamp": "Časové razítko", + "host": "Host", "type": "Typ autorizace", - "username": "Uživatelské jméno" + "username": "Uživatelské jméno", + "advance_config": "Pokročilá konfigurace", + "advance_config_description": "Hoppscotch automaticky přiřadí výchozí hodnoty určitým polím, pokud není zadána žádná explicitní hodnota", + "algorithm": "Algoritmus", + "payload": "Payload", + "secret": "Secret", + "aws_signature": { + "access_key": "Access Key", + "secret_key": "Secret Key", + "service_name": "Název služby", + "aws_region": "AWS Region", + "service_token": "Service Token" + }, + "digest": { + "realm": "Realm", + "nonce": "Nonce", + "algorithm": "Algoritmus", + "qop": "qop", + "nonce_count": "Nonce Count", + "client_nonce": "Client Nonce", + "opaque": "Opaque", + "disable_retry": "Zakázat opakování požadavku" + }, + "akamai": { + "headers_to_sign": "Hlavičky k podepsání", + "max_body_size": "Maximální velikost těla" + }, + "hawk": { + "id": "HAWK Auth ID", + "key": "HAWK Auth Key", + "ext": "ext", + "app": "app", + "dlg": "dlg", + "include": "Zahrnout Payload Hash" + }, + "jwt": { + "params_name": "Název parametrů", + "param_name": "Název parametru", + "header_prefix": "Předpona hlavičky", + "placeholder_request_header": "Předpona hlavičky požadavku", + "placeholder_request_param": "Název parametru požadavku", + "secret_base64_encoded": "Secret Base64 Encoded", + "headers": "JWT hlavičky", + "private_key": "Soukromý klíč", + "placeholder_headers": "JWT hlavičky" + }, + "ntlm": { + "domain": "Doména", + "workstation": "Pracovní stanice", + "disable_retrying_request": "Zakázat opakování požadavku" + }, + "asap": { + "issuer": "Vydavatel", + "audience": "Audience", + "expires_in": "Platnost vyprší za", + "key_id": "ID klíče", + "optional_config": "Volitelná konfigurace", + "subject": "Subject", + "additional_claims": "Additional Claims" + } }, "collection": { - "created": "Sbírka vytvořena", - "different_parent": "Cannot reorder collection with different parent", - "edit": "Upravit sbírku", - "import_or_create": "Importovat nebo vytvořit sbírku", - "import_collection": "Importovat sbírku", - "invalid_name": "Uveďte prosím název sbírky", - "invalid_root_move": "Collection already in the root", - "moved": "Moved Successfully", - "my_collections": "Moje sbírky", - "name": "Moje nová sbírka", - "name_length_insufficient": "Collection name should be at least 3 characters long", - "new": "Nová sbírka", - "order_changed": "Collection Order Updated", - "properties": "Vlastnosti sbírky", - "properties_updated": "Vlastnosti sbírky aktualizovány", - "renamed": "Sbírka přejmenována", - "request_in_use": "Request in use", + "title": "Kolekce", + "run": "Spustit kolekci", + "created": "Kolekce vytvořena", + "different_parent": "Nelze změnit pořadí kolekce s jiným nadřazeným prvkem", + "edit": "Upravit kolekci", + "import_or_create": "Importovat nebo vytvořit kolekci", + "import_collection": "Importovat kolekci", + "invalid_name": "Uveďte prosím název kolekce", + "invalid_root_move": "Kolekce už je v kořeni", + "moved": "Kolekce přesunuta", + "my_collections": "Moje kolekce", + "name": "Moje nová kolekce", + "name_length_insufficient": "Název kolekce musí mít alespoň 3 znaky", + "new": "Nová kolekce", + "order_changed": "Pořadí kolekce aktualizováno", + "properties": "Vlastnosti kolekce", + "properties_updated": "Vlastnosti kolekce aktualizovány", + "renamed": "Kolekce přejmenována", + "request_in_use": "Požadavek se používá", "save_as": "Uložit jako", - "save_to_collection": "Uložit do sbírky", - "select": "Vyberte sbírku", + "save_to_collection": "Uložit do kolekce", + "select": "Vyberte kolekci", "select_location": "Vyberte umístění", - "details": "Details", - "select_team": "Vyberte tým", - "team_collections": "Týmové sbírky" + "sorted": "Kolekce seřazena", + "details": "Podrobnosti", + "duplicated": "Kolekce duplikována" }, "confirm": { "close_unsaved_tab": "Opravdu chcete zavřít tuto kartu?", "close_unsaved_tabs": "Opravdu chcete zavřít všechny karty? {count} neuložených karet bude ztraceno.", - "exit_team": "Opravdu chcete opustit tento tým?", + "delete_all_activity_logs": "Opravdu chcete smazat všechny protokoly aktivit?", + "exit_team": "Opravdu chcete opustit tento pracovní prostor?", "logout": "Opravdu se chcete odhlásit?", - "remove_collection": "Opravdu chcete tuto sbírku trvale smazat?", + "remove_collection": "Opravdu chcete tuto kolekci trvale smazat?", "remove_environment": "Opravdu chcete toto prostředí trvale odstranit?", "remove_folder": "Opravdu chcete tuto složku trvale smazat?", "remove_history": "Opravdu chcete trvale smazat celou historii?", "remove_request": "Opravdu chcete tento požadavek trvale smazat?", + "remove_response": "Opravdu chcete tuto odpověď trvale smazat?", "remove_shared_request": "Opravdu chcete trvale smazat tento sdílený požadavek?", - "remove_team": "Opravdu chcete tento tým smazat?", + "remove_team": "Opravdu chcete tento pracovní prostor smazat?", "remove_telemetry": "Opravdu se chcete odhlásit z telemetrie?", "request_change": "Opravdu chcete zrušit aktuální požadavek? Neuložené změny budou ztraceny.", "save_unsaved_tab": "Chcete uložit změny provedené na této kartě?", - "sync": "Opravdu chcete synchronizovat tento pracovní prostor?", - "delete_access_token": "Are you sure you want to delete the access token {tokenLabel}?" + "sync": "Chcete obnovit pracovní prostor z cloudu? Tím se ztratí váš lokální postup.", + "delete_access_token": "Opravdu chcete smazat přístupový token {tokenLabel}?", + "delete_mock_server": "Opravdu chcete smazat tento mock server?" }, "context_menu": { "add_parameters": "Přidat k parametrům", @@ -224,24 +383,28 @@ }, "cookies": { "modal": { - "cookie_expires": "Expires", - "cookie_name": "Name", - "cookie_path": "Path", - "cookie_string": "Cookie string", - "cookie_value": "Value", - "empty_domain": "Domain is empty", - "empty_domains": "Domain list is empty", - "enter_cookie_string": "Enter cookie string", - "interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.", - "managed_tab": "Managed", - "new_domain_name": "New domain name", - "no_cookies_in_domain": "No cookies set for this domain", - "raw_tab": "Raw", - "set": "Set a cookie" + "cookie_expires": "Platnost do", + "cookie_name": "Název", + "cookie_path": "Cesta", + "cookie_string": "Řetězec cookie", + "cookie_value": "Hodnota", + "empty_domain": "Doména je prázdná", + "empty_domains": "Seznam domén je prázdný", + "enter_cookie_string": "Zadejte řetězec cookie", + "interceptor_no_support": "Aktuálně zvolený interceptor nepodporuje cookies. Zvolte jiný interceptor a zkuste to znovu.", + "managed_tab": "Spravované", + "new_domain_name": "Nový název domény", + "no_cookies_in_domain": "Pro tuto doménu nejsou nastavené žádné cookies", + "raw_tab": "Surové", + "set": "Nastavit cookie" } }, "count": { - "header": "Záhlaví {count}", + "currentValue": "Aktuální hodnota {count}", + "description": "Popis {count}", + "header": "Hlavičky {count}", + "initialValue": "Počáteční hodnota {count}", + "key": "Klíč {count}", "message": "Zpráva {count}", "parameter": "Parametr {count}", "protocol": "Protokol {count}", @@ -249,70 +412,227 @@ "variable": "Proměnná {count}" }, "documentation": { + "add_description": "Sem přidejte popis této kolekce...", + "add_description_placeholder": "Zde přidejte popis...", + "add_request_description": "Sem přidejte popis tohoto požadavku...", + "auth": { + "access_key": "Access Key", + "access_token": "Access Token", + "add_to": "Přidat do", + "akamai_edgegrid": "Akamai EdgeGrid", + "algorithm": "Algoritmus", + "api_key": "Klíč API", + "app_id": "ID aplikace", + "auth_id": "ID ověření", + "auth_key": "Auth Key", + "auth_url": "Auth URL", + "aws_signature": "AWS Signature", + "basic_auth": "Basic Auth", + "bearer_token": "Bearer Token", + "client_id": "Client ID", + "client_nonce": "Client Nonce", + "client_secret": "Client Secret", + "client_token": "Client Token", + "delegation": "Delegace", + "digest_auth": "Digest Auth", + "extra_data": "Extra Data", + "grant_type": "Typ grantu", + "hawk_auth": "HAWK Auth", + "headers_to_sign": "Hlavičky k podepsání", + "host": "Hostitel", + "include_payload_hash": "Zahrnout Payload Hash", + "jwt_auth": "JWT Auth", + "max_body_size": "Maximální velikost těla", + "no_auth": "Žádná autorizace", + "nonce": "Nonce", + "oauth_2": "OAuth 2.0", + "opaque": "Neprůhledný", + "password": "Heslo", + "payload": "Payload", + "qop": "QOP", + "realm": "Realm", + "region": "Region", + "scope": "Rozsah", + "secret_key": "Secret Key", + "service_name": "Název služby", + "timestamp": "Časové razítko", + "title": "Autentizace", + "token_url": "Token URL", + "user": "Uživatel", + "username": "Uživatelské jméno" + }, + "body": { + "content_type": "Typ obsahu", + "no_body": "Není definováno žádné tělo", + "title": "Tělo" + }, + "copied_to_clipboard": "Zkopírováno do schránky!", + "curl": { + "click_to_load": "Kliknutím načtete příkaz cURL", + "copied": "Příkaz cURL zkopírován do schránky!", + "copy_to_clipboard": "Kopírovat do schránky", + "generating": "Generování příkazu cURL...", + "load": "Načíst cURL", + "title": "cURL" + }, + "description": "Popis", + "error_rendering_markdown": "Chyba při vykreslování značky:", + "fetching_documentation": "Načítání dokumentace...", "generate": "Generovat dokumentaci", - "generate_message": "Importujte jakoukoli kolekci Hoppscotch a generujte dokumentaci API na cestách." + "generate_message": "Importujte libovolnou kolekci Hoppscotch a vygenerujte dokumentaci API.", + "headers": { + "no_headers": "Nejsou definovány žádné hlavičky", + "title": "Hlavičky" + }, + "hide_all_documentation": "Skrýt veškerou dokumentaci", + "inherited_from": "Zděděno od {name}", + "inherited_with_type": "Zděděno {type} od {name}", + "key": "Klíč", + "loading": "Načítání...", + "loading_collection_data": "Načítání dat kolekce...", + "no": "Žádný", + "no_collection_data": "Nejsou k dispozici žádná data kolekce", + "no_documentation_found": "Nebyla nalezena žádná dokumentace pro složky nebo požadavky", + "no_request_data": "Nejsou k dispozici žádná data požadavku", + "no_requests_or_folders": "Žádné požadavky ani složky", + "not_set": "Nenastaveno", + "open_request_in_new_tab": "Otevřít požadavek na nové kartě", + "parameters": { + "no_params": "Nejsou definovány žádné parametry", + "title": "Parametry" + }, + "percent_complete": "% dokončeno", + "processing_documentation": "Zpracování dokumentace", + "publish": { + "already_published": "Tato kolekce je již publikována", + "auto_sync": "Automatická synchronizace s kolekcí", + "auto_sync_description": "Automaticky aktualizovat publikované dokumenty, když se změní kolekce", + "button": "Publikovat", + "copy_url": "Kopírovat URL", + "delete": "Smazat dokumentaci", + "unpublish_doc": "Opravdu chcete zrušit publikování dokumentace?", + "delete_success": "Publikovaná dokumentace byla úspěšně smazána", + "doc_title": "Název", + "doc_version": "Verze", + "edit_published_doc": "Upravit publikovanou dokumentaci", + "last_updated": "Poslední aktualizace", + "metadata": "Metadata (JSON)", + "open_published_doc": "Otevřít publikovanou dokumentaci na nové kartě", + "publish_error": "Dokumentaci se nepodařilo publikovat", + "publish_success": "Dokumentace byla úspěšně zveřejněna!", + "published": "Publikováno", + "published_url": "Publikované URL", + "title": "Publikovat dokumentaci", + "update_button": "Aktualizovat", + "update_error": "Aktualizace dokumentace se nezdařila", + "update_published_docs": "Aktualizovat publikované dokumenty", + "update_success": "Dokumentace byla úspěšně aktualizována!", + "unpublish": "Zrušit publikování", + "update_title": "Aktualizovat publikovanou dokumentaci", + "url_copied": "URL zkopírováno do schránky!", + "view_published": "Zobrazit publikované dokumenty", + "view_title": "Zobrazit publikovanou dokumentaci" + }, + "request_opened_in_new_tab": "Požadavek byl otevřen na nové kartě!", + "response": { + "body": "Tělo odpovědi", + "copy": "Kopírovat odpověď", + "example_copied": "Příklad odpovědi zkopírován do schránky!", + "example_copy_failed": "Příklad odpovědi se nepodařilo zkopírovat", + "headers": "Hlavičky odpovědi", + "no_examples": "Nejsou k dispozici žádné příklady odpovědí", + "title": "Příklady odpovědí" + }, + "save_error": "Při ukládání dokumentace došlo k chybě", + "save_success": "Dokumentace byla úspěšně uložena", + "saved_items_status": "Uloženo {success} položek. Nepodařilo se uložit {failure} položky.", + "show_all_documentation": "Zobrazit veškerou dokumentaci", + "source": "Zdroj", + "title": "Dokumentace", + "unsaved_changes": "Máte {count} neuložených změn. Před zavřením prosím uložte.", + "untitled_collection": "Kolekce bez názvu", + "untitled_request": "Požadavek bez názvu", + "value": "Hodnota", + "variables": { + "no_vars": "Nejsou definovány žádné proměnné", + "title": "Proměnné" + }, + "yes": "Ano" }, "empty": { + "activity_logs": "Nebyly nalezeny žádné protokoly aktivit", "authorization": "Tento požadavek nepoužívá žádnou autorizaci", - "body": "Tato žádost nemá tělo", - "collection": "Sbírka je prázdná", - "collections": "Sbírky jsou prázdné", - "documentation": "Connect to a GraphQL endpoint to view documentation", - "endpoint": "Endpoint cannot be empty", - "environments": "Prostředí je prázdné", + "body": "Tento požadavek nemá tělo", + "collection": "Kolekce je prázdná", + "collections": "Kolekce jsou prázdné", + "documentation": "Připojte se ke GraphQL endpointu pro zobrazení dokumentace", + "empty_schema": "Nebylo nalezeno žádné schéma", + "endpoint": "Endpoint nesmí být prázdný", + "environments": "Prostředí jsou prázdná", + "collection_variables": "Proměnné kolekce jsou prázdné", "folder": "Složka je prázdná", - "headers": "Tento požadavek nemá žádná záhlaví", + "headers": "Tento požadavek nemá žádné hlavičky", "history": "Historie je prázdná", - "invites": "Invite list is empty", - "members": "Tým je prázdný", + "invites": "Seznam pozvánek je prázdný", + "members": "Pracovní prostor je prázdný", "parameters": "Tento požadavek nemá žádné parametry", - "pending_invites": "There are no pending invites for this team", - "profile": "Login to view your profile", + "pending_invites": "Pro tento pracovní prostor nejsou žádné čekající pozvánky", + "profile": "Pro zobrazení profilu se přihlaste", "protocols": "Protokoly jsou prázdné", - "request_variables": "This request does not have any request variables", - "schema": "Připojte se ke koncovému bodu GraphQL", - "secret_environments": "Secrets are not synced to Hoppscotch", - "shared_requests": "Shared requests are empty", - "shared_requests_logout": "Login to view your shared requests or create a new one", - "subscription": "Subscriptions are empty", - "team_name": "Název týmu prázdný", - "teams": "Týmy jsou prázdné", + "request_variables": "Tento požadavek nemá žádné proměnné", + "schema": "Připojte se ke GraphQL endpointu pro zobrazení schématu", + "search_environment": "Nebylo nalezeno žádné odpovídající prostředí pro", + "secret_environments": "Tajné údaje nejsou synchronizovány do Hoppscotch", + "shared_requests": "Sdílené požadavky jsou prázdné", + "shared_requests_logout": "Přihlaste se pro zobrazení sdílených požadavků nebo vytvořte nový", + "subscription": "Odběry jsou prázdné", + "team_name": "Název pracovního prostoru je prázdný", + "teams": "Nejste členem žádného pracovního prostoru", "tests": "Pro tento požadavek neexistují žádné testy", - "access_tokens": "Access tokens are empty", - "shortcodes": "Shortcodes are empty" + "access_tokens": "Přístupové tokeny jsou prázdné", + "response": "Nebyla přijata žádná odpověď", + "mock_servers": "Nebyly nalezeny žádné mock servery" }, "environment": { - "add_to_global": "Add to Global", - "added": "Environment addition", + "heading": "Prostředí", + "add_to_global": "Přidat do globálního", + "added": "Prostředí přidáno", "create_new": "Vytvořit nové prostředí", - "created": "Environment created", - "deleted": "Environment deletion", - "duplicated": "Environment duplicated", + "created": "Prostředí vytvořeno", + "current_value": "Aktuální hodnota", + "deleted": "Prostředí odstraněno", + "duplicated": "Prostředí duplikováno", "edit": "Upravit prostředí", "empty_variables": "Žádné proměnné", "global": "Globální", "global_variables": "Globální proměnné", - "import_or_create": "Import or create a environment", + "import_or_create": "Importovat nebo vytvořit prostředí", + "initial_value": "Počáteční hodnota", "invalid_name": "Zadejte platný název prostředí", "list": "Proměnné prostředí", "my_environments": "Moje prostředí", "name": "Název", - "nested_overflow": "nested environment variables are limited to 10 levels", + "nested_overflow": "Vnořené proměnné prostředí jsou omezeny na 10 úrovní", "new": "Nové prostředí", "no_active_environment": "Žádné aktivní prostředí", "no_environment": "Žádné prostředí", - "no_environment_description": "No environments were selected. Choose what to do with the following variables.", + "no_environment_description": "Nebyla vybrána žádná prostředí. Zvolte, co udělat s následujícími proměnnými.", "quick_peek": "Rychlý náhled na prostředí", + "replace_all_current_with_initial": "Vyměňte veškerý proud za počáteční", + "replace_all_initial_with_current": "Nahraďte všechny iniciály aktuálními", + "replace_current_with_initial": "Nahraďte počátečním", + "replace_initial_with_current": "Vyměňte za proud", "replace_with_variable": "Nahradit proměnnou", "scope": "Oblast platnosti", - "secrets": "Secrets", - "secret_value": "Secret value", + "secrets": "Tajné údaje", + "secret_value": "Tajná hodnota", "select": "Vyberte prostředí", "set": "Nastavit prostředí", "set_as_environment": "Nastavit jako prostředí", - "team_environments": "Týmová prostředí", + "short_name": "Prostředí musí mít v názvu minimálně 1 znak", + "team_environments": "Prostředí pracovního prostoru", "title": "Prostředí", - "updated": "Environment updation", + "updated": "Prostředí aktualizováno", "value": "Hodnota", "variable": "Proměnná", "variables": "Proměnné", @@ -321,44 +641,93 @@ "details": "Podrobnosti" }, "error": { - "authproviders_load_error": "Unable to load auth providers", + "network": { + "heading": "Chyba sítě", + "description": "Síťové připojení se nezdařilo. {message}: {cause}" + }, + "timeout": { + "heading": "Chyba vypršení časového limitu", + "description": "Časový limit požadavku vypršel během {phase}. {message}" + }, + "certificate": { + "heading": "Chyba certifikátu", + "description": "Neplatný certifikát. {message}: {cause}" + }, + "auth": { + "heading": "Chyba ověření", + "description": "Přístup odepřen. {message}: {cause}" + }, + "proxy": { + "heading": "Chyba proxy", + "description": "Připojení proxy se nezdařilo. {message}: {cause}" + }, + "parse": { + "heading": "Chyba analýzy", + "description": "Analýza odpovědi se nezdařila. {message}: {cause}" + }, + "version": { + "heading": "Chyba verze", + "description": "Nekompatibilní verze. {message}: {cause}" + }, + "abort": { + "heading": "Požadavek zrušen", + "description": "Operace zrušena. {message}: {cause}" + }, + "unknown": { + "heading": "Neznámá chyba", + "description": "Došlo k neznámé chybě.", + "cause": "Neznámá příčina" + }, + "extension": { + "heading": "Chyba rozšíření", + "description": "Spuštění požadavku na rozšíření se nezdařilo" + }, + "authproviders_load_error": "Nelze načíst poskytovatele přihlášení", "browser_support_sse": "Zdá se, že tento prohlížeč nemá podporu událostí odeslaných serverem.", - "check_console_details": "Podrobnosti najdete v protokolu konzoly.", - "check_how_to_add_origin": "Check how you can add an origin", + "check_console_details": "Podrobnosti najdete v konzoli.", + "check_how_to_add_origin": "Podívejte se, jak přidat origin", "curl_invalid_format": "cURL nemá správný formát", - "danger_zone": "Danger zone", - "delete_account": "Your account is currently an owner in these teams:", - "delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.", + "danger_zone": "Nebezpečná zóna", + "delete_account": "Váš účet je aktuálně jediným vlastníkem těchto pracovních prostorů:", + "delete_account_description": "Před smazáním účtu se z těchto pracovních prostorů odeberte, převeďte vlastnictví, nebo je smažte.", + "delete_activity_log": "Protokol aktivit se nepodařilo smazat", + "delete_all_activity_logs": "Nepodařilo se odstranit všechny protokoly aktivit", + "email_already_exists": "Tento e-mail již existuje s jiným účtem. Nastavte prosím jinou e-mailovou adresu.", "empty_email_address": "E-mailová adresa nemůže být prázdná", "empty_profile_name": "Název profilu nemůže být prázdný", - "empty_req_name": "Název prázdného požadavku", + "empty_req_name": "Název požadavku je prázdný", + "fetch_activity_logs": "Protokoly aktivit se nepodařilo načíst", "f12_details": "(F12 pro podrobnosti)", - "gql_prettify_invalid_query": "Neplatný dotaz nelze předběžně upravit, vyřešit chyby syntaxe dotazu a zkusit to znovu", - "incomplete_config_urls": "Incomplete configuration URLs", - "incorrect_email": "Incorrect email", - "invalid_link": "Invalid link", - "invalid_link_description": "The link you clicked is invalid or expired.", - "invalid_embed_link": "The embed does not exist or is invalid.", - "json_parsing_failed": "Invalid JSON", - "json_prettify_invalid_body": "Nelze předtifikovat neplatné tělo, vyřešit chyby syntaxe json a zkusit to znovu", - "network_error": "There seems to be a network error. Please try again.", + "gql_prettify_invalid_query": "Neplatný dotaz nelze formátovat pomocí Prettify. Opravte chyby syntaxe dotazu a zkuste to znovu.", + "incomplete_config_urls": "Neúplné konfigurační URL", + "incorrect_email": "Nesprávný e-mail", + "invalid_file_type": "Neplatný typ souboru pro `{filename}`.", + "invalid_link": "Neplatný odkaz", + "invalid_link_description": "Odkaz, na který jste klikli, je neplatný nebo expirovaný.", + "invalid_embed_link": "Embed odkaz neexistuje nebo je neplatný.", + "json_parsing_failed": "Neplatný JSON", + "json_prettify_invalid_body": "Neplatné tělo nelze Prettify. Opravte chyby syntaxe JSON a zkuste to znovu.", + "network_error": "Zdá se, že došlo k chybě sítě. Zkuste to prosím znovu.", "network_fail": "Požadavek nelze odeslat", - "no_collections_to_export": "No collections to export. Please create a collection to get started.", + "no_collections_to_export": "Žádné kolekce k exportu. Nejprve prosím vytvořte kolekci.", "no_duration": "Žádné trvání", - "no_environments_to_export": "No environments to export. Please create an environment to get started.", - "no_results_found": "No matches found", - "page_not_found": "This page could not be found", - "please_install_extension": "Please install the extension and add origin to the extension.", - "proxy_error": "Proxy error", + "no_environments_to_export": "Žádná prostředí k exportu. Nejprve prosím vytvořte prostředí.", + "no_results_found": "Nebyly nalezeny žádné shody", + "page_not_found": "Tuto stránku se nepodařilo najít", + "please_install_extension": "Nainstalujte rozšíření a přidejte origin do rozšíření.", + "proxy_error": "Chyba proxy", "same_email_address": "Aktualizovaná e-mailová adresa se shoduje s aktuální", - "same_profile_name": "Updated profile name is same as the current profile name", - "script_fail": "Skript předběžného požadavku nelze spustit", + "same_profile_name": "Aktualizovaný název profilu je stejný jako aktuální", + "script_fail": "Pre-request skript nelze spustit", "something_went_wrong": "Něco se pokazilo", - "post_request_script_fail": "Could not execute post-request script", - "reading_files": "Error while reading one or more files.", - "fetching_access_tokens_list": "Something went wrong while fetching the list of tokens", - "generate_access_token": "Something went wrong while generating the access token", - "delete_access_token": "Something went wrong while deleting the access token" + "subscription_error": "Nepodařilo se přihlásit k odběru tématu: {error}", + "post_request_script_fail": "Nepodařilo se spustit post-request skript", + "reading_files": "Chyba při čtení jednoho nebo více souborů.", + "fetching_access_tokens_list": "Při načítání seznamu tokenů se něco pokazilo", + "generate_access_token": "Při generování přístupového tokenu se něco pokazilo", + "delete_access_token": "Při mazání přístupového tokenu se něco pokazilo", + "extension_not_found": "Rozšíření nenalezeno", + "invalid_request": "Neplatná data požadavku" }, "export": { "as_json": "Exportovat jako JSON", @@ -366,39 +735,72 @@ "create_secret_gist_tooltip_text": "Exportovat jako tajný Gist", "failed": "Během exportování se něco nepodařilo", "secret_gist_success": "Tajný Gist úspěšně exportován", - "require_github": "Přihlaste se pomocí GitHub a vytvořte tajný seznam", + "require_github": "Přihlaste se přes GitHub a vytvořte tajný Gist", "title": "Exportovat", - "success": "Úspěšně exportováno", - "gist_created": "Podstata vytvořena" + "success": "Úspěšně exportováno" + }, + "file_upload": { + "choose_file": "Vyberte soubor", + "max_size_format": "Maximálně 5 MB. Podporuje JPEG, PNG, GIF, WebP", + "profile_photo_updated": "Profilová fotka byla úspěšně aktualizována", + "profile_photo_removed": "Profilová fotka byla úspěšně odstraněna", + "org_logo_updated": "Logo organizace bylo úspěšně aktualizováno", + "error_size_limit": "Velikost souboru musí být menší než 5 MB", + "error_invalid_format": "Soubor musí být obrázek (JPEG, PNG, GIF nebo WebP)", + "error_invalid_upload_type": "Neplatný typ nahrávání", + "error_invalid_org_id": "Neplatný formát ID organizace", + "error_upload_failed": "Nahrání se nezdařilo. Zkuste to prosím znovu", + "error_network_failed": "Chyba sítě. Zkontrolujte prosím své připojení", + "error_timeout": "Časový limit nahrávání vypršel po 30 sekundách. Zkuste to prosím znovu", + "error_missing_backend_url": "Backend URL není nakonfigurován. Nastavte prosím proměnnou prostředí VITE_BACKEND_API_URL v nastavení prostředí", + "error_invalid_backend_url": "Neplatná konfigurace backendu URL" + }, + "filename": { + "cookie_key_value_pairs": "Cookie", + "codegen": "{request_name} - kód", + "graphql_response": "GraphQL odpověď", + "lens": "{request_name} - odpověď", + "realtime_response": "Odpověď v reálném čase", + "response_interface": "Rozhraní odpovědi" }, "filter": { - "all": "All", - "none": "None", - "starred": "Starred" + "all": "Vše", + "none": "Žádné", + "starred": "S hvězdičkou" }, "folder": { "created": "Složka vytvořena", "edit": "Upravit složku", "invalid_name": "Zadejte název složky", - "name_length_insufficient": "Folder name should be at least 3 characters long", + "name_length_insufficient": "Název složky musí mít alespoň 3 znaky", "new": "Nová složka", - "renamed": "Složka přejmenována" + "run": "Spustit složku", + "renamed": "Složka přejmenována", + "sorted": "Složka seřazena" }, "graphql": { - "connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?", - "connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is", - "connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is", + "arguments": "Argumenty", + "connection_switch_confirm": "Chcete se připojit k nejnovějšímu GraphQL endpointu?", + "connection_error_http": "Nepodařilo se načíst schéma GraphQL kvůli chybě sítě.", + "connection_switch_new_url": "Přepnutí na kartu vás odpojí od aktivního GraphQL připojení. Nová adresa připojení je", + "connection_switch_url": "Jste připojeni ke GraphQL endpointu, adresa připojení je", + "deprecated": "Zastaralé", + "fields": "Pole", + "mutation": "Mutace", "mutations": "Mutace", "schema": "Schéma", - "subscriptions": "Předplatné", - "switch_connection": "Switch connection", - "url_placeholder": "Enter a GraphQL endpoint URL" + "show_depricated_values": "Zobrazit zastaralé hodnoty", + "subscription": "Předplatné", + "subscriptions": "Odběry", + "switch_connection": "Přepnout připojení", + "url_placeholder": "Zadejte URL GraphQL endpointu", + "query": "Query" }, "graphql_collections": { - "title": "GraphQL Collections" + "title": "GraphQL kolekce" }, "group": { - "time": "Time", + "time": "Čas", "url": "URL" }, "header": { @@ -408,89 +810,187 @@ }, "helpers": { "authorization": "Autorizační hlavička se automaticky vygeneruje při odeslání požadavku.", - "collection_properties_authorization": "Tato autorizace se nastaví pro každý požadavek z této sbírky.", - "collection_properties_header": "Toto záhlaví se nastaví pro každý požadavek z této sbírky.", + "collection_properties_authorization": "Tato autorizace se nastaví pro každý požadavek z této kolekce.", + "collection_properties_header": "Tato hlavička se nastaví pro každý požadavek z této kolekce.", "generate_documentation_first": "Nejprve vytvořte dokumentaci", - "network_fail": "Nelze dosáhnout koncového bodu API. Zkontrolujte připojení k síti a zkuste to znovu.", - "offline": "Zdá se, že jste offline. Data v tomto pracovním prostoru nemusí být aktuální.", - "offline_short": "Zdá se, že jste offline.", - "post_request_tests": "Testovací skripty jsou napsány v JavaScriptu a jsou spuštěny po přijetí odpovědi.", - "pre_request_script": "Skripty před požadavkem jsou napsány v JavaScriptu a jsou spuštěny před odesláním požadavku.", - "script_fail": "Zdá se, že ve skriptu předběžného požadavku je chyba. Zkontrolujte níže uvedenou chybu a opravte skript odpovídajícím způsobem.", - "post_request_script_fail": "There seems to be an error with post-request script. Please fix the errors and run tests again", - "post_request_script": "Napište testovací skript pro automatizaci ladění." + "network_fail": "K API endpointu se nelze připojit. Zkontrolujte síťové připojení nebo zvolte jiný Interceptor a zkuste to znovu.", + "offline": "Používáte Hoppscotch offline. Aktualizace se po připojení synchronizují podle nastavení pracovního prostoru.", + "offline_short": "Používáte Hoppscotch offline.", + "post_request_tests": "Post-request skripty se píšou v JavaScriptu a spouštějí se po přijetí odpovědi.", + "pre_request_script": "Pre-request skripty jsou napsané v JavaScriptu a spouštějí se před odesláním požadavku.", + "script_fail": "Zdá se, že v pre-request skriptu je chyba. Zkontrolujte níže uvedenou chybu a skript opravte.", + "post_request_script_fail": "Zdá se, že v post-request skriptu je chyba. Opravte chyby a spusťte testy znovu.", + "post_request_script": "Napište post-request skript pro automatizaci ladění." }, "hide": { - "collection": "Collapse Collection Panel", + "collection": "Sbalit panel kolekcí", "more": "Skrýt více", "preview": "Skrýt náhled", - "sidebar": "Skrýt postranní lištu" + "sidebar": "Skrýt postranní lištu", + "password": "Skrýt heslo" }, "import": { - "collections": "Import sbírek", + "collections": "Import kolekcí", "curl": "Importovat cURL", - "environments_from_gist": "Import From Gist", - "environments_from_gist_description": "Import Hoppscotch Environments From Gist", - "failed": "Import se nezdařil", - "from_file": "Import from File", + "environments_from_gist": "Importovat z Gist", + "environments_from_gist_description": "Importovat prostředí Hoppscotch z Gist", + "failed": "Chyba při importu: formát nebyl rozpoznán", + "from_file": "Importovat ze souboru", "from_gist": "Import z Gist", - "from_gist_description": "Import from Gist URL", - "from_insomnia": "Import from Insomnia", - "from_insomnia_description": "Import from Insomnia collection", - "from_json": "Import from Hoppscotch", - "from_json_description": "Import from Hoppscotch collection file", - "from_my_collections": "Importovat z mých sbírek", - "from_my_collections_description": "Import from My Collections file", - "from_openapi": "Import from OpenAPI", - "from_openapi_description": "Import from OpenAPI specification file (YML/JSON)", - "from_postman": "Import from Postman", - "from_postman_description": "Import from Postman collection", - "from_url": "Import from URL", - "gist_url": "Zadejte URL adresy", - "gql_collections_from_gist_description": "Import GraphQL Collections From Gist", - "hoppscotch_environment": "Hoppscotch Environment", - "hoppscotch_environment_description": "Import Hoppscotch Environment JSON file", - "import_from_url_invalid_fetch": "Couldn't get data from the url", - "import_from_url_invalid_file_format": "Error while importing collections", - "import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'", - "import_from_url_success": "Collections Imported", - "insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file", - "json_description": "Import collections from a Hoppscotch Collections JSON file", - "postman_environment": "Postman Environment", - "postman_environment_description": "Import Postman Environment from a JSON file", + "from_gist_description": "Importovat z URL Gist", + "from_gist_import_summary": "Budou importovány všechny funkce Hoppscotch.", + "from_hoppscotch_importer_summary": "Budou importovány všechny funkce Hoppscotch.", + "from_insomnia": "Importovat z Insomnia", + "from_insomnia_description": "Importovat kolekci Insomnia", + "from_insomnia_import_summary": "Kolekce a požadavky budou importovány.", + "from_json": "Importovat z kolekce Hoppscotch", + "from_json_description": "Importovat ze souboru kolekce Hoppscotch", + "from_my_collections": "Importovat z mých kolekcí", + "from_my_collections_description": "Importovat soubor z mých kolekcí", + "from_all_collections": "Import z jiného pracovního prostoru", + "from_all_collections_description": "Importovat libovolné kolekce z jiného pracovního prostoru do aktuálního pracovního prostoru.", + "from_openapi": "Importovat z OpenAPI", + "from_openapi_description": "Importovat soubor specifikace OpenAPI (YML/JSON)", + "from_openapi_import_summary": "Budou importovány kolekce (vytvořené ze štítků), požadavky i ukázkové odpovědi.", + "from_postman": "Importovat z Postman", + "from_postman_description": "Importovat kolekci Postman", + "from_postman_import_summary": "Budou importovány kolekce, požadavky a ukázky odpovědí.", + "import_scripts": "Importovat skripty", + "import_scripts_description": "Podporuje Postman kolekce v2.0/v2.1.", + "from_url": "Importovat z URL", + "gist_url": "Zadejte URL Gistu", + "from_har": "Import z HAR", + "from_har_description": "Import ze souboru HAR", + "from_har_import_summary": "Požadavky budou importovány do výchozí kolekce.", + "gql_collections_from_gist_description": "Importovat GraphQL kolekce z Gist", + "hoppscotch_environment": "Prostředí Hoppscotch", + "hoppscotch_environment_description": "Importovat JSON soubor prostředí Hoppscotch", + "import_from_url_invalid_fetch": "Z URL se nepodařilo načíst data", + "import_from_url_invalid_file_format": "Chyba při importu kolekcí", + "import_from_url_invalid_type": "Nepodporovaný typ. Přijímané hodnoty jsou 'hoppscotch', 'openapi', 'postman', 'insomnia'", + "import_from_url_success": "Kolekce byly importovány", + "insomnia_environment_description": "Importovat prostředí Insomnia ze souboru JSON/YAML", + "json_description": "Importovat kolekce ze souboru Hoppscotch Collections JSON", + "postman_environment": "Prostředí Postman", + "postman_environment_description": "Importovat prostředí Postman ze souboru JSON", "title": "Import", - "file_size_limit_exceeded_warning_multiple_files": "Chosen files exceed the recommended limit of 10MB. Only the first {files} selected will be imported", - "file_size_limit_exceeded_warning_single_file": "The currently chosen file exceeds the recommended limit of 10MB. Please select another file.", - "success": "Successfully imported" + "file_size_limit_exceeded_warning_multiple_files": "Vybrané soubory překračují doporučený limit {sizeLimit} MB. Importuje se pouze prvních {files} vybraných souborů.", + "file_size_limit_exceeded_warning_single_file": "Vybraný soubor překračuje doporučený limit {sizeLimit} MB. Vyberte prosím jiný soubor.", + "success": "Úspěšně importováno", + "import_summary_collections_title": "Kolekce", + "import_summary_requests_title": "Požadavky", + "import_summary_responses_title": "Odpovědi", + "import_summary_pre_request_scripts_title": "Pre-request skripty", + "import_summary_post_request_scripts_title": "Post-request skripty", + "import_summary_not_supported_by_hoppscotch_import": "V současné době nepodporujeme import {featureLabel} z tohoto zdroje.", + "import_summary_script_found": "skript nalezen, ale nebyl importován", + "import_summary_scripts_found": "skripty byly nalezeny, ale nebyly importovány", + "import_summary_enable_experimental_sandbox": "Chcete-li importovat Postman skripty, povolte v nastavení \"Experimentální skriptovací sandbox\". Poznámka: Tato funkce je experimentální.", + "cors_error_modal": { + "title": "Byla zjištěna chyba CORS", + "description": "Import se nezdařil kvůli omezením CORS (Cross-Origin Resource Sharing) uloženým serverem.", + "explanation": "Jedná se o bezpečnostní funkci, která zabraňuje webovým stránkám zadávat požadavky na různé domény. Toto omezení můžete obejít pomocí naší proxy služby.", + "url_label": "Cílové URL", + "retry_with_proxy": "Zkuste to znovu s proxy" + } + }, + "instances": { + "switch": "Přepnout instanci Hoppscotch", + "enter_server_url": "Připojit se k self-hostované instanci", + "already_connected": "K této instanci jste již připojeni", + "recent_connections": "Nedávná připojení", + "add_instance": "Přidat instanci", + "add_new": "Přidat novou instanci", + "confirm_remove": "Potvrdit odebrání", + "remove_warning": "Opravdu chcete odstranit tuto instanci?", + "clear_cached_bundles": "Vymazat balíčky uložené v mezipaměti", + "opening_add_modal": "Otevírá se dialog pro přidání instance", + "closed_add_modal": "Dialog Přidat instanci byl uzavřen", + "cancelled_removal": "Odebrání instance bylo zrušeno", + "connection_cancelled": "Připojení bylo zrušeno ověřením před připojením", + "post_connect_completed": "Nastavení po připojení bylo dokončeno", + "connecting": "Připojování k instanci...", + "confirm_removal": "Potvrdit odebrání instance", + "removal_cancelled": "Odstranění instance bylo zrušeno ověřením před odstraněním", + "post_remove_completed": "Čištění po odstranění bylo dokončeno", + "removing": "Odebírání instance...", + "clearing_cache": "Mazání mezipaměti...", + "initialized": "Přepínač instancí inicializován", + "connecting_state": "Navazování spojení...", + "connected_state": "Úspěšně připojeno k instanci", + "disconnected_state": "Odpojeno od instance", + "stream_error": "Sledování stavu připojení se nezdařilo", + "recent_instances_error": "Nepodařilo se načíst nedávné instance", + "instance_changed": "Přepnuto na instanci", + "current_instance_error": "Aktuální instanci se nepodařilo sledovat", + "not_available": "Přepínání instancí není k dispozici", + "cleanup_completed": "Vyčištění přepínače instancí dokončeno" }, "inspections": { - "description": "Prozkoumat možné chyby", + "description": "Zkontrolovat možné chyby", "environment": { - "add_environment": "Add to Environment", - "add_environment_value": "Add value", - "empty_value": "Environment value is empty for the variable '{variable}' ", - "not_found": "Environment variable “{environment}” not found." + "add_environment": "Přidat do prostředí", + "add_environment_value": "Přidat hodnotu", + "empty_value": "Hodnota prostředí je prázdná pro proměnnou '{variable}'", + "not_found": "Proměnná prostředí „{environment}“ nebyla nalezena." }, "header": { - "cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead." + "cookie": "Prohlížeč nedovoluje Hoppscotch nastavit hlavičku Cookie. Dokud pracujeme na desktopové aplikaci Hoppscotch (již brzy), použijte místo toho hlavičku Authorization." }, "response": { - "401_error": "Please check your authentication credentials.", - "404_error": "Please check your request URL and method type.", - "cors_error": "Please check your Cross-Origin Resource Sharing configuration.", - "default_error": "Please check your request.", - "network_error": "Please check your network connection." + "401_error": "Zkontrolujte prosím své přihlašovací údaje.", + "404_error": "Zkontrolujte prosím URL požadavku a metodu.", + "cors_error": "Zkontrolujte prosím konfiguraci CORS.", + "default_error": "Zkontrolujte prosím svůj požadavek.", + "network_error": "Zkontrolujte prosím síťové připojení." }, - "title": "Inspector", + "title": "Inspektor", "url": { - "extension_not_installed": "Extension not installed.", - "extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.", - "extention_enable_action": "Enable Browser Extension", - "extention_not_enabled": "Extension not enabled." + "extension_not_installed": "Rozšíření není nainstalováno.", + "extension_unknown_origin": "Ujistěte se, že jste přidali origin API endpointu do seznamu povolených originů rozšíření Hoppscotch.", + "extention_enable_action": "Povolit rozšíření prohlížeče", + "extention_not_enabled": "Rozšíření není povoleno.", + "localaccess_unsupported": "Aktuální zachycovač nepodporuje místní přístup, zvažte použití agenta, zachycovače rozšíření nebo desktopové aplikace" + }, + "auth": { + "digest": "Při použití autorizace Digest se doporučuje zachycovač agentů ve webové aplikaci nebo nativní zachycovač v aplikaci pro stolní počítače.", + "hawk": "Při použití autorizace Hawk se doporučuje zachycovač agentů ve webové aplikaci nebo nativní zachycovač v aplikaci pro stolní počítače." + }, + "body": { + "binary": "Odesílání binárních dat přes aktuální interceptor zatím není podporováno." + }, + "scripting_interceptor": { + "pre_request": "pre-request skript", + "post_request": "post-request skript", + "both_scripts": "pre-request a post-request skripty", + "unsupported_interceptor": "Vaše {scriptType} používá {apiUsed}. Pro spolehlivé provádění skriptů přepněte na Agent interceptor (webová aplikace) nebo Native interceptor (desktopová aplikace). Zachycovač {interceptor} má omezenou podporu pro požadavky skriptování a nemusí fungovat podle očekávání.", + "same_origin_csrf_warning": "Bezpečnostní varování: Vaše {scriptType} odesílá požadavky stejného původu pomocí {apiUsed}. Protože tato platforma používá ověřování založené na souborech cookie, tyto požadavky automaticky zahrnují soubory cookie vaší relace, což potenciálně umožňuje škodlivým skriptům provádět neoprávněné akce. Použijte Agent interceptor pro požadavky stejného původu nebo spouštějte pouze skripty, kterým důvěřujete." + } + }, + "interceptor": { + "native": { + "name": "Nativní", + "settings_title": "Nativní" + }, + "agent": { + "name": "Agent", + "settings_title": "Agent" + }, + "proxy": { + "name": "Proxy", + "settings_title": "Proxy" + }, + "browser": { + "name": "Prohlížeč", + "settings_title": "Prohlížeč" + }, + "extension": { + "name": "Rozšíření", + "settings_title": "Rozšíření" } }, "layout": { - "collapse_collection": "Sbalit nebo rozbalit sbírky", + "collapse_collection": "Sbalit nebo rozbalit kolekce", "collapse_sidebar": "Sbalit nebo rozbalit postranní lištu", "column": "Svislé rozvržení", "name": "Rozvržení", @@ -498,388 +998,573 @@ }, "modal": { "close_unsaved_tab": "Máte neuložené změny", - "collections": "Sbírky", + "collections": "Kolekce", "confirm": "Potvrdit", "customize_request": "Přizpůsobit požadavek", "edit_request": "Upravit požadavek", + "edit_response": "Upravit odpověď", "import_export": "Importovat/exportovat", + "response_name": "Název odpovědi", "share_request": "Sdílet požadavek" }, "mqtt": { - "already_subscribed": "You are already subscribed to this topic.", - "clean_session": "Clean Session", - "clear_input": "Clear input", - "clear_input_on_send": "Clear input on send", + "already_subscribed": "K tomuto tématu už jste přihlášeni.", + "clean_session": "Čistá relace", + "clear_input": "Vymazat vstup", + "clear_input_on_send": "Vymazat vstup při odeslání", "client_id": "Client ID", - "color": "Pick a color", - "communication": "Sdělení", - "connection_config": "Connection Config", - "connection_not_authorized": "This MQTT connection does not use any authentication.", - "invalid_topic": "Please provide a topic for the subscription", - "keep_alive": "Keep Alive", + "color": "Vybrat barvu", + "communication": "Komunikace", + "connection_config": "Konfigurace připojení", + "connection_not_authorized": "Toto MQTT připojení nepoužívá žádné ověření.", + "invalid_topic": "Zadejte téma pro odběr", + "keep_alive": "Udržovat spojení", "log": "Záznam", - "lw_message": "Last-Will Message", + "lw_message": "Last-Will zpráva", "lw_qos": "Last-Will QoS", "lw_retain": "Last-Will Retain", - "lw_topic": "Last-Will Topic", + "lw_topic": "Last-Will téma", "message": "Zpráva", - "new": "New Subscription", - "not_connected": "Please start a MQTT connection first.", + "new": "Nové předplatné", + "not_connected": "Nejprve spusťte MQTT připojení.", "publish": "Publikovat", "qos": "QoS", "ssl": "SSL", - "subscribe": "předplatit", + "subscribe": "Přihlásit k odběru", "topic": "Téma", "topic_name": "Název tématu", - "topic_title": "Téma Publikovat / Přihlásit se k odběru", + "topic_title": "Téma – publikovat / přihlásit k odběru", "unsubscribe": "Odhlásit odběr", "url": "URL" }, "navigation": { - "doc": "Docs", + "admin_dashboard": "Administrace", + "doc": "Dokumentace", "graphql": "GraphQL", - "profile": "Profile", + "profile": "Profil", "realtime": "Reálný čas", "rest": "REST", - "settings": "Nastavení" + "mock_servers": "Mock servery", + "settings": "Nastavení", + "goto_app": "Přejít na aplikaci", + "authentication": "Autentizace" + }, + "mock_server": { + "confirm_delete_log": "Opravdu chcete tento protokol smazat?", + "create_mock_server": "Nakonfigurovat Mock Server", + "mock_server_configuration": "Konfigurace Mock Serveru", + "mock_server_name": "Název mock serveru", + "mock_server_name_placeholder": "Zadejte název mock serveru", + "base_url": "Base URL", + "start_server": "Spustit server", + "stop_server": "Zastavit server", + "mock_server_created": "Mock server byl úspěšně vytvořen", + "mock_server_started": "Mock server byl úspěšně spuštěn", + "mock_server_stopped": "Mock server byl úspěšně zastaven", + "active": "Mock server je aktivní", + "inactive": "Mock server je neaktivní", + "edit_mock_server": "Upravit Mock Server", + "path_based_url": "URL založená na cestě", + "subdomain_based_url": "URL založená na subdoméně", + "mock_server_updated": "Mock server byl úspěšně aktualizován", + "no_collection": "Žádná kolekce", + "collection_deleted": "Přidružená kolekce byla odstraněna.", + "private_access_hint": "U soukromých mock serverů přidejte hlavičku 'x-api-key' s vaším osobním Access Tokenem (vytvoříte ho v profilu).", + "private_access_instruction": "Pro přístup k tomuto soukromému mock serveru přidejte hlavičku 'x-api-key' s vaším osobním Access Tokenem.", + "create_token_here": "Vytvořit zde", + "status": "Stav", + "server_running": "Server běží", + "server_stopped": "Server je zastaven", + "delay_ms": "Zpoždění odezvy (ms)", + "delay_placeholder": "Zadejte zpoždění v milisekundách", + "delay_description": "Přidat umělé zpoždění do mock odpovědí", + "public_access": "Veřejný přístup", + "public": "Veřejný", + "private": "Soukromý", + "public_description": "Kdokoli s URL má přístup k tomuto mock serveru", + "private_description": "K tomuto mock serveru mají přístup pouze ověření uživatelé", + "select_collection": "Vyberte kolekci", + "select_collection_error": "Vyberte prosím kolekci", + "invalid_collection_error": "Nepodařilo se vytvořit mock server pro kolekci.", + "url_copied": "URL zkopírováno do schránky", + "make_public": "Zveřejnit", + "view_logs": "Zobrazit protokoly", + "logs_title": "Logy mock serveru", + "no_logs": "Nejsou k dispozici žádné protokoly", + "request_headers": "Hlavičky požadavku", + "request_body": "Tělo požadavku", + "response_status": "Stav odpovědi", + "response_headers": "Hlavičky odpovědi", + "response_body": "Tělo odpovědi", + "log_deleted": "Protokol byl úspěšně smazán", + "description": "Mock servery vám umožňují simulovat odpovědi API na základě vašich příkladů odpovědí kolekce.", + "set_in_environment": "Nastavit v prostředí", + "set_in_environment_hint": "URL mock serveru bude automaticky přidáno jako proměnná 'mockUrl' do prostředí kolekce", + "environment_variable_added": "Mock URL přidáno do prostředí", + "environment_variable_updated": "Mock URL aktualizováno v prostředí", + "environment_created_with_variable": "Prostředí vytvořeno s mock URL", + "add_example_request": "Přidat ukázkový požadavek", + "add_example_request_hint": "Bude vytvořena kolekce s ukázkovým požadavkem, který ukazuje použití mock serveru", + "create_example_collection": "Vytvořit ukázkovou kolekci", + "create_example_collection_hint": "Vytvořit ukázkovou kolekci Pet Store se vzorovými požadavky (GET, POST, PUT, DELETE)", + "creating_example_collection": "Vytváření příkladu kolekce...", + "failed_to_create_collection": "Nepodařilo se vytvořit příklad kolekce", + "enable_example_collection_hint": "Pro nový režim kolekce zapněte přepínač 'Vytvořit ukázkovou kolekci'", + "new_collection_name_hint": "Kolekce bude vytvořena se stejným názvem jako váš mock server", + "existing_collection": "Existující kolekce", + "new_collection": "Nová kolekce" }, "preRequest": { "javascript_code": "JavaScriptový kód", "learn": "Přečtěte si dokumentaci", - "script": "Předběžný skript", + "script": "Pre-request skript", "snippets": "Úryvky" }, "profile": { - "app_settings": "App Settings", - "default_hopp_displayname": "Unnamed User", + "app_settings": "Nastavení aplikace", + "default_hopp_displayname": "Uživatel bez jména", "editor": "Editor", - "editor_description": "Editors can add, edit, and delete requests.", - "email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.", - "no_permission": "You do not have permission to perform this action.", - "owner": "Owner", - "owner_description": "Owners can add, edit, and delete requests, collections and team members.", - "roles": "Roles", - "roles_description": "Roles are used to control access to the shared collections.", - "updated": "Profile updated", - "viewer": "Viewer", - "viewer_description": "Viewers can only view and use requests." + "editor_description": "Editoři mohou přidávat, upravovat a mazat požadavky.", + "email_verification_mail": "Na vaši e-mailovou adresu byl odeslán ověřovací e-mail. Klikněte na odkaz pro ověření e-mailu.", + "no_permission": "Nemáte oprávnění k této akci.", + "owner": "Vlastník", + "owner_description": "Vlastníci mohou přidávat, upravovat a mazat požadavky, kolekce a členy pracovního prostoru.", + "roles": "Role", + "roles_description": "Role se používají k řízení přístupu ke sdíleným kolekcím.", + "updated": "Profil aktualizován", + "viewer": "Čtenář", + "viewer_description": "Čtenáři mohou požadavky pouze zobrazovat a používat.", + "verified_email_sent": "Na vaši e-mailovou adresu byl odeslán ověřovací e-mail. Po ověření e-mailové adresy prosím obnovte stránku. Pokud tento e-mail není přidružen k žádnému jinému účtu, obdržíte e-mail." }, "remove": { - "star": "Odstraňte hvězdu" + "star": "Odebrat hvězdičku" }, "request": { - "added": "Žádost přidána", + "added": "Požadavek přidán", "add": "Přidat požadavek", "authorization": "Autorizace", - "body": "Žádost", - "choose_language": "Vyber jazyk", + "body": "Tělo", + "choose_language": "Vyberte jazyk", "content_type": "Typ obsahu", "content_type_titles": { - "others": "Others", - "structured": "Structured", - "text": "Text" + "others": "Ostatní", + "structured": "Strukturované", + "text": "Text", + "binary": "Binární" }, - "different_collection": "Cannot reorder requests from different collections", - "duplicated": "Request duplicated", + "show_content_type": "Zobrazit typ obsahu", + "different_collection": "Nelze měnit pořadí požadavků z různých kolekcí", + "duplicated": "Požadavek duplikován", "duration": "Doba trvání", "enter_curl": "Zadejte cURL", - "generate_code": "Vygenerujte kód", + "generate_code": "Vygenerovat kód", "generated_code": "Generovaný kód", - "go_to_authorization_tab": "Go to Authorization tab", - "go_to_body_tab": "Go to Body tab", - "header_list": "Seznam záhlaví", - "invalid_name": "Uveďte prosím název žádosti", + "go_to_authorization_tab": "Přejít na kartu Autorizace", + "go_to_body_tab": "Přejít na kartu Tělo", + "header_list": "Seznam hlaviček", + "invalid_name": "Uveďte prosím název požadavku", "method": "Metoda", "moved": "Požadavek přesunut", - "name": "Vyžádejte si jméno", + "name": "Název požadavku", "new": "Nový požadavek", "order_changed": "Pořadí požadavků aktualizováno", - "override": "Override", - "override_help": "Set Content-Type in Headers", - "overriden": "Overridden", - "parameter_list": "Parametry dotazu", + "override": "Přepsat", + "override_help": "Nastavit Content-Type v hlavičkách", + "overriden": "Přepsáno", + "parameter_list": "Query parametry", "parameters": "Parametry", "path": "Cesta", - "payload": "Užitečné zatížení", - "query": "Dotaz", - "raw_body": "Raw Request Body", + "payload": "Payload", + "query": "Query", + "raw_body": "Raw body požadavku", "rename": "Přejmenovat požadavek", "renamed": "Požadavek přejmenován", - "request_variables": "Request variables", - "run": "Běh", + "request_variables": "Proměnné požadavku", + "response_name_exists": "Název odpovědi již existuje", + "run": "Spustit", "save": "Uložit", "save_as": "Uložit jako", "saved": "Požadavek uložen", "share": "Sdílet", - "share_description": "Share Hoppscotch with your friends", + "share_description": "Sdílejte Hoppscotch se svými přáteli", "share_request": "Sdílet požadavek", - "stop": "Stop", - "title": "Žádost", + "stop": "Zastavit", + "title": "Požadavek", "type": "Typ požadavku", "url": "URL", - "url_placeholder": "Enter a URL or paste a cURL command", + "url_placeholder": "Zadejte URL nebo vložte příkaz cURL", "variables": "Proměnné", - "view_my_links": "View my links", - "copy_link": "Kopírovat odkaz" + "view_my_links": "Zobrazit moje odkazy", + "generate_name_error": "Vygenerování názvu požadavku se nezdařilo." }, "response": { "audio": "Audio", "body": "Tělo odpovědi", - "filter_response_body": "Filter JSON response body (uses jq syntax)", - "headers": "Záhlaví", + "duplicated": "Odpověď duplikována", + "duplicate_name_error": "Odpověď se stejným názvem již existuje", + "filter_response_body": "Filtrovat JSON tělo odpovědi (používá syntaxi jq)", + "headers": "Hlavičky", + "request_headers": "Hlavičky požadavku", "html": "HTML", "image": "Obrázek", "json": "JSON", "pdf": "PDF", + "please_save_request": "Uložte požadavek pro vytvoření příkladu", "preview_html": "Náhled HTML", "raw": "Surové", + "renamed": "Odpověď přejmenována", + "same_name_inspector_warning": "Název odpovědi již pro tento požadavek existuje, pokud bude uložen, přepíše stávající odpověď", "size": "Velikost", "status": "Stav", "time": "Čas", - "title": "Odezva", + "title": "Odpověď", "video": "Video", - "waiting_for_connection": "čekání na připojení", + "waiting_for_connection": "Čekání na připojení", "xml": "XML", "generate_data_schema": "Vygenerovat schéma dat", - "data_schema": "Schéma dat" + "data_schema": "Schéma dat", + "saved": "Odpověď uložena", + "invalid_name": "Zadejte prosím název odpovědi" }, "settings": { "accent_color": "Akcentní barva", "account": "Účet", - "account_deleted": "Your account has been deleted", + "account_deleted": "Váš účet byl smazán", "account_description": "Přizpůsobte si nastavení účtu.", - "account_email_description": "Vaše primární e -mailová adresa.", + "account_email_description": "Vaše primární e-mailová adresa.", "account_name_description": "Toto je vaše zobrazované jméno.", - "additional": "Additional Settings", + "additional": "Další nastavení", + "agent_not_running": "Hoppscotch Agent nebyl zjištěn. Zkontrolujte, zda je Agent spuštěn.", + "agent_not_running_short": "Zkontrolujte stav agenta.", + "agent_running": "Hoppscotch Agent je aktivní.", + "agent_running_short": "Hoppscotch Agent je aktivní.", + "agent_discard_registration": "Zrušit registraci agenta", + "agent_registered": "Registrován agent", + "agent_registration_successful": "Agent byl úspěšně zaregistrován", + "agent_registration_fetch_failed": "Nepodařilo se načíst registrační informace agenta. Prosím znovu zaregistrujte Agenta.", + "agent_registration_already_in_progress": "Registrace agenta již probíhá. Dokončete nebo zrušte aktuální registraci a zkuste to znovu.", + "auto_encode_mode": "Auto", + "auto_encode_mode_tooltip": "Parametry v požadavku zakódujte pouze v případě, že jsou přítomny nějaké speciální znaky", "background": "Pozadí", "black_mode": "Černé", - "choose_language": "Vyber jazyk", + "choose_language": "Vyberte jazyk", "dark_mode": "Tmavé", - "delete_account": "Delete account", - "delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.", + "delete_account": "Smazat účet", + "delete_account_description": "Po smazání účtu budou všechna vaše data trvale odstraněna. Tuto akci nelze vrátit.", + "disable_encode_mode_tooltip": "Nikdy nekódujte parametry v požadavku", + "enable_encode_mode_tooltip": "Parametry v požadavku vždy zakódujte", + "enter_otp": "Zadejte kód agenta", "expand_navigation": "Rozšířit navigaci", "experiments": "Experimenty", "experiments_notice": "Soubor experimentů, na kterých pracujeme a které se mohou ukázat jako užitečné, zábavné, obojí nebo ani jedno z toho. Nejsou konečné a nemusí být stabilní, takže pokud se stane něco příliš divného, nepanikařte. Prostě tu nebezpečnou věc vypněte. Vtipy stranou,", "extension_ver_not_reported": "Nehlášeno", "extension_version": "Verze rozšíření", - "extensions": "Rozšíření", - "extensions_use_toggle": "K odeslání požadavků použijte rozšíření prohlížeče (je -li k dispozici)", - "follow": "Follow Us", + "extensions": "Rozšíření prohlížeče", + "extensions_use_toggle": "K odeslání požadavků použijte rozšíření prohlížeče (je-li k dispozici)", + "follow": "Sledujte nás", + "general": "Obecné", + "general_description": "Obecná nastavení použitá v aplikaci", "interceptor": "Interceptor", "interceptor_description": "Middleware mezi aplikací a API.", + "kernel_interceptor": "Interceptor", + "kernel_interceptor_description": "Middleware mezi aplikací a API.", "language": "Jazyk", "light_mode": "Světlé", - "official_proxy_hosting": "Oficiální proxy je hostitelem Hoppscotch.", - "profile": "Profile", - "profile_description": "Update your profile details", - "profile_email": "Email address", - "profile_name": "Profile name", + "official_proxy_hosting": "Oficiální proxy hostuje Hoppscotch.", + "query_parameters_encoding": "Kódování query parametrů", + "query_parameters_encoding_description": "Nastavte kódování query parametrů v požadavcích", + "profile": "Profil", + "profile_description": "Aktualizovat údaje profilu", + "profile_email": "E-mailová adresa", + "profile_name": "Název profilu", + "profile_photo": "Profilový obrázek", "proxy": "Proxy", "proxy_url": "Proxy URL", - "proxy_use_toggle": "K odesílání žádostí použijte middleware proxy", - "read_the": "Číst", - "reset_default": "Obnovit do základního nastavení", - "short_codes": "Short codes", - "short_codes_description": "Short codes which were created by you.", + "proxy_use_toggle": "Použít proxy middleware pro odesílání požadavků", + "read_the": "Přečíst", + "register_agent": "Registrovat agenta", + "reset_default": "Použít výchozí proxy", + "short_codes": "Zkratkové kódy", + "short_codes_description": "Zkratkové kódy, které jste vytvořili.", "sidebar_on_left": "Postranní lišta vlevo", + "ai_experiments": "AI experimenty", + "ai_request_naming_style": "Styl pojmenování požadavků", + "ai_request_naming_style_descriptive_with_spaces": "Popisný s mezerami", + "ai_request_naming_style_camel_case": "Camel Case ( camelCase )", + "ai_request_naming_style_snake_case": "Snake Case ( snake_case )", + "ai_request_naming_style_pascal_case": "Pascal Case ( PascalCase )", + "ai_request_naming_style_custom": "Vlastní", + "ai_request_naming_style_custom_placeholder": "Zadejte vlastní šablonu stylu pojmenování...", + "experimental_scripting_sandbox": "Experimentální skriptovací sandbox", + "enable_experimental_mock_servers": "Povolit mock servery", + "enable_experimental_documentation": "Povolit dokumentaci", "sync": "Synchronizovat", - "sync_collections": "Sbírky", + "sync_collections": "Kolekce", "sync_description": "Tato nastavení jsou synchronizována s cloudem.", "sync_environments": "Prostředí", "sync_history": "Historie", - "system_mode": "Systémové", + "history_disabled": "Historie je zakázána. Pro její povolení kontaktujte správce organizace.", + "system_mode": "Systémový", "telemetry": "Telemetrie", - "telemetry_helps_us": "Telemetrie nám pomáhá personalizovat naše operace a poskytovat vám to nejlepší prostředí.", + "telemetry_helps_us": "Telemetrie nám pomáhá zlepšovat naše služby a poskytovat vám co nejlepší prostředí.", "theme": "Motiv vzhledu", "theme_description": "Přizpůsobte si motiv vzhledu aplikace.", "use_experimental_url_bar": "Použijte experimentální lištu URL se zvýrazněním prostředí", "user": "Uživatel", - "verified_email": "Verified email", - "verify_email": "Verify email" + "verified_email": "Ověřený e-mail", + "verify_email": "Ověřit e-mail", + "validate_certificates": "Ověřit SSL/TLS certifikáty", + "verify_host": "Ověřit hostitele", + "verify_peer": "Ověřit peer", + "follow_redirects": "Sledovat přesměrování", + "client_certificates": "Klientské certifikáty", + "certificate_settings": "Nastavení certifikátu", + "certificate": "Certifikát", + "key": "Soukromý klíč", + "pfx_or_p12": "PFX/PKCS#12", + "password": "Heslo", + "select_file": "Vyberte soubor", + "domain": "Doména", + "add_certificate": "Přidat certifikát", + "add_cert_file": "Přidat soubor certifikátu", + "add_key_file": "Přidat soubor klíče", + "add_pfx_file": "Přidat soubor PFX", + "global_defaults": "Globální výchozí nastavení", + "add_domain_override": "Přidat přepsání domény", + "domain_override": "Přepsání domény", + "manage_domains_overrides": "Správa přepsání domén", + "add_domain": "Přidat doménu", + "remove_domain": "Odebrat doménu", + "ca_certificate": "Certifikát CA", + "ca_certificates": "Certifikáty CA", + "ca_certificates_support": "Hoppscotch podporuje soubory .crt, .cer nebo .pem obsahující jeden či více certifikátů.", + "proxy_capabilities": "Hoppscotch Agent a Desktop App podporují HTTP/HTTPS/SOCKS proxy s podporou NTLM a Basic Auth.", + "proxy_auth": "Do pole URL můžete také zahrnout uživatelské jméno a heslo." }, "shared_requests": { - "button": "Button", - "button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.", - "copy_html": "Copy HTML", - "copy_link": "Copy Link", - "copy_markdown": "Copy Markdown", - "creating_widget": "Creating widget", - "customize": "Customize", - "deleted": "Shared request deleted", - "description": "Select a widget, you can change and customize this later", - "embed": "Embed", - "embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.", - "link": "Link", - "link_info": "Create a shareable link to share with anyone on the internet with view access.", - "modified": "Shared request modified", - "not_found": "Shared request not found", - "open_new_tab": "Open in new tab", - "preview": "Preview", - "run_in_hoppscotch": "Run in Hoppscotch", + "button": "Tlačítko", + "button_info": "Vytvořte tlačítko „Spustit v Hoppscotch“ pro svůj web, blog nebo README.", + "copy_html": "Kopírovat HTML", + "copy_link": "Kopírovat odkaz", + "copy_markdown": "Kopírovat Markdown", + "creating_widget": "Vytváření widgetu", + "customize": "Přizpůsobit", + "deleted": "Sdílený požadavek byl smazán", + "description": "Vyberte widget; později jej můžete změnit a přizpůsobit.", + "embed": "Vložit", + "embed_info": "Přidejte mini „Hoppscotch API Playground“ na web, blog nebo do dokumentace.", + "link": "Odkaz", + "link_info": "Vytvořte sdílitelný odkaz s přístupem pro zobrazení.", + "modified": "Sdílený požadavek byl upraven", + "not_found": "Sdílený požadavek nebyl nalezen", + "open_new_tab": "Otevřít na nové kartě", + "preview": "Náhled", + "run_in_hoppscotch": "Spustit v Hoppscotch", "theme": { - "dark": "Dark", - "light": "Light", - "system": "System", - "title": "Theme" - } + "dark": "Tmavý", + "light": "Světlý", + "system": "Systém", + "title": "Motiv" + }, + "action": "Akce", + "clear_filter": "Vymazat filtr", + "confirm_request_deletion": "Potvrdit smazání vybraného sdíleného požadavku?", + "copy": "Kopie", + "created_on": "Vytvořeno dne", + "delete": "Vymazat", + "email": "E-mail", + "filter": "Filtr", + "filter_by_email": "Filtrujte podle e-mailu", + "id": "ID", + "load_list_error": "Nelze načíst seznam sdílených požadavků", + "no_requests": "Nebyly nalezeny žádné sdílené požadavky", + "open_request": "Otevřete požadavek", + "properties": "Vlastnosti", + "request": "Požadavek", + "show_more": "Zobrazit více", + "title": "Sdílené požadavky", + "url": "URL" }, "shortcut": { "general": { "close_current_menu": "Zavřít aktuální nabídku", - "command_menu": "Nabídka Hledat a příkazy", + "command_menu": "Nabídka Vyhledávání a příkazy", "help_menu": "Nabídka nápovědy", "show_all": "Klávesové zkratky", - "title": "Všeobecné" + "title": "Všeobecné", + "comment_uncomment": "Komentář/Odkomentování", + "close_tab": "Zavřít kartu", + "undo": "Vrátit zpět", + "redo": "Předělat" }, "miscellaneous": { "invite": "Pozvěte lidi na Hoppscotch", - "title": "Smíšený" + "title": "Různé" }, "navigation": { - "back": "Vraťte se na předchozí stránku", + "back": "Přejít na předchozí stránku", "documentation": "Přejděte na stránku dokumentace", "forward": "Přejít na další stránku", "graphql": "Přejděte na stránku GraphQL", - "profile": "Go to Profile page", + "profile": "Přejděte na stránku profilu", "realtime": "Přejít na stránku v reálném čase", "rest": "Přejděte na stránku REST", "settings": "Přejděte na stránku Nastavení", "title": "Navigace" }, "others": { - "prettify": "Prettify Editor's Content", - "title": "Others" + "prettify": "Prettify obsah editoru", + "title": "Ostatní" }, "request": { - "delete_method": "Vyberte metodu ODSTRANIT", - "get_method": "Vyberte metodu ZÍSKAT", + "delete_method": "Vyberte metodu DELETE", + "get_method": "Vyberte metodu GET", "head_method": "Vyberte metodu HEAD", "import_curl": "Import cURL", "method": "Metoda", - "next_method": "Vyberte Další metoda", + "next_method": "Vyberte další metodu", "post_method": "Vyberte metodu POST", "previous_method": "Vyberte předchozí metodu", "put_method": "Vyberte metodu PUT", "rename": "Přejmenovat požadavek", "reset_request": "Resetovat požadavek", "save_request": "Uložit požadavek", - "save_to_collections": "Uložit do sbírek", - "send_request": "Poslat požadavek", + "save_to_collections": "Uložit do kolekcí", + "send_request": "Odeslat požadavek", "share_request": "Sdílet požadavek", - "show_code": "Generate code snippet", - "title": "Žádost", - "copy_request_link": "Kopírovat požadavek na odkaz" + "show_code": "Vygenerovat úryvek kódu", + "title": "Požadavek", + "focus_url": "Zaostřete na pruh URL" }, "response": { - "copy": "Copy response to clipboard", - "download": "Download response as file", - "title": "Response" + "copy": "Zkopírovat odpověď do schránky", + "download": "Stáhnout odpověď jako soubor", + "title": "Odpověď" + }, + "tabs": { + "title": "Karty", + "new_tab": "Nová karta", + "close_tab": "Zavřít kartu", + "reopen_tab": "Znovu otevřít zavřenou kartu", + "next_tab": "Další karta", + "previous_tab": "Předchozí karta", + "first_tab": "Přepněte na první kartu", + "last_tab": "Přepnout na poslední kartu", + "mru_switch": "Přepnout na poslední kartu (MRU)", + "mru_switch_reverse": "Přepnout na předchozí poslední kartu (MRU)" }, "theme": { - "black": "Switch theme to black mode", - "dark": "Switch theme to dark mode", - "light": "Switch theme to light mode", - "system": "Switch theme to system mode", - "title": "Theme" + "black": "Přepnout motiv na černý režim", + "dark": "Přepnout motiv na tmavý režim", + "light": "Přepnout motiv na světlý režim", + "system": "Přepnout motiv na systémový režim", + "title": "Motiv" } }, "show": { "code": "Zobrazit kód", - "collection": "Expand Collection Panel", + "collection": "Rozbalit panel kolekcí", "more": "Zobrazit více", - "sidebar": "Zobrazit postranní lištu" + "sidebar": "Zobrazit postranní lištu", + "password": "Zobrazit heslo" }, "socketio": { - "communication": "Sdělení", - "connection_not_authorized": "This SocketIO connection does not use any authentication.", + "communication": "Komunikace", + "connection_not_authorized": "Toto Socket.IO připojení nepoužívá žádné ověření.", "event_name": "Název události", "events": "Události", "log": "Záznam", "url": "URL" }, "spotlight": { - "change_language": "Change Language", + "change_language": "Změnit jazyk", "environments": { - "delete": "Delete current environment", - "duplicate": "Duplicate current environment", - "duplicate_global": "Duplicate global environment", - "edit": "Edit current environment", - "edit_global": "Edit global environment", - "new": "Create new environment", - "new_variable": "Create a new environment variable", - "title": "Environments" + "delete": "Smazat aktuální prostředí", + "duplicate": "Duplikovat aktuální prostředí", + "duplicate_global": "Duplikovat globální prostředí", + "edit": "Upravit aktuální prostředí", + "edit_global": "Upravit globální prostředí", + "new": "Vytvořit nové prostředí", + "new_variable": "Vytvořit novou proměnnou prostředí", + "title": "Prostředí" }, "general": { - "chat": "Chat with support", - "help_menu": "Help and support", - "open_docs": "Read Documentation", - "open_github": "Open GitHub repository", - "open_keybindings": "Keyboard shortcuts", - "social": "Social", - "title": "General" + "chat": "Chatovat s podporou", + "help_menu": "Nápověda a podpora", + "open_docs": "Přečíst dokumentaci", + "open_github": "Otevřít repozitář GitHub", + "open_keybindings": "Klávesové zkratky", + "social": "Sociální sítě", + "title": "Obecné" }, "graphql": { - "connect": "Connect to server", - "disconnect": "Disconnect from server" + "connect": "Připojit k serveru", + "disconnect": "Odpojit od serveru" }, "miscellaneous": { - "invite": "Invite your friends to Hoppscotch", - "title": "Miscellaneous" + "invite": "Pozvěte přátele do Hoppscotch", + "title": "Různé" }, "request": { "save_as_new": "Uložit jako nový požadavek", - "select_method": "Select method", - "switch_to": "Switch to", - "tab_authorization": "Authorization tab", - "tab_body": "Body tab", - "tab_headers": "Headers tab", - "tab_parameters": "Parameters tab", - "tab_pre_request_script": "Pre-request script tab", - "tab_query": "Query tab", - "tab_tests": "Tests tab", - "tab_variables": "Variables tab" + "select_method": "Vybrat metodu", + "switch_to": "Přepnout na", + "tab_authorization": "Karta Autorizace", + "tab_body": "Karta Tělo", + "tab_headers": "Karta Hlavičky", + "tab_parameters": "Karta Parametry", + "tab_pre_request_script": "Karta Pre-request skriptu", + "tab_query": "Karta Query", + "tab_tests": "Karta Testy", + "tab_variables": "Karta Proměnné" }, "response": { - "copy": "Copy response", - "download": "Download response as file", - "title": "Response" + "copy": "Kopírovat odpověď", + "download": "Stáhnout odpověď jako soubor", + "title": "Odpověď" }, "section": { "interceptor": "Interceptor", - "interface": "Interface", - "theme": "Theme", - "user": "User" + "interface": "Rozhraní", + "theme": "Motiv", + "user": "Uživatel" }, "settings": { - "change_interceptor": "Change Interceptor", - "change_language": "Change Language", + "change_interceptor": "Změnit interceptor", + "change_language": "Změnit jazyk", "theme": { - "black": "Black", - "dark": "Dark", - "light": "Light", - "system": "System preference" + "black": "Černý", + "dark": "Tmavý", + "light": "Světlý", + "system": "Systémové nastavení" } }, "tab": { - "close_current": "Close current tab", - "close_others": "Close all other tabs", - "duplicate": "Duplicate current tab", - "new_tab": "Open a new tab", - "title": "Tabs" + "close_current": "Zavřít aktuální kartu", + "close_others": "Zavřít všechny ostatní karty", + "duplicate": "Duplikovat aktuální kartu", + "new_tab": "Otevřít novou kartu", + "next": "Přepnout na další kartu", + "previous": "Přepnout na předchozí kartu", + "switch_to_first": "Přepnout na první kartu", + "switch_to_last": "Přepnout na poslední kartu", + "mru_switch": "Přepnout na naposledy použitou kartu", + "mru_switch_reverse": "Přepnout na předchozí naposledy použitou kartu", + "title": "Karty" }, "workspace": { - "delete": "Delete current team", - "edit": "Edit current team", - "invite": "Invite people to team", - "new": "Create new team", - "switch_to_personal": "Switch to your personal workspace", - "title": "Teams" + "delete": "Smazat aktuální pracovní prostor", + "edit": "Upravit aktuální pracovní prostor", + "invite": "Pozvat lidi do pracovního prostoru", + "new": "Vytvořit nový pracovní prostor", + "switch_to_personal": "Přepnout na osobní pracovní prostor", + "title": "Pracovní prostory" }, "phrases": { - "try": "Try", - "import_collections": "Import collections", - "create_environment": "Create environment", - "create_workspace": "Create workspace", + "try": "Zkuste", + "import_collections": "Importovat kolekce", + "create_environment": "Vytvořit prostředí", + "create_workspace": "Vytvořit pracovní prostor", "share_request": "Sdílet požadavek" } }, @@ -890,162 +1575,292 @@ }, "state": { "bulk_mode": "Hromadná úprava", - "bulk_mode_placeholder": "Záznamy jsou odděleny novým řádkem\nKlíče a hodnoty jsou odděleny:\nPředřadte # do libovolného řádku, který chcete přidat, ale ponechte jej deaktivovaný", + "bulk_mode_placeholder": "Záznamy jsou odděleny novým řádkem\nKlíče a hodnoty jsou odděleny:\nPředřaďte # do libovolného řádku, který chcete přidat, ale ponechte jej deaktivovaný", "cleared": "Vymazáno", "connected": "Připojeno", "connected_to": "Připojeno k {name}", - "connecting_to": "Připojování k {name} ...", - "connection_error": "Failed to connect", - "connection_failed": "Connection failed", - "connection_lost": "Connection lost", - "copied_interface_to_clipboard": "Copied {language} interface type to clipboard", + "connecting_to": "Připojování k {name}...", + "connection_error": "Nepodařilo se připojit", + "connection_failed": "Připojení selhalo", + "connection_lost": "Připojení ztraceno", + "copied_interface_to_clipboard": "Typ rozhraní {language} zkopírován do schránky", "copied_to_clipboard": "Zkopírováno do schránky", "deleted": "Smazáno", "deprecated": "ZASTARALÉ", "disabled": "Zakázáno", "disconnected": "Odpojeno", "disconnected_from": "Odpojeno od {name}", - "docs_generated": "Vygenerovaná dokumentace", - "download_failed": "Download failed", + "docs_generated": "Dokumentace vygenerována", + "download_failed": "Stahování selhalo", "download_started": "Stahování zahájeno", "enabled": "Povoleno", + "experimental": "Experimentální", "file_imported": "Soubor importován", "finished_in": "Hotovo za {duration} ms", - "hide": "Hide", + "hide": "Skrýt", "history_deleted": "Historie odstraněna", "linewrap": "Zalamovat řádky", "loading": "Načítání...", - "message_received": "Message: {message} arrived on topic: {topic}", - "mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}", + "message_received": "Zpráva: {message} dorazila na téma: {topic}", + "mqtt_subscription_failed": "Při odběru tématu {topic} se něco pokazilo", + "no_content_found": "Nebyl nalezen žádný obsah", "none": "Žádný", "nothing_found": "Nic nebylo nalezeno pro", - "published_error": "Something went wrong while publishing msg: {topic} to topic: {message}", - "published_message": "Published message: {message} to topic: {topic}", - "reconnection_error": "Failed to reconnect", - "show": "Show", - "subscribed_failed": "Failed to subscribe to topic: {topic}", - "subscribed_success": "Successfully subscribed to topic: {topic}", - "unsubscribed_failed": "Failed to unsubscribe from topic: {topic}", - "unsubscribed_success": "Successfully unsubscribed from topic: {topic}", - "waiting_send_request": "Čekání na odeslání požadavku" + "published_error": "Při publikování zprávy {message} do tématu {topic} se něco pokazilo", + "published_message": "Zpráva {message} byla publikována do tématu {topic}", + "reconnection_error": "Nepodařilo se znovu připojit", + "saved": "Uloženo", + "show": "Zobrazit", + "subscribed_failed": "Nepodařilo se přihlásit k odběru tématu: {topic}", + "subscribed_success": "Úspěšně přihlášeno k odběru tématu: {topic}", + "unsubscribed_failed": "Nepodařilo se odhlásit odběr tématu: {topic}", + "unsubscribed_success": "Úspěšně odhlášeno z odběru tématu: {topic}", + "waiting_send_request": "Čekání na odeslání požadavku", + "user_deactivated": "Váš účet je deaktivovaný. Pro opětovnou aktivaci kontaktujte správce.", + "add_user_failure": "Nepodařilo se přidat uživatele do pracovního prostoru.", + "add_user_success": "Uživatel byl přidán do pracovního prostoru.", + "admin_failure": "Nepodařilo se nastavit uživatele jako správce.", + "admin_success": "Uživatel je nyní správce.", + "and": "a", + "clear_selection": "Vymazat výběr", + "configure_auth": "Nastavte poskytovatele autentizace v nastavení správce nebo si projděte dokumentaci.", + "confirm_admin_to_user": "Chcete tomuto uživateli odebrat status správce?", + "confirm_admins_to_users": "Chcete vybraným uživatelům odebrat status správce?", + "confirm_delete_infra_token": "Opravdu chcete smazat infra token {tokenLabel}?", + "confirm_delete_invite": "Chcete vybranou pozvánku zrušit?", + "confirm_delete_invites": "Chcete zrušit vybrané pozvánky?", + "confirm_user_deletion": "Potvrdit smazání uživatele?", + "confirm_users_deletion": "Chcete smazat vybrané uživatele?", + "confirm_user_to_admin": "Chcete z tohoto uživatele udělat správce?", + "confirm_users_to_admin": "Chcete z vybraných uživatelů udělat správce?", + "confirm_user_deactivation": "Potvrdit deaktivaci uživatele?", + "confirm_logout": "Potvrdit odhlášení", + "created_on": "Vytvořeno dne", + "continue_email": "Pokračovat e-mailem", + "continue_github": "Pokračovat s GitHubem", + "continue_google": "Pokračovat s Googlem", + "continue_microsoft": "Pokračovat s Microsoftem", + "create_team_failure": "Nepodařilo se vytvořit pracovní prostor.", + "create_team_success": "Pracovní prostor byl úspěšně vytvořen.", + "data_sharing_failure": "Aktualizace nastavení sdílení dat se nezdařila", + "delete_infra_token_failure": "Při mazání infra tokenu se něco pokazilo", + "delete_invite_failure": "Nepodařilo se smazat pozvánku.", + "delete_invites_failure": "Nepodařilo se smazat vybrané pozvánky.", + "delete_invite_success": "Pozvánka byla úspěšně smazána.", + "delete_invites_success": "Vybrané pozvánky byly úspěšně smazány.", + "delete_request_failure": "Nepodařilo se smazat sdílený požadavek.", + "delete_request_success": "Sdílený požadavek byl úspěšně smazán.", + "delete_team_failure": "Nepodařilo se smazat pracovní prostor.", + "delete_team_success": "Pracovní prostor byl úspěšně smazán.", + "delete_some_users_failure": "Počet nesmazaných uživatelů: {count}", + "delete_some_users_success": "Počet smazaných uživatelů: {count}", + "delete_user_failed_only_one_admin": "Nepodařilo se smazat uživatele. Musí zůstat alespoň jeden správce.", + "delete_user_failure": "Nepodařilo se smazat uživatele.", + "delete_users_failure": "Nepodařilo se smazat vybrané uživatele.", + "delete_user_success": "Uživatel byl úspěšně smazán.", + "delete_users_success": "Vybraní uživatelé byli úspěšně smazáni.", + "email": "E-mail", + "email_failure": "Odeslání pozvánky se nezdařilo", + "email_signin_failure": "Nepodařilo se přihlásit pomocí e-mailu", + "email_success": "E-mailová pozvánka byla úspěšně odeslána", + "emails_cannot_be_same": "Nemůžete pozvat sami sebe. Zvolte jinou e-mailovou adresu.", + "enter_team_email": "Zadejte e-mail vlastníka pracovního prostoru.", + "error": "Něco se pokazilo", + "error_auth_providers": "Nelze načíst poskytovatele ověření", + "generate_infra_token_failure": "Při generování infra tokenu se něco pokazilo", + "github_signin_failure": "Nepodařilo se přihlásit přes GitHub", + "google_signin_failure": "Nepodařilo se přihlásit pomocí Google", + "infra_token_label_short": "Popisek infra tokenu je příliš krátký.", + "invalid_email": "Zadejte prosím platnou e-mailovou adresu", + "link_copied_to_clipboard": "Odkaz zkopírován do schránky", + "logged_out": "Odhlášen", + "login_as_admin": "a přihlaste se pomocí účtu správce.", + "login_using_email": "Požádejte uživatele, aby zkontroloval e-mail, nebo mu sdílejte odkaz níže.", + "login_using_link": "Požádejte uživatele, aby se přihlásil pomocí odkazu níže.", + "logout": "Odhlášení", + "magic_link_sign_in": "Pro přihlášení klikněte na odkaz.", + "magic_link_success": "Poslali jsme magic link na", + "microsoft_signin_failure": "Přihlášení k Microsoftu se nezdařilo", + "newsletter_failure": "Nastavení newsletteru nelze aktualizovat", + "non_admin_logged_in": "Přihlášen jako uživatel bez oprávnění správce.", + "non_admin_login": "Jste přihlášeni, ale nemáte roli správce.", + "owner_not_present": "V týmu musí být alespoň jeden vlastník.", + "privacy_policy": "Zásady ochrany osobních údajů", + "reenter_email": "Zadejte e-mail znovu", + "remove_admin_failure": "Nepodařilo se odebrat roli správce.", + "remove_admin_failure_only_one_admin": "Nepodařilo se odebrat roli správce. Musí zůstat alespoň jeden správce.", + "remove_admin_success": "Role správce byla odebrána.", + "remove_admin_from_users_failure": "Nepodařilo se odebrat roli správce vybraným uživatelům.", + "remove_admin_from_users_success": "Role správce byla vybraným uživatelům odebrána.", + "remove_admin_to_delete_user": "Před smazáním uživatele odeberte roli správce.", + "remove_owner_to_delete_user": "Před smazáním uživatele odeberte roli vlastníka týmu.", + "remove_owner_failure_only_one_owner": "Nepodařilo se odebrat člena. V týmu musí zůstat alespoň jeden vlastník.", + "remove_admin_for_deletion": "Před smazáním nejdříve odeberte roli správce.", + "remove_owner_for_deletion": "Jeden nebo více uživatelů je vlastníkem týmu. Před smazáním upravte vlastnictví.", + "remove_invitee_failure": "Nepodařilo se odebrat pozvaného uživatele.", + "remove_invitee_success": "Pozvaný uživatel byl úspěšně odebrán.", + "remove_member_failure": "Nepodařilo se odebrat člena.", + "remove_member_success": "Člen byl úspěšně odebrán.", + "rename_team_failure": "Nepodařilo se přejmenovat pracovní prostor.", + "rename_team_success": "Pracovní prostor byl úspěšně přejmenován.", + "rename_user_failure": "Nepodařilo se přejmenovat uživatele.", + "rename_user_success": "Uživatel byl úspěšně přejmenován.", + "require_auth_provider": "Pro přihlášení musíte nastavit alespoň jednoho poskytovatele ověření.", + "role_update_failed": "Aktualizace rolí se nezdařila.", + "role_update_success": "Role byly úspěšně aktualizovány.", + "selected": "Vybráno {count}", + "self_host_docs": "Dokumentace k self-hostingu", + "send_magic_link": "Odeslat magic link", + "setup_failure": "Nastavení se nezdařilo.", + "setup_success": "Nastavení bylo úspěšně dokončeno.", + "sign_in_agreement": "Přihlášením souhlasíte s naším", + "sign_in_options": "Všechny možnosti přihlášení", + "sign_out": "Odhlásit se", + "something_went_wrong": "Něco se pokazilo", + "team_name_too_short": "Název pracovního prostoru musí mít alespoň 6 znaků.", + "user_already_invited": "Nepodařilo se odeslat pozvánku. Uživatel už je pozván.", + "user_not_found": "Uživatel nebyl nalezen v infrastruktuře.", + "users_to_admin_success": "Vybraní uživatelé byli povýšeni na správce.", + "users_to_admin_failure": "Nepodařilo se povýšit vybrané uživatele na správce.", + "loading_workspaces": "Načítání pracovních prostorů", + "loading_collections_in_workspace": "Načítání kolekcí v pracovním prostoru" }, "support": { "changelog": "Přečtěte si více o nejnovějších verzích", - "chat": "Otázky? Popovídejte si s námi!", + "chat": "Máte otázky? Napište nám!", "community": "Ptejte se a pomáhejte ostatním", "documentation": "Přečtěte si více o aplikaci Hoppscotch", "forum": "Ptejte se a dostávejte odpovědi", - "github": "Sledujte nás na Githubu", + "github": "Sledujte nás na GitHubu", "shortcuts": "Procházejte aplikaci rychleji", "title": "Podpora", - "twitter": "Sledujte nás na Twitteru", - "team": "Spojte se s týmem" + "twitter": "Sledujte nás na Twitteru" }, "tab": { "authorization": "Autorizace", "body": "Tělo", "close": "Zavřít kartu", "close_others": "Zavřít ostatní karty", - "collections": "Sbírky", + "collections": "Kolekce", "documentation": "Dokumentace", "duplicate": "Duplikovat kartu", "environments": "Prostředí", - "headers": "Záhlaví", + "headers": "Hlavičky", "history": "Historie", "mqtt": "MQTT", "parameters": "Parametry", - "pre_request_script": "Předběžný skript", + "post_request_script": "Post-request skript", + "pre_request_script": "Pre-request skript", "queries": "Dotazy", - "query": "Dotaz", + "query": "Query", "schema": "Schéma", "shared_requests": "Sdílené požadavky", - "codegen": "Generate Code", - "code_snippet": "Code snippet", + "codegen": "Generovat kód", + "code_snippet": "Úryvek kódu", + "mock_servers": "Mock servery", "share_tab_request": "Sdílet kartu s požadavkem", "socketio": "Socket.IO", "sse": "SSE", - "tests": "Testy", "types": "Typy", "variables": "Proměnné", - "websocket": "WebSocket" + "websocket": "WebSocket", + "all_tests": "Všechny testy", + "passed": "Úspěšné", + "failed": "Neúspěšné" }, "team": { - "already_member": "You are already a member of this team. Contact your team owner.", - "create_new": "Vytvořte nový tým", - "deleted": "Tým smazán", - "edit": "Upravit tým", - "email": "E-mailem", - "email_do_not_match": "Email doesn't match with your account details. Contact your team owner.", - "exit": "Ukončete tým", - "exit_disabled": "Pouze vlastník nemůže opustit tým", - "failed_invites": "Failed invites", - "invalid_coll_id": "Invalid collection ID", - "invalid_email_format": "Formát e -mailu je neplatný", - "invalid_id": "Invalid team ID. Contact your team owner.", - "invalid_invite_link": "Invalid invite link", - "invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.", - "invalid_member_permission": "Poskytněte prosím platné oprávnění členovi týmu", - "invite": "Invite", - "invite_more": "Invite more", - "invite_tooltip": "Invite people to this workspace", - "invited_to_team": "{owner} invited you to join {team}", - "join": "Invitation accepted", - "join_team": "Join {team}", - "joined_team": "You have joined {team}", - "joined_team_description": "You are now a member of this team", - "left": "Opustil jsi tým", - "login_to_continue": "Login to continue", - "login_to_continue_description": "You need to be logged in to join a team.", - "logout_and_try_again": "Logout and sign in with another account", - "member_has_invite": "This email ID already has an invite. Contact your team owner.", - "member_not_found": "Member not found. Contact your team owner.", - "member_removed": "Uživatel odstraněn", + "activity_logs": "Protokoly aktivit", + "already_member": "Tento e-mail je už přiřazen k existujícímu uživateli.", + "create_new": "Vytvořit nový pracovní prostor", + "deleted": "Pracovní prostor smazán", + "delete_all_activity_logs": "Smazat všechny protokoly aktivit", + "successfully_deleted_all_activity_logs": "Všechny protokoly aktivit byly úspěšně smazány", + "delete_activity_log": "Smazat protokol aktivit", + "deleted_activity_log": "Vybraný protokol aktivit byl smazán", + "deleted_all_activity_logs": "Smazány všechny protokoly aktivit", + "edit": "Upravit pracovní prostor", + "email": "E-mail", + "email_do_not_match": "E-mail se neshoduje s údaji vašeho účtu. Kontaktujte vlastníka pracovního prostoru.", + "exit": "Opustit pracovní prostor", + "exit_disabled": "Pracovní prostor nemůže opustit jen vlastník", + "failed_invites": "Neúspěšné pozvánky", + "invalid_coll_id": "Neplatné ID kolekce", + "invalid_email_format": "Formát e-mailu je neplatný", + "invalid_id": "Neplatné ID pracovního prostoru. Kontaktujte vlastníka pracovního prostoru.", + "invalid_invite_link": "Neplatný odkaz na pozvánku", + "invalid_invite_link_description": "Odkaz, který jste použili, je neplatný. Kontaktujte vlastníka pracovního prostoru.", + "invalid_member_permission": "Zadejte prosím platné oprávnění pro člena pracovního prostoru", + "invite": "Pozvat", + "invite_more": "Pozvat další", + "invite_tooltip": "Pozvat lidi do tohoto pracovního prostoru", + "invited_to_team": "{owner} vás pozval do pracovního prostoru {workspace}", + "join": "Pozvánka přijata", + "join_team": "Připojit se k {workspace}", + "joined_team": "Připojili jste se k {workspace}", + "joined_team_description": "Nyní jste členem tohoto pracovního prostoru", + "left": "Opustili jste pracovní prostor", + "login_to_continue": "Přihlaste se pro pokračování", + "login_to_continue_description": "Pro připojení k pracovnímu prostoru musíte být přihlášeni.", + "logout_and_try_again": "Odhlaste se a přihlaste se jiným účtem", + "member_has_invite": "Uživatel už má pozvánku. Požádejte ho, ať zkontroluje doručenou poštu, nebo pozvánku odvolejte a odešlete znovu.", + "member_not_found": "Člen nebyl nalezen. Kontaktujte vlastníka pracovního prostoru.", + "member_removed": "Uživatel byl odebrán", "member_role_updated": "Role uživatelů aktualizovány", "members": "Členové", - "more_members": "+{count} more", - "name_length_insufficient": "Název týmu by měl mít alespoň 6 znaků", - "name_updated": "Team name updated", - "new": "Nový tým", - "new_created": "Vytvořen nový tým", - "new_name": "Můj nový tým", - "no_access": "K těmto kolekcím nemáte přístup k úpravám", - "no_invite_found": "Invitation not found. Contact your team owner.", - "no_request_found": "Request not found.", - "not_found": "Team not found. Contact your team owner.", - "not_valid_viewer": "You are not a valid viewer. Contact your team owner.", - "parent_coll_move": "Cannot move collection to a child collection", - "pending_invites": "Pending invites", + "more_members": "+{count} dalších", + "name_length_insufficient": "Název pracovního prostoru nesmí být prázdný", + "name_updated": "Název pracovního prostoru aktualizován", + "new": "Nový pracovní prostor", + "new_created": "Vytvořen nový pracovní prostor", + "new_name": "Můj nový pracovní prostor", + "no_access": "K tomuto pracovnímu prostoru nemáte oprávnění k úpravám", + "no_invite_found": "Pozvánka nenalezena. Kontaktujte vlastníka pracovního prostoru.", + "no_request_found": "Požadavek nenalezen.", + "not_found": "Pracovní prostor nebyl nalezen. Kontaktujte vlastníka pracovního prostoru.", + "not_valid_viewer": "Nejste platný čtenář. Kontaktujte vlastníka pracovního prostoru.", + "parent_coll_move": "Nelze přesunout kolekci do podřízené kolekce", + "pending_invites": "Čekající pozvánky", "permissions": "Oprávnění", - "same_target_destination": "Same target and destination", - "saved": "Tým uložen", - "select_a_team": "Select a team", - "success_invites": "Success invites", - "title": "Týmy", - "we_sent_invite_link": "We sent an invite link to all invitees!", - "invite_sent_smtp_disabled": "Invite links generated", - "we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team.", - "invite_sent_smtp_disabled_description": "Sending invite emails is disabled for this instance of Hoppscotch. Please use the Copy link button to copy and share the invite link manually.", - "copy_invite_link": "Copy Invite Link", - "search_title": "Team Requests", - "join_beta": "Připojte se k beta programu a získejte přístup k týmům." + "same_target_destination": "Stejný zdroj i cíl", + "saved": "Pracovní prostor uložen", + "select_a_team": "Vyberte pracovní prostor", + "success_invites": "Úspěšné pozvánky", + "title": "Pracovní prostory", + "we_sent_invite_link": "Pozvánky jsou na cestě", + "invite_sent_smtp_disabled": "Odkazy na pozvánky byly vygenerovány", + "we_sent_invite_link_description": "Nově pozvaní uživatelé obdrží odkaz pro připojení k pracovnímu prostoru. Stávající členové a čekající pozvaní nový odkaz nedostanou.", + "invite_sent_smtp_disabled_description": "Odesílání e-mailů s pozvánkami je v této instanci Hoppscotch zakázáno. Použijte tlačítko Kopírovat odkaz a sdílejte pozvánku ručně.", + "copy_invite_link": "Kopírovat odkaz na pozvánku", + "search_title": "Požadavky týmu", + "user_not_found": "Uživatel nebyl v instanci nalezen.", + "invite_members": "Pozvat členy" }, "team_environment": { - "deleted": "Environment Deleted", - "duplicate": "Environment Duplicated", - "not_found": "Environment not found." + "deleted": "Prostředí odstraněno", + "duplicate": "Prostředí duplikováno", + "not_found": "Prostředí nenalezeno." }, "test": { - "failed": "test failed", + "requests": "Požadavky", + "selection": "Výběr", + "failed": "Test selhal", "javascript_code": "JavaScriptový kód", "learn": "Přečtěte si dokumentaci", - "passed": "test passed", - "report": "Protokol o zkoušce", + "passed": "Test prošel", + "report": "Test report", "results": "Výsledky testů", "script": "Skript", - "snippets": "Úryvky" + "snippets": "Úryvky", + "run": "Běh", + "run_again": "Spusťte znovu", + "stop": "Zastavit", + "new_run": "Nový běh", + "iterations": "Iterace", + "duration": "Trvání", + "avg_resp": "Prům. Doba odezvy" }, "websocket": { - "communication": "Sdělení", + "communication": "Komunikace", "log": "Záznam", "message": "Zpráva", "protocols": "Protokoly", @@ -1053,58 +1868,432 @@ }, "workspace": { "change": "Změnit pracovní prostor", - "personal": "Můj pracovní prostor", + "personal": "Osobní pracovní prostor", "other_workspaces": "Moje pracovní prostory", - "team": "Team Workspace", - "title": "Workspaces" + "team": "Pracovní prostor", + "title": "Pracovní prostory" }, "site_protection": { - "login_to_continue": "Login to continue", - "login_to_continue_description": "You need to be logged in to access this Hoppscotch Enterprise Instance.", - "error_fetching_site_protection_status": "Something Went Wrong While Fetching Site Protection Status" + "login_to_continue": "Přihlaste se pro pokračování", + "login_to_continue_description": "Pro přístup k této Hoppscotch Enterprise instanci musíte být přihlášeni.", + "error_fetching_site_protection_status": "Při načítání stavu ochrany webu se něco pokazilo" }, "access_tokens": { - "tab_title": "Tokens", - "section_title": "Personal Access Tokens", - "section_description": "Personal access tokens currently helps you connect the CLI to your Hoppscotch account", - "last_used_on": "Last used on", - "expires_on": "Expires on", - "no_expiration": "No expiration", - "expired": "Expired", - "copy_token_warning": "Make sure to copy your personal access token now. You won't be able to see it again!", - "token_purpose": "What's this token for?", - "expiration_label": "Expiration", - "scope_label": "Scope", - "workspace_read_only_access": "Read-only access to workspace data.", - "personal_workspace_access_limitation": "Personal Access Tokens can't access your personal workspace.", - "generate_token": "Generate Token", - "invalid_label": "Please provide a label for the token", - "no_expiration_verbose": "This token will never expire!", - "token_expires_on": "This token will expire on", - "generate_new_token": "Generate new token", - "generate_modal_title": "New Personal Access Token", - "deletion_success": "The access token {label} has been deleted" + "tab_title": "Tokeny", + "section_title": "Osobní přístupové tokeny", + "section_description": "Osobní přístupové tokeny vám zatím umožňují připojit CLI k vašemu účtu Hoppscotch", + "last_used_on": "Naposledy použito", + "expires_on": "Platí do", + "no_expiration": "Bez expirace", + "expired": "Expirovaný", + "copy_token_warning": "Ujistěte se, že si osobní přístupový token nyní zkopírujete. Později ho už neuvidíte!", + "token_purpose": "K čemu je tento token?", + "expiration_label": "Expirace", + "scope_label": "Rozsah", + "workspace_read_only_access": "Přístup pouze pro čtení k datům pracovního prostoru.", + "personal_workspace_access_limitation": "Osobní přístupové tokeny nemohou přistupovat k vašemu osobnímu pracovnímu prostoru.", + "generate_token": "Vygenerovat token", + "invalid_label": "Zadejte prosím popisek tokenu", + "no_expiration_verbose": "Tento token nikdy nevyprší!", + "token_expires_on": "Tento token vyprší", + "generate_new_token": "Vygenerovat nový token", + "generate_modal_title": "Nový osobní přístupový token", + "deletion_success": "Přístupový token {label} byl smazán" }, "collection_runner": { - "collection_id": "Collection ID", - "environment_id": "Environment ID", - "cli_collection_id_description": "This collection ID will be used by the CLI collection runner for Hoppscotch.", - "cli_environment_id_description": "This environment ID will be used by the CLI collection runner for Hoppscotch.", - "include_active_environment": "Include active environment:", + "collection_id": "ID kolekce", + "environment_id": "ID prostředí", + "cli_collection_id_description": "Toto ID kolekce se použije v CLI Collection Runneru Hoppscotch.", + "cli_environment_id_description": "Toto ID prostředí se použije v CLI Collection Runneru Hoppscotch.", + "include_active_environment": "Zahrnout aktivní prostředí:", "cli": "CLI", - "ui": "Runner (coming soon)", - "cli_command_generation_description_cloud": "Copy the below command and run it from the CLI. Please specify a personal access token.", - "cli_command_generation_description_sh": "Copy the below command and run it from the CLI. Please specify a personal access token and verify the generated SH instance server URL.", - "cli_command_generation_description_sh_with_server_url_placeholder": "Copy the below command and run it from the CLI. Please specify a personal access token and the SH instance server URL.", - "run_collection": "Spustit sbírku" - }, - "shortcodes": { - "actions": "Actions", - "created_on": "Created on", - "deleted": "Shortcode deleted", - "method": "Method", - "not_found": "Shortcode not found", - "short_code": "Short code", - "url": "URL" + "cli_comming_soon_for_personal_collection": "Collection Runner pro osobní kolekce v CLI bude brzy k dispozici.", + "delay": "Zpoždění", + "negative_delay": "Zpoždění nemůže být záporné", + "ui": "Runner", + "running_collection": "Běží kolekce", + "run_config": "Konfigurace běhu", + "advanced_settings": "Pokročilá nastavení", + "stop_on_error": "Zastavit běh při chybě", + "persist_responses": "Uchovávat odpovědi", + "keep_variable_values": "Zachovat hodnoty proměnných", + "collection_not_found": "Kolekce nebyla nalezena. Mohla být smazána nebo přesunuta.", + "empty_collection": "Kolekce je prázdná. Přidejte požadavky ke spuštění.", + "no_response_persist": "Collection Runner je aktuálně nastavený tak, aby neuchovával odpovědi. Proto se nezobrazují data odpovědi. Pro změnu spusťte novou konfiguraci běhu.", + "select_request": "Vyberte požadavek pro zobrazení odpovědi a výsledků testů", + "response_body_lost_rerun": "Tělo odpovědi není k dispozici. Spusťte kolekci znovu.", + "cli_command_generation_description_cloud": "Zkopírujte níže uvedený příkaz a spusťte jej v CLI. Uveďte prosím osobní přístupový token.", + "cli_command_generation_description_sh": "Zkopírujte níže uvedený příkaz a spusťte jej v CLI. Uveďte prosím osobní přístupový token a ověřte vygenerovanou URL serveru instance SH.", + "cli_command_generation_description_sh_with_server_url_placeholder": "Zkopírujte níže uvedený příkaz a spusťte jej v CLI. Uveďte prosím osobní přístupový token a URL serveru instance SH.", + "run_collection": "Spustit kolekci", + "no_passed_tests": "Žádné testy neprošly", + "no_failed_tests": "Žádné testy se nezdařily" + }, + "ai_experiments": { + "generate_request_name": "Generování názvu požadavku pomocí AI", + "generate_or_modify_request_body": "Vygenerovat nebo upravit tělo požadavku", + "modify_with_ai": "Upravte pomocí AI", + "generate": "Generovat", + "generate_or_modify_request_body_input_placeholder": "Zadejte výzvu k úpravě těla požadavku", + "accept_change": "Přijmout změnu", + "feedback_success": "Zpětná vazba byla úspěšně odeslána", + "feedback_failure": "Zpětnou vazbu se nepodařilo odeslat", + "feedback_thank_you": "Děkujeme za vaši zpětnou vazbu!", + "feedback_cta_text_long": "Hodnotit generaci, pomáhá nám se zlepšovat", + "feedback_cta_request_name": "Líbilo se vám vygenerované jméno?", + "modify_request_body_error": "Nepodařilo se upravit tělo požadavku", + "generate_or_modify_prerequest_input_placeholder": "Zadejte výzvu ke generování nebo úpravě skriptu pre-request", + "generate_or_modify_post_request_script_input_placeholder": "Zadejte výzvu ke generování nebo úpravě skriptu post-request", + "modify_post_request_script_error": "Nepodařilo se upravit skript post-request", + "modify_prerequest_error": "Nepodařilo se upravit skript pre-request" + }, + "configs": { + "auth_providers": { + "callback_url": "CALLBACK URL", + "client_id": "CLIENT ID", + "client_secret": "CLIENT SECRET", + "description": "Nakonfigurujte poskytovatele autentizace pro váš server", + "provider_not_specified": "Povolte alespoň jednoho poskytovatele autentizace", + "scope": "SCOPE", + "tenant": "TENANT", + "title": "Poskytovatelé autentizace", + "update_failure": "Aktualizace konfigurace poskytovatelů autentizace se nezdařila." + }, + "confirm_changes": "Hoppscotch server se musí restartovat, aby se projevily nové změny. Potvrdit změny provedené v konfiguraci serveru?", + "input_empty": "Před aktualizací konfigurací vyplňte všechna pole", + "data_sharing": { + "title": "Sdílení dat", + "description": "Pomozte vylepšit Hoppscotch sdílením anonymních dat", + "enable": "Povolit sdílení dat", + "secondary_title": "Konfigurace sdílení dat", + "see_shared": "Podívejte se, co je sdíleno", + "toggle_description": "Sdílejte anonymní data", + "update_failure": "Aktualizace konfigurace sdílení dat se nezdařila." + }, + "load_error": "Nelze načíst konfigurace serveru", + "mail_configs": { + "address_from": "MAILER FROM ADDRESS", + "custom_smtp_configs": "Použít vlastní SMTP konfiguraci", + "description": "Nakonfigurujte SMTP", + "enable_email_auth": "Povolit ověřování založené na e-mailu", + "enable_smtp": "Povolit SMTP", + "host": "MAILER HOST", + "password": "MAILER PASSWORD", + "port": "MAILER PORT", + "secure": "MAILER SECURE", + "smtp_url": "MAILER SMTP URL", + "tls_reject_unauthorized": "TLS REJECT UNAUTHORIZED", + "title": "Konfigurace SMTP", + "toggle_failure": "Nepodařilo se přepnout SMTP.", + "update_failure": "Aktualizace SMTP konfigurace se nezdařila.", + "user": "MAILER USER" + }, + "reset": { + "confirm_reset": "Hoppscotch server se musí restartovat, aby se projevily nové změny. Potvrdit resetování konfigurace serveru?", + "description": "Výchozí konfigurace budou načteny, jak je uvedeno v souboru prostředí", + "failure": "Reset konfigurace se nezdařil.", + "title": "Obnovit konfigurace", + "info": "Resetovat konfigurace serveru" + }, + "restart": { + "description": "Do automatického opětovného načtení této stránky zbývá {duration} sekund", + "initiate": "Probíhá restartování serveru...", + "title": "Server se restartuje" + }, + "save_changes": "Uložit změny", + "title": "Konfigurace", + "update_failure": "Aktualizace konfigurace serveru se nezdařila", + "restrict_access": "Omezit přístup", + "site_protection": { + "control_access": "Určete, kdo má přístup k aplikaci Hoppscotch", + "description": "Přizpůsobte si způsob přístupu návštěvníků k vaší aplikaci Hoppscotch pomocí nastavení ochrany webu.", + "enable": "Povolit ochranu webu", + "note": "Uživatelům, kteří navštíví aplikaci Hoppscotch, se zobrazí přihlašovací stránka, pro přístup k aplikaci je vyžadováno povinné přihlášení. Pro autorizaci je stále vyžadován souhlas administrátora", + "update_failure": "Aktualizace konfigurace ochrany webu se nezdařila." + }, + "domain_whitelisting": { + "add_domain": "Přidat novou doménu", + "description": "Uživatelé s e-mailovým ID registrovaným v doménách na seznamu povolených nevyžadují výslovný souhlas správce pro přístup k aplikaci Hoppscotch", + "enable": "Povolit přidávání domén na seznam povolených", + "enter_domain": "Zadejte doménu", + "title": "Domény na seznamu povolených", + "toggle_failure": "Nepodařilo se přepnout na seznam povolených domén", + "update_failure": "Aktualizace konfigurace whitelistu domén se nezdařila." + }, + "oidc_configs": { + "auth_url": "Auth URL", + "callback_url": "Zpětné volání URL", + "client_id": "Client ID", + "client_secret": "Client Secret", + "description": "Nakonfigurujte OIDC", + "enable": "Povolit ověřování založené na OIDC", + "issuer": "Vydavatel", + "provider_name": "Jméno poskytovatele", + "scope": "Rozsah", + "title": "Konfigurace OIDC", + "token_url": "Token URL", + "update_failure": "Aktualizace OIDC konfigurace se nezdařila.", + "user_info_url": "Informace o uživateli URL" + }, + "saml": { + "audience": "Audience", + "callback_url": "Zpětné volání URL", + "certificate": "Certifikát", + "description": "Nakonfigurujte SAML", + "enable": "Povolit ověřování založené na SAML", + "entry_point": "Vstupní bod", + "issuer": "Vydavatel", + "title": "Konfigurace SAML", + "update_failure": "Aktualizace SAML konfigurace se nezdařila.", + "want_assertions_signed": "Podepsat tvrzení", + "want_response_signed": "Podepsat odpověď" + } + }, + "data_sharing": { + "description": "Sdílejte anonymní využití dat za účelem zlepšení Hoppscotch", + "enable": "Povolit sdílení dat", + "see_shared": "Podívejte se, co je sdíleno", + "toggle_description": "Sdílejte data a vylepšete Hoppscotch", + "title": "Vylepšete Hoppscotch", + "welcome": "Vítejte na" + }, + "infra_tokens": { + "copy_token_warning": "Ujistěte se, že jste si zkopírovali infra token. Už to nebudete moci vidět!", + "deletion_success": "Infra token {label} byl smazán", + "empty": "Infra tokeny jsou prázdné", + "expired": "Platnost vypršela", + "expiration_label": "Vypršení platnosti", + "expires_on": "Vyprší dne", + "generate_modal_title": "Nový infra token", + "generate_new_token": "Vygenerovat nový token", + "generate_token": "Vygenerovat token", + "invalid_label": "Zadejte prosím popisek tokenu", + "last_used_on": "Naposledy použito", + "no_expiration": "Žádná expirace", + "no_expiration_verbose": "Platnost tohoto tokenu nikdy nevyprší!", + "section_description": "Spravujte své Hoppscotch uživatele prostřednictvím API s tokeny Infra", + "section_title": "Infra tokeny", + "tab_title": "Infra tokeny", + "token_expires_on": "Platnost tohoto tokenu vyprší dne", + "token_purpose": "Zadejte štítek k identifikaci tohoto tokenu" + }, + "metrics": { + "dashboard": "Dashboard", + "no_metrics": "Nebyly nalezeny žádné metriky", + "total_collections": "Celkový počet kolekcí", + "total_requests": "Celkový počet požadavků", + "total_teams": "Celkový počet pracovních prostorů", + "total_users": "Celkový počet uživatelů" + }, + "newsletter": { + "description": "Získejte aktualizace o našich nejnovějších zprávách", + "subscribe": "Přihlásit odběr", + "title": "Zůstaňte v kontaktu", + "toggle_description": "Získejte aktualizace o nejnovějších na Hoppscotch", + "unsubscribe": "Odhlásit odběr" + }, + "teams": { + "add_member": "Přidat člena", + "add_members": "Přidat členy", + "add_new": "Přidat nový", + "admin": "Admin", + "admin_Email": "E-mail správce", + "admin_id": "ID správce", + "cancel": "Zrušit", + "confirm_team_deletion": "Potvrdit smazání pracovního prostoru?", + "copy": "Kopírovat", + "create_team": "Vytvořit pracovní prostor", + "date": "Datum", + "delete_team": "Smazat pracovní prostor", + "details": "Podrobnosti", + "edit": "Upravit", + "editor": "Editor", + "editor_description": "Editoři mohou přidávat, upravovat a odstraňovat požadavky a kolekce.", + "email": "E-mail vlastníka pracovního prostoru", + "email_address": "E-mailová adresa", + "email_title": "E-mail", + "empty_name": "Název pracovního prostoru nemůže být prázdný.", + "error": "Něco se pokazilo. Zkuste to znovu později.", + "id": "ID pracovního prostoru", + "invited_email": "E-mail pozvaného", + "invited_on": "Pozván dne", + "invites": "Pozvánky", + "load_info_error": "Informace o pracovním prostoru nelze načíst", + "load_list_error": "Nelze načíst seznam pracovních prostorů", + "members": "Počet členů", + "no_invite": "Žádné pozvánky", + "no_invite_description": "Pozvěte svůj tým do pracovního prostoru a začněte spolupracovat", + "owner": "Vlastník", + "owner_description": "Vlastníci mohou přidávat, upravovat a odstraňovat požadavky, kolekce a členy pracovního prostoru.", + "permissions": "Oprávnění", + "name": "Název pracovního prostoru", + "no_members": "V tomto pracovním prostoru nejsou žádní členové. Přidejte členy do tohoto pracovního prostoru, abyste mohli spolupracovat", + "no_pending_invites": "Žádné nevyřízené pozvánky", + "no_teams": "Nebyly nalezeny žádné pracovní prostory.", + "no_teams_description": "Vytvořte pracovní prostor pro spolupráci se svým týmem", + "pending_invites": "Nevyřízené pozvánky", + "roles": "Role", + "roles_description": "Role se používají k řízení přístupu ke sdíleným kolekcím.", + "remove": "Odstranit", + "rename": "Přejmenovat", + "save": "Uložit", + "save_changes": "Uložit změny", + "send_invite": "Odeslat pozvánku", + "show_more": "Zobrazit více", + "team_details": "Podrobnosti o pracovním prostoru", + "team_members": "Členové", + "team_members_tab": "Členové pracovního prostoru", + "teams": "Pracovní prostory", + "uid": "UID", + "unnamed": "(Nepojmenovaný pracovní prostor)", + "viewer": "Čtenář", + "viewer_description": "Čtenáři mohou požadavky pouze zobrazovat a používat.", + "valid_name": "Zadejte prosím platný název pracovního prostoru", + "valid_owner_email": "Zadejte prosím platný e-mail vlastníka" + }, + "users": { + "add_user": "Přidat uživatele", + "admin": "Admin", + "admin_id": "ID správce", + "cancel": "Zrušit", + "created_on": "Vytvořeno dne", + "copy_invite_link": "Zkopírovat odkaz na pozvánku", + "copy_link": "Kopírovat odkaz", + "date": "Datum", + "delete": "Vymazat", + "delete_user": "Smazat uživatele", + "delete_users": "Smazat uživatele", + "details": "Podrobnosti", + "edit": "Upravit", + "email": "E-mail", + "email_address": "E-mailová adresa", + "empty_name": "Jméno nemůže být prázdné.", + "id": "ID uživatele", + "invalid_user": "Neplatný uživatel", + "invite_load_list_error": "Nelze načíst seznam pozvaných uživatelů", + "invite_user": "Pozvat uživatele", + "invited_by": "Pozván od", + "invited_on": "Pozván dne", + "invited_users": "Pozvaní uživatelé", + "invitee_email": "E-mail pozvaného", + "last_active_on": "Poslední aktivní", + "load_info_error": "Nelze načíst informace o uživateli", + "load_list_error": "Nelze načíst seznam uživatelů", + "make_admin": "Nastavit jako správce", + "name": "Jméno", + "new_user_added": "Přidán nový uživatel", + "no_invite": "Nebyly nalezeny žádné nevyřízené pozvánky", + "no_invite_description": "Žádné nevyřízené pozvánky. Začněte zvát kolegy do Hoppscotch.", + "no_shared_requests": "Žádné sdílené požadavky vytvořené uživatelem", + "no_users": "Nebyli nalezeni žádní uživatelé", + "not_available": "Není k dispozici", + "not_found": "Uživatel nenalezen", + "pending_invites": "Nevyřízené pozvánky", + "remove_admin_privilege": "Odebrat oprávnění správce", + "remove_admin_status": "Odebrat roli správce", + "rename": "Přejmenovat", + "revoke_invitation": "Zrušit pozvánku", + "searchbar_placeholder": "Hledat podle jména nebo e-mailu...", + "send_invite": "Odeslat pozvánku", + "show_more": "Zobrazit více", + "uid": "UID", + "unnamed": "(Nepojmenovaný uživatel)", + "user_not_found": "Uživatel nebyl nalezen v infrastruktuře.", + "users": "Uživatelé", + "valid_email": "Zadejte prosím platnou e-mailovou adresu", + "deactivate": "Deaktivovat", + "deactivate_user": "Deaktivovat uživatele" + }, + "organization": { + "login_to_continue_description": "Chcete-li se připojit k instanci organizace, musíte být přihlášeni.", + "create_an_organization": "Vytvořte organizaci", + "deactivate_user_failure": "Nepodařilo se deaktivovat uživatele.", + "deactivate_user_success": "Uživatel byl úspěšně deaktivován.", + "delete_account_description": "Tímto smažete všechna data spojená s vaším účtem Hoppscotch, včetně této a všech dalších organizací, kterých jste součástí.", + "delete_account": "Smazat účet Hoppscotch", + "user_deletion_failed_sole_admin": "Uživatel je jediný správce v jedné nebo více organizacích. Před smazáním ho nejdříve degradujte.", + "user_deletion_failed_sole_team_owner": "Uživatel je jediným vlastníkem pracovního prostoru v jedné nebo více instancích organizace. Před pokusem o smazání prosím převeďte vlastnictví nebo smažte příslušné pracovní prostory.", + "no_organizations": "Nejste členem žádné organizace", + "admin": "Admin" + }, + "organization_sidebar": { + "instances": "Instance", + "hoppscotch_cloud": "Hoppscotch Cloud", + "admin": "Admin", + "no_orgs_title": "Zatím žádné organizace", + "no_orgs_description": "Připojte se nebo vytvořte organizaci a spolupracujte se svým týmem", + "error_loading": "Organizace se nepodařilo načíst", + "inactive_orgs": "Neaktivní organizace", + "multi_account_notice": "Každá organizace si uchovává své vlastní přihlašovací údaje pomocí posledního účtu, ke kterému se přihlásila.", + "inactive_orgs_tooltip": "Požádejte o pomoc podporu." + }, + "billing": { + "confirm": { + "update_seat_count": "Opravdu chcete aktualizovat počet míst na {newSeatCount}?", + "update_billing_cycle": "Opravdu chcete aktualizovat fakturační cyklus na {newBillingCycle}?" + }, + "cancel_subscription": "Zrušit předplatné" + }, + "app_console": { + "entries": "Záznamy konzole", + "no_entries": "Žádné záznamy" + }, + "mockServer": { + "create_modal": { + "title": "Vytvořit Mock Server", + "name_label": "Název mock serveru", + "name_placeholder": "Zadejte název mock serveru", + "name_required": "Název mock serveru je povinný", + "collection_source_label": "Zdroj kolekce", + "existing_collection": "Existující kolekce", + "new_collection": "Nová kolekce", + "select_collection_label": "Vyberte kolekci", + "select_collection_placeholder": "Vyberte kolekci", + "collection_required": "Vyberte prosím kolekci", + "collection_name_label": "Název kolekce", + "collection_name_placeholder": "Zadejte název kolekce", + "collection_name_required": "Název kolekce je povinný", + "request_config_label": "Konfigurace požadavku", + "add_request": "Přidat požadavek", + "create_button": "Vytvořit Mock Server", + "success": "Mock server byl úspěšně vytvořen", + "error": "Nepodařilo se vytvořit mock server", + "no_collections": "Není k dispozici žádná kolekce" + }, + "edit_modal": { + "title": "Upravit Mock Server", + "name_label": "Název mock serveru", + "name_placeholder": "Zadejte název mock serveru", + "name_required": "Název mock serveru je povinný", + "active_label": "Aktivní", + "url_label": "URL mock serveru", + "collection_label": "Kolekce", + "update_button": "Aktualizovat Mock Server", + "success": "Mock server byl úspěšně aktualizován", + "error": "Aktualizace mock serveru se nezdařila", + "url_copied": "URL zkopírováno do schránky" + }, + "dashboard": { + "title": "Mock servery", + "subtitle": "Vytvářejte a spravujte své API mock servery", + "create_button": "Vytvořit Mock Server", + "create_first": "Vytvořte svůj první mock server", + "empty_title": "Nebyly nalezeny žádné mock servery", + "empty_description": "Vytvořte mock servery na základě API kolekcí, abyste mohli vyvíjet frontend a mobilní aplikace bez závislosti na backendu.", + "collection": "Kolekce", + "active": "Aktivní", + "inactive": "Neaktivní", + "mock_url": "Mock URL", + "endpoints": "Endpointy", + "created": "Vytvořeno", + "view_collection": "Zobrazit kolekci", + "documentation": "Dokumentace", + "doc_description": "Použijte toto URL jako base URL API ve svých aplikacích:", + "url_copied": "Mock server URL zkopírován do schránky", + "delete_title": "Smazat Mock Server", + "delete_description": "Opravdu chcete smazat tento mock server?", + "delete_success": "Mock server byl úspěšně smazán", + "delete_error": "Smazání mock serveru se nezdařilo" + } } } diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index 6c2e91f3d3b..f91583a4367 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -379,7 +379,9 @@ "context_menu": { "add_parameters": "Add to parameters", "open_request_in_new_tab": "Open request in new tab", - "set_environment_variable": "Set as variable" + "set_environment_variable": "Set as variable", + "encode_uri_component": "Encode URL component", + "decode_uri_component": "Decode URL component" }, "cookies": { "modal": { @@ -531,7 +533,33 @@ "update_title": "Update Published Documentation", "url_copied": "URL copied to clipboard!", "view_published": "View Published Docs", - "view_title": "View Published Documentation" + "view_title": "Published Documentation Snapshot", + "versions": "Versions", + "create_new_version": "Create New Version", + "invalid_version": "Version must only contain alphanumeric characters, dots, and hyphens", + "snapshot_description": "This will snapshot the current documentation as this version", + "live": "Live", + "snapshot": "Snapshot", + "version_immutable": "Published versions are read-only snapshots", + "view_snapshot": "View Snapshot", + "current_version": "CURRENT", + "not_found": "Published documentation not found", + "no_doc_id": "No document ID provided", + "version_label": "v{version}", + "unpublish_version": "Unpublish this version", + "loading_snapshot": "Loading snapshot...", + "retry_snapshot": "Retry", + "sensitive_data_warning": "Please make sure no sensitive data is exposed in the published documentation.", + "snapshot_preview": "Snapshot Preview", + "snapshot_load_error": "Failed to load snapshot preview", + "snapshot_empty": "No requests or folders in this snapshot", + "snapshot_item_count": "{count} items", + "auto_sync_live_notice": "This version auto-syncs with the live collection", + "untitled_project": "Untitled Project", + "first_publish_hint": "Your documentation will be published as a live version that automatically stays in sync with your collection", + "environment": "Environment", + "no_environment": "No environment", + "environment_description": "Attach an environment to resolve variables in the published documentation" }, "request_opened_in_new_tab": "Request opened in new tab!", "response": { @@ -924,7 +952,8 @@ "instance_changed": "Switched to instance", "current_instance_error": "Failed to track current instance", "not_available": "Instance switching is not available", - "cleanup_completed": "Instance switcher cleanup completed" + "cleanup_completed": "Instance switcher cleanup completed", + "self_hosted": "Self-hosted instances" }, "inspections": { "description": "Inspect possible errors", @@ -2217,13 +2246,14 @@ "admin": "Admin" }, "organization_sidebar": { - "instances": "Instances", "hoppscotch_cloud": "Hoppscotch Cloud", + "cloud_locked": "Default instance cannot be removed", "admin": "Admin", - "no_orgs_title": "No organizations yet", - "no_orgs_description": "Join or create an organization to collaborate with your team", "error_loading": "Failed to load organizations", "inactive_orgs": "Inactive Organizations", + "no_orgs_found": "No organizations found", + "no_active_orgs_found": "No active organizations", + "organizations_for": "Organizations for {email}", "multi_account_notice": "Each organization keeps its own login, using the last account accessed.", "inactive_orgs_tooltip": "Contact support for assistance." }, diff --git a/packages/hoppscotch-common/package.json b/packages/hoppscotch-common/package.json index 463dc3d7976..0e8896186fb 100644 --- a/packages/hoppscotch-common/package.json +++ b/packages/hoppscotch-common/package.json @@ -1,7 +1,7 @@ { "name": "@hoppscotch/common", "private": true, - "version": "2026.1.1", + "version": "2026.2.0", "scripts": { "dev": "pnpm exec npm-run-all -p -l dev:*", "test": "vitest --run", @@ -52,14 +52,14 @@ "@types/hawk": "9.0.7", "@types/markdown-it": "14.1.2", "@types/node": "24.10.1", - "@unhead/vue": "2.1.2", + "@unhead/vue": "2.1.4", "@urql/core": "6.0.1", "@urql/devtools": "2.0.3", "@urql/exchange-auth": "3.0.0", - "@vueuse/core": "14.1.0", + "@vueuse/core": "14.2.1", "acorn-walk": "8.3.4", "aws4fetch": "1.0.20", - "axios": "1.13.2", + "axios": "1.13.5", "buffer": "6.0.3", "cookie-es": "2.0.0", "dioc": "3.0.2", @@ -80,17 +80,17 @@ "js-md5": "0.8.3", "js-yaml": "4.1.1", "jsonc-parser": "3.3.1", - "lodash-es": "4.17.22", + "lodash-es": "4.17.23", "lossless-json": "4.3.0", - "markdown-it": "14.1.0", + "markdown-it": "14.1.1", "minisearch": "7.2.0", "monaco-editor": "0.55.1", "nprogress": "0.2.0", "paho-mqtt": "1.1.0", "path": "0.12.7", - "postman-collection": "5.2.0", + "postman-collection": "5.2.1", "process": "0.11.10", - "qs": "6.14.1", + "qs": "6.15.0", "quicktype-core": "23.2.6", "rollup": "4.55.3", "rxjs": "7.8.2", @@ -111,7 +111,7 @@ "util": "0.12.5", "uuid": "13.0.0", "verzod": "0.4.0", - "vue": "3.5.27", + "vue": "3.5.28", "vue-i18n": "11.2.8", "vue-json-pretty": "2.6.0", "vue-pdf-embed": "2.1.3", @@ -131,15 +131,15 @@ "@eslint/js": "9.39.2", "@graphql-codegen/add": "6.0.0", "@graphql-codegen/cli": "6.1.1", - "@graphql-codegen/typed-document-node": "6.1.5", - "@graphql-codegen/typescript": "5.0.7", - "@graphql-codegen/typescript-operations": "5.0.7", + "@graphql-codegen/typed-document-node": "6.1.6", + "@graphql-codegen/typescript": "5.0.8", + "@graphql-codegen/typescript-operations": "5.0.8", "@graphql-codegen/typescript-urql-graphcache": "3.1.1", "@graphql-codegen/urql-introspection": "3.0.1", "@graphql-typed-document-node/core": "3.2.0", - "@iconify-json/lucide": "1.2.86", + "@iconify-json/lucide": "1.2.91", "@import-meta-env/cli": "0.7.4", - "@intlify/unplugin-vue-i18n": "11.0.3", + "@intlify/unplugin-vue-i18n": "11.0.7", "@relmify/jest-fp-ts": "2.1.1", "@rushstack/eslint-patch": "1.15.0", "@types/har-format": "1.2.16", @@ -151,28 +151,28 @@ "@types/qs": "6.14.0", "@types/splitpanes": "2.2.6", "@types/yargs-parser": "21.0.3", - "@typescript-eslint/eslint-plugin": "8.53.1", - "@typescript-eslint/parser": "8.53.1", - "@vitejs/plugin-vue": "6.0.3", - "@vue/compiler-sfc": "3.5.27", - "@vue/eslint-config-typescript": "14.6.0", - "@vue/runtime-core": "3.5.27", - "autoprefixer": "10.4.23", + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@vitejs/plugin-vue": "6.0.4", + "@vue/compiler-sfc": "3.5.28", + "@vue/eslint-config-typescript": "14.7.0", + "@vue/runtime-core": "3.5.28", + "autoprefixer": "10.4.24", "cross-env": "10.1.0", - "dotenv": "17.2.3", + "dotenv": "17.3.1", "eslint": "9.39.2", "eslint-plugin-prettier": "5.5.5", - "eslint-plugin-vue": "10.6.2", - "glob": "13.0.0", + "eslint-plugin-vue": "10.8.0", + "glob": "13.0.5", "globals": "16.5.0", "jsdom": "27.4.0", "npm-run-all": "4.1.5", "openapi-types": "12.1.3", "postcss": "8.5.6", - "prettier": "3.8.0", + "prettier": "3.8.1", "prettier-plugin-tailwindcss": "0.7.1", "rollup-plugin-polyfill-node": "0.13.0", - "sass": "1.97.2", + "sass": "1.97.3", "tailwindcss": "3.4.16", "tsup": "8.5.1", "typescript": "5.9.3", @@ -187,7 +187,7 @@ "vite-plugin-pages-sitemap": "1.7.1", "vite-plugin-pwa": "1.2.0", "vite-plugin-vue-layouts": "0.11.0", - "vitest": "4.0.17", + "vitest": "4.0.18", "vue-tsc": "1.8.8" } } diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index d0329b81c8d..6a8215eabff 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -56,11 +56,14 @@ declare module 'vue' { CollectionsDocumentation: typeof import('./components/collections/documentation/index.vue')['default'] CollectionsDocumentationCollectionPreview: typeof import('./components/collections/documentation/CollectionPreview.vue')['default'] CollectionsDocumentationCollectionStructure: typeof import('./components/collections/documentation/CollectionStructure.vue')['default'] + CollectionsDocumentationEnvironmentPicker: typeof import('./components/collections/documentation/EnvironmentPicker.vue')['default'] CollectionsDocumentationFolderItem: typeof import('./components/collections/documentation/FolderItem.vue')['default'] CollectionsDocumentationLazyDocumentationItem: typeof import('./components/collections/documentation/LazyDocumentationItem.vue')['default'] CollectionsDocumentationMarkdownEditor: typeof import('./components/collections/documentation/MarkdownEditor.vue')['default'] CollectionsDocumentationPreview: typeof import('./components/collections/documentation/Preview.vue')['default'] + CollectionsDocumentationPublishDocForm: typeof import('./components/collections/documentation/PublishDocForm.vue')['default'] CollectionsDocumentationPublishDocModal: typeof import('./components/collections/documentation/PublishDocModal.vue')['default'] + CollectionsDocumentationPublishDocSnapshotPreview: typeof import('./components/collections/documentation/PublishDocSnapshotPreview.vue')['default'] CollectionsDocumentationRequestItem: typeof import('./components/collections/documentation/RequestItem.vue')['default'] CollectionsDocumentationRequestPreview: typeof import('./components/collections/documentation/RequestPreview.vue')['default'] CollectionsDocumentationSectionsAuth: typeof import('./components/collections/documentation/sections/Auth.vue')['default'] @@ -231,15 +234,19 @@ declare module 'vue' { HttpTestTestResult: typeof import('./components/http/test/TestResult.vue')['default'] HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default'] IconLucideActivity: typeof import('~icons/lucide/activity')['default'] + IconLucideAlertCircle: typeof import('~icons/lucide/alert-circle')['default'] IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default'] IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'] IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default'] IconLucideBrush: typeof import('~icons/lucide/brush')['default'] + IconLucideCheck: typeof import('~icons/lucide/check')['default'] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] + IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideCircleCheck: typeof import('~icons/lucide/circle-check')['default'] IconLucideFileQuestion: typeof import('~icons/lucide/file-question')['default'] IconLucideFileText: typeof import('~icons/lucide/file-text')['default'] + IconLucideFileX: typeof import('~icons/lucide/file-x')['default'] IconLucideFolder: typeof import('~icons/lucide/folder')['default'] IconLucideFolderOpen: typeof import('~icons/lucide/folder-open')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] @@ -252,6 +259,7 @@ declare module 'vue' { IconLucideLock: typeof import('~icons/lucide/lock')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucidePlusCircle: typeof import('~icons/lucide/plus-circle')['default'] + IconLucideRefreshCw: typeof import('~icons/lucide/refresh-cw')['default'] IconLucideRss: typeof import('~icons/lucide/rss')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideTerminal: typeof import('~icons/lucide/terminal')['default'] @@ -268,13 +276,6 @@ declare module 'vue' { ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default'] ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default'] InstanceSwitcher: typeof import('./components/instance/Switcher.vue')['default'] - InterceptorsAgentModalNativeCACertificates: typeof import('./components/interceptors/agent/ModalNativeCACertificates.vue')['default'] - InterceptorsAgentModalNativeClientCertificates: typeof import('./components/interceptors/agent/ModalNativeClientCertificates.vue')['default'] - InterceptorsAgentModalNativeClientCertsAdd: typeof import('./components/interceptors/agent/ModalNativeClientCertsAdd.vue')['default'] - InterceptorsAgentRegistrationModal: typeof import('./components/interceptors/agent/RegistrationModal.vue')['default'] - InterceptorsAgentRootExt: typeof import('./components/interceptors/agent/RootExt.vue')['default'] - InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default'] - InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default'] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default'] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default'] LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default'] @@ -294,6 +295,7 @@ declare module 'vue' { MockServerMockServerDashboard: typeof import('./components/mockServer/MockServerDashboard.vue')['default'] MockServerMockServerLogs: typeof import('./components/mockServer/MockServerLogs.vue')['default'] MonacoScriptEditor: typeof import('./components/MonacoScriptEditor.vue')['default'] + OrganizationSwitcher: typeof import('./components/organization/Switcher.vue')['default'] Profile: typeof import('./components/profile/index.vue')['default'] ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default'] RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default'] @@ -305,6 +307,7 @@ declare module 'vue' { SettingsAgentSubtitle: typeof import('./components/settings/AgentSubtitle.vue')['default'] SettingsExtension: typeof import('./components/settings/Extension.vue')['default'] SettingsExtensionSubtitle: typeof import('./components/settings/ExtensionSubtitle.vue')['default'] + SettingsInterceptorErrorPlaceholder: typeof import('./components/settings/InterceptorErrorPlaceholder.vue')['default'] SettingsNative: typeof import('./components/settings/Native.vue')['default'] SettingsProxy: typeof import('./components/settings/Proxy.vue')['default'] Share: typeof import('./components/share/index.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/app/Header.vue b/packages/hoppscotch-common/src/components/app/Header.vue index 29948a26b24..863a367e0e1 100644 --- a/packages/hoppscotch-common/src/components/app/Header.vue +++ b/packages/hoppscotch-common/src/components/app/Header.vue @@ -14,33 +14,44 @@ }" >
- + -
- - {{ - platform.instance.getCurrentInstance?.()?.displayName || - "Hoppscotch" - }} - - -
+
@@ -360,7 +371,9 @@ import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core" import { useService } from "dioc/vue" import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" +import type { Instance } from "tippy.js" import { computed, onMounted, reactive, ref, watch } from "vue" + import { useToast } from "~/composables/toast" import { GetMyTeamsQuery, TeamAccessRole } from "~/helpers/backend/graphql" import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team" @@ -388,8 +401,16 @@ const kernelMode = getKernelMode() const downloadableLinksRef = kernelMode === "web" ? ref(null) : ref(null) -const instanceSwitcherRef = - kernelMode === "desktop" ? ref(null) : ref(null) +const switcherRef = ref(null) + +// Reserve scrollbar gutter so content width doesn't shift when the list +// grows long enough to scroll inside the popover's `max-h-[45vh]` container. +const onSwitcherCreate = (instance: Instance) => { + const content = instance.popper?.querySelector(".tippy-content") + if (content instanceof HTMLElement) { + content.style.scrollbarGutter = "stable" + } +} const isUserAdmin = ref(false) diff --git a/packages/hoppscotch-common/src/components/app/spotlight/index.vue b/packages/hoppscotch-common/src/components/app/spotlight/index.vue index e731b29ce51..285d9daa2df 100644 --- a/packages/hoppscotch-common/src/components/app/spotlight/index.vue +++ b/packages/hoppscotch-common/src/components/app/spotlight/index.vue @@ -104,8 +104,6 @@ import { } from "~/services/spotlight/searchers/environment.searcher" import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher" import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher" -// NOTE: Old interceptors -// import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher" import { KernelInterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/kernel-interceptor.searcher" import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher" import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher" @@ -146,8 +144,6 @@ useService(EnvironmentsSpotlightSearcherService) useService(SwitchEnvSpotlightSearcherService) useService(WorkspaceSpotlightSearcherService) useService(SwitchWorkspaceSpotlightSearcherService) -// NOTE: Old interceptors -// useService(InterceptorSpotlightSearcherService) useService(KernelInterceptorSpotlightSearcherService) useService(TeamsSpotlightSearcherService) diff --git a/packages/hoppscotch-common/src/components/collections/Properties.vue b/packages/hoppscotch-common/src/components/collections/Properties.vue index 18505c137f2..a11e10258f4 100644 --- a/packages/hoppscotch-common/src/components/collections/Properties.vue +++ b/packages/hoppscotch-common/src/components/collections/Properties.vue @@ -4,7 +4,7 @@ dialog :title="t('collection.properties')" :full-width-body="true" - styles="sm:max-w-5xl" + styles="sm:max-w-5xl lg:max-w-6xl xl:max-w-7xl 2xl:max-w-[80vw]" @close="hideModal" > + + diff --git a/packages/hoppscotch-common/src/components/collections/documentation/PublishDocModal.vue b/packages/hoppscotch-common/src/components/collections/documentation/PublishDocModal.vue index cb7255f1eed..9021c6a95ed 100644 --- a/packages/hoppscotch-common/src/components/collections/documentation/PublishDocModal.vue +++ b/packages/hoppscotch-common/src/components/collections/documentation/PublishDocModal.vue @@ -3,97 +3,41 @@ v-if="show" dialog :title="modalTitle" - styles="sm:max-w-2xl" + :styles="mode === 'view' ? 'sm:max-w-6xl' : 'sm:max-w-2xl'" @close="hideModal" >