Compare commits

..

8 Commits

Author SHA1 Message Date
nivedin
7411e36880 chore: remove readonly type 2024-04-24 21:13:58 +05:30
nivedin
05ad84f372 chore: add fallback url and add reqIndex for graphql saving 2024-04-24 21:13:55 +05:30
nivedin
0c993d0e90 chore: add i18n and readonly to envinput 2024-04-24 21:12:28 +05:30
nivedin
10cb900bd7 fix: revert text update in envinput for empty state 2024-04-24 21:12:28 +05:30
nivedin
8fd6b2ffb0 chore: update default tab to have empty endpoint 2024-04-24 21:12:28 +05:30
nivedin
e6e300ca86 chore: add fallback url if endpoint is empty while saving and sharing req 2024-04-24 21:12:28 +05:30
nivedin
5bac6222a0 chore: update URL input 2024-04-24 21:12:28 +05:30
nivedin
cf37fbd610 feat: hover placeholder transition for smartenvinput 2024-04-24 21:12:27 +05:30
102 changed files with 1453 additions and 1974 deletions

View File

@@ -47,7 +47,6 @@ RATE_LIMIT_MAX=100 # Max requests per IP
# Base URLs
VITE_BACKEND_LOGIN_API_URL=http://localhost:5444
VITE_BASE_URL=http://localhost:3000
VITE_SHORTCODE_BASE_URL=http://localhost:3000
VITE_ADMIN_URL=http://localhost:3100

View File

