Compare commits

..

1 Commits

Author SHA1 Message Date
Nivedin
9d2e5b465e fix: tab head rename bug 2023-08-25 00:19:44 +05:30
104 changed files with 3003 additions and 3476 deletions

View File

@@ -1,66 +0,0 @@
name: "Push containers to Docker Hub on release"
on:
push:
tags:
- '*.*.*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
run: cp .env.example .env
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push `${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: backend
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: app
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: sh_admin
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:${{ github.ref_name }}
- name: Build and push `${{ secrets.DOCKER_AIO_CONTAINER_NAME }}`
uses: docker/build-push-action@v4
with:
context: .
file: ./prod.Dockerfile
target: aio
push: true
tags: |
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:latest
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:${{ github.ref_name }}

View File

@@ -6,8 +6,8 @@ We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
@@ -22,17 +22,17 @@ community include:
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
@@ -82,15 +82,15 @@ behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
@@ -106,27 +106,23 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

190
README.md
View File

@@ -2,18 +2,23 @@
<a href="https://hoppscotch.io">
<img
src="https://avatars.githubusercontent.com/u/56705483"
alt="Hoppscotch"
alt="Hoppscotch Logo"
height="64"
/>
</a>
<h3>
<br />
<p>
<h3>
<b>
Hoppscotch
</b>
</h3>
</p>
<p>
<b>
Hoppscotch
Open source API development ecosystem
</b>
</h3>
<b>
Open Source API Development Ecosystem
</b>
</p>
<p>
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen?logo=github)](CODE_OF_CONDUCT.md) [![Website](https://img.shields.io/website?url=https%3A%2F%2Fhoppscotch.io&logo=hoppscotch)](https://hoppscotch.io) [![Tests](https://github.com/hoppscotch/hoppscotch/actions/workflows/tests.yml/badge.svg)](https://github.com/hoppscotch/hoppscotch/actions) [![Tweet](https://img.shields.io/twitter/url?url=https%3A%2F%2Fhoppscotch.io%2F)](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
@@ -29,18 +34,23 @@
</p>
<br />
<p>
<a href="https://hoppscotch.io">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./packages/hoppscotch-common/public/images/banner-dark.png">
<source media="(prefers-color-scheme: light)" srcset="./packages/hoppscotch-common/public/images/banner-light.png">
<img alt="Hoppscotch" src="./packages/hoppscotch-common/public/images/banner-dark.png">
</picture>
<a href="https://hoppscotch.io/#gh-light-mode-only" target="_blank">
<img
src="./packages/hoppscotch-common/public/images/banner-light.png"
alt="Hoppscotch"
width="100%"
/>
</a>
<a href="https://hoppscotch.io/#gh-dark-mode-only" target="_blank">
<img
src="./packages/hoppscotch-common/public/images/banner-dark.png"
alt="Hoppscotch"
width="100%"
/>
</a>
</p>
</div>
_We highly recommend you take a look at the [**Hoppscotch Documentation**](https://docs.hoppscotch.io) to learn more about the app._
#### **Support**
[![Chat on Discord](https://img.shields.io/badge/chat-Discord-7289DA?logo=discord)](https://hoppscotch.io/discord) [![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-2CA5E0?logo=telegram)](https://hoppscotch.io/telegram) [![Discuss on GitHub](https://img.shields.io/badge/discussions-GitHub-333333?logo=github)](https://github.com/hoppscotch/hoppscotch/discussions)
@@ -49,9 +59,9 @@ _We highly recommend you take a look at the [**Hoppscotch Documentation**](https
❤️ **Lightweight:** Crafted with minimalistic UI design.
⚡️ **Fast:** Send requests and get responses in real time.
⚡️ **Fast:** Send requests and get/copy responses in real-time.
🗄️ **HTTP Methods:** Request methods define the type of action you are requesting to be performed.
**HTTP Methods**
- `GET` - Requests retrieve resource information
- `POST` - The server creates a new entry in a database
@@ -64,15 +74,17 @@ _We highly recommend you take a look at the [**Hoppscotch Documentation**](https
- `TRACE` - Performs a message loop-back test along the path to the target resource
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
🌈 **Theming:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
🌈 **Make it yours:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
- Choose a theme: System preference, Light, Dark, and Black
- Choose accent colors: Green, Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
**Theming**
- Choose a theme: System (default), Light, Dark, and Black
- Choose accent color: Green (default), Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
- Distraction-free Zen mode
_Customized themes are synced with your cloud/local session._
_Customized themes are synced with cloud / local session_
🔥 **PWA:** Install as a [Progressive Web App](https://web.dev/progressive-web-apps) on your device.
🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device.
- Instant loading with Service Workers
- Offline support
@@ -95,7 +107,7 @@ _Customized themes are synced with your cloud/local session._
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
🌩 **Socket.IO:** Send and Receive data with the SocketIO server.
🌩 **Socket.IO:** Send and Receive data with SocketIO server.
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
@@ -115,7 +127,7 @@ _Customized themes are synced with your cloud/local session._
- OAuth 2.0
- OIDC Access Token/PKCE
📢 **Headers:** Describes the format the body of your request is being sent in.
📢 **Headers:** Describes the format the body of your request is being sent as.
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
@@ -125,14 +137,14 @@ _Customized themes are synced with your cloud/local session._
- FormData, JSON, and many more
- Toggle between key-value and RAW input parameter list
📮 **Response:** Contains the status line, headers, and the message/response body.
👋 **Response:** Contains the status line, headers, and the message/response body.
- Copy the response to the clipboard
- Download the response as a file
- Copy response to clipboard
- Download response as a file
- View response headers
- View raw and preview HTML, image, JSON, and XML responses
- View raw and preview of HTML, image, JSON, XML responses
**History:** Request entries are synced with your cloud/local session storage.
**History:** Request entries are synced with cloud / local session storage to restore with a single click.
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
@@ -140,32 +152,7 @@ _Customized themes are synced with your cloud/local session._
- Nested folders
- Export and import as a file or GitHub gist
_Collections are synced with your cloud/local session storage._
📜 **Pre-Request Scripts:** Snippets of code associated with a request that is executed before the request is sent.
- Set environment variables
- Include timestamp in the request headers
- Send a random alphanumeric string in the URL parameters
- Any JavaScript functions
👨‍👩‍👧‍👦 **Teams:** Helps you collaborate across your teams to design, develop, and test APIs faster.
- Create unlimited teams
- Create unlimited shared collections
- Create unlimited team members
- Role-based access control
- Cloud sync
- Multiple devices
👥 **Workspaces:** Organize your personal and team collections environments into workspaces. Easily switch between workspaces to manage multiple projects.
- Create unlimited workspaces
- Switch between personal and team workspaces
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
_Collections are synced with cloud / local session storage_
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
@@ -174,31 +161,60 @@ _Collections are synced with your cloud/local session storage._
- Access APIs served in non-HTTPS (`http://`) endpoints
- Use your Proxy URL
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**._
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**_
📜 **Pre-Request Scripts β:** Snippets of code associated with a request that is executed before the request is sent.
- Set environment variables
- Include timestamp in the request headers
- Send a random alphanumeric string in the URL parameters
- Any JavaScript functions
📄 **API Documentation:** Create and share dynamic API documentation easily, quickly.
1. Add your requests to Collections and Folders
2. Export Collections and easily share your APIs with the rest of your team
3. Import Collections and Generate Documentation on-the-go
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
🌎 **i18n:** Experience the app in your language.
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md) and the process for submitting pull requests to us.
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us.
☁️ **Auth + Sync:** Sign in and sync your data in real-time across all your devices.
📦 **Add-ons:** Official add-ons for hoppscotch.
**Sign in with:**
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch
- **[CLI β](https://github.com/hoppscotch/hopp-cli)** - A CLI solution for Hoppscotch
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that simplifies access to Hoppscotch
[![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_16x16.png) **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) &nbsp;|&nbsp; [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_16x16.png) **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
> **Extensions fixes `CORS` issues.**
- **[Hopp-Doc-Gen](https://github.com/hoppscotch/hopp-doc-gen)** - An API doc generator CLI for Hoppscotch
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
☁️ **Auth + Sync:** Sign in and sync your data in real-time.
**Sign in with**
- GitHub
- Google
- Microsoft
- Email
- SSO (Single Sign-On)[^EE]
**🔄 Synchronize your data:** Handoff to continue tasks on your other devices.
**Synchronize your data**
- Workspaces
- History
- Collections
- Environments
- Settings
**Post-Request Tests:** Write tests associated with a request that is executed after the request's response.
**Post-Request Tests β:** Write tests associated with a request that is executed after the request's response.
- Check the status code as an integer
- Filter response headers
@@ -206,7 +222,7 @@ Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) f
- Set environment variables
- Write JavaScript code
🌱 **Environments:** Environment variables allow you to store and reuse values in your requests and scripts.
🌱 **Environments** : Environment variables allow you to store and reuse values in your requests and scripts.
- Unlimited environments and variables
- Initialize through the pre-request script
@@ -225,31 +241,22 @@ Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) f
</details>
👨‍👩‍👧‍👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
- Unlimited teams
- Unlimited shared collections
- Unlimited team members
- Role-based access control
- Cloud sync
- Multiple devices
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
- Entries are separated by newline
- Keys and values are separated by `:`
- Prepend `#` to any row you want to add but keep disabled
🎛️ **Admin dashboard:** Manage your team and invite members.
- Insights
- Manage users
- Manage teams
📦 **Add-ons:** Official add-ons for hoppscotch.
- **[Hoppscotch CLI](https://github.com/hoppscotch/hopp-cli)** - Command-line interface for Hoppscotch.
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch.
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that enhance your Hoppscotch experience.
[![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_16x16.png) **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) &nbsp;|&nbsp; [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_16x16.png) **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
> **Extensions fix `CORS` issues.**
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
**For a complete list of features, please read our [documentation](https://docs.hoppscotch.io).**
**For more features, please read our [documentation](https://docs.hoppscotch.io).**
## **Demo**
@@ -261,9 +268,18 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
2. Click "Send" to simulate the request
3. View the response
## **Built with**
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML)
- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), [SCSS](https://sass-lang.com), [Windi CSS](https://windicss.org)
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [TypeScript](https://www.typescriptlang.org)
- [Vue](https://vuejs.org)
- [Vite](https://vitejs.dev)
## **Developing**
Follow our [self-hosting documentation](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
Follow our [self-hosting guide](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
## **Contributing**
@@ -281,7 +297,7 @@ See the [`CHANGELOG`](CHANGELOG.md) file for details.
## **Authors**
This project owes its existence to the collective efforts of all those who contribute — [contribute now](CONTRIBUTING.md).
This project exists thanks to all the people who contribute — [contribute](CONTRIBUTING.md).
<div align="center">
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
@@ -293,6 +309,4 @@ This project owes its existence to the collective efforts of all those who contr
## **License**
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) see the [`LICENSE`](LICENSE) file for details.
[^EE]: Enterprise edition feature. [Learn more](https://docs.hoppscotch.io/documentation/self-host/getting-started).
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see the [`LICENSE`](LICENSE) file for details.

View File

@@ -2,9 +2,8 @@
This document outlines security procedures and general policies for the Hoppscotch project.
- [Security Policy](#security-policy)
- [Reporting a security vulnerability](#reporting-a-security-vulnerability)
- [Incident response process](#incident-response-process)
1. [Reporting a security vulnerability](#reporting-a-security-vulnerability)
3. [Incident response process](#incident-response-process)
## Reporting a security vulnerability

View File

@@ -9,24 +9,26 @@ Before you start working on a new language, please look through the [open pull r
if there is no existing translation, you can create a new one by following these steps:
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
2. **Checkout the `main` branch for latest translations.**
3. **Create a new branch for your translation with base branch `main`.**
2. **Checkout the `i18n` branch for latest translations.**
3. **Create a new branch for your translation with base branch `i18n`.**
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
5. **Copy the contents of the source file [`/packages/hoppscotch-common/locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/locales/en.json) to the target language file.**
6. **Translate the strings in the target language file.**
7. **Add your language entry to [`/packages/hoppscotch-common/languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/languages.json).**
8. **Save and commit changes.**
8. **Save & commit changes.**
9. **Send a pull request.**
_You may send a pull request before all steps above are complete: e.g., you may want to ask for help with translations, or getting tests to pass. However, your pull request will not be merged until all steps above are complete._
`i18n` branch will be merged into `main` branch once every week.
Completing an initial translation of the whole site is a fairly large task. One way to break that task up is to work with other translators through pull requests on your fork. You can also [add collaborators to your fork](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) if you'd like to invite other translators to commit directly to your fork and share responsibility for merging pull requests.
## Updating a translation
### Corrections
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, [open an issue](https://github.com/hoppscotch/hoppscotch/issues/new/choose).
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, open an issue.
### Broken links

View File

@@ -32,14 +32,5 @@
"@types/node": "^17.0.24",
"cross-env": "^7.0.3",
"http-server": "^14.1.1"
},
"pnpm": {
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {
"ajv": "6.12.3"
}
}
}
}
}

View File

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

View File

@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
orderBy: {
createdOn: 'desc',
},
skip: args.cursor ? 1 : 0,
skip: 1,
take: args.take,
cursor: args.cursor ? { id: args.cursor } : undefined,
});

View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support@hoppscotch.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,19 +1,29 @@
<div align="center">
<a href="https://hoppscotch.io">
<img
src="https://avatars.githubusercontent.com/u/56705483"
alt="Hoppscotch Logo"
height="64"
/>
</a>
</div>
<div align="center">
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
A CLI to run Hoppscotch Test Scripts in CI environments.
</div>
A CLI to run Hoppscotch test scripts in CI environments.
### **Commands:**
- `hopp test [options] [file]`: testing hoppscotch collection.json file
### **Usage:**
```bash
```
hopp [options or commands] arguments
```
### **Options:**
- `-v`, `--ver`: see the current version of the CLI
- `-h`, `--help`: display help for command
@@ -35,21 +45,17 @@ hopp [options or commands] arguments
- Executes and outputs test-script response.
#### Options:
##### `-e <file_path>` / `--env <file_path>`
- Accepts path to env.json with contents in below format:
```json
{
"ENV1":"value1",
"ENV2":"value2"
}
```
- You can now access those variables using `pw.env.get('<var_name>')`
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
## Install
@@ -69,59 +75,4 @@ npm i -g @hoppscotch/cli
## **Contributing:**
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org).
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer merge it for you.
## Set Up The Development Environment
1. After cloning the repository, execute the following commands:
```bash
pnpm install
pnpm run build
```
2. In order to test locally, you can use two types of package linking:
1. The 'pnpm exec' way (preferred since it does not hamper your original installation of the CLI):
```bash
pnpm link @hoppscotch/cli
// Then to use or test the CLI:
pnpm exec hopp
// After testing, to remove the package linking:
pnpm rm @hoppscotch/cli
```
2. The 'global' way (warning: this might override the globally installed CLI, if exists):
```bash
sudo pnpm link --global
// Then to use or test the CLI:
hopp
// After testing, to remove the package linking:
sudo pnpm rm --global @hoppscotch/cli
```
3. To use the Typescript watch scripts:
```bash
pnpm run dev
```
To get started contributing to the repository, please read **[CONTRIBUTING.md](./CONTRIBUTING.md)**

View File

@@ -2,20 +2,7 @@
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.688rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--sidebar-primary-sticky-fold: 2rem;
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
}
@mixin dark-theme {
@@ -213,8 +200,8 @@
:root {
@include base-theme;
@include dark-theme;
@include dark-editor-theme;
@include green-theme;
@include dark-editor-theme;
}
:root.light {
@@ -270,3 +257,63 @@
:root[data-accent="yellow"] {
@include yellow-theme;
}
@mixin font-small {
--font-size-body: 0.75rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin font-medium {
--font-size-body: 0.875rem;
--line-height-body: 1.25rem;
--upper-primary-sticky-fold: 4.375rem;
--upper-secondary-sticky-fold: 6.688rem;
--upper-tertiary-sticky-fold: 9rem;
--upper-mobile-primary-sticky-fold: 7.125rem;
--upper-mobile-secondary-sticky-fold: 9.438rem;
--upper-mobile-sticky-fold: 11.75rem;
--upper-mobile-tertiary-sticky-fold: 9rem;
--lower-primary-sticky-fold: 3.25rem;
--lower-secondary-sticky-fold: 5.563rem;
--lower-tertiary-sticky-fold: 7.875rem;
--sidebar-primary-sticky-fold: 2.25rem;
}
@mixin font-large {
--font-size-body: 1rem;
--line-height-body: 1.5rem;
--upper-primary-sticky-fold: 4.625rem;
--upper-secondary-sticky-fold: 7.188rem;
--upper-tertiary-sticky-fold: 9.75rem;
--upper-mobile-primary-sticky-fold: 7.625rem;
--upper-mobile-secondary-sticky-fold: 10.188rem;
--upper-mobile-sticky-fold: 12.75rem;
--upper-mobile-tertiary-sticky-fold: 9.75rem;
--lower-primary-sticky-fold: 3.5rem;
--lower-secondary-sticky-fold: 6.063rem;
--lower-tertiary-sticky-fold: 8.625rem;
--sidebar-primary-sticky-fold: 2.5rem;
}
:root[data-font-size="small"] {
@include font-small;
}
:root[data-font-size="medium"] {
@include font-medium;
}
:root[data-font-size="large"] {
@include font-large;
}

View File

@@ -159,8 +159,8 @@
},
"context_menu": {
"set_environment_variable": "Set as variable",
"add_parameters": "Add to parameters",
"open_request_in_new_tab": "Open request in new tab"
"add_parameter": "Add to parameter",
"open_link_in_new_tab": "Open link in new tab"
},
"count": {
"header": "Header {count}",
@@ -606,7 +606,7 @@
"delete_method": "Select DELETE method",
"get_method": "Select GET method",
"head_method": "Select HEAD method",
"rename": "Rename Request",
"rename": "Rename Current Request",
"import_curl": "Import cURL",
"show_code": "Generate code snippet",
"method": "Method",
@@ -649,62 +649,51 @@
},
"spotlight": {
"general": {
"help_menu": "Help and support",
"help_menu": "Open help and support menu",
"chat": "Chat with support",
"open_docs": "Read Documentation",
"open_keybindings": "Keyboard shortcuts",
"open_github": "Open GitHub repository",
"social": "Social",
"open_keybindings": "Open keyboard shortcuts",
"social": "Social links and GitHub",
"title": "General"
},
"miscellaneous": {
"invite": "Invite your friends to Hoppscotch",
"invite": "Invite people to Hoppscotch",
"title": "Miscellaneous"
},
"request": {
"switch_to": "Switch to",
"select_method": "Select method",
"save_as_new": "Save as new request",
"tab_parameters": "Parameters tab",
"tab_body": "Body tab",
"tab_headers": "Headers tab",
"tab_authorization": "Authorization tab",
"tab_pre_request_script": "Pre-request script tab",
"tab_tests": "Tests tab",
"tab_query": "Query tab",
"tab_variables": "Variables tab"
},
"graphql": {
"connect": "Connect to server",
"disconnect": "Disconnect from server"
"tab_parameters": "Open parameters tab",
"tab_body": "Open body tab",
"tab_headers": "Open headers tab",
"tab_authorization": "Open authorization tab",
"tab_pre_request_script": "Open pre-request script tab",
"tab_tests": "Open tests tab"
},
"response": {
"copy": "Copy response",
"copy": "Copy response as JSON",
"download": "Download response as file",
"title": "Response"
},
"environments": {
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"edit": "Edit current environment",
"delete": "Delete current environment",
"duplicate": "Duplicate current environment",
"edit": "Edit selected environment",
"delete": "Delete selected environment",
"duplicate": "Duplicate selected environment",
"edit_global": "Edit global environment",
"duplicate_global": "Duplicate global environment",
"title": "Environments"
},
"workspace": {
"new": "Create new team",
"edit": "Edit current team",
"delete": "Delete current team",
"edit": "Edit selected team",
"delete": "Delete selected team",
"invite": "Invite people to team",
"switch_to_personal": "Switch to your personal workspace",
"switch_to_personal": "Switch to personal workspace",
"title": "Teams"
},
"tab": {
"duplicate": "Duplicate current tab",
"close_current": "Close current tab",
"close_others": "Close all other tabs",
"close_others": "Close other tabs",
"new_tab": "Open a new tab",
"title": "Tabs"
},
@@ -714,21 +703,24 @@
"interface": "Interface",
"interceptor": "Interceptor"
},
"change_interceptor": "Change Interceptor",
"change_language": "Change Language",
"install_extension": "Install Browser Extension",
"settings": {
"theme": {
"black": "Black",
"dark": "Dark",
"light": "Light",
"system": "System preference"
"black": "Black Mode",
"dark": "Dark Mode",
"light": "Light Mode",
"system": "System Mode"
},
"font": {
"size_sm": "Small",
"size_md": "Medium",
"size_lg": "Large"
"size_sm": "Change to Small",
"size_md": "Change to Medium",
"size_lg": "Change to Large"
},
"change_interceptor": "Change Interceptor",
"change_language": "Change Language"
"change_language": "Change Language",
"install_extension": "Install Browser Extension"
}
},
"sse": {

View File

@@ -118,22 +118,22 @@
},
"collection": {
"created": "Koleksi dibuat",
"different_parent": "Tidak dapat mengubah urutan koleksi dengan induk yang berbeda",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Mengubah Koleksi",
"invalid_name": "Berikan nama untuk Koleksi",
"invalid_root_move": "Koleksi sudah berada di akar direktori",
"moved": "Berhasil Dipindahkan",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Koleksi Saya",
"name": "Koleksi Baru Saya",
"name_length_insufficient": "Nama koleksi harus minimal 3 karakter",
"new": "Koleksi baru",
"order_changed": "Pembaruan Urutan Koleksi",
"order_changed": "Collection Order Updated",
"renamed": "Koleksi berganti nama",
"request_in_use": "Permintaan sedang digunakan",
"save_as": "Simpan Sebagai",
"select": "Pilih Koleksi",
"select_location": "Pilih lokasi",
"select_team": "Pilih tim",
"select_team": "Pilih team",
"team_collections": "Koleksi Tim"
},
"confirm": {
@@ -147,7 +147,7 @@
"remove_team": "Apakah Anda yakin ingin menghapus tim ini?",
"remove_telemetry": "Apakah Anda yakin ingin menyisih dari Telemetri?",
"request_change": "Apakah Anda yakin ingin membuang permintaan saat ini, perubahan yang belum disimpan akan hilang.",
"save_unsaved_tab": "Apakah Anda ingin menyimpan perubahan yang dibuat di tab ini?",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
"sync": "Apakah Anda ingin memulihkan ruang kerja Anda dari cloud? Ini akan membuang kemajuan lokal Anda."
},
"count": {
@@ -180,8 +180,8 @@
"profile": "Masuk untuk melihat profil Anda",
"protocols": "Protokol kosong",
"schema": "Hubungkan ke endpoint GraphQL untuk melihat skema",
"shortcodes": "Shortcodes kosong",
"subscription": "Langganan kosong",
"shortcodes": "Shortcodes are empty",
"subscription": "Subscriptions are empty",
"team_name": "Nama team kosong",
"teams": "Kamu bukan di team manapun",
"tests": "Tidak ada tes untuk permintaan ini"
@@ -189,19 +189,19 @@
"environment": {
"add_to_global": "Tambahkan ke Global",
"added": "Tambahan Environment",
"create_new": "Membuat environment baru",
"create_new": "Membuat baru environment",
"created": "Environment dibuat",
"deleted": "Environment dihapus",
"edit": "Sunting Environment",
"invalid_name": "Tolong beri nama untuk environment",
"my_environments": "Environment Saya",
"nested_overflow": "Variabel environment bersarang dibatasi hingga 10 level",
"my_environments": "My Environments",
"nested_overflow": "variabel environment bersarang dibatasi hingga 10 level",
"new": "Environment Baru",
"no_environment": "No environment",
"no_environment_description": "Tidak ada environment yang dipilih. Pilih apa yang harus dilakukan dengan variabel berikut.",
"select": "Pilih environment",
"team_environments": "Environment Tim",
"title": "Environment",
"team_environments": "Team Environments",
"title": "Environments",
"updated": "Environment diperbarui",
"variable_list": "Daftar Variable"
},
@@ -210,8 +210,8 @@
"check_console_details": "Periksa console log untuk detailnya.",
"curl_invalid_format": "cURL tidak diformat dengan benar",
"danger_zone": "Danger zone",
"delete_account": "Akun Anda saat ini merupakan pemilik dalam tim-tim ini:",
"delete_account_description": "Anda harus menghapus diri Anda dari tim-tim ini, mentransfer kepemilikan, atau menghapus tim-tim ini sebelum Anda dapat menghapus akun Anda.",
"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": "Nama Permintaan Kosong",
"f12_details": "(F12 untuk detailnya)",
"gql_prettify_invalid_query": "Tidak dapat prettify kueri yang tidak valid, menyelesaikan kesalahan sintaksis kueri, dan coba lagi",
@@ -294,7 +294,7 @@
"from_json_description": "Impor dari Hoppscotch berkas koleksi",
"from_my_collections": "Impor dari Koleksi Saya",
"from_my_collections_description": "Impor dari Berkas Koleksi Saya",
"from_openapi": "Impor dari OpenAPI",
"from_openapi": "Import dari OpenAPI",
"from_openapi_description": "Impor dari OpenAPI syarat berkas (YML/JSON)",
"from_postman": "Impor dari Postman",
"from_postman_description": "Impor dari Koleksi Postman",
@@ -316,23 +316,23 @@
"zen_mode": "Zen mode"
},
"modal": {
"close_unsaved_tab": "Anda memiliki perubahan yang belum disimpan",
"close_unsaved_tab": "You have unsaved changes",
"collections": "Koleksi",
"confirm": "Mengonfirmasi",
"edit_request": "Edit Request",
"import_export": "Impor / Ekspor"
},
"mqtt": {
"already_subscribed": "Anda sudah berlangganan topik ini.",
"clean_session": "Sesi Bersih",
"clear_input": "Hapus input",
"clear_input_on_send": "Hapus input saat mengirim",
"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": "Pilih warna",
"color": "Pick a color",
"communication": "Komunikasi",
"connection_config": "Konfigurasi Koneksi",
"connection_not_authorized": "Koneksi MQTT ini tidak menggunakan otentikasi",
"invalid_topic": "Harap berikan topik untuk langganan",
"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": "Log",
"lw_message": "Last-Will Message",
@@ -340,8 +340,8 @@
"lw_retain": "Last-Will Retain",
"lw_topic": "Last-Will Topic",
"message": "Pesan",
"new": "Langganan Baru",
"not_connected": "Mulai koneksi MQTT terlebih dahulu",
"new": "New Subscription",
"not_connected": "Please start a MQTT connection first.",
"publish": "Menerbitkan",
"qos": "QoS",
"ssl": "SSL",
@@ -396,19 +396,19 @@
"text": "Text"
},
"copy_link": "Salin tautan",
"different_collection": "Tidak dapat mengubah urutan permintaan dari koleksi yang berbeda",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Durasi",
"enter_curl": "Masukkan cURL",
"generate_code": "Hasilkan kode",
"generated_code": "Hasilkan kode",
"generate_code": "Generate code",
"generated_code": "Generated code",
"header_list": "Daftar Header",
"invalid_name": "Harap berikan nama untuk request",
"method": "Method",
"moved": "Request moved",
"name": "Request nama",
"new": "Request baru",
"order_changed": "Urutan Request Diperbarui",
"order_changed": "Request Order Updated",
"override": "Membatalkan",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Diganti",
@@ -453,7 +453,7 @@
"settings": {
"accent_color": "Accent color",
"account": "Akun",
"account_deleted": "Akun Anda telah dihapus",
"account_deleted": "Your account has been deleted",
"account_description": "Sesuaikan pengaturan akun Anda.",
"account_email_description": "Alamat surel utama Anda.",
"account_name_description": "Ini adalah nama tampilan Anda.",
@@ -609,7 +609,7 @@
"file_imported": "File diimpor",
"finished_in": "Selesai dalam {duration} ms",
"history_deleted": "Riwayat dihapus",
"linewrap": "Bungkus baris",
"linewrap": "Wrap lines",
"loading": "Memuat...",
"message_received": "Pesan: {message} tiba di topik: {topic}",
"mqtt_subscription_failed": "Terjadi masalah saat berlangganan topik: {topic}",
@@ -666,7 +666,7 @@
"email_do_not_match": "Surel tidak cocok dengan detail akun Anda. Hubungi pemilik tim Anda.",
"exit": "Keluar dari Tim",
"exit_disabled": "Hanya pemilik yang tidak dapat keluar dari tim",
"invalid_coll_id": "ID koleksi tidak valid",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Format surel tidak valid",
"invalid_id": "ID tim tidak valid. Hubungi pemilik tim Anda.",
"invalid_invite_link": "Tautan undangan tidak valid",
@@ -690,7 +690,7 @@
"member_removed": "Pengguna dihapus",
"member_role_updated": "Peran pengguna diperbarui",
"members": "Anggota",
"more_members": "+{count} lebih",
"more_members": "+{count} more",
"name_length_insufficient": "Nama tim harus setidaknya 6 karakter",
"name_updated": "Nama tim diperbarui",
"new": "Tim Baru",
@@ -698,13 +698,13 @@
"new_name": "Tim baru saya",
"no_access": "Anda tidak memiliki akses edit ke collections ini",
"no_invite_found": "Undangan tidak ditemukan. Hubungi pemilik tim Anda.",
"no_request_found": "Request tidak ditemukan.",
"no_request_found": "Request not found.",
"not_found": "Tim tidak ditemukan. Hubungi pemilik tim Anda.",
"not_valid_viewer": "Anda bukan penonton yang valid. Hubungi pemilik tim Anda.",
"parent_coll_move": "Tidak dapat memindahkan koleksi ke dalam koleksi anak",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Undangan tertunda",
"permissions": "Izin",
"same_target_destination": "Sama tujuan dan destinasi",
"same_target_destination": "Same target and destination",
"saved": "Tim disimpan",
"select_a_team": "Pilih tim",
"title": "tim",
@@ -712,9 +712,9 @@
"we_sent_invite_link_description": "Minta semua undangan untuk memeriksa kotak masuk mereka. Klik tautan untuk bergabung dengan tim."
},
"team_environment": {
"deleted": "Environment dihapus",
"duplicate": "Environment diduplikasi",
"not_found": "Environment tidak ditemukan."
"deleted": "Environment Deleted",
"duplicate": "Environment Duplicated",
"not_found": "Environment not found."
},
"test": {
"failed": "Tes gagal",
@@ -734,9 +734,9 @@
"url": "URL"
},
"workspace": {
"change": "Beralih workspace",
"personal": "Workspace Saya",
"team": "Workspace Tim",
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

View File

@@ -1,50 +1,48 @@
{
"action": {
"autoscroll": "Автоскрол",
"autoscroll": "Autoscroll",
"cancel": "Отменить",
"choose_file": "Выберите файл",
"clear": "Очистить",
"clear_all": "Очистить все",
"clear_history": "Очистить всю историю",
"close": "Закрыть",
"close": "Close",
"connect": "Подключиться",
"connecting": "Соединение...",
"connecting": "Connecting",
"copy": "Скопировать",
"delete": "Удалить",
"disconnect": "Отключиться",
"dismiss": "Скрыть",
"dont_save": "Не сохранять",
"dont_save": "Don't save",
"download_file": "Скачать файл",
"drag_to_reorder": "Перетягивайте для сортировки",
"drag_to_reorder": "Drag to reorder",
"duplicate": "Дублировать",
"edit": "Редактировать",
"filter": "Фильтр",
"filter": "Filter",
"go_back": "Вернуться",
"go_forward": "Вперёд",
"group_by": "Сгруппировать по",
"go_forward": "Go forward",
"group_by": "Group by",
"label": "Название",
"learn_more": "Узнать больше",
"less": "Меньше",
"less": "Less",
"more": "Больше",
"new": "Создать новый",
"no": "Нет",
"open_workspace": "Открыть пространство",
"paste": "Вставить",
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Форматировать",
"rename": "Переименовать",
"remove": "Удалить",
"restore": "Восстановить",
"save": "Сохранить",
"scroll_to_bottom": "Вниз",
"scroll_to_top": "Вверх",
"scroll_to_bottom": "Scroll to bottom",
"scroll_to_top": "Scroll to top",
"search": "Поиск",
"send": "Отправить",
"start": "Начать",
"starting": "Запускаю",
"starting": "Starting",
"stop": "Стоп",
"to_close": "что бы закрыть",
"to_navigate": "для навигации",
"to_select": "выборать",
"to_close": "to close",
"to_navigate": "to navigate",
"to_select": "to select",
"turn_off": "Выключить",
"turn_on": "Включить",
"undo": "Отменить",
@@ -58,9 +56,9 @@
"chat_with_us": "Связаться с нами",
"contact_us": "Свяжитесь с нами",
"copy": "Копировать",
"copy_user_id": "Копировать токен пользователя",
"developer_option": "Настройки разработчика",
"developer_option_description": "Инструмент разработчика помогает обслуживить и развивить 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": "Документация",
"github": "GitHub",
@@ -69,13 +67,11 @@
"invite": "Пригласить",
"invite_description": "В Hoppscotch мы разработали простой и интуитивно понятный интерфейс для создания и управления вашими API. Hoppscotch - это инструмент, который помогает создавать, тестировать, документировать и делиться своими API.",
"invite_your_friends": "Пригласить своих друзей",
"social_links": "Социальные сети",
"social_description": "Подписывайся на наши соц. сети и оставайся всегда в курсе последних новостей, обновлений и релизов.",
"join_discord_community": "Присоединяйтесь к нашему сообществу Discord",
"keyboard_shortcuts": "Горячие клавиши",
"name": "Hoppscotch",
"new_version_found": "Найдена новая версия. Перезагрузите для обновления.",
"options": "Настройки",
"options": "Options",
"proxy_privacy_policy": "Политика конфиденциальности прокси",
"reload": "Перезагрузить",
"search": "Поиск",
@@ -83,7 +79,7 @@
"shortcuts": "Ярлыки",
"spotlight": "Прожектор",
"status": "Статус",
"status_description": "Проверить состояние сайта",
"status_description": "Check the status of the website",
"terms_and_privacy": "Условия и конфиденциальность",
"twitter": "Twitter",
"type_a_command_search": "Введите команду или выполните поиск…",
@@ -97,7 +93,7 @@
"continue_with_email": "Продолжить с электронной почтой",
"continue_with_github": "Продолжить с GitHub",
"continue_with_google": "Продолжить с Google",
"continue_with_microsoft": "Продолжить с Microsoft",
"continue_with_microsoft": "Continue with Microsoft",
"email": "Электронное письмо",
"logged_out": "Вышли из",
"login": "Авторизоваться",
@@ -122,20 +118,19 @@
},
"collection": {
"created": "Коллекция создана",
"different_parent": "Нельзя сортировать коллекцию с разной родительской коллекцией",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Редактировать коллекцию",
"invalid_name": "Укажите допустимое название коллекции",
"invalid_root_move": "Коллекция уже в корне",
"moved": "Перемещено успешно",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "Мои коллекции",
"name": "Новая коллекция",
"name_length_insufficient": "Имя коллекции должно иметь 3 или более символов",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "Создать коллекцию",
"order_changed": "Порядок коллекции обновлён",
"order_changed": "Collection Order Updated",
"renamed": "Коллекция переименована",
"request_in_use": "Запрос обрабатывается",
"save_as": "Сохранить как",
"save_to_collection": "Сохранить в коллекцию",
"select": "Выбрать коллекцию",
"select_location": "Выберите местоположение",
"select_team": "Выберите команду",
@@ -151,17 +146,10 @@
"remove_request": "Вы уверены, что хотите навсегда удалить этот запрос?",
"remove_team": "Вы уверены, что хотите удалить эту команду?",
"remove_telemetry": "Вы действительно хотите отказаться от телеметрии?",
"request_change": "Вы уверены что хотите сбросить текущий запрос, все не сохранённые данные будт утеряны?",
"save_unsaved_tab": "Вы хотите сохранить изменения в этой вкладке?",
"close_unsaved_tab": "Вы уверены что хотите закрыть эту вкладку?",
"close_unsaved_tabs": "ВЫ уверены что хотите закрыть все эти вкладки? Несохранённые данные {count} вкладок будут утеряны.",
"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": "Вы уверены, что хотите синхронизировать это рабочее пространство?"
},
"context_menu": {
"set_environment_variable": "Назначить как переменную",
"add_parameters": "Добавить в параметры",
"open_request_in_new_tab": "Открыть в новой вкладке"
},
"count": {
"header": "Заголовок {count}",
"message": "Тело {count}",
@@ -192,102 +180,83 @@
"profile": "Войдите, чтобы просмотреть свой профиль",
"protocols": "Протоколы пустые",
"schema": "Подключиться к конечной точке GraphQL",
"shortcodes": "Нет коротких ссылок",
"subscription": "Нет подписок",
"shortcodes": "Shortcodes are empty",
"subscription": "Subscriptions are empty",
"team_name": "Название команды пусто",
"teams": "Команды пустые",
"tests": "Для этого запроса нет тестов"
},
"environment": {
"add_to_global": "Добавить в глобальное окружение",
"added": "Окружение добавлено",
"create_new": "Создать новое окружение",
"created": "Окружение создано",
"deleted": "Окружение удалено",
"duplicated": "Окружение скопировано",
"global": "Глобальное окружение",
"empty_variables": "Нет переменных",
"global_variables": "Глобальные переменные",
"edit": "Редактировать окружение",
"invalid_name": "Укажите допустимое имя для окружения",
"list": "Переменные окружения",
"my_environments": "Мои окружения",
"name": "Название",
"nested_overflow": "максимальный уровень вложения переменных окружения - 10",
"add_to_global": "Add to Global",
"added": "Environment addition",
"create_new": "Создать новую среду",
"created": "Environment created",
"deleted": "Environment deletion",
"edit": "Редактировать среду",
"invalid_name": "Укажите допустимое имя для среды",
"my_environments": "My Environments",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "Новая среда",
"no_active_environment": "Нет активных окружений",
"no_environment": "Нет окружения",
"no_environment_description": "Не выбрано окружение, выберите что делать с переменными.",
"quick_peek": "Быстрый просмотр окружения",
"replace_with_variable": "Заменить переменной",
"scope": "Scope",
"no_environment": "Нет окружающей среды",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"select": "Выберите среду",
"set": "Выбрать окружение",
"set_as_environment": "Установить как окружение",
"team_environments": "Окружения команды",
"title": "Окружения",
"updated": "Окружение обновлено",
"value": "Значение",
"variable": "Переменная",
"team_environments": "Team Environments",
"title": "Среды",
"updated": "Environment updation",
"variable_list": "Список переменных"
},
"error": {
"browser_support_sse": "Похоже, в этом браузере нет поддержки событий, отправленных сервером.",
"check_console_details": "Подробности смотрите в журнале консоли.",
"curl_invalid_format": "cURL неправильно отформатирован",
"danger_zone": "Опасная зона",
"delete_account": "Вы являетесь владельцем этой команды:",
"delete_account_description": "Прежде чем удалить аккаунт вам необходимо либо назначить владельцом другого пользователя, либо удалить команды в которых вы являетесь владельцем.",
"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": "Пустое имя запроса",
"f12_details": "(F12 для подробностей)",
"gql_prettify_invalid_query": "Не удалось определить недопустимый запрос, устранить синтаксические ошибки запроса и повторить попытку.",
"incomplete_config_urls": "Не заполнены URL конфигурации",
"incorrect_email": "Не корректный Email",
"invalid_link": "Не корректная ссылка",
"incomplete_config_urls": "Incomplete configuration URLs",
"incorrect_email": "Incorrect email",
"invalid_link": "Invalid link",
"invalid_link_description": "Ссылка, по которой вы перешли, - недействительна, либо срок ее действия истек.",
"json_parsing_failed": "Не корректный JSON",
"json_parsing_failed": "Invalid JSON",
"json_prettify_invalid_body": "Не удалось определить недопустимое тело, устранить синтаксические ошибки json и повторить попытку.",
"network_error": "Похоже, возникла проблема с соединением. Попробуйте еще раз.",
"network_fail": "Не удалось отправить запрос",
"no_duration": "Без продолжительности",
"no_results_found": "Совпадения не найдены",
"page_not_found": "Эта страница не найдена",
"proxy_error": "Ошибка прокси",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"script_fail": "Не удалось выполнить сценарий предварительного запроса",
"something_went_wrong": "Что-то пошло не так",
"test_script_fail": "Не удалось выполнить тестирование запроса"
"test_script_fail": "Could not execute post-request script"
},
"export": {
"as_json": "Экспорт как JSON",
"create_secret_gist": "Создать секретный Gist",
"gist_created": "Gist создан",
"require_github": "Войдите через GitHub, чтобы создать секретную суть",
"title": "Экспорт"
"title": "Export"
},
"filter": {
"all": "Все",
"none": "Не указано",
"starred": "Отмечено"
"all": "All",
"none": "None",
"starred": "Starred"
},
"folder": {
"created": "Папка создана",
"edit": "Редактировать папку",
"invalid_name": "Укажите имя для папки",
"name_length_insufficient": "Имя папки должно содержать 3 или более символов",
"name_length_insufficient": "Folder name should be at least 3 characters long",
"new": "Новая папка",
"renamed": "Папка переименована"
},
"graphql": {
"mutations": "Мутации",
"schema": "Схема",
"subscriptions": "Подписки",
"switch_connection": "Изменить соединение",
"connection_switch_url": "Вы присоединились к GraphQL, URL соединения",
"connection_switch_new_url": "Смена вкладки разорвёт текущее GraphQL соединение. Новый URL соединения будет",
"connection_switch_confirm": "Вы желаете соединиться с последним GraphQL сервером?"
"subscriptions": "Подписки"
},
"group": {
"time": "Время",
"time": "Time",
"url": "URL"
},
"header": {
@@ -304,11 +273,11 @@
"post_request_tests": "Сценарии тестирования написаны на JavaScript и запускаются после получения ответа.",
"pre_request_script": "Скрипты предварительного запроса написаны на JavaScript и запускаются перед отправкой запроса.",
"script_fail": "Похоже, в скрипте предварительного запроса есть сбой. Проверьте ошибку ниже и исправьте скрипт соответствующим образом.",
"test_script_fail": "Похоже, что скрипт тестирования содержит ошибку. Пожалуйста исправьте её и попробуйте снова",
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
"tests": "Напишите тестовый сценарий для автоматизации отладки."
},
"hide": {
"collection": "Свернуть панель соединения",
"collection": "Collapse Collection Panel",
"more": "Скрыть больше",
"preview": "Скрыть предварительный просмотр",
"sidebar": "Скрыть боковую панель"
@@ -318,85 +287,61 @@
"curl": "Импортировать cURL",
"failed": "Ошибка импорта",
"from_gist": "Импорт из Gist",
"from_gist_description": "Импортировать через Gist URL",
"from_insomnia": "Импортировать с Insomnia",
"from_insomnia_description": "Импортировать из коллекции Insomnia",
"from_json": "Импортировать из Hoppscotch",
"from_json_description": "Импортировать из файла коллекции Hoppscotch",
"from_gist_description": "Import from Gist URL",
"from_insomnia": "Import from Insomnia",
"from_insomnia_description": "Import from Insomnia collection",
"from_json": "Import from Hoppscotch",
"from_json_description": "Import from Hoppscotch collection file",
"from_my_collections": "Импортировать из моих коллекций",
"from_my_collections_description": "Импортировать коллекции из моего файла",
"from_openapi": "Импортировать из OpenAPI",
"from_openapi_description": "Импортировать из OpenAPI файла описания API (YML/JSON)",
"from_postman": "Импортировать из Postman",
"from_postman_description": "Импортировать из коллекции Postman",
"from_url": "Импортировать из URL",
"from_my_collections_description": "Import from My Collections file",
"from_openapi": "Import from OpenAPI",
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_url": "Import from URL",
"gist_url": "Введите URL-адрес Gist",
"import_from_url_invalid_fetch": "Не удалить получить данные по этому URL",
"import_from_url_invalid_file_format": "Ошибка при импорте коллекций",
"import_from_url_invalid_type": "Неподдерживаемый тип. Поддерживаемые типы: 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Коллекция импортирована",
"json_description": "Импортировать из коллекции Hoppscotch",
"import_from_url_invalid_fetch": "Couldn't get data from the url",
"import_from_url_invalid_file_format": "Error while importing collections",
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Collections Imported",
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"title": "Импортировать"
},
"inspections": {
"title": "Инспектор",
"description": "Помогает обноружить возможные проблемы",
"environment": {
"add_environment": "Добавить в окружение",
"not_found": "Переменная окружения “{environment}” не найдена."
},
"header": {
"cookie": "Браузерная версия Hoppscotch не может использовать Cookie Header. Пока мы работаем над созданием десктоп версии Hoppscotch, пожалуйста используйте Authorization Header."
},
"response": {
"401_error": "Пожалуйста проверьте ваши параметры авторизации",
"404_error": "Пожалуйста проверьте URL и Метод вашего запроса",
"network_error": "Пожалуйста проверьте соединение",
"cors_error": "Пожалуйста проверьте вашу Cross-Origin Resource Sharing настройку сервера.",
"default_error": "Проверьте ваш запрос."
},
"url": {
"extension_not_installed": "Расширение не установлено",
"extention_not_enabled": "Расширение не включено",
"extention_enable_action": "Включить расширение браузера",
"extension_unknown_origin": "Убедитесь что вы добавили адрес сервера в Hoppscotch расширение."
}
},
"layout": {
"collapse_collection": "Свернуть или развернуть коллекции",
"collapse_sidebar": "Свернуть или развернуть боковую панель",
"column": "Вертикальная развёртка",
"name": "Развёртка",
"row": "Горизонтальная развертка",
"collapse_collection": "Collapse or Expand Collections",
"collapse_sidebar": "Collapse or Expand the sidebar",
"column": "Вертикальное оформление",
"name": "Layout",
"row": "Горизонтальное оформление",
"zen_mode": "Спокойный режим"
},
"modal": {
"close_unsaved_tab": "У вас есть не сохранённые изменения",
"close_unsaved_tab": "You have unsaved changes",
"collections": "Коллекции",
"confirm": "Подтверждать",
"edit_request": "Изменить запрос",
"import_export": "Импорт Экспорт"
},
"mqtt": {
"already_subscribed": "Вы уже подписаны на этот топик",
"clean_session": "Очистить сессию",
"clear_input": "Очистить ввод",
"clear_input_on_send": "Очистить ввод перед отправкой",
"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": "Выбрать цвет",
"color": "Pick a color",
"communication": "Коммуникация",
"connection_config": "Конфигурация соединения",
"connection_not_authorized": "Это соединение MQTT не использует какую-либо авторизацию.",
"invalid_topic": "Пожалуйста выберите topic для подписки",
"keep_alive": "Поддерживать соединение",
"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": "Лог",
"lw_message": "Last-Will Message",
"lw_qos": "Last-Will QoS",
"lw_retain": "Last-Will Retain",
"lw_topic": "Last-Will Topic",
"message": "Сообщение",
"new": "Новая подписка",
"not_connected": "Пожалуйста, сначала запустите MQTT соединение.",
"new": "New Subscription",
"not_connected": "Please start a MQTT connection first.",
"publish": "Публиковать",
"qos": "QoS",
"ssl": "SSL",
@@ -410,7 +355,7 @@
"navigation": {
"doc": "Документы",
"graphql": "GraphQL",
"profile": "Профиль",
"profile": "Profile",
"realtime": "В реальном времени",
"rest": "REST",
"settings": "Настройки"
@@ -418,12 +363,12 @@
"preRequest": {
"javascript_code": "Код JavaScript",
"learn": "Читать документацию",
"script": "Предворительный скрипт запроса",
"snippets": "Готовый код"
"script": "Сценарий предварительного запроса",
"snippets": "Фрагменты"
},
"profile": {
"app_settings": "Настройки приложения",
"default_hopp_displayname": "Безымянный",
"default_hopp_displayname": "Unnamed User",
"editor": "Редактор",
"editor_description": "Редакторы могут добавлять, редактировать, а так же удалять запросы.",
"email_verification_mail": "На вашу электронную почту отправлено письмо для подтверждения. Перейдите по ссылке из письма, чтобы подтвердить свой электронный адрес.",
@@ -446,13 +391,13 @@
"choose_language": "Выберите язык",
"content_type": "Тип содержимого",
"content_type_titles": {
"others": "Другие",
"structured": "Структурированный",
"text": "Текст"
"others": "Others",
"structured": "Structured",
"text": "Text"
},
"copy_link": "Копировать ссылку",
"different_collection": "Нельзя изменять порядок запросов из разных коллекций",
"duplicated": "Запрос скопирован",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Продолжительность",
"enter_curl": "Введите cURL",
"generate_code": "Сгенерировать код",
@@ -460,13 +405,13 @@
"header_list": "Список заголовков",
"invalid_name": "Укажите имя для запроса",
"method": "Методика",
"moved": "Запрос перемещён",
"moved": "Request moved",
"name": "Имя запроса",
"new": "Новый запрос",
"order_changed": "Порядок запроса изменён",
"override": "Переопределить",
"override_help": "Установить <kbd>Content-Type</kbd> в Заголовках",
"overriden": "Переопределено",
"new": "New Request",
"order_changed": "Request Order Updated",
"override": "Override",
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
"overriden": "Overridden",
"parameter_list": "Параметры запроса",
"parameters": "Параметры",
"path": "Путь",
@@ -479,17 +424,17 @@
"save_as": "Сохранить как",
"saved": "Запрос сохранен",
"share": "Делиться",
"share_description": "Поделиться Hoppscotch с друзьями",
"share_description": "Share Hoppscotch with your friends",
"title": "Запрос",
"type": "Тип запроса",
"url": "URL",
"variables": "Переменные",
"view_my_links": "Посмотреть мои ссылки"
"view_my_links": "View my links"
},
"response": {
"audio": "Аудио",
"audio": "Audio",
"body": "Тело ответа",
"filter_response_body": "Отфильтровать ответ в формате JSON (используется синтаксис JSONPath)",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Заголовки",
"html": "HTML",
"image": "Изображение",
@@ -501,14 +446,14 @@
"status": "Статус",
"time": "Время",
"title": "Ответ",
"video": "Видео",
"video": "Video",
"waiting_for_connection": "Ожидание соединения",
"xml": "XML"
},
"settings": {
"accent_color": "Основной цвет",
"account": "Счет",
"account_deleted": "Ваш аккаунт был удалён",
"account_deleted": "Your account has been deleted",
"account_description": "Настройте параметры своей учетной записи.",
"account_email_description": "Ваш основной адрес электронной почты.",
"account_name_description": "Это ваше отображаемое имя.",
@@ -517,8 +462,8 @@
"change_font_size": "Изменить размер шрифта",
"choose_language": "Выберите язык",
"dark_mode": "Темный",
"delete_account": "Удалить аккаунт",
"delete_account_description": "Удаление аккаунта нельзя отменить",
"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": "Раскрыть панель навигации",
"experiments": "Эксперименты",
"experiments_notice": "Это набор экспериментов, над которыми мы работаем, которые могут оказаться полезными, интересными, и тем, и другим, или ни тем, ни другим. Они не окончательные и могут быть нестабильными, поэтому, если произойдет что-то слишком странное, не паникуйте. Просто выключи эту чертову штуку. Шутки в сторону,",
@@ -545,8 +490,8 @@
"proxy_use_toggle": "Используйте промежуточное ПО прокси для отправки запросов",
"read_the": "Прочтите",
"reset_default": "Восстановление значений по умолчанию",
"short_codes": "Короткие ссылки",
"short_codes_description": "Короткие ссылки, созданные вами",
"short_codes": "Short codes",
"short_codes_description": "Short codes which were created by you.",
"sidebar_on_left": "Панель слева",
"sync": "Синхронизировать",
"sync_collections": "Коллекции",
@@ -560,16 +505,16 @@
"theme_description": "Настройте тему своего приложения.",
"use_experimental_url_bar": "Использовать экспериментальную строку URL с выделением среды",
"user": "Пользователь",
"verified_email": "Проверенный Email",
"verify_email": "Подтвердить Email"
"verified_email": "Verified email",
"verify_email": "Подтвердить почту"
},
"shortcodes": {
"actions": "Действия",
"created_on": "Создано",
"deleted": "Удалёна",
"method": "Метод",
"not_found": "Короткая ссылка не найдена",
"short_code": "Короткая ссылка",
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"short_code": "Short code",
"url": "URL"
},
"shortcut": {
@@ -580,10 +525,6 @@
"show_all": "Горячие клавиши",
"title": "Общий"
},
"others": {
"title": "Другие",
"prettify": "Прекрасные редакторы"
},
"miscellaneous": {
"invite": "Пригласите людей в Hoppscotch",
"title": "Разное"
@@ -604,9 +545,6 @@
"delete_method": "Выберите метод DELETE",
"get_method": "Выберите метод GET",
"head_method": "Выберите метод HEAD",
"rename": "Переименовать запрос",
"import_curl": "Импорт из cURL",
"show_code": "Сгенерировать готовый код",
"method": "Методика",
"next_method": "Выберите следующий метод",
"post_method": "Выберите метод POST",
@@ -615,120 +553,35 @@
"reset_request": "Сбросить запрос",
"save_to_collections": "Сохранить в коллекции",
"send_request": "Послать запрос",
"save_request": "Сохарнить запрос",
"title": "Запрос"
},
"response": {
"copy": "Копировать запрос в буфер обмена",
"download": "Скачать запрос как файл",
"title": "Запрос"
"copy": "Copy response to clipboard",
"download": "Download response as file",
"title": "Response"
},
"theme": {
"black": "Черный режим",
"dark": "Тёмный режим",
"light": "Светлый режим",
"system": "Определяется системой",
"title": "Тема"
"black": "Switch theme to black mode",
"dark": "Switch theme to dark mode",
"light": "Switch theme to light mode",
"system": "Switch theme to system mode",
"title": "Theme"
}
},
"show": {
"code": "Показать код",
"collection": "Развернуть панель коллекций",
"collection": "Expand Collection Panel",
"more": "Показать больше",
"sidebar": "Показать боковую панель"
},
"socketio": {
"communication": "Коммуникация",
"connection_not_authorized": "Это SocketIO соединение не использует какую-либо авторизацию.",
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
"event_name": "Название события",
"events": "События",
"log": "Лог",
"url": "URL"
},
"spotlight": {
"general": {
"help_menu": "Помощь и поддержка",
"chat": "Чат с поддержкой",
"open_docs": "Прочитать документацию",
"open_keybindings": "Горячие клавиши",
"open_github": "Открыть GitHub репозиторий",
"social": "Соц. сети",
"title": "Основное"
},
"miscellaneous": {
"invite": "Пригласите друзей в Hoppscotch",
"title": "Разное"
},
"request": {
"switch_to": "Перейти к",
"select_method": "Выбрать метод",
"save_as_new": "Сохранить как новый запрос",
"tab_parameters": "Параметры",
"tab_body": "Тело",
"tab_headers": "Заголовки",
"tab_authorization": "Авторизация",
"tab_pre_request_script": "Пред-скрипт",
"tab_tests": "Тесты",
"tab_query": "Запрос",
"tab_variables": "Переменные"
},
"graphql": {
"connect": "Соединиться",
"disconnect": "Разъединить"
},
"response": {
"copy": "Копировать ответ",
"download": "Скачать ответ",
"title": "Ответ"
},
"environments": {
"new": "Создать новое окружение",
"new_variable": "Создать новую переменную в окружении",
"edit": "Изменить текущее окружение",
"delete": "Удалить текущее окружение",
"duplicate": "Скопировать текущее окружение",
"edit_global": "Изменить глобальное окружение",
"duplicate_global": "Скопировать глобальное окружение",
"title": "Окружения"
},
"workspace": {
"new": "Создать новую команду",
"edit": "Изменить текущую команду",
"delete": "Удалить текущую команду",
"invite": "Пригласить людей в команду",
"switch_to_personal": "Переключиться на личное пространство",
"title": "Команды"
},
"tab": {
"duplicate": "Скопировать вкладку",
"close_current": "Закрыть вкладку",
"close_others": "Закрыть все кроме этой",
"new_tab": "Открыть в новой вкладке",
"title": "Вкладки"
},
"section": {
"user": "Пользователь",
"theme": "Тема",
"interface": "Интерфейс",
"interceptor": "Перехватчик"
},
"change_language": "Изменить язык",
"settings": {
"theme": {
"black": "Чёрная",
"dark": "Тёмная",
"light": "Светлая",
"system": "Определяется системой"
},
"font": {
"size_sm": "Маленький",
"size_md": "Средний",
"size_lg": "Большой"
},
"change_interceptor": "Изменить перехватчик",
"change_language": "Изменить язык"
}
},
"sse": {
"event_type": "Тип события",
"log": "Лог",
@@ -736,14 +589,14 @@
},
"state": {
"bulk_mode": "Множественное редактирование",
"bulk_mode_placeholder": "Каждый параметр должен начинаться с новой строки\nКлючи и значения разедляются двоеточием\nИспользуйте # для комментария",
"bulk_mode_placeholder": "Entries are separated by newline\nKeys and values are separated by :\nPrepend # to any row you want to add but keep disabled",
"cleared": "Очищено",
"connected": "Связаны",
"connected_to": "Подключено к {name}",
"connecting_to": "Подключение к {name} ...",
"connection_error": "Ошибка подключения",
"connection_failed": "Не удалось установить соединение",
"connection_lost": "Соединение утеряно",
"connection_error": "Failed to connect",
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "Скопировано в буфер обмена",
"deleted": "Удалено",
"deprecated": "УСТАРЕЛО",
@@ -758,17 +611,17 @@
"history_deleted": "История удалена",
"linewrap": "Обернуть линии",
"loading": "Загрузка...",
"message_received": "Сообщение: {message} получено по топику: {topic}",
"mqtt_subscription_failed": "Что-то пошло не так, при попытке подписаться на топик: {topic}",
"message_received": "Message: {message} arrived on topic: {topic}",
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
"none": "Никто",
"nothing_found": "Ничего не найдено для",
"published_error": "Что-то пошло не так при попытке опубликовать сообщение в топик {topic}: {message}",
"published_message": "Опубликовано сообщение: {message} в топик: {topic}",
"reconnection_error": "Не удалось переподключиться",
"subscribed_failed": "Не удалось подписаться на топик: {topic}",
"subscribed_success": "Успешно подписался на топик: {topic}",
"unsubscribed_failed": "Не удалось отписаться от топика: {topic}",
"unsubscribed_success": "Успешно отписался от топика: {topic}",
"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": "Ожидание отправки запроса"
},
"support": {
@@ -777,7 +630,7 @@
"community": "Задавайте вопросы и помогайте другим",
"documentation": "Узнать больше о Hoppscotch",
"forum": "Задавайте вопросы и получайте ответы",
"github": "Подпишитесь на нас на Github",
"github": "Follow us on Github",
"shortcuts": "Просматривайте приложение быстрее",
"team": "Свяжитесь с командой",
"title": "Служба поддержки",
@@ -788,15 +641,15 @@
"body": "Тело",
"collections": "Коллекции",
"documentation": "Документация",
"environments": "Окружения",
"environments": "Environments",
"headers": "Заголовки",
"history": "История",
"mqtt": "MQTT",
"parameters": "Параметры",
"pre_request_script": "Пред-скрипт",
"pre_request_script": "Скрипт предварительного запроса",
"queries": "Запросы",
"query": "Запрос",
"schema": "Схема",
"schema": "Schema",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Тесты",
@@ -813,7 +666,7 @@
"email_do_not_match": "Электронная почта, которой Вы воспользовались не соответсвует указанной в данных Вашей учетной записи.",
"exit": "Выйти из команды",
"exit_disabled": "Только владелец не может выйти из команды",
"invalid_coll_id": "Не верный идентификатор коллекции",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Формат электронной почты недействителен",
"invalid_id": "Некорректный ID команды. Свяжитесь с руководителем команды.",
"invalid_invite_link": "Ссылка недействительна",
@@ -837,7 +690,7 @@
"member_removed": "Пользователь удален",
"member_role_updated": "Роли пользователей обновлены",
"members": "Участники",
"more_members": "+{count}",
"more_members": "+{count} more",
"name_length_insufficient": "Название команды должно быть не менее 6 символов.",
"name_updated": "Название команды обновлено",
"new": "Новая команда",
@@ -845,13 +698,13 @@
"new_name": "Моя новая команда",
"no_access": "У вас нет прав на редактирование этих коллекций",
"no_invite_found": "Такое приглашение мы не смогли найти. Свяжитесь с руководителем команды.",
"no_request_found": "Запрос не найден",
"not_found": "Команда не найдена, свяжитесь с владельцем команды",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "У Вас нет прав просматривать это. Свяжитесь с руководителем команды.",
"parent_coll_move": "Не удалось переместить коллекцию в дочернюю",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Ожидающие приглашения",
"permissions": "Разрешения",
"same_target_destination": "Таже цель и конечная точка",
"same_target_destination": "Same target and destination",
"saved": "Команда сохранена",
"select_a_team": "Выбрать команду",
"title": "Команды",
@@ -859,9 +712,9 @@
"we_sent_invite_link_description": "Попросите тех, кого Вы пригласили, проверить их почтовые ящики. Им нужно перейди по ссылке, чтобы подтвердить вступление в эту команду."
},
"team_environment": {
"deleted": "Окружение удалено",
"duplicate": "Окружение скопировано",
"not_found": "Окружение не найдено"
"deleted": "Environment Deleted",
"duplicate": "Environment Duplicated",
"not_found": "Environment not found."
},
"test": {
"failed": "Тест не пройден",
@@ -881,9 +734,9 @@
"url": "URL"
},
"workspace": {
"change": "Изменить пространство",
"personal": "Моё пространство",
"team": "Пространство команды",
"title": "Рабочие пространства"
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"title": "Workspaces"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.8.0",
"version": "2023.4.8",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -63,7 +63,7 @@
"graphql": "^16.8.0",
"graphql-language-service-interface": "^2.9.1",
"graphql-tag": "^2.12.6",
"httpsnippet": "^3.0.1",
"httpsnippet": "^2.0.0",
"insomnia-importers": "^3.6.0",
"io-ts": "^2.2.20",
"js-yaml": "^4.1.0",
@@ -117,7 +117,6 @@
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.3.3",
"@types/har-format": "^1.2.12",
"@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.8",
"@types/lossless-json": "^1.0.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 KiB

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 KiB

After

Width:  |  Height:  |  Size: 382 KiB

View File

@@ -18,7 +18,6 @@ import { HOPP_MODULES } from "@modules/."
import { isLoadingInitialRoute } from "@modules/router"
import { useI18n } from "@composables/i18n"
import { APP_IS_IN_DEV_MODE } from "@helpers/dev"
import { platform } from "./platform"
const t = useI18n()
@@ -46,5 +45,4 @@ if (APP_IS_IN_DEV_MODE) {
// Run module root component setup code
HOPP_MODULES.forEach((mod) => mod.onRootSetup?.())
platform.addedHoppModules?.forEach((mod) => mod.onRootSetup?.())
</script>

View File

@@ -29,7 +29,6 @@ declare module 'vue' {
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default']

View File

@@ -1,6 +1,7 @@
<template>
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
<AppShare :show="showShare" @hide-modal="showShare = false" />
<AppSocial :show="showSocial" @hide-modal="showSocial = false" />
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
<HoppSmartConfirmModal
@@ -17,6 +18,7 @@ import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { showChat } from "~/modules/crisp"
import { useToast } from "~/composables/toast"
import { useI18n } from "~/composables/i18n"
@@ -25,6 +27,7 @@ const t = useI18n()
const showShortcuts = ref(false)
const showShare = ref(false)
const showSocial = ref(false)
const showLogin = ref(false)
const confirmRemove = ref(false)
@@ -57,10 +60,18 @@ defineActionHandler("modals.share.toggle", () => {
showShare.value = !showShare.value
})
defineActionHandler("modals.social.toggle", () => {
showSocial.value = !showSocial.value
})
defineActionHandler("modals.login.toggle", () => {
showLogin.value = !showLogin.value
})
defineActionHandler("flyouts.chat.open", () => {
showChat()
})
defineActionHandler("modals.team.delete", ({ teamId }) => {
teamID.value = teamId
confirmRemove.value = true

View File

@@ -10,6 +10,18 @@
:class="{ '-rotate-180': !EXPAND_NAVIGATION }"
@click="EXPAND_NAVIGATION = !EXPAND_NAVIGATION"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="`${ZEN_MODE ? t('action.turn_off') : t('action.turn_on')} ${t(
'layout.zen_mode'
)}`"
:icon="ZEN_MODE ? IconMinimize : IconMaximize"
:class="{
'!text-accent !focus-visible:text-accentDark !hover:text-accentDark':
ZEN_MODE,
}"
@click="ZEN_MODE = !ZEN_MODE"
/>
<tippy interactive trigger="click" theme="popover">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -64,7 +76,6 @@
}
"
/>
<!--
<HoppSmartItem
ref="chat"
:icon="IconMessageCircle"
@@ -77,34 +88,20 @@
}
"
/>
-->
<template
v-for="footerItem in platform.ui?.additionalFooterMenuItems"
:key="footerItem.id"
>
<template v-if="footerItem.action.type === 'link'">
<HoppSmartItem
:icon="footerItem.icon"
:label="footerItem.text(t)"
:to="footerItem.action.href"
blank
@click="hide()"
/>
</template>
<HoppSmartItem
v-else
:icon="footerItem.icon"
:label="footerItem.text(t)"
blank
@click="
() => {
// @ts-expect-error TypeScript not understanding the type
footerItem.action.do()
hide()
}
"
/>
</template>
<HoppSmartItem
:icon="IconGift"
:label="`${t('app.whats_new')}`"
to="https://docs.hoppscotch.io/documentation/changelog"
blank
@click="hide()"
/>
<HoppSmartItem
:icon="IconActivity"
:label="t('app.status')"
to="https://status.hoppscotch.io"
blank
@click="hide()"
/>
<hr />
<HoppSmartItem
:icon="IconGithub"
@@ -199,20 +196,26 @@
</template>
<script setup lang="ts">
import { ref } from "vue"
import { ref, watch } from "vue"
import { version } from "~/../package.json"
import IconSidebar from "~icons/lucide/sidebar"
import IconMinimize from "~icons/lucide/minimize"
import IconMaximize from "~icons/lucide/maximize"
import IconZap from "~icons/lucide/zap"
import IconShare2 from "~icons/lucide/share-2"
import IconColumns from "~icons/lucide/columns"
import IconSidebarOpen from "~icons/lucide/sidebar-open"
import IconShieldCheck from "~icons/lucide/shield-check"
import IconBook from "~icons/lucide/book"
import IconMessageCircle from "~icons/lucide/message-circle"
import IconGift from "~icons/lucide/gift"
import IconActivity from "~icons/lucide/activity"
import IconGithub from "~icons/lucide/github"
import IconTwitter from "~icons/lucide/twitter"
import IconUserPlus from "~icons/lucide/user-plus"
import IconLock from "~icons/lucide/lock"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import { showChat } from "@modules/crisp"
import { useSetting } from "@composables/settings"
import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream"
@@ -227,6 +230,7 @@ const showDeveloperOptions = ref(false)
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
const SIDEBAR = useSetting("SIDEBAR")
const ZEN_MODE = useSetting("ZEN_MODE")
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
@@ -237,6 +241,13 @@ const currentUser = useReadonlyStream(
platform.auth.getCurrentUser()
)
watch(
() => ZEN_MODE.value,
() => {
EXPAND_NAVIGATION.value = !ZEN_MODE.value
}
)
const nativeShare = () => {
if (navigator.share) {
navigator
@@ -251,6 +262,10 @@ const nativeShare = () => {
}
}
const chatWithUs = () => {
showChat()
}
const showDeveloperOptionModal = () => {
if (currentUser.value) {
showDeveloperOptions.value = true

View File

@@ -30,55 +30,144 @@
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
{{ t("support.title") }}
</h2>
<template
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
:key="item.id"
>
<HoppSmartItem
v-if="item.action.type === 'link'"
:icon="item.icon"
:label="item.text(t)"
:to="item.action.href"
:description="item.subtitle(t)"
:info-icon="IconChevronRight"
active
blank
@click="hideModal()"
/>
<HoppSmartItem
v-else
:icon="item.icon"
:label="item.text(t)"
:description="item.subtitle(t)"
:info-icon="IconChevronRight"
active
@click="
() => {
// @ts-expect-error Typescript isn't able to understand
item.action.do()
hideModal()
}
"
/>
</template>
<HoppSmartItem
:icon="IconBook"
:label="t('app.documentation')"
to="https://docs.hoppscotch.io"
:description="t('support.documentation')"
:info-icon="IconChevronRight"
active
blank
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconGift"
:label="t('app.whats_new')"
to="https://docs.hoppscotch.io/documentation/changelog"
:description="t('support.changelog')"
:info-icon="IconChevronRight"
active
blank
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconActivity"
:label="t('app.status')"
to="https://status.hoppscotch.io"
blank
:description="t('app.status_description')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconLock"
:label="`${t('app.terms_and_privacy')}`"
to="https://docs.hoppscotch.io/support/privacy"
blank
:description="t('app.terms_and_privacy')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
{{ t("settings.follow") }}
</h2>
<HoppSmartItem
:icon="IconDiscord"
:label="t('app.discord')"
to="https://hoppscotch.io/discord"
blank
:description="t('app.join_discord_community')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconTwitter"
:label="t('app.twitter')"
to="https://hoppscotch.io/twitter"
blank
:description="t('support.twitter')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconGithub"
:label="`${t('app.github')}`"
to="https://github.com/hoppscotch/hoppscotch"
blank
:description="t('support.github')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconMessageCircle"
:label="t('app.chat_with_us')"
:description="t('support.chat')"
:info-icon="IconChevronRight"
active
@click="chatWithUs()"
/>
<HoppSmartItem
:icon="IconUserPlus"
:label="`${t('app.invite')}`"
:description="t('shortcut.miscellaneous.invite')"
:info-icon="IconChevronRight"
active
@click="expandInvite()"
/>
<HoppSmartItem
v-if="navigatorShare"
v-tippy="{ theme: 'tooltip' }"
:icon="IconShare2"
:label="`${t('request.share')}`"
:description="t('request.share_description')"
:info-icon="IconChevronRight"
active
@click="nativeShare()"
/>
</div>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { watch } from "vue"
import IconSidebar from "~icons/lucide/sidebar"
import IconSidebarOpen from "~icons/lucide/sidebar-open"
import IconBook from "~icons/lucide/book"
import IconGift from "~icons/lucide/gift"
import IconActivity from "~icons/lucide/activity"
import IconLock from "~icons/lucide/lock"
import IconDiscord from "~icons/brands/discord"
import IconTwitter from "~icons/brands/twitter"
import IconGithub from "~icons/lucide/github"
import IconMessageCircle from "~icons/lucide/message-circle"
import IconUserPlus from "~icons/lucide/user-plus"
import IconShare2 from "~icons/lucide/share-2"
import IconChevronRight from "~icons/lucide/chevron-right"
import { useSetting } from "@composables/settings"
import { invokeAction } from "~/helpers/actions"
import { showChat } from "@modules/crisp"
import { useI18n } from "@composables/i18n"
import { platform } from "~/platform"
const t = useI18n()
const navigatorShare = !!navigator.share
const ZEN_MODE = useSetting("ZEN_MODE")
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
const SIDEBAR = useSetting("SIDEBAR")
watch(
() => ZEN_MODE.value,
() => {
EXPAND_NAVIGATION.value = !ZEN_MODE.value
}
)
defineProps<{
show: boolean
}>()
@@ -87,6 +176,11 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const chatWithUs = () => {
showChat()
hideModal()
}
const expandNavigation = () => {
EXPAND_NAVIGATION.value = !EXPAND_NAVIGATION.value
hideModal()
@@ -97,6 +191,24 @@ const expandCollection = () => {
hideModal()
}
const expandInvite = () => {
invokeAction("modals.share.toggle")
}
const nativeShare = () => {
if (navigator.share) {
navigator
.share({
title: "Hoppscotch",
text: "Hoppscotch • Open source API development ecosystem - Helps you create requests faster, saving precious time on development.",
url: "https://hoppscotch.io",
})
.catch(console.error)
} else {
// fallback
}
}
const hideModal = () => {
emit("hide-modal")
}

View File

@@ -0,0 +1,135 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('app.social_links')"
@close="hideModal"
>
<template #body>
<div class="flex flex-col space-y-2">
<div class="grid grid-cols-3 gap-4">
<a
v-for="(platform, index) in platforms"
:key="`platform-${index}`"
:href="platform.link"
target="_blank"
class="social-link"
tabindex="0"
>
<component :is="platform.icon" class="w-6 h-6" />
<span class="mt-3">
{{ platform.name }}
</span>
</a>
<button class="social-link" @click="copyAppLink">
<component :is="copyIcon" class="w-6 h-6 text-xl" />
<span class="mt-3">
{{ t("app.copy") }}
</span>
</button>
</div>
</div>
</template>
<template #footer>
<p class="text-secondaryLight">
{{ t("app.social_description") }}
</p>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import IconFacebook from "~icons/brands/facebook"
import IconLinkedIn from "~icons/brands/linkedin"
import IconReddit from "~icons/brands/reddit"
import IconTwitter from "~icons/brands/twitter"
import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy"
import IconGitHub from "~icons/lucide/github"
const t = useI18n()
const toast = useToast()
defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const url = "https://hoppscotch.io"
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const platforms = [
{
name: "GitHub",
icon: IconGitHub,
link: `https://hoppscotch.io/github`,
},
{
name: "Twitter",
icon: IconTwitter,
link: `https://twitter.com/hoppscotch_io`,
},
{
name: "Facebook",
icon: IconFacebook,
link: `https://www.facebook.com/hoppscotch.io`,
},
{
name: "Reddit",
icon: IconReddit,
link: `https://www.reddit.com/r/hoppscotch`,
},
{
name: "LinkedIn",
icon: IconLinkedIn,
link: `https://www.linkedin.com/company/hoppscotch/`,
},
]
const copyAppLink = () => {
copyToClipboard(url)
copyIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const hideModal = () => {
emit("hide-modal")
}
</script>
<style lang="scss" scoped>
.social-link {
@apply border border-dividerLight;
@apply rounded;
@apply flex-col flex;
@apply p-4;
@apply items-center;
@apply justify-center;
@apply font-semibold;
@apply hover: (bg-primaryLight text-secondaryDark);
@apply focus: outline-none;
@apply focus-visible: border-divider;
svg {
@apply opacity-80;
}
&:hover {
svg {
@apply opacity-100;
}
}
}
</style>

View File

@@ -8,46 +8,89 @@
>
<template #body>
<div class="flex flex-col space-y-2">
<template
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
:key="item.id"
>
<HoppSmartItem
v-if="item.action.type === 'link'"
:icon="item.icon"
:label="item.text(t)"
:to="item.action.href"
:description="item.subtitle(t)"
:info-icon="IconChevronRight"
active
blank
@click="hideModal()"
/>
<HoppSmartItem
v-else
:icon="item.icon"
:label="item.text(t)"
:description="item.subtitle(t)"
:info-icon="IconChevronRight"
active
@click="
() => {
// @ts-expect-error Typescript isn't able to understand
item.action.do()
hideModal()
}
"
/>
</template>
<HoppSmartItem
:icon="IconBook"
:label="t('app.documentation')"
to="https://docs.hoppscotch.io"
:description="t('support.documentation')"
:info-icon="IconChevronRight"
active
blank
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconZap"
:label="t('app.keyboard_shortcuts')"
:description="t('support.shortcuts')"
:info-icon="IconChevronRight"
active
@click="showShortcuts()"
/>
<HoppSmartItem
:icon="IconGift"
:label="t('app.whats_new')"
to="https://docs.hoppscotch.io/documentation/changelog"
:description="t('support.changelog')"
:info-icon="IconChevronRight"
active
blank
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconMessageCircle"
:label="t('app.chat_with_us')"
:description="t('support.chat')"
:info-icon="IconChevronRight"
active
@click="chatWithUs()"
/>
<HoppSmartItem
:icon="IconGitHub"
:label="t('app.github')"
to="https://hoppscotch.io/github"
blank
:description="t('support.github')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconDiscord"
:label="t('app.join_discord_community')"
to="https://hoppscotch.io/discord"
blank
:description="t('support.community')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconTwitter"
:label="t('app.twitter')"
to="https://hoppscotch.io/twitter"
blank
:description="t('support.twitter')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
</div>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import IconTwitter from "~icons/brands/twitter"
import IconDiscord from "~icons/brands/discord"
import IconGitHub from "~icons/lucide/github"
import IconMessageCircle from "~icons/lucide/message-circle"
import IconGift from "~icons/lucide/gift"
import IconZap from "~icons/lucide/zap"
import IconBook from "~icons/lucide/book"
import IconChevronRight from "~icons/lucide/chevron-right"
import { invokeAction } from "@helpers/actions"
import { showChat } from "@modules/crisp"
import { useI18n } from "@composables/i18n"
import { platform } from "~/platform"
const t = useI18n()
@@ -59,6 +102,16 @@ const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const chatWithUs = () => {
showChat()
hideModal()
}
const showShortcuts = () => {
invokeAction("flyouts.keybinds.toggle")
hideModal()
}
const hideModal = () => {
emit("hide-modal")
}

View File

@@ -119,8 +119,5 @@ watch(
&.active {
@apply after:bg-accentLight;
}
scroll-padding: 4rem !important;
scroll-margin: 4rem !important;
}
</style>

View File

@@ -1,3 +0,0 @@
<template>
<IconLucideCheckCircle class="text-accent" />
</template>

View File

@@ -40,7 +40,7 @@
:key="`result-${result.id}`"
:entry="result"
:active="isEqual(selectedEntry, [sectionIndex, entryIndex])"
@mouseover="onMouseOver($event, sectionIndex, entryIndex)"
@mouseover="selectedEntry = [sectionIndex, entryIndex]"
@action="runAction(sectionID, result)"
/>
</div>
@@ -111,7 +111,6 @@ import {
SwitchWorkspaceSpotlightSearcherService,
WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
const t = useI18n()
@@ -139,7 +138,6 @@ useService(EnvironmentsSpotlightSearcherService)
useService(SwitchEnvSpotlightSearcherService)
useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService)
const search = ref("")
@@ -178,24 +176,6 @@ function runAction(searcherID: string, result: SpotlightSearcherResult) {
emit("hide-modal")
}
let lastMousePosition: { x: number; y: number }
const onMouseOver = (
e: MouseEvent,
sectionIndex: number,
entryIndex: number
) => {
const mousePosition = {
x: e.clientX,
y: e.clientY,
}
// if the position is same, do nothing
if (isEqual(lastMousePosition, mousePosition)) return
selectedEntry.value = [sectionIndex, entryIndex]
lastMousePosition = mousePosition
}
function newUseArrowKeysForNavigation() {
const selectedEntry = ref<[number, number]>([0, 0]) // [sectionIndex, entryIndex]
@@ -284,3 +264,4 @@ function newUseArrowKeysForNavigation() {
return { selectedEntry }
}
</script>
~/services/spotlight/searchers/workspace.searcher

View File

@@ -239,7 +239,6 @@ import {
resetTeamRequestsContext,
} from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler } from "~/helpers/actions"
const t = useI18n()
const toast = useToast()
@@ -2068,8 +2067,4 @@ const getErrorMessage = (err: GQLError<string>) => {
}
}
}
defineActionHandler("collection.new", () => {
displayModalAdd(true)
})
</script>

View File

@@ -21,12 +21,7 @@
<label for="value" class="font-semibold min-w-10">{{
t("environment.value")
}}</label>
<input
v-model="editingValue"
type="text"
class="input"
:placeholder="t('environment.value')"
/>
<input type="text" :value="value" class="input" />
</div>
<div class="flex items-center space-x-8 ml-2">
<label for="scope" class="font-semibold min-w-10">
@@ -110,12 +105,9 @@ watch(
scope.value = {
type: "global",
}
replaceWithVariable.value = false
editingName.value = ""
editingValue.value = ""
replaceWithVariable.value = false
}
editingName.value = props.name
editingValue.value = props.value
}
)
@@ -140,7 +132,6 @@ const scope = ref<Scope>({
const replaceWithVariable = ref(false)
const editingName = ref(props.name)
const editingValue = ref(props.value)
const addEnvironment = async () => {
if (!editingName.value) {
@@ -150,13 +141,13 @@ const addEnvironment = async () => {
if (scope.value.type === "global") {
addGlobalEnvVariable({
key: editingName.value,
value: editingValue.value,
value: props.value,
})
toast.success(`${t("environment.updated")}`)
} else if (scope.value.type === "my-environment") {
addEnvironmentVariable(scope.value.index, {
key: editingName.value,
value: editingValue.value,
value: props.value,
})
toast.success(`${t("environment.updated")}`)
} else {
@@ -164,7 +155,7 @@ const addEnvironment = async () => {
...scope.value.environment.environment.variables,
{
key: editingName.value,
value: editingValue.value,
value: props.value,
},
]
await pipe(
@@ -191,7 +182,7 @@ const addEnvironment = async () => {
//replace the currenttab endpoint containing the value in the text with variablename
currentActiveTab.value.document.request.endpoint =
currentActiveTab.value.document.request.endpoint.replace(
editingValue.value,
props.value,
variableName
)
}

View File

@@ -32,7 +32,6 @@
@keyup.escape="hide()"
>
<HoppSmartItem
v-if="!isScopeSelector"
:label="`${t('environment.no_environment')}`"
:info-icon="
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
@@ -49,21 +48,6 @@
}
"
/>
<HoppSmartItem
v-else-if="isScopeSelector && modelValue"
:label="t('environment.global')"
:icon="IconGlobe"
:info-icon="modelValue.type === 'global' ? IconCheck : undefined"
:active-info-icon="modelValue.type === 'global'"
@click="
() => {
$emit('update:modelValue', {
type: 'global',
})
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 ${
@@ -82,14 +66,14 @@
:key="`gen-${index}`"
:icon="IconLayers"
:label="gen.name"
:info-icon="isEnvActive(index) ? IconCheck : undefined"
:active-info-icon="isEnvActive(index)"
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
:active-info-icon="index === selectedEnv.index"
@click="
() => {
handleEnvironmentChange(index, {
type: 'my-environment',
environment: gen,
})
selectedEnvironmentIndex = {
type: 'MY_ENV',
index: index,
}
hide()
}
"
@@ -129,14 +113,18 @@
:key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name"
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
:active-info-icon="isEnvActive(gen.id)"
:info-icon="
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
@click="
() => {
handleEnvironmentChange(index, {
type: 'team-environment',
environment: gen,
})
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide()
}
"
@@ -297,7 +285,6 @@ import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
import IconEye from "~icons/lucide/eye"
import IconEdit from "~icons/lucide/edit"
import IconGlobe from "~icons/lucide/globe"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient"
@@ -308,39 +295,11 @@ import {
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import { workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { invokeAction } from "~/helpers/actions"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { Environment } from "@hoppscotch/data"
import { onMounted } from "vue"
import { onLoggedIn } from "~/composables/auth"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
type Scope =
| {
type: "global"
}
| {
type: "my-environment"
environment: Environment
index: number
}
| {
type: "team-environment"
environment: TeamEnvironment
}
const props = defineProps<{
isScopeSelector?: boolean
modelValue?: Scope
}>()
const emit = defineEmits<{
(e: "update:modelValue", data: Scope): void
}>()
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
@@ -355,38 +314,6 @@ const myEnvironments = useReadonlyStream(environments$, [])
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// TeamList-Adapter
const teamListAdapter = new TeamListAdapter(true)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
REMEMBERED_TEAM_ID.value = team.id
changeWorkspace({
teamID: team.id,
teamName: team.name,
type: "team",
})
}
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) switchToTeamWorkspace(team)
}
}
}
)
// TeamEnv List Adapter
const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
@@ -395,70 +322,6 @@ const teamEnvironmentList = useReadonlyStream(
[]
)
const handleEnvironmentChange = (
index: number,
env?:
| {
type: "my-environment"
environment: Environment
}
| {
type: "team-environment"
environment: TeamEnvironment
}
) => {
if (props.isScopeSelector && env) {
if (env.type === "my-environment") {
emit("update:modelValue", {
type: "my-environment",
environment: env.environment,
index,
})
} else if (env.type === "team-environment") {
emit("update:modelValue", {
type: "team-environment",
environment: env.environment,
})
}
} else {
if (env && env.type === "my-environment") {
selectedEnvironmentIndex.value = {
type: "MY_ENV",
index,
}
} else if (env && env.type === "team-environment") {
selectedEnvironmentIndex.value = {
type: "TEAM_ENV",
teamEnvID: env.environment.id,
teamID: env.environment.teamID,
environment: env.environment.environment,
}
}
}
}
const isEnvActive = (id: string | number) => {
if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") {
return props.modelValue.index === id
} else if (props.modelValue?.type === "team-environment") {
return (
props.modelValue?.type === "team-environment" &&
props.modelValue.environment &&
props.modelValue.environment.id === id
)
}
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return selectedEnv.value.index === id
} else {
return (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnv.value.teamEnvID === id
)
}
}
}
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
@@ -486,90 +349,34 @@ watch(
)
const selectedEnv = computed(() => {
if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") {
return {
type: "MY_ENV",
index: props.modelValue.index,
name: props.modelValue.environment?.name,
}
} else if (props.modelValue?.type === "team-environment") {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
myEnvironments.value[selectedEnvironmentIndex.value.index]
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: environment.name,
variables: environment.variables,
}
} 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: props.modelValue.environment.environment.name,
teamEnvID: props.modelValue.environment.id,
}
} else {
return { type: "global", name: "Global" }
}
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
myEnvironments.value[selectedEnvironmentIndex.value.index]
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: environment.name,
variables: environment.variables,
}
} 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,
variables: teamEnv.environment.variables,
}
} else {
return { type: "NO_ENV_SELECTED" }
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
}
} else {
return { type: "NO_ENV_SELECTED" }
}
}
})
// Set the selected environment as initial scope value
onMounted(() => {
if (props.isScopeSelector) {
if (
selectedEnvironmentIndex.value.type === "MY_ENV" &&
selectedEnvironmentIndex.value.index !== undefined
) {
emit("update:modelValue", {
type: "my-environment",
environment: myEnvironments.value[selectedEnvironmentIndex.value.index],
index: selectedEnvironmentIndex.value.index,
})
} else if (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID &&
teamEnvironmentList.value &&
teamEnvironmentList.value.length > 0
) {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
emit("update:modelValue", {
type: "team-environment",
environment: teamEnv,
})
}
} else {
emit("update:modelValue", {
type: "global",
})
}
} else {
return { type: "NO_ENV_SELECTED" }
}
})

