Compare commits
36 Commits
fix/switch
...
fix/db-url
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74933031c6 | ||
|
|
1cce117b0a | ||
|
|
abc7b4b6f3 | ||
|
|
05e32ef9e4 | ||
|
|
f0a1fc319c | ||
|
|
385cabc6aa | ||
|
|
397b26a9f3 | ||
|
|
9a40058329 | ||
|
|
7ec2380ed5 | ||
|
|
3d4825305d | ||
|
|
26e564288b | ||
|
|
385a587cfd | ||
|
|
215df02783 | ||
|
|
7c7ed68b20 | ||
|
|
c910a0314a | ||
|
|
ddaec1b9ac | ||
|
|
9dbdef9286 | ||
|
|
e77eef1532 | ||
|
|
1fe0b8861d | ||
|
|
aeb9172144 | ||
|
|
1b413e2f47 | ||
|
|
d6c8400116 | ||
|
|
4a0205e622 | ||
|
|
c2520006ac | ||
|
|
99817fd8bd | ||
|
|
3f35fedd9d | ||
|
|
b7c2d13992 | ||
|
|
a6426587fb | ||
|
|
5f68356278 | ||
|
|
08f61e7408 | ||
|
|
9beda15f00 | ||
|
|
09d1663f81 | ||
|
|
f43b6e7cff | ||
|
|
6581eb4fd1 | ||
|
|
caedfe5c1e | ||
|
|
f6a234aaf9 |
@@ -51,7 +51,7 @@ VITE_ADMIN_URL=http://localhost:3100
|
||||
|
||||
# Backend URLs
|
||||
VITE_BACKEND_GQL_URL=http://localhost:3170/graphql
|
||||
VITE_BACKEND_WS_URL=wss://localhost:3170/graphql
|
||||
VITE_BACKEND_WS_URL=ws://localhost:3170/graphql
|
||||
VITE_BACKEND_API_URL=http://localhost:3170/v1
|
||||
|
||||
# Terms Of Service And Privacy Policy Links (Optional)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"dev": "pnpm -r do-dev",
|
||||
"gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl",
|
||||
"generate": "pnpm -r do-build-prod",
|
||||
"start": "http-server packages/hoppscotch-web/dist -p 3000",
|
||||
"start": "http-server packages/hoppscotch-selfhost-web/dist -p 3000",
|
||||
"lint": "pnpm -r do-lint",
|
||||
"typecheck": "pnpm -r do-typecheck",
|
||||
"lintfix": "pnpm -r do-lintfix",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.4.1",
|
||||
"version": "2023.4.4",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -232,7 +232,7 @@ export class AuthService {
|
||||
template: 'code-your-own',
|
||||
variables: {
|
||||
inviteeEmail: email,
|
||||
magicLink: `${url}/magic-link?token=${generatedTokens.token}`,
|
||||
magicLink: `${url}/enter?token=${generatedTokens.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export const authCookieHandler = (
|
||||
});
|
||||
|
||||
if (!redirect) {
|
||||
res.status(HttpStatus.OK).send();
|
||||
return res.status(HttpStatus.OK).send();
|
||||
}
|
||||
|
||||
// check to see if redirectUrl is a whitelisted url
|
||||
@@ -72,7 +72,7 @@ export const authCookieHandler = (
|
||||
// if it is not redirect by default to REDIRECT_URL
|
||||
redirectUrl = process.env.REDIRECT_URL;
|
||||
|
||||
res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,6 @@ import { emitGQLSchemaFile } from './gql-schema';
|
||||
async function bootstrap() {
|
||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||
console.log(`Port: ${process.env.PORT}`);
|
||||
console.log(`Database: ${process.env.DATABASE_URL}`);
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ export class TeamInvitationService {
|
||||
template: 'team-invitation',
|
||||
variables: {
|
||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||
action_url: `https://hoppscotch.io/join-team?id=${invitation.id}`,
|
||||
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${invitation.id}`,
|
||||
invite_team_name: team.name,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -360,13 +360,15 @@ describe('UserHistoryService', () => {
|
||||
});
|
||||
describe('removeRequestFromHistory', () => {
|
||||
test('Should resolve right and delete request from users history', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.delete.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn: executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -376,7 +378,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn: executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -384,7 +386,7 @@ describe('UserHistoryService', () => {
|
||||
await userHistoryService.removeRequestFromHistory('abc', '1'),
|
||||
).toEqualRight(userHistory);
|
||||
});
|
||||
test('Should resolve left and error out when req id is invalid ', async () => {
|
||||
test('Should resolve left and error out when req id is invalid', async () => {
|
||||
mockPrisma.userHistory.delete.mockResolvedValueOnce(null);
|
||||
|
||||
return expect(
|
||||
|
||||
@@ -207,16 +207,19 @@
|
||||
:root.light {
|
||||
@include light-theme;
|
||||
@include light-editor-theme;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@include dark-theme;
|
||||
@include dark-editor-theme;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root.black {
|
||||
@include black-theme;
|
||||
@include black-editor-theme;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-accent="blue"] {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"edit": "编辑",
|
||||
"filter": "过滤",
|
||||
"go_back": "返回",
|
||||
"go_forward": "Go forward",
|
||||
"go_forward": "前进",
|
||||
"group_by": "分组方式",
|
||||
"label": "标签",
|
||||
"learn_more": "了解更多",
|
||||
@@ -40,9 +40,9 @@
|
||||
"start": "开始",
|
||||
"starting": "正在开始",
|
||||
"stop": "停止",
|
||||
"to_close": "以关闭",
|
||||
"to_navigate": "以定位",
|
||||
"to_select": "以选择",
|
||||
"to_close": "关闭",
|
||||
"to_navigate": "定位",
|
||||
"to_select": "选择",
|
||||
"turn_off": "关闭",
|
||||
"turn_on": "开启",
|
||||
"undo": "撤消",
|
||||
@@ -118,16 +118,16 @@
|
||||
},
|
||||
"collection": {
|
||||
"created": "集合已创建",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"different_parent": "不能用不同的父类来重新排序集合",
|
||||
"edit": "编辑集合",
|
||||
"invalid_name": "请提供有效的集合名称",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"invalid_root_move": "该集合已经在根级了",
|
||||
"moved": "移动完成",
|
||||
"my_collections": "我的集合",
|
||||
"name": "我的新集合",
|
||||
"name_length_insufficient": "集合名字至少需要 3 个字符",
|
||||
"new": "新建集合",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"order_changed": "集合顺序已更新",
|
||||
"renamed": "集合已更名",
|
||||
"request_in_use": "请求正在使用中",
|
||||
"save_as": "另存为",
|
||||
@@ -147,7 +147,7 @@
|
||||
"remove_team": "你确定要删除该团队吗?",
|
||||
"remove_telemetry": "你确定要退出遥测服务吗?",
|
||||
"request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"save_unsaved_tab": "你想保存在此标签页中所作的修改吗?",
|
||||
"sync": "您确定要同步该工作区吗?"
|
||||
},
|
||||
"count": {
|
||||
@@ -177,7 +177,7 @@
|
||||
"members": "团队为空",
|
||||
"parameters": "该请求没有任何参数",
|
||||
"pending_invites": "此团队无待办邀请",
|
||||
"profile": "登录以查看你的个人档案",
|
||||
"profile": "登录以查看你的个人资料",
|
||||
"protocols": "协议为空",
|
||||
"schema": "连接至 GraphQL 端点",
|
||||
"shortcodes": "Shortcodes 为空",
|
||||
@@ -209,7 +209,7 @@
|
||||
"browser_support_sse": "该浏览器似乎不支持 SSE。",
|
||||
"check_console_details": "检查控制台日志以获悉详情",
|
||||
"curl_invalid_format": "cURL 格式不正确",
|
||||
"danger_zone": "Danger zone",
|
||||
"danger_zone": "危险区域",
|
||||
"delete_account": "您的帐号目前为这些团队的拥有者:",
|
||||
"delete_account_description": "您在删除帐号前必须先将您自己从团队中移除、转移拥有权,或是删除团队。",
|
||||
"empty_req_name": "空请求名称",
|
||||
@@ -219,7 +219,7 @@
|
||||
"incorrect_email": "电子邮箱错误",
|
||||
"invalid_link": "无效链接",
|
||||
"invalid_link_description": "你点击的链接无效或已过期。",
|
||||
"json_parsing_failed": "Invalid JSON",
|
||||
"json_parsing_failed": "不合法的 JSON",
|
||||
"json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试",
|
||||
"network_error": "好像发生了网络错误,请重试。",
|
||||
"network_fail": "无法发送请求",
|
||||
@@ -316,14 +316,14 @@
|
||||
"zen_mode": "ZEN 模式"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"close_unsaved_tab": "有未保存的变更",
|
||||
"collections": "集合",
|
||||
"confirm": "确认",
|
||||
"edit_request": "编辑请求",
|
||||
"import_export": "导入/导出"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "您已经订阅了此主題。",
|
||||
"already_subscribed": "您已经订阅了此主题。",
|
||||
"clean_session": "清除会话",
|
||||
"clear_input": "清除输入",
|
||||
"clear_input_on_send": "发送后清除输入",
|
||||
@@ -355,7 +355,7 @@
|
||||
"navigation": {
|
||||
"doc": "文档",
|
||||
"graphql": "GraphQL",
|
||||
"profile": "个人档案",
|
||||
"profile": "个人资料",
|
||||
"realtime": "实时",
|
||||
"rest": "REST",
|
||||
"settings": "设置"
|
||||
@@ -377,7 +377,7 @@
|
||||
"owner_description": "所有者可以添加、编辑和删除请求、集合及团队成员。",
|
||||
"roles": "角色",
|
||||
"roles_description": "角色用以控制共享集合的访问权限。",
|
||||
"updated": "档案已更新",
|
||||
"updated": "已更新",
|
||||
"viewer": "查看者",
|
||||
"viewer_description": "查看者只可查看与使用请求。"
|
||||
},
|
||||
@@ -396,8 +396,8 @@
|
||||
"text": "文字"
|
||||
},
|
||||
"copy_link": "复制链接",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"different_collection": "不能对来自不同集合的请求进行重新排序",
|
||||
"duplicated": "重复的请求",
|
||||
"duration": "持续时间",
|
||||
"enter_curl": "输入 cURL",
|
||||
"generate_code": "生成代码",
|
||||
@@ -405,10 +405,10 @@
|
||||
"header_list": "请求头列表",
|
||||
"invalid_name": "请提供请求名称",
|
||||
"method": "方法",
|
||||
"moved": "Request moved",
|
||||
"moved": "请求移动完成",
|
||||
"name": "请求名称",
|
||||
"new": "新请求",
|
||||
"order_changed": "Request Order Updated",
|
||||
"order_changed": "请求顺序更新完成",
|
||||
"override": "覆盖",
|
||||
"override_help": "设置 <kbd>Content-Type</kbd> 头",
|
||||
"overriden": "覆盖",
|
||||
@@ -479,10 +479,10 @@
|
||||
"language": "语言",
|
||||
"light_mode": "亮色",
|
||||
"official_proxy_hosting": "官方代理由 Hoppscotch 托管。",
|
||||
"profile": "个人档案",
|
||||
"profile_description": "更新你的档案详情",
|
||||
"profile": "个人资料",
|
||||
"profile_description": "更新你的资料",
|
||||
"profile_email": "电子邮箱地址",
|
||||
"profile_name": "档案名称",
|
||||
"profile_name": "名称",
|
||||
"proxy": "网络代理",
|
||||
"proxy_url": "代理网址",
|
||||
"proxy_use_toggle": "使用代理中间件发送请求",
|
||||
@@ -532,7 +532,7 @@
|
||||
"documentation": "前往文档页面",
|
||||
"forward": "前往下一页面",
|
||||
"graphql": "前往 GraphQL 页面",
|
||||
"profile": "前往个人档案页面",
|
||||
"profile": "前往个人资料页面",
|
||||
"realtime": "前往实时页面",
|
||||
"rest": "前往 REST 页面",
|
||||
"settings": "前往设置页面",
|
||||
@@ -574,7 +574,7 @@
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "通讯",
|
||||
"connection_not_authorized": "此SocketIO连接未使用任何验证。",
|
||||
"connection_not_authorized": "此 SocketIO 连接未使用任何验证。",
|
||||
"event_name": "事件名称",
|
||||
"events": "事件",
|
||||
"log": "日志",
|
||||
@@ -614,12 +614,12 @@
|
||||
"none": "无",
|
||||
"nothing_found": "没有找到",
|
||||
"published_error": "将信息:{topic}发布至主题:{message}时发生错误",
|
||||
"published_message": "已将此信息:{message}发布至主题:{topic}",
|
||||
"published_message": "已将此信息:{message} 发布至主题:{topic}",
|
||||
"reconnection_error": "重连失败",
|
||||
"subscribed_failed": "无法订阅此主題:{topic}",
|
||||
"subscribed_success": "成功订阅此主題:{topic}",
|
||||
"unsubscribed_failed": "无法取消订阅此主題:{topic}",
|
||||
"unsubscribed_success": "成功取消订阅此主題:{topic}",
|
||||
"subscribed_failed": "无法订阅此主题:{topic}",
|
||||
"subscribed_success": "成功订阅此主题:{topic}",
|
||||
"unsubscribed_failed": "无法取消订阅此主题:{topic}",
|
||||
"unsubscribed_success": "成功取消订阅此主题:{topic}",
|
||||
"waiting_send_request": "等待发送请求"
|
||||
},
|
||||
"support": {
|
||||
@@ -639,7 +639,7 @@
|
||||
"body": "请求体",
|
||||
"collections": "集合",
|
||||
"documentation": "帮助文档",
|
||||
"environments": "Environments",
|
||||
"environments": "环境",
|
||||
"headers": "请求头",
|
||||
"history": "历史记录",
|
||||
"mqtt": "MQTT",
|
||||
@@ -664,7 +664,7 @@
|
||||
"email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。",
|
||||
"exit": "退出团队",
|
||||
"exit_disabled": "团队所有者无法退出团队",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_coll_id": "无效的集合 ID",
|
||||
"invalid_email_format": "电子邮箱格式无效",
|
||||
"invalid_id": "无效的团队 ID,请联系你的团队者。",
|
||||
"invalid_invite_link": "无效的邀请链接",
|
||||
@@ -688,7 +688,7 @@
|
||||
"member_removed": "用户已移除",
|
||||
"member_role_updated": "用户角色已更新",
|
||||
"members": "成员",
|
||||
"more_members": "+{count} more",
|
||||
"more_members": "+{count} 更多",
|
||||
"name_length_insufficient": "团队名称至少为 6 个字符",
|
||||
"name_updated": "团队名称已更新",
|
||||
"new": "新团队",
|
||||
@@ -696,13 +696,13 @@
|
||||
"new_name": "我的新团队",
|
||||
"no_access": "你没有编辑集合的权限",
|
||||
"no_invite_found": "未找到邀请。请联系你的团队者。",
|
||||
"no_request_found": "Request not found.",
|
||||
"no_request_found": "请求不存在",
|
||||
"not_found": "没有找到团队,请联系您的团队所有者。",
|
||||
"not_valid_viewer": "你不是有效的查看者。请联系你的团队者。",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"parent_coll_move": "不能将集合移动到一个子集合",
|
||||
"pending_invites": "待办邀请",
|
||||
"permissions": "权限",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"same_target_destination": "目标相同",
|
||||
"saved": "团队已保存",
|
||||
"select_a_team": "选择团队",
|
||||
"title": "团队",
|
||||
@@ -732,9 +732,9 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "切换工作空间",
|
||||
"personal": "我的工作空间",
|
||||
"team": "团队工作空间",
|
||||
"title": "工作空间"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Response Body",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Headers",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Time",
|
||||
"title": "Response",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "waiting for connection",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
{
|
||||
"action": {
|
||||
"autoscroll": "Autoscroll",
|
||||
"autoscroll": "Desplazamiento automático",
|
||||
"cancel": "Cancelar",
|
||||
"choose_file": "Seleccionar archivo",
|
||||
"clear": "Limpiar",
|
||||
"clear_all": "Limpiar todo",
|
||||
"close": "Cerrar",
|
||||
"connect": "Conectar",
|
||||
"connecting": "Connecting",
|
||||
"connecting": "Conectando",
|
||||
"copy": "Copiar",
|
||||
"delete": "Borrar",
|
||||
"disconnect": "Desconectar",
|
||||
"dismiss": "Descartar",
|
||||
"dont_save": "Don't save",
|
||||
"dont_save": "No guardar",
|
||||
"download_file": "Descargar archivo",
|
||||
"drag_to_reorder": "Arrastrar para reordenar",
|
||||
"duplicate": "Duplicar",
|
||||
"edit": "Editar",
|
||||
"filter": "Filter",
|
||||
"filter": "Filtrar",
|
||||
"go_back": "Volver",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"go_forward": "Adelante",
|
||||
"group_by": "Agrupar por",
|
||||
"label": "Etiqueta",
|
||||
"learn_more": "Aprender más",
|
||||
"less": "Menos",
|
||||
"more": "Más",
|
||||
"new": "Nuevo",
|
||||
"no": "No",
|
||||
"open_workspace": "Open workspace",
|
||||
"open_workspace": "Abrir espacio de trabajo",
|
||||
"paste": "Pegar",
|
||||
"prettify": "Embellecer",
|
||||
"remove": "Eliminar",
|
||||
"restore": "Restaurar",
|
||||
"save": "Guardar",
|
||||
"scroll_to_bottom": "Scroll to bottom",
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"scroll_to_bottom": "Desplazar hacia abajo",
|
||||
"scroll_to_top": "Desplazar hacia arriba",
|
||||
"search": "Buscar",
|
||||
"send": "Enviar",
|
||||
"start": "Comenzar",
|
||||
"starting": "Starting",
|
||||
"starting": "Iniciando",
|
||||
"stop": "Detener",
|
||||
"to_close": "para cerrar",
|
||||
"to_navigate": "para navegar",
|
||||
@@ -56,16 +56,16 @@
|
||||
"chat_with_us": "Habla con nosotros",
|
||||
"contact_us": "Contáctanos",
|
||||
"copy": "Copiar",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
"copy_user_id": "Copiar token de autenticación de usuario",
|
||||
"developer_option": "Opciones para desarrolladores",
|
||||
"developer_option_description": "Herramientas para desarrolladores que ayudan en el desarrollo y mantenimiento de Hoppscotch.",
|
||||
"discord": "Discord",
|
||||
"documentation": "Documentación",
|
||||
"github": "GitHub",
|
||||
"help": "Ayuda y comentarios",
|
||||
"home": "Inicio",
|
||||
"invite": "Invitar",
|
||||
"invite_description": "En Hoppscotch, diseñamos una interfaz simple e intuitiva para crear y administrar sus APIs. Hoppscotch es una herramienta que le ayuda a crear, probar, documentar y compartir sus APIs.",
|
||||
"invite_description": "En Hoppscotch, diseñamos una interfaz simple e intuitiva para crear y administrar tus APIs. Hoppscotch es una herramienta que le ayuda a crear, probar, documentar y compartir tus APIs.",
|
||||
"invite_your_friends": "Invita a tus amigos",
|
||||
"join_discord_community": "Únete a nuestra comunidad Discord",
|
||||
"keyboard_shortcuts": "Atajos de teclado",
|
||||
@@ -79,7 +79,7 @@
|
||||
"shortcuts": "Atajos",
|
||||
"spotlight": "Destacar",
|
||||
"status": "Estado",
|
||||
"status_description": "Check the status of the website",
|
||||
"status_description": "Comprobar el estado del sitio web",
|
||||
"terms_and_privacy": "Términos y privacidad",
|
||||
"twitter": "Twitter",
|
||||
"type_a_command_search": "Escribe un comando o buscar algo…",
|
||||
@@ -118,18 +118,18 @@
|
||||
},
|
||||
"collection": {
|
||||
"created": "Colección creada",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"different_parent": "No se puede reordenar la colección con un padre diferente",
|
||||
"edit": "Editar colección",
|
||||
"invalid_name": "Proporciona un nombre válido para la colección.",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"invalid_root_move": "La colección ya está en la raíz",
|
||||
"moved": "Movido con éxito",
|
||||
"my_collections": "Mis colecciones",
|
||||
"name": "Mi nueva colección",
|
||||
"name_length_insufficient": "El nombre de la colección debe tener al menos 3 caracteres",
|
||||
"new": "Nueva colección",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"order_changed": "Orden de colección actualizada",
|
||||
"renamed": "Colección renombrada",
|
||||
"request_in_use": "Petición en uso",
|
||||
"request_in_use": "Solicitud en uso",
|
||||
"save_as": "Guardar como",
|
||||
"select": "Seleccionar colección",
|
||||
"select_location": "Seleccionar ubicación",
|
||||
@@ -138,17 +138,17 @@
|
||||
},
|
||||
"confirm": {
|
||||
"exit_team": "¿Estás seguro de que quieres dejar este equipo?",
|
||||
"logout": "¿Está seguro de que desea cerrar la sesión?",
|
||||
"remove_collection": "¿Está seguro de que desea eliminar esta colección de forma permanente?",
|
||||
"remove_environment": "¿Está seguro de que desea eliminar este entorno de forma permanente?",
|
||||
"remove_folder": "¿Está seguro de que desea eliminar esta carpeta de forma permanente?",
|
||||
"remove_history": "¿Está seguro de que desea eliminar todo el historial de forma permanente?",
|
||||
"remove_request": "¿Está seguro de que desea eliminar esta petición de forma permanente?",
|
||||
"remove_team": "¿Está seguro de que desea eliminar este equipo?",
|
||||
"remove_telemetry": "¿Está seguro de que desea darse de baja de la telemetría?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"sync": "¿Está seguro de que desea sincronizar este espacio de trabajo?"
|
||||
"logout": "¿Estás seguro de que deseas cerrar la sesión?",
|
||||
"remove_collection": "¿Estás seguro de que deseas eliminar esta colección de forma permanente?",
|
||||
"remove_environment": "¿Estás seguro de que deseas eliminar este entorno de forma permanente?",
|
||||
"remove_folder": "¿Estás seguro de que deseas eliminar esta carpeta de forma permanente?",
|
||||
"remove_history": "¿Estás seguro de que deseas eliminar todo el historial de forma permanente?",
|
||||
"remove_request": "¿Estás seguro de que deseas eliminar esta solicitud de forma permanente?",
|
||||
"remove_team": "¿Estás seguro de que deseas eliminar este equipo?",
|
||||
"remove_telemetry": "¿Estás seguro de que deseas darse de baja de la telemetría?",
|
||||
"request_change": "¿Estás seguro de que deseas descartar la solicitud actual, los cambios no guardados se perderán.",
|
||||
"save_unsaved_tab": "¿Deseas guardar los cambios realizados en esta pestaña?",
|
||||
"sync": "¿Estás seguro de que deseas sincronizar este espacio de trabajo?"
|
||||
},
|
||||
"count": {
|
||||
"header": "Encabezado {count}",
|
||||
@@ -163,28 +163,28 @@
|
||||
"generate_message": "Importar cualquier colección de Hoppscotch para generar documentación de la API sobre la marcha."
|
||||
},
|
||||
"empty": {
|
||||
"authorization": "Esta petición no utiliza ninguna autorización.",
|
||||
"body": "Esta petición no tiene cuerpo",
|
||||
"authorization": "Esta solicitud no utiliza ninguna autorización.",
|
||||
"body": "Esta solicitud no tiene cuerpo",
|
||||
"collection": "La colección está vacía",
|
||||
"collections": "Las colecciones están vacías",
|
||||
"documentation": "Conectarse a un punto final de GraphQL para ver la documentación",
|
||||
"endpoint": "El punto final no puede estar vacío",
|
||||
"environments": "Los entornos están vacíos",
|
||||
"folder": "La carpeta está vacía",
|
||||
"headers": "Esta petición no tiene encabezados",
|
||||
"headers": "Esta solicitud no tiene encabezados",
|
||||
"history": "El historial está vacío",
|
||||
"invites": "La lista de invitados está vacía",
|
||||
"members": "El equipo está vacío",
|
||||
"parameters": "Esta petición no tiene ningún parámetro",
|
||||
"parameters": "Esta solicitud no tiene ningún parámetro",
|
||||
"pending_invites": "No hay invitaciones pendientes para este equipo",
|
||||
"profile": "Iniciar sesión para ver tu perfil",
|
||||
"protocols": "Los protocolos están vacíos",
|
||||
"schema": "Conectarse a un punto final de GraphQL",
|
||||
"shortcodes": "Los shortcodes están vacíos",
|
||||
"shortcodes": "Aún no se han creado Shortcodes",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Nombre del equipo vacío",
|
||||
"teams": "Los equipos están vacíos",
|
||||
"tests": "No hay pruebas para esta petición"
|
||||
"tests": "No hay pruebas para esta solicitud"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "Añadir a Global",
|
||||
@@ -194,38 +194,38 @@
|
||||
"deleted": "Eliminar el entorno",
|
||||
"edit": "Editar entorno",
|
||||
"invalid_name": "Proporciona un nombre válido para el entorno.",
|
||||
"my_environments": "My Environments",
|
||||
"my_environments": "Mis entornos",
|
||||
"nested_overflow": "las variables de entorno anidadas están limitadas a 10 niveles",
|
||||
"new": "Nuevo entorno",
|
||||
"no_environment": "Sin entorno",
|
||||
"no_environment_description": "No se ha seleccionado ningún entorno. Elije qué hacer con las siguientes variables.",
|
||||
"select": "Seleccionar entorno",
|
||||
"team_environments": "Team Environments",
|
||||
"team_environments": "Entornos de trabajo en equipo",
|
||||
"title": "Entornos",
|
||||
"updated": "Actualización del entorno",
|
||||
"updated": "Entorno actualizado",
|
||||
"variable_list": "Lista de variables"
|
||||
},
|
||||
"error": {
|
||||
"browser_support_sse": "Este navegador no parece ser compatible con los eventos enviados por el servidor.",
|
||||
"check_console_details": "Consulta el registro de la consola para obtener más detalles.",
|
||||
"curl_invalid_format": "cURL no está formateado correctamente",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "Nombre de petición vacío",
|
||||
"danger_zone": "Zona de peligro",
|
||||
"delete_account": "Tu cuenta es actualmente propietaria en estos equipos:",
|
||||
"delete_account_description": "Para poder eliminar tu cuenta, debes darte de baja, transferir la propiedad o eliminar estos equipos.",
|
||||
"empty_req_name": "Nombre de solicitud vacío",
|
||||
"f12_details": "(F12 para más detalles)",
|
||||
"gql_prettify_invalid_query": "No se puede aplicar embellecedor a una consulta no válida, resuelve los errores de sintaxis de la consulta y vuelve a intentarlo",
|
||||
"incomplete_config_urls": "URLs de configuración incompletas",
|
||||
"incorrect_email": "Correo electrónico incorrecto",
|
||||
"invalid_link": "Enlace no válido",
|
||||
"invalid_link_description": "El enlace que has pulsado no es válido o ha caducado.",
|
||||
"json_parsing_failed": "Invalid JSON",
|
||||
"json_parsing_failed": "JSON no válido",
|
||||
"json_prettify_invalid_body": "No se puede aplicar embellecedor a un cuerpo inválido, resuelve errores de sintaxis json y vuelve a intentarlo",
|
||||
"network_error": "Parece que hay un error de red. Por favor, inténtalo de nuevo.",
|
||||
"network_fail": "No se pudo enviar la petición",
|
||||
"network_fail": "No se pudo enviar la solicitud",
|
||||
"no_duration": "Sin duración",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"no_results_found": "No se han encontrado coincidencias",
|
||||
"page_not_found": "No se ha podido encontrar esta página",
|
||||
"script_fail": "No se pudo ejecutar el script de solicitud previa",
|
||||
"something_went_wrong": "Algo salió mal",
|
||||
"test_script_fail": "No se ha podido ejecutar la secuencia de comandos posterior a la solicitud"
|
||||
@@ -256,7 +256,7 @@
|
||||
"subscriptions": "Suscripciones"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"time": "Tiempo",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
@@ -265,19 +265,19 @@
|
||||
"save_workspace": "Guardar mi espacio de trabajo"
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "El encabezado de autorización se generará automáticamente cuando se envía la petición.",
|
||||
"authorization": "El encabezado de autorización se generará automáticamente cuando se envía la solicitud.",
|
||||
"generate_documentation_first": "Generar la documentación primero",
|
||||
"network_fail": "No se puede acceder a la API. Comprueba tu conexión de red y vuelve a intentarlo.",
|
||||
"offline": "Parece estar desconectado. Es posible que los datos de este espacio de trabajo no estén actualizados.",
|
||||
"offline_short": "Pareces estar desconectado.",
|
||||
"post_request_tests": "Los scripts de prueba están escritos en JavaScript y se ejecutan después de recibir la respuesta.",
|
||||
"pre_request_script": "Los scripts previos a la petición están escritos en JavaScript y se ejecutan antes de que se envíe la petición.",
|
||||
"pre_request_script": "Los scripts previos a la solicitud están escritos en JavaScript y se ejecutan antes de que se envíe la solicitud.",
|
||||
"script_fail": "Parece que hay un problema técnico en el script de solicitud previa. Comprueba el error a continuación y corrige el script en consecuencia.",
|
||||
"test_script_fail": "Parece que hay un error con el script de prueba. Por favor, corrige los errores y ejecute las pruebas de nuevo",
|
||||
"tests": "Escribir un script de prueba para automatizar la depuración."
|
||||
},
|
||||
"hide": {
|
||||
"collection": "Collapse Collection Panel",
|
||||
"collection": "Colapsar el panel de colecciones",
|
||||
"more": "Ocultar más",
|
||||
"preview": "Ocultar vista previa",
|
||||
"sidebar": "Ocultar barra lateral"
|
||||
@@ -308,40 +308,40 @@
|
||||
"title": "Importar"
|
||||
},
|
||||
"layout": {
|
||||
"collapse_collection": "Collapse or Expand Collections",
|
||||
"collapse_sidebar": "Collapse or Expand the sidebar",
|
||||
"collapse_collection": "Contraer o expandir colecciones",
|
||||
"collapse_sidebar": "Contraer o expandir la barra lateral",
|
||||
"column": "Disposición vertical",
|
||||
"name": "Layout",
|
||||
"name": "Diseño",
|
||||
"row": "Disposición horizontal",
|
||||
"zen_mode": "Modo zen"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"close_unsaved_tab": "Tienes cambios sin guardar",
|
||||
"collections": "Colecciones",
|
||||
"confirm": "Confirmar",
|
||||
"edit_request": "Editar petición",
|
||||
"edit_request": "Editar solicitud",
|
||||
"import_export": "Importación y exportación"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"already_subscribed": "Ya estás suscrito a este tema.",
|
||||
"clean_session": "Borrar sesión",
|
||||
"clear_input": "Borrar entrada",
|
||||
"clear_input_on_send": "Borrar entrada al enviar",
|
||||
"client_id": "Identificación del cliente",
|
||||
"color": "Elige un color",
|
||||
"communication": "Comunicación",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"connection_config": "Configuración de conexión",
|
||||
"connection_not_authorized": "Esta conexión MQTT no utiliza ninguna autenticación.",
|
||||
"invalid_topic": "Indica un tema para la suscripción",
|
||||
"keep_alive": "Mantenerse vivo",
|
||||
"log": "Registro",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"lw_message": "Mensaje de última voluntad",
|
||||
"lw_qos": "QoS de última voluntad",
|
||||
"lw_retain": "Última voluntad",
|
||||
"lw_topic": "Tema de última voluntad",
|
||||
"message": "Mensaje",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"new": "Nueva suscripción",
|
||||
"not_connected": "Por favor, inicia primero una conexión MQTT.",
|
||||
"publish": "Publicar",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
@@ -353,7 +353,7 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"navigation": {
|
||||
"doc": "Docs",
|
||||
"doc": "Documentación",
|
||||
"graphql": "GraphQL",
|
||||
"profile": "Perfil",
|
||||
"realtime": "Tiempo real",
|
||||
@@ -363,7 +363,7 @@
|
||||
"preRequest": {
|
||||
"javascript_code": "Código JavaScript",
|
||||
"learn": "Leer documentación",
|
||||
"script": "Script previo a la petición",
|
||||
"script": "Script previo a la solicitud",
|
||||
"snippets": "Fragmentos"
|
||||
},
|
||||
"profile": {
|
||||
@@ -385,55 +385,55 @@
|
||||
"star": "Eliminar estrella"
|
||||
},
|
||||
"request": {
|
||||
"added": "Petición agregada",
|
||||
"added": "Solicitud agregada",
|
||||
"authorization": "Autorización",
|
||||
"body": "Cuerpo de la petición",
|
||||
"body": "Cuerpo de la solicitud",
|
||||
"choose_language": "Seleccionar lenguaje",
|
||||
"content_type": "Tipo de contenido",
|
||||
"content_type_titles": {
|
||||
"others": "Others",
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
"others": "Otros",
|
||||
"structured": "Estructurado",
|
||||
"text": "Texto"
|
||||
},
|
||||
"copy_link": "Copiar enlace",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"different_collection": "No se pueden reordenar solicitudes de diferentes colecciones",
|
||||
"duplicated": "Solicitud duplicada",
|
||||
"duration": "Duración",
|
||||
"enter_curl": "Ingrese cURL",
|
||||
"generate_code": "Generar código",
|
||||
"generated_code": "Código generado",
|
||||
"header_list": "Lista de encabezados",
|
||||
"invalid_name": "Proporciona un nombre para la petición.",
|
||||
"invalid_name": "Proporciona un nombre para la solicitud.",
|
||||
"method": "Método",
|
||||
"moved": "Request moved",
|
||||
"name": "Nombre de petición",
|
||||
"new": "New Request",
|
||||
"order_changed": "Request Order Updated",
|
||||
"override": "Override",
|
||||
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
|
||||
"overriden": "Overridden",
|
||||
"name": "Nombre de solicitud",
|
||||
"new": "Nueva solicitud",
|
||||
"order_changed": "Orden de solicitudes actualizadas",
|
||||
"override": "Anular",
|
||||
"override_help": "Establecer <kbd>Content-Type</kbd> en las cabeceras",
|
||||
"overriden": "Anulado",
|
||||
"parameter_list": "Parámetros de consulta",
|
||||
"parameters": "Parámetros",
|
||||
"path": "Ruta",
|
||||
"payload": "Carga útil",
|
||||
"query": "Consulta",
|
||||
"raw_body": "Cuerpo de petición sin procesar",
|
||||
"renamed": "Petición renombrada",
|
||||
"raw_body": "Cuerpo de solicitud sin procesar",
|
||||
"renamed": "Solicitud renombrada",
|
||||
"run": "Ejecutar",
|
||||
"save": "Guardar",
|
||||
"save_as": "Guardar como",
|
||||
"saved": "Petición guardada",
|
||||
"saved": "Solicitud guardada",
|
||||
"share": "Compartir",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"title": "Petición",
|
||||
"type": "Tipo de petición",
|
||||
"share_description": "Comparte Hoppscotch con tus amigos",
|
||||
"title": "Solicitud",
|
||||
"type": "Tipo de solicitud",
|
||||
"url": "URL",
|
||||
"variables": "Variables",
|
||||
"view_my_links": "Ver mis enlaces"
|
||||
},
|
||||
"response": {
|
||||
"body": "Cuerpo de respuesta",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
|
||||
"headers": "Encabezados",
|
||||
"html": "HTML",
|
||||
"image": "Imagen",
|
||||
@@ -451,7 +451,7 @@
|
||||
"settings": {
|
||||
"accent_color": "Color de acentuación",
|
||||
"account": "Cuenta",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_deleted": "Tu cuenta ha sido eliminada",
|
||||
"account_description": "Personaliza la configuración de tu cuenta.",
|
||||
"account_email_description": "Tu dirección de correo electrónico principal.",
|
||||
"account_name_description": "Este es tu nombre para mostrar.",
|
||||
@@ -460,8 +460,8 @@
|
||||
"change_font_size": "Cambiar tamaño de fuente",
|
||||
"choose_language": "Elegir idioma",
|
||||
"dark_mode": "Oscuro",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"delete_account": "Eliminar cuenta",
|
||||
"delete_account_description": "Una vez que elimines tu cuenta, todos tus datos se borrarán permanentemente. Esta acción no se puede deshacer.",
|
||||
"expand_navigation": "Expandir la navegación",
|
||||
"experiments": "Experimentos",
|
||||
"experiments_notice": "Esta es una colección de experimentos en los que estamos trabajando que podrían resultar útiles, divertidos, ambos o ninguno. No son definitivos y es posible que no sean estables, por lo que si sucede algo demasiado extraño, no se asuste. Solo apaga la maldita cosa. Fuera de bromas,",
|
||||
@@ -480,7 +480,7 @@
|
||||
"light_mode": "Luz",
|
||||
"official_proxy_hosting": "El proxy oficial está alojado en Hoppscotch.",
|
||||
"profile": "Perfil",
|
||||
"profile_description": "Update your profile details",
|
||||
"profile_description": "Actualiza los datos de tu perfil",
|
||||
"profile_email": "Correo electrónico",
|
||||
"profile_name": "Nombre de perfil",
|
||||
"proxy": "Proxy",
|
||||
@@ -488,8 +488,8 @@
|
||||
"proxy_use_toggle": "Utilizar el middleware de proxy para enviar peticiones",
|
||||
"read_the": "Leer el",
|
||||
"reset_default": "Restablecer a los predeterminados",
|
||||
"short_codes": "Short codes",
|
||||
"short_codes_description": "Short codes which were created by you.",
|
||||
"short_codes": "Shortcodes",
|
||||
"short_codes_description": "Shortcodes creados por ti.",
|
||||
"sidebar_on_left": "Barra lateral a la izquierda",
|
||||
"sync": "Sincronizar",
|
||||
"sync_collections": "Colecciones",
|
||||
@@ -503,15 +503,15 @@
|
||||
"theme_description": "Personaliza el tema de tu aplicación.",
|
||||
"use_experimental_url_bar": "Utilizar la barra de URL experimental con resaltado de entorno",
|
||||
"user": "Usuario",
|
||||
"verified_email": "Verified email",
|
||||
"verified_email": "Correo electrónico verificado",
|
||||
"verify_email": "Verificar correo electrónico"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"actions": "Acciones",
|
||||
"created_on": "Creado el",
|
||||
"deleted": "Código corto eliminado",
|
||||
"method": "Método",
|
||||
"not_found": "Shortcode no encontrado",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
},
|
||||
@@ -539,7 +539,7 @@
|
||||
"title": "Navegación"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Copiar enlace de petición",
|
||||
"copy_request_link": "Copiar enlace de solicitud",
|
||||
"delete_method": "Seleccionar método DELETE",
|
||||
"get_method": "Seleccionar método GET",
|
||||
"head_method": "Seleccionar método HEAD",
|
||||
@@ -548,10 +548,10 @@
|
||||
"post_method": "Seleccionar método POST",
|
||||
"previous_method": "Seleccionar método anterior",
|
||||
"put_method": "Seleccionar método PUT",
|
||||
"reset_request": "Petición de reinicio",
|
||||
"reset_request": "Solicitud de reinicio",
|
||||
"save_to_collections": "Guardar en colecciones",
|
||||
"send_request": "Enviar petición",
|
||||
"title": "Petición"
|
||||
"send_request": "Enviar solicitud",
|
||||
"title": "Solicitud"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copiar la respuesta al portapapeles",
|
||||
@@ -593,8 +593,8 @@
|
||||
"connected_to": "Conectado a {name}",
|
||||
"connecting_to": "Conectando con {name}...",
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_failed": "Error de conexión",
|
||||
"connection_lost": "Conexión perdida",
|
||||
"copied_to_clipboard": "Copiado al portapapeles",
|
||||
"deleted": "Eliminado",
|
||||
"deprecated": "OBSOLETO",
|
||||
@@ -609,18 +609,18 @@
|
||||
"history_deleted": "Historial eliminado",
|
||||
"linewrap": "Envolver líneas",
|
||||
"loading": "Cargando...",
|
||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
||||
"message_received": "Mensaje: {mensaje} llegó sobre el tema: {topic}",
|
||||
"mqtt_subscription_failed": "Algo ha ido mal al suscribirse al tema: {topic}",
|
||||
"none": "Ninguno",
|
||||
"nothing_found": "Nada encontrado para",
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
||||
"waiting_send_request": "Esperando para enviar petición"
|
||||
"published_error": "Algo ha ido mal al publicar el mensaje: {topic} al tema: {message}",
|
||||
"published_message": "Mensaje publicado: {mensaje} al tema: {topic}",
|
||||
"reconnection_error": "Fallo en la reconexión",
|
||||
"subscribed_failed": "Error al suscribirse al tema: {topic}",
|
||||
"subscribed_success": "Suscrito con éxito al tema: {topic}",
|
||||
"unsubscribed_failed": "Error al darse de baja del tema: {topic}",
|
||||
"unsubscribed_success": "Se ha cancelado la suscripción al tema: {topic}",
|
||||
"waiting_send_request": "Esperando para enviar solicitud"
|
||||
},
|
||||
"support": {
|
||||
"changelog": "Leer más sobre los últimos lanzamientos",
|
||||
@@ -644,7 +644,7 @@
|
||||
"history": "Historial",
|
||||
"mqtt": "MQTT",
|
||||
"parameters": "Parámetros",
|
||||
"pre_request_script": "Script previo a la petición",
|
||||
"pre_request_script": "Script previo a la solicitud",
|
||||
"queries": "Consultas",
|
||||
"query": "Consulta",
|
||||
"schema": "Esquema",
|
||||
@@ -664,9 +664,9 @@
|
||||
"email_do_not_match": "El correo electrónico no coincide con los datos de tu cuenta. Ponte en contacto con el propietario de tu equipo.",
|
||||
"exit": "Salir del equipo",
|
||||
"exit_disabled": "Solo el propietario puede salir del equipo",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_coll_id": "Identificador de colección no válido",
|
||||
"invalid_email_format": "El formato de correo electrónico no es válido",
|
||||
"invalid_id": "ID de equipo inválido. Ponte en contacto con el propietario de tu equipo.",
|
||||
"invalid_id": "Identificador de equipo inválido. Ponte en contacto con el propietario de tu equipo.",
|
||||
"invalid_invite_link": "Enlace de invitación inválido",
|
||||
"invalid_invite_link_description": "El enlace que has seguido no es válido. Ponte en contacto con el propietario de tu equipo.",
|
||||
"invalid_member_permission": "Proporcionar un permiso válido al miembro del equipo",
|
||||
@@ -683,7 +683,7 @@
|
||||
"login_to_continue": "Iniciar sesión para continuar",
|
||||
"login_to_continue_description": "Tienes que estar conectado para unirte a un equipo.",
|
||||
"logout_and_try_again": "Cerrar la sesión e iniciar sesión con otra cuenta",
|
||||
"member_has_invite": "Este ID de correo electrónico ya tiene una invitación. Ponte en contacto con el propietario de tu equipo.",
|
||||
"member_has_invite": "Este Identificador de correo electrónico ya tiene una invitación. Ponte en contacto con el propietario de tu equipo.",
|
||||
"member_not_found": "Miembro no encontrado. Ponte en contacto con el propietario de tu equipo.",
|
||||
"member_removed": "Usuario eliminado",
|
||||
"member_role_updated": "Funciones de usuario actualizadas",
|
||||
@@ -696,10 +696,10 @@
|
||||
"new_name": "Mi nuevo equipo",
|
||||
"no_access": "No tienes acceso de edición a estas colecciones.",
|
||||
"no_invite_found": "No se ha encontrado la invitación. Ponte en contacto con el propietario de tu equipo.",
|
||||
"no_request_found": "Request not found.",
|
||||
"no_request_found": "Solicitud no encontrada.",
|
||||
"not_found": "Equipo no encontrado. Ponte en contacto con el propietario de tu equipo.",
|
||||
"not_valid_viewer": "No eres un espectador válido. Ponte en contacto con el propietario de tu equipo.",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"parent_coll_move": "No se puede mover la colección a una colección hija",
|
||||
"pending_invites": "Invitaciones pendientes",
|
||||
"permissions": "Permisos",
|
||||
"same_target_destination": "Same target and destination",
|
||||
@@ -707,12 +707,12 @@
|
||||
"select_a_team": "Seleccionar un equipo",
|
||||
"title": "Equipos",
|
||||
"we_sent_invite_link": "¡Hemos enviado un enlace de invitación a todos los invitados!",
|
||||
"we_sent_invite_link_description": "Pide a todos los invitados que revisen su bandeja de entrada. Haz clic en el enlace para unirse al equipo."
|
||||
"we_sent_invite_link_description": "Pide a todos los invitados que revisen tu bandeja de entrada. Haz clic en el enlace para unirse al equipo."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
"deleted": "Entorno eliminado",
|
||||
"duplicate": "Entorno duplicado",
|
||||
"not_found": "Entorno no encontrado."
|
||||
},
|
||||
"test": {
|
||||
"failed": "prueba fallida",
|
||||
@@ -732,9 +732,9 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "Cambiar el espacio de trabajo",
|
||||
"personal": "Mi espacio de trabajo",
|
||||
"team": "Espacio de trabajo en equipo",
|
||||
"title": "Espacios de trabajo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ export const APP_INFO = {
|
||||
keywords:
|
||||
"hoppscotch, hopp scotch, hoppscotch online, hoppscotch app, postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket, sse, graphql, socketio",
|
||||
app: {
|
||||
background: "#202124",
|
||||
background: "#181818",
|
||||
lightThemeColor: "#ffffff",
|
||||
darkThemeColor: "#181818",
|
||||
},
|
||||
social: {
|
||||
twitter: "@hoppscotch_io",
|
||||
@@ -108,7 +110,17 @@ export const META_TAGS = (env: Record<string, string>): IHTMLTag[] => [
|
||||
// PWA
|
||||
{
|
||||
name: "theme-color",
|
||||
content: APP_INFO.app.background,
|
||||
content: APP_INFO.app.darkThemeColor,
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
},
|
||||
{
|
||||
name: "theme-color",
|
||||
content: APP_INFO.app.lightThemeColor,
|
||||
media: "(prefers-color-scheme: light)",
|
||||
},
|
||||
{
|
||||
name: "supported-color-schemes",
|
||||
content: "light dark",
|
||||
},
|
||||
{
|
||||
name: "mask-icon",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "2023.4.1",
|
||||
"version": "2023.4.4",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"dev:vite": "vite",
|
||||
@@ -92,6 +92,7 @@
|
||||
"vuedraggable-es": "^4.1.1",
|
||||
"wonka": "^4.0.15",
|
||||
"workbox-window": "^6.5.4",
|
||||
"xml-formatter": "^3.4.1",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
|
Before Width: | Height: | Size: 595 KiB After Width: | Height: | Size: 666 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-1024x1024.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-128x128.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-16x16.png
Normal file
|
After Width: | Height: | Size: 400 B |
BIN
packages/hoppscotch-common/public/icons/pwa-192x192.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-256x256.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-32x32.png
Normal file
|
After Width: | Height: | Size: 871 B |
BIN
packages/hoppscotch-common/public/icons/pwa-512x512.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 358 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 382 KiB |
@@ -57,6 +57,7 @@ declare module '@vue/runtime-core' {
|
||||
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
||||
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
||||
EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default']
|
||||
EnvironmentsSelector: typeof import('./components/environments/Selector.vue')['default']
|
||||
EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
|
||||
EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']
|
||||
EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default']
|
||||
@@ -84,6 +85,7 @@ declare module '@vue/runtime-core' {
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||
@@ -129,7 +131,6 @@ declare module '@vue/runtime-core' {
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||
@@ -140,7 +141,6 @@ declare module '@vue/runtime-core' {
|
||||
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
|
||||
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||
ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
|
||||
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
||||
ProfileShortcodes: typeof import('./components/profile/Shortcodes.vue')['default']
|
||||
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
|
||||
@@ -164,6 +164,7 @@ declare module '@vue/runtime-core' {
|
||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
||||
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
@@ -144,7 +144,7 @@
|
||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||
"
|
||||
/>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -53,28 +53,22 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
loadingState: boolean
|
||||
editingRequestName: string
|
||||
modelValue?: string
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
loadingState: false,
|
||||
editingRequestName: "",
|
||||
modelValue: "",
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit", name: string): void
|
||||
(e: "hide-modal"): void
|
||||
(e: "update:modelValue", value: string): void
|
||||
}>()
|
||||
|
||||
const name = ref("")
|
||||
|
||||
watch(
|
||||
() => props.editingRequestName,
|
||||
(newName) => {
|
||||
name.value = newName
|
||||
}
|
||||
)
|
||||
const name = useVModel(props, "modelValue")
|
||||
|
||||
const editRequest = () => {
|
||||
if (name.value.trim() === "") {
|
||||
|
||||
@@ -136,11 +136,11 @@ const requestName = ref(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [currentActiveTab.value.document.request.name, gqlRequestName.value],
|
||||
() => [currentActiveTab.value, gqlRequestName.value],
|
||||
() => {
|
||||
if (props.mode === "rest")
|
||||
requestName.value = currentActiveTab.value.document.request.name
|
||||
else requestName.value = gqlRequestName.value
|
||||
if (props.mode === "rest") {
|
||||
requestName.value = currentActiveTab.value?.document.request.name ?? ""
|
||||
} else requestName.value = gqlRequestName.value
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
/>
|
||||
<CollectionsEditRequest
|
||||
:show="showModalEditRequest"
|
||||
:editing-request-name="editingRequest ? editingRequest.name : ''"
|
||||
:model-value="editingRequest ? editingRequest.name : ''"
|
||||
:loading-state="modalLoadingState"
|
||||
@submit="updateEditingRequest"
|
||||
@hide-modal="displayModalEditRequest(false)"
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLayers"
|
||||
:label="
|
||||
mdAndLarger
|
||||
? selectedEnv.type !== 'NO_ENV_SELECTED'
|
||||
? selectedEnv.name
|
||||
: `${t('environment.select')}`
|
||||
: ''
|
||||
"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
role="menu"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="
|
||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||
? IconCheck
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="
|
||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||
"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedEnvTab"
|
||||
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartTab
|
||||
:id="'my-environments'"
|
||||
:label="`${t('environment.my_environments')}`"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in myEnvironments"
|
||||
:key="`gen-${index}`"
|
||||
:icon="IconLayers"
|
||||
:label="gen.name"
|
||||
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
|
||||
:active-info-icon="index === selectedEnv.index"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="myEnvironments.length === 0"
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'team-environments'"
|
||||
:label="`${t('environment.team_environments')}`"
|
||||
:disabled="!isTeamSelected || workspace.type === 'personal'"
|
||||
>
|
||||
<div
|
||||
v-if="teamListLoading"
|
||||
class="flex flex-col items-center justify-center p-4"
|
||||
>
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div v-if="isTeamSelected" class="flex flex-col">
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in teamEnvironmentList"
|
||||
:key="`gen-team-${index}`"
|
||||
:icon="IconLayers"
|
||||
:label="gen.environment.name"
|
||||
:info-icon="
|
||||
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
|
||||
"
|
||||
:active-info-icon="gen.id === selectedEnv.teamEnvID"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = {
|
||||
type: 'TEAM_ENV',
|
||||
teamEnvID: gen.id,
|
||||
teamID: gen.teamID,
|
||||
environment: gen.environment,
|
||||
}
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="teamEnvironmentList.length === 0"
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!teamListLoading && teamAdapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(teamAdapterError) }}
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconLayers from "~icons/lucide/layers"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { useReadonlyStream, useStream } from "~/composables/stream"
|
||||
import {
|
||||
environments$,
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const mdAndLarger = breakpoints.greater("md")
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
type EnvironmentType = "my-environments" | "team-environments"
|
||||
|
||||
const myEnvironments = useReadonlyStream(environments$, [])
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
|
||||
const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
|
||||
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
|
||||
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
|
||||
const teamEnvironmentList = useReadonlyStream(
|
||||
teamEnvListAdapter.teamEnvironmentList$,
|
||||
[]
|
||||
)
|
||||
|
||||
const selectedEnvironmentIndex = useStream(
|
||||
selectedEnvironmentIndex$,
|
||||
{ type: "NO_ENV_SELECTED" },
|
||||
setSelectedEnvironmentIndex
|
||||
)
|
||||
|
||||
const isTeamSelected = computed(
|
||||
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
|
||||
)
|
||||
|
||||
const selectedEnvTab = ref<EnvironmentType>("my-environments")
|
||||
|
||||
watch(
|
||||
() => workspace.value,
|
||||
(newVal) => {
|
||||
if (newVal.type === "personal") {
|
||||
selectedEnvTab.value = "my-environments"
|
||||
} else {
|
||||
selectedEnvTab.value = "team-environments"
|
||||
if (newVal.teamID) {
|
||||
teamEnvListAdapter.changeTeamID(newVal.teamID)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const selectedEnv = computed(() => {
|
||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||
return {
|
||||
type: "MY_ENV",
|
||||
index: selectedEnvironmentIndex.value.index,
|
||||
name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
|
||||
}
|
||||
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
||||
const teamEnv = teamEnvironmentList.value.find(
|
||||
(env) =>
|
||||
env.id ===
|
||||
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||
selectedEnvironmentIndex.value.teamEnvID)
|
||||
)
|
||||
if (teamEnv) {
|
||||
return {
|
||||
type: "TEAM_ENV",
|
||||
name: teamEnv.environment.name,
|
||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||
}
|
||||
} else {
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
} else {
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
})
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -4,153 +4,6 @@
|
||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
||||
>
|
||||
<WorkspaceCurrent :section="t('tab.environments')" />
|
||||
<tippy
|
||||
v-if="environmentType.type === 'my-environments'"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="
|
||||
selectedEnv.type === 'MY_ENV' && selectedEnv.index !== undefined
|
||||
"
|
||||
:label="myEnvironments[selectedEnv.index].name"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('environment.select')}`"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
role="menu"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="
|
||||
selectedEnvironmentIndex.type !== 'MY_ENV'
|
||||
? IconCheck
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="selectedEnvironmentIndex.type !== 'MY_ENV'"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<hr v-if="myEnvironments.length > 0" />
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in myEnvironments"
|
||||
:key="`gen-${index}`"
|
||||
:label="gen.name"
|
||||
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
|
||||
:active-info-icon="index === selectedEnv.index"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<tippy v-else interactive trigger="click" theme="popover">
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="bg-transparent border-b border-dividerLight select-wrapper"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="selectedEnv.name"
|
||||
:label="selectedEnv.name"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('environment.select')}`"
|
||||
class="flex-1 !justify-start pr-8 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
class="flex flex-col"
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:label="`${t('environment.no_environment')}`"
|
||||
:info-icon="
|
||||
selectedEnvironmentIndex.type !== 'TEAM_ENV'
|
||||
? IconCheck
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="selectedEnvironmentIndex.type !== 'TEAM_ENV'"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex flex-col items-center justify-center p-4"
|
||||
>
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<hr v-if="teamEnvironmentList.length > 0" />
|
||||
<div
|
||||
v-if="environmentType.selectedTeam !== undefined"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in teamEnvironmentList"
|
||||
:key="`gen-team-${index}`"
|
||||
:label="gen.environment.name"
|
||||
:info-icon="
|
||||
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
|
||||
"
|
||||
:active-info-icon="gen.id === selectedEnv.teamEnvID"
|
||||
@click="
|
||||
() => {
|
||||
selectedEnvironmentIndex = {
|
||||
type: 'TEAM_ENV',
|
||||
teamEnvID: gen.id,
|
||||
teamID: gen.teamID,
|
||||
environment: gen.environment,
|
||||
}
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loading && adapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ getErrorMessage(adapterError) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<EnvironmentsMyEnvironment
|
||||
environment-index="Global"
|
||||
:environment="globalEnvironment"
|
||||
@@ -184,15 +37,11 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import {
|
||||
environments$,
|
||||
globalEnv$,
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
@@ -261,7 +110,7 @@ const switchToMyEnvironments = () => {
|
||||
adapter.changeTeamID(undefined)
|
||||
}
|
||||
|
||||
const updateSelectedTeam = (newSelectedTeam: SelectedTeam) => {
|
||||
const updateSelectedTeam = (newSelectedTeam: SelectedTeam | undefined) => {
|
||||
if (newSelectedTeam) {
|
||||
environmentType.value.selectedTeam = newSelectedTeam
|
||||
REMEMBERED_TEAM_ID.value = newSelectedTeam.id
|
||||
@@ -287,22 +136,21 @@ onLoggedIn(() => {
|
||||
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
|
||||
// Used to switch environment 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 environment and select the team
|
||||
// If there is no teamID, switch to my environment
|
||||
watch(
|
||||
() => workspace.value.teamID,
|
||||
(teamID) => {
|
||||
if (!teamID) {
|
||||
switchToMyEnvironments()
|
||||
} else if (teamID) {
|
||||
const team = myTeams.value?.find((t) => t.id === teamID)
|
||||
if (team) {
|
||||
updateSelectedTeam(team)
|
||||
}
|
||||
// Switch to my environments if workspace is personal and to team environments if workspace is team
|
||||
// also resets selected environment if workspace is personal and the previous selected environment was a team environment
|
||||
watch(workspace, (newWorkspace) => {
|
||||
if (newWorkspace.type === "personal") {
|
||||
switchToMyEnvironments()
|
||||
if (selectedEnvironmentIndex.value.type !== "MY_ENV") {
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "NO_ENV_SELECTED",
|
||||
})
|
||||
}
|
||||
} else if (newWorkspace.type === "team") {
|
||||
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
|
||||
updateSelectedTeam(team)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentUser.value,
|
||||
@@ -343,8 +191,6 @@ defineActionHandler(
|
||||
}
|
||||
)
|
||||
|
||||
const myEnvironments = useReadonlyStream(environments$, [])
|
||||
|
||||
const selectedEnvironmentIndex = useStream(
|
||||
selectedEnvironmentIndex$,
|
||||
{ type: "NO_ENV_SELECTED" },
|
||||
@@ -387,47 +233,4 @@ watch(
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const selectedEnv = computed(() => {
|
||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||
return {
|
||||
type: "MY_ENV",
|
||||
index: selectedEnvironmentIndex.value.index,
|
||||
}
|
||||
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
||||
const teamEnv = teamEnvironmentList.value.find(
|
||||
(env) =>
|
||||
env.id ===
|
||||
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||
selectedEnvironmentIndex.value.teamEnvID)
|
||||
)
|
||||
if (teamEnv) {
|
||||
return {
|
||||
type: "TEAM_ENV",
|
||||
name: teamEnv.environment.name,
|
||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||
}
|
||||
} else {
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
} else {
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
})
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
</script>
|
||||
|
||||
@@ -165,8 +165,8 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
action: "edit" | "new"
|
||||
editingEnvironmentIndex: number | "Global" | null
|
||||
editingVariableName: string | null
|
||||
editingEnvironmentIndex?: number | "Global" | null
|
||||
editingVariableName?: string | null
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
|
||||
@@ -140,7 +140,7 @@ import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import { parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { Environment, parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { clone } from "lodash-es"
|
||||
import { useToast } from "@composables/toast"
|
||||
@@ -173,16 +173,20 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
show: boolean
|
||||
action: "edit" | "new"
|
||||
editingEnvironment: TeamEnvironment | null
|
||||
editingEnvironment?: TeamEnvironment | null
|
||||
editingTeamId: string | undefined
|
||||
editingVariableName: string | null
|
||||
isViewer: boolean
|
||||
editingVariableName?: string | null
|
||||
isViewer?: boolean
|
||||
envVars?: () => Environment["variables"]
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
action: "edit",
|
||||
editingEnvironment: null,
|
||||
editingTeamId: "",
|
||||
editingVariableName: null,
|
||||
isViewer: false,
|
||||
envVars: () => [],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -226,10 +230,16 @@ watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
if (props.editingEnvironment === null) {
|
||||
if (props.action === "new") {
|
||||
name.value = null
|
||||
vars.value = []
|
||||
} else {
|
||||
vars.value = pipe(
|
||||
props.envVars() ?? [],
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
id: idTicker.value++,
|
||||
env: clone(e),
|
||||
}))
|
||||
)
|
||||
} else if (props.editingEnvironment !== null) {
|
||||
name.value = props.editingEnvironment.environment.name ?? null
|
||||
vars.value = pipe(
|
||||
props.editingEnvironment.environment.variables ?? [],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperSecondaryStickyFold border-dividerLight bg-primary"
|
||||
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="team === undefined || team.myRole === 'VIEWER'"
|
||||
|
||||
@@ -84,6 +84,7 @@ import { useToast } from "@composables/toast"
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
import jsonLinter from "~/helpers/editor/linting/json"
|
||||
import { readFileAsText } from "~/helpers/functional/files"
|
||||
import xmlFormat from "xml-formatter"
|
||||
|
||||
type PossibleContentTypes = Exclude<
|
||||
ValidContentTypes,
|
||||
@@ -197,26 +198,10 @@ const prettifyRequestBody = () => {
|
||||
}
|
||||
|
||||
const prettifyXML = (xml: string) => {
|
||||
const PADDING = " ".repeat(2) // set desired indent size here
|
||||
const reg = /(>)(<)(\/*)/g
|
||||
let pad = 0
|
||||
xml = xml.replace(reg, "$1\r\n$2$3")
|
||||
return xml
|
||||
.split("\r\n")
|
||||
.map((node) => {
|
||||
let indent = 0
|
||||
if (node.match(/.+<\/\w[^>]*>$/)) {
|
||||
indent = 0
|
||||
} else if (node.match(/^<\/\w/) && pad > 0) {
|
||||
pad -= 1
|
||||
} else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
|
||||
indent = 1
|
||||
} else {
|
||||
indent = 0
|
||||
}
|
||||
pad += indent
|
||||
return PADDING.repeat(pad - indent) + node
|
||||
})
|
||||
.join("\r\n")
|
||||
return xmlFormat(xml, {
|
||||
indentation: " ",
|
||||
collapseContent: true,
|
||||
lineSeparator: "\n",
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -229,11 +229,10 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useStreamSubscriber } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
|
||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { isLeft, isRight } from "fp-ts/lib/Either"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { computed, onBeforeUnmount, ref } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||
@@ -259,6 +258,8 @@ import IconSave from "~icons/lucide/save"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { platform } from "~/platform"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -311,39 +312,6 @@ const clearAll = ref<any | null>(null)
|
||||
const copyRequestAction = ref<any | null>(null)
|
||||
const saveRequestAction = ref<any | null>(null)
|
||||
|
||||
// Update Nuxt Loading bar
|
||||
watch(loading, () => {
|
||||
if (loading.value) {
|
||||
startPageProgress()
|
||||
} else {
|
||||
completePageProgress()
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: make this oAuthURL() work
|
||||
|
||||
// function oAuthURL() {
|
||||
// const auth = useReadonlyStream(props.request.auth$, {
|
||||
// authType: "none",
|
||||
// authActive: true,
|
||||
// })
|
||||
|
||||
// const oauth2Token = pluckRef(auth as Ref<HoppRESTAuthOAuth2>, "token")
|
||||
|
||||
// onBeforeMount(async () => {
|
||||
// try {
|
||||
// const tokenInfo = await oauthRedirect()
|
||||
// if (Object.prototype.hasOwnProperty.call(tokenInfo, "access_token")) {
|
||||
// if (typeof tokenInfo === "object") {
|
||||
// oauth2Token.value = tokenInfo.access_token
|
||||
// }
|
||||
// }
|
||||
|
||||
// // eslint-disable-next-line no-empty
|
||||
// } catch (_) {}
|
||||
// })
|
||||
// }
|
||||
|
||||
const newSendRequest = async () => {
|
||||
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||
toast.error(`${t("empty.endpoint")}`)
|
||||
@@ -354,6 +322,12 @@ const newSendRequest = async () => {
|
||||
|
||||
loading.value = true
|
||||
|
||||
// Log the request run into analytics
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform: "rest",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
|
||||
// Double calling is because the function returns a TaskEither than should be executed
|
||||
const streamResult = await runRESTRequest$(tab)()
|
||||
|
||||
@@ -574,6 +548,10 @@ const saveRequest = () => {
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (loading.value) cancelRequest()
|
||||
})
|
||||
|
||||
defineActionHandler("request.send-cancel", () => {
|
||||
if (!loading.value) newSendRequest()
|
||||
else cancelRequest()
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { startPageProgress, completePageProgress } from "@modules/loadingbar"
|
||||
import { computed, ref } from "vue"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
|
||||
@@ -34,9 +33,4 @@ const hasResponse = computed(
|
||||
)
|
||||
|
||||
const loading = computed(() => tab.value.response?.type === "loading")
|
||||
|
||||
watch(loading, (isLoading) => {
|
||||
if (isLoading) startPageProgress()
|
||||
else completePageProgress()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -197,11 +197,20 @@
|
||||
/>
|
||||
</div>
|
||||
<EnvironmentsMyDetails
|
||||
:show="showModalDetails"
|
||||
:show="showMyEnvironmentDetailsModal"
|
||||
action="new"
|
||||
:env-vars="getAdditionVars"
|
||||
@hide-modal="displayModalAdd(false)"
|
||||
/>
|
||||
<EnvironmentsTeamsDetails
|
||||
:show="showTeamEnvironmentDetailsModal"
|
||||
action="new"
|
||||
:env-vars="getAdditionVars"
|
||||
:editing-team-id="
|
||||
workspace.type === 'team' ? workspace.teamID : undefined
|
||||
"
|
||||
@hide-modal="displayModalAdd(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -225,6 +234,7 @@ import IconClose from "~icons/lucide/x"
|
||||
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: HoppTestResult | null | undefined
|
||||
@@ -239,10 +249,15 @@ const testResults = useVModel(props, "modelValue", emit)
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const showModalDetails = ref(false)
|
||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
||||
|
||||
const showMyEnvironmentDetailsModal = ref(false)
|
||||
const showTeamEnvironmentDetailsModal = ref(false)
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
showModalDetails.value = shouldDisplay
|
||||
if (workspace.value.type === "personal")
|
||||
showMyEnvironmentDetailsModal.value = shouldDisplay
|
||||
else showTeamEnvironmentDetailsModal.value = shouldDisplay
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
|
||||
>
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t(
|
||||
'action.download_file'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
|
||||
:icon="downloadIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center overflow-auto">
|
||||
<audio controls :src="audiosrc"></audio>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useDownloadResponse } from "@composables/lens-actions"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import * as S from "fp-ts/string"
|
||||
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { objFieldMatches } from "~/helpers/functional/object"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse & {
|
||||
type: "success" | "fail"
|
||||
}
|
||||
}>()
|
||||
|
||||
const audiosrc = computed(() =>
|
||||
URL.createObjectURL(
|
||||
new Blob([props.response.body], {
|
||||
type: "audio/mp3",
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const responseType = computed(() =>
|
||||
pipe(
|
||||
props.response,
|
||||
O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
|
||||
O.chain(
|
||||
// Try getting content-type
|
||||
flow(
|
||||
(res) => res.headers,
|
||||
A.findFirst((h) => h.key.toLowerCase() === "content-type"),
|
||||
O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
|
||||
)
|
||||
),
|
||||
O.getOrElse(() => "text/plain")
|
||||
)
|
||||
)
|
||||
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
responseType.value,
|
||||
computed(() => props.response.body)
|
||||
)
|
||||
|
||||
defineActionHandler("response.file.download", () => downloadResponse())
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-lowerSecondaryStickyFold"
|
||||
>
|
||||
<label class="font-semibold truncate text-secondaryLight">
|
||||
{{ t("response.body") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-if="response.body"
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t(
|
||||
'action.download_file'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
|
||||
:icon="downloadIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center overflow-auto">
|
||||
<video controls :src="videosrc"></video>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useDownloadResponse } from "@composables/lens-actions"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { flow, pipe } from "fp-ts/function"
|
||||
import * as S from "fp-ts/string"
|
||||
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { objFieldMatches } from "~/helpers/functional/object"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse & {
|
||||
type: "success" | "fail"
|
||||
}
|
||||
}>()
|
||||
|
||||
const videosrc = computed(() =>
|
||||
URL.createObjectURL(
|
||||
new Blob([props.response.body], {
|
||||
type: "video/mp4",
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const responseType = computed(() =>
|
||||
pipe(
|
||||
props.response,
|
||||
O.fromPredicate(objFieldMatches("type", ["fail", "success"] as const)),
|
||||
O.chain(
|
||||
// Try getting content-type
|
||||
flow(
|
||||
(res) => res.headers,
|
||||
A.findFirst((h) => h.key.toLowerCase() === "content-type"),
|
||||
O.map(flow((h) => h.value, S.split(";"), RNEA.head, S.toLowerCase))
|
||||
)
|
||||
),
|
||||
O.getOrElse(() => "text/plain")
|
||||
)
|
||||
)
|
||||
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
responseType.value,
|
||||
computed(() => props.response.body)
|
||||
)
|
||||
|
||||
defineActionHandler("response.file.download", () => downloadResponse())
|
||||
</script>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
tabindex="0"
|
||||
class="relative flex items-center justify-center overflow-visible cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-primaryDark"
|
||||
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||
>
|
||||
<img
|
||||
v-if="url"
|
||||
class="absolute object-cover object-center transition bg-primaryDark"
|
||||
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||
:src="url"
|
||||
:alt="alt"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="absolute flex items-center justify-center object-cover object-center transition bg-primaryDark text-accentContrast"
|
||||
:class="[`rounded-${rounded}`, `w-${size} h-${size}`]"
|
||||
:style="`background-color: ${initial ? toHex(initial) : '#480000'}`"
|
||||
>
|
||||
<template v-if="initial && initial.charAt(0).toUpperCase()">
|
||||
{{ initial.charAt(0).toUpperCase() }}
|
||||
</template>
|
||||
|
||||
<icon-lucide-user v-else></icon-lucide-user>
|
||||
</div>
|
||||
<span
|
||||
v-if="indicator"
|
||||
class="border-primary border-2 h-2.5 -top-0.5 -right-0.5 w-2.5 absolute"
|
||||
:class="[`rounded-${rounded}`, indicatorStyles]"
|
||||
></span>
|
||||
<!-- w-5 h-5 rounded-lg -->
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: "Profile picture",
|
||||
},
|
||||
indicator: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
indicatorStyles: {
|
||||
type: String,
|
||||
default: "bg-green-500",
|
||||
},
|
||||
rounded: {
|
||||
type: String,
|
||||
default: "full",
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: "5",
|
||||
},
|
||||
initial: {
|
||||
type: String as PropType<string | undefined | null>,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toHex(initial: string) {
|
||||
let hash = 0
|
||||
if (initial.length === 0) return hash
|
||||
for (let i = 0; i < initial.length; i++) {
|
||||
hash = initial.charCodeAt(i) + ((hash << 5) - hash)
|
||||
hash = hash & hash
|
||||
}
|
||||
let color = "#"
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 255
|
||||
color += `00${value.toString(16)}`.slice(-2)
|
||||
}
|
||||
return color
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -34,6 +34,7 @@ import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
||||
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -219,6 +220,7 @@ onMounted(() => {
|
||||
if (editor.value) {
|
||||
if (!view.value) initView(editor.value)
|
||||
if (props.selectTextOnMount) triggerTextSelection()
|
||||
platform.ui?.onCodemirrorInstanceMount?.(editor.value)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:key="`member-${index}`"
|
||||
class="inline-flex"
|
||||
>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-if="member.user.photoURL"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:url="member.user.photoURL"
|
||||
@@ -14,7 +14,7 @@
|
||||
class="ring-primary ring-2"
|
||||
@click="handleClick()"
|
||||
/>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="getUserName(member)"
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
baseHighlightStyle,
|
||||
} from "@helpers/editor/themes/baseTheme"
|
||||
import { HoppEnvironmentPlugin } from "@helpers/editor/extensions/HoppEnvironment"
|
||||
import xmlFormat from "xml-formatter"
|
||||
import { platform } from "~/platform"
|
||||
// TODO: Migrate from legacy mode
|
||||
|
||||
type ExtendedEditorConfig = {
|
||||
@@ -151,6 +153,27 @@ const getLanguage = (langMime: string): Language | null => {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses xml-formatter to format the XML document
|
||||
* @param doc Document to parse
|
||||
* @param langMime Language mime type
|
||||
* @returns Parsed document if mime type is xml, else returns the original document
|
||||
*/
|
||||
const parseDoc = (
|
||||
doc: string | undefined,
|
||||
langMime: string
|
||||
): string | undefined => {
|
||||
if (langMime === "application/xml" && doc) {
|
||||
return xmlFormat(doc, {
|
||||
indentation: " ",
|
||||
collapseContent: true,
|
||||
lineSeparator: "\n",
|
||||
})
|
||||
} else {
|
||||
return doc
|
||||
}
|
||||
}
|
||||
|
||||
const getEditorLanguage = (
|
||||
langMime: string,
|
||||
linter: LinterDefinition | undefined,
|
||||
@@ -186,6 +209,8 @@ export function useCodemirror(
|
||||
: null
|
||||
|
||||
const initView = (el: any) => {
|
||||
if (el) platform.ui?.onCodemirrorInstanceMount?.(el)
|
||||
|
||||
const extensions = [
|
||||
basicSetup,
|
||||
baseTheme,
|
||||
@@ -258,7 +283,7 @@ export function useCodemirror(
|
||||
view.value = new EditorView({
|
||||
parent: el,
|
||||
state: EditorState.create({
|
||||
doc: value.value,
|
||||
doc: parseDoc(value.value, options.extendedEditorConfig.mode ?? ""),
|
||||
extensions,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { pipe, flow } from "fp-ts/function"
|
||||
import { tupleToRecord } from "~/helpers/functional/record"
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
import { optionChoose } from "~/helpers/functional/option"
|
||||
import xmlFormat from "xml-formatter"
|
||||
|
||||
const isJSON = flow(safeParseJSON, O.isSome)
|
||||
|
||||
@@ -213,45 +214,18 @@ export function parseBody(
|
||||
*/
|
||||
|
||||
/**
|
||||
* Prettifies XML string
|
||||
* Prettifies XML string using xml-formatter
|
||||
* @param sourceXml The string to format
|
||||
* @returns Indented XML string (uses spaces)
|
||||
*/
|
||||
function prettifyXml(sourceXml: string) {
|
||||
return pipe(
|
||||
O.tryCatch(() => {
|
||||
const xmlDoc = new DOMParser().parseFromString(
|
||||
sourceXml,
|
||||
"application/xml"
|
||||
)
|
||||
|
||||
if (xmlDoc.querySelector("parsererror")) {
|
||||
throw new Error("Unstructured Body")
|
||||
}
|
||||
|
||||
const xsltDoc = new DOMParser().parseFromString(
|
||||
[
|
||||
// describes how we want to modify the XML - indent everything
|
||||
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">',
|
||||
' <xsl:strip-space elements="*"/>',
|
||||
' <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes
|
||||
' <xsl:value-of select="normalize-space(.)"/>',
|
||||
" </xsl:template>",
|
||||
' <xsl:template match="node()|@*">',
|
||||
' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
|
||||
" </xsl:template>",
|
||||
' <xsl:output indent="yes"/>',
|
||||
"</xsl:stylesheet>",
|
||||
].join("\n"),
|
||||
"application/xml"
|
||||
)
|
||||
|
||||
const xsltProcessor = new XSLTProcessor()
|
||||
xsltProcessor.importStylesheet(xsltDoc)
|
||||
const resultDoc = xsltProcessor.transformToDocument(xmlDoc)
|
||||
const resultXml = new XMLSerializer().serializeToString(resultDoc)
|
||||
|
||||
return resultXml
|
||||
return xmlFormat(sourceXml, {
|
||||
indentation: " ",
|
||||
collapseContent: true,
|
||||
lineSeparator: "\n",
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -121,8 +121,8 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
|
||||
else if (val === "arrowright") return "right"
|
||||
|
||||
// Check letter keys
|
||||
if (val.length === 1 && val.toUpperCase() !== val.toLowerCase())
|
||||
return val as Key
|
||||
const isLetter = ev.code.toLowerCase().startsWith("key")
|
||||
if (isLetter) return ev.code.toLowerCase().substring(3) as Key
|
||||
|
||||
// Check if number keys
|
||||
if (val.length === 1 && !isNaN(val as any)) return val as Key
|
||||
|
||||
16
packages/hoppscotch-common/src/helpers/lenses/audioLens.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineAsyncComponent } from "vue"
|
||||
import { Lens } from "./lenses"
|
||||
|
||||
const audioLens: Lens = {
|
||||
lensName: "response.audio",
|
||||
isSupportedContentType: (contentType) =>
|
||||
/\baudio\/(?:wav|mpeg|mp4|aac|aacp|ogg|webm|x-caf|flac|mp3|)\b/i.test(
|
||||
contentType
|
||||
),
|
||||
renderer: "audiores",
|
||||
rendererImport: defineAsyncComponent(
|
||||
() => import("~/components/lenses/renderers/AudioLensRenderer.vue")
|
||||
),
|
||||
}
|
||||
|
||||
export default audioLens
|
||||
@@ -5,6 +5,8 @@ import imageLens from "./imageLens"
|
||||
import htmlLens from "./htmlLens"
|
||||
import xmlLens from "./xmlLens"
|
||||
import pdfLens from "./pdfLens"
|
||||
import audioLens from "./audioLens"
|
||||
import videoLens from "./videoLens"
|
||||
import { defineAsyncComponent } from "vue"
|
||||
|
||||
export type Lens = {
|
||||
@@ -20,6 +22,8 @@ export const lenses: Lens[] = [
|
||||
htmlLens,
|
||||
xmlLens,
|
||||
pdfLens,
|
||||
audioLens,
|
||||
videoLens,
|
||||
rawLens,
|
||||
]
|
||||
|
||||
|
||||
16
packages/hoppscotch-common/src/helpers/lenses/videoLens.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineAsyncComponent } from "vue"
|
||||
import { Lens } from "./lenses"
|
||||
|
||||
const videoLens: Lens = {
|
||||
lensName: "response.video",
|
||||
isSupportedContentType: (contentType) =>
|
||||
/\bvideo\/(?:webm|x-m4v|quicktime|x-ms-wmv|x-flv|mpeg|x-msvideo|x-ms-asf|mp4|)\b/i.test(
|
||||
contentType
|
||||
),
|
||||
renderer: "videores",
|
||||
rendererImport: defineAsyncComponent(
|
||||
() => import("~/components/lenses/renderers/VideoLensRenderer.vue")
|
||||
),
|
||||
}
|
||||
|
||||
export default videoLens
|
||||
@@ -7,8 +7,8 @@ pw.env.set("variable", "value");`,
|
||||
{
|
||||
name: "Environment: Set timestamp variable",
|
||||
script: `\n\n// Set timestamp variable
|
||||
const cuttentTime = Date.now();
|
||||
pw.env.set("timestamp", cuttentTime.toString());`,
|
||||
const currentTime = Date.now();
|
||||
pw.env.set("timestamp", currentTime.toString());`,
|
||||
},
|
||||
{
|
||||
name: "Environment: Set random number variable",
|
||||
|
||||
@@ -24,7 +24,10 @@
|
||||
>
|
||||
<Pane class="flex flex-1 !overflow-auto">
|
||||
<main class="flex flex-1 w-full" role="main">
|
||||
<RouterView v-slot="{ Component }" class="flex flex-1">
|
||||
<RouterView
|
||||
v-slot="{ Component }"
|
||||
class="flex flex-1 min-w-0"
|
||||
>
|
||||
<Transition name="fade" mode="out-in" appear>
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
|
||||
@@ -37,13 +37,27 @@ type EnvironmentStore = typeof defaultEnvironmentsState
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setSelectedEnvironmentIndex(
|
||||
_: EnvironmentStore,
|
||||
store: EnvironmentStore,
|
||||
{
|
||||
selectedEnvironmentIndex,
|
||||
}: { selectedEnvironmentIndex: SelectedEnvironmentIndex }
|
||||
) {
|
||||
return {
|
||||
selectedEnvironmentIndex,
|
||||
if (selectedEnvironmentIndex.type === "MY_ENV") {
|
||||
if (store.environments[selectedEnvironmentIndex.index]) {
|
||||
return {
|
||||
selectedEnvironmentIndex,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
selectedEnvironmentIndex: {
|
||||
type: "NO_ENV_SELECTED",
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
selectedEnvironmentIndex,
|
||||
}
|
||||
}
|
||||
},
|
||||
appendEnvironments(
|
||||
@@ -325,21 +339,22 @@ export const selectedEnvironmentIndex$ = environmentsStore.subject$.pipe(
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const currentEnvironment$ = environmentsStore.subject$.pipe(
|
||||
map(({ environments, selectedEnvironmentIndex }) => {
|
||||
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
|
||||
const env: Environment = {
|
||||
name: "No environment",
|
||||
variables: [],
|
||||
export const currentEnvironment$: Observable<Environment | undefined> =
|
||||
environmentsStore.subject$.pipe(
|
||||
map(({ environments, selectedEnvironmentIndex }) => {
|
||||
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
|
||||
const env: Environment = {
|
||||
name: "No environment",
|
||||
variables: [],
|
||||
}
|
||||
return env
|
||||
} else if (selectedEnvironmentIndex.type === "MY_ENV") {
|
||||
return environments[selectedEnvironmentIndex.index]
|
||||
} else {
|
||||
return selectedEnvironmentIndex.environment
|
||||
}
|
||||
return env
|
||||
} else if (selectedEnvironmentIndex.type === "MY_ENV") {
|
||||
return environments[selectedEnvironmentIndex.index]
|
||||
} else {
|
||||
return selectedEnvironmentIndex.environment
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
export type AggregateEnvironment = {
|
||||
key: string
|
||||
@@ -358,7 +373,7 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
|
||||
map(([selectedEnv, globalVars]) => {
|
||||
const results: AggregateEnvironment[] = []
|
||||
|
||||
selectedEnv.variables.forEach(({ key, value }) =>
|
||||
selectedEnv?.variables.forEach(({ key, value }) =>
|
||||
results.push({ key, value, sourceEnv: selectedEnv.name })
|
||||
)
|
||||
globalVars.forEach(({ key, value }) =>
|
||||
|
||||
40
packages/hoppscotch-common/src/newstore/syncing.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { distinctUntilChanged, pluck } from "rxjs"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
|
||||
type SyncState = {
|
||||
isInitialSync: boolean
|
||||
shouldSync: boolean
|
||||
}
|
||||
|
||||
type CurrentSyncingState = {
|
||||
currentSyncingItem: SyncState
|
||||
}
|
||||
|
||||
const initialState: CurrentSyncingState = {
|
||||
currentSyncingItem: {
|
||||
isInitialSync: false,
|
||||
shouldSync: false,
|
||||
},
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
changeCurrentSyncStatus(_, { syncItem }: { syncItem: SyncState }) {
|
||||
return {
|
||||
currentSyncingItem: syncItem,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const currentSyncStore = new DispatchingStore(initialState, dispatchers)
|
||||
|
||||
export const currentSyncingStatus$ = currentSyncStore.subject$.pipe(
|
||||
pluck("currentSyncingItem"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export function changeCurrentSyncStatus(syncItem: SyncState) {
|
||||
currentSyncStore.dispatch({
|
||||
dispatcher: "changeCurrentSyncStatus",
|
||||
payload: { syncItem },
|
||||
})
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
:title="tab.document.request.name"
|
||||
class="truncate px-2"
|
||||
@dblclick="openReqRenameModal()"
|
||||
>
|
||||
<span
|
||||
class="font-semibold text-tiny"
|
||||
@@ -55,12 +56,21 @@
|
||||
@update:model-value="onTabUpdate"
|
||||
/>
|
||||
</HoppSmartWindow>
|
||||
<template #actions>
|
||||
<EnvironmentsSelector class="h-full" />
|
||||
</template>
|
||||
</HoppSmartWindows>
|
||||
</template>
|
||||
<template #sidebar>
|
||||
<HttpSidebar />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
<CollectionsEditRequest
|
||||
v-model="reqName"
|
||||
:show="showRenamingReqNameModal"
|
||||
@submit="renameReqName"
|
||||
@hide-modal="showRenamingReqNameModal = false"
|
||||
/>
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmingCloseForTabID !== null"
|
||||
:confirm="t('modal.close_unsaved_tab')"
|
||||
@@ -77,7 +87,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, onBeforeMount } from "vue"
|
||||
import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
|
||||
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||
import { useRoute } from "vue-router"
|
||||
@@ -113,16 +123,26 @@ import { useToast } from "~/composables/toast"
|
||||
import { PersistableRESTTabState } from "~/helpers/rest/tab"
|
||||
import { watchDebounced } from "@vueuse/core"
|
||||
import { oauthRedirect } from "~/helpers/oauth"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import {
|
||||
changeCurrentSyncStatus,
|
||||
currentSyncingStatus$,
|
||||
} from "~/newstore/syncing"
|
||||
|
||||
const savingRequest = ref(false)
|
||||
const confirmingCloseForTabID = ref<string | null>(null)
|
||||
const showRenamingReqNameModal = ref(false)
|
||||
const reqName = ref<string>("")
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const tabs = getActiveTabs()
|
||||
|
||||
const confirmSync = ref(false)
|
||||
const confirmSync = useReadonlyStream(currentSyncingStatus$, {
|
||||
isInitialSync: false,
|
||||
shouldSync: true,
|
||||
})
|
||||
const tabStateForSync = ref<PersistableRESTTabState | null>(null)
|
||||
|
||||
function bindRequestToURLParams() {
|
||||
@@ -166,6 +186,20 @@ const removeTab = (tabID: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const openReqRenameModal = () => {
|
||||
showRenamingReqNameModal.value = true
|
||||
reqName.value = currentActiveTab.value.document.request.name
|
||||
}
|
||||
|
||||
const renameReqName = () => {
|
||||
const tab = getTabRef(currentTabID.value)
|
||||
if (tab.value) {
|
||||
tab.value.document.request.name = reqName.value
|
||||
updateTab(tab.value)
|
||||
}
|
||||
showRenamingReqNameModal.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is closed when the confirm tab is closed by some means (even saving triggers close)
|
||||
*/
|
||||
@@ -203,29 +237,6 @@ const onSaveModalClose = () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(confirmSync, (newValue) => {
|
||||
if (newValue) {
|
||||
toast.show(t("confirm.sync"), {
|
||||
duration: 0,
|
||||
action: [
|
||||
{
|
||||
text: `${t("action.yes")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
syncTabState()
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: `${t("action.no")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const syncTabState = () => {
|
||||
if (tabStateForSync.value) loadTabsFromPersistedState(tabStateForSync.value)
|
||||
}
|
||||
@@ -264,6 +275,35 @@ function startTabStateSync(): Subscription {
|
||||
return sub
|
||||
}
|
||||
|
||||
const showSyncToast = () => {
|
||||
toast.show(t("confirm.sync"), {
|
||||
duration: 0,
|
||||
action: [
|
||||
{
|
||||
text: `${t("action.yes")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
syncTabState()
|
||||
changeCurrentSyncStatus({
|
||||
isInitialSync: true,
|
||||
shouldSync: true,
|
||||
})
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: `${t("action.no")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
changeCurrentSyncStatus({
|
||||
isInitialSync: true,
|
||||
shouldSync: false,
|
||||
})
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
function setupTabStateSync() {
|
||||
const route = useRoute()
|
||||
|
||||
@@ -279,9 +319,15 @@ function setupTabStateSync() {
|
||||
const tabStateFromSync =
|
||||
await platform.sync.tabState.loadTabStateFromSync()
|
||||
|
||||
if (tabStateFromSync) {
|
||||
if (tabStateFromSync && !confirmSync.value.isInitialSync) {
|
||||
tabStateForSync.value = tabStateFromSync
|
||||
confirmSync.value = true
|
||||
showSyncToast()
|
||||
// Have to set isInitialSync to true here because the toast is shown
|
||||
// and the user does not click on any of the actions
|
||||
changeCurrentSyncStatus({
|
||||
isInitialSync: true,
|
||||
shouldSync: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
></div>
|
||||
<div class="flex flex-col justify-between px-4 space-y-8 md:flex-row">
|
||||
<div class="flex items-end">
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-if="currentUser.photoURL"
|
||||
:url="currentUser.photoURL"
|
||||
:alt="
|
||||
@@ -44,7 +44,7 @@
|
||||
size="16"
|
||||
rounded="lg"
|
||||
/>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-else
|
||||
:initial="currentUser.displayName || currentUser.email"
|
||||
rounded="lg"
|
||||
|
||||
@@ -5,4 +5,5 @@ export type UIPlatformDef = {
|
||||
paddingTop?: Ref<string>
|
||||
paddingLeft?: Ref<string>
|
||||
}
|
||||
onCodemirrorInstanceMount?: (element: HTMLElement) => void
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hoppscotch - Open source API development ecosystem</title>
|
||||
<title>Hoppscotch • Open source API development ecosystem</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@@ -8,7 +8,9 @@ export const APP_INFO = {
|
||||
keywords:
|
||||
"hoppscotch, hopp scotch, hoppscotch online, hoppscotch app, postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket, sse, graphql, socketio",
|
||||
app: {
|
||||
background: "#202124",
|
||||
background: "#181818",
|
||||
lightThemeColor: "#ffffff",
|
||||
darkThemeColor: "#181818",
|
||||
},
|
||||
social: {
|
||||
twitter: "@hoppscotch_io",
|
||||
@@ -108,7 +110,17 @@ export const META_TAGS = (env: Record<string, string>): IHTMLTag[] => [
|
||||
// PWA
|
||||
{
|
||||
name: "theme-color",
|
||||
content: APP_INFO.app.background,
|
||||
content: APP_INFO.app.darkThemeColor,
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
},
|
||||
{
|
||||
name: "theme-color",
|
||||
content: APP_INFO.app.lightThemeColor,
|
||||
media: "(prefers-color-scheme: light)",
|
||||
},
|
||||
{
|
||||
name: "supported-color-schemes",
|
||||
content: "light dark",
|
||||
},
|
||||
{
|
||||
name: "mask-icon",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/selfhost-web",
|
||||
"private": true,
|
||||
"version": "2023.4.1",
|
||||
"version": "2023.4.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:vite": "vite",
|
||||
|
||||
@@ -150,20 +150,54 @@ export default defineConfig({
|
||||
short_name: APP_INFO.name,
|
||||
description: APP_INFO.shortDescription,
|
||||
start_url: "/?source=pwa",
|
||||
id: "/?source=pwa",
|
||||
protocol_handlers: [
|
||||
{
|
||||
protocol: "web+hoppscotch",
|
||||
url: "/%s",
|
||||
},
|
||||
{
|
||||
protocol: "web+hopp",
|
||||
url: "/%s",
|
||||
},
|
||||
],
|
||||
background_color: APP_INFO.app.background,
|
||||
theme_color: APP_INFO.app.background,
|
||||
icons: [
|
||||
{
|
||||
src: "/icon.png",
|
||||
sizes: "512x512",
|
||||
src: "/icons/pwa-16x16.png",
|
||||
sizes: "16x16",
|
||||
type: "image/png",
|
||||
purpose: "any maskable",
|
||||
},
|
||||
{
|
||||
src: "/logo.svg",
|
||||
sizes: "48x48 72x72 96x96 128x128 256x256 512x512",
|
||||
type: "image/svg+xml",
|
||||
purpose: "any maskable",
|
||||
src: "/icons/pwa-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/icons/pwa-128x128.png",
|
||||
sizes: "128x128",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/icons/pwa-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/icons/pwa-256x256.png",
|
||||
sizes: "256x256",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/icons/pwa-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/icons/pwa-1024x1024.png",
|
||||
sizes: "1024x1024",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ input::placeholder,
|
||||
textarea::placeholder,
|
||||
.cm-placeholder {
|
||||
@apply text-secondary;
|
||||
@apply opacity-35;
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
input,
|
||||
@@ -323,9 +323,10 @@ pre.ace_editor {
|
||||
@apply after:justify-center;
|
||||
@apply after:pointer-events-none;
|
||||
@apply after:font-icon;
|
||||
@apply after:text-secondaryLight;
|
||||
@apply after:text-current;
|
||||
@apply after:right-3;
|
||||
@apply after:content-["\e313"];
|
||||
@apply after:text-lg;
|
||||
}
|
||||
|
||||
.info-response {
|
||||
@@ -416,7 +417,6 @@ pre.ace_editor {
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply bg-primaryLight;
|
||||
@apply before:absolute;
|
||||
@apply before:inset-0;
|
||||
@apply before:bg-accentLight;
|
||||
@@ -424,48 +424,47 @@ pre.ace_editor {
|
||||
@apply before:z-20;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply after:absolute;
|
||||
@apply after:inset-0;
|
||||
@apply after:z-20;
|
||||
@apply after:transition;
|
||||
@apply after:flex;
|
||||
@apply after:items-center;
|
||||
@apply after:justify-center;
|
||||
@apply after:text-dividerDark;
|
||||
@apply after:font-icon;
|
||||
@apply hover:before:opacity-100;
|
||||
@apply hover:after:text-accentDark;
|
||||
}
|
||||
|
||||
.no-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply bg-primaryLight;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-1;
|
||||
@apply w-0;
|
||||
@apply before:-left-0.5;
|
||||
@apply before:-right-0.5;
|
||||
@apply before:h-full;
|
||||
@apply after:content-["\e5d4"];
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-1;
|
||||
@apply h-0;
|
||||
@apply before:-top-0.5;
|
||||
@apply before:-bottom-0.5;
|
||||
@apply before:w-full;
|
||||
@apply after:content-["\e5d3"];
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-0.5;
|
||||
@apply w-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-0.5;
|
||||
@apply h-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal .splitpanes__pane {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.splitpanes--vertical .splitpanes__pane {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
}
|
||||
|
||||
@mixin dark-theme {
|
||||
--primary-color: theme('colors.neutral.900');
|
||||
--primary-color: theme('colors.dark.800');
|
||||
--primary-light-color: theme('colors.dark.600');
|
||||
--primary-dark-color: theme('colors.neutral.800');
|
||||
--primary-contrast-color: #161616;
|
||||
--primary-contrast-color: theme('colors.neutral.900');
|
||||
|
||||
--secondary-color: theme('colors.neutral.400');
|
||||
--secondary-light-color: theme('colors.neutral.500');
|
||||
--secondary-dark-color: theme('colors.neutral.100');
|
||||
--secondary-dark-color: theme('colors.neutral.50');
|
||||
|
||||
--divider-color: theme('colors.neutral.800');
|
||||
--divider-light-color: theme('colors.dark.500');
|
||||
--divider-dark-color: theme('colors.dark.300');
|
||||
|
||||
--error-color: theme('colors.stone.800');
|
||||
--tooltip-color: theme('colors.neutral.100');
|
||||
--popover-color: theme('colors.dark.700');
|
||||
@@ -24,15 +27,18 @@
|
||||
|
||||
@mixin light-theme {
|
||||
--primary-color: theme('colors.white');
|
||||
--primary-light-color: theme('colors.neutral.50');
|
||||
--primary-dark-color: theme('colors.neutral.100');
|
||||
--primary-contrast-color: #fefefe;
|
||||
--secondary-color: theme('colors.neutral.500');
|
||||
--secondary-light-color: theme('colors.neutral.400');
|
||||
--secondary-dark-color: theme('colors.neutral.900');
|
||||
--primary-light-color: theme('colors.gray.50');
|
||||
--primary-dark-color: theme('colors.gray.100');
|
||||
--primary-contrast-color: theme('colors.light.50');
|
||||
|
||||
--secondary-color: theme('colors.gray.500');
|
||||
--secondary-light-color: theme('colors.gray.400');
|
||||
--secondary-dark-color: theme('colors.gray.900');
|
||||
|
||||
--divider-color: theme('colors.gray.100');
|
||||
--divider-light-color: theme('colors.neutral.100');
|
||||
--divider-dark-color: theme('colors.neutral.300');
|
||||
--divider-light-color: theme('colors.gray.100');
|
||||
--divider-dark-color: theme('colors.gray.300');
|
||||
|
||||
--error-color: theme('colors.yellow.100');
|
||||
--tooltip-color: theme('colors.neutral.800');
|
||||
--popover-color: theme('colors.white');
|
||||
@@ -43,16 +49,19 @@
|
||||
--primary-color: theme('colors.dark.900');
|
||||
--primary-light-color: theme('colors.neutral.900');
|
||||
--primary-dark-color: theme('colors.dark.800');
|
||||
--primary-contrast-color: #0e0e0e;
|
||||
--primary-contrast-color: theme('colors.dark.900');
|
||||
|
||||
--secondary-color: theme('colors.neutral.400');
|
||||
--secondary-light-color: theme('colors.neutral.500');
|
||||
--secondary-dark-color: theme('colors.neutral.100');
|
||||
--divider-color: theme('colors.neutral.800');
|
||||
|
||||
--divider-color: theme('colors.dark.600');
|
||||
--divider-light-color: theme('colors.dark.800');
|
||||
--divider-dark-color: theme('colors.dark.300');
|
||||
--divider-dark-color: theme('colors.dark.200');
|
||||
|
||||
--error-color: theme('colors.stone.900');
|
||||
--tooltip-color: theme('colors.neutral.100');
|
||||
--popover-color: theme('colors.dark.600');
|
||||
--popover-color: theme('colors.dark.900');
|
||||
--editor-theme: 'twilight';
|
||||
}
|
||||
|
||||
@@ -188,6 +197,67 @@
|
||||
--gradient-to-color: theme('colors.pink.600');
|
||||
}
|
||||
|
||||
:root {
|
||||
@include base-theme;
|
||||
@include dark-theme;
|
||||
@include green-theme;
|
||||
@include dark-editor-theme;
|
||||
}
|
||||
|
||||
:root.light {
|
||||
@include light-theme;
|
||||
@include light-editor-theme;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@include dark-theme;
|
||||
@include dark-editor-theme;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root.black {
|
||||
@include black-theme;
|
||||
@include black-editor-theme;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-accent='blue'] {
|
||||
@include blue-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='green'] {
|
||||
@include green-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='teal'] {
|
||||
@include teal-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='indigo'] {
|
||||
@include indigo-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='purple'] {
|
||||
@include purple-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='orange'] {
|
||||
@include orange-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='pink'] {
|
||||
@include pink-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='red'] {
|
||||
@include red-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='yellow'] {
|
||||
@include yellow-theme;
|
||||
}
|
||||
|
||||
@mixin font-small {
|
||||
--font-size-body: 0.75rem;
|
||||
--line-height-body: 1rem;
|
||||
@@ -236,65 +306,6 @@
|
||||
--sidebar-primary-sticky-fold: 2.5rem;
|
||||
}
|
||||
|
||||
:root {
|
||||
@include base-theme;
|
||||
@include dark-theme;
|
||||
@include green-theme;
|
||||
@include dark-editor-theme;
|
||||
@include font-medium;
|
||||
}
|
||||
|
||||
:root.light {
|
||||
@include light-theme;
|
||||
@include light-editor-theme;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@include dark-theme;
|
||||
@include dark-editor-theme;
|
||||
}
|
||||
|
||||
:root.black {
|
||||
@include black-theme;
|
||||
@include black-editor-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='blue'] {
|
||||
@include blue-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='green'] {
|
||||
@include green-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='teal'] {
|
||||
@include teal-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='indigo'] {
|
||||
@include indigo-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='purple'] {
|
||||
@include purple-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='orange'] {
|
||||
@include orange-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='pink'] {
|
||||
@include pink-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='red'] {
|
||||
@include red-theme;
|
||||
}
|
||||
|
||||
:root[data-accent='yellow'] {
|
||||
@include yellow-theme;
|
||||
}
|
||||
|
||||
:root[data-font-size='small'] {
|
||||
@include font-small;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "hoppscotch-sh-admin",
|
||||
"private": true,
|
||||
"version": "2023.4.1",
|
||||
"version": "2023.4.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
arrow
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
@@ -37,7 +37,7 @@
|
||||
:alt="currentUser.displayName ?? 'No Name'"
|
||||
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
|
||||
/>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="currentUser.displayName ?? currentUser.email ?? 'No Name'"
|
||||
|
||||
@@ -6,7 +6,7 @@ const isAdmin = () => {
|
||||
return user ? user.isAdmin : false;
|
||||
};
|
||||
|
||||
const GUEST_ROUTES = ['index', 'magic-link'];
|
||||
const GUEST_ROUTES = ['index', 'enter'];
|
||||
|
||||
const isGuestRoute = (to: unknown) => GUEST_ROUTES.includes(to as string);
|
||||
|
||||
|
||||
@@ -1,3 +1,64 @@
|
||||
* {
|
||||
@apply backface-hidden;
|
||||
@apply before:backface-hidden;
|
||||
@apply after:backface-hidden;
|
||||
@apply selection:bg-accentDark;
|
||||
@apply selection:text-accentContrast;
|
||||
}
|
||||
|
||||
:root {
|
||||
@apply antialiased;
|
||||
accent-color: var(--accent-color);
|
||||
font-variant-ligatures: common-ligatures;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
@apply border-solid border-l border-dividerLight border-t-0 border-b-0 border-r-0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-divider bg-clip-content;
|
||||
@apply rounded-full;
|
||||
@apply border-solid border-transparent border-4;
|
||||
@apply hover:bg-dividerDark;
|
||||
@apply hover:bg-clip-content;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-4;
|
||||
@apply h-0;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder,
|
||||
.cm-placeholder {
|
||||
@apply text-secondary;
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
@apply text-secondaryDark;
|
||||
@apply font-medium;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-primary;
|
||||
@apply text-secondary text-body;
|
||||
@apply font-medium;
|
||||
@apply select-none;
|
||||
@apply overflow-x-hidden;
|
||||
@apply leading-body;
|
||||
animation: fade 300ms forwards;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
@apply opacity-0;
|
||||
@@ -69,6 +130,89 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip {
|
||||
.tippy-box {
|
||||
@apply shadow-none;
|
||||
@apply fixed;
|
||||
@apply inline-flex;
|
||||
@apply -mt-8;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="tooltip"] {
|
||||
@apply bg-tooltip;
|
||||
@apply border-solid border-tooltip;
|
||||
@apply rounded;
|
||||
@apply shadow;
|
||||
|
||||
.tippy-content {
|
||||
@apply flex;
|
||||
@apply text-tiny text-primary;
|
||||
@apply font-semibold;
|
||||
@apply py-1 px-2;
|
||||
@apply truncate;
|
||||
@apply leading-normal;
|
||||
@apply items-center;
|
||||
|
||||
kbd {
|
||||
@apply hidden;
|
||||
@apply font-sans;
|
||||
@apply bg-gray-500/45;
|
||||
@apply text-primaryLight;
|
||||
@apply rounded-sm;
|
||||
@apply px-1;
|
||||
@apply my-0 ml-1;
|
||||
@apply truncate;
|
||||
@apply sm:inline-flex;
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
@apply transition;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-tooltip;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
@apply fill-tooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="popover"] {
|
||||
@apply bg-popover;
|
||||
@apply border-solid border-dividerDark;
|
||||
@apply rounded;
|
||||
@apply shadow-lg;
|
||||
|
||||
.tippy-content {
|
||||
@apply flex flex-col;
|
||||
@apply max-h-56;
|
||||
@apply items-stretch;
|
||||
@apply overflow-y-auto;
|
||||
@apply text-secondary text-body;
|
||||
@apply p-2;
|
||||
@apply leading-normal;
|
||||
@apply focus:outline-none;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
svg:first-child {
|
||||
@apply fill-dividerDark;
|
||||
}
|
||||
|
||||
svg:last-child {
|
||||
@apply fill-popover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-v-tippy] {
|
||||
@apply flex flex-1;
|
||||
}
|
||||
@@ -102,6 +246,252 @@ hr {
|
||||
@apply focus-visible:border-dividerDark;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
button {
|
||||
@apply truncate;
|
||||
@apply transition;
|
||||
@apply text-body;
|
||||
@apply leading-body;
|
||||
@apply focus:outline-none;
|
||||
@apply disabled:cursor-not-allowed;
|
||||
}
|
||||
|
||||
.input[type="file"],
|
||||
.input[type="radio"],
|
||||
#installPWA {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.floating-input ~ label {
|
||||
@apply absolute;
|
||||
@apply px-2 py-0.5;
|
||||
@apply m-2;
|
||||
@apply rounded;
|
||||
@apply transition;
|
||||
@apply origin-top-left;
|
||||
}
|
||||
|
||||
.floating-input:focus-within ~ label,
|
||||
.floating-input:not(:placeholder-shown) ~ label {
|
||||
@apply bg-primary;
|
||||
@apply transform;
|
||||
@apply origin-top-left;
|
||||
@apply scale-75;
|
||||
@apply translate-x-1 -translate-y-4;
|
||||
}
|
||||
|
||||
.floating-input:focus-within ~ label {
|
||||
@apply text-secondaryDark;
|
||||
}
|
||||
|
||||
.floating-input ~ .end-actions {
|
||||
@apply absolute;
|
||||
@apply right-0.2;
|
||||
@apply inset-y-0;
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
}
|
||||
|
||||
.floating-input:has(~ .end-actions) {
|
||||
@apply pr-12;
|
||||
}
|
||||
|
||||
pre.ace_editor {
|
||||
@apply font-mono;
|
||||
@apply resize-none;
|
||||
@apply z-0;
|
||||
}
|
||||
|
||||
.select {
|
||||
@apply appearance-none;
|
||||
@apply cursor-pointer;
|
||||
|
||||
&::-ms-expand {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
@apply flex flex-1;
|
||||
@apply relative;
|
||||
@apply after:absolute;
|
||||
@apply after:flex;
|
||||
@apply after:inset-y-0;
|
||||
@apply after:items-center;
|
||||
@apply after:justify-center;
|
||||
@apply after:pointer-events-none;
|
||||
@apply after:font-icon;
|
||||
@apply after:text-current;
|
||||
@apply after:right-3;
|
||||
@apply after:content-["\e313"];
|
||||
@apply after:text-lg;
|
||||
}
|
||||
|
||||
.info-response {
|
||||
@apply text-pink-500;
|
||||
}
|
||||
|
||||
.success-response {
|
||||
@apply text-green-500;
|
||||
}
|
||||
|
||||
.redir-response {
|
||||
@apply text-yellow-500;
|
||||
}
|
||||
|
||||
.cl-error-response {
|
||||
@apply text-red-500;
|
||||
}
|
||||
|
||||
.sv-error-response {
|
||||
@apply text-red-600;
|
||||
}
|
||||
|
||||
.missing-data-response {
|
||||
@apply text-secondaryLight;
|
||||
}
|
||||
|
||||
.toasted-container {
|
||||
@apply max-w-md;
|
||||
|
||||
.toasted {
|
||||
&.toasted-primary {
|
||||
@apply px-4 py-2;
|
||||
@apply bg-tooltip;
|
||||
@apply border-secondaryDark;
|
||||
@apply text-primary text-body;
|
||||
@apply justify-between;
|
||||
@apply shadow-lg;
|
||||
@apply font-semibold;
|
||||
@apply transition;
|
||||
@apply leading-body;
|
||||
@apply sm:rounded;
|
||||
@apply sm:border;
|
||||
|
||||
.action {
|
||||
@apply relative;
|
||||
@apply flex flex-shrink-0;
|
||||
@apply text-body;
|
||||
@apply px-4;
|
||||
@apply my-1;
|
||||
@apply ml-auto;
|
||||
@apply normal-case;
|
||||
@apply font-semibold;
|
||||
@apply leading-body;
|
||||
@apply tracking-normal;
|
||||
@apply rounded;
|
||||
@apply last:ml-4;
|
||||
@apply sm:ml-8;
|
||||
@apply before:absolute;
|
||||
@apply before:bg-current;
|
||||
@apply before:opacity-10;
|
||||
@apply before:inset-0;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply hover:no-underline;
|
||||
@apply hover:before:opacity-20;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
@apply bg-accent;
|
||||
@apply text-accentContrast;
|
||||
@apply border-accentDark;
|
||||
}
|
||||
|
||||
&.error {
|
||||
@apply bg-red-200;
|
||||
@apply text-red-800;
|
||||
@apply border-red-400;
|
||||
}
|
||||
|
||||
&.success {
|
||||
@apply bg-green-200;
|
||||
@apply text-green-800;
|
||||
@apply border-green-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smart-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
@apply before:absolute;
|
||||
@apply before:inset-0;
|
||||
@apply before:bg-accentLight;
|
||||
@apply before:opacity-0;
|
||||
@apply before:z-20;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply hover:before:opacity-100;
|
||||
}
|
||||
|
||||
.no-splitter .splitpanes__splitter {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-0;
|
||||
@apply before:-left-0.5;
|
||||
@apply before:-right-0.5;
|
||||
@apply before:h-full;
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.smart-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-0;
|
||||
@apply before:-top-0.5;
|
||||
@apply before:-bottom-0.5;
|
||||
@apply before:w-full;
|
||||
@apply bg-divider;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--vertical > .splitpanes__splitter {
|
||||
@apply w-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.no-splitter.splitpanes--horizontal > .splitpanes__splitter {
|
||||
@apply h-0;
|
||||
@apply pointer-events-none;
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal .splitpanes__pane {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.splitpanes--vertical .splitpanes__pane {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
@apply select-auto;
|
||||
@apply outline-none #{!important};
|
||||
|
||||
.cm-activeLine {
|
||||
@apply bg-primaryLight;
|
||||
}
|
||||
|
||||
.cm-activeLineGutter {
|
||||
@apply bg-primaryDark;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
.cm-line::selection {
|
||||
@apply bg-accentDark #{!important};
|
||||
@apply text-accentContrast #{!important};
|
||||
}
|
||||
|
||||
.cm-line ::selection {
|
||||
@apply bg-accentDark #{!important};
|
||||
@apply text-accentContrast #{!important};
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
@apply inline-flex;
|
||||
@apply font-sans;
|
||||
@@ -118,3 +508,62 @@ hr {
|
||||
@apply shadow-sm;
|
||||
@apply <sm:hidden;
|
||||
}
|
||||
|
||||
.capitalize-first {
|
||||
@apply first-letter:capitalize;
|
||||
}
|
||||
|
||||
details {
|
||||
@apply select-none;
|
||||
}
|
||||
|
||||
details summary::-webkit-details-marker {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
details summary .indicator {
|
||||
@apply transition;
|
||||
}
|
||||
|
||||
details[open] summary .indicator {
|
||||
@apply transform;
|
||||
@apply rotate-90;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
main {
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
.env-highlight {
|
||||
@apply text-accentContrast;
|
||||
|
||||
&.env-found {
|
||||
@apply bg-accentDark;
|
||||
@apply hover:bg-accent;
|
||||
}
|
||||
|
||||
&.env-not-found {
|
||||
@apply bg-red-500;
|
||||
@apply hover:bg-red-600;
|
||||
}
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
@apply bg-accent #{!important};
|
||||
}
|
||||
|
||||
.color-picker[type="color"] {
|
||||
@apply appearance-none;
|
||||
}
|
||||
|
||||
.color-picker[type="color"]::-webkit-color-swatch-wrapper {
|
||||
@apply rounded;
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
.color-picker[type="color"]::-webkit-color-swatch {
|
||||
@apply rounded;
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
@@ -207,16 +207,19 @@
|
||||
:root.light {
|
||||
@include light-theme;
|
||||
@include light-editor-theme;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@include dark-theme;
|
||||
@include dark-editor-theme;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root.black {
|
||||
@include black-theme;
|
||||
@include black-editor-theme;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-accent="blue"] {
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
<template>
|
||||
<div class="autocomplete-wrapper">
|
||||
<input ref="acInput" v-model="text" type="text" autocomplete="off" :placeholder="placeholder" :spellcheck="spellcheck"
|
||||
:autocapitalize="autocapitalize" :class="styles" @input.stop="onInput" @keyup="updateSuggestions"
|
||||
@click="updateSuggestions" @keydown="handleKeystroke" @change="emit('change', $event)" />
|
||||
|
||||
<ul v-if="suggestions.length > 0 && suggestionsVisible" class="suggestions"
|
||||
:style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }">
|
||||
<li v-for="(suggestion, index) in suggestions" :key="`suggestion-${index}`"
|
||||
:class="{ active: currentSuggestionIndex === index }" @click.prevent="forceSuggestion(suggestion)">
|
||||
<input
|
||||
ref="acInput"
|
||||
v-model="text"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="placeholder"
|
||||
:spellcheck="spellcheck"
|
||||
:autocapitalize="autocapitalize"
|
||||
:class="styles"
|
||||
@input.stop="onInput"
|
||||
@keyup="updateSuggestions"
|
||||
@click="updateSuggestions"
|
||||
@keydown="handleKeystroke"
|
||||
@change="emit('change', $event)"
|
||||
/>
|
||||
<ul v-if="suggestions.length > 0 && suggestionsVisible" class="suggestions">
|
||||
<li
|
||||
v-for="(suggestion, index) in suggestions"
|
||||
:key="`suggestion-${index}`"
|
||||
:class="{ active: currentSuggestionIndex === index }"
|
||||
@click.prevent="forceSuggestion(suggestion)"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -62,11 +76,9 @@ const emit = defineEmits<{
|
||||
|
||||
const text = ref(props.value)
|
||||
const selectionStart = ref(0)
|
||||
const suggestionsOffsetLeft = ref(0)
|
||||
const currentSuggestionIndex = ref(-1)
|
||||
const suggestionsVisible = ref(false)
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
updateSuggestions({
|
||||
target: acInput,
|
||||
@@ -82,14 +94,11 @@ const suggestions = computed(() => {
|
||||
entry.toLowerCase().startsWith(input.toLowerCase()) &&
|
||||
input.toLowerCase() !== entry.toLowerCase()
|
||||
)
|
||||
// Cut off the part that's already been typed.
|
||||
.map((entry) => entry.substring(selectionStart.value))
|
||||
// We only want the top 10 suggestions.
|
||||
.slice(0, 10)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
function updateSuggestions(event: any) {
|
||||
// Hide suggestions if ESC pressed.
|
||||
if (event.code && event.code === "Escape") {
|
||||
@@ -102,18 +111,16 @@ function updateSuggestions(event: any) {
|
||||
// As suggestions is a reactive property, this implicitly
|
||||
// causes suggestions to update.
|
||||
selectionStart.value = acInput.value?.selectionStart ?? -1
|
||||
suggestionsOffsetLeft.value = 12 * selectionStart.value
|
||||
suggestionsVisible.value = true
|
||||
}
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
emit('input', (e.target as HTMLInputElement).value)
|
||||
emit("input", (e.target as HTMLInputElement).value)
|
||||
updateSuggestions(e)
|
||||
}
|
||||
|
||||
function forceSuggestion(str: string) {
|
||||
const input = text.value.substring(0, selectionStart.value)
|
||||
text.value = input + str
|
||||
function forceSuggestion(suggestion: string) {
|
||||
text.value = suggestion
|
||||
|
||||
selectionStart.value = text.value.length
|
||||
suggestionsVisible.value = true
|
||||
@@ -124,18 +131,9 @@ function forceSuggestion(str: string) {
|
||||
|
||||
function handleKeystroke(event: any) {
|
||||
switch (event.code) {
|
||||
case "Enter":
|
||||
event.preventDefault()
|
||||
if (currentSuggestionIndex.value > -1)
|
||||
forceSuggestion(
|
||||
suggestions.value.find(
|
||||
(_item, index) => index === currentSuggestionIndex.value
|
||||
)!
|
||||
)
|
||||
break
|
||||
|
||||
case "ArrowUp":
|
||||
event.preventDefault()
|
||||
|
||||
currentSuggestionIndex.value =
|
||||
currentSuggestionIndex.value - 1 >= 0
|
||||
? currentSuggestionIndex.value - 1
|
||||
@@ -144,53 +142,63 @@ function handleKeystroke(event: any) {
|
||||
|
||||
case "ArrowDown":
|
||||
event.preventDefault()
|
||||
|
||||
currentSuggestionIndex.value =
|
||||
currentSuggestionIndex.value < suggestions.value.length - 1
|
||||
? currentSuggestionIndex.value + 1
|
||||
: suggestions.value.length - 1
|
||||
break
|
||||
|
||||
case "Enter":
|
||||
event.preventDefault()
|
||||
|
||||
if (currentSuggestionIndex.value > -1)
|
||||
forceSuggestion(
|
||||
suggestions.value.find(
|
||||
(_item, index) => index === currentSuggestionIndex.value
|
||||
)!
|
||||
)
|
||||
break
|
||||
|
||||
case "Tab": {
|
||||
event.preventDefault()
|
||||
|
||||
const activeSuggestion =
|
||||
suggestions.value[
|
||||
currentSuggestionIndex.value >= 0 ? currentSuggestionIndex.value : 0
|
||||
currentSuggestionIndex.value >= 0 ? currentSuggestionIndex.value : 0
|
||||
]
|
||||
|
||||
if (!activeSuggestion) {
|
||||
return
|
||||
}
|
||||
if (!activeSuggestion) return
|
||||
|
||||
event.preventDefault()
|
||||
const input = text.value.substring(0, selectionStart.value)
|
||||
text.value = input + activeSuggestion
|
||||
forceSuggestion(activeSuggestion)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.autocomplete-wrapper {
|
||||
@apply relative;
|
||||
@apply contents;
|
||||
|
||||
input:focus+ul.suggestions,
|
||||
input:focus + ul.suggestions,
|
||||
ul.suggestions:hover {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
ul.suggestions {
|
||||
@apply absolute;
|
||||
@apply hidden;
|
||||
@apply bg-popover;
|
||||
@apply absolute;
|
||||
@apply mx-2;
|
||||
@apply left-0;
|
||||
@apply -left-px;
|
||||
@apply z-50;
|
||||
@apply shadow-lg;
|
||||
@apply max-h-46;
|
||||
@apply overflow-y-auto;
|
||||
top: calc(100% - 4px);
|
||||
@apply border-b border-x border-divider;
|
||||
|
||||
top: calc(100% + 1px);
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
li {
|
||||
@@ -198,15 +206,16 @@ function handleKeystroke(event: any) {
|
||||
@apply block;
|
||||
@apply py-2 px-4;
|
||||
@apply text-secondary;
|
||||
@apply font-semibold;
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply bg-accentDark;
|
||||
@apply text-accentContrast;
|
||||
@apply bg-primaryDark;
|
||||
@apply text-secondaryDark;
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
:src="url"
|
||||
:alt="alt"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
@@ -21,8 +22,7 @@
|
||||
<template v-if="initial && initial.charAt(0).toUpperCase()">
|
||||
{{ initial.charAt(0).toUpperCase() }}
|
||||
</template>
|
||||
|
||||
<icon-lucide-user v-else></icon-lucide-user>
|
||||
<icon-lucide-user v-else />
|
||||
</div>
|
||||
<span
|
||||
v-if="indicator"
|
||||
@@ -33,55 +33,40 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
url: string
|
||||
alt: string
|
||||
indicator: boolean
|
||||
indicatorStyles: string
|
||||
rounded: string
|
||||
size: string
|
||||
initial: string | undefined | null
|
||||
}>(),
|
||||
{
|
||||
url: "",
|
||||
alt: "Profile picture",
|
||||
indicator: false,
|
||||
indicatorStyles: "bg-green-500",
|
||||
rounded: "full",
|
||||
size: "5",
|
||||
initial: "",
|
||||
}
|
||||
)
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: 'Profile picture',
|
||||
},
|
||||
indicator: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
indicatorStyles: {
|
||||
type: String,
|
||||
default: 'bg-green-500',
|
||||
},
|
||||
rounded: {
|
||||
type: String,
|
||||
default: 'full',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '5',
|
||||
},
|
||||
initial: {
|
||||
type: String as PropType<string | undefined | null>,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toHex(initial: string) {
|
||||
let hash = 0;
|
||||
if (initial.length === 0) return hash;
|
||||
for (let i = 0; i < initial.length; i++) {
|
||||
hash = initial.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash = hash & hash;
|
||||
}
|
||||
let color = '#';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 255;
|
||||
color += `00${value.toString(16)}`.slice(-2);
|
||||
}
|
||||
return color;
|
||||
},
|
||||
},
|
||||
});
|
||||
const toHex = (initial: string) => {
|
||||
let hash = 0
|
||||
if (initial.length === 0) return hash
|
||||
for (let i = 0; i < initial.length; i++) {
|
||||
hash = initial.charCodeAt(i) + ((hash << 5) - hash)
|
||||
hash = hash & hash
|
||||
}
|
||||
let color = "#"
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const value = (hash >> (i * 8)) & 255
|
||||
color += `00${value.toString(16)}`.slice(-2)
|
||||
}
|
||||
return color
|
||||
}
|
||||
</script>
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
||||
<div
|
||||
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto tabs bg-primaryLight group-tabs"
|
||||
class="relative sticky top-0 z-10 flex-shrink-0 overflow-x-auto divide-x divide-dividerLight bg-primaryLight tabs group-tabs"
|
||||
>
|
||||
<div
|
||||
class="flex flex-1 flex-shrink-0 w-0 overflow-x-auto"
|
||||
ref="scrollContainer"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between divide-x divide-divider"
|
||||
class="flex justify-between divide-x divide-dividerLight"
|
||||
@wheel.prevent="scroll"
|
||||
>
|
||||
<div class="flex">
|
||||
@@ -23,7 +23,8 @@
|
||||
<template #item="{ element: [tabID, tabMeta] }">
|
||||
<button
|
||||
:key="`removable-tab-${tabID}`"
|
||||
class="tab group px-2"
|
||||
:id="`removable-tab-${tabID}`"
|
||||
class="px-2 tab group"
|
||||
:class="[{ active: modelValue === tabID }]"
|
||||
:aria-label="tabMeta.label || ''"
|
||||
role="button"
|
||||
@@ -39,14 +40,14 @@
|
||||
|
||||
<div
|
||||
v-if="!tabMeta.tabhead"
|
||||
class="truncate w-full text-left px-2"
|
||||
class="w-full px-2 text-left truncate"
|
||||
>
|
||||
<span class="truncate">
|
||||
{{ tabMeta.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="truncate w-full text-left">
|
||||
<div v-else class="w-full text-left truncate">
|
||||
<component :is="tabMeta.tabhead" />
|
||||
</div>
|
||||
|
||||
@@ -72,7 +73,7 @@
|
||||
},
|
||||
'close',
|
||||
]"
|
||||
class="!p-0.25 rounded"
|
||||
class="rounded !p-0.25"
|
||||
@click.stop="emit('removeTab', tabID)"
|
||||
/>
|
||||
</button>
|
||||
@@ -80,40 +81,42 @@
|
||||
</draggable>
|
||||
</div>
|
||||
<div
|
||||
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-8"
|
||||
class="sticky right-0 flex items-center justify-center flex-shrink-0 overflow-x-auto z-14"
|
||||
>
|
||||
<slot name="actions">
|
||||
<span
|
||||
v-if="canAddNewTab"
|
||||
class="flex items-center justify-center px-3 bg-primaryLight z-8 h-full"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="newText ?? t?.('action.new') ?? 'New'"
|
||||
:icon="IconPlus"
|
||||
class="rounded !text-secondaryDark !p-1"
|
||||
filled
|
||||
@click="addTab"
|
||||
/>
|
||||
</span>
|
||||
</slot>
|
||||
<span
|
||||
v-if="canAddNewTab"
|
||||
class="flex items-center justify-center h-full px-3 bg-primaryLight z-8"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="newText ?? t?.('action.new') ?? 'New'"
|
||||
:icon="IconPlus"
|
||||
class="rounded create-new-tab !text-secondaryDark !p-1"
|
||||
filled
|
||||
@click="addTab"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasActions" :class="mdAndLarger ? 'w-64' : 'w-16'">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
:max="MAX_SCROLL_VALUE"
|
||||
v-model="thumbPosition"
|
||||
class="slider absolute bottom-0 hidden left-0"
|
||||
class="absolute bottom-0 left-0 hidden slider"
|
||||
:class="{
|
||||
'!block': scrollThumb.show,
|
||||
}"
|
||||
:style="{
|
||||
'--thumb-width': scrollThumb.width + 'px',
|
||||
}"
|
||||
style="width: calc(100% - 3rem)"
|
||||
:style="[
|
||||
`--thumb-width: ${scrollThumb.width}px`,
|
||||
`width: calc(100% - ${hasActions ? mdAndLarger ? '19rem' : '7rem' : '3rem'})`,
|
||||
]"
|
||||
id="myRange"
|
||||
/>
|
||||
</div>
|
||||
@@ -131,8 +134,17 @@ import { pipe } from "fp-ts/function"
|
||||
import { not } from "fp-ts/Predicate"
|
||||
import * as A from "fp-ts/Array"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { ref, ComputedRef, computed, provide, inject, watch } from "vue"
|
||||
import { useElementSize } from "@vueuse/core"
|
||||
import {
|
||||
ref,
|
||||
ComputedRef,
|
||||
computed,
|
||||
provide,
|
||||
inject,
|
||||
watch,
|
||||
nextTick,
|
||||
useSlots,
|
||||
} from "vue"
|
||||
import { breakpointsTailwind, useBreakpoints, useElementSize } from "@vueuse/core"
|
||||
import type { Slot } from "vue"
|
||||
import draggable from "vuedraggable-es"
|
||||
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
|
||||
@@ -155,6 +167,9 @@ export type TabProvider = {
|
||||
removeTabEntry: (tabID: string) => void
|
||||
}
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const mdAndLarger = breakpoints.greater("md")
|
||||
|
||||
const { t } = inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -182,13 +197,20 @@ const emit = defineEmits<{
|
||||
(e: "addTab"): void
|
||||
}>()
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const hasActions = computed(() => {
|
||||
return !!slots.actions
|
||||
})
|
||||
|
||||
const throwError = (message: string): never => {
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const TAB_WIDTH = 184
|
||||
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
||||
const tabStyles = computed(() => ({
|
||||
maxWidth: `${tabEntries.value.length * 184}px`,
|
||||
maxWidth: `${tabEntries.value.length * TAB_WIDTH}px`,
|
||||
width: "100%",
|
||||
minWidth: "0px",
|
||||
// transition: "max-width 0.2s",
|
||||
@@ -292,6 +314,49 @@ watch(thumbPosition, (newVal) => {
|
||||
const maxScroll = scrollWidth - clientWidth
|
||||
scrollContainer.value!.scrollLeft = maxScroll * (newVal / MAX_SCROLL_VALUE)
|
||||
})
|
||||
|
||||
/*
|
||||
* Watch TabID changes
|
||||
* and scroll to the tab if it's not visible
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(tabID) => {
|
||||
nextTick(() => {
|
||||
const element = document.getElementById(`removable-tab-${tabID}`)
|
||||
|
||||
const changeThumbPosition: IntersectionObserverCallback = (
|
||||
entries,
|
||||
observer
|
||||
) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.target === element && entry.intersectionRatio >= 1.0) {
|
||||
// Element is visible now. Stop listening for intersection changes
|
||||
observer.disconnect()
|
||||
|
||||
// We still need setTimeout here because the element might not be fully in position yet
|
||||
setTimeout(() => {
|
||||
const { scrollWidth, clientWidth, scrollLeft } =
|
||||
scrollContainer.value!
|
||||
const maxScroll = scrollWidth - clientWidth
|
||||
thumbPosition.value = (scrollLeft / maxScroll) * MAX_SCROLL_VALUE
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let observer = new IntersectionObserver(changeThumbPosition, {
|
||||
root: null,
|
||||
rootMargin: "0px",
|
||||
threshold: 1.0,
|
||||
})
|
||||
observer.observe(element!)
|
||||
|
||||
element?.scrollIntoView({ behavior: "smooth", inline: "center" })
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -336,6 +401,13 @@ watch(thumbPosition, (newVal) => {
|
||||
@apply text-secondaryDark;
|
||||
@apply bg-primary;
|
||||
@apply before: bg-accent;
|
||||
@apply after: absolute;
|
||||
@apply after: inset-x-0;
|
||||
@apply after: bottom-0;
|
||||
@apply after: bg-primary;
|
||||
@apply after: z-12;
|
||||
@apply after: h-0.25;
|
||||
@apply after: content-DEFAULT;
|
||||
}
|
||||
|
||||
.close {
|
||||
@@ -348,6 +420,16 @@ watch(thumbPosition, (newVal) => {
|
||||
}
|
||||
}
|
||||
|
||||
.create-new-tab {
|
||||
@apply after: absolute;
|
||||
@apply after: inset-x-0;
|
||||
@apply after: bottom-0;
|
||||
@apply after: bg-dividerLight;
|
||||
@apply after: z-14;
|
||||
@apply after: h-0.25;
|
||||
@apply after: content-DEFAULT;
|
||||
}
|
||||
|
||||
$slider-height: 4px;
|
||||
|
||||
.slider {
|
||||
|
||||
@@ -18,3 +18,4 @@ export { default as HoppSmartTabs } from "./Tabs.vue"
|
||||
export { default as HoppSmartToggle } from "./Toggle.vue"
|
||||
export { default as HoppSmartWindow } from "./Window.vue"
|
||||
export { default as HoppSmartWindows } from "./Windows.vue"
|
||||
export { default as HoppSmartPicture } from "./Picture.vue"
|
||||
|
||||