@@ -38,7 +38,7 @@
},
"packageExtensions": {
"httpsnippet@3.0.1": {
"dependencies": {
"peerDependencies": {
"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

View File

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

View File

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

View File

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

@@ -374,8 +374,7 @@
"mutations": "Mutations",
"schema": "Schema",
"subscriptions": "Subscriptions",
"switch_connection": "Switch connection",
"url_placeholder": "Enter a GraphQL endpoint URL"
"switch_connection": "Switch connection"
},
"graphql_collections": {
"title": "GraphQL Collections"
@@ -569,7 +568,9 @@
"generated_code": "Generated code",
"go_to_authorization_tab": "Go to Authorization tab",
"go_to_body_tab": "Go to Body tab",
"graphql_placeholder": "Enter a URL",
"header_list": "Header List",
"http_placeholder":"Enter a URL or cURL command",
"invalid_name": "Please provide a name for the request",
"method": "Method",
"moved": "Request moved",
@@ -599,7 +600,6 @@
"title": "Request",
"type": "Request type",
"url": "URL",
"url_placeholder": "Enter a URL or paste a cURL command",
"variables": "Variables",
"view_my_links": "View my links"
},

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2024.3.3",
"version": "2024.3.1",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -50,7 +50,7 @@
"axios": "1.6.2",
"buffer": "6.0.3",
"cookie-es": "1.0.0",
"dioc": "3.0.1",
"dioc": "1.0.1",
"esprima": "4.0.1",
"events": "3.3.0",
"fp-ts": "2.16.1",

View File

@@ -32,7 +32,6 @@ declare module 'vue' {
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.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']
Collections: typeof import('./components/collections/index.vue')['default']
CollectionsAdd: typeof import('./components/collections/Add.vue')['default']

View File

@@ -43,19 +43,12 @@
@click="invokeAction('modals.support.toggle')"
/>
</div>
<div
class="flex"
:class="{
'flex-row-reverse gap-2':
workspaceSelectorFlagEnabled && !currentUser,
}"
>
<div class="flex">
<div
v-if="currentUser === null"
class="inline-flex items-center space-x-2"
>
<HoppButtonSecondary
v-if="!workspaceSelectorFlagEnabled"
:icon="IconUploadCloud"
:label="t('header.save_workspace')"
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"
@@ -67,22 +60,18 @@
@click="invokeAction('modals.login.toggle')"
/>
</div>
<TeamsMemberStack
v-else-if="
currentUser !== null &&
workspace.type === 'team' &&
selectedTeam &&
selectedTeam.teamMembers.length > 1
"
:team-members="selectedTeam.teamMembers"
show-count
class="mx-2"
@handle-click="handleTeamEdit()"
/>
<div
v-if="workspaceSelectorFlagEnabled || currentUser"
class="inline-flex items-center space-x-2"
>
<div v-else class="inline-flex items-center space-x-2">
<TeamsMemberStack
v-if="
workspace.type === 'team' &&
selectedTeam &&
selectedTeam.teamMembers.length > 1
"
:team-members="selectedTeam.teamMembers"
show-count
class="mx-2"
@handle-click="handleTeamEdit()"
/>
<div
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"
>
@@ -95,7 +84,6 @@
/>
<HoppButtonSecondary
v-if="
currentUser &&
workspace.type === 'team' &&
selectedTeam &&
selectedTeam?.myRole === 'OWNER'
@@ -136,7 +124,7 @@
</div>
</template>
</tippy>
<span v-if="currentUser" class="px-2">
<span class="px-2">
<tippy
interactive
trigger="click"
@@ -271,13 +259,6 @@ import {
const t = useI18n()
const toast = useToast()
/**
* Feature flag to enable the workspace selector login conversion
*/
const workspaceSelectorFlagEnabled = computed(
() => !!platform.platformFeatureFlags.workspaceSwitcherLogin?.value
)
/**
* Once the PWA code is initialized, this holds a method
* that can be called to show the user the installation
@@ -399,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
const handleInvite = () => {
if (!currentUser.value) return invokeAction("modals.login.toggle")
if (
workspace.value.type === "team" &&
workspace.value.teamID &&

View File

@@ -32,6 +32,7 @@ import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconOpenAPI from "~icons/lucide/file"
@@ -54,15 +55,16 @@ import { teamCollectionsExporter } from "~/helpers/import-export/export/teamColl
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { ImporterOrExporter } from "~/components/importExport/types"
import { TeamWorkspace } from "~/services/workspace.service"
const t = useI18n()
const toast = useToast()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: TeamWorkspace
selectedTeam: SelectedTeam
}
| { type: "my-collections" }
@@ -431,7 +433,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
props.collectionsType.selectedTeam
) {
const res = await teamCollectionsExporter(
props.collectionsType.selectedTeam.teamID
props.collectionsType.selectedTeam.id
)
if (E.isRight(res)) {
@@ -567,8 +569,8 @@ const hasTeamWriteAccess = computed(() => {
}
return (
collectionsType.selectedTeam.role === "EDITOR" ||
collectionsType.selectedTeam.role === "OWNER"
collectionsType.selectedTeam.myRole === "EDITOR" ||
collectionsType.selectedTeam.myRole === "OWNER"
)
})
@@ -576,17 +578,17 @@ const selectedTeamID = computed(() => {
const { collectionsType } = props
return collectionsType.type === "team-collections"
? collectionsType.selectedTeam?.teamID
? collectionsType.selectedTeam?.id
: undefined
})
const getCollectionJSON = async () => {
if (
props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam?.teamID
props.collectionsType.selectedTeam?.id
) {
const res = await getTeamCollectionJSON(
props.collectionsType.selectedTeam?.teamID
props.collectionsType.selectedTeam?.id
)
return E.isRight(res)

View File

@@ -56,25 +56,23 @@
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { computed, nextTick, reactive, ref, watch } from "vue"
import { cloneDeep } from "lodash-es"
import {
HoppGQLRequest,
HoppRESTRequest,
isHoppRESTRequest,
} from "@hoppscotch/data"
import { computedWithControl } from "@vueuse/core"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { cloneDeep } from "lodash-es"
import { computed, nextTick, reactive, ref, watch } from "vue"
import { GQLError } from "~/helpers/backend/GQLClient"
import * as TE from "fp-ts/TaskEither"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import {
createRequestInCollection,
updateTeamRequest,
} from "~/helpers/backend/mutations/TeamRequest"
import { Picked } from "~/helpers/types/HoppPicked"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import {
cascadeParentCollectionForHeaderAuth,
editGraphqlRequest,
@@ -82,10 +80,14 @@ import {
saveGraphqlRequestAs,
saveRESTRequestAs,
} from "~/newstore/collections"
import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core"
import { platform } from "~/platform"
import { GQLTabService } from "~/services/tab/graphql"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { TeamWorkspace } from "~/services/workspace.service"
import { GQLTabService } from "~/services/tab/graphql"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
const t = useI18n()
const toast = useToast()
@@ -93,10 +95,12 @@ const toast = useToast()
const RESTTabs = useService(RESTTabService)
const GQLTabs = useService(GQLTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: TeamWorkspace
selectedTeam: SelectedTeam
}
| { type: "my-collections"; selectedTeam: undefined }
@@ -190,7 +194,7 @@ watch(
}
)
const updateTeam = (newTeam: TeamWorkspace) => {
const updateTeam = (newTeam: SelectedTeam) => {
collectionsType.value.selectedTeam = newTeam
}
@@ -219,6 +223,15 @@ const saveRequestAs = async () => {
requestUpdated.name = requestName.value
if (props.mode === "rest") {
;(requestUpdated as HoppRESTRequest).endpoint =
(requestUpdated as HoppRESTRequest).endpoint ||
getDefaultRESTRequest().endpoint
} else {
;(requestUpdated as HoppGQLRequest).url =
(requestUpdated as HoppGQLRequest).url || getDefaultGQLRequest().url
}
if (picked.value.pickedType === "my-collection") {
if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request")
@@ -491,7 +504,7 @@ const updateTeamCollectionOrFolder = (
const data = {
title: requestUpdated.name,
request: JSON.stringify(requestUpdated),
teamID: collectionsType.value.selectedTeam.teamID,
teamID: collectionsType.value.selectedTeam.id,
}
pipe(
createRequestInCollection(collectionID, data),

View File

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

View File

@@ -193,6 +193,7 @@ import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { EditingProperties } from "../Properties.vue"
import { defineActionHandler } from "~/helpers/actions"
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
const t = useI18n()
const toast = useToast()
@@ -200,7 +201,7 @@ const toast = useToast()
defineProps<{
// Whether to activate the ability to pick items (activates 'select' events)
saveRequest: boolean
picked: Picked | null
picked: Picked
}>()
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
@@ -380,32 +381,26 @@ const editCollection = (
displayModalEdit(true)
}
const onAddRequest = ({
name,
path,
index,
}: {
name: string
path: string
index: number
}) => {
const onAddRequest = ({ name, path }: { name: string; path: string }) => {
const newRequest = {
...tabs.currentActiveTab.value.document.request,
name,
url:
tabs.currentActiveTab.value.document.request.url ||
getDefaultGQLRequest().url,
}
saveGraphqlRequestAs(path, newRequest)
const insertionIndex = saveGraphqlRequestAs(path, newRequest)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
path,
"graphql"
)
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: path,
requestIndex: index,
requestIndex: insertionIndex,
},
request: newRequest,
isDirty: false,

View File

@@ -178,6 +178,7 @@ import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked"
import { useReadonlyStream } from "~/composables/stream"
import { useLocalState } from "~/newstore/localstate"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import {
@@ -244,7 +245,7 @@ import {
} from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering"
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 { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
@@ -253,6 +254,7 @@ import { PersistenceService } from "~/services/persistence"
import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { EditingProperties } from "./Properties.vue"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
const t = useI18n()
const toast = useToast()
@@ -273,14 +275,16 @@ const props = defineProps({
const emit = defineEmits<{
(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
}>()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: TeamWorkspace
selectedTeam: SelectedTeam
}
| { type: "my-collections"; selectedTeam: undefined }
@@ -327,7 +331,9 @@ const requestMoveLoading = ref<string[]>([])
// TeamList-Adapter
const workspaceService = useService(WorkspaceService)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const teamListFetched = ref(false)
// Team Collection Adapter
const teamCollectionAdapter = new TeamCollectionAdapter(null)
@@ -373,7 +379,7 @@ watch(
filterTexts,
(newFilterText) => {
if (collectionsType.value.type === "team-collections") {
const selectedTeamID = collectionsType.value.selectedTeam?.teamID
const selectedTeamID = collectionsType.value.selectedTeam?.id
selectedTeamID &&
debouncedSearch(newFilterText, selectedTeamID)?.catch(() => {})
@@ -430,6 +436,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 = () => {
collectionsType.value.type = "my-collections"
collectionsType.value.selectedTeam = undefined
@@ -461,12 +489,11 @@ const expandTeamCollection = (collectionID: string) => {
teamCollectionAdapter.expandCollection(collectionID)
}
const updateSelectedTeam = (team: TeamWorkspace) => {
const updateSelectedTeam = (team: SelectedTeam) => {
if (team) {
collectionsType.value.type = "team-collections"
teamCollectionAdapter.changeTeamID(team.teamID)
collectionsType.value.selectedTeam = team
REMEMBERED_TEAM_ID.value = team.teamID
REMEMBERED_TEAM_ID.value = team.id
emit("update-team", team)
emit("update-collection-type", "team-collections")
}
@@ -475,14 +502,23 @@ const updateSelectedTeam = (team: TeamWorkspace) => {
const workspace = workspaceService.currentWorkspace
// 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(
workspace,
(newWorkspace) => {
if (newWorkspace.type === "personal") {
switchToMyCollections()
} else if (newWorkspace.type === "team") {
updateSelectedTeam(newWorkspace)
() => {
const space = workspace.value
return space.type === "personal" ? undefined : space.teamID
},
(teamID) => {
if (teamID) {
const team = myTeams.value?.find((t) => t.id === teamID)
if (team) {
updateSelectedTeam(team)
}
return
}
return switchToMyCollections()
},
{
immediate: true,
@@ -510,7 +546,7 @@ const hasTeamWriteAccess = computed(() => {
return false
}
const role = collectionsType.value.selectedTeam?.role
const role = collectionsType.value.selectedTeam?.myRole
return role === "OWNER" || role === "EDITOR"
})
@@ -725,7 +761,7 @@ const addNewRootCollection = (name: string) => {
})
pipe(
createNewRootCollection(name, collectionsType.value.selectedTeam.teamID),
createNewRootCollection(name, collectionsType.value.selectedTeam.id),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
@@ -755,6 +791,9 @@ const onAddRequest = (requestName: string) => {
const newRequest = {
...cloneDeep(tabs.currentActiveTab.value.document.request),
name: requestName,
endpoint:
tabs.currentActiveTab.value.document.request.endpoint ||
getDefaultRESTRequest().endpoint,
}
const path = editingFolderPath.value
@@ -796,7 +835,7 @@ const onAddRequest = (requestName: string) => {
const data = {
request: JSON.stringify(newRequest),
teamID: collectionsType.value.selectedTeam.teamID,
teamID: collectionsType.value.selectedTeam.id,
title: requestName,
}
@@ -1123,7 +1162,7 @@ const duplicateRequest = (payload: {
const data = {
request: JSON.stringify(newRequest),
teamID: collectionsType.value.selectedTeam.teamID,
teamID: collectionsType.value.selectedTeam.id,
title: `${request.name} - ${t("action.duplicate")}`,
}

View File

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

View File

@@ -46,38 +46,41 @@
</template>
<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 { 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 { 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 {
deleteEnvironment,
getSelectedEnvironmentIndex,
globalEnv$,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { defineActionHandler } from "~/helpers/actions"
import { useLocalState } from "~/newstore/localstate"
import { platform } from "~/platform"
import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service"
import { pipe } from "fp-ts/function"
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 toast = useToast()
type EnvironmentType = "my-environments" | "team-environments"
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type EnvironmentsChooseType = {
type: EnvironmentType
selectedTeam: TeamWorkspace | undefined
selectedTeam: SelectedTeam
}
const environmentType = ref<EnvironmentsChooseType>({
@@ -99,7 +102,11 @@ const currentUser = useReadonlyStream(
platform.auth.getCurrentUser()
)
// TeamList-Adapter
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 adapter = new TeamEnvironmentAdapter(undefined)
@@ -111,17 +118,29 @@ const loading = computed(
() => 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 = () => {
environmentType.value.selectedTeam = undefined
updateEnvironmentType("my-environments")
adapter.changeTeamID(undefined)
}
const updateSelectedTeam = (newSelectedTeam: TeamWorkspace | undefined) => {
const updateSelectedTeam = (newSelectedTeam: SelectedTeam | undefined) => {
if (newSelectedTeam) {
adapter.changeTeamID(newSelectedTeam.teamID)
environmentType.value.selectedTeam = newSelectedTeam
REMEMBERED_TEAM_ID.value = newSelectedTeam.teamID
REMEMBERED_TEAM_ID.value = newSelectedTeam.id
updateEnvironmentType("team-environments")
}
}
@@ -129,6 +148,15 @@ const updateEnvironmentType = (newEnvironmentType: EnvironmentType) => {
environmentType.value.type = newEnvironmentType
}
watch(
() => environmentType.value.selectedTeam,
(newTeam) => {
if (newTeam) {
adapter.changeTeamID(newTeam.id)
}
}
)
const workspace = workspaceService.currentWorkspace
// 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") {
updateSelectedTeam(newWorkspace)
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
}
})

View File

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

View File

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

View File

@@ -3,16 +3,13 @@
class="sticky top-0 z-10 flex flex-shrink-0 space-x-2 overflow-x-auto bg-primary p-4"
>
<div class="inline-flex flex-1 space-x-2">
<input
id="url"
<SmartEnvInput
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
class="w-full rounded border border-divider bg-primaryLight px-4 py-2 text-secondaryDark"
:placeholder="`${t('graphql.url_placeholder')}`"
:disabled="connected"
@keyup.enter="onConnectClick"
:placeholder="getDefaultGQLRequest().url"
:placeholder-hover-string="t('request.graphql_placeholder')"
:readonly="connected"
class="rounded border border-divider bg-primaryLight"
@enter="onConnectClick"
/>
<HoppButtonPrimary
id="get"
@@ -72,6 +69,7 @@ import { InterceptorService } from "~/services/interceptor.service"
import { useService } from "dioc/vue"
import { defineActionHandler } from "~/helpers/actions"
import { GQLTabService } from "~/services/tab/graphql"
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
const t = useI18n()
const tabs = useService(GQLTabService)
@@ -98,7 +96,10 @@ const onConnectClick = () => {
}
const gqlConnect = () => {
connect(url.value, tabs.currentActiveTab.value?.document.request.headers)
connect(
url.value || getDefaultGQLRequest().url,
tabs.currentActiveTab.value?.document.request.headers
)
platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
@@ -118,7 +119,9 @@ watch(
tabs.currentActiveTab,
(newVal) => {
if (newVal) {
lastTwoUrls.value.push(newVal.document.request.url)
lastTwoUrls.value.push(
newVal.document.request.url ?? getDefaultGQLRequest().url
)
if (lastTwoUrls.value.length > 2) {
lastTwoUrls.value.shift()
}

View File

@@ -76,6 +76,7 @@ import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
const VALID_GQL_OPERATIONS = [
"query",
@@ -119,7 +120,9 @@ const request = useVModel(props, "modelValue", emit)
const url = computedWithControl(
() => tabs.currentActiveTab.value,
() => tabs.currentActiveTab.value.document.request.url
() =>
tabs.currentActiveTab.value.document.request.url ||
getDefaultGQLRequest().url
)
const activeGQLHeadersCount = computed(
@@ -247,10 +250,16 @@ const saveRequest = () => {
tabs.currentActiveTab.value.document.saveContext.originLocation ===
"user-collection"
) {
const finalRequest = {
...tabs.currentActiveTab.value.document.request,
url:
tabs.currentActiveTab.value.document.request.url ||
getDefaultGQLRequest().url,
}
editGraphqlRequest(
tabs.currentActiveTab.value.document.saveContext.folderPath,
tabs.currentActiveTab.value.document.saveContext.requestIndex,
tabs.currentActiveTab.value.document.request
finalRequest
)
tabs.currentActiveTab.value.document.isDirty = false

View File

@@ -54,9 +54,10 @@
>
<SmartEnvInput
v-model="tab.document.request.endpoint"
:placeholder="`${t('request.url_placeholder')}`"
:placeholder="getDefaultRESTRequest().endpoint"
:auto-complete-source="userHistories"
:auto-complete-env="true"
:placeholder-hover-string="t('request.http_placeholder')"
:inspection-results="tabResults"
@paste="onPasteUrl($event)"
@enter="newSendRequest"
@@ -331,12 +332,12 @@ const tabs = useService(RESTTabService)
const workspaceService = useService(WorkspaceService)
const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
if (/^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`)
return
}
ensureMethodInEndpoint()
if (newEndpoint.value) ensureMethodInEndpoint()
loading.value = true
@@ -348,7 +349,20 @@ const newSendRequest = async () => {
workspaceType: workspaceService.currentWorkspace.value.type,
})
const [cancel, streamPromise] = runRESTRequest$(tab)
const finalTab = ref({
...tab.value,
document: {
...tab.value.document,
request: {
...tab.value.document.request,
endpoint:
tab.value.document.request.endpoint ||
getDefaultRESTRequest().endpoint,
},
},
})
const [cancel, streamPromise] = runRESTRequest$(finalTab)
const streamResult = await streamPromise
requestCancelFunc.value = cancel
@@ -472,8 +486,13 @@ const fetchingShareLink = ref(false)
const shareRequest = () => {
if (currentUser.value) {
const finalRequest = {
...tab.value.document.request,
endpoint:
tab.value.document.request.endpoint || getDefaultRESTRequest().endpoint,
}
invokeAction("share.request", {
request: tab.value.document.request,
request: finalRequest,
})
} else {
invokeAction("modals.login.toggle")
@@ -513,11 +532,17 @@ const saveRequest = () => {
showSaveRequestModal.value = true
return
}
if (saveCtx.originLocation === "user-collection") {
const req = tab.value.document.request
const req = tab.value.document.request
const finalRequest = {
...req,
endpoint: req.endpoint.trim() || getDefaultRESTRequest().endpoint,
}
if (saveCtx.originLocation === "user-collection") {
try {
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, finalRequest)
tab.value.document.isDirty = false
@@ -534,8 +559,6 @@ const saveRequest = () => {
saveRequest()
}
} else if (saveCtx.originLocation === "team-collection") {
const req = tab.value.document.request
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
try {
platform.analytics?.logEvent({
@@ -549,7 +572,7 @@ const saveRequest = () => {
requestID: saveCtx.requestID,
data: {
title: req.name,
request: JSON.stringify(req),
request: JSON.stringify(finalRequest),
},
})().then((result) => {
if (E.isLeft(result)) {

View File

@@ -134,6 +134,7 @@ import * as E from "fp-ts/Either"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
import { watch } from "vue"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
const t = useI18n()
const colorMode = useColorMode()
@@ -511,7 +512,10 @@ const openRequestInNewTab = (request: HoppRESTRequest) => {
}
defineActionHandler("share.request", ({ request }) => {
requestToShare.value = request
requestToShare.value = {
...request,
endpoint: request.endpoint || getDefaultRESTRequest().endpoint,
}
displayShareRequestModal(true)
})
</script>

View File

@@ -73,7 +73,12 @@ import {
keymap,
tooltips,
} from "@codemirror/view"
import { EditorSelection, EditorState, Extension } from "@codemirror/state"
import {
Compartment,
EditorSelection,
EditorState,
Extension,
} from "@codemirror/state"
import { clone } from "lodash-es"
import { history, historyKeymap } from "@codemirror/commands"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
@@ -109,6 +114,7 @@ const props = withDefaults(
contextMenuEnabled?: boolean
secret?: boolean
autoCompleteEnv?: boolean
placeholderHoverString: string
}>(),
{
modelValue: "",
@@ -124,6 +130,7 @@ const props = withDefaults(
contextMenuEnabled: true,
secret: false,
autoCompleteEnvSource: false,
placeholderHoverString: "",
}
)
@@ -137,6 +144,8 @@ const emit = defineEmits<{
(e: "click", ev: any): void
}>()
const placeholderString = ref(props.placeholder)
const cachedValue = ref(props.modelValue)
const view = ref<EditorView>()
@@ -441,6 +450,9 @@ function handleTextSelection() {
}
}
const placeholderCompt = new Compartment()
const readOnlyCompt = new Compartment()
// Debounce to prevent double click from selecting the word
const debouncedTextSelection = (time: number) =>
useDebounceFn(() => {
@@ -475,6 +487,7 @@ const getExtensions = (readonly: boolean): Extension => {
}),
EditorState.changeFilter.of(() => !readonly),
inputTheme,
readOnlyCompt.of(EditorState.readOnly.of(readonly)),
readonly
? EditorView.theme({
".cm-content": {
@@ -490,7 +503,8 @@ const getExtensions = (readonly: boolean): Extension => {
position: "absolute",
}),
props.environmentHighlights ? envTooltipPlugin : [],
placeholderExt(props.placeholder),
placeholderCompt.of(placeholderExt(props.placeholder)),
EditorView.domEventHandlers({
paste(ev) {
clipboardEv = ev
@@ -505,6 +519,27 @@ const getExtensions = (readonly: boolean): Extension => {
debouncedTextSelection(30)()
}
},
mouseenter() {
//change placeholder to hover string if provided
if (props.placeholderHoverString && !props.readonly) {
placeholderString.value = props.placeholderHoverString
view.value?.dispatch({
effects: placeholderCompt.reconfigure(
placeholderExt(props.placeholderHoverString)
),
})
}
},
mouseleave() {
//change placeholder back to original string
if (props.placeholderHoverString && !props.readonly) {
view.value?.dispatch({
effects: placeholderCompt.reconfigure(
placeholderExt(props.placeholder)
),
})
}
},
}),
props.autoCompleteEnv
? autocompletion({
@@ -568,6 +603,38 @@ const getExtensions = (readonly: boolean): Extension => {
return extensions
}
watch(
() => props.readonly,
(readonly) => {
if (readonly) {
view.value!.dispatch({
effects: [
readOnlyCompt.reconfigure([
EditorState.readOnly.of(readonly),
EditorView.theme({
".cm-content": {
caretColor: "var(--secondary-dark-color)",
color: "var(--secondary-dark-color)",
backgroundColor: "var(--divider-color)",
opacity: 0.25,
},
}),
]),
],
})
} else {
view.value!.dispatch({
effects: [
readOnlyCompt.reconfigure([
EditorState.readOnly.of(readonly),
EditorView.theme({}),
]),
],
})
}
}
)
const triggerTextSelection = () => {
nextTick(() => {
view.value?.focus()

View File

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

View File

@@ -59,18 +59,14 @@
/>
</div>
<div
v-else-if="teamListAdapterError"
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>
<TeamsAdd
:show="showModalAdd"
:switch-workspace-after-creation="true"
@hide-modal="displayModalAdd(false)"
/>
<TeamsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
</div>
</template>
<script setup lang="ts">
@@ -85,7 +81,7 @@ import { useColorMode } from "@composables/theming"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconDone from "~icons/lucide/check"
import { useLocalState } from "~/newstore/localstate"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { useElementVisibility, useIntervalFn } from "@vueuse/core"
@@ -158,7 +154,6 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
teamID: team.id,
teamName: team.name,
type: "team",
role: team.myRole,
})
}
@@ -174,14 +169,11 @@ watch(
(user) => {
if (!user) {
switchToPersonalWorkspace()
teamListadapter.dispose()
}
}
)
const displayModalAdd = (shouldDisplay: boolean) => {
if (!currentUser.value) return invokeAction("modals.login.toggle")
showModalAdd.value = shouldDisplay
teamListadapter.fetchList()
}

View File

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

View File

@@ -201,7 +201,7 @@ export class TeamSearchService extends Service {
expandingCollections: 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
searchTeams = async (query: string, teamID: string) => {
if (!query.length) {

View File

@@ -1,5 +1,5 @@
import { HoppModule } from "."
import { Container, ServiceClassInstance } from "dioc"
import { Container, Service } from "dioc"
import { diocPlugin } from "dioc/vue"
import { DebugService } from "~/services/debug.service"
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
* 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
): InstanceType<T> {
return serviceContainer.bind(service)
@@ -30,10 +30,11 @@ export function getService<T extends ServiceClassInstance<any>>(
export default <HoppModule>{
onVueAppInit(app) {
// TODO: look into this
// @ts-expect-error Something weird with Vue versions
app.use(diocPlugin, {
container: serviceContainer,
})
for (const service of platform.addedServices ?? []) {
serviceContainer.bind(service)
}

View File

@@ -1393,6 +1393,14 @@ export function editGraphqlRequest(
}
export function saveGraphqlRequestAs(path: string, request: HoppGQLRequest) {
// For calculating the insertion request index
const targetLocation = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
path.split("/").map((x) => parseInt(x))
)
const insertionIndex = targetLocation!.requests.length
graphqlCollectionStore.dispatch({
dispatcher: "saveRequestAs",
payload: {
@@ -1400,6 +1408,8 @@ export function saveGraphqlRequestAs(path: string, request: HoppGQLRequest) {
request,
},
})
return insertionIndex
}
export function removeGraphqlRequest(

View File

@@ -1,10 +1,9 @@
import { pluck, distinctUntilChanged } from "rxjs/operators"
import { cloneDeep, defaultsDeep, has } from "lodash-es"
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 type { KeysMatching } from "~/types/ts-utils"
export const HoppBgColors = ["system", "light", "dark", "black"] as const
@@ -70,63 +69,51 @@ export type SettingsDef = {
HAS_OPENED_SPOTLIGHT: boolean
}
export const getDefaultSettings = (): SettingsDef => {
const defaultSettings: SettingsDef = {
syncCollections: true,
syncHistory: true,
syncEnvironments: true,
export const getDefaultSettings = (): SettingsDef => ({
syncCollections: true,
syncHistory: true,
syncEnvironments: true,
WRAP_LINES: {
httpRequestBody: true,
httpResponseBody: true,
httpHeaders: true,
httpParams: true,
httpUrlEncoded: true,
httpPreRequest: true,
httpTest: true,
httpRequestVariables: true,
graphqlQuery: true,
graphqlResponseBody: true,
graphqlHeaders: false,
graphqlVariables: false,
graphqlSchema: true,
importCurl: true,
codeGen: true,
cookie: true,
},
WRAP_LINES: {
httpRequestBody: true,
httpResponseBody: true,
httpHeaders: true,
httpParams: true,
httpUrlEncoded: true,
httpPreRequest: true,
httpTest: true,
httpRequestVariables: true,
graphqlQuery: true,
graphqlResponseBody: true,
graphqlHeaders: false,
graphqlVariables: false,
graphqlSchema: true,
importCurl: true,
codeGen: 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
PROXY_URL: "https://proxy.hoppscotch.io/",
URL_EXCLUDES: {
auth: true,
httpUser: true,
httpPassword: true,
bearerToken: true,
oauth2Token: true,
},
THEME_COLOR: "indigo",
BG_COLOR: "system",
TELEMETRY_ENABLED: true,
EXPAND_NAVIGATION: false,
SIDEBAR: true,
SIDEBAR_ON_LEFT: false,
COLUMN_LAYOUT: true,
// TODO: Interceptor related settings should move under the interceptor systems
PROXY_URL: "https://proxy.hoppscotch.io/",
URL_EXCLUDES: {
auth: true,
httpUser: true,
httpPassword: true,
bearerToken: true,
oauth2Token: true,
},
THEME_COLOR: "indigo",
BG_COLOR: "system",
TELEMETRY_ENABLED: true,
EXPAND_NAVIGATION: false,
SIDEBAR: true,
SIDEBAR_ON_LEFT: false,
COLUMN_LAYOUT: true,
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
}
HAS_OPENED_SPOTLIGHT: false,
})
type ApplySettingPayload = {
[K in keyof SettingsDef]: {

View File

@@ -112,7 +112,10 @@ const activeTabs = tabs.getActiveTabs()
const addNewTab = () => {
const tab = tabs.createNewTab({
request: getDefaultGQLRequest(),
request: {
...getDefaultGQLRequest(),
url: "",
},
isDirty: false,
})

View File

@@ -64,6 +64,13 @@
@submit="renameReqName"
@hide-modal="showRenamingReqNameModal = false"
/>
<HoppSmartConfirmModal
:show="confirmingCloseForTabID !== null"
:confirm="t('modal.close_unsaved_tab')"
:title="t('confirm.save_unsaved_tab')"
@hide-modal="onCloseConfirmSaveTab"
@resolve="onResolveConfirmSaveTab"
/>
<HoppSmartConfirmModal
:show="confirmingCloseAllTabs"
:confirm="t('modal.close_unsaved_tab')"
@@ -71,36 +78,6 @@
@hide-modal="confirmingCloseAllTabs = false"
@resolve="onResolveConfirmCloseAllTabs"
/>
<HoppSmartModal
v-if="confirmingCloseForTabID !== null"
dialog
role="dialog"
aria-modal="true"
:title="t('modal.close_unsaved_tab')"
@close="confirmingCloseForTabID = null"
>
<template #body>
<div class="text-center">
{{ t("confirm.save_unsaved_tab") }}
</div>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
v-focus
:label="t?.('action.yes')"
outline
@click="onResolveConfirmSaveTab"
/>
<HoppButtonSecondary
:label="t?.('action.no')"
filled
outline
@click="onCloseConfirmSaveTab"
/>
</span>
</template>
</HoppSmartModal>
<CollectionsSaveRequest
v-if="savingRequest"
mode="rest"
@@ -201,7 +178,10 @@ const onTabUpdate = (tab: HoppTab<HoppRESTDocument>) => {
const addNewTab = () => {
const tab = tabs.createNewTab({
request: getDefaultRESTRequest(),
request: {
...getDefaultRESTRequest(),
endpoint: "",
},
isDirty: false,
})
@@ -318,8 +298,14 @@ const shareTabRequest = (tabID: string) => {
const tab = tabs.getTabRef(tabID)
if (tab.value) {
if (currentUser.value) {
const finalRequest = {
...tab.value.document.request,
endpoint:
tab.value.document.request.endpoint ||
getDefaultRESTRequest().endpoint,
}
invokeAction("share.request", {
request: tab.value.document.request,
request: finalRequest,
})
} else {
invokeAction("modals.login.toggle")

View File

@@ -8,15 +8,14 @@ import { AnalyticsPlatformDef } from "./analytics"
import { InterceptorsPlatformDef } from "./interceptors"
import { HoppModule } from "~/modules"
import { InspectorsPlatformDef } from "./inspectors"
import { ServiceClassInstance } from "dioc"
import { Service } from "dioc"
import { IOPlatformDef } from "./io"
import { SpotlightPlatformDef } from "./spotlight"
import { Ref } from "vue"
export type PlatformDef = {
ui?: UIPlatformDef
addedHoppModules?: HoppModule[]
addedServices?: Array<ServiceClassInstance<unknown>>
addedServices?: Array<typeof Service<unknown> & { ID: string }>
auth: AuthPlatformDef
analytics?: AnalyticsPlatformDef
io: IOPlatformDef
@@ -46,11 +45,6 @@ export type PlatformDef = {
* If a value is not given, then the value is assumed to be true
*/
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"
/**
@@ -8,9 +8,8 @@ export type PlatformInspectorsDef = {
// We are keeping this as the only mode for now
// So that if we choose to add other modes, we can do without breaking
type: "service"
// TODO: I don't think this type is effective, we have to come up with a better impl
service: ServiceClassInstance<unknown> & {
new (c: Container): Inspector
service: typeof Service<unknown> & { ID: string } & {
new (): Service & Inspector
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,9 @@ export class ParameterMenuService extends Service implements ContextMenu {
private readonly contextMenu = this.bind(ContextMenuService)
override onServiceInit() {
constructor() {
super()
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 restTab = this.bind(RESTTabService)
override onServiceInit() {
constructor() {
super()
this.contextMenu.registerMenu(this)
}

View File

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

View File

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

View File

@@ -107,7 +107,9 @@ export class InspectionService extends Service {
private readonly restTab = this.bind(RESTTabService)
override onServiceInit() {
constructor() {
super()
this.initializeListeners()
}

View File

@@ -53,7 +53,9 @@ export class EnvironmentInspectorService extends Service implements Inspector {
}
)[0]
override onServiceInit() {
constructor() {
super()
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 interceptorService = this.bind(InterceptorService)
override onServiceInit() {
constructor() {
super()
this.inspection.registerInspector(this)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ import IconEdit from "~icons/lucide/edit"
import IconLayers from "~icons/lucide/layers"
import IconTrash2 from "~icons/lucide/trash-2"
import { Container, Service } from "dioc"
import { Service } from "dioc"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
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(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}
@@ -280,7 +277,9 @@ export class SwitchEnvSpotlightSearcherService
private readonly workspaceService = this.bind(WorkspaceService)
private teamEnvironmentList: TeamEnvironment[] = []
override onServiceInit() {
constructor() {
super()
this.spotlight.registerSearcher(this)
}

View File

@@ -15,7 +15,6 @@ import IconBook from "~icons/lucide/book"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import IconZap from "~icons/lucide/zap"
import { platform } from "~/platform"
import { Container } from "dioc"
type Doc = {
text: string | string[]
@@ -90,18 +89,15 @@ export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherServ
},
})
// TODO: This is not recommended as of dioc > 3. Move to onServiceInit instead
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}

View File

@@ -66,7 +66,9 @@ export class HistorySpotlightSearcherService
}
)[0]
override onServiceInit() {
constructor() {
super()
this.spotlight.registerSearcher(this)
}

View File

@@ -31,7 +31,9 @@ export class InterceptorSpotlightSearcherService
private readonly spotlight = this.bind(SpotlightService)
private interceptorService = this.bind(InterceptorService)
override onServiceInit() {
constructor() {
super()
this.spotlight.registerSearcher(this)
}

View File

@@ -8,7 +8,6 @@ import {
} from "./base/static.searcher"
import IconShare from "~icons/lucide/share"
import { Container } from "dioc"
type Doc = {
text: string
@@ -40,18 +39,15 @@ export class MiscellaneousSpotlightSearcherService extends StaticSpotlightSearch
},
})
// TODO: Constructors are no longer recommended as of dioc > 3, move to onServiceInit
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}

View File

@@ -8,7 +8,6 @@ import {
} from "./base/static.searcher"
import IconArrowRight from "~icons/lucide/arrow-right"
import { Container } from "dioc"
type Doc = {
text: string
@@ -62,18 +61,15 @@ export class NavigationSpotlightSearcherService extends StaticSpotlightSearcherS
private docKeys = Object.keys(this.documents)
// TODO: Constructors are no longer recommended as of dioc > 3, use onServiceInit instead
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}

View File

@@ -20,7 +20,6 @@ import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { RESTTabService } from "~/services/tab/rest"
import { Container } from "dioc"
type Doc = {
text: string | string[]
@@ -225,18 +224,15 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
},
})
// TODO: Constructors are no longer recommended as of dioc > 3, use onServiceInit instead
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}

View File

@@ -9,7 +9,6 @@ import {
import IconDownload from "~icons/lucide/download"
import IconCopy from "~icons/lucide/copy"
import { Container } from "dioc"
type Doc = {
text: string
@@ -57,18 +56,15 @@ export class ResponseSpotlightSearcherService extends StaticSpotlightSearcherSer
},
})
// TODO: Constructors are no longer recommended as of dioc > 3, move to onServiceInit
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}

View File

@@ -15,7 +15,6 @@ import IconMonitor from "~icons/lucide/monitor"
import IconMoon from "~icons/lucide/moon"
import IconSun from "~icons/lucide/sun"
import IconCheckCircle from "~icons/lucide/check-circle"
import { Container } from "dioc"
type Doc = {
text: string | string[]
@@ -101,18 +100,15 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
},
})
// TODO: Constuctors are no longer recommended as of dioc > 3, move to onServiceInit
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}

View File

@@ -14,7 +14,6 @@ import IconXSquare from "~icons/lucide/x-square"
import { invokeAction } from "~/helpers/actions"
import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
import { Container } from "dioc"
type Doc = {
text: string | string[]
@@ -90,18 +89,15 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
},
})
// TODO: Constructors are no longer recommended as of dioc > 3, use onServiceInit instead
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}

View File

@@ -39,7 +39,9 @@ export class TeamsSpotlightSearcherService
private readonly tabs = this.bind(RESTTabService)
override onServiceInit() {
constructor() {
super()
this.spotlight.registerSearcher(this)
}

View File

@@ -9,7 +9,6 @@ import { useStreamStatic } from "~/composables/stream"
import IconLogin from "~icons/lucide/log-in"
import IconLogOut from "~icons/lucide/log-out"
import { activeActions$, invokeAction } from "~/helpers/actions"
import { Container } from "dioc"
type Doc = {
text: string
@@ -60,18 +59,15 @@ export class UserSpotlightSearcherService extends StaticSpotlightSearcherService
},
})
// TODO: Constructors are no longer recommended as of dioc > 3, move to onServiceInit
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit(): void {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}

View File

@@ -21,7 +21,7 @@ import {
StaticSpotlightSearcherService,
} from "./base/static.searcher"
import { Container, Service } from "dioc"
import { Service } from "dioc"
import * as E from "fp-ts/Either"
import MiniSearch from "minisearch"
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
@@ -102,18 +102,15 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
},
})
// TODO: Constructors are no longer recommended as of dioc > 3, move to onServiceInit
constructor(c: Container) {
super(c, {
constructor() {
super({
searchFields: ["text", "alternates"],
fieldWeights: {
text: 2,
alternates: 1,
},
})
}
override onServiceInit() {
this.setDocuments(this.documents)
this.spotlight.registerSearcher(this)
}
@@ -169,7 +166,9 @@ export class SwitchWorkspaceSpotlightSearcherService
private readonly spotlight = this.bind(SpotlightService)
private readonly workspaceService = this.bind(WorkspaceService)
override onServiceInit() {
constructor() {
super()
this.spotlight.registerSearcher(this)
}

View File

@@ -6,7 +6,9 @@ import { reactive } from "vue"
class MockTabService extends TabService<{ request: string }> {
public static readonly ID = "MOCK_TAB_SERVICE"
override onServiceInit() {
constructor() {
super()
this.tabMap = reactive(
new Map([
[

View File

@@ -3,20 +3,20 @@ import { getDefaultGQLRequest } from "~/helpers/graphql/default"
import { HoppGQLDocument, HoppGQLSaveContext } from "~/helpers/graphql/document"
import { TabService } from "./tab"
import { computed } from "vue"
import { Container } from "dioc"
export class GQLTabService extends TabService<HoppGQLDocument> {
public static readonly ID = "GQL_TAB_SERVICE"
// TODO: Moving this to `onServiceInit` breaks `persistableTabState`
// Figure out how to fix this
constructor(c: Container) {
super(c)
constructor() {
super()
this.tabMap.set("test", {
id: "test",
document: {
request: getDefaultGQLRequest(),
request: {
...getDefaultGQLRequest(),
url: "",
},
isDirty: false,
optionTabPreference: "query",
},

View File

@@ -3,20 +3,20 @@ import { computed } from "vue"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { HoppRESTDocument, HoppRESTSaveContext } from "~/helpers/rest/document"
import { TabService } from "./tab"
import { Container } from "dioc"
export class RESTTabService extends TabService<HoppRESTDocument> {
public static readonly ID = "REST_TAB_SERVICE"
// TODO: Moving this to `onServiceInit` breaks `persistableTabState`
// Figure out how to fix this
constructor(c: Container) {
super(c)
constructor() {
super()
this.tabMap.set("test", {
id: "test",
document: {
request: getDefaultRESTRequest(),
request: {
...getDefaultRESTRequest(),
endpoint: "",
},
isDirty: false,
optionTabPreference: "params",
},

View File

@@ -5,24 +5,13 @@ import { useStreamStatic } from "~/composables/stream"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { platform } from "~/platform"
import { min } from "lodash-es"
import { TeamMemberRole } from "~/helpers/backend/graphql"
/**
* Defines a workspace and its information
*/
export type PersonalWorkspace = {
type: "personal"
}
export type TeamWorkspace = {
type: "team"
teamID: string
teamName: string
role: TeamMemberRole | null | undefined
}
export type Workspace = PersonalWorkspace | TeamWorkspace
export type Workspace =
| { type: "personal" }
| { type: "team"; teamID: string; teamName: string }
export type WorkspaceServiceEvent = {
type: "managed-team-list-adapter-polled"
@@ -59,7 +48,8 @@ export class WorkspaceService extends Service<WorkspaceServiceEvent> {
-1
)
override onServiceInit() {
constructor() {
super()
// Dispose the managed team list adapter when the user logs out
// and initialize it when the user logs in
watch(

View File

@@ -14,21 +14,3 @@ Since TypeScript cannot handle type information for `.vue` imports, they are shi
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
## Building
### Prequisites
- Install Rust: `curl https://sh.rustup.rs -sSf | sh`
- If you're on Mac, remove the installation from brew first if any (brew uninstall rust && brew cleanup)
- libsoup2.4-dev installed if Linux: `sudo apt install libsoup2.4-dev`
- Node v18.20 installed and currently active if you're using nvm
### Build Instruction
1. Install latest pnpm `curl -fsSL https://get.pnpm.io/install.sh | sh -` or upgrade `pnpm add -g pnpm`
2. Setup the .env of the root project folder, you should deploy the self hosted backend first
3. Run `pnpm install` on root project folder
4. Run `pnpm dev:gql-codegen` on this folder
5. Run `pnpm tauri dev` to run debug mode (optional)
6. Run `pnpm tauri build` to build release mode
- `pnpm tauri build --target universal-apple-darwin` for Mac

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/selfhost-desktop",
"private": true,
"version": "2024.3.3",
"version": "2024.3.0",
"type": "module",
"scripts": {
"dev:vite": "vite",
@@ -17,14 +17,13 @@
"@fontsource-variable/material-symbols-rounded": "5.0.16",
"@fontsource-variable/roboto-mono": "5.0.16",
"@hoppscotch/common": "workspace:^",
"@hoppscotch/data": "workspace:^",
"@platform/auth": "0.1.106",
"@tauri-apps/api": "1.5.1",
"@tauri-apps/cli": "1.5.6",
"@vueuse/core": "10.5.0",
"axios": "0.21.4",
"buffer": "6.0.3",
"dioc": "3.0.1",
"dioc": "1.0.1",
"environments.api": "link:@platform/environments/environments.api",
"event": "link:@tauri-apps/api/event",
"fp-ts": "2.16.1",
@@ -37,13 +36,7 @@
"tauri-plugin-store-api": "0.0.0",
"util": "0.12.5",
"vue": "3.3.9",
"workbox-window": "6.6.0",
"zod": "3.22.4",
"@urql/core": "^4.1.1",
"cookie": "^0.5.0",
"subscriptions-transport-ws": "^0.11.0",
"tauri-plugin-websocket-api": "github:tauri-apps/tauri-plugin-websocket#v1",
"wonka": "^6.3.4"
"workbox-window": "6.6.0"
},
"devDependencies": {
"@graphql-codegen/add": "5.0.0",
@@ -83,8 +76,6 @@
"vite-plugin-pwa": "0.13.1",
"vite-plugin-static-copy": "0.12.0",
"vite-plugin-vue-layouts": "0.7.0",
"vue-tsc": "1.8.8",
"@types/cookie": "^0.5.1"
"vue-tsc": "1.8.8"
}
}
}

View File

@@ -1260,7 +1260,7 @@ dependencies = [
[[package]]
name = "hoppscotch-desktop"
version = "24.3.3"
version = "24.3.1"
dependencies = [
"cocoa 0.25.0",
"hex_color",

View File

@@ -1,6 +1,6 @@
[package]
name = "hoppscotch-desktop"
version = "24.3.3"
version = "24.3.0"
description = "A Tauri App"
authors = ["you"]
license = ""
@@ -25,13 +25,11 @@ tauri = { version = "1.5.3", features = [
] }
tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-deep-link = { git = "https://github.com/FabianLars/tauri-plugin-deep-link", branch = "main" }
tauri-plugin-websocket = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tauri-plugin-window-state = "0.1.0"
reqwest = "0.11.22"
serde_json = "1.0.108"
url = "2.5.0"
hex_color = "3.0.0"
time = "0.3.36"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0"

View File

@@ -21,7 +21,6 @@ fn main() {
tauri_plugin_deep_link::prepare("io.hoppscotch.desktop");
tauri::Builder::default()
.plugin(tauri_plugin_websocket::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.plugin(tauri_plugin_store::Builder::default().build())
.setup(|app| {

View File

@@ -8,7 +8,7 @@
},
"package": {
"productName": "Hoppscotch",
"version": "24.3.3"
"version": "24.3.1"
},
"tauri": {
"allowlist": {
@@ -49,7 +49,7 @@
"targets": "all"
},
"security": {
"csp": null
"csp": "none"
},
"updater": {
"active": false

View File

@@ -1,168 +0,0 @@
import { ref } from "vue"
import { makeResult, makeErrorResult, Exchange, Operation } from "@urql/core"
import { makeFetchBody, makeFetchOptions } from "@urql/core/internal"
import { filter, make, merge, mergeMap, pipe, takeUntil, map } from "wonka"
import { gqlClientError$ } from "@hoppscotch/common/helpers/backend/GQLClient"
import { Store } from "tauri-plugin-store-api"
import { Body, getClient } from "@tauri-apps/api/http"
import { parse, serialize } from "cookie"
import { SubscriptionClient } from "subscriptions-transport-ws"
import { platform } from "@hoppscotch/common/platform"
import WSWrapper from "./ws_wrapper"
const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
export async function addCookieToFetchHeaders(
store: Store,
headers: HeadersInit = {}
) {
try {
const accessToken = await store.get<{ value: string }>("access_token")
const refreshToken = await store.get<{ value: string }>("refresh_token")
if (accessToken?.value && refreshToken?.value) {
// Assert headers as an indexable type
const headersIndexable = headers as { [key: string]: string }
const existingCookies = parse(headersIndexable["Cookie"] || "")
if (!existingCookies.access_token) {
existingCookies.access_token = accessToken.value
}
if (!existingCookies.refresh_token) {
existingCookies.refresh_token = refreshToken.value
}
// Serialize the cookies back into the headers
const serializedCookies = Object.entries(existingCookies)
.map(([name, value]) => serialize(name, value))
.join("; ")
headersIndexable["Cookie"] = serializedCookies
}
return headers
} catch (error) {
console.error("error while injecting cookie")
}
}
function createHttpSource(operation: Operation, store: Store) {
return make(({ next, complete }) => {
getClient().then(async (httpClient) => {
const fetchOptions = makeFetchOptions(operation)
let headers = fetchOptions.headers
headers = await addCookieToFetchHeaders(store, headers)
const fetchBody = makeFetchBody(operation)
httpClient
.post(operation.context.url, Body.json(fetchBody), {
headers,
})
.then((result) => {
next(result.data)
complete()
})
.catch((error) => {
next(makeErrorResult(operation, error))
complete()
})
})
return () => {}
})
}
export const tauriGQLFetchExchange =
(store: Store): Exchange =>
({ forward }) =>
(ops$) => {
const subscriptionResults$ = pipe(
ops$,
filter((op) => op.kind === "query" || op.kind === "mutation"),
mergeMap((operation) => {
const { key, context } = operation
const teardown$ = pipe(
ops$,
filter((op: Operation) => op.kind === "teardown" && op.key === key)
)
const source = createHttpSource(operation, store)
return pipe(
source,
takeUntil(teardown$),
map((result) => makeResult(operation, result as any))
)
})
)
const forward$ = pipe(
ops$,
filter(
(op: Operation) => op.kind === "teardown" || op.kind != "subscription"
),
forward
)
return merge([subscriptionResults$, forward$])
}
const createSubscriptionClient = () => {
return new SubscriptionClient(
import.meta.env.VITE_BACKEND_WS_URL,
{
reconnect: true,
connectionParams: () => platform.auth.getBackendHeaders(),
connectionCallback(error) {
if (error?.length > 0) {
gqlClientError$.next({
type: "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT",
errors: error,
})
}
},
wsOptionArguments: [
{
store: new Store(APP_DATA_PATH),
},
],
},
WSWrapper
)
}
let subscriptionClient: SubscriptionClient | null
const isBackendGQLEventAdded = ref(false)
const resetSubscriptionClient = () => {
if (subscriptionClient) {
subscriptionClient.close()
}
subscriptionClient = createSubscriptionClient()
if (!isBackendGQLEventAdded.value) {
subscriptionClient.onConnected(() => {
platform.auth.onBackendGQLClientShouldReconnect(() => {
const currentUser = platform.auth.getCurrentUser()
if (currentUser && subscriptionClient) {
subscriptionClient?.client?.close()
}
if (currentUser && !subscriptionClient) {
resetSubscriptionClient()
}
if (!currentUser && subscriptionClient) {
subscriptionClient.close()
resetSubscriptionClient()
}
})
})
isBackendGQLEventAdded.value = true
}
}
export const getSubscriptionClient = () => {
if (!subscriptionClient) resetSubscriptionClient()
return subscriptionClient
}

View File

@@ -1,154 +0,0 @@
import { Store } from "tauri-plugin-store-api"
import TauriWebSocket, {
Message,
ConnectionConfig,
} from "tauri-plugin-websocket-api"
import { addCookieToFetchHeaders } from "./GQLClient"
/**
* This is a wrapper around tauri-plugin-websocket-api with cookie injection support. This is required because
* subscriptions-transport-ws client expects a custom websocket implementation in the shape of native browser WebSocket.
*/
export default class WebSocketWrapper extends EventTarget implements WebSocket {
public client: TauriWebSocket | undefined
private tauriWebSocketConfig:
| (ConnectionConfig & { store: Store })
| undefined
private isConnected: boolean = false
binaryType: BinaryType = "blob"
extensions = ""
onclose: ((this: WebSocket, ev: CloseEvent) => any) | null = null
onerror: ((this: WebSocket, ev: Event) => any) | null = null
onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null = null
onopen: ((this: WebSocket, ev: Event) => any) | null = null
protocol = ""
url: string
public static readonly CONNECTING = 0
public static readonly OPEN = 1
public static readonly CLOSING = 2
public static readonly CLOSED = 3
readonly CONNECTING = 0
readonly OPEN = 1
readonly CLOSING = 2
readonly CLOSED = 3
constructor(
url: string,
protocols?: string | string[],
config?: ConnectionConfig & { store: Store }
) {
super()
this.url = url
this.tauriWebSocketConfig = config
this.setup()
}
private async setup() {
if (this.tauriWebSocketConfig?.store) {
const headersStringified =
this.tauriWebSocketConfig.headers || ("{}" as any)
let headers = JSON.parse(headersStringified)
headers = await addCookieToFetchHeaders(
this.tauriWebSocketConfig.store,
headers
)
this.tauriWebSocketConfig = {
...this.tauriWebSocketConfig,
headers,
}
}
this.client = await TauriWebSocket.connect(this.url, {
headers: {
"sec-websocket-protocol": "graphql-ws",
...this.tauriWebSocketConfig?.headers,
},
}).catch((error) => {
this.isConnected = false
if (this.onerror) {
this.onerror(new Event("error"))
}
throw new Error(error)
})
this.isConnected = true
this.client.addListener(this.handleMessage.bind(this))
if (this.onopen) {
this.onopen(new Event("open"))
}
}
get readyState(): number {
return this.client
? this.isConnected
? this.OPEN
: this.CLOSED
: this.CONNECTING
}
get bufferedAmount(): number {
// TODO implement
return 0
}
close(code?: number, reason?: string): void {
this.client?.disconnect().then(() => {
if (this.onclose) {
this.onclose(new CloseEvent("close"))
}
})
}
send(data: string | ArrayBufferLike | Blob | ArrayBufferView) {
if (
typeof data === "string" ||
data instanceof ArrayBuffer ||
data instanceof Blob
) {
this.client?.send(data as string).catch((error) => {
console.error("error while sending data", data)
if (this.onerror) {
this.onerror(new Event("error"))
}
})
} else {
// TODO implement, drop the record for now
console.warn(
"WebSocketWrapper.send() not implemented for non-string data"
)
}
}
private handleMessage(message: Message): void {
switch (message.type) {
case "Close": {
if (this.onclose) {
this.onclose(new CloseEvent("close"))
}
return
}
case "Ping": {
this.client?.send("Pong").catch((error) => {
console.error("error while sending Pong data", message)
if (this.onerror) {
this.onerror(new Event("error"))
}
})
return
}
default: {
if (this.onmessage) {
this.onmessage(
new MessageEvent("message", {
data: message.data,
origin: this.url,
})
)
}
}
}
}
}

View File

@@ -1,5 +1,4 @@
import { getService } from "@hoppscotch/common/modules/dioc"
import axios from "axios"
import {
AuthEvent,
AuthPlatformDef,
@@ -14,50 +13,15 @@ import { open } from '@tauri-apps/api/shell'
import { BehaviorSubject, Subject } from "rxjs"
import { Store } from "tauri-plugin-store-api"
import { Ref, ref, watch } from "vue"
import { z } from "zod"
import * as E from "fp-ts/Either"
import { subscriptionExchange } from "@urql/core"
import {
getSubscriptionClient,
tauriGQLFetchExchange,
} from "../helpers/GQLClient"
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
export const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
const persistenceService = getService(PersistenceService)
const expectedAllowedProvidersSchema = z.object({
// currently supported values are "GOOGLE", "GITHUB", "EMAIL", "MICROSOFT", "SAML"
// keeping it as string to avoid backend accidentally breaking frontend when adding new providers
providers: z.array(z.string()),
})
export const getAllowedAuthProviders = async () => {
try {
const res = await axios.get(
`${import.meta.env.VITE_BACKEND_API_URL}/auth/providers`,
{
withCredentials: true,
}
)
const parseResult = expectedAllowedProvidersSchema.safeParse(res.data)
if (!parseResult.success) {
return E.left("SOMETHING_WENT_WRONG")
}
return E.right(parseResult.data.providers)
} catch (_) {
return E.left("SOMETHING_WENT_WRONG")
}
}
async function logout() {
let client = await getClient();
await client.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`)
@@ -73,7 +37,7 @@ async function signInUserWithGithubFB() {
}
async function signInUserWithGoogleFB() {
await open(`${import.meta.env.VITE_BACKEND_LOGIN_API_URL}/authenticate`);
await open(`${import.meta.env.VITE_BACKEND_API_URL}/auth/google?redirect_uri=desktop`);
}
async function signInUserWithMicrosoftFB() {
@@ -260,15 +224,6 @@ export const def: AuthPlatformDef = {
},
getGQLClientOptions() {
return {
exchanges: [
subscriptionExchange({
forwardSubscription(fetchBody) {
const subscriptionClient = getSubscriptionClient()
return subscriptionClient!.request(fetchBody)
},
}),
tauriGQLFetchExchange(new Store(APP_DATA_PATH)),
],
fetchOptions: {
credentials: "include",
},
@@ -416,5 +371,4 @@ export const def: AuthPlatformDef = {
event: "logout",
})
},
getAllowedAuthProviders,
}

View File

@@ -138,6 +138,10 @@ export class NativeInterceptorService extends Service implements Interceptor {
public cookieJarService = this.bind(CookieJarService)
constructor() {
super()
}
public runRequest(req: any) {
const processedReq = preProcessRequest(req)

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/selfhost-web",
"private": true,
"version": "2024.3.3",
"version": "2024.3.1",
"type": "module",
"scripts": {
"dev:vite": "vite",

View File

@@ -52,8 +52,7 @@
"title": "Server is restarting"
},
"save_changes": "Save Changes",
"title": "Configurations",
"update_failure": "Failed to update server configurations"
"title": "Configurations"
},
"data_sharing": {
"description": "Share anonymous data usage to improve Hoppscotch",

View File

@@ -1,7 +1,7 @@
{
"name": "hoppscotch-sh-admin",
"private": true,
"version": "2024.3.3",
"version": "2024.3.1",
"type": "module",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",

View File

@@ -69,18 +69,18 @@
import { useVModel } from '@vueuse/core';
import { reactive } from 'vue';
import { useI18n } from '~/composables/i18n';
import { ServerConfigs, SsoAuthProviders } from '~/helpers/configs';
import { Config, SsoAuthProviders } from '~/composables/useConfigHandler';
import IconEye from '~icons/lucide/eye';
import IconEyeOff from '~icons/lucide/eye-off';
const t = useI18n();
const props = defineProps<{
config: ServerConfigs;
config: Config;
}>();
const emit = defineEmits<{
(e: 'update:config', v: ServerConfigs): void;
(e: 'update:config', v: Config): void;
}>();
const workingConfigs = useVModel(props, 'config', emit);
@@ -93,7 +93,7 @@ const capitalize = (text: string) =>
type ProviderFieldKeys = keyof ProviderFields;
type ProviderFields = {
[Field in keyof ServerConfigs['providers'][SsoAuthProviders]['fields']]: boolean;
[Field in keyof Config['providers'][SsoAuthProviders]['fields']]: boolean;
} & Partial<{ tenant: boolean }>;
type ProviderFieldMetadata = {

View File

@@ -9,14 +9,14 @@
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { ServerConfigs } from '~/helpers/configs';
import { Config } from '~/composables/useConfigHandler';
const props = defineProps<{
config: ServerConfigs;
config: Config;
}>();
const emit = defineEmits<{
(e: 'update:config', v: ServerConfigs): void;
(e: 'update:config', v: Config): void;
}>();
const workingConfigs = useVModel(props, 'config', emit);

View File

@@ -38,17 +38,17 @@
import { useVModel } from '@vueuse/core';
import { computed } from 'vue';
import { useI18n } from '~/composables/i18n';
import { ServerConfigs } from '~/helpers/configs';
import { Config } from '~/composables/useConfigHandler';
import IconShieldQuestion from '~icons/lucide/shield-question';
const t = useI18n();
const props = defineProps<{
config: ServerConfigs;
config: Config;
}>();
const emit = defineEmits<{
(e: 'update:config', v: ServerConfigs): void;
(e: 'update:config', v: Config): void;
}>();
const workingConfigs = useVModel(props, 'config', emit);

View File

@@ -17,21 +17,20 @@ import { useMutation } from '@urql/vue';
import { onMounted, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import { useToast } from '~/composables/toast';
import { useConfigHandler } from '~/composables/useConfigHandler';
import { Config, useConfigHandler } from '~/composables/useConfigHandler';
import {
EnableAndDisableSsoDocument,
ResetInfraConfigsDocument,
ToggleAnalyticsCollectionDocument,
UpdateInfraConfigsDocument,
ToggleAnalyticsCollectionDocument,
} from '~/helpers/backend/graphql';
import { ServerConfigs } from '~/helpers/configs';
const t = useI18n();
const toast = useToast();
const props = withDefaults(
defineProps<{
workingConfigs?: ServerConfigs;
workingConfigs?: Config;
reset?: boolean;
}>(),
{

View File

@@ -58,18 +58,18 @@
import { useVModel } from '@vueuse/core';
import { computed, reactive } from 'vue';
import { useI18n } from '~/composables/i18n';
import { ServerConfigs } from '~/helpers/configs';
import { Config } from '~/composables/useConfigHandler';
import IconEye from '~icons/lucide/eye';
import IconEyeOff from '~icons/lucide/eye-off';
const t = useI18n();
const props = defineProps<{
config: ServerConfigs;
config: Config;
}>();
const emit = defineEmits<{
(e: 'update:config', v: ServerConfigs): void;
(e: 'update:config', v: Config): void;
}>();
const workingConfigs = useVModel(props, 'config', emit);
@@ -87,7 +87,7 @@ const smtpConfigs = computed({
// Mask sensitive fields
type Field = {
name: string;
key: keyof ServerConfigs['mailConfigs']['fields'];
key: keyof Config['mailConfigs']['fields'];
};
const smtpConfigFields = reactive<Field[]>([
@@ -100,10 +100,10 @@ const maskState = reactive<Record<string, boolean>>({
mailer_from_address: true,
});
const toggleMask = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) => {
const toggleMask = (fieldKey: keyof Config['mailConfigs']['fields']) => {
maskState[fieldKey] = !maskState[fieldKey];
};
const isMasked = (fieldKey: keyof ServerConfigs['mailConfigs']['fields']) =>
const isMasked = (fieldKey: keyof Config['mailConfigs']['fields']) =>
maskState[fieldKey];
</script>

View File

@@ -1,39 +1,83 @@
import { AnyVariables, UseMutationResponse } from '@urql/vue';
import { cloneDeep } from 'lodash-es';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from '~/composables/i18n';
import {
AllowedAuthProvidersDocument,
AuthProvider,
EnableAndDisableSsoArgs,
EnableAndDisableSsoMutation,
InfraConfigArgs,
InfraConfigEnum,
InfraConfigsDocument,
ResetInfraConfigsMutation,
ServiceStatus,
ToggleAnalyticsCollectionMutation,
UpdateInfraConfigsMutation,
} from '~/helpers/backend/graphql';
import {
ALL_CONFIGS,
ConfigSection,
ConfigTransform,
GITHUB_CONFIGS,
GOOGLE_CONFIGS,
MAIL_CONFIGS,
MICROSOFT_CONFIGS,
ServerConfigs,
UpdatedConfigs,
} from '~/helpers/configs';
import { useToast } from './toast';
import { useClientHandler } from './useClientHandler';
// Types
export type SsoAuthProviders = 'google' | 'microsoft' | 'github';
export type Config = {
providers: {
google: {
name: SsoAuthProviders;
enabled: boolean;
fields: {
client_id: string;
client_secret: string;
callback_url: string;
scope: string;
};
};
github: {
name: SsoAuthProviders;
enabled: boolean;
fields: {
client_id: string;
client_secret: string;
callback_url: string;
scope: string;
};
};
microsoft: {
name: SsoAuthProviders;
enabled: boolean;
fields: {
client_id: string;
client_secret: string;
callback_url: string;
scope: string;
tenant: string;
};
};
};
mailConfigs: {
name: string;
enabled: boolean;
fields: {
mailer_smtp_url: string;
mailer_from_address: string;
};
};
dataSharingConfigs: {
name: string;
enabled: boolean;
};
};
type UpdatedConfigs = {
name: string;
value: string;
};
/** Composable that handles all operations related to server configurations
* @param updatedConfigs A Config Object contatining the updated configs
*/
export function useConfigHandler(updatedConfigs?: ServerConfigs) {
export function useConfigHandler(updatedConfigs?: Config) {
const t = useI18n();
const toast = useToast();
@@ -46,9 +90,24 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
} = useClientHandler(
InfraConfigsDocument,
{
configNames: ALL_CONFIGS.flat().map(
({ name }) => name
) as InfraConfigEnum[],
configNames: [
'GOOGLE_CLIENT_ID',
'GOOGLE_CLIENT_SECRET',
'GOOGLE_CALLBACK_URL',
'GOOGLE_SCOPE',
'MICROSOFT_CLIENT_ID',
'MICROSOFT_CLIENT_SECRET',
'MICROSOFT_CALLBACK_URL',
'MICROSOFT_SCOPE',
'MICROSOFT_TENANT',
'GITHUB_CLIENT_ID',
'GITHUB_CLIENT_SECRET',
'GITHUB_CALLBACK_URL',
'GITHUB_SCOPE',
'MAILER_SMTP_URL',
'MAILER_ADDRESS_FROM',
'ALLOW_ANALYTICS_COLLECTION',
] as InfraConfigEnum[],
},
(x) => x.infraConfigs
);
@@ -66,14 +125,14 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
);
// Current and working configs
const currentConfigs = ref<ServerConfigs>();
const workingConfigs = ref<ServerConfigs>();
const currentConfigs = ref<Config>();
const workingConfigs = ref<Config>();
onMounted(async () => {
await fetchInfraConfigs();
await fetchAllowedAuthProviders();
const getFieldValue = (name: InfraConfigEnum) =>
const getFieldValue = (name: string) =>
infraConfigs.value.find((x) => x.name === name)?.value ?? '';
// Transforming the fetched data into a Configs object
@@ -81,42 +140,42 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
providers: {
google: {
name: 'google',
enabled: allowedAuthProviders.value.includes(AuthProvider.Google),
enabled: allowedAuthProviders.value.includes('GOOGLE'),
fields: {
client_id: getFieldValue(InfraConfigEnum.GoogleClientId),
client_secret: getFieldValue(InfraConfigEnum.GoogleClientSecret),
callback_url: getFieldValue(InfraConfigEnum.GoogleCallbackUrl),
scope: getFieldValue(InfraConfigEnum.GoogleScope),
client_id: getFieldValue('GOOGLE_CLIENT_ID'),
client_secret: getFieldValue('GOOGLE_CLIENT_SECRET'),
callback_url: getFieldValue('GOOGLE_CALLBACK_URL'),
scope: getFieldValue('GOOGLE_SCOPE'),
},
},
github: {
name: 'github',
enabled: allowedAuthProviders.value.includes(AuthProvider.Github),
enabled: allowedAuthProviders.value.includes('GITHUB'),
fields: {
client_id: getFieldValue(InfraConfigEnum.GithubClientId),
client_secret: getFieldValue(InfraConfigEnum.GithubClientSecret),
callback_url: getFieldValue(InfraConfigEnum.GoogleCallbackUrl),
scope: getFieldValue(InfraConfigEnum.GithubScope),
client_id: getFieldValue('GITHUB_CLIENT_ID'),
client_secret: getFieldValue('GITHUB_CLIENT_SECRET'),
callback_url: getFieldValue('GITHUB_CALLBACK_URL'),
scope: getFieldValue('GITHUB_SCOPE'),
},
},
microsoft: {
name: 'microsoft',
enabled: allowedAuthProviders.value.includes(AuthProvider.Microsoft),
enabled: allowedAuthProviders.value.includes('MICROSOFT'),
fields: {
client_id: getFieldValue(InfraConfigEnum.MicrosoftClientId),
client_secret: getFieldValue(InfraConfigEnum.MicrosoftClientSecret),
callback_url: getFieldValue(InfraConfigEnum.MicrosoftCallbackUrl),
scope: getFieldValue(InfraConfigEnum.MicrosoftScope),
tenant: getFieldValue(InfraConfigEnum.MicrosoftTenant),
client_id: getFieldValue('MICROSOFT_CLIENT_ID'),
client_secret: getFieldValue('MICROSOFT_CLIENT_SECRET'),
callback_url: getFieldValue('MICROSOFT_CALLBACK_URL'),
scope: getFieldValue('MICROSOFT_SCOPE'),
tenant: getFieldValue('MICROSOFT_TENANT'),
},
},
},
mailConfigs: {
name: 'email',
enabled: allowedAuthProviders.value.includes(AuthProvider.Email),
enabled: allowedAuthProviders.value.includes('EMAIL'),
fields: {
mailer_smtp_url: getFieldValue(InfraConfigEnum.MailerSmtpUrl),
mailer_from_address: getFieldValue(InfraConfigEnum.MailerAddressFrom),
mailer_smtp_url: getFieldValue('MAILER_SMTP_URL'),
mailer_from_address: getFieldValue('MAILER_ADDRESS_FROM'),
},
},
dataSharingConfigs: {
@@ -132,13 +191,138 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
workingConfigs.value = cloneDeep(currentConfigs.value);
});
/*
Check if any of the config fields are empty
*/
// Transforming the working configs back into the format required by the mutations
const updatedInfraConfigs = computed(() => {
let config: UpdatedConfigs[] = [
{
name: '',
value: '',
},
];
if (updatedConfigs?.providers.google.enabled) {
config.push(
{
name: 'GOOGLE_CLIENT_ID',
value: updatedConfigs?.providers.google.fields.client_id ?? '',
},
{
name: 'GOOGLE_CLIENT_SECRET',
value: updatedConfigs?.providers.google.fields.client_secret ?? '',
},
{
name: 'GOOGLE_CALLBACK_URL',
value: updatedConfigs?.providers.google.fields.callback_url ?? '',
},
{
name: 'GOOGLE_SCOPE',
value: updatedConfigs?.providers.google.fields.scope ?? '',
}
);
} else {
config = config.filter(
(item) =>
item.name !== 'GOOGLE_CLIENT_ID' &&
item.name !== 'GOOGLE_CLIENT_SECRET' &&
item.name !== 'GOOGLE_CALLBACK_URL' &&
item.name !== 'GOOGLE_SCOPE'
);
}
if (updatedConfigs?.providers.microsoft.enabled) {
config.push(
{
name: 'MICROSOFT_CLIENT_ID',
value: updatedConfigs?.providers.microsoft.fields.client_id ?? '',
},
{
name: 'MICROSOFT_CLIENT_SECRET',
value: updatedConfigs?.providers.microsoft.fields.client_secret ?? '',
},
{
name: 'MICROSOFT_CALLBACK_URL',
value: updatedConfigs?.providers.microsoft.fields.callback_url ?? '',
},
{
name: 'MICROSOFT_SCOPE',
value: updatedConfigs?.providers.microsoft.fields.scope ?? '',
},
{
name: 'MICROSOFT_TENANT',
value: updatedConfigs?.providers.microsoft.fields.tenant ?? '',
}
);
} else {
config = config.filter(
(item) =>
item.name !== 'MICROSOFT_CLIENT_ID' &&
item.name !== 'MICROSOFT_CLIENT_SECRET' &&
item.name !== 'MICROSOFT_CALLBACK_URL' &&
item.name !== 'MICROSOFT_SCOPE' &&
item.name !== 'MICROSOFT_TENANT'
);
}
if (updatedConfigs?.providers.github.enabled) {
config.push(
{
name: 'GITHUB_CLIENT_ID',
value: updatedConfigs?.providers.github.fields.client_id ?? '',
},
{
name: 'GITHUB_CLIENT_SECRET',
value: updatedConfigs?.providers.github.fields.client_secret ?? '',
},
{
name: 'GITHUB_CALLBACK_URL',
value: updatedConfigs?.providers.github.fields.callback_url ?? '',
},
{
name: 'GITHUB_SCOPE',
value: updatedConfigs?.providers.github.fields.scope ?? '',
}
);
} else {
config = config.filter(
(item) =>
item.name !== 'GITHUB_CLIENT_ID' &&
item.name !== 'GITHUB_CLIENT_SECRET' &&
item.name !== 'GITHUB_CALLBACK_URL' &&
item.name !== 'GITHUB_SCOPE'
);
}
if (updatedConfigs?.mailConfigs.enabled) {
config.push(
{
name: 'MAILER_SMTP_URL',
value: updatedConfigs?.mailConfigs.fields.mailer_smtp_url ?? '',
},
{
name: 'MAILER_ADDRESS_FROM',
value: updatedConfigs?.mailConfigs.fields.mailer_from_address ?? '',
}
);
} else {
config = config.filter(
(item) =>
item.name !== 'MAILER_SMTP_URL' && item.name !== 'MAILER_ADDRESS_FROM'
);
}
config = config.filter((item) => item.name !== '');
return config;
});
// Checking if any of the config fields are empty
const isFieldEmpty = (field: string) => field.trim() === '';
const AreAnyConfigFieldsEmpty = (config: ServerConfigs): boolean => {
type ConfigSection = {
enabled: boolean;
fields: Record<string, string>;
};
const AreAnyConfigFieldsEmpty = (config: Config): boolean => {
const sections: Array<ConfigSection> = [
config.providers.github,
config.providers.google,
@@ -153,44 +337,28 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
};
// Transforming the working configs back into the format required by the mutations
const transformInfraConfigs = () => {
const updatedWorkingConfigs: ConfigTransform[] = [
const updatedAllowedAuthProviders = computed(() => {
return [
{
config: GOOGLE_CONFIGS,
enabled: updatedConfigs?.providers.google.enabled,
fields: updatedConfigs?.providers.google.fields,
provider: 'GOOGLE',
status: updatedConfigs?.providers.google.enabled ? 'ENABLE' : 'DISABLE',
},
{
config: GITHUB_CONFIGS,
enabled: updatedConfigs?.providers.github.enabled,
fields: updatedConfigs?.providers.github.fields,
provider: 'MICROSOFT',
status: updatedConfigs?.providers.microsoft.enabled
? 'ENABLE'
: 'DISABLE',
},
{
config: MICROSOFT_CONFIGS,
enabled: updatedConfigs?.providers.microsoft.enabled,
fields: updatedConfigs?.providers.microsoft.fields,
provider: 'GITHUB',
status: updatedConfigs?.providers.github.enabled ? 'ENABLE' : 'DISABLE',
},
{
config: MAIL_CONFIGS,
enabled: updatedConfigs?.mailConfigs.enabled,
fields: updatedConfigs?.mailConfigs.fields,
provider: 'EMAIL',
status: updatedConfigs?.mailConfigs.enabled ? 'ENABLE' : 'DISABLE',
},
];
const transformedConfigs: UpdatedConfigs[] = [];
updatedWorkingConfigs.forEach(({ config, enabled, fields }) => {
config.forEach(({ name, key }) => {
if (enabled && fields) {
const value =
typeof fields === 'string' ? fields : String(fields[key]);
transformedConfigs.push({ name, value });
}
});
});
return transformedConfigs;
};
});
// Generic function to handle mutation execution and error handling
const executeMutation = async <T, V>(
@@ -211,59 +379,27 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
// Updating the auth provider configurations
const updateAuthProvider = (
updateProviderStatus: UseMutationResponse<EnableAndDisableSsoMutation>
) => {
const updatedAllowedAuthProviders: EnableAndDisableSsoArgs[] = [
{
provider: AuthProvider.Google,
status: updatedConfigs?.providers.google.enabled
? ServiceStatus.Enable
: ServiceStatus.Disable,
},
{
provider: AuthProvider.Microsoft,
status: updatedConfigs?.providers.microsoft.enabled
? ServiceStatus.Enable
: ServiceStatus.Disable,
},
{
provider: AuthProvider.Github,
status: updatedConfigs?.providers.github.enabled
? ServiceStatus.Enable
: ServiceStatus.Disable,
},
{
provider: AuthProvider.Email,
status: updatedConfigs?.mailConfigs.enabled
? ServiceStatus.Enable
: ServiceStatus.Disable,
},
];
return executeMutation(
) =>
executeMutation(
updateProviderStatus,
{
providerInfo: updatedAllowedAuthProviders,
providerInfo:
updatedAllowedAuthProviders.value as EnableAndDisableSsoArgs[],
},
'configs.auth_providers.update_failure'
);
};
// Updating the infra configurations
const updateInfraConfigs = (
updateInfraConfigsMutation: UseMutationResponse<UpdateInfraConfigsMutation>
) => {
const infraConfigs: InfraConfigArgs[] = updatedConfigs
? transformInfraConfigs()
: [];
return executeMutation(
) =>
executeMutation(
updateInfraConfigsMutation,
{
infraConfigs,
infraConfigs: updatedInfraConfigs.value as InfraConfigArgs[],
},
'configs.update_failure'
'configs.mail_configs.update_failure'
);
};
// Resetting the infra configurations
const resetInfraConfigs = (
@@ -275,6 +411,7 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
'configs.reset.failure'
);
// Updating the data sharing configurations
const updateDataSharingConfigs = (
toggleDataSharingMutation: UseMutationResponse<ToggleAnalyticsCollectionMutation>
) =>
@@ -282,8 +419,8 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
toggleDataSharingMutation,
{
status: updatedConfigs?.dataSharingConfigs.enabled
? ServiceStatus.Enable
: ServiceStatus.Disable,
? 'ENABLE'
: 'DISABLE',
},
'configs.data_sharing.update_failure'
);
@@ -291,6 +428,8 @@ export function useConfigHandler(updatedConfigs?: ServerConfigs) {
return {
currentConfigs,
workingConfigs,
updatedInfraConfigs,
updatedAllowedAuthProviders,
updateAuthProvider,
updateDataSharingConfigs,
updateInfraConfigs,

View File

@@ -1,160 +0,0 @@
import { InfraConfigEnum } from './backend/graphql';
export type SsoAuthProviders = 'google' | 'microsoft' | 'github';
export type ServerConfigs = {
providers: {
google: {
name: SsoAuthProviders;
enabled: boolean;
fields: {
client_id: string;
client_secret: string;
callback_url: string;
scope: string;
};
};
github: {
name: SsoAuthProviders;
enabled: boolean;
fields: {
client_id: string;
client_secret: string;
callback_url: string;
scope: string;
};
};
microsoft: {
name: SsoAuthProviders;
enabled: boolean;
fields: {
client_id: string;
client_secret: string;
callback_url: string;
scope: string;
tenant: string;
};
};
};
mailConfigs: {
name: string;
enabled: boolean;
fields: {
mailer_smtp_url: string;
mailer_from_address: string;
};
};
dataSharingConfigs: {
name: string;
enabled: boolean;
};
};
export type UpdatedConfigs = {
name: InfraConfigEnum;
value: string;
};
export type ConfigTransform = {
config: Config[];
enabled?: boolean;
fields?: Record<string, string | boolean> | string;
};
export type ConfigSection = {
enabled: boolean;
fields: Record<string, string>;
};
export type Config = {
name: InfraConfigEnum;
key: string;
};
export const GOOGLE_CONFIGS: Config[] = [
{
name: InfraConfigEnum.GoogleClientId,
key: 'client_id',
},
{
name: InfraConfigEnum.GoogleClientSecret,
key: 'client_secret',
},
{
name: InfraConfigEnum.GoogleCallbackUrl,
key: 'callback_url',
},
{
name: InfraConfigEnum.GoogleScope,
key: 'scope',
},
];
export const MICROSOFT_CONFIGS: Config[] = [
{
name: InfraConfigEnum.MicrosoftClientId,
key: 'client_id',
},
{
name: InfraConfigEnum.MicrosoftClientSecret,
key: 'client_secret',
},
{
name: InfraConfigEnum.MicrosoftCallbackUrl,
key: 'callback_url',
},
{
name: InfraConfigEnum.MicrosoftScope,
key: 'scope',
},
{
name: InfraConfigEnum.MicrosoftTenant,
key: 'tenant',
},
];
export const GITHUB_CONFIGS: Config[] = [
{
name: InfraConfigEnum.GithubClientId,
key: 'client_id',
},
{
name: InfraConfigEnum.GithubClientSecret,
key: 'client_secret',
},
{
name: InfraConfigEnum.GithubCallbackUrl,
key: 'callback_url',
},
{
name: InfraConfigEnum.GithubScope,
key: 'scope',
},
];
export const MAIL_CONFIGS: Config[] = [
{
name: InfraConfigEnum.MailerSmtpUrl,
key: 'mailer_smtp_url',
},
{
name: InfraConfigEnum.MailerAddressFrom,
key: 'mailer_from_address',
},
];
const DATA_SHARING_CONFIGS: Omit<Config, 'key'>[] = [
{
name: InfraConfigEnum.AllowAnalyticsCollection,
},
];
export const ALL_CONFIGS = [
GOOGLE_CONFIGS,
MICROSOFT_CONFIGS,
GITHUB_CONFIGS,
MAIL_CONFIGS,
DATA_SHARING_CONFIGS,
];

View File

@@ -208,14 +208,13 @@ const deleteUserMutation = async (id: string | null) => {
if (result.error) {
toast.error(t('state.delete_user_failure'));
} else {
const deletedUser = result.data?.removeUsersByAdmin || [];
handleUserDeletion(deletedUser);
const deletedUsers = result.data?.removeUsersByAdmin || [];
const { isDeleted } = deletedUser[0];
if (isDeleted) router.push('/users');
handleUserDeletion(deletedUsers);
}
confirmDeletion.value = false;
deleteUserUID.value = null;
!result.error && router.push('/users');
};
</script>

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