Compare commits

..

12 Commits

Author SHA1 Message Date
Andrew Bastin
cdc8fc925e chore: bump version to 2023.4.2 2023-05-11 13:53:28 +05:30
Nivedin
1395c934d5 fix: reset envs when user switches workspaces (#3039)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-11 02:03:35 +05:30
Anwarul Islam
ed9f412c5c fix: tab system breaks when a new tab is created while waiting for response in another tab (#3031) 2023-05-10 19:16:28 +05:30
Akash K
8765c1a8ac fix: invalid environment index can break the app (#3041) 2023-05-10 19:14:16 +05:30
Akash K
b2693d6ba2 chore: add onCodemirrorInstanceMount hook to platform (#3043) 2023-05-10 18:59:57 +05:30
Anwarul Islam
d9ed10bcca feat: scroll to show the new active tab header (#3013)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-09 15:58:44 +05:30
Mir Arif Hasan
87685b8cd9 fix: magic link URL (#3028) 2023-05-09 15:55:38 +05:30
Mir Arif Hasan
00fcc78f85 fix: returning response from authCookieHandler (#3025) 2023-05-09 15:55:01 +05:30
Anwarul Islam
81e090bbba feat: picture component moved to hoppscotch-ui (#3032) 2023-05-09 00:32:54 +05:30
Anwarul Islam
87ba02053b Fix issue with disappearing tab when opening request tabs with long text in body/script (#3030)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-09 00:30:27 +05:30
Akash K
fb08147c66 fix: update the hoppscotch-sh-admin magic link route to match hoppscotch-app (#3029) 2023-05-03 23:12:50 +05:30
Nivedin
d129676cd6 fix: pane layout broken when wrap line is off (#3027)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-05-03 20:39:22 +05:30
44 changed files with 407 additions and 1160 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.4.3",
"version": "2023.4.2",
"description": "",
"author": "",
"private": true,

View File

@@ -360,15 +360,13 @@ 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: executedOn,
executedOn: new Date(),
isStarred: false,
});
@@ -378,7 +376,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST,
executedOn: executedOn,
executedOn: new Date(),
isStarred: false,
};
@@ -386,7 +384,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(

View File

@@ -207,19 +207,16 @@
: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"] {

View File

@@ -432,7 +432,6 @@
"view_my_links": "View my links"
},
"response": {
"audio": "Audio",
"body": "Response Body",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers",
@@ -446,7 +445,6 @@
"status": "Status",
"time": "Time",
"title": "Response",
"video": "Video",
"waiting_for_connection": "waiting for connection",
"xml": "XML"
},

View File

@@ -1,44 +1,44 @@
{
"action": {
"autoscroll": "Desplazamiento automático",
"autoscroll": "Autoscroll",
"cancel": "Cancelar",
"choose_file": "Seleccionar archivo",
"clear": "Limpiar",
"clear_all": "Limpiar todo",
"close": "Cerrar",
"connect": "Conectar",
"connecting": "Conectando",
"connecting": "Connecting",
"copy": "Copiar",
"delete": "Borrar",
"disconnect": "Desconectar",
"dismiss": "Descartar",
"dont_save": "No guardar",
"dont_save": "Don't save",
"download_file": "Descargar archivo",
"drag_to_reorder": "Arrastrar para reordenar",
"duplicate": "Duplicar",
"edit": "Editar",
"filter": "Filtrar",
"filter": "Filter",
"go_back": "Volver",
"go_forward": "Adelante",
"group_by": "Agrupar por",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Etiqueta",
"learn_more": "Aprender más",
"less": "Menos",
"more": "Más",
"new": "Nuevo",
"no": "No",
"open_workspace": "Abrir espacio de trabajo",
"open_workspace": "Open workspace",
"paste": "Pegar",
"prettify": "Embellecer",
"remove": "Eliminar",
"restore": "Restaurar",
"save": "Guardar",
"scroll_to_bottom": "Desplazar hacia abajo",
"scroll_to_top": "Desplazar hacia arriba",
"scroll_to_bottom": "Scroll to bottom",
"scroll_to_top": "Scroll to top",
"search": "Buscar",
"send": "Enviar",
"start": "Comenzar",
"starting": "Iniciando",
"starting": "Starting",
"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": "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.",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of 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 tus APIs. Hoppscotch es una herramienta que le ayuda a crear, probar, documentar y compartir tus APIs.",
"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_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": "Comprobar el estado del sitio web",
"status_description": "Check the status of the website",
"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": "No se puede reordenar la colección con un padre diferente",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Editar colección",
"invalid_name": "Proporciona un nombre válido para la colección.",
"invalid_root_move": "La colección ya está en la raíz",
"moved": "Movido con éxito",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"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": "Orden de colección actualizada",
"order_changed": "Collection Order Updated",
"renamed": "Colección renombrada",
"request_in_use": "Solicitud en uso",
"request_in_use": "Petición 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á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?"
"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?"
},
"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 solicitud no utiliza ninguna autorización.",
"body": "Esta solicitud no tiene cuerpo",
"authorization": "Esta petición no utiliza ninguna autorización.",
"body": "Esta petición 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 solicitud no tiene encabezados",
"headers": "Esta petición 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 solicitud no tiene ningún parámetro",
"parameters": "Esta petición 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": "Aún no se han creado Shortcodes",
"shortcodes": "Los shortcodes están vacíos",
"subscription": "Subscriptions are empty",
"team_name": "Nombre del equipo vacío",
"teams": "Los equipos están vacíos",
"tests": "No hay pruebas para esta solicitud"
"tests": "No hay pruebas para esta petición"
},
"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": "Mis entornos",
"my_environments": "My Environments",
"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": "Entornos de trabajo en equipo",
"team_environments": "Team Environments",
"title": "Entornos",
"updated": "Entorno actualizado",
"updated": "Actualización del entorno",
"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": "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",
"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",
"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": "JSON no válido",
"json_parsing_failed": "Invalid JSON",
"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 solicitud",
"network_fail": "No se pudo enviar la petición",
"no_duration": "Sin duración",
"no_results_found": "No se han encontrado coincidencias",
"page_not_found": "No se ha podido encontrar esta página",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"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": "Tiempo",
"time": "Time",
"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 solicitud.",
"authorization": "El encabezado de autorización se generará automáticamente cuando se envía la petición.",
"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 solicitud están escritos en JavaScript y se ejecutan antes de que se envíe la solicitud.",
"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.",
"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": "Colapsar el panel de colecciones",
"collection": "Collapse Collection Panel",
"more": "Ocultar más",
"preview": "Ocultar vista previa",
"sidebar": "Ocultar barra lateral"
@@ -308,40 +308,40 @@
"title": "Importar"
},
"layout": {
"collapse_collection": "Contraer o expandir colecciones",
"collapse_sidebar": "Contraer o expandir la barra lateral",
"collapse_collection": "Collapse or Expand Collections",
"collapse_sidebar": "Collapse or Expand the sidebar",
"column": "Disposición vertical",
"name": "Diseño",
"name": "Layout",
"row": "Disposición horizontal",
"zen_mode": "Modo zen"
},
"modal": {
"close_unsaved_tab": "Tienes cambios sin guardar",
"close_unsaved_tab": "You have unsaved changes",
"collections": "Colecciones",
"confirm": "Confirmar",
"edit_request": "Editar solicitud",
"edit_request": "Editar petición",
"import_export": "Importación y exportación"
},
"mqtt": {
"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",
"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",
"communication": "Comunicación",
"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",
"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",
"log": "Registro",
"lw_message": "Mensaje de última voluntad",
"lw_qos": "QoS de última voluntad",
"lw_retain": "Última voluntad",
"lw_topic": "Tema de última voluntad",
"lw_message": "Last-Will Message",
"lw_qos": "Last-Will QoS",
"lw_retain": "Last-Will Retain",
"lw_topic": "Last-Will Topic",
"message": "Mensaje",
"new": "Nueva suscripción",
"not_connected": "Por favor, inicia primero una conexión MQTT.",
"new": "New Subscription",
"not_connected": "Please start a MQTT connection first.",
"publish": "Publicar",
"qos": "QoS",
"ssl": "SSL",
@@ -353,7 +353,7 @@
"url": "URL"
},
"navigation": {
"doc": "Documentación",
"doc": "Docs",
"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 solicitud",
"script": "Script previo a la petición",
"snippets": "Fragmentos"
},
"profile": {
@@ -385,55 +385,55 @@
"star": "Eliminar estrella"
},
"request": {
"added": "Solicitud agregada",
"added": "Petición agregada",
"authorization": "Autorización",
"body": "Cuerpo de la solicitud",
"body": "Cuerpo de la petición",
"choose_language": "Seleccionar lenguaje",
"content_type": "Tipo de contenido",
"content_type_titles": {
"others": "Otros",
"structured": "Estructurado",
"text": "Texto"
"others": "Others",
"structured": "Structured",
"text": "Text"
},
"copy_link": "Copiar enlace",
"different_collection": "No se pueden reordenar solicitudes de diferentes colecciones",
"duplicated": "Solicitud duplicada",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"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 solicitud.",
"invalid_name": "Proporciona un nombre para la petición.",
"method": "Método",
"moved": "Request moved",
"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",
"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",
"parameter_list": "Parámetros de consulta",
"parameters": "Parámetros",
"path": "Ruta",
"payload": "Carga útil",
"query": "Consulta",
"raw_body": "Cuerpo de solicitud sin procesar",
"renamed": "Solicitud renombrada",
"raw_body": "Cuerpo de petición sin procesar",
"renamed": "Petición renombrada",
"run": "Ejecutar",
"save": "Guardar",
"save_as": "Guardar como",
"saved": "Solicitud guardada",
"saved": "Petición guardada",
"share": "Compartir",
"share_description": "Comparte Hoppscotch con tus amigos",
"title": "Solicitud",
"type": "Tipo de solicitud",
"share_description": "Share Hoppscotch with your friends",
"title": "Petición",
"type": "Tipo de petición",
"url": "URL",
"variables": "Variables",
"view_my_links": "Ver mis enlaces"
},
"response": {
"body": "Cuerpo de respuesta",
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Encabezados",
"html": "HTML",
"image": "Imagen",
@@ -451,7 +451,7 @@
"settings": {
"accent_color": "Color de acentuación",
"account": "Cuenta",
"account_deleted": "Tu cuenta ha sido eliminada",
"account_deleted": "Your account has been deleted",
"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": "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.",
"delete_account": "Delete account",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"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": "Actualiza los datos de tu perfil",
"profile_description": "Update your profile details",
"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": "Shortcodes",
"short_codes_description": "Shortcodes creados por ti.",
"short_codes": "Short codes",
"short_codes_description": "Short codes which were created by you.",
"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": "Correo electrónico verificado",
"verified_email": "Verified email",
"verify_email": "Verificar correo electrónico"
},
"shortcodes": {
"actions": "Acciones",
"created_on": "Creado el",
"deleted": "Código corto eliminado",
"method": "Método",
"not_found": "Shortcode no encontrado",
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"short_code": "Short code",
"url": "URL"
},
@@ -539,7 +539,7 @@
"title": "Navegación"
},
"request": {
"copy_request_link": "Copiar enlace de solicitud",
"copy_request_link": "Copiar enlace de petición",
"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": "Solicitud de reinicio",
"reset_request": "Petición de reinicio",
"save_to_collections": "Guardar en colecciones",
"send_request": "Enviar solicitud",
"title": "Solicitud"
"send_request": "Enviar petición",
"title": "Petición"
},
"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": "Error de conexión",
"connection_lost": "Conexión perdida",
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"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": "Mensaje: {mensaje} llegó sobre el tema: {topic}",
"mqtt_subscription_failed": "Algo ha ido mal al suscribirse al tema: {topic}",
"message_received": "Message: {message} arrived on topic: {topic}",
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
"none": "Ninguno",
"nothing_found": "Nada encontrado para",
"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"
"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"
},
"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 solicitud",
"pre_request_script": "Script previo a la petición",
"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": "Identificador de colección no válido",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "El formato de correo electrónico no es válido",
"invalid_id": "Identificador de equipo inválido. Ponte en contacto con el propietario de tu equipo.",
"invalid_id": "ID 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 Identificador de correo electrónico ya tiene una invitación. Ponte en contacto con el propietario de tu equipo.",
"member_has_invite": "Este ID 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": "Solicitud no encontrada.",
"no_request_found": "Request not found.",
"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": "No se puede mover la colección a una colección hija",
"parent_coll_move": "Cannot move collection to a child collection",
"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 tu bandeja de entrada. Haz clic en el enlace para unirse al equipo."
"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."
},
"team_environment": {
"deleted": "Entorno eliminado",
"duplicate": "Entorno duplicado",
"not_found": "Entorno no encontrado."
"deleted": "Environment Deleted",
"duplicate": "Environment Duplicated",
"not_found": "Environment not found."
},
"test": {
"failed": "prueba fallida",
@@ -732,9 +732,9 @@
"url": "URL"
},
"workspace": {
"change": "Cambiar el espacio de trabajo",
"personal": "Mi espacio de trabajo",
"team": "Espacio de trabajo en equipo",
"title": "Espacios de trabajo"
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -8,9 +8,7 @@ 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: "#181818",
lightThemeColor: "#ffffff",
darkThemeColor: "#181818",
background: "#202124",
},
social: {
twitter: "@hoppscotch_io",
@@ -110,17 +108,7 @@ export const META_TAGS = (env: Record<string, string>): IHTMLTag[] => [
// PWA
{
name: "theme-color",
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",
content: APP_INFO.app.background,
},
{
name: "mask-icon",

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.4.3",
"version": "2023.4.2",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"dev:vite": "vite",
@@ -92,7 +92,6 @@
"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": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -131,6 +131,7 @@ 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']
@@ -141,6 +142,7 @@ 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']

View File

@@ -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,22 +53,28 @@ const props = withDefaults(
defineProps<{
show: boolean
loadingState: boolean
modelValue?: string
editingRequestName: string
}>(),
{
show: false,
loadingState: false,
modelValue: "",
editingRequestName: "",
}
)
const emit = defineEmits<{
(e: "submit", name: string): void
(e: "hide-modal"): void
(e: "update:modelValue", value: string): void
}>()
const name = useVModel(props, "modelValue")
const name = ref("")
watch(
() => props.editingRequestName,
(newName) => {
name.value = newName
}
)
const editRequest = () => {
if (name.value.trim() === "") {

View File

@@ -126,7 +126,7 @@
/>
<CollectionsEditRequest
:show="showModalEditRequest"
v-bind:model-value="editingRequest ? editingRequest.name : ''"
:editing-request-name="editingRequest ? editingRequest.name : ''"
:loading-state="modalLoadingState"
@submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)"

View File

@@ -36,12 +36,13 @@
? IconCheck
: undefined
"
class="my-2"
:active-info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
"
@click="
() => {
selectedEnvironmentIndex = { type: 'NO_ENV_SELECTED' }
setSelectedEnvironmentIndex({ type: 'NO_ENV_SELECTED' })
hide()
}
"

View File

@@ -121,7 +121,7 @@ const switchToMyEnvironments = () => {
adapter.changeTeamID(undefined)
}
const updateSelectedTeam = (newSelectedTeam: SelectedTeam | undefined) => {
const updateSelectedTeam = (newSelectedTeam: SelectedTeam) => {
if (newSelectedTeam) {
environmentType.value.selectedTeam = newSelectedTeam
REMEMBERED_TEAM_ID.value = newSelectedTeam.id
@@ -150,23 +150,25 @@ 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, (newWorkspace) => {
if (newWorkspace.type === "personal") {
watch(
() => workspace.value.type === "team" && workspace.value.teamID,
(teamID) => {
if (!teamID) {
switchToMyEnvironments()
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
} else {
const team = myTeams.value?.find((t) => t.id === teamID)
if (team) {
updateSelectedTeam(team)
if (selectedEnvironmentIndex.value.type !== "MY_ENV") {
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
}
}
})
}
)
watch(
() => currentUser.value,

View File

@@ -84,7 +84,6 @@ 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,
@@ -198,10 +197,26 @@ const prettifyRequestBody = () => {
}
const prettifyXML = (xml: string) => {
return xmlFormat(xml, {
indentation: " ",
collapseContent: true,
lineSeparator: "\n",
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")
}
</script>

View File

@@ -258,8 +258,6 @@ 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()
@@ -322,12 +320,6 @@ 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)()

View File

@@ -1,79 +0,0 @@
<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>

View File

@@ -1,79 +0,0 @@
<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>

View File

@@ -38,7 +38,6 @@ 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
@@ -153,27 +152,6 @@ 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,
@@ -209,8 +187,6 @@ export function useCodemirror(
: null
const initView = (el: any) => {
if (el) platform.ui?.onCodemirrorInstanceMount?.(el)
const extensions = [
basicSetup,
baseTheme,
@@ -283,7 +259,7 @@ export function useCodemirror(
view.value = new EditorView({
parent: el,
state: EditorState.create({
doc: parseDoc(value.value, options.extendedEditorConfig.mode ?? ""),
doc: value.value,
extensions,
}),
})
@@ -292,6 +268,7 @@ export function useCodemirror(
onMounted(() => {
if (el.value) {
if (!view.value) initView(el.value)
platform.ui?.onCodemirrorInstanceMount?.(el.value)
}
})

View File

@@ -6,7 +6,6 @@ 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)
@@ -214,18 +213,45 @@ export function parseBody(
*/
/**
* Prettifies XML string using xml-formatter
* Prettifies XML string
* @param sourceXml The string to format
* @returns Indented XML string (uses spaces)
*/
function prettifyXml(sourceXml: string) {
return pipe(
O.tryCatch(() => {
return xmlFormat(sourceXml, {
indentation: " ",
collapseContent: true,
lineSeparator: "\n",
})
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
})
)
}

View File

@@ -121,8 +121,8 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
else if (val === "arrowright") return "right"
// Check letter keys
const isLetter = ev.code.toLowerCase().startsWith("key")
if (isLetter) return ev.code.toLowerCase().substring(3) as Key
if (val.length === 1 && val.toUpperCase() !== val.toLowerCase())
return val as Key
// Check if number keys
if (val.length === 1 && !isNaN(val as any)) return val as Key

View File

@@ -1,16 +0,0 @@
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

View File

@@ -5,8 +5,6 @@ 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 = {
@@ -22,8 +20,6 @@ export const lenses: Lens[] = [
htmlLens,
xmlLens,
pdfLens,
audioLens,
videoLens,
rawLens,
]

View File

@@ -1,16 +0,0 @@
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

View File

@@ -7,8 +7,8 @@ pw.env.set("variable", "value");`,
{
name: "Environment: Set timestamp variable",
script: `\n\n// Set timestamp variable
const currentTime = Date.now();
pw.env.set("timestamp", currentTime.toString());`,
const cuttentTime = Date.now();
pw.env.set("timestamp", cuttentTime.toString());`,
},
{
name: "Environment: Set random number variable",

View File

@@ -42,21 +42,16 @@ const dispatchers = defineDispatchers({
selectedEnvironmentIndex,
}: { selectedEnvironmentIndex: SelectedEnvironmentIndex }
) {
if (selectedEnvironmentIndex.type === "MY_ENV") {
if (store.environments[selectedEnvironmentIndex.index]) {
if (
selectedEnvironmentIndex.type === "MY_ENV" &&
!!store.environments[selectedEnvironmentIndex.index]
) {
return {
selectedEnvironmentIndex,
}
} else {
return {
selectedEnvironmentIndex: {
type: "NO_ENV_SELECTED",
},
}
}
} else {
return {
selectedEnvironmentIndex,
}
}
},

View File

@@ -23,7 +23,6 @@
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="tab.document.request.name"
class="truncate px-2"
@dblclick="openReqRenameModal()"
>
<span
class="font-semibold text-tiny"
@@ -62,12 +61,6 @@
<HttpSidebar />
</template>
</AppPaneLayout>
<CollectionsEditRequest
:show="showRenamingReqNameModal"
v-model="reqName"
@submit="renameReqName"
@hide-modal="showRenamingReqNameModal = false"
/>
<HoppSmartConfirmModal
:show="confirmingCloseForTabID !== null"
:confirm="t('modal.close_unsaved_tab')"
@@ -123,8 +116,6 @@ import { oauthRedirect } from "~/helpers/oauth"
const savingRequest = ref(false)
const confirmingCloseForTabID = ref<string | null>(null)
const showRenamingReqNameModal = ref(false)
const reqName = ref<string>("")
const t = useI18n()
const toast = useToast()
@@ -175,20 +166,6 @@ 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)
*/

View File

@@ -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" />

View File

@@ -8,9 +8,7 @@ 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: "#181818",
lightThemeColor: "#ffffff",
darkThemeColor: "#181818",
background: "#202124",
},
social: {
twitter: "@hoppscotch_io",
@@ -110,17 +108,7 @@ export const META_TAGS = (env: Record<string, string>): IHTMLTag[] => [
// PWA
{
name: "theme-color",
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",
content: APP_INFO.app.background,
},
{
name: "mask-icon",

View File

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

View File

@@ -150,54 +150,20 @@ 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: "/icons/pwa-16x16.png",
sizes: "16x16",
type: "image/png",
},
{
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",
src: "/icon.png",
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
{
src: "/icons/pwa-1024x1024.png",
sizes: "1024x1024",
type: "image/png",
src: "/logo.svg",
sizes: "48x48 72x72 96x96 128x128 256x256 512x512",
type: "image/svg+xml",
purpose: "any maskable",
},
],
},

View File

@@ -34,7 +34,7 @@ input::placeholder,
textarea::placeholder,
.cm-placeholder {
@apply text-secondary;
@apply opacity-50;
@apply opacity-35;
}
input,
@@ -323,10 +323,9 @@ pre.ace_editor {
@apply after:justify-center;
@apply after:pointer-events-none;
@apply after:font-icon;
@apply after:text-current;
@apply after:text-secondaryLight;
@apply after:right-3;
@apply after:content-["\e313"];
@apply after:text-lg;
}
.info-response {
@@ -417,6 +416,7 @@ pre.ace_editor {
.smart-splitter .splitpanes__splitter {
@apply relative;
@apply bg-primaryLight;
@apply before:absolute;
@apply before:inset-0;
@apply before:bg-accentLight;
@@ -424,47 +424,48 @@ 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-0;
@apply w-1;
@apply before:-left-0.5;
@apply before:-right-0.5;
@apply before:h-full;
@apply bg-divider;
@apply after:content-["\e5d4"];
}
.smart-splitter.splitpanes--horizontal > .splitpanes__splitter {
@apply h-0;
@apply h-1;
@apply before:-top-0.5;
@apply before:-bottom-0.5;
@apply before:w-full;
@apply bg-divider;
@apply after:content-["\e5d3"];
}
.no-splitter.splitpanes--vertical > .splitpanes__splitter {
@apply w-0;
@apply w-0.5;
@apply pointer-events-none;
@apply bg-dividerLight;
}
.no-splitter.splitpanes--horizontal > .splitpanes__splitter {
@apply h-0;
@apply h-0.5;
@apply pointer-events-none;
@apply bg-dividerLight;
}
.splitpanes--horizontal .splitpanes__pane {
@apply transition-none;
}
.splitpanes--vertical .splitpanes__pane {
@apply transition-none;
}
.cm-focused {

View File

@@ -6,19 +6,16 @@
}
@mixin dark-theme {
--primary-color: theme('colors.dark.800');
--primary-color: theme('colors.neutral.900');
--primary-light-color: theme('colors.dark.600');
--primary-dark-color: theme('colors.neutral.800');
--primary-contrast-color: theme('colors.neutral.900');
--primary-contrast-color: #161616;
--secondary-color: theme('colors.neutral.400');
--secondary-light-color: theme('colors.neutral.500');
--secondary-dark-color: theme('colors.neutral.50');
--secondary-dark-color: theme('colors.neutral.100');
--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');
@@ -27,18 +24,15 @@
@mixin light-theme {
--primary-color: theme('colors.white');
--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');
--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');
--divider-color: theme('colors.gray.100');
--divider-light-color: theme('colors.gray.100');
--divider-dark-color: theme('colors.gray.300');
--divider-light-color: theme('colors.neutral.100');
--divider-dark-color: theme('colors.neutral.300');
--error-color: theme('colors.yellow.100');
--tooltip-color: theme('colors.neutral.800');
--popover-color: theme('colors.white');
@@ -49,19 +43,16 @@
--primary-color: theme('colors.dark.900');
--primary-light-color: theme('colors.neutral.900');
--primary-dark-color: theme('colors.dark.800');
--primary-contrast-color: theme('colors.dark.900');
--primary-contrast-color: #0e0e0e;
--secondary-color: theme('colors.neutral.400');
--secondary-light-color: theme('colors.neutral.500');
--secondary-dark-color: theme('colors.neutral.100');
--divider-color: theme('colors.dark.600');
--divider-color: theme('colors.neutral.800');
--divider-light-color: theme('colors.dark.800');
--divider-dark-color: theme('colors.dark.200');
--divider-dark-color: theme('colors.dark.300');
--error-color: theme('colors.stone.900');
--tooltip-color: theme('colors.neutral.100');
--popover-color: theme('colors.dark.900');
--popover-color: theme('colors.dark.600');
--editor-theme: 'twilight';
}
@@ -197,67 +188,6 @@
--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;
@@ -306,6 +236,65 @@
--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;
}

View File

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

View File

@@ -1,64 +1,3 @@
* {
@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;
@@ -130,89 +69,6 @@ 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;
}
@@ -246,252 +102,6 @@ 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;
@@ -508,62 +118,3 @@ pre.ace_editor {
@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;
}

View File

@@ -207,19 +207,16 @@
: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"] {

View File

@@ -1,27 +1,13 @@
<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">
<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"
:style="{ transform: `translate(${suggestionsOffsetLeft}px, 0)` }">
<li v-for="(suggestion, index) in suggestions" :key="`suggestion-${index}`"
:class="{ active: currentSuggestionIndex === index }" @click.prevent="forceSuggestion(suggestion)">
{{ suggestion }}
</li>
</ul>
@@ -76,9 +62,11 @@ 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,
@@ -94,11 +82,14 @@ 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") {
@@ -111,16 +102,18 @@ 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(suggestion: string) {
text.value = suggestion
function forceSuggestion(str: string) {
const input = text.value.substring(0, selectionStart.value)
text.value = input + str
selectionStart.value = text.value.length
suggestionsVisible.value = true
@@ -131,27 +124,8 @@ function forceSuggestion(suggestion: string) {
function handleKeystroke(event: any) {
switch (event.code) {
case "ArrowUp":
event.preventDefault()
currentSuggestionIndex.value =
currentSuggestionIndex.value - 1 >= 0
? currentSuggestionIndex.value - 1
: 0
break
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(
@@ -160,45 +134,63 @@ function handleKeystroke(event: any) {
)
break
case "Tab": {
case "ArrowUp":
event.preventDefault()
currentSuggestionIndex.value =
currentSuggestionIndex.value - 1 >= 0
? currentSuggestionIndex.value - 1
: 0
break
case "ArrowDown":
event.preventDefault()
currentSuggestionIndex.value =
currentSuggestionIndex.value < suggestions.value.length - 1
? currentSuggestionIndex.value + 1
: suggestions.value.length - 1
break
case "Tab": {
const activeSuggestion =
suggestions.value[
currentSuggestionIndex.value >= 0 ? currentSuggestionIndex.value : 0
]
if (!activeSuggestion) return
if (!activeSuggestion) {
return
}
forceSuggestion(activeSuggestion)
event.preventDefault()
const input = text.value.substring(0, selectionStart.value)
text.value = input + 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 -left-px;
@apply absolute;
@apply mx-2;
@apply left-0;
@apply z-50;
@apply shadow-lg;
@apply max-h-46;
@apply overflow-y-auto;
@apply border-b border-x border-divider;
top: calc(100% + 1px);
top: calc(100% - 4px);
border-radius: 0 0 8px 8px;
li {
@@ -206,16 +198,15 @@ function handleKeystroke(event: any) {
@apply block;
@apply py-2 px-4;
@apply text-secondary;
@apply font-semibold;
&:last-child {
border-radius: 0 0 0 8px;
border-radius: 0 0 8px 8px;
}
&:hover,
&.active {
@apply bg-primaryDark;
@apply text-secondaryDark;
@apply bg-accentDark;
@apply text-accentContrast;
@apply cursor-pointer;
}
}

View File

@@ -11,7 +11,6 @@
:src="url"
:alt="alt"
loading="lazy"
referrerpolicy="no-referrer"
/>
<div
v-else
@@ -22,7 +21,8 @@
<template v-if="initial && initial.charAt(0).toUpperCase()">
{{ initial.charAt(0).toUpperCase() }}
</template>
<icon-lucide-user v-else />
<icon-lucide-user v-else></icon-lucide-user>
</div>
<span
v-if="indicator"

31
pnpm-lock.yaml generated
View File

@@ -574,9 +574,6 @@ importers:
workbox-window:
specifier: ^6.5.4
version: 6.5.4
xml-formatter:
specifier: ^3.4.1
version: 3.4.1
yargs-parser:
specifier: ^21.1.1
version: 21.1.1
@@ -8958,7 +8955,7 @@ packages:
hasBin: true
/after@0.8.2:
resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==}
resolution: {integrity: sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=}
dev: false
/agent-base@6.0.2:
@@ -9570,7 +9567,7 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
/base64-arraybuffer@0.1.4:
resolution: {integrity: sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==}
resolution: {integrity: sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=}
engines: {node: '>= 0.6.0'}
dev: false
@@ -10217,14 +10214,14 @@ packages:
dev: true
/component-bind@1.0.0:
resolution: {integrity: sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==}
resolution: {integrity: sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=}
dev: false
/component-emitter@1.3.0:
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
/component-inherit@0.0.3:
resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==}
resolution: {integrity: sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=}
dev: false
/concat-map@0.0.1:
@@ -13395,7 +13392,7 @@ packages:
dev: false
/has-cors@1.1.0:
resolution: {integrity: sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==}
resolution: {integrity: sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=}
dev: false
/has-flag@3.0.0:
@@ -13788,7 +13785,7 @@ packages:
engines: {node: '>=8'}
/indexof@0.0.1:
resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
resolution: {integrity: sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=}
dev: false
/inflight@1.0.6:
@@ -19489,7 +19486,7 @@ packages:
dev: true
/to-array@0.1.4:
resolution: {integrity: sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==}
resolution: {integrity: sha1-F+bBH3PdTz10zaek/zI46a2b+JA=}
dev: false
/to-fast-properties@2.0.0:
@@ -21871,13 +21868,6 @@ packages:
optional: true
dev: false
/xml-formatter@3.4.1:
resolution: {integrity: sha512-C7VwnZpz662mZlKtrdREucsABAIlmdph/nMEUszTMsRAGGPMSNfyNOU4UaPBqxXYVadb9uSpc1Xibbj6XpbGRA==}
engines: {node: '>= 14'}
dependencies:
xml-parser-xo: 4.1.0
dev: false
/xml-name-validator@3.0.0:
resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==}
dev: true
@@ -21887,11 +21877,6 @@ packages:
engines: {node: '>=12'}
dev: true
/xml-parser-xo@4.1.0:
resolution: {integrity: sha512-9mQMLmq8J++XlQH9WF57oQxFVbR3YM6dPPtTuV+++aMe2gRoRU/kj819/6IptUmfhC1d2DSFiYxEcpkoLabeJw==}
engines: {node: '>= 14'}
dev: false
/xml2js@0.4.23:
resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==}
engines: {node: '>=4.0.0'}
@@ -22063,7 +22048,7 @@ packages:
dev: false
/yeast@0.1.2:
resolution: {integrity: sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==}
resolution: {integrity: sha1-AI4G2AlDIMNy28L47XagymyKxBk=}
dev: false
/yn@3.1.1: