Compare commits

..

6 Commits

Author SHA1 Message Date
mirarifhasan
ca6522f370 test: fix test cases 2024-04-29 23:00:57 +06:00
mirarifhasan
872623231f refactor: remove unused imports 2024-04-29 22:37:07 +06:00
mirarifhasan
20ac35becb refactor: getTeamsOfUser function 2024-04-29 22:35:40 +06:00
mirarifhasan
5eed5ce176 refactor: getTeamMemberTE function removed 2024-04-29 22:33:11 +06:00
mirarifhasan
9694c0d373 refactor: getMembersOfTeam function improved 2024-04-29 22:29:39 +06:00
mirarifhasan
293c93cc79 chore: getTeamWithIDTE function removed 2024-04-29 22:26:00 +06:00
135 changed files with 2552 additions and 13475 deletions

View File

@@ -34,11 +34,11 @@
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"vue": "3.4.27" "vue": "3.3.9"
}, },
"packageExtensions": { "packageExtensions": {
"httpsnippet@3.0.1": { "httpsnippet@3.0.1": {
"dependencies": { "peerDependencies": {
"ajv": "6.12.3" "ajv": "6.12.3"
} }
} }

View File

@@ -1,4 +1,4 @@
FROM node:20.12.2 AS builder FROM node:18.8.0 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app

View File

@@ -3,7 +3,9 @@
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"assets": [{ "include": "mailer/templates/**/*", "outDir": "dist" }], "assets": [
"**/*.hbs"
],
"watchAssets": true "watchAssets": true
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoppscotch-backend", "name": "hoppscotch-backend",
"version": "2024.3.3", "version": "2024.3.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -35,7 +35,6 @@
"@nestjs/passport": "10.0.2", "@nestjs/passport": "10.0.2",
"@nestjs/platform-express": "10.2.7", "@nestjs/platform-express": "10.2.7",
"@nestjs/schedule": "4.0.1", "@nestjs/schedule": "4.0.1",
"@nestjs/terminus": "10.2.3",
"@nestjs/throttler": "5.0.1", "@nestjs/throttler": "5.0.1",
"@prisma/client": "5.8.1", "@prisma/client": "5.8.1",
"argon2": "0.30.3", "argon2": "0.30.3",

View File

@@ -11,6 +11,7 @@ import {
EMAIL_FAILED, EMAIL_FAILED,
INVALID_EMAIL, INVALID_EMAIL,
ONLY_ONE_ADMIN_ACCOUNT, ONLY_ONE_ADMIN_ACCOUNT,
TEAM_INVALID_ID,
TEAM_INVITE_ALREADY_MEMBER, TEAM_INVITE_ALREADY_MEMBER,
TEAM_INVITE_NO_INVITE_FOUND, TEAM_INVITE_NO_INVITE_FOUND,
USERS_NOT_FOUND, USERS_NOT_FOUND,
@@ -318,11 +319,11 @@ export class AdminService {
const user = await this.userService.findUserByEmail(userEmail); const user = await this.userService.findUserByEmail(userEmail);
if (O.isNone(user)) return E.left(USER_NOT_FOUND); if (O.isNone(user)) return E.left(USER_NOT_FOUND);
const teamMember = await this.teamService.getTeamMemberTE( const teamMember = await this.teamService.getTeamMember(
teamID, teamID,
user.value.uid, user.value.uid,
)(); );
if (E.isLeft(teamMember)) { if (!teamMember) {
const addedUser = await this.teamService.addMemberToTeamWithEmail( const addedUser = await this.teamService.addMemberToTeamWithEmail(
teamID, teamID,
userEmail, userEmail,
@@ -590,9 +591,9 @@ export class AdminService {
* @returns an Either of `Team` or error * @returns an Either of `Team` or error
*/ */
async getTeamInfo(teamID: string) { async getTeamInfo(teamID: string) {
const team = await this.teamService.getTeamWithIDTE(teamID)(); const team = await this.teamService.getTeamWithID(teamID);
if (E.isLeft(team)) return E.left(team.left); if (!team) return E.left(TEAM_INVALID_ID);
return E.right(team.right); return E.right(team);
} }
/** /**

View File

@@ -26,7 +26,6 @@ import { loadInfraConfiguration } from './infra-config/helper';
import { MailerModule } from './mailer/mailer.module'; import { MailerModule } from './mailer/mailer.module';
import { PosthogModule } from './posthog/posthog.module'; import { PosthogModule } from './posthog/posthog.module';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { HealthModule } from './health/health.module';
@Module({ @Module({
imports: [ imports: [
@@ -101,7 +100,6 @@ import { HealthModule } from './health/health.module';
InfraConfigModule, InfraConfigModule,
PosthogModule, PosthogModule,
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
HealthModule,
], ],
providers: [GQLComplexityPlugin], providers: [GQLComplexityPlugin],
controllers: [AppController], controllers: [AppController],

View File

@@ -1,24 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
PrismaHealthIndicator,
} from '@nestjs/terminus';
import { PrismaService } from 'src/prisma/prisma.service';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private prismaHealth: PrismaHealthIndicator,
private prisma: PrismaService,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
async () => this.prismaHealth.pingCheck('database', this.prisma),
]);
}
}

View File

@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
import { TerminusModule } from '@nestjs/terminus';
@Module({
imports: [PrismaModule, TerminusModule],
controllers: [HealthController],
})
export class HealthModule {}

View File

@@ -15,7 +15,11 @@ import * as TE from 'fp-ts/TaskEither';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model'; import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors'; import {
TEAM_INVALID_ID,
TEAM_INVITE_NO_INVITE_FOUND,
USER_NOT_FOUND,
} from 'src/errors';
import { GqlUser } from 'src/decorators/gql-user.decorator'; import { GqlUser } from 'src/decorators/gql-user.decorator';
import { User } from 'src/user/user.model'; import { User } from 'src/user/user.model';
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
@@ -50,10 +54,9 @@ export class TeamInvitationResolver {
description: 'Get the team associated to the invite', description: 'Get the team associated to the invite',
}) })
async team(@Parent() teamInvitation: TeamInvitation): Promise<Team> { async team(@Parent() teamInvitation: TeamInvitation): Promise<Team> {
return pipe( const team = await this.teamService.getTeamWithID(teamInvitation.teamID);
this.teamService.getTeamWithIDTE(teamInvitation.teamID), if (!team) throwErr(TEAM_INVALID_ID);
TE.getOrElse(throwErr), return team;
)();
} }
@ResolveField(() => User, { @ResolveField(() => User, {

View File

@@ -11,6 +11,7 @@ import {
} from '../errors'; } from '../errors';
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import { skip } from 'rxjs';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
@@ -755,6 +756,8 @@ describe('getMembersOfTeam', () => {
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({ expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
take: 10, take: 10,
skip: 0,
cursor: undefined,
where: { where: {
teamID: team.id, teamID: team.id,
}, },
@@ -806,6 +809,8 @@ describe('getTeamsOfUser', () => {
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({ expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
take: 10, take: 10,
skip: 0,
cursor: undefined,
where: { where: {
userUid: dbTeamMember.userUid, userUid: dbTeamMember.userUid,
}, },

View File

@@ -15,7 +15,6 @@ import {
} from '../errors'; } from '../errors';
import { PubSubService } from '../pubsub/pubsub.service'; import { PubSubService } from '../pubsub/pubsub.service';
import { flow, pipe } from 'fp-ts/function'; import { flow, pipe } from 'fp-ts/function';
import * as TE from 'fp-ts/TaskEither';
import * as TO from 'fp-ts/TaskOption'; import * as TO from 'fp-ts/TaskOption';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
@@ -264,37 +263,25 @@ export class TeamService implements UserDataHandler, OnModuleInit {
} }
async getTeamsOfUser(uid: string, cursor: string | null): Promise<Team[]> { async getTeamsOfUser(uid: string, cursor: string | null): Promise<Team[]> {
if (!cursor) { const entries = await this.prisma.teamMember.findMany({
const entries = await this.prisma.teamMember.findMany({ take: 10,
take: 10, skip: cursor ? 1 : 0,
where: { cursor: cursor
userUid: uid, ? {
}, teamID_userUid: {
include: { teamID: cursor,
team: true, userUid: uid,
}, },
}); }
: undefined,
return entries.map((entry) => entry.team); where: {
} else { userUid: uid,
const entries = await this.prisma.teamMember.findMany({ },
take: 10, include: {
skip: 1, team: true,
cursor: { },
teamID_userUid: { });
teamID: cursor, return entries.map((entry) => entry.team);
userUid: uid,
},
},
where: {
userUid: uid,
},
include: {
team: true,
},
});
return entries.map((entry) => entry.team);
}
} }
async getTeamWithID(teamID: string): Promise<Team | null> { async getTeamWithID(teamID: string): Promise<Team | null> {
@@ -311,19 +298,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
} }
} }
getTeamWithIDTE(teamID: string): TE.TaskEither<'team/invalid_id', Team> {
return pipe(
() => this.getTeamWithID(teamID),
TE.fromTask,
TE.chain(
TE.fromPredicate(
(x): x is Team => !!x,
() => TEAM_INVALID_ID,
),
),
);
}
/** /**
* Filters out team members that we weren't able to match * Filters out team members that we weren't able to match
* (also deletes the membership) * (also deletes the membership)
@@ -375,19 +349,6 @@ export class TeamService implements UserDataHandler, OnModuleInit {
} }
} }
getTeamMemberTE(teamID: string, userUid: string) {
return pipe(
() => this.getTeamMember(teamID, userUid),
TE.fromTask,
TE.chain(
TE.fromPredicate(
(x): x is TeamMember => !!x,
() => TEAM_MEMBER_NOT_FOUND,
),
),
);
}
async getRoleOfUserInTeam( async getRoleOfUserInTeam(
teamID: string, teamID: string,
userUid: string, userUid: string,
@@ -472,25 +433,12 @@ export class TeamService implements UserDataHandler, OnModuleInit {
): Promise<TeamMember[]> { ): Promise<TeamMember[]> {
let teamMembers: DbTeamMember[]; let teamMembers: DbTeamMember[];
if (!cursor) { teamMembers = await this.prisma.teamMember.findMany({
teamMembers = await this.prisma.teamMember.findMany({ take: 10,
take: 10, skip: cursor ? 1 : 0,
where: { cursor: cursor ? { id: cursor } : undefined,
teamID, where: { teamID },
}, });
});
} else {
teamMembers = await this.prisma.teamMember.findMany({
take: 10,
skip: 1,
cursor: {
id: cursor,
},
where: {
teamID,
},
});
}
const members = teamMembers.map( const members = teamMembers.map(
(entry) => (entry) =>

View File

@@ -374,8 +374,7 @@
"mutations": "Mutations", "mutations": "Mutations",
"schema": "Schema", "schema": "Schema",
"subscriptions": "Subscriptions", "subscriptions": "Subscriptions",
"switch_connection": "Switch connection", "switch_connection": "Switch connection"
"url_placeholder": "Enter a GraphQL endpoint URL"
}, },
"graphql_collections": { "graphql_collections": {
"title": "GraphQL Collections" "title": "GraphQL Collections"
@@ -599,7 +598,6 @@
"title": "Request", "title": "Request",
"type": "Request type", "type": "Request type",
"url": "URL", "url": "URL",
"url_placeholder": "Enter a URL or paste a cURL command",
"variables": "Variables", "variables": "Variables",
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
@@ -1027,8 +1025,7 @@
"personal": "Personal Workspace", "personal": "Personal Workspace",
"other_workspaces": "My Workspaces", "other_workspaces": "My Workspaces",
"team": "Workspace", "team": "Workspace",
"title": "Workspaces", "title": "Workspaces"
"no_workspace": "No Workspace"
}, },
"site_protection": { "site_protection": {
"login_to_continue": "Login to continue", "login_to_continue": "Login to continue",

View File

@@ -58,6 +58,24 @@
"new": "Ajouter un nouveau", "new": "Ajouter un nouveau",
"star": "Ajouter une étoile" "star": "Ajouter une étoile"
}, },
"cookies": {
"modal": {
"new_domain_name": "Nouveau nom de domaine",
"set": "Définir un cookie",
"cookie_string": "Chaîne de caractères de cookie",
"enter_cookie_string": "Saisir la chaîne de caractères du cookie",
"cookie_name": "Nom",
"cookie_value": "Valeur",
"cookie_path": "Chemin d'accès",
"cookie_expires": "Expiration",
"managed_tab": "Gestion",
"raw_tab": "Brut",
"interceptor_no_support": "L'intercepteur que vous avez sélectionné ne prend pas en charge les cookies. Sélectionnez un autre intercepteur et réessayez.",
"empty_domains": "La liste des domaines est vide",
"empty_domain": "Le domaine est vide",
"no_cookies_in_domain": "Aucun cookie n'est défini pour ce domaine"
}
},
"app": { "app": {
"chat_with_us": "Discuter avec nous", "chat_with_us": "Discuter avec nous",
"contact_us": "Nous contacter", "contact_us": "Nous contacter",
@@ -169,7 +187,7 @@
}, },
"confirm": { "confirm": {
"close_unsaved_tab": "Êtes-vous sûr de vouloir fermer cet onglet ?", "close_unsaved_tab": "Êtes-vous sûr de vouloir fermer cet onglet ?",
"close_unsaved_tabs": "Êtes-vous sûr de vouloir fermer tous les onglets ? {count} onglets non enregistrés seront perdus", "close_unsaved_tabs": "Êtes-vous sûr de vouloir fermer tous les onglets ? {Les onglets non enregistrés seront perdus.",
"exit_team": "Êtes-vous sûr de vouloir quitter cette équipe ?", "exit_team": "Êtes-vous sûr de vouloir quitter cette équipe ?",
"logout": "Êtes-vous sûr de vouloir vous déconnecter?", "logout": "Êtes-vous sûr de vouloir vous déconnecter?",
"remove_collection": "Voulez-vous vraiment supprimer définitivement cette collection ?", "remove_collection": "Voulez-vous vraiment supprimer définitivement cette collection ?",
@@ -189,24 +207,6 @@
"open_request_in_new_tab": "Ouvrir la demande dans un nouvel onglet", "open_request_in_new_tab": "Ouvrir la demande dans un nouvel onglet",
"set_environment_variable": "Définir comme variable" "set_environment_variable": "Définir comme variable"
}, },
"cookies": {
"modal": {
"new_domain_name": "Nouveau nom de domaine",
"set": "Définir un cookie",
"cookie_string": "Chaîne de caractères de cookie",
"enter_cookie_string": "Saisir la chaîne de caractères du cookie",
"cookie_name": "Nom",
"cookie_value": "Valeur",
"cookie_path": "Chemin d'accès",
"cookie_expires": "Expiration",
"managed_tab": "Gestion",
"raw_tab": "Brut",
"interceptor_no_support": "L'intercepteur que vous avez sélectionné ne prend pas en charge les cookies. Sélectionnez un autre intercepteur et réessayez.",
"empty_domains": "La liste des domaines est vide",
"empty_domain": "Le domaine est vide",
"no_cookies_in_domain": "Aucun cookie n'est défini pour ce domaine"
}
},
"count": { "count": {
"header": "En-tête {count}", "header": "En-tête {count}",
"message": "Message {compte}", "message": "Message {compte}",
@@ -410,7 +410,7 @@
"description": "Inspecter les erreurs possibles", "description": "Inspecter les erreurs possibles",
"environment": { "environment": {
"add_environment": "Ajouter à l'environnement", "add_environment": "Ajouter à l'environnement",
"not_found": "La variable d'environnement “{environment}“ n'a pas été trouvée." "not_found": "La variable d'environnement “{environnement}“ n'a pas été trouvée."
}, },
"header": { "header": {
"cookie": "Le navigateur ne permet pas à Hoppscotch de définir l'en-tête Cookie. Pendant que nous travaillons sur l'application de bureau Hoppscotch (bientôt disponible), veuillez utiliser l'en-tête d'autorisation à la place." "cookie": "Le navigateur ne permet pas à Hoppscotch de définir l'en-tête Cookie. Pendant que nous travaillons sur l'application de bureau Hoppscotch (bientôt disponible), veuillez utiliser l'en-tête d'autorisation à la place."

File diff suppressed because it is too large Load Diff

View File

@@ -11,13 +11,12 @@
"connect": "Подключиться", "connect": "Подключиться",
"connecting": "Соединение...", "connecting": "Соединение...",
"copy": "Скопировать", "copy": "Скопировать",
"create": "Создать", "create": "Create",
"delete": "Удалить", "delete": "Удалить",
"disconnect": "Отключиться", "disconnect": "Отключиться",
"dismiss": "Скрыть", "dismiss": "Скрыть",
"dont_save": "Не сохранять", "dont_save": "Не сохранять",
"download_file": "Скачать файл", "download_file": "Скачать файл",
"download_here": "Download here",
"drag_to_reorder": "Перетягивайте для сортировки", "drag_to_reorder": "Перетягивайте для сортировки",
"duplicate": "Дублировать", "duplicate": "Дублировать",
"edit": "Редактировать", "edit": "Редактировать",
@@ -25,7 +24,6 @@
"go_back": "Вернуться", "go_back": "Вернуться",
"go_forward": "Вперёд", "go_forward": "Вперёд",
"group_by": "Сгруппировать по", "group_by": "Сгруппировать по",
"hide_secret": "Hide secret",
"label": "Название", "label": "Название",
"learn_more": "Узнать больше", "learn_more": "Узнать больше",
"less": "Меньше", "less": "Меньше",
@@ -35,7 +33,7 @@
"open_workspace": "Открыть пространство", "open_workspace": "Открыть пространство",
"paste": "Вставить", "paste": "Вставить",
"prettify": "Форматировать", "prettify": "Форматировать",
"properties": "Параметры", "properties": "Properties",
"remove": "Удалить", "remove": "Удалить",
"rename": "Переименовать", "rename": "Переименовать",
"restore": "Восстановить", "restore": "Восстановить",
@@ -44,14 +42,13 @@
"scroll_to_top": "Вверх", "scroll_to_top": "Вверх",
"search": "Поиск", "search": "Поиск",
"send": "Отправить", "send": "Отправить",
"share": "Поделиться", "share": "Share",
"show_secret": "Show secret",
"start": "Начать", "start": "Начать",
"starting": "Запускаю", "starting": "Запускаю",
"stop": "Стоп", "stop": "Стоп",
"to_close": "закрыть", "to_close": "что бы закрыть",
"to_navigate": "для навигации", "to_navigate": "для навигации",
"to_select": "выбрать", "to_select": "выборать",
"turn_off": "Выключить", "turn_off": "Выключить",
"turn_on": "Включить", "turn_on": "Включить",
"undo": "Отменить", "undo": "Отменить",
@@ -69,12 +66,12 @@
"copy_interface_type": "Copy interface type", "copy_interface_type": "Copy interface type",
"copy_user_id": "Копировать токен пользователя", "copy_user_id": "Копировать токен пользователя",
"developer_option": "Настройки разработчика", "developer_option": "Настройки разработчика",
"developer_option_description": "Инструмент разработчика помогает обслуживать и развивать Hoppscotch", "developer_option_description": "Инструмент разработчика помогает обслуживить и развивить Hoppscotch",
"discord": "Discord", "discord": "Discord",
"documentation": "Документация", "documentation": "Документация",
"github": "GitHub", "github": "GitHub",
"help": "Справка, отзывы и документация", "help": "Справка, отзывы и документация",
"home": "На главную", "home": "Дом",
"invite": "Пригласить", "invite": "Пригласить",
"invite_description": "В Hoppscotch мы разработали простой и интуитивно понятный интерфейс для создания и управления вашими API. Hoppscotch - это инструмент, который помогает создавать, тестировать, документировать и делиться своими API.", "invite_description": "В Hoppscotch мы разработали простой и интуитивно понятный интерфейс для создания и управления вашими API. Hoppscotch - это инструмент, который помогает создавать, тестировать, документировать и делиться своими API.",
"invite_your_friends": "Пригласить своих друзей", "invite_your_friends": "Пригласить своих друзей",
@@ -88,7 +85,7 @@
"reload": "Перезагрузить", "reload": "Перезагрузить",
"search": "Поиск", "search": "Поиск",
"share": "Поделиться", "share": "Поделиться",
"shortcuts": "Горячие клавиши", "shortcuts": "Ярлыки",
"social_description": "Подписывайся на наши соц. сети и оставайся всегда в курсе последних новостей, обновлений и релизов.", "social_description": "Подписывайся на наши соц. сети и оставайся всегда в курсе последних новостей, обновлений и релизов.",
"social_links": "Социальные сети", "social_links": "Социальные сети",
"spotlight": "Прожектор", "spotlight": "Прожектор",
@@ -99,19 +96,17 @@
"type_a_command_search": "Введите команду или выполните поиск…", "type_a_command_search": "Введите команду или выполните поиск…",
"we_use_cookies": "Мы используем куки", "we_use_cookies": "Мы используем куки",
"whats_new": "Что нового?", "whats_new": "Что нового?",
"wiki": "Узнать больше" "wiki": "Вики"
}, },
"auth": { "auth": {
"account_exists": "Учетная запись существует с разными учетными данными - войдите, чтобы связать обе учетные записи", "account_exists": "Учетная запись существует с разными учетными данными - войдите, чтобы связать обе учетные записи",
"all_sign_in_options": "Все варианты входа", "all_sign_in_options": "Все варианты входа",
"continue_with_auth_provider": "Continue with {provider}",
"continue_with_email": "Продолжить с электронной почтой", "continue_with_email": "Продолжить с электронной почтой",
"continue_with_github": "Продолжить с GitHub", "continue_with_github": "Продолжить с GitHub",
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
"continue_with_google": "Продолжить с Google", "continue_with_google": "Продолжить с Google",
"continue_with_microsoft": "Продолжить с Microsoft", "continue_with_microsoft": "Продолжить с Microsoft",
"email": "Электронное письмо", "email": "Электронное письмо",
"logged_out": "Успешно вышли. Будем скучать!", "logged_out": "Вышли из",
"login": "Авторизоваться", "login": "Авторизоваться",
"login_success": "Успешный вход в систему", "login_success": "Успешный вход в систему",
"login_to_hoppscotch": "Войти в Hoppscotch", "login_to_hoppscotch": "Войти в Hoppscotch",
@@ -126,7 +121,7 @@
"generate_token": "Сгенерировать токен", "generate_token": "Сгенерировать токен",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init", "graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Добавить в URL", "include_in_url": "Добавить в URL",
"inherited_from": "Унаследован тип аутентификации {auth} из родительской коллекции {collection}", "inherited_from": "Inherited {auth} from parent collection {collection} ",
"learn": "Узнать больше", "learn": "Узнать больше",
"oauth": { "oauth": {
"redirect_auth_server_returned_error": "Auth Server returned an error state", "redirect_auth_server_returned_error": "Auth Server returned an error state",
@@ -140,32 +135,11 @@
"redirect_no_token_endpoint": "No Token Endpoint 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_oauth_redirect": "Something went wrong during OAuth Redirect",
"something_went_wrong_on_token_generation": "Something went wrong on token generation", "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", "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",
"label_client_id": "Client ID",
"label_client_secret": "Client Secret",
"label_code_challenge": "Code Challenge",
"label_code_challenge_method": "Code Challenge Method",
"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_by_headers_label": "Headers",
"pass_by_query_params_label": "Query Parameters",
"pass_key_by": "Pass by", "pass_key_by": "Pass by",
"password": "Пароль", "password": "Пароль",
"save_to_inherit": "Чтобы унаследовать аутентификации, нужно сохранить запрос в коллекции", "save_to_inherit": "Please save this request in any collection to inherit the authorization",
"token": "Токен", "token": "Токен",
"type": "Метод авторизации", "type": "Метод авторизации",
"username": "Имя пользователя" "username": "Имя пользователя"
@@ -175,7 +149,6 @@
"different_parent": "Нельзя сортировать коллекцию с разной родительской коллекцией", "different_parent": "Нельзя сортировать коллекцию с разной родительской коллекцией",
"edit": "Редактировать коллекцию", "edit": "Редактировать коллекцию",
"import_or_create": "Вы можете импортировать существующую или создать новую коллекцию", "import_or_create": "Вы можете импортировать существующую или создать новую коллекцию",
"import_collection": "Импортировать коллекцию",
"invalid_name": "Укажите допустимое название коллекции", "invalid_name": "Укажите допустимое название коллекции",
"invalid_root_move": "Коллекция уже в корне", "invalid_root_move": "Коллекция уже в корне",
"moved": "Перемещено успешно", "moved": "Перемещено успешно",
@@ -184,36 +157,38 @@
"name_length_insufficient": "Имя коллекции должно иметь 3 или более символов", "name_length_insufficient": "Имя коллекции должно иметь 3 или более символов",
"new": "Создать коллекцию", "new": "Создать коллекцию",
"order_changed": "Порядок коллекции обновлён", "order_changed": "Порядок коллекции обновлён",
"properties": "Параметры коллекции", "properties": "Collection Properties",
"properties_updated": "Параметры коллекции обновлены", "properties_updated": "Collection Properties Updated",
"renamed": "Коллекция переименована", "renamed": "Коллекция переименована",
"request_in_use": "Запрос обрабатывается", "request_in_use": "Запрос обрабатывается",
"save_as": "Сохранить как", "save_as": "Сохранить как",
"save_to_collection": "Сохранить в коллекцию", "save_to_collection": "Сохранить в коллекцию",
"select": "Выбрать коллекцию", "select": "Выбрать коллекцию",
"select_location": "Выберите местоположение" "select_location": "Выберите местоположение",
"select_team": "Выберите команду",
"team_collections": "Коллекции команд"
}, },
"confirm": { "confirm": {
"close_unsaved_tab": "Вы уверены, что хотите закрыть эту вкладку?", "close_unsaved_tab": "Вы уверены что хотите закрыть эту вкладку?",
"close_unsaved_tabs": "Вы уверены, что хотите закрыть все эти вкладки? Несохранённые данные {count} вкладок будут утеряны.", "close_unsaved_tabs": "Вы уверены что хотите закрыть все эти вкладки? Несохранённые данные {count} вкладок будут утеряны.",
"exit_team": "Вы точно хотите покинуть эту команду?", "exit_team": "Вы точно хотите покинуть эту команду?",
"logout": "Вы действительно хотите выйти?", "logout": "Вы действительно хотите выйти?",
"remove_collection": "Вы уверены, что хотите навсегда удалить эту коллекцию?", "remove_collection": "Вы уверены, что хотите навсегда удалить эту коллекцию?",
"remove_environment": "Вы действительно хотите удалить это окружение без возможности восстановления?", "remove_environment": "Вы действительно хотите удалить эту среду без возможности восстановления?",
"remove_folder": "Вы уверены, что хотите навсегда удалить эту папку?", "remove_folder": "Вы уверены, что хотите навсегда удалить эту папку?",
"remove_history": "Вы уверены, что хотите навсегда удалить всю историю?", "remove_history": "Вы уверены, что хотите навсегда удалить всю историю?",
"remove_request": "Вы уверены, что хотите навсегда удалить этот запрос?", "remove_request": "Вы уверены, что хотите навсегда удалить этот запрос?",
"remove_shared_request": "Вы уверены, что хотите навсегда удалить этот запрос?", "remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Вы уверены, что хотите удалить эту команду?", "remove_team": "Вы уверены, что хотите удалить эту команду?",
"remove_telemetry": "Вы действительно хотите отказаться от телеметрии?", "remove_telemetry": "Вы действительно хотите отказаться от телеметрии?",
"request_change": "Вы уверены, что хотите сбросить текущий запрос, все не сохранённые данные будт утеряны?", "request_change": "Вы уверены что хотите сбросить текущий запрос, все не сохранённые данные будт утеряны?",
"save_unsaved_tab": "Вы хотите сохранить изменения в этой вкладке?", "save_unsaved_tab": "Вы хотите сохранить изменения в этой вкладке?",
"sync": "Вы уверены, что хотите синхронизировать это рабочее пространство?" "sync": "Вы уверены, что хотите синхронизировать это рабочее пространство?"
}, },
"context_menu": { "context_menu": {
"add_parameters": "Добавить в список параметров", "add_parameters": "Add to parameters",
"open_request_in_new_tab": "Открыть запрос в новом окне", "open_request_in_new_tab": "Open request in new tab",
"set_environment_variable": "Добавить значение в переменную" "set_environment_variable": "Set as variable"
}, },
"cookies": { "cookies": {
"modal": { "modal": {
@@ -252,25 +227,24 @@
"collections": "Коллекции пустые", "collections": "Коллекции пустые",
"documentation": "Подключите GraphQL endpoint, чтобы увидеть документацию.", "documentation": "Подключите GraphQL endpoint, чтобы увидеть документацию.",
"endpoint": "Endpoint не может быть пустым", "endpoint": "Endpoint не может быть пустым",
"environments": "Переменных окружения нет", "environments": "Окружения пусты",
"folder": "Папка пуста", "folder": "Папка пуста",
"headers": "У этого запроса нет заголовков", "headers": "У этого запроса нет заголовков",
"history": "История пуста", "history": "История пуста",
"invites": "Вы еще никого не приглашали", "invites": "Вы еще никого не приглашали",
"members": "В этой команде еще нет участников", "members": "В этой команде еще нет участников",
"parameters": "Этот запрос не содержит параметров", "parameters": "Этот запрос не имеет параметров",
"pending_invites": "Пока что нет ожидающих заявок на вступление в команду", "pending_invites": "Пока что нет ожидающих заявок на вступление в команду",
"profile": "Войдите, чтобы просмотреть свой профиль", "profile": "Войдите, чтобы просмотреть свой профиль",
"protocols": "Протоколы пустые", "protocols": "Протоколы пустые",
"request_variables": "Этот запрос не содержит никаких переменных",
"secret_environments": "Секреты хранятся только на этом устройстве и не синхронизируются с сервером",
"schema": "Подключиться к конечной точке GraphQL", "schema": "Подключиться к конечной точке GraphQL",
"shared_requests": "Вы еще не делились запросами с другими", "shared_requests": "Shared requests are empty",
"shared_requests_logout": "Нужно войти, чтобы делиться запросами и управлять ими", "shared_requests_logout": "Login to view your shared requests or create a new one",
"subscription": "Нет подписок", "subscription": "Нет подписок",
"team_name": "Название команды пусто", "team_name": "Название команды пусто",
"teams": "Команды пустые", "teams": "Команды пустые",
"tests": "Для этого запроса нет тестов" "tests": "Для этого запроса нет тестов",
"shortcodes": "Нет коротких ссылок"
}, },
"environment": { "environment": {
"add_to_global": "Добавить в глобальное окружение", "add_to_global": "Добавить в глобальное окружение",
@@ -278,57 +252,53 @@
"create_new": "Создать новое окружение", "create_new": "Создать новое окружение",
"created": "Окружение создано", "created": "Окружение создано",
"deleted": "Окружение удалено", "deleted": "Окружение удалено",
"duplicated": "Окружение продублировано", "duplicated": "Environment duplicated",
"edit": "Редактировать окружение", "edit": "Редактировать окружение",
"empty_variables": "Переменные еще не добавлены", "empty_variables": "No variables",
"global": "Global", "global": "Global",
"global_variables": "Глобальные переменные", "global_variables": "Global variables",
"import_or_create": "Импортировать или создать новое окружение", "import_or_create": "Импортировать или создать новое окружение",
"invalid_name": "Укажите допустимое имя для окружения", "invalid_name": "Укажите допустимое имя для окружения",
"list": "Переменные окружения", "list": "Переменные окружения",
"my_environments": "Мои окружения", "my_environments": "Мои окружения",
"name": "Имя", "name": "Name",
"nested_overflow": "максимальный уровень вложения переменных окружения - 10", "nested_overflow": "максимальный уровень вложения переменных окружения - 10",
"new": "Новая среда", "new": "Новая среда",
"no_active_environment": "Нет активных окружений", "no_active_environment": "Нет активных окружений",
"no_environment": "Нет окружения", "no_environment": "Нет окружения",
"no_environment_description": "Не выбрано окружение, выберите что делать с переменными.", "no_environment_description": "Не выбрано окружение, выберите что делать с переменными.",
"quick_peek": "Быстрый просмотр переменных", "quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable", "replace_with_variable": "Replace with variable",
"scope": "Scope", "scope": "Scope",
"secrets": "Секретные переменные",
"secret_value": "Секретное значение",
"select": "Выберите среду", "select": "Выберите среду",
"set": "Выбрать окружение", "set": "Set environment",
"set_as_environment": "Поместить значение в переменную", "set_as_environment": "Set as environment",
"team_environments": "Окружения команды", "team_environments": "Окружения команды",
"title": "Окружения", "title": "Окружения",
"updated": "Окружение обновлено", "updated": "Окружение обновлено",
"value": "Значение", "value": "Value",
"variable": "Переменная", "variable": "Variable",
"variables": "Переменные",
"variable_list": "Список переменных" "variable_list": "Список переменных"
}, },
"error": { "error": {
"authproviders_load_error": "Unable to load auth providers", "authproviders_load_error": "Unable to load auth providers",
"browser_support_sse": "Похоже, в этом браузере нет поддержки событий, отправленных сервером.", "browser_support_sse": "Похоже, в этом браузере нет поддержки событий, отправленных сервером.",
"check_console_details": "Подробности смотрите в журнале консоли.", "check_console_details": "Подробности смотрите в журнале консоли.",
"check_how_to_add_origin": "Инструкция как это сделать", "check_how_to_add_origin": "Инструкция как добавить origin в настройки расширения",
"curl_invalid_format": "cURL неправильно отформатирован", "curl_invalid_format": "cURL неправильно отформатирован",
"danger_zone": "Опасная зона", "danger_zone": "Опасная зона",
"delete_account": "Вы являетесь владельцем этой команды:", "delete_account": "Вы являетесь владельцем этой команды:",
"delete_account_description": "Прежде чем удалить аккаунт вам необходимо либо назначить владельцом другого пользователя, либо удалить команды в которых вы являетесь владельцем.", "delete_account_description": "Прежде чем удалить аккаунт вам необходимо либо назначить владельцом другого пользователя, либо удалить команды в которых вы являетесь владельцем.",
"empty_profile_name": "Имя пользователя не может быть пустым",
"empty_req_name": "Пустое имя запроса", "empty_req_name": "Пустое имя запроса",
"f12_details": "(F12 для подробностей)", "f12_details": "(F12 для подробностей)",
"gql_prettify_invalid_query": "Не удалось отформатировать, т.к. в запросе есть синтаксические ошибки. Устраните их и повторите попытку.", "gql_prettify_invalid_query": "Не удалось определить недопустимый запрос, устранить синтаксические ошибки запроса и повторить попытку.",
"incomplete_config_urls": "Не заполнены URL конфигурации", "incomplete_config_urls": "Не заполнены URL конфигурации",
"incorrect_email": "Не корректный Email", "incorrect_email": "Не корректный Email",
"invalid_link": "Не корректная ссылка", "invalid_link": "Не корректная ссылка",
"invalid_link_description": "Ссылка, по которой вы перешли, - недействительна, либо срок ее действия истек.", "invalid_link_description": "Ссылка, по которой вы перешли, - недействительна, либо срок ее действия истек.",
"invalid_embed_link": "The embed does not exist or is invalid.", "invalid_embed_link": "The embed does not exist or is invalid.",
"json_parsing_failed": "Не корректный JSON", "json_parsing_failed": "Не корректный JSON",
"json_prettify_invalid_body": "Не удалось определить формат строки, устраните синтаксические ошибки и повторите попытку.", "json_prettify_invalid_body": "Не удалось определить недопустимое тело, устранить синтаксические ошибки json и повторить попытку.",
"network_error": "Похоже, возникла проблема с соединением. Попробуйте еще раз.", "network_error": "Похоже, возникла проблема с соединением. Попробуйте еще раз.",
"network_fail": "Не удалось отправить запрос", "network_fail": "Не удалось отправить запрос",
"no_collections_to_export": "Нечего экспортировать. Для начала нужно создать коллекцию.", "no_collections_to_export": "Нечего экспортировать. Для начала нужно создать коллекцию.",
@@ -336,10 +306,8 @@
"no_environments_to_export": "Нечего экспортировать. Для начала нужно создать переменные окружения.", "no_environments_to_export": "Нечего экспортировать. Для начала нужно создать переменные окружения.",
"no_results_found": "Совпадения не найдены", "no_results_found": "Совпадения не найдены",
"page_not_found": "Эта страница не найдена", "page_not_found": "Эта страница не найдена",
"please_install_extension": "Ничего страшного. Просто нужно установить специальное расширение в браузере.", "please_install_extension": "Нужно установить специальное расширение и добавить этот домен как новый origin в настройках расширения.",
"proxy_error": "Proxy error", "proxy_error": "Proxy error",
"reading_files": "Произошла ошибка при чтении файла или нескольких файлов",
"same_profile_name": "Задано имя пользователя такое же как и было",
"script_fail": "Не удалось выполнить сценарий предварительного запроса", "script_fail": "Не удалось выполнить сценарий предварительного запроса",
"something_went_wrong": "Что-то пошло не так", "something_went_wrong": "Что-то пошло не так",
"test_script_fail": "Не удалось выполнить тестирование запроса" "test_script_fail": "Не удалось выполнить тестирование запроса"
@@ -347,12 +315,13 @@
"export": { "export": {
"as_json": "Экспорт как JSON", "as_json": "Экспорт как JSON",
"create_secret_gist": "Создать секретный Gist", "create_secret_gist": "Создать секретный Gist",
"create_secret_gist_tooltip_text": "Экспортировать как секретный Gist", "create_secret_gist_tooltip_text": "Export as secret Gist",
"failed": "Произошла ошибка во время экспорта", "failed": "Something went wrong while exporting",
"secret_gist_success": "Успешно экспортировано как секретный Gist", "secret_gist_success": "Successfully exported as secret Gist",
"require_github": "Войдите через GitHub, чтобы создать секретную суть", "require_github": "Войдите через GitHub, чтобы создать секретную суть",
"title": "Экспорт", "title": "Экспорт",
"success": "Успешно экспортировано" "success": "Successfully exported",
"gist_created": "Gist создан"
}, },
"filter": { "filter": {
"all": "Все", "all": "Все",
@@ -377,7 +346,7 @@
"switch_connection": "Изменить соединение" "switch_connection": "Изменить соединение"
}, },
"graphql_collections": { "graphql_collections": {
"title": "Коллекции GraphQL" "title": "GraphQL Collections"
}, },
"group": { "group": {
"time": "Время", "time": "Время",
@@ -390,8 +359,8 @@
}, },
"helpers": { "helpers": {
"authorization": "Заголовок авторизации будет автоматически сгенерирован при отправке запроса.", "authorization": "Заголовок авторизации будет автоматически сгенерирован при отправке запроса.",
"collection_properties_authorization": "Этот заголовок авторизации будет подставляться при каждом запросе в этой коллекции.", "collection_properties_authorization": " This authorization will be set for every request in this collection.",
"collection_properties_header": "Этот заголовок будет подставляться при каждом запросе в этой коллекции.", "collection_properties_header": "This header will be set for every request in this collection.",
"generate_documentation_first": "Сначала создайте документацию", "generate_documentation_first": "Сначала создайте документацию",
"network_fail": "Невозможно достичь конечной точки API. Проверьте подключение к сети и попробуйте еще раз.", "network_fail": "Невозможно достичь конечной точки API. Проверьте подключение к сети и попробуйте еще раз.",
"offline": "Кажется, вы не в сети. Данные в этой рабочей области могут быть устаревшими.", "offline": "Кажется, вы не в сети. Данные в этой рабочей области могут быть устаревшими.",
@@ -411,12 +380,10 @@
"import": { "import": {
"collections": "Импортировать коллекции", "collections": "Импортировать коллекции",
"curl": "Импортировать из cURL", "curl": "Импортировать из cURL",
"environments_from_gist": "Импортировать из Gist", "environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Импортировать переменные окружения Hoppscotch из Gist", "environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"failed": "Ошибка импорта", "failed": "Ошибка импорта",
"file_size_limit_exceeded_warning_multiple_files": "Выбранные файлы превышают рекомендованный лимит в 10MB. Были импортированы только первые {files}", "from_file": "Import from File",
"file_size_limit_exceeded_warning_single_file": "Размер выбранного в данный момент файла превышает рекомендуемый лимит в 10 МБ. Пожалуйста, выберите другой файл.",
"from_file": "Импортировать из одного или нескольких файлов",
"from_gist": "Импорт из Gist", "from_gist": "Импорт из Gist",
"from_gist_description": "Импортировать через Gist URL", "from_gist_description": "Импортировать через Gist URL",
"from_insomnia": "Импортировать с Insomnia", "from_insomnia": "Импортировать с Insomnia",
@@ -431,9 +398,9 @@
"from_postman_description": "Импортировать из коллекции Postman", "from_postman_description": "Импортировать из коллекции Postman",
"from_url": "Импортировать из URL", "from_url": "Импортировать из URL",
"gist_url": "Введите URL-адрес Gist", "gist_url": "Введите URL-адрес Gist",
"gql_collections_from_gist_description": "Импортировать GraphQL коллекцию из Gist", "gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
"hoppscotch_environment": "Hoppscotch Environment", "hoppscotch_environment": "Hoppscotch Environment",
"hoppscotch_environment_description": "Импортировать окружение Hoppscotch из JSON файла", "hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
"import_from_url_invalid_fetch": "Не удалить получить данные по этому URL", "import_from_url_invalid_fetch": "Не удалить получить данные по этому URL",
"import_from_url_invalid_file_format": "Ошибка при импорте коллекций", "import_from_url_invalid_file_format": "Ошибка при импорте коллекций",
"import_from_url_invalid_type": "Неподдерживаемый тип. Поддерживаемые типы: 'hoppscotch', 'openapi', 'postman', 'insomnia'", "import_from_url_invalid_type": "Неподдерживаемый тип. Поддерживаемые типы: 'hoppscotch', 'openapi', 'postman', 'insomnia'",
@@ -442,19 +409,16 @@
"json_description": "Импортировать из коллекции Hoppscotch", "json_description": "Импортировать из коллекции Hoppscotch",
"postman_environment": "Postman Environment", "postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment from a JSON file", "postman_environment_description": "Import Postman Environment from a JSON file",
"success": "Успешно импортировано",
"title": "Импортировать" "title": "Импортировать"
}, },
"inspections": { "inspections": {
"description": "Показать возможные ошибки", "description": "Inspect possible errors",
"environment": { "environment": {
"add_environment": "Добавить переменную", "add_environment": "Add to Environment",
"add_environment_value": "Заполнить значение", "not_found": "Environment variable “{environment}” not found."
"empty_value": "Значение переменной окружения '{variable}' пустое",
"not_found": "Переменная окружения “{environment}” не задана."
}, },
"header": { "header": {
"cookie": "Из-за ограничений безопасности в веб версии нельзя задать Cookie параметры. Пожалуйста, используйте Hoppscotch Desktop приложение или используйте заголовок Authorization вместо этого." "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."
}, },
"response": { "response": {
"401_error": "Please check your authentication credentials.", "401_error": "Please check your authentication credentials.",
@@ -463,12 +427,12 @@
"default_error": "Please check your request.", "default_error": "Please check your request.",
"network_error": "Please check your network connection." "network_error": "Please check your network connection."
}, },
"title": "Помощник", "title": "Inspector",
"url": { "url": {
"extension_not_installed": "Расширение не установлено.", "extension_not_installed": "Extension not installed.",
"extension_unknown_origin": "Убедитесь, что текущий домен добавлен в список доверенных ресурсов в расширении браузера", "extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
"extention_enable_action": "Подключить расширение", "extention_enable_action": "Enable Browser Extension",
"extention_not_enabled": "Расширение в браузере не подключено." "extention_not_enabled": "Extension not enabled."
} }
}, },
"layout": { "layout": {
@@ -481,11 +445,11 @@
"modal": { "modal": {
"close_unsaved_tab": "У вас есть не сохранённые изменения", "close_unsaved_tab": "У вас есть не сохранённые изменения",
"collections": "Коллекции", "collections": "Коллекции",
"confirm": "Подтвердите действие", "confirm": "Подтверждать",
"customize_request": "Customize Request", "customize_request": "Customize Request",
"edit_request": "Изменить запрос", "edit_request": "Изменить запрос",
"import_export": "Импорт Экспорт", "import_export": "Импорт Экспорт",
"share_request": "Поделиться запросом" "share_request": "Share Request"
}, },
"mqtt": { "mqtt": {
"already_subscribed": "Вы уже подписаны на этот топик", "already_subscribed": "Вы уже подписаны на этот топик",
@@ -521,7 +485,7 @@
"doc": "Документы", "doc": "Документы",
"graphql": "GraphQL", "graphql": "GraphQL",
"profile": "Профиль", "profile": "Профиль",
"realtime": "Realtime", "realtime": "В реальном времени",
"rest": "REST", "rest": "REST",
"settings": "Настройки" "settings": "Настройки"
}, },
@@ -543,8 +507,8 @@
"roles": "Роли", "roles": "Роли",
"roles_description": "Роли позволяют настраивать доступ конкретным людям к публичным коллекциям.", "roles_description": "Роли позволяют настраивать доступ конкретным людям к публичным коллекциям.",
"updated": "Профиль обновлен", "updated": "Профиль обновлен",
"viewer": "Читатель", "viewer": "Зритель",
"viewer_description": "Могут только просматривать и использовать запросы." "viewer_description": "Зрительно могут только просматривать и использовать запросы."
}, },
"remove": { "remove": {
"star": "Удалить звезду" "star": "Удалить звезду"
@@ -566,11 +530,11 @@
"enter_curl": "Введите сюда команду cURL", "enter_curl": "Введите сюда команду cURL",
"generate_code": "Сгенерировать код", "generate_code": "Сгенерировать код",
"generated_code": "Сгенерированный код", "generated_code": "Сгенерированный код",
"go_to_authorization_tab": "Перейти на вкладку авторизации", "go_to_authorization_tab": "Go to Authorization",
"go_to_body_tab": "Перейти на вкладку тела запроса", "go_to_body_tab": "Go to Body tab",
"header_list": "Список заголовков", "header_list": "Список заголовков",
"invalid_name": "Укажите имя для запроса", "invalid_name": "Укажите имя для запроса",
"method": "Метод", "method": "Методика",
"moved": "Запрос перемещён", "moved": "Запрос перемещён",
"name": "Имя запроса", "name": "Имя запроса",
"new": "Новый запрос", "new": "Новый запрос",
@@ -584,22 +548,22 @@
"payload": "Полезная нагрузка", "payload": "Полезная нагрузка",
"query": "Запрос", "query": "Запрос",
"raw_body": "Необработанное тело запроса", "raw_body": "Необработанное тело запроса",
"rename": "Переименовать запрос", "rename": "Переименость запрос",
"renamed": "Запрос переименован", "renamed": "Запрос переименован",
"request_variables": "Переменные запроса",
"run": "Запустить", "run": "Запустить",
"save": "Сохранить", "save": "Сохранить",
"save_as": "Сохранить как", "save_as": "Сохранить как",
"saved": "Запрос сохранен", "saved": "Запрос сохранен",
"share": "Поделиться", "share": "Делиться",
"share_description": "Поделиться Hoppscotch с друзьями", "share_description": "Поделиться Hoppscotch с друзьями",
"share_request": "Поделиться запросом", "share_request": "Share Request",
"stop": "Стоп", "stop": "Stop",
"title": "Запрос", "title": "Запрос",
"type": "Тип запроса", "type": "Тип запроса",
"url": "URL", "url": "URL",
"variables": "Переменные", "variables": "Переменные",
"view_my_links": "Посмотреть мои ссылки" "view_my_links": "Посмотреть мои ссылки",
"copy_link": "Копировать ссылку"
}, },
"response": { "response": {
"audio": "Аудио", "audio": "Аудио",
@@ -622,7 +586,7 @@
}, },
"settings": { "settings": {
"accent_color": "Основной цвет", "accent_color": "Основной цвет",
"account": "Аккаунт", "account": "Счет",
"account_deleted": "Ваш аккаунт был удалён", "account_deleted": "Ваш аккаунт был удалён",
"account_description": "Настройте параметры своей учетной записи.", "account_description": "Настройте параметры своей учетной записи.",
"account_email_description": "Ваш основной адрес электронной почты.", "account_email_description": "Ваш основной адрес электронной почты.",
@@ -675,29 +639,29 @@
"verify_email": "Подтвердить Email" "verify_email": "Подтвердить Email"
}, },
"shared_requests": { "shared_requests": {
"button": "Кнопка", "button": "Button",
"button_info": "Создать кнопку 'Run in Hoppscotch' на свой сайт, блог или README.", "button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
"copy_html": "Копировать HTML код", "copy_html": "Copy HTML",
"copy_link": "Копировать ссылку", "copy_link": "Copy Link",
"copy_markdown": "Копировать Markdown", "copy_markdown": "Copy Markdown",
"creating_widget": "Создание виджет", "creating_widget": "Creating widget",
"customize": "Настроить", "customize": "Customize",
"deleted": "Запрос удален", "deleted": "Shared request deleted",
"description": "Выберите вид как вы поделитесь запросом, позже вы сможете дополнительно его настроить", "description": "Select a widget, you can change and customize this later",
"embed": "Встраиваемое окно", "embed": "Embed",
"embed_info": "Добавьте небольшую площадку 'Hoppscotch API Playground' на свой веб-сайт, блог или документацию.", "embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
"link": "Ссылка", "link": "Link",
"link_info": "Создайте общедоступную ссылку, которой можно поделиться с любым пользователем, имеющим доступ к просмотру.", "link_info": "Create a shareable link to share with anyone on the internet with view access.",
"modified": "Запрос изменен", "modified": "Shared request modified",
"not_found": "Такой ссылке не нашлось", "not_found": "Shared request not found",
"open_new_tab": "Открыть в новом окне", "open_new_tab": "Open in new tab",
"preview": "Preview", "preview": "Preview",
"run_in_hoppscotch": "Run in Hoppscotch", "run_in_hoppscotch": "Run in Hoppscotch",
"theme": { "theme": {
"dark": "Темная", "dark": "Dark",
"light": "Светлая", "light": "Light",
"system": "Системная", "system": "System",
"title": "Тема" "title": "Theme"
} }
}, },
"shortcut": { "shortcut": {
@@ -705,7 +669,7 @@
"close_current_menu": "Закрыть текущее меню", "close_current_menu": "Закрыть текущее меню",
"command_menu": "Меню поиска и команд", "command_menu": "Меню поиска и команд",
"help_menu": "Меню помощи", "help_menu": "Меню помощи",
"show_all": "Список горячих клавиш", "show_all": "Горячие клавиши",
"title": "Общий" "title": "Общий"
}, },
"miscellaneous": { "miscellaneous": {
@@ -732,19 +696,20 @@
"get_method": "Выберите метод GET", "get_method": "Выберите метод GET",
"head_method": "Выберите метод HEAD", "head_method": "Выберите метод HEAD",
"import_curl": "Импортировать из cURL", "import_curl": "Импортировать из cURL",
"method": "Метод", "method": "Методика",
"next_method": "Выберите следующий метод", "next_method": "Выберите следующий метод",
"post_method": "Выберите метод POST", "post_method": "Выберите метод POST",
"previous_method": "Выбрать предыдущий метод", "previous_method": "Выбрать предыдущий метод",
"put_method": "Выберите метод PUT", "put_method": "Выберите метод PUT",
"rename": "Переименовать запрос", "rename": "Переименовать запрос",
"reset_request": "Сбросить запрос", "reset_request": "Сбросить запрос",
"save_request": "Сохранить запрос", "save_request": "Сохарнить запрос",
"save_to_collections": "Сохранить в коллекции", "save_to_collections": "Сохранить в коллекции",
"send_request": "Послать запрос", "send_request": "Послать запрос",
"share_request": "Поделиться запросом", "share_request": "Share Request",
"show_code": "Сгенерировать фрагмент кода из запроса", "show_code": "Generate code snippet",
"title": "Запрос" "title": "Запрос",
"copy_request_link": "Копировать ссылку на запрос"
}, },
"response": { "response": {
"copy": "Копировать запрос в буфер обмена", "copy": "Копировать запрос в буфер обмена",
@@ -752,11 +717,11 @@
"title": "Запрос" "title": "Запрос"
}, },
"theme": { "theme": {
"black": "Переключить на черный режим", "black": "Черный режим",
"dark": "Переключить на тёмный режим", "dark": "Тёмный режим",
"light": "Переключить на светлый режим", "light": "Светлый режим",
"system": "Переключить на тему, исходя из настроек системы", "system": "Определяется системой",
"title": "Внешний вид" "title": "Тема"
} }
}, },
"show": { "show": {
@@ -765,11 +730,6 @@
"more": "Показать больше", "more": "Показать больше",
"sidebar": "Показать боковую панель" "sidebar": "Показать боковую панель"
}, },
"site_protection": {
"error_fetching_site_protection_status": "Something Went Wrong While Fetching Site Protection Status",
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to access this Hoppscotch Enterprise Instance."
},
"socketio": { "socketio": {
"communication": "Коммуникация", "communication": "Коммуникация",
"connection_not_authorized": "Это SocketIO соединение не использует какую-либо авторизацию.", "connection_not_authorized": "Это SocketIO соединение не использует какую-либо авторизацию.",
@@ -779,89 +739,82 @@
"url": "URL" "url": "URL"
}, },
"spotlight": { "spotlight": {
"change_language": "Изменить язык", "change_language": "Change Language",
"environments": { "environments": {
"delete": "Удалить текущее окружение", "delete": "Delete current environment",
"duplicate": "Дублировать текущее окружение", "duplicate": "Duplicate current environment",
"duplicate_global": "Дублировать глобальное окружение", "duplicate_global": "Duplicate global environment",
"edit": "Редактировать текущее окружение", "edit": "Edit current environment",
"edit_global": "Редактировать глобальное окружение", "edit_global": "Edit global environment",
"new": "Создать новое окружение", "new": "Create new environment",
"new_variable": "Создать новую переменную окружения", "new_variable": "Create a new environment variable",
"title": "Окружение" "title": "Environments"
}, },
"general": { "general": {
"chat": "Чат с поддержкой", "chat": "Chat with support",
"help_menu": "Помощь", "help_menu": "Help and support",
"open_docs": "Почитать документацию", "open_docs": "Read Documentation",
"open_github": "Открыть GitHub репозиторий", "open_github": "Open GitHub repository",
"open_keybindings": "Горячие клавиши", "open_keybindings": "Keyboard shortcuts",
"social": "Соц. сети", "social": "Social",
"title": "Общее" "title": "General"
}, },
"graphql": { "graphql": {
"connect": "Подключиться к серверу", "connect": "Connect to server",
"disconnect": "Отключиться от сервера" "disconnect": "Disconnect from server"
}, },
"miscellaneous": { "miscellaneous": {
"invite": "Пригласить друзей в Hoppscotch", "invite": "Invite your friends to Hoppscotch",
"title": "Другое" "title": "Miscellaneous"
},
"phrases": {
"create_environment": "Создать окружение",
"create_workspace": "Создать пространство",
"import_collections": "Импортировать коллекцию",
"share_request": "Поделиться запросом",
"try": "Попробовать"
}, },
"request": { "request": {
"save_as_new": "Сохранить как новый запрос", "save_as_new": "Save as new request",
"select_method": "Выбрать метод", "select_method": "Select method",
"switch_to": "Переключиться", "switch_to": "Switch to",
"tab_authorization": "На вкладку авторизации", "tab_authorization": "Authorization tab",
"tab_body": "На вкладку тела запроса", "tab_body": "Body tab",
"tab_headers": "На вкладку заголовков", "tab_headers": "Headers tab",
"tab_parameters": "На вкладку параметров", "tab_parameters": "Parameters tab",
"tab_pre_request_script": "На вкладку пред-скрипта запроса", "tab_pre_request_script": "Pre-request script tab",
"tab_query": "На вкладку запроса", "tab_query": "Query tab",
"tab_tests": "На вкладку тестов", "tab_tests": "Tests tab",
"tab_variables": "На вкладку переменных запроса" "tab_variables": "Variables tab"
}, },
"response": { "response": {
"copy": "Копировать содержимое ответа", "copy": "Copy response",
"download": "Сказать содержимое ответа как файл", "download": "Download response as file",
"title": "Ответ запроса" "title": "Response"
}, },
"section": { "section": {
"interceptor": "Перехватчик", "interceptor": "Interceptor",
"interface": "Интерфейс", "interface": "Interface",
"theme": "Внешний вид", "theme": "Theme",
"user": "Пользователь" "user": "User"
}, },
"settings": { "settings": {
"change_interceptor": "Изменить перехватчик", "change_interceptor": "Change Interceptor",
"change_language": "Изменить язык", "change_language": "Change Language",
"theme": { "theme": {
"black": "Черная", "black": "Black",
"dark": "Темная", "dark": "Dark",
"light": "Светлая", "light": "Light",
"system": "Как задано в системе" "system": "System preference"
} }
}, },
"tab": { "tab": {
"close_current": "Закрыть текущую вкладку", "close_current": "Close current tab",
"close_others": "Закрыть все вкладки", "close_others": "Close all other tabs",
"duplicate": "Продублировать текущую вкладку", "duplicate": "Duplicate current tab",
"new_tab": "Открыть в новой вкладке", "new_tab": "Open a new tab",
"title": "Вкладки" "title": "Tabs"
}, },
"workspace": { "workspace": {
"delete": "Удалить текущую команду", "delete": "Delete current team",
"edit": "Редактировать текущую команду", "edit": "Edit current team",
"invite": "Пригласить людей в команду", "invite": "Invite people to team",
"new": "Создать новую команду", "new": "Create new team",
"switch_to_personal": "Переключить на персональное пространство", "switch_to_personal": "Switch to your personal workspace",
"title": "Команды" "title": "Teams"
} }
}, },
"sse": { "sse": {
@@ -871,7 +824,7 @@
}, },
"state": { "state": {
"bulk_mode": "Множественное редактирование", "bulk_mode": "Множественное редактирование",
"bulk_mode_placeholder": "Каждый параметр должен начинаться с новой строки\nКлючи и значения разделяются двоеточием\nИспользуйте # для комментария", "bulk_mode_placeholder": "Каждый параметр должен начинаться с новой строки\nКлючи и значения разедляются двоеточием\nИспользуйте # для комментария",
"cleared": "Очищено", "cleared": "Очищено",
"connected": "Связаны", "connected": "Связаны",
"connected_to": "Подключено к {name}", "connected_to": "Подключено к {name}",
@@ -890,20 +843,20 @@
"download_failed": "Download failed", "download_failed": "Download failed",
"download_started": "Скачивание началось", "download_started": "Скачивание началось",
"enabled": "Включено", "enabled": "Включено",
"file_imported": "Файл успешно импортирован", "file_imported": "Файл импортирован",
"finished_in": "Завершено через {duration} мс", "finished_in": "Завершено через {duration} мс",
"hide": "Скрыть", "hide": "Hide",
"history_deleted": "История удалена", "history_deleted": "История удалена",
"linewrap": "Обернуть линии", "linewrap": "Обернуть линии",
"loading": "Загрузка...", "loading": "Загрузка...",
"message_received": "Сообщение: {message} получено по топику: {topic}", "message_received": "Сообщение: {message} получено по топику: {topic}",
"mqtt_subscription_failed": "Что-то пошло не так, при попытке подписаться на топик: {topic}", "mqtt_subscription_failed": "Что-то пошло не так, при попытке подписаться на топик: {topic}",
"none": "Не задан", "none": "Никто",
"nothing_found": "Ничего не найдено для", "nothing_found": "Ничего не найдено для",
"published_error": "Что-то пошло не так при попытке опубликовать сообщение в топик {topic}: {message}", "published_error": "Что-то пошло не так при попытке опубликовать сообщение в топик {topic}: {message}",
"published_message": "Опубликовано сообщение: {message} в топик: {topic}", "published_message": "Опубликовано сообщение: {message} в топик: {topic}",
"reconnection_error": "Не удалось переподключиться", "reconnection_error": "Не удалось переподключиться",
"show": "Показать", "show": "Show",
"subscribed_failed": "Не удалось подписаться на топик: {topic}", "subscribed_failed": "Не удалось подписаться на топик: {topic}",
"subscribed_success": "Успешно подписался на топик: {topic}", "subscribed_success": "Успешно подписался на топик: {topic}",
"unsubscribed_failed": "Не удалось отписаться от топика: {topic}", "unsubscribed_failed": "Не удалось отписаться от топика: {topic}",
@@ -918,6 +871,7 @@
"forum": "Задавайте вопросы и получайте ответы", "forum": "Задавайте вопросы и получайте ответы",
"github": "Подпишитесь на нас на Github", "github": "Подпишитесь на нас на Github",
"shortcuts": "Просматривайте приложение быстрее", "shortcuts": "Просматривайте приложение быстрее",
"team": "Свяжитесь с командой",
"title": "Служба поддержки", "title": "Служба поддержки",
"twitter": "Следуйте за нами на Twitter" "twitter": "Следуйте за нами на Twitter"
}, },
@@ -928,7 +882,7 @@
"close_others": "Закрыть остальные вкладки", "close_others": "Закрыть остальные вкладки",
"collections": "Коллекции", "collections": "Коллекции",
"documentation": "Документация", "documentation": "Документация",
"duplicate": "Дублировать вкладку", "duplicate": "Duplicate Tab",
"environments": "Окружения", "environments": "Окружения",
"headers": "Заголовки", "headers": "Заголовки",
"history": "История", "history": "История",
@@ -938,8 +892,7 @@
"queries": "Запросы", "queries": "Запросы",
"query": "Запрос", "query": "Запрос",
"schema": "Схема", "schema": "Схема",
"shared_requests": "Запросы в общем доступе", "shared_requests": "Shared Requests",
"share_tab_request": "Поделиться запросом",
"socketio": "Socket.IO", "socketio": "Socket.IO",
"sse": "SSE", "sse": "SSE",
"tests": "Тесты", "tests": "Тесты",
@@ -968,6 +921,7 @@
"invite_tooltip": "Пригласить людей в Ваше рабочее пространство", "invite_tooltip": "Пригласить людей в Ваше рабочее пространство",
"invited_to_team": "{owner} приглашает Вас присоединиться к команде {team}", "invited_to_team": "{owner} приглашает Вас присоединиться к команде {team}",
"join": "Приглашение принято", "join": "Приглашение принято",
"join_beta": "Присоединяйтесь к бета-программе, чтобы получить доступ к командам.",
"join_team": "Присоединиться к {team}", "join_team": "Присоединиться к {team}",
"joined_team": "Вы присоединились к команде {team}", "joined_team": "Вы присоединились к команде {team}",
"joined_team_description": "Теперь Вы участник этой команды", "joined_team_description": "Теперь Вы участник этой команды",
@@ -996,7 +950,6 @@
"permissions": "Разрешения", "permissions": "Разрешения",
"same_target_destination": "Таже цель и конечная точка", "same_target_destination": "Таже цель и конечная точка",
"saved": "Команда сохранена", "saved": "Команда сохранена",
"search_title": "Team Requests",
"select_a_team": "Выбрать команду", "select_a_team": "Выбрать команду",
"success_invites": "Принятые приглашения", "success_invites": "Принятые приглашения",
"title": "Команды", "title": "Команды",
@@ -1028,8 +981,16 @@
"workspace": { "workspace": {
"change": "Изменить пространство", "change": "Изменить пространство",
"personal": "Моё пространство", "personal": "Моё пространство",
"other_workspaces": "Пространства",
"team": "Пространство команды", "team": "Пространство команды",
"title": "Рабочие пространства" "title": "Рабочие пространства"
},
"shortcodes": {
"actions": "Действия",
"created_on": "Создано",
"deleted": "Удалёна",
"method": "Метод",
"not_found": "Короткая ссылка не найдена",
"short_code": "Короткая ссылка",
"url": "URL"
} }
} }

View File

@@ -68,9 +68,9 @@
"developer_option": "Developer options", "developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.", "developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
"discord": "Discord", "discord": "Discord",
"documentation": "Dokümanlar", "documentation": "Dökümanlar",
"github": "GitHub", "github": "GitHub",
"help": "Yardım, geri bildirim ve dokümanlar", "help": "Yardım, geri bildirim ve dökümanlar",
"home": "Ana sayfa", "home": "Ana sayfa",
"invite": "Davet et", "invite": "Davet et",
"invite_description": "Hoppscotch'ta API'lerinizi oluşturmak ve yönetmek için basit ve sezgisel bir arayüz tasarladık. Hoppscotch, API'lerinizi oluşturmanıza, test etmenize, belgelemenize ve paylaşmanıza yardımcı olan bir araçtır.", "invite_description": "Hoppscotch'ta API'lerinizi oluşturmak ve yönetmek için basit ve sezgisel bir arayüz tasarladık. Hoppscotch, API'lerinizi oluşturmanıza, test etmenize, belgelemenize ve paylaşmanıza yardımcı olan bir araçtır.",
@@ -225,7 +225,7 @@
"body": "Bu isteğin bir gövdesi yok", "body": "Bu isteğin bir gövdesi yok",
"collection": "Koleksiyon boş", "collection": "Koleksiyon boş",
"collections": "Koleksiyonlar boş", "collections": "Koleksiyonlar boş",
"documentation": "Dokümanları görmek için GraphQL uç noktasını bağlayın", "documentation": "Dökümanları görmek için GraphQL uç noktasını bağlayın",
"endpoint": "Uç nokta boş olamaz", "endpoint": "Uç nokta boş olamaz",
"environments": "Ortamlar boş", "environments": "Ortamlar boş",
"folder": "Klasör boş", "folder": "Klasör boş",
@@ -735,7 +735,7 @@
"url": "Bağlantı" "url": "Bağlantı"
}, },
"spotlight": { "spotlight": {
"change_language": "Dil Değiştir", "change_language": "Change Language",
"environments": { "environments": {
"delete": "Delete current environment", "delete": "Delete current environment",
"duplicate": "Duplicate current environment", "duplicate": "Duplicate current environment",
@@ -744,7 +744,7 @@
"edit_global": "Edit global environment", "edit_global": "Edit global environment",
"new": "Create new environment", "new": "Create new environment",
"new_variable": "Create a new environment variable", "new_variable": "Create a new environment variable",
"title": "Ortamlar" "title": "Environments"
}, },
"general": { "general": {
"chat": "Chat with support", "chat": "Chat with support",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@hoppscotch/common", "name": "@hoppscotch/common",
"private": true, "private": true,
"version": "2024.3.3", "version": "2024.3.1",
"scripts": { "scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run", "test": "vitest --run",
@@ -50,7 +50,7 @@
"axios": "1.6.2", "axios": "1.6.2",
"buffer": "6.0.3", "buffer": "6.0.3",
"cookie-es": "1.0.0", "cookie-es": "1.0.0",
"dioc": "3.0.1", "dioc": "1.0.1",
"esprima": "4.0.1", "esprima": "4.0.1",
"events": "3.3.0", "events": "3.3.0",
"fp-ts": "2.16.1", "fp-ts": "2.16.1",
@@ -90,7 +90,7 @@
"util": "0.12.5", "util": "0.12.5",
"uuid": "9.0.1", "uuid": "9.0.1",
"verzod": "0.2.2", "verzod": "0.2.2",
"vue": "3.4.27", "vue": "3.3.9",
"vue-i18n": "9.8.0", "vue-i18n": "9.8.0",
"vue-pdf-embed": "1.2.1", "vue-pdf-embed": "1.2.1",
"vue-router": "4.2.5", "vue-router": "4.2.5",

View File

@@ -32,7 +32,6 @@ declare module 'vue' {
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default'] AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default'] AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
AppSpotlightEntryRESTTeamRequestEntry: typeof import('./components/app/spotlight/entry/RESTTeamRequestEntry.vue')['default'] AppSpotlightEntryRESTTeamRequestEntry: typeof import('./components/app/spotlight/entry/RESTTeamRequestEntry.vue')['default']
AppSpotlightSearch: typeof import('./components/app/SpotlightSearch.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default'] AppSupport: typeof import('./components/app/Support.vue')['default']
Collections: typeof import('./components/collections/index.vue')['default'] Collections: typeof import('./components/collections/index.vue')['default']
CollectionsAdd: typeof import('./components/collections/Add.vue')['default'] CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
@@ -181,10 +180,6 @@ declare module 'vue' {
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default'] LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default'] LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default'] LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
NewCollections: typeof import('./components/new-collections/index.vue')['default']
NewCollectionsRest: typeof import('./components/new-collections/rest/index.vue')['default']
NewCollectionsRestCollection: typeof import('./components/new-collections/rest/Collection.vue')['default']
NewCollectionsRestRequest: typeof import('./components/new-collections/rest/Request.vue')['default']
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default'] ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default'] RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default'] RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
@@ -216,8 +211,6 @@ declare module 'vue' {
TeamsTeam: typeof import('./components/teams/Team.vue')['default'] TeamsTeam: typeof import('./components/teams/Team.vue')['default']
Tippy: typeof import('vue-tippy')['Tippy'] Tippy: typeof import('vue-tippy')['Tippy']
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default'] WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
WorkspacePersonalWorkspaceSelector: typeof import('./components/workspace/PersonalWorkspaceSelector.vue')['default']
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default'] WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
WorkspaceTestWorkspaceSelector: typeof import('./components/workspace/TestWorkspaceSelector.vue')['default']
} }
} }

View File

@@ -2,222 +2,208 @@
<div> <div>
<header <header
ref="headerRef" ref="headerRef"
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2" class="grid grid-cols-5 grid-rows-1 gap-2 overflow-x-auto overflow-y-hidden p-2"
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()" @mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
> >
<div <div
class="inline-flex flex-1 items-center justify-start space-x-2" class="col-span-2 flex items-center justify-between space-x-2"
:style="{ :style="{
paddingTop: platform.ui?.appHeader?.paddingTop?.value, paddingTop: platform.ui?.appHeader?.paddingTop?.value,
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value, paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
}" }"
> >
<HoppButtonSecondary <div class="flex">
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')"
to="/"
/>
</div>
<div class="inline-flex flex-1 items-center justify-center space-x-2">
<button
class="flex max-w-[15rem] flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 py-1 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
@click="invokeAction('modals.search.toggle')"
>
<span class="inline-flex flex-1 items-center">
<icon-lucide-search class="svg-icons mr-2" />
{{ t("app.search") }}
</span>
<span class="flex space-x-1">
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
<kbd class="shortcut-key">K</kbd>
</span>
</button>
<HoppButtonSecondary
v-if="showInstallButton"
v-tippy="{ theme: 'tooltip' }"
:title="t('header.install_pwa')"
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="installPWA()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${
mdAndLarger ? t('support.title') : t('app.options')
} <kbd>?</kbd>`"
:icon="IconLifeBuoy"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="invokeAction('modals.support.toggle')"
/>
</div>
<div class="inline-flex flex-1 items-center justify-end space-x-2">
<div
v-if="currentUser === null"
class="inline-flex items-center space-x-2"
>
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconUploadCloud" class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('header.save_workspace')" :label="t('app.name')"
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 hidden border border-green-600/25 bg-green-500/[.15] !text-green-500 hover:border-green-800/50 hover:bg-green-400/10 focus-visible:border-green-800/50 focus-visible:bg-green-400/10 md:flex" to="/"
@click="invokeAction('modals.login.toggle')"
/>
<HoppButtonPrimary
:label="t('header.login')"
@click="invokeAction('modals.login.toggle')"
/> />
</div> </div>
<div v-else class="inline-flex items-center space-x-2"> </div>
<TeamsMemberStack <div class="col-span-1 flex items-center justify-between space-x-2">
v-if=" <AppSpotlightSearch />
workspace.type === 'team' && </div>
selectedTeam && <div class="col-span-2 flex items-center justify-between space-x-2">
selectedTeam.teamMembers.length > 1 <div class="flex">
" <HoppButtonSecondary
:team-members="selectedTeam.teamMembers" v-if="showInstallButton"
show-count v-tippy="{ theme: 'tooltip' }"
class="mx-2" :title="t('header.install_pwa')"
@handle-click="handleTeamEdit()" :icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="installPWA()"
/> />
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${
mdAndLarger ? t('support.title') : t('app.options')
} <kbd>?</kbd>`"
:icon="IconLifeBuoy"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="invokeAction('modals.support.toggle')"
/>
</div>
<div class="flex">
<div <div
class="flex divide-x divide-green-600/25 rounded border border-green-600/25 bg-green-500/[.15] focus-within:divide-green-800/50 focus-within:border-green-800/50 focus-within:bg-green-400/10 hover:divide-green-800/50 hover:border-green-800/50 hover:bg-green-400/10" v-if="currentUser === null"
class="inline-flex items-center space-x-2"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" :icon="IconUploadCloud"
:title="t('team.invite_tooltip')" :label="t('header.save_workspace')"
:icon="IconUserPlus" class="!focus-visible:text-emerald-600 !hover:text-emerald-600 hidden h-8 border border-emerald-600/25 bg-emerald-500/10 !text-emerald-500 hover:border-emerald-600/20 hover:bg-emerald-600/20 focus-visible:border-emerald-600/20 focus-visible:bg-emerald-600/20 md:flex"
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 !text-green-500" @click="invokeAction('modals.login.toggle')"
@click="handleInvite()"
/> />
<HoppButtonSecondary <HoppButtonPrimary
:label="t('header.login')"
class="h-8"
@click="invokeAction('modals.login.toggle')"
/>
</div>
<div v-else class="inline-flex items-center space-x-2">
<TeamsMemberStack
v-if=" v-if="
workspace.type === 'team' && workspace.type === 'team' &&
selectedTeam && selectedTeam &&
selectedTeam?.myRole === 'OWNER' selectedTeam.teamMembers.length > 1
" "
v-tippy="{ theme: 'tooltip' }" :team-members="selectedTeam.teamMembers"
:title="t('team.edit')" show-count
:icon="IconSettings" class="mx-2"
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 !text-green-500" @handle-click="handleTeamEdit()"
@click="handleTeamEdit()"
/> />
</div> <div
<tippy class="flex h-8 divide-x divide-emerald-600/25 rounded border border-emerald-600/25 bg-emerald-500/10 focus-within:divide-emerald-600/20 focus-within:border-emerald-600/20 focus-within:bg-emerald-600/20 hover:divide-emerald-600/20 hover:border-emerald-600/20 hover:bg-emerald-600/20"
interactive >
trigger="click" <HoppButtonSecondary
theme="popover" v-tippy="{ theme: 'tooltip' }"
:on-shown="() => accountActions.focus()" :title="t('team.invite_tooltip')"
> :icon="IconUserPlus"
<HoppButtonSecondary class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
v-tippy="{ theme: 'tooltip' }" @click="handleInvite()"
:title="t('workspace.change')" />
:label="mdAndLarger ? activeWorkspaceName : ``" <HoppButtonSecondary
:icon="activeWorkspaceIcon" v-if="
class="select-wrapper !focus-visible:text-blue-600 !hover:text-blue-600 rounded border border-blue-600/25 bg-blue-500/[.15] py-[0.4375rem] pr-8 !text-blue-500 hover:border-blue-800/50 hover:bg-blue-400/10 focus-visible:border-blue-800/50 focus-visible:bg-blue-400/10" workspace.type === 'team' &&
/> selectedTeam &&
<template #content="{ hide }"> selectedTeam?.myRole === 'OWNER'
<div "
ref="accountActions" v-tippy="{ theme: 'tooltip' }"
class="flex flex-col focus:outline-none" :title="t('team.edit')"
tabindex="0" :icon="IconSettings"
@keyup.escape="hide()" class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
@click="hide()" @click="handleTeamEdit()"
> />
<WorkspaceSelector /> </div>
</div>
</template>
</tippy>
<span class="px-2">
<tippy <tippy
interactive interactive
trigger="click" trigger="click"
theme="popover" theme="popover"
:on-shown="() => tippyActions.focus()" :on-shown="() => accountActions.focus()"
> >
<HoppSmartPicture <HoppSmartSelectWrapper
v-if="currentUser.photoURL" class="!text-blue-500 !focus-visible:text-blue-600 !hover:text-blue-600"
v-tippy="{ >
theme: 'tooltip', <HoppButtonSecondary
}" v-tippy="{ theme: 'tooltip' }"
:url="currentUser.photoURL" :title="t('workspace.change')"
:alt=" :label="mdAndLarger ? workspaceName : ``"
currentUser.displayName || :icon="workspace.type === 'personal' ? IconUser : IconUsers"
t('profile.default_hopp_displayname') class="!focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20"
" />
:title=" </HoppSmartSelectWrapper>
currentUser.displayName ||
currentUser.email ||
t('profile.default_hopp_displayname')
"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<HoppSmartPicture
v-else
v-tippy="{ theme: 'tooltip' }"
:title="
currentUser.displayName ||
currentUser.email ||
t('profile.default_hopp_displayname')
"
:initial="currentUser.displayName || currentUser.email"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<template #content="{ hide }"> <template #content="{ hide }">
<div <div
ref="tippyActions" ref="accountActions"
class="flex flex-col focus:outline-none" class="flex flex-col focus:outline-none"
tabindex="0" tabindex="0"
@keyup.p="profile.$el.click()"
@keyup.s="settings.$el.click()"
@keyup.l="logout.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
@click="hide()"
> >
<div class="flex flex-col px-2 text-tiny"> <WorkspaceSelector />
<span class="inline-flex truncate font-semibold">
{{
currentUser.displayName ||
t("profile.default_hopp_displayname")
}}
</span>
<span class="inline-flex truncate text-secondaryLight">
{{ currentUser.email }}
</span>
</div>
<hr />
<HoppSmartItem
ref="profile"
to="/profile"
:icon="IconUser"
:label="t('navigation.profile')"
:shortcut="['P']"
@click="hide()"
/>
<HoppSmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
/>
</div> </div>
</template> </template>
</tippy> </tippy>
</span> <span class="px-2">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions.focus()"
>
<HoppSmartPicture
v-tippy="{
theme: 'tooltip',
}"
:name="currentUser.uid"
:title="
currentUser.displayName ||
currentUser.email ||
t('profile.default_hopp_displayname')
"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.p="profile.$el.click()"
@keyup.s="settings.$el.click()"
@keyup.l="logout.$el.click()"
@keyup.escape="hide()"
>
<div class="flex flex-col px-2">
<span class="inline-flex truncate font-semibold">
{{
currentUser.displayName ||
t("profile.default_hopp_displayname")
}}
</span>
<span
class="inline-flex truncate text-secondaryLight text-tiny"
>
{{ currentUser.email }}
</span>
</div>
<hr />
<HoppSmartItem
ref="profile"
to="/profile"
:icon="IconUser"
:label="t('navigation.profile')"
:shortcut="['P']"
@click="hide()"
/>
<HoppSmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
/>
</div>
</template>
</tippy>
</span>
</div>
</div> </div>
</div> </div>
</header> </header>
<AppBanner v-if="bannerContent" :banner="bannerContent" /> <AppBanner
v-if="bannerContent"
:banner="bannerContent"
@dismiss="dismissOfflineBanner"
/>
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" /> <TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
<TeamsInvite <TeamsInvite
v-if="workspace.type === 'team' && workspace.teamID" v-if="workspace.type === 'team' && workspace.teamID"
@@ -233,7 +219,6 @@
@invite-team="inviteTeam(editingTeamName, editingTeamID)" @invite-team="inviteTeam(editingTeamName, editingTeamID)"
@refetch-teams="refetchTeams" @refetch-teams="refetchTeams"
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmRemove" :show="confirmRemove"
:title="t('confirm.remove_team')" :title="t('confirm.remove_team')"
@@ -247,31 +232,29 @@
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { defineActionHandler, invokeAction } from "@helpers/actions" import { defineActionHandler, invokeAction } from "@helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { installPWA, pwaDefferedPrompt } from "@modules/pwa" import { installPWA, pwaDefferedPrompt } from "@modules/pwa"
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core" 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 { computed, reactive, ref, watch } from "vue" import { computed, reactive, ref, watch } from "vue"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { GetMyTeamsQuery, TeamMemberRole } from "~/helpers/backend/graphql" import { GetMyTeamsQuery, TeamMemberRole } from "~/helpers/backend/graphql"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { platform } from "~/platform" import { platform } from "~/platform"
import {
BANNER_PRIORITY_HIGH,
BannerContent,
BannerService,
} from "~/services/banner.service"
import { NewWorkspaceService } from "~/services/new-workspace"
import { WorkspaceService } from "~/services/workspace.service"
import IconDownload from "~icons/lucide/download" import IconDownload from "~icons/lucide/download"
import IconLifeBuoy from "~icons/lucide/life-buoy" import IconLifeBuoy from "~icons/lucide/life-buoy"
import IconSettings from "~icons/lucide/settings" import IconSettings from "~icons/lucide/settings"
import IconUploadCloud from "~icons/lucide/upload-cloud" import IconUploadCloud from "~icons/lucide/upload-cloud"
import IconUser from "~icons/lucide/user" import IconUser from "~icons/lucide/user"
import IconUserPlus from "~icons/lucide/user-plus" import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import {
BannerService,
BannerContent,
BANNER_PRIORITY_HIGH,
} from "~/services/banner.service"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -294,10 +277,11 @@ const bannerContent = computed(() => banner.content.value?.content)
let bannerID: number | null = null let bannerID: number | null = null
const offlineBanner: BannerContent = { const offlineBanner: BannerContent = {
type: "info", type: "warning",
text: (t) => t("helpers.offline"), text: (t) => t("helpers.offline"),
alternateText: (t) => t("helpers.offline_short"), alternateText: (t) => t("helpers.offline_short"),
score: BANNER_PRIORITY_HIGH, score: BANNER_PRIORITY_HIGH,
dismissible: true,
} }
const network = reactive(useNetwork()) const network = reactive(useNetwork())
@@ -314,6 +298,8 @@ watch(isOnline, () => {
} }
}) })
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
const currentUser = useReadonlyStream( const currentUser = useReadonlyStream(
platform.auth.getProbableUserStream(), platform.auth.getProbableUserStream(),
platform.auth.getProbableUser() platform.auth.getProbableUser()
@@ -331,6 +317,12 @@ const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const workspace = workspaceService.currentWorkspace const workspace = workspaceService.currentWorkspace
const workspaceName = computed(() => {
return workspace.value.type === "personal"
? t("workspace.personal")
: workspace.value.teamName
})
const refetchTeams = () => { const refetchTeams = () => {
teamListAdapter.fetchList() teamListAdapter.fetchList()
} }
@@ -365,23 +357,6 @@ watch(
} }
) )
const newWorkspaceService = useService(NewWorkspaceService)
const activeWorkspaceName = computed(() => {
const activeWorkspaceHandleRef =
newWorkspaceService.activeWorkspaceHandle.value?.get()
if (activeWorkspaceHandleRef?.value.type === "ok") {
return activeWorkspaceHandleRef.value.data.name
}
return t("workspace.no_workspace")
})
const activeWorkspaceIcon = computed(() => {
return newWorkspaceService.activeWorkspaceDecor.value?.value.headerCurrentIcon
})
const showModalInvite = ref(false) const showModalInvite = ref(false)
const showModalEdit = ref(false) const showModalEdit = ref(false)
@@ -405,8 +380,6 @@ const inviteTeam = (team: { name: string }, teamID: string) => {
// Show the workspace selected team invite modal if the user is an owner of the team else show the default invite modal // Show the workspace selected team invite modal if the user is an owner of the team else show the default invite modal
const handleInvite = () => { const handleInvite = () => {
if (!currentUser.value) return invokeAction("modals.login.toggle")
if ( if (
workspace.value.type === "team" && workspace.value.type === "team" &&
workspace.value.teamID && workspace.value.teamID &&

View File

@@ -30,8 +30,9 @@ import { defineStep } from "~/composables/step-components"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { restCollections$ } from "~/newstore/collections" import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue" import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconFolderPlus from "~icons/lucide/folder-plus" import IconFolderPlus from "~icons/lucide/folder-plus"
import IconOpenAPI from "~icons/lucide/file" import IconOpenAPI from "~icons/lucide/file"
@@ -47,23 +48,23 @@ import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
import { platform } from "~/platform" import { platform } from "~/platform"
import { initializeDownloadFile } from "~/helpers/import-export/export" import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { gistExporter } from "~/helpers/import-export/export/gist" import { gistExporter } from "~/helpers/import-export/export/gist"
import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections"
import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections" import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource" import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { ImporterOrExporter } from "~/components/importExport/types" import { ImporterOrExporter } from "~/components/importExport/types"
import { useService } from "dioc/vue"
import { NewWorkspaceService } from "~/services/new-workspace"
import { TeamWorkspace } from "~/services/workspace.service"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType = type CollectionType =
| { | {
type: "team-collections" type: "team-collections"
selectedTeam: TeamWorkspace selectedTeam: SelectedTeam
} }
| { type: "my-collections" } | { type: "my-collections" }
@@ -85,45 +86,15 @@ const currentUser = useReadonlyStream(
const myCollections = useReadonlyStream(restCollections$, []) const myCollections = useReadonlyStream(restCollections$, [])
const workspaceService = useService(NewWorkspaceService)
const activeWorkspaceHandle = workspaceService.activeWorkspaceHandle
const showImportFailedError = () => { const showImportFailedError = () => {
toast.error(t("import.failed")) toast.error(t("import.failed"))
} }
const handleImportToStore = async (collections: HoppCollection[]) => { const handleImportToStore = async (collections: HoppCollection[]) => {
if (props.collectionsType.type === "my-collections") { const importResult =
if (!activeWorkspaceHandle.value) { props.collectionsType.type === "my-collections"
return ? await importToPersonalWorkspace(collections)
} : await importToTeamsWorkspace(collections)
const collectionHandleResult = await workspaceService.importRESTCollections(
activeWorkspaceHandle.value,
collections
)
if (E.isLeft(collectionHandleResult)) {
// INVALID_WORKSPACE_HANDLE
return toast.error(t("import.failed"))
}
const resultHandle = collectionHandleResult.right
const requestHandleRef = resultHandle.get()
if (requestHandleRef.value.type === "invalid") {
// WORKSPACE_INVALIDATED
}
toast.success(t("state.file_imported"))
emit("hide-modal")
return
}
const importResult = await importToTeamsWorkspace(collections)
if (E.isRight(importResult)) { if (E.isRight(importResult)) {
toast.success(t("state.file_imported")) toast.success(t("state.file_imported"))
@@ -133,6 +104,13 @@ const handleImportToStore = async (collections: HoppCollection[]) => {
} }
} }
const importToPersonalWorkspace = (collections: HoppCollection[]) => {
appendRESTCollections(collections)
return E.right({
success: true,
})
}
function translateToTeamCollectionFormat(x: HoppCollection) { function translateToTeamCollectionFormat(x: HoppCollection) {
const folders: HoppCollection[] = (x.folders ?? []).map( const folders: HoppCollection[] = (x.folders ?? []).map(
translateToTeamCollectionFormat translateToTeamCollectionFormat
@@ -412,34 +390,28 @@ const HoppMyCollectionsExporter: ImporterOrExporter = {
applicableTo: ["personal-workspace"], applicableTo: ["personal-workspace"],
isLoading: isHoppMyCollectionExporterInProgress, isLoading: isHoppMyCollectionExporterInProgress,
}, },
action: async () => { action: () => {
if (!myCollections.value.length) { if (!myCollections.value.length) {
return toast.error(t("error.no_collections_to_export")) return toast.error(t("error.no_collections_to_export"))
} }
if (!activeWorkspaceHandle.value) {
return
}
isHoppMyCollectionExporterInProgress.value = true isHoppMyCollectionExporterInProgress.value = true
const result = await workspaceService.exportRESTCollections( const message = initializeDownloadCollection(
activeWorkspaceHandle.value, myCollectionsExporter(myCollections.value),
myCollections.value "Collections"
) )
if (E.isLeft(result)) { if (E.isRight(message)) {
// INVALID_WORKSPACE_HANDLE toast.success(t(message.right))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
} }
toast.success(t("state.download_started"))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
isHoppMyCollectionExporterInProgress.value = false isHoppMyCollectionExporterInProgress.value = false
}, },
} }
@@ -461,7 +433,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
props.collectionsType.selectedTeam props.collectionsType.selectedTeam
) { ) {
const res = await teamCollectionsExporter( const res = await teamCollectionsExporter(
props.collectionsType.selectedTeam.teamID props.collectionsType.selectedTeam.id
) )
if (E.isRight(res)) { if (E.isRight(res)) {
@@ -473,7 +445,10 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
return toast.error(t("error.no_collections_to_export")) return toast.error(t("error.no_collections_to_export"))
} }
initializeDownloadFile(exportCollectionsToJSON, "team-collections") initializeDownloadCollection(
exportCollectionsToJSON,
"team-collections"
)
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION", type: "HOPP_EXPORT_COLLECTION",
@@ -512,7 +487,7 @@ const HoppGistCollectionsExporter: ImporterOrExporter = {
const collectionJSON = await getCollectionJSON() const collectionJSON = await getCollectionJSON()
const accessToken = currentUser.value?.accessToken const accessToken = currentUser.value?.accessToken
if (!accessToken || E.isLeft(collectionJSON)) { if (!accessToken) {
toast.error(t("error.something_went_wrong")) toast.error(t("error.something_went_wrong"))
isHoppGistCollectionExporterInProgress.value = false isHoppGistCollectionExporterInProgress.value = false
return return
@@ -594,8 +569,8 @@ const hasTeamWriteAccess = computed(() => {
} }
return ( return (
collectionsType.selectedTeam.role === "EDITOR" || collectionsType.selectedTeam.myRole === "EDITOR" ||
collectionsType.selectedTeam.role === "OWNER" collectionsType.selectedTeam.myRole === "OWNER"
) )
}) })
@@ -603,49 +578,26 @@ const selectedTeamID = computed(() => {
const { collectionsType } = props const { collectionsType } = props
return collectionsType.type === "team-collections" return collectionsType.type === "team-collections"
? collectionsType.selectedTeam?.teamID ? collectionsType.selectedTeam?.id
: undefined : undefined
}) })
const getCollectionJSON = async () => { const getCollectionJSON = async () => {
// TODO: Implement `getRESTCollectionJSONView` for team workspace
if ( if (
props.collectionsType.type === "team-collections" && props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam?.teamID props.collectionsType.selectedTeam?.id
) { ) {
const res = await getTeamCollectionJSON( const res = await getTeamCollectionJSON(
props.collectionsType.selectedTeam?.teamID props.collectionsType.selectedTeam?.id
) )
return E.isRight(res) return E.isRight(res)
? E.right(res.right.exportCollectionsToJSON) ? E.right(res.right.exportCollectionsToJSON)
: E.left(res.left.error.toString()) : E.left(res.left)
} }
if (props.collectionsType.type === "my-collections") { if (props.collectionsType.type === "my-collections") {
if (!activeWorkspaceHandle.value) { return E.right(JSON.stringify(myCollections.value, null, 2))
return E.left("INVALID_WORKSPACE_HANDLE")
}
const collectionJSONHandleResult =
await workspaceService.getRESTCollectionJSONView(
activeWorkspaceHandle.value
)
if (E.isLeft(collectionJSONHandleResult)) {
return E.left(collectionJSONHandleResult.left.error)
}
const collectionJSONHandle = collectionJSONHandleResult.right
const collectionJSONHandleRef = collectionJSONHandle.get()
if (collectionJSONHandleRef.value.type === "invalid") {
// WORKSPACE_INVALIDATED
return E.left("WORKSPACE_INVALIDATED")
}
return E.right(collectionJSONHandleRef.value.data.content)
} }
return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE") return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE")

View File

@@ -75,7 +75,7 @@ import {
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { clone } from "lodash-es" import { clone } from "lodash-es"
import { ref, toRefs, watch } from "vue" import { ref, watch } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { PersistenceService } from "~/services/persistence" import { PersistenceService } from "~/services/persistence"
@@ -100,15 +100,11 @@ const props = withDefaults(
editingProperties: EditingProperties | null editingProperties: EditingProperties | null
source: "REST" | "GraphQL" source: "REST" | "GraphQL"
modelValue: string modelValue: string
// TODO: Purpose of this prop is to maintain backwards compatibility
// To be removed after porting all usages of this component
emitWithFullCollection: boolean
}>(), }>(),
{ {
show: false, show: false,
loadingState: false, loadingState: false,
editingProperties: null, editingProperties: null,
emitWithFullCollection: true,
} }
) )
@@ -182,24 +178,15 @@ watch(
const saveEditedCollection = () => { const saveEditedCollection = () => {
if (!props.editingProperties) return if (!props.editingProperties) return
const finalCollection = clone(editableCollection.value) const finalCollection = clone(editableCollection.value)
delete finalCollection.body
const { path } = toRefs(props.editingProperties)
const collection = { const collection = {
path: path.value, path: props.editingProperties.path,
collection: { collection: {
...props.editingProperties.collection, ...props.editingProperties.collection,
...finalCollection, ...finalCollection,
}, },
isRootCollection: props.editingProperties.isRootCollection, isRootCollection: props.editingProperties.isRootCollection,
} }
emit("set-collection-properties", collection as EditingProperties)
const data = props.emitWithFullCollection
? collection
: { ...finalCollection, collIndexPath: path.value }
emit("set-collection-properties", data as EditingProperties)
persistenceService.removeLocalConfig("unsaved_collection_properties") persistenceService.removeLocalConfig("unsaved_collection_properties")
} }

View File

@@ -20,25 +20,19 @@
<label class="p-4"> <label class="p-4">
{{ t("collection.select_location") }} {{ t("collection.select_location") }}
</label> </label>
<!-- <CollectionsGraphql <CollectionsGraphql
v-if="mode === 'graphql'" v-if="mode === 'graphql'"
:picked="picked" :picked="picked"
:save-request="true" :save-request="true"
@select="onSelect" @select="onSelect"
/> --> />
<!-- <Collections <Collections
v-else v-else
:picked="picked" :picked="picked"
:save-request="true" :save-request="true"
@select="onSelect" @select="onSelect"
@update-team="updateTeam" @update-team="updateTeam"
@update-collection-type="updateCollectionType" @update-collection-type="updateCollectionType"
/> -->
<NewCollections
:picked="picked"
:save-request="true"
platform="rest"
@select="onSelect"
/> />
</div> </div>
</template> </template>
@@ -62,40 +56,51 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "@composables/i18n" import { computed, nextTick, reactive, ref, watch } from "vue"
import { useToast } from "@composables/toast" import { cloneDeep } from "lodash-es"
import { import {
HoppGQLRequest, HoppGQLRequest,
HoppRESTRequest, HoppRESTRequest,
isHoppRESTRequest, isHoppRESTRequest,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import { computedWithControl } from "@vueuse/core" import { pipe } from "fp-ts/function"
import { useService } from "dioc/vue" import * as TE from "fp-ts/TaskEither"
import * as E from "fp-ts/Either" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { cloneDeep } from "lodash-es" import {
import { computed, nextTick, reactive, ref, watch } from "vue" createRequestInCollection,
updateTeamRequest,
} from "~/helpers/backend/mutations/TeamRequest"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { cascadeParentCollectionForHeaderAuth } from "~/newstore/collections" import { useI18n } from "@composables/i18n"
import { NewWorkspaceService } from "~/services/new-workspace" import { useToast } from "@composables/toast"
import { GQLTabService } from "~/services/tab/graphql" import {
cascadeParentCollectionForHeaderAuth,
editGraphqlRequest,
editRESTRequest,
saveGraphqlRequestAs,
saveRESTRequestAs,
} from "~/newstore/collections"
import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core"
import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const RESTTabs = useService(RESTTabService) const RESTTabs = useService(RESTTabService)
const GQLTabs = useService(GQLTabService) const GQLTabs = useService(GQLTabService)
const workspaceService = useService(NewWorkspaceService)
// type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
// type CollectionType = type CollectionType =
// | { | {
// type: "team-collections" type: "team-collections"
// selectedTeam: SelectedTeam selectedTeam: SelectedTeam
// } }
// | { type: "my-collections"; selectedTeam: undefined } | { type: "my-collections"; selectedTeam: undefined }
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -163,10 +168,10 @@ const requestData = reactive({
requestIndex: undefined as number | undefined, requestIndex: undefined as number | undefined,
}) })
// const collectionsType = ref<CollectionType>({ const collectionsType = ref<CollectionType>({
// type: "my-collections", type: "my-collections",
// selectedTeam: undefined, selectedTeam: undefined,
// }) })
const picked = ref<Picked | null>(null) const picked = ref<Picked | null>(null)
@@ -187,14 +192,13 @@ watch(
} }
) )
// TODO: To be removed const updateTeam = (newTeam: SelectedTeam) => {
// const updateTeam = (newTeam: SelectedTeam) => { collectionsType.value.selectedTeam = newTeam
// collectionsType.value.selectedTeam = newTeam }
// }
// const updateCollectionType = (type: CollectionType["type"]) => { const updateCollectionType = (type: CollectionType["type"]) => {
// collectionsType.value.type = type collectionsType.value.type = type
// } }
const onSelect = (pickedVal: Picked | null) => { const onSelect = (pickedVal: Picked | null) => {
picked.value = pickedVal picked.value = pickedVal
@@ -210,109 +214,66 @@ const saveRequestAs = async () => {
return return
} }
const updatedRequest = const requestUpdated =
props.mode === "rest" props.mode === "rest"
? cloneDeep(RESTTabs.currentActiveTab.value.document.request) ? cloneDeep(RESTTabs.currentActiveTab.value.document.request)
: cloneDeep(GQLTabs.currentActiveTab.value.document.request) : cloneDeep(GQLTabs.currentActiveTab.value.document.request)
updatedRequest.name = requestName.value requestUpdated.name = requestName.value
if (!workspaceService.activeWorkspaceHandle.value) { if (picked.value.pickedType === "my-collection") {
return if (!isHoppRESTRequest(requestUpdated))
}
if (
picked.value.pickedType === "my-collection" ||
picked.value.pickedType === "my-folder"
) {
if (!isHoppRESTRequest(updatedRequest))
throw new Error("requestUpdated is not a REST Request") throw new Error("requestUpdated is not a REST Request")
const collectionPathIndex = const insertionIndex = saveRESTRequestAs(
picked.value.pickedType === "my-collection" `${picked.value.collectionIndex}`,
? picked.value.collectionIndex.toString() requestUpdated
: picked.value.folderPath
const collectionHandleResult = await workspaceService.getCollectionHandle(
workspaceService.activeWorkspaceHandle.value,
collectionPathIndex
) )
if (E.isLeft(collectionHandleResult)) {
// INVALID_WORKSPACE_HANDLE | INVALID_COLLECTION_ID | INVALID_PATH
return
}
const collectionHandle = collectionHandleResult.right
const requestHandleResult = await workspaceService.createRESTRequest(
collectionHandle,
updatedRequest
)
if (E.isLeft(requestHandleResult)) {
// WORKSPACE_INVALIDATED | INVALID_COLLECTION_HANDLE
return
}
const requestHandle = requestHandleResult.right
const requestHandleRef = requestHandle.get()
if (requestHandleRef.value.type === "invalid") {
// WORKSPACE_INVALIDATED | INVALID_COLLECTION_HANDLE
return
}
RESTTabs.currentActiveTab.value.document = { RESTTabs.currentActiveTab.value.document = {
request: updatedRequest, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
originLocation: "workspace-user-collection", originLocation: "user-collection",
requestHandle, folderPath: `${picked.value.collectionIndex}`,
requestIndex: insertionIndex,
}, },
} }
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "personal",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "my-request") { } else if (picked.value.pickedType === "my-folder") {
if (!isHoppRESTRequest(updatedRequest)) if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request") throw new Error("requestUpdated is not a REST Request")
const requestHandleResult = await workspaceService.getRequestHandle( const insertionIndex = saveRESTRequestAs(
workspaceService.activeWorkspaceHandle.value, picked.value.folderPath,
`${picked.value.folderPath}/${picked.value.requestIndex.toString()}` requestUpdated
) )
if (E.isLeft(requestHandleResult)) {
// INVALID_COLLECTION_HANDLE | INVALID_REQUEST_ID | REQUEST_NOT_FOUND
return
}
const requestHandle = requestHandleResult.right
const requestHandleRef = requestHandle.get()
if (requestHandleRef.value.type === "invalid") {
// WORKSPACE_INVALIDATED
return
}
const updateRequestResult = await workspaceService.updateRESTRequest(
requestHandle,
updatedRequest
)
if (E.isLeft(updateRequestResult)) {
// WORKSPACE_INVALIDATED | INVALID_REQUEST_HANDLE
return
}
RESTTabs.currentActiveTab.value.document = { RESTTabs.currentActiveTab.value.document = {
request: updatedRequest, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
originLocation: "workspace-user-collection", originLocation: "user-collection",
requestHandle, folderPath: picked.value.folderPath,
requestIndex: insertionIndex,
}, },
} }
@@ -326,147 +287,190 @@ const saveRequestAs = async () => {
headers, headers,
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "personal",
})
requestSaved()
} else if (picked.value.pickedType === "my-request") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
editRESTRequest(
picked.value.folderPath,
picked.value.requestIndex,
requestUpdated
)
RESTTabs.currentActiveTab.value.document = {
request: requestUpdated,
isDirty: false,
saveContext: {
originLocation: "user-collection",
folderPath: picked.value.folderPath,
requestIndex: picked.value.requestIndex,
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "rest",
workspaceType: "personal",
})
requestSaved()
} else if (picked.value.pickedType === "teams-collection") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
updateTeamCollectionOrFolder(picked.value.collectionID, requestUpdated)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "team",
})
} else if (picked.value.pickedType === "teams-folder") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
updateTeamCollectionOrFolder(picked.value.folderID, requestUpdated)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "team",
})
} else if (picked.value.pickedType === "teams-request") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
if (
collectionsType.value.type !== "team-collections" ||
!collectionsType.value.selectedTeam
)
throw new Error("Collections Type mismatch")
modalLoadingState.value = true
const data = {
request: JSON.stringify(requestUpdated),
title: requestUpdated.name,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "rest",
workspaceType: "team",
})
pipe(
updateTeamRequest(picked.value.requestID, data),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
modalLoadingState.value = false
},
() => {
modalLoadingState.value = false
requestSaved()
}
)
)()
} else if (picked.value.pickedType === "gql-my-request") {
// TODO: Check for GQL request ?
editGraphqlRequest(
picked.value.folderPath,
picked.value.requestIndex,
requestUpdated as HoppGQLRequest
)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "gql",
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved()
} else if (picked.value.pickedType === "gql-my-folder") {
// TODO: Check for GQL request ?
saveGraphqlRequestAs(
picked.value.folderPath,
requestUpdated as HoppGQLRequest
)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "gql",
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved()
} else if (picked.value.pickedType === "gql-my-collection") {
// TODO: Check for GQL request ?
saveGraphqlRequestAs(
`${picked.value.collectionIndex}`,
requestUpdated as HoppGQLRequest
)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "gql",
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved() requestSaved()
} }
// TODO: To be removed
// else if (picked.value.pickedType === "teams-collection") {
// if (!isHoppRESTRequest(updatedRequest))
// throw new Error("requestUpdated is not a REST Request")
// updateTeamCollectionOrFolder(picked.value.collectionID, updatedRequest)
// platform.analytics?.logEvent({
// type: "HOPP_SAVE_REQUEST",
// createdNow: true,
// platform: "rest",
// workspaceType: "team",
// })
// } else if (picked.value.pickedType === "teams-folder") {
// if (!isHoppRESTRequest(updatedRequest))
// throw new Error("requestUpdated is not a REST Request")
// updateTeamCollectionOrFolder(picked.value.folderID, updatedRequest)
// platform.analytics?.logEvent({
// type: "HOPP_SAVE_REQUEST",
// createdNow: true,
// platform: "rest",
// workspaceType: "team",
// })
// } else if (picked.value.pickedType === "teams-request") {
// if (!isHoppRESTRequest(updatedRequest))
// throw new Error("requestUpdated is not a REST Request")
// if (
// collectionsType.value.type !== "team-collections" ||
// !collectionsType.value.selectedTeam
// )
// throw new Error("Collections Type mismatch")
// modalLoadingState.value = true
// const data = {
// request: JSON.stringify(updatedRequest),
// title: updatedRequest.name,
// }
// platform.analytics?.logEvent({
// type: "HOPP_SAVE_REQUEST",
// createdNow: false,
// platform: "rest",
// workspaceType: "team",
// })
// pipe(
// updateTeamRequest(picked.value.requestID, data),
// TE.match(
// (err: GQLError<string>) => {
// toast.error(`${getErrorMessage(err)}`)
// modalLoadingState.value = false
// },
// () => {
// modalLoadingState.value = false
// requestSaved()
// }
// )
// )()
// } else if (picked.value.pickedType === "gql-my-request") {
// // TODO: Check for GQL request ?
// editGraphqlRequest(
// picked.value.folderPath,
// picked.value.requestIndex,
// updatedRequest as HoppGQLRequest
// )
// platform.analytics?.logEvent({
// type: "HOPP_SAVE_REQUEST",
// createdNow: false,
// platform: "gql",
// workspaceType: "team",
// })
// const { auth, headers } = cascadeParentCollectionForHeaderAuth(
// picked.value.folderPath,
// "graphql"
// )
// GQLTabs.currentActiveTab.value.document.inheritedProperties = {
// auth,
// headers,
// }
// requestSaved()
// } else if (picked.value.pickedType === "gql-my-folder") {
// // TODO: Check for GQL request ?
// saveGraphqlRequestAs(
// picked.value.folderPath,
// updatedRequest as HoppGQLRequest
// )
// platform.analytics?.logEvent({
// type: "HOPP_SAVE_REQUEST",
// createdNow: true,
// platform: "gql",
// workspaceType: "team",
// })
// const { auth, headers } = cascadeParentCollectionForHeaderAuth(
// picked.value.folderPath,
// "graphql"
// )
// GQLTabs.currentActiveTab.value.document.inheritedProperties = {
// auth,
// headers,
// }
// requestSaved()
// } else if (picked.value.pickedType === "gql-my-collection") {
// // TODO: Check for GQL request ?
// saveGraphqlRequestAs(
// `${picked.value.collectionIndex}`,
// updatedRequest as HoppGQLRequest
// )
// platform.analytics?.logEvent({
// type: "HOPP_SAVE_REQUEST",
// createdNow: true,
// platform: "gql",
// workspaceType: "team",
// })
// const { auth, headers } = cascadeParentCollectionForHeaderAuth(
// `${picked.value.collectionIndex}`,
// "graphql"
// )
// GQLTabs.currentActiveTab.value.document.inheritedProperties = {
// auth,
// headers,
// }
// requestSaved()
// }
} }
/** /**
@@ -474,50 +478,50 @@ const saveRequestAs = async () => {
* @param collectionID - ID of the collection or folder * @param collectionID - ID of the collection or folder
* @param requestUpdated - Updated request * @param requestUpdated - Updated request
*/ */
// const updateTeamCollectionOrFolder = ( const updateTeamCollectionOrFolder = (
// collectionID: string, collectionID: string,
// requestUpdated: HoppRESTRequest requestUpdated: HoppRESTRequest
// ) => { ) => {
// if ( if (
// collectionsType.value.type !== "team-collections" || collectionsType.value.type !== "team-collections" ||
// !collectionsType.value.selectedTeam !collectionsType.value.selectedTeam
// ) )
// throw new Error("Collections Type mismatch") throw new Error("Collections Type mismatch")
// modalLoadingState.value = true modalLoadingState.value = true
// const data = { const data = {
// title: requestUpdated.name, title: requestUpdated.name,
// request: JSON.stringify(requestUpdated), request: JSON.stringify(requestUpdated),
// teamID: collectionsType.value.selectedTeam.id, teamID: collectionsType.value.selectedTeam.id,
// } }
// pipe( pipe(
// createRequestInCollection(collectionID, data), createRequestInCollection(collectionID, data),
// TE.match( TE.match(
// (err: GQLError<string>) => { (err: GQLError<string>) => {
// toast.error(`${getErrorMessage(err)}`) toast.error(`${getErrorMessage(err)}`)
// modalLoadingState.value = false modalLoadingState.value = false
// }, },
// (result) => { (result) => {
// const { createRequestInCollection } = result const { createRequestInCollection } = result
// RESTTabs.currentActiveTab.value.document = { RESTTabs.currentActiveTab.value.document = {
// request: requestUpdated, request: requestUpdated,
// isDirty: false, isDirty: false,
// saveContext: { saveContext: {
// originLocation: "team-collection", originLocation: "team-collection",
// requestID: createRequestInCollection.id, requestID: createRequestInCollection.id,
// collectionID: createRequestInCollection.collection.id, collectionID: createRequestInCollection.collection.id,
// teamID: createRequestInCollection.collection.team.id, teamID: createRequestInCollection.collection.team.id,
// }, },
// } }
// modalLoadingState.value = false modalLoadingState.value = false
// requestSaved() requestSaved()
// } }
// ) )
// )() )()
// } }
const requestSaved = () => { const requestSaved = () => {
toast.success(`${t("request.added")}`) toast.success(`${t("request.added")}`)
@@ -532,24 +536,24 @@ const hideModal = () => {
emit("hide-modal") emit("hide-modal")
} }
// const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
// console.error(err) console.error(err)
// if (err.type === "network_error") { if (err.type === "network_error") {
// return t("error.network_error") return t("error.network_error")
// } }
// switch (err.error) { switch (err.error) {
// case "team_coll/short_title": case "team_coll/short_title":
// return t("collection.name_length_insufficient") return t("collection.name_length_insufficient")
// case "team/invalid_coll_id": case "team/invalid_coll_id":
// return t("team.invalid_id") return t("team.invalid_id")
// case "team/not_required_role": case "team/not_required_role":
// return t("profile.no_permission") return t("profile.no_permission")
// case "team_req/not_required_role": case "team_req/not_required_role":
// return t("profile.no_permission") return t("profile.no_permission")
// case "Forbidden resource": case "Forbidden resource":
// return t("profile.no_permission") return t("profile.no_permission")
// default: default:
// return t("error.something_went_wrong") return t("error.something_went_wrong")
// } }
// } }
</script> </script>

View File

@@ -387,6 +387,7 @@ import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down" import IconImport from "~icons/lucide/folder-down"
import { computed, PropType, Ref, toRef } from "vue" import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { TeamCollection } from "~/helpers/teams/TeamCollection" import { TeamCollection } from "~/helpers/teams/TeamCollection"
@@ -399,16 +400,17 @@ import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js" import { Picked } from "~/helpers/types/HoppPicked.js"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { TeamWorkspace } from "~/services/workspace.service"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const tabs = useService(RESTTabService) const tabs = useService(RESTTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType = type CollectionType =
| { | {
type: "team-collections" type: "team-collections"
selectedTeam: TeamWorkspace selectedTeam: SelectedTeam
} }
| { type: "my-collections"; selectedTeam: undefined } | { type: "my-collections"; selectedTeam: undefined }
@@ -612,7 +614,7 @@ const hasNoTeamAccess = computed(
() => () =>
props.collectionsType.type === "team-collections" && props.collectionsType.type === "team-collections" &&
(props.collectionsType.selectedTeam === undefined || (props.collectionsType.selectedTeam === undefined ||
props.collectionsType.selectedTeam.role === "VIEWER") props.collectionsType.selectedTeam.myRole === "VIEWER")
) )
const isSelected = ({ const isSelected = ({

View File

@@ -21,7 +21,7 @@ import { GistSource } from "~/helpers/import-export/import/import-sources/GistSo
import IconFolderPlus from "~icons/lucide/folder-plus" import IconFolderPlus from "~icons/lucide/folder-plus"
import IconUser from "~icons/lucide/user" import IconUser from "~icons/lucide/user"
import { initializeDownloadFile } from "~/helpers/import-export/export" import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { useReadonlyStream } from "~/composables/stream" import { useReadonlyStream } from "~/composables/stream"
import { platform } from "~/platform" import { platform } from "~/platform"
@@ -133,12 +133,12 @@ const GqlCollectionsHoppExporter: ImporterOrExporter = {
disabled: false, disabled: false,
applicableTo: ["personal-workspace", "team-workspace"], applicableTo: ["personal-workspace", "team-workspace"],
}, },
action: async () => { action: () => {
if (!gqlCollections.value.length) { if (!gqlCollections.value.length) {
return toast.error(t("error.no_collections_to_export")) return toast.error(t("error.no_collections_to_export"))
} }
const message = await initializeDownloadFile( const message = initializeDownloadCollection(
gqlCollectionsExporter(gqlCollections.value), gqlCollectionsExporter(gqlCollections.value),
"GQLCollections" "GQLCollections"
) )

View File

@@ -200,7 +200,7 @@ const toast = useToast()
defineProps<{ defineProps<{
// Whether to activate the ability to pick items (activates 'select' events) // Whether to activate the ability to pick items (activates 'select' events)
saveRequest: boolean saveRequest: boolean
picked: Picked | null picked: Picked
}>() }>()
const collections = useReadonlyStream(graphqlCollections$, [], "deep") const collections = useReadonlyStream(graphqlCollections$, [], "deep")

View File

@@ -178,6 +178,7 @@ import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { useReadonlyStream } from "~/composables/stream" import { useReadonlyStream } from "~/composables/stream"
import { useLocalState } from "~/newstore/localstate" import { useLocalState } from "~/newstore/localstate"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { import {
@@ -244,7 +245,7 @@ import {
} from "~/helpers/collection/collection" } from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering" import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler, invokeAction } from "~/helpers/actions" import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties" import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
@@ -273,14 +274,16 @@ const props = defineProps({
const emit = defineEmits<{ const emit = defineEmits<{
(event: "select", payload: Picked | null): void (event: "select", payload: Picked | null): void
(event: "update-team", team: TeamWorkspace): void (event: "update-team", team: SelectedTeam): void
(event: "update-collection-type", type: CollectionType["type"]): void (event: "update-collection-type", type: CollectionType["type"]): void
}>() }>()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType = type CollectionType =
| { | {
type: "team-collections" type: "team-collections"
selectedTeam: TeamWorkspace selectedTeam: SelectedTeam
} }
| { type: "my-collections"; selectedTeam: undefined } | { type: "my-collections"; selectedTeam: undefined }
@@ -327,7 +330,9 @@ const requestMoveLoading = ref<string[]>([])
// TeamList-Adapter // TeamList-Adapter
const workspaceService = useService(WorkspaceService) const workspaceService = useService(WorkspaceService)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null) const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID") const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const teamListFetched = ref(false)
// Team Collection Adapter // Team Collection Adapter
const teamCollectionAdapter = new TeamCollectionAdapter(null) const teamCollectionAdapter = new TeamCollectionAdapter(null)
@@ -373,7 +378,7 @@ watch(
filterTexts, filterTexts,
(newFilterText) => { (newFilterText) => {
if (collectionsType.value.type === "team-collections") { if (collectionsType.value.type === "team-collections") {
const selectedTeamID = collectionsType.value.selectedTeam?.teamID const selectedTeamID = collectionsType.value.selectedTeam?.id
selectedTeamID && selectedTeamID &&
debouncedSearch(newFilterText, selectedTeamID)?.catch(() => {}) debouncedSearch(newFilterText, selectedTeamID)?.catch(() => {})
@@ -430,6 +435,28 @@ onMounted(() => {
} }
}) })
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) updateSelectedTeam(team)
}
}
}
)
watch(
() => collectionsType.value.selectedTeam,
(newTeam) => {
if (newTeam) {
teamCollectionAdapter.changeTeamID(newTeam.id)
}
}
)
const switchToMyCollections = () => { const switchToMyCollections = () => {
collectionsType.value.type = "my-collections" collectionsType.value.type = "my-collections"
collectionsType.value.selectedTeam = undefined collectionsType.value.selectedTeam = undefined
@@ -461,12 +488,11 @@ const expandTeamCollection = (collectionID: string) => {
teamCollectionAdapter.expandCollection(collectionID) teamCollectionAdapter.expandCollection(collectionID)
} }
const updateSelectedTeam = (team: TeamWorkspace) => { const updateSelectedTeam = (team: SelectedTeam) => {
if (team) { if (team) {
collectionsType.value.type = "team-collections" collectionsType.value.type = "team-collections"
teamCollectionAdapter.changeTeamID(team.teamID)
collectionsType.value.selectedTeam = team collectionsType.value.selectedTeam = team
REMEMBERED_TEAM_ID.value = team.teamID REMEMBERED_TEAM_ID.value = team.id
emit("update-team", team) emit("update-team", team)
emit("update-collection-type", "team-collections") emit("update-collection-type", "team-collections")
} }
@@ -475,14 +501,23 @@ const updateSelectedTeam = (team: TeamWorkspace) => {
const workspace = workspaceService.currentWorkspace const workspace = workspaceService.currentWorkspace
// Used to switch collection type and team when user switch workspace in the global workspace switcher // Used to switch collection type and team when user switch workspace in the global workspace switcher
// Check if there is a teamID in the workspace, if yes, switch to team collections and select the team
// If there is no teamID, switch to my collections
watch( watch(
workspace, () => {
(newWorkspace) => { const space = workspace.value
if (newWorkspace.type === "personal") { return space.type === "personal" ? undefined : space.teamID
switchToMyCollections() },
} else if (newWorkspace.type === "team") { (teamID) => {
updateSelectedTeam(newWorkspace) if (teamID) {
const team = myTeams.value?.find((t) => t.id === teamID)
if (team) {
updateSelectedTeam(team)
}
return
} }
return switchToMyCollections()
}, },
{ {
immediate: true, immediate: true,
@@ -510,7 +545,7 @@ const hasTeamWriteAccess = computed(() => {
return false return false
} }
const role = collectionsType.value.selectedTeam?.role const role = collectionsType.value.selectedTeam?.myRole
return role === "OWNER" || role === "EDITOR" return role === "OWNER" || role === "EDITOR"
}) })
@@ -725,7 +760,7 @@ const addNewRootCollection = (name: string) => {
}) })
pipe( pipe(
createNewRootCollection(name, collectionsType.value.selectedTeam.teamID), createNewRootCollection(name, collectionsType.value.selectedTeam.id),
TE.match( TE.match(
(err: GQLError<string>) => { (err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`) toast.error(`${getErrorMessage(err)}`)
@@ -796,7 +831,7 @@ const onAddRequest = (requestName: string) => {
const data = { const data = {
request: JSON.stringify(newRequest), request: JSON.stringify(newRequest),
teamID: collectionsType.value.selectedTeam.teamID, teamID: collectionsType.value.selectedTeam.id,
title: requestName, title: requestName,
} }
@@ -1123,7 +1158,7 @@ const duplicateRequest = (payload: {
const data = { const data = {
request: JSON.stringify(newRequest), request: JSON.stringify(newRequest),
teamID: collectionsType.value.selectedTeam.teamID, teamID: collectionsType.value.selectedTeam.id,
title: `${request.name} - ${t("action.duplicate")}`, title: `${request.name} - ${t("action.duplicate")}`,
} }

View File

@@ -37,7 +37,7 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
import IconPostman from "~icons/hopp/postman" import IconPostman from "~icons/hopp/postman"
import IconInsomnia from "~icons/hopp/insomnia" import IconInsomnia from "~icons/hopp/insomnia"
import IconUser from "~icons/lucide/user" import IconUser from "~icons/lucide/user"
import { initializeDownloadFile } from "~/helpers/import-export/export" import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { computed } from "vue" import { computed } from "vue"
import { useReadonlyStream } from "~/composables/stream" import { useReadonlyStream } from "~/composables/stream"
import { environmentsExporter } from "~/helpers/import-export/export/environments" import { environmentsExporter } from "~/helpers/import-export/export/environments"
@@ -230,12 +230,12 @@ const HoppEnvironmentsExport: ImporterOrExporter = {
disabled: false, disabled: false,
applicableTo: ["personal-workspace", "team-workspace"], applicableTo: ["personal-workspace", "team-workspace"],
}, },
action: async () => { action: () => {
if (!environmentJson.value.length) { if (!environmentJson.value.length) {
return toast.error(t("error.no_environments_to_export")) return toast.error(t("error.no_environments_to_export"))
} }
const message = await initializeDownloadFile( const message = initializeDownloadCollection(
environmentsExporter(environmentJson.value), environmentsExporter(environmentJson.value),
"Environments" "Environments"
) )

View File

@@ -364,7 +364,6 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
teamID: team.id, teamID: team.id,
teamName: team.name, teamName: team.name,
type: "team", type: "team",
role: team.myRole,
}) })
} }
watch( watch(

View File

@@ -46,38 +46,41 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useReadonlyStream, useStream } from "@composables/stream"
import { Environment } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { isEqual } from "lodash-es"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { isEqual } from "lodash-es"
import { platform } from "~/platform"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { defineActionHandler } from "~/helpers/actions"
import { GQLError } from "~/helpers/backend/GQLClient"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { import {
deleteEnvironment,
getSelectedEnvironmentIndex, getSelectedEnvironmentIndex,
globalEnv$, globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { defineActionHandler } from "~/helpers/actions"
import { useLocalState } from "~/newstore/localstate" import { useLocalState } from "~/newstore/localstate"
import { platform } from "~/platform" import { pipe } from "fp-ts/function"
import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service" import * as TE from "fp-ts/TaskEither"
import { GQLError } from "~/helpers/backend/GQLClient"
import { deleteEnvironment } from "~/newstore/environments"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { useToast } from "~/composables/toast"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { Environment } from "@hoppscotch/data"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
type EnvironmentType = "my-environments" | "team-environments" type EnvironmentType = "my-environments" | "team-environments"
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type EnvironmentsChooseType = { type EnvironmentsChooseType = {
type: EnvironmentType type: EnvironmentType
selectedTeam: TeamWorkspace | undefined selectedTeam: SelectedTeam
} }
const environmentType = ref<EnvironmentsChooseType>({ const environmentType = ref<EnvironmentsChooseType>({
@@ -99,7 +102,11 @@ const currentUser = useReadonlyStream(
platform.auth.getCurrentUser() platform.auth.getCurrentUser()
) )
// TeamList-Adapter
const workspaceService = useService(WorkspaceService) const workspaceService = useService(WorkspaceService)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID") const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const adapter = new TeamEnvironmentAdapter(undefined) const adapter = new TeamEnvironmentAdapter(undefined)
@@ -111,17 +118,29 @@ const loading = computed(
() => adapterLoading.value && teamEnvironmentList.value.length === 0 () => adapterLoading.value && teamEnvironmentList.value.length === 0
) )
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) updateSelectedTeam(team)
}
}
}
)
const switchToMyEnvironments = () => { const switchToMyEnvironments = () => {
environmentType.value.selectedTeam = undefined environmentType.value.selectedTeam = undefined
updateEnvironmentType("my-environments") updateEnvironmentType("my-environments")
adapter.changeTeamID(undefined) adapter.changeTeamID(undefined)
} }
const updateSelectedTeam = (newSelectedTeam: TeamWorkspace | undefined) => { const updateSelectedTeam = (newSelectedTeam: SelectedTeam | undefined) => {
if (newSelectedTeam) { if (newSelectedTeam) {
adapter.changeTeamID(newSelectedTeam.teamID)
environmentType.value.selectedTeam = newSelectedTeam environmentType.value.selectedTeam = newSelectedTeam
REMEMBERED_TEAM_ID.value = newSelectedTeam.teamID REMEMBERED_TEAM_ID.value = newSelectedTeam.id
updateEnvironmentType("team-environments") updateEnvironmentType("team-environments")
} }
} }
@@ -129,6 +148,15 @@ const updateEnvironmentType = (newEnvironmentType: EnvironmentType) => {
environmentType.value.type = newEnvironmentType environmentType.value.type = newEnvironmentType
} }
watch(
() => environmentType.value.selectedTeam,
(newTeam) => {
if (newTeam) {
adapter.changeTeamID(newTeam.id)
}
}
)
const workspace = workspaceService.currentWorkspace const workspace = workspaceService.currentWorkspace
// Switch to my environments if workspace is personal and to team environments if workspace is team // Switch to my environments if workspace is personal and to team environments if workspace is team
@@ -142,7 +170,8 @@ watch(workspace, (newWorkspace) => {
}) })
} }
} else if (newWorkspace.type === "team") { } else if (newWorkspace.type === "team") {
updateSelectedTeam(newWorkspace) const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
} }
}) })

View File

@@ -54,7 +54,9 @@
:key="tab.id" :key="tab.id"
:label="tab.label" :label="tab.label"
> >
<div class="divide-y divide-dividerLight"> <div
class="divide-y divide-dividerLight rounded border border-divider"
>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="tab.variables.length === 0" v-if="tab.variables.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"

View File

@@ -56,7 +56,9 @@
:key="tab.id" :key="tab.id"
:label="tab.label" :label="tab.label"
> >
<div class="divide-y divide-dividerLight"> <div
class="divide-y divide-dividerLight rounded border border-divider"
>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="tab.variables.length === 0" v-if="tab.variables.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"

View File

@@ -4,7 +4,7 @@
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary" class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="team === undefined || team.role === 'VIEWER'" v-if="team === undefined || team.myRole === 'VIEWER'"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
disabled disabled
class="!rounded-none" class="!rounded-none"
@@ -28,7 +28,7 @@
:icon="IconHelpCircle" :icon="IconHelpCircle"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-if="team !== undefined && team.role === 'VIEWER'" v-if="team !== undefined && team.myRole === 'VIEWER'"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
disabled disabled
:icon="IconImport" :icon="IconImport"
@@ -84,7 +84,7 @@
)" )"
:key="`environment-${index}`" :key="`environment-${index}`"
:environment="environment" :environment="environment"
:is-viewer="team?.role === 'VIEWER'" :is-viewer="team?.myRole === 'VIEWER'"
@edit-environment="editEnvironment(environment)" @edit-environment="editEnvironment(environment)"
/> />
</div> </div>
@@ -103,16 +103,16 @@
:show="showModalDetails" :show="showModalDetails"
:action="action" :action="action"
:editing-environment="editingEnvironment" :editing-environment="editingEnvironment"
:editing-team-id="team?.teamID" :editing-team-id="team?.id"
:editing-variable-name="editingVariableName" :editing-variable-name="editingVariableName"
:is-secret-option-selected="secretOptionSelected" :is-secret-option-selected="secretOptionSelected"
:is-viewer="team?.role === 'VIEWER'" :is-viewer="team?.myRole === 'VIEWER'"
@hide-modal="displayModalEdit(false)" @hide-modal="displayModalEdit(false)"
/> />
<EnvironmentsImportExport <EnvironmentsImportExport
v-if="showModalImportExport" v-if="showModalImportExport"
:team-environments="teamEnvironments" :team-environments="teamEnvironments"
:team-id="team?.teamID" :team-id="team?.id"
environment-type="TEAM_ENV" environment-type="TEAM_ENV"
@hide-modal="displayModalImportExport(false)" @hide-modal="displayModalImportExport(false)"
/> />
@@ -129,14 +129,16 @@ import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down" import IconImport from "~icons/lucide/folder-down"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { TeamWorkspace } from "~/services/workspace.service" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
const props = defineProps<{ const props = defineProps<{
team: TeamWorkspace | undefined team: SelectedTeam
teamEnvironments: TeamEnvironment[] teamEnvironments: TeamEnvironment[]
adapterError: GQLError<string> | null adapterError: GQLError<string> | null
loading: boolean loading: boolean
@@ -149,7 +151,7 @@ const editingEnvironment = ref<TeamEnvironment | null>(null)
const editingVariableName = ref("") const editingVariableName = ref("")
const secretOptionSelected = ref(false) const secretOptionSelected = ref(false)
const isTeamViewer = computed(() => props.team?.role === "VIEWER") const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
const displayModalAdd = (shouldDisplay: boolean) => { const displayModalAdd = (shouldDisplay: boolean) => {
action.value = "new" action.value = "new"

View File

@@ -10,7 +10,7 @@
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
class="w-full rounded border border-divider bg-primaryLight px-4 py-2 text-secondaryDark" class="w-full rounded border border-divider bg-primaryLight px-4 py-2 text-secondaryDark"
:placeholder="`${t('graphql.url_placeholder')}`" :placeholder="`${t('request.url')}`"
:disabled="connected" :disabled="connected"
@keyup.enter="onConnectClick" @keyup.enter="onConnectClick"
/> />

View File

@@ -54,7 +54,7 @@
> >
<SmartEnvInput <SmartEnvInput
v-model="tab.document.request.endpoint" v-model="tab.document.request.endpoint"
:placeholder="`${t('request.url_placeholder')}`" :placeholder="`${t('request.url')}`"
:auto-complete-source="userHistories" :auto-complete-source="userHistories"
:auto-complete-env="true" :auto-complete-env="true"
:inspection-results="tabResults" :inspection-results="tabResults"
@@ -236,28 +236,16 @@ import { useI18n } from "@composables/i18n"
import { useSetting } from "@composables/settings" import { useSetting } from "@composables/settings"
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream" import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { Ref, computed, onUnmounted, ref } from "vue" import { Ref, computed, ref, onUnmounted } from "vue"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { defineActionHandler, invokeAction } from "~/helpers/actions" import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { runMutation } from "~/helpers/backend/GQLClient" import { runMutation } from "~/helpers/backend/GQLClient"
import { UpdateRequestDocument } from "~/helpers/backend/graphql" import { UpdateRequestDocument } from "~/helpers/backend/graphql"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { getDefaultRESTRequest } from "~/helpers/rest/default" import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history" import { editRESTRequest } from "~/newstore/collections"
import { platform } from "~/platform"
import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service"
import { NewWorkspaceService } from "~/services/new-workspace"
import { HoppTab } from "~/services/tab"
import { RESTTabService } from "~/services/tab/rest"
import { WorkspaceService } from "~/services/workspace.service"
import IconChevronDown from "~icons/lucide/chevron-down" import IconChevronDown from "~icons/lucide/chevron-down"
import IconCode2 from "~icons/lucide/code-2" import IconCode2 from "~icons/lucide/code-2"
import IconFileCode from "~icons/lucide/file-code" import IconFileCode from "~icons/lucide/file-code"
@@ -265,10 +253,21 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
import IconRotateCCW from "~icons/lucide/rotate-ccw" import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save" import IconSave from "~icons/lucide/save"
import IconShare2 from "~icons/lucide/share-2" import IconShare2 from "~icons/lucide/share-2"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
import { platform } from "~/platform"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { RESTTabService } from "~/services/tab/rest"
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
import { WorkspaceService } from "~/services/workspace.service"
const t = useI18n() const t = useI18n()
const interceptorService = useService(InterceptorService) const interceptorService = useService(InterceptorService)
const newWorkspaceService = useService(NewWorkspaceService)
const methods = [ const methods = [
"GET", "GET",
@@ -507,61 +506,34 @@ const cycleDownMethod = () => {
} }
} }
const saveRequest = async () => { const saveRequest = () => {
const { saveContext } = tab.value.document const saveCtx = tab.value.document.saveContext
if (!saveContext) { if (!saveCtx) {
showSaveRequestModal.value = true showSaveRequestModal.value = true
return return
} }
if (saveCtx.originLocation === "user-collection") {
const req = tab.value.document.request
if (saveContext.originLocation === "workspace-user-collection") { try {
const updatedRequest = tab.value.document.request editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
if ( tab.value.document.isDirty = false
!newWorkspaceService.activeWorkspaceHandle.value ||
!saveContext.requestHandle platform.analytics?.logEvent({
) { type: "HOPP_SAVE_REQUEST",
return platform: "rest",
createdNow: false,
workspaceType: "personal",
})
toast.success(`${t("request.saved")}`)
} catch (e) {
tab.value.document.saveContext = undefined
saveRequest()
} }
} else if (saveCtx.originLocation === "team-collection") {
const { requestHandle } = saveContext
const requestHandleRef = requestHandle.get()
if (!requestHandleRef.value) {
return
}
if (requestHandleRef.value.type === "invalid") {
showSaveRequestModal.value = true
return
}
const updateRequestResult = await newWorkspaceService.updateRESTRequest(
requestHandle,
updatedRequest
)
if (E.isLeft(updateRequestResult)) {
// INVALID_REQUEST_HANDLE
showSaveRequestModal.value = true
if (!tab.value.document.isDirty) {
tab.value.document.isDirty = true
}
return
}
tab.value.document.isDirty = false
tab.value.document.saveContext = {
...saveContext,
requestHandle,
}
toast.success(`${t("request.saved")}`)
} else if (saveContext.originLocation === "team-collection") {
const req = tab.value.document.request const req = tab.value.document.request
// TODO: handle error case (NOTE: overwriteRequestTeams is async) // TODO: handle error case (NOTE: overwriteRequestTeams is async)
@@ -574,7 +546,7 @@ const saveRequest = async () => {
}) })
runMutation(UpdateRequestDocument, { runMutation(UpdateRequestDocument, {
requestID: saveContext.requestID, requestID: saveCtx.requestID,
data: { data: {
title: req.name, title: req.name,
request: JSON.stringify(req), request: JSON.stringify(req),

View File

@@ -17,7 +17,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { watch } from "vue" import { watch } from "vue"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { cloneDeep, isEqual } from "lodash-es" import { cloneDeep } from "lodash-es"
import { isEqualHoppRESTRequest } from "@hoppscotch/data"
import { HoppTab } from "~/services/tab" import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document" import { HoppRESTDocument } from "~/helpers/rest/document"
@@ -31,42 +32,15 @@ const emit = defineEmits<{
const tab = useVModel(props, "modelValue", emit) const tab = useVModel(props, "modelValue", emit)
// TODO: Come up with a better dirty check
let oldRequest = cloneDeep(tab.value.document.request) let oldRequest = cloneDeep(tab.value.document.request)
watch( watch(
() => tab.value.document.request, () => tab.value.document.request,
(updatedValue) => { (updatedValue) => {
// Request from the collection tree
if ( if (
tab.value.document.saveContext?.originLocation === !tab.value.document.isDirty &&
"workspace-user-collection" !isEqualHoppRESTRequest(oldRequest, updatedValue)
) { ) {
const requestHandleRef =
tab.value.document.saveContext.requestHandle?.get()
if (!requestHandleRef || requestHandleRef.value.type === "invalid") {
return
}
if (
!tab.value.document.isDirty &&
!isEqual(oldRequest, requestHandleRef?.value.data.request)
) {
tab.value.document.isDirty = true
}
if (
tab.value.document.isDirty &&
isEqual(oldRequest, requestHandleRef?.value.data.request)
) {
tab.value.document.isDirty = false
}
return
}
// Unsaved request
if (!tab.value.document.isDirty && !isEqual(oldRequest, updatedValue)) {
tab.value.document.isDirty = true tab.value.document.isDirty = true
} }

View File

@@ -10,8 +10,7 @@
:icon="IconFolder" :icon="IconFolder"
:label="`${t('tab.collections')}`" :label="`${t('tab.collections')}`"
> >
<!-- <Collections /> --> <Collections />
<NewCollections :platform="'rest'" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
:id="'env'" :id="'env'"
@@ -38,13 +37,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { ref } from "vue"
import IconClock from "~icons/lucide/clock" import IconClock from "~icons/lucide/clock"
import IconFolder from "~icons/lucide/folder"
import IconLayers from "~icons/lucide/layers" import IconLayers from "~icons/lucide/layers"
import IconFolder from "~icons/lucide/folder"
import IconShare2 from "~icons/lucide/share-2" import IconShare2 from "~icons/lucide/share-2"
import { ref } from "vue"
import { useI18n } from "@composables/i18n"
const t = useI18n() const t = useI18n()

View File

@@ -106,15 +106,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring" import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { HoppTab } from "~/services/tab" import { useI18n } from "~/composables/i18n"
import IconCopy from "~icons/lucide/copy"
import IconFileEdit from "~icons/lucide/file-edit"
import IconShare2 from "~icons/lucide/share-2"
import IconXCircle from "~icons/lucide/x-circle" import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square" import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy"
import IconShare2 from "~icons/lucide/share-2"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
const t = useI18n() const t = useI18n()

View File

@@ -1,32 +0,0 @@
<template>
<div v-if="!activeWorkspaceHandle">No Workspace Selected.</div>
<NewCollectionsRest
v-else-if="platform === 'rest'"
:picked="picked"
:save-request="saveRequest"
:workspace-handle="activeWorkspaceHandle"
@select="(payload) => emit('select', payload)"
/>
</template>
<script setup lang="ts">
import { useService } from "dioc/vue"
import { Picked } from "~/helpers/types/HoppPicked"
import { NewWorkspaceService } from "~/services/new-workspace"
defineProps<{
picked?: Picked | null
platform: "rest" | "gql"
saveRequest?: boolean
}>()
const emit = defineEmits<{
(event: "select", payload: Picked | null): void
}>()
const workspaceService = useService(NewWorkspaceService)
const activeWorkspaceHandle = workspaceService.activeWorkspaceHandle
</script>

View File

@@ -1,450 +0,0 @@
<template>
<div class="flex flex-col">
<div
class="h-1 w-full transition"
:class="[
{
'bg-accentDark': isReorderable,
},
]"
@drop="orderUpdateCollectionEvent"
@dragover.prevent="ordering = true"
@dragleave="ordering = false"
@dragend="resetDragState"
></div>
<div class="relative flex flex-col">
<div
class="z-[1] pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
:class="{
'opacity-25':
dragging && notSameDestination && notSameParentDestination,
}"
></div>
<div
class="z-[3] group pointer-events-auto relative flex cursor-pointer items-stretch"
:draggable="true"
@dragstart="dragStart"
@dragover="handleDragOver($event)"
@dragleave="resetDragState"
@dragend="
() => {
resetDragState()
dropItemID = ''
}
"
@drop="handleDrop($event)"
@contextmenu.prevent="options?.tippy?.show()"
>
<div
class="flex min-w-0 flex-1 items-center justify-center"
@click="emit('toggle-children')"
>
<span
class="pointer-events-none flex items-center justify-center px-4"
>
<component
:is="collectionIcon"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
class="pointer-events-none flex min-w-0 flex-1 py-2 pr-2 transition group-hover:text-secondaryDark"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ collectionView.name }}
</span>
</span>
</div>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconFilePlus"
:title="t('request.new')"
class="hidden group-hover:inline-flex"
@click="addRequest"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconFolderPlus"
:title="t('folder.new')"
class="hidden group-hover:inline-flex"
@click="addChildCollection"
/>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.r="requestAction?.$el.click()"
@keyup.n="folderAction?.$el.click()"
@keyup.e="edit?.$el.click()"
@keyup.delete="deleteAction?.$el.click()"
@keyup.x="exportAction?.$el.click()"
@keyup.p="propertiesAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="requestAction"
:icon="IconFilePlus"
:label="t('request.new')"
:shortcut="['R']"
@click="
() => {
addRequest()
hide()
}
"
/>
<HoppSmartItem
ref="folderAction"
:icon="IconFolderPlus"
:label="t('folder.new')"
:shortcut="['N']"
@click="
() => {
addChildCollection()
hide()
}
"
/>
<HoppSmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
editCollection()
hide()
}
"
/>
<HoppSmartItem
ref="exportAction"
:icon="IconDownload"
:label="t('export.title')"
:shortcut="['X']"
@click="
() => {
emit('export-collection', collectionView.collectionID)
hide()
}
"
/>
<HoppSmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
removeCollection()
hide()
}
"
/>
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
:label="t('action.properties')"
:shortcut="['P']"
@click="
() => {
emit(
'edit-collection-properties',
collectionView.collectionID
)
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
</div>
<div
v-if="collectionView.isLastItem"
class="w-full transition"
:class="[
{
'bg-accentDark': isLastItemReorderable,
'h-1 ': collectionView.isLastItem,
},
]"
@drop="updateLastItemOrder"
@dragover.prevent="orderingLastItem = true"
@dragleave="orderingLastItem = false"
@dragend="resetDragState"
></div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { useReadonlyStream } from "~/composables/stream"
import {
currentReorderingStatus$,
changeCurrentReorderStatus,
} from "~/newstore/reordering"
import { RESTCollectionViewCollection } from "~/services/new-workspace/view"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconDownload from "~icons/lucide/download"
import IconEdit from "~icons/lucide/edit"
import IconFilePlus from "~icons/lucide/file-plus"
import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconSettings2 from "~icons/lucide/settings-2"
import IconTrash2 from "~icons/lucide/trash-2"
const t = useI18n()
const props = defineProps<{
collectionView: RESTCollectionViewCollection
isOpen: boolean
isSelected?: boolean | null
}>()
const emit = defineEmits<{
(event: "add-child-collection", parentCollectionIndexPath: string): void
(event: "add-request", parentCollectionIndexPath: string): void
(event: "dragging", payload: boolean): void
(event: "drop-event", payload: DataTransfer): void
(event: "drag-event", payload: DataTransfer): void
(
event: "edit-child-collection",
payload: { collectionIndexPath: string; collectionName: string }
): void
(event: "edit-collection-properties", collectionIndexPath: string): void
(
event: "edit-root-collection",
payload: { collectionIndexPath: string; collectionName: string }
): void
(event: "export-collection", collectionIndexPath: string): void
(event: "remove-child-collection", collectionIndexPath: string): void
(event: "remove-root-collection", collectionIndexPath: string): void
(event: "toggle-children"): void
(event: "update-collection-order", payload: DataTransfer): void
(event: "update-last-collection-order", payload: DataTransfer): void
}>()
const tippyActions = ref<TippyComponent | null>(null)
const requestAction = ref<HTMLButtonElement | null>(null)
const folderAction = ref<HTMLButtonElement | null>(null)
const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const exportAction = ref<HTMLButtonElement | null>(null)
const propertiesAction = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null)
const dragging = ref(false)
const ordering = ref(false)
const orderingLastItem = ref(false)
const dropItemID = ref("")
const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
type: "collection",
id: "",
parentID: "",
})
// Used to determine if the collection is being dragged to a different destination
// This is used to make the highlight effect work
watch(
() => dragging.value,
(val) => {
if (val && notSameDestination.value && notSameParentDestination.value) {
emit("dragging", true)
} else {
emit("dragging", false)
}
}
)
const collectionIcon = computed(() => {
if (props.isSelected) {
return IconCheckCircle
}
return props.isOpen ? IconFolderOpen : IconFolder
})
const notSameParentDestination = computed(() => {
return (
currentReorderingStatus.value.parentID !== props.collectionView.collectionID
)
})
const isRequestDragging = computed(() => {
return currentReorderingStatus.value.type === "request"
})
const isSameParent = computed(() => {
return (
currentReorderingStatus.value.parentID ===
props.collectionView.parentCollectionID
)
})
const isReorderable = computed(() => {
return (
ordering.value &&
notSameDestination.value &&
!isRequestDragging.value &&
isSameParent.value
)
})
const isLastItemReorderable = computed(() => {
return (
orderingLastItem.value &&
notSameDestination.value &&
!isRequestDragging.value &&
isSameParent.value
)
})
const addChildCollection = () => {
emit("add-child-collection", props.collectionView.collectionID)
}
const addRequest = () => {
emit("add-request", props.collectionView.collectionID)
}
const editCollection = () => {
const { collectionID: collectionIndexPath, name: collectionName } =
props.collectionView
const data = {
collectionIndexPath,
collectionName,
}
collectionIndexPath.split("/").length > 1
? emit("edit-child-collection", data)
: emit("edit-root-collection", data)
}
const dragStart = ({ dataTransfer }: DragEvent) => {
if (dataTransfer) {
emit("drag-event", dataTransfer)
dropItemID.value = dataTransfer.getData("collectionIndex")
dragging.value = !dragging.value
changeCurrentReorderStatus({
type: "collection",
id: props.collectionView.collectionID,
parentID: props.collectionView.parentCollectionID,
})
}
}
// Trigger the re-ordering event when a collection is dragged over another collection's top section
const handleDragOver = (e: DragEvent) => {
dragging.value = true
if (
e.offsetY < 10 &&
notSameDestination.value &&
!isRequestDragging.value &&
isSameParent.value
) {
ordering.value = true
dragging.value = false
orderingLastItem.value = false
} else if (
e.offsetY > 18 &&
notSameDestination.value &&
!isRequestDragging.value &&
isSameParent.value &&
props.collectionView.isLastItem
) {
orderingLastItem.value = true
dragging.value = false
ordering.value = false
} else {
ordering.value = false
orderingLastItem.value = false
}
}
const handleDrop = (e: DragEvent) => {
if (ordering.value) {
orderUpdateCollectionEvent(e)
} else if (orderingLastItem.value) {
updateLastItemOrder(e)
} else {
notSameParentDestination.value ? dropEvent(e) : e.stopPropagation()
}
}
const dropEvent = (e: DragEvent) => {
if (e.dataTransfer) {
e.stopPropagation()
emit("drop-event", e.dataTransfer)
resetDragState()
}
}
const orderUpdateCollectionEvent = (e: DragEvent) => {
if (e.dataTransfer) {
e.stopPropagation()
emit("update-collection-order", e.dataTransfer)
resetDragState()
}
}
const updateLastItemOrder = (e: DragEvent) => {
if (e.dataTransfer) {
e.stopPropagation()
emit("update-last-collection-order", e.dataTransfer)
resetDragState()
}
}
const notSameDestination = computed(() => {
return dropItemID.value !== props.collectionView.collectionID
})
const removeCollection = () => {
const { collectionID } = props.collectionView
collectionID.split("/").length > 1
? emit("remove-child-collection", collectionID)
: emit("remove-root-collection", collectionID)
}
const resetDragState = () => {
dragging.value = false
ordering.value = false
orderingLastItem.value = false
}
</script>

View File

@@ -1,334 +0,0 @@
<template>
<div class="flex flex-col">
<div
class="h-1 w-full transition"
:class="[
{
'bg-accentDark': isReorderable,
},
]"
@drop="updateRequestOrder"
@dragover.prevent="ordering = true"
@dragleave="resetDragState"
@dragend="resetDragState"
></div>
<div
class="group flex items-stretch"
:draggable="true"
@dragstart="dragStart"
@dragover="handleDragOver($event)"
@dragleave="resetDragState"
@dragend="resetDragState"
@drop="handleDrop"
@contextmenu.prevent="options?.tippy?.show()"
>
<div
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
@click="selectRequest"
>
<span
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
:class="requestLabelColor"
:style="{ color: requestLabelColor }"
>
<component
:is="IconCheckCircle"
v-if="isSelected"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
<span v-else class="truncate text-tiny font-semibold">
{{ requestView.request.method }}
</span>
</span>
<span
class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ requestView.request.name }}
</span>
<span
v-if="props.isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
>
</span>
<span
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
></span>
</span>
</span>
</div>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconRotateCCW"
:title="t('action.restore')"
class="hidden group-hover:inline-flex"
@click="selectRequest"
/>
<span>
<tippy
ref="options"
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions?.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.e="edit?.$el.click()"
@keyup.d="duplicate?.$el.click()"
@keyup.delete="deleteAction?.$el.click()"
@keyup.s="shareAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="edit"
:icon="IconEdit"
:label="t('action.edit')"
:shortcut="['E']"
@click="
() => {
emit('edit-request', {
requestIndexPath: requestView.requestID,
requestName: requestView.request.name,
})
hide()
}
"
/>
<HoppSmartItem
ref="duplicate"
:icon="IconCopy"
:label="t('action.duplicate')"
:shortcut="['D']"
@click="
() => {
emit('duplicate-request', requestView.requestID)
hide()
}
"
/>
<HoppSmartItem
ref="deleteAction"
:icon="IconTrash2"
:label="t('action.delete')"
:shortcut="['⌫']"
@click="
() => {
emit('remove-request', requestView.requestID)
hide()
}
"
/>
<HoppSmartItem
ref="shareAction"
:icon="IconShare2"
:label="t('action.share')"
:shortcut="['S']"
@click="
() => {
emit('share-request', requestView.request)
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
<div
class="w-full transition"
:class="[
{
'bg-accentDark': isLastItemReorderable,
'h-1 ': props.requestView.isLastItem,
},
]"
@drop="handleDrop"
@dragover.prevent="orderingLastItem = true"
@dragleave="resetDragState"
@dragend="resetDragState"
></div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { computed, ref } from "vue"
import { TippyComponent } from "vue-tippy"
import { useReadonlyStream } from "~/composables/stream"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import {
currentReorderingStatus$,
changeCurrentReorderStatus,
} from "~/newstore/reordering"
import { RESTCollectionViewRequest } from "~/services/new-workspace/view"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconCopy from "~icons/lucide/copy"
import IconEdit from "~icons/lucide/edit"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconShare2 from "~icons/lucide/share-2"
import IconTrash2 from "~icons/lucide/trash-2"
const t = useI18n()
const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
type: "collection",
id: "",
parentID: "",
})
const props = defineProps<{
isActive: boolean
requestView: RESTCollectionViewRequest
isSelected: boolean | null | undefined
}>()
const emit = defineEmits<{
(event: "duplicate-request", requestIndexPath: string): void
(
event: "edit-request",
payload: {
requestIndexPath: string
requestName: string
}
): void
(event: "remove-request", requestIndexPath: string): void
(event: "select-request", requestIndexPath: string): void
(event: "share-request", request: HoppRESTRequest): void
(event: "drag-request", payload: DataTransfer): void
(event: "update-request-order", payload: DataTransfer): void
(event: "update-last-request-order", payload: DataTransfer): void
}>()
const tippyActions = ref<TippyComponent | null>(null)
const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null)
const duplicate = ref<HTMLButtonElement | null>(null)
const shareAction = ref<HTMLButtonElement | null>(null)
const dragging = ref(false)
const ordering = ref(false)
const orderingLastItem = ref(false)
const isCollectionDragging = computed(() => {
return currentReorderingStatus.value.type === "collection"
})
const isLastItemReorderable = computed(() => {
return (
orderingLastItem.value && isSameParent.value && !isCollectionDragging.value
)
})
const isReorderable = computed(() => {
return (
ordering.value &&
!isCollectionDragging.value &&
isSameParent.value &&
!isSameRequest.value
)
})
const isSameParent = computed(() => {
return (
currentReorderingStatus.value.parentID === props.requestView.collectionID
)
})
const isSameRequest = computed(() => {
return currentReorderingStatus.value.id === props.requestView.requestID
})
const requestLabelColor = computed(() =>
getMethodLabelColorClassOf(props.requestView.request)
)
const dragStart = ({ dataTransfer }: DragEvent) => {
if (dataTransfer) {
emit("drag-request", dataTransfer)
dragging.value = !dragging.value
changeCurrentReorderStatus({
type: "request",
id: props.requestView.requestID,
parentID: props.requestView.collectionID,
})
}
}
const handleDrop = (e: DragEvent) => {
if (ordering.value) {
updateRequestOrder(e)
} else if (orderingLastItem.value) {
updateLastItemOrder(e)
} else {
updateRequestOrder(e)
}
}
// Trigger the re-ordering event when a request is dragged over another request's top section
const handleDragOver = (e: DragEvent) => {
dragging.value = true
if (e.offsetY < 10) {
ordering.value = true
dragging.value = false
orderingLastItem.value = false
} else if (e.offsetY > 18) {
orderingLastItem.value = true
dragging.value = false
ordering.value = false
} else {
ordering.value = false
orderingLastItem.value = false
}
}
const resetDragState = () => {
dragging.value = false
ordering.value = false
orderingLastItem.value = false
}
const selectRequest = () => emit("select-request", props.requestView.requestID)
const updateRequestOrder = (e: DragEvent) => {
if (e.dataTransfer) {
e.stopPropagation()
resetDragState()
emit("update-request-order", e.dataTransfer)
}
}
const updateLastItemOrder = (e: DragEvent) => {
if (e.dataTransfer) {
e.stopPropagation()
resetDragState()
emit("update-last-request-order", e.dataTransfer)
}
}
</script>

View File

@@ -37,17 +37,13 @@ import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
import { useLocalState } from "~/newstore/localstate"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const props = defineProps<{ defineProps<{
show: boolean show: boolean
switchWorkspaceAfterCreation?: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -56,12 +52,8 @@ const emit = defineEmits<{
const editingName = ref<string | null>(null) const editingName = ref<string | null>(null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const isLoading = ref(false) const isLoading = ref(false)
const workspaceService = useService(WorkspaceService)
const addNewTeam = async () => { const addNewTeam = async () => {
isLoading.value = true isLoading.value = true
await pipe( await pipe(
@@ -84,19 +76,8 @@ const addNewTeam = async () => {
// Handle GQL errors (use err obj) // Handle GQL errors (use err obj)
} }
}, },
(team) => { () => {
toast.success(`${t("team.new_created")}`) toast.success(`${t("team.new_created")}`)
if (props.switchWorkspaceAfterCreation) {
REMEMBERED_TEAM_ID.value = team.id
workspaceService.changeWorkspace({
teamID: team.id,
teamName: team.name,
type: "team",
role: team.myRole,
})
}
hideModal() hideModal()
} }
) )

View File

@@ -3,7 +3,7 @@
class="flex items-center overflow-x-auto whitespace-nowrap border-b border-dividerLight px-4 py-2 text-tiny text-secondaryLight" class="flex items-center overflow-x-auto whitespace-nowrap border-b border-dividerLight px-4 py-2 text-tiny text-secondaryLight"
> >
<span class="truncate"> <span class="truncate">
{{ workspaceName ?? t("workspace.no_workspace") }} {{ currentWorkspace }}
</span> </span>
<icon-lucide-chevron-right v-if="section" class="mx-2" /> <icon-lucide-chevron-right v-if="section" class="mx-2" />
{{ section }} {{ section }}
@@ -14,24 +14,29 @@
import { computed } from "vue" import { computed } from "vue"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { NewWorkspaceService } from "~/services/new-workspace" import { WorkspaceService } from "~/services/workspace.service"
defineProps<{ const props = defineProps<{
section?: string section?: string
isOnlyPersonal?: boolean
}>() }>()
const t = useI18n() const t = useI18n()
const workspaceService = useService(NewWorkspaceService) const workspaceService = useService(WorkspaceService)
const activeWorkspaceHandle = workspaceService.activeWorkspaceHandle const workspace = workspaceService.currentWorkspace
const workspaceName = computed(() => { const currentWorkspace = computed(() => {
const activeWorkspaceHandleRef = activeWorkspaceHandle.value?.get() if (props.isOnlyPersonal || workspace.value.type === "personal") {
return t("workspace.personal")
if (activeWorkspaceHandleRef?.value.type === "ok") {
return activeWorkspaceHandleRef.value.data.name
} }
return teamWorkspaceName.value
})
return undefined const teamWorkspaceName = computed(() => {
if (workspace.value.type === "team" && workspace.value.teamName) {
return workspace.value.teamName
}
return `${t("workspace.team")}`
}) })
</script> </script>

View File

@@ -1,54 +0,0 @@
<template>
<div>
<div class="flex flex-col">
<HoppSmartItem
:label="'Personal Workspace'"
:info-icon="
activeWorkspaceInfo?.provider ===
personalWorkspaceProviderService.providerID &&
activeWorkspaceInfo.workspaceID === 'personal'
? IconCheck
: undefined
"
:active-info-icon="
activeWorkspaceInfo?.provider ===
personalWorkspaceProviderService.providerID &&
activeWorkspaceInfo.workspaceID === 'personal'
"
@click="selectWorkspace"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useService } from "dioc/vue"
import { NewWorkspaceService } from "~/services/new-workspace"
import { computed } from "vue"
import { PersonalWorkspaceProviderService } from "~/services/new-workspace/providers/personal.workspace"
import IconCheck from "~icons/lucide/check"
const workspaceService = useService(NewWorkspaceService)
const personalWorkspaceProviderService = useService(
PersonalWorkspaceProviderService
)
const activeWorkspaceInfo = computed(() => {
const activeWorkspaceHandleRef =
workspaceService.activeWorkspaceHandle.value?.get()
if (activeWorkspaceHandleRef?.value.type === "ok") {
return {
provider: activeWorkspaceHandleRef.value.data.providerID,
workspaceID: activeWorkspaceHandleRef.value.data.workspaceID,
}
}
return undefined
})
function selectWorkspace() {
workspaceService.activeWorkspaceHandle.value =
personalWorkspaceProviderService.getPersonalWorkspaceHandle()
}
</script>

View File

@@ -1,36 +1,190 @@
<template> <template>
<div ref="rootEl"> <div ref="rootEl">
<div class="flex flex-col"> <div class="flex flex-col">
<div <div class="flex flex-col">
v-for="(selectorComponent, index) in workspaceSelectorComponents" <HoppSmartItem
:key="index" :label="t('workspace.personal')"
class="flex flex-col" :icon="IconUser"
> :info-icon="workspace.type === 'personal' ? IconDone : undefined"
<component :is="selectorComponent" /> :active-info-icon="workspace.type === 'personal'"
@click="switchToPersonalWorkspace"
/>
<hr /> <hr />
</div> </div>
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
<HoppSmartSpinner class="mb-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<HoppSmartPlaceholder
v-if="!loading && myTeams.length === 0"
:src="`/images/states/${colorMode.value}/add_group.svg`"
:alt="`${t('empty.teams')}`"
:text="`${t('empty.teams')}`"
>
<template #body>
<HoppButtonSecondary
:label="t('team.create_new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
</template>
</HoppSmartPlaceholder>
<div v-else-if="!loading" class="flex flex-col">
<div
class="sticky top-0 z-10 mb-2 flex items-center justify-between bg-popover py-2 pl-2"
>
<div class="flex items-center px-2 font-semibold text-secondaryLight">
{{ t("workspace.other_workspaces") }}
</div>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconPlus"
:title="`${t('team.create_new')}`"
outline
filled
class="ml-8 rounded !p-0.75"
@click="displayModalAdd(true)"
/>
</div>
<HoppSmartItem
v-for="(team, index) in myTeams"
:key="`team-${String(index)}`"
:icon="IconUsers"
:label="team.name"
:info-icon="isActiveWorkspace(team.id) ? IconDone : undefined"
:active-info-icon="isActiveWorkspace(team.id)"
@click="switchToTeamWorkspace(team)"
/>
</div>
<div
v-if="!loading && teamListAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="svg-icons mb-4" />
{{ t("error.something_went_wrong") }}
</div>
</div> </div>
<TeamsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from "vue"
import { useReadonlyStream } from "~/composables/stream"
import { platform } from "~/platform"
import { useI18n } from "@composables/i18n"
import IconUser from "~icons/lucide/user"
import IconUsers from "~icons/lucide/users"
import IconPlus from "~icons/lucide/plus"
import { useColorMode } from "@composables/theming"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconDone from "~icons/lucide/check"
import { useLocalState } from "~/newstore/localstate"
import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { NewWorkspaceService } from "~/services/new-workspace" import { useElementVisibility, useIntervalFn } from "@vueuse/core"
import { TestWorkspaceProviderService } from "~/services/new-workspace/providers/test.workspace"
useService(TestWorkspaceProviderService) const t = useI18n()
const colorMode = useColorMode()
const newWorkspaceService = useService(NewWorkspaceService) const showModalAdd = ref(false)
const workspaceSelectorComponents =
newWorkspaceService.workspaceSelectorComponents
// TODO: Handle the updates to these actions const currentUser = useReadonlyStream(
// defineActionHandler("modals.team.new", () => { platform.auth.getProbableUserStream(),
// displayModalAdd(true) platform.auth.getProbableUser()
// }) )
//
// defineActionHandler("workspace.switch.personal", switchToPersonalWorkspace) const workspaceService = useService(WorkspaceService)
// defineActionHandler("workspace.switch", ({ teamId }) => { const teamListadapter = workspaceService.acquireTeamListAdapter(null)
// const team = myTeams.value.find((t) => t.id === teamId) const myTeams = useReadonlyStream(teamListadapter.teamList$, [])
// if (team) switchToTeamWorkspace(team) const isTeamListLoading = useReadonlyStream(teamListadapter.loading$, false)
// }) const teamListAdapterError = useReadonlyStream(teamListadapter.error$, null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const teamListFetched = ref(false)
const rootEl = ref<HTMLElement>()
const elVisible = useElementVisibility(rootEl)
const { pause: pauseListPoll, resume: resumeListPoll } = useIntervalFn(() => {
if (teamListadapter.isInitialized) {
teamListadapter.fetchList()
}
}, 10000)
watch(
elVisible,
() => {
if (elVisible.value) {
teamListadapter.fetchList()
resumeListPoll()
} else {
pauseListPoll()
}
},
{ immediate: true }
)
watch(myTeams, (teams) => {
if (teams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
const team = teams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) switchToTeamWorkspace(team)
}
}
})
const loading = computed(
() => isTeamListLoading.value && myTeams.value.length === 0
)
const workspace = workspaceService.currentWorkspace
const isActiveWorkspace = computed(() => (id: string) => {
if (workspace.value.type === "personal") return false
return workspace.value.teamID === id
})
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
REMEMBERED_TEAM_ID.value = team.id
workspaceService.changeWorkspace({
teamID: team.id,
teamName: team.name,
type: "team",
})
}
const switchToPersonalWorkspace = () => {
REMEMBERED_TEAM_ID.value = undefined
workspaceService.changeWorkspace({
type: "personal",
})
}
watch(
() => currentUser.value,
(user) => {
if (!user) {
switchToPersonalWorkspace()
}
}
)
const displayModalAdd = (shouldDisplay: boolean) => {
showModalAdd.value = shouldDisplay
teamListadapter.fetchList()
}
defineActionHandler("modals.team.new", () => {
displayModalAdd(true)
})
defineActionHandler("workspace.switch.personal", switchToPersonalWorkspace)
defineActionHandler("workspace.switch", ({ teamId }) => {
const team = myTeams.value.find((t) => t.id === teamId)
if (team) switchToTeamWorkspace(team)
})
</script> </script>

View File

@@ -1,65 +0,0 @@
<template>
<div>
<div class="flex flex-col">
<HoppSmartItem
v-for="candidate in candidates"
:key="candidate.id"
:label="candidate.name"
:info-icon="
activeWorkspaceInfo?.provider ===
testWorkspaceProviderService.providerID &&
activeWorkspaceInfo.workspaceID === candidate.id
? IconCheck
: undefined
"
:active-info-icon="
activeWorkspaceInfo?.provider ===
testWorkspaceProviderService.providerID &&
activeWorkspaceInfo.workspaceID === candidate.id
"
@click="selectWorkspace(candidate.id)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useService } from "dioc/vue"
import { computed } from "vue"
import { NewWorkspaceService } from "~/services/new-workspace"
import { TestWorkspaceProviderService } from "~/services/new-workspace/providers/test.workspace"
import IconCheck from "~icons/lucide/check"
import * as E from "fp-ts/Either"
const workspaceService = useService(NewWorkspaceService)
const testWorkspaceProviderService = useService(TestWorkspaceProviderService)
const candidates = testWorkspaceProviderService.getWorkspaceCandidates()
const activeWorkspaceInfo = computed(() => {
const activeWorkspaceHandle = workspaceService.activeWorkspaceHandle.value
const activeWorkspaceHandleRef = activeWorkspaceHandle?.get()
if (activeWorkspaceHandleRef?.value.type === "ok") {
return {
provider: activeWorkspaceHandleRef.value.data.providerID,
workspaceID: activeWorkspaceHandleRef.value.data.workspaceID,
}
}
return undefined
})
async function selectWorkspace(workspaceID: string) {
const result =
await testWorkspaceProviderService.getWorkspaceHandle(workspaceID)
// TODO: Re-evaluate this ?
if (E.isLeft(result)) {
console.error(result)
return
}
workspaceService.activeWorkspaceHandle.value = result.right
}
</script>

View File

@@ -1,92 +0,0 @@
import { HoppCollection } from "@hoppscotch/data"
import { ChildrenResult, SmartTreeAdapter } from "@hoppscotch/ui"
import { Ref, computed, ref } from "vue"
import { navigateToFolderWithIndexPath } from "~/newstore/collections"
import { RESTCollectionViewItem } from "~/services/new-workspace/view"
export class WorkspaceRESTSearchCollectionTreeAdapter
implements SmartTreeAdapter<RESTCollectionViewItem>
{
constructor(public data: Ref<HoppCollection[]>) {}
getChildren(
nodeID: string | null
): Ref<ChildrenResult<RESTCollectionViewItem>> {
const result = ref<ChildrenResult<RESTCollectionViewItem>>({
status: "loading",
})
return computed(() => {
if (nodeID === null) {
result.value = {
status: "loaded",
data: this.data.value.map((item, index) => ({
id: index.toString(),
data: <RESTCollectionViewItem>{
type: "collection",
value: {
collectionID: index.toString(),
isLastItem: index === this.data.value.length - 1,
name: item.name,
parentCollectionID: null,
},
},
})),
}
} else {
const indexPath = nodeID.split("/").map((x) => parseInt(x))
const item = navigateToFolderWithIndexPath(this.data.value, indexPath)
if (item) {
const collections = item.folders.map(
(childCollection, childCollectionID) => {
return {
id: `${nodeID}/${childCollectionID}`,
data: <RESTCollectionViewItem>{
type: "collection",
value: {
isLastItem:
item.folders?.length > 1
? childCollectionID === item.folders.length - 1
: false,
collectionID: `${nodeID}/${childCollectionID}`,
name: childCollection.name,
parentCollectionID: nodeID,
},
},
}
}
)
const requests = item.requests.map((request, requestID) => {
return {
id: `${nodeID}/${requestID}`,
data: <RESTCollectionViewItem>{
type: "request",
value: {
isLastItem:
item.requests?.length > 1
? requestID === item.requests.length - 1
: false,
parentCollectionID: nodeID,
collectionID: nodeID,
requestID: `${nodeID}/${requestID}`,
request,
},
},
}
})
result.value = {
status: "loaded",
data: [...collections, ...requests],
}
}
}
return result.value
})
}
}

View File

@@ -1,134 +0,0 @@
import {
ChildrenResult,
SmartTreeAdapter,
} from "@hoppscotch/ui/dist/src/helpers/treeAdapter"
import * as E from "fp-ts/Either"
import { Ref, ref, watchEffect } from "vue"
import { NewWorkspaceService } from "~/services/new-workspace"
import { Handle } from "~/services/new-workspace/handle"
import { RESTCollectionViewItem } from "~/services/new-workspace/view"
import { Workspace } from "~/services/new-workspace/workspace"
export class WorkspaceRESTCollectionTreeAdapter
implements SmartTreeAdapter<RESTCollectionViewItem>
{
constructor(
private workspaceHandle: Handle<Workspace>,
private workspaceService: NewWorkspaceService
) {}
public getChildren(
nodeID: string | null,
nodeType?: string
): Ref<ChildrenResult<RESTCollectionViewItem>> {
const workspaceHandleRef = this.workspaceHandle.get()
if (workspaceHandleRef.value.type !== "ok") {
throw new Error("Cannot issue children with invalid workspace handle")
}
const result = ref<ChildrenResult<RESTCollectionViewItem>>({
status: "loading",
})
if (nodeID !== null) {
;(async () => {
if (nodeType === "request") {
result.value = {
status: "loaded",
data: [],
}
return
}
const collectionHandleResult =
await this.workspaceService.getCollectionHandle(
this.workspaceHandle,
nodeID
)
// TODO: Better error handling
if (E.isLeft(collectionHandleResult)) {
throw new Error(JSON.stringify(collectionHandleResult.left.error))
}
const collectionHandle = collectionHandleResult.right
const collectionChildrenResult =
await this.workspaceService.getRESTCollectionChildrenView(
collectionHandle
)
// TODO: Better error handling
if (E.isLeft(collectionChildrenResult)) {
throw new Error(JSON.stringify(collectionChildrenResult.left.error))
}
const collectionChildrenViewHandle =
collectionChildrenResult.right.get()
watchEffect(() => {
if (collectionChildrenViewHandle.value.type !== "ok") return
if (collectionChildrenViewHandle.value.data.loading.value) {
result.value = {
status: "loading",
}
} else {
result.value = {
status: "loaded",
data: collectionChildrenViewHandle.value.data.content.value.map(
(item) => ({
id:
item.type === "request"
? item.value.requestID
: item.value.collectionID,
data: item,
})
),
}
}
})
})()
} else {
;(async () => {
const viewResult =
await this.workspaceService.getRESTRootCollectionView(
this.workspaceHandle
)
// TODO: Better error handling
if (E.isLeft(viewResult)) {
throw new Error(JSON.stringify(viewResult.left.error))
}
const viewHandle = viewResult.right.get()
watchEffect(() => {
if (viewHandle.value.type !== "ok") return
if (viewHandle.value.data.loading.value) {
result.value = {
status: "loading",
}
} else {
result.value = {
status: "loaded",
data: viewHandle.value.data.collections.value.map((coll) => ({
id: coll.collectionID,
data: {
type: "collection",
value: coll,
},
})),
}
}
})
})()
}
return result
}
}

View File

@@ -1,15 +1,12 @@
import { HoppCollection } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
import { getService } from "~/modules/dioc"
import { GQLTabService } from "~/services/tab/graphql"
import { RESTTabService } from "~/services/tab/rest"
import { runGQLQuery } from "../backend/GQLClient"
import { GetSingleRequestDocument } from "../backend/graphql"
import { HoppGQLSaveContext } from "../graphql/document"
import { HoppRESTSaveContext } from "../rest/document"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { getAffectedIndexes } from "./affectedIndex" import { getAffectedIndexes } from "./affectedIndex"
import { GetSingleRequestDocument } from "../backend/graphql"
import { runGQLQuery } from "../backend/GQLClient"
import * as E from "fp-ts/Either"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { GQLTabService } from "~/services/tab/graphql"
/** /**
* Resolve save context on reorder * Resolve save context on reorder
@@ -21,12 +18,15 @@ import { getAffectedIndexes } from "./affectedIndex"
* @returns * @returns
*/ */
export function resolveSaveContextOnCollectionReorder(payload: { export function resolveSaveContextOnCollectionReorder(
lastIndex: number payload: {
newIndex: number lastIndex: number
folderPath: string newIndex: number
length?: number // better way to do this? now it could be undefined folderPath: string
}) { length?: number // better way to do this? now it could be undefined
},
type: "remove" | "drop" = "remove"
) {
const { lastIndex, folderPath, length } = payload const { lastIndex, folderPath, length } = payload
let { newIndex } = payload let { newIndex } = payload
@@ -41,6 +41,12 @@ export function resolveSaveContextOnCollectionReorder(payload: {
if (newIndex === -1) { if (newIndex === -1) {
// if (newIndex === -1) remove it from the map because it will be deleted // if (newIndex === -1) remove it from the map because it will be deleted
affectedIndexes.delete(lastIndex) affectedIndexes.delete(lastIndex)
// when collection deleted opended requests from that collection be affected
if (type === "remove") {
resetSaveContextForAffectedRequests(
folderPath ? `${folderPath}/${lastIndex}` : lastIndex.toString()
)
}
} }
// add folder path as prefix to the affected indexes // add folder path as prefix to the affected indexes
@@ -56,27 +62,10 @@ export function resolveSaveContextOnCollectionReorder(payload: {
const tabService = getService(RESTTabService) const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => { const tabs = tabService.getTabsRefTo((tab) => {
if (tab.document.saveContext?.originLocation === "user-collection") { return (
return affectedPaths.has(tab.document.saveContext.folderPath) tab.document.saveContext?.originLocation === "user-collection" &&
} affectedPaths.has(tab.document.saveContext.folderPath)
)
if (
tab.document.saveContext?.originLocation !== "workspace-user-collection"
) {
return false
}
const requestHandleRef = tab.document.saveContext.requestHandle?.get()
if (!requestHandleRef || requestHandleRef.value.type === "invalid") {
return false
}
const { requestID } = requestHandleRef.value.data
const collectionID = requestID.split("/").slice(0, -1).join("/")
return affectedPaths.has(collectionID)
}) })
for (const tab of tabs) { for (const tab of tabs) {
@@ -86,34 +75,6 @@ export function resolveSaveContextOnCollectionReorder(payload: {
)! )!
tab.value.document.saveContext.folderPath = newPath tab.value.document.saveContext.folderPath = newPath
} }
if (
tab.value.document.saveContext?.originLocation !==
"workspace-user-collection"
) {
return false
}
const requestHandleRef = tab.value.document.saveContext.requestHandle?.get()
if (!requestHandleRef || requestHandleRef.value.type === "invalid") {
return false
}
const { requestID } = requestHandleRef.value.data
const collectionID = requestID.split("/").slice(0, -1).join("/")
const newCollectionID = affectedPaths.get(collectionID)
const newRequestID = `${newCollectionID}/${
requestID.split("/").slice(-1)[0]
}`
requestHandleRef.value.data = {
...requestHandleRef.value.data,
collectionID: newCollectionID!,
requestID: newRequestID,
}
} }
} }
@@ -125,63 +86,25 @@ export function resolveSaveContextOnCollectionReorder(payload: {
*/ */
export function updateSaveContextForAffectedRequests( export function updateSaveContextForAffectedRequests(
draggedCollectionIndex: string, oldFolderPath: string,
destinationCollectionIndex: string newFolderPath: string
) { ) {
const tabService = getService(RESTTabService) const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(oldFolderPath)
)
})
const activeTabs = tabService.getActiveTabs() for (const tab of tabs) {
if (tab.value.document.saveContext?.originLocation === "user-collection") {
for (const tab of activeTabs.value) { tab.value.document.saveContext = {
if (tab.document.saveContext?.originLocation === "user-collection") { ...tab.value.document.saveContext,
const { folderPath } = tab.document.saveContext folderPath: tab.value.document.saveContext.folderPath.replace(
oldFolderPath,
if (folderPath.startsWith(draggedCollectionIndex)) { newFolderPath
const newFolderPath = folderPath.replace( ),
draggedCollectionIndex,
destinationCollectionIndex
)
tab.document.saveContext = {
...tab.document.saveContext,
folderPath: newFolderPath,
}
}
return
}
if (
tab.document.saveContext?.originLocation === "workspace-user-collection"
) {
const requestHandleRef = tab.document.saveContext.requestHandle?.get()
if (!requestHandleRef || requestHandleRef.value.type === "invalid") {
return false
}
const { requestID } = requestHandleRef.value.data
const collectionID = requestID.split("/").slice(0, -1).join("/")
const requestIndex = requestID.split("/").slice(-1)[0]
if (collectionID.startsWith(draggedCollectionIndex)) {
const newCollectionID = collectionID.replace(
draggedCollectionIndex,
destinationCollectionIndex
)
const newRequestID = `${newCollectionID}/${requestIndex}`
tab.document.saveContext = {
...tab.document.saveContext,
requestID: newRequestID,
}
requestHandleRef.value.data = {
...requestHandleRef.value.data,
collectionID: newCollectionID,
requestID: newRequestID,
}
} }
} }
} }
@@ -243,33 +166,6 @@ function removeDuplicatesAndKeepLast(arr: HoppInheritedProperty["headers"]) {
return result return result
} }
function getSaveContextCollectionID(
saveContext: HoppRESTSaveContext | HoppGQLSaveContext | undefined
): string | undefined {
if (!saveContext) {
return
}
const { originLocation } = saveContext
if (originLocation === "team-collection") {
return saveContext.collectionID
}
if (originLocation === "user-collection") {
return saveContext.folderPath
}
const requestHandleRef = saveContext.requestHandle?.get()
if (!requestHandleRef || requestHandleRef.value.type === "invalid") {
return
}
// TODO: Remove `collectionID` and obtain it from `requestID`
return requestHandleRef.value.data.collectionID
}
export function updateInheritedPropertiesForAffectedRequests( export function updateInheritedPropertiesForAffectedRequests(
path: string, path: string,
inheritedProperties: HoppInheritedProperty, inheritedProperties: HoppInheritedProperty,
@@ -281,17 +177,22 @@ export function updateInheritedPropertiesForAffectedRequests(
const effectedTabs = tabService.getTabsRefTo((tab) => { const effectedTabs = tabService.getTabsRefTo((tab) => {
const saveContext = tab.document.saveContext const saveContext = tab.document.saveContext
const collectionID = getSaveContextCollectionID(saveContext) const saveContextPath =
return collectionID?.startsWith(path) ?? false saveContext?.originLocation === "team-collection"
? saveContext.collectionID
: saveContext?.folderPath
return saveContextPath?.startsWith(path) ?? false
}) })
effectedTabs.map((tab) => { effectedTabs.map((tab) => {
const inheritedParentID = const inheritedParentID =
tab.value.document.inheritedProperties?.auth.parentID tab.value.document.inheritedProperties?.auth.parentID
const contextPath = getSaveContextCollectionID( const contextPath =
tab.value.document.saveContext tab.value.document.saveContext?.originLocation === "team-collection"
) ? tab.value.document.saveContext.collectionID
: tab.value.document.saveContext?.folderPath
const effectedPath = folderPathCloseToSaveContext( const effectedPath = folderPathCloseToSaveContext(
inheritedParentID, inheritedParentID,
@@ -326,6 +227,21 @@ export function updateInheritedPropertiesForAffectedRequests(
}) })
} }
function resetSaveContextForAffectedRequests(folderPath: string) {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(folderPath)
)
})
for (const tab of tabs) {
tab.value.document.saveContext = null
tab.value.document.isDirty = true
}
}
/** /**
* Reset save context to null if requests are deleted from the team collection or its folder * Reset save context to null if requests are deleted from the team collection or its folder
* only runs when collection or folder is deleted * only runs when collection or folder is deleted

View File

@@ -3,10 +3,9 @@ import {
HoppGQLRequest, HoppGQLRequest,
HoppRESTRequest, HoppRESTRequest,
} from "@hoppscotch/data" } from "@hoppscotch/data"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { getAffectedIndexes } from "./affectedIndex" import { getAffectedIndexes } from "./affectedIndex"
import { RESTTabService } from "~/services/tab/rest"
import { getService } from "~/modules/dioc"
/** /**
* Resolve save context on reorder * Resolve save context on reorder
@@ -30,76 +29,30 @@ export function resolveSaveContextOnRequestReorder(payload: {
if (newIndex > lastIndex) newIndex-- // there is a issue when going down? better way to resolve this? if (newIndex > lastIndex) newIndex-- // there is a issue when going down? better way to resolve this?
if (lastIndex === newIndex) return if (lastIndex === newIndex) return
const affectedIndices = getAffectedIndexes( const affectedIndexes = getAffectedIndexes(
lastIndex, lastIndex,
newIndex === -1 ? length! : newIndex newIndex === -1 ? length! : newIndex
) )
// if (newIndex === -1) remove it from the map because it will be deleted // if (newIndex === -1) remove it from the map because it will be deleted
if (newIndex === -1) affectedIndices.delete(lastIndex) if (newIndex === -1) affectedIndexes.delete(lastIndex)
const tabService = getService(RESTTabService) const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => { const tabs = tabService.getTabsRefTo((tab) => {
if (tab.document.saveContext?.originLocation === "user-collection") { return (
return ( tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath === folderPath && tab.document.saveContext.folderPath === folderPath &&
affectedIndices.has(tab.document.saveContext.requestIndex) affectedIndexes.has(tab.document.saveContext.requestIndex)
) )
}
if (
tab.document.saveContext?.originLocation !== "workspace-user-collection"
) {
return false
}
const requestHandleRef = tab.document.saveContext.requestHandle?.get()
if (!requestHandleRef || requestHandleRef.value.type === "invalid") {
return false
}
const { requestID } = requestHandleRef.value.data
const collectionID = requestID.split("/").slice(0, -1).join("/")
const requestIndex = parseInt(requestID.split("/").slice(-1)[0])
return collectionID === folderPath && affectedIndices.has(requestIndex)
}) })
for (const tab of tabs) { for (const tab of tabs) {
if (tab.value.document.saveContext?.originLocation === "user-collection") { if (tab.value.document.saveContext?.originLocation === "user-collection") {
const newIndex = affectedIndices.get( const newIndex = affectedIndexes.get(
tab.value.document.saveContext?.requestIndex tab.value.document.saveContext?.requestIndex
)! )!
tab.value.document.saveContext.requestIndex = newIndex tab.value.document.saveContext.requestIndex = newIndex
} }
if (
tab.value.document.saveContext?.originLocation !==
"workspace-user-collection"
) {
return
}
const requestHandleRef = tab.value.document.saveContext.requestHandle?.get()
if (!requestHandleRef || requestHandleRef.value.type === "invalid") {
return
}
const { requestID } = requestHandleRef.value.data
const requestIDArr = requestID.split("/")
const requestIndex = affectedIndices.get(
parseInt(requestIDArr[requestIDArr.length - 1])
)!
requestIDArr[requestIDArr.length - 1] = requestIndex.toString()
requestHandleRef.value.data.requestID = requestIDArr.join("/")
requestHandleRef.value.data.collectionID = requestIDArr
.slice(0, -1)
.join("/")
} }
} }
@@ -114,7 +67,7 @@ export function getRequestsByPath(
if (pathArray.length === 1) { if (pathArray.length === 1) {
const latestVersionedRequests = currentCollection.requests.filter( const latestVersionedRequests = currentCollection.requests.filter(
(req): req is HoppRESTRequest => req.v === "4" (req): req is HoppRESTRequest => req.v === "3"
) )
return latestVersionedRequests return latestVersionedRequests
@@ -125,7 +78,7 @@ export function getRequestsByPath(
} }
const latestVersionedRequests = currentCollection.requests.filter( const latestVersionedRequests = currentCollection.requests.filter(
(req): req is HoppRESTRequest => req.v === "4" (req): req is HoppRESTRequest => req.v === "3"
) )
return latestVersionedRequests return latestVersionedRequests

View File

@@ -1,31 +1,32 @@
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { platform } from "~/platform"
/** /**
* Create a downloadable file from a collection and prompts the user to download it. * Create a downloadable file from a collection and prompts the user to download it.
* @param collectionJSON - JSON string of the collection * @param collectionJSON - JSON string of the collection
* @param name - Name of the collection set as the file name * @param name - Name of the collection set as the file name
*/ */
export const initializeDownloadFile = async ( export const initializeDownloadCollection = (
collectionJSON: string, collectionJSON: string,
name: string | null name: string | null
) => { ) => {
const result = await platform.io.saveFileWithDialog({ const file = new Blob([collectionJSON], { type: "application/json" })
data: collectionJSON, const a = document.createElement("a")
contentType: "application/json", const url = URL.createObjectURL(file)
suggestedFilename: `${name ?? "collection"}.json`, a.href = url
filters: [
{
name: "Hoppscotch Collection/Environment JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") { if (name) {
return E.right("state.download_started") a.download = `${name}.json`
} else {
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
} }
return E.left("state.download_failed") document.body.appendChild(a)
a.click()
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
return E.right("state.download_started")
} }

View File

@@ -0,0 +1,5 @@
import { HoppCollection } from "@hoppscotch/data"
export const myCollectionsExporter = (myCollections: HoppCollection[]) => {
return JSON.stringify(myCollections, null, 2)
}

View File

@@ -3,37 +3,8 @@ import { HoppRESTResponse } from "../types/HoppRESTResponse"
import { HoppTestResult } from "../types/HoppTestResult" import { HoppTestResult } from "../types/HoppTestResult"
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue" import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties" import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { Handle } from "~/services/new-workspace/handle"
import { WorkspaceRequest } from "~/services/new-workspace/workspace"
export type HoppRESTSaveContext = export type HoppRESTSaveContext =
| {
/**
* The origin source of the request
*/
// TODO: Make this `user-collection` after porting all usages
// Future TODO: Keep separate types for the IDs (specific to persistence) & `requestHandle` (only existing at runtime)
originLocation: "workspace-user-collection"
/**
* ID of the workspace
*/
workspaceID?: string
/**
* ID of the provider
*/
providerID?: string
/**
* Path to the request in the collection tree
*/
requestID?: string
/**
* Handle to the request open in the tab
*/
requestHandle?: Handle<WorkspaceRequest>
}
| { | {
/** /**
* The origin source of the request * The origin source of the request

View File

@@ -50,7 +50,6 @@ export default class TeamListAdapter {
} }
public dispose() { public dispose() {
this.teamList$.next([])
this.isDispose = true this.isDispose = true
clearTimeout(this.timeoutHandle as any) clearTimeout(this.timeoutHandle as any)
this.timeoutHandle = null this.timeoutHandle = null

View File

@@ -201,7 +201,7 @@ export class TeamSearchService extends Service {
expandingCollections: Ref<string[]> = ref([]) expandingCollections: Ref<string[]> = ref([])
expandedCollections: Ref<string[]> = ref([]) expandedCollections: Ref<string[]> = ref([])
// TODO: ideally this should return the search results / formatted results instead of directly manipulating the result set // FUTURE-TODO: ideally this should return the search results / formatted results instead of directly manipulating the result set
// eg: do the spotlight formatting in the spotlight searcher and not here // eg: do the spotlight formatting in the spotlight searcher and not here
searchTeams = async (query: string, teamID: string) => { searchTeams = async (query: string, teamID: string) => {
if (!query.length) { if (!query.length) {

View File

@@ -17,24 +17,3 @@ export type HoppInheritedProperty = {
inheritedHeader: HoppRESTHeader | GQLHeader inheritedHeader: HoppRESTHeader | GQLHeader
}[] }[]
} }
type ModifiedAuth<T, AuthType> = {
[K in keyof T]: K extends "inheritedAuth" ? Extract<T[K], AuthType> : T[K]
}
type ModifiedHeaders<T, HeaderType> = {
[K in keyof T]: K extends "inheritedHeader" ? Extract<T[K], HeaderType> : T[K]
}
type ModifiedHoppInheritedProperty<AuthType, HeaderType> = {
auth: ModifiedAuth<HoppInheritedProperty["auth"], AuthType>
headers: ModifiedHeaders<
HoppInheritedProperty["headers"][number],
HeaderType
>[]
}
export type HoppInheritedRESTProperty = ModifiedHoppInheritedProperty<
HoppRESTAuth,
HoppRESTHeader
>

View File

@@ -20,29 +20,28 @@ export type Picked =
pickedType: "my-collection" pickedType: "my-collection"
collectionIndex: number collectionIndex: number
} }
// TODO: Enable this when rest of the implementation is in place | {
// | { pickedType: "teams-request"
// pickedType: "teams-request" requestID: string
// requestID: string }
// } | {
// | { pickedType: "teams-folder"
// pickedType: "teams-folder" folderID: string
// folderID: string }
// } | {
// | { pickedType: "teams-collection"
// pickedType: "teams-collection" collectionID: string
// collectionID: string }
// } | {
// | { pickedType: "gql-my-request"
// pickedType: "gql-my-request" folderPath: string
// folderPath: string requestIndex: number
// requestIndex: number }
// } | {
// | { pickedType: "gql-my-folder"
// pickedType: "gql-my-folder" folderPath: string
// folderPath: string }
// } | {
// | { pickedType: "gql-my-collection"
// pickedType: "gql-my-collection" collectionIndex: number
// collectionIndex: number }
// }

View File

@@ -1,17 +0,0 @@
/**
* Create a function that will only run the given function once and caches the result.
*/
export function lazy<T>(fn: () => T): () => T {
let funcRan = false
let result: T | null = null
return () => {
if (!funcRan) {
result = fn()
funcRan = true
return result
}
return result!
}
}

View File

@@ -3,40 +3,36 @@ import { createApp } from "vue"
import { initializeApp } from "./helpers/app" import { initializeApp } from "./helpers/app"
import { initBackendGQLClient } from "./helpers/backend/GQLClient" import { initBackendGQLClient } from "./helpers/backend/GQLClient"
import { performMigrations } from "./helpers/migrations" import { performMigrations } from "./helpers/migrations"
import { getService } from "./modules/dioc"
import { PlatformDef, setPlatformDef } from "./platform" import { PlatformDef, setPlatformDef } from "./platform"
import { PersonalWorkspaceProviderService } from "./services/new-workspace/providers/personal.workspace"
import { TestWorkspaceProviderService } from "./services/new-workspace/providers/test.workspace"
import { PersistenceService } from "./services/persistence"
import "nprogress/nprogress.css"
import "../assets/scss/styles.scss"
import "../assets/scss/tailwind.scss" import "../assets/scss/tailwind.scss"
import "../assets/themes/themes.scss" import "../assets/themes/themes.scss"
import "../assets/scss/styles.scss"
import "nprogress/nprogress.css"
import "unfonts.css"
import App from "./App.vue" import App from "./App.vue"
import { getService } from "./modules/dioc"
import { PersistenceService } from "./services/persistence"
export function createHoppApp(el: string | Element, platformDef: PlatformDef) { export function createHoppApp(el: string | Element, platformDef: PlatformDef) {
setPlatformDef(platformDef) setPlatformDef(platformDef)
const app = createApp(App) const app = createApp(App)
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
platformDef.addedHoppModules?.forEach((mod) => mod.onVueAppInit?.(app))
// Some basic work that needs to be done before module inits even // Some basic work that needs to be done before module inits even
initBackendGQLClient() initBackendGQLClient()
initializeApp() initializeApp()
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
platformDef.addedHoppModules?.forEach((mod) => mod.onVueAppInit?.(app))
// TODO: Explore possibilities of moving this invocation to the service constructor // TODO: Explore possibilities of moving this invocation to the service constructor
// `toast` was coming up as `null` in the previous attempts // `toast` was coming up as `null` in the previous attempts
getService(PersistenceService).setupLocalPersistence() getService(PersistenceService).setupLocalPersistence()
performMigrations() performMigrations()
// TODO: Remove this
getService(TestWorkspaceProviderService)
getService(PersonalWorkspaceProviderService)
app.mount(el) app.mount(el)
console.info( console.info(

View File

@@ -1,5 +1,5 @@
import { HoppModule } from "." import { HoppModule } from "."
import { Container, ServiceClassInstance } from "dioc" import { Container, Service } from "dioc"
import { diocPlugin } from "dioc/vue" import { diocPlugin } from "dioc/vue"
import { DebugService } from "~/services/debug.service" import { DebugService } from "~/services/debug.service"
import { platform } from "~/platform" import { platform } from "~/platform"
@@ -22,7 +22,7 @@ if (import.meta.env.DEV) {
* services. Please use `useService` if within components or try to convert your * services. Please use `useService` if within components or try to convert your
* legacy subsystem into a service if possible. * legacy subsystem into a service if possible.
*/ */
export function getService<T extends ServiceClassInstance<any>>( export function getService<T extends typeof Service<any> & { ID: string }>(
service: T service: T
): InstanceType<T> { ): InstanceType<T> {
return serviceContainer.bind(service) return serviceContainer.bind(service)
@@ -30,10 +30,11 @@ export function getService<T extends ServiceClassInstance<any>>(
export default <HoppModule>{ export default <HoppModule>{
onVueAppInit(app) { onVueAppInit(app) {
// TODO: look into this
// @ts-expect-error Something weird with Vue versions
app.use(diocPlugin, { app.use(diocPlugin, {
container: serviceContainer, container: serviceContainer,
}) })
for (const service of platform.addedServices ?? []) { for (const service of platform.addedServices ?? []) {
serviceContainer.bind(service) serviceContainer.bind(service)
} }

View File

@@ -319,7 +319,6 @@ const restCollectionDispatchers = defineDispatchers({
) )
return {} return {}
} }
// We get the index path to the folder itself, // We get the index path to the folder itself,
// we have to find the folder containing the target folder, // we have to find the folder containing the target folder,
// so we pop the last path index // so we pop the last path index
@@ -691,11 +690,17 @@ const restCollectionDispatchers = defineDispatchers({
// if the destination is null, we are moving to the end of the list // if the destination is null, we are moving to the end of the list
if (destinationRequestIndex === null) { if (destinationRequestIndex === null) {
// TODO: Verify if this can be safely removed // move to the end of the list
targetLocation.requests.push( targetLocation.requests.push(
targetLocation.requests.splice(requestIndex, 1)[0] targetLocation.requests.splice(requestIndex, 1)[0]
) )
resolveSaveContextOnRequestReorder({
lastIndex: requestIndex,
newIndex: targetLocation.requests.length,
folderPath: destinationCollectionPath,
})
return { return {
state: newState, state: newState,
} }
@@ -703,6 +708,12 @@ const restCollectionDispatchers = defineDispatchers({
reorderItems(targetLocation.requests, requestIndex, destinationRequestIndex) reorderItems(targetLocation.requests, requestIndex, destinationRequestIndex)
resolveSaveContextOnRequestReorder({
lastIndex: requestIndex,
newIndex: destinationRequestIndex,
folderPath: destinationCollectionPath,
})
return { return {
state: newState, state: newState,
} }

View File

@@ -1,10 +1,9 @@
import { pluck, distinctUntilChanged } from "rxjs/operators"
import { cloneDeep, defaultsDeep, has } from "lodash-es" import { cloneDeep, defaultsDeep, has } from "lodash-es"
import { Observable } from "rxjs" import { Observable } from "rxjs"
import { distinctUntilChanged, pluck } from "rxjs/operators"
import { nextTick } from "vue"
import { platform } from "~/platform"
import type { KeysMatching } from "~/types/ts-utils"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore" import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import type { KeysMatching } from "~/types/ts-utils"
export const HoppBgColors = ["system", "light", "dark", "black"] as const export const HoppBgColors = ["system", "light", "dark", "black"] as const
@@ -70,63 +69,51 @@ export type SettingsDef = {
HAS_OPENED_SPOTLIGHT: boolean HAS_OPENED_SPOTLIGHT: boolean
} }
export const getDefaultSettings = (): SettingsDef => { export const getDefaultSettings = (): SettingsDef => ({
const defaultSettings: SettingsDef = { syncCollections: true,
syncCollections: true, syncHistory: true,
syncHistory: true, syncEnvironments: true,
syncEnvironments: true,
WRAP_LINES: { WRAP_LINES: {
httpRequestBody: true, httpRequestBody: true,
httpResponseBody: true, httpResponseBody: true,
httpHeaders: true, httpHeaders: true,
httpParams: true, httpParams: true,
httpUrlEncoded: true, httpUrlEncoded: true,
httpPreRequest: true, httpPreRequest: true,
httpTest: true, httpTest: true,
httpRequestVariables: true, httpRequestVariables: true,
graphqlQuery: true, graphqlQuery: true,
graphqlResponseBody: true, graphqlResponseBody: true,
graphqlHeaders: false, graphqlHeaders: false,
graphqlVariables: false, graphqlVariables: false,
graphqlSchema: true, graphqlSchema: true,
importCurl: true, importCurl: true,
codeGen: true, codeGen: true,
cookie: true, cookie: true,
}, },
CURRENT_INTERCEPTOR_ID: "", CURRENT_INTERCEPTOR_ID: "browser", // TODO: Allow the platform definition to take this place
// TODO: Interceptor related settings should move under the interceptor systems // TODO: Interceptor related settings should move under the interceptor systems
PROXY_URL: "https://proxy.hoppscotch.io/", PROXY_URL: "https://proxy.hoppscotch.io/",
URL_EXCLUDES: { URL_EXCLUDES: {
auth: true, auth: true,
httpUser: true, httpUser: true,
httpPassword: true, httpPassword: true,
bearerToken: true, bearerToken: true,
oauth2Token: true, oauth2Token: true,
}, },
THEME_COLOR: "indigo", THEME_COLOR: "indigo",
BG_COLOR: "system", BG_COLOR: "system",
TELEMETRY_ENABLED: true, TELEMETRY_ENABLED: true,
EXPAND_NAVIGATION: false, EXPAND_NAVIGATION: false,
SIDEBAR: true, SIDEBAR: true,
SIDEBAR_ON_LEFT: false, SIDEBAR_ON_LEFT: false,
COLUMN_LAYOUT: true, COLUMN_LAYOUT: true,
HAS_OPENED_SPOTLIGHT: false, HAS_OPENED_SPOTLIGHT: false,
} })
// Wait for platform to initialize before setting CURRENT_INTERCEPTOR_ID
nextTick(() => {
applySetting(
"CURRENT_INTERCEPTOR_ID",
platform?.interceptors.default || "browser"
)
})
return defaultSettings
}
type ApplySettingPayload = { type ApplySettingPayload = {
[K in keyof SettingsDef]: { [K in keyof SettingsDef]: {

View File

@@ -31,7 +31,7 @@
</template> </template>
<template #suffix> <template #suffix>
<span <span
v-if="getTabDirtyStatus(tab)" v-if="tab.document.isDirty"
class="flex w-4 items-center justify-center text-secondary group-hover:hidden" class="flex w-4 items-center justify-center text-secondary group-hover:hidden"
> >
<svg <svg
@@ -95,24 +95,24 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from "@composables/i18n" import { ref, onMounted } from "vue"
import { safelyExtractRESTRequest } from "@hoppscotch/data" import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import { cloneDeep } from "lodash-es"
import { onMounted, ref } from "vue"
import { useRoute } from "vue-router"
import { useReadonlyStream } from "~/composables/stream"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams" import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { defineActionHandler, invokeAction } from "~/helpers/actions" import { useRoute } from "vue-router"
import { useI18n } from "@composables/i18n"
import { getDefaultRESTRequest } from "~/helpers/rest/default" import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { HoppRESTDocument } from "~/helpers/rest/document" import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useReadonlyStream } from "~/composables/stream"
import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection" import { InspectionService } from "~/services/inspection"
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector" import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector" import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
import { HoppTab } from "~/services/tab" import { cloneDeep } from "lodash-es"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
const savingRequest = ref(false) const savingRequest = ref(false)
const confirmingCloseForTabID = ref<string | null>(null) const confirmingCloseForTabID = ref<string | null>(null)
@@ -193,7 +193,7 @@ const inspectionService = useService(InspectionService)
const removeTab = (tabID: string) => { const removeTab = (tabID: string) => {
const tabState = tabs.getTabRef(tabID).value const tabState = tabs.getTabRef(tabID).value
if (getTabDirtyStatus(tabState)) { if (tabState.document.isDirty) {
confirmingCloseForTabID.value = tabID confirmingCloseForTabID.value = tabID
} else { } else {
tabs.closeTab(tabState.id) tabs.closeTab(tabState.id)
@@ -202,10 +202,8 @@ const removeTab = (tabID: string) => {
} }
const closeOtherTabsAction = (tabID: string) => { const closeOtherTabsAction = (tabID: string) => {
const isTabDirty = tabs.getTabRef(tabID).value?.document.isDirty
const dirtyTabCount = tabs.getDirtyTabsCount() const dirtyTabCount = tabs.getDirtyTabsCount()
const isTabDirty = getTabDirtyStatus(tabs.getTabRef(tabID).value)
// If current tab is dirty, so we need to subtract 1 from the dirty tab count // If current tab is dirty, so we need to subtract 1 from the dirty tab count
const balanceDirtyTabCount = isTabDirty ? dirtyTabCount - 1 : dirtyTabCount const balanceDirtyTabCount = isTabDirty ? dirtyTabCount - 1 : dirtyTabCount
@@ -270,24 +268,15 @@ const onCloseConfirmSaveTab = () => {
* Called when the user confirms they want to save the tab * Called when the user confirms they want to save the tab
*/ */
const onResolveConfirmSaveTab = () => { const onResolveConfirmSaveTab = () => {
const { saveContext } = tabs.currentActiveTab.value.document if (tabs.currentActiveTab.value.document.saveContext) {
invokeAction("request.save")
// There're two cases where the save request under a collection modal should open if (confirmingCloseForTabID.value) {
// 1. Attempting to save a request that is not under a collection (When the save context is not available) tabs.closeTab(confirmingCloseForTabID.value)
// 2. Deleting a request from the collection tree and attempting to save it while closing the respective tab (When the request handle is invalid) confirmingCloseForTabID.value = null
if ( }
!saveContext || } else {
(saveContext.originLocation === "workspace-user-collection" && savingRequest.value = true
saveContext.requestHandle?.get().value.type === "invalid")
) {
return (savingRequest.value = true)
}
invokeAction("request.save")
if (confirmingCloseForTabID.value) {
tabs.closeTab(confirmingCloseForTabID.value)
confirmingCloseForTabID.value = null
} }
} }
@@ -315,17 +304,6 @@ const shareTabRequest = (tabID: string) => {
} }
} }
const getTabDirtyStatus = (tab: HoppTab<HoppRESTDocument>) => {
if (tab.document.isDirty) {
return true
}
return (
tab.document.saveContext?.originLocation === "workspace-user-collection" &&
tab.document.saveContext.requestHandle?.get().value.type === "invalid"
)
}
defineActionHandler("contextmenu.open", ({ position, text }) => { defineActionHandler("contextmenu.open", ({ position, text }) => {
if (text) { if (text) {
contextMenu.value = { contextMenu.value = {

View File

@@ -8,15 +8,14 @@ import { AnalyticsPlatformDef } from "./analytics"
import { InterceptorsPlatformDef } from "./interceptors" import { InterceptorsPlatformDef } from "./interceptors"
import { HoppModule } from "~/modules" import { HoppModule } from "~/modules"
import { InspectorsPlatformDef } from "./inspectors" import { InspectorsPlatformDef } from "./inspectors"
import { ServiceClassInstance } from "dioc" import { Service } from "dioc"
import { IOPlatformDef } from "./io" import { IOPlatformDef } from "./io"
import { SpotlightPlatformDef } from "./spotlight" import { SpotlightPlatformDef } from "./spotlight"
import { Ref } from "vue"
export type PlatformDef = { export type PlatformDef = {
ui?: UIPlatformDef ui?: UIPlatformDef
addedHoppModules?: HoppModule[] addedHoppModules?: HoppModule[]
addedServices?: Array<ServiceClassInstance<unknown>> addedServices?: Array<typeof Service<unknown> & { ID: string }>
auth: AuthPlatformDef auth: AuthPlatformDef
analytics?: AnalyticsPlatformDef analytics?: AnalyticsPlatformDef
io: IOPlatformDef io: IOPlatformDef
@@ -46,11 +45,6 @@ export type PlatformDef = {
* If a value is not given, then the value is assumed to be true * If a value is not given, then the value is assumed to be true
*/ */
promptAsUsingCookies?: boolean promptAsUsingCookies?: boolean
/**
* Whether to show the A/B testing workspace switcher click login flow or not
*/
workspaceSwitcherLogin?: Ref<boolean>
} }
} }

View File

@@ -1,4 +1,4 @@
import { Container, ServiceClassInstance } from "dioc" import { Service } from "dioc"
import { Inspector } from "~/services/inspection" import { Inspector } from "~/services/inspection"
/** /**
@@ -8,9 +8,8 @@ export type PlatformInspectorsDef = {
// We are keeping this as the only mode for now // We are keeping this as the only mode for now
// So that if we choose to add other modes, we can do without breaking // So that if we choose to add other modes, we can do without breaking
type: "service" type: "service"
// TODO: I don't think this type is effective, we have to come up with a better impl service: typeof Service<unknown> & { ID: string } & {
service: ServiceClassInstance<unknown> & { new (): Service & Inspector
new (c: Container): Inspector
} }
} }

View File

@@ -1,13 +1,12 @@
import { Container, ServiceClassInstance } from "dioc" import { Service } from "dioc"
import { Interceptor } from "~/services/interceptor.service" import { Interceptor } from "~/services/interceptor.service"
export type PlatformInterceptorDef = export type PlatformInterceptorDef =
| { type: "standalone"; interceptor: Interceptor } | { type: "standalone"; interceptor: Interceptor }
| { | {
type: "service" type: "service"
// TODO: I don't think this type is effective, we have to come up with a better impl service: typeof Service<unknown> & { ID: string } & {
service: ServiceClassInstance<unknown> & { new (): Service & Interceptor
new (c: Container): Interceptor
} }
} }

View File

@@ -1,10 +1,10 @@
import { Container, ServiceClassInstance } from "dioc" import { Service } from "dioc"
import { SpotlightSearcher } from "~/services/spotlight" import { SpotlightSearcher } from "~/services/spotlight"
export type SpotlightPlatformDef = { export type SpotlightPlatformDef = {
additionalSearchers?: Array< additionalSearchers?: Array<
ServiceClassInstance<unknown> & { typeof Service<unknown> & { ID: string } & {
new (c: Container): SpotlightSearcher new (): Service & SpotlightSearcher
} }
> >
} }

View File

@@ -31,7 +31,9 @@ export class ExtensionInspectorService extends Service implements Inspector {
private readonly inspection = this.bind(InspectionService) private readonly inspection = this.bind(InspectionService)
override onServiceInit() { constructor() {
super()
this.inspection.registerInspector(this) this.inspection.registerInspector(this)
} }

View File

@@ -133,7 +133,9 @@ export class ExtensionInterceptorService
public selectable = { type: "selectable" as const } public selectable = { type: "selectable" as const }
override onServiceInit() { constructor() {
super()
this.listenForExtensionStatus() this.listenForExtensionStatus()
} }

View File

@@ -13,17 +13,15 @@ export const browserIODef: IOPlatformDef = {
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url a.href = url
a.download = a.download = pipe(
opts.suggestedFilename ?? url,
pipe( S.split("/"),
url, RNEA.last,
S.split("/"), S.split("#"),
RNEA.last, RNEA.head,
S.split("#"), S.split("?"),
RNEA.head, RNEA.head
S.split("?"), )
RNEA.head
)
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()

View File

@@ -24,7 +24,9 @@ export class EnvironmentMenuService extends Service implements ContextMenu {
private readonly contextMenu = this.bind(ContextMenuService) private readonly contextMenu = this.bind(ContextMenuService)
override onServiceInit() { constructor() {
super()
this.contextMenu.registerMenu(this) this.contextMenu.registerMenu(this)
} }

View File

@@ -41,7 +41,9 @@ export class ParameterMenuService extends Service implements ContextMenu {
private readonly contextMenu = this.bind(ContextMenuService) private readonly contextMenu = this.bind(ContextMenuService)
override onServiceInit() { constructor() {
super()
this.contextMenu.registerMenu(this) this.contextMenu.registerMenu(this)
} }

View File

@@ -39,7 +39,9 @@ export class URLMenuService extends Service implements ContextMenu {
private readonly contextMenu = this.bind(ContextMenuService) private readonly contextMenu = this.bind(ContextMenuService)
private readonly restTab = this.bind(RESTTabService) private readonly restTab = this.bind(RESTTabService)
override onServiceInit() { constructor() {
super()
this.contextMenu.registerMenu(this) this.contextMenu.registerMenu(this)
} }

View File

@@ -20,6 +20,10 @@ export class CookieJarService extends Service {
*/ */
public cookieJar = ref(new Map<string, string[]>()) public cookieJar = ref(new Map<string, string[]>())
constructor() {
super()
}
public parseSetCookieString(setCookieString: string) { public parseSetCookieString(setCookieString: string) {
return setCookieParse(setCookieString) return setCookieParse(setCookieString)
} }

View File

@@ -14,7 +14,9 @@ import { Service } from "dioc"
export class DebugService extends Service { export class DebugService extends Service {
public static readonly ID = "DEBUG_SERVICE" public static readonly ID = "DEBUG_SERVICE"
override onServiceInit() { constructor() {
super()
console.log("DebugService is initialized...") console.log("DebugService is initialized...")
const container = this.getContainer() const container = this.getContainer()

View File

@@ -107,7 +107,9 @@ export class InspectionService extends Service {
private readonly restTab = this.bind(RESTTabService) private readonly restTab = this.bind(RESTTabService)
override onServiceInit() { constructor() {
super()
this.initializeListeners() this.initializeListeners()
} }
@@ -121,11 +123,6 @@ export class InspectionService extends Service {
} }
private initializeListeners() { private initializeListeners() {
console.log(
`Current active tab from inspection service is `,
this.restTab.currentActiveTab.value
)
watch( watch(
() => [this.inspectors.entries(), this.restTab.currentActiveTab.value.id], () => [this.inspectors.entries(), this.restTab.currentActiveTab.value.id],
() => { () => {

View File

@@ -53,7 +53,9 @@ export class EnvironmentInspectorService extends Service implements Inspector {
} }
)[0] )[0]
override onServiceInit() { constructor() {
super()
this.inspection.registerInspector(this) this.inspection.registerInspector(this)
} }

View File

@@ -22,7 +22,9 @@ export class HeaderInspectorService extends Service implements Inspector {
private readonly inspection = this.bind(InspectionService) private readonly inspection = this.bind(InspectionService)
private readonly interceptorService = this.bind(InterceptorService) private readonly interceptorService = this.bind(InterceptorService)
override onServiceInit() { constructor() {
super()
this.inspection.registerInspector(this) this.inspection.registerInspector(this)
} }

View File

@@ -23,7 +23,9 @@ export class ResponseInspectorService extends Service implements Inspector {
private readonly inspection = this.bind(InspectionService) private readonly inspection = this.bind(InspectionService)
override onServiceInit() { constructor() {
super()
this.inspection.registerInspector(this) this.inspection.registerInspector(this)
} }

View File

@@ -178,7 +178,9 @@ export class InterceptorService extends Service {
return this.interceptors.get(this.currentInterceptorID.value) return this.interceptors.get(this.currentInterceptorID.value)
}) })
override onServiceInit() { constructor() {
super()
// If the current interceptor is unselectable, select the first selectable one, else null // If the current interceptor is unselectable, select the first selectable one, else null
watch([() => this.interceptors, this.currentInterceptorID], () => { watch([() => this.interceptors, this.currentInterceptorID], () => {
if (!this.currentInterceptorID.value) return if (!this.currentInterceptorID.value) return

View File

@@ -1,18 +0,0 @@
import { Ref, WritableComputedRef } from "vue"
export type Handle<T, InvalidateReason = unknown> = {
get: () => HandleRef<T, InvalidateReason>
}
export type HandleRef<T, InvalidateReason = unknown> = Ref<
HandleState<T, InvalidateReason>
>
export type HandleState<T, InvalidateReason> =
| { type: "ok"; data: T }
| { type: "invalid"; reason: InvalidateReason }
export type WritableHandleRef<
T,
InvalidateReason = unknown,
> = WritableComputedRef<HandleState<T, InvalidateReason>>

View File

@@ -1,48 +0,0 @@
import { Ref } from "vue"
import { HandleRef } from "./handle"
import { Workspace, WorkspaceCollection, WorkspaceRequest } from "./workspace"
export const isValidWorkspaceHandle = (
workspaceHandle: HandleRef<Workspace>,
providerID: string,
workspaceID: string
): workspaceHandle is Ref<{
data: Workspace
type: "ok"
}> => {
return (
workspaceHandle.value.type === "ok" &&
workspaceHandle.value.data.providerID === providerID &&
workspaceHandle.value.data.workspaceID === workspaceID
)
}
export const isValidCollectionHandle = (
collectionHandle: HandleRef<WorkspaceCollection>,
providerID: string,
workspaceID: string
): collectionHandle is Ref<{
data: WorkspaceCollection
type: "ok"
}> => {
return (
collectionHandle.value.type === "ok" &&
collectionHandle.value.data.providerID === providerID &&
collectionHandle.value.data.workspaceID === workspaceID
)
}
export const isValidRequestHandle = (
requestHandle: HandleRef<WorkspaceRequest>,
providerID: string,
workspaceID: string
): requestHandle is Ref<{
data: WorkspaceRequest
type: "ok"
}> => {
return (
requestHandle.value.type === "ok" &&
requestHandle.value.data.providerID === providerID &&
requestHandle.value.data.workspaceID === workspaceID
)
}

View File

@@ -1,807 +0,0 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { Service } from "dioc"
import * as E from "fp-ts/Either"
import {
Component,
Ref,
computed,
markRaw,
shallowReactive,
shallowRef,
watch,
} from "vue"
import { Handle } from "./handle"
import { WorkspaceProvider } from "./provider"
import {
RESTCollectionChildrenView,
RESTCollectionJSONView,
RESTCollectionLevelAuthHeadersView,
RESTSearchResultsView,
RootRESTCollectionView,
} from "./view"
import { Workspace, WorkspaceCollection, WorkspaceRequest } from "./workspace"
export type WorkspaceError<ServiceErr> =
| { type: "SERVICE_ERROR"; error: ServiceErr }
| { type: "PROVIDER_ERROR"; error: unknown }
export class NewWorkspaceService extends Service {
public static readonly ID = "NEW_WORKSPACE_SERVICE"
private registeredProviders = shallowReactive(
new Map<string, WorkspaceProvider>()
)
public activeWorkspaceHandle: Ref<Handle<Workspace> | undefined> =
shallowRef()
public activeWorkspaceDecor = computed(() => {
const activeWorkspaceHandleRef = this.activeWorkspaceHandle.value?.get()
if (activeWorkspaceHandleRef?.value.type !== "ok") {
return undefined
}
return this.registeredProviders.get(
activeWorkspaceHandleRef.value.data.providerID
)!.workspaceDecor
})
public workspaceSelectorComponents = computed(() => {
const items: Component[] = []
const sortedProviders = Array.from(this.registeredProviders.values()).sort(
(a, b) =>
(b.workspaceDecor?.value.workspaceSelectorPriority ?? 0) -
(a.workspaceDecor?.value.workspaceSelectorPriority ?? 0)
)
for (const workspace of sortedProviders) {
if (workspace.workspaceDecor?.value?.workspaceSelectorComponent) {
items.push(workspace.workspaceDecor.value.workspaceSelectorComponent)
}
}
return items
})
override onServiceInit() {
// Watch for situations where the handle is invalidated
// so the active workspace handle definition can be invalidated
watch(
() => {
return this.activeWorkspaceHandle.value
? [
this.activeWorkspaceHandle.value,
this.activeWorkspaceHandle.value?.get(),
]
: [this.activeWorkspaceHandle.value]
},
() => {
if (!this.activeWorkspaceHandle.value) return
const activeWorkspaceHandleRef = this.activeWorkspaceHandle.value.get()
if (activeWorkspaceHandleRef.value.type === "invalid") {
this.activeWorkspaceHandle.value = undefined
}
},
{ deep: true }
)
}
public async getWorkspaceHandle(
providerID: string,
workspaceID: string
): Promise<E.Either<WorkspaceError<"INVALID_PROVIDER">, Handle<Workspace>>> {
const provider = this.registeredProviders.get(providerID)
if (!provider) {
return Promise.resolve(
E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" as const })
)
}
const handleResult = await provider.getWorkspaceHandle(workspaceID)
if (E.isLeft(handleResult)) {
return E.left({ type: "PROVIDER_ERROR", error: handleResult.left })
}
return E.right(handleResult.right)
}
public async getCollectionHandle(
workspaceHandle: Handle<Workspace>,
collectionID: string
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<WorkspaceCollection>
>
> {
const workspaceHandleRef = workspaceHandle.get()
if (workspaceHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
workspaceHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.getCollectionHandle(
workspaceHandle,
collectionID
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async getRequestHandle(
workspaceHandle: Handle<Workspace>,
requestID: string
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<WorkspaceRequest>
>
> {
const workspaceHandleRef = workspaceHandle.get()
if (workspaceHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
workspaceHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.getRequestHandle(workspaceHandle, requestID)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async createRESTRootCollection(
workspaceHandle: Handle<Workspace>,
newCollection: Partial<Exclude<HoppCollection, "id">> & { name: string }
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<WorkspaceCollection>
>
> {
const workspaceHandleRef = workspaceHandle.get()
if (workspaceHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
workspaceHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.createRESTRootCollection(
workspaceHandle,
newCollection
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async createRESTChildCollection(
parentCollectionHandle: Handle<WorkspaceCollection>,
newChildCollection: Partial<HoppCollection> & { name: string }
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<WorkspaceCollection>
>
> {
const parentCollectionHandleRef = parentCollectionHandle.get()
if (parentCollectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
parentCollectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.createRESTChildCollection(
parentCollectionHandle,
newChildCollection
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async updateRESTCollection(
collectionHandle: Handle<WorkspaceCollection>,
updatedCollection: Partial<HoppCollection>
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const collectionHandleRef = collectionHandle.get()
if (collectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
collectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.updateRESTCollection(
collectionHandle,
updatedCollection
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(undefined)
}
public async removeRESTCollection(
collectionHandle: Handle<WorkspaceCollection>
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const collectionHandleRef = collectionHandle.get()
if (collectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
collectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.removeRESTCollection(collectionHandle)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(undefined)
}
public async createRESTRequest(
parentCollectionHandle: Handle<WorkspaceCollection>,
newRequest: HoppRESTRequest
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<WorkspaceRequest>
>
> {
const parentCollectionHandleRef = parentCollectionHandle.get()
if (parentCollectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
parentCollectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.createRESTRequest(
parentCollectionHandle,
newRequest
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async removeRESTRequest(
requestHandle: Handle<WorkspaceRequest>
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const requestHandleRef = requestHandle.get()
if (requestHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
requestHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.removeRESTRequest(requestHandle)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(undefined)
}
public async updateRESTRequest(
requestHandle: Handle<WorkspaceRequest>,
updatedRequest: Partial<HoppRESTRequest>
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const requestHandleRef = requestHandle.get()
if (requestHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
requestHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.updateRESTRequest(
requestHandle,
updatedRequest
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async importRESTCollections(
workspaceHandle: Handle<Workspace>,
collections: HoppCollection[]
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<WorkspaceCollection>
>
> {
const workspaceHandleRef = workspaceHandle.get()
if (workspaceHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
workspaceHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.importRESTCollections(
workspaceHandle,
collections
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async exportRESTCollections(
workspaceHandle: Handle<Workspace>,
collections: HoppCollection[]
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const workspaceHandleRef = workspaceHandle.get()
if (workspaceHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
workspaceHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.exportRESTCollections(
workspaceHandle,
collections
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async exportRESTCollection(
collectionHandle: Handle<WorkspaceCollection>,
collection: HoppCollection
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const collectionHandleRef = collectionHandle.get()
if (collectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
collectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.exportRESTCollection(
collectionHandle,
collection
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async reorderRESTCollection(
collectionHandle: Handle<WorkspaceCollection>,
destinationCollectionID: string | null
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const collectionHandleRef = collectionHandle.get()
if (collectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
collectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.reorderRESTCollection(
collectionHandle,
destinationCollectionID
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async moveRESTCollection(
collectionHandle: Handle<WorkspaceCollection>,
destinationCollectionID: string | null
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const collectionHandleRef = collectionHandle.get()
if (collectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
collectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.moveRESTCollection(
collectionHandle,
destinationCollectionID
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async reorderRESTRequest(
requestHandle: Handle<WorkspaceRequest>,
destinationCollectionID: string,
destinationRequestID: string | null
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const requestHandleRef = requestHandle.get()
if (requestHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
requestHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.reorderRESTRequest(
requestHandle,
destinationCollectionID,
destinationRequestID
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async moveRESTRequest(
requestHandle: Handle<WorkspaceRequest>,
destinationCollectionID: string
): Promise<
E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
> {
const requestHandleRef = requestHandle.get()
if (requestHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
requestHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.moveRESTRequest(
requestHandle,
destinationCollectionID
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async getRESTCollectionChildrenView(
collectionHandle: Handle<WorkspaceCollection>
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<RESTCollectionChildrenView>
>
> {
const collectionHandleRef = collectionHandle.get()
if (collectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
collectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result =
await provider.getRESTCollectionChildrenView(collectionHandle)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async getRESTRootCollectionView(
workspaceHandle: Handle<Workspace>
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<RootRESTCollectionView>
>
> {
const workspaceHandleRef = workspaceHandle.get()
if (workspaceHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
workspaceHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.getRESTRootCollectionView(workspaceHandle)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async getRESTCollectionLevelAuthHeadersView(
collectionHandle: Handle<WorkspaceCollection>
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<RESTCollectionLevelAuthHeadersView>
>
> {
const collectionHandleRef = collectionHandle.get()
if (collectionHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
collectionHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result =
await provider.getRESTCollectionLevelAuthHeadersView(collectionHandle)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async getRESTSearchResultsView(
workspaceHandle: Handle<Workspace>,
searchQuery: Ref<string>
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<RESTSearchResultsView>
>
> {
const workspaceHandleRef = workspaceHandle.get()
if (workspaceHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
workspaceHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.getRESTSearchResultsView(
workspaceHandle,
searchQuery
)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public async getRESTCollectionJSONView(
workspaceHandle: Handle<Workspace>
): Promise<
E.Either<
WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
Handle<RESTCollectionJSONView>
>
> {
const workspaceHandleRef = workspaceHandle.get()
if (workspaceHandleRef.value.type === "invalid") {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
}
const provider = this.registeredProviders.get(
workspaceHandleRef.value.data.providerID
)
if (!provider) {
return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
}
const result = await provider.getRESTCollectionJSONView(workspaceHandle)
if (E.isLeft(result)) {
return E.left({ type: "PROVIDER_ERROR", error: result.left })
}
return E.right(result.right)
}
public registerWorkspaceProvider(provider: WorkspaceProvider) {
if (this.registeredProviders.has(provider.providerID)) {
console.warn(
"Ignoring attempt to re-register workspace provider that is already existing:",
provider
)
return
}
this.registeredProviders.set(provider.providerID, markRaw(provider))
}
}

View File

@@ -1,111 +0,0 @@
import { Ref } from "vue"
import * as E from "fp-ts/Either"
import { Handle } from "./handle"
import {
Workspace,
WorkspaceCollection,
WorkspaceDecor,
WorkspaceRequest,
} from "./workspace"
import {
RESTCollectionLevelAuthHeadersView,
RESTCollectionChildrenView,
RootRESTCollectionView,
RESTSearchResultsView,
RESTCollectionJSONView,
} from "./view"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
export interface WorkspaceProvider {
providerID: string
workspaceDecor?: Ref<WorkspaceDecor>
getWorkspaceHandle(
workspaceID: string
): Promise<E.Either<unknown, Handle<Workspace>>>
getCollectionHandle(
workspaceHandle: Handle<Workspace>,
collectionID: string
): Promise<E.Either<unknown, Handle<WorkspaceCollection>>>
getRequestHandle(
workspaceHandle: Handle<Workspace>,
requestID: string
): Promise<E.Either<unknown, Handle<WorkspaceRequest>>>
getRESTRootCollectionView(
workspaceHandle: Handle<Workspace>
): Promise<E.Either<never, Handle<RootRESTCollectionView>>>
getRESTCollectionChildrenView(
collectionHandle: Handle<WorkspaceCollection>
): Promise<E.Either<never, Handle<RESTCollectionChildrenView>>>
getRESTCollectionLevelAuthHeadersView(
collectionHandle: Handle<WorkspaceCollection>
): Promise<E.Either<never, Handle<RESTCollectionLevelAuthHeadersView>>>
getRESTSearchResultsView(
workspaceHandle: Handle<Workspace>,
searchQuery: Ref<string>
): Promise<E.Either<never, Handle<RESTSearchResultsView>>>
getRESTCollectionJSONView(
workspaceHandle: Handle<Workspace>
): Promise<E.Either<never, Handle<RESTCollectionJSONView>>>
createRESTRootCollection(
workspaceHandle: Handle<Workspace>,
newCollection: Partial<Exclude<HoppCollection, "id">> & { name: string }
): Promise<E.Either<unknown, Handle<WorkspaceCollection>>>
createRESTChildCollection(
parentCollectionHandle: Handle<WorkspaceCollection>,
newChildCollection: Partial<HoppCollection> & { name: string }
): Promise<E.Either<unknown, Handle<WorkspaceCollection>>>
updateRESTCollection(
collectionHandle: Handle<WorkspaceCollection>,
updatedCollection: Partial<HoppCollection>
): Promise<E.Either<unknown, void>>
removeRESTCollection(
collectionHandle: Handle<WorkspaceCollection>
): Promise<E.Either<unknown, void>>
createRESTRequest(
parentCollectionHandle: Handle<WorkspaceCollection>,
newRequest: HoppRESTRequest
): Promise<E.Either<unknown, Handle<WorkspaceRequest>>>
updateRESTRequest(
requestHandle: Handle<WorkspaceRequest>,
updatedRequest: Partial<HoppRESTRequest>
): Promise<E.Either<unknown, void>>
removeRESTRequest(
requestHandle: Handle<WorkspaceRequest>
): Promise<E.Either<unknown, void>>
importRESTCollections(
workspaceHandle: Handle<Workspace>,
collections: HoppCollection[]
): Promise<E.Either<unknown, Handle<WorkspaceCollection>>>
exportRESTCollections(
workspaceHandle: Handle<Workspace>,
collections: HoppCollection[]
): Promise<E.Either<unknown, void>>
exportRESTCollection(
collectionHandle: Handle<WorkspaceCollection>,
collection: HoppCollection
): Promise<E.Either<unknown, void>>
reorderRESTCollection(
collectionHandle: Handle<WorkspaceCollection>,
destinationCollectionID: string | null
): Promise<E.Either<unknown, void>>
moveRESTCollection(
collectionHandle: Handle<WorkspaceCollection>,
destinationCollectionID: string | null
): Promise<E.Either<unknown, void>>
reorderRESTRequest(
requestHandle: Handle<WorkspaceRequest>,
destinationCollectionID: string,
destinationRequestID: string | null
): Promise<E.Either<unknown, void>>
moveRESTRequest(
requestHandle: Handle<WorkspaceRequest>,
destinationCollectionID: string
): Promise<E.Either<unknown, void>>
}

View File

@@ -1,52 +0,0 @@
import { getDefaultRESTRequest } from "@hoppscotch/data"
import { ref, computed } from "vue"
import { HandleState } from "../../handle"
import { WorkspaceRequest } from "../../workspace"
export const generateIssuedHandleValues = (
collectionsAndRequests: { collectionID: string; requestCount: number }[]
) => {
const providerID = "PERSONAL_WORKSPACE_PROVIDER"
const workspaceID = "personal"
const issuedHandleValues: HandleState<WorkspaceRequest, unknown>[] = []
collectionsAndRequests.forEach(({ collectionID, requestCount }) => {
for (let i = 0; i < requestCount; i++) {
const requestID = `${collectionID}/${i}`
issuedHandleValues.push({
type: "ok" as const,
data: {
providerID: providerID,
workspaceID: workspaceID,
collectionID,
requestID,
request: {
...getDefaultRESTRequest(),
name: `req-${requestID}`,
},
},
})
}
})
return issuedHandleValues
}
export const getWritableHandle = (
value: HandleState<WorkspaceRequest, unknown>
) => {
const handleRefData = ref(value)
const writableHandle = computed({
get() {
return handleRefData.value
},
set(newValue) {
handleRefData.value = newValue
},
})
return writableHandle
}

View File

@@ -1,309 +0,0 @@
import { computed, markRaw, reactive, ref } from "vue"
import { useTimestamp } from "@vueuse/core"
import { Service } from "dioc"
import { WorkspaceProvider } from "../provider"
import * as E from "fp-ts/Either"
import { Handle, HandleRef } from "../handle"
import { Workspace, WorkspaceCollection } from "../workspace"
import { NewWorkspaceService } from ".."
import TestWorkspaceSelector from "~/components/workspace/TestWorkspaceSelector.vue"
import { RESTCollectionChildrenView, RootRESTCollectionView } from "../view"
import IconUser from "~icons/lucide/user"
import { get } from "lodash-es"
type TestReqDef = {
name: string
}
type TestCollDef = {
name: string
collections: TestCollDef[]
requests: TestReqDef[]
}
const timestamp = useTimestamp({ interval: 3000 })
// const timestamp = ref(Date.now())
const testData = reactive({
workspaceA: {
name: computed(() => `Workspace A: ${timestamp.value}`),
collections: [
<TestCollDef>{
name: "Collection A",
collections: [
{
name: "Collection B",
collections: [
{ name: "Collection C", collections: [], requests: [] },
],
requests: [],
},
],
requests: [{ name: "Request C" }],
},
],
},
workspaceB: {
name: "Workspace B",
collections: [
<TestCollDef>{
name: "Collection D",
collections: [{ name: "Collection E", collections: [], requests: [] }],
requests: [{ name: "Request F" }],
},
],
},
})
;(window as any).testData = testData
export class TestWorkspaceProviderService
extends Service
implements WorkspaceProvider
{
public static readonly ID = "TEST_WORKSPACE_PROVIDER_SERVICE"
public providerID = "TEST_WORKSPACE_PROVIDER"
public workspaceDecor = ref({
workspaceSelectorComponent: markRaw(TestWorkspaceSelector),
headerCurrentIcon: markRaw(IconUser),
workspaceSelectorPriority: 10,
})
private readonly workspaceService = this.bind(NewWorkspaceService)
override onServiceInit() {
this.workspaceService.registerWorkspaceProvider(this)
}
public createRESTRootCollection(
workspaceHandle: HandleRef<Workspace>,
collectionName: string
): Promise<
E.Either<"INVALID_WORKSPACE_HANDLE", Handle<WorkspaceCollection>>
> {
if (workspaceHandle.value.type !== "ok") {
return Promise.resolve(E.left("INVALID_WORKSPACE_HANDLE" as const))
}
const workspaceID = workspaceHandle.value.data.workspaceID
const newCollID =
testData[workspaceID as keyof typeof testData].collections.length
testData[workspaceID as keyof typeof testData].collections.push({
name: collectionName,
collections: [],
requests: [],
})
return this.getCollectionHandle(workspaceHandle, newCollID.toString())
}
public createRESTChildCollection(
parentCollHandle: HandleRef<WorkspaceCollection>,
collectionName: string
): Promise<E.Either<unknown, Handle<WorkspaceCollection>>> {
// TODO: Implement
throw new Error("Method not implemented.")
}
public getWorkspaceHandle(
workspaceID: string
): Promise<E.Either<never, Handle<Workspace>>> {
return Promise.resolve(
E.right(
computed(() => {
if (!(workspaceID in testData)) {
return {
type: "invalid",
reason: "WORKSPACE_WENT_OUT" as const,
}
}
return {
type: "ok",
data: {
providerID: this.providerID,
workspaceID,
name: testData[workspaceID as keyof typeof testData].name,
collectionsAreReadonly: false,
},
}
})
)
)
}
public getCollectionHandle(
workspaceHandle: HandleRef<Workspace>,
collectionID: string
): Promise<
E.Either<"INVALID_WORKSPACE_HANDLE", Handle<WorkspaceCollection>>
> {
return Promise.resolve(
E.right(
computed(() => {
if (workspaceHandle.value.type !== "ok") {
return {
type: "invalid",
reason: "WORKSPACE_INVALIDATED" as const,
}
}
const workspaceID = workspaceHandle.value.data.workspaceID
const collectionPath = collectionID
.split("/")
.flatMap((x) => ["collections", x])
const result: TestCollDef | undefined = get(
testData[workspaceID as keyof typeof testData],
collectionPath
)
if (!result) {
return {
type: "invalid",
reason: "INVALID_COLL_ID",
}
}
return {
type: "ok",
data: {
providerID: this.providerID,
workspaceID,
collectionID,
name: result.name,
},
}
})
)
)
}
public getRESTCollectionChildrenView(
collectionHandle: HandleRef<WorkspaceCollection>
): Promise<E.Either<never, Handle<RESTCollectionChildrenView>>> {
return Promise.resolve(
E.right(
computed(() => {
if (collectionHandle.value.type === "invalid") {
return {
type: "invalid",
reason: "COLL_HANDLE_IS_INVALID" as const,
}
}
const workspaceID = collectionHandle.value.data.workspaceID
const collectionID = collectionHandle.value.data.collectionID
if (!(workspaceID in testData)) {
return {
type: "invalid",
reason: "WORKSPACE_NOT_PRESENT" as const,
}
}
const collectionPath = collectionID
.split("/")
.flatMap((x) => ["collections", x])
return markRaw({
type: "ok",
data: {
providerID: this.providerID,
workspaceID,
collectionID,
loading: ref(false),
content: computed(() => [
...(
get(testData[workspaceID as keyof typeof testData], [
...collectionPath,
"collections",
]) as TestCollDef[]
).map((item, i) => ({
type: "collection" as const,
value: {
collectionID: `${collectionID}/${i}`,
name: item.name,
},
})),
...(
get(testData[workspaceID as keyof typeof testData], [
...collectionPath,
"requests",
]) as TestReqDef[]
).map((item, i) => ({
type: "request" as const,
value: {
requestID: `${collectionID}/${i}`,
name: item.name,
method: "get",
},
})),
]),
},
})
})
)
)
}
public getRESTRootCollectionView(
workspaceHandle: HandleRef<Workspace>
): Promise<E.Either<never, Handle<RootRESTCollectionView>>> {
return Promise.resolve(
E.right(
computed(() => {
if (workspaceHandle.value.type === "invalid") {
return {
type: "invalid",
reason: "WORKSPACE_IS_INVALID" as const,
}
}
const workspaceID = workspaceHandle.value.data.workspaceID
if (!(workspaceID in testData)) {
return {
type: "invalid",
reason: "WORKSPACE_NOT_PRESENT" as const,
}
}
return markRaw({
type: "ok",
data: {
providerID: this.providerID,
workspaceID,
loading: ref(false),
collections: computed(() => {
return testData[
workspaceID as keyof typeof testData
].collections.map((x, i) => ({
collectionID: i.toString(),
name: x.name,
}))
}),
},
})
})
)
)
}
public getWorkspaceCandidates() {
return computed(() =>
Object.keys(testData).map((workspaceID) => ({
id: workspaceID,
name: testData[workspaceID as keyof typeof testData].name,
}))
)
}
}

View File

@@ -1,64 +0,0 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { Ref } from "vue"
import { HoppInheritedRESTProperty } from "~/helpers/types/HoppInheritedProperties"
export type RESTCollectionLevelAuthHeadersView = {
auth: HoppInheritedRESTProperty["auth"]
headers: HoppInheritedRESTProperty["headers"]
}
export type RESTCollectionViewCollection = {
collectionID: string
isLastItem: boolean
name: string
parentCollectionID: string | null
}
export type RESTCollectionViewRequest = {
collectionID: string
requestID: string
request: HoppRESTRequest
isLastItem: boolean
}
export type RESTCollectionViewItem =
| { type: "collection"; value: RESTCollectionViewCollection }
| { type: "request"; value: RESTCollectionViewRequest }
export interface RootRESTCollectionView {
providerID: string
workspaceID: string
loading: Ref<boolean>
collections: Ref<RESTCollectionViewCollection[]>
}
export interface RESTCollectionChildrenView {
providerID: string
workspaceID: string
collectionID: string
loading: Ref<boolean>
content: Ref<RESTCollectionViewItem[]>
}
export interface RESTSearchResultsView {
providerID: string
workspaceID: string
loading: Ref<boolean>
results: Ref<HoppCollection[]>
onSessionEnd: () => void
}
export interface RESTCollectionJSONView {
providerID: string
workspaceID: string
content: string
}

View File

@@ -1,35 +0,0 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { Component } from "vue"
export type Workspace = {
providerID: string
workspaceID: string
name: string
}
export type WorkspaceCollection = {
providerID: string
workspaceID: string
collectionID: string
name: string
}
export type WorkspaceRequest = {
providerID: string
workspaceID: string
collectionID: string
requestID: string
request: HoppRESTRequest
}
export type WorkspaceDecor = {
headerComponent?: Component
headerCurrentIcon?: Component | object
workspaceSelectorComponent?: Component
workspaceSelectorPriority?: number
}

View File

@@ -109,6 +109,10 @@ export class OauthAuthService extends Service {
public static readonly ID = "OAUTH_AUTH_SERVICE" public static readonly ID = "OAUTH_AUTH_SERVICE"
static redirectURI = `${window.location.origin}/oauth` static redirectURI = `${window.location.origin}/oauth`
constructor() {
super()
}
} }
export const generateRandomString = () => { export const generateRandomString = () => {

View File

@@ -580,6 +580,8 @@ describe("PersistenceService", () => {
invokeSetupLocalPersistence() invokeSetupLocalPersistence()
// toastErrorFn = vi.fn()
expect(getItemSpy).toHaveBeenCalledWith(settingsKey) expect(getItemSpy).toHaveBeenCalledWith(settingsKey)
expect(toastErrorFn).not.toHaveBeenCalledWith(settingsKey) expect(toastErrorFn).not.toHaveBeenCalledWith(settingsKey)

View File

@@ -89,6 +89,10 @@ export class PersistenceService extends Service {
public hoppLocalConfigStorage: StorageLike = localStorage public hoppLocalConfigStorage: StorageLike = localStorage
constructor() {
super()
}
private showErrorToast(localStorageKey: string) { private showErrorToast(localStorageKey: string) {
const toast = useToast() const toast = useToast()
toast.error( toast.error(

View File

@@ -492,15 +492,6 @@ const HoppRESTResponseSchema = z.discriminatedUnion("type", [
const HoppRESTSaveContextSchema = z.nullable( const HoppRESTSaveContextSchema = z.nullable(
z.discriminatedUnion("originLocation", [ z.discriminatedUnion("originLocation", [
z
.object({
originLocation: z.literal("workspace-user-collection"),
workspaceID: z.optional(z.string()),
providerID: z.optional(z.string()),
requestID: z.optional(z.string()),
requestHandle: z.optional(z.record(z.unknown())),
})
.strict(),
z z
.object({ .object({
originLocation: z.literal("user-collection"), originLocation: z.literal("user-collection"),

View File

@@ -27,6 +27,10 @@ export class SecretEnvironmentService extends Service {
*/ */
public secretEnvironments = reactive(new Map<string, SecretVariable[]>()) public secretEnvironments = reactive(new Map<string, SecretVariable[]>())
constructor() {
super()
}
/** /**
* Add a new secret environment. * Add a new secret environment.
* @param id ID of the environment * @param id ID of the environment

View File

@@ -6,7 +6,6 @@ import {
import { nextTick, reactive, ref } from "vue" import { nextTick, reactive, ref } from "vue"
import { SpotlightSearcherResult } from "../../.." import { SpotlightSearcherResult } from "../../.."
import { TestContainer } from "dioc/testing" import { TestContainer } from "dioc/testing"
import { Container } from "dioc"
async function flushPromises() { async function flushPromises() {
return await new Promise((r) => setTimeout(r)) return await new Promise((r) => setTimeout(r))
@@ -33,15 +32,12 @@ describe("StaticSpotlightSearcherService", () => {
}, },
}) })
// TODO: dioc > v3 does not recommend using constructors, move to onServiceInit constructor() {
constructor(c: Container) { super({
super(c, {
searchFields: ["text"], searchFields: ["text"],
fieldWeights: {}, fieldWeights: {},
}) })
}
override onServiceInit() {
this.setDocuments(this.documents) this.setDocuments(this.documents)
} }
@@ -98,15 +94,12 @@ describe("StaticSpotlightSearcherService", () => {
}, },
}) })
// TODO: dioc > v3 does not recommend using constructors, move to onServiceInit constructor() {
constructor(c: Container) { super({
super(c, {
searchFields: ["text"], searchFields: ["text"],
fieldWeights: {}, fieldWeights: {},
}) })
}
override onServiceInit() {
this.setDocuments(this.documents) this.setDocuments(this.documents)
} }
@@ -166,15 +159,12 @@ describe("StaticSpotlightSearcherService", () => {
}, },
}) })
// TODO: dioc > v3 does not recommend using constructors, move to onServiceInit constructor() {
constructor(c: Container) { super({
super(c, {
searchFields: ["text"], searchFields: ["text"],
fieldWeights: {}, fieldWeights: {},
}) })
}
override onServiceInit() {
this.setDocuments(this.documents) this.setDocuments(this.documents)
} }
@@ -234,15 +224,12 @@ describe("StaticSpotlightSearcherService", () => {
}, },
}) })
// TODO: dioc > v3 does not recommend using constructors, move to onServiceInit constructor() {
constructor(c: Container) { super({
super(c, {
searchFields: ["text"], searchFields: ["text"],
fieldWeights: {}, fieldWeights: {},
}) })
}
override onServiceInit() {
this.setDocuments(this.documents) this.setDocuments(this.documents)
} }
@@ -298,15 +285,12 @@ describe("StaticSpotlightSearcherService", () => {
}, },
}) })
// TODO: dioc > v3 does not recommend using constructors, move to onServiceInit constructor() {
constructor(c: Container) { super({
super(c, {
searchFields: ["text"], searchFields: ["text"],
fieldWeights: {}, fieldWeights: {},
}) })
}
override onServiceInit() {
this.setDocuments(this.documents) this.setDocuments(this.documents)
} }
@@ -370,15 +354,12 @@ describe("StaticSpotlightSearcherService", () => {
}, },
}) })
// TODO: dioc > v3 does not recommend using constructors, move to onServiceInit constructor() {
constructor(c: Container) { super({
super(c, {
searchFields: ["text", "alternate"], searchFields: ["text", "alternate"],
fieldWeights: {}, fieldWeights: {},
}) })
}
override onServiceInit() {
this.setDocuments(this.documents) this.setDocuments(this.documents)
} }

View File

@@ -1,4 +1,4 @@
import { Container, Service } from "dioc" import { Service } from "dioc"
import { import {
type SpotlightSearcher, type SpotlightSearcher,
type SpotlightSearcherResult, type SpotlightSearcherResult,
@@ -67,12 +67,8 @@ export abstract class StaticSpotlightSearcherService<
private _documents: Record<string, Doc> = {} private _documents: Record<string, Doc> = {}
// TODO: This pattern is no longer recommended in dioc > 3, move to something else constructor(private opts: StaticSpotlightSearcherOptions<Doc>) {
constructor( super()
c: Container,
private opts: StaticSpotlightSearcherOptions<Doc>
) {
super(c)
this.minisearch = new MiniSearch({ this.minisearch = new MiniSearch({
fields: opts.searchFields as string[], fields: opts.searchFields as string[],

View File

@@ -50,7 +50,9 @@ export class CollectionsSpotlightSearcherService
private readonly spotlight = this.bind(SpotlightService) private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService) private readonly workspaceService = this.bind(WorkspaceService)
override onServiceInit() { constructor() {
super()
this.spotlight.registerSearcher(this) this.spotlight.registerSearcher(this)
} }

View File

@@ -26,7 +26,7 @@ import IconEdit from "~icons/lucide/edit"
import IconLayers from "~icons/lucide/layers" import IconLayers from "~icons/lucide/layers"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import { Container, Service } from "dioc" import { Service } from "dioc"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
@@ -164,18 +164,15 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
}, },
}) })
// TODO: This pattern is no longer recommended in dioc > 3, move to something else constructor() {
constructor(c: Container) { super({
super(c, {
searchFields: ["text", "alternates"], searchFields: ["text", "alternates"],
fieldWeights: { fieldWeights: {
text: 2, text: 2,
alternates: 1, alternates: 1,
}, },
}) })
}
override onServiceInit() {
this.setDocuments(this.documents) this.setDocuments(this.documents)
this.spotlight.registerSearcher(this) this.spotlight.registerSearcher(this)
} }
@@ -280,7 +277,9 @@ export class SwitchEnvSpotlightSearcherService
private readonly workspaceService = this.bind(WorkspaceService) private readonly workspaceService = this.bind(WorkspaceService)
private teamEnvironmentList: TeamEnvironment[] = [] private teamEnvironmentList: TeamEnvironment[] = []
override onServiceInit() { constructor() {
super()
this.spotlight.registerSearcher(this) this.spotlight.registerSearcher(this)
} }

Some files were not shown because too many files have changed in this diff Show More