Compare commits
16 Commits
hotfix/fet
...
perf/raw-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743f693e46 | ||
|
|
dcbbd34247 | ||
|
|
1431ecc6d7 | ||
|
|
f34d896095 | ||
|
|
e95ebb9226 | ||
|
|
57365eeae0 | ||
|
|
b22bd97818 | ||
|
|
b953b32ff4 | ||
|
|
0eacd6763b | ||
|
|
8499ac7fec | ||
|
|
4adac4af38 | ||
|
|
fd162e242c | ||
|
|
3e83828722 | ||
|
|
f7dc36e3f1 | ||
|
|
a7566dfd86 | ||
|
|
d4d7a20fbd |
66
.github/workflows/release-push-docker.yml
vendored
Normal file
66
.github/workflows/release-push-docker.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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 }}
|
||||||
@@ -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
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
and orientation.
|
identity and orientation.
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
diverse, inclusive, and healthy community.
|
diverse, inclusive, and healthy community.
|
||||||
@@ -22,17 +22,17 @@ community include:
|
|||||||
* Giving and gracefully accepting constructive feedback
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
overall community
|
community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
advances of any kind
|
any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email
|
* Publishing others' private information, such as a physical or email address,
|
||||||
address, without their explicit permission
|
without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
@@ -82,15 +82,15 @@ behavior was inappropriate. A public apology may be requested.
|
|||||||
|
|
||||||
### 2. Warning
|
### 2. Warning
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
**Community Impact**: A violation through a single incident or series of
|
||||||
of actions.
|
actions.
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
interaction with the people involved, including unsolicited interaction with
|
interaction with the people involved, including unsolicited interaction with
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
like social media. Violating these terms may lead to a temporary or
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
permanent ban.
|
ban.
|
||||||
|
|
||||||
### 3. Temporary Ban
|
### 3. Temporary Ban
|
||||||
|
|
||||||
@@ -106,23 +106,27 @@ Violating these terms may lead to a permanent ban.
|
|||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**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.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
the community.
|
community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
version 2.0, available at
|
version 2.1, available at
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
Community Impact Guidelines were inspired by
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
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/faq][FAQ]. Translations are available at
|
||||||
https://www.contributor-covenant.org/translations.
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[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
|
||||||
|
|||||||
190
README.md
190
README.md
@@ -2,23 +2,18 @@
|
|||||||
<a href="https://hoppscotch.io">
|
<a href="https://hoppscotch.io">
|
||||||
<img
|
<img
|
||||||
src="https://avatars.githubusercontent.com/u/56705483"
|
src="https://avatars.githubusercontent.com/u/56705483"
|
||||||
alt="Hoppscotch Logo"
|
alt="Hoppscotch"
|
||||||
height="64"
|
height="64"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<h3>
|
||||||
<p>
|
|
||||||
<h3>
|
|
||||||
<b>
|
|
||||||
Hoppscotch
|
|
||||||
</b>
|
|
||||||
</h3>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<b>
|
<b>
|
||||||
Open source API development ecosystem
|
Hoppscotch
|
||||||
</b>
|
</b>
|
||||||
</p>
|
</h3>
|
||||||
|
<b>
|
||||||
|
Open Source API Development Ecosystem
|
||||||
|
</b>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](CODE_OF_CONDUCT.md) [](https://hoppscotch.io) [](https://github.com/hoppscotch/hoppscotch/actions) [](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)
|
[](CODE_OF_CONDUCT.md) [](https://hoppscotch.io) [](https://github.com/hoppscotch/hoppscotch/actions) [](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)
|
||||||
@@ -34,23 +29,18 @@
|
|||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<p>
|
||||||
<a href="https://hoppscotch.io/#gh-light-mode-only" target="_blank">
|
<a href="https://hoppscotch.io">
|
||||||
<img
|
<picture>
|
||||||
src="./packages/hoppscotch-common/public/images/banner-light.png"
|
<source media="(prefers-color-scheme: dark)" srcset="./packages/hoppscotch-common/public/images/banner-dark.png">
|
||||||
alt="Hoppscotch"
|
<source media="(prefers-color-scheme: light)" srcset="./packages/hoppscotch-common/public/images/banner-light.png">
|
||||||
width="100%"
|
<img alt="Hoppscotch" src="./packages/hoppscotch-common/public/images/banner-dark.png">
|
||||||
/>
|
</picture>
|
||||||
</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>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
_We highly recommend you take a look at the [**Hoppscotch Documentation**](https://docs.hoppscotch.io) to learn more about the app._
|
||||||
|
|
||||||
#### **Support**
|
#### **Support**
|
||||||
|
|
||||||
[](https://hoppscotch.io/discord) [](https://hoppscotch.io/telegram) [](https://github.com/hoppscotch/hoppscotch/discussions)
|
[](https://hoppscotch.io/discord) [](https://hoppscotch.io/telegram) [](https://github.com/hoppscotch/hoppscotch/discussions)
|
||||||
@@ -59,9 +49,9 @@
|
|||||||
|
|
||||||
❤️ **Lightweight:** Crafted with minimalistic UI design.
|
❤️ **Lightweight:** Crafted with minimalistic UI design.
|
||||||
|
|
||||||
⚡️ **Fast:** Send requests and get/copy responses in real-time.
|
⚡️ **Fast:** Send requests and get responses in real time.
|
||||||
|
|
||||||
**HTTP Methods**
|
🗄️ **HTTP Methods:** Request methods define the type of action you are requesting to be performed.
|
||||||
|
|
||||||
- `GET` - Requests retrieve resource information
|
- `GET` - Requests retrieve resource information
|
||||||
- `POST` - The server creates a new entry in a database
|
- `POST` - The server creates a new entry in a database
|
||||||
@@ -74,17 +64,15 @@
|
|||||||
- `TRACE` - Performs a message loop-back test along the path to the target resource
|
- `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.
|
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
|
||||||
|
|
||||||
🌈 **Make it yours:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
|
🌈 **Theming:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
|
||||||
|
|
||||||
**Theming**
|
- Choose a theme: System preference, Light, Dark, and Black
|
||||||
|
- Choose accent colors: Green, Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
|
||||||
- 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
|
- Distraction-free Zen mode
|
||||||
|
|
||||||
_Customized themes are synced with cloud / local session_
|
_Customized themes are synced with your cloud/local session._
|
||||||
|
|
||||||
🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device.
|
🔥 **PWA:** Install as a [Progressive Web App](https://web.dev/progressive-web-apps) on your device.
|
||||||
|
|
||||||
- Instant loading with Service Workers
|
- Instant loading with Service Workers
|
||||||
- Offline support
|
- Offline support
|
||||||
@@ -107,7 +95,7 @@ _Customized themes are synced with cloud / local session_
|
|||||||
|
|
||||||
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
|
📡 **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 SocketIO server.
|
🌩 **Socket.IO:** Send and Receive data with the SocketIO server.
|
||||||
|
|
||||||
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
|
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
|
||||||
|
|
||||||
@@ -127,7 +115,7 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- OAuth 2.0
|
- OAuth 2.0
|
||||||
- OIDC Access Token/PKCE
|
- OIDC Access Token/PKCE
|
||||||
|
|
||||||
📢 **Headers:** Describes the format the body of your request is being sent as.
|
📢 **Headers:** Describes the format the body of your request is being sent in.
|
||||||
|
|
||||||
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
|
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
|
||||||
|
|
||||||
@@ -137,14 +125,14 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- FormData, JSON, and many more
|
- FormData, JSON, and many more
|
||||||
- Toggle between key-value and RAW input parameter list
|
- 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 response to clipboard
|
- Copy the response to the clipboard
|
||||||
- Download response as a file
|
- Download the response as a file
|
||||||
- View response headers
|
- View response headers
|
||||||
- View raw and preview of HTML, image, JSON, XML responses
|
- View raw and preview HTML, image, JSON, and XML responses
|
||||||
|
|
||||||
⏰ **History:** Request entries are synced with cloud / local session storage to restore with a single click.
|
⏰ **History:** Request entries are synced with your cloud/local session storage.
|
||||||
|
|
||||||
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
|
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
|
||||||
|
|
||||||
@@ -152,7 +140,32 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- Nested folders
|
- Nested folders
|
||||||
- Export and import as a file or GitHub gist
|
- Export and import as a file or GitHub gist
|
||||||
|
|
||||||
_Collections are synced with cloud / local session storage_
|
_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)**
|
||||||
|
|
||||||
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
|
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
|
||||||
|
|
||||||
@@ -161,60 +174,31 @@ _Collections are synced with cloud / local session storage_
|
|||||||
- Access APIs served in non-HTTPS (`http://`) endpoints
|
- Access APIs served in non-HTTPS (`http://`) endpoints
|
||||||
- Use your Proxy URL
|
- 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.
|
🌎 **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.
|
||||||
|
|
||||||
📦 **Add-ons:** Official add-ons for hoppscotch.
|
☁️ **Auth + Sync:** Sign in and sync your data in real-time across all your devices.
|
||||||
|
|
||||||
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch
|
**Sign in with:**
|
||||||
- **[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://addons.mozilla.org/en-US/firefox/addon/hoppscotch) | [ **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
|
- GitHub
|
||||||
- Google
|
- Google
|
||||||
- Microsoft
|
- Microsoft
|
||||||
- Email
|
- Email
|
||||||
|
- SSO (Single Sign-On)[^EE]
|
||||||
|
|
||||||
**Synchronize your data**
|
**🔄 Synchronize your data:** Handoff to continue tasks on your other devices.
|
||||||
|
|
||||||
|
- Workspaces
|
||||||
- History
|
- History
|
||||||
- Collections
|
- Collections
|
||||||
- Environments
|
- Environments
|
||||||
- Settings
|
- 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
|
- Check the status code as an integer
|
||||||
- Filter response headers
|
- Filter response headers
|
||||||
@@ -222,7 +206,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
- Set environment variables
|
- Set environment variables
|
||||||
- Write JavaScript code
|
- 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
|
- Unlimited environments and variables
|
||||||
- Initialize through the pre-request script
|
- Initialize through the pre-request script
|
||||||
@@ -241,22 +225,31 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
|
|
||||||
</details>
|
</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.
|
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
|
||||||
|
|
||||||
- Entries are separated by newline
|
- Entries are separated by newline
|
||||||
- Keys and values are separated by `:`
|
- Keys and values are separated by `:`
|
||||||
- Prepend `#` to any row you want to add but keep disabled
|
- Prepend `#` to any row you want to add but keep disabled
|
||||||
|
|
||||||
**For more features, please read our [documentation](https://docs.hoppscotch.io).**
|
🎛️ **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://addons.mozilla.org/en-US/firefox/addon/hoppscotch) | [ **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).**
|
||||||
|
|
||||||
## **Demo**
|
## **Demo**
|
||||||
|
|
||||||
@@ -268,18 +261,9 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
2. Click "Send" to simulate the request
|
2. Click "Send" to simulate the request
|
||||||
3. View the response
|
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**
|
## **Developing**
|
||||||
|
|
||||||
Follow our [self-hosting guide](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
|
Follow our [self-hosting documentation](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
|
||||||
|
|
||||||
## **Contributing**
|
## **Contributing**
|
||||||
|
|
||||||
@@ -297,7 +281,7 @@ See the [`CHANGELOG`](CHANGELOG.md) file for details.
|
|||||||
|
|
||||||
## **Authors**
|
## **Authors**
|
||||||
|
|
||||||
This project exists thanks to all the people who contribute — [contribute](CONTRIBUTING.md).
|
This project owes its existence to the collective efforts of all those who contribute — [contribute now](CONTRIBUTING.md).
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
|
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
|
||||||
@@ -309,4 +293,6 @@ This project exists thanks to all the people who contribute — [contribute](CON
|
|||||||
|
|
||||||
## **License**
|
## **License**
|
||||||
|
|
||||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see the [`LICENSE`](LICENSE) file for details.
|
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).
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
This document outlines security procedures and general policies for the Hoppscotch project.
|
This document outlines security procedures and general policies for the Hoppscotch project.
|
||||||
|
|
||||||
1. [Reporting a security vulnerability](#reporting-a-security-vulnerability)
|
- [Security Policy](#security-policy)
|
||||||
3. [Incident response process](#incident-response-process)
|
- [Reporting a security vulnerability](#reporting-a-security-vulnerability)
|
||||||
|
- [Incident response process](#incident-response-process)
|
||||||
|
|
||||||
## Reporting a security vulnerability
|
## Reporting a security vulnerability
|
||||||
|
|
||||||
|
|||||||
@@ -9,26 +9,24 @@ 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:
|
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).**
|
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
|
||||||
2. **Checkout the `i18n` branch for latest translations.**
|
2. **Checkout the `main` branch for latest translations.**
|
||||||
3. **Create a new branch for your translation with base branch `i18n`.**
|
3. **Create a new branch for your translation with base branch `main`.**
|
||||||
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
|
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.**
|
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.**
|
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).**
|
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 & commit changes.**
|
8. **Save and commit changes.**
|
||||||
9. **Send a pull request.**
|
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._
|
_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.
|
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
|
## Updating a translation
|
||||||
|
|
||||||
### Corrections
|
### 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.
|
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).
|
||||||
|
|
||||||
### Broken links
|
### Broken links
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
- PORT=3170
|
- PORT=3170
|
||||||
volumes:
|
volumes:
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
- ./packages/hoppscotch-backend/:/usr/src/app/packages/hoppscotch-backend
|
||||||
- /usr/src/app/node_modules/
|
- /usr/src/app/node_modules/
|
||||||
depends_on:
|
depends_on:
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
|
|||||||
@@ -32,5 +32,14 @@
|
|||||||
"@types/node": "^17.0.24",
|
"@types/node": "^17.0.24",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"http-server": "^14.1.1"
|
"http-server": "^14.1.1"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"packageExtensions": {
|
||||||
|
"httpsnippet@^3.0.1": {
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "6.12.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.4.8",
|
"version": "2023.8.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"do-test": "pnpm run test"
|
"do-test": "pnpm run test",
|
||||||
|
"seed": "node --loader ts-node/esm prisma/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs-modules/mailer": "^1.8.1",
|
"@nestjs-modules/mailer": "^1.8.1",
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-microsoft": "^1.0.0",
|
"passport-microsoft": "^1.0.0",
|
||||||
|
"pg": "^8.11.3",
|
||||||
"prisma": "^4.16.2",
|
"prisma": "^4.16.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TeamMember_userUid_idx" ON "TeamMember"("userUid");
|
||||||
@@ -26,6 +26,7 @@ model TeamMember {
|
|||||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([teamID, userUid])
|
@@unique([teamID, userUid])
|
||||||
|
@@index([userUid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamInvitation {
|
model TeamInvitation {
|
||||||
|
|||||||
58
packages/hoppscotch-backend/prisma/seed.ts
Normal file
58
packages/hoppscotch-backend/prisma/seed.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { PrismaClient, TeamMemberRole } from '@prisma/client';
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const noOfUsers = 600000;
|
||||||
|
|
||||||
|
const getAllUser = async () => {
|
||||||
|
const users = await prisma.user.findMany();
|
||||||
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUsers = async () => {
|
||||||
|
for (let i = 1; i <= noOfUsers; i++) {
|
||||||
|
try {
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `${i}@gmail.com`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTeams = async () => {
|
||||||
|
const users = await getAllUser();
|
||||||
|
|
||||||
|
for (let i = 0; i < users.length; i++) {
|
||||||
|
try {
|
||||||
|
await prisma.team.create({
|
||||||
|
data: {
|
||||||
|
name: `Team ${i + 1}`,
|
||||||
|
members: {
|
||||||
|
create: {
|
||||||
|
userUid: users[i].uid,
|
||||||
|
role: TeamMemberRole.OWNER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Seeding...');
|
||||||
|
|
||||||
|
await createUsers();
|
||||||
|
await createTeams();
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
console.error(e);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -20,6 +20,7 @@ import { ShortcodeModule } from './shortcode/shortcode.module';
|
|||||||
import { COOKIES_NOT_FOUND } from './errors';
|
import { COOKIES_NOT_FOUND } from './errors';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
|
import { DbModule } from './db/db.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -66,6 +67,7 @@ import { AppController } from './app.controller';
|
|||||||
ttl: +process.env.RATE_LIMIT_TTL,
|
ttl: +process.env.RATE_LIMIT_TTL,
|
||||||
limit: +process.env.RATE_LIMIT_MAX,
|
limit: +process.env.RATE_LIMIT_MAX,
|
||||||
}),
|
}),
|
||||||
|
DbModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
|
|||||||
1
packages/hoppscotch-backend/src/constants.ts
Normal file
1
packages/hoppscotch-backend/src/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const PG_CONNECTION = 'PG_CONNECTION';
|
||||||
21
packages/hoppscotch-backend/src/db/db.module.ts
Normal file
21
packages/hoppscotch-backend/src/db/db.module.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
import { PG_CONNECTION } from 'src/constants';
|
||||||
|
|
||||||
|
const dbProvider = {
|
||||||
|
provide: PG_CONNECTION,
|
||||||
|
useValue: new Pool({
|
||||||
|
user: 'postgres',
|
||||||
|
host: 'hoppscotch-db',
|
||||||
|
database: 'hoppscotch',
|
||||||
|
password: 'testpass',
|
||||||
|
port: 5432,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [dbProvider],
|
||||||
|
exports: [dbProvider],
|
||||||
|
})
|
||||||
|
export class DbModule {}
|
||||||
@@ -7,7 +7,11 @@ export class PrismaService
|
|||||||
implements OnModuleInit, OnModuleDestroy
|
implements OnModuleInit, OnModuleDestroy
|
||||||
{
|
{
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super(
|
||||||
|
{
|
||||||
|
log: ['query', 'info', 'warn', 'error'],
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.$connect();
|
await this.$connect();
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdOn: 'desc',
|
createdOn: 'desc',
|
||||||
},
|
},
|
||||||
skip: 1,
|
skip: args.cursor ? 1 : 0,
|
||||||
take: args.take,
|
take: args.take,
|
||||||
cursor: args.cursor ? { id: args.cursor } : undefined,
|
cursor: args.cursor ? { id: args.cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { throwErr } from 'src/utils';
|
|||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
|
import { cons } from 'fp-ts/lib/ReadonlyNonEmptyArray';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Team)
|
@Resolver(() => Team)
|
||||||
@@ -55,8 +56,13 @@ export class TeamResolver {
|
|||||||
description: 'Returns the list of members of a team',
|
description: 'Returns the list of members of a team',
|
||||||
complexity: 10,
|
complexity: 10,
|
||||||
})
|
})
|
||||||
teamMembers(@Parent() team: Team): Promise<TeamMember[]> {
|
async teamMembers(@Parent() team: Team): Promise<TeamMember[]> {
|
||||||
return this.teamService.getTeamMembers(team.id);
|
const startR = Date.now();
|
||||||
|
const members = await this.teamService.getTeamMembers(team.id);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (teamMembers)', endR - startR, 'ms');
|
||||||
|
|
||||||
|
return members;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => TeamMemberRole, {
|
@ResolveField(() => TeamMemberRole, {
|
||||||
@@ -64,41 +70,61 @@ export class TeamResolver {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard)
|
@UseGuards(GqlAuthGuard)
|
||||||
myRole(
|
async myRole(
|
||||||
@Parent() team: Team,
|
@Parent() team: Team,
|
||||||
@GqlUser() user: AuthUser,
|
@GqlUser() user: AuthUser,
|
||||||
): Promise<TeamMemberRole | null> {
|
): Promise<TeamMemberRole | null> {
|
||||||
return this.teamService.getRoleOfUserInTeam(team.id, user.uid);
|
const startR = Date.now();
|
||||||
|
const role = await this.teamService.getRoleOfUserInTeam(team.id, user.uid);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (myRole)', endR - startR, 'ms');
|
||||||
|
|
||||||
|
return role;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => Int, {
|
@ResolveField(() => Int, {
|
||||||
description: 'The number of users with the OWNER role in the team',
|
description: 'The number of users with the OWNER role in the team',
|
||||||
})
|
})
|
||||||
ownersCount(@Parent() team: Team): Promise<number> {
|
async ownersCount(@Parent() team: Team): Promise<number> {
|
||||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
const startR = Date.now();
|
||||||
|
const count = await this.teamService.getCountOfUsersWithRoleInTeam(
|
||||||
team.id,
|
team.id,
|
||||||
TeamMemberRole.OWNER,
|
TeamMemberRole.OWNER,
|
||||||
);
|
);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (ownersCount)', endR - startR, 'ms');
|
||||||
|
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => Int, {
|
@ResolveField(() => Int, {
|
||||||
description: 'The number of users with the EDITOR role in the team',
|
description: 'The number of users with the EDITOR role in the team',
|
||||||
})
|
})
|
||||||
editorsCount(@Parent() team: Team): Promise<number> {
|
async editorsCount(@Parent() team: Team): Promise<number> {
|
||||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
const startR = Date.now();
|
||||||
|
const count = await this.teamService.getCountOfUsersWithRoleInTeam(
|
||||||
team.id,
|
team.id,
|
||||||
TeamMemberRole.EDITOR,
|
TeamMemberRole.EDITOR,
|
||||||
);
|
);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (editorsCount)', endR - startR, 'ms');
|
||||||
|
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => Int, {
|
@ResolveField(() => Int, {
|
||||||
description: 'The number of users with the VIEWER role in the team',
|
description: 'The number of users with the VIEWER role in the team',
|
||||||
})
|
})
|
||||||
viewersCount(@Parent() team: Team): Promise<number> {
|
async viewersCount(@Parent() team: Team): Promise<number> {
|
||||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
const startR = Date.now();
|
||||||
|
const count = await this.teamService.getCountOfUsersWithRoleInTeam(
|
||||||
team.id,
|
team.id,
|
||||||
TeamMemberRole.VIEWER,
|
TeamMemberRole.VIEWER,
|
||||||
);
|
);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (viewersCount)', endR - startR, 'ms');
|
||||||
|
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query
|
// Query
|
||||||
@@ -106,7 +132,7 @@ export class TeamResolver {
|
|||||||
description: 'List of teams that the executing user belongs to.',
|
description: 'List of teams that the executing user belongs to.',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard)
|
@UseGuards(GqlAuthGuard)
|
||||||
myTeams(
|
async myTeams(
|
||||||
@GqlUser() user: AuthUser,
|
@GqlUser() user: AuthUser,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'cursor',
|
name: 'cursor',
|
||||||
@@ -117,7 +143,15 @@ export class TeamResolver {
|
|||||||
})
|
})
|
||||||
cursor?: string,
|
cursor?: string,
|
||||||
): Promise<Team[]> {
|
): Promise<Team[]> {
|
||||||
return this.teamService.getTeamsOfUser(user.uid, cursor ?? null);
|
const startR = Date.now();
|
||||||
|
const teams = await this.teamService.getTeamsOfUser(
|
||||||
|
user.uid,
|
||||||
|
cursor ?? null,
|
||||||
|
);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (myTeams)', endR - startR, 'ms');
|
||||||
|
|
||||||
|
return teams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => Team, {
|
@Query(() => Team, {
|
||||||
@@ -130,7 +164,7 @@ export class TeamResolver {
|
|||||||
TeamMemberRole.EDITOR,
|
TeamMemberRole.EDITOR,
|
||||||
TeamMemberRole.OWNER,
|
TeamMemberRole.OWNER,
|
||||||
)
|
)
|
||||||
team(
|
async team(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'teamID',
|
name: 'teamID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -138,7 +172,12 @@ export class TeamResolver {
|
|||||||
})
|
})
|
||||||
teamID: string,
|
teamID: string,
|
||||||
): Promise<Team | null> {
|
): Promise<Team | null> {
|
||||||
return this.teamService.getTeamWithID(teamID);
|
const startR = Date.now();
|
||||||
|
const team = await this.teamService.getTeamWithID(teamID);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (team)', endR - startR, 'ms');
|
||||||
|
|
||||||
|
return team;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutation
|
// Mutation
|
||||||
@@ -151,7 +190,11 @@ export class TeamResolver {
|
|||||||
@Args({ name: 'name', description: 'Displayed name of the team' })
|
@Args({ name: 'name', description: 'Displayed name of the team' })
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<Team> {
|
): Promise<Team> {
|
||||||
|
const startR = Date.now();
|
||||||
const team = await this.teamService.createTeam(name, user.uid);
|
const team = await this.teamService.createTeam(name, user.uid);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (createTeam)', endR - startR, 'ms');
|
||||||
|
|
||||||
if (E.isLeft(team)) throwErr(team.left);
|
if (E.isLeft(team)) throwErr(team.left);
|
||||||
return team.right;
|
return team.right;
|
||||||
}
|
}
|
||||||
@@ -169,7 +212,11 @@ export class TeamResolver {
|
|||||||
})
|
})
|
||||||
teamID: string,
|
teamID: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const startR = Date.now();
|
||||||
const isUserLeft = await this.teamService.leaveTeam(teamID, user.uid);
|
const isUserLeft = await this.teamService.leaveTeam(teamID, user.uid);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (leaveTeam)', endR - startR, 'ms');
|
||||||
|
|
||||||
if (E.isLeft(isUserLeft)) throwErr(isUserLeft.left);
|
if (E.isLeft(isUserLeft)) throwErr(isUserLeft.left);
|
||||||
return isUserLeft.right;
|
return isUserLeft.right;
|
||||||
}
|
}
|
||||||
@@ -194,7 +241,11 @@ export class TeamResolver {
|
|||||||
})
|
})
|
||||||
userUid: string,
|
userUid: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const startR = Date.now();
|
||||||
const isRemoved = await this.teamService.leaveTeam(teamID, userUid);
|
const isRemoved = await this.teamService.leaveTeam(teamID, userUid);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (removeTeamMember)', endR - startR, 'ms');
|
||||||
|
|
||||||
if (E.isLeft(isRemoved)) throwErr(isRemoved.left);
|
if (E.isLeft(isRemoved)) throwErr(isRemoved.left);
|
||||||
return isRemoved.right;
|
return isRemoved.right;
|
||||||
}
|
}
|
||||||
@@ -210,7 +261,11 @@ export class TeamResolver {
|
|||||||
@Args({ name: 'newName', description: 'The updated name of the team' })
|
@Args({ name: 'newName', description: 'The updated name of the team' })
|
||||||
newName: string,
|
newName: string,
|
||||||
): Promise<Team> {
|
): Promise<Team> {
|
||||||
|
const startR = Date.now();
|
||||||
const team = await this.teamService.renameTeam(teamID, newName);
|
const team = await this.teamService.renameTeam(teamID, newName);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (renameTeam)', endR - startR, 'ms');
|
||||||
|
|
||||||
if (E.isLeft(team)) throwErr(team.left);
|
if (E.isLeft(team)) throwErr(team.left);
|
||||||
return team.right;
|
return team.right;
|
||||||
}
|
}
|
||||||
@@ -224,7 +279,11 @@ export class TeamResolver {
|
|||||||
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
|
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
|
||||||
teamID: string,
|
teamID: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const startR = Date.now();
|
||||||
const isDeleted = await this.teamService.deleteTeam(teamID);
|
const isDeleted = await this.teamService.deleteTeam(teamID);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log('response generation: (deleteTeam)', endR - startR, 'ms');
|
||||||
|
|
||||||
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
|
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
|
||||||
return isDeleted.right;
|
return isDeleted.right;
|
||||||
}
|
}
|
||||||
@@ -254,11 +313,19 @@ export class TeamResolver {
|
|||||||
})
|
})
|
||||||
newRole: TeamMemberRole,
|
newRole: TeamMemberRole,
|
||||||
): Promise<TeamMember> {
|
): Promise<TeamMember> {
|
||||||
|
const startR = Date.now();
|
||||||
const teamMember = await this.teamService.updateTeamMemberRole(
|
const teamMember = await this.teamService.updateTeamMemberRole(
|
||||||
teamID,
|
teamID,
|
||||||
userUid,
|
userUid,
|
||||||
newRole,
|
newRole,
|
||||||
);
|
);
|
||||||
|
const endR = Date.now();
|
||||||
|
console.log(
|
||||||
|
'response generation: (updateTeamMemberRole)',
|
||||||
|
endR - startR,
|
||||||
|
'ms',
|
||||||
|
);
|
||||||
|
|
||||||
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||||
return teamMember.right;
|
return teamMember.right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { TeamMember, TeamMemberRole, Team } from './team.model';
|
import { TeamMember, TeamMemberRole, Team } from './team.model';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { TeamMember as DbTeamMember } from '@prisma/client';
|
import { TeamMember as DbTeamMember } from '@prisma/client';
|
||||||
@@ -23,6 +23,8 @@ import * as T from 'fp-ts/Task';
|
|||||||
import * as A from 'fp-ts/Array';
|
import * as A from 'fp-ts/Array';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { AuthUser } from '../types/AuthUser';
|
import { AuthUser } from '../types/AuthUser';
|
||||||
|
import { PG_CONNECTION } from 'src/constants';
|
||||||
|
import { Client } from 'pg';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamService implements UserDataHandler, OnModuleInit {
|
export class TeamService implements UserDataHandler, OnModuleInit {
|
||||||
@@ -30,8 +32,11 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
|
@Inject(PG_CONNECTION) private conn: Client,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
enableRawSql: boolean = false;
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
this.userService.registerUserDataHandler(this);
|
this.userService.registerUserDataHandler(this);
|
||||||
}
|
}
|
||||||
@@ -52,12 +57,37 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
teamID: string,
|
teamID: string,
|
||||||
role: TeamMemberRole,
|
role: TeamMemberRole,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
return await this.prisma.teamMember.count({
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
const count = await this.conn.query(
|
||||||
|
`SELECT COUNT(*) FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "role" = '${role}';`,
|
||||||
|
);
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getCountOfUsersWithRoleInTeam >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms >>>>>',
|
||||||
|
count.rows,
|
||||||
|
);
|
||||||
|
return count.rows[0].count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startQ = Date.now();
|
||||||
|
const count = await this.prisma.teamMember.count({
|
||||||
where: {
|
where: {
|
||||||
teamID,
|
teamID,
|
||||||
role,
|
role,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getCountOfUsersWithRoleInTeam >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms >>>>>',
|
||||||
|
count,
|
||||||
|
);
|
||||||
|
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMemberToTeamWithEmail(
|
async addMemberToTeamWithEmail(
|
||||||
@@ -77,6 +107,11 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
uid: string,
|
uid: string,
|
||||||
role: TeamMemberRole,
|
role: TeamMemberRole,
|
||||||
): Promise<TeamMember> {
|
): Promise<TeamMember> {
|
||||||
|
const tm = await this.conn.query(
|
||||||
|
`INSERT INTO "TeamMember" (id, userUid, teamID, role) VALUES ('${new Date().toISOString()}', '${uid}', '${teamID}', '${role}') RETURNING *;`,
|
||||||
|
);
|
||||||
|
console.log('addMemberToTeam >>>>>>>>>>', tm.rows[0]);
|
||||||
|
|
||||||
const teamMember = await this.prisma.teamMember.create({
|
const teamMember = await this.prisma.teamMember.create({
|
||||||
data: {
|
data: {
|
||||||
userUid: uid,
|
userUid: uid,
|
||||||
@@ -101,6 +136,31 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteTeam(teamID: string): Promise<E.Left<string> | E.Right<boolean>> {
|
async deleteTeam(teamID: string): Promise<E.Left<string> | E.Right<boolean>> {
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
const t = await this.conn.query(
|
||||||
|
`SELECT * FROM "Team" WHERE "id" = '${teamID}'`,
|
||||||
|
);
|
||||||
|
if (t.rows.length === 0) return E.left(TEAM_INVALID_ID);
|
||||||
|
|
||||||
|
await this.conn.query(
|
||||||
|
`DELETE FROM "TeamMember" WHERE "teamID" = '${teamID}' RETURNING *`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.conn.query(`DELETE FROM "Team" WHERE "id" = '${teamID}'`);
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'deleteTeam >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
t.rows[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startQ = Date.now();
|
||||||
const team = await this.prisma.team.findUnique({
|
const team = await this.prisma.team.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: teamID,
|
id: teamID,
|
||||||
@@ -119,6 +179,8 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
id: teamID,
|
id: teamID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log('deleteTeam >>>>>>>>>>', endQ - startQ, 'ms', '>>>>>', team);
|
||||||
|
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
}
|
}
|
||||||
@@ -135,6 +197,26 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
const isValidTitle = this.validateTeamName(newName);
|
const isValidTitle = this.validateTeamName(newName);
|
||||||
if (E.isLeft(isValidTitle)) return isValidTitle;
|
if (E.isLeft(isValidTitle)) return isValidTitle;
|
||||||
|
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
const ut = await this.conn.query(
|
||||||
|
`UPDATE "Team" SET "name" = '${newName}' WHERE "id" = '${teamID}' RETURNING *`,
|
||||||
|
);
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'renameTeam >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
ut.rows[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(<Team>{
|
||||||
|
id: ut.rows[0].id,
|
||||||
|
name: ut.rows[0].name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updatedTeam = await this.prisma.team.update({
|
const updatedTeam = await this.prisma.team.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -156,6 +238,48 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
userUid: string,
|
userUid: string,
|
||||||
newRole: TeamMemberRole,
|
newRole: TeamMemberRole,
|
||||||
): Promise<E.Left<string> | E.Right<TeamMember>> {
|
): Promise<E.Left<string> | E.Right<TeamMember>> {
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
const oc = await this.conn.query(
|
||||||
|
`SELECT COUNT(*) FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "role" = '${TeamMemberRole.OWNER}';`,
|
||||||
|
);
|
||||||
|
const tm = await this.conn.query(
|
||||||
|
`SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "userUid" = '${userUid}'`,
|
||||||
|
);
|
||||||
|
if (tm.rows.length === 0) return E.left(TEAM_MEMBER_NOT_FOUND);
|
||||||
|
|
||||||
|
const ownerCount = oc.rows[0].count;
|
||||||
|
if (
|
||||||
|
tm.rows[0].role === TeamMemberRole.OWNER &&
|
||||||
|
newRole != TeamMemberRole.OWNER &&
|
||||||
|
ownerCount === 1
|
||||||
|
) {
|
||||||
|
return E.left(TEAM_ONLY_ONE_OWNER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const utm = await this.conn.query(
|
||||||
|
`UPDATE "teamMember" SET "role" = '${newRole}' WHERE "teamID" = '${teamID}' AND "userUid" = '${userUid}'`,
|
||||||
|
);
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'updateTeamMemberRole >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
utm.rows[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedMember: TeamMember = {
|
||||||
|
membershipID: utm.rows[0].id,
|
||||||
|
userUid: utm.rows[0].userUid,
|
||||||
|
role: TeamMemberRole[utm.rows[0].role],
|
||||||
|
};
|
||||||
|
this.pubsub.publish(`team/${teamID}/member_updated`, updatedMember);
|
||||||
|
|
||||||
|
return E.right(updatedMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startQ = Date.now();
|
||||||
const ownerCount = await this.prisma.teamMember.count({
|
const ownerCount = await this.prisma.teamMember.count({
|
||||||
where: {
|
where: {
|
||||||
teamID,
|
teamID,
|
||||||
@@ -192,6 +316,14 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
role: newRole,
|
role: newRole,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'updateTeamMemberRole >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
|
||||||
const updatedMember: TeamMember = {
|
const updatedMember: TeamMember = {
|
||||||
membershipID: result.id,
|
membershipID: result.id,
|
||||||
@@ -208,6 +340,30 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
teamID: string,
|
teamID: string,
|
||||||
userUid: string,
|
userUid: string,
|
||||||
): Promise<E.Left<string> | E.Right<boolean>> {
|
): Promise<E.Left<string> | E.Right<boolean>> {
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const oc = await this.conn.query(
|
||||||
|
`SELECT COUNT(*) FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "role" = '${TeamMemberRole.OWNER}';`,
|
||||||
|
);
|
||||||
|
const ownerCount = oc.rows[0].count;
|
||||||
|
console.log('leaveTeam >>>>>>>>>>', oc.rows);
|
||||||
|
|
||||||
|
const member = await this.getTeamMember(teamID, userUid);
|
||||||
|
if (!member) return E.left(TEAM_INVALID_ID_OR_USER);
|
||||||
|
|
||||||
|
if (ownerCount === 1 && member.role === TeamMemberRole.OWNER) {
|
||||||
|
return E.left(TEAM_ONLY_ONE_OWNER);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dtm = await this.conn.query(
|
||||||
|
`DELETE FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "userUid" = '${userUid}'`,
|
||||||
|
);
|
||||||
|
console.log('leaveTeam >>>>>>>>>>', dtm);
|
||||||
|
|
||||||
|
this.pubsub.publish(`team/${teamID}/member_removed`, userUid);
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
}
|
||||||
|
|
||||||
const ownerCount = await this.prisma.teamMember.count({
|
const ownerCount = await this.prisma.teamMember.count({
|
||||||
where: {
|
where: {
|
||||||
teamID,
|
teamID,
|
||||||
@@ -248,6 +404,34 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
const isValidName = this.validateTeamName(name);
|
const isValidName = this.validateTeamName(name);
|
||||||
if (E.isLeft(isValidName)) return isValidName;
|
if (E.isLeft(isValidName)) return isValidName;
|
||||||
|
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
const t = await this.conn.query(
|
||||||
|
`INSERT INTO "Team" (id, name) VALUES ('${new Date().toISOString()}', '${name}') RETURNING *`,
|
||||||
|
);
|
||||||
|
const tm = await this.conn.query(
|
||||||
|
`INSERT INTO "TeamMember" ("id", "userUid", "teamID", "role") VALUES ('${new Date().toISOString()}', '${creatorUid}' , '${
|
||||||
|
t.rows[0].id
|
||||||
|
}', '${TeamMemberRole.OWNER}') RETURNING *`,
|
||||||
|
);
|
||||||
|
const endQ = Date.now();
|
||||||
|
``;
|
||||||
|
console.log(
|
||||||
|
'createTeam >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
t.rows[0],
|
||||||
|
tm.rows[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(<Team>{
|
||||||
|
id: t.rows[0].id,
|
||||||
|
name: t.rows[0].name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const startQ = Date.now();
|
||||||
const team = await this.prisma.team.create({
|
const team = await this.prisma.team.create({
|
||||||
data: {
|
data: {
|
||||||
name: name,
|
name: name,
|
||||||
@@ -259,12 +443,42 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log('createTeam >>>>>>>>>> ', endQ - startQ, 'ms', '>>>>>', team);
|
||||||
|
|
||||||
return E.right(team);
|
return E.right(team);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTeamsOfUser(uid: string, cursor: string | null): Promise<Team[]> {
|
async getTeamsOfUser(uid: string, cursor: string | null): Promise<Team[]> {
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
let users;
|
||||||
|
if (cursor) {
|
||||||
|
users = await this.conn.query(
|
||||||
|
`SELECT * FROM "TeamMember" LEFT JOIN "Team" ON "TeamMember"."teamID" = "Team"."id" WHERE "TeamMember"."userUid" = '${uid}' and "TeamMember"."teamID" > '${cursor}' LIMIT 10`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
users = await this.conn.query(
|
||||||
|
`SELECT * FROM "TeamMember" LEFT JOIN "Team" ON "TeamMember"."teamID" = "Team"."id" WHERE "TeamMember"."userUid" = '${uid}' LIMIT 10`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getTeamsOfUser >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
users.rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
return users.rows.map((entry) => ({
|
||||||
|
id: entry.teamID,
|
||||||
|
name: entry.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
|
const startQ = Date.now();
|
||||||
const entries = await this.prisma.teamMember.findMany({
|
const entries = await this.prisma.teamMember.findMany({
|
||||||
take: 10,
|
take: 10,
|
||||||
where: {
|
where: {
|
||||||
@@ -274,9 +488,18 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
team: true,
|
team: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getTeamsOfUser >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
entries,
|
||||||
|
);
|
||||||
|
|
||||||
return entries.map((entry) => entry.team);
|
return entries.map((entry) => entry.team);
|
||||||
} else {
|
} else {
|
||||||
|
const startQ = Date.now();
|
||||||
const entries = await this.prisma.teamMember.findMany({
|
const entries = await this.prisma.teamMember.findMany({
|
||||||
take: 10,
|
take: 10,
|
||||||
skip: 1,
|
skip: 1,
|
||||||
@@ -293,17 +516,56 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
team: true,
|
team: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getTeamsOfUser >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
entries,
|
||||||
|
);
|
||||||
|
|
||||||
return entries.map((entry) => entry.team);
|
return entries.map((entry) => entry.team);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTeamWithID(teamID: string): Promise<Team | null> {
|
async getTeamWithID(teamID: string): Promise<Team | null> {
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
const team = await this.conn.query(
|
||||||
|
`SELECT * FROM "Team" WHERE "id" = '${teamID}'`,
|
||||||
|
);
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getTeamWithID >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
team.rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (team.rows.length === 0) return null;
|
||||||
|
return <Team>{
|
||||||
|
id: team.rows[0].id,
|
||||||
|
name: team.rows[0].name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const startQ = Date.now();
|
||||||
const team = await this.prisma.team.findUnique({
|
const team = await this.prisma.team.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: teamID,
|
id: teamID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getTeamWithID >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
team,
|
||||||
|
);
|
||||||
|
|
||||||
return team;
|
return team;
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
@@ -353,7 +615,30 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
teamID: string,
|
teamID: string,
|
||||||
userUid: string,
|
userUid: string,
|
||||||
): Promise<TeamMember | null> {
|
): Promise<TeamMember | null> {
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
const member = await this.conn.query(
|
||||||
|
`SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "userUid" = '${userUid}'`,
|
||||||
|
);
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getTeamMember >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
member.rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (member.rows.length === 0) return null;
|
||||||
|
return <TeamMember>{
|
||||||
|
membershipID: member.rows[0].id,
|
||||||
|
userUid: member.rows[0].userUid,
|
||||||
|
role: TeamMemberRole[member.rows[0].role],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const startQ = Date.now();
|
||||||
const teamMember = await this.prisma.teamMember.findUnique({
|
const teamMember = await this.prisma.teamMember.findUnique({
|
||||||
where: {
|
where: {
|
||||||
teamID_userUid: {
|
teamID_userUid: {
|
||||||
@@ -362,6 +647,14 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getTeamMember >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
teamMember,
|
||||||
|
);
|
||||||
|
|
||||||
if (!teamMember) return null;
|
if (!teamMember) return null;
|
||||||
|
|
||||||
@@ -433,11 +726,44 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTeamMembers(teamID: string): Promise<TeamMember[]> {
|
async getTeamMembers(teamID: string): Promise<TeamMember[]> {
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
const members = await this.conn.query(
|
||||||
|
`SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}'`,
|
||||||
|
);
|
||||||
|
const endQ = Date.now();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'getTeamMembers >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
members.rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
return members.rows.map((entry) => {
|
||||||
|
return {
|
||||||
|
membershipID: entry.id,
|
||||||
|
userUid: entry.userUid,
|
||||||
|
role: TeamMemberRole[entry.role],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const startQ = Date.now();
|
||||||
const dbTeamMembers = await this.prisma.teamMember.findMany({
|
const dbTeamMembers = await this.prisma.teamMember.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamID,
|
teamID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getTeamMembers >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
dbTeamMembers,
|
||||||
|
);
|
||||||
|
|
||||||
const members = dbTeamMembers.map(
|
const members = dbTeamMembers.map(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
@@ -470,8 +796,39 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
teamID: string,
|
teamID: string,
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
): Promise<TeamMember[]> {
|
): Promise<TeamMember[]> {
|
||||||
|
if (this.enableRawSql) {
|
||||||
|
const startQ = Date.now();
|
||||||
|
let members;
|
||||||
|
if (cursor) {
|
||||||
|
members = await this.conn.query(
|
||||||
|
`SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "id" > '${cursor}' LIMIT 10`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
members = await this.conn.query(
|
||||||
|
`SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}' LIMIT 10`,
|
||||||
|
);
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getMembersOfTeam >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
members.rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
return members.rows.map(
|
||||||
|
(entry) =>
|
||||||
|
<TeamMember>{
|
||||||
|
membershipID: entry.id,
|
||||||
|
userUid: entry.userUid,
|
||||||
|
role: TeamMemberRole[entry.role],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let teamMembers: DbTeamMember[];
|
let teamMembers: DbTeamMember[];
|
||||||
|
|
||||||
|
const startQ = Date.now();
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
teamMembers = await this.prisma.teamMember.findMany({
|
teamMembers = await this.prisma.teamMember.findMany({
|
||||||
take: 10,
|
take: 10,
|
||||||
@@ -491,6 +848,14 @@ export class TeamService implements UserDataHandler, OnModuleInit {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const endQ = Date.now();
|
||||||
|
console.log(
|
||||||
|
'getMembersOfTeam >>>>>>>>>>',
|
||||||
|
endQ - startQ,
|
||||||
|
'ms',
|
||||||
|
'>>>>>',
|
||||||
|
teamMembers,
|
||||||
|
);
|
||||||
|
|
||||||
const members = teamMembers.map(
|
const members = teamMembers.map(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,29 +1,19 @@
|
|||||||
<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>
|
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
|
||||||
|
|
||||||
</div>
|
A CLI to run Hoppscotch Test Scripts in CI environments.
|
||||||
|
|
||||||
A CLI to run Hoppscotch test scripts in CI environments.
|
|
||||||
|
|
||||||
### **Commands:**
|
### **Commands:**
|
||||||
|
|
||||||
- `hopp test [options] [file]`: testing hoppscotch collection.json file
|
- `hopp test [options] [file]`: testing hoppscotch collection.json file
|
||||||
|
|
||||||
### **Usage:**
|
### **Usage:**
|
||||||
```
|
|
||||||
|
```bash
|
||||||
hopp [options or commands] arguments
|
hopp [options or commands] arguments
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Options:**
|
### **Options:**
|
||||||
|
|
||||||
- `-v`, `--ver`: see the current version of the CLI
|
- `-v`, `--ver`: see the current version of the CLI
|
||||||
- `-h`, `--help`: display help for command
|
- `-h`, `--help`: display help for command
|
||||||
|
|
||||||
@@ -45,17 +35,21 @@ hopp [options or commands] arguments
|
|||||||
- Executes and outputs test-script response.
|
- Executes and outputs test-script response.
|
||||||
|
|
||||||
#### Options:
|
#### Options:
|
||||||
|
|
||||||
##### `-e <file_path>` / `--env <file_path>`
|
##### `-e <file_path>` / `--env <file_path>`
|
||||||
|
|
||||||
- Accepts path to env.json with contents in below format:
|
- Accepts path to env.json with contents in below format:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ENV1":"value1",
|
"ENV1":"value1",
|
||||||
"ENV2":"value2"
|
"ENV2":"value2"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- You can now access those variables using `pw.env.get('<var_name>')`
|
- 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
|
## Install
|
||||||
|
|
||||||
@@ -75,4 +69,59 @@ npm i -g @hoppscotch/cli
|
|||||||
|
|
||||||
## **Contributing:**
|
## **Contributing:**
|
||||||
|
|
||||||
To get started contributing to the repository, please read **[CONTRIBUTING.md](./CONTRIBUTING.md)**
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -653,11 +653,12 @@
|
|||||||
"chat": "Chat with support",
|
"chat": "Chat with support",
|
||||||
"open_docs": "Read Documentation",
|
"open_docs": "Read Documentation",
|
||||||
"open_keybindings": "Keyboard shortcuts",
|
"open_keybindings": "Keyboard shortcuts",
|
||||||
|
"open_github": "Open GitHub repository",
|
||||||
"social": "Social",
|
"social": "Social",
|
||||||
"title": "General"
|
"title": "General"
|
||||||
},
|
},
|
||||||
"miscellaneous": {
|
"miscellaneous": {
|
||||||
"invite": "Invite people to Hoppscotch",
|
"invite": "Invite your friends to Hoppscotch",
|
||||||
"title": "Miscellaneous"
|
"title": "Miscellaneous"
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
@@ -669,7 +670,13 @@
|
|||||||
"tab_headers": "Headers tab",
|
"tab_headers": "Headers tab",
|
||||||
"tab_authorization": "Authorization tab",
|
"tab_authorization": "Authorization tab",
|
||||||
"tab_pre_request_script": "Pre-request script tab",
|
"tab_pre_request_script": "Pre-request script tab",
|
||||||
"tab_tests": "Tests tab"
|
"tab_tests": "Tests tab",
|
||||||
|
"tab_query": "Query tab",
|
||||||
|
"tab_variables": "Variables tab"
|
||||||
|
},
|
||||||
|
"graphql": {
|
||||||
|
"connect": "Connect to server",
|
||||||
|
"disconnect": "Disconnect from server"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"copy": "Copy response",
|
"copy": "Copy response",
|
||||||
@@ -679,25 +686,25 @@
|
|||||||
"environments": {
|
"environments": {
|
||||||
"new": "Create new environment",
|
"new": "Create new environment",
|
||||||
"new_variable": "Create a new environment variable",
|
"new_variable": "Create a new environment variable",
|
||||||
"edit": "Edit selected environment",
|
"edit": "Edit current environment",
|
||||||
"delete": "Delete selected environment",
|
"delete": "Delete current environment",
|
||||||
"duplicate": "Duplicate selected environment",
|
"duplicate": "Duplicate current environment",
|
||||||
"edit_global": "Edit global environment",
|
"edit_global": "Edit global environment",
|
||||||
"duplicate_global": "Duplicate global environment",
|
"duplicate_global": "Duplicate global environment",
|
||||||
"title": "Environments"
|
"title": "Environments"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"new": "Create new team",
|
"new": "Create new team",
|
||||||
"edit": "Edit selected team",
|
"edit": "Edit current team",
|
||||||
"delete": "Delete selected team",
|
"delete": "Delete current team",
|
||||||
"invite": "Invite people to team",
|
"invite": "Invite people to team",
|
||||||
"switch_to_personal": "Switch to personal workspace",
|
"switch_to_personal": "Switch to your personal workspace",
|
||||||
"title": "Teams"
|
"title": "Teams"
|
||||||
},
|
},
|
||||||
"tab": {
|
"tab": {
|
||||||
"duplicate": "Duplicate tab",
|
"duplicate": "Duplicate current tab",
|
||||||
"close_current": "Close current tab",
|
"close_current": "Close current tab",
|
||||||
"close_others": "Close other tabs",
|
"close_others": "Close all other tabs",
|
||||||
"new_tab": "Open a new tab",
|
"new_tab": "Open a new tab",
|
||||||
"title": "Tabs"
|
"title": "Tabs"
|
||||||
},
|
},
|
||||||
@@ -710,15 +717,15 @@
|
|||||||
"change_language": "Change Language",
|
"change_language": "Change Language",
|
||||||
"settings": {
|
"settings": {
|
||||||
"theme": {
|
"theme": {
|
||||||
"black": "Black Mode",
|
"black": "Black",
|
||||||
"dark": "Dark Mode",
|
"dark": "Dark",
|
||||||
"light": "Light Mode",
|
"light": "Light",
|
||||||
"system": "System Mode"
|
"system": "System preference"
|
||||||
},
|
},
|
||||||
"font": {
|
"font": {
|
||||||
"size_sm": "Change to Small",
|
"size_sm": "Small",
|
||||||
"size_md": "Change to Medium",
|
"size_md": "Medium",
|
||||||
"size_lg": "Change to Large"
|
"size_lg": "Large"
|
||||||
},
|
},
|
||||||
"change_interceptor": "Change Interceptor",
|
"change_interceptor": "Change Interceptor",
|
||||||
"change_language": "Change Language"
|
"change_language": "Change Language"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.8",
|
"version": "2023.8.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"graphql": "^16.8.0",
|
"graphql": "^16.8.0",
|
||||||
"graphql-language-service-interface": "^2.9.1",
|
"graphql-language-service-interface": "^2.9.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"httpsnippet": "^2.0.0",
|
"httpsnippet": "^3.0.1",
|
||||||
"insomnia-importers": "^3.6.0",
|
"insomnia-importers": "^3.6.0",
|
||||||
"io-ts": "^2.2.20",
|
"io-ts": "^2.2.20",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
@@ -117,6 +117,7 @@
|
|||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "^2.1.1",
|
||||||
"@rushstack/eslint-patch": "^1.3.3",
|
"@rushstack/eslint-patch": "^1.3.3",
|
||||||
|
"@types/har-format": "^1.2.12",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/lodash-es": "^4.17.8",
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@types/lossless-json": "^1.0.1",
|
"@types/lossless-json": "^1.0.1",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 666 KiB After Width: | Height: | Size: 926 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 510 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 535 KiB |
@@ -18,6 +18,7 @@ import { HOPP_MODULES } from "@modules/."
|
|||||||
import { isLoadingInitialRoute } from "@modules/router"
|
import { isLoadingInitialRoute } from "@modules/router"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { APP_IS_IN_DEV_MODE } from "@helpers/dev"
|
import { APP_IS_IN_DEV_MODE } from "@helpers/dev"
|
||||||
|
import { platform } from "./platform"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -45,4 +46,5 @@ if (APP_IS_IN_DEV_MODE) {
|
|||||||
|
|
||||||
// Run module root component setup code
|
// Run module root component setup code
|
||||||
HOPP_MODULES.forEach((mod) => mod.onRootSetup?.())
|
HOPP_MODULES.forEach((mod) => mod.onRootSetup?.())
|
||||||
|
platform.addedHoppModules?.forEach((mod) => mod.onRootSetup?.())
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { pipe } from "fp-ts/function"
|
|||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
||||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
import { showChat } from "~/modules/crisp"
|
|
||||||
import { useToast } from "~/composables/toast"
|
import { useToast } from "~/composables/toast"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
@@ -62,10 +61,6 @@ defineActionHandler("modals.login.toggle", () => {
|
|||||||
showLogin.value = !showLogin.value
|
showLogin.value = !showLogin.value
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("flyouts.chat.open", () => {
|
|
||||||
showChat()
|
|
||||||
})
|
|
||||||
|
|
||||||
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
||||||
teamID.value = teamId
|
teamID.value = teamId
|
||||||
confirmRemove.value = true
|
confirmRemove.value = true
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<!--
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="chat"
|
ref="chat"
|
||||||
:icon="IconMessageCircle"
|
:icon="IconMessageCircle"
|
||||||
@@ -88,20 +89,34 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
-->
|
||||||
:icon="IconGift"
|
<template
|
||||||
:label="`${t('app.whats_new')}`"
|
v-for="footerItem in platform.ui?.additionalFooterMenuItems"
|
||||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
:key="footerItem.id"
|
||||||
blank
|
>
|
||||||
@click="hide()"
|
<template v-if="footerItem.action.type === 'link'">
|
||||||
/>
|
<HoppSmartItem
|
||||||
<HoppSmartItem
|
:icon="footerItem.icon"
|
||||||
:icon="IconActivity"
|
:label="footerItem.text(t)"
|
||||||
:label="t('app.status')"
|
:to="footerItem.action.href"
|
||||||
to="https://status.hoppscotch.io"
|
blank
|
||||||
blank
|
@click="hide()"
|
||||||
@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>
|
||||||
<hr />
|
<hr />
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
:icon="IconGithub"
|
:icon="IconGithub"
|
||||||
@@ -207,15 +222,11 @@ import IconColumns from "~icons/lucide/columns"
|
|||||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
||||||
import IconShieldCheck from "~icons/lucide/shield-check"
|
import IconShieldCheck from "~icons/lucide/shield-check"
|
||||||
import IconBook from "~icons/lucide/book"
|
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 IconGithub from "~icons/lucide/github"
|
||||||
import IconTwitter from "~icons/lucide/twitter"
|
import IconTwitter from "~icons/lucide/twitter"
|
||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
import IconUserPlus from "~icons/lucide/user-plus"
|
||||||
import IconLock from "~icons/lucide/lock"
|
import IconLock from "~icons/lucide/lock"
|
||||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||||
import { showChat } from "@modules/crisp"
|
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
@@ -262,10 +273,6 @@ const nativeShare = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatWithUs = () => {
|
|
||||||
showChat()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showDeveloperOptionModal = () => {
|
const showDeveloperOptionModal = () => {
|
||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
showDeveloperOptions.value = true
|
showDeveloperOptions.value = true
|
||||||
|
|||||||
@@ -30,105 +30,37 @@
|
|||||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
||||||
{{ t("support.title") }}
|
{{ t("support.title") }}
|
||||||
</h2>
|
</h2>
|
||||||
<HoppSmartItem
|
<template
|
||||||
:icon="IconBook"
|
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
|
||||||
:label="t('app.documentation')"
|
:key="item.id"
|
||||||
to="https://docs.hoppscotch.io"
|
>
|
||||||
:description="t('support.documentation')"
|
<HoppSmartItem
|
||||||
:info-icon="IconChevronRight"
|
v-if="item.action.type === 'link'"
|
||||||
active
|
:icon="item.icon"
|
||||||
blank
|
:label="item.text(t)"
|
||||||
@click="hideModal()"
|
:to="item.action.href"
|
||||||
/>
|
:description="item.subtitle(t)"
|
||||||
<HoppSmartItem
|
:info-icon="IconChevronRight"
|
||||||
:icon="IconGift"
|
active
|
||||||
:label="t('app.whats_new')"
|
blank
|
||||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
@click="hideModal()"
|
||||||
:description="t('support.changelog')"
|
/>
|
||||||
:info-icon="IconChevronRight"
|
<HoppSmartItem
|
||||||
active
|
v-else
|
||||||
blank
|
:icon="item.icon"
|
||||||
@click="hideModal()"
|
:label="item.text(t)"
|
||||||
/>
|
:description="item.subtitle(t)"
|
||||||
<HoppSmartItem
|
:info-icon="IconChevronRight"
|
||||||
:icon="IconActivity"
|
active
|
||||||
:label="t('app.status')"
|
@click="
|
||||||
to="https://status.hoppscotch.io"
|
() => {
|
||||||
blank
|
// @ts-expect-error Typescript isn't able to understand
|
||||||
:description="t('app.status_description')"
|
item.action.do()
|
||||||
:info-icon="IconChevronRight"
|
hideModal()
|
||||||
active
|
}
|
||||||
@click="hideModal()"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
</template>
|
||||||
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartModal>
|
</HoppSmartModal>
|
||||||
@@ -138,24 +70,12 @@
|
|||||||
import { watch } from "vue"
|
import { watch } from "vue"
|
||||||
import IconSidebar from "~icons/lucide/sidebar"
|
import IconSidebar from "~icons/lucide/sidebar"
|
||||||
import IconSidebarOpen from "~icons/lucide/sidebar-open"
|
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 IconChevronRight from "~icons/lucide/chevron-right"
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
|
||||||
import { showChat } from "@modules/crisp"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const navigatorShare = !!navigator.share
|
|
||||||
|
|
||||||
const ZEN_MODE = useSetting("ZEN_MODE")
|
const ZEN_MODE = useSetting("ZEN_MODE")
|
||||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||||
@@ -176,11 +96,6 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const chatWithUs = () => {
|
|
||||||
showChat()
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const expandNavigation = () => {
|
const expandNavigation = () => {
|
||||||
EXPAND_NAVIGATION.value = !EXPAND_NAVIGATION.value
|
EXPAND_NAVIGATION.value = !EXPAND_NAVIGATION.value
|
||||||
hideModal()
|
hideModal()
|
||||||
@@ -191,24 +106,6 @@ const expandCollection = () => {
|
|||||||
hideModal()
|
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 = () => {
|
const hideModal = () => {
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,89 +8,46 @@
|
|||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<HoppSmartItem
|
<template
|
||||||
:icon="IconBook"
|
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
|
||||||
:label="t('app.documentation')"
|
:key="item.id"
|
||||||
to="https://docs.hoppscotch.io"
|
>
|
||||||
:description="t('support.documentation')"
|
<HoppSmartItem
|
||||||
:info-icon="IconChevronRight"
|
v-if="item.action.type === 'link'"
|
||||||
active
|
:icon="item.icon"
|
||||||
blank
|
:label="item.text(t)"
|
||||||
@click="hideModal()"
|
:to="item.action.href"
|
||||||
/>
|
:description="item.subtitle(t)"
|
||||||
<HoppSmartItem
|
:info-icon="IconChevronRight"
|
||||||
:icon="IconZap"
|
active
|
||||||
:label="t('app.keyboard_shortcuts')"
|
blank
|
||||||
:description="t('support.shortcuts')"
|
@click="hideModal()"
|
||||||
:info-icon="IconChevronRight"
|
/>
|
||||||
active
|
<HoppSmartItem
|
||||||
@click="showShortcuts()"
|
v-else
|
||||||
/>
|
:icon="item.icon"
|
||||||
<HoppSmartItem
|
:label="item.text(t)"
|
||||||
:icon="IconGift"
|
:description="item.subtitle(t)"
|
||||||
:label="t('app.whats_new')"
|
:info-icon="IconChevronRight"
|
||||||
to="https://docs.hoppscotch.io/documentation/changelog"
|
active
|
||||||
:description="t('support.changelog')"
|
@click="
|
||||||
:info-icon="IconChevronRight"
|
() => {
|
||||||
active
|
// @ts-expect-error Typescript isn't able to understand
|
||||||
blank
|
item.action.do()
|
||||||
@click="hideModal()"
|
hideModal()
|
||||||
/>
|
}
|
||||||
<HoppSmartItem
|
"
|
||||||
:icon="IconMessageCircle"
|
/>
|
||||||
:label="t('app.chat_with_us')"
|
</template>
|
||||||
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartModal>
|
</HoppSmartModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 IconChevronRight from "~icons/lucide/chevron-right"
|
||||||
import { invokeAction } from "@helpers/actions"
|
|
||||||
import { showChat } from "@modules/crisp"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -102,16 +59,6 @@ const emit = defineEmits<{
|
|||||||
(e: "hide-modal"): void
|
(e: "hide-modal"): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const chatWithUs = () => {
|
|
||||||
showChat()
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showShortcuts = () => {
|
|
||||||
invokeAction("flyouts.keybinds.toggle")
|
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideModal = () => {
|
const hideModal = () => {
|
||||||
emit("hide-modal")
|
emit("hide-modal")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,12 @@
|
|||||||
<label for="value" class="font-semibold min-w-10">{{
|
<label for="value" class="font-semibold min-w-10">{{
|
||||||
t("environment.value")
|
t("environment.value")
|
||||||
}}</label>
|
}}</label>
|
||||||
<input type="text" :value="value" class="input" />
|
<input
|
||||||
|
v-model="editingValue"
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:placeholder="t('environment.value')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-8 ml-2">
|
<div class="flex items-center space-x-8 ml-2">
|
||||||
<label for="scope" class="font-semibold min-w-10">
|
<label for="scope" class="font-semibold min-w-10">
|
||||||
@@ -105,9 +110,12 @@ watch(
|
|||||||
scope.value = {
|
scope.value = {
|
||||||
type: "global",
|
type: "global",
|
||||||
}
|
}
|
||||||
editingName.value = ""
|
|
||||||
replaceWithVariable.value = false
|
replaceWithVariable.value = false
|
||||||
|
editingName.value = ""
|
||||||
|
editingValue.value = ""
|
||||||
}
|
}
|
||||||
|
editingName.value = props.name
|
||||||
|
editingValue.value = props.value
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,6 +140,7 @@ const scope = ref<Scope>({
|
|||||||
const replaceWithVariable = ref(false)
|
const replaceWithVariable = ref(false)
|
||||||
|
|
||||||
const editingName = ref(props.name)
|
const editingName = ref(props.name)
|
||||||
|
const editingValue = ref(props.value)
|
||||||
|
|
||||||
const addEnvironment = async () => {
|
const addEnvironment = async () => {
|
||||||
if (!editingName.value) {
|
if (!editingName.value) {
|
||||||
@@ -141,13 +150,13 @@ const addEnvironment = async () => {
|
|||||||
if (scope.value.type === "global") {
|
if (scope.value.type === "global") {
|
||||||
addGlobalEnvVariable({
|
addGlobalEnvVariable({
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: props.value,
|
value: editingValue.value,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.updated")}`)
|
toast.success(`${t("environment.updated")}`)
|
||||||
} else if (scope.value.type === "my-environment") {
|
} else if (scope.value.type === "my-environment") {
|
||||||
addEnvironmentVariable(scope.value.index, {
|
addEnvironmentVariable(scope.value.index, {
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: props.value,
|
value: editingValue.value,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.updated")}`)
|
toast.success(`${t("environment.updated")}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -155,7 +164,7 @@ const addEnvironment = async () => {
|
|||||||
...scope.value.environment.environment.variables,
|
...scope.value.environment.environment.variables,
|
||||||
{
|
{
|
||||||
key: editingName.value,
|
key: editingName.value,
|
||||||
value: props.value,
|
value: editingValue.value,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
await pipe(
|
await pipe(
|
||||||
@@ -182,7 +191,7 @@ const addEnvironment = async () => {
|
|||||||
//replace the currenttab endpoint containing the value in the text with variablename
|
//replace the currenttab endpoint containing the value in the text with variablename
|
||||||
currentActiveTab.value.document.request.endpoint =
|
currentActiveTab.value.document.request.endpoint =
|
||||||
currentActiveTab.value.document.request.endpoint.replace(
|
currentActiveTab.value.document.request.endpoint.replace(
|
||||||
props.value,
|
editingValue.value,
|
||||||
variableName
|
variableName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
v-if="!isScopeSelector"
|
||||||
:label="`${t('environment.no_environment')}`"
|
:label="`${t('environment.no_environment')}`"
|
||||||
:info-icon="
|
:info-icon="
|
||||||
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
selectedEnvironmentIndex.type === 'NO_ENV_SELECTED'
|
||||||
@@ -48,6 +49,21 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<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
|
<HoppSmartTabs
|
||||||
v-model="selectedEnvTab"
|
v-model="selectedEnvTab"
|
||||||
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
|
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
|
||||||
@@ -66,14 +82,14 @@
|
|||||||
:key="`gen-${index}`"
|
:key="`gen-${index}`"
|
||||||
:icon="IconLayers"
|
:icon="IconLayers"
|
||||||
:label="gen.name"
|
:label="gen.name"
|
||||||
:info-icon="index === selectedEnv.index ? IconCheck : undefined"
|
:info-icon="isEnvActive(index) ? IconCheck : undefined"
|
||||||
:active-info-icon="index === selectedEnv.index"
|
:active-info-icon="isEnvActive(index)"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
selectedEnvironmentIndex = {
|
handleEnvironmentChange(index, {
|
||||||
type: 'MY_ENV',
|
type: 'my-environment',
|
||||||
index: index,
|
environment: gen,
|
||||||
}
|
})
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -113,18 +129,14 @@
|
|||||||
:key="`gen-team-${index}`"
|
:key="`gen-team-${index}`"
|
||||||
:icon="IconLayers"
|
:icon="IconLayers"
|
||||||
:label="gen.environment.name"
|
:label="gen.environment.name"
|
||||||
:info-icon="
|
:info-icon="isEnvActive(gen.id) ? IconCheck : undefined"
|
||||||
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
|
:active-info-icon="isEnvActive(gen.id)"
|
||||||
"
|
|
||||||
:active-info-icon="gen.id === selectedEnv.teamEnvID"
|
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
selectedEnvironmentIndex = {
|
handleEnvironmentChange(index, {
|
||||||
type: 'TEAM_ENV',
|
type: 'team-environment',
|
||||||
teamEnvID: gen.id,
|
environment: gen,
|
||||||
teamID: gen.teamID,
|
})
|
||||||
environment: gen.environment,
|
|
||||||
}
|
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -285,6 +297,7 @@ import IconCheck from "~icons/lucide/check"
|
|||||||
import IconLayers from "~icons/lucide/layers"
|
import IconLayers from "~icons/lucide/layers"
|
||||||
import IconEye from "~icons/lucide/eye"
|
import IconEye from "~icons/lucide/eye"
|
||||||
import IconEdit from "~icons/lucide/edit"
|
import IconEdit from "~icons/lucide/edit"
|
||||||
|
import IconGlobe from "~icons/lucide/globe"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { TippyComponent } from "vue-tippy"
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
@@ -295,11 +308,39 @@ import {
|
|||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
setSelectedEnvironmentIndex,
|
setSelectedEnvironmentIndex,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
|
||||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
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 breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
const mdAndLarger = breakpoints.greater("md")
|
const mdAndLarger = breakpoints.greater("md")
|
||||||
@@ -314,6 +355,38 @@ const myEnvironments = useReadonlyStream(environments$, [])
|
|||||||
|
|
||||||
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
|
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 teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
|
||||||
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
|
const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
|
||||||
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
|
const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
|
||||||
@@ -322,6 +395,70 @@ 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(
|
const selectedEnvironmentIndex = useStream(
|
||||||
selectedEnvironmentIndex$,
|
selectedEnvironmentIndex$,
|
||||||
{ type: "NO_ENV_SELECTED" },
|
{ type: "NO_ENV_SELECTED" },
|
||||||
@@ -349,34 +486,90 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const selectedEnv = computed(() => {
|
const selectedEnv = computed(() => {
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
if (props.isScopeSelector) {
|
||||||
const environment =
|
if (props.modelValue?.type === "my-environment") {
|
||||||
myEnvironments.value[selectedEnvironmentIndex.value.index]
|
return {
|
||||||
return {
|
type: "MY_ENV",
|
||||||
type: "MY_ENV",
|
index: props.modelValue.index,
|
||||||
index: selectedEnvironmentIndex.value.index,
|
name: props.modelValue.environment?.name,
|
||||||
name: environment.name,
|
}
|
||||||
variables: environment.variables,
|
} else if (props.modelValue?.type === "team-environment") {
|
||||||
}
|
|
||||||
} 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 {
|
return {
|
||||||
type: "TEAM_ENV",
|
type: "TEAM_ENV",
|
||||||
name: teamEnv.environment.name,
|
name: props.modelValue.environment.environment.name,
|
||||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
teamEnvID: props.modelValue.environment.id,
|
||||||
variables: teamEnv.environment.variables,
|
}
|
||||||
|
} 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" }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return { type: "NO_ENV_SELECTED" }
|
return { type: "NO_ENV_SELECTED" }
|
||||||
}
|
}
|
||||||
} 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ watch(
|
|||||||
|
|
||||||
defineActionHandler("modals.environment.add", ({ envName, variableName }) => {
|
defineActionHandler("modals.environment.add", ({ envName, variableName }) => {
|
||||||
editingVariableName.value = envName
|
editingVariableName.value = envName
|
||||||
editingVariableValue.value = variableName
|
if (variableName) editingVariableValue.value = variableName
|
||||||
displayModalNew(true)
|
displayModalNew(true)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -144,8 +144,6 @@ const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
|
|||||||
const gqlQueryString = useVModel(props, "modelValue", emit)
|
const gqlQueryString = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
const debouncedOnUpdateQueryState = debounce((update: ViewUpdate) => {
|
const debouncedOnUpdateQueryState = debounce((update: ViewUpdate) => {
|
||||||
if (!update.selectionSet) return
|
|
||||||
|
|
||||||
const selectedPos = update.state.selection.main.head
|
const selectedPos = update.state.selection.main.head
|
||||||
const queryString = update.state.doc.toJSON().join(update.state.lineBreak)
|
const queryString = update.state.doc.toJSON().join(update.state.lineBreak)
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import { connect } from "~/helpers/graphql/connection"
|
|||||||
import { disconnect } from "~/helpers/graphql/connection"
|
import { disconnect } from "~/helpers/graphql/connection"
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -140,4 +141,12 @@ const cancelSwitch = () => {
|
|||||||
if (connected.value) disconnect()
|
if (connected.value) disconnect()
|
||||||
connectionSwitchModal.value = false
|
connectionSwitchModal.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineActionHandler(
|
||||||
|
"gql.connect",
|
||||||
|
gqlConnect,
|
||||||
|
computed(() => !connected.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
defineActionHandler("gql.disconnect", disconnect, connected)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ import { useService } from "dioc/vue"
|
|||||||
import { InterceptorService } from "~/services/interceptor.service"
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
import { editGraphqlRequest } from "~/newstore/collections"
|
import { editGraphqlRequest } from "~/newstore/collections"
|
||||||
|
|
||||||
type OptionTabs = "query" | "headers" | "variables" | "authorization"
|
export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization"
|
||||||
const selectedOptionTab = ref<OptionTabs>("query")
|
const selectedOptionTab = ref<GQLOptionTabs>("query")
|
||||||
const interceptorService = useService(InterceptorService)
|
const interceptorService = useService(InterceptorService)
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -206,4 +206,8 @@ defineActionHandler("request.save-as", () => {
|
|||||||
showSaveRequestModal.value = true
|
showSaveRequestModal.value = true
|
||||||
})
|
})
|
||||||
defineActionHandler("request.reset", clearGQLQuery)
|
defineActionHandler("request.reset", clearGQLQuery)
|
||||||
|
|
||||||
|
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||||
|
selectedOptionTab.value = tab as GQLOptionTabs
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -128,8 +128,14 @@ const downloadResponse = (str: string) => {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("response.file.download", () =>
|
defineActionHandler(
|
||||||
downloadResponse(responseString.value)
|
"response.file.download",
|
||||||
|
() => downloadResponse(responseString.value),
|
||||||
|
computed(() => !!props.response && props.response.length > 0)
|
||||||
|
)
|
||||||
|
defineActionHandler(
|
||||||
|
"response.copy",
|
||||||
|
() => copyResponse(responseString.value),
|
||||||
|
computed(() => !!props.response && props.response.length > 0)
|
||||||
)
|
)
|
||||||
defineActionHandler("response.copy", () => copyResponse(responseString.value))
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -508,30 +508,17 @@ const changeTab = (tab: ComputedHeader["source"]) => {
|
|||||||
|
|
||||||
const inspectionService = useService(InspectionService)
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
const allTabResults = inspectionService.tabs
|
const headerKeyResults = inspectionService.getResultViewFor(
|
||||||
|
currentTabID.value,
|
||||||
|
(result) =>
|
||||||
|
result.locations.type === "header" && result.locations.position === "key"
|
||||||
|
)
|
||||||
|
|
||||||
const headerKeyResults = computed(() => {
|
const headerValueResults = inspectionService.getResultViewFor(
|
||||||
return (
|
currentTabID.value,
|
||||||
allTabResults.value
|
(result) =>
|
||||||
.get(currentTabID.value)
|
result.locations.type === "header" && result.locations.position === "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) => {
|
const getInspectorResult = (results: InspectorResult[], index: number) => {
|
||||||
return results.filter((result) => {
|
return results.filter((result) => {
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
|
|||||||
import IconCircle from "~icons/lucide/circle"
|
import IconCircle from "~icons/lucide/circle"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
import { computed, reactive, ref, watch } from "vue"
|
import { reactive, ref, watch } from "vue"
|
||||||
import { flow, pipe } from "fp-ts/function"
|
import { flow, pipe } from "fp-ts/function"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
@@ -409,30 +409,18 @@ const clearContent = () => {
|
|||||||
|
|
||||||
const inspectionService = useService(InspectionService)
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
const allTabResults = inspectionService.tabs
|
const parameterKeyResults = inspectionService.getResultViewFor(
|
||||||
|
currentTabID.value,
|
||||||
|
(result) =>
|
||||||
|
result.locations.type === "parameter" && result.locations.position === "key"
|
||||||
|
)
|
||||||
|
|
||||||
const parameterKeyResults = computed(() => {
|
const parameterValueResults = inspectionService.getResultViewFor(
|
||||||
return (
|
currentTabID.value,
|
||||||
allTabResults.value
|
(result) =>
|
||||||
.get(currentTabID.value)
|
result.locations.type === "parameter" &&
|
||||||
.filter(
|
result.locations.position === "value"
|
||||||
(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) => {
|
const getInspectorResult = (results: InspectorResult[], index: number) => {
|
||||||
return results.filter((result) => {
|
return results.filter((result) => {
|
||||||
|
|||||||
@@ -642,9 +642,5 @@ const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
|||||||
|
|
||||||
const inspectionService = useService(InspectionService)
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
const allTabResults = inspectionService.tabs
|
const tabResults = inspectionService.getResultViewFor(currentTabID.value)
|
||||||
|
|
||||||
const tabResults = computed(() => {
|
|
||||||
return allTabResults.value.get(currentTabID.value) ?? []
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -101,6 +101,6 @@ const newActiveHeadersCount$ = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("request.open-tab", ({ tab }) => {
|
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||||
selectedOptionsTab.value = tab
|
selectedOptionsTab.value = tab as RequestOptionTabs
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -145,13 +145,8 @@ const statusCategory = computed(() => {
|
|||||||
|
|
||||||
const inspectionService = useService(InspectionService)
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
const allTabResults = inspectionService.tabs
|
const tabResults = inspectionService.getResultViewFor(
|
||||||
|
currentTabID.value,
|
||||||
const tabResults = computed(() => {
|
(result) => result.locations.type === "response"
|
||||||
return (
|
)
|
||||||
allTabResults.value
|
|
||||||
.get(currentTabID.value)
|
|
||||||
?.filter((result) => result.locations.type === "response") ?? []
|
|
||||||
)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
* For example, sending a request.
|
* For example, sending a request.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
|
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
|
||||||
import { BehaviorSubject } from "rxjs"
|
import { BehaviorSubject } from "rxjs"
|
||||||
import { HoppRESTDocument } from "./rest/document"
|
import { HoppRESTDocument } from "./rest/document"
|
||||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||||
import { HoppGQLSaveContext } from "./graphql/document"
|
import { HoppGQLSaveContext } from "./graphql/document"
|
||||||
|
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
export type HoppAction =
|
export type HoppAction =
|
||||||
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
| "contextmenu.open" // Send/Cancel a Hoppscotch Request
|
||||||
@@ -16,7 +18,7 @@ export type HoppAction =
|
|||||||
| "request.copy-link" // Copy Request Link
|
| "request.copy-link" // Copy Request Link
|
||||||
| "request.save" // Save to Collections
|
| "request.save" // Save to Collections
|
||||||
| "request.save-as" // Save As
|
| "request.save-as" // Save As
|
||||||
| "rest.request.rename" // Rename
|
| "request.rename" // Rename request on REST or GraphQL
|
||||||
| "request.method.next" // Select Next Method
|
| "request.method.next" // Select Next Method
|
||||||
| "request.method.prev" // Select Previous Method
|
| "request.method.prev" // Select Previous Method
|
||||||
| "request.method.get" // Select GET Method
|
| "request.method.get" // Select GET Method
|
||||||
@@ -26,6 +28,11 @@ export type HoppAction =
|
|||||||
| "request.method.delete" // Select DELETE Method
|
| "request.method.delete" // Select DELETE Method
|
||||||
| "request.import-curl" // Import cURL
|
| "request.import-curl" // Import cURL
|
||||||
| "request.show-code" // Show generated code
|
| "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
|
| "collection.new" // Create root collection
|
||||||
| "flyouts.chat.open" // Shows the keybinds flyout
|
| "flyouts.chat.open" // Shows the keybinds flyout
|
||||||
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
||||||
@@ -106,11 +113,11 @@ type HoppActionArgsMap = {
|
|||||||
request: HoppGQLRequest
|
request: HoppGQLRequest
|
||||||
}
|
}
|
||||||
"request.open-tab": {
|
"request.open-tab": {
|
||||||
tab: RequestOptionTabs
|
tab: RequestOptionTabs | GQLOptionTabs
|
||||||
}
|
}
|
||||||
|
|
||||||
"request.duplicate-tab": {
|
"tab.duplicate-tab": {
|
||||||
tabID: string
|
tabID?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
"gql.request.open": {
|
"gql.request.open": {
|
||||||
@@ -150,7 +157,7 @@ type BoundActionList = {
|
|||||||
[A in HoppAction | HoppActionWithArgs]?: Array<ActionFunc<A>>
|
[A in HoppAction | HoppActionWithArgs]?: Array<ActionFunc<A>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundActions: BoundActionList = {}
|
const boundActions: BoundActionList = reactive({})
|
||||||
|
|
||||||
export const activeActions$ = new BehaviorSubject<
|
export const activeActions$ = new BehaviorSubject<
|
||||||
(HoppAction | HoppActionWithArgs)[]
|
(HoppAction | HoppActionWithArgs)[]
|
||||||
@@ -206,6 +213,15 @@ export function unbindAction<A extends HoppAction | HoppActionWithArgs>(
|
|||||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
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
|
* A composable function that defines a component can handle a given
|
||||||
* HoppAction. The handler will be bound when the component is mounted
|
* HoppAction. The handler will be bound when the component is mounted
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const bindings: {
|
|||||||
"alt-x": "request.method.delete",
|
"alt-x": "request.method.delete",
|
||||||
"ctrl-k": "modals.search.toggle",
|
"ctrl-k": "modals.search.toggle",
|
||||||
"ctrl-/": "flyouts.keybinds.toggle",
|
"ctrl-/": "flyouts.keybinds.toggle",
|
||||||
"?": "modals.support.toggle",
|
"shift-/": "modals.support.toggle",
|
||||||
"ctrl-m": "modals.share.toggle",
|
"ctrl-m": "modals.share.toggle",
|
||||||
"alt-r": "navigation.jump.rest",
|
"alt-r": "navigation.jump.rest",
|
||||||
"alt-q": "navigation.jump.graphql",
|
"alt-q": "navigation.jump.graphql",
|
||||||
@@ -120,7 +120,8 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPressedKey(ev: KeyboardEvent): Key | null {
|
function getPressedKey(ev: KeyboardEvent): Key | null {
|
||||||
const val = ev.key.toLowerCase()
|
const val = ev.code.toLowerCase()
|
||||||
|
|
||||||
// Check arrow keys
|
// Check arrow keys
|
||||||
if (val === "arrowup") return "up"
|
if (val === "arrowup") return "up"
|
||||||
else if (val === "arrowdown") return "down"
|
else if (val === "arrowdown") return "down"
|
||||||
@@ -128,21 +129,20 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
|
|||||||
else if (val === "arrowright") return "right"
|
else if (val === "arrowright") return "right"
|
||||||
|
|
||||||
// Check letter keys
|
// Check letter keys
|
||||||
const isLetter = ev.code.toLowerCase().startsWith("key")
|
const isLetter = val.startsWith("key")
|
||||||
if (isLetter) return ev.code.toLowerCase().substring(3) as Key
|
if (isLetter) return val.substring(3) as Key
|
||||||
|
|
||||||
// Check if number keys
|
// Check if number keys
|
||||||
if (val.length === 1 && !isNaN(val as any)) return val as Key
|
const isDigit = val.startsWith("digit")
|
||||||
|
if (isDigit) return val.substring(5) as Key
|
||||||
|
|
||||||
// Check if question mark
|
// Check if slash
|
||||||
if (val === "?") return "?"
|
if (val === "slash") return "/"
|
||||||
|
|
||||||
// Check if question mark
|
|
||||||
if (val === "/") return "/"
|
|
||||||
|
|
||||||
// Check if period
|
// Check if period
|
||||||
if (val === ".") return "."
|
if (val === "period") return "."
|
||||||
|
|
||||||
|
// Check if enter
|
||||||
if (val === "enter") return "enter"
|
if (val === "enter") return "enter"
|
||||||
|
|
||||||
// If no other cases match, this is not a valid key
|
// If no other cases match, this is not a valid key
|
||||||
|
|||||||
@@ -98,7 +98,11 @@ const buildHarPostData = (req: HoppRESTRequest): Har.PostData | undefined => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildHarRequest = (req: HoppRESTRequest): Har.Request => {
|
export const buildHarRequest = (
|
||||||
|
req: HoppRESTRequest
|
||||||
|
): Har.Request & {
|
||||||
|
postData: Har.PostData & Exclude<Har.PostData, undefined>
|
||||||
|
} => {
|
||||||
return {
|
return {
|
||||||
bodySize: -1, // TODO: It would be cool if we can calculate the body size
|
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
|
headersSize: -1, // TODO: It would be cool if we can calculate the header size
|
||||||
@@ -108,6 +112,9 @@ export const buildHarRequest = (req: HoppRESTRequest): Har.Request => {
|
|||||||
method: req.method,
|
method: req.method,
|
||||||
queryString: buildHarQueryStrings(req),
|
queryString: buildHarQueryStrings(req),
|
||||||
url: req.endpoint,
|
url: req.endpoint,
|
||||||
postData: buildHarPostData(req),
|
postData: buildHarPostData(req) ?? {
|
||||||
|
mimeType: "x-unknown",
|
||||||
|
params: [],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as HTTPSnippet from "httpsnippet"
|
import { HTTPSnippet } from "httpsnippet"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
|||||||
import { getDefaultRESTRequest } from "./default"
|
import { getDefaultRESTRequest } from "./default"
|
||||||
import { HoppTestResult } from "../types/HoppTestResult"
|
import { HoppTestResult } from "../types/HoppTestResult"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { nextTick } from "vue"
|
||||||
|
|
||||||
export type HoppRESTTab = {
|
export type HoppRESTTab = {
|
||||||
id: string
|
id: string
|
||||||
@@ -178,7 +179,9 @@ export function closeTab(tabID: string) {
|
|||||||
|
|
||||||
tabOrdering.value.splice(tabOrdering.value.indexOf(tabID), 1)
|
tabOrdering.value.splice(tabOrdering.value.indexOf(tabID), 1)
|
||||||
|
|
||||||
tabMap.delete(tabID)
|
nextTick(() => {
|
||||||
|
tabMap.delete(tabID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeOtherTabs(tabID: string) {
|
export function closeOtherTabs(tabID: string) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export function createHoppApp(el: string | Element, platformDef: PlatformDef) {
|
|||||||
performMigrations()
|
performMigrations()
|
||||||
|
|
||||||
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
|
HOPP_MODULES.forEach((mod) => mod.onVueAppInit?.(app))
|
||||||
|
platformDef.addedHoppModules?.forEach((mod) => mod.onVueAppInit?.(app))
|
||||||
|
|
||||||
app.mount(el)
|
app.mount(el)
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
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"])
|
|
||||||
},
|
|
||||||
])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -53,6 +53,9 @@ export default <HoppModule>{
|
|||||||
HOPP_MODULES.forEach((mod) => {
|
HOPP_MODULES.forEach((mod) => {
|
||||||
mod.onBeforeRouteChange?.(to, from, router)
|
mod.onBeforeRouteChange?.(to, from, router)
|
||||||
})
|
})
|
||||||
|
platform.addedHoppModules?.forEach((mod) => {
|
||||||
|
mod.onBeforeRouteChange?.(to, from, router)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Instead of this a better architecture is for the router
|
// Instead of this a better architecture is for the router
|
||||||
@@ -66,10 +69,14 @@ export default <HoppModule>{
|
|||||||
HOPP_MODULES.forEach((mod) => {
|
HOPP_MODULES.forEach((mod) => {
|
||||||
mod.onAfterRouteChange?.(to, router)
|
mod.onAfterRouteChange?.(to, router)
|
||||||
})
|
})
|
||||||
|
platform.addedHoppModules?.forEach((mod) => {
|
||||||
|
mod.onAfterRouteChange?.(to, router)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
HOPP_MODULES.forEach((mod) => mod.onRouterInit?.(app, router))
|
HOPP_MODULES.forEach((mod) => mod.onRouterInit?.(app, router))
|
||||||
|
platform.addedHoppModules?.forEach((mod) => mod.onRouterInit?.(app, router))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
@open-rename-modal="openReqRenameModal(tab)"
|
@open-rename-modal="openReqRenameModal(tab)"
|
||||||
@close-tab="removeTab(tab.id)"
|
@close-tab="removeTab(tab.id)"
|
||||||
@close-other-tabs="closeOtherTabsAction(tab.id)"
|
@close-other-tabs="closeOtherTabsAction(tab.id)"
|
||||||
@duplicate-tab="duplicateTab(tab)"
|
@duplicate-tab="duplicateTab(tab.id)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -203,12 +203,15 @@ const renameReqName = () => {
|
|||||||
showRenamingReqNameModalForTabID.value = undefined
|
showRenamingReqNameModalForTabID.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicateTab = (tab: HoppGQLTab) => {
|
const duplicateTab = (tabID: string) => {
|
||||||
const newTab = createNewTab({
|
const tab = getTabRef(tabID)
|
||||||
request: tab.document.request,
|
if (tab.value) {
|
||||||
isDirty: true,
|
const newTab = createNewTab({
|
||||||
})
|
request: tab.value.document.request,
|
||||||
currentTabID.value = newTab.id
|
isDirty: true,
|
||||||
|
})
|
||||||
|
currentTabID.value = newTab.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("gql.request.open", ({ request, saveContext }) => {
|
defineActionHandler("gql.request.open", ({ request, saveContext }) => {
|
||||||
@@ -218,4 +221,19 @@ defineActionHandler("gql.request.open", ({ request, saveContext }) => {
|
|||||||
isDirty: false,
|
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>
|
</script>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, onBeforeUnmount, onBeforeMount, watch } from "vue"
|
import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"
|
||||||
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
||||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
@@ -140,7 +140,6 @@ import { useService } from "dioc/vue"
|
|||||||
import { InspectionService } from "~/services/inspection"
|
import { InspectionService } from "~/services/inspection"
|
||||||
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
|
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
|
||||||
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
|
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
|
||||||
import { URLInspectorService } from "~/services/inspection/inspectors/url.inspector"
|
|
||||||
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
|
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
|
||||||
|
|
||||||
const savingRequest = ref(false)
|
const savingRequest = ref(false)
|
||||||
@@ -215,6 +214,8 @@ const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
|
|||||||
updateTabOrdering(e.oldIndex, e.newIndex)
|
updateTabOrdering(e.oldIndex, e.newIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inspectionService = useService(InspectionService)
|
||||||
|
|
||||||
const removeTab = (tabID: string) => {
|
const removeTab = (tabID: string) => {
|
||||||
const tabState = getTabRef(tabID).value
|
const tabState = getTabRef(tabID).value
|
||||||
|
|
||||||
@@ -460,22 +461,22 @@ defineActionHandler("rest.request.open", ({ doc }) => {
|
|||||||
createNewTab(doc)
|
createNewTab(doc)
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("rest.request.rename", openReqRenameModal)
|
defineActionHandler("request.rename", openReqRenameModal)
|
||||||
defineActionHandler("request.duplicate-tab", ({ tabID }) => {
|
defineActionHandler("tab.duplicate-tab", ({ tabID }) => {
|
||||||
duplicateTab(tabID)
|
duplicateTab(tabID ?? currentTabID.value)
|
||||||
})
|
})
|
||||||
|
defineActionHandler("tab.close-current", () => {
|
||||||
|
removeTab(currentTabID.value)
|
||||||
|
})
|
||||||
|
defineActionHandler("tab.close-other", () => {
|
||||||
|
closeOtherTabs(currentTabID.value)
|
||||||
|
})
|
||||||
|
defineActionHandler("tab.open-new", addNewTab)
|
||||||
|
|
||||||
const inspectionService = useService(InspectionService)
|
|
||||||
useService(HeaderInspectorService)
|
useService(HeaderInspectorService)
|
||||||
useService(EnvironmentInspectorService)
|
useService(EnvironmentInspectorService)
|
||||||
useService(URLInspectorService)
|
|
||||||
useService(ResponseInspectorService)
|
useService(ResponseInspectorService)
|
||||||
|
for (const inspectorDef of platform.additionalInspectors ?? []) {
|
||||||
watch(
|
useService(inspectorDef.service)
|
||||||
() => currentTabID.value,
|
}
|
||||||
() => {
|
|
||||||
inspectionService.initializeTabInspectors()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import { HistoryPlatformDef } from "./history"
|
|||||||
import { TabStatePlatformDef } from "./tab"
|
import { TabStatePlatformDef } from "./tab"
|
||||||
import { AnalyticsPlatformDef } from "./analytics"
|
import { AnalyticsPlatformDef } from "./analytics"
|
||||||
import { InterceptorsPlatformDef } from "./interceptors"
|
import { InterceptorsPlatformDef } from "./interceptors"
|
||||||
|
import { HoppModule } from "~/modules"
|
||||||
|
import { InspectorsPlatformDef } from "./inspectors"
|
||||||
|
|
||||||
export type PlatformDef = {
|
export type PlatformDef = {
|
||||||
ui?: UIPlatformDef
|
ui?: UIPlatformDef
|
||||||
|
addedHoppModules?: HoppModule[]
|
||||||
auth: AuthPlatformDef
|
auth: AuthPlatformDef
|
||||||
analytics?: AnalyticsPlatformDef
|
analytics?: AnalyticsPlatformDef
|
||||||
sync: {
|
sync: {
|
||||||
@@ -20,6 +23,7 @@ export type PlatformDef = {
|
|||||||
tabState: TabStatePlatformDef
|
tabState: TabStatePlatformDef
|
||||||
}
|
}
|
||||||
interceptors: InterceptorsPlatformDef
|
interceptors: InterceptorsPlatformDef
|
||||||
|
additionalInspectors?: InspectorsPlatformDef
|
||||||
platformFeatureFlags: {
|
platformFeatureFlags: {
|
||||||
exportAsGIST: boolean
|
exportAsGIST: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
16
packages/hoppscotch-common/src/platform/inspectors.ts
Normal file
16
packages/hoppscotch-common/src/platform/inspectors.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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[]
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { TestContainer } from "dioc/testing"
|
import { TestContainer } from "dioc/testing"
|
||||||
import { describe, expect, it, vi } from "vitest"
|
import { describe, expect, it, vi } from "vitest"
|
||||||
import { URLInspectorService } from "../url.inspector"
|
import { ExtensionInspectorService } from "../extension.inspector"
|
||||||
import { InspectionService } from "../../index"
|
import { InspectionService } from "~/services/inspection"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
|
||||||
|
|
||||||
vi.mock("~/modules/i18n", () => ({
|
vi.mock("~/modules/i18n", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
getI18n: () => (x: string) => x,
|
getI18n: () => (x: string) => x,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe("URLInspectorService", () => {
|
describe("ExtensionInspectorService", () => {
|
||||||
it("registers with the inspection service upon initialization", () => {
|
it("registers with the inspection service upon initialization", () => {
|
||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ describe("URLInspectorService", () => {
|
|||||||
registerInspector: registerInspectorFn,
|
registerInspector: registerInspectorFn,
|
||||||
})
|
})
|
||||||
|
|
||||||
const urlInspector = container.bind(URLInspectorService)
|
const urlInspector = container.bind(ExtensionInspectorService)
|
||||||
|
|
||||||
expect(registerInspectorFn).toHaveBeenCalledOnce()
|
expect(registerInspectorFn).toHaveBeenCalledOnce()
|
||||||
expect(registerInspectorFn).toHaveBeenCalledWith(urlInspector)
|
expect(registerInspectorFn).toHaveBeenCalledWith(urlInspector)
|
||||||
@@ -28,55 +30,57 @@ describe("URLInspectorService", () => {
|
|||||||
describe("getInspectorFor", () => {
|
describe("getInspectorFor", () => {
|
||||||
it("should return an inspector result when localhost is in URL and extension is not available", () => {
|
it("should return an inspector result when localhost is in URL and extension is not available", () => {
|
||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const urlInspector = container.bind(URLInspectorService)
|
const urlInspector = container.bind(ExtensionInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://localhost:8000/api/data",
|
endpoint: "http://localhost:8000/api/data",
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = urlInspector.getInspectorFor(req)
|
const result = urlInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
expect(result.value).toContainEqual(
|
||||||
expect.objectContaining({ id: "url", isApplicable: true })
|
expect.objectContaining({ id: "url", isApplicable: true })
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not return an inspector result when localhost is not in URL", () => {
|
it("should not return an inspector result when localhost is not in URL", () => {
|
||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const urlInspector = container.bind(URLInspectorService)
|
|
||||||
|
|
||||||
const req = {
|
container.bindMock(ExtensionInterceptorService, {
|
||||||
|
extensionStatus: ref("unknown-origin" as const),
|
||||||
|
})
|
||||||
|
|
||||||
|
const urlInspector = container.bind(ExtensionInspectorService)
|
||||||
|
|
||||||
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://example.com/api/data",
|
endpoint: "http://example.com/api/data",
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = urlInspector.getInspectorFor(req)
|
const result = urlInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toHaveLength(0)
|
expect(result.value).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add the correct text to the results when extension is not installed", () => {
|
it("should add the correct text to the results when extension is not installed", () => {
|
||||||
vi.mock("~/newstore/HoppExtension", async () => {
|
|
||||||
const { BehaviorSubject }: any = await vi.importActual("rxjs")
|
|
||||||
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
extensionStatus$: new BehaviorSubject("waiting"),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const urlInspector = container.bind(URLInspectorService)
|
|
||||||
|
|
||||||
const req = {
|
container.bindMock(ExtensionInterceptorService, {
|
||||||
|
extensionStatus: ref("waiting" as const),
|
||||||
|
})
|
||||||
|
|
||||||
|
const urlInspector = container.bind(ExtensionInspectorService)
|
||||||
|
|
||||||
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://localhost:8000/api/data",
|
endpoint: "http://localhost:8000/api/data",
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = urlInspector.getInspectorFor(req)
|
const result = urlInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toHaveLength(1)
|
expect(result.value).toHaveLength(1)
|
||||||
expect(result[0]).toMatchObject({
|
expect(result.value[0]).toMatchObject({
|
||||||
text: { type: "text", text: "inspections.url.extension_not_installed" },
|
text: { type: "text", text: "inspections.url.extension_not_installed" },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/hoppscotch-common/src/platform/std/ui/footerItem.ts
Normal file
25
packages/hoppscotch-common/src/platform/std/ui/footerItem.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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]
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
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/discord",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const invite: HoppSupportOptionsMenuItem = {
|
||||||
|
id: "invite",
|
||||||
|
text: (t) => t("app.invite"),
|
||||||
|
subtitle: (t) => t("shortcut.miscellaneous.invite"),
|
||||||
|
icon: IconUserPlus,
|
||||||
|
action: {
|
||||||
|
type: "custom",
|
||||||
|
do() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stdSupportOptionItems: HoppSupportOptionsMenuItem[] = [
|
||||||
|
documentation,
|
||||||
|
shortcuts,
|
||||||
|
changelog,
|
||||||
|
github,
|
||||||
|
discord,
|
||||||
|
twitter,
|
||||||
|
invite,
|
||||||
|
]
|
||||||
@@ -1,4 +1,20 @@
|
|||||||
import { Ref } from "vue"
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
export type UIPlatformDef = {
|
export type UIPlatformDef = {
|
||||||
appHeader?: {
|
appHeader?: {
|
||||||
@@ -6,4 +22,15 @@ export type UIPlatformDef = {
|
|||||||
paddingLeft?: Ref<string>
|
paddingLeft?: Ref<string>
|
||||||
}
|
}
|
||||||
onCodemirrorInstanceMount?: (element: HTMLElement) => void
|
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[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ describe("EnvironmentMenuService", () => {
|
|||||||
expect(actionsMock.invokeAction).toHaveBeenCalledWith(
|
expect(actionsMock.invokeAction).toHaveBeenCalledWith(
|
||||||
"modals.environment.add",
|
"modals.environment.add",
|
||||||
{
|
{
|
||||||
envName: "test",
|
envName: "",
|
||||||
variableName: test,
|
variableName: test,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class EnvironmentMenuService extends Service implements ContextMenu {
|
|||||||
icon: markRaw(IconPlusCircle),
|
icon: markRaw(IconPlusCircle),
|
||||||
action: () => {
|
action: () => {
|
||||||
invokeAction("modals.environment.add", {
|
invokeAction("modals.environment.add", {
|
||||||
envName: "test",
|
envName: "",
|
||||||
variableName: text,
|
variableName: text,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect } from "vitest"
|
import { describe, it, expect } from "vitest"
|
||||||
import { Inspector, InspectionService, InspectorResult } from "../"
|
import { Inspector, InspectionService, InspectorResult } from "../"
|
||||||
import { TestContainer } from "dioc/testing"
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
const inspectorResultMock: InspectorResult[] = [
|
const inspectorResultMock: InspectorResult[] = [
|
||||||
{
|
{
|
||||||
@@ -21,7 +22,7 @@ const inspectorResultMock: InspectorResult[] = [
|
|||||||
|
|
||||||
const testInspector: Inspector = {
|
const testInspector: Inspector = {
|
||||||
inspectorID: "inspector1",
|
inspectorID: "inspector1",
|
||||||
getInspectorFor: () => inspectorResultMock,
|
getInspections: () => ref(inspectorResultMock),
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("InspectionService", () => {
|
describe("InspectionService", () => {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { refDebounced } from "@vueuse/core"
|
||||||
import { Service } from "dioc"
|
import { Service } from "dioc"
|
||||||
|
import { computed, markRaw, reactive } from "vue"
|
||||||
import { Component, Ref, ref, watch } from "vue"
|
import { Component, Ref, ref, watch } from "vue"
|
||||||
import { currentActiveTab, currentTabID } from "~/helpers/rest/tab"
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,15 +82,16 @@ export interface Inspector {
|
|||||||
*/
|
*/
|
||||||
inspectorID: string
|
inspectorID: string
|
||||||
/**
|
/**
|
||||||
* Returns the inspector results for the request
|
* Returns the inspector results for the request.
|
||||||
* @param req The request to inspect
|
* NOTE: The refs passed down are readonly and are debounced to avoid performance issues
|
||||||
* @param res The response to inspect
|
* @param req The ref to the request to inspect
|
||||||
* @returns The inspector results
|
* @param res The ref to the response to inspect
|
||||||
|
* @returns The ref to the inspector results
|
||||||
*/
|
*/
|
||||||
getInspectorFor: (
|
getInspections: (
|
||||||
req: HoppRESTRequest,
|
req: Readonly<Ref<HoppRESTRequest>>,
|
||||||
res?: HoppRESTResponse
|
res: Readonly<Ref<HoppRESTResponse | null | undefined>>
|
||||||
) => InspectorResult[]
|
) => Ref<InspectorResult[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,38 +101,73 @@ export interface Inspector {
|
|||||||
export class InspectionService extends Service {
|
export class InspectionService extends Service {
|
||||||
public static readonly ID = "INSPECTION_SERVICE"
|
public static readonly ID = "INSPECTION_SERVICE"
|
||||||
|
|
||||||
private inspectors: Map<string, Inspector> = new Map()
|
private inspectors: Map<string, Inspector> = reactive(new Map())
|
||||||
|
|
||||||
public tabs: Ref<Map<string, InspectorResult[]>> = ref(new Map())
|
private tabs: Ref<Map<string, InspectorResult[]>> = ref(new Map())
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.initializeListeners()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a inspector with the inspection service
|
* Registers a inspector with the inspection service
|
||||||
* @param inspector The inspector instance to register
|
* @param inspector The inspector instance to register
|
||||||
*/
|
*/
|
||||||
public registerInspector(inspector: Inspector) {
|
public registerInspector(inspector: Inspector) {
|
||||||
this.inspectors.set(inspector.inspectorID, inspector)
|
// markRaw is required here so that the inspector is not made reactive
|
||||||
|
this.inspectors.set(inspector.inspectorID, markRaw(inspector))
|
||||||
}
|
}
|
||||||
|
|
||||||
public initializeTabInspectors() {
|
private initializeListeners() {
|
||||||
watch(
|
watch(
|
||||||
currentActiveTab.value,
|
() => [this.inspectors.entries(), currentActiveTab.value.id],
|
||||||
(tab) => {
|
() => {
|
||||||
if (!tab) return
|
const reqRef = computed(() => currentActiveTab.value.document.request)
|
||||||
const req = currentActiveTab.value.document.request
|
const resRef = computed(() => currentActiveTab.value.response)
|
||||||
const res = currentActiveTab.value.response
|
|
||||||
const inspectors = Array.from(this.inspectors.values()).map((x) =>
|
const debouncedReq = refDebounced(reqRef, 1000, { maxWait: 2000 })
|
||||||
x.getInspectorFor(req, res)
|
const debouncedRes = refDebounced(resRef, 1000, { maxWait: 2000 })
|
||||||
|
|
||||||
|
const inspectorRefs = Array.from(this.inspectors.values()).map((x) =>
|
||||||
|
x.getInspections(debouncedReq, debouncedRes)
|
||||||
)
|
)
|
||||||
this.tabs.value.set(
|
|
||||||
currentTabID.value,
|
const activeInspections = computed(() =>
|
||||||
inspectors.flatMap((x) => x)
|
inspectorRefs.flatMap((x) => x!.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [...inspectorRefs.flatMap((x) => x!.value)],
|
||||||
|
() => {
|
||||||
|
this.tabs.value.set(
|
||||||
|
currentActiveTab.value.id,
|
||||||
|
activeInspections.value
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ immediate: true, deep: true }
|
{ immediate: true, flush: "pre" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteTabInspectorResult(tabID: string) {
|
public deleteTabInspectorResult(tabID: string) {
|
||||||
|
// TODO: Move Tabs into a service and implement this with an event instead
|
||||||
this.tabs.value.delete(tabID)
|
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) ?? [])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,23 @@ import { describe, expect, it, vi } from "vitest"
|
|||||||
import { EnvironmentInspectorService } from "../environment.inspector"
|
import { EnvironmentInspectorService } from "../environment.inspector"
|
||||||
import { InspectionService } from "../../index"
|
import { InspectionService } from "../../index"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
vi.mock("~/modules/i18n", () => ({
|
vi.mock("~/modules/i18n", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
getI18n: () => (x: string) => x,
|
getI18n: () => (x: string) => x,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock("~/newstore/environments", () => ({
|
vi.mock("~/newstore/environments", async () => {
|
||||||
__esModule: true,
|
const { BehaviorSubject }: any = await vi.importActual("rxjs")
|
||||||
getAggregateEnvs: () => [{ key: "EXISTING_ENV_VAR", value: "test_value" }],
|
|
||||||
}))
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
aggregateEnvs$: new BehaviorSubject([
|
||||||
|
{ key: "EXISTING_ENV_VAR", value: "test_value" },
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
describe("EnvironmentInspectorService", () => {
|
describe("EnvironmentInspectorService", () => {
|
||||||
it("registers with the inspection service upon initialization", () => {
|
it("registers with the inspection service upon initialization", () => {
|
||||||
@@ -35,14 +42,14 @@ describe("EnvironmentInspectorService", () => {
|
|||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const envInspector = container.bind(EnvironmentInspectorService)
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "<<UNDEFINED_ENV_VAR>>",
|
endpoint: "<<UNDEFINED_ENV_VAR>>",
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = envInspector.getInspectorFor(req)
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
expect(result.value).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "environment",
|
id: "environment",
|
||||||
isApplicable: true,
|
isApplicable: true,
|
||||||
@@ -58,31 +65,31 @@ describe("EnvironmentInspectorService", () => {
|
|||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const envInspector = container.bind(EnvironmentInspectorService)
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "<<EXISTING_ENV_VAR>>",
|
endpoint: "<<EXISTING_ENV_VAR>>",
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = envInspector.getInspectorFor(req)
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toHaveLength(0)
|
expect(result.value).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return an inspector result when the headers contain undefined environment variables", () => {
|
it("should return an inspector result when the headers contain undefined environment variables", () => {
|
||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const envInspector = container.bind(EnvironmentInspectorService)
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://example.com/api/data",
|
endpoint: "http://example.com/api/data",
|
||||||
headers: [
|
headers: [
|
||||||
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
|
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
|
||||||
],
|
],
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = envInspector.getInspectorFor(req)
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
expect(result.value).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "environment",
|
id: "environment",
|
||||||
isApplicable: true,
|
isApplicable: true,
|
||||||
@@ -98,34 +105,34 @@ describe("EnvironmentInspectorService", () => {
|
|||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const envInspector = container.bind(EnvironmentInspectorService)
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://example.com/api/data",
|
endpoint: "http://example.com/api/data",
|
||||||
headers: [
|
headers: [
|
||||||
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
|
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
|
||||||
],
|
],
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = envInspector.getInspectorFor(req)
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toHaveLength(0)
|
expect(result.value).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return an inspector result when the params contain undefined environment variables", () => {
|
it("should return an inspector result when the params contain undefined environment variables", () => {
|
||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const envInspector = container.bind(EnvironmentInspectorService)
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://example.com/api/data",
|
endpoint: "http://example.com/api/data",
|
||||||
params: [
|
params: [
|
||||||
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
|
{ key: "<<UNDEFINED_ENV_VAR>>", value: "some-value", active: true },
|
||||||
],
|
],
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = envInspector.getInspectorFor(req)
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
expect(result.value).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "environment",
|
id: "environment",
|
||||||
isApplicable: true,
|
isApplicable: true,
|
||||||
@@ -141,18 +148,18 @@ describe("EnvironmentInspectorService", () => {
|
|||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const envInspector = container.bind(EnvironmentInspectorService)
|
const envInspector = container.bind(EnvironmentInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://example.com/api/data",
|
endpoint: "http://example.com/api/data",
|
||||||
headers: [],
|
headers: [],
|
||||||
params: [
|
params: [
|
||||||
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
|
{ key: "<<EXISTING_ENV_VAR>>", value: "some-value", active: true },
|
||||||
],
|
],
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = envInspector.getInspectorFor(req)
|
const result = envInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toHaveLength(0)
|
expect(result.value).toHaveLength(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"
|
|||||||
import { HeaderInspectorService } from "../header.inspector"
|
import { HeaderInspectorService } from "../header.inspector"
|
||||||
import { InspectionService } from "../../index"
|
import { InspectionService } from "../../index"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
vi.mock("~/modules/i18n", () => ({
|
vi.mock("~/modules/i18n", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
@@ -30,15 +31,15 @@ describe("HeaderInspectorService", () => {
|
|||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const headerInspector = container.bind(HeaderInspectorService)
|
const headerInspector = container.bind(HeaderInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://example.com/api/data",
|
endpoint: "http://example.com/api/data",
|
||||||
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
|
headers: [{ key: "Cookie", value: "some-cookie", active: true }],
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = headerInspector.getInspectorFor(req)
|
const result = headerInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toContainEqual(
|
expect(result.value).toContainEqual(
|
||||||
expect.objectContaining({ id: "header", isApplicable: true })
|
expect.objectContaining({ id: "header", isApplicable: true })
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -47,15 +48,15 @@ describe("HeaderInspectorService", () => {
|
|||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
const headerInspector = container.bind(HeaderInspectorService)
|
const headerInspector = container.bind(HeaderInspectorService)
|
||||||
|
|
||||||
const req = {
|
const req = ref({
|
||||||
...getDefaultRESTRequest(),
|
...getDefaultRESTRequest(),
|
||||||
endpoint: "http://example.com/api/data",
|
endpoint: "http://example.com/api/data",
|
||||||
headers: [{ key: "Authorization", value: "Bearer abcd", active: true }],
|
headers: [{ key: "Authorization", value: "Bearer abcd", active: true }],
|
||||||
}
|
})
|
||||||
|
|
||||||
const result = headerInspector.getInspectorFor(req)
|
const result = headerInspector.getInspections(req)
|
||||||
|
|
||||||
expect(result).toHaveLength(0)
|
expect(result.value).toHaveLength(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,11 +6,13 @@ import {
|
|||||||
InspectorResult,
|
InspectorResult,
|
||||||
} from ".."
|
} from ".."
|
||||||
import { Service } from "dioc"
|
import { Service } from "dioc"
|
||||||
import { Ref, markRaw, ref } from "vue"
|
import { Ref, markRaw } from "vue"
|
||||||
import IconPlusCircle from "~icons/lucide/plus-circle"
|
import IconPlusCircle from "~icons/lucide/plus-circle"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { getAggregateEnvs } from "~/newstore/environments"
|
import { aggregateEnvs$ } from "~/newstore/environments"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useStreamStatic } from "~/composables/stream"
|
||||||
|
|
||||||
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
||||||
|
|
||||||
@@ -34,6 +36,10 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
|
|
||||||
private readonly inspection = this.bind(InspectionService)
|
private readonly inspection = this.bind(InspectionService)
|
||||||
|
|
||||||
|
private aggregateEnvs = useStreamStatic(aggregateEnvs$, [], () => {
|
||||||
|
/* noop */
|
||||||
|
})[0]
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
@@ -49,11 +55,11 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
*/
|
*/
|
||||||
private validateEnvironmentVariables = (
|
private validateEnvironmentVariables = (
|
||||||
target: any[],
|
target: any[],
|
||||||
results: Ref<InspectorResult[]>,
|
|
||||||
locations: InspectorLocation
|
locations: InspectorLocation
|
||||||
) => {
|
) => {
|
||||||
const env = getAggregateEnvs()
|
const newErrors: InspectorResult[] = []
|
||||||
const envKeys = env.map((e) => e.key)
|
|
||||||
|
const envKeys = this.aggregateEnvs.value.map((e) => e.key)
|
||||||
|
|
||||||
target.forEach((element, index) => {
|
target.forEach((element, index) => {
|
||||||
if (isENVInString(element)) {
|
if (isENVInString(element)) {
|
||||||
@@ -83,7 +89,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!envKeys.includes(formattedExEnv)) {
|
if (!envKeys.includes(formattedExEnv)) {
|
||||||
results.value.push({
|
newErrors.push({
|
||||||
id: "environment",
|
id: "environment",
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -96,8 +102,8 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
text: this.t("inspections.environment.add_environment"),
|
text: this.t("inspections.environment.add_environment"),
|
||||||
apply: () => {
|
apply: () => {
|
||||||
invokeAction("modals.environment.add", {
|
invokeAction("modals.environment.add", {
|
||||||
envName: "test",
|
envName: formattedExEnv,
|
||||||
variableName: formattedExEnv,
|
variableName: "",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -114,54 +120,61 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return newErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
|
||||||
* Returns the inspector results for the request
|
return computed(() => {
|
||||||
* It checks if any env is used in the request ie, url, headers, params
|
const results: InspectorResult[] = []
|
||||||
* 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.headers
|
const headers = req.value.headers
|
||||||
|
|
||||||
const params = req.params
|
const params = req.value.params
|
||||||
|
|
||||||
this.validateEnvironmentVariables([req.endpoint], results, {
|
results.push(
|
||||||
type: "url",
|
...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
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Service } from "dioc"
|
|||||||
import { InspectionService, Inspector, InspectorResult } from ".."
|
import { InspectionService, Inspector, InspectorResult } from ".."
|
||||||
import { getI18n } from "~/modules/i18n"
|
import { getI18n } from "~/modules/i18n"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { markRaw, ref } from "vue"
|
import { Ref, computed, markRaw } from "vue"
|
||||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,53 +26,50 @@ export class HeaderInspectorService extends Service implements Inspector {
|
|||||||
this.inspection.registerInspector(this)
|
this.inspection.registerInspector(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private cookiesCheck(headerKey: string) {
|
||||||
* Checks if the header contains cookies
|
const cookieKeywords = ["Cookie", "Set-Cookie", "Cookie2", "Set-Cookie2"]
|
||||||
* @param req The request to inspect
|
|
||||||
* @returns The inspector results
|
|
||||||
*/
|
|
||||||
getInspectorFor(req: HoppRESTRequest): InspectorResult[] {
|
|
||||||
const results = ref<InspectorResult[]>([])
|
|
||||||
|
|
||||||
const cookiesCheck = (headerKey: string) => {
|
return cookieKeywords.includes(headerKey)
|
||||||
const cookieKeywords = ["Cookie", "Set-Cookie", "Cookie2", "Set-Cookie2"]
|
}
|
||||||
|
|
||||||
return cookieKeywords.includes(headerKey)
|
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
|
||||||
}
|
return computed(() => {
|
||||||
|
const results: InspectorResult[] = []
|
||||||
|
|
||||||
const headers = req.headers
|
const headers = req.value.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) {
|
if (isContainCookies) {
|
||||||
headerKeys.forEach((headerKey, index) => {
|
headerKeys.forEach((headerKey, index) => {
|
||||||
if (cookiesCheck(headerKey)) {
|
if (this.cookiesCheck(headerKey)) {
|
||||||
results.value.push({
|
results.push({
|
||||||
id: "header",
|
id: "header",
|
||||||
icon: markRaw(IconAlertTriangle),
|
icon: markRaw(IconAlertTriangle),
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: this.t("inspections.header.cookie"),
|
text: this.t("inspections.header.cookie"),
|
||||||
},
|
},
|
||||||
severity: 2,
|
severity: 2,
|
||||||
isApplicable: true,
|
isApplicable: true,
|
||||||
locations: {
|
locations: {
|
||||||
type: "header",
|
type: "header",
|
||||||
position: "key",
|
position: "key",
|
||||||
key: headerKey,
|
key: headerKey,
|
||||||
index: index,
|
index: index,
|
||||||
},
|
},
|
||||||
doc: {
|
doc: {
|
||||||
text: this.t("action.learn_more"),
|
text: this.t("action.learn_more"),
|
||||||
link: "https://docs.hoppscotch.io/",
|
link: "https://docs.hoppscotch.io/",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.value
|
return results
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { Service } from "dioc"
|
|||||||
import { InspectionService, Inspector, InspectorResult } from ".."
|
import { InspectionService, Inspector, InspectorResult } from ".."
|
||||||
import { getI18n } from "~/modules/i18n"
|
import { getI18n } from "~/modules/i18n"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { markRaw, ref } from "vue"
|
import { markRaw } from "vue"
|
||||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
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.
|
* This inspector is responsible for inspecting the response of a request.
|
||||||
@@ -27,47 +29,50 @@ export class ResponseInspectorService extends Service implements Inspector {
|
|||||||
this.inspection.registerInspector(this)
|
this.inspection.registerInspector(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
getInspectorFor(
|
getInspections(
|
||||||
req: HoppRESTRequest,
|
_req: Readonly<Ref<HoppRESTRequest>>,
|
||||||
res: HoppRESTResponse | undefined
|
res: Readonly<Ref<HoppRESTResponse | null | undefined>>
|
||||||
): InspectorResult[] {
|
) {
|
||||||
const results = ref<InspectorResult[]>([])
|
return computed(() => {
|
||||||
if (!res) return results.value
|
const results: InspectorResult[] = []
|
||||||
|
if (!res.value) return results
|
||||||
|
|
||||||
const hasErrors = res && (res.type !== "success" || res.statusCode !== 200)
|
const hasErrors =
|
||||||
|
res && (res.value.type !== "success" || res.value.statusCode !== 200)
|
||||||
|
|
||||||
let text
|
let text: string | undefined = undefined
|
||||||
|
|
||||||
if (res.type === "network_fail") {
|
if (res.value.type === "network_fail" && !navigator.onLine) {
|
||||||
text = this.t("inspections.response.network_error")
|
text = this.t("inspections.response.network_error")
|
||||||
} else if (res.type === "fail") {
|
} else if (res.value.type === "fail") {
|
||||||
text = this.t("inspections.response.default_error")
|
text = this.t("inspections.response.default_error")
|
||||||
} else if (res.type === "success" && res.statusCode === 404) {
|
} else if (res.value.type === "success" && res.value.statusCode === 404) {
|
||||||
text = this.t("inspections.response.404_error")
|
text = this.t("inspections.response.404_error")
|
||||||
} else if (res.type === "success" && res.statusCode === 401) {
|
} else if (res.value.type === "success" && res.value.statusCode === 401) {
|
||||||
text = this.t("inspections.response.401_error")
|
text = this.t("inspections.response.401_error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasErrors && text) {
|
if (hasErrors && text) {
|
||||||
results.value.push({
|
results.push({
|
||||||
id: "url",
|
id: "url",
|
||||||
icon: markRaw(IconAlertTriangle),
|
icon: markRaw(IconAlertTriangle),
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: text,
|
text: text,
|
||||||
},
|
},
|
||||||
severity: 2,
|
severity: 2,
|
||||||
isApplicable: true,
|
isApplicable: true,
|
||||||
locations: {
|
locations: {
|
||||||
type: "response",
|
type: "response",
|
||||||
},
|
},
|
||||||
doc: {
|
doc: {
|
||||||
text: this.t("action.learn_more"),
|
text: this.t("action.learn_more"),
|
||||||
link: "https://docs.hoppscotch.io/",
|
link: "https://docs.hoppscotch.io/",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.value
|
return results
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -91,8 +91,9 @@ export abstract class StaticSpotlightSearcherService<
|
|||||||
private async addDocsToSearchIndex() {
|
private async addDocsToSearchIndex() {
|
||||||
this.loading.value = true
|
this.loading.value = true
|
||||||
|
|
||||||
this.minisearch.removeAll()
|
this.minisearch = new MiniSearch({
|
||||||
this.minisearch.vacuum()
|
fields: this.opts.searchFields as string[],
|
||||||
|
})
|
||||||
|
|
||||||
await this.minisearch.addAllAsync(
|
await this.minisearch.addAllAsync(
|
||||||
Object.entries(this._documents).map(([id, doc]) => ({
|
Object.entries(this._documents).map(([id, doc]) => ({
|
||||||
|
|||||||
@@ -45,8 +45,11 @@ import {
|
|||||||
setSelectedEnvironmentIndex,
|
setSelectedEnvironmentIndex,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
|
|
||||||
|
import IconCheckCircle from "~/components/app/spotlight/entry/IconSelected.vue"
|
||||||
|
import IconCircle from "~icons/lucide/circle"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string
|
text: string | string[]
|
||||||
alternates: string[]
|
alternates: string[]
|
||||||
icon: object | Component
|
icon: object | Component
|
||||||
excludeFromSearch?: boolean
|
excludeFromSearch?: boolean
|
||||||
@@ -88,40 +91,61 @@ export class EnvironmentsSpotlightSearcherService extends StaticSpotlightSearche
|
|||||||
|
|
||||||
private documents: Record<string, Doc> = reactive({
|
private documents: Record<string, Doc> = reactive({
|
||||||
new_environment: {
|
new_environment: {
|
||||||
text: this.t("spotlight.environments.new"),
|
text: [
|
||||||
|
this.t("spotlight.environments.title"),
|
||||||
|
this.t("spotlight.environments.new"),
|
||||||
|
],
|
||||||
alternates: ["new", "environment"],
|
alternates: ["new", "environment"],
|
||||||
icon: markRaw(IconLayers),
|
icon: markRaw(IconLayers),
|
||||||
},
|
},
|
||||||
new_environment_variable: {
|
new_environment_variable: {
|
||||||
text: this.t("spotlight.environments.new_variable"),
|
text: [
|
||||||
|
this.t("spotlight.environments.title"),
|
||||||
|
this.t("spotlight.environments.new_variable"),
|
||||||
|
],
|
||||||
alternates: ["new", "environment", "variable"],
|
alternates: ["new", "environment", "variable"],
|
||||||
icon: markRaw(IconLayers),
|
icon: markRaw(IconLayers),
|
||||||
},
|
},
|
||||||
edit_selected_env: {
|
edit_selected_env: {
|
||||||
text: this.t("spotlight.environments.edit"),
|
text: [
|
||||||
|
this.t("spotlight.environments.title"),
|
||||||
|
this.t("spotlight.environments.edit"),
|
||||||
|
],
|
||||||
alternates: ["edit", "environment"],
|
alternates: ["edit", "environment"],
|
||||||
icon: markRaw(IconEdit),
|
icon: markRaw(IconEdit),
|
||||||
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
|
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
|
||||||
},
|
},
|
||||||
delete_selected_env: {
|
delete_selected_env: {
|
||||||
text: this.t("spotlight.environments.delete"),
|
text: [
|
||||||
|
this.t("spotlight.environments.title"),
|
||||||
|
this.t("spotlight.environments.delete"),
|
||||||
|
],
|
||||||
alternates: ["delete", "environment"],
|
alternates: ["delete", "environment"],
|
||||||
icon: markRaw(IconTrash2),
|
icon: markRaw(IconTrash2),
|
||||||
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
|
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
|
||||||
},
|
},
|
||||||
duplicate_selected_env: {
|
duplicate_selected_env: {
|
||||||
text: this.t("spotlight.environments.duplicate"),
|
text: [
|
||||||
|
this.t("spotlight.environments.title"),
|
||||||
|
this.t("spotlight.environments.duplicate"),
|
||||||
|
],
|
||||||
alternates: ["duplicate", "environment"],
|
alternates: ["duplicate", "environment"],
|
||||||
icon: markRaw(IconCopy),
|
icon: markRaw(IconCopy),
|
||||||
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
|
excludeFromSearch: computed(() => !this.hasSelectedEnv.value),
|
||||||
},
|
},
|
||||||
edit_global_env: {
|
edit_global_env: {
|
||||||
text: this.t("spotlight.environments.edit_global"),
|
text: [
|
||||||
|
this.t("spotlight.environments.title"),
|
||||||
|
this.t("spotlight.environments.edit_global"),
|
||||||
|
],
|
||||||
alternates: ["edit", "global", "environment"],
|
alternates: ["edit", "global", "environment"],
|
||||||
icon: markRaw(IconEdit),
|
icon: markRaw(IconEdit),
|
||||||
},
|
},
|
||||||
duplicate_global_env: {
|
duplicate_global_env: {
|
||||||
text: this.t("spotlight.environments.duplicate_global"),
|
text: [
|
||||||
|
this.t("spotlight.environments.title"),
|
||||||
|
this.t("spotlight.environments.duplicate_global"),
|
||||||
|
],
|
||||||
alternates: ["duplicate", "global", "environment"],
|
alternates: ["duplicate", "global", "environment"],
|
||||||
icon: markRaw(IconCopy),
|
icon: markRaw(IconCopy),
|
||||||
},
|
},
|
||||||
@@ -245,6 +269,16 @@ export class SwitchEnvSpotlightSearcherService
|
|||||||
this.spotlight.registerSearcher(this)
|
this.spotlight.registerSearcher(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private selectedEnvIndex = useStreamStatic(
|
||||||
|
selectedEnvironmentIndex$,
|
||||||
|
{
|
||||||
|
type: "NO_ENV_SELECTED",
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
)[0]
|
||||||
|
|
||||||
private environmentSearchable = useStreamStatic(
|
private environmentSearchable = useStreamStatic(
|
||||||
activeActions$.pipe(
|
activeActions$.pipe(
|
||||||
map((actions) => actions.includes("modals.environment.add"))
|
map((actions) => actions.includes("modals.environment.add"))
|
||||||
@@ -262,16 +296,25 @@ export class SwitchEnvSpotlightSearcherService
|
|||||||
const results = ref<SpotlightSearcherResult[]>([])
|
const results = ref<SpotlightSearcherResult[]>([])
|
||||||
|
|
||||||
const minisearch = new MiniSearch({
|
const minisearch = new MiniSearch({
|
||||||
fields: ["name"],
|
fields: ["name", "alternates"],
|
||||||
storeFields: ["name"],
|
storeFields: ["name"],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.environmentSearchable.value) {
|
if (this.environmentSearchable.value) {
|
||||||
minisearch.addAll(
|
minisearch.addAll(
|
||||||
environmentsStore.value.environments.map((entry, index) => {
|
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 {
|
return {
|
||||||
id: `environment-${index}`,
|
id,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
|
alternates: ["environment", "change", entry.name],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -298,7 +341,9 @@ export class SwitchEnvSpotlightSearcherService
|
|||||||
.map((x) => {
|
.map((x) => {
|
||||||
return {
|
return {
|
||||||
id: x.id,
|
id: x.id,
|
||||||
icon: markRaw(IconLayers),
|
icon: markRaw(
|
||||||
|
x.id.endsWith("-selected") ? IconCheckCircle : IconCircle
|
||||||
|
),
|
||||||
score: x.score,
|
score: x.score,
|
||||||
text: {
|
text: {
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ import {
|
|||||||
|
|
||||||
import IconLinkedIn from "~icons/brands/linkedin"
|
import IconLinkedIn from "~icons/brands/linkedin"
|
||||||
import IconTwitter from "~icons/brands/twitter"
|
import IconTwitter from "~icons/brands/twitter"
|
||||||
import IconBook from "~icons/lucide/book"
|
import IconDiscord from "~icons/brands/discord"
|
||||||
import IconDiscord from "~icons/lucide/link"
|
|
||||||
import IconGitHub from "~icons/lucide/github"
|
import IconGitHub from "~icons/lucide/github"
|
||||||
|
import IconBook from "~icons/lucide/book"
|
||||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||||
import IconMessageCircle from "~icons/lucide/message-circle"
|
|
||||||
import IconZap from "~icons/lucide/zap"
|
import IconZap from "~icons/lucide/zap"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string | string[]
|
text: string | string[]
|
||||||
alternates: string[]
|
alternates: string[]
|
||||||
icon: object | Component
|
icon: object | Component
|
||||||
|
action: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,41 +43,48 @@ export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
text: this.t("spotlight.general.help_menu"),
|
text: this.t("spotlight.general.help_menu"),
|
||||||
alternates: ["help", "hoppscotch"],
|
alternates: ["help", "hoppscotch"],
|
||||||
icon: markRaw(IconLifeBuoy),
|
icon: markRaw(IconLifeBuoy),
|
||||||
},
|
action() {
|
||||||
chat_with_support: {
|
invokeAction("modals.support.toggle")
|
||||||
text: this.t("spotlight.general.chat"),
|
},
|
||||||
alternates: ["chat", "support", "hoppscotch"],
|
|
||||||
icon: markRaw(IconMessageCircle),
|
|
||||||
},
|
},
|
||||||
open_docs: {
|
open_docs: {
|
||||||
text: this.t("spotlight.general.open_docs"),
|
text: this.t("spotlight.general.open_docs"),
|
||||||
alternates: ["docs", "documentation", "hoppscotch"],
|
alternates: ["docs", "documentation", "hoppscotch"],
|
||||||
icon: markRaw(IconBook),
|
icon: markRaw(IconBook),
|
||||||
|
action: () => this.openURL("https://docs.hoppscotch.io"),
|
||||||
},
|
},
|
||||||
open_keybindings: {
|
open_keybindings: {
|
||||||
text: this.t("spotlight.general.open_keybindings"),
|
text: this.t("spotlight.general.open_keybindings"),
|
||||||
alternates: ["key", "shortcuts", "binding"],
|
alternates: ["key", "shortcuts", "binding"],
|
||||||
icon: markRaw(IconZap),
|
icon: markRaw(IconZap),
|
||||||
|
action() {
|
||||||
|
invokeAction("flyouts.keybinds.toggle")
|
||||||
|
},
|
||||||
},
|
},
|
||||||
link_github: {
|
open_github: {
|
||||||
text: [this.t("spotlight.general.social"), "GitHub"],
|
text: this.t("spotlight.general.open_github"),
|
||||||
alternates: ["social", "github", "link"],
|
alternates: ["repository", "github", "documentation", "hoppscotch"],
|
||||||
icon: markRaw(IconGitHub),
|
icon: markRaw(IconGitHub),
|
||||||
|
action: () => this.openURL("https://hoppscotch.io/github"),
|
||||||
},
|
},
|
||||||
link_twitter: {
|
link_twitter: {
|
||||||
text: [this.t("spotlight.general.social"), "Twitter"],
|
text: [this.t("spotlight.general.social"), "Twitter"],
|
||||||
alternates: ["social", "twitter", "link"],
|
alternates: ["social", "twitter", "link"],
|
||||||
icon: markRaw(IconTwitter),
|
icon: markRaw(IconTwitter),
|
||||||
|
action: () => this.openURL("https://twitter.com/hoppscotch_io"),
|
||||||
},
|
},
|
||||||
link_discord: {
|
link_discord: {
|
||||||
text: [this.t("spotlight.general.social"), "Discord"],
|
text: [this.t("spotlight.general.social"), "Discord"],
|
||||||
alternates: ["social", "discord", "link"],
|
alternates: ["social", "discord", "link"],
|
||||||
icon: markRaw(IconDiscord),
|
icon: markRaw(IconDiscord),
|
||||||
|
action: () => this.openURL("https://hoppscotch.io/discord"),
|
||||||
},
|
},
|
||||||
link_linkedin: {
|
link_linkedin: {
|
||||||
text: [this.t("spotlight.general.social"), "LinkedIn"],
|
text: [this.t("spotlight.general.social"), "LinkedIn"],
|
||||||
alternates: ["social", "linkedin", "link"],
|
alternates: ["social", "linkedin", "link"],
|
||||||
icon: markRaw(IconLinkedIn),
|
icon: markRaw(IconLinkedIn),
|
||||||
|
action: () =>
|
||||||
|
this.openURL("https://www.linkedin.com/company/hoppscotch/"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -110,31 +117,11 @@ export class GeneralSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDocSelected(id: string): void {
|
public onDocSelected(id: string): void {
|
||||||
switch (id) {
|
this.documents[id]?.action()
|
||||||
case "open_help":
|
}
|
||||||
invokeAction("modals.support.toggle")
|
|
||||||
break
|
public addCustomEntries(docs: Record<string, Doc>) {
|
||||||
case "chat_with_support":
|
this.documents = { ...this.documents, ...docs }
|
||||||
invokeAction("flyouts.chat.open")
|
this.setDocuments(this.documents)
|
||||||
break
|
|
||||||
case "open_docs":
|
|
||||||
this.openURL("https://docs.hoppscotch.io")
|
|
||||||
break
|
|
||||||
case "open_keybindings":
|
|
||||||
invokeAction("flyouts.keybinds.toggle")
|
|
||||||
break
|
|
||||||
case "link_github":
|
|
||||||
this.openURL("https://hoppscotch.io/github")
|
|
||||||
break
|
|
||||||
case "link_twitter":
|
|
||||||
this.openURL("https://twitter.com/hoppscotch_io")
|
|
||||||
break
|
|
||||||
case "link_discord":
|
|
||||||
this.openURL("https://hoppscotch.io/discord")
|
|
||||||
break
|
|
||||||
case "link_linkedin":
|
|
||||||
this.openURL("https://www.linkedin.com/company/hoppscotch/")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, computed, markRaw, reactive } from "vue"
|
import { Component, computed, markRaw, reactive } from "vue"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction, isActionBound } from "~/helpers/actions"
|
||||||
import { getI18n } from "~/modules/i18n"
|
import { getI18n } from "~/modules/i18n"
|
||||||
import { SpotlightSearcherResult, SpotlightService } from ".."
|
import { SpotlightSearcherResult, SpotlightService } from ".."
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +19,7 @@ import IconRename from "~icons/lucide/file-edit"
|
|||||||
import IconPlay from "~icons/lucide/play"
|
import IconPlay from "~icons/lucide/play"
|
||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
import IconSave from "~icons/lucide/save"
|
import IconSave from "~icons/lucide/save"
|
||||||
|
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string | string[]
|
text: string | string[]
|
||||||
@@ -46,39 +47,51 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
private route = useRoute()
|
private route = useRoute()
|
||||||
private isRESTPage = computed(() => this.route.name === "index")
|
private isRESTPage = computed(() => this.route.name === "index")
|
||||||
private isGQLPage = computed(() => this.route.name === "graphql")
|
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({
|
private documents: Record<string, Doc> = reactive({
|
||||||
send_request: {
|
send_request: {
|
||||||
text: this.t("shortcut.request.send_request"),
|
text: this.t("shortcut.request.send_request"),
|
||||||
alternates: ["request", "send"],
|
alternates: ["request", "send"],
|
||||||
icon: markRaw(IconPlay),
|
icon: markRaw(IconPlay),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.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: {
|
save_to_collections: {
|
||||||
text: this.t("spotlight.request.save_as_new"),
|
text: this.t("spotlight.request.save_as_new"),
|
||||||
alternates: ["save", "collections"],
|
alternates: ["save", "collections"],
|
||||||
icon: markRaw(IconSave),
|
icon: markRaw(IconSave),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
save_request: {
|
save_request: {
|
||||||
text: this.t("shortcut.request.save_request"),
|
text: this.t("shortcut.request.save_request"),
|
||||||
alternates: ["save", "request"],
|
alternates: ["save", "request"],
|
||||||
icon: markRaw(IconSave),
|
icon: markRaw(IconSave),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
rename_request: {
|
rename_request: {
|
||||||
text: this.t("shortcut.request.rename"),
|
text: this.t("shortcut.request.rename"),
|
||||||
alternates: ["rename", "request"],
|
alternates: ["rename", "request"],
|
||||||
icon: markRaw(IconRename),
|
icon: markRaw(IconRename),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
copy_request_link: {
|
copy_request_link: {
|
||||||
text: this.t("shortcut.request.copy_request_link"),
|
text: this.t("shortcut.request.copy_request_link"),
|
||||||
@@ -90,7 +103,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
text: this.t("shortcut.request.reset_request"),
|
text: this.t("shortcut.request.reset_request"),
|
||||||
alternates: ["reset", "request"],
|
alternates: ["reset", "request"],
|
||||||
icon: markRaw(IconRotateCCW),
|
icon: markRaw(IconRotateCCW),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
},
|
},
|
||||||
import_curl: {
|
import_curl: {
|
||||||
text: this.t("shortcut.request.import_curl"),
|
text: this.t("shortcut.request.import_curl"),
|
||||||
@@ -143,9 +156,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
],
|
],
|
||||||
alternates: ["parameters", "tab"],
|
alternates: ["parameters", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tab_body: {
|
tab_body: {
|
||||||
text: [
|
text: [
|
||||||
@@ -154,9 +165,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
],
|
],
|
||||||
alternates: ["body", "tab"],
|
alternates: ["body", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tab_headers: {
|
tab_headers: {
|
||||||
text: [
|
text: [
|
||||||
@@ -165,9 +174,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
],
|
],
|
||||||
alternates: ["headers", "tab"],
|
alternates: ["headers", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tab_authorization: {
|
tab_authorization: {
|
||||||
text: [
|
text: [
|
||||||
@@ -176,9 +183,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
],
|
],
|
||||||
alternates: ["authorization", "tab"],
|
alternates: ["authorization", "tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(() => !this.isRESTOrGQLPage.value),
|
||||||
() => !this.isRESTPage.value ?? !this.isGQLPage.value
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
tab_pre_request_script: {
|
tab_pre_request_script: {
|
||||||
text: [
|
text: [
|
||||||
@@ -198,6 +203,24 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconWindow),
|
||||||
excludeFromSearch: computed(() => !this.isRESTPage.value),
|
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),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -224,7 +247,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private openRequestTab(tab: RequestOptionTabs): void {
|
private openRequestTab(tab: RequestOptionTabs | GQLOptionTabs): void {
|
||||||
invokeAction("request.open-tab", {
|
invokeAction("request.open-tab", {
|
||||||
tab,
|
tab,
|
||||||
})
|
})
|
||||||
@@ -235,6 +258,12 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
case "send_request":
|
case "send_request":
|
||||||
invokeAction("request.send-cancel")
|
invokeAction("request.send-cancel")
|
||||||
break
|
break
|
||||||
|
case "gql_connect":
|
||||||
|
invokeAction("gql.connect")
|
||||||
|
break
|
||||||
|
case "gql_disconnect":
|
||||||
|
invokeAction("gql.disconnect")
|
||||||
|
break
|
||||||
case "save_to_collections":
|
case "save_to_collections":
|
||||||
invokeAction("request.save-as", {
|
invokeAction("request.save-as", {
|
||||||
requestType: "rest",
|
requestType: "rest",
|
||||||
@@ -245,7 +274,7 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
invokeAction("request.save")
|
invokeAction("request.save")
|
||||||
break
|
break
|
||||||
case "rename_request":
|
case "rename_request":
|
||||||
invokeAction("rest.request.rename")
|
invokeAction("request.rename")
|
||||||
break
|
break
|
||||||
case "copy_request_link":
|
case "copy_request_link":
|
||||||
invokeAction("request.copy-link")
|
invokeAction("request.copy-link")
|
||||||
@@ -292,6 +321,12 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
|
|||||||
case "tab_tests":
|
case "tab_tests":
|
||||||
this.openRequestTab("tests")
|
this.openRequestTab("tests")
|
||||||
break
|
break
|
||||||
|
case "tab_query":
|
||||||
|
this.openRequestTab("query")
|
||||||
|
break
|
||||||
|
case "tab_variables":
|
||||||
|
this.openRequestTab("variables")
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, computed, markRaw, reactive } from "vue"
|
import { Component, computed, markRaw, reactive } from "vue"
|
||||||
import { activeActions$, invokeAction } from "~/helpers/actions"
|
import { invokeAction, isActionBound } from "~/helpers/actions"
|
||||||
import { getI18n } from "~/modules/i18n"
|
import { getI18n } from "~/modules/i18n"
|
||||||
import { SpotlightSearcherResult, SpotlightService } from ".."
|
import { SpotlightSearcherResult, SpotlightService } from ".."
|
||||||
import {
|
import {
|
||||||
@@ -9,8 +9,6 @@ import {
|
|||||||
|
|
||||||
import IconDownload from "~icons/lucide/download"
|
import IconDownload from "~icons/lucide/download"
|
||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import { map } from "rxjs"
|
|
||||||
import { useStreamStatic } from "~/composables/stream"
|
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string
|
text: string
|
||||||
@@ -35,23 +33,11 @@ export class ResponseSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
|
|
||||||
private readonly spotlight = this.bind(SpotlightService)
|
private readonly spotlight = this.bind(SpotlightService)
|
||||||
|
|
||||||
private copyResponseActionEnabled = useStreamStatic(
|
private copyResponseActionEnabled = isActionBound("response.copy")
|
||||||
activeActions$.pipe(map((actions) => actions.includes("response.copy"))),
|
|
||||||
activeActions$.value.includes("response.copy"),
|
|
||||||
() => {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
private downloadResponseActionEnabled = useStreamStatic(
|
private downloadResponseActionEnabled = isActionBound(
|
||||||
activeActions$.pipe(
|
"response.file.download"
|
||||||
map((actions) => actions.includes("response.file.download"))
|
)
|
||||||
),
|
|
||||||
activeActions$.value.includes("response.file.download"),
|
|
||||||
() => {
|
|
||||||
/* noop */
|
|
||||||
}
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
private documents: Record<string, Doc> = reactive({
|
private documents: Record<string, Doc> = reactive({
|
||||||
copy_response: {
|
copy_response: {
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import IconGlobe from "~icons/lucide/globe"
|
|||||||
import IconMonitor from "~icons/lucide/monitor"
|
import IconMonitor from "~icons/lucide/monitor"
|
||||||
import IconMoon from "~icons/lucide/moon"
|
import IconMoon from "~icons/lucide/moon"
|
||||||
import IconSun from "~icons/lucide/sun"
|
import IconSun from "~icons/lucide/sun"
|
||||||
import IconType from "~icons/lucide/type"
|
import IconCircle from "~icons/lucide/circle"
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string | string[]
|
text: string | string[]
|
||||||
excludeFromSearch?: boolean
|
|
||||||
alternates: string[]
|
alternates: string[]
|
||||||
icon: object | Component
|
icon: object | Component
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
private t = getI18n()
|
private t = getI18n()
|
||||||
|
|
||||||
private activeFontSize = useSetting("FONT_SIZE")
|
private activeFontSize = useSetting("FONT_SIZE")
|
||||||
|
private activeTheme = useSetting("BG_COLOR")
|
||||||
|
|
||||||
public readonly searcherID = "settings"
|
public readonly searcherID = "settings"
|
||||||
public searcherSectionTitle = this.t("navigation.settings")
|
public searcherSectionTitle = this.t("navigation.settings")
|
||||||
@@ -48,7 +49,11 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
this.t("spotlight.settings.theme.system"),
|
this.t("spotlight.settings.theme.system"),
|
||||||
],
|
],
|
||||||
alternates: ["theme"],
|
alternates: ["theme"],
|
||||||
icon: markRaw(IconMonitor),
|
icon: computed(() =>
|
||||||
|
this.activeTheme.value === "system"
|
||||||
|
? markRaw(IconCheckCircle)
|
||||||
|
: markRaw(IconMonitor)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
theme_light: {
|
theme_light: {
|
||||||
text: [
|
text: [
|
||||||
@@ -56,7 +61,11 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
this.t("spotlight.settings.theme.light"),
|
this.t("spotlight.settings.theme.light"),
|
||||||
],
|
],
|
||||||
alternates: ["theme"],
|
alternates: ["theme"],
|
||||||
icon: markRaw(IconSun),
|
icon: computed(() =>
|
||||||
|
this.activeTheme.value === "light"
|
||||||
|
? markRaw(IconCheckCircle)
|
||||||
|
: markRaw(IconSun)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
theme_dark: {
|
theme_dark: {
|
||||||
text: [
|
text: [
|
||||||
@@ -64,7 +73,11 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
this.t("spotlight.settings.theme.dark"),
|
this.t("spotlight.settings.theme.dark"),
|
||||||
],
|
],
|
||||||
alternates: ["theme"],
|
alternates: ["theme"],
|
||||||
icon: markRaw(IconCloud),
|
icon: computed(() =>
|
||||||
|
this.activeTheme.value === "dark"
|
||||||
|
? markRaw(IconCheckCircle)
|
||||||
|
: markRaw(IconCloud)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
theme_black: {
|
theme_black: {
|
||||||
text: [
|
text: [
|
||||||
@@ -72,7 +85,11 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
this.t("spotlight.settings.theme.black"),
|
this.t("spotlight.settings.theme.black"),
|
||||||
],
|
],
|
||||||
alternates: ["theme"],
|
alternates: ["theme"],
|
||||||
icon: markRaw(IconMoon),
|
icon: computed(() =>
|
||||||
|
this.activeTheme.value === "black"
|
||||||
|
? markRaw(IconCheckCircle)
|
||||||
|
: markRaw(IconMoon)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
font_size_sm: {
|
font_size_sm: {
|
||||||
text: [
|
text: [
|
||||||
@@ -82,42 +99,51 @@ export class SettingsSpotlightSearcherService extends StaticSpotlightSearcherSer
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
console.log("clicked")
|
console.log("clicked")
|
||||||
},
|
},
|
||||||
excludeFromSearch: computed(() => this.activeFontSize.value === "small"),
|
|
||||||
alternates: [
|
alternates: [
|
||||||
"font size",
|
"font size",
|
||||||
"change font size",
|
"change font size",
|
||||||
"change font",
|
"change font",
|
||||||
"increase font",
|
"increase font",
|
||||||
],
|
],
|
||||||
icon: markRaw(IconType),
|
icon: computed(() =>
|
||||||
|
this.activeFontSize.value === "small"
|
||||||
|
? markRaw(IconCheckCircle)
|
||||||
|
: markRaw(IconCircle)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
font_size_md: {
|
font_size_md: {
|
||||||
text: [
|
text: [
|
||||||
this.t("settings.font_size"),
|
this.t("settings.font_size"),
|
||||||
this.t("spotlight.settings.font.size_md"),
|
this.t("spotlight.settings.font.size_md"),
|
||||||
],
|
],
|
||||||
excludeFromSearch: computed(() => this.activeFontSize.value === "medium"),
|
|
||||||
alternates: [
|
alternates: [
|
||||||
"font size",
|
"font size",
|
||||||
"change font size",
|
"change font size",
|
||||||
"change font",
|
"change font",
|
||||||
"increase font",
|
"increase font",
|
||||||
],
|
],
|
||||||
icon: markRaw(IconType),
|
icon: computed(() =>
|
||||||
|
this.activeFontSize.value === "medium"
|
||||||
|
? markRaw(IconCheckCircle)
|
||||||
|
: markRaw(IconCircle)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
font_size_lg: {
|
font_size_lg: {
|
||||||
text: [
|
text: [
|
||||||
this.t("settings.font_size"),
|
this.t("settings.font_size"),
|
||||||
this.t("spotlight.settings.font.size_lg"),
|
this.t("spotlight.settings.font.size_lg"),
|
||||||
],
|
],
|
||||||
excludeFromSearch: computed(() => this.activeFontSize.value === "large"),
|
|
||||||
alternates: [
|
alternates: [
|
||||||
"font size",
|
"font size",
|
||||||
"change font size",
|
"change font size",
|
||||||
"change font",
|
"change font",
|
||||||
"increase font",
|
"increase font",
|
||||||
],
|
],
|
||||||
icon: markRaw(IconType),
|
icon: computed(() =>
|
||||||
|
this.activeFontSize.value === "large"
|
||||||
|
? markRaw(IconCheckCircle)
|
||||||
|
: markRaw(IconCircle)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
change_lang: {
|
change_lang: {
|
||||||
text: [
|
text: [
|
||||||
|
|||||||
@@ -7,19 +7,16 @@ import {
|
|||||||
} from "./base/static.searcher"
|
} from "./base/static.searcher"
|
||||||
|
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import {
|
import IconCopyPlus from "~icons/lucide/copy-plus"
|
||||||
closeOtherTabs,
|
import IconXCircle from "~icons/lucide/x-circle"
|
||||||
closeTab,
|
import IconXSquare from "~icons/lucide/x-square"
|
||||||
createNewTab,
|
|
||||||
currentTabID,
|
|
||||||
getActiveTabs,
|
|
||||||
} from "~/helpers/rest/tab"
|
|
||||||
import IconWindow from "~icons/lucide/app-window"
|
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
|
import { getActiveTabs as getRESTActiveTabs } from "~/helpers/rest/tab"
|
||||||
|
import { getActiveTabs as getGQLActiveTabs } from "~/helpers/graphql/tab"
|
||||||
|
|
||||||
type Doc = {
|
type Doc = {
|
||||||
text: string
|
text: string | string[]
|
||||||
alternates: string[]
|
alternates: string[]
|
||||||
icon: object | Component
|
icon: object | Component
|
||||||
excludeFromSearch?: boolean
|
excludeFromSearch?: boolean
|
||||||
@@ -43,36 +40,49 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||||||
|
|
||||||
private route = useRoute()
|
private route = useRoute()
|
||||||
private showAction = computed(
|
private showAction = computed(
|
||||||
() => this.route.name === "index" ?? this.route.name === "graphql"
|
() => 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({
|
private documents: Record<string, Doc> = reactive({
|
||||||
duplicate_tab: {
|
duplicate_tab: {
|
||||||
text: this.t("spotlight.tab.duplicate"),
|
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.duplicate")],
|
||||||
alternates: ["tab", "duplicate", "duplicate tab"],
|
alternates: ["tab", "duplicate", "duplicate tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconCopy),
|
||||||
excludeFromSearch: computed(() => !this.showAction.value),
|
excludeFromSearch: computed(() => !this.showAction.value),
|
||||||
},
|
},
|
||||||
close_current_tab: {
|
close_current_tab: {
|
||||||
text: this.t("spotlight.tab.close_current"),
|
text: [
|
||||||
|
this.t("spotlight.tab.title"),
|
||||||
|
this.t("spotlight.tab.close_current"),
|
||||||
|
],
|
||||||
alternates: ["tab", "close", "close tab"],
|
alternates: ["tab", "close", "close tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconXCircle),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(
|
||||||
() => !this.showAction.value ?? getActiveTabs().value.length === 1
|
() => !this.showAction.value || this.isOnlyTab.value
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
close_other_tabs: {
|
close_other_tabs: {
|
||||||
text: this.t("spotlight.tab.close_others"),
|
text: [
|
||||||
|
this.t("spotlight.tab.title"),
|
||||||
|
this.t("spotlight.tab.close_others"),
|
||||||
|
],
|
||||||
alternates: ["tab", "close", "close all"],
|
alternates: ["tab", "close", "close all"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconXSquare),
|
||||||
excludeFromSearch: computed(
|
excludeFromSearch: computed(
|
||||||
() => !this.showAction.value ?? getActiveTabs().value.length < 2
|
() => !this.showAction.value || this.isOnlyTab.value
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
open_new_tab: {
|
open_new_tab: {
|
||||||
text: this.t("spotlight.tab.new_tab"),
|
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.new_tab")],
|
||||||
alternates: ["tab", "new", "open tab"],
|
alternates: ["tab", "new", "open tab"],
|
||||||
icon: markRaw(IconWindow),
|
icon: markRaw(IconCopyPlus),
|
||||||
excludeFromSearch: computed(() => !this.showAction.value),
|
excludeFromSearch: computed(() => !this.showAction.value),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -102,16 +112,9 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDocSelected(id: string): void {
|
public onDocSelected(id: string): void {
|
||||||
if (id === "duplicate_tab")
|
if (id === "duplicate_tab") invokeAction("tab.duplicate-tab", {})
|
||||||
invokeAction("request.duplicate-tab", {
|
if (id === "close_current_tab") invokeAction("tab.close-current")
|
||||||
tabID: currentTabID.value,
|
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/selfhost-web",
|
"name": "@hoppscotch/selfhost-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.8",
|
"version": "2023.8.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
@@ -28,8 +28,8 @@
|
|||||||
"@fontsource-variable/roboto-mono": "^5.0.9",
|
"@fontsource-variable/roboto-mono": "^5.0.9",
|
||||||
"@hoppscotch/common": "workspace:^",
|
"@hoppscotch/common": "workspace:^",
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"axios": "^1.4.0",
|
|
||||||
"@import-meta-env/unplugin": "^0.4.8",
|
"@import-meta-env/unplugin": "^0.4.8",
|
||||||
|
"axios": "^1.4.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"fp-ts": "^2.16.1",
|
"fp-ts": "^2.16.1",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
"unplugin-icons": "^0.16.5",
|
"unplugin-icons": "^0.16.5",
|
||||||
"unplugin-vue-components": "^0.25.1",
|
"unplugin-vue-components": "^0.25.1",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
|
"vite-plugin-fonts": "^0.6.0",
|
||||||
"vite-plugin-html-config": "^1.0.11",
|
"vite-plugin-html-config": "^1.0.11",
|
||||||
"vite-plugin-inspect": "^0.7.38",
|
"vite-plugin-inspect": "^0.7.38",
|
||||||
"vite-plugin-pages": "^0.31.0",
|
"vite-plugin-pages": "^0.31.0",
|
||||||
@@ -75,7 +76,6 @@
|
|||||||
"vite-plugin-windicss": "^1.9.1",
|
"vite-plugin-windicss": "^1.9.1",
|
||||||
"vitest": "^0.34.2",
|
"vitest": "^0.34.2",
|
||||||
"vue-tsc": "^1.8.8",
|
"vue-tsc": "^1.8.8",
|
||||||
"vite-plugin-fonts": "^0.6.0",
|
|
||||||
"windicss": "^3.5.6"
|
"windicss": "^3.5.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ import { def as historyDef } from "./platform/history/history.platform"
|
|||||||
import { def as tabStateDef } from "./platform/tabState/tabState.platform"
|
import { def as tabStateDef } from "./platform/tabState/tabState.platform"
|
||||||
import { browserInterceptor } from "@hoppscotch/common/platform/std/interceptors/browser"
|
import { browserInterceptor } from "@hoppscotch/common/platform/std/interceptors/browser"
|
||||||
import { proxyInterceptor } from "@hoppscotch/common/platform/std/interceptors/proxy"
|
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 { 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", {
|
createHoppApp("#app", {
|
||||||
|
ui: {
|
||||||
|
additionalFooterMenuItems: stdFooterItems,
|
||||||
|
additionalSupportOptionsMenuItems: stdSupportOptionItems,
|
||||||
|
},
|
||||||
auth: authDef,
|
auth: authDef,
|
||||||
sync: {
|
sync: {
|
||||||
environments: environmentsDef,
|
environments: environmentsDef,
|
||||||
@@ -26,6 +33,9 @@ createHoppApp("#app", {
|
|||||||
{ type: "service", service: ExtensionInterceptorService },
|
{ type: "service", service: ExtensionInterceptorService },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
additionalInspectors: [
|
||||||
|
{ type: "service", service: ExtensionInspectorService },
|
||||||
|
],
|
||||||
platformFeatureFlags: {
|
platformFeatureFlags: {
|
||||||
exportAsGIST: false,
|
exportAsGIST: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-font-size="large">
|
<html lang="en" data-font-size="medium">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<script>
|
<script>
|
||||||
@@ -17,4 +17,4 @@
|
|||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-sh-admin",
|
"name": "hoppscotch-sh-admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.8",
|
"version": "2023.8.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
|
|||||||
43
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
43
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -1,44 +1,12 @@
|
|||||||
// generated by unplugin-vue-components
|
// generated by unplugin-vue-components
|
||||||
// We suggest you to commit this file into source control
|
// We suggest you to commit this file into source control
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
import '@vue/runtime-core';
|
import '@vue/runtime-core'
|
||||||
|
|
||||||
export {};
|
export {}
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
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']
|
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||||
AppLogin: typeof import('./components/app/Login.vue')['default']
|
AppLogin: typeof import('./components/app/Login.vue')['default']
|
||||||
AppLogout: typeof import('./components/app/Logout.vue')['default']
|
AppLogout: typeof import('./components/app/Logout.vue')['default']
|
||||||
@@ -49,9 +17,15 @@ declare module '@vue/runtime-core' {
|
|||||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||||
|
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
|
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
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']
|
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
||||||
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
|
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
|
||||||
@@ -63,4 +37,5 @@ declare module '@vue/runtime-core' {
|
|||||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
||||||
UsersTable: typeof import('./components/users/Table.vue')['default']
|
UsersTable: typeof import('./components/users/Table.vue')['default']
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="text-secondaryDark hover:bg-zinc-800 hover:cursor-pointer rounded-xl"
|
class="text-secondaryDark hover:bg-zinc-800 hover:cursor-pointer rounded-xl"
|
||||||
>
|
>
|
||||||
<td class="py-2 px-3 max-w-30">
|
<td class="py-2 px-3 max-w-36">
|
||||||
<div>
|
<div class="flex">
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
{{ user?.adminUid }}
|
{{ user?.adminUid }}
|
||||||
</span>
|
</span>
|
||||||
@@ -56,10 +56,12 @@
|
|||||||
{{ user?.adminEmail }}
|
{{ user?.adminEmail }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3 max-w-52">
|
||||||
<span>
|
<div class="flex">
|
||||||
{{ user?.inviteeEmail }}
|
<span class="truncate">
|
||||||
</span>
|
{{ user?.inviteeEmail }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
572
pnpm-lock.yaml
generated
572
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -18,11 +18,11 @@ RUN pnpm exec prisma generate
|
|||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
# Remove the env file to avoid backend copying it in and using it
|
# Remove the env file to avoid backend copying it in and using it
|
||||||
RUN rm "../../.env"
|
RUN rm "../../.env"
|
||||||
ENV PRODUCTION="true"
|
ENV PRODUCTION="false"
|
||||||
ENV PORT=3170
|
ENV PORT=3170
|
||||||
ENV APP_PORT=${PORT}
|
ENV APP_PORT=${PORT}
|
||||||
ENV DB_URL=${DATABASE_URL}
|
ENV DB_URL=${DATABASE_URL}
|
||||||
CMD ["pnpm", "run", "start:prod"]
|
CMD ["pnpm", "run", "start:dev"]
|
||||||
EXPOSE 3170
|
EXPOSE 3170
|
||||||
|
|
||||||
FROM base_builder as fe_builder
|
FROM base_builder as fe_builder
|
||||||
|
|||||||
Reference in New Issue
Block a user