View File

@@ -34,13 +34,6 @@
@hide-modal="displayModalNew(false)"
/>
</div>
<HoppSmartConfirmModal
:show="showConfirmRemoveEnvModal"
:title="t('confirm.remove_team')"
@hide-modal="showConfirmRemoveEnvModal = false"
@resolve="removeSelectedEnvironment()"
/>
</template>
<script setup lang="ts">
@@ -51,7 +44,6 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n"
import {
getSelectedEnvironmentIndex,
globalEnv$,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
@@ -62,15 +54,8 @@ import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { GQLError } from "~/helpers/backend/GQLClient"
import { deleteEnvironment } from "~/newstore/environments"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { useToast } from "~/composables/toast"
const t = useI18n()
const toast = useToast()
type EnvironmentType = "my-environments" | "team-environments"
@@ -183,7 +168,6 @@ watch(
}
)
const showConfirmRemoveEnvModal = ref(false)
const showModalNew = ref(false)
const showModalDetails = ref(false)
const action = ref<"new" | "edit">("edit")
@@ -210,30 +194,6 @@ const editEnvironment = (environmentIndex: "Global") => {
displayModalEdit(true)
}
const removeSelectedEnvironment = () => {
const selectedEnvIndex = getSelectedEnvironmentIndex()
if (selectedEnvIndex?.type === "NO_ENV_SELECTED") return
if (selectedEnvIndex?.type === "MY_ENV") {
deleteEnvironment(selectedEnvIndex.index)
toast.success(`${t("state.deleted")}`)
}
if (selectedEnvIndex?.type === "TEAM_ENV") {
pipe(
deleteTeamEnvironment(selectedEnvIndex.teamEnvID),
TE.match(
(err: GQLError<string>) => {
console.error(err)
},
() => {
toast.success(`${t("team_environment.deleted")}`)
}
)
)()
}
}
const resetSelectedData = () => {
editingEnvironmentIndex.value = null
}
@@ -243,10 +203,6 @@ defineActionHandler("modals.environment.new", () => {
showModalDetails.value = true
})
defineActionHandler("modals.environment.delete-selected", () => {
showConfirmRemoveEnvModal.value = true
})
defineActionHandler(
"modals.my.environment.edit",
({ envName, variableName }) => {
@@ -300,7 +256,7 @@ watch(
defineActionHandler("modals.environment.add", ({ envName, variableName }) => {
editingVariableName.value = envName
if (variableName) editingVariableValue.value = variableName
editingVariableValue.value = variableName
displayModalNew(true)
})
</script>

View File

@@ -312,10 +312,8 @@ const authProviders: AuthProviderItem[] = [
},
]
// Do not format the `import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS` call into multiple lines!
// prettier-ignore
const allowedAuthProvidersIDsString =
import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS
const allowedAuthProvidersIDsString: string | undefined = import.meta.env
.VITE_ALLOWED_AUTH_PROVIDERS
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
? allowedAuthProvidersIDsString.split(",")

View File

@@ -144,6 +144,8 @@ const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
const gqlQueryString = useVModel(props, "modelValue", emit)
const debouncedOnUpdateQueryState = debounce((update: ViewUpdate) => {
if (!update.selectionSet) return
const selectedPos = update.state.selection.main.head
const queryString = update.state.doc.toJSON().join(update.state.lineBreak)

View File

@@ -71,7 +71,6 @@ import { connect } from "~/helpers/graphql/connection"
import { disconnect } from "~/helpers/graphql/connection"
import { InterceptorService } from "~/services/interceptor.service"
import { useService } from "dioc/vue"
import { defineActionHandler } from "~/helpers/actions"
const t = useI18n()
@@ -141,12 +140,4 @@ const cancelSwitch = () => {
if (connected.value) disconnect()
connectionSwitchModal.value = false
}
defineActionHandler(
"gql.connect",
gqlConnect,
computed(() => !connected.value)
)
defineActionHandler("gql.disconnect", disconnect, connected)
</script>

View File

@@ -69,8 +69,8 @@ import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections"
export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization"
const selectedOptionTab = ref<GQLOptionTabs>("query")
type OptionTabs = "query" | "headers" | "variables" | "authorization"
const selectedOptionTab = ref<OptionTabs>("query")
const interceptorService = useService(InterceptorService)
const t = useI18n()
@@ -206,8 +206,4 @@ defineActionHandler("request.save-as", () => {
showSaveRequestModal.value = true
})
defineActionHandler("request.reset", clearGQLQuery)
defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionTab.value = tab as GQLOptionTabs
})
</script>

View File

@@ -128,14 +128,10 @@ const downloadResponse = (str: string) => {
}, 1000)
}
defineActionHandler(
"response.file.download",
() => downloadResponse(responseString.value),
computed(() => !!props.response && props.response.length > 0)
defineActionHandler("response.file.download", () =>
downloadResponse.bind(responseString.value)
)
defineActionHandler(
"response.copy",
() => copyResponse(responseString.value),
computed(() => !!props.response && props.response.length > 0)
defineActionHandler("response.copy", () =>
copyResponse.bind(responseString.value)
)
</script>

View File

@@ -508,17 +508,30 @@ const changeTab = (tab: ComputedHeader["source"]) => {
const inspectionService = useService(InspectionService)
const headerKeyResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "header" && result.locations.position === "key"
)
const allTabResults = inspectionService.tabs
const headerValueResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "header" && result.locations.position === "value"
)
const headerKeyResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
.filter(
(result) =>
result.locations.type === "header" &&
result.locations.position === "key"
) ?? []
)
})
const headerValueResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
.filter(
(result) =>
result.locations.type === "header" &&
result.locations.position === "value"
) ?? []
)
})
const getInspectorResult = (results: InspectorResult[], index: number) => {
return results.filter((result) => {

View File

@@ -9,7 +9,7 @@
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/rest-api-testing"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/using-parameters"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
@@ -178,7 +178,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
import IconCircle from "~icons/lucide/circle"
import IconTrash from "~icons/lucide/trash"
import IconWrapText from "~icons/lucide/wrap-text"
import { reactive, ref, watch } from "vue"
import { computed, reactive, ref, watch } from "vue"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -409,18 +409,30 @@ const clearContent = () => {
const inspectionService = useService(InspectionService)
const parameterKeyResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "parameter" && result.locations.position === "key"
)
const allTabResults = inspectionService.tabs
const parameterValueResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) =>
result.locations.type === "parameter" &&
result.locations.position === "value"
)
const parameterKeyResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
.filter(
(result) =>
result.locations.type === "parameter" &&
result.locations.position === "key"
) ?? []
)
})
const parameterValueResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
.filter(
(result) =>
result.locations.type === "parameter" &&
result.locations.position === "value"
) ?? []
)
})
const getInspectorResult = (results: InspectorResult[], index: number) => {
return results.filter((result) => {

View File

@@ -642,5 +642,9 @@ const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService)
const tabResults = inspectionService.getResultViewFor(currentTabID.value)
const allTabResults = inspectionService.tabs
const tabResults = computed(() => {
return allTabResults.value.get(currentTabID.value) ?? []
})
</script>

View File

@@ -101,6 +101,6 @@ const newActiveHeadersCount$ = computed(() => {
})
defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionsTab.value = tab as RequestOptionTabs
selectedOptionsTab.value = tab
})
</script>

