Compare commits

...

57 Commits

Author SHA1 Message Date
Mir Arif Hasan
743f693e46 feat: db index added 2023-09-21 12:54:32 +06:00
Mir Arif Hasan
dcbbd34247 perf: db metrix 2023-09-15 12:54:58 +06:00
Joel Jacob Stephen
1431ecc6d7 refactor: keyboard shortcuts now supports different keyboard layouts including Dvorak (#3332)
* refactor: support mulitple keyboard layouts such as dvorak

* chore: replace redundant variable usage
2023-09-08 22:02:39 +05:30
Liyas Thomas
f34d896095 docs: updated screenshots and features list (#3310) 2023-09-05 12:06:47 +05:30
Andrew Bastin
e95ebb9226 chore: add release tag ci pipeline to push to docker hub 2023-08-31 15:49:32 +05:30
Andrew Bastin
57365eeae0 chore: bump version to 2023.8.0 2023-08-31 13:55:36 +05:30
Joel Jacob Stephen
b22bd97818 style: updated font size and truncation on fields in the invited users table in admin dashboard (#3300)
style: updated font size and fixed truncation issue on invited table
2023-08-28 23:27:55 +05:30
Anwarul Islam
b953b32ff4 fix: spotlight actions on graphql (#3299)
* fix: spotlight actions for graphql

* fix: environment actions

* fix: gql rename request

* fix: graphql spotlight actions

* fix: tab shortcuts not working properly

* fix: only show download and copy response when there is a response

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-28 20:40:01 +05:30
Liyas Thomas
0eacd6763b chore: improved command labels and icons (#3295)
* chore: improved command labels and icons

* chore: fix tests

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-28 18:15:00 +05:30
Anwarul Islam
8499ac7fec fix: graphql operation highlight on focus changed (#3297) 2023-08-28 17:55:42 +05:30
Nivedin
4adac4af38 fix: inspections bugs (#3277)
* fix: environment add bug in inspection

* chore: add 127.0.0.1 in url inspection

* chore: update browserextension inspection help url

* fix: team env not showing bug in selector

* chore: rework inspector systems to be reactive

* chore: handling tab changes gracefully

* refactor: move out url interceptor from the platform

* chore: add view function in inspector service to get views into the list

* fix: interceptors not kicking in on initial load

* fix: don't show no internet connection error unless browser deems so

* chore: fix tests

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-28 17:43:46 +05:30
Akash K
fd162e242c fix: issues with codegen (#3293)
* fix: fix issues with httpsnippet upgrade

* chore: fix HttpSnippet import
2023-08-28 15:57:44 +05:30
Andrew Bastin
3e83828722 chore: correct spelling for footer custom entries 2023-08-26 04:43:34 +05:30
Andrew Bastin
f7dc36e3f1 fix: correct typo 'additionalFooterMenuItems' 2023-08-26 03:09:11 +05:30
Andrew Bastin
a7566dfd86 feat: move crisp out of common (#3287)
* feat: move crisp out of common

* fix: update static spotlight searcher

* chore: fix typo
2023-08-26 03:00:58 +05:30
Mir Arif Hasan
d4d7a20fbd HBE-258 hotfix: skip parameter in findMany in shortcode module (#3294)
fix: skip parameter in findMany
2023-08-26 01:35:51 +05:30
Andrew Bastin
dfb281bcf7 chore: update prod.Dockerfile to add step for the backend container to not copy .env in 2023-08-25 21:03:00 +05:30
Andrew Bastin
c62482e81f fix: login component in app not respecting allowed auth provider ids 2023-08-25 19:13:03 +05:30
Anwarul Islam
886847ab7b fix: corrections for spotlight searchers (#3275) 2023-08-25 01:44:29 +05:30
Nivedin
a268cab11e fix: context menu bugs (#3279) 2023-08-25 00:27:03 +05:30
Nivedin
e9509b9fa1 fix: tab right click rename bug (#3286) 2023-08-25 00:20:08 +05:30
Liyas Thomas
8db452089c fix: icons inside tooltip (#3283) 2023-08-24 23:55:22 +05:30
Andrew Bastin
a1764023f3 fix: sh-admin not properly loading in env variables 2023-08-24 20:41:46 +05:30
Andrew Bastin
b08b63dc73 feat: cleaner save context handling for graphql (#3282)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-24 19:07:17 +05:30
Nivedin
a9a4ebf595 fix: autocomplete bug (#3285)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-24 18:46:58 +05:30
Andrew Bastin
a8e279db28 fix: codegen breaking 2023-08-24 15:03:50 +05:30
Andrew Bastin
d09a3e9237 fix: import-meta-env crashes while using dev mode 2023-08-24 09:43:52 +05:30
Andrew Bastin
efa40cf6ea feat: container registry friendly docker images and all-in-one container (#3193)
Co-authored-by: Balu Babu <balub997@gmail.com>
2023-08-24 00:01:28 +05:30
Akash K
1a3d9f18ab fix: issues in displaying the suggestions menu on EnvInput (#3280) 2023-08-23 21:18:48 +05:30
Akash K
653ccd3240 fix: vertical scroll not working on codemirror instance when lines are not wrapped (#3276)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-23 18:14:12 +05:30
Anwarul Islam
c0806cfd07 chore: removed unnecessary dependencies from hoppscotch-ui (#3077) 2023-08-23 18:13:19 +05:30
Liyas Thomas
008eb6b77b chore: minor ui improvements (#3274) 2023-08-22 22:22:43 +05:30
Joel Jacob Stephen
ac60843183 refactor: autofocus can be disabled in smart input hopp ui component (#3273)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-22 20:10:41 +05:30
Anwarul Islam
3c3fb1e4a9 fix: minor spotlight related issues (#3271)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-22 17:58:32 +05:30
Anwarul Islam
88212e8cfe feat: gql revamp (#2644)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-22 17:43:43 +05:30
Andrew Bastin
191fa376d2 chore: remove go to docs search entry and update i18n 2023-08-22 01:09:48 +05:30
Andrew Bastin
6efae3a395 fix: crash when closing tab (fixes HFE-146) 2023-08-22 01:07:16 +05:30
Andrew Bastin
cb8678f07f fix: codegen modal breaking, downgrade back to 2.0 2023-08-22 00:49:13 +05:30
Andrew Bastin
b32b0f9bcb fix: modals inputs not working properly 2023-08-22 00:17:03 +05:30
Anwarul Islam
5a91fb53b2 feat: expanded search capabilities of spotlight (#3255)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-21 20:03:51 +05:30
Liyas Thomas
b0b6edc58e fix: search input autofocus (#3265) 2023-08-21 14:58:44 +05:30
Akash K
8c57d81718 chore: bump dependencies (#3258)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-21 09:06:30 +05:30
Andrew Bastin
10bb68a538 refactor: move from network strategies to generic interceptor service (#3242) 2023-08-21 07:50:35 +05:30
Anwarul Islam
d4d1e27ba9 feat: smart-tree component added to hoppscotch-ui (#3210) 2023-08-20 20:48:32 +05:30
Liyas Thomas
d5c887f311 fix: placeholder size and text overflow on tab head (#3261) 2023-08-18 21:32:10 +05:30
Akash K
ce7adf6da3 feat: load allowed login methods from .env (#3264)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-18 21:23:56 +05:30
Andrew Bastin
c626fb9241 feat: spotlight collection searcher (#3262)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-18 20:56:45 +05:30
Nivedin
f21ed30e10 feat: inspections (#3213)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-18 01:37:21 +05:30
Anwarul Islam
b55970cc7a spotlight: settings based actions added (#3244)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-08-17 22:04:58 +05:30
Joel Jacob Stephen
74ad2e43a4 feat: ability to conditionally enable auth providers in dashboard (#3225) 2023-08-17 21:53:34 +05:30
Liyas Thomas
2d6282cf8b refactor: polish environment selector (#3260) 2023-08-17 16:46:45 +05:30
Liyas Thomas
e255c46455 feat: environment quick peek (#3119) 2023-08-15 19:30:37 +05:30
Andrew Bastin
15c2c7bb5b feat: divider for the additional platform login items 2023-08-15 16:15:03 +05:30
Andrew Bastin
71bcd22444 feat: allow platforms to define additional entries in the login dialog 2023-08-14 22:18:37 +05:30
Joel Jacob Stephen
2d104160f2 refactor: revoke team invitation in admin dashboard (#3232) 2023-08-14 17:37:05 +05:30
Anwarul Islam
f7c1825de5 spotlight: navigation searcher added (#3245)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-08-12 22:21:45 +05:30
Mir Arif Hasan
2c1fd5d711 feat: prefix VITE_ added in conditional auth provider env variable (#3246) (HBE-248) 2023-08-08 14:16:13 +05:30
251 changed files with 17841 additions and 9454 deletions

View File

@@ -1 +1,2 @@
*/**/node_modules node_modules
**/*/node_modules

View File

@@ -13,7 +13,7 @@ SESSION_SECRET='add some secret here'
# Hoppscotch App Domain Config # Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000" REDIRECT_URL="http://localhost:3000"
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100" WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
# Google Auth Config # Google Auth Config
GOOGLE_CLIENT_ID="************************************************" GOOGLE_CLIENT_ID="************************************************"

View 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 }}

View File

@@ -1,3 +1,8 @@
module.exports = { module.exports = {
semi: false semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
} }

View File

@@ -6,8 +6,8 @@ We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body 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
@@ -109,20 +109,24 @@ Violating these terms may lead to a permanent ban.
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

180
README.md
View File

@@ -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 />
<p>
<h3> <h3>
<b> <b>
Hoppscotch Hoppscotch
</b> </b>
</h3> </h3>
</p>
<p>
<b> <b>
Open source API development ecosystem Open Source API Development Ecosystem
</b> </b>
</p>
<p> <p>
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen?logo=github)](CODE_OF_CONDUCT.md) [![Website](https://img.shields.io/website?url=https%3A%2F%2Fhoppscotch.io&logo=hoppscotch)](https://hoppscotch.io) [![Tests](https://github.com/hoppscotch/hoppscotch/actions/workflows/tests.yml/badge.svg)](https://github.com/hoppscotch/hoppscotch/actions) [![Tweet](https://img.shields.io/twitter/url?url=https%3A%2F%2Fhoppscotch.io%2F)](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io) [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen?logo=github)](CODE_OF_CONDUCT.md) [![Website](https://img.shields.io/website?url=https%3A%2F%2Fhoppscotch.io&logo=hoppscotch)](https://hoppscotch.io) [![Tests](https://github.com/hoppscotch/hoppscotch/actions/workflows/tests.yml/badge.svg)](https://github.com/hoppscotch/hoppscotch/actions) [![Tweet](https://img.shields.io/twitter/url?url=https%3A%2F%2Fhoppscotch.io%2F)](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
@@ -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**
[![Chat on Discord](https://img.shields.io/badge/chat-Discord-7289DA?logo=discord)](https://hoppscotch.io/discord) [![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-2CA5E0?logo=telegram)](https://hoppscotch.io/telegram) [![Discuss on GitHub](https://img.shields.io/badge/discussions-GitHub-333333?logo=github)](https://github.com/hoppscotch/hoppscotch/discussions) [![Chat on Discord](https://img.shields.io/badge/chat-Discord-7289DA?logo=discord)](https://hoppscotch.io/discord) [![Chat on Telegram](https://img.shields.io/badge/chat-Telegram-2CA5E0?logo=telegram)](https://hoppscotch.io/telegram) [![Discuss on GitHub](https://img.shields.io/badge/discussions-GitHub-333333?logo=github)](https://github.com/hoppscotch/hoppscotch/discussions)
@@ -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://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_16x16.png) **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) &nbsp;|&nbsp; [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_16x16.png) **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
> **Extensions fixes `CORS` issues.**
- **[Hopp-Doc-Gen](https://github.com/hoppscotch/hopp-doc-gen)** - An API doc generator CLI for Hoppscotch
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
☁️ **Auth + Sync:** Sign in and sync your data in real-time.
**Sign in with**
- GitHub - 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://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_16x16.png) **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) &nbsp;|&nbsp; [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_16x16.png) **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
> **Extensions fix `CORS` issues.**
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
**For a complete list of features, please read our [documentation](https://docs.hoppscotch.io).**
## **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).

View File

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

View File

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

11
aio.Caddyfile Normal file
View File

@@ -0,0 +1,11 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin
file_server
}

72
aio_run.mjs Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/local/bin/node
// @ts-check
import { execSync, spawn } from "child_process"
import fs from "fs"
import process from "process"
function runChildProcessWithPrefix(command, args, prefix) {
const childProcess = spawn(command, args);
childProcess.stdout.on('data', (data) => {
const output = data.toString().trim().split('\n');
output.forEach((line) => {
console.log(`${prefix} | ${line}`);
});
});
childProcess.stderr.on('data', (data) => {
const error = data.toString().trim().split('\n');
error.forEach((line) => {
console.error(`${prefix} | ${line}`);
});
});
childProcess.on('close', (code) => {
console.log(`${prefix} Child process exited with code ${code}`);
});
childProcess.on('error', (stuff) => {
console.log("error")
console.log(stuff)
})
return childProcess
}
const envFileContent = Object.entries(process.env)
.filter(([env]) => env.startsWith("VITE_"))
.map(([env, val]) => `${env}=${
(val.startsWith("\"") && val.endsWith("\""))
? val
: `"${val}"`
}`)
.join("\n")
fs.writeFileSync("build.env", envFileContent)
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
fs.rmSync("build.env")
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
caddyProcess.on("exit", (code) => {
console.log(`Exiting process because Caddy Server exited with code ${code}`)
process.exit(code)
})
backendProcess.on("exit", (code) => {
console.log(`Exiting process because Backend Server exited with code ${code}`)
process.exit(code)
})
process.on('SIGINT', () => {
console.log("SIGINT received, exiting...")
caddyProcess.kill("SIGINT")
backendProcess.kill("SIGINT")
process.exit(0)
})

View File

@@ -7,6 +7,103 @@ services:
# This service runs the backend app in the port 3170 # This service runs the backend app in the port 3170
hoppscotch-backend: hoppscotch-backend:
container_name: hoppscotch-backend container_name: hoppscotch-backend
build:
dockerfile: prod.Dockerfile
context: .
target: backend
env_file:
- ./.env
restart: always
environment:
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
- PORT=3170
volumes:
# 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/node_modules/
depends_on:
hoppscotch-db:
condition: service_healthy
ports:
- "3170:3170"
# The main hoppscotch app. This will be hosted at port 3000
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
# the SH admin dashboard server at packages/hoppscotch-selfhost-web/Caddyfile
hoppscotch-app:
container_name: hoppscotch-app
build:
dockerfile: prod.Dockerfile
context: .
target: app
env_file:
- ./.env
depends_on:
- hoppscotch-backend
ports:
- "3000:8080"
# The Self Host dashboard for managing the app. This will be hosted at port 3100
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
# the SH admin dashboard server at packages/hoppscotch-sh-admin/Caddyfile
hoppscotch-sh-admin:
container_name: hoppscotch-sh-admin
build:
dockerfile: prod.Dockerfile
context: .
target: sh_admin
env_file:
- ./.env
depends_on:
- hoppscotch-backend
ports:
- "3100:8080"
# The service that spins up all 3 services at once in one container
hoppscotch-aio:
container_name: hoppscotch-aio
build:
dockerfile: prod.Dockerfile
context: .
target: aio
env_file:
- ./.env
depends_on:
hoppscotch-db:
condition: service_healthy
ports:
- "3000:3000"
- "3100:3100"
- "3170:3170"
# The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance
# This will be exposed at port 5432
hoppscotch-db:
image: postgres:15
ports:
- "5432:5432"
user: postgres
environment:
# The default user defined by the docker image
POSTGRES_USER: postgres
# NOTE: Please UPDATE THIS PASSWORD!
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch
healthcheck:
test:
[
"CMD-SHELL",
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
]
interval: 5s
timeout: 5s
retries: 10
# All the services listed below are deprececated
hoppscotch-old-backend:
container_name: hoppscotch-old-backend
build: build:
dockerfile: packages/hoppscotch-backend/Dockerfile dockerfile: packages/hoppscotch-backend/Dockerfile
context: . context: .
@@ -28,54 +125,26 @@ services:
ports: ports:
- "3170:3000" - "3170:3000"
# The main hoppscotch app. This will be hosted at port 3000 hoppscotch-old-app:
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for container_name: hoppscotch-old-app
# the SH admin dashboard server at packages/hoppscotch-selfhost-web/Caddyfile
hoppscotch-app:
container_name: hoppscotch-app
build: build:
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
context: . context: .
env_file: env_file:
- ./.env - ./.env
depends_on: depends_on:
- hoppscotch-backend - hoppscotch-old-backend
ports: ports:
- "3000:8080" - "3000:8080"
# The Self Host dashboard for managing the app. This will be hosted at port 3100 hoppscotch-old-sh-admin:
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for container_name: hoppscotch-old-sh-admin
# the SH admin dashboard server at packages/hoppscotch-sh-admin/Caddyfile
hoppscotch-sh-admin:
container_name: hoppscotch-sh-admin
build: build:
dockerfile: packages/hoppscotch-sh-admin/Dockerfile dockerfile: packages/hoppscotch-sh-admin/Dockerfile
context: . context: .
env_file: env_file:
- ./.env - ./.env
depends_on: depends_on:
- hoppscotch-backend - hoppscotch-old-backend
ports: ports:
- "3100:8080" - "3100:8080"
# The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance
# This will be exposed at port 5432
hoppscotch-db:
image: postgres:15
ports:
- "5432:5432"
user: postgres
environment:
# The default user defined by the docker image
POSTGRES_USER: postgres
# NOTE: Please UPDATE THIS PASSWORD!
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch
healthcheck:
test: ["CMD-SHELL", "sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"]
interval: 5s
timeout: 5s
retries: 10

14
healthcheck.sh Normal file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
curlCheck() {
if ! curl -s --head "$1" | head -n 1 | grep -q "HTTP/1.[01] [23].."; then
echo "URL request failed!"
exit 1
else
echo "URL request succeeded!"
fi
}
curlCheck "http://localhost:3000"
curlCheck "http://localhost:3100"
curlCheck "http://localhost:3170/ping"

View File

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

View File

@@ -17,12 +17,12 @@
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@codemirror/language": "^6.2.0", "@codemirror/language": "^6.9.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.1.6",
"@lezer/lr": "^1.2.0" "@lezer/lr": "^1.3.10"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.1.0", "@lezer/generator": "^1.5.0",
"mocha": "^9.2.2", "mocha": "^9.2.2",
"rollup": "^2.70.2", "rollup": "^2.70.2",
"rollup-plugin-dts": "^4.2.1", "rollup-plugin-dts": "^4.2.1",

View File

@@ -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",
@@ -33,7 +34,7 @@
"@nestjs/passport": "^9.0.0", "@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.2.1", "@nestjs/platform-express": "^9.2.1",
"@nestjs/throttler": "^4.0.0", "@nestjs/throttler": "^4.0.0",
"@prisma/client": "^4.7.1", "@prisma/client": "^4.16.2",
"apollo-server-express": "^3.11.1", "apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1", "apollo-server-plugin-base": "^3.7.1",
"argon2": "^0.30.3", "argon2": "^0.30.3",
@@ -57,7 +58,8 @@
"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",
"prisma": "^4.7.1", "pg": "^8.11.3",
"prisma": "^4.16.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.6.0" "rxjs": "^7.6.0"

View File

@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "TeamMember_userUid_idx" ON "TeamMember"("userUid");

View File

@@ -5,7 +5,7 @@ datasource db {
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x"] binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"]
} }
model Team { model Team {
@@ -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 {

View 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);
});

View File

@@ -0,0 +1,9 @@
import { Controller, Get } from '@nestjs/common';
@Controller('ping')
export class AppController {
@Get()
ping(): string {
return 'Success';
}
}

View File

@@ -19,6 +19,8 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
import { ShortcodeModule } from './shortcode/shortcode.module'; 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 { DbModule } from './db/db.module';
@Module({ @Module({
imports: [ imports: [
@@ -65,6 +67,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
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,
@@ -81,5 +84,6 @@ import { ThrottlerModule } from '@nestjs/throttler';
ShortcodeModule, ShortcodeModule,
], ],
providers: [GQLComplexityPlugin], providers: [GQLComplexityPlugin],
controllers: [AppController],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -107,7 +107,7 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
}; };
/** /**
* Check to see if given auth provider is present in the ALLOWED_AUTH_PROVIDERS env variable * Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable
* *
* @param provider Provider we want to check the presence of * @param provider Provider we want to check the presence of
* @returns Boolean if provider specified is present or not * @returns Boolean if provider specified is present or not
@@ -117,8 +117,8 @@ export function authProviderCheck(provider: string) {
throwErr(AUTH_PROVIDER_NOT_SPECIFIED); throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
} }
const envVariables = process.env.ALLOWED_AUTH_PROVIDERS const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
? process.env.ALLOWED_AUTH_PROVIDERS.split(',').map((provider) => ? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
provider.trim().toUpperCase(), provider.trim().toUpperCase(),
) )
: []; : [];

View File

@@ -0,0 +1 @@
export const PG_CONNECTION = 'PG_CONNECTION';

View 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 {}

View File

@@ -29,22 +29,22 @@ export const JSON_INVALID = 'json_invalid';
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified'; export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
/** /**
* Environment variable "ALLOWED_AUTH_PROVIDERS" is not present in .env file * Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
*/ */
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS = export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
'"ALLOWED_AUTH_PROVIDERS" is not present in .env file'; '"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file';
/** /**
* Environment variable "ALLOWED_AUTH_PROVIDERS" is empty in .env file * Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
*/ */
export const ENV_EMPTY_AUTH_PROVIDERS = export const ENV_EMPTY_AUTH_PROVIDERS =
'"ALLOWED_AUTH_PROVIDERS" is empty in .env file'; '"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
/** /**
* Environment variable "ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file * Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
*/ */
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS = export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
'"ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file'; '"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
/** /**
* Tried to delete a user data document from fb firestore but failed. * Tried to delete a user data document from fb firestore but failed.

View File

@@ -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();

View File

@@ -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,
}); });

View File

@@ -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;
} }

View File

@@ -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) =>

View File

@@ -24,6 +24,8 @@ beforeEach(() => {
mockPubSub.publish.mockClear(); mockPubSub.publish.mockClear();
}); });
const date = new Date();
describe('UserHistoryService', () => { describe('UserHistoryService', () => {
describe('fetchUserHistory', () => { describe('fetchUserHistory', () => {
test('Should return a list of users REST history if exists', async () => { test('Should return a list of users REST history if exists', async () => {
@@ -400,7 +402,7 @@ describe('UserHistoryService', () => {
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn: date,
isStarred: false, isStarred: false,
}); });
@@ -410,7 +412,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn: date,
isStarred: false, isStarred: false,
}; };

View File

@@ -9,7 +9,12 @@ import * as E from 'fp-ts/Either';
import * as A from 'fp-ts/Array'; import * as A from 'fp-ts/Array';
import { TeamMemberRole } from './team/team.model'; import { TeamMemberRole } from './team/team.model';
import { User } from './user/user.model'; import { User } from './user/user.model';
import { ENV_EMPTY_AUTH_PROVIDERS, ENV_NOT_FOUND_KEY_AUTH_PROVIDERS, ENV_NOT_SUPPORT_AUTH_PROVIDERS, JSON_INVALID } from './errors'; import {
ENV_EMPTY_AUTH_PROVIDERS,
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
JSON_INVALID,
} from './errors';
import { AuthProvider } from './auth/helper'; import { AuthProvider } from './auth/helper';
/** /**
@@ -156,21 +161,21 @@ export function isValidLength(title: string, length: number) {
/** /**
* This function is called by bootstrap() in main.ts * This function is called by bootstrap() in main.ts
* It checks if the "ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not. * It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
* If not, it throws an error. * If not, it throws an error.
*/ */
export function checkEnvironmentAuthProvider() { export function checkEnvironmentAuthProvider() {
if (!process.env.hasOwnProperty('ALLOWED_AUTH_PROVIDERS')) { if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS); throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
} }
if (process.env.ALLOWED_AUTH_PROVIDERS === '') { if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
throw new Error(ENV_EMPTY_AUTH_PROVIDERS); throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
} }
const givenAuthProviders = process.env.ALLOWED_AUTH_PROVIDERS.split(',').map( const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
(provider) => provider.toLocaleUpperCase(), ',',
); ).map((provider) => provider.toLocaleUpperCase());
const supportedAuthProviders = Object.values(AuthProvider).map( const supportedAuthProviders = Object.values(AuthProvider).map(
(provider: string) => provider.toLocaleUpperCase(), (provider: string) => provider.toLocaleUpperCase(),
); );

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,14 +35,18 @@ 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"`
@@ -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
```

View File

@@ -29,8 +29,18 @@ module.exports = {
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154 "import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
"no-console": "off", "no-console": "off",
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn", "no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
"prettier/prettier": "prettier/prettier": [
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn", process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
{},
{
semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2,
},
],
"vue/multi-word-component-names": "off", "vue/multi-word-component-names": "off",
"vue/no-side-effects-in-computed-properties": "off", "vue/no-side-effects-in-computed-properties": "off",
"import/no-named-as-default": "off", "import/no-named-as-default": "off",

View File

@@ -1,3 +1,8 @@
module.exports = { module.exports = {
semi: false semi: false,
trailingComma: "es5",
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
} }

View File

@@ -166,12 +166,6 @@ a {
@apply truncate; @apply truncate;
@apply sm:inline-flex; @apply sm:inline-flex;
} }
.env-icon {
@apply transition;
@apply inline-flex;
@apply items-center;
}
} }
.tippy-svg-arrow { .tippy-svg-arrow {
@@ -190,10 +184,11 @@ a {
@apply border-solid border-dividerDark; @apply border-solid border-dividerDark;
@apply rounded; @apply rounded;
@apply shadow-lg; @apply shadow-lg;
@apply max-w-[45vw] #{!important};
.tippy-content { .tippy-content {
@apply flex flex-col; @apply flex flex-col;
@apply max-h-56; @apply max-h-[45vh];
@apply items-stretch; @apply items-stretch;
@apply overflow-y-auto; @apply overflow-y-auto;
@apply text-secondary text-body; @apply text-secondary text-body;
@@ -201,6 +196,10 @@ a {
@apply leading-normal; @apply leading-normal;
@apply focus:outline-none; @apply focus:outline-none;
scroll-behavior: smooth; scroll-behavior: smooth;
& > span {
@apply block #{!important};
}
} }
.tippy-svg-arrow { .tippy-svg-arrow {
@@ -216,6 +215,7 @@ a {
[data-v-tippy] { [data-v-tippy] {
@apply flex flex-1; @apply flex flex-1;
@apply truncate;
} }
[interactive] > div { [interactive] > div {
@@ -326,7 +326,7 @@ pre.ace_editor {
@apply after:font-icon; @apply after:font-icon;
@apply after:text-current; @apply after:text-current;
@apply after:right-3; @apply after:right-3;
@apply after:content-["\e313"]; @apply after:content-["\e5cf"];
@apply after:text-lg; @apply after:text-lg;
} }
@@ -481,6 +481,10 @@ pre.ace_editor {
} }
} }
.cm-scroller {
@apply overscroll-y-auto;
}
.cm-editor { .cm-editor {
.cm-line::selection { .cm-line::selection {
@apply bg-accentDark #{!important}; @apply bg-accentDark #{!important};
@@ -568,3 +572,11 @@ details[open] summary .indicator {
@apply rounded; @apply rounded;
@apply border-0; @apply border-0;
} }
.gql-operation-not-highlight {
@apply opacity-50;
}
.gql-operation-highlight {
@apply opacity-100;
}

View File

@@ -69,6 +69,8 @@
"invite": "Invite", "invite": "Invite",
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.", "invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
"invite_your_friends": "Invite your friends", "invite_your_friends": "Invite your friends",
"social_links": "Social links",
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
"join_discord_community": "Join our Discord community", "join_discord_community": "Join our Discord community",
"keyboard_shortcuts": "Keyboard shortcuts", "keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch", "name": "Hoppscotch",
@@ -151,13 +153,14 @@
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?", "remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.", "request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?", "save_unsaved_tab": "Do you want to save changes made in this tab?",
"close_unsaved_tab": "Are you sure you want to close this tab?",
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.", "close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress." "sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
}, },
"context_menu": { "context_menu": {
"set_environment_variable": "Set as variable", "set_environment_variable": "Set as variable",
"add_parameter": "Add to parameter", "add_parameters": "Add to parameters",
"open_link_in_new_tab": "Open link in new tab" "open_request_in_new_tab": "Open request in new tab"
}, },
"count": { "count": {
"header": "Header {count}", "header": "Header {count}",
@@ -182,7 +185,6 @@
"folder": "Folder is empty", "folder": "Folder is empty",
"headers": "This request does not have any headers", "headers": "This request does not have any headers",
"history": "History is empty", "history": "History is empty",
"history_suggestions": "History does not have any matching entries",
"invites": "Invite list is empty", "invites": "Invite list is empty",
"members": "Team is empty", "members": "Team is empty",
"parameters": "This request does not have any parameters", "parameters": "This request does not have any parameters",
@@ -202,18 +204,25 @@
"create_new": "Create new environment", "create_new": "Create new environment",
"created": "Environment created", "created": "Environment created",
"deleted": "Environment deletion", "deleted": "Environment deletion",
"duplicated": "Environment duplicated",
"edit": "Edit Environment", "edit": "Edit Environment",
"global": "Global", "global": "Global",
"empty_variables": "No variables",
"global_variables": "Global variables",
"invalid_name": "Please provide a name for the environment", "invalid_name": "Please provide a name for the environment",
"list": "Environment variables",
"my_environments": "My Environments", "my_environments": "My Environments",
"name": "Name", "name": "Name",
"nested_overflow": "nested environment variables are limited to 10 levels", "nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment", "new": "New Environment",
"no_active_environment": "No active environment",
"no_environment": "No environment", "no_environment": "No environment",
"no_environment_description": "No environments were selected. Choose what to do with the following variables.", "no_environment_description": "No environments were selected. Choose what to do with the following variables.",
"quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable", "replace_with_variable": "Replace with variable",
"scope": "Scope", "scope": "Scope",
"select": "Select environment", "select": "Select environment",
"set": "Set environment",
"set_as_environment": "Set as environment", "set_as_environment": "Set as environment",
"team_environments": "Team Environments", "team_environments": "Team Environments",
"title": "Environments", "title": "Environments",
@@ -243,6 +252,7 @@
"no_duration": "No duration", "no_duration": "No duration",
"no_results_found": "No matches found", "no_results_found": "No matches found",
"page_not_found": "This page could not be found", "page_not_found": "This page could not be found",
"proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script", "script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong", "something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script" "test_script_fail": "Could not execute post-request script"
@@ -270,6 +280,10 @@
"graphql": { "graphql": {
"mutations": "Mutations", "mutations": "Mutations",
"schema": "Schema", "schema": "Schema",
"switch_connection": "Switch connection",
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
"subscriptions": "Subscriptions" "subscriptions": "Subscriptions"
}, },
"group": { "group": {
@@ -299,6 +313,30 @@
"preview": "Hide Preview", "preview": "Hide Preview",
"sidebar": "Collapse sidebar" "sidebar": "Collapse sidebar"
}, },
"inspections": {
"title": "Inspector",
"description": "Inspect possible errors",
"environment": {
"add_environment": "Add to Environment",
"not_found": "Environment variable “{environment}” not found."
},
"header": {
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
},
"response": {
"401_error": "Please check your authentication credentials.",
"404_error": "Please check your request URL and method type.",
"network_error": "Please check your network connection.",
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
"default_error": "Please check your request."
},
"url": {
"extension_not_installed": "Extension not installed.",
"extention_not_enabled": "Extension not enabled.",
"extention_enable_action": "Enable Browser Extension",
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list."
}
},
"import": { "import": {
"collections": "Import collections", "collections": "Import collections",
"curl": "Import cURL", "curl": "Import cURL",
@@ -438,6 +476,7 @@
"rename": "Rename Request", "rename": "Rename Request",
"renamed": "Request renamed", "renamed": "Request renamed",
"run": "Run", "run": "Run",
"stop": "Stop",
"save": "Save", "save": "Save",
"save_as": "Save as", "save_as": "Save as",
"saved": "Request saved", "saved": "Request saved",
@@ -477,9 +516,9 @@
"account_name_description": "This is your display name.", "account_name_description": "This is your display name.",
"background": "Background", "background": "Background",
"black_mode": "Black", "black_mode": "Black",
"dark_mode": "Dark",
"change_font_size": "Change font size", "change_font_size": "Change font size",
"choose_language": "Choose language", "choose_language": "Choose language",
"dark_mode": "Dark",
"delete_account": "Delete account", "delete_account": "Delete account",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.", "delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
"expand_navigation": "Expand navigation", "expand_navigation": "Expand navigation",
@@ -543,6 +582,10 @@
"show_all": "Keyboard shortcuts", "show_all": "Keyboard shortcuts",
"title": "General" "title": "General"
}, },
"others": {
"title": "Others",
"prettify": "Prettify Editor's Content"
},
"miscellaneous": { "miscellaneous": {
"invite": "Invite people to Hoppscotch", "invite": "Invite people to Hoppscotch",
"title": "Miscellaneous" "title": "Miscellaneous"
@@ -563,6 +606,9 @@
"delete_method": "Select DELETE method", "delete_method": "Select DELETE method",
"get_method": "Select GET method", "get_method": "Select GET method",
"head_method": "Select HEAD method", "head_method": "Select HEAD method",
"rename": "Rename Request",
"import_curl": "Import cURL",
"show_code": "Generate code snippet",
"method": "Method", "method": "Method",
"next_method": "Select Next method", "next_method": "Select Next method",
"post_method": "Select POST method", "post_method": "Select POST method",
@@ -571,6 +617,7 @@
"reset_request": "Reset Request", "reset_request": "Reset Request",
"save_to_collections": "Save to Collections", "save_to_collections": "Save to Collections",
"send_request": "Send Request", "send_request": "Send Request",
"save_request": "Save Request",
"title": "Request" "title": "Request"
}, },
"response": { "response": {
@@ -579,10 +626,10 @@
"title": "Response" "title": "Response"
}, },
"theme": { "theme": {
"black": "Switch theme to black mode", "black": "Switch theme to Black Mode",
"dark": "Switch theme to dark mode", "dark": "Switch theme to Dark Mode",
"light": "Switch theme to light mode", "light": "Switch theme to Light Mode",
"system": "Switch theme to system mode", "system": "Switch theme to System Mode",
"title": "Theme" "title": "Theme"
} }
}, },
@@ -601,8 +648,87 @@
"url": "URL" "url": "URL"
}, },
"spotlight": { "spotlight": {
"general": {
"help_menu": "Help and support",
"chat": "Chat with support",
"open_docs": "Read Documentation",
"open_keybindings": "Keyboard shortcuts",
"open_github": "Open GitHub repository",
"social": "Social",
"title": "General"
},
"miscellaneous": {
"invite": "Invite your friends to Hoppscotch",
"title": "Miscellaneous"
},
"request": {
"switch_to": "Switch to",
"select_method": "Select method",
"save_as_new": "Save as new request",
"tab_parameters": "Parameters tab",
"tab_body": "Body tab",
"tab_headers": "Headers tab",
"tab_authorization": "Authorization tab",
"tab_pre_request_script": "Pre-request script tab",
"tab_tests": "Tests tab",
"tab_query": "Query tab",
"tab_variables": "Variables tab"
},
"graphql": {
"connect": "Connect to server",
"disconnect": "Disconnect from server"
},
"response": {
"copy": "Copy response",
"download": "Download response as file",
"title": "Response"
},
"environments": {
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"edit": "Edit current environment",
"delete": "Delete current environment",
"duplicate": "Duplicate current environment",
"edit_global": "Edit global environment",
"duplicate_global": "Duplicate global environment",
"title": "Environments"
},
"workspace": {
"new": "Create new team",
"edit": "Edit current team",
"delete": "Delete current team",
"invite": "Invite people to team",
"switch_to_personal": "Switch to your personal workspace",
"title": "Teams"
},
"tab": {
"duplicate": "Duplicate current tab",
"close_current": "Close current tab",
"close_others": "Close all other tabs",
"new_tab": "Open a new tab",
"title": "Tabs"
},
"section": { "section": {
"user": "User" "user": "User",
"theme": "Theme",
"interface": "Interface",
"interceptor": "Interceptor"
},
"change_language": "Change Language",
"settings": {
"theme": {
"black": "Black",
"dark": "Dark",
"light": "Light",
"system": "System preference"
},
"font": {
"size_sm": "Small",
"size_md": "Medium",
"size_lg": "Large"
},
"change_interceptor": "Change Interceptor",
"change_language": "Change Language"
} }
}, },
"sse": { "sse": {

View File

@@ -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",
@@ -22,140 +22,141 @@
}, },
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.0.3", "@codemirror/autocomplete": "^6.9.0",
"@codemirror/commands": "^6.0.1", "@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.0.1", "@codemirror/lang-javascript": "^6.1.9",
"@codemirror/lang-json": "^6.0.0", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.2.0", "@codemirror/language": "^6.9.0",
"@codemirror/legacy-modes": "^6.1.0", "@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.0.0", "@codemirror/lint": "^6.4.0",
"@codemirror/search": "^6.0.0", "@codemirror/search": "^6.5.1",
"@codemirror/state": "^6.1.0", "@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.0.2", "@codemirror/view": "^6.16.0",
"@fontsource-variable/inter": "^5.0.5", "@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.5", "@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.6", "@fontsource-variable/roboto-mono": "^5.0.9",
"@hoppscotch/codemirror-lang-graphql": "workspace:^", "@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^", "@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^", "@hoppscotch/js-sandbox": "workspace:^",
"@hoppscotch/ui": "workspace:^", "@hoppscotch/ui": "workspace:^",
"@hoppscotch/vue-toasted": "^0.1.0", "@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.1.6",
"@sentry/tracing": "^7.13.0", "@sentry/tracing": "^7.64.0",
"@sentry/vue": "^7.13.0", "@sentry/vue": "^7.64.0",
"@urql/core": "^2.5.0", "@urql/core": "^4.1.1",
"@urql/devtools": "^2.0.3", "@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^0.1.7", "@urql/exchange-auth": "^2.1.6",
"@urql/exchange-graphcache": "^4.4.3", "@urql/exchange-graphcache": "^6.3.2",
"@vitejs/plugin-legacy": "^2.3.0", "@vitejs/plugin-legacy": "^4.1.1",
"@vueuse/core": "^8.9.4", "@vueuse/core": "^10.3.0",
"@vueuse/head": "^0.7.9", "@vueuse/head": "^1.3.1",
"acorn-walk": "^8.2.0", "acorn-walk": "^8.2.0",
"axios": "^0.21.4", "axios": "^1.4.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"dioc": "workspace:^", "dioc": "workspace:^",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"events": "^3.3.0", "events": "^3.3.0",
"fp-ts": "^2.12.1", "fp-ts": "^2.16.1",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"globalthis": "^1.0.3", "globalthis": "^1.0.3",
"graphql": "^15.5.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.3.0", "insomnia-importers": "^3.6.0",
"io-ts": "^2.2.16", "io-ts": "^2.2.20",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonpath-plus": "^7.0.0", "jsonpath-plus": "^7.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lossless-json": "^2.0.8", "lossless-json": "^2.0.11",
"minisearch": "^6.1.0", "minisearch": "^6.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0", "paho-mqtt": "^1.1.0",
"path": "^0.12.7", "path": "^0.12.7",
"postman-collection": "^4.1.4", "postman-collection": "^4.2.0",
"process": "^0.11.10", "process": "^0.11.10",
"qs": "^6.10.3", "qs": "^6.11.2",
"rxjs": "^7.5.5", "rxjs": "^7.8.1",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0", "socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"socket.io-client-v3": "npm:socket.io-client@^3.1.3", "socket.io-client-v3": "npm:socket.io-client@^3.1.3",
"socket.io-client-v4": "npm:socket.io-client@^4.4.1", "socket.io-client-v4": "npm:socket.io-client@^4.4.1",
"socketio-wildcard": "^2.0.0", "socketio-wildcard": "^2.0.0",
"splitpanes": "^3.1.1", "splitpanes": "^3.1.5",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"tern": "^0.24.3", "tern": "^0.24.3",
"timers": "^0.1.1", "timers": "^0.1.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"url": "^0.11.0", "url": "^0.11.1",
"util": "^0.12.4", "util": "^0.12.5",
"uuid": "^8.3.2", "uuid": "^9.0.0",
"vue": "^3.2.25", "vue": "^3.3.4",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-pdf-embed": "^1.1.4", "vue-pdf-embed": "^1.1.6",
"vue-router": "^4.0.16", "vue-router": "^4.2.4",
"vue-tippy": "6.0.0-alpha.58", "vue-tippy": "6.3.1",
"vuedraggable-es": "^4.1.1", "vuedraggable-es": "^4.1.1",
"wonka": "^4.0.15", "wonka": "^6.3.4",
"workbox-window": "^6.5.4", "workbox-window": "^7.0.0",
"xml-formatter": "^3.4.1", "xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1" "yargs-parser": "^21.1.1"
}, },
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.1.1", "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.1.4", "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@graphql-codegen/add": "^3.2.0", "@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^2.8.0", "@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/typed-document-node": "^2.3.1", "@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^2.7.1", "@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^2.5.1", "@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "^2.3.1", "@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
"@graphql-codegen/urql-introspection": "^2.2.0", "@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-typed-document-node/core": "^3.1.1", "@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/lucide": "^1.1.109", "@iconify-json/lucide": "^1.1.119",
"@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.1.4", "@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.6", "@types/lodash-es": "^4.17.8",
"@types/lossless-json": "^1.0.1", "@types/lossless-json": "^1.0.1",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/paho-mqtt": "^1.0.6", "@types/paho-mqtt": "^1.0.7",
"@types/postman-collection": "^3.5.7", "@types/postman-collection": "^3.5.7",
"@types/splitpanes": "^2.2.1", "@types/splitpanes": "^2.2.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^9.0.2",
"@types/yargs-parser": "^21.0.0", "@types/yargs-parser": "^21.0.0",
"@typescript-eslint/eslint-plugin": "^5.19.0", "@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^5.19.0", "@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-vue": "^3.1.0", "@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.2.39", "@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^11.0.1", "@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-core": "^3.2.39", "@vue/runtime-core": "^3.3.4",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.0.3", "dotenv": "^16.3.1",
"eslint": "^8.24.0", "eslint": "^8.47.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.5.1", "eslint-plugin-vue": "^9.17.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"openapi-types": "^12.0.0", "openapi-types": "^12.1.3",
"rollup-plugin-polyfill-node": "^0.10.1", "rollup-plugin-polyfill-node": "^0.12.0",
"sass": "^1.53.0", "sass": "^1.66.0",
"typescript": "^4.5.4", "typescript": "^5.1.6",
"unplugin-fonts": "^1.0.3", "unplugin-fonts": "^1.0.3",
"unplugin-icons": "^0.14.9", "unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.21.0", "unplugin-vue-components": "^0.25.1",
"vite": "^3.1.4", "vite": "^4.4.9",
"vite-plugin-checker": "^0.5.1", "vite-plugin-checker": "^0.6.1",
"vite-plugin-html-config": "^1.0.10", "vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.4", "vite-plugin-inspect": "^0.7.38",
"vite-plugin-pages": "^0.26.0", "vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.4.5", "vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.13.1", "vite-plugin-pwa": "^0.16.4",
"vite-plugin-vue-layouts": "^0.7.0", "vite-plugin-vue-layouts": "^0.8.0",
"vite-plugin-windicss": "^1.8.8", "vite-plugin-windicss": "^1.9.1",
"vitest": "^0.32.2", "vitest": "^0.34.2",
"vue-tsc": "^0.38.2", "vue-tsc": "^1.8.8",
"windicss": "^3.5.6" "windicss": "^3.5.6"
} }
} }

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

View File

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

View File

@@ -1,222 +1,37 @@
// generated by unplugin-vue-components /* eslint-disable */
// We suggest you to commit this file into source control /* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import "@vue/runtime-core"
export {} export {}
declare module "@vue/runtime-core" { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AppActionHandler: typeof import("./components/app/ActionHandler.vue")["default"]
AppAnnouncement: typeof import("./components/app/Announcement.vue")["default"]
AppDeveloperOptions: typeof import("./components/app/DeveloperOptions.vue")["default"]
AppFooter: typeof import("./components/app/Footer.vue")["default"]
AppGitHubStarButton: typeof import("./components/app/GitHubStarButton.vue")["default"]
AppHeader: typeof import("./components/app/Header.vue")["default"]
AppInterceptor: typeof import("./components/app/Interceptor.vue")["default"]
AppLogo: typeof import("./components/app/Logo.vue")["default"]
AppOptions: typeof import("./components/app/Options.vue")["default"]
AppPaneLayout: typeof import("./components/app/PaneLayout.vue")["default"]
AppShare: typeof import("./components/app/Share.vue")["default"]
AppShortcuts: typeof import("./components/app/Shortcuts.vue")["default"]
AppShortcutsEntry: typeof import("./components/app/ShortcutsEntry.vue")["default"]
AppShortcutsPrompt: typeof import("./components/app/ShortcutsPrompt.vue")["default"]
AppSidenav: typeof import("./components/app/Sidenav.vue")["default"]
AppSpotlight: typeof import("./components/app/spotlight/index.vue")["default"]
AppSpotlightEntry: typeof import("./components/app/spotlight/Entry.vue")["default"]
AppSpotlightEntryGQLHistory: typeof import("./components/app/spotlight/entry/GQLHistory.vue")["default"]
AppSpotlightEntryRESTHistory: typeof import("./components/app/spotlight/entry/RESTHistory.vue")["default"]
AppSupport: typeof import("./components/app/Support.vue")["default"]
ButtonPrimary: typeof import("./../../hoppscotch-ui/src/components/button/Primary.vue")["default"]
ButtonSecondary: typeof import("./../../hoppscotch-ui/src/components/button/Secondary.vue")["default"]
Collections: typeof import("./components/collections/index.vue")["default"]
CollectionsAdd: typeof import("./components/collections/Add.vue")["default"]
CollectionsAddFolder: typeof import("./components/collections/AddFolder.vue")["default"]
CollectionsAddRequest: typeof import("./components/collections/AddRequest.vue")["default"]
CollectionsCollection: typeof import("./components/collections/Collection.vue")["default"]
CollectionsEdit: typeof import("./components/collections/Edit.vue")["default"]
CollectionsEditFolder: typeof import("./components/collections/EditFolder.vue")["default"]
CollectionsEditRequest: typeof import("./components/collections/EditRequest.vue")["default"]
CollectionsGraphql: typeof import("./components/collections/graphql/index.vue")["default"]
CollectionsGraphqlAdd: typeof import("./components/collections/graphql/Add.vue")["default"]
CollectionsGraphqlAddFolder: typeof import("./components/collections/graphql/AddFolder.vue")["default"]
CollectionsGraphqlAddRequest: typeof import("./components/collections/graphql/AddRequest.vue")["default"]
CollectionsGraphqlCollection: typeof import("./components/collections/graphql/Collection.vue")["default"]
CollectionsGraphqlEdit: typeof import("./components/collections/graphql/Edit.vue")["default"]
CollectionsGraphqlEditFolder: typeof import("./components/collections/graphql/EditFolder.vue")["default"]
CollectionsGraphqlEditRequest: typeof import("./components/collections/graphql/EditRequest.vue")["default"]
CollectionsGraphqlFolder: typeof import("./components/collections/graphql/Folder.vue")["default"]
CollectionsGraphqlImportExport: typeof import("./components/collections/graphql/ImportExport.vue")["default"]
CollectionsGraphqlRequest: typeof import("./components/collections/graphql/Request.vue")["default"]
CollectionsImportExport: typeof import("./components/collections/ImportExport.vue")["default"]
CollectionsMyCollections: typeof import("./components/collections/MyCollections.vue")["default"]
CollectionsRequest: typeof import("./components/collections/Request.vue")["default"]
CollectionsSaveRequest: typeof import("./components/collections/SaveRequest.vue")["default"]
CollectionsTeamCollections: typeof import("./components/collections/TeamCollections.vue")["default"]
Environments: typeof import("./components/environments/index.vue")["default"]
EnvironmentsImportExport: typeof import("./components/environments/ImportExport.vue")["default"]
EnvironmentsMy: typeof import("./components/environments/my/index.vue")["default"]
EnvironmentsMyDetails: typeof import("./components/environments/my/Details.vue")["default"]
EnvironmentsMyEnvironment: typeof import("./components/environments/my/Environment.vue")["default"]
EnvironmentsSelector: typeof import("./components/environments/Selector.vue")["default"]
EnvironmentsTeams: typeof import("./components/environments/teams/index.vue")["default"]
EnvironmentsTeamsDetails: typeof import("./components/environments/teams/Details.vue")["default"]
EnvironmentsTeamsEnvironment: typeof import("./components/environments/teams/Environment.vue")["default"]
FirebaseLogin: typeof import("./components/firebase/Login.vue")["default"]
FirebaseLogout: typeof import("./components/firebase/Logout.vue")["default"]
GraphqlAuthorization: typeof import("./components/graphql/Authorization.vue")["default"]
GraphqlField: typeof import("./components/graphql/Field.vue")["default"]
GraphqlRequest: typeof import("./components/graphql/Request.vue")["default"]
GraphqlRequestOptions: typeof import("./components/graphql/RequestOptions.vue")["default"]
GraphqlResponse: typeof import("./components/graphql/Response.vue")["default"]
GraphqlSidebar: typeof import("./components/graphql/Sidebar.vue")["default"]
GraphqlType: typeof import("./components/graphql/Type.vue")["default"]
GraphqlTypeLink: typeof import("./components/graphql/TypeLink.vue")["default"]
History: typeof import("./components/history/index.vue")["default"]
HistoryGraphqlCard: typeof import("./components/history/graphql/Card.vue")["default"]
HistoryRestCard: typeof import("./components/history/rest/Card.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"]
HoppSmartCheckbox: typeof import("@hoppscotch/ui")["HoppSmartCheckbox"]
HoppSmartConfirmModal: typeof import("@hoppscotch/ui")["HoppSmartConfirmModal"]
HoppSmartExpand: typeof import("@hoppscotch/ui")["HoppSmartExpand"]
HoppSmartFileChip: typeof import("@hoppscotch/ui")["HoppSmartFileChip"]
HoppSmartInput: typeof import("@hoppscotch/ui")["HoppSmartInput"]
HoppSmartIntersection: typeof import("@hoppscotch/ui")["HoppSmartIntersection"]
HoppSmartItem: typeof import("@hoppscotch/ui")["HoppSmartItem"]
HoppSmartLink: typeof import("@hoppscotch/ui")["HoppSmartLink"]
HoppSmartModal: typeof import("@hoppscotch/ui")["HoppSmartModal"]
HoppSmartPicture: typeof import("@hoppscotch/ui")["HoppSmartPicture"]
HoppSmartPlaceholder: typeof import("@hoppscotch/ui")["HoppSmartPlaceholder"]
HoppSmartProgressRing: typeof import("@hoppscotch/ui")["HoppSmartProgressRing"]
HoppSmartRadioGroup: typeof import("@hoppscotch/ui")["HoppSmartRadioGroup"]
HoppSmartSlideOver: typeof import("@hoppscotch/ui")["HoppSmartSlideOver"]
HoppSmartSpinner: typeof import("@hoppscotch/ui")["HoppSmartSpinner"]
HoppSmartTab: typeof import("@hoppscotch/ui")["HoppSmartTab"]
HoppSmartTabs: typeof import("@hoppscotch/ui")["HoppSmartTabs"]
HoppSmartToggle: typeof import("@hoppscotch/ui")["HoppSmartToggle"]
HoppSmartWindow: typeof import("@hoppscotch/ui")["HoppSmartWindow"]
HoppSmartWindows: typeof import("@hoppscotch/ui")["HoppSmartWindows"]
HttpAuthorization: typeof import("./components/http/Authorization.vue")["default"]
HttpAuthorizationApiKey: typeof import("./components/http/authorization/ApiKey.vue")["default"]
HttpAuthorizationBasic: typeof import("./components/http/authorization/Basic.vue")["default"]
HttpBody: typeof import("./components/http/Body.vue")["default"]
HttpBodyParameters: typeof import("./components/http/BodyParameters.vue")["default"]
HttpCodegenModal: typeof import("./components/http/CodegenModal.vue")["default"]
HttpHeaders: typeof import("./components/http/Headers.vue")["default"]
HttpImportCurl: typeof import("./components/http/ImportCurl.vue")["default"]
HttpOAuth2Authorization: typeof import("./components/http/OAuth2Authorization.vue")["default"]
HttpParameters: typeof import("./components/http/Parameters.vue")["default"]
HttpPreRequestScript: typeof import("./components/http/PreRequestScript.vue")["default"]
HttpRawBody: typeof import("./components/http/RawBody.vue")["default"]
HttpReqChangeConfirmModal: typeof import("./components/http/ReqChangeConfirmModal.vue")["default"]
HttpRequest: typeof import("./components/http/Request.vue")["default"]
HttpRequestOptions: typeof import("./components/http/RequestOptions.vue")["default"]
HttpRequestTab: typeof import("./components/http/RequestTab.vue")["default"]
HttpResponse: typeof import("./components/http/Response.vue")["default"]
HttpResponseMeta: typeof import("./components/http/ResponseMeta.vue")["default"]
HttpSidebar: typeof import("./components/http/Sidebar.vue")["default"]
HttpTestResult: typeof import("./components/http/TestResult.vue")["default"]
HttpTestResultEntry: typeof import("./components/http/TestResultEntry.vue")["default"]
HttpTestResultEnv: typeof import("./components/http/TestResultEnv.vue")["default"]
HttpTestResultReport: typeof import("./components/http/TestResultReport.vue")["default"]
HttpTests: typeof import("./components/http/Tests.vue")["default"]
HttpURLEncodedParams: typeof import("./components/http/URLEncodedParams.vue")["default"]
IconLucideAlertTriangle: typeof import("~icons/lucide/alert-triangle")["default"]
IconLucideArrowLeft: typeof import("~icons/lucide/arrow-left")["default"]
IconLucideCheckCircle: typeof import("~icons/lucide/check-circle")["default"]
IconLucideChevronRight: typeof import("~icons/lucide/chevron-right")["default"]
IconLucideGlobe: typeof import("~icons/lucide/globe")["default"]
IconLucideHelpCircle: typeof import("~icons/lucide/help-circle")["default"]
IconLucideInbox: typeof import("~icons/lucide/inbox")["default"]
IconLucideInfo: typeof import("~icons/lucide/info")["default"]
IconLucideLayers: typeof import("~icons/lucide/layers")["default"]
IconLucideListEnd: typeof import("~icons/lucide/list-end")["default"]
IconLucideMinus: typeof import("~icons/lucide/minus")["default"]
IconLucideSearch: typeof import("~icons/lucide/search")["default"]
IconLucideUsers: typeof import("~icons/lucide/users")["default"]
IconLucideVerified: typeof import("~icons/lucide/verified")["default"]
LensesHeadersRenderer: typeof import("./components/lenses/HeadersRenderer.vue")["default"]
LensesHeadersRendererEntry: typeof import("./components/lenses/HeadersRendererEntry.vue")["default"]
LensesRenderersAudioLensRenderer: typeof import("./components/lenses/renderers/AudioLensRenderer.vue")["default"]
LensesRenderersHTMLLensRenderer: typeof import("./components/lenses/renderers/HTMLLensRenderer.vue")["default"]
LensesRenderersImageLensRenderer: typeof import("./components/lenses/renderers/ImageLensRenderer.vue")["default"]
LensesRenderersJSONLensRenderer: typeof import("./components/lenses/renderers/JSONLensRenderer.vue")["default"]
LensesRenderersPDFLensRenderer: typeof import("./components/lenses/renderers/PDFLensRenderer.vue")["default"]
LensesRenderersRawLensRenderer: typeof import("./components/lenses/renderers/RawLensRenderer.vue")["default"]
LensesRenderersVideoLensRenderer: typeof import("./components/lenses/renderers/VideoLensRenderer.vue")["default"]
LensesRenderersXMLLensRenderer: typeof import("./components/lenses/renderers/XMLLensRenderer.vue")["default"]
LensesResponseBodyRenderer: typeof import("./components/lenses/ResponseBodyRenderer.vue")["default"]
ProfileShortcode: typeof import("./components/profile/Shortcode.vue")["default"]
ProfileShortcodes: typeof import("./components/profile/Shortcodes.vue")["default"]
ProfileUserDelete: typeof import("./components/profile/UserDelete.vue")["default"]
RealtimeCommunication: typeof import("./components/realtime/Communication.vue")["default"]
RealtimeConnectionConfig: typeof import("./components/realtime/ConnectionConfig.vue")["default"]
RealtimeLog: typeof import("./components/realtime/Log.vue")["default"]
RealtimeLogEntry: typeof import("./components/realtime/LogEntry.vue")["default"]
RealtimeSubscription: typeof import("./components/realtime/Subscription.vue")["default"]
SmartAccentModePicker: typeof import("./components/smart/AccentModePicker.vue")["default"]
SmartAnchor: typeof import("./../../hoppscotch-ui/src/components/smart/Anchor.vue")["default"]
SmartAutoComplete: typeof import("./../../hoppscotch-ui/src/components/smart/AutoComplete.vue")["default"]
SmartChangeLanguage: typeof import("./components/smart/ChangeLanguage.vue")["default"]
SmartCheckbox: typeof import("./../../hoppscotch-ui/src/components/smart/Checkbox.vue")["default"]
SmartColorModePicker: typeof import("./components/smart/ColorModePicker.vue")["default"]
SmartConfirmModal: typeof import("./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue")["default"]
SmartEnvInput: typeof import("./components/smart/EnvInput.vue")["default"]
SmartExpand: typeof import("./../../hoppscotch-ui/src/components/smart/Expand.vue")["default"]
SmartFileChip: typeof import("./../../hoppscotch-ui/src/components/smart/FileChip.vue")["default"]
SmartFontSizePicker: typeof import("./components/smart/FontSizePicker.vue")["default"]
SmartInput: typeof import("./../../hoppscotch-ui/src/components/smart/Input.vue")["default"]
SmartIntersection: typeof import("./../../hoppscotch-ui/src/components/smart/Intersection.vue")["default"]
SmartItem: typeof import("./../../hoppscotch-ui/src/components/smart/Item.vue")["default"]
SmartLink: typeof import("./../../hoppscotch-ui/src/components/smart/Link.vue")["default"]
SmartModal: typeof import("./../../hoppscotch-ui/src/components/smart/Modal.vue")["default"]
SmartPicture: typeof import("./../../hoppscotch-ui/src/components/smart/Picture.vue")["default"]
SmartPlaceholder: typeof import("./../../hoppscotch-ui/src/components/smart/Placeholder.vue")["default"]
SmartProgressRing: typeof import("./../../hoppscotch-ui/src/components/smart/ProgressRing.vue")["default"]
SmartRadio: typeof import("./../../hoppscotch-ui/src/components/smart/Radio.vue")["default"]
SmartRadioGroup: typeof import("./../../hoppscotch-ui/src/components/smart/RadioGroup.vue")["default"]
SmartSlideOver: typeof import("./../../hoppscotch-ui/src/components/smart/SlideOver.vue")["default"]
SmartSpinner: typeof import("./../../hoppscotch-ui/src/components/smart/Spinner.vue")["default"]
SmartTab: typeof import("./../../hoppscotch-ui/src/components/smart/Tab.vue")["default"]
SmartTabs: typeof import("./../../hoppscotch-ui/src/components/smart/Tabs.vue")["default"]
SmartToggle: typeof import("./../../hoppscotch-ui/src/components/smart/Toggle.vue")["default"]
SmartTree: typeof import("./components/smart/Tree.vue")["default"]
SmartTreeBranch: typeof import("./components/smart/TreeBranch.vue")["default"]
SmartWindow: typeof import("./../../hoppscotch-ui/src/components/smart/Window.vue")["default"]
SmartWindows: typeof import("./../../hoppscotch-ui/src/components/smart/Windows.vue")["default"]
TabPrimary: typeof import("./components/tab/Primary.vue")["default"]
TabSecondary: typeof import("./components/tab/Secondary.vue")["default"]
Teams: typeof import("./components/teams/index.vue")["default"]
TeamsAdd: typeof import("./components/teams/Add.vue")["default"]
TeamsEdit: typeof import("./components/teams/Edit.vue")["default"]
TeamsInvite: typeof import("./components/teams/Invite.vue")["default"]
TeamsMemberStack: typeof import("./components/teams/MemberStack.vue")["default"]
TeamsModal: typeof import("./components/teams/Modal.vue")["default"]
TeamsTeam: typeof import("./components/teams/Team.vue")["default"]
Tippy: typeof import("vue-tippy")["Tippy"]
WorkspaceCurrent: typeof import("./components/workspace/Current.vue")["default"]
WorkspaceSelector: typeof import("./components/workspace/Selector.vue")["default"]
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default'] AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default'] AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default'] AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
AppFooter: typeof import('./components/app/Footer.vue')['default'] AppFooter: typeof import('./components/app/Footer.vue')['default']
AppFuse: typeof import('./components/app/Fuse.vue')['default']
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default'] AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default'] AppHeader: typeof import('./components/app/Header.vue')['default']
AppInspection: typeof import('./components/app/Inspection.vue')['default']
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default'] AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
AppLogo: typeof import('./components/app/Logo.vue')['default'] AppLogo: typeof import('./components/app/Logo.vue')['default']
AppOptions: typeof import('./components/app/Options.vue')['default'] AppOptions: typeof import('./components/app/Options.vue')['default']
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default'] AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
AppPowerSearch: typeof import('./components/app/PowerSearch.vue')['default']
AppPowerSearchEntry: typeof import('./components/app/PowerSearchEntry.vue')['default']
AppShare: typeof import('./components/app/Share.vue')['default'] AppShare: typeof import('./components/app/Share.vue')['default']
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default'] AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default'] AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default'] AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default'] AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSocial: typeof import('./components/app/Social.vue')['default']
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default'] AppSupport: typeof import('./components/app/Support.vue')['default']
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default'] ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default'] ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
@@ -245,6 +60,7 @@ declare module "@vue/runtime-core" {
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default'] CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default'] Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default'] EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default'] EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default'] EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
@@ -257,12 +73,18 @@ declare module "@vue/runtime-core" {
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default'] FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default'] GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
GraphqlField: typeof import('./components/graphql/Field.vue')['default'] GraphqlField: typeof import('./components/graphql/Field.vue')['default']
GraphqlHeaders: typeof import('./components/graphql/Headers.vue')['default']
GraphqlQuery: typeof import('./components/graphql/Query.vue')['default']
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default'] GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default'] GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default'] GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default'] GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
GraphqlType: typeof import('./components/graphql/Type.vue')['default'] GraphqlType: typeof import('./components/graphql/Type.vue')['default']
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default'] GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
History: typeof import('./components/history/index.vue')['default'] History: typeof import('./components/history/index.vue')['default']
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default'] HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default'] HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
@@ -274,6 +96,7 @@ declare module "@vue/runtime-core" {
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand'] HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip'] HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection'] HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'] HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
@@ -281,12 +104,14 @@ declare module "@vue/runtime-core" {
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'] HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder'] HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing'] HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup'] HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver'] HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'] HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab'] HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs'] HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle'] HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow'] HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows'] HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default'] HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
@@ -315,8 +140,10 @@ declare module "@vue/runtime-core" {
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default'] HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
HttpTests: typeof import('./components/http/Tests.vue')['default'] HttpTests: typeof import('./components/http/Tests.vue')['default']
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default'] HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default'] IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'] IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -326,9 +153,11 @@ declare module "@vue/runtime-core" {
IconLucideLayers: typeof import('~icons/lucide/layers')['default'] IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default'] IconLucideVerified: typeof import('~icons/lucide/verified')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default'] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default'] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default'] LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
@@ -348,6 +177,8 @@ declare module "@vue/runtime-core" {
RealtimeLog: typeof import('./components/realtime/Log.vue')['default'] RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default'] RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default'] RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default'] SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default'] SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default'] SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
@@ -359,11 +190,13 @@ declare module "@vue/runtime-core" {
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default'] SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default'] SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default'] SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default'] SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default'] SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default'] SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default'] SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default'] SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default'] SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default'] SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default'] SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
@@ -372,8 +205,8 @@ declare module "@vue/runtime-core" {
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default'] SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default'] SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default'] SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./components/smart/Tree.vue')['default'] SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
SmartTreeBranch: typeof import('./components/smart/TreeBranch.vue')['default'] SmartTreeBranch: typeof import('./../../hoppscotch-ui/src/components/smart/TreeBranch.vue')['default']
SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default'] SmartWindow: typeof import('./../../hoppscotch-ui/src/components/smart/Window.vue')['default']
SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default'] SmartWindows: typeof import('./../../hoppscotch-ui/src/components/smart/Windows.vue')['default']
TabPrimary: typeof import('./components/tab/Primary.vue')['default'] TabPrimary: typeof import('./components/tab/Primary.vue')['default']

View File

@@ -2,16 +2,53 @@
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" /> <AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
<AppShare :show="showShare" @hide-modal="showShare = false" /> <AppShare :show="showShare" @hide-modal="showShare = false" />
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" /> <FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@hide-modal="confirmRemove = false"
@resolve="deleteTeam()"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import { defineActionHandler } from "~/helpers/actions" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { useToast } from "~/composables/toast"
import { useI18n } from "~/composables/i18n"
const toast = useToast()
const t = useI18n()
const showShortcuts = ref(false) const showShortcuts = ref(false)
const showShare = ref(false) const showShare = ref(false)
const showLogin = ref(false) const showLogin = ref(false)
const confirmRemove = ref(false)
const teamID = ref<string | null>(null)
const deleteTeam = () => {
if (!teamID.value) return
pipe(
backendDeleteTeam(teamID.value),
TE.match(
(err) => {
// TODO: Better errors ? We know the possible errors now
toast.error(`${t("error.something_went_wrong")}`)
console.error(err)
},
() => {
invokeAction("workspace.switch.personal")
toast.success(`${t("team.deleted")}`)
}
)
)() // Tasks (and TEs) are lazy, so call the function returned
}
defineActionHandler("flyouts.keybinds.toggle", () => { defineActionHandler("flyouts.keybinds.toggle", () => {
showShortcuts.value = !showShortcuts.value showShortcuts.value = !showShortcuts.value
}) })
@@ -23,4 +60,9 @@ defineActionHandler("modals.share.toggle", () => {
defineActionHandler("modals.login.toggle", () => { defineActionHandler("modals.login.toggle", () => {
showLogin.value = !showLogin.value showLogin.value = !showLogin.value
}) })
defineActionHandler("modals.team.delete", ({ teamId }) => {
teamID.value = teamId
confirmRemove.value = true
})
</script> </script>

View File

@@ -76,6 +76,7 @@
} }
" "
/> />
<!--
<HoppSmartItem <HoppSmartItem
ref="chat" ref="chat"
:icon="IconMessageCircle" :icon="IconMessageCircle"
@@ -88,20 +89,34 @@
} }
" "
/> />
-->
<template
v-for="footerItem in platform.ui?.additionalFooterMenuItems"
:key="footerItem.id"
>
<template v-if="footerItem.action.type === 'link'">
<HoppSmartItem <HoppSmartItem
:icon="IconGift" :icon="footerItem.icon"
:label="`${t('app.whats_new')}`" :label="footerItem.text(t)"
to="https://docs.hoppscotch.io/documentation/changelog" :to="footerItem.action.href"
blank blank
@click="hide()" @click="hide()"
/> />
</template>
<HoppSmartItem <HoppSmartItem
:icon="IconActivity" v-else
:label="t('app.status')" :icon="footerItem.icon"
to="https://status.hoppscotch.io" :label="footerItem.text(t)"
blank blank
@click="hide()" @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

View File

@@ -18,14 +18,14 @@
</div> </div>
<div class="inline-flex items-center justify-center flex-1 space-x-2"> <div class="inline-flex items-center justify-center flex-1 space-x-2">
<button <button
class="flex flex-1 items-center justify-between px-2 py-1 bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-xs hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary" class="flex flex-1 items-center justify-between px-2 py-1 self-stretch bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-60 hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
@click="invokeAction('modals.search.toggle')" @click="invokeAction('modals.search.toggle')"
> >
<span class="inline-flex flex-1 items-center"> <span class="inline-flex flex-1 items-center">
<icon-lucide-search class="mr-2 svg-icons" /> <icon-lucide-search class="mr-2 svg-icons" />
{{ t("app.search") }} {{ t("app.search") }}
</span> </span>
<span class="flex"> <span class="flex space-x-1">
<kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd> <kbd class="shortcut-key">{{ getPlatformSpecialKey() }}</kbd>
<kbd class="shortcut-key">K</kbd> <kbd class="shortcut-key">K</kbd>
</span> </span>
@@ -254,8 +254,10 @@ import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { onLoggedIn } from "~/composables/auth" import { onLoggedIn } from "~/composables/auth"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { getPlatformSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { useToast } from "~/composables/toast"
const t = useI18n() const t = useI18n()
const toast = useToast()
/** /**
* Once the PWA code is initialized, this holds a method * Once the PWA code is initialized, this holds a method
@@ -372,6 +374,8 @@ const handleTeamEdit = () => {
editingTeamID.value = workspace.value.teamID editingTeamID.value = workspace.value.teamID
editingTeamName.value = { name: selectedTeam.value.name } editingTeamName.value = { name: selectedTeam.value.name }
displayModalEdit(true) displayModalEdit(true)
} else {
noPermission()
} }
} }
@@ -382,6 +386,19 @@ const settings = ref<any | null>(null)
const logout = ref<any | null>(null) const logout = ref<any | null>(null)
const accountActions = ref<any | null>(null) const accountActions = ref<any | null>(null)
defineActionHandler("modals.team.edit", handleTeamEdit)
defineActionHandler("modals.team.invite", () => {
if (
selectedTeam.value?.myRole === "OWNER" ||
selectedTeam.value?.myRole === "EDITOR"
) {
inviteTeam({ name: selectedTeam.value.name }, selectedTeam.value.id)
} else {
noPermission()
}
})
defineActionHandler( defineActionHandler(
"user.login", "user.login",
() => { () => {
@@ -389,4 +406,8 @@ defineActionHandler(
}, },
computed(() => !currentUser.value) computed(() => !currentUser.value)
) )
const noPermission = () => {
toast.error(`${t("profile.no_permission")}`)
}
</script> </script>

View File

@@ -0,0 +1,112 @@
<template>
<div v-if="inspectionResults && inspectionResults.length > 0">
<tippy interactive trigger="click" theme="popover">
<div class="flex justify-center items-center flex-1 flex-col">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconAlertTriangle"
:class="severityColor(getHighestSeverity.severity)"
:title="t('inspections.description')"
/>
</div>
<template #content="{ hide }">
<div class="flex flex-col space-y-2 items-start flex-1">
<div
class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
>
<span class="flex items-center flex-1">
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
<span class="font-bold">
{{ t("inspections.title") }}
</span>
</span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/inspections"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
</div>
<div
v-for="(inspector, index) in inspectionResults"
:key="index"
class="flex self-stretch max-w-md w-full"
>
<div
class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
>
<span
v-if="inspector.text.type === 'text'"
class="flex-1 px-3 py-2"
>
{{ inspector.text.text }}
<HoppSmartLink
blank
:to="inspector.doc.link"
class="text-accent hover:text-accentDark transition"
>
{{ inspector.doc.text }}
<icon-lucide-arrow-up-right class="svg-icons" />
</HoppSmartLink>
</span>
<span v-if="inspector.action" class="flex p-2 space-x-2">
<HoppButtonSecondary
:label="inspector.action.text"
outline
filled
@click="
() => {
inspector.action?.apply()
hide()
}
"
/>
</span>
</div>
</div>
</div>
</template>
</tippy>
</div>
</template>
<script lang="ts" setup>
import { InspectorResult } from "~/services/inspection"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import IconHelpCircle from "~icons/lucide/help-circle"
import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
const t = useI18n()
const props = defineProps<{
inspectionResults: InspectorResult[] | undefined
}>()
const getHighestSeverity = computed(() => {
if (props.inspectionResults) {
return props.inspectionResults.reduce(
(prev, curr) => {
return prev.severity > curr.severity ? prev : curr
},
{ severity: 0 }
)
} else {
return { severity: 0 }
}
})
const severityColor = (severity: number) => {
switch (severity) {
case 1:
return "!text-green-500 hover:!text-green-600"
case 2:
return "!text-yellow-500 hover:!text-yellow-600"
case 3:
return "!text-red-500 hover:!text-red-600"
default:
return "!text-gray-500 hover:!text-gray-600"
}
}
</script>

View File

@@ -8,91 +8,41 @@
{{ t("settings.interceptor_description") }} {{ t("settings.interceptor_description") }}
</p> </p>
</div> </div>
<HoppSmartRadioGroup
v-model="interceptorSelection" <div>
:radios="interceptors"
/>
<div <div
v-if="interceptorSelection == 'EXTENSIONS_ENABLED' && !extensionVersion" v-for="interceptor in interceptors"
class="flex space-x-2" :key="interceptor.interceptorID"
class="flex flex-col"
> >
<HoppButtonSecondary <HoppSmartRadio
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld" :value="interceptor.interceptorID"
blank :label="unref(interceptor.name(t))"
:icon="IconChrome" :selected="interceptorSelection === interceptor.interceptorID"
label="Chrome" @change="interceptorSelection = interceptor.interceptorID"
outline
class="!flex-1"
/> />
<HoppButtonSecondary
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch" <component
blank :is="interceptor.selectorSubtitle"
:icon="IconFirefox" v-if="interceptor.selectorSubtitle"
label="Firefox"
outline
class="!flex-1"
/> />
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconChrome from "~icons/brands/chrome"
import IconFirefox from "~icons/brands/firefox"
import { computed } from "vue"
import { applySetting, toggleSetting } from "~/newstore/settings"
import { useSetting } from "@composables/settings"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useService } from "dioc/vue"
import { extensionStatus$ } from "~/newstore/HoppExtension" import { Ref, unref } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
const t = useI18n() const t = useI18n()
const PROXY_ENABLED = useSetting("PROXY_ENABLED") const interceptorService = useService(InterceptorService)
const EXTENSIONS_ENABLED = useSetting("EXTENSIONS_ENABLED")
const currentExtensionStatus = useReadonlyStream(extensionStatus$, null) const interceptorSelection =
interceptorService.currentInterceptorID as Ref<string>
const extensionVersion = computed(() => { const interceptors = interceptorService.availableInterceptors
return currentExtensionStatus.value === "available"
? window.__POSTWOMAN_EXTENSION_HOOK__?.getVersion() ?? null
: null
})
const interceptors = computed(() => [
{ value: "BROWSER_ENABLED" as const, label: t("state.none") },
{ value: "PROXY_ENABLED" as const, label: t("settings.proxy") },
{
value: "EXTENSIONS_ENABLED" as const,
label:
`${t("settings.extensions")}: ` +
(extensionVersion.value !== null
? `v${extensionVersion.value.major}.${extensionVersion.value.minor}`
: t("settings.extension_ver_not_reported")),
},
])
type InterceptorMode = (typeof interceptors)["value"][number]["value"]
const interceptorSelection = computed<InterceptorMode>({
get() {
if (PROXY_ENABLED.value) return "PROXY_ENABLED"
if (EXTENSIONS_ENABLED.value) return "EXTENSIONS_ENABLED"
return "BROWSER_ENABLED"
},
set(val) {
if (val === "EXTENSIONS_ENABLED") {
applySetting("EXTENSIONS_ENABLED", true)
if (PROXY_ENABLED.value) toggleSetting("PROXY_ENABLED")
}
if (val === "PROXY_ENABLED") {
applySetting("PROXY_ENABLED", true)
if (EXTENSIONS_ENABLED.value) toggleSetting("EXTENSIONS_ENABLED")
}
if (val === "BROWSER_ENABLED") {
applySetting("PROXY_ENABLED", false)
applySetting("EXTENSIONS_ENABLED", false)
}
},
})
</script> </script>

View File

@@ -30,134 +30,52 @@
<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>
<template
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
:key="item.id"
>
<HoppSmartItem <HoppSmartItem
:icon="IconBook" v-if="item.action.type === 'link'"
:label="t('app.documentation')" :icon="item.icon"
to="https://docs.hoppscotch.io" :label="item.text(t)"
:description="t('support.documentation')" :to="item.action.href"
:description="item.subtitle(t)"
:info-icon="IconChevronRight" :info-icon="IconChevronRight"
active active
blank blank
@click="hideModal()" @click="hideModal()"
/> />
<HoppSmartItem <HoppSmartItem
:icon="IconGift" v-else
:label="t('app.whats_new')" :icon="item.icon"
to="https://docs.hoppscotch.io/documentation/changelog" :label="item.text(t)"
:description="t('support.changelog')" :description="item.subtitle(t)"
:info-icon="IconChevronRight" :info-icon="IconChevronRight"
active active
blank @click="
@click="hideModal()" () => {
/> // @ts-expect-error Typescript isn't able to understand
<HoppSmartItem item.action.do()
:icon="IconActivity" hideModal()
:label="t('app.status')" }
to="https://status.hoppscotch.io" "
blank
:description="t('app.status_description')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconLock"
:label="`${t('app.terms_and_privacy')}`"
to="https://docs.hoppscotch.io/support/privacy"
blank
:description="t('app.terms_and_privacy')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
{{ t("settings.follow") }}
</h2>
<HoppSmartItem
:icon="IconDiscord"
:label="t('app.discord')"
to="https://hoppscotch.io/discord"
blank
:description="t('app.join_discord_community')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconTwitter"
:label="t('app.twitter')"
to="https://hoppscotch.io/twitter"
blank
:description="t('support.twitter')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconGithub"
:label="`${t('app.github')}`"
to="https://github.com/hoppscotch/hoppscotch"
blank
:description="t('support.github')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconMessageCircle"
:label="t('app.chat_with_us')"
:description="t('support.chat')"
:info-icon="IconChevronRight"
active
@click="chatWithUs()"
/>
<HoppSmartItem
:icon="IconUserPlus"
:label="`${t('app.invite')}`"
:description="t('shortcut.miscellaneous.invite')"
:info-icon="IconChevronRight"
active
@click="expandInvite()"
/>
<HoppSmartItem
v-if="navigatorShare"
v-tippy="{ theme: 'tooltip' }"
:icon="IconShare2"
:label="`${t('request.share')}`"
:description="t('request.share_description')"
:info-icon="IconChevronRight"
active
@click="nativeShare()"
/> />
</template>
</div> </div>
<AppShare :show="showShare" @hide-modal="showShare = false" />
</template> </template>
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, 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 { defineActionHandler } 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 showShare = ref(false)
const ZEN_MODE = useSetting("ZEN_MODE") const ZEN_MODE = useSetting("ZEN_MODE")
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION") const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
@@ -174,19 +92,10 @@ defineProps<{
show: boolean show: boolean
}>() }>()
defineActionHandler("modals.share.toggle", () => {
showShare.value = !showShare.value
})
const emit = defineEmits<{ 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()
@@ -197,24 +106,6 @@ const expandCollection = () => {
hideModal() hideModal()
} }
const expandInvite = () => {
showShare.value = true
}
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")
} }

View File

@@ -18,13 +18,18 @@
:horizontal="COLUMN_LAYOUT" :horizontal="COLUMN_LAYOUT"
@resize="setPaneEvent($event, 'horizontal')" @resize="setPaneEvent($event, 'horizontal')"
> >
<Pane :size="PANE_MAIN_TOP_SIZE" class="flex flex-col !overflow-auto"> <Pane
:size="PANE_MAIN_TOP_SIZE"
class="flex flex-col !overflow-auto"
min-size="25"
>
<slot name="primary" /> <slot name="primary" />
</Pane> </Pane>
<Pane <Pane
v-if="hasSecondary" v-if="hasSecondary"
:size="PANE_MAIN_BOTTOM_SIZE" :size="PANE_MAIN_BOTTOM_SIZE"
class="flex flex-col !overflow-auto" class="flex flex-col !overflow-auto"
min-size="25"
> >
<slot name="secondary" /> <slot name="secondary" />
</Pane> </Pane>
@@ -33,7 +38,7 @@
<Pane <Pane
v-if="SIDEBAR && hasSidebar" v-if="SIDEBAR && hasSidebar"
:size="PANE_SIDEBAR_SIZE" :size="PANE_SIDEBAR_SIZE"
min-size="20" min-size="25"
class="flex flex-col !overflow-auto bg-primaryContrast" class="flex flex-col !overflow-auto bg-primaryContrast"
> >
<slot name="sidebar" /> <slot name="sidebar" />
@@ -78,10 +83,10 @@ type PaneEvent = {
size: number size: number
} }
const PANE_MAIN_SIZE = ref(74) const PANE_MAIN_SIZE = ref(70)
const PANE_SIDEBAR_SIZE = ref(26) const PANE_SIDEBAR_SIZE = ref(30)
const PANE_MAIN_TOP_SIZE = ref(42) const PANE_MAIN_TOP_SIZE = ref(35)
const PANE_MAIN_BOTTOM_SIZE = ref(58) const PANE_MAIN_BOTTOM_SIZE = ref(65)
if (!COLUMN_LAYOUT.value) { if (!COLUMN_LAYOUT.value) {
PANE_MAIN_TOP_SIZE.value = 50 PANE_MAIN_TOP_SIZE.value = 50

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col items-center justify-center text-secondaryLight"> <div class="flex flex-col items-center justify-center text-secondaryLight">
<div class="flex pb-4 my-4 space-x-2"> <div class="flex mb-4 space-x-2">
<div class="flex flex-col items-end space-y-4 text-right"> <div class="flex flex-col items-end space-y-4 text-right">
<span class="flex items-center flex-1"> <span class="flex items-center flex-1">
{{ t("shortcut.request.send_request") }} {{ t("shortcut.request.send_request") }}

View File

@@ -8,89 +8,46 @@
> >
<template #body> <template #body>
<div class="flex flex-col space-y-2"> <div class="flex flex-col space-y-2">
<template
v-for="item in platform.ui?.additionalSupportOptionsMenuItems"
:key="item.id"
>
<HoppSmartItem <HoppSmartItem
:icon="IconBook" v-if="item.action.type === 'link'"
:label="t('app.documentation')" :icon="item.icon"
to="https://docs.hoppscotch.io" :label="item.text(t)"
:description="t('support.documentation')" :to="item.action.href"
:description="item.subtitle(t)"
:info-icon="IconChevronRight" :info-icon="IconChevronRight"
active active
blank blank
@click="hideModal()" @click="hideModal()"
/> />
<HoppSmartItem <HoppSmartItem
:icon="IconZap" v-else
:label="t('app.keyboard_shortcuts')" :icon="item.icon"
:description="t('support.shortcuts')" :label="item.text(t)"
:description="item.subtitle(t)"
:info-icon="IconChevronRight" :info-icon="IconChevronRight"
active active
@click="showShortcuts()" @click="
/> () => {
<HoppSmartItem // @ts-expect-error Typescript isn't able to understand
:icon="IconGift" item.action.do()
:label="t('app.whats_new')" hideModal()
to="https://docs.hoppscotch.io/documentation/changelog" }
:description="t('support.changelog')" "
:info-icon="IconChevronRight"
active
blank
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconMessageCircle"
:label="t('app.chat_with_us')"
:description="t('support.chat')"
:info-icon="IconChevronRight"
active
@click="chatWithUs()"
/>
<HoppSmartItem
:icon="IconGitHub"
:label="t('app.github')"
to="https://hoppscotch.io/github"
blank
:description="t('support.github')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconDiscord"
:label="t('app.join_discord_community')"
to="https://hoppscotch.io/discord"
blank
:description="t('support.community')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/>
<HoppSmartItem
:icon="IconTwitter"
:label="t('app.twitter')"
to="https://hoppscotch.io/twitter"
blank
:description="t('support.twitter')"
:info-icon="IconChevronRight"
active
@click="hideModal()"
/> />
</template>
</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")
} }

View File

@@ -80,7 +80,8 @@ const props = defineProps<{
active: boolean active: boolean
}>() }>()
const formattedShortcutKeys = computed(() => const formattedShortcutKeys = computed(
() =>
props.entry.meta?.keyboardShortcut?.map((key) => { props.entry.meta?.keyboardShortcut?.map((key) => {
return SPECIAL_KEY_CHARS[key] ?? capitalize(key) return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
}) })

View File

@@ -0,0 +1,65 @@
<template>
<span class="flex flex-1 space-x-2 items-center">
<template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { graphqlCollectionStore } from "~/newstore/collections"
const props = defineProps<{
folderPath: string
}>()
const pathFolders = computed(() => {
try {
const folderIndicies = props.folderPath
.split("/")
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection<HoppGQLRequest>[] = []
let currentFolder =
graphqlCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
pathItems.push(folder)
currentFolder = folder
}
return pathItems
} catch (e) {
console.error(e)
return []
}
})
const request = computed(() => {
try {
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex
]
} catch (e) {
return null
}
})
</script>

View File

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

View File

@@ -0,0 +1,71 @@
<template>
<span class="flex flex-1 items-center space-x-2">
<template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span
v-if="request"
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
:class="getMethodLabelColorClassOf(request)"
>
{{ request.method.toUpperCase() }}
</span>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { restCollectionStore } from "~/newstore/collections"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
const props = defineProps<{
folderPath: string
}>()
const pathFolders = computed(() => {
try {
const folderIndicies = props.folderPath
.split("/")
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection<HoppRESTRequest>[] = []
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)
while (folderIndicies.length > 0) {
const folderIndex = folderIndicies.shift()!
const folder = currentFolder.folders[folderIndex]
pathItems.push(folder)
currentFolder = folder
}
return pathItems
} catch (e) {
console.error(e)
return []
}
})
const request = computed(() => {
try {
const requestIndex = parseInt(props.folderPath.split("/").at(-1)!)
return pathFolders.value[pathFolders.value.length - 1].requests[
requestIndex
]
} catch (e) {
return null
}
})
</script>

View File

@@ -95,6 +95,23 @@ import {
import { isEqual } from "lodash-es" import { isEqual } from "lodash-es"
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher" import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher" import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
import {
EnvironmentsSpotlightSearcherService,
SwitchEnvSpotlightSearcherService,
} from "~/services/spotlight/searchers/environment.searcher"
import {
SwitchWorkspaceSpotlightSearcherService,
WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
const t = useI18n() const t = useI18n()
@@ -110,6 +127,19 @@ const spotlightService = useService(SpotlightService)
useService(HistorySpotlightSearcherService) useService(HistorySpotlightSearcherService)
useService(UserSpotlightSearcherService) useService(UserSpotlightSearcherService)
useService(NavigationSpotlightSearcherService)
useService(SettingsSpotlightSearcherService)
useService(CollectionsSpotlightSearcherService)
useService(MiscellaneousSpotlightSearcherService)
useService(TabSpotlightSearcherService)
useService(GeneralSpotlightSearcherService)
useService(ResponseSpotlightSearcherService)
useService(RequestSpotlightSearcherService)
useService(EnvironmentsSpotlightSearcherService)
useService(SwitchEnvSpotlightSearcherService)
useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService)
const search = ref("") const search = ref("")

View File

@@ -7,7 +7,7 @@
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
:label="t('action.label')" :label="t('action.label')"
input-styles="floating-input" input-styles="floating-input"
@@ -57,28 +57,28 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const name = ref("") const editingName = ref("")
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (!show) { if (!show) {
name.value = "" editingName.value = ""
} }
} }
) )
const addNewCollection = () => { const addNewCollection = () => {
if (!name.value) { if (!editingName.value) {
toast.error(t("collection.invalid_name")) toast.error(t("collection.invalid_name"))
return return
} }
emit("submit", name.value) emit("submit", editingName.value)
} }
const hideModal = () => { const hideModal = () => {
name.value = "" editingName.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -7,7 +7,7 @@
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
input-styles="floating-input" input-styles="floating-input"
:label="t('action.label')" :label="t('action.label')"
@@ -57,27 +57,27 @@ const emit = defineEmits<{
(e: "add-folder", name: string): void (e: "add-folder", name: string): void
}>() }>()
const name = ref("") const editingName = ref("")
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (!show) { if (!show) {
name.value = "" editingName.value = ""
} }
} }
) )
const addFolder = () => { const addFolder = () => {
if (name.value.trim() === "") { if (editingName.value.trim() === "") {
toast.error(t("folder.invalid_name")) toast.error(t("folder.invalid_name"))
return return
} }
emit("add-folder", name.value) emit("add-folder", editingName.value)
} }
const hideModal = () => { const hideModal = () => {
name.value = "" editingName.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -7,7 +7,7 @@
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
:label="t('action.label')" :label="t('action.label')"
input-styles="floating-input" input-styles="floating-input"
@@ -58,23 +58,23 @@ const emit = defineEmits<{
(event: "add-request", name: string): void (event: "add-request", name: string): void
}>() }>()
const name = ref("") const editingName = ref("")
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
name.value = currentActiveTab.value.document.request.name editingName.value = currentActiveTab.value.document.request.name
} }
} }
) )
const addRequest = () => { const addRequest = () => {
if (name.value.trim() === "") { if (editingName.value.trim() === "") {
toast.error(`${t("error.empty_req_name")}`) toast.error(`${t("error.empty_req_name")}`)
return return
} }
emit("add-request", name.value) emit("add-request", editingName.value)
} }
const hideModal = () => { const hideModal = () => {

View File

@@ -7,7 +7,7 @@
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
input-styles="floating-input" input-styles="floating-input"
:label="t('action.label')" :label="t('action.label')"
@@ -59,26 +59,26 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const name = ref("") const editingName = ref("")
watch( watch(
() => props.editingCollectionName, () => props.editingCollectionName,
(newName) => { (newName) => {
name.value = newName editingName.value = newName
} }
) )
const saveCollection = () => { const saveCollection = () => {
if (name.value.trim() === "") { if (editingName.value.trim() === "") {
toast.error(t("collection.invalid_name")) toast.error(t("collection.invalid_name"))
return return
} }
emit("submit", name.value) emit("submit", editingName.value)
} }
const hideModal = () => { const hideModal = () => {
name.value = "" editingName.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -7,7 +7,7 @@
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
:label="t('action.label')" :label="t('action.label')"
input-styles="floating-input" input-styles="floating-input"
@@ -59,26 +59,26 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const name = ref("") const editingName = ref("")
watch( watch(
() => props.editingFolderName, () => props.editingFolderName,
(newName) => { (newName) => {
name.value = newName editingName.value = newName
} }
) )
const editFolder = () => { const editFolder = () => {
if (name.value.trim() === "") { if (editingName.value.trim() === "") {
toast.error(t("folder.invalid_name")) toast.error(t("folder.invalid_name"))
return return
} }
emit("submit", name.value) emit("submit", editingName.value)
} }
const hideModal = () => { const hideModal = () => {
name.value = "" editingName.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -7,7 +7,7 @@
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
:label="t('action.label')" :label="t('action.label')"
input-styles="floating-input" input-styles="floating-input"
@@ -60,19 +60,19 @@ const emit = defineEmits<{
(e: "update:modelValue", value: string): void (e: "update:modelValue", value: string): void
}>() }>()
const name = useVModel(props, "modelValue") const editingName = useVModel(props, "modelValue")
const editRequest = () => { const editRequest = () => {
if (name.value.trim() === "") { if (editingName.value.trim() === "") {
toast.error(t("request.invalid_name")) toast.error(t("request.invalid_name"))
return return
} }
emit("submit", name.value) emit("submit", editingName.value)
} }
const hideModal = () => { const hideModal = () => {
name.value = "" editingName.value = ""
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -32,7 +32,7 @@
</span> </span>
</div> </div>
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<SmartTree :adapter="myAdapter"> <HoppSmartTree :adapter="myAdapter">
<template <template
#content="{ node, toggleChildren, isOpen, highlightChildren }" #content="{ node, toggleChildren, isOpen, highlightChildren }"
> >
@@ -291,7 +291,7 @@
> >
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</template> </template>
</SmartTree> </HoppSmartTree>
</div> </div>
</div> </div>
</template> </template>
@@ -303,7 +303,10 @@ import IconHelpCircle from "~icons/lucide/help-circle"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed, PropType, Ref, toRef } from "vue" import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter" import {
ChildrenResult,
SmartTreeAdapter,
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"

View File

@@ -71,7 +71,6 @@ import {
updateTeamRequest, updateTeamRequest,
} from "~/helpers/backend/mutations/TeamRequest" } from "~/helpers/backend/mutations/TeamRequest"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { getGQLSession, useGQLRequestName } from "~/newstore/GQLSession"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { import {
@@ -82,8 +81,9 @@ import {
} from "~/newstore/collections" } from "~/newstore/collections"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core" import { computedWithControl } from "@vueuse/core"
import { currentActiveTab } from "~/helpers/rest/tab"
import { platform } from "~/platform" import { platform } from "~/platform"
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -122,10 +122,14 @@ const emit = defineEmits<{
(e: "hide-modal"): void (e: "hide-modal"): void
}>() }>()
const gqlRequestName = useGQLRequestName() const gqlRequestName = computedWithControl(
() => activeGQLTab.value,
() => activeGQLTab.value.document.request.name
)
const restRequestName = computedWithControl( const restRequestName = computedWithControl(
() => currentActiveTab.value, () => activeRESTTab.value,
() => currentActiveTab.value.document.request.name () => activeRESTTab.value.document.request.name
) )
const reqName = computed(() => { const reqName = computed(() => {
@@ -141,11 +145,13 @@ const reqName = computed(() => {
const requestName = ref(reqName.value) const requestName = ref(reqName.value)
watch( watch(
() => [currentActiveTab.value, gqlRequestName.value], () => [activeRESTTab.value, activeGQLTab.value],
() => { () => {
if (props.mode === "rest") { if (props.mode === "rest") {
requestName.value = currentActiveTab.value?.document.request.name ?? "" requestName.value = activeRESTTab.value?.document.request.name ?? ""
} else requestName.value = gqlRequestName.value } else {
requestName.value = activeGQLTab.value?.document.request.name ?? ""
}
} }
) )
@@ -202,15 +208,10 @@ const saveRequestAs = async () => {
return return
} }
let requestUpdated const requestUpdated =
props.mode === "rest"
if (props.request) { ? cloneDeep(activeRESTTab.value.document.request)
requestUpdated = cloneDeep(props.request) : cloneDeep(activeGQLTab.value.document.request)
} else if (props.mode === "rest") {
requestUpdated = cloneDeep(currentActiveTab.value.document.request)
} else {
requestUpdated = cloneDeep(getGQLSession().request)
}
requestUpdated.name = requestName.value requestUpdated.name = requestName.value
@@ -223,7 +224,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
currentActiveTab.value.document = { activeRESTTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -250,7 +251,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
currentActiveTab.value.document = { activeRESTTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -278,7 +279,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
currentActiveTab.value.document = { activeRESTTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -438,7 +439,7 @@ const updateTeamCollectionOrFolder = (
(result) => { (result) => {
const { createRequestInCollection } = result const { createRequestInCollection } = result
currentActiveTab.value.document = { activeRESTTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -459,7 +460,7 @@ const updateTeamCollectionOrFolder = (
const requestSaved = () => { const requestSaved = () => {
toast.success(`${t("request.added")}`) toast.success(`${t("request.added")}`)
nextTick(() => { nextTick(() => {
currentActiveTab.value.document.isDirty = false activeRESTTab.value.document.isDirty = false
}) })
hideModal() hideModal()
} }

View File

@@ -46,7 +46,7 @@
</span> </span>
</div> </div>
<div class="flex flex-col overflow-hidden"> <div class="flex flex-col overflow-hidden">
<SmartTree :adapter="teamAdapter"> <HoppSmartTree :adapter="teamAdapter">
<template <template
#content="{ node, toggleChildren, isOpen, highlightChildren }" #content="{ node, toggleChildren, isOpen, highlightChildren }"
> >
@@ -311,7 +311,7 @@
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</div> </div>
</template> </template>
</SmartTree> </HoppSmartTree>
</div> </div>
</div> </div>
</template> </template>
@@ -326,7 +326,10 @@ import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { TeamCollection } from "~/helpers/teams/TeamCollection" import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { TeamRequest } from "~/helpers/teams/TeamRequest" import { TeamRequest } from "~/helpers/teams/TeamRequest"
import { ChildrenResult, SmartTreeAdapter } from "~/helpers/treeAdapter" import {
ChildrenResult,
SmartTreeAdapter,
} from "@hoppscotch/ui/dist/helpers/treeAdapter"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { HoppRESTRequest } from "@hoppscotch/data" import { HoppRESTRequest } from "@hoppscotch/data"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"

View File

@@ -7,7 +7,7 @@
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
:label="t('action.label')" :label="t('action.label')"
input-styles="floating-input" input-styles="floating-input"
@@ -36,7 +36,7 @@
import { ref, watch } from "vue" import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { getGQLSession } from "~/newstore/GQLSession" import { currentActiveTab } from "~/helpers/graphql/tab"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
@@ -57,24 +57,24 @@ const emit = defineEmits<{
): void ): void
}>() }>()
const name = ref("") const editingName = ref("")
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
name.value = getGQLSession().request.name editingName.value = currentActiveTab.value?.document.request.name
} }
} }
) )
const addRequest = () => { const addRequest = () => {
if (!name.value) { if (!editingName.value) {
toast.error(`${t("error.empty_req_name")}`) toast.error(`${t("error.empty_req_name")}`)
return return
} }
emit("add-request", { emit("add-request", {
name: name.value, name: editingName.value,
path: props.folderPath, path: props.folderPath,
}) })
hideModal() hideModal()

View File

@@ -37,6 +37,7 @@
@click=" @click="
emit('add-request', { emit('add-request', {
path: `${collectionIndex}`, path: `${collectionIndex}`,
index: collection.requests.length,
}) })
" "
/> />
@@ -219,6 +220,7 @@ import {
moveGraphqlRequest, moveGraphqlRequest,
} from "~/newstore/collections" } from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { getTabsRefTo } from "~/helpers/graphql/tab"
const props = defineProps({ const props = defineProps({
picked: { type: Object, default: null }, picked: { type: Object, default: null },
@@ -293,6 +295,22 @@ const removeCollection = () => {
emit("select", null) emit("select", null)
} }
const possibleTabs = getTabsRefTo((tab) => {
const ctx = tab.document.saveContext
if (!ctx) return false
return (
ctx.originLocation === "user-collection" &&
ctx.folderPath.startsWith(props.collectionIndex.toString())
)
})
for (const tab of possibleTabs) {
tab.value.document.saveContext = undefined
tab.value.document.isDirty = true
}
removeGraphqlCollection(props.collectionIndex, props.collection.id) removeGraphqlCollection(props.collectionIndex, props.collection.id)
toast.success(`${t("state.deleted")}`) toast.success(`${t("state.deleted")}`)
} }

View File

@@ -7,7 +7,7 @@
> >
<template #body> <template #body>
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
:label="t('action.label')" :label="t('action.label')"
input-styles="floating-input" input-styles="floating-input"
@@ -52,17 +52,17 @@ const emit = defineEmits<{
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const name = ref<string | null>() const editingName = ref<string | null>()
watch( watch(
() => props.editingCollectionName, () => props.editingCollectionName,
(val) => { (val) => {
name.value = val editingName.value = val
} }
) )
const saveCollection = () => { const saveCollection = () => {
if (!name.value) { if (!editingName.value) {
toast.error(`${t("collection.invalid_name")}`) toast.error(`${t("collection.invalid_name")}`)
return return
} }
@@ -70,7 +70,7 @@ const saveCollection = () => {
// TODO: Better typechecking here ? // TODO: Better typechecking here ?
const collectionUpdated = { const collectionUpdated = {
...(props.editingCollection as any), ...(props.editingCollection as any),
name: name.value, name: editingName.value,
} }
editGraphqlCollection(props.editingCollectionIndex, collectionUpdated) editGraphqlCollection(props.editingCollectionIndex, collectionUpdated)
@@ -78,7 +78,7 @@ const saveCollection = () => {
} }
const hideModal = () => { const hideModal = () => {
name.value = null editingName.value = null
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -34,7 +34,12 @@
:icon="IconFilePlus" :icon="IconFilePlus"
:title="t('request.new')" :title="t('request.new')"
class="hidden group-hover:inline-flex" class="hidden group-hover:inline-flex"
@click="emit('add-request', { path: folderPath })" @click="
emit('add-request', {
path: folderPath,
index: folder.requests.length,
})
"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -198,6 +203,7 @@ import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections" import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { getTabsRefTo } from "~/helpers/graphql/tab"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
@@ -249,10 +255,8 @@ const collectionIcon = computed(() => {
const pick = () => { const pick = () => {
emit("select", { emit("select", {
picked: {
pickedType: "gql-my-folder", pickedType: "gql-my-folder",
folderPath: props.folderPath, folderPath: props.folderPath,
},
}) })
} }
@@ -273,6 +277,22 @@ const removeFolder = () => {
emit("select", { picked: null }) emit("select", { picked: null })
} }
const possibleTabs = getTabsRefTo((tab) => {
const ctx = tab.document.saveContext
if (!ctx) return false
return (
ctx.originLocation === "user-collection" &&
ctx.folderPath.startsWith(props.folderPath)
)
})
for (const tab of possibleTabs) {
tab.value.document.saveContext = undefined
tab.value.document.isDirty = true
}
removeGraphqlFolder(props.folderPath, props.folder.id) removeGraphqlFolder(props.folderPath, props.folder.id)
toast.success(t("state.deleted")) toast.success(t("state.deleted"))
} }

View File

@@ -20,22 +20,28 @@
/> />
</span> </span>
<span <span
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark" class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="selectRequest()" @click="selectRequest()"
> >
<span class="truncate" :class="{ 'text-accent': isSelected }"> <span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }} {{ request.name }}
</span> </span>
<span
v-if="isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
>
</span>
<span
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
></span>
</span>
</span> </span>
<div class="flex"> <div class="flex">
<HoppButtonSecondary
v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }"
:icon="IconRotateCCW"
:title="t('action.restore')"
class="hidden group-hover:inline-flex"
@click="selectRequest()"
/>
<span> <span>
<tippy <tippy
ref="options" ref="options"
@@ -121,7 +127,6 @@
<script setup lang="ts"> <script setup lang="ts">
import IconCheckCircle from "~icons/lucide/check-circle" import IconCheckCircle from "~icons/lucide/check-circle"
import IconFile from "~icons/lucide/file" import IconFile from "~icons/lucide/file"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconMoreVertical from "~icons/lucide/more-vertical" import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit" import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
@@ -132,7 +137,12 @@ import { useToast } from "@composables/toast"
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data" import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { removeGraphqlRequest } from "~/newstore/collections" import { removeGraphqlRequest } from "~/newstore/collections"
import { setGQLSession } from "~/newstore/GQLSession" import {
createNewTab,
getTabRefWithSaveContext,
currentTabID,
currentActiveTab,
} from "~/helpers/graphql/tab"
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
@@ -154,6 +164,18 @@ const props = defineProps({
requestIndex: { type: Number, default: null }, requestIndex: { type: Number, default: null },
}) })
const isActive = computed(() => {
const saveCtx = currentActiveTab.value?.document.saveContext
if (!saveCtx) return false
return (
saveCtx.originLocation === "user-collection" &&
saveCtx.folderPath === props.folderPath &&
saveCtx.requestIndex === props.requestIndex
)
})
// TODO: Better types please // TODO: Better types please
const emit = defineEmits(["select", "edit-request", "duplicate-request"]) const emit = defineEmits(["select", "edit-request", "duplicate-request"])
@@ -179,7 +201,24 @@ const selectRequest = () => {
if (props.saveRequest) { if (props.saveRequest) {
pick() pick()
} else { } else {
setGQLSession({ const possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
})
// Switch to that request if that request is open
if (possibleTab) {
currentTabID.value = possibleTab.value.id
return
}
createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
},
request: cloneDeep( request: cloneDeep(
makeGQLRequest({ makeGQLRequest({
name: props.request.name, name: props.request.name,
@@ -190,8 +229,7 @@ const selectRequest = () => {
auth: props.request.auth, auth: props.request.auth,
}) })
), ),
schema: "", isDirty: false,
response: "",
}) })
} }
} }
@@ -214,6 +252,18 @@ const removeRequest = () => {
emit("select", null) emit("select", null)
} }
// Detach the request from any of the tabs
const possibleTab = getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
})
if (possibleTab) {
possibleTab.value.document.saveContext = undefined
possibleTab.value.document.isDirty = true
}
removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id) removeGraphqlRequest(props.folderPath, props.requestIndex, props.request.id)
toast.success(`${t("state.deleted")}`) toast.success(`${t("state.deleted")}`)
} }

View File

@@ -137,7 +137,6 @@ import {
addGraphqlFolder, addGraphqlFolder,
saveGraphqlRequestAs, saveGraphqlRequestAs,
} from "~/newstore/collections" } from "~/newstore/collections"
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
@@ -146,6 +145,7 @@ import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { platform } from "~/platform" import { platform } from "~/platform"
import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -265,17 +265,22 @@ export default defineComponent({
this.$data.editingCollectionIndex = collectionIndex this.$data.editingCollectionIndex = collectionIndex
this.displayModalEdit(true) this.displayModalEdit(true)
}, },
onAddRequest({ name, path }) { onAddRequest({ name, path, index }) {
const newRequest = { const newRequest = {
...getGQLSession().request, ...currentActiveTab.value.document.request,
name, name,
} }
saveGraphqlRequestAs(path, newRequest) saveGraphqlRequestAs(path, newRequest)
setGQLSession({
createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: path,
requestIndex: index,
},
request: newRequest, request: newRequest,
schema: "", isDirty: false,
response: "",
}) })
platform.analytics?.logEvent({ platform.analytics?.logEvent({

View File

@@ -24,6 +24,7 @@
:placeholder="t('action.search')" :placeholder="t('action.search')"
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0" input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
type="search" type="search"
:autofocus="false"
:disabled="collectionsType.type === 'team-collections'" :disabled="collectionsType.type === 'team-collections'"
/> />
</div> </div>
@@ -238,6 +239,7 @@ import {
resetTeamRequestsContext, resetTeamRequestsContext,
} from "~/helpers/collection/collection" } from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering" import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler } from "~/helpers/actions"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -2066,4 +2068,8 @@ const getErrorMessage = (err: GQLError<string>) => {
} }
} }
} }
defineActionHandler("collection.new", () => {
displayModalAdd(true)
})
</script> </script>

View File

@@ -11,7 +11,7 @@
t("environment.name") t("environment.name")
}}</label> }}</label>
<input <input
v-model="name" v-model="editingName"
type="text" type="text"
:placeholder="t('environment.variable')" :placeholder="t('environment.variable')"
class="input" class="input"
@@ -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">
@@ -88,7 +93,6 @@ const props = defineProps<{
position: { top: number; left: number } position: { top: number; left: number }
name: string name: string
value: string value: string
replaceWithVariable: boolean
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@@ -106,9 +110,12 @@ watch(
scope.value = { scope.value = {
type: "global", type: "global",
} }
name.value = ""
replaceWithVariable.value = false replaceWithVariable.value = false
editingName.value = ""
editingValue.value = ""
} }
editingName.value = props.name
editingValue.value = props.value
} }
) )
@@ -132,31 +139,32 @@ const scope = ref<Scope>({
const replaceWithVariable = ref(false) const replaceWithVariable = ref(false)
const name = ref("") const editingName = ref(props.name)
const editingValue = ref(props.value)
const addEnvironment = async () => { const addEnvironment = async () => {
if (!name.value) { if (!editingName.value) {
toast.error(`${t("environment.invalid_name")}`) toast.error(`${t("environment.invalid_name")}`)
return return
} }
if (scope.value.type === "global") { if (scope.value.type === "global") {
addGlobalEnvVariable({ addGlobalEnvVariable({
key: name.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: name.value, key: editingName.value,
value: props.value, value: editingValue.value,
}) })
toast.success(`${t("environment.updated")}`) toast.success(`${t("environment.updated")}`)
} else { } else {
const newVariables = [ const newVariables = [
...scope.value.environment.environment.variables, ...scope.value.environment.environment.variables,
{ {
key: name.value, key: editingName.value,
value: props.value, value: editingValue.value,
}, },
] ]
await pipe( await pipe(
@@ -179,11 +187,11 @@ const addEnvironment = async () => {
} }
if (replaceWithVariable.value) { if (replaceWithVariable.value) {
//replace the current tab endpoint with the variable name with << and >> //replace the current tab endpoint with the variable name with << and >>
const variableName = `<<${name.value}>>` const variableName = `<<${editingName.value}>>`
//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
) )
} }

View File

@@ -1,14 +1,15 @@
<template> <template>
<div class="flex divide-x divide-dividerLight">
<tippy <tippy
interactive interactive
trigger="click" trigger="click"
theme="popover" theme="popover"
:on-shown="() => tippyActions!.focus()" :on-shown="() => envSelectorActions!.focus()"
> >
<span <span
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`" :title="`${t('environment.select')}`"
class="bg-transparent select-wrapper" class="select-wrapper"
> >
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconLayers" :icon="IconLayers"
@@ -22,10 +23,9 @@
class="flex-1 !justify-start pr-8 rounded-none" class="flex-1 !justify-start pr-8 rounded-none"
/> />
</span> </span>
<template #content="{ hide }"> <template #content="{ hide }">
<div <div
ref="tippyActions" ref="envSelectorActions"
role="menu" role="menu"
class="flex flex-col focus:outline-none" class="flex flex-col focus:outline-none"
tabindex="0" tabindex="0"
@@ -66,7 +66,11 @@
/> />
<HoppSmartTabs <HoppSmartTabs
v-model="selectedEnvTab" v-model="selectedEnvTab"
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary" :styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${
!isTeamSelected || workspace.type === 'personal'
? 'bg-primaryLight'
: ''
}`"
render-inactive-tabs render-inactive-tabs
> >
<HoppSmartTab <HoppSmartTab
@@ -90,13 +94,20 @@
} }
" "
/> />
<HoppSmartPlaceholder <div
v-if="myEnvironments.length === 0" v-if="myEnvironments.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`" class="flex flex-col items-center justify-center text-secondaryLight"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
> >
</HoppSmartPlaceholder> <img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
:id="'team-environments'" :id="'team-environments'"
@@ -108,9 +119,11 @@
class="flex flex-col items-center justify-center p-4" class="flex flex-col items-center justify-center p-4"
> >
<HoppSmartSpinner class="my-4" /> <HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span> <span class="text-secondaryLight">
{{ t("state.loading") }}
</span>
</div> </div>
<div v-else-if="isTeamSelected" class="flex flex-col"> <div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in teamEnvironmentList" v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`" :key="`gen-team-${index}`"
@@ -128,14 +141,20 @@
} }
" "
/> />
<div
<HoppSmartPlaceholder
v-if="teamEnvironmentList.length === 0" v-if="teamEnvironmentList.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`" class="flex flex-col items-center justify-center text-secondaryLight"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
> >
</HoppSmartPlaceholder> <img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
{{ t("empty.environments") }}
</span>
</div>
</div> </div>
<div <div
v-if="!teamListLoading && teamAdapterError" v-if="!teamListLoading && teamAdapterError"
@@ -149,12 +168,135 @@
</div> </div>
</template> </template>
</tippy> </tippy>
<span class="flex">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => envQuickPeekActions!.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.quick_peek')}`"
:icon="IconEye"
class="!px-4"
/>
<template #content="{ hide }">
<div
ref="envQuickPeekActions"
role="menu"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<div
class="sticky top-0 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
>
{{ t("environment.global_variables") }}
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.edit')"
:icon="IconEdit"
@click="
() => {
editGlobalEnv()
hide()
}
"
/>
</div>
<div class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4">
<span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span class="w-full min-w-32 truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
<div
v-for="(variable, index) in globalEnvs"
:key="index"
class="flex flex-1 space-x-4"
>
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
{{ variable.key }}
</span>
<span class="text-secondaryLight w-full min-w-32 truncate">
{{ variable.value }}
</span>
</div>
<div v-if="globalEnvs.length === 0" class="text-secondaryLight">
{{ t("environment.empty_variables") }}
</div>
</div>
<div
class="sticky top-0 mt-2 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
:class="{
'bg-primaryLight': !selectedEnv.variables,
}"
>
{{ t("environment.list") }}
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:disabled="!selectedEnv.variables"
:title="t('action.edit')"
:icon="IconEdit"
@click="
() => {
editEnv()
hide()
}
"
/>
</div>
<div
v-if="selectedEnv.type === 'NO_ENV_SELECTED'"
class="text-secondaryLight my-2 flex flex-col flex-1 pl-4"
>
{{ t("environment.no_active_environment") }}
</div>
<div v-else class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4">
<span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span class="w-full min-w-32 truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
<div
v-for="(variable, index) in environmentVariables"
:key="index"
class="flex flex-1 space-x-4"
>
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
{{ variable.key }}
</span>
<span class="text-secondaryLight w-full min-w-32 truncate">
{{ variable.value }}
</span>
</div>
<div
v-if="environmentVariables.length === 0"
class="text-secondaryLight"
>
{{ t("environment.empty_variables") }}
</div>
</div>
</div>
</template>
</tippy>
</span>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check" 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 IconEdit from "~icons/lucide/edit"
import IconGlobe from "~icons/lucide/globe" 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"
@@ -162,6 +304,7 @@ import { GQLError } from "~/helpers/backend/GQLClient"
import { useReadonlyStream, useStream } from "~/composables/stream" import { useReadonlyStream, useStream } from "~/composables/stream"
import { import {
environments$, environments$,
globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
@@ -169,12 +312,14 @@ 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 TeamListAdapter from "~/helpers/teams/TeamListAdapter" import { invokeAction } from "~/helpers/actions"
import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment" import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { Environment } from "@hoppscotch/data" 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 Scope =
| { | {
@@ -189,12 +334,10 @@ type Scope =
type: "team-environment" type: "team-environment"
environment: TeamEnvironment environment: TeamEnvironment
} }
const props = defineProps<{ const props = defineProps<{
isScopeSelector?: boolean isScopeSelector?: boolean
modelValue?: Scope modelValue?: Scope
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", data: Scope): void (e: "update:modelValue", data: Scope): void
}>() }>()
@@ -230,7 +373,6 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
type: "team", type: "team",
}) })
} }
watch( watch(
() => myTeams.value, () => myTeams.value,
(newTeams) => { (newTeams) => {
@@ -253,32 +395,6 @@ const teamEnvironmentList = useReadonlyStream(
[] []
) )
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
setSelectedEnvironmentIndex
)
const isTeamSelected = computed(
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
)
const selectedEnvTab = ref<EnvironmentType>("my-environments")
watch(
() => workspace.value,
(newVal) => {
if (newVal.type === "personal") {
selectedEnvTab.value = "my-environments"
} else {
selectedEnvTab.value = "team-environments"
if (newVal.teamID) {
teamEnvListAdapter.changeTeamID(newVal.teamID)
}
}
}
)
const handleEnvironmentChange = ( const handleEnvironmentChange = (
index: number, index: number,
env?: env?:
@@ -320,7 +436,6 @@ const handleEnvironmentChange = (
} }
} }
} }
const isEnvActive = (id: string | number) => { const isEnvActive = (id: string | number) => {
if (props.isScopeSelector) { if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") { if (props.modelValue?.type === "my-environment") {
@@ -344,6 +459,32 @@ const isEnvActive = (id: string | number) => {
} }
} }
const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" },
setSelectedEnvironmentIndex
)
const isTeamSelected = computed(
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
)
const selectedEnvTab = ref<EnvironmentType>("my-environments")
watch(
() => workspace.value,
(newVal) => {
if (newVal.type === "personal") {
selectedEnvTab.value = "my-environments"
} else {
selectedEnvTab.value = "team-environments"
if (newVal.teamID) {
teamEnvListAdapter.changeTeamID(newVal.teamID)
}
}
}
)
const selectedEnv = computed(() => { const selectedEnv = computed(() => {
if (props.isScopeSelector) { if (props.isScopeSelector) {
if (props.modelValue?.type === "my-environment") { if (props.modelValue?.type === "my-environment") {
@@ -363,10 +504,13 @@ const selectedEnv = computed(() => {
} }
} else { } else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") { if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
myEnvironments.value[selectedEnvironmentIndex.value.index]
return { return {
type: "MY_ENV", type: "MY_ENV",
index: selectedEnvironmentIndex.value.index, index: selectedEnvironmentIndex.value.index,
name: myEnvironments.value[selectedEnvironmentIndex.value.index].name, name: environment.name,
variables: environment.variables,
} }
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") { } else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find( const teamEnv = teamEnvironmentList.value.find(
@@ -380,6 +524,7 @@ const selectedEnv = computed(() => {
type: "TEAM_ENV", type: "TEAM_ENV",
name: teamEnv.environment.name, name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID, teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
} }
} else { } else {
return { type: "NO_ENV_SELECTED" } return { type: "NO_ENV_SELECTED" }
@@ -429,7 +574,8 @@ onMounted(() => {
}) })
// Template refs // Template refs
const tippyActions = ref<TippyComponent | null>(null) const envSelectorActions = ref<TippyComponent | null>(null)
const envQuickPeekActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") { if (err.type === "network_error") {
@@ -443,4 +589,32 @@ const getErrorMessage = (err: GQLError<string>) => {
} }
} }
} }
const globalEnvs = useReadonlyStream(globalEnv$, [])
const environmentVariables = computed(() => {
if (selectedEnv.value.variables) {
return selectedEnv.value.variables
} else {
return []
}
})
const editGlobalEnv = () => {
invokeAction("modals.my.environment.edit", {
envName: "Global",
})
}
const editEnv = () => {
if (selectedEnv.value.type === "MY_ENV" && selectedEnv.value.name) {
invokeAction("modals.my.environment.edit", {
envName: selectedEnv.value.name,
})
} else if (selectedEnv.value.type === "TEAM_ENV" && selectedEnv.value.name) {
invokeAction("modals.team.environment.edit", {
envName: selectedEnv.value.name,
})
}
}
</script> </script>

View File

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

View File

@@ -8,7 +8,7 @@
<template #body> <template #body>
<div class="flex flex-col"> <div class="flex flex-col">
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
:label="t('action.label')" :label="t('action.label')"
input-styles="floating-input" input-styles="floating-input"
@@ -81,7 +81,6 @@
<HoppButtonSecondary <HoppButtonSecondary
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
class="mb-4"
@click="addEnvironmentVariable" @click="addEnvironmentVariable"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -171,7 +170,7 @@ const emit = defineEmits<{
const idTicker = ref(0) const idTicker = ref(0)
const name = ref<string | null>(null) const editingName = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([ const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } }, { id: idTicker.value++, env: { key: "", value: "" } },
]) ])
@@ -224,10 +223,12 @@ const liveEnvs = computed(() => {
} }
if (props.editingEnvironmentIndex === "Global") { if (props.editingEnvironmentIndex === "Global") {
return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))] return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
]
} else { } else {
return [ return [
...vars.value.map((x) => ({ ...x.env, source: name.value! })), ...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
...globalVars.value.map((x) => ({ ...x, source: "Global" })), ...globalVars.value.map((x) => ({ ...x, source: "Global" })),
] ]
} }
@@ -237,7 +238,7 @@ watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
name.value = workingEnv.value?.name ?? null editingName.value = workingEnv.value?.name ?? null
vars.value = pipe( vars.value = pipe(
workingEnv.value?.variables ?? [], workingEnv.value?.variables ?? [],
A.map((e) => ({ A.map((e) => ({
@@ -270,7 +271,7 @@ const removeEnvironmentVariable = (index: number) => {
} }
const saveEnvironment = () => { const saveEnvironment = () => {
if (!name.value) { if (!editingName.value) {
toast.error(`${t("environment.invalid_name")}`) toast.error(`${t("environment.invalid_name")}`)
return return
} }
@@ -286,13 +287,13 @@ const saveEnvironment = () => {
) )
const environmentUpdated: Environment = { const environmentUpdated: Environment = {
name: name.value, name: editingName.value,
variables: filterdVariables, variables: filterdVariables,
} }
if (props.action === "new") { if (props.action === "new") {
// Creating a new environment // Creating a new environment
createEnvironment(name.value, environmentUpdated.variables) createEnvironment(editingName.value, environmentUpdated.variables)
setSelectedEnvironmentIndex({ setSelectedEnvironmentIndex({
type: "MY_ENV", type: "MY_ENV",
index: envList.value.length - 1, index: envList.value.length - 1,
@@ -330,7 +331,7 @@ const saveEnvironment = () => {
} }
const hideModal = () => { const hideModal = () => {
name.value = null editingName.value = null
emit("hide-modal") emit("hide-modal")
} }
</script> </script>

View File

@@ -158,5 +158,7 @@ const duplicateEnvironments = () => {
cloneDeep(getGlobalVariables()) cloneDeep(getGlobalVariables())
) )
} else duplicateEnvironment(props.environmentIndex) } else duplicateEnvironment(props.environmentIndex)
toast.success(`${t("environment.duplicated")}`)
} }
</script> </script>

View File

@@ -42,7 +42,6 @@
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
outline outline
class="mb-4"
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -109,7 +108,7 @@ const resetSelectedData = () => {
defineActionHandler( defineActionHandler(
"modals.my.environment.edit", "modals.my.environment.edit",
({ envName, variableName }) => { ({ envName, variableName }) => {
editingVariableName.value = variableName if (variableName) editingVariableName.value = variableName
const envIndex: number = environments.value.findIndex( const envIndex: number = environments.value.findIndex(
(environment: Environment) => { (environment: Environment) => {
return environment.name === envName return environment.name === envName

View File

@@ -8,7 +8,7 @@
<template #body> <template #body>
<div class="flex flex-col px-2"> <div class="flex flex-col px-2">
<HoppSmartInput <HoppSmartInput
v-model="name" v-model="editingName"
placeholder=" " placeholder=" "
:input-styles="['floating-input', isViewer && 'opacity-25']" :input-styles="['floating-input', isViewer && 'opacity-25']"
:label="t('action.label')" :label="t('action.label')"
@@ -86,13 +86,11 @@
disabled disabled
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
class="mb-4"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
class="mb-4"
@click="addEnvironmentVariable" @click="addEnvironmentVariable"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -182,7 +180,7 @@ const emit = defineEmits<{
const idTicker = ref(0) const idTicker = ref(0)
const name = ref<string | null>(null) const editingName = ref<string | null>(null)
const vars = ref<EnvironmentVariable[]>([ const vars = ref<EnvironmentVariable[]>([
{ id: idTicker.value++, env: { key: "", value: "" } }, { id: idTicker.value++, env: { key: "", value: "" } },
]) ])
@@ -208,7 +206,9 @@ const liveEnvs = computed(() => {
if (evnExpandError.value) { if (evnExpandError.value) {
return [] return []
} else { } else {
return [...vars.value.map((x) => ({ ...x.env, source: name.value! }))] return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
]
} }
}) })
@@ -217,7 +217,7 @@ watch(
(show) => { (show) => {
if (show) { if (show) {
if (props.action === "new") { if (props.action === "new") {
name.value = null editingName.value = null
vars.value = pipe( vars.value = pipe(
props.envVars() ?? [], props.envVars() ?? [],
A.map((e: { key: string; value: string }) => ({ A.map((e: { key: string; value: string }) => ({
@@ -226,7 +226,7 @@ watch(
})) }))
) )
} else if (props.editingEnvironment !== null) { } else if (props.editingEnvironment !== null) {
name.value = props.editingEnvironment.environment.name ?? null editingName.value = props.editingEnvironment.environment.name ?? null
vars.value = pipe( vars.value = pipe(
props.editingEnvironment.environment.variables ?? [], props.editingEnvironment.environment.variables ?? [],
A.map((e: { key: string; value: string }) => ({ A.map((e: { key: string; value: string }) => ({
@@ -264,7 +264,7 @@ const isLoading = ref(false)
const saveEnvironment = async () => { const saveEnvironment = async () => {
isLoading.value = true isLoading.value = true
if (!name.value) { if (!editingName.value) {
toast.error(`${t("environment.invalid_name")}`) toast.error(`${t("environment.invalid_name")}`)
return return
} }
@@ -289,7 +289,7 @@ const saveEnvironment = async () => {
createTeamEnvironment( createTeamEnvironment(
JSON.stringify(filterdVariables), JSON.stringify(filterdVariables),
props.editingTeamId, props.editingTeamId,
name.value editingName.value
), ),
TE.match( TE.match(
(err: GQLError<string>) => { (err: GQLError<string>) => {
@@ -312,7 +312,7 @@ const saveEnvironment = async () => {
updateTeamEnvironment( updateTeamEnvironment(
JSON.stringify(filterdVariables), JSON.stringify(filterdVariables),
props.editingEnvironment.id, props.editingEnvironment.id,
name.value editingName.value
), ),
TE.match( TE.match(
(err: GQLError<string>) => { (err: GQLError<string>) => {
@@ -331,7 +331,7 @@ const saveEnvironment = async () => {
} }
const hideModal = () => { const hideModal = () => {
name.value = null editingName.value = null
emit("hide-modal") emit("hide-modal")
} }

View File

@@ -154,7 +154,7 @@ const duplicateEnvironments = () => {
toast.error(`${getErrorMessage(err)}`) toast.error(`${getErrorMessage(err)}`)
}, },
() => { () => {
toast.success(`${t("team_environment.duplicate")}`) toast.success(`${t("environment.duplicated")}`)
} }
) )
)() )()

View File

@@ -54,7 +54,6 @@
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
disabled disabled
filled filled
class="mb-4"
:icon="IconPlus" :icon="IconPlus"
:title="t('team.no_access')" :title="t('team.no_access')"
:label="t('action.new')" :label="t('action.new')"
@@ -64,7 +63,6 @@
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
outline outline
class="mb-4"
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
@@ -178,7 +176,7 @@ const getErrorMessage = (err: GQLError<string>) => {
defineActionHandler( defineActionHandler(
"modals.team.environment.edit", "modals.team.environment.edit",
({ envName, variableName }) => { ({ envName, variableName }) => {
editingVariableName.value = variableName if (variableName) editingVariableName.value = variableName
const teamEnvToEdit = props.teamEnvironments.find( const teamEnvToEdit = props.teamEnvironments.find(
(environment) => environment.environment.name === envName (environment) => environment.environment.name === envName
) )

View File

@@ -9,27 +9,22 @@
<template #body> <template #body>
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2"> <div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
<HoppSmartItem <HoppSmartItem
:loading="signingInWithGitHub" v-for="provider in allowedAuthProviders"
:icon="IconGithub" :key="provider.id"
:label="`${t('auth.continue_with_github')}`" :loading="provider.isLoading.value"
@click="signInWithGithub" :icon="provider.icon"
:label="provider.label"
@click="provider.action"
/> />
<hr v-if="additonalLoginItems.length > 0" />
<HoppSmartItem <HoppSmartItem
:loading="signingInWithGoogle" v-for="loginItem in additonalLoginItems"
:icon="IconGoogle" :key="loginItem.id"
:label="`${t('auth.continue_with_google')}`" :icon="loginItem.icon"
@click="signInWithGoogle" :label="loginItem.text(t)"
/> @click="doAdditionalLoginItemClickAction(loginItem)"
<HoppSmartItem
:loading="signingInWithMicrosoft"
:icon="IconMicrosoft"
:label="`${t('auth.continue_with_microsoft')}`"
@click="signInWithMicrosoft"
/>
<HoppSmartItem
:icon="IconEmail"
:label="`${t('auth.continue_with_email')}`"
@click="mode = 'email'"
/> />
</div> </div>
<form <form
@@ -113,68 +108,80 @@
</HoppSmartModal> </HoppSmartModal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue" import { Ref, computed, onMounted, ref } from "vue"
import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { platform } from "~/platform" import { platform } from "~/platform"
import { setLocalConfig } from "~/newstore/localpersistence"
import IconGithub from "~icons/auth/github" import IconGithub from "~icons/auth/github"
import IconGoogle from "~icons/auth/google" import IconGoogle from "~icons/auth/google"
import IconEmail from "~icons/auth/email" import IconEmail from "~icons/auth/email"
import IconMicrosoft from "~icons/auth/microsoft" import IconMicrosoft from "~icons/auth/microsoft"
import IconArrowLeft from "~icons/lucide/arrow-left" import IconArrowLeft from "~icons/lucide/arrow-left"
import { setLocalConfig } from "~/newstore/localpersistence"
import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
export default defineComponent({ import { LoginItemDef } from "~/platform/auth"
props: {
show: Boolean, defineProps<{
}, show: boolean
emits: ["hide-modal"], }>()
setup() {
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const { subscribeToStream } = useStreamSubscriber() const { subscribeToStream } = useStreamSubscriber()
const t = useI18n()
const toast = useToast()
const form = {
email: "",
}
const signingInWithGoogle = ref(false)
const signingInWithGitHub = ref(false)
const signingInWithMicrosoft = ref(false)
const signingInWithEmail = ref(false)
const mode = ref("sign-in")
const tosLink = import.meta.env.VITE_APP_TOS_LINK const tosLink = import.meta.env.VITE_APP_TOS_LINK
const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK const privacyPolicyLink = import.meta.env.VITE_APP_PRIVACY_POLICY_LINK
return { type AuthProviderItem = {
subscribeToStream, id: string
t: useI18n(), icon: typeof IconGithub
toast: useToast(), label: string
IconGithub, action: (...args: any[]) => any
IconGoogle, isLoading: Ref<boolean>
IconEmail,
IconMicrosoft,
IconArrowLeft,
tosLink,
privacyPolicyLink,
} }
},
data() { const additonalLoginItems = computed(
return { () => platform.auth.additionalLoginItems ?? []
form: { )
email: "",
}, const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
signingInWithGoogle: false, await item.onClick()
signingInWithGitHub: false, emit("hide-modal")
signingInWithMicrosoft: false,
signingInWithEmail: false,
mode: "sign-in",
} }
},
mounted() { onMounted(() => {
const currentUser$ = platform.auth.getCurrentUserStream() const currentUser$ = platform.auth.getCurrentUserStream()
this.subscribeToStream(currentUser$, (user) => { subscribeToStream(currentUser$, (user) => {
if (user) this.hideModal() if (user) hideModal()
}) })
}, })
methods: {
showLoginSuccess() { const showLoginSuccess = () => {
this.toast.success(`${this.t("auth.login_success")}`) toast.success(`${t("auth.login_success")}`)
}, }
async signInWithGoogle() {
this.signingInWithGoogle = true const signInWithGoogle = async () => {
signingInWithGoogle.value = true
try { try {
await platform.auth.signInUserWithGoogle() await platform.auth.signInUserWithGoogle()
@@ -184,32 +191,33 @@ export default defineComponent({
A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers A auth/account-exists-with-different-credential Firebase error wont happen between Google and any other providers
Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25 Seems Google account overwrites accounts of other providers https://github.com/firebase/firebase-android-sdk/issues/25
*/ */
this.toast.error(`${this.t("error.something_went_wrong")}`) toast.error(`${t("error.something_went_wrong")}`)
} }
this.signingInWithGoogle = false signingInWithGoogle.value = false
}, }
async signInWithGithub() {
this.signingInWithGitHub = true const signInWithGithub = async () => {
signingInWithGitHub.value = true
const result = await platform.auth.signInUserWithGithub() const result = await platform.auth.signInUserWithGithub()
if (!result) { if (!result) {
this.signingInWithGitHub = false signingInWithGitHub.value = false
return return
} }
if (result.type === "success") { if (result.type === "success") {
// this.showLoginSuccess() // this.showLoginSuccess()
} else if (result.type === "account-exists-with-different-cred") { } else if (result.type === "account-exists-with-different-cred") {
this.toast.info(`${this.t("auth.account_exists")}`, { toast.info(`${t("auth.account_exists")}`, {
duration: 0, duration: 0,
closeOnSwipe: false, closeOnSwipe: false,
action: { action: {
text: `${this.t("action.yes")}`, text: `${t("action.yes")}`,
onClick: async (_, toastObject) => { onClick: async (_, toastObject) => {
await result.link() await result.link()
this.showLoginSuccess() showLoginSuccess()
toastObject.goAway(0) toastObject.goAway(0)
}, },
@@ -217,13 +225,14 @@ export default defineComponent({
}) })
} else { } else {
console.log("error logging into github", result.err) console.log("error logging into github", result.err)
this.toast.error(`${this.t("error.something_went_wrong")}`) toast.error(`${t("error.something_went_wrong")}`)
} }
this.signingInWithGitHub = false signingInWithGitHub.value = false
}, }
async signInWithMicrosoft() {
this.signingInWithMicrosoft = true const signInWithMicrosoft = async () => {
signingInWithMicrosoft.value = true
try { try {
await platform.auth.signInUserWithMicrosoft() await platform.auth.signInUserWithMicrosoft()
@@ -238,34 +247,84 @@ export default defineComponent({
@firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set @firebase/auth: Auth (9.6.11): INTERNAL ASSERTION FAILED: Pending promise was never set
They may be related to https://github.com/firebase/firebaseui-web/issues/947 They may be related to https://github.com/firebase/firebaseui-web/issues/947
*/ */
this.toast.error(`${this.t("error.something_went_wrong")}`) toast.error(`${t("error.something_went_wrong")}`)
} }
this.signingInWithMicrosoft = false signingInWithMicrosoft.value = false
}, }
async signInWithEmail() {
this.signingInWithEmail = true const signInWithEmail = async () => {
signingInWithEmail.value = true
await platform.auth await platform.auth
.signInWithEmail(this.form.email) .signInWithEmail(form.email)
.then(() => { .then(() => {
this.mode = "email-sent" mode.value = "email-sent"
setLocalConfig("emailForSignIn", this.form.email) setLocalConfig("emailForSignIn", form.email)
}) })
.catch((e) => { .catch((e) => {
console.error(e) console.error(e)
this.toast.error(e.message) toast.error(e.message)
this.signingInWithEmail = false signingInWithEmail.value = false
}) })
.finally(() => { .finally(() => {
this.signingInWithEmail = false signingInWithEmail.value = false
}) })
}
const hideModal = () => {
mode.value = "sign-in"
toast.clear()
emit("hide-modal")
}
const authProviders: AuthProviderItem[] = [
{
id: "GITHUB",
icon: IconGithub,
label: t("auth.continue_with_github"),
action: signInWithGithub,
isLoading: signingInWithGitHub,
}, },
hideModal() { {
this.mode = "sign-in" id: "GOOGLE",
this.toast.clear() icon: IconGoogle,
this.$emit("hide-modal") label: t("auth.continue_with_google"),
action: signInWithGoogle,
isLoading: signingInWithGoogle,
}, },
{
id: "MICROSOFT",
icon: IconMicrosoft,
label: t("auth.continue_with_microsoft"),
action: signInWithMicrosoft,
isLoading: signingInWithMicrosoft,
}, },
}) {
id: "EMAIL",
icon: IconEmail,
label: t("auth.continue_with_email"),
action: () => {
mode.value = "email"
},
isLoading: signingInWithEmail,
},
]
// Do not format the `import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS` call into multiple lines!
// prettier-ignore
const allowedAuthProvidersIDsString =
import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
? allowedAuthProvidersIDsString.split(",")
: []
const allowedAuthProviders =
allowedAuthProvidersIDs.length > 0
? authProviders.filter((provider) =>
allowedAuthProvidersIDs.includes(provider.id)
)
: authProviders
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<div <div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold" class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
> >
<span class="flex items-center"> <span class="flex items-center">
<label class="font-semibold truncate text-secondaryLight"> <label class="font-semibold truncate text-secondaryLight">
@@ -32,7 +32,7 @@
:active="authName === 'None'" :active="authName === 'None'"
@click=" @click="
() => { () => {
authType = 'none' auth.authType = 'none'
hide() hide()
} }
" "
@@ -43,7 +43,7 @@
:active="authName === 'Basic Auth'" :active="authName === 'Basic Auth'"
@click=" @click="
() => { () => {
authType = 'basic' auth.authType = 'basic'
hide() hide()
} }
" "
@@ -54,7 +54,7 @@
:active="authName === 'Bearer'" :active="authName === 'Bearer'"
@click=" @click="
() => { () => {
authType = 'bearer' auth.authType = 'bearer'
hide() hide()
} }
" "
@@ -65,7 +65,7 @@
:active="authName === 'OAuth 2.0'" :active="authName === 'OAuth 2.0'"
@click=" @click="
() => { () => {
authType = 'oauth-2' auth.authType = 'oauth-2'
hide() hide()
} }
" "
@@ -76,7 +76,7 @@
:active="authName === 'API key'" :active="authName === 'API key'"
@click=" @click="
() => { () => {
authType = 'api-key' auth.authType = 'api-key'
hide() hide()
} }
" "
@@ -90,7 +90,7 @@
:on="!URLExcludes.auth" :on="!URLExcludes.auth"
@change="setExclude('auth', !$event)" @change="setExclude('auth', !$event)"
> >
{{ t("authorization.include_in_url") }} {{ $t("authorization.include_in_url") }}
</HoppSmartCheckbox>--> </HoppSmartCheckbox>-->
<HoppSmartCheckbox <HoppSmartCheckbox
:on="authActive" :on="authActive"
@@ -115,7 +115,7 @@
</div> </div>
</div> </div>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="authType === 'none'" v-if="auth.authType === 'none'"
:src="`/images/states/${colorMode.value}/login.svg`" :src="`/images/states/${colorMode.value}/login.svg`"
:alt="`${t('empty.authorization')}`" :alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')" :text="t('empty.authorization')"
@@ -127,114 +127,47 @@
blank blank
:icon="IconExternalLink" :icon="IconExternalLink"
reverse reverse
class="mb-4"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight"> <div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight"> <div class="w-2/3 border-r border-dividerLight">
<div v-if="authType === 'basic'"> <div v-if="auth.authType === 'basic'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="basicUsername" v-model="auth.username"
:environment-highlights="false" :environment-highlights="false"
:placeholder="t('authorization.username')" :placeholder="t('authorization.username')"
/> />
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="basicPassword" v-model="auth.password"
:environment-highlights="false" :environment-highlights="false"
:placeholder="t('authorization.password')" :placeholder="t('authorization.password')"
/> />
</div> </div>
</div> </div>
<div v-if="authType === 'bearer'"> <div v-if="auth.authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="bearerToken" v-model="auth.token"
:environment-highlights="false" :environment-highlights="false"
placeholder="Token" placeholder="Token"
/> />
</div> </div>
</div> </div>
<div v-if="authType === 'oauth-2'"> <div v-if="auth.authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="oauth2Token" v-model="auth.token"
:environment-highlights="false" :environment-highlights="false"
placeholder="Token" placeholder="Token"
/> />
</div> </div>
<HttpOAuth2Authorization /> <HttpOAuth2Authorization v-model="auth" />
</div>
<div v-if="authType === 'api-key'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="apiKey"
:environment-highlights="false"
placeholder="Key"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="apiValue"
:environment-highlights="false"
placeholder="Value"
/>
</div>
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
<label class="ml-4 text-secondaryLight">
{{ t("authorization.pass_key_by") }}
</label>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => authTippyActions.focus()"
>
<span class="select-wrapper">
<HoppButtonSecondary
:label="addTo || t('state.none')"
class="pr-8 ml-2 rounded-none"
/>
</span>
<template #content="{ hide }">
<div
ref="authTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="addTo === 'Headers' ? IconCircleDot : IconCircle"
:active="addTo === 'Headers'"
:label="'Headers'"
@click="
() => {
addTo = 'Headers'
hide()
}
"
/>
<HoppSmartItem
:icon="
addTo === 'Query params' ? IconCircleDot : IconCircle
"
:active="addTo === 'Query params'"
:label="'Query params'"
@click="
() => {
addTo = 'Query params'
hide()
}
"
/>
</div>
</template>
</tippy>
</span>
</div> </div>
<div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" />
</div> </div>
</div> </div>
<div <div
@@ -257,55 +190,45 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, Ref } from "vue"
import {
HoppGQLAuthAPIKey,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthOAuth2,
} from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { gqlAuth$, setGQLAuth } from "~/newstore/GQLSession"
import IconTrash2 from "~icons/lucide/trash-2"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconExternalLink from "~icons/lucide/external-link" import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot" import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle" import IconCircle from "~icons/lucide/circle"
import { computed, ref } from "vue"
import { HoppGQLAuth } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const auth = useStream( const props = defineProps<{
gqlAuth$, modelValue: HoppGQLAuth
{ authType: "none", authActive: true }, }>()
setGQLAuth
) const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLAuth): void
}>()
const auth = useVModel(props, "modelValue", emit)
const AUTH_KEY_NAME = {
basic: "Basic Auth",
bearer: "Bearer",
"oauth-2": "OAuth 2.0",
"api-key": "API key",
none: "None",
} as const
const authType = pluckRef(auth, "authType") const authType = pluckRef(auth, "authType")
const authName = computed(() => { const authName = computed(() =>
if (authType.value === "basic") return "Basic Auth" AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
else if (authType.value === "bearer") return "Bearer" )
else if (authType.value === "oauth-2") return "OAuth 2.0"
else if (authType.value === "api-key") return "API key"
else return "None"
})
const authActive = pluckRef(auth, "authActive") const authActive = pluckRef(auth, "authActive")
const basicUsername = pluckRef(auth as Ref<HoppGQLAuthBasic>, "username")
const basicPassword = pluckRef(auth as Ref<HoppGQLAuthBasic>, "password")
const bearerToken = pluckRef(auth as Ref<HoppGQLAuthBearer>, "token")
const oauth2Token = pluckRef(auth as Ref<HoppGQLAuthOAuth2>, "token")
const apiKey = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "key")
const apiValue = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "value")
const addTo = pluckRef(auth as Ref<HoppGQLAuthAPIKey>, "addTo")
if (typeof addTo.value === "undefined") {
addTo.value = "Headers"
apiKey.value = ""
apiValue.value = ""
}
const clearContent = () => { const clearContent = () => {
auth.value = { auth.value = {
@@ -316,5 +239,4 @@ const clearContent = () => {
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
const authTippyActions = ref<any | null>(null)
</script> </script>

View File

@@ -0,0 +1,430 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("tab.headers") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearContent()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
:icon="IconEdit"
:class="{ '!text-accent': bulkMode }"
@click="bulkMode = !bulkMode"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
:disabled="bulkMode"
@click="addHeader"
/>
</div>
</div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
<div v-else>
<draggable
v-model="workingHeaders"
:item-key="(header: any) => `header-${header.id}`"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<HoppButtonSecondary
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
content:
index !== workingHeaders?.length - 1
? t('action.drag_to_reorder')
: null,
}"
:icon="IconGripVertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingHeaders?.length - 1,
}"
tabindex="-1"
/>
</span>
<HoppSmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
"
class="flex-1 !flex"
@input="
updateHeader(index, {
id: header.id,
key: $event,
value: header.value,
active: header.active,
})
"
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateHeader(index, {
id: header.id,
key: header.key,
value: ($event!.target! as HTMLInputElement).value,
active: header.active,
})
"
/>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
header.hasOwnProperty('active')
? header.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:icon="
header.hasOwnProperty('active')
? header.active
? IconCheckCircle
: IconCircle
: IconCheckCircle
"
color="green"
@click="
updateHeader(index, {
id: header.id,
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteHeader(index)"
/>
</span>
</div>
</template>
</draggable>
<HoppSmartPlaceholder
v-if="workingHeaders.length === 0"
:src="`/images/states/${colorMode.value}/add_category.svg`"
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addHeader"
/>
</HoppSmartPlaceholder>
</div>
</template>
<script setup lang="ts">
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconPlus from "~icons/lucide/plus"
import IconGripVertical from "~icons/lucide/grip-vertical"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconTrash from "~icons/lucide/trash"
import IconCircle from "~icons/lucide/circle"
import IconWrapText from "~icons/lucide/wrap-text"
import { reactive, ref, watch } from "vue"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import { pipe, flow } from "fp-ts/function"
import {
GQLHeader,
rawKeyValueEntriesToString,
parseRawKeyValueEntriesE,
RawKeyValueEntry,
HoppGQLRequest,
} from "@hoppscotch/data"
import draggable from "vuedraggable-es"
import { clone, cloneDeep, isEqual } from "lodash-es"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { commonHeaders } from "~/helpers/headers"
import { useCodemirror } from "@composables/codemirror"
import { objRemoveKey } from "~/helpers/functional/object"
import { useVModel } from "@vueuse/core"
const colorMode = useColorMode()
const t = useI18n()
const toast = useToast()
// v-model integration with props and emit
const props = defineProps<{ modelValue: HoppGQLRequest }>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLRequest): void
}>()
const request = useVModel(props, "modelValue", emit)
const idTicker = ref(0)
const linewrapEnabled = ref(false)
const bulkMode = ref(false)
const bulkHeaders = ref("")
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const bulkEditor = ref<any | null>(null)
useCodemirror(
bulkEditor,
bulkHeaders,
reactive({
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
// The UI representation of the headers list (has the empty end header)
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Rule: Working Headers always have one empty header or the last element is always an empty header
watch(workingHeaders, (headersList) => {
if (
headersList.length > 0 &&
headersList[headersList.length - 1].key !== ""
) {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
// Sync logic between headers and working headers
watch(
props.modelValue.headers,
(newHeadersList) => {
// Sync should overwrite working headers
const filteredWorkingHeaders = pipe(
workingHeaders.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(bulkHeaders.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
workingHeaders.value = pipe(
newHeadersList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
}
},
{ immediate: true }
)
watch(workingHeaders, (newWorkingHeaders) => {
const fixedHeaders = pipe(
newWorkingHeaders,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(request.value.headers, fixedHeaders)) {
request.value.headers = cloneDeep(fixedHeaders)
}
})
// Bulk Editor Syncing with Working Headers
watch(bulkHeaders, (newBulkHeaders) => {
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(newBulkHeaders),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(request.value.headers, filteredBulkHeaders)) {
request.value.headers = filteredBulkHeaders
}
})
watch(workingHeaders, (newHeadersList) => {
// If we are in bulk mode, don't apply direct changes
if (bulkMode.value) return
try {
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addHeader = () => {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
workingHeaders.value = workingHeaders.value.map((h, i) =>
i === index ? header : h
)
}
const deleteHeader = (index: number) => {
const headersBeforeDeletion = clone(workingHeaders.value)
if (
!(
headersBeforeDeletion.length > 0 &&
index === headersBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_: any, toastObject: any) => {
workingHeaders.value = headersBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingHeaders.value.splice(index, 1)
}
const clearContent = () => {
// set headers list to the initial state
workingHeaders.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkHeaders.value = ""
}
</script>

View File

@@ -0,0 +1,235 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.query") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-if="subscriptionState === 'SUBSCRIBED'"
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
allowHTML: true,
}"
:title="`${t('request.stop')}`"
:label="`${t('request.stop')}`"
:icon="IconStop"
class="rounded-none !text-accent !hover:text-accentDark"
@click="unsubscribe()"
/>
<HoppButtonSecondary
v-if="selectedOperation && subscriptionState !== 'SUBSCRIBED'"
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
allowHTML: true,
}"
:title="`${t('request.run')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
:icon="IconPlay"
:disabled="!selectedOperation"
class="rounded-none !text-accent !hover:text-accentDark"
@click="runQuery(selectedOperation)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="`${t('request.save')}`"
:icon="IconSave"
class="rounded-none"
@click="saveRequest"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLQuery()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyQueryIcon"
@click="prettifyQuery"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyQueryIcon"
@click="copyQuery"
/>
</div>
</div>
<div ref="queryEditor" class="flex flex-col flex-1"></div>
</template>
<script setup lang="ts">
import IconPlay from "~icons/lucide/play"
import IconStop from "~icons/lucide/stop-circle"
import IconSave from "~icons/lucide/save"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconInfo from "~icons/lucide/info"
import IconWand from "~icons/lucide/wand"
import IconWrapText from "~icons/lucide/wrap-text"
import { onMounted, reactive, ref, markRaw } from "vue"
import { copyToClipboard } from "@helpers/utils/clipboard"
import { useCodemirror } from "@composables/codemirror"
import { useI18n } from "@composables/i18n"
import { refAutoReset, useVModel } from "@vueuse/core"
import { useToast } from "~/composables/toast"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import * as gql from "graphql"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
import { selectedGQLOpHighlight } from "~/helpers/editor/gql/operation"
import { debounce } from "lodash-es"
import { ViewUpdate } from "@codemirror/view"
import { defineActionHandler } from "~/helpers/actions"
import {
schema,
socketDisconnect,
subscriptionState,
} from "~/helpers/graphql/connection"
// Template refs
const queryEditor = ref<any | null>(null)
const t = useI18n()
const toast = useToast()
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: "save-request"): void
(e: "update:modelValue", val: string): void
(e: "run-query", definition: gql.OperationDefinitionNode | null): void
}>()
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const prettifyQueryIcon = refAutoReset<
typeof IconWand | typeof IconCheck | typeof IconInfo
>(IconWand, 1000)
const linewrapEnabled = ref(true)
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
const gqlQueryString = useVModel(props, "modelValue", emit)
const debouncedOnUpdateQueryState = debounce((update: ViewUpdate) => {
const selectedPos = update.state.selection.main.head
const queryString = update.state.doc.toJSON().join(update.state.lineBreak)
try {
const operations = gql.parse(queryString)
if (operations.definitions.length === 1) {
selectedOperation.value = operations
.definitions[0] as gql.OperationDefinitionNode
return
}
selectedOperation.value =
(operations.definitions.find((def) => {
if (def.kind !== "OperationDefinition") return false
const { start, end } = def.loc!
return selectedPos >= start && selectedPos <= end
}) as gql.OperationDefinitionNode) ?? null
} catch (error) {
// console.error(error)
}
}, 300)
onMounted(() => {
try {
const operations = gql.parse(gqlQueryString.value)
if (operations.definitions.length) {
selectedOperation.value = operations
.definitions[0] as gql.OperationDefinitionNode
return
}
} catch (error) {}
})
useCodemirror(
queryEditor,
gqlQueryString,
reactive({
extendedEditorConfig: {
mode: "graphql",
placeholder: `${t("request.query")}`,
lineWrapping: linewrapEnabled,
},
linter: createGQLQueryLinter(schema),
completer: queryCompleter(schema),
environmentHighlights: false,
additionalExts: [markRaw(selectedGQLOpHighlight)],
onUpdate: debouncedOnUpdateQueryState,
})
)
// operations on graphql query string
// const operations = useReadonlyStream(props.request.operations$, [])
const prettifyQuery = () => {
try {
gqlQueryString.value = gql.print(
gql.parse(gqlQueryString.value, {
allowLegacyFragmentVariables: true,
})
)
prettifyQueryIcon.value = IconCheck
} catch (e) {
toast.error(`${t("error.gql_prettify_invalid_query")}`)
prettifyQueryIcon.value = IconInfo
}
}
const copyQuery = () => {
copyToClipboard(gqlQueryString.value)
copyQueryIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const clearGQLQuery = () => {
gqlQueryString.value = ""
}
const runQuery = (definition: gql.OperationDefinitionNode | null = null) => {
emit("run-query", definition)
}
const unsubscribe = () => {
socketDisconnect()
}
const saveRequest = () => {
emit("save-request")
}
defineActionHandler("editor.format", prettifyQuery)
</script>

View File

@@ -17,55 +17,136 @@
<HoppButtonPrimary <HoppButtonPrimary
id="get" id="get"
name="get" name="get"
:loading="isLoading" :loading="connection.state === 'CONNECTING'"
:label="!connected ? t('action.connect') : t('action.disconnect')" :label="!connected ? t('action.connect') : t('action.disconnect')"
class="w-32" class="w-32"
@click="onConnectClick" @click="onConnectClick"
/> />
</div> </div>
</div> </div>
<HoppSmartModal
v-if="connectionSwitchModal"
dialog
:dimissible="false"
:title="t('graphql.switch_connection')"
@close="connectionSwitchModal = false"
>
<template #body>
<p class="mb-4">
{{ t("graphql.connection_switch_url") }}:
<kbd class="shortcut-key !ml-0"> {{ lastTwoUrls.at(0) }} </kbd>
</p>
<p class="mb-4">
{{ t("graphql.connection_switch_new_url") }}:
<kbd class="shortcut-key !ml-0"> {{ lastTwoUrls.at(1) }} </kbd>
</p>
<p>{{ t("graphql.connection_switch_confirm") }}</p>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
:label="t('action.connect')"
:loading="connection.state === 'CONNECTING'"
outline
@click="switchConnection()"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="cancelSwitch()"
/>
</span>
</template>
</HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { platform } from "~/platform" import { platform } from "~/platform"
import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { import { currentActiveTab } from "~/helpers/graphql/tab"
gqlAuth$, import { computed, ref, watch } from "vue"
gqlHeaders$, import { connection } from "~/helpers/graphql/connection"
gqlURL$, import { connect } from "~/helpers/graphql/connection"
setGQLURL, import { disconnect } from "~/helpers/graphql/connection"
} from "~/newstore/GQLSession" import { InterceptorService } from "~/services/interceptor.service"
import { useService } from "dioc/vue"
import { defineActionHandler } from "~/helpers/actions"
const t = useI18n() const t = useI18n()
const props = defineProps<{ const interceptorService = useService(InterceptorService)
conn: GQLConnection
}>()
const connected = useReadonlyStream(props.conn.connected$, false) const connectionSwitchModal = ref(false)
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
const headers = useReadonlyStream(gqlHeaders$, []) const connected = computed(() => connection.state === "CONNECTED")
const auth = useReadonlyStream(gqlAuth$, {
authType: "none", const url = computed({
authActive: true, get: () => currentActiveTab.value?.document.request.url ?? "",
set: (value) => {
currentActiveTab.value!.document.request.url = value
},
}) })
const url = useStream(gqlURL$, "", setGQLURL)
const onConnectClick = () => { const onConnectClick = () => {
if (!connected.value) { if (!connected.value) {
props.conn.connect(url.value, headers.value as any, auth.value) gqlConnect()
} else {
disconnect()
}
}
const gqlConnect = () => {
connect(url.value, currentActiveTab.value?.document.request.headers)
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN", type: "HOPP_REQUEST_RUN",
platform: "graphql-schema", platform: "graphql-schema",
strategy: getCurrentStrategyID(), strategy: interceptorService.currentInterceptorID.value!,
}) })
} else { }
props.conn.disconnect()
const switchConnection = () => {
gqlConnect()
connectionSwitchModal.value = false
}
const lastTwoUrls = ref<string[]>([])
watch(
currentActiveTab,
(newVal) => {
if (newVal) {
lastTwoUrls.value.push(newVal.document.request.url)
if (lastTwoUrls.value.length > 2) {
lastTwoUrls.value.shift()
} }
} }
if (
connected.value &&
lastTwoUrls.value.length === 2 &&
lastTwoUrls.value.at(0) !== lastTwoUrls.value.at(1)
) {
disconnect()
connectionSwitchModal.value = true
}
},
{
immediate: true,
}
)
const cancelSwitch = () => {
if (connected.value) disconnect()
connectionSwitchModal.value = false
}
defineActionHandler(
"gql.connect",
gqlConnect,
computed(() => !connected.value)
)
defineActionHandler("gql.disconnect", disconnect, connected)
</script> </script>

View File

@@ -2,311 +2,42 @@
<div class="flex flex-col flex-1 h-full"> <div class="flex flex-col flex-1 h-full">
<HoppSmartTabs <HoppSmartTabs
v-model="selectedOptionTab" v-model="selectedOptionTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperPrimaryStickyFold z-10" styles="sticky top-0 bg-primary z-10 border-b-0"
render-inactive-tabs :render-inactive-tabs="true"
> >
<HoppSmartTab <HoppSmartTab
:id="'query'" :id="'query'"
:label="`${t('tab.query')}`" :label="`${t('tab.query')}`"
:indicator="gqlQueryString && gqlQueryString.length > 0 ? true : false" :indicator="request.query && request.query.length > 0 ? true : false"
> >
<div <GraphqlQuery
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold gqlRunQuery" v-model="request.query"
> @run-query="runQuery"
<label class="font-semibold truncate text-secondaryLight"> @save-request="saveRequest"
{{ t("request.query") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'request.run'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${t('request.run')}`"
:icon="IconPlay"
class="rounded-none !text-accent !hover:text-accentDark"
@click="runQuery()"
/> />
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="`${t('request.save')}`"
:icon="IconSave"
class="rounded-none"
@click="saveRequest"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLQuery()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabledQuery }"
:icon="IconWrapText"
@click.prevent="linewrapEnabledQuery = !linewrapEnabledQuery"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyQueryIcon"
@click="prettifyQuery"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyQueryIcon"
@click="copyQuery"
/>
</div>
</div>
<div ref="queryEditor" class="flex flex-col flex-1"></div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
:id="'variables'" :id="'variables'"
:label="`${t('tab.variables')}`" :label="`${t('tab.variables')}`"
:indicator="variableString && variableString.length > 0 ? true : false" :indicator="
> request.variables && request.variables.length > 0 ? true : false
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("request.variables") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLVariables()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabledVariable }"
:icon="IconWrapText"
@click.prevent="
linewrapEnabledVariable = !linewrapEnabledVariable
" "
>
<GraphqlVariable
v-model="request.variables"
@run-query="runQuery"
@save-request="saveRequest"
/> />
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyVariablesIcon"
@click="prettifyVariableString"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyVariablesIcon"
@click="copyVariables"
/>
</div>
</div>
<div ref="variableEditor" class="flex flex-col flex-1"></div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
:id="'headers'" :id="'headers'"
:label="`${t('tab.headers')}`" :label="`${t('tab.headers')}`"
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`" :info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
> >
<div <GraphqlHeaders v-model="request" />
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
{{ t("tab.headers") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearContent()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
:icon="IconEdit"
:class="{ '!text-accent': bulkMode }"
@click="bulkMode = !bulkMode"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
:disabled="bulkMode"
@click="addHeader"
/>
</div>
</div>
<div
v-if="bulkMode"
ref="bulkEditor"
class="flex flex-col flex-1"
></div>
<div v-else>
<draggable
v-model="workingHeaders"
:item-key="(header) => `header-${header.id}`"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
>
<span>
<HoppButtonSecondary
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
content:
index !== workingHeaders?.length - 1
? t('action.drag_to_reorder')
: null,
}"
:icon="IconGripVertical"
class="cursor-auto text-primary hover:text-primary"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
index !== workingHeaders?.length - 1,
}"
tabindex="-1"
/>
</span>
<HoppSmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
"
class="flex-1 !flex"
@input="
updateHeader(index, {
id: header.id,
key: $event,
value: header.value,
active: header.active,
})
"
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateHeader(index, {
id: header.id,
key: header.key,
value: ($event!.target! as HTMLInputElement).value,
active: header.active,
})
"
/>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
header.hasOwnProperty('active')
? header.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:icon="
header.hasOwnProperty('active')
? header.active
? IconCheckCircle
: IconCircle
: IconCheckCircle
"
color="green"
@click="
updateHeader(index, {
id: header.id,
key: header.key,
value: header.value,
active: !header.active,
})
"
/>
</span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteHeader(index)"
/>
</span>
</div>
</template>
</draggable>
<HoppSmartPlaceholder
v-if="workingHeaders.length === 0"
:src="`/images/states/${colorMode.value}/add_category.svg`"
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
class="mb-4"
@click="addHeader"
/>
</HoppSmartPlaceholder>
</div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`"> <HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<GraphqlAuthorization /> <GraphqlAuthorization v-model="request.auth" />
</HoppSmartTab> </HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
<CollectionsSaveRequest <CollectionsSaveRequest
@@ -318,481 +49,165 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconPlay from "~icons/lucide/play"
import IconSave from "~icons/lucide/save"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconPlus from "~icons/lucide/plus"
import IconGripVertical from "~icons/lucide/grip-vertical"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconTrash from "~icons/lucide/trash"
import IconCircle from "~icons/lucide/circle"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconInfo from "~icons/lucide/info"
import IconWand2 from "~icons/lucide/wand-2"
import IconWrapText from "~icons/lucide/wrap-text"
import { Ref, computed, reactive, ref, watch } from "vue"
import * as gql from "graphql"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import * as RA from "fp-ts/ReadonlyArray"
import { pipe, flow } from "fp-ts/function"
import {
GQLHeader,
makeGQLRequest,
rawKeyValueEntriesToString,
parseRawKeyValueEntriesE,
RawKeyValueEntry,
} from "@hoppscotch/data"
import draggable from "vuedraggable-es"
import { clone, cloneDeep, isEqual } from "lodash-es"
import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useColorMode } from "@composables/theming"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { startPageProgress, completePageProgress } from "@modules/loadingbar" import { completePageProgress, startPageProgress } from "@modules/loadingbar"
import { import * as gql from "graphql"
gqlAuth$, import { clone } from "lodash-es"
gqlHeaders$, import { computed, ref, watch } from "vue"
gqlQuery$,
gqlResponse$,
gqlURL$,
gqlVariables$,
setGQLAuth,
setGQLHeaders,
setGQLQuery,
setGQLResponse,
setGQLVariables,
} from "~/newstore/GQLSession"
import { commonHeaders } from "~/helpers/headers"
import { GQLConnection } from "~/helpers/GQLConnection"
import { makeGQLHistoryEntry, addGraphqlHistoryEntry } from "~/newstore/history"
import { platform } from "~/platform"
import { getCurrentStrategyID } from "~/helpers/network"
import { useCodemirror } from "@composables/codemirror"
import jsonLinter from "~/helpers/editor/linting/json"
import { createGQLQueryLinter } from "~/helpers/editor/linting/gqlQuery"
import queryCompleter from "~/helpers/editor/completion/gqlQuery"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { HoppGQLRequest } from "@hoppscotch/data"
import { objRemoveKey } from "~/helpers/functional/object" import { platform } from "~/platform"
import { currentActiveTab } from "~/helpers/graphql/tab"
import { computedWithControl } from "@vueuse/core"
import {
GQLResponseEvent,
runGQLOperation,
gqlMessageEvent,
} from "~/helpers/graphql/connection"
import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections"
type OptionTabs = "query" | "headers" | "variables" | "authorization" export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization"
const selectedOptionTab = ref<GQLOptionTabs>("query")
const colorMode = useColorMode() const interceptorService = useService(InterceptorService)
const selectedOptionTab = ref<OptionTabs>("query")
const t = useI18n() const t = useI18n()
const props = defineProps<{
conn: GQLConnection
}>()
const toast = useToast() const toast = useToast()
const url = useReadonlyStream(gqlURL$, "") // v-model integration with props and emit
const gqlQueryString = useStream(gqlQuery$, "", setGQLQuery) const props = withDefaults(
const variableString = useStream(gqlVariables$, "", setGQLVariables) defineProps<{
modelValue: HoppGQLRequest
const idTicker = ref(0) response?: GQLResponseEvent[] | null
tabId: string
const bulkMode = ref(false) }>(),
const bulkHeaders = ref("")
const bulkEditor = ref<any | null>(null)
const linewrapEnabled = ref(true)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
useCodemirror(
bulkEditor,
bulkHeaders,
reactive({
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
// The functional headers list (the headers actually in the system)
const headers = useStream(gqlHeaders$, [], setGQLHeaders) as Ref<GQLHeader[]>
const auth = useStream(
gqlAuth$,
{ authType: "none", authActive: true },
setGQLAuth
)
// The UI representation of the headers list (has the empty end header)
const workingHeaders = ref<Array<GQLHeader & { id: number }>>([
{ {
id: idTicker.value++, response: null,
key: "",
value: "",
active: true,
},
])
// Rule: Working Headers always have one empty header or the last element is always an empty header
watch(workingHeaders, (headersList) => {
if (
headersList.length > 0 &&
headersList[headersList.length - 1].key !== ""
) {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
} }
}) )
const emit = defineEmits(["update:modelValue", "update:response"])
const request = ref(props.modelValue)
// Sync logic between headers and working headers
watch( watch(
headers, () => request.value,
(newHeadersList) => { (newVal) => {
// Sync should overwrite working headers emit("update:modelValue", newVal)
const filteredWorkingHeaders = pipe(
workingHeaders.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(bulkHeaders.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newHeadersList, filteredWorkingHeaders)) {
workingHeaders.value = pipe(
newHeadersList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newHeadersList, filteredBulkHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(newHeadersList)
}
}, },
{ immediate: true } { deep: true }
) )
watch(workingHeaders, (newWorkingHeaders) => { const url = computedWithControl(
const fixedHeaders = pipe( () => currentActiveTab.value,
newWorkingHeaders, () => currentActiveTab.value.document.request.url
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
) )
)
)
if (!isEqual(headers.value, fixedHeaders)) {
headers.value = cloneDeep(fixedHeaders)
}
})
// Bulk Editor Syncing with Working Headers
watch(bulkHeaders, (newBulkHeaders) => {
const filteredBulkHeaders = pipe(
parseRawKeyValueEntriesE(newBulkHeaders),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(headers.value, filteredBulkHeaders)) {
headers.value = filteredBulkHeaders
}
})
watch(workingHeaders, (newHeadersList) => {
// If we are in bulk mode, don't apply direct changes
if (bulkMode.value) return
try {
const currentBulkHeaders = bulkHeaders.value.split("\n").map((item) => ({
key: item.substring(0, item.indexOf(":")).trimLeft().replace(/^#/, ""),
value: item.substring(item.indexOf(":") + 1).trimLeft(),
active: !item.trim().startsWith("#"),
}))
const filteredHeaders = newHeadersList.filter((x) => x.key !== "")
if (!isEqual(currentBulkHeaders, filteredHeaders)) {
bulkHeaders.value = rawKeyValueEntriesToString(filteredHeaders)
}
} catch (e) {
toast.error(`${t("error.something_went_wrong")}`)
console.error(e)
}
})
const addHeader = () => {
workingHeaders.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateHeader = (index: number, header: GQLHeader & { id: number }) => {
workingHeaders.value = workingHeaders.value.map((h, i) =>
i === index ? header : h
)
}
const deleteHeader = (index: number) => {
const headersBeforeDeletion = clone(workingHeaders.value)
if (
!(
headersBeforeDeletion.length > 0 &&
index === headersBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingHeaders.value = headersBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingHeaders.value.splice(index, 1)
}
const clearContent = () => {
// set headers list to the initial state
workingHeaders.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkHeaders.value = ""
}
const activeGQLHeadersCount = computed( const activeGQLHeadersCount = computed(
() => () =>
headers.value.filter((x) => x.active && (x.key !== "" || x.value !== "")) request.value.headers.filter(
.length (x) => x.active && (x.key !== "" || x.value !== "")
).length
) )
const variableEditor = ref<any | null>(null)
const linewrapEnabledVariable = ref(true)
useCodemirror(
variableEditor,
variableString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
placeholder: `${t("request.variables")}`,
lineWrapping: linewrapEnabledVariable,
},
linter: computed(() =>
variableString.value.length > 0 ? jsonLinter : null
),
completer: null,
environmentHighlights: false,
})
)
const queryEditor = ref<any | null>(null)
const schema = useReadonlyStream(props.conn.schema$, null, "noclone")
const linewrapEnabledQuery = ref(true)
useCodemirror(
queryEditor,
gqlQueryString,
reactive({
extendedEditorConfig: {
mode: "graphql",
placeholder: `${t("request.query")}`,
lineWrapping: linewrapEnabledQuery,
},
linter: createGQLQueryLinter(schema),
completer: queryCompleter(schema),
environmentHighlights: false,
})
)
const copyQueryIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const prettifyQueryIcon = refAutoReset<
typeof IconWand2 | typeof IconCheck | typeof IconInfo
>(IconWand2, 1000)
const prettifyVariablesIcon = refAutoReset<
typeof IconWand2 | typeof IconCheck | typeof IconInfo
>(IconWand2, 1000)
const showSaveRequestModal = ref(false) const showSaveRequestModal = ref(false)
const runQuery = async (
const copyQuery = () => { definition: gql.OperationDefinitionNode | null = null
copyToClipboard(gqlQueryString.value) ) => {
copyQueryIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const response = useStream(gqlResponse$, "", setGQLResponse)
const runQuery = async () => {
const startTime = Date.now() const startTime = Date.now()
startPageProgress() startPageProgress()
response.value = "loading"
try { try {
const runURL = clone(url.value) const runURL = clone(url.value)
const runHeaders = clone(headers.value) const runHeaders = clone(request.value.headers)
const runQuery = clone(gqlQueryString.value) const runQuery = clone(request.value.query)
const runVariables = clone(variableString.value) const runVariables = clone(request.value.variables)
const runAuth = clone(auth.value) const runAuth = clone(request.value.auth)
const responseText = await props.conn.runQuery( await runGQLOperation({
runURL, name: request.value.name,
runHeaders,
runQuery,
runVariables,
runAuth
)
const duration = Date.now() - startTime
completePageProgress()
response.value = JSON.stringify(JSON.parse(responseText), null, 2)
addGraphqlHistoryEntry(
makeGQLHistoryEntry({
request: makeGQLRequest({
name: "",
url: runURL, url: runURL,
query: runQuery,
headers: runHeaders, headers: runHeaders,
query: runQuery,
variables: runVariables, variables: runVariables,
auth: runAuth, auth: runAuth,
}), operationName: definition?.name?.value,
response: response.value, operationType: definition?.operation ?? "query",
star: false,
}) })
) const duration = Date.now() - startTime
completePageProgress()
toast.success(`${t("state.finished_in", { duration })}`) toast.success(`${t("state.finished_in", { duration })}`)
} catch (e: any) { } catch (e: any) {
response.value = `${e}` console.log(e)
// response.value = [`${e}`]
completePageProgress() completePageProgress()
toast.error( toast.error(
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`, `${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
{} {}
) )
console.error(e) console.error(e)
} }
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN", type: "HOPP_REQUEST_RUN",
platform: "graphql-query", platform: "graphql-query",
strategy: getCurrentStrategyID(), strategy: interceptorService.currentInterceptorID.value!,
}) })
} }
watch(
() => gqlMessageEvent.value,
(event) => {
if (event === "reset") {
emit("update:response", [])
return
}
try {
if (event?.operationType !== "subscription") {
// response.value = [event]
emit("update:response", [event])
} else {
emit("update:response", [...(props.response ?? []), event])
// TODO: subscription indicator??
}
} catch (error) {
console.log(error)
}
},
{ deep: true }
)
const hideRequestModal = () => { const hideRequestModal = () => {
showSaveRequestModal.value = false showSaveRequestModal.value = false
} }
const prettifyQuery = () => {
try {
gqlQueryString.value = gql.print(gql.parse(gqlQueryString.value))
prettifyQueryIcon.value = IconCheck
} catch (e) {
toast.error(`${t("error.gql_prettify_invalid_query")}`)
prettifyQueryIcon.value = IconInfo
}
}
const saveRequest = () => { const saveRequest = () => {
if (
currentActiveTab.value.document.saveContext &&
currentActiveTab.value.document.saveContext.originLocation ===
"user-collection"
) {
editGraphqlRequest(
currentActiveTab.value.document.saveContext.folderPath,
currentActiveTab.value.document.saveContext.requestIndex,
currentActiveTab.value.document.request
)
currentActiveTab.value.document.isDirty = false
} else {
showSaveRequestModal.value = true showSaveRequestModal.value = true
} }
const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
} }
const prettifyVariableString = () => {
try {
const jsonObj = JSON.parse(variableString.value)
variableString.value = JSON.stringify(jsonObj, null, 2)
prettifyVariablesIcon.value = IconCheck
} catch (e) {
console.error(e)
prettifyVariablesIcon.value = IconInfo
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
}
const clearGQLQuery = () => { const clearGQLQuery = () => {
gqlQueryString.value = "" request.value.query = ""
} }
const clearGQLVariables = () => {
variableString.value = ""
}
defineActionHandler("request.send-cancel", runQuery) defineActionHandler("request.send-cancel", runQuery)
defineActionHandler("request.save", saveRequest) defineActionHandler("request.save", saveRequest)
defineActionHandler("request.save-as", () => {
showSaveRequestModal.value = true
})
defineActionHandler("request.reset", clearGQLQuery) defineActionHandler("request.reset", clearGQLQuery)
defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionTab.value = tab as GQLOptionTabs
})
</script> </script>

View File

@@ -0,0 +1,50 @@
<template>
<AppPaneLayout layout-id="gql-primary">
<template #primary>
<GraphqlRequestOptions
v-model="tab.document.request"
v-model:response="tab.response"
:tab-id="tab.id"
/>
</template>
<template #secondary>
<GraphqlResponse :response="tab.response" />
</template>
</AppPaneLayout>
</template>
<script setup lang="ts">
import { useVModel } from "@vueuse/core"
import { cloneDeep } from "lodash-es"
import { watch } from "vue"
import { isEqualHoppGQLRequest } from "~/helpers/graphql"
import { HoppGQLTab } from "~/helpers/graphql/tab"
// TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppGQLTab }>()
const emit = defineEmits<{
(e: "update:modelValue", val: HoppGQLTab): void
}>()
const tab = useVModel(props, "modelValue", emit)
// TODO: Come up with a better dirty check
let oldRequest = cloneDeep(tab.value.document.request)
watch(
() => tab.value.document.request,
(updatedValue) => {
// TODO: Check equality of request
if (
!tab.value.document.isDirty &&
!isEqualHoppGQLRequest(oldRequest, updatedValue)
) {
tab.value.document.isDirty = true
}
oldRequest = cloneDeep(updatedValue)
},
{ deep: true }
)
</script>

View File

@@ -1,14 +1,6 @@
<template> <template>
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap"> <div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
<HoppSmartPlaceholder <div v-if="response?.length === 1" class="flex flex-col flex-1">
v-if="responseString === 'loading'"
:text="t('state.loading')"
>
<template #icon>
<HoppSmartSpinner class="my-4" />
</template>
</HoppSmartPlaceholder>
<div v-else-if="responseString" class="flex flex-col flex-1">
<div <div
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight" class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
> >
@@ -37,12 +29,18 @@
'action.copy' 'action.copy'
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`" )} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
:icon="copyResponseIcon" :icon="copyResponseIcon"
@click="copyResponse" @click="copyResponse(response[0].data)"
/> />
</div> </div>
</div> </div>
<div ref="schemaEditor" class="flex flex-col flex-1"></div> <div ref="schemaEditor" class="flex flex-col flex-1"></div>
</div> </div>
<div
v-else-if="response && response?.length > 1"
class="flex flex-col flex-1"
>
<GraphqlSubscriptionLog :log="response" />
</div>
<AppShortcutsPrompt v-else class="p-4" /> <AppShortcutsPrompt v-else class="p-4" />
</div> </div>
</template> </template>
@@ -52,22 +50,34 @@ import IconWrapText from "~icons/lucide/wrap-text"
import IconDownload from "~icons/lucide/download" import IconDownload from "~icons/lucide/download"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import { reactive, ref } from "vue" import { computed, reactive, ref } from "vue"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard" import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { gqlResponse$ } from "~/newstore/GQLSession"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { GQLResponseEvent } from "~/helpers/graphql/connection"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const responseString = useReadonlyStream(gqlResponse$, "") const props = withDefaults(
defineProps<{
response: GQLResponseEvent[] | null
}>(),
{
response: null,
}
)
const responseString = computed(() => {
if (props.response?.length === 1) {
return JSON.stringify(JSON.parse(props.response[0].data), null, 2)
}
return ""
})
const schemaEditor = ref<any | null>(null) const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const linewrapEnabled = ref(true)
@@ -95,14 +105,14 @@ const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
1000 1000
) )
const copyResponse = () => { const copyResponse = (str: string) => {
copyToClipboard(responseString.value!) copyToClipboard(str)
copyResponseIcon.value = IconCheck copyResponseIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`) toast.success(`${t("state.copied_to_clipboard")}`)
} }
const downloadResponse = () => { const downloadResponse = (str: string) => {
const dataToWrite = responseString.value const dataToWrite = str
const file = new Blob([dataToWrite!], { type: "application/json" }) const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a") const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
@@ -118,6 +128,14 @@ const downloadResponse = () => {
}, 1000) }, 1000)
} }
defineActionHandler("response.file.download", () => downloadResponse()) defineActionHandler(
defineActionHandler("response.copy", () => copyResponse()) "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)
)
</script> </script>

View File

@@ -5,20 +5,6 @@
vertical vertical
render-inactive-tabs render-inactive-tabs
> >
<HoppSmartTab
:id="'history'"
:icon="IconClock"
:label="`${t('tab.history')}`"
>
<History :page="'graphql'" @use-history="handleUseHistory" />
</HoppSmartTab>
<HoppSmartTab
:id="'collections'"
:icon="IconFolder"
:label="`${t('tab.collections')}`"
>
<CollectionsGraphql />
</HoppSmartTab>
<HoppSmartTab <HoppSmartTab
:id="'docs'" :id="'docs'"
:icon="IconBookOpen" :icon="IconBookOpen"
@@ -173,6 +159,21 @@
> >
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab
:id="'collections'"
:icon="IconFolder"
:label="`${t('tab.collections')}`"
>
<CollectionsGraphql />
</HoppSmartTab>
<HoppSmartTab
:id="'history'"
:icon="IconClock"
:label="`${t('tab.history')}`"
>
<History :page="'graphql'" />
</HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
</template> </template>
@@ -188,29 +189,24 @@ import IconCopy from "~icons/lucide/copy"
import IconBox from "~icons/lucide/box" import IconBox from "~icons/lucide/box"
import { computed, nextTick, reactive, ref } from "vue" import { computed, nextTick, reactive, ref } from "vue"
import { GraphQLField, GraphQLType } from "graphql" import { GraphQLField, GraphQLType } from "graphql"
import { map } from "rxjs/operators"
import { GQLHeader } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { GQLConnection } from "@helpers/GQLConnection"
import { copyToClipboard } from "@helpers/utils/clipboard" import { copyToClipboard } from "@helpers/utils/clipboard"
import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { import {
setGQLAuth, graphqlTypes,
setGQLHeaders, mutationFields,
setGQLQuery, queryFields,
setGQLResponse, schemaString,
setGQLURL, subscriptionFields,
setGQLVariables, } from "~/helpers/graphql/connection"
} from "~/newstore/GQLSession"
type NavigationTabs = "history" | "collection" | "docs" | "schema" type NavigationTabs = "history" | "collection" | "docs" | "schema"
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types" type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
const selectedNavigationTab = ref<NavigationTabs>("history") const selectedNavigationTab = ref<NavigationTabs>("docs")
const selectedGqlTab = ref<GqlTabs>("queries") const selectedGqlTab = ref<GqlTabs>("queries")
const t = useI18n() const t = useI18n()
@@ -270,40 +266,8 @@ function resolveRootType(type: GraphQLType) {
return t return t
} }
type GQLHistoryEntry = {
url: string
headers: GQLHeader[]
query: string
response: string
variables: string
}
const props = defineProps<{
conn: GQLConnection
}>()
const toast = useToast() const toast = useToast()
const queryFields = useReadonlyStream(
props.conn.queryFields$.pipe(map((x) => x ?? [])),
[]
)
const mutationFields = useReadonlyStream(
props.conn.mutationFields$.pipe(map((x) => x ?? [])),
[]
)
const subscriptionFields = useReadonlyStream(
props.conn.subscriptionFields$.pipe(map((x) => x ?? [])),
[]
)
const graphqlTypes = useReadonlyStream(
props.conn.graphqlTypes$.pipe(map((x) => x ?? [])),
[]
)
const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>( const downloadSchemaIcon = refAutoReset<typeof IconDownload | typeof IconCheck>(
IconDownload, IconDownload,
1000 1000
@@ -390,11 +354,6 @@ const handleJumpToType = async (type: GraphQLType) => {
} }
} }
const schemaString = useReadonlyStream(
props.conn.schemaString$.pipe(map((x) => x ?? "")),
""
)
const schemaEditor = ref<any | null>(null) const schemaEditor = ref<any | null>(null)
const linewrapEnabled = ref(true) const linewrapEnabled = ref(true)
@@ -436,23 +395,4 @@ const copySchema = () => {
copyToClipboard(schemaString.value) copyToClipboard(schemaString.value)
copySchemaIcon.value = IconCheck copySchemaIcon.value = IconCheck
} }
const handleUseHistory = (entry: GQLHistoryEntry) => {
const url = entry.url
const headers = entry.headers
const gqlQueryString = entry.query
const variableString = entry.variables
const responseText = entry.response
setGQLURL(url)
setGQLHeaders(headers)
setGQLQuery(gqlQueryString)
setGQLVariables(variableString)
setGQLResponse(responseText)
setGQLAuth({
authType: "none",
authActive: true,
})
props.conn.reset()
}
</script> </script>

View File

@@ -0,0 +1,125 @@
<template>
<div ref="container" class="flex flex-col flex-1">
<div
class="sticky top-0 z-10 flex items-center justify-between flex-none pl-4 border-b bg-primary border-dividerLight"
>
<label for="log" class="py-2 font-semibold text-secondaryLight">
{{ "Subscription Log" }}
</label>
<div>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
:icon="IconTrash"
@click="emit('delete')"
/>
<HoppButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.scroll_to_top')"
:icon="IconArrowUp"
@click="scrollTo('top')"
/>
<HoppButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.scroll_to_bottom')"
:icon="IconArrowDown"
@click="scrollTo('bottom')"
/>
<HoppButtonSecondary
id="bottompage"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.autoscroll')"
:icon="IconChevronsDown"
:class="toggleAutoscrollColor"
@click="toggleAutoscroll()"
/>
</div>
</div>
<div
v-if="log.length !== 0"
ref="logs"
class="overflow-y-auto border-b border-dividerLight"
>
<div
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
>
<RealtimeLogEntry
v-for="(entry, index) in log"
:key="`entry-${index}`"
:is-open="log.length - 1 === index"
:entry="{ ts: entry.time, source: 'info', payload: entry.data }"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, PropType, computed, watch, Ref } from "vue"
import IconTrash from "~icons/lucide/trash"
import IconArrowUp from "~icons/lucide/arrow-up"
import IconArrowDown from "~icons/lucide/arrow-down"
import IconChevronsDown from "~icons/lucide/chevron-down"
import { useThrottleFn, useScroll } from "@vueuse/core"
import { useI18n } from "@composables/i18n"
import { GQLResponseEvent } from "~/helpers/graphql/connection"
const props = defineProps({
log: { type: Array as PropType<GQLResponseEvent[]>, default: () => [] },
title: {
type: String,
default: "",
},
})
const emit = defineEmits<{
(e: "delete"): void
}>()
const t = useI18n()
const container = ref<HTMLElement | null>(null)
const logs = ref<HTMLElement | null>(null)
const autoScrollEnabled = ref(true)
const logListScroll = useScroll(logs as Ref<HTMLElement>)
// Disable autoscroll when scrolling to top
watch(logListScroll.isScrolling, (isScrolling) => {
if (isScrolling && logListScroll.directions.top)
autoScrollEnabled.value = false
})
const scrollTo = (position: "top" | "bottom") => {
if (position === "top") {
logs.value?.scroll({
behavior: "smooth",
top: 0,
})
} else if (position === "bottom") {
logs.value?.scroll({
behavior: "smooth",
top: logs.value?.scrollHeight,
})
}
}
watch(
() => props.log,
useThrottleFn(() => {
if (autoScrollEnabled.value) scrollTo("bottom")
}, 200),
{ flush: "post" }
)
const toggleAutoscroll = () => {
autoScrollEnabled.value = !autoScrollEnabled.value
}
const toggleAutoscrollColor = computed(() =>
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
)
</script>

View File

@@ -0,0 +1,118 @@
<template>
<div
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="tab.document.request.name"
class="truncate px-2 flex items-center"
@dblclick="emit('open-rename-modal')"
@contextmenu.prevent="options?.tippy?.show()"
@click.middle="emit('close-tab')"
>
<tippy
ref="options"
trigger="manual"
interactive
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span class="leading-8 px-2 truncate">
{{ tab.document.request.name }}
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.r="renameAction?.$el.click()"
@keyup.d="duplicateAction?.$el.click()"
@keyup.w="closeAction?.$el.click()"
@keyup.x="closeOthersAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
ref="renameAction"
:icon="IconFileEdit"
:label="t('request.rename')"
:shortcut="['R']"
@click="
() => {
emit('open-rename-modal')
hide()
}
"
/>
<HoppSmartItem
ref="duplicateAction"
:icon="IconCopy"
:label="t('tab.duplicate')"
:shortcut="['D']"
@click="
() => {
emit('duplicate-tab')
hide()
}
"
/>
<HoppSmartItem
v-if="isRemovable"
ref="closeAction"
:icon="IconXCircle"
:label="t('tab.close')"
:shortcut="['W']"
@click="
() => {
emit('close-tab')
hide()
}
"
/>
<HoppSmartItem
v-if="isRemovable"
ref="closeOthersAction"
:icon="IconXSquare"
:label="t('tab.close_others')"
:shortcut="['X']"
@click="
() => {
emit('close-other-tabs')
hide()
}
"
/>
</div>
</template>
</tippy>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n"
import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy"
import { HoppGQLTab } from "~/helpers/graphql/tab"
const t = useI18n()
defineProps<{
tab: HoppGQLTab
isRemovable: boolean
}>()
const emit = defineEmits<{
(event: "open-rename-modal"): void
(event: "close-tab"): void
(event: "close-other-tabs"): void
(event: "duplicate-tab"): void
}>()
const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null)
const renameAction = ref<HTMLButtonElement | null>(null)
const closeAction = ref<HTMLButtonElement | null>(null)
const closeOthersAction = ref<HTMLButtonElement | null>(null)
const duplicateAction = ref<HTMLButtonElement | null>(null)
</script>

View File

@@ -55,51 +55,48 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
// TODO: TypeScript + Setup Script this at some point :)
import { defineComponent } from "vue"
import { import {
GraphQLEnumType, GraphQLEnumType,
GraphQLInputObjectType, GraphQLInputObjectType,
GraphQLInterfaceType, GraphQLInterfaceType,
} from "graphql" } from "graphql"
import { computed } from "vue"
export default defineComponent({ const props = defineProps({
props: { gqlType: {
// eslint-disable-next-line vue/require-default-prop, vue/require-prop-types type: Object,
gqlType: {}, required: true,
},
gqlTypes: { type: Array, default: () => [] }, gqlTypes: { type: Array, default: () => [] },
jumpTypeCallback: { type: Function, default: () => ({}) }, jumpTypeCallback: { type: Function, default: () => ({}) },
isHighlighted: { type: Boolean, default: false }, isHighlighted: { type: Boolean, default: false },
highlightedFields: { type: Array, default: () => [] }, highlightedFields: { type: Array, default: () => [] },
},
computed: {
isInput() {
return this.gqlType instanceof GraphQLInputObjectType
},
isInterface() {
return this.gqlType instanceof GraphQLInterfaceType
},
isEnum() {
return this.gqlType instanceof GraphQLEnumType
},
interfaces() {
return (this.gqlType.getInterfaces && this.gqlType.getInterfaces()) || []
},
children() {
return this.gqlTypes.filter(
(type) =>
type.getInterfaces && type.getInterfaces().includes(this.gqlType)
)
},
},
methods: {
isFieldHighlighted({ field }) {
return !!this.highlightedFields.find(({ name }) => name === field.name)
},
},
}) })
const isInput = computed(() => {
return props.gqlType instanceof GraphQLInputObjectType
})
const isInterface = computed(() => {
return props.gqlType instanceof GraphQLInterfaceType
})
const isEnum = computed(() => {
return props.gqlType instanceof GraphQLEnumType
})
const interfaces = computed(() => {
return (props.gqlType.getInterfaces && props.gqlType.getInterfaces()) || []
})
const children = computed(() => {
return props.gqlTypes.filter(
(type) => type.getInterfaces && type.getInterfaces().includes(props.gqlType)
)
})
const isFieldHighlighted = ({ field }) => {
return !!props.highlightedFields.find(({ name }) => name === field.name)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -0,0 +1,172 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.variables") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-if="subscriptionState === 'SUBSCRIBED'"
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
allowHTML: true,
}"
:title="`${t('request.stop')}`"
:label="`${t('request.stop')}`"
:icon="IconStop"
class="rounded-none !text-accent !hover:text-accentDark"
@click="unsubscribe()"
/>
<HoppButtonSecondary
v-if="selectedOperation && subscriptionState !== 'SUBSCRIBED'"
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
allowHTML: true,
}"
:title="`${t('request.run')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
:icon="IconPlay"
:disabled="!selectedOperation"
class="rounded-none !text-accent !hover:text-accentDark"
@click="runQuery(selectedOperation)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/graphql-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearGQLVariables()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.prettify')"
:icon="prettifyVariablesIcon"
@click="prettifyVariableString"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.copy')"
:icon="copyVariablesIcon"
@click="copyVariables"
/>
</div>
</div>
<div ref="variableEditor" class="flex flex-col flex-1"></div>
</template>
<script setup lang="ts">
import IconPlay from "~icons/lucide/play"
import IconStop from "~icons/lucide/stop-circle"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check"
import IconInfo from "~icons/lucide/info"
import IconWand from "~icons/lucide/wand"
import IconWrapText from "~icons/lucide/wrap-text"
import { computed, reactive, ref } from "vue"
import jsonLinter from "~/helpers/editor/linting/json"
import { copyToClipboard } from "@helpers/utils/clipboard"
import { useCodemirror } from "@composables/codemirror"
import * as gql from "graphql"
import { useI18n } from "@composables/i18n"
import { refAutoReset, useVModel } from "@vueuse/core"
import { useToast } from "~/composables/toast"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import {
socketDisconnect,
subscriptionState,
} from "~/helpers/graphql/connection"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: "save-request"): void
(e: "update:modelValue", val: string): void
(e: "run-query", definition: gql.OperationDefinitionNode | null): void
}>()
// Watch operations on graphql query string
const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)
const variableString = useVModel(props, "modelValue", emit)
const variableEditor = ref<any | null>(null)
const linewrapEnabled = ref(false)
const copyVariablesIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
)
const prettifyVariablesIcon = refAutoReset<
typeof IconWand | typeof IconCheck | typeof IconInfo
>(IconWand, 1000)
useCodemirror(
variableEditor,
variableString,
reactive({
extendedEditorConfig: {
mode: "application/ld+json",
placeholder: `${t("request.variables")}`,
lineWrapping: linewrapEnabled,
},
linter: computed(() =>
variableString.value.length > 0 ? jsonLinter : null
),
completer: null,
environmentHighlights: false,
})
)
const copyVariables = () => {
copyToClipboard(variableString.value)
copyVariablesIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const prettifyVariableString = () => {
try {
const jsonObj = JSON.parse(variableString.value)
variableString.value = JSON.stringify(jsonObj, null, 2)
prettifyVariablesIcon.value = IconCheck
} catch (e) {
console.error(e)
prettifyVariablesIcon.value = IconInfo
toast.error(`${t("error.json_prettify_invalid_body")}`)
}
}
const clearGQLVariables = () => {
variableString.value = ""
}
const runQuery = (definition: gql.OperationDefinitionNode | null = null) => {
emit("run-query", definition)
}
const unsubscribe = () => {
socketDisconnect()
}
</script>

View File

@@ -56,9 +56,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { makeGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import { setGQLSession } from "~/newstore/GQLSession"
import { GQLHistoryEntry } from "~/newstore/history" import { GQLHistoryEntry } from "~/newstore/history"
import { shortDateTime } from "~/helpers/utils/date" import { shortDateTime } from "~/helpers/utils/date"
@@ -69,6 +66,8 @@ import IconMinimize2 from "~icons/lucide/minimize-2"
import IconMaximize2 from "~icons/lucide/maximize-2" import IconMaximize2 from "~icons/lucide/maximize-2"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { makeGQLRequest } from "@hoppscotch/data"
import { createNewTab } from "~/helpers/graphql/tab"
const t = useI18n() const t = useI18n()
@@ -94,19 +93,16 @@ const query = computed(() =>
) )
const useEntry = () => { const useEntry = () => {
setGQLSession({ createNewTab({
request: cloneDeep( request: makeGQLRequest({
makeGQLRequest({
name: props.entry.request.name, name: props.entry.request.name,
url: props.entry.request.url, url: props.entry.request.url,
headers: props.entry.request.headers, headers: props.entry.request.headers,
query: props.entry.request.query, query: props.entry.request.query,
variables: props.entry.request.variables, variables: props.entry.request.variables,
auth: props.entry.request.auth, auth: props.entry.request.auth,
}) }),
), isDirty: false,
schema: "",
response: props.entry.response,
}) })
} }
</script> </script>

View File

@@ -126,7 +126,6 @@
blank blank
:icon="IconExternalLink" :icon="IconExternalLink"
reverse reverse
class="mb-4"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight"> <div v-else class="flex flex-1 border-b border-dividerLight">

View File

@@ -28,7 +28,9 @@
> >
<HoppSmartItem <HoppSmartItem
:label="t('state.none')" :label="t('state.none')"
:info-icon="(body.contentType === null ? IconDone : null) as any" :info-icon="
(body.contentType === null ? IconDone : null) as any
"
:active-info-icon="body.contentType === null" :active-info-icon="body.contentType === null"
@click=" @click="
() => { () => {
@@ -115,7 +117,6 @@
blank blank
:icon="IconExternalLink" :icon="IconExternalLink"
reverse reverse
class="mb-4"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</div> </div>

View File

@@ -162,7 +162,6 @@
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
:icon="IconPlus" :icon="IconPlus"
class="mb-4"
@click="addBodyParam" @click="addBodyParam"
/> />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>

View File

@@ -20,7 +20,7 @@
<span class="select-wrapper"> <span class="select-wrapper">
<HoppButtonSecondary <HoppButtonSecondary
:label=" :label="
CodegenDefinitions.find((x) => x.name === codegenType).caption CodegenDefinitions.find((x) => x.name === codegenType)!.caption
" "
outline outline
class="flex-1 pr-8" class="flex-1 pr-8"
@@ -57,12 +57,7 @@
" "
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if=" v-if="filteredCodegenDefinitions.length === 0"
!(
filteredCodegenDefinitions.length !== 0 ||
CodegenDefinitions.length === 0
)
"
:text="`${t('state.nothing_found')}${searchQuery}`" :text="`${t('state.nothing_found')}${searchQuery}`"
> >
<template #icon> <template #icon>

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