View File

@@ -145,8 +145,13 @@ const statusCategory = computed(() => {
const inspectionService = useService(InspectionService)
const tabResults = inspectionService.getResultViewFor(
currentTabID.value,
(result) => result.locations.type === "response"
)
const allTabResults = inspectionService.tabs
const tabResults = computed(() => {
return (
allTabResults.value
.get(currentTabID.value)
?.filter((result) => result.locations.type === "response") ?? []
)
})
</script>

View File

@@ -17,7 +17,7 @@
<span
v-tippy="{ theme: 'tooltip' }"
:title="relativeTime"
class="mx-auto truncate text-tiny text-secondaryLight hover:text-secondary hover:text-center"
class="mx-auto truncate ts-font text-secondaryLight hover:text-secondary hover:text-center"
>
{{ shortDateTime(entry.ts) }}
</span>
@@ -406,4 +406,8 @@ const icon = computed(() => markRaw(ICONS[props.entry.source].icon))
@apply transition;
@apply hover: text-secondary;
}
.ts-font {
font-size: 0.6rem;
}
</style>

View File

@@ -311,36 +311,35 @@ const envVars = computed(() =>
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
function handleTextSelection() {
const selection = view.value?.state.selection.main
if (selection) {
const from = selection.from
const to = selection.to
if (from === to) return
const text = view.value?.state.doc.sliceString(from, to)
const { top, left } = view.value?.coordsAtPos(from)
if (text) {
invokeAction("contextmenu.open", {
position: {
top,
left,
},
text,
})
showSuggestionPopover.value = false
} else {
invokeAction("contextmenu.open", {
position: {
top,
left,
},
text: null,
})
const initView = (el: any) => {
function handleTextSelection() {
const selection = view.value?.state.selection.main
if (selection) {
const from = selection.from
const to = selection.to
const text = view.value?.state.doc.sliceString(from, to)
const { top, left } = view.value?.coordsAtPos(from)
if (text) {
invokeAction("contextmenu.open", {
position: {
top,
left,
},
text,
})
showSuggestionPopover.value = false
} else {
invokeAction("contextmenu.open", {
position: {
top,
left,
},
text: null,
})
}
}
}
}
const initView = (el: any) => {
// Debounce to prevent double click from selecting the word
const debounceFn = useDebounceFn(() => {
handleTextSelection()
@@ -382,11 +381,6 @@ const initView = (el: any) => {
drop(ev) {
ev.preventDefault()
},
scroll(event) {
if (event.target) {
handleTextSelection()
}
},
}),
ViewPlugin.fromClass(
class {

View File

@@ -0,0 +1,71 @@
<template>
<span class="inline-flex">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions.focus()"
>
<span class="select-wrapper">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('settings.change_font_size')"
class="pr-8"
:icon="IconType"
outline
:label="`${getFontSizeName(
fontSizes.find((size) => size === active)
)}`"
/>
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
v-for="(size, index) in fontSizes"
:key="`size-${index}`"
:label="`${getFontSizeName(size)}`"
:icon="size === active ? IconCircleDot : IconCircle"
:active="size === active"
@click="
() => {
setActiveFont(size)
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</template>
<script setup lang="ts">
import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
import IconType from "~icons/lucide/type"
import { HoppFontSizes, HoppFontSize, applySetting } from "~/newstore/settings"
import { useSetting } from "@composables/settings"
import { useI18n } from "@composables/i18n"
import { ref } from "vue"
const t = useI18n()
const fontSizes = HoppFontSizes
const active = useSetting("FONT_SIZE")
const getFontSizeName = (size: HoppFontSize) => {
return t(`settings.font_size_${size}`)
}
const setActiveFont = (size: HoppFontSize) => {
applySetting("FONT_SIZE", size)
}
// Template refs
const tippyActions = ref<any | null>(null)
</script>

View File

@@ -216,33 +216,6 @@ export function useCodemirror(
? new HoppEnvironmentPlugin(subscribeToStream, view)
: null
function handleTextSelection() {
const selection = view.value?.state.selection.main
if (selection) {
const from = selection.from
const to = selection.to
const text = view.value?.state.doc.sliceString(from, to)
const { top, left } = view.value?.coordsAtPos(from)
if (text) {
invokeAction("contextmenu.open", {
position: {
top,
left,
},
text,
})
} else {
invokeAction("contextmenu.open", {
position: {
top,
left,
},
text: null,
})
}
}
}
const initView = (el: any) => {
if (el) platform.ui?.onCodemirrorInstanceMount?.(el)
@@ -253,6 +226,33 @@ export function useCodemirror(
ViewPlugin.fromClass(
class {
update(update: ViewUpdate) {
function handleTextSelection() {
const selection = view.value?.state.selection.main
if (selection) {
const from = selection.from
const to = selection.to
const text = view.value?.state.doc.sliceString(from, to)
const { top, left } = view.value?.coordsAtPos(from)
if (text) {
invokeAction("contextmenu.open", {
position: {
top,
left,
},
text,
})
} else {
invokeAction("contextmenu.open", {
position: {
top,
left,
},
text: null,
})
}
}
}
// Debounce to prevent double click from selecting the word
const debounceFn = useDebounceFn(() => {
handleTextSelection()
@@ -296,13 +296,6 @@ export function useCodemirror(
}
}
),
EditorView.domEventHandlers({
scroll(event) {
if (event.target) {
handleTextSelection()
}
},
}),
EditorView.updateListener.of((update) => {
if (options.extendedEditorConfig.readOnly) {
update.view.contentDOM.inputMode = "none"

View File

@@ -2,14 +2,12 @@
* For example, sending a request.
*/
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
import { BehaviorSubject } from "rxjs"
import { HoppRESTDocument } from "./rest/document"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
import { HoppGQLSaveContext } from "./graphql/document"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { computed } from "vue"
export type HoppAction =
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
@@ -18,7 +16,7 @@ export type HoppAction =
| "request.copy-link" // Copy Request Link
| "request.save" // Save to Collections
| "request.save-as" // Save As
| "request.rename" // Rename request on REST or GraphQL
| "rest.request.rename" // Rename
| "request.method.next" // Select Next Method
| "request.method.prev" // Select Previous Method
| "request.method.get" // Select GET Method
@@ -28,20 +26,14 @@ export type HoppAction =
| "request.method.delete" // Select DELETE Method
| "request.import-curl" // Import cURL
| "request.show-code" // Show generated code
| "gql.connect" // Connect to GraphQL endpoint given
| "gql.disconnect" // Disconnect from GraphQL endpoint given
| "tab.close-current" // Close current tab
| "tab.close-other" // Close other tabs
| "tab.open-new" // Open new tab
| "collection.new" // Create root collection
| "flyouts.chat.open" // Shows the keybinds flyout
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
| "modals.search.toggle" // Shows the search modal
| "modals.support.toggle" // Shows the support modal
| "modals.share.toggle" // Shows the share modal
| "modals.social.toggle" // Shows the social links modal
| "modals.environment.add" // Show add environment modal via context menu
| "modals.environment.new" // Add new environment
| "modals.environment.delete-selected" // Delete Selected Environment
| "modals.my.environment.edit" // Edit current personal environment
| "modals.team.environment.edit" // Edit current team environment
| "modals.team.new" // Add new team
@@ -113,11 +105,7 @@ type HoppActionArgsMap = {
request: HoppGQLRequest
}
"request.open-tab": {
tab: RequestOptionTabs | GQLOptionTabs
}
"tab.duplicate-tab": {
tabID?: string
tab: RequestOptionTabs
}
"gql.request.open": {
@@ -157,7 +145,7 @@ type BoundActionList = {
[A in HoppAction | HoppActionWithArgs]?: Array<ActionFunc<A>>
}
const boundActions: BoundActionList = reactive({})
const boundActions: BoundActionList = {}
export const activeActions$ = new BehaviorSubject<
(HoppAction | HoppActionWithArgs)[]
@@ -213,15 +201,6 @@ export function unbindAction<A extends HoppAction | HoppActionWithArgs>(
activeActions$.next(Object.keys(boundActions) as HoppAction[])
}
/**
* Returns a ref that indicates whether a given action is bound at a given time
*
* @param action The action to check
*/
export function isActionBound(action: HoppAction): Ref<boolean> {
return computed(() => !!boundActions[action])
}
/**
* A composable function that defines a component can handle a given
* HoppAction. The handler will be bound when the component is mounted

View File

@@ -56,7 +56,7 @@ export const bindings: {
"alt-x": "request.method.delete",
"ctrl-k": "modals.search.toggle",
"ctrl-/": "flyouts.keybinds.toggle",
"shift-/": "modals.support.toggle",
"?": "modals.support.toggle",
"ctrl-m": "modals.share.toggle",
"alt-r": "navigation.jump.rest",
"alt-q": "navigation.jump.graphql",
@@ -120,8 +120,7 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
}
function getPressedKey(ev: KeyboardEvent): Key | null {
const val = ev.code.toLowerCase()
const val = ev.key.toLowerCase()
// Check arrow keys
if (val === "arrowup") return "up"
else if (val === "arrowdown") return "down"
@@ -129,20 +128,21 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
else if (val === "arrowright") return "right"
// Check letter keys
const isLetter = val.startsWith("key")
if (isLetter) return val.substring(3) as Key
const isLetter = ev.code.toLowerCase().startsWith("key")
if (isLetter) return ev.code.toLowerCase().substring(3) as Key
// Check if number keys
const isDigit = val.startsWith("digit")
if (isDigit) return val.substring(5) as Key
if (val.length === 1 && !isNaN(val as any)) return val as Key
// Check if slash
if (val === "slash") return "/"
// Check if question mark
if (val === "?") return "?"
// Check if question mark
if (val === "/") return "/"
// Check if period
if (val === "period") return "."
if (val === ".") return "."
// Check if enter
if (val === "enter") return "enter"
// If no other cases match, this is not a valid key

View File

@@ -98,11 +98,7 @@ const buildHarPostData = (req: HoppRESTRequest): Har.PostData | undefined => {
}
}
export const buildHarRequest = (
req: HoppRESTRequest
): Har.Request & {
postData: Har.PostData & Exclude<Har.PostData, undefined>
} => {
export const buildHarRequest = (req: HoppRESTRequest): Har.Request => {
return {
bodySize: -1, // TODO: It would be cool if we can calculate the body size
headersSize: -1, // TODO: It would be cool if we can calculate the header size
@@ -112,9 +108,6 @@ export const buildHarRequest = (
method: req.method,
queryString: buildHarQueryStrings(req),
url: req.endpoint,
postData: buildHarPostData(req) ?? {
mimeType: "x-unknown",
params: [],
},
postData: buildHarPostData(req),
}
}

View File

@@ -1,4 +1,4 @@
import { HTTPSnippet } from "httpsnippet"
import * as HTTPSnippet from "httpsnippet"
import { HoppRESTRequest } from "@hoppscotch/data"
import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"

View File

@@ -7,7 +7,6 @@ import { HoppRESTResponse } from "../types/HoppRESTResponse"
import { getDefaultRESTRequest } from "./default"
import { HoppTestResult } from "../types/HoppTestResult"
import { platform } from "~/platform"
import { nextTick } from "vue"
export type HoppRESTTab = {
id: string
@@ -179,9 +178,7 @@ export function closeTab(tabID: string) {
tabOrdering.value.splice(tabOrdering.value.indexOf(tabID), 1)
nextTick(() => {
tabMap.delete(tabID)
})
tabMap.delete(tabID)
}
export function closeOtherTabs(tabID: string) {

View File

@@ -28,7 +28,6 @@ export function createHoppApp(el: string | Element, platformDef: PlatformDef) {
performMigrations()
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
platformDef.addedHoppModules?.forEach((mod) => mod.onVueAppInit?.(app))
app.mount(el)

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex w-screen h-screen">
<Splitpanes class="no-splitter" :dbl-click-splitter="false" horizontal>
<Pane style="height: auto">
<Pane v-if="!zenMode" style="height: auto">
<AppHeader />
</Pane>
<Pane :class="spacerClass" class="flex flex-1 !overflow-auto md:mb-0">
@@ -79,7 +79,9 @@ const router = useRouter()
const showSearch = ref(false)
const showSupport = ref(false)
const fontSize = useSetting("FONT_SIZE")
const expandNavigation = useSetting("EXPAND_NAVIGATION")
const zenMode = useSetting("ZEN_MODE")
const rightSidebar = useSetting("SIDEBAR")
const columnLayout = useSetting("COLUMN_LAYOUT")
@@ -131,9 +133,24 @@ watch(mdAndLarger, () => {
columnLayout.value = true
}
})
const spacerClass = computed(() =>
expandNavigation.value ? "spacer-small" : "spacer-expand"
)
const spacerClass = computed(() => {
if (fontSize.value === "small" && expandNavigation.value)
return "spacer-small"
if (fontSize.value === "medium" && expandNavigation.value)
return "spacer-medium"
if (fontSize.value === "large" && expandNavigation.value)
return "spacer-large"
if (
(fontSize.value === "small" ||
fontSize.value === "medium" ||
fontSize.value === "large") &&
!expandNavigation.value
)
return "spacer-expand"
return ""
})
defineActionHandler("modals.search.toggle", () => {
showSearch.value = !showSearch.value

View File

@@ -0,0 +1,32 @@
import { HoppModule } from "."
export const showChat = () => {
;(window as any).$crisp.push([
"do",
"chat:show",
(window as any).$crisp.push(["do", "chat:open"]),
])
}
export default <HoppModule>{
onVueAppInit() {
// TODO: Env variable this ?
;(window as any).$crisp = []
;(window as any).CRISP_WEBSITE_ID = "3ad30257-c192-4773-955d-fb05a4b41af3"
const d = document
const s = d.createElement("script")
s.src = "https://client.crisp.chat/l.js"
s.async = true
d.getElementsByTagName("head")[0].appendChild(s)
;(window as any).$crisp.push(["do", "chat:hide"])
;(window as any).$crisp.push([
"on",
"chat:closed",
() => {
;(window as any).$crisp.push(["do", "chat:hide"])
},
])
},
}

View File

@@ -53,9 +53,6 @@ export default <HoppModule>{
HOPP_MODULES.forEach((mod) => {
mod.onBeforeRouteChange?.(to, from, router)
})
platform.addedHoppModules?.forEach((mod) => {
mod.onBeforeRouteChange?.(to, from, router)
})
})
// Instead of this a better architecture is for the router
@@ -69,14 +66,10 @@ export default <HoppModule>{
HOPP_MODULES.forEach((mod) => {
mod.onAfterRouteChange?.(to, router)
})
platform.addedHoppModules?.forEach((mod) => {
mod.onAfterRouteChange?.(to, router)
})
})
app.use(router)
HOPP_MODULES.forEach((mod) => mod.onRouterInit?.(app, router))
platform.addedHoppModules?.forEach((mod) => mod.onRouterInit?.(app, router))
},
}

View File

@@ -68,9 +68,23 @@ const applyAccentColor = (_app: App) => {
)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const applyFontSize = (_app: App) => {
const [pref] = useSettingStatic("FONT_SIZE")
watch(
pref,
(newPref) => {
document.documentElement.setAttribute("data-font-size", newPref)
},
{ immediate: true }
)
}
export default <HoppModule>{
onVueAppInit(app) {
applyColorMode(app)
applyAccentColor(app)
applyFontSize(app)
},
}

View File

@@ -427,10 +427,6 @@ export function getCurrentEnvironment(): Environment {
}
}
export function getSelectedEnvironmentIndex() {
return environmentsStore.value.selectedEnvironmentIndex
}
export function getSelectedEnvironmentType() {
return environmentsStore.value.selectedEnvironmentIndex.type
}

View File

@@ -23,6 +23,10 @@ export const HoppAccentColors = [
export type HoppAccentColor = (typeof HoppAccentColors)[number]
export const HoppFontSizes = ["small", "medium", "large"] as const
export type HoppFontSize = (typeof HoppFontSizes)[number]
export type SettingsDef = {
syncCollections: boolean
syncHistory: boolean
@@ -45,6 +49,8 @@ export type SettingsDef = {
EXPAND_NAVIGATION: boolean
SIDEBAR: boolean
SIDEBAR_ON_LEFT: boolean
ZEN_MODE: boolean
FONT_SIZE: HoppFontSize
COLUMN_LAYOUT: boolean
}
@@ -70,6 +76,8 @@ export const getDefaultSettings = (): SettingsDef => ({
EXPAND_NAVIGATION: true,
SIDEBAR: true,
SIDEBAR_ON_LEFT: true,
ZEN_MODE: false,
FONT_SIZE: "small",
COLUMN_LAYOUT: true,
})

View File

@@ -27,7 +27,7 @@
@open-rename-modal="openReqRenameModal(tab)"
@close-tab="removeTab(tab.id)"
@close-other-tabs="closeOtherTabsAction(tab.id)"
@duplicate-tab="duplicateTab(tab.id)"
@duplicate-tab="duplicateTab(tab)"
/>
</template>
@@ -203,15 +203,12 @@ const renameReqName = () => {
showRenamingReqNameModalForTabID.value = undefined
}
const duplicateTab = (tabID: string) => {
const tab = getTabRef(tabID)
if (tab.value) {
const newTab = createNewTab({
request: tab.value.document.request,
isDirty: true,
})
currentTabID.value = newTab.id
}
const duplicateTab = (tab: HoppGQLTab) => {
const newTab = createNewTab({
request: tab.document.request,
isDirty: true,
})
currentTabID.value = newTab.id
}
defineActionHandler("gql.request.open", ({ request, saveContext }) => {
@@ -221,19 +218,4 @@ defineActionHandler("gql.request.open", ({ request, saveContext }) => {
isDirty: false,
})
})
defineActionHandler("request.rename", () => {
openReqRenameModal(getTabRef(currentTabID.value).value!)
})
defineActionHandler("tab.duplicate-tab", ({ tabID }) => {
duplicateTab(tabID ?? currentTabID.value)
})
defineActionHandler("tab.close-current", () => {
removeTab(currentTabID.value)
})
defineActionHandler("tab.close-other", () => {
closeOtherTabs(currentTabID.value)
})
defineActionHandler("tab.open-new", addNewTab)
</script>

View File

@@ -94,7 +94,7 @@
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
import { ref, onMounted, onBeforeUnmount, onBeforeMount, watch } from "vue"
import { safelyExtractRESTRequest } from "@hoppscotch/data"
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
import { useRoute } from "vue-router"
@@ -140,8 +140,8 @@ import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
import { URLInspectorService } from "~/services/inspection/inspectors/url.inspector"
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
import { cloneDeep } from "lodash-es"
const savingRequest = ref(false)
const confirmingCloseForTabID = ref<string | null>(null)
@@ -215,8 +215,6 @@ const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
updateTabOrdering(e.oldIndex, e.newIndex)
}
const inspectionService = useService(InspectionService)
const removeTab = (tabID: string) => {
const tabState = getTabRef(tabID).value
@@ -229,14 +227,11 @@ const removeTab = (tabID: string) => {
}
const closeOtherTabsAction = (tabID: string) => {
const isTabDirty = getTabRef(tabID).value?.document.isDirty
const dirtyTabCount = getDirtyTabsCount()
// If current tab is dirty, so we need to subtract 1 from the dirty tab count
const balanceDirtyTabCount = isTabDirty ? dirtyTabCount - 1 : dirtyTabCount
// If there are dirty tabs, show the confirm modal
if (balanceDirtyTabCount > 0) {
if (dirtyTabCount > 0) {
confirmingCloseAllTabs.value = true
unsavedTabsCount.value = balanceDirtyTabCount
unsavedTabsCount.value = dirtyTabCount
exceptedTabID.value = tabID
} else {
closeOtherTabs(tabID)
@@ -247,7 +242,7 @@ const duplicateTab = (tabID: string) => {
const tab = getTabRef(tabID)
if (tab.value) {
const newTab = createNewTab({
request: cloneDeep(tab.value.document.request),
request: tab.value.document.request,
isDirty: true,
})
currentTabID.value = newTab.id
@@ -465,22 +460,19 @@ defineActionHandler("rest.request.open", ({ doc }) => {
createNewTab(doc)
})
defineActionHandler("request.rename", openReqRenameModal)
defineActionHandler("tab.duplicate-tab", ({ tabID }) => {
duplicateTab(tabID ?? currentTabID.value)
})
defineActionHandler("tab.close-current", () => {
removeTab(currentTabID.value)
})
defineActionHandler("tab.close-other", () => {
closeOtherTabs(currentTabID.value)
})
defineActionHandler("tab.open-new", addNewTab)
defineActionHandler("rest.request.rename", openReqRenameModal)
const inspectionService = useService(InspectionService)
useService(HeaderInspectorService)
useService(EnvironmentInspectorService)
useService(URLInspectorService)
useService(ResponseInspectorService)
for (const inspectorDef of platform.additionalInspectors ?? []) {
useService(inspectorDef.service)
}
watch(
() => currentTabID.value,
() => {
inspectionService.initializeTabInspectors()
},
{ immediate: true }
)
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<div class="container divide-y divide-dividerLight">
<div class="container space-y-8 divide-y divide-dividerLight">
<div class="md:grid md:gap-4 md:grid-cols-3">
<div class="p-8 md:col-span-1">
<h3 class="heading">
@@ -36,6 +36,14 @@
<SmartAccentModePicker />
</div>
</section>
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.font_size") }}
</h4>
<div class="mt-4">
<SmartFontSizePicker />
</div>
</section>
<section>
<h4 class="font-semibold text-secondaryDark">
{{ t("settings.language") }}
@@ -82,6 +90,14 @@
{{ t("settings.sidebar_on_left") }}
</HoppSmartToggle>
</div>
<div class="flex items-center">
<HoppSmartToggle
:on="ZEN_MODE"
@change="toggleSetting('ZEN_MODE')"
>
{{ t("layout.zen_mode") }}
</HoppSmartToggle>
</div>
</div>
</section>
</div>
@@ -162,6 +178,7 @@ const PROXY_URL = useSetting("PROXY_URL")
const TELEMETRY_ENABLED = useSetting("TELEMETRY_ENABLED")
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
const ZEN_MODE = useSetting("ZEN_MODE")
const confirmRemove = ref(false)
@@ -169,6 +186,10 @@ const proxySettings = computed(() => ({
url: PROXY_URL.value,
}))
watch(ZEN_MODE, (mode) => {
applySetting("EXPAND_NAVIGATION", !mode)
})
watch(
proxySettings,
({ url }) => {

View File

@@ -7,12 +7,9 @@ import { HistoryPlatformDef } from "./history"
import { TabStatePlatformDef } from "./tab"
import { AnalyticsPlatformDef } from "./analytics"
import { InterceptorsPlatformDef } from "./interceptors"
import { HoppModule } from "~/modules"
import { InspectorsPlatformDef } from "./inspectors"
export type PlatformDef = {
ui?: UIPlatformDef
addedHoppModules?: HoppModule[]
auth: AuthPlatformDef
analytics?: AnalyticsPlatformDef
sync: {
@@ -23,7 +20,6 @@ export type PlatformDef = {
tabState: TabStatePlatformDef
}
interceptors: InterceptorsPlatformDef
additionalInspectors?: InspectorsPlatformDef
platformFeatureFlags: {
exportAsGIST: boolean
}

View File

@@ -1,16 +0,0 @@
import { Service } from "dioc"
import { Inspector } from "~/services/inspection"
/**
* Defines an added interceptor by the platform
*/
export type PlatformInspectorsDef = {
// We are keeping this as the only mode for now
// So that if we choose to add other modes, we can do without breaking
type: "service"
service: typeof Service<unknown> & { ID: string } & {
new (): Service & Inspector
}
}
export type InspectorsPlatformDef = PlatformInspectorsDef[]

View File

@@ -1,107 +0,0 @@
import { Service } from "dioc"
import {
InspectionService,
Inspector,
InspectorResult,
} from "~/services/inspection"
import { getI18n } from "~/modules/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { computed, markRaw } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import { Ref } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
/**
* This inspector is responsible for inspecting the URL of a request.
* It checks if the URL contains localhost and if the extension is installed.
* It also provides an action to enable the extension.
*
* NOTE: Initializing this service registers it as a inspector with the Inspection Service.
*/
export class ExtensionInspectorService extends Service implements Inspector {
public static readonly ID = "EXTENSION_INSPECTOR_SERVICE"
private t = getI18n()
public readonly inspectorID = "extension"
private readonly interceptorService = this.bind(InterceptorService)
private readonly extensionService = this.bind(ExtensionInterceptorService)
private readonly inspection = this.bind(InspectionService)
constructor() {
super()
this.inspection.registerInspector(this)
}
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
const currentExtensionStatus = this.extensionService.extensionStatus
const isExtensionInstalled = computed(
() => currentExtensionStatus.value === "available"
)
const EXTENSIONS_ENABLED = computed(
() => this.interceptorService.currentInterceptorID.value === "extension"
)
return computed(() => {
const results: InspectorResult[] = []
const url = req.value.endpoint
const localHostURLs = ["localhost", "127.0.0.1"]
const isContainLocalhost = localHostURLs.some((host) =>
url.includes(host)
)
if (
isContainLocalhost &&
(!EXTENSIONS_ENABLED.value || !isExtensionInstalled.value)
) {
let text
if (!isExtensionInstalled.value) {
if (currentExtensionStatus.value === "unknown-origin") {
text = this.t("inspections.url.extension_unknown_origin")
} else {
text = this.t("inspections.url.extension_not_installed")
}
} else if (!EXTENSIONS_ENABLED.value) {
text = this.t("inspections.url.extention_not_enabled")
} else {
text = this.t("inspections.url.localhost")
}
results.push({
id: "url",
icon: markRaw(IconAlertTriangle),
text: {
type: "text",
text: text,
},
action: {
text: this.t("inspections.url.extention_enable_action"),
apply: () => {
this.interceptorService.currentInterceptorID.value = "extension"
},
},
severity: 2,
isApplicable: true,
locations: {
type: "url",
},
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension",
},
})
}
return results
})
}
}

View File

@@ -1,25 +0,0 @@
import { HoppFooterMenuItem } from "../../ui"
import IconGift from "~icons/lucide/gift"
import IconActivity from "~icons/lucide/activity"
export const whatsNew: HoppFooterMenuItem = {
id: "whats-new",
text: (t) => t("app.whats_new"),
icon: IconGift,
action: {
type: "link",
href: "https://docs.hoppscotch.io/documentation/changelog",
},
}
export const status: HoppFooterMenuItem = {
id: "status",
text: (t) => t("app.status"),
icon: IconActivity,
action: {
type: "link",
href: "https://status.hoppscotch.io",
},
}
export const stdFooterItems = [whatsNew, status]

View File

@@ -1,100 +0,0 @@
import { invokeAction } from "~/helpers/actions"
import { HoppSupportOptionsMenuItem } from "~/platform/ui"
import IconBook from "~icons/lucide/book"
import IconGift from "~icons/lucide/gift"
import IconZap from "~icons/lucide/zap"
import IconGitHub from "~icons/lucide/github"
import IconTwitter from "~icons/brands/twitter"
import IconDiscord from "~icons/brands/discord"
import IconUserPlus from "~icons/lucide/user-plus"
export const documentation: HoppSupportOptionsMenuItem = {
id: "documentation",
text: (t) => t("app.documentation"),
subtitle: (t) => t("support.documentation"),
icon: IconBook,
action: {
type: "link",
href: "https://docs.hoppscotch.io",
},
}
export const shortcuts: HoppSupportOptionsMenuItem = {
id: "shortcuts",
text: (t) => t("app.keyboard_shortcuts"),
subtitle: (t) => t("support.shortcuts"),
icon: IconZap,
action: {
type: "custom",
do() {
invokeAction("flyouts.keybinds.toggle")
},
},
}
export const changelog: HoppSupportOptionsMenuItem = {
id: "changelog",
text: (t) => t("app.whats_new"),
subtitle: (t) => t("support.changelog"),
icon: IconGift,
action: {
type: "link",
href: "https://docs.hoppscotch.io/documentation/changelog",
},
}
export const github: HoppSupportOptionsMenuItem = {
id: "github",
text: (t) => t("app.github"),
subtitle: (t) => t("support.github"),
icon: IconGitHub,
action: {
type: "link",
href: "https://hoppscotch.io/github",
},
}
export const discord: HoppSupportOptionsMenuItem = {
id: "discord",
text: (t) => t("app.join_discord_community"),
subtitle: (t) => t("support.community"),
icon: IconDiscord,
action: {
type: "link",
href: "https://hoppscotch.io/discord",
},
}
export const twitter: HoppSupportOptionsMenuItem = {
id: "discord",
text: (t) => t("app.twitter"),
subtitle: (t) => t("support.twitter"),
icon: IconTwitter,
action: {
type: "link",
href: "https://hoppscotch.io/twitter",
},
}
export const invite: HoppSupportOptionsMenuItem = {
id: "invite",
text: (t) => t("app.invite"),
subtitle: (t) => t("shortcut.miscellaneous.invite"),
icon: IconUserPlus,
action: {
type: "custom",
do() {
invokeAction("modals.share.toggle")
},
},
}
export const stdSupportOptionItems: HoppSupportOptionsMenuItem[] = [
documentation,
shortcuts,
changelog,
github,
discord,
twitter,
invite,
]

View File

@@ -1,20 +1,4 @@
import { Ref, Component } from "vue"
import { getI18n } from "~/modules/i18n"
export type HoppFooterMenuItem = {
id: string
text: (t: ReturnType<typeof getI18n>) => string
icon: Component
action: { type: "link"; href: string } | { type: "custom"; do: () => void }
}
export type HoppSupportOptionsMenuItem = {
id: string
text: (t: ReturnType<typeof getI18n>) => string
subtitle: (t: ReturnType<typeof getI18n>) => string
icon: Component
action: { type: "link"; href: string } | { type: "custom"; do: () => void }
}
import { Ref } from "vue"
export type UIPlatformDef = {
appHeader?: {
@@ -22,15 +6,4 @@ export type UIPlatformDef = {
paddingLeft?: Ref<string>
}
onCodemirrorInstanceMount?: (element: HTMLElement) => void
/**
* Additonal menu items shown in the "Help and Feedback" menu
* in the app footer.
*/
additionalFooterMenuItems?: HoppFooterMenuItem[]
/**
* Additional Support Options menu items shown in the app header
*/
additionalSupportOptionsMenuItems?: HoppSupportOptionsMenuItem[]
}

View File

@@ -61,7 +61,7 @@ describe("EnvironmentMenuService", () => {
expect(actionsMock.invokeAction).toHaveBeenCalledWith(
"modals.environment.add",
{
envName: "",
envName: "test",
variableName: test,
}
)

View File

@@ -42,7 +42,7 @@ export class EnvironmentMenuService extends Service implements ContextMenu {
icon: markRaw(IconPlusCircle),
action: () => {
invokeAction("modals.environment.add", {
envName: "",
envName: "test",
variableName: text,
})
},

View File

@@ -114,7 +114,7 @@ export class ParameterMenuService extends Service implements ContextMenu {
id: "environment",
text: {
type: "text",
text: this.t("context_menu.add_parameters"),
text: this.t("context_menu.add_parameter"),
},
icon: markRaw(IconArrowDownRight),
action: () => {

View File

@@ -70,7 +70,7 @@ export class URLMenuService extends Service implements ContextMenu {
id: "link-tab",
text: {
type: "text",
text: this.t("context_menu.open_request_in_new_tab"),
text: this.t("context_menu.open_link_in_new_tab"),
},
icon: markRaw(IconCopyPlus),
action: () => {

View File

@@ -1,7 +1,6 @@
import { describe, it, expect } from "vitest"
import { Inspector, InspectionService, InspectorResult } from "../"
import { TestContainer } from "dioc/testing"
import { ref } from "vue"
const inspectorResultMock: InspectorResult[] = [
{
@@ -22,7 +21,7 @@ const inspectorResultMock: InspectorResult[] = [
const testInspector: Inspector = {
inspectorID: "inspector1",
getInspections: () => ref(inspectorResultMock),
getInspectorFor: () => inspectorResultMock,
}
describe("InspectionService", () => {

View File

@@ -1,9 +1,7 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import { refDebounced } from "@vueuse/core"
import { Service } from "dioc"
import { computed, markRaw, reactive } from "vue"
import { Component, Ref, ref, watch } from "vue"
import { currentActiveTab } from "~/helpers/rest/tab"
import { currentActiveTab, currentTabID } from "~/helpers/rest/tab"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
/**
@@ -82,16 +80,15 @@ export interface Inspector {
*/
inspectorID: string
/**
* Returns the inspector results for the request.
* NOTE: The refs passed down are readonly and are debounced to avoid performance issues
* @param req The ref to the request to inspect
* @param res The ref to the response to inspect
* @returns The ref to the inspector results
* Returns the inspector results for the request
* @param req The request to inspect
* @param res The response to inspect
* @returns The inspector results
*/
getInspections: (
req: Readonly<Ref<HoppRESTRequest>>,
res: Readonly<Ref<HoppRESTResponse | null | undefined>>
) => Ref<InspectorResult[]>
getInspectorFor: (
req: HoppRESTRequest,
res?: HoppRESTResponse
) => InspectorResult[]
}
/**
@@ -101,73 +98,38 @@ export interface Inspector {
export class InspectionService extends Service {
public static readonly ID = "INSPECTION_SERVICE"
private inspectors: Map<string, Inspector> = reactive(new Map())
private inspectors: Map<string, Inspector> = new Map()
private tabs: Ref<Map<string, InspectorResult[]>> = ref(new Map())
constructor() {
super()
this.initializeListeners()
}
public tabs: Ref<Map<string, InspectorResult[]>> = ref(new Map())
/**
* Registers a inspector with the inspection service
* @param inspector The inspector instance to register
*/
public registerInspector(inspector: Inspector) {
// markRaw is required here so that the inspector is not made reactive
this.inspectors.set(inspector.inspectorID, markRaw(inspector))
this.inspectors.set(inspector.inspectorID, inspector)
}
private initializeListeners() {
public initializeTabInspectors() {
watch(
() => [this.inspectors.entries(), currentActiveTab.value.id],
() => {
const reqRef = computed(() => currentActiveTab.value.document.request)
const resRef = computed(() => currentActiveTab.value.response)
const debouncedReq = refDebounced(reqRef, 1000, { maxWait: 2000 })
const debouncedRes = refDebounced(resRef, 1000, { maxWait: 2000 })
const inspectorRefs = Array.from(this.inspectors.values()).map((x) =>
x.getInspections(debouncedReq, debouncedRes)
currentActiveTab.value,
(tab) => {
if (!tab) return
const req = currentActiveTab.value.document.request
const res = currentActiveTab.value.response
const inspectors = Array.from(this.inspectors.values()).map((x) =>
x.getInspectorFor(req, res)
)
const activeInspections = computed(() =>
inspectorRefs.flatMap((x) => x!.value)
)
watch(
() => [...inspectorRefs.flatMap((x) => x!.value)],
() => {
this.tabs.value.set(
currentActiveTab.value.id,
activeInspections.value
)
},
{ immediate: true }
this.tabs.value.set(
currentTabID.value,
inspectors.flatMap((x) => x)
)
},
{ immediate: true, flush: "pre" }
{ immediate: true, deep: true }
)
}
public deleteTabInspectorResult(tabID: string) {
// TODO: Move Tabs into a service and implement this with an event instead
this.tabs.value.delete(tabID)
}
/**
* Returns a reactive view into the inspector results for a specific tab
* @param tabID The ID of the tab to get the results for
* @param filter The filter to apply to the results.
* @returns The ref into the inspector results, if the tab doesn't exist, a ref into an empty array is returned
*/
public getResultViewFor(
tabID: string,
filter: (x: InspectorResult) => boolean = () => true
) {
return computed(() => this.tabs.value.get(tabID)?.filter(filter) ?? [])
}
}

View File

@@ -3,23 +3,16 @@ import { describe, expect, it, vi } from "vitest"
import { EnvironmentInspectorService } from "../environment.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
getI18n: () => (x: string) => x,
}))
vi.mock("~/newstore/environments", async () => {
const { BehaviorSubject }: any = await vi.importActual("rxjs")
return {
__esModule: true,
aggregateEnvs$: new BehaviorSubject([
{ key: "EXISTING_ENV_VAR", value: "test_value" },
]),
}
})
vi.mock("~/newstore/environments", () => ({
__esModule: true,
getAggregateEnvs: () => [{ key: "EXISTING_ENV_VAR", value: "test_value" }],
}))
describe("EnvironmentInspectorService", () => {
it("registers with the inspection service upon initialization", () => {
@@ -42,14 +35,14 @@ describe("EnvironmentInspectorService", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "<<UNDEFINED_ENV_VAR>>",
})
}
const result = envInspector.getInspections(req)
const result = envInspector.getInspectorFor(req)
expect(result.value).toContainEqual(
expect(result).toContainEqual(
expect.objectContaining({
id: "environment",
isApplicable: true,
@@ -65,31 +58,31 @@ describe("EnvironmentInspectorService", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "<<EXISTING_ENV_VAR>>",
})
}
const result = envInspector.getInspections(req)
const result = envInspector.getInspectorFor(req)
expect(result.value).toHaveLength(0)
expect(result).toHaveLength(0)
})
it("should return an inspector result when the headers contain undefined environment variables", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
],
})
}
const result = envInspector.getInspections(req)
const result = envInspector.getInspectorFor(req)
expect(result.value).toContainEqual(
expect(result).toContainEqual(
expect.objectContaining({
id: "environment",
isApplicable: true,
@@ -105,34 +98,34 @@ describe("EnvironmentInspectorService", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
],
})
}
const result = envInspector.getInspections(req)
const result = envInspector.getInspectorFor(req)
expect(result.value).toHaveLength(0)
expect(result).toHaveLength(0)
})
it("should return an inspector result when the params contain undefined environment variables", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
params: [
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
],
})
}
const result = envInspector.getInspections(req)
const result = envInspector.getInspectorFor(req)
expect(result.value).toContainEqual(
expect(result).toContainEqual(
expect.objectContaining({
id: "environment",
isApplicable: true,
@@ -148,18 +141,18 @@ describe("EnvironmentInspectorService", () => {
const container = new TestContainer()
const envInspector = container.bind(EnvironmentInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [],
params: [
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
],
})
}
const result = envInspector.getInspections(req)
const result = envInspector.getInspectorFor(req)
expect(result.value).toHaveLength(0)
expect(result).toHaveLength(0)
})
})
})

View File

@@ -3,7 +3,6 @@ import { describe, expect, it, vi } from "vitest"
import { HeaderInspectorService } from "../header.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
@@ -31,15 +30,15 @@ describe("HeaderInspectorService", () => {
const container = new TestContainer()
const headerInspector = container.bind(HeaderInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
})
}
const result = headerInspector.getInspections(req)
const result = headerInspector.getInspectorFor(req)
expect(result.value).toContainEqual(
expect(result).toContainEqual(
expect.objectContaining({ id: "header", isApplicable: true })
)
})
@@ -48,15 +47,15 @@ describe("HeaderInspectorService", () => {
const container = new TestContainer()
const headerInspector = container.bind(HeaderInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
headers: [{ key: "Authorization", value: "Bearer abcd", active: true }],
})
}
const result = headerInspector.getInspections(req)
const result = headerInspector.getInspectorFor(req)
expect(result.value).toHaveLength(0)
expect(result).toHaveLength(0)
})
})
})

View File

@@ -1,245 +0,0 @@
import { TestContainer } from "dioc/testing"
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"
import { ResponseInspectorService } from "../response.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
getI18n: () => (x: string) => x,
}))
describe("ResponseInspectorService", () => {
beforeEach(() => {
vi.stubGlobal("navigator", {
onLine: true,
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
it("registers with the inspection service upon initialization", () => {
const container = new TestContainer()
const registerInspectorFn = vi.fn()
container.bindMock(InspectionService, {
registerInspector: registerInspectorFn,
})
const responseInspector = container.bind(ResponseInspectorService)
expect(registerInspectorFn).toHaveBeenCalledOnce()
expect(registerInspectorFn).toHaveBeenCalledWith(responseInspector)
})
describe("getInspectorFor", () => {
it("should return an empty array when response is undefined", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const result = responseInspector.getInspections(req, ref(undefined))
expect(result.value).toHaveLength(0)
})
it("should return an inspector result when response type is not success or status code is not 200 and if the network is not available", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "network_fail",
error: new Error("test"),
req: req.value,
})
vi.stubGlobal("navigator", {
onLine: false,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toContainEqual(
expect.objectContaining({ id: "url", isApplicable: true })
)
})
it("should return no inspector result when response type is not success or status code is not 200 and if the network is not available", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "network_fail",
error: new Error("test"),
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toHaveLength(0)
})
it("should handle network_fail responses and return nothing when no network is present", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "network_fail",
error: new Error("test"),
req: req.value,
})
vi.stubGlobal("navigator", {
onLine: false,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.network_error" },
})
)
})
it("should handle network_fail responses and return nothing when network is present", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "network_fail",
error: new Error("test"),
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toHaveLength(0)
})
it("should handle fail responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "fail",
statusCode: 500,
body: Uint8Array.from([]),
headers: [],
meta: { responseDuration: 0, responseSize: 0 },
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.default_error" },
})
)
})
it("should handle 404 responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "success",
statusCode: 404,
body: Uint8Array.from([]),
headers: [],
meta: { responseDuration: 0, responseSize: 0 },
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.404_error" },
})
)
})
it("should handle 401 responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "success",
statusCode: 401,
body: Uint8Array.from([]),
headers: [],
meta: { responseDuration: 0, responseSize: 0 },
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.401_error" },
})
)
})
it("should handle successful responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = ref({
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
const res = ref<HoppRESTResponse>({
type: "success",
statusCode: 200,
body: Uint8Array.from([]),
headers: [],
meta: { responseDuration: 0, responseSize: 0 },
req: req.value,
})
const result = responseInspector.getInspections(req, res)
expect(result.value).toHaveLength(0)
})
})
})

View File

@@ -0,0 +1,151 @@
import { TestContainer } from "dioc/testing"
import { describe, expect, it, vi } from "vitest"
import { ResponseInspectorService } from "../response.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
getI18n: () => (x: string) => x,
}))
describe("ResponseInspectorService", () => {
it("registers with the inspection service upon initialization", () => {
const container = new TestContainer()
const registerInspectorFn = vi.fn()
container.bindMock(InspectionService, {
registerInspector: registerInspectorFn,
})
const responseInspector = container.bind(ResponseInspectorService)
expect(registerInspectorFn).toHaveBeenCalledOnce()
expect(registerInspectorFn).toHaveBeenCalledWith(responseInspector)
})
describe("getInspectorFor", () => {
it("should return an empty array when response is undefined", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
}
const result = responseInspector.getInspectorFor(req, undefined)
expect(result).toHaveLength(0)
})
it("should return an inspector result when response type is not success or status code is not 200", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
}
const res = { type: "network_fail", statusCode: 400 }
const result = responseInspector.getInspectorFor(req, res)
expect(result).toContainEqual(
expect.objectContaining({ id: "url", isApplicable: true })
)
})
it("should handle network_fail responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
}
const res = { type: "network_fail", statusCode: 500 }
const result = responseInspector.getInspectorFor(req, res)
expect(result).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.network_error" },
})
)
})
it("should handle fail responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
}
const res = { type: "fail", statusCode: 500 }
const result = responseInspector.getInspectorFor(req, res)
expect(result).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.default_error" },
})
)
})
it("should handle 404 responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
}
const res = { type: "success", statusCode: 404 }
const result = responseInspector.getInspectorFor(req, res)
expect(result).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.404_error" },
})
)
})
it("should handle 401 responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
}
const res = { type: "success", statusCode: 401 }
const result = responseInspector.getInspectorFor(req, res)
expect(result).toContainEqual(
expect.objectContaining({
text: { type: "text", text: "inspections.response.401_error" },
})
)
})
it("should handle successful responses", () => {
const container = new TestContainer()
const responseInspector = container.bind(ResponseInspectorService)
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
}
const res = { type: "success", statusCode: 200 }
const result = responseInspector.getInspectorFor(req, res)
expect(result).toHaveLength(0)
})
})
})

View File

@@ -1,17 +1,15 @@
import { TestContainer } from "dioc/testing"
import { describe, expect, it, vi } from "vitest"
import { ExtensionInspectorService } from "../extension.inspector"
import { InspectionService } from "~/services/inspection"
import { URLInspectorService } from "../url.inspector"
import { InspectionService } from "../../index"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { ref } from "vue"
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
vi.mock("~/modules/i18n", () => ({
__esModule: true,
getI18n: () => (x: string) => x,
}))
describe("ExtensionInspectorService", () => {
describe("URLInspectorService", () => {
it("registers with the inspection service upon initialization", () => {
const container = new TestContainer()
@@ -21,7 +19,7 @@ describe("ExtensionInspectorService", () => {
registerInspector: registerInspectorFn,
})
const urlInspector = container.bind(ExtensionInspectorService)
const urlInspector = container.bind(URLInspectorService)
expect(registerInspectorFn).toHaveBeenCalledOnce()
expect(registerInspectorFn).toHaveBeenCalledWith(urlInspector)
@@ -30,57 +28,55 @@ describe("ExtensionInspectorService", () => {
describe("getInspectorFor", () => {
it("should return an inspector result when localhost is in URL and extension is not available", () => {
const container = new TestContainer()
const urlInspector = container.bind(ExtensionInspectorService)
const urlInspector = container.bind(URLInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://localhost:8000/api/data",
})
}
const result = urlInspector.getInspections(req)
const result = urlInspector.getInspectorFor(req)
expect(result.value).toContainEqual(
expect(result).toContainEqual(
expect.objectContaining({ id: "url", isApplicable: true })
)
})
it("should not return an inspector result when localhost is not in URL", () => {
const container = new TestContainer()
const urlInspector = container.bind(URLInspectorService)
container.bindMock(ExtensionInterceptorService, {
extensionStatus: ref("unknown-origin" as const),
})
const urlInspector = container.bind(ExtensionInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://example.com/api/data",
})
}
const result = urlInspector.getInspections(req)
const result = urlInspector.getInspectorFor(req)
expect(result.value).toHaveLength(0)
expect(result).toHaveLength(0)
})
it("should add the correct text to the results when extension is not installed", () => {
const container = new TestContainer()
vi.mock("~/newstore/HoppExtension", async () => {
const { BehaviorSubject }: any = await vi.importActual("rxjs")
container.bindMock(ExtensionInterceptorService, {
extensionStatus: ref("waiting" as const),
return {
__esModule: true,
extensionStatus$: new BehaviorSubject("waiting"),
}
})
const container = new TestContainer()
const urlInspector = container.bind(URLInspectorService)
const urlInspector = container.bind(ExtensionInspectorService)
const req = ref({
const req = {
...getDefaultRESTRequest(),
endpoint: "http://localhost:8000/api/data",
})
}
const result = urlInspector.getInspections(req)
const result = urlInspector.getInspectorFor(req)
expect(result.value).toHaveLength(1)
expect(result.value[0]).toMatchObject({
expect(result).toHaveLength(1)
expect(result[0]).toMatchObject({
text: { type: "text", text: "inspections.url.extension_not_installed" },
})
})

View File

@@ -6,13 +6,11 @@ import {
InspectorResult,
} from ".."
import { Service } from "dioc"
import { Ref, markRaw } from "vue"
import { Ref, markRaw, ref } from "vue"
import IconPlusCircle from "~icons/lucide/plus-circle"
import { HoppRESTRequest } from "@hoppscotch/data"
import { aggregateEnvs$ } from "~/newstore/environments"
import { getAggregateEnvs } from "~/newstore/environments"
import { invokeAction } from "~/helpers/actions"
import { computed } from "vue"
import { useStreamStatic } from "~/composables/stream"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
@@ -36,10 +34,6 @@ export class EnvironmentInspectorService extends Service implements Inspector {
private readonly inspection = this.bind(InspectionService)
private aggregateEnvs = useStreamStatic(aggregateEnvs$, [], () => {
/* noop */
})[0]
constructor() {
super()
@@ -55,11 +49,11 @@ export class EnvironmentInspectorService extends Service implements Inspector {
*/
private validateEnvironmentVariables = (
target: any[],
results: Ref<InspectorResult[]>,
locations: InspectorLocation
) => {
const newErrors: InspectorResult[] = []
const envKeys = this.aggregateEnvs.value.map((e) => e.key)
const env = getAggregateEnvs()
const envKeys = env.map((e) => e.key)
target.forEach((element, index) => {
if (isENVInString(element)) {
@@ -89,7 +83,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
}
}
if (!envKeys.includes(formattedExEnv)) {
newErrors.push({
results.value.push({
id: "environment",
text: {
type: "text",
@@ -102,8 +96,8 @@ export class EnvironmentInspectorService extends Service implements Inspector {
text: this.t("inspections.environment.add_environment"),
apply: () => {
invokeAction("modals.environment.add", {
envName: formattedExEnv,
variableName: "",
envName: "test",
variableName: formattedExEnv,
})
},
},
@@ -120,61 +114,54 @@ export class EnvironmentInspectorService extends Service implements Inspector {
}
}
})
return newErrors
}
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
return computed(() => {
const results: InspectorResult[] = []
/**
* Returns the inspector results for the request
* It checks if any env is used in the request ie, url, headers, params
* and checks if the env is defined in the environment using the validateEnvironmentVariables function
* @param req The request to inspect
* @returns The inspector results
*/
getInspectorFor(req: HoppRESTRequest): InspectorResult[] {
const results = ref<InspectorResult[]>([])
const headers = req.value.headers
const headers = req.headers
const params = req.value.params
const params = req.params
results.push(
...this.validateEnvironmentVariables([req.value.endpoint], {
type: "url",
})
)
const headerKeys = Object.values(headers).map((header) => header.key)
results.push(
...this.validateEnvironmentVariables(headerKeys, {
type: "header",
position: "key",
})
)
const headerValues = Object.values(headers).map((header) => header.value)
results.push(
...this.validateEnvironmentVariables(headerValues, {
type: "header",
position: "value",
})
)
const paramsKeys = Object.values(params).map((param) => param.key)
results.push(
...this.validateEnvironmentVariables(paramsKeys, {
type: "parameter",
position: "key",
})
)
const paramsValues = Object.values(params).map((param) => param.value)
results.push(
...this.validateEnvironmentVariables(paramsValues, {
type: "parameter",
position: "value",
})
)
return results
this.validateEnvironmentVariables([req.endpoint], results, {
type: "url",
})
const headerKeys = Object.values(headers).map((header) => header.key)
this.validateEnvironmentVariables(headerKeys, results, {
type: "header",
position: "key",
})
const headerValues = Object.values(headers).map((header) => header.value)
this.validateEnvironmentVariables(headerValues, results, {
type: "header",
position: "value",
})
const paramsKeys = Object.values(params).map((param) => param.key)
this.validateEnvironmentVariables(paramsKeys, results, {
type: "parameter",
position: "key",
})
const paramsValues = Object.values(params).map((param) => param.value)
this.validateEnvironmentVariables(paramsValues, results, {
type: "parameter",
position: "value",
})
return results.value
}
}

View File

@@ -2,7 +2,7 @@ import { Service } from "dioc"
import { InspectionService, Inspector, InspectorResult } from ".."
import { getI18n } from "~/modules/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { Ref, computed, markRaw } from "vue"
import { markRaw, ref } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
/**
@@ -26,50 +26,53 @@ export class HeaderInspectorService extends Service implements Inspector {
this.inspection.registerInspector(this)
}
private cookiesCheck(headerKey: string) {
const cookieKeywords = ["Cookie", "Set-Cookie", "Cookie2", "Set-Cookie2"]
/**
* Checks if the header contains cookies
* @param req The request to inspect
* @returns The inspector results
*/
getInspectorFor(req: HoppRESTRequest): InspectorResult[] {
const results = ref<InspectorResult[]>([])
return cookieKeywords.includes(headerKey)
}
const cookiesCheck = (headerKey: string) => {
const cookieKeywords = ["Cookie", "Set-Cookie", "Cookie2", "Set-Cookie2"]
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
return computed(() => {
const results: InspectorResult[] = []
return cookieKeywords.includes(headerKey)
}
const headers = req.value.headers
const headers = req.headers
const headerKeys = Object.values(headers).map((header) => header.key)
const headerKeys = Object.values(headers).map((header) => header.key)
const isContainCookies = headerKeys.includes("Cookie")
const isContainCookies = headerKeys.includes("Cookie")
if (isContainCookies) {
headerKeys.forEach((headerKey, index) => {
if (this.cookiesCheck(headerKey)) {
results.push({
id: "header",
icon: markRaw(IconAlertTriangle),
text: {
type: "text",
text: this.t("inspections.header.cookie"),
},
severity: 2,
isApplicable: true,
locations: {
type: "header",
position: "key",
key: headerKey,
index: index,
},
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/",
},
})
}
})
}
if (isContainCookies) {
headerKeys.forEach((headerKey, index) => {
if (cookiesCheck(headerKey)) {
results.value.push({
id: "header",
icon: markRaw(IconAlertTriangle),
text: {
type: "text",
text: this.t("inspections.header.cookie"),
},
severity: 2,
isApplicable: true,
locations: {
type: "header",
position: "key",
key: headerKey,
index: index,
},
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/",
},
})
}
})
}
return results
})
return results.value
}
}

View File

@@ -2,11 +2,9 @@ import { Service } from "dioc"
import { InspectionService, Inspector, InspectorResult } from ".."
import { getI18n } from "~/modules/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { markRaw } from "vue"
import { markRaw, ref } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { Ref } from "vue"
import { computed } from "vue"
/**
* This inspector is responsible for inspecting the response of a request.
@@ -29,50 +27,47 @@ export class ResponseInspectorService extends Service implements Inspector {
this.inspection.registerInspector(this)
}
getInspections(
_req: Readonly<Ref<HoppRESTRequest>>,
res: Readonly<Ref<HoppRESTResponse | null | undefined>>
) {
return computed(() => {
const results: InspectorResult[] = []
if (!res.value) return results
getInspectorFor(
req: HoppRESTRequest,
res: HoppRESTResponse | undefined
): InspectorResult[] {
const results = ref<InspectorResult[]>([])
if (!res) return results.value
const hasErrors =
res && (res.value.type !== "success" || res.value.statusCode !== 200)
const hasErrors = res && (res.type !== "success" || res.statusCode !== 200)
let text: string | undefined = undefined
let text
if (res.value.type === "network_fail" && !navigator.onLine) {
text = this.t("inspections.response.network_error")
} else if (res.value.type === "fail") {
text = this.t("inspections.response.default_error")
} else if (res.value.type === "success" && res.value.statusCode === 404) {
text = this.t("inspections.response.404_error")
} else if (res.value.type === "success" && res.value.statusCode === 401) {
text = this.t("inspections.response.401_error")
}
if (res.type === "network_fail") {
text = this.t("inspections.response.network_error")
} else if (res.type === "fail") {
text = this.t("inspections.response.default_error")
} else if (res.type === "success" && res.statusCode === 404) {
text = this.t("inspections.response.404_error")
} else if (res.type === "success" && res.statusCode === 401) {
text = this.t("inspections.response.401_error")
}
if (hasErrors && text) {
results.push({
id: "url",
icon: markRaw(IconAlertTriangle),
text: {
type: "text",
text: text,
},
severity: 2,
isApplicable: true,
locations: {
type: "response",
},
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/",
},
})
}
if (hasErrors && text) {
results.value.push({
id: "url",
icon: markRaw(IconAlertTriangle),
text: {
type: "text",
text: text,
},
severity: 2,
isApplicable: true,
locations: {
type: "response",
},
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/",
},
})
}
return results
})
return results.value
}
}

View File

@@ -0,0 +1,96 @@
import { Service } from "dioc"
import { InspectionService, Inspector, InspectorResult } from ".."
import { getI18n } from "~/modules/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import { computed, markRaw, ref } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import { useReadonlyStream } from "~/composables/stream"
import { extensionStatus$ } from "~/newstore/HoppExtension"
import { useSetting } from "~/composables/settings"
import { applySetting, toggleSetting } from "~/newstore/settings"
/**
* This inspector is responsible for inspecting the URL of a request.
* It checks if the URL contains localhost and if the extension is installed.
* It also provides an action to enable the extension.
*
* NOTE: Initializing this service registers it as a inspector with the Inspection Service.
*/
export class URLInspectorService extends Service implements Inspector {
public static readonly ID = "URL_INSPECTOR_SERVICE"
private t = getI18n()
public readonly inspectorID = "url"
private readonly inspection = this.bind(InspectionService)
constructor() {
super()
this.inspection.registerInspector(this)
}
getInspectorFor(req: HoppRESTRequest): InspectorResult[] {
const PROXY_ENABLED = useSetting("PROXY_ENABLED")
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null)
const isExtensionInstalled = computed(() => {
return currentExtensionStatus.value === "available"
})
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
const results = ref<InspectorResult[]>([])
const url = req.endpoint
const isContainLocalhost = url.includes("localhost")
if (
isContainLocalhost &&
(!EXTENSIONS_ENABLED.value || !isExtensionInstalled.value)
) {
let text
if (!isExtensionInstalled.value) {
if (currentExtensionStatus.value === "unknown-origin") {
text = this.t("inspections.url.extension_unknown_origin")
} else {
text = this.t("inspections.url.extension_not_installed")
}
} else if (!EXTENSIONS_ENABLED.value) {
text = this.t("inspections.url.extention_not_enabled")
} else {
text = this.t("inspections.url.localhost")
}
results.value.push({
id: "url",
icon: markRaw(IconAlertTriangle),
text: {
type: "text",
text: text,
},
action: {
text: this.t("inspections.url.extention_enable_action"),
apply: () => {
applySetting("EXTENSIONS_ENABLED", true)
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
},
},
severity: 2,
isApplicable: true,
locations: {
type: "url",
},
doc: {
text: this.t("action.learn_more"),
link: "https://docs.hoppscotch.io/",
},
})
}
return results.value
}
}

View File

@@ -91,9 +91,8 @@ export abstract class StaticSpotlightSearcherService<
private async addDocsToSearchIndex() {
this.loading.value = true
this.minisearch = new MiniSearch({
fields: this.opts.searchFields as string[],
})
this.minisearch.removeAll()
this.minisearch.vacuum()
await this.minisearch.addAllAsync(
Object.entries(this._documents).map(([id, doc]) => ({

View File

@@ -1,6 +1,5 @@
import { Service } from "dioc"
import {
SpotlightResultTextType,
SpotlightSearcher,
SpotlightSearcherResult,
SpotlightSearcherSessionState,
@@ -27,7 +26,6 @@ import {
} from "@hoppscotch/data"
import { hoppWorkspaceStore } from "~/newstore/workspace"
import { changeWorkspace } from "~/newstore/workspace"
import { invokeAction } from "~/helpers/actions"
/**
* A spotlight searcher that searches through the user's collections
@@ -145,13 +143,6 @@ export class CollectionsSpotlightSearcherService
},
})
if (pageCategory === "rest" || pageCategory === "graphql") {
minisearch.add({
id: `create-collection`,
name: this.t("collection.new"),
})
}
if (pageCategory === "rest") {
this.loadRESTDocsIntoMinisearch(minisearch)
} else if (pageCategory === "graphql") {
@@ -162,11 +153,6 @@ export class CollectionsSpotlightSearcherService
const scopeHandle = effectScope()
const newCollectionText: SpotlightResultTextType<any> = {
type: "text",
text: this.t("collection.new"),
}
scopeHandle.run(() => {
watch(query, (query) => {
if (pageCategory === "other") {
@@ -179,34 +165,28 @@ export class CollectionsSpotlightSearcherService
results.value = searchResults.map((result) => ({
id: result.id,
text:
result.id === "create-collection"
? newCollectionText
: {
type: "custom",
component: markRaw(RESTRequestSpotlightEntry),
componentProps: {
folderPath: result.id.split("rest-")[1],
},
},
text: {
type: "custom",
component: markRaw(RESTRequestSpotlightEntry),
componentProps: {
folderPath: result.id.split("rest-")[1],
},
},
icon: markRaw(IconFolder),
score: result.score,
}))
} else if (pageCategory === "graphql") {
} else {
const searchResults = minisearch.search(query).slice(0, 10)
results.value = searchResults.map((result) => ({
id: result.id,
text:
result.id === "create-collection"
? newCollectionText
: {
type: "custom",
component: markRaw(GQLRequestSpotlightEntry),
componentProps: {
folderPath: result.id.split("gql-")[1],
},
},
text: {
type: "custom",
component: markRaw(GQLRequestSpotlightEntry),
componentProps: {
folderPath: result.id.split("gql-")[1],
},
},
icon: markRaw(IconFolder),
score: result.score,
}))
@@ -276,8 +256,6 @@ export class CollectionsSpotlightSearcherService
}
public onResultSelect(result: SpotlightSearcherResult): void {
if (result.id === "create-collection") return invokeAction("collection.new")
const [type, path] = result.id.split("-")
if (type === "rest") {

View File

@@ -21,35 +21,33 @@ import {
StaticSpotlightSearcherService,
} from "./base/static.searcher"
import IconCopy from "~icons/lucide/copy"
import IconEdit from "~icons/lucide/edit"
import IconLayers from "~icons/lucide/layers"
import IconTrash2 from "~icons/lucide/trash-2"
import IconCopy from "~icons/lucide/copy"
import IconLayers from "~icons/lucide/layers"
import { Service } from "dioc"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { cloneDeep } from "lodash-es"
import MiniSearch from "minisearch"
import { map } from "rxjs"
import { useStreamStatic } from "~/composables/stream"
import { GQLError } from "~/helpers/backend/GQLClient"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import {
createEnvironment,
currentEnvironment$,
deleteEnvironment,
duplicateEnvironment,
environmentsStore,
getGlobalVariables,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
import IconCircle from "~icons/lucide/circle"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { GQLError } from "~/helpers/backend/GQLClient"
import { cloneDeep } from "lodash-es"
import { Service } from "dioc"
import MiniSearch from "minisearch"
import { map } from "rxjs"
type Doc = {
text: string | string[]
text: string
alternates: string[]
icon: object | Component
excludeFromSearch?: boolean
@@ -91,61 +89,40 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
private documents: Record<string, Doc> = reactive({
new_environment: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.new"),
],
text: this.t("spotlight.environments.new"),
alternates: ["new", "environment"],
icon: markRaw(IconLayers),
},
new_environment_variable: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.new_variable"),
],
text: this.t("spotlight.environments.new_variable"),
alternates: ["new", "environment", "variable"],
icon: markRaw(IconLayers),
},
edit_selected_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.edit"),
],
text: this.t("spotlight.environments.edit"),
alternates: ["edit", "environment"],
icon: markRaw(IconEdit),
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
},
delete_selected_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.delete"),
],
text: this.t("spotlight.environments.delete"),
alternates: ["delete", "environment"],
icon: markRaw(IconTrash2),
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
},
duplicate_selected_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.duplicate"),
],
text: this.t("spotlight.environments.duplicate"),
alternates: ["duplicate", "environment"],
icon: markRaw(IconCopy),
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
},
edit_global_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.edit_global"),
],
text: this.t("spotlight.environments.edit_global"),
alternates: ["edit", "global", "environment"],
icon: markRaw(IconEdit),
},
duplicate_global_env: {
text: [
this.t("spotlight.environments.title"),
this.t("spotlight.environments.duplicate_global"),
],
text: this.t("spotlight.environments.duplicate_global"),
alternates: ["duplicate", "global", "environment"],
icon: markRaw(IconCopy),
},
@@ -211,6 +188,29 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
}
}
removeSelectedEnvironment = () => {
if (this.selectedEnvIndex.value?.type === "NO_ENV_SELECTED") return
if (this.selectedEnvIndex.value?.type === "MY_ENV") {
deleteEnvironment(this.selectedEnvIndex.value.index)
// this.toast.success(`${t("state.deleted")}`)
}
if (this.selectedEnvIndex.value?.type === "TEAM_ENV") {
pipe(
deleteTeamEnvironment(this.selectedEnvIndex.value.teamEnvID),
TE.match(
(err: GQLError<string>) => {
console.error(err)
},
() => {
// this.toast.success(`${this.t("team_environment.deleted")}`)
}
)
)()
}
}
public onDocSelected(id: string): void {
switch (id) {
case "new_environment":
@@ -229,7 +229,7 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
})
break
case "delete_selected_env":
invokeAction(`modals.environment.delete-selected`)
this.removeSelectedEnvironment()
break
case "duplicate_selected_env":
this.duplicateSelectedEnv()
@@ -269,16 +269,6 @@ export class SwitchEnvSpotlightSearcherService
this.spotlight.registerSearcher(this)
}
private selectedEnvIndex = useStreamStatic(
selectedEnvironmentIndex$,
{
type: "NO_ENV_SELECTED",
},
() => {
/* noop */
}
)[0]
private environmentSearchable = useStreamStatic(
activeActions$.pipe(
map((actions) => actions.includes("modals.environment.add"))
@@ -296,25 +286,16 @@ export class SwitchEnvSpotlightSearcherService
const results = ref<SpotlightSearcherResult[]>([])
const minisearch = new MiniSearch({
fields: ["name", "alternates"],
fields: ["name"],
storeFields: ["name"],
})
if (this.environmentSearchable.value) {
minisearch.addAll(
environmentsStore.value.environments.map((entry, index) => {
let id = `environment-${index}`
if (
this.selectedEnvIndex.value?.type === "MY_ENV" &&
this.selectedEnvIndex.value.index === index
) {
id += "-selected"
}
return {
id,
id: `environment-${index}`,
name: entry.name,
alternates: ["environment", "change", entry.name],
}
})
)
@@ -341,9 +322,7 @@ export class SwitchEnvSpotlightSearcherService
.map((x) => {
return {
id: x.id,
icon: markRaw(
x.id.endsWith("-selected") ? IconCheckCircle : IconCircle
),
icon: markRaw(IconLayers),
score: x.score,
text: {
type: "text",

View File

@@ -7,19 +7,16 @@ import {
StaticSpotlightSearcherService,
} from "./base/static.searcher"
import IconLinkedIn from "~icons/brands/linkedin"
import IconTwitter from "~icons/brands/twitter"
import IconDiscord from "~icons/brands/discord"
import IconGitHub from "~icons/lucide/github"
import IconBook from "~icons/lucide/book"
import IconGithub from "~icons/lucide/github"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import IconMessageCircle from "~icons/lucide/message-circle"
import IconZap from "~icons/lucide/zap"
type Doc = {
text: string | string[]
text: string
alternates: string[]
icon: object | Component
action: () => void
}
/**
@@ -43,48 +40,26 @@ export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherServ
text: this.t("spotlight.general.help_menu"),
alternates: ["help", "hoppscotch"],
icon: markRaw(IconLifeBuoy),
action() {
invokeAction("modals.support.toggle")
},
},
chat_with_support: {
text: this.t("spotlight.general.chat"),
alternates: ["chat", "support", "hoppscotch"],
icon: markRaw(IconMessageCircle),
},
open_docs: {
text: this.t("spotlight.general.open_docs"),
alternates: ["docs", "documentation", "hoppscotch"],
icon: markRaw(IconBook),
action: () => this.openURL("https://docs.hoppscotch.io"),
},
open_keybindings: {
text: this.t("spotlight.general.open_keybindings"),
alternates: ["key", "shortcuts", "binding"],
icon: markRaw(IconZap),
action() {
invokeAction("flyouts.keybinds.toggle")
},
},
open_github: {
text: this.t("spotlight.general.open_github"),
alternates: ["repository", "github", "documentation", "hoppscotch"],
icon: markRaw(IconGitHub),
action: () => this.openURL("https://hoppscotch.io/github"),
},
link_twitter: {
text: [this.t("spotlight.general.social"), "Twitter"],
alternates: ["social", "twitter", "link"],
icon: markRaw(IconTwitter),
action: () => this.openURL("https://twitter.com/hoppscotch_io"),
},
link_discord: {
text: [this.t("spotlight.general.social"), "Discord"],
alternates: ["social", "discord", "link"],
icon: markRaw(IconDiscord),
action: () => this.openURL("https://hoppscotch.io/discord"),
},
link_linkedin: {
text: [this.t("spotlight.general.social"), "LinkedIn"],
alternates: ["social", "linkedin", "link"],
icon: markRaw(IconLinkedIn),
action: () =>
this.openURL("https://www.linkedin.com/company/hoppscotch/"),
social_links: {
text: this.t("spotlight.general.social"),
alternates: ["social", "github", "binding"],
icon: markRaw(IconGithub),
},
})
@@ -112,16 +87,28 @@ export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherServ
}
}
private openURL(url: string) {
private openDocs() {
const url = "https://docs.hoppscotch.io"
window.open(url, "_blank")
}
public onDocSelected(id: string): void {
this.documents[id]?.action()
}
public addCustomEntries(docs: Record<string, Doc>) {
this.documents = { ...this.documents, ...docs }
this.setDocuments(this.documents)
switch (id) {
case "open_help":
invokeAction("modals.support.toggle")
break
case "chat_with_support":
invokeAction("flyouts.chat.open")
break
case "open_docs":
this.openDocs()
break
case "open_keybindings":
invokeAction("flyouts.keybinds.toggle")
break
case "social_links":
invokeAction("modals.social.toggle")
break
}
}
}

View File

@@ -1,126 +0,0 @@
import { Ref, computed, effectScope, markRaw, ref, unref, watch } from "vue"
import { getI18n } from "~/modules/i18n"
import {
SpotlightSearcher,
SpotlightSearcherResult,
SpotlightSearcherSessionState,
SpotlightService,
} from ".."
import { Service } from "dioc"
import { useService } from "dioc/vue"
import MiniSearch from "minisearch"
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
import { InterceptorService } from "~/services/interceptor.service"
import IconCircle from "~icons/lucide/circle"
/**
* This searcher is responsible for searching through the interceptor.
* And switching between them.
*/
export class InterceptorSpotlightSearcherService
extends Service
implements SpotlightSearcher
{
public static readonly ID = "INTERCEPTOR_SPOTLIGHT_SEARCHER_SERVICE"
private t = getI18n()
public searcherID = "interceptor"
public searcherSectionTitle = this.t("settings.interceptor")
private readonly spotlight = this.bind(SpotlightService)
constructor() {
super()
this.spotlight.registerSearcher(this)
}
private interceptorService = useService(InterceptorService)
createSearchSession(
query: Readonly<Ref<string>>
): [Ref<SpotlightSearcherSessionState>, () => void] {
const loading = ref(false)
const results = ref<SpotlightSearcherResult[]>([])
const minisearch = new MiniSearch({
fields: ["name", "alternates"],
storeFields: ["name"],
})
const interceptorSelection = this.interceptorService
.currentInterceptorID as Ref<string>
const interceptors = this.interceptorService.availableInterceptors
minisearch.addAll(
interceptors.value.map((entry) => {
let id = `interceptor-${entry.interceptorID}`
if (entry.interceptorID === interceptorSelection.value) {
id += "-selected"
}
const name = unref(entry.name(this.t))
return {
id,
name,
alternates: ["interceptor", "change", name],
}
})
)
const scopeHandle = effectScope()
scopeHandle.run(() => {
watch(
[query],
([query]) => {
results.value = minisearch
.search(query, {
prefix: true,
fuzzy: true,
boost: {
reltime: 2,
},
weights: {
fuzzy: 0.2,
prefix: 0.8,
},
})
.map((x) => {
return {
id: x.id,
icon: markRaw(
x.id.endsWith("-selected") ? IconCheckCircle : IconCircle
),
score: x.score,
text: {
type: "text",
text: [this.t("spotlight.section.interceptor"), x.name],
},
}
})
},
{ immediate: true }
)
})
const onSessionEnd = () => {
scopeHandle.stop()
minisearch.removeAll()
}
const resultObj = computed<SpotlightSearcherSessionState>(() => ({
loading: loading.value,
results: results.value,
}))
return [resultObj, onSessionEnd]
}
onResultSelect(result: SpotlightSearcherResult): void {
const selectedInterceptor = result.id.split("-")[1]
this.interceptorService.currentInterceptorID.value = selectedInterceptor
}
}

View File

@@ -1,5 +1,5 @@
import { Component, computed, markRaw, reactive } from "vue"
import { invokeAction, isActionBound } from "~/helpers/actions"
import { Component, markRaw, reactive } from "vue"
import { invokeAction } from "~/helpers/actions"
import { getI18n } from "~/modules/i18n"
import { SpotlightSearcherResult, SpotlightService } from ".."
import {
@@ -7,11 +7,12 @@ import {
StaticSpotlightSearcherService,
} from "./base/static.searcher"
import { useRoute } from "vue-router"
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
import { currentActiveTab } from "~/helpers/rest/tab"
import IconWindow from "~icons/lucide/app-window"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconCheck from "~icons/lucide/check"
import IconChevronLeft from "~icons/lucide/chevron-left"
import IconChevronRight from "~icons/lucide/chevron-right"
import IconCode2 from "~icons/lucide/code-2"
import IconCopy from "~icons/lucide/copy"
import IconFileCode from "~icons/lucide/file-code"
@@ -19,13 +20,11 @@ import IconRename from "~icons/lucide/file-edit"
import IconPlay from "~icons/lucide/play"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
type Doc = {
text: string | string[]
alternates: string[]
icon: object | Component
excludeFromSearch?: boolean
}
/**
@@ -44,182 +43,116 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
private readonly spotlight = this.bind(SpotlightService)
private route = useRoute()
private isRESTPage = computed(() => this.route.name === "index")
private isGQLPage = computed(() => this.route.name === "graphql")
private isRESTOrGQLPage = computed(
() => this.isRESTPage.value || this.isGQLPage.value
)
private isGQLConnectBound = isActionBound("gql.connect")
private isGQLDisconnectBound = isActionBound("gql.disconnect")
private documents: Record<string, Doc> = reactive({
send_request: {
text: this.t("shortcut.request.send_request"),
alternates: ["request", "send"],
icon: markRaw(IconPlay),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
gql_connect: {
text: [this.t("navigation.graphql"), this.t("spotlight.graphql.connect")],
alternates: ["connect", "server", "graphql"],
icon: markRaw(IconPlay),
excludeFromSearch: computed(() => !this.isGQLConnectBound.value),
},
gql_disconnect: {
text: [
this.t("navigation.graphql"),
this.t("spotlight.graphql.disconnect"),
],
alternates: ["disconnect", "stop", "graphql"],
icon: markRaw(IconPlay),
excludeFromSearch: computed(() => !this.isGQLDisconnectBound.value),
},
save_to_collections: {
text: this.t("spotlight.request.save_as_new"),
text: [
this.t("request.save_as"),
this.t("shortcut.request.save_to_collections"),
],
alternates: ["save", "collections"],
icon: markRaw(IconSave),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
save_request: {
text: this.t("shortcut.request.save_request"),
alternates: ["save", "request"],
icon: markRaw(IconSave),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
rename_request: {
text: this.t("shortcut.request.rename"),
alternates: ["rename", "request"],
icon: markRaw(IconRename),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
copy_request_link: {
text: this.t("shortcut.request.copy_request_link"),
alternates: ["copy", "link"],
icon: markRaw(IconCopy),
excludeFromSearch: computed(() => !this.isRESTPage.value),
},
reset_request: {
text: this.t("shortcut.request.reset_request"),
alternates: ["reset", "request"],
icon: markRaw(IconRotateCCW),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
import_curl: {
text: this.t("shortcut.request.import_curl"),
alternates: ["import", "curl"],
icon: markRaw(IconFileCode),
excludeFromSearch: computed(() => !this.isRESTPage.value),
},
show_code: {
text: this.t("shortcut.request.show_code"),
alternates: ["show", "code"],
icon: markRaw(IconCode2),
excludeFromSearch: computed(() => !this.isRESTPage.value),
},
// Change request method
next_method: {
text: this.t("shortcut.request.next_method"),
alternates: ["next", "method"],
icon: markRaw(IconChevronRight),
},
previous_method: {
text: this.t("shortcut.request.previous_method"),
alternates: ["previous", "method"],
icon: markRaw(IconChevronLeft),
},
get_method: {
text: [this.t("spotlight.request.select_method"), "GET"],
text: this.t("shortcut.request.get_method"),
alternates: ["get", "method"],
icon: markRaw(IconCheckCircle),
excludeFromSearch: computed(() => !this.isRESTPage.value),
icon: markRaw(IconCheck),
},
head_method: {
text: [this.t("spotlight.request.select_method"), "HEAD"],
text: this.t("shortcut.request.head_method"),
alternates: ["head", "method"],
icon: markRaw(IconCheckCircle),
excludeFromSearch: computed(() => !this.isRESTPage.value),
icon: markRaw(IconCheck),
},
post_method: {
text: [this.t("spotlight.request.select_method"), "POST"],
text: this.t("shortcut.request.post_method"),
alternates: ["post", "method"],
icon: markRaw(IconCheckCircle),
excludeFromSearch: computed(() => !this.isRESTPage.value),
icon: markRaw(IconCheck),
},
put_method: {
text: [this.t("spotlight.request.select_method"), "PUT"],
text: this.t("shortcut.request.put_method"),
alternates: ["put", "method"],
icon: markRaw(IconCheckCircle),
excludeFromSearch: computed(() => !this.isRESTPage.value),
icon: markRaw(IconCheck),
},
delete_method: {
text: [this.t("spotlight.request.select_method"), "DELETE"],
text: this.t("shortcut.request.delete_method"),
alternates: ["delete", "method"],
icon: markRaw(IconCheckCircle),
excludeFromSearch: computed(() => !this.isRESTPage.value),
icon: markRaw(IconCheck),
},
// Change sub tabs
tab_parameters: {
text: [
this.t("spotlight.request.switch_to"),
this.t("spotlight.request.tab_parameters"),
],
text: this.t("spotlight.request.tab_parameters"),
alternates: ["parameters", "tab"],
icon: markRaw(IconWindow),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
tab_body: {
text: [
this.t("spotlight.request.switch_to"),
this.t("spotlight.request.tab_body"),
],
text: this.t("spotlight.request.tab_body"),
alternates: ["body", "tab"],
icon: markRaw(IconWindow),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
tab_headers: {
text: [
this.t("spotlight.request.switch_to"),
this.t("spotlight.request.tab_headers"),
],
text: this.t("spotlight.request.tab_headers"),
alternates: ["headers", "tab"],
icon: markRaw(IconWindow),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
tab_authorization: {
text: [
this.t("spotlight.request.switch_to"),
this.t("spotlight.request.tab_authorization"),
],
text: this.t("spotlight.request.tab_authorization"),
alternates: ["authorization", "tab"],
icon: markRaw(IconWindow),
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
},
tab_pre_request_script: {
text: [
this.t("spotlight.request.switch_to"),
this.t("spotlight.request.tab_pre_request_script"),
],
text: this.t("spotlight.request.tab_pre_request_script"),
alternates: ["pre-request", "script", "tab"],
icon: markRaw(IconWindow),
excludeFromSearch: computed(() => !this.isRESTPage.value),
},
tab_tests: {
text: [
this.t("spotlight.request.switch_to"),
this.t("spotlight.request.tab_tests"),
],
text: this.t("spotlight.request.tab_tests"),
alternates: ["tests", "tab"],
icon: markRaw(IconWindow),
excludeFromSearch: computed(() => !this.isRESTPage.value),
},
tab_query: {
text: [
this.t("spotlight.request.switch_to"),
this.t("spotlight.request.tab_query"),
],
alternates: ["query", "tab"],
icon: markRaw(IconWindow),
excludeFromSearch: computed(() => !this.isGQLPage.value),
},
tab_variables: {
text: [
this.t("spotlight.request.switch_to"),
this.t("spotlight.request.tab_variables"),
],
alternates: ["variables", "tab"],
icon: markRaw(IconWindow),
excludeFromSearch: computed(() => !this.isGQLPage.value),
},
})
@@ -247,7 +180,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
}
}
private openRequestTab(tab: RequestOptionTabs | GQLOptionTabs): void {
private openRequestTab(tab: RequestOptionTabs): void {
invokeAction("request.open-tab", {
tab,
})
@@ -258,12 +191,6 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
case "send_request":
invokeAction("request.send-cancel")
break
case "gql_connect":
invokeAction("gql.connect")
break
case "gql_disconnect":
invokeAction("gql.disconnect")
break
case "save_to_collections":
invokeAction("request.save-as", {
requestType: "rest",
@@ -274,7 +201,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
invokeAction("request.save")
break
case "rename_request":
invokeAction("request.rename")
invokeAction("rest.request.rename")
break
case "copy_request_link":
invokeAction("request.copy-link")
@@ -282,6 +209,12 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
case "reset_request":
invokeAction("request.reset")
break
case "next_method":
invokeAction("request.method.next")
break
case "previous_method":
invokeAction("request.method.prev")
break
case "get_method":
invokeAction("request.method.get")
break
@@ -321,12 +254,6 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
case "tab_tests":
this.openRequestTab("tests")
break
case "tab_query":
this.openRequestTab("query")
break
case "tab_variables":
this.openRequestTab("variables")
break
}
}
}

View File

@@ -1,5 +1,5 @@
import { Component, computed, markRaw, reactive } from "vue"
import { invokeAction, isActionBound } from "~/helpers/actions"
import { activeActions$, invokeAction } from "~/helpers/actions"
import { getI18n } from "~/modules/i18n"
import { SpotlightSearcherResult, SpotlightService } from ".."
import {
@@ -9,6 +9,8 @@ import {
import IconDownload from "~icons/lucide/download"
import IconCopy from "~icons/lucide/copy"
import { map } from "rxjs"
import { useStreamStatic } from "~/composables/stream"
type Doc = {
text: string
@@ -33,11 +35,23 @@ export class ResponseSpotlightSearcherService extends StaticSpotlightSearcherSer
private readonly spotlight = this.bind(SpotlightService)
private copyResponseActionEnabled = isActionBound("response.copy")
private copyResponseActionEnabled = useStreamStatic(
activeActions$.pipe(map((actions) => actions.includes("response.copy"))),
activeActions$.value.includes("response.copy"),
() => {
/* noop */
}
)[0]
private downloadResponseActionEnabled = isActionBound(
"response.file.download"
)
private downloadResponseActionEnabled = useStreamStatic(
activeActions$.pipe(
map((actions) => actions.includes("response.file.download"))
),
activeActions$.value.includes("response.file.download"),
() => {
/* noop */
}
)[0]
private documents: Record<string, Doc> = reactive({
copy_response: {

View File

@@ -10,14 +10,16 @@ import {
} from "./base/static.searcher"
import IconCloud from "~icons/lucide/cloud"
import IconGlobe from "~icons/lucide/globe"
import IconMonitor from "~icons/lucide/monitor"
import IconMoon from "~icons/lucide/moon"
import IconSun from "~icons/lucide/sun"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconGlobe from "~icons/lucide/globe"
import IconShieldCheck from "~icons/lucide/shield-check"
import IconType from "~icons/lucide/type"
type Doc = {
text: string | string[]
excludeFromSearch?: boolean
alternates: string[]
icon: object | Component
}
@@ -33,7 +35,7 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
private t = getI18n()
private activeTheme = useSetting("BG_COLOR")
private activeFontSize = useSetting("FONT_SIZE")
public readonly searcherID = "settings"
public searcherSectionTitle = this.t("navigation.settings")
@@ -47,11 +49,7 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
this.t("spotlight.settings.theme.system"),
],
alternates: ["theme"],
icon: computed(() =>
this.activeTheme.value === "system"
? markRaw(IconCheckCircle)
: markRaw(IconMonitor)
),
icon: markRaw(IconMonitor),
},
theme_light: {
text: [
@@ -59,11 +57,7 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
this.t("spotlight.settings.theme.light"),
],
alternates: ["theme"],
icon: computed(() =>
this.activeTheme.value === "light"
? markRaw(IconCheckCircle)
: markRaw(IconSun)
),
icon: markRaw(IconSun),
},
theme_dark: {
text: [
@@ -71,11 +65,7 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
this.t("spotlight.settings.theme.dark"),
],
alternates: ["theme"],
icon: computed(() =>
this.activeTheme.value === "dark"
? markRaw(IconCheckCircle)
: markRaw(IconCloud)
),
icon: markRaw(IconCloud),
},
theme_black: {
text: [
@@ -83,13 +73,53 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
this.t("spotlight.settings.theme.black"),
],
alternates: ["theme"],
icon: computed(() =>
this.activeTheme.value === "black"
? markRaw(IconCheckCircle)
: markRaw(IconMoon)
),
icon: markRaw(IconMoon),
},
font_size_sm: {
text: [
this.t("settings.font_size"),
this.t("spotlight.settings.font.size_sm"),
],
onClick: () => {
console.log("clicked")
},
excludeFromSearch: computed(() => this.activeFontSize.value === "small"),
alternates: [
"font size",
"change font size",
"change font",
"increase font",
],
icon: markRaw(IconType),
},
font_size_md: {
text: [
this.t("settings.font_size"),
this.t("spotlight.settings.font.size_md"),
],
excludeFromSearch: computed(() => this.activeFontSize.value === "medium"),
alternates: [
"font size",
"change font size",
"change font",
"increase font",
],
icon: markRaw(IconType),
},
font_size_lg: {
text: [
this.t("settings.font_size"),
this.t("spotlight.settings.font.size_lg"),
],
excludeFromSearch: computed(() => this.activeFontSize.value === "large"),
alternates: [
"font size",
"change font size",
"change font",
"increase font",
],
icon: markRaw(IconType),
},
change_lang: {
text: [
this.t("spotlight.section.interface"),
@@ -98,6 +128,22 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
alternates: ["language", "change language"],
icon: markRaw(IconGlobe),
},
change_interceptor: {
text: [
this.t("spotlight.section.interceptor"),
this.t("spotlight.settings.change_interceptor"),
],
alternates: ["interceptor", "change interceptor"],
icon: markRaw(IconShieldCheck),
},
install_ext: {
text: [
this.t("spotlight.section.interceptor"),
this.t("spotlight.settings.install_extension"),
],
alternates: ["install extension", "extension", "interceptor"],
icon: markRaw(IconShieldCheck),
},
})
constructor() {
@@ -128,12 +174,27 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
applySetting("BG_COLOR", theme)
}
installExtension() {
const url = navigator.userAgent.includes("Firefox")
? "https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
: "https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
window.open(url, "_blank")
}
public onDocSelected(id: string): void {
switch (id) {
case "change_interceptor":
invokeAction("navigation.jump.settings")
break
case "change_lang":
invokeAction("navigation.jump.settings")
break
case "install_ext":
this.installExtension()
break
// theme actions
case "theme_system":
invokeAction("settings.theme.system")
@@ -147,6 +208,17 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
case "theme_black":
invokeAction("settings.theme.black")
break
// font size actions
case "font_size_sm":
this.activeFontSize.value = "small"
break
case "font_size_md":
this.activeFontSize.value = "medium"
break
case "font_size_lg":
this.activeFontSize.value = "large"
break
}
}
}

View File

@@ -1,4 +1,4 @@
import { Component, computed, markRaw, reactive } from "vue"
import { Component, markRaw, reactive } from "vue"
import { getI18n } from "~/modules/i18n"
import { SpotlightSearcherResult, SpotlightService } from ".."
import {
@@ -6,20 +6,19 @@ import {
StaticSpotlightSearcherService,
} from "./base/static.searcher"
import { useRoute } from "vue-router"
import IconCopy from "~icons/lucide/copy"
import IconCopyPlus from "~icons/lucide/copy-plus"
import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square"
import { invokeAction } from "~/helpers/actions"
import { getActiveTabs as getRESTActiveTabs } from "~/helpers/rest/tab"
import { getActiveTabs as getGQLActiveTabs } from "~/helpers/graphql/tab"
import {
closeOtherTabs,
closeTab,
createNewTab,
currentTabID,
} from "~/helpers/rest/tab"
import IconWindow from "~icons/lucide/app-window"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
type Doc = {
text: string | string[]
text: string
alternates: string[]
icon: object | Component
excludeFromSearch?: boolean
}
/**
@@ -38,52 +37,21 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
private readonly spotlight = this.bind(SpotlightService)
private route = useRoute()
private showAction = computed(
() => this.route.name === "index" || this.route.name === "graphql"
)
private gqlActiveTabs = getGQLActiveTabs()
private restActiveTabs = getRESTActiveTabs()
private isOnlyTab = computed(() =>
this.route.name === "graphql"
? this.gqlActiveTabs.value.length === 1
: this.restActiveTabs.value.length === 1
)
private documents: Record<string, Doc> = reactive({
duplicate_tab: {
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.duplicate")],
alternates: ["tab", "duplicate", "duplicate tab"],
icon: markRaw(IconCopy),
excludeFromSearch: computed(() => !this.showAction.value),
},
close_current_tab: {
text: [
this.t("spotlight.tab.title"),
this.t("spotlight.tab.close_current"),
],
text: this.t("spotlight.tab.close_current"),
alternates: ["tab", "close", "close tab"],
icon: markRaw(IconXCircle),
excludeFromSearch: computed(
() => !this.showAction.value || this.isOnlyTab.value
),
icon: markRaw(IconWindow),
},
close_other_tabs: {
text: [
this.t("spotlight.tab.title"),
this.t("spotlight.tab.close_others"),
],
text: this.t("spotlight.tab.close_others"),
alternates: ["tab", "close", "close all"],
icon: markRaw(IconXSquare),
excludeFromSearch: computed(
() => !this.showAction.value || this.isOnlyTab.value
),
icon: markRaw(IconWindow),
},
open_new_tab: {
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.new_tab")],
text: this.t("spotlight.tab.new_tab"),
alternates: ["tab", "new", "open tab"],
icon: markRaw(IconCopyPlus),
excludeFromSearch: computed(() => !this.showAction.value),
icon: markRaw(IconWindow),
},
})
@@ -112,9 +80,12 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
}
public onDocSelected(id: string): void {
if (id === "duplicate_tab") invokeAction("tab.duplicate-tab", {})
if (id === "close_current_tab") invokeAction("tab.close-current")
if (id === "close_other_tabs") invokeAction("tab.close-other")
if (id === "open_new_tab") invokeAction("tab.open-new")
if (id === "close_current_tab") closeTab(currentTabID.value)
if (id === "close_other_tabs") closeOtherTabs(currentTabID.value)
if (id === "open_new_tab")
createNewTab({
request: getDefaultRESTRequest(),
isDirty: false,
})
}
}

View File

@@ -8,7 +8,7 @@ import {
ref,
watch,
} from "vue"
import { invokeAction } from "~/helpers/actions"
import { activeActions$, invokeAction } from "~/helpers/actions"
import { getI18n } from "~/modules/i18n"
import {
SpotlightSearcher,
@@ -24,7 +24,6 @@ import {
import { Service } from "dioc"
import * as E from "fp-ts/Either"
import MiniSearch from "minisearch"
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
import { useStreamStatic } from "~/composables/stream"
import { runGQLQuery } from "~/helpers/backend/GQLClient"
import { GetMyTeamsDocument, GetMyTeamsQuery } from "~/helpers/backend/graphql"
@@ -37,7 +36,7 @@ import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users"
type Doc = {
text: string | string[]
text: string
alternates: string[]
icon: object | Component
excludeFromSearch?: boolean
@@ -67,6 +66,14 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
}
)[0]
private activeActions = useStreamStatic(activeActions$, [], () => {
/* noop */
})[0]
private isLoggedInUser = computed(() =>
this.activeActions.value.includes("user.logout")
)
private isTeamSelected = computed(
() =>
this.workspace.value.type === "team" &&
@@ -75,33 +82,31 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
private documents: Record<string, Doc> = reactive({
new_team: {
text: [this.t("team.title"), this.t("spotlight.workspace.new")],
text: this.t("spotlight.workspace.new"),
alternates: ["new", "team", "workspace"],
icon: markRaw(IconUsers),
excludeFromSearch: computed(() => !this.isLoggedInUser.value),
},
edit_team: {
text: [this.t("team.title"), this.t("spotlight.workspace.edit")],
text: this.t("spotlight.workspace.edit"),
alternates: ["edit", "team", "workspace"],
icon: markRaw(IconEdit),
excludeFromSearch: computed(() => !this.isTeamSelected.value),
},
invite_members: {
text: [this.t("team.title"), this.t("spotlight.workspace.invite")],
text: this.t("spotlight.workspace.invite"),
alternates: ["invite", "members", "workspace"],
icon: markRaw(IconUserPlus),
excludeFromSearch: computed(() => !this.isTeamSelected.value),
},
delete_team: {
text: [this.t("team.title"), this.t("spotlight.workspace.delete")],
text: this.t("spotlight.workspace.delete"),
alternates: ["delete", "team", "workspace"],
icon: markRaw(IconTrash2),
excludeFromSearch: computed(() => !this.isTeamSelected.value),
},
switch_to_personal: {
text: [
this.t("team.title"),
this.t("spotlight.workspace.switch_to_personal"),
],
text: this.t("spotlight.workspace.switch_to_personal"),
alternates: ["switch", "team", "workspace", "personal"],
icon: markRaw(IconUser),
excludeFromSearch: computed(() => !this.isTeamSelected.value),
@@ -140,13 +145,8 @@ export class WorkspaceSpotlightSearcherService extends StaticSpotlightSearcherSe
}
public onDocSelected(id: string): void {
if (id === "new_team") {
if (platform.auth.getCurrentUser()) {
invokeAction(`modals.team.new`)
} else {
invokeAction(`modals.login.toggle`)
}
} else if (id === "edit_team") invokeAction(`modals.team.edit`)
if (id === "new_team") invokeAction(`modals.team.new`)
else if (id === "edit_team") invokeAction(`modals.team.edit`)
else if (id === "invite_members") invokeAction(`modals.team.invite`)
else if (id === "delete_team") this.deleteTeam()
else if (id === "switch_to_personal")
@@ -197,14 +197,6 @@ export class SwitchWorkspaceSpotlightSearcherService
})
}
private workspace = useStreamStatic(
workspaceStatus$,
{ type: "personal" },
() => {
/* noop */
}
)[0]
createSearchSession(
query: Readonly<Ref<string>>
): [Ref<SpotlightSearcherSessionState>, () => void] {
@@ -219,16 +211,8 @@ export class SwitchWorkspaceSpotlightSearcherService
this.fetchMyTeams().then((teams) => {
minisearch.addAll(
teams.map((entry) => {
let id = `workspace-${entry.id}`
// if id matches add -selected to it
if (
this.workspace.value.type === "team" &&
this.workspace.value.teamID === entry.id
) {
id += "-selected"
}
return {
id,
id: `workspace-${entry.id}`,
name: entry.name,
alternates: ["team", "workspace", "change", "switch"],
}
@@ -257,9 +241,7 @@ export class SwitchWorkspaceSpotlightSearcherService
.map((x) => {
return {
id: x.id,
icon: markRaw(
x.id.endsWith("-selected") ? IconCheckCircle : IconUsers
),
icon: markRaw(IconUsers),
score: x.score,
text: {
type: "text",

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/selfhost-web",
"private": true,
"version": "2023.8.0",
"version": "2023.4.8",
"type": "module",
"scripts": {
"dev:vite": "vite",
@@ -28,8 +28,8 @@
"@fontsource-variable/roboto-mono": "^5.0.9",
"@hoppscotch/common": "workspace:^",
"@hoppscotch/data": "workspace:^",
"@import-meta-env/unplugin": "^0.4.8",
"axios": "^1.4.0",
"@import-meta-env/unplugin": "^0.4.8",
"buffer": "^6.0.3",
"fp-ts": "^2.16.1",
"process": "^0.11.10",
@@ -65,7 +65,6 @@
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.9",
"vite-plugin-fonts": "^0.6.0",
"vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.38",
"vite-plugin-pages": "^0.31.0",
@@ -76,6 +75,7 @@
"vite-plugin-windicss": "^1.9.1",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8",
"vite-plugin-fonts": "^0.6.0",
"windicss": "^3.5.6"
}
}

View File

@@ -7,16 +7,9 @@ import { def as historyDef } from "./platform/history/history.platform"
import { def as tabStateDef } from "./platform/tabState/tabState.platform"
import { browserInterceptor } from "@hoppscotch/common/platform/std/interceptors/browser"
import { proxyInterceptor } from "@hoppscotch/common/platform/std/interceptors/proxy"
import { ExtensionInspectorService } from "@hoppscotch/common/platform/std/inspections/extension.inspector"
import { ExtensionInterceptorService } from "@hoppscotch/common/platform/std/interceptors/extension"
import { stdFooterItems } from "@hoppscotch/common/platform/std/ui/footerItem"
import { stdSupportOptionItems } from "@hoppscotch/common/platform/std/ui/supportOptionsItem"
createHoppApp("#app", {
ui: {
additionalFooterMenuItems: stdFooterItems,
additionalSupportOptionsMenuItems: stdSupportOptionItems,
},
auth: authDef,
sync: {
environments: environmentsDef,
@@ -33,9 +26,6 @@ createHoppApp("#app", {
{ type: "service", service: ExtensionInterceptorService },
],
},
additionalInspectors: [
{ type: "service", service: ExtensionInspectorService },
],
platformFeatureFlags: {
exportAsGIST: false,
},

View File

@@ -1,21 +1,8 @@
@mixin base-theme {
--font-sans: 'Inter Variable', sans-serif;
--font-icon: 'Material Symbols Rounded Variable';
--font-mono: 'Roboto Mono Variable', monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.688rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--sidebar-primary-sticky-fold: 2rem;
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
}
@mixin dark-theme {
@@ -213,8 +200,8 @@
:root {
@include base-theme;
@include dark-theme;
@include dark-editor-theme;
@include green-theme;
@include dark-editor-theme;
}
:root.light {
@@ -270,3 +257,63 @@
:root[data-accent='yellow'] {
@include yellow-theme;
}
@mixin font-small {
--font-size-body: 0.75rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin font-medium {
--font-size-body: 0.875rem;
--line-height-body: 1.25rem;
--upper-primary-sticky-fold: 4.375rem;
--upper-secondary-sticky-fold: 6.688rem;
--upper-tertiary-sticky-fold: 9rem;
--upper-mobile-primary-sticky-fold: 7.125rem;
--upper-mobile-secondary-sticky-fold: 9.438rem;
--upper-mobile-sticky-fold: 11.75rem;
--upper-mobile-tertiary-sticky-fold: 9rem;
--lower-primary-sticky-fold: 3.25rem;
--lower-secondary-sticky-fold: 5.563rem;
--lower-tertiary-sticky-fold: 7.875rem;
--sidebar-primary-sticky-fold: 2.25rem;
}
@mixin font-large {
--font-size-body: 1rem;
--line-height-body: 1.5rem;
--upper-primary-sticky-fold: 4.625rem;
--upper-secondary-sticky-fold: 7.188rem;
--upper-tertiary-sticky-fold: 9.75rem;
--upper-mobile-primary-sticky-fold: 7.625rem;
--upper-mobile-secondary-sticky-fold: 10.188rem;
--upper-mobile-sticky-fold: 12.75rem;
--upper-mobile-tertiary-sticky-fold: 9.75rem;
--lower-primary-sticky-fold: 3.5rem;
--lower-secondary-sticky-fold: 6.063rem;
--lower-tertiary-sticky-fold: 8.625rem;
--sidebar-primary-sticky-fold: 2.5rem;
}
:root[data-font-size='small'] {
@include font-small;
}
:root[data-font-size='medium'] {
@include font-medium;
}
:root[data-font-size='large'] {
@include font-large;
}

View File

@@ -1,17 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script>
globalThis.import_meta_env = JSON.parse('"import_meta_env_placeholder"');
</script>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hoppscotch Admin</title>
<meta property="og:image" content="/cover.jpg" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<html lang="en" data-font-size="large">
<head>
<script>
globalThis.import_meta_env = JSON.parse('"import_meta_env_placeholder"')
</script>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hoppscotch Admin</title>
<meta property="og:image" content="/cover.jpg" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

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

View File

@@ -1,12 +1,44 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
import '@vue/runtime-core';
export {}
export {};
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AppHeader: typeof import('./components/app/Header.vue')['default'];
AppLogin: typeof import('./components/app/Login.vue')['default'];
AppLogout: typeof import('./components/app/Logout.vue')['default'];
AppModal: typeof import('./components/app/Modal.vue')['default'];
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'];
AppToast: typeof import('./components/app/Toast.vue')['default'];
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'];
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'];
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete'];
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'];
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab'];
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'];
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'];
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default'];
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'];
IconLucideUser: typeof import('~icons/lucide/user')['default'];
TeamsAdd: typeof import('./components/teams/Add.vue')['default'];
TeamsDetails: typeof import('./components/teams/Details.vue')['default'];
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'];
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
TeamsTable: typeof import('./components/teams/Table.vue')['default'];
Tippy: typeof import('vue-tippy')['Tippy'];
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
UsersTable: typeof import('./components/users/Table.vue')['default'];
AppHeader: typeof import('./components/app/Header.vue')['default']
AppLogin: typeof import('./components/app/Login.vue')['default']
AppLogout: typeof import('./components/app/Logout.vue')['default']
@@ -17,15 +49,9 @@ declare module '@vue/runtime-core' {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
@@ -37,5 +63,4 @@ declare module '@vue/runtime-core' {
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
UsersTable: typeof import('./components/users/Table.vue')['default']
}
